aboutsummaryrefslogtreecommitdiffstats
path: root/lib/ssh
diff options
context:
space:
mode:
Diffstat (limited to 'lib/ssh')
-rw-r--r--lib/ssh/doc/src/book.xml4
-rw-r--r--lib/ssh/doc/src/fascicules.xml2
-rw-r--r--lib/ssh/doc/src/introduction.xml2
-rw-r--r--lib/ssh/doc/src/notes.xml172
-rw-r--r--lib/ssh/doc/src/part_notes.xml4
-rw-r--r--lib/ssh/doc/src/ref_man.xml4
-rw-r--r--lib/ssh/doc/src/ssh.xml39
-rw-r--r--lib/ssh/doc/src/ssh_app.xml4
-rw-r--r--lib/ssh/doc/src/ssh_channel.xml2
-rw-r--r--lib/ssh/doc/src/ssh_client_key_api.xml2
-rw-r--r--lib/ssh/doc/src/ssh_connection.xml2
-rw-r--r--lib/ssh/doc/src/ssh_protocol.xml4
-rw-r--r--lib/ssh/doc/src/ssh_server_key_api.xml2
-rw-r--r--lib/ssh/doc/src/ssh_sftp.xml4
-rw-r--r--lib/ssh/doc/src/ssh_sftpd.xml2
-rw-r--r--lib/ssh/doc/src/usersguide.xml4
-rw-r--r--lib/ssh/doc/src/using_ssh.xml6
-rw-r--r--lib/ssh/src/ssh.app.src4
-rw-r--r--lib/ssh/src/ssh.appup.src16
-rw-r--r--lib/ssh/src/ssh.erl31
-rw-r--r--lib/ssh/src/ssh.hrl1
-rw-r--r--lib/ssh/src/ssh_acceptor.erl44
-rw-r--r--lib/ssh/src/ssh_auth.erl11
-rw-r--r--lib/ssh/src/ssh_bits.erl4
-rw-r--r--lib/ssh/src/ssh_connection.erl28
-rw-r--r--lib/ssh/src/ssh_connection_handler.erl19
-rw-r--r--lib/ssh/src/ssh_io.erl2
-rw-r--r--lib/ssh/src/ssh_message.erl4
-rw-r--r--lib/ssh/src/ssh_sftp.erl7
-rw-r--r--lib/ssh/src/ssh_sftpd.erl20
-rw-r--r--lib/ssh/src/ssh_xfer.erl57
-rw-r--r--lib/ssh/test/ssh_basic_SUITE.erl146
-rw-r--r--lib/ssh/test/ssh_test_lib.erl16
-rw-r--r--lib/ssh/test/ssh_unicode_SUITE.erl587
-rw-r--r--lib/ssh/test/ssh_unicode_SUITE_data/sftp.txt1
-rw-r--r--lib/ssh/test/ssh_unicode_SUITE_data/sftp瑞点.txt1
-rw-r--r--lib/ssh/test/ssh_unicode_SUITE_data/ssh_host_dsa_key13
-rw-r--r--lib/ssh/test/ssh_unicode_SUITE_data/ssh_host_dsa_key.pub11
-rw-r--r--lib/ssh/vsn.mk2
39 files changed, 1165 insertions, 119 deletions
diff --git a/lib/ssh/doc/src/book.xml b/lib/ssh/doc/src/book.xml
index 3c2375f96d..c031d872d7 100644
--- a/lib/ssh/doc/src/book.xml
+++ b/lib/ssh/doc/src/book.xml
@@ -1,10 +1,10 @@
-<?xml version="1.0" encoding="iso-8859-1" ?>
+<?xml version="1.0" encoding="utf-8" ?>
<!DOCTYPE book SYSTEM "book.dtd">
<book xmlns:xi="http://www.w3.org/2001/XInclude">
<header titlestyle="normal">
<copyright>
- <year>2005</year><year>2012</year>
+ <year>2005</year><year>2013</year>
<holder>Ericsson AB. All Rights Reserved.</holder>
</copyright>
<legalnotice>
diff --git a/lib/ssh/doc/src/fascicules.xml b/lib/ssh/doc/src/fascicules.xml
index 069d9002e0..7e99398c16 100644
--- a/lib/ssh/doc/src/fascicules.xml
+++ b/lib/ssh/doc/src/fascicules.xml
@@ -1,4 +1,4 @@
-<?xml version="1.0" encoding="iso-8859-1" ?>
+<?xml version="1.0" encoding="utf-8" ?>
<!DOCTYPE fascicules SYSTEM "fascicules.dtd">
<fascicules>
diff --git a/lib/ssh/doc/src/introduction.xml b/lib/ssh/doc/src/introduction.xml
index e76aa20d64..b42910cb34 100644
--- a/lib/ssh/doc/src/introduction.xml
+++ b/lib/ssh/doc/src/introduction.xml
@@ -1,4 +1,4 @@
-<?xml version="1.0" encoding="iso-8859-1" ?>
+<?xml version="1.0" encoding="utf-8" ?>
<!DOCTYPE chapter SYSTEM "chapter.dtd">
<chapter>
diff --git a/lib/ssh/doc/src/notes.xml b/lib/ssh/doc/src/notes.xml
index 0d88cbda7a..84d5e5c86e 100644
--- a/lib/ssh/doc/src/notes.xml
+++ b/lib/ssh/doc/src/notes.xml
@@ -4,7 +4,7 @@
<chapter>
<header>
<copyright>
- <year>2004</year><year>2013</year>
+ <year>2004</year><year>2014</year>
<holder>Ericsson AB. All Rights Reserved.</holder>
</copyright>
<legalnotice>
@@ -29,6 +29,176 @@
<file>notes.xml</file>
</header>
+<section><title>Ssh 3.0.2</title>
+
+ <section><title>Fixed Bugs and Malfunctions</title>
+ <list>
+ <item>
+ <p>
+ Fixed timeout bug in ssh:connect.</p>
+ <p>
+ Own Id: OTP-11908</p>
+ </item>
+ </list>
+ </section>
+
+
+ <section><title>Improvements and New Features</title>
+ <list>
+ <item>
+ <p>
+ Option <c>max_sessions</c> added to
+ <c>ssh:daemon/{2,3}</c>. This option, if set, limits the
+ number of simultaneous connections accepted by the
+ daemon.</p>
+ <p>
+ Own Id: OTP-11885</p>
+ </item>
+ </list>
+ </section>
+
+</section>
+
+<section><title>Ssh 3.0.1</title>
+
+ <section><title>Fixed Bugs and Malfunctions</title>
+ <list>
+ <item>
+ <p>
+ Fixes the problem that ssh_cli in some cases could delay
+ the prompt if a tty was not requested by the client.</p>
+ <p>
+ Own Id: OTP-10732</p>
+ </item>
+ <item>
+ <p>
+ The variable NewCol is now correctly calculated allowing
+ for tab-completion of function calls even when preceded
+ with blank space (Thanks to Alexander Demidenko)</p>
+ <p>
+ Own Id: OTP-11566</p>
+ </item>
+ <item>
+ <p>
+ Fix incorrect dialyzer spec and types, also enhance
+ documentation. </p>
+ <p>
+ Thanks to Ayaz Tuncer.</p>
+ <p>
+ Own Id: OTP-11627</p>
+ </item>
+ <item>
+ <p>
+ Fixed a bug when ssh:exec executes a linux command on a
+ linux ssh daemon. If the result is sent back from
+ standard error, the length information was not stripped
+ off correctly.</p>
+ <p>
+ Own Id: OTP-11667</p>
+ </item>
+ <item>
+ <p>
+ Fixed a bug with the ssh file 'known_hosts' which made
+ the file grow with many equal entries.</p>
+ <p>
+ Own Id: OTP-11671</p>
+ </item>
+ <item>
+ <p>
+ Some local implementations of removing the last element
+ from a list are replaced by <c>lists:droplast/1</c>. Note
+ that this requires at least <c>stdlib-2.0</c>, which is
+ the stdlib version delivered in OTP 17.0. (Thanks to Hans
+ Svensson)</p>
+ <p>
+ Own Id: OTP-11678</p>
+ </item>
+ <item>
+ <p>
+ Bug fix for <c>ssh:daemon/2,3</c> so that the failfun is
+ called when it should.</p>
+ <p>
+ Own Id: OTP-11680</p>
+ </item>
+ <item>
+ <p>
+ Fixed bug which crashed ssh when SSH_MSG_KEX_DH_GEX_GROUP
+ is received. This could cause a vm-crash for eheap_alloc
+ during garbage collect.</p>
+ <p>
+ Own Id: OTP-11696 Aux Id: 12547, 12532 </p>
+ </item>
+ <item>
+ <p>
+ Fixes a bug that breaks keyboard-interactive
+ authentication. Thanks to Simon Cornish for reporting and
+ suggesting a fix.</p>
+ <p>
+ Own Id: OTP-11698</p>
+ </item>
+ <item>
+ <p>
+ dialyzer specs are now correct for <c>ssh:start/0</c>,
+ <c>ssh:start/1</c>, <c>ssh:stop/0</c> and
+ <c>ssh_connection_handler:open_channel/5</c>. (Thanks to
+ Johannes Weißl )</p>
+ <p>
+ Own Id: OTP-11705</p>
+ </item>
+ <item>
+ <p>
+ Application upgrade (appup) files are corrected for the
+ following applications: </p>
+ <p>
+ <c>asn1, common_test, compiler, crypto, debugger,
+ dialyzer, edoc, eldap, erl_docgen, et, eunit, gs, hipe,
+ inets, observer, odbc, os_mon, otp_mibs, parsetools,
+ percept, public_key, reltool, runtime_tools, ssh,
+ syntax_tools, test_server, tools, typer, webtool, wx,
+ xmerl</c></p>
+ <p>
+ A new test utility for testing appup files is added to
+ test_server. This is now used by most applications in
+ OTP.</p>
+ <p>
+ (Thanks to Tobias Schlager)</p>
+ <p>
+ Own Id: OTP-11744</p>
+ </item>
+ <item>
+ <p>
+ Fixed dialyzer warning for <c>ssh_connection:send</c>.</p>
+ <p>
+ Own Id: OTP-11821</p>
+ </item>
+ <item>
+ <p>
+ <c>ssh:daemon/2,3</c> : Added options
+ <c>negotiation_timeout</c> and <c>parallel_login</c> to
+ tune the authentication behaviour.</p>
+ <p>
+ Own Id: OTP-11823</p>
+ </item>
+ </list>
+ </section>
+
+
+ <section><title>Improvements and New Features</title>
+ <list>
+ <item>
+ <p>
+ Ssh now fully supports unicode filenames, filecontents,
+ shell and cli. Please note that the underlying os and
+ emulator must also give support for unicode. You may want
+ to start the emulator with "<c>erl +fnu</c>" on Linux.</p>
+ <p>
+ Own Id: OTP-10953</p>
+ </item>
+ </list>
+ </section>
+
+</section>
+
<section><title>Ssh 3.0</title>
<section><title>Fixed Bugs and Malfunctions</title>
diff --git a/lib/ssh/doc/src/part_notes.xml b/lib/ssh/doc/src/part_notes.xml
index 1b47a12021..c5cc163717 100644
--- a/lib/ssh/doc/src/part_notes.xml
+++ b/lib/ssh/doc/src/part_notes.xml
@@ -1,10 +1,10 @@
-<?xml version="1.0" encoding="latin1" ?>
+<?xml version="1.0" encoding="utf-8" ?>
<!DOCTYPE part SYSTEM "part.dtd">
<part xmlns:xi="http://www.w3.org/2001/XInclude">
<header>
<copyright>
- <year>2004</year><year>2010</year>
+ <year>2004</year><year>2013</year>
<holder>Ericsson AB. All Rights Reserved.</holder>
</copyright>
<legalnotice>
diff --git a/lib/ssh/doc/src/ref_man.xml b/lib/ssh/doc/src/ref_man.xml
index 88203b5034..55339298e8 100644
--- a/lib/ssh/doc/src/ref_man.xml
+++ b/lib/ssh/doc/src/ref_man.xml
@@ -1,10 +1,10 @@
-<?xml version="1.0" encoding="iso-8859-1" ?>
+<?xml version="1.0" encoding="utf-8" ?>
<!DOCTYPE application SYSTEM "application.dtd">
<application xmlns:xi="http://www.w3.org/2001/XInclude">
<header>
<copyright>
- <year>2004</year><year>2012</year>
+ <year>2004</year><year>2013</year>
<holder>Ericsson AB. All Rights Reserved.</holder>
</copyright>
<legalnotice>
diff --git a/lib/ssh/doc/src/ssh.xml b/lib/ssh/doc/src/ssh.xml
index c1a51d57fc..5a141ced3c 100644
--- a/lib/ssh/doc/src/ssh.xml
+++ b/lib/ssh/doc/src/ssh.xml
@@ -1,4 +1,4 @@
-<?xml version="1.0" encoding="iso-8859-1" ?>
+<?xml version="1.0" encoding="utf-8" ?>
<!DOCTYPE erlref SYSTEM "erlref.dtd">
<erlref>
@@ -38,6 +38,8 @@
<item>Supported SSH version is 2.0 </item>
<item>Supported MAC algorithms: hmac-sha1</item>
<item>Supported encryption algorithms: aes128-cb and 3des-cbc</item>
+ <item>Supports unicode filenames if the emulator and the underlaying OS supports it. See the DESCRIPTION section in <seealso marker="kernel:file">file</seealso> for information about this subject</item>
+ <item>Supports unicode in shell and cli</item>
</list>
</section>
@@ -302,6 +304,36 @@
<c><![CDATA[true]]></c> if the password is valid and
<c><![CDATA[false]]></c> otherwise.</p>
</item>
+
+ <tag><c><![CDATA[{negotiation_timeout, integer()}]]></c></tag>
+ <item>
+ <p>Max time in milliseconds for the authentication negotiation. The default value is 2 minutes. If the client fails to login within this time, the connection is closed.
+ </p>
+ </item>
+
+ <tag><c><![CDATA[{max_sessions, pos_integer()}]]></c></tag>
+ <item>
+ <p>The maximum number of simultaneous sessions that are accepted at any time for this daemon. This includes sessions that are being authorized. So if set to <c>N</c>, and <c>N</c> clients have connected but not started the login process, the <c>N+1</c> connection attempt will be aborted. If <c>N</c> connections are authenticated and still logged in, no more loggins will be accepted until one of the existing ones log out.
+ </p>
+ <p>The counter is per listening port, so if two daemons are started, one with <c>{max_sessions,N}</c> and the other with <c>{max_sessions,M}</c> there will be in total <c>N+M</c> connections accepted for the whole ssh application.
+ </p>
+ <p>Note that if <c>parallel_login</c> is <c>false</c>, only one client at a time may be in the authentication phase.
+ </p>
+ <p>As default, the option is not set. This means that the number is not limited.
+ </p>
+ </item>
+
+ <tag><c><![CDATA[{parallel_login, boolean()}]]></c></tag>
+ <item>
+ <p>If set to false (the default value), only one login is handled a time. If set to true, an unlimited number of login attempts will be allowed simultanously.
+ </p>
+ <p>If the <c>max_sessions</c> option is set to <c>N</c> and <c>parallel_login</c> is set to <c>true</c>, the max number of simultaneous login attempts at any time is limited to <c>N-K</c> where <c>K</c> is the number of authenticated connections present at this daemon.
+ </p>
+ <warning>
+ <p>Do not enable <c>parallel_logins</c> without protecting the server by other means, for example the <c>max_sessions</c> option or a firewall configuration. If set to <c>true</c>, there is no protection against DOS attacks.</p>
+ </warning>
+ </item>
+
<tag><c><![CDATA[{key_cb, atom()}]]></c></tag>
<item>
<p>Module implementing the behaviour <seealso marker="ssh_server_key_api">ssh_server_key_api</seealso>.
@@ -365,8 +397,11 @@
</func>
<func>
- <name>stop() -> ok </name>
+ <name>stop() -> ok | {error, Reason}</name>
<fsummary>Stops the SSH application.</fsummary>
+ <type>
+ <v>Reason = term()</v>
+ </type>
<desc>
<p>Stops the SSH application. See also
<seealso marker="kernel:application">application(3)</seealso></p>
diff --git a/lib/ssh/doc/src/ssh_app.xml b/lib/ssh/doc/src/ssh_app.xml
index c01f44936a..a1d2402790 100644
--- a/lib/ssh/doc/src/ssh_app.xml
+++ b/lib/ssh/doc/src/ssh_app.xml
@@ -1,10 +1,10 @@
-<?xml version="1.0" encoding="iso-8859-1" ?>
+<?xml version="1.0" encoding="utf-8" ?>
<!DOCTYPE appref SYSTEM "appref.dtd">
<appref>
<header>
<copyright>
- <year>2012</year>
+ <year>2012</year><year>2013</year>
<holder>Ericsson AB. All Rights Reserved.</holder>
</copyright>
<legalnotice>
diff --git a/lib/ssh/doc/src/ssh_channel.xml b/lib/ssh/doc/src/ssh_channel.xml
index 66b3b8b656..a52a6a115e 100644
--- a/lib/ssh/doc/src/ssh_channel.xml
+++ b/lib/ssh/doc/src/ssh_channel.xml
@@ -1,4 +1,4 @@
-<?xml version="1.0" encoding="iso-8859-1" ?>
+<?xml version="1.0" encoding="utf-8" ?>
<!DOCTYPE erlref SYSTEM "erlref.dtd">
<erlref>
diff --git a/lib/ssh/doc/src/ssh_client_key_api.xml b/lib/ssh/doc/src/ssh_client_key_api.xml
index 2fa06f8bf1..f3d05a8980 100644
--- a/lib/ssh/doc/src/ssh_client_key_api.xml
+++ b/lib/ssh/doc/src/ssh_client_key_api.xml
@@ -1,4 +1,4 @@
-<?xml version="1.0" encoding="iso-8859-1" ?>
+<?xml version="1.0" encoding="utf-8" ?>
<!DOCTYPE erlref SYSTEM "erlref.dtd">
<erlref>
diff --git a/lib/ssh/doc/src/ssh_connection.xml b/lib/ssh/doc/src/ssh_connection.xml
index efd4865a6f..72e7252536 100644
--- a/lib/ssh/doc/src/ssh_connection.xml
+++ b/lib/ssh/doc/src/ssh_connection.xml
@@ -1,4 +1,4 @@
-<?xml version="1.0" encoding="iso-8859-1" ?>
+<?xml version="1.0" encoding="utf-8" ?>
<!DOCTYPE erlref SYSTEM "erlref.dtd">
<erlref>
diff --git a/lib/ssh/doc/src/ssh_protocol.xml b/lib/ssh/doc/src/ssh_protocol.xml
index 28f42f5707..7288266cf7 100644
--- a/lib/ssh/doc/src/ssh_protocol.xml
+++ b/lib/ssh/doc/src/ssh_protocol.xml
@@ -1,10 +1,10 @@
-<?xml version="1.0" encoding="iso-8859-1" ?>
+<?xml version="1.0" encoding="utf-8" ?>
<!DOCTYPE chapter SYSTEM "chapter.dtd">
<!-- %EricssonCopyright% -->
<chapter>
<header>
<copyright>
- <year>2013</year>
+ <year>2013</year><year>2013</year>
<holder>Ericsson AB. All Rights Reserved.</holder>
</copyright>
<legalnotice>
diff --git a/lib/ssh/doc/src/ssh_server_key_api.xml b/lib/ssh/doc/src/ssh_server_key_api.xml
index ee537f2f60..f7133e4ba5 100644
--- a/lib/ssh/doc/src/ssh_server_key_api.xml
+++ b/lib/ssh/doc/src/ssh_server_key_api.xml
@@ -1,4 +1,4 @@
-<?xml version="1.0" encoding="iso-8859-1" ?>
+<?xml version="1.0" encoding="utf-8" ?>
<!DOCTYPE erlref SYSTEM "erlref.dtd">
<erlref>
diff --git a/lib/ssh/doc/src/ssh_sftp.xml b/lib/ssh/doc/src/ssh_sftp.xml
index 0d61e57edb..e55d092fe2 100644
--- a/lib/ssh/doc/src/ssh_sftp.xml
+++ b/lib/ssh/doc/src/ssh_sftp.xml
@@ -1,10 +1,10 @@
-<?xml version="1.0" encoding="iso-8859-1" ?>
+<?xml version="1.0" encoding="utf-8" ?>
<!DOCTYPE erlref SYSTEM "erlref.dtd">
<erlref>
<header>
<copyright>
- <year>2005</year><year>2012</year>
+ <year>2005</year><year>2013</year>
<holder>Ericsson AB. All Rights Reserved.</holder>
</copyright>
<legalnotice>
diff --git a/lib/ssh/doc/src/ssh_sftpd.xml b/lib/ssh/doc/src/ssh_sftpd.xml
index a73d6e52d4..81c2acc575 100644
--- a/lib/ssh/doc/src/ssh_sftpd.xml
+++ b/lib/ssh/doc/src/ssh_sftpd.xml
@@ -1,4 +1,4 @@
-<?xml version="1.0" encoding="iso-8859-1" ?>
+<?xml version="1.0" encoding="utf-8" ?>
<!DOCTYPE erlref SYSTEM "erlref.dtd">
<erlref>
diff --git a/lib/ssh/doc/src/usersguide.xml b/lib/ssh/doc/src/usersguide.xml
index c818003090..8ab14c2945 100644
--- a/lib/ssh/doc/src/usersguide.xml
+++ b/lib/ssh/doc/src/usersguide.xml
@@ -1,10 +1,10 @@
-<?xml version="1.0" encoding="iso-8859-1" ?>
+<?xml version="1.0" encoding="utf-8" ?>
<!DOCTYPE part SYSTEM "part.dtd">
<part xmlns:xi="http://www.w3.org/2001/XInclude">
<header>
<copyright>
- <year>2012</year>
+ <year>2012</year><year>2013</year>
<holder>Ericsson AB. All Rights Reserved.</holder>
</copyright>
<legalnotice>
diff --git a/lib/ssh/doc/src/using_ssh.xml b/lib/ssh/doc/src/using_ssh.xml
index c994c1c56c..9ab71260d3 100644
--- a/lib/ssh/doc/src/using_ssh.xml
+++ b/lib/ssh/doc/src/using_ssh.xml
@@ -1,4 +1,4 @@
-<?xml version="1.0" encoding="iso-8859-1" ?>
+<?xml version="1.0" encoding="utf-8" ?>
<!DOCTYPE chapter SYSTEM "chapter.dtd">
<chapter>
@@ -33,7 +33,7 @@
all needed applications (crypto, public_key and ssh). All examples
are run in an Erlang shell, or in a bash shell using openssh to
illustrate how the erlang ssh application can be used. The
- exampels are run as the user otptest on a local network where the
+ examples are run as the user otptest on a local network where the
user is authorized to login in over ssh to the host "tarlop". If
nothing else is stated it is persumed that the otptest user has an
entry in tarlop's authorized_keys file (may log in via ssh without
@@ -88,7 +88,7 @@
[...]
</code>
- <p>Create the file /tmp/otptest_user/.ssh/authrized_keys and add the content
+ <p>Create the file /tmp/otptest_user/.ssh/authorized_keys and add the content
of /tmp/otptest_user/.ssh/id_rsa.pub Now we can do</p>
<code type="erl">
diff --git a/lib/ssh/src/ssh.app.src b/lib/ssh/src/ssh.app.src
index 74d7293be0..e0a51b3574 100644
--- a/lib/ssh/src/ssh.app.src
+++ b/lib/ssh/src/ssh.app.src
@@ -38,6 +38,8 @@
{registered, []},
{applications, [kernel, stdlib, crypto, public_key]},
{env, []},
- {mod, {ssh_app, []}}]}.
+ {mod, {ssh_app, []}},
+ {runtime_dependencies, ["stdlib-2.0","public_key-0.22","kernel-3.0",
+ "erts-6.0","crypto-3.3"]}]}.
diff --git a/lib/ssh/src/ssh.appup.src b/lib/ssh/src/ssh.appup.src
index 32f7cc470b..42eb2167e0 100644
--- a/lib/ssh/src/ssh.appup.src
+++ b/lib/ssh/src/ssh.appup.src
@@ -1,7 +1,7 @@
-%%
+%% -*- erlang -*-
%% %CopyrightBegin%
%%
-%% Copyright Ericsson AB 2004-2013. All Rights Reserved.
+%% Copyright Ericsson AB 2004-2014. All Rights Reserved.
%%
%% The contents of this file are subject to the Erlang Public License,
%% Version 1.1, (the "License"); you may not use this file except in
@@ -19,13 +19,13 @@
{"%VSN%",
[
- {<<"2.1\\.*">>, [{restart_application, ssh}]},
- {<<"2.0\\.*">>, [{restart_application, ssh}]},
- {<<"1\\.*">>, [{restart_application, ssh}]}
+ {"3.0.1", [{load_module, ssh, soft_purge, soft_purge, []},
+ {load_module, ssh_acceptor, soft_purge, soft_purge, []}]},
+ {<<".*">>, [{restart_application, ssh}]}
],
[
- {<<"2.1\\.*">>,[{restart_application, ssh}]},
- {<<"2.0\\.*">>, [{restart_application, ssh}]},
- {<<"1\\.*">>, [{restart_application, ssh}]}
+ {"3.0.1", [{load_module, ssh, soft_purge, soft_purge, []},
+ {load_module, ssh_acceptor, soft_purge, soft_purge, []}]},
+ {<<".*">>, [{restart_application, ssh}]}
]
}.
diff --git a/lib/ssh/src/ssh.erl b/lib/ssh/src/ssh.erl
index 2685b1553b..240de69eff 100644
--- a/lib/ssh/src/ssh.erl
+++ b/lib/ssh/src/ssh.erl
@@ -1,7 +1,7 @@
-%%
+%
%% %CopyrightBegin%
%%
-%% Copyright Ericsson AB 2004-2013. All Rights Reserved.
+%% Copyright Ericsson AB 2004-2014. All Rights Reserved.
%%
%% The contents of this file are subject to the Erlang Public License,
%% Version 1.1, (the "License"); you may not use this file except in
@@ -32,8 +32,8 @@
shell/1, shell/2, shell/3]).
%%--------------------------------------------------------------------
--spec start() -> ok.
--spec start(permanent | transient | temporary) -> ok.
+-spec start() -> ok | {error, term()}.
+-spec start(permanent | transient | temporary) -> ok | {error, term()}.
%%
%% Description: Starts the ssh application. Default type
%% is temporary. see application(3)
@@ -51,7 +51,7 @@ start(Type) ->
application:start(ssh, Type).
%%--------------------------------------------------------------------
--spec stop() -> ok.
+-spec stop() -> ok | {error, term()}.
%%
%% Description: Stops the ssh application.
%%--------------------------------------------------------------------
@@ -59,8 +59,8 @@ stop() ->
application:stop(ssh).
%%--------------------------------------------------------------------
--spec connect(string(), integer(), proplists:proplists()) -> {ok, pid()} | {error, term()}.
--spec connect(string(), integer(), proplists:proplists(), timeout()) -> {ok, pid()} | {error, term()}.
+-spec connect(string(), integer(), proplists:proplist()) -> {ok, pid()} | {error, term()}.
+-spec connect(string(), integer(), proplists:proplist(), timeout()) -> {ok, pid()} | {error, term()}.
%%
%% Description: Starts an ssh connection.
%%--------------------------------------------------------------------
@@ -73,8 +73,9 @@ connect(Host, Port, Options, Timeout) ->
{SocketOptions, SshOptions} ->
{_, Transport, _} = TransportOpts =
proplists:get_value(transport, Options, {tcp, gen_tcp, tcp_closed}),
+ ConnectionTimeout = proplists:get_value(connect_timeout, Options, infinity),
Inet = proplists:get_value(inet, SshOptions, inet),
- try Transport:connect(Host, Port, [ {active, false}, Inet | SocketOptions], Timeout) of
+ try Transport:connect(Host, Port, [ {active, false}, Inet | SocketOptions], ConnectionTimeout) of
{ok, Socket} ->
Opts = [{user_pid, self()}, {host, Host} | fix_idle_time(SshOptions)],
ssh_connection_handler:start_connection(client, Socket, Opts, Timeout);
@@ -332,6 +333,14 @@ handle_option([{idle_time, _} = Opt | Rest], SocketOptions, SshOptions) ->
handle_option(Rest, SocketOptions, [handle_ssh_option(Opt) | SshOptions]);
handle_option([{rekey_limit, _} = Opt|Rest], SocketOptions, SshOptions) ->
handle_option(Rest, SocketOptions, [handle_ssh_option(Opt) | SshOptions]);
+handle_option([{max_sessions, _} = Opt|Rest], SocketOptions, SshOptions) ->
+ handle_option(Rest, SocketOptions, [handle_ssh_option(Opt) | SshOptions]);
+handle_option([{negotiation_timeout, _} = Opt|Rest], SocketOptions, SshOptions) ->
+ handle_option(Rest, SocketOptions, [handle_ssh_option(Opt) | SshOptions]);
+handle_option([{parallel_login, _} = Opt|Rest], SocketOptions, SshOptions) ->
+ handle_option(Rest, SocketOptions, [handle_ssh_option(Opt) | SshOptions]);
+handle_option([parallel_login|Rest], SocketOptions, SshOptions) ->
+ handle_option(Rest, SocketOptions, [handle_ssh_option({parallel_login,true}) | SshOptions]);
handle_option([Opt | Rest], SocketOptions, SshOptions) ->
handle_option(Rest, [handle_inet_option(Opt) | SocketOptions], SshOptions).
@@ -360,6 +369,12 @@ handle_ssh_option({pref_public_key_algs, Value} = Opt) when is_list(Value), leng
end;
handle_ssh_option({connect_timeout, Value} = Opt) when is_integer(Value); Value == infinity ->
Opt;
+handle_ssh_option({max_sessions, Value} = Opt) when is_integer(Value), Value>0 ->
+ Opt;
+handle_ssh_option({negotiation_timeout, Value} = Opt) when is_integer(Value); Value == infinity ->
+ Opt;
+handle_ssh_option({parallel_login, Value} = Opt) when Value==true ; Value==false ->
+ Opt;
handle_ssh_option({user, Value} = Opt) when is_list(Value) ->
Opt;
handle_ssh_option({dsa_pass_phrase, Value} = Opt) when is_list(Value) ->
diff --git a/lib/ssh/src/ssh.hrl b/lib/ssh/src/ssh.hrl
index 94ced9da6f..0c4d34f89c 100644
--- a/lib/ssh/src/ssh.hrl
+++ b/lib/ssh/src/ssh.hrl
@@ -54,6 +54,7 @@
-define(uint32(X), << ?UINT32(X) >> ).
-define(uint64(X), << ?UINT64(X) >> ).
-define(string(X), << ?STRING(list_to_binary(X)) >> ).
+-define(string_utf8(X), << ?STRING(unicode:characters_to_binary(X)) >> ).
-define(binary(X), << ?STRING(X) >>).
-define(SSH_CIPHER_NONE, 0).
diff --git a/lib/ssh/src/ssh_acceptor.erl b/lib/ssh/src/ssh_acceptor.erl
index 91905b2eaf..7302196674 100644
--- a/lib/ssh/src/ssh_acceptor.erl
+++ b/lib/ssh/src/ssh_acceptor.erl
@@ -80,15 +80,36 @@ acceptor_loop(Callback, Port, Address, Opts, ListenSocket, AcceptTimeout) ->
ListenSocket, AcceptTimeout)
end.
-handle_connection(_Callback, Address, Port, Options, Socket) ->
+handle_connection(Callback, Address, Port, Options, Socket) ->
SystemSup = ssh_system_sup:system_supervisor(Address, Port),
- {ok, SubSysSup} = ssh_system_sup:start_subsystem(SystemSup, Options),
- ConnectionSup = ssh_subsystem_sup:connection_supervisor(SubSysSup),
- ssh_connection_handler:start_connection(server, Socket,
- [{supervisors, [{system_sup, SystemSup},
- {subsystem_sup, SubSysSup},
- {connection_sup, ConnectionSup}]}
- | Options], infinity).
+ SSHopts = proplists:get_value(ssh_opts, Options, []),
+ MaxSessions = proplists:get_value(max_sessions,SSHopts,infinity),
+ case number_of_connections(SystemSup) < MaxSessions of
+ true ->
+ {ok, SubSysSup} = ssh_system_sup:start_subsystem(SystemSup, Options),
+ ConnectionSup = ssh_subsystem_sup:connection_supervisor(SubSysSup),
+ Timeout = proplists:get_value(negotiation_timeout, SSHopts, 2*60*1000),
+ ssh_connection_handler:start_connection(server, Socket,
+ [{supervisors, [{system_sup, SystemSup},
+ {subsystem_sup, SubSysSup},
+ {connection_sup, ConnectionSup}]}
+ | Options], Timeout);
+ false ->
+ Callback:close(Socket),
+ IPstr = if is_tuple(Address) -> inet:ntoa(Address);
+ true -> Address
+ end,
+ Str = try io_lib:format('~s:~p',[IPstr,Port])
+ catch _:_ -> "port "++integer_to_list(Port)
+ end,
+ error_logger:info_report("Ssh login attempt to "++Str++" denied due to option "
+ "max_sessions limits to "++ io_lib:write(MaxSessions) ++
+ " sessions."
+ ),
+ {error,max_sessions}
+ end.
+
+
handle_error(timeout) ->
ok;
@@ -114,3 +135,10 @@ handle_error(Reason) ->
String = lists:flatten(io_lib:format("Accept error: ~p", [Reason])),
error_logger:error_report(String),
exit({accept_failed, String}).
+
+
+number_of_connections(SystemSup) ->
+ length([X ||
+ {R,X,supervisor,[ssh_subsystem_sup]} <- supervisor:which_children(SystemSup),
+ is_reference(R)
+ ]).
diff --git a/lib/ssh/src/ssh_auth.erl b/lib/ssh/src/ssh_auth.erl
index 1fa3df847f..45fd907383 100644
--- a/lib/ssh/src/ssh_auth.erl
+++ b/lib/ssh/src/ssh_auth.erl
@@ -83,7 +83,7 @@ password_msg([#ssh{opts = Opts, io_cb = IoCb,
method = "password",
data =
<<?BOOLEAN(?FALSE),
- ?STRING(list_to_binary(Password))>>},
+ ?STRING(unicode:characters_to_binary(Password))>>},
Ssh)
end.
@@ -190,14 +190,13 @@ handle_userauth_request(#ssh_msg_userauth_request{user = User,
data = Data}, _,
#ssh{opts = Opts} = Ssh) ->
<<_:8, ?UINT32(Sz), BinPwd:Sz/binary>> = Data,
- Password = binary_to_list(BinPwd),
-
+ Password = unicode:characters_to_list(BinPwd),
case check_password(User, Password, Opts) of
true ->
{authorized, User,
ssh_transport:ssh_packet(#ssh_msg_userauth_success{}, Ssh)};
false ->
- {not_authorized, {User, {passwd, Password}},
+ {not_authorized, {User, {error,"Bad user or password"}},
ssh_transport:ssh_packet(#ssh_msg_userauth_failure{
authentications = "",
partial_success = false}, Ssh)}
@@ -229,7 +228,7 @@ handle_userauth_request(#ssh_msg_userauth_request{user = User,
ssh_transport:ssh_packet(
#ssh_msg_userauth_success{}, Ssh)};
false ->
- {not_authorized, {User, {error, "Invalid signature"}},
+ {not_authorized, {User, undefined},
ssh_transport:ssh_packet(#ssh_msg_userauth_failure{
authentications="publickey,password",
partial_success = false}, Ssh)}
@@ -352,7 +351,7 @@ verify_sig(SessionId, User, Service, Alg, KeyBlob, SigWLen, Opts) ->
build_sig_data(SessionId, User, Service, KeyBlob, Alg) ->
Sig = [?binary(SessionId),
?SSH_MSG_USERAUTH_REQUEST,
- ?string(User),
+ ?string_utf8(User),
?string(Service),
?binary(<<"publickey">>),
?TRUE,
diff --git a/lib/ssh/src/ssh_bits.erl b/lib/ssh/src/ssh_bits.erl
index 2b0241cb83..8aaff93b9f 100644
--- a/lib/ssh/src/ssh_bits.erl
+++ b/lib/ssh/src/ssh_bits.erl
@@ -116,6 +116,10 @@ enc(Xs, [string|Ts], Offset) ->
X0 = hd(Xs),
Y = ?string(X0),
[Y | enc(tl(Xs),Ts,Offset+size(Y))];
+enc(Xs, [string_utf8|Ts], Offset) ->
+ X0 = hd(Xs),
+ Y = ?string_utf8(X0),
+ [Y | enc(tl(Xs),Ts,Offset+size(Y))];
enc(Xs, [binary|Ts], Offset) ->
X0 = hd(Xs),
Y = ?binary(X0),
diff --git a/lib/ssh/src/ssh_connection.erl b/lib/ssh/src/ssh_connection.erl
index 03dddae3c8..b377614949 100644
--- a/lib/ssh/src/ssh_connection.erl
+++ b/lib/ssh/src/ssh_connection.erl
@@ -271,10 +271,36 @@ cancel_tcpip_forward(ConnectionHandler, BindIP, Port) ->
%%--------------------------------------------------------------------
%%% Internal API
%%--------------------------------------------------------------------
+l2b(L) when is_integer(hd(L)) ->
+ try list_to_binary(L)
+ of
+ B -> B
+ catch
+ _:_ ->
+ unicode:characters_to_binary(L)
+ end;
+l2b([H|T]) ->
+ << (l2b(H))/binary, (l2b(T))/binary >>;
+l2b(B) when is_binary(B) ->
+ B;
+l2b([]) ->
+ <<>>.
+
+
+
channel_data(ChannelId, DataType, Data, Connection, From)
when is_list(Data)->
channel_data(ChannelId, DataType,
- list_to_binary(Data), Connection, From);
+%% list_to_binary(Data), Connection, From);
+ l2b(Data), Connection, From);
+ %% try list_to_binary(Data)
+ %% of
+ %% B -> B
+ %% catch
+ %% _:_ -> io:format('BAD BINARY: ~p~n',[Data]),
+ %% unicode:characters_to_binary(Data)
+ %% end,
+ %% Connection, From);
channel_data(ChannelId, DataType, Data,
#connection{channel_cache = Cache} = Connection,
diff --git a/lib/ssh/src/ssh_connection_handler.erl b/lib/ssh/src/ssh_connection_handler.erl
index 3462b98172..06866392da 100644
--- a/lib/ssh/src/ssh_connection_handler.erl
+++ b/lib/ssh/src/ssh_connection_handler.erl
@@ -110,8 +110,16 @@ start_connection(server = Role, Socket, Options, Timeout) ->
{ok, Pid} = ssh_connection_sup:start_child(ConnectionSup, [Role, Socket, Opts]),
{_, Callback, _} = proplists:get_value(transport, Options, {tcp, gen_tcp, tcp_closed}),
socket_control(Socket, Pid, Callback),
- Ref = erlang:monitor(process, Pid),
- handshake(Pid, Ref, Timeout)
+ case proplists:get_value(parallel_login, Opts, false) of
+ true ->
+ spawn(fun() ->
+ Ref = erlang:monitor(process, Pid),
+ handshake(Pid, Ref, Timeout)
+ end);
+ false ->
+ Ref = erlang:monitor(process, Pid),
+ handshake(Pid, Ref, Timeout)
+ end
catch
exit:{noproc, _} ->
{error, ssh_not_started};
@@ -157,7 +165,7 @@ init([Role, Socket, SshOpts]) ->
%%--------------------------------------------------------------------
-spec open_channel(pid(), string(), iodata(), integer(), integer(),
- timeout()) -> {open, channel_id()} | {open_error, term(), string(), string()}.
+ timeout()) -> {open, channel_id()} | {error, term()}.
%%--------------------------------------------------------------------
open_channel(ConnectionHandler, ChannelType, ChannelSpecificData,
InitialWindowSize,
@@ -206,7 +214,7 @@ global_request(ConnectionHandler, Type, false = Reply, Data) ->
send_all_state_event(ConnectionHandler, {global_request, self(), Type, Reply, Data}).
%%--------------------------------------------------------------------
--spec send(pid(), channel_id(), integer(), iolist(), timeout()) ->
+-spec send(pid(), channel_id(), integer(), iodata(), timeout()) ->
ok | {error, timeout} | {error, closed}.
%%--------------------------------------------------------------------
send(ConnectionHandler, ChannelId, Type, Data, Timeout) ->
@@ -1474,8 +1482,7 @@ ssh_channel_info([ _ | Rest], Channel, Acc) ->
log_error(Reason) ->
Report = io_lib:format("Erlang ssh connection handler failed with reason: "
- "~p ~n, Stacktace: ~p ~n"
- "please report this to [email protected] \n",
+ "~p ~n, Stacktrace: ~p ~n",
[Reason, erlang:get_stacktrace()]),
error_logger:error_report(Report),
"Internal error".
diff --git a/lib/ssh/src/ssh_io.erl b/lib/ssh/src/ssh_io.erl
index 832b144db9..35336bce8b 100644
--- a/lib/ssh/src/ssh_io.erl
+++ b/lib/ssh/src/ssh_io.erl
@@ -81,6 +81,8 @@ format(Fmt, Args) ->
trim(Line) when is_list(Line) ->
lists:reverse(trim1(lists:reverse(trim1(Line))));
+trim(Line) when is_binary(Line) ->
+ trim(unicode:characters_to_list(Line));
trim(Other) -> Other.
trim1([$\s|Cs]) -> trim(Cs);
diff --git a/lib/ssh/src/ssh_message.erl b/lib/ssh/src/ssh_message.erl
index a63881db64..8d6c77c0ed 100644
--- a/lib/ssh/src/ssh_message.erl
+++ b/lib/ssh/src/ssh_message.erl
@@ -120,7 +120,7 @@ encode(#ssh_msg_userauth_request{
data = Data
}) ->
ssh_bits:encode([?SSH_MSG_USERAUTH_REQUEST, User, Service, Method, Data],
- [byte, string, string, string, '...']);
+ [byte, string_utf8, string, string, '...']);
encode(#ssh_msg_userauth_failure{
authentications = Auths,
partial_success = Bool
@@ -135,7 +135,7 @@ encode(#ssh_msg_userauth_banner{
language = Lang
}) ->
ssh_bits:encode([?SSH_MSG_USERAUTH_BANNER, Banner, Lang],
- [byte, string, string]);
+ [byte, string_utf8, string]);
encode(#ssh_msg_userauth_pk_ok{
algorithm_name = Alg,
diff --git a/lib/ssh/src/ssh_sftp.erl b/lib/ssh/src/ssh_sftp.erl
index 10167a9223..0ea2366ac7 100644
--- a/lib/ssh/src/ssh_sftp.erl
+++ b/lib/ssh/src/ssh_sftp.erl
@@ -352,7 +352,7 @@ write_file(Pid, Name, List) ->
write_file(Pid, Name, List, ?FILEOP_TIMEOUT).
write_file(Pid, Name, List, FileOpTimeout) when is_list(List) ->
- write_file(Pid, Name, list_to_binary(List), FileOpTimeout);
+ write_file(Pid, Name, unicode:characters_to_binary(List), FileOpTimeout);
write_file(Pid, Name, Bin, FileOpTimeout) ->
case open(Pid, Name, [write, binary], FileOpTimeout) of
{ok, Handle} ->
@@ -514,7 +514,7 @@ do_handle_call({pread,Async,Handle,At,Length}, From, State) ->
case get_mode(Handle, State2) of
binary -> {{ok,Data}, State2};
text ->
- {{ok,binary_to_list(Data)}, State2}
+ {{ok,unicode:characters_to_list(Data)}, State2}
end;
(Rep, State2) ->
{Rep, State2}
@@ -535,8 +535,7 @@ do_handle_call({read,Async,Handle,Length}, From, State) ->
fun({ok,Data}, State2) ->
case get_mode(Handle, State2) of
binary -> {{ok,Data}, State2};
- text ->
- {{ok,binary_to_list(Data)}, State2}
+ text -> {{ok,binary_to_list(Data)}, State2}
end;
(Rep, State2) -> {Rep, State2}
end);
diff --git a/lib/ssh/src/ssh_sftpd.erl b/lib/ssh/src/ssh_sftpd.erl
index 174ca0126b..52665635f0 100644
--- a/lib/ssh/src/ssh_sftpd.erl
+++ b/lib/ssh/src/ssh_sftpd.erl
@@ -214,8 +214,7 @@ handle_op(?SSH_FXP_INIT, Version, B, State) when is_binary(B) ->
handle_op(?SSH_FXP_REALPATH, ReqId,
<<?UINT32(Rlen), RPath:Rlen/binary>>,
State0) ->
- RelPath0 = binary_to_list(RPath),
- RelPath = relate_file_name(RelPath0, State0, _Canonicalize=false),
+ RelPath = relate_file_name(RPath, State0, _Canonicalize=false),
{Res, State} = resolve_symlinks(RelPath, State0),
case Res of
{ok, AbsPath} ->
@@ -231,7 +230,7 @@ handle_op(?SSH_FXP_OPENDIR, ReqId,
<<?UINT32(RLen), RPath:RLen/binary>>,
State0 = #state{xf = #ssh_xfer{vsn = Vsn},
file_handler = FileMod, file_state = FS0}) ->
- RelPath = binary_to_list(RPath),
+ RelPath = unicode:characters_to_list(RPath),
AbsPath = relate_file_name(RelPath, State0),
XF = State0#state.xf,
@@ -312,9 +311,8 @@ handle_op(?SSH_FXP_WRITE, ReqId,
?SSH_FX_INVALID_HANDLE),
State
end;
-handle_op(?SSH_FXP_READLINK, ReqId, <<?UINT32(PLen), BPath:PLen/binary>>,
+handle_op(?SSH_FXP_READLINK, ReqId, <<?UINT32(PLen), RelPath:PLen/binary>>,
State = #state{file_handler = FileMod, file_state = FS0}) ->
- RelPath = binary_to_list(BPath),
AbsPath = relate_file_name(RelPath, State),
{Res, FS1} = FileMod:read_link(AbsPath, FS0),
case Res of
@@ -524,10 +522,10 @@ close_our_file({_,Fd}, FileMod, FS0) ->
%%% stat: do the stat
stat(Vsn, ReqId, Data, State, F) when Vsn =< 3->
<<?UINT32(BLen), BPath:BLen/binary>> = Data,
- stat(ReqId, binary_to_list(BPath), State, F);
+ stat(ReqId, unicode:characters_to_list(BPath), State, F);
stat(Vsn, ReqId, Data, State, F) when Vsn >= 4->
<<?UINT32(BLen), BPath:BLen/binary, ?UINT32(_Flags)>> = Data,
- stat(ReqId, binary_to_list(BPath), State, F).
+ stat(ReqId, unicode:characters_to_list(BPath), State, F).
fstat(Vsn, ReqId, Data, State) when Vsn =< 3->
<<?UINT32(HLen), Handle:HLen/binary>> = Data,
@@ -609,13 +607,13 @@ decode_4_acess([]) ->
open(Vsn, ReqId, Data, State) when Vsn =< 3 ->
<<?UINT32(BLen), BPath:BLen/binary, ?UINT32(PFlags),
_Attrs/binary>> = Data,
- Path = binary_to_list(BPath),
+ Path = unicode:characters_to_list(BPath),
Flags = ssh_xfer:decode_open_flags(Vsn, PFlags),
do_open(ReqId, State, Path, Flags);
open(Vsn, ReqId, Data, State) when Vsn >= 4 ->
<<?UINT32(BLen), BPath:BLen/binary, ?UINT32(Access),
?UINT32(PFlags), _Attrs/binary>> = Data,
- Path = binary_to_list(BPath),
+ Path = unicode:characters_to_list(BPath),
FlagBits = ssh_xfer:decode_open_flags(Vsn, PFlags),
AcessBits = ssh_xfer:decode_ace_mask(Access),
%% TODO: This is to make sure the Access flags are not ignored
@@ -675,7 +673,7 @@ resolve_symlinks_2(["." | RestPath], State0, LinkCnt, AccPath) ->
resolve_symlinks_2([".." | RestPath], State0, LinkCnt, AccPath) ->
%% Remove the last path component
AccPathComps0 = filename:split(AccPath),
- Path = case lists:reverse(tl(lists:reverse(AccPathComps0))) of
+ Path = case lists:droplast(AccPathComps0) of
[] ->
"";
AccPathComps ->
@@ -712,7 +710,7 @@ relate_file_name(File, State) ->
relate_file_name(File, State, _Canonicalize=true).
relate_file_name(File, State, Canonicalize) when is_binary(File) ->
- relate_file_name(binary_to_list(File), State, Canonicalize);
+ relate_file_name(unicode:characters_to_list(File), State, Canonicalize);
relate_file_name(File, #state{cwd = CWD, root = ""}, Canonicalize) ->
relate_filename_to_path(File, CWD, Canonicalize);
relate_file_name(File, #state{root = Root}, Canonicalize) ->
diff --git a/lib/ssh/src/ssh_xfer.erl b/lib/ssh/src/ssh_xfer.erl
index e18e18a9a9..63d01fd9de 100644
--- a/lib/ssh/src/ssh_xfer.erl
+++ b/lib/ssh/src/ssh_xfer.erl
@@ -72,7 +72,6 @@ protocol_version_request(XF) ->
open(XF, ReqID, FileName, Access, Flags, Attrs) ->
Vsn = XF#ssh_xfer.vsn,
- FileName1 = unicode:characters_to_binary(FileName),
MBits = if Vsn >= 5 ->
M = encode_ace_mask(Access),
?uint32(M);
@@ -82,7 +81,7 @@ open(XF, ReqID, FileName, Access, Flags, Attrs) ->
F = encode_open_flags(Flags),
xf_request(XF,?SSH_FXP_OPEN,
[?uint32(ReqID),
- ?binary(FileName1),
+ ?string_utf8(FileName),
MBits,
?uint32(F),
encode_ATTR(Vsn,Attrs)]).
@@ -90,7 +89,7 @@ open(XF, ReqID, FileName, Access, Flags, Attrs) ->
opendir(XF, ReqID, DirName) ->
xf_request(XF, ?SSH_FXP_OPENDIR,
[?uint32(ReqID),
- ?string(DirName)]).
+ ?string_utf8(DirName)]).
close(XF, ReqID, Handle) ->
@@ -127,13 +126,11 @@ write(XF,ReqID, Handle, Offset, Data) ->
remove(XF, ReqID, File) ->
xf_request(XF, ?SSH_FXP_REMOVE,
[?uint32(ReqID),
- ?string(File)]).
+ ?string_utf8(File)]).
%% Rename a file/directory
-rename(XF, ReqID, Old, New, Flags) ->
+rename(XF, ReqID, OldPath, NewPath, Flags) ->
Vsn = XF#ssh_xfer.vsn,
- OldPath = unicode:characters_to_binary(Old),
- NewPath = unicode:characters_to_binary(New),
FlagBits
= if Vsn >= 5 ->
F0 = encode_rename_flags(Flags),
@@ -143,30 +140,27 @@ rename(XF, ReqID, Old, New, Flags) ->
end,
xf_request(XF, ?SSH_FXP_RENAME,
[?uint32(ReqID),
- ?binary(OldPath),
- ?binary(NewPath),
+ ?string_utf8(OldPath),
+ ?string_utf8(NewPath),
FlagBits]).
%% Create directory
mkdir(XF, ReqID, Path, Attrs) ->
- Path1 = unicode:characters_to_binary(Path),
xf_request(XF, ?SSH_FXP_MKDIR,
[?uint32(ReqID),
- ?binary(Path1),
+ ?string_utf8(Path),
encode_ATTR(XF#ssh_xfer.vsn, Attrs)]).
%% Remove a directory
rmdir(XF, ReqID, Dir) ->
- Dir1 = unicode:characters_to_binary(Dir),
xf_request(XF, ?SSH_FXP_RMDIR,
[?uint32(ReqID),
- ?binary(Dir1)]).
+ ?string_utf8(Dir)]).
%% Stat file
stat(XF, ReqID, Path, Flags) ->
- Path1 = unicode:characters_to_binary(Path),
Vsn = XF#ssh_xfer.vsn,
AttrFlags = if Vsn >= 5 ->
F = encode_attr_flags(Vsn, Flags),
@@ -176,13 +170,12 @@ stat(XF, ReqID, Path, Flags) ->
end,
xf_request(XF, ?SSH_FXP_STAT,
[?uint32(ReqID),
- ?binary(Path1),
+ ?string_utf8(Path),
AttrFlags]).
%% Stat file - follow symbolic links
lstat(XF, ReqID, Path, Flags) ->
- Path1 = unicode:characters_to_binary(Path),
Vsn = XF#ssh_xfer.vsn,
AttrFlags = if Vsn >= 5 ->
F = encode_attr_flags(Vsn, Flags),
@@ -192,7 +185,7 @@ lstat(XF, ReqID, Path, Flags) ->
end,
xf_request(XF, ?SSH_FXP_LSTAT,
[?uint32(ReqID),
- ?binary(Path1),
+ ?string_utf8(Path),
AttrFlags]).
%% Stat open file
@@ -211,10 +204,9 @@ fstat(XF, ReqID, Handle, Flags) ->
%% Modify file attributes
setstat(XF, ReqID, Path, Attrs) ->
- Path1 = unicode:characters_to_binary(Path),
xf_request(XF, ?SSH_FXP_SETSTAT,
[?uint32(ReqID),
- ?binary(Path1),
+ ?string_utf8(Path),
encode_ATTR(XF#ssh_xfer.vsn, Attrs)]).
@@ -227,10 +219,9 @@ fsetstat(XF, ReqID, Handle, Attrs) ->
%% Read a symbolic link
readlink(XF, ReqID, Path) ->
- Path1 = unicode:characters_to_binary(Path),
xf_request(XF, ?SSH_FXP_READLINK,
[?uint32(ReqID),
- ?binary(Path1)]).
+ ?string_utf8(Path)]).
%% Create a symbolic link
@@ -244,10 +235,9 @@ symlink(XF, ReqID, LinkPath, TargetPath) ->
%% Convert a path into a 'canonical' form
realpath(XF, ReqID, Path) ->
- Path1 = unicode:characters_to_binary(Path),
xf_request(XF, ?SSH_FXP_REALPATH,
[?uint32(ReqID),
- ?binary(Path1)]).
+ ?string_utf8(Path)]).
extended(XF, ReqID, Request, Data) ->
xf_request(XF, ?SSH_FXP_EXTENDED,
@@ -296,7 +286,10 @@ xf_send_names(#ssh_xfer{cm = CM, channel = Channel, vsn = Vsn},
Count = length(NamesAndAttrs),
{Data, Len} = encode_names(Vsn, NamesAndAttrs),
Size = 1 + 4 + 4 + Len,
- ToSend = [<<?UINT32(Size), ?SSH_FXP_NAME, ?UINT32(ReqId), ?UINT32(Count)>>,
+ ToSend = [<<?UINT32(Size),
+ ?SSH_FXP_NAME,
+ ?UINT32(ReqId),
+ ?UINT32(Count)>>,
Data],
ssh_connection:send(CM, Channel, ToSend).
@@ -818,25 +811,27 @@ decode_names(_Vsn, 0, _Data) ->
decode_names(Vsn, I, <<?UINT32(Len), FileName:Len/binary,
?UINT32(LLen), _LongName:LLen/binary,
Tail/binary>>) when Vsn =< 3 ->
- Name = binary_to_list(FileName),
+ Name = unicode:characters_to_list(FileName),
{A, Tail2} = decode_ATTR(Vsn, Tail),
[{Name, A} | decode_names(Vsn, I-1, Tail2)];
decode_names(Vsn, I, <<?UINT32(Len), FileName:Len/binary,
Tail/binary>>) when Vsn >= 4 ->
- Name = binary_to_list(FileName),
+ Name = unicode:characters_to_list(FileName),
{A, Tail2} = decode_ATTR(Vsn, Tail),
[{Name, A} | decode_names(Vsn, I-1, Tail2)].
encode_names(Vsn, NamesAndAttrs) ->
lists:mapfoldl(fun(N, L) -> encode_name(Vsn, N, L) end, 0, NamesAndAttrs).
-encode_name(Vsn, {Name,Attr}, Len) when Vsn =< 3 ->
+encode_name(Vsn, {NameUC,Attr}, Len) when Vsn =< 3 ->
+ Name = binary_to_list(unicode:characters_to_binary(NameUC)),
NLen = length(Name),
EncAttr = encode_ATTR(Vsn, Attr),
ALen = size(EncAttr),
NewLen = Len + NLen*2 + 4 + 4 + ALen,
{[<<?UINT32(NLen)>>, Name, <<?UINT32(NLen)>>, Name, EncAttr], NewLen};
-encode_name(Vsn, {Name,Attr}, Len) when Vsn >= 4 ->
+encode_name(Vsn, {NameUC,Attr}, Len) when Vsn >= 4 ->
+ Name = binary_to_list(unicode:characters_to_binary(NameUC)),
NLen = length(Name),
EncAttr = encode_ATTR(Vsn, Attr),
ALen = size(EncAttr),
@@ -851,9 +846,9 @@ encode_acl_items([ACE|As]) ->
Type = encode_ace_type(ACE#ssh_xfer_ace.type),
Flag = encode_ace_flag(ACE#ssh_xfer_ace.flag),
Mask = encode_ace_mask(ACE#ssh_xfer_ace.mask),
- Who = list_to_binary(ACE#ssh_xfer_ace.who),
+ Who = ACE#ssh_xfer_ace.who,
[?uint32(Type), ?uint32(Flag), ?uint32(Mask),
- ?binary(Who) | encode_acl_items(As)];
+ ?string_utf8(Who) | encode_acl_items(As)];
encode_acl_items([]) ->
[].
@@ -872,7 +867,7 @@ decode_acl_items(I, <<?UINT32(Type),
[#ssh_xfer_ace { type = decode_ace_type(Type),
flag = decode_ace_flag(Flag),
mask = decode_ace_mask(Mask),
- who = binary_to_list(BWho)} | Acc]).
+ who = unicode:characters_to_list(BWho)} | Acc]).
encode_extensions(Exts) ->
Count = length(Exts),
diff --git a/lib/ssh/test/ssh_basic_SUITE.erl b/lib/ssh/test/ssh_basic_SUITE.erl
index b4e3871efd..ba38c1da40 100644
--- a/lib/ssh/test/ssh_basic_SUITE.erl
+++ b/lib/ssh/test/ssh_basic_SUITE.erl
@@ -1,7 +1,7 @@
%%
%% %CopyrightBegin%
%%
-%% Copyright Ericsson AB 2008-2013. All Rights Reserved.
+%% Copyright Ericsson AB 2008-2014. All Rights Reserved.
%%
%% The contents of this file are subject to the Erlang Public License,
%% Version 1.1, (the "License"); you may not use this file except in
@@ -38,6 +38,7 @@ suite() ->
all() ->
[app_test,
+ appup_test,
{group, dsa_key},
{group, rsa_key},
{group, dsa_pass_key},
@@ -46,21 +47,28 @@ all() ->
daemon_already_started,
server_password_option,
server_userpassword_option,
- double_close].
+ double_close,
+ ssh_connect_timeout,
+ ssh_connect_arg4_timeout,
+ {group, hardening_tests}
+ ].
groups() ->
[{dsa_key, [], basic_tests()},
{rsa_key, [], basic_tests()},
{dsa_pass_key, [], [pass_phrase]},
{rsa_pass_key, [], [pass_phrase]},
- {internal_error, [], [internal_error]}
+ {internal_error, [], [internal_error]},
+ {hardening_tests, [], [max_sessions]}
].
+
basic_tests() ->
[send, close, peername_sockname,
exec, exec_compressed, shell, cli, known_hosts,
idle_time, rekey, openssh_zlib_basic_test].
+
%%--------------------------------------------------------------------
init_per_suite(Config) ->
case catch crypto:start() of
@@ -73,6 +81,8 @@ end_per_suite(_Config) ->
ssh:stop(),
crypto:stop().
%%--------------------------------------------------------------------
+init_per_group(hardening_tests, Config) ->
+ init_per_group(dsa_key, Config);
init_per_group(dsa_key, Config) ->
DataDir = ?config(data_dir, Config),
PrivDir = ?config(priv_dir, Config),
@@ -102,6 +112,8 @@ init_per_group(internal_error, Config) ->
init_per_group(_, Config) ->
Config.
+end_per_group(hardening_tests, Config) ->
+ end_per_group(dsa_key, Config);
end_per_group(dsa_key, Config) ->
PrivDir = ?config(priv_dir, Config),
ssh_test_lib:clean_dsa(PrivDir),
@@ -150,6 +162,11 @@ app_test(Config) when is_list(Config) ->
?t:app_test(ssh),
ok.
%%--------------------------------------------------------------------
+appup_test() ->
+ [{doc, "Appup file consistency test."}].
+appup_test(Config) when is_list(Config) ->
+ ok = ?t:appup_test(ssh).
+%%--------------------------------------------------------------------
misc_ssh_options() ->
[{doc, "Test that we can set some misc options not tested elsewhere, "
"some options not yet present are not decided if we should support or "
@@ -614,6 +631,86 @@ double_close(Config) when is_list(Config) ->
ok = ssh:close(CM).
%%--------------------------------------------------------------------
+ssh_connect_timeout() ->
+ [{doc, "Test connect_timeout option in ssh:connect/4"}].
+ssh_connect_timeout(_Config) ->
+ ConnTimeout = 2000,
+ {error,{faked_transport,connect,TimeoutToTransport}} =
+ ssh:connect("localhost", 12345,
+ [{transport,{tcp,?MODULE,tcp_closed}},
+ {connect_timeout,ConnTimeout}],
+ 1000),
+ case TimeoutToTransport of
+ ConnTimeout -> ok;
+ Other ->
+ ct:log("connect_timeout is ~p but transport received ~p",[ConnTimeout,Other]),
+ {fail,"ssh:connect/4 wrong connect_timeout received in transport"}
+ end.
+
+%% Help for the test above
+connect(_Host, _Port, _Opts, Timeout) ->
+ {error, {faked_transport,connect,Timeout}}.
+
+
+%%--------------------------------------------------------------------
+ssh_connect_arg4_timeout() ->
+ [{doc, "Test fourth argument in ssh:connect/4"}].
+ssh_connect_arg4_timeout(_Config) ->
+ Timeout = 1000,
+ Parent = self(),
+ %% start the server
+ Server = spawn(fun() ->
+ {ok,Sl} = gen_tcp:listen(0,[]),
+ {ok,{_,Port}} = inet:sockname(Sl),
+ Parent ! {port,self(),Port},
+ Rsa = gen_tcp:accept(Sl),
+ ct:log("Server gen_tcp:accept got ~p",[Rsa]),
+ receive after 2*Timeout -> ok end %% let client timeout first
+ end),
+
+ %% Get listening port
+ Port = receive
+ {port,Server,ServerPort} -> ServerPort
+ end,
+
+ %% try to connect with a timeout, but "supervise" it
+ Client = spawn(fun() ->
+ T0 = now(),
+ Rc = ssh:connect("localhost",Port,[],Timeout),
+ ct:log("Client ssh:connect got ~p",[Rc]),
+ Parent ! {done,self(),Rc,T0}
+ end),
+
+ %% Wait for client reaction on the connection try:
+ receive
+ {done, Client, {error,_E}, T0} ->
+ Msp = ms_passed(T0, now()),
+ exit(Server,hasta_la_vista___baby),
+ Low = 0.9*Timeout,
+ High = 1.1*Timeout,
+ ct:log("Timeout limits: ~p--~p, timeout was ~p, expected ~p",[Low,High,Msp,Timeout]),
+ if
+ Low<Msp, Msp<High -> ok;
+ true -> {fail, "timeout not within limits"}
+ end;
+ {done, Client, {ok,_Ref}, _T0} ->
+ {fail,"ssh-connected ???"}
+ after
+ 5000 ->
+ exit(Server,hasta_la_vista___baby),
+ exit(Client,hasta_la_vista___baby),
+ {fail, "Didn't timeout"}
+ end.
+
+
+%% Help function
+%% N2-N1
+ms_passed(N1={_,_,M1}, N2={_,_,M2}) ->
+ {0,{0,Min,Sec}} = calendar:time_difference(calendar:now_to_local_time(N1),
+ calendar:now_to_local_time(N2)),
+ 1000 * (Min*60 + Sec + (M2-M1)/1000000).
+
+%%--------------------------------------------------------------------
openssh_zlib_basic_test() ->
[{doc, "Test basic connection with openssh_zlib"}].
@@ -633,6 +730,49 @@ openssh_zlib_basic_test(Config) ->
ssh:stop_daemon(Pid).
%%--------------------------------------------------------------------
+
+max_sessions(Config) ->
+ SystemDir = filename:join(?config(priv_dir, Config), system),
+ UserDir = ?config(priv_dir, Config),
+ MaxSessions = 2,
+ {Pid, Host, Port} = ssh_test_lib:daemon([{system_dir, SystemDir},
+ {user_dir, UserDir},
+ {user_passwords, [{"carni", "meat"}]},
+ {parallel_login, true},
+ {max_sessions, MaxSessions}
+ ]),
+
+ Connect = fun() ->
+ R=ssh_test_lib:connect(Host, Port, [{silently_accept_hosts, true},
+ {user_dir, UserDir},
+ {user_interaction, false},
+ {user, "carni"},
+ {password, "meat"}
+ ]),
+ ct:log("Connection ~p up",[R])
+ end,
+
+ try [Connect() || _ <- lists:seq(1,MaxSessions)]
+ of
+ _ ->
+ ct:pal("Expect Info Report:",[]),
+ try Connect()
+ of
+ _ConnectionRef ->
+ ssh:stop_daemon(Pid),
+ {fail,"Too many connections accepted"}
+ catch
+ error:{badmatch,{error,"Connection closed"}} ->
+ ssh:stop_daemon(Pid),
+ ok
+ end
+ catch
+ error:{badmatch,{error,"Connection closed"}} ->
+ ssh:stop_daemon(Pid),
+ {fail,"Too few connections accepted"}
+ end.
+
+%%--------------------------------------------------------------------
%% Internal functions ------------------------------------------------
%%--------------------------------------------------------------------
diff --git a/lib/ssh/test/ssh_test_lib.erl b/lib/ssh/test/ssh_test_lib.erl
index 6ed3dfa68c..00c25bf394 100644
--- a/lib/ssh/test/ssh_test_lib.erl
+++ b/lib/ssh/test/ssh_test_lib.erl
@@ -63,8 +63,13 @@ daemon(Host, Port, Options) ->
Error
end.
+
+
start_shell(Port, IOServer, UserDir) ->
- spawn_link(?MODULE, init_shell, [Port, IOServer, [{user_dir, UserDir}]]).
+ start_shell(Port, IOServer, UserDir, []).
+
+start_shell(Port, IOServer, UserDir, Options) ->
+ spawn_link(?MODULE, init_shell, [Port, IOServer, [{user_dir, UserDir}|Options]]).
start_shell(Port, IOServer) ->
spawn_link(?MODULE, init_shell, [Port, IOServer, []]).
@@ -91,18 +96,23 @@ loop_io_server(TestCase, Buff0) ->
{input, TestCase, Line} ->
loop_io_server(TestCase, Buff0 ++ [Line]);
{io_request, From, ReplyAs, Request} ->
+%%ct:pal("~p",[{io_request, From, ReplyAs, Request}]),
{ok, Reply, Buff} = io_request(Request, TestCase, From,
ReplyAs, Buff0),
+%%ct:pal("io_request(~p)-->~p",[Request,{ok, Reply, Buff}]),
io_reply(From, ReplyAs, Reply),
loop_io_server(TestCase, Buff);
{'EXIT',_, _} ->
- erlang:display('EXIT'),
+ erlang:display('ssh_test_lib:loop_io_server/2 EXIT'),
ok
end.
io_request({put_chars, Chars}, TestCase, _, _, Buff) ->
reply(TestCase, Chars),
{ok, ok, Buff};
+io_request({put_chars, unicode, Chars}, TestCase, _, _, Buff) when is_binary(Chars) ->
+ reply(TestCase, Chars),
+ {ok, ok, Buff};
io_request({put_chars, Enc, Chars}, TestCase, _, _, Buff) ->
reply(TestCase, unicode:characters_to_binary(Chars,Enc,latin1)),
{ok, ok, Buff};
@@ -120,11 +130,13 @@ io_request({get_line, _Enc,_}, _, _, _, [Line | Buff]) ->
io_reply(_, _, []) ->
ok;
io_reply(From, ReplyAs, Reply) ->
+%%ct:pal("io_reply ~p sending ~p ! ~p",[self(),From, {io_reply, ReplyAs, Reply}]),
From ! {io_reply, ReplyAs, Reply}.
reply(_, []) ->
ok;
reply(TestCase, Result) ->
+%%ct:pal("reply ~p sending ~p ! ~p",[self(), TestCase, Result]),
TestCase ! Result.
receive_exec_result(Msg) ->
diff --git a/lib/ssh/test/ssh_unicode_SUITE.erl b/lib/ssh/test/ssh_unicode_SUITE.erl
new file mode 100644
index 0000000000..cc916673b3
--- /dev/null
+++ b/lib/ssh/test/ssh_unicode_SUITE.erl
@@ -0,0 +1,587 @@
+%%
+%% %CopyrightBegin%
+%%
+%% Copyright Ericsson AB 2005-2014. All Rights Reserved.
+%%
+%% The contents of this file are subject to the Erlang Public License,
+%% Version 1.1, (the "License"); you may not use this file except in
+%% compliance with the License. You should have received a copy of the
+%% Erlang Public License along with this software. If not, it can be
+%% retrieved online at http://www.erlang.org/.
+%%
+%% Software distributed under the License is distributed on an "AS IS"
+%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
+%% the License for the specific language governing rights and limitations
+%% under the License.
+%%
+%% %CopyrightEnd%
+%%
+
+%% gerl +fnu
+%% ct:run_test([{suite,"ssh_unicode_SUITE"}, {logdir,"LOG"}]).
+
+-module(ssh_unicode_SUITE).
+
+%% Note: This directive should only be used in test suites.
+-compile(export_all).
+
+-include_lib("common_test/include/ct.hrl").
+-include_lib("kernel/include/file.hrl").
+
+% Default timetrap timeout
+-define(default_timeout, ?t:minutes(1)).
+
+-define(USER, "åke高兴").
+-define(PASSWD, "ärlig日本じん").
+-define('sftp.txt', "sftp瑞点.txt").
+-define('test.txt', "testハンス.txt").
+-define('link_test.txt', "link_test語.txt").
+
+-define(bindata, unicode:characters_to_binary("foobar å 一二三四いちにさんち") ).
+
+-define(NEWLINE, <<"\r\n">>).
+
+%%--------------------------------------------------------------------
+%% Common Test interface functions -----------------------------------
+%%--------------------------------------------------------------------
+
+%% suite() ->
+%% [{ct_hooks,[ts_install_cth]}].
+
+all() ->
+ [{group, sftp},
+ {group, shell}
+ ].
+
+
+init_per_suite(Config) ->
+ case {file:native_name_encoding(), (catch crypto:start())} of
+ {utf8, ok} ->
+ ssh:start(),
+ Config;
+ {utf8, _} ->
+ {skip,"Could not start crypto!"};
+ _ ->
+ {skip,"Not unicode filename enabled emulator"}
+ end.
+
+end_per_suite(Config) ->
+ ssh:stop(),
+ crypto:stop(),
+ Config.
+
+%%--------------------------------------------------------------------
+groups() ->
+ [{shell, [], [shell_no_unicode, shell_unicode_string]},
+ {sftp, [], [open_close_file, open_close_dir, read_file, read_dir,
+ write_file, rename_file, mk_rm_dir, remove_file, links,
+ retrieve_attributes, set_attributes, async_read, async_read_bin,
+ async_write
+ %% , position, pos_read, pos_write
+ ]}].
+
+init_per_group(Group, Config) when Group==sftp
+ ; Group==shell ->
+ PrivDir = ?config(priv_dir, Config),
+ SysDir = ?config(data_dir, Config),
+ Sftpd =
+ ssh_test_lib:daemon([{system_dir, SysDir},
+ {user_dir, PrivDir},
+ {user_passwords, [{?USER, ?PASSWD}]}]),
+ [{group,Group}, {sftpd, Sftpd} | Config];
+
+init_per_group(Group, Config) ->
+ [{group,Group} | Config].
+
+
+end_per_group(erlang_server, Config) ->
+ Config;
+end_per_group(_, Config) ->
+ Config.
+
+%%--------------------------------------------------------------------
+init_per_testcase(_Case, Config) ->
+ prep(Config),
+ TmpConfig0 = lists:keydelete(watchdog, 1, Config),
+ TmpConfig = lists:keydelete(sftp, 1, TmpConfig0),
+ Dog = ct:timetrap(?default_timeout),
+
+ case ?config(group, Config) of
+ sftp ->
+ {_Pid, Host, Port} = ?config(sftpd, Config),
+ {ok, ChannelPid, Connection} =
+ ssh_sftp:start_channel(Host, Port,
+ [{user, ?USER},
+ {password, ?PASSWD},
+ {user_interaction, false},
+ {silently_accept_hosts, true}]),
+ Sftp = {ChannelPid, Connection},
+ [{sftp, Sftp}, {watchdog, Dog} | TmpConfig];
+ shell ->
+ UserDir = ?config(priv_dir, Config),
+ process_flag(trap_exit, true),
+ {_Pid, _Host, Port} = ?config(sftpd, Config),
+ ct:sleep(500),
+ IO = ssh_test_lib:start_io_server(),
+ Shell = ssh_test_lib:start_shell(Port, IO, UserDir,
+ [{silently_accept_hosts, true},
+ {user,?USER},{password,?PASSWD}]),
+%%ct:pal("IO=~p, Shell=~p, self()=~p",[IO,Shell,self()]),
+ wait_for_erlang_first_line([{io,IO}, {shell,Shell} | Config])
+ end.
+
+
+wait_for_erlang_first_line(Config) ->
+ receive
+ {'EXIT', _, _} ->
+ {fail,no_ssh_connection};
+ <<"Eshell ",_/binary>> = ErlShellStart ->
+%% ct:pal("Erlang shell start: ~p~n", [ErlShellStart]),
+ Config;
+ Other ->
+ ct:pal("Unexpected answer from ssh server: ~p",[Other]),
+ {fail,unexpected_answer}
+ after 10000 ->
+ ct:pal("No answer from ssh-server"),
+ {fail,timeout}
+ end.
+
+
+
+end_per_testcase(rename_file, Config) ->
+ PrivDir = ?config(priv_dir, Config),
+ NewFileName = filename:join(PrivDir, ?'test.txt'),
+ file:delete(NewFileName),
+ end_per_testcase(Config);
+end_per_testcase(_TC, Config) ->
+ end_per_testcase(Config).
+
+end_per_testcase(Config) ->
+ catch exit(?config(shell,Config), kill),
+ case ?config(sftp, Config) of
+ {Sftp, Connection} ->
+ ssh_sftp:stop_channel(Sftp),
+ ssh:close(Connection);
+ _ ->
+ ok
+ end.
+
+%%--------------------------------------------------------------------
+%% Test Cases --------------------------------------------------------
+
+-define(chk_expected(Received,Expected),
+ (fun(R_,E_) when R_==E_ -> ok;
+ (R_,E_) -> ct:pal("Expected: ~p~nReceived: ~p~n", [E_,R_]),
+ E_ = R_
+ end)(Received,Expected)).
+
+-define(receive_chk(Ref,Expected),
+ (fun(E__) ->
+ receive
+ {async_reply, Ref, Received} when Received==E__ ->
+ ?chk_expected(Received, E__);
+ {async_reply, Ref, Received} when Received=/=E__ ->
+ ct:pal("Expected: ~p~nReceived: ~p~n", [E__,Received]),
+ E__ = Received;
+ Msg ->
+ ct:pal("Expected (Ref=~p): ~p", [Ref,E__]),
+ ct:fail(Msg)
+ end
+ end)(Expected)).
+
+%%--------------------------------------------------------------------
+
+
+open_close_file() ->
+ [{doc, "Test API functions open/3 and close/2"}].
+open_close_file(Config) when is_list(Config) ->
+ PrivDir = ?config(priv_dir, Config),
+ FileName = filename:join(PrivDir, ?'sftp.txt'),
+ {Sftp, _} = ?config(sftp, Config),
+
+ lists:foreach(
+ fun(Mode) ->
+ ct:log("Mode: ~p",[Mode]),
+ %% list_dir(PrivDir),
+ ok = open_close_file(Sftp, FileName, Mode)
+ end,
+ [
+ [read],
+ [write],
+ [write, creat],
+ [write, trunc],
+ [append],
+ [read, binary]
+ ]).
+
+open_close_file(Server, File, Mode) ->
+ {ok, Handle} = ssh_sftp:open(Server, File, Mode),
+ ok = ssh_sftp:close(Server, Handle).
+
+%%--------------------------------------------------------------------
+open_close_dir() ->
+ [{doc, "Test API functions opendir/2 and close/2"}].
+open_close_dir(Config) when is_list(Config) ->
+ PrivDir = ?config(priv_dir, Config),
+ {Sftp, _} = ?config(sftp, Config),
+ FileName = filename:join(PrivDir, ?'sftp.txt'),
+
+ {ok, Handle} = ssh_sftp:opendir(Sftp, PrivDir),
+ ok = ssh_sftp:close(Sftp, Handle),
+ {error, _} = ssh_sftp:opendir(Sftp, FileName).
+
+%%--------------------------------------------------------------------
+read_file() ->
+ [{doc, "Test API funtion read_file/2"}].
+read_file(Config) when is_list(Config) ->
+ PrivDir = ?config(priv_dir, Config),
+ FileName = filename:join(PrivDir, ?'sftp.txt'),
+ {Sftp, _} = ?config(sftp, Config),
+ ?chk_expected(ssh_sftp:read_file(Sftp,FileName), file:read_file(FileName)).
+
+%%--------------------------------------------------------------------
+read_dir() ->
+ [{doc,"Test API function list_dir/2"}].
+read_dir(Config) when is_list(Config) ->
+ PrivDir = ?config(priv_dir, Config),
+ {Sftp, _} = ?config(sftp, Config),
+ {ok, Files} = ssh_sftp:list_dir(Sftp, PrivDir),
+ ct:pal("sftp list dir: ~ts~n", [Files]).
+
+%%--------------------------------------------------------------------
+write_file() ->
+ [{doc, "Test API function write_file/2"}].
+write_file(Config) when is_list(Config) ->
+ PrivDir = ?config(priv_dir, Config),
+ FileName = filename:join(PrivDir, ?'sftp.txt'),
+ {Sftp, _} = ?config(sftp, Config),
+ ok = ssh_sftp:write_file(Sftp, FileName, [?bindata]),
+ ?chk_expected(file:read_file(FileName), {ok,?bindata}).
+
+%%--------------------------------------------------------------------
+remove_file() ->
+ [{doc,"Test API function delete/2"}].
+remove_file(Config) when is_list(Config) ->
+ PrivDir = ?config(priv_dir, Config),
+ FileName = filename:join(PrivDir, ?'sftp.txt'),
+ {Sftp, _} = ?config(sftp, Config),
+
+ {ok, Files} = ssh_sftp:list_dir(Sftp, PrivDir),
+ true = lists:member(filename:basename(FileName), Files),
+ ok = ssh_sftp:delete(Sftp, FileName),
+ {ok, NewFiles} = ssh_sftp:list_dir(Sftp, PrivDir),
+ false = lists:member(filename:basename(FileName), NewFiles),
+ {error, _} = ssh_sftp:delete(Sftp, FileName).
+%%--------------------------------------------------------------------
+rename_file() ->
+ [{doc, "Test API function rename_file/2"}].
+rename_file(Config) when is_list(Config) ->
+ PrivDir = ?config(priv_dir, Config),
+ FileName = filename:join(PrivDir, ?'sftp.txt'),
+ NewFileName = filename:join(PrivDir, ?'test.txt'),
+
+ {Sftp, _} = ?config(sftp, Config),
+ {ok, Files} = ssh_sftp:list_dir(Sftp, PrivDir),
+ ct:pal("FileName: ~ts~nFiles: ~ts~n", [FileName, [[$\n,$ ,F]||F<-Files] ]),
+ true = lists:member(filename:basename(FileName), Files),
+ false = lists:member(filename:basename(NewFileName), Files),
+ ok = ssh_sftp:rename(Sftp, FileName, NewFileName),
+ {ok, NewFiles} = ssh_sftp:list_dir(Sftp, PrivDir),
+ ct:pal("FileName: ~ts, Files: ~ts~n", [FileName, [[$\n,F]||F<-NewFiles] ]),
+
+ false = lists:member(filename:basename(FileName), NewFiles),
+ true = lists:member(filename:basename(NewFileName), NewFiles).
+
+%%--------------------------------------------------------------------
+mk_rm_dir() ->
+ [{doc,"Test API functions make_dir/2, del_dir/2"}].
+mk_rm_dir(Config) when is_list(Config) ->
+ PrivDir = ?config(priv_dir, Config),
+ {Sftp, _} = ?config(sftp, Config),
+
+ DirName = filename:join(PrivDir, "test"),
+ ok = ssh_sftp:make_dir(Sftp, DirName),
+ ok = ssh_sftp:del_dir(Sftp, DirName),
+ NewDirName = filename:join(PrivDir, "foo/bar"),
+ {error, _} = ssh_sftp:make_dir(Sftp, NewDirName),
+ {error, _} = ssh_sftp:del_dir(Sftp, PrivDir).
+
+%%--------------------------------------------------------------------
+links() ->
+ [{doc,"Tests API function make_symlink/3"}].
+links(Config) when is_list(Config) ->
+ case os:type() of
+ {win32, _} ->
+ {skip, "Links are not fully supported by windows"};
+ _ ->
+ {Sftp, _} = ?config(sftp, Config),
+ PrivDir = ?config(priv_dir, Config),
+ FileName = filename:join(PrivDir, ?'sftp.txt'),
+ LinkFileName = filename:join(PrivDir, ?'link_test.txt'),
+
+ ok = ssh_sftp:make_symlink(Sftp, LinkFileName, FileName),
+ {ok, FileName} = ssh_sftp:read_link(Sftp, LinkFileName)
+ end.
+
+%%--------------------------------------------------------------------
+retrieve_attributes() ->
+ [{doc, "Test API function read_file_info/3"}].
+retrieve_attributes(Config) when is_list(Config) ->
+ PrivDir = ?config(priv_dir, Config),
+ FileName = filename:join(PrivDir, ?'sftp.txt'),
+
+ {Sftp, _} = ?config(sftp, Config),
+ {ok, FileInfo} = ssh_sftp:read_file_info(Sftp, FileName),
+ {ok, NewFileInfo} = file:read_file_info(FileName),
+
+ %% TODO comparison. There are some differences now is that ok?
+ ct:pal("SFTP: ~p~nFILE: ~p~n", [FileInfo, NewFileInfo]).
+
+%%--------------------------------------------------------------------
+set_attributes() ->
+ [{doc,"Test API function write_file_info/3"}].
+set_attributes(Config) when is_list(Config) ->
+ PrivDir = ?config(priv_dir, Config),
+ FileName = filename:join(PrivDir, ?'test.txt'),
+
+ {Sftp, _} = ?config(sftp, Config),
+ {ok,Fd} = file:open(FileName, write),
+ io:put_chars(Fd,"foo"),
+ ok = ssh_sftp:write_file_info(Sftp, FileName, #file_info{mode=8#400}),
+ {error, eacces} = file:write_file(FileName, "hello again"),
+ ssh_sftp:write_file_info(Sftp, FileName, #file_info{mode=8#600}),
+ ok = file:write_file(FileName, "hello again").
+
+%%--------------------------------------------------------------------
+
+async_read() ->
+ [{doc,"Test API aread/3"}].
+async_read(Config) when is_list(Config) ->
+ do_async_read(Config, false).
+
+async_read_bin() ->
+ [{doc,"Test API aread/3"}].
+async_read_bin(Config) when is_list(Config) ->
+ do_async_read(Config, true).
+
+do_async_read(Config, BinaryFlag) ->
+ {Sftp, _} = ?config(sftp, Config),
+ PrivDir = ?config(priv_dir, Config),
+ FileName = filename:join(PrivDir, ?'sftp.txt'),
+ {ok,ExpDataBin} = file:read_file(FileName),
+ ExpData = case BinaryFlag of
+ true -> ExpDataBin;
+ false -> binary_to_list(ExpDataBin)
+ end,
+ {ok, Handle} = ssh_sftp:open(Sftp, FileName, [read|case BinaryFlag of
+ true -> [binary];
+ false -> []
+ end]),
+ {async, Ref} = ssh_sftp:aread(Sftp, Handle, 20),
+ ?receive_chk(Ref, {ok,ExpData}).
+
+%%--------------------------------------------------------------------
+async_write() ->
+ [{doc,"Test API awrite/3"}].
+async_write(Config) when is_list(Config) ->
+ {Sftp, _} = ?config(sftp, Config),
+ PrivDir = ?config(priv_dir, Config),
+ FileName = filename:join(PrivDir, ?'test.txt'),
+ {ok, Handle} = ssh_sftp:open(Sftp, FileName, [write]),
+ Expected = ?bindata,
+ {async, Ref} = ssh_sftp:awrite(Sftp, Handle, Expected),
+
+ receive
+ {async_reply, Ref, ok} ->
+ {ok, Data} = file:read_file(FileName),
+ ?chk_expected(Data, Expected);
+ Msg ->
+ ct:fail(Msg)
+ end.
+
+%%--------------------------------------------------------------------
+
+position() ->
+ [{doc, "Test API functions position/3"}].
+position(Config) when is_list(Config) ->
+ PrivDir = ?config(priv_dir, Config),
+ FileName = filename:join(PrivDir, ?'test.txt'),
+ {Sftp, _} = ?config(sftp, Config),
+
+ Data = list_to_binary("1234567890"),
+ ssh_sftp:write_file(Sftp, FileName, [Data]),
+ {ok, Handle} = ssh_sftp:open(Sftp, FileName, [read]),
+
+ {ok, 3} = ssh_sftp:position(Sftp, Handle, {bof, 3}),
+ {ok, "4"} = ssh_sftp:read(Sftp, Handle, 1),
+
+ {ok, 10} = ssh_sftp:position(Sftp, Handle, eof),
+ eof = ssh_sftp:read(Sftp, Handle, 1),
+
+ {ok, 6} = ssh_sftp:position(Sftp, Handle, {bof, 6}),
+ {ok, "7"} = ssh_sftp:read(Sftp, Handle, 1),
+
+ {ok, 9} = ssh_sftp:position(Sftp, Handle, {cur, 2}),
+ {ok, "0"} = ssh_sftp:read(Sftp, Handle, 1),
+
+ {ok, 0} = ssh_sftp:position(Sftp, Handle, bof),
+ {ok, "1"} = ssh_sftp:read(Sftp, Handle, 1),
+
+ {ok, 1} = ssh_sftp:position(Sftp, Handle, cur),
+ {ok, "2"} = ssh_sftp:read(Sftp, Handle, 1).
+
+%%--------------------------------------------------------------------
+pos_read() ->
+ [{doc,"Test API functions pread/3 and apread/3"}].
+pos_read(Config) when is_list(Config) ->
+ PrivDir = ?config(priv_dir, Config),
+ FileName = filename:join(PrivDir, ?'test.txt'),
+ {Sftp, _} = ?config(sftp, Config),
+ Data = ?bindata,
+ ssh_sftp:write_file(Sftp, FileName, [Data]),
+
+ {ok, Handle} = ssh_sftp:open(Sftp, FileName, [read]),
+ {async, Ref} = ssh_sftp:apread(Sftp, Handle, {bof,5}, 4),
+
+ ?receive_chk(Ref, {ok,binary_part(Data,5,4)}),
+ ?chk_expected(ssh_sftp:pread(Sftp,Handle,{bof,4},4), {ok,binary_part(Data,4,4)}).
+
+
+%%--------------------------------------------------------------------
+pos_write() ->
+ [{doc,"Test API functions pwrite/4 and apwrite/4"}].
+pos_write(Config) when is_list(Config) ->
+ PrivDir = ?config(priv_dir, Config),
+ FileName = filename:join(PrivDir, ?'test.txt'),
+ {Sftp, _} = ?config(sftp, Config),
+
+ {ok, Handle} = ssh_sftp:open(Sftp, FileName, [write]),
+
+ Data = unicode:characters_to_list("再见"),
+ ssh_sftp:write_file(Sftp, FileName, [Data]),
+
+ NewData = unicode:characters_to_list(" さようなら"),
+ {async, Ref} = ssh_sftp:apwrite(Sftp, Handle, {bof, 2}, NewData),
+ ?receive_chk(Ref, ok),
+
+ ok = ssh_sftp:pwrite(Sftp, Handle, eof, unicode:characters_to_list(" adjö ")),
+
+ ?chk_expected(ssh_sftp:read_file(Sftp,FileName),
+ {ok,unicode:characters_to_binary("再见 さようなら adjö ")}).
+
+%%--------------------------------------------------------------------
+sftp_nonexistent_subsystem() ->
+ [{doc, "Try to execute sftp subsystem on a server that does not support it"}].
+sftp_nonexistent_subsystem(Config) when is_list(Config) ->
+ {_,Host, Port} = ?config(sftpd, Config),
+ {error,"server failed to start sftp subsystem"} =
+ ssh_sftp:start_channel(Host, Port,
+ [{user_interaction, false},
+ {user, ?USER},
+ {password, ?PASSWD},
+ {silently_accept_hosts, true}]).
+
+%%--------------------------------------------------------------------
+shell_no_unicode(Config) ->
+ do_shell(?config(io,Config),
+ [new_prompt,
+ {type,"io:format(\"hej ~p~n\",[42])."},
+ {expect,"hej 42"}
+ ]).
+
+%%--------------------------------------------------------------------
+shell_unicode_string(Config) ->
+ do_shell(?config(io,Config),
+ [new_prompt,
+ {type,"io:format(\"こにちわ~ts~n\",[\"四二\"])."},
+ {expect,"こにちわ四二"},
+ {expect,"ok"}
+ ]).
+
+%%--------------------------------------------------------------------
+%% Internal functions ------------------------------------------------
+%%--------------------------------------------------------------------
+prep(Config) ->
+ PrivDir = ?config(priv_dir, Config),
+ TestFile = filename:join(PrivDir, ?'sftp.txt'),
+ TestFile1 = filename:join(PrivDir, ?'test.txt'),
+ TestLink = filename:join(PrivDir, ?'link_test.txt'),
+
+ file:delete(TestFile),
+ file:delete(TestFile1),
+ file:delete(TestLink),
+
+ %% Initial config
+ DataDir = ?config(data_dir, Config),
+ FileName = filename:join(DataDir, ?'sftp.txt'),
+ {ok,_BytesCopied} = file:copy(FileName, TestFile),
+ Mode = 8#00400 bor 8#00200 bor 8#00040, % read & write owner, read group
+ {ok, FileInfo} = file:read_file_info(TestFile),
+ ok = file:write_file_info(TestFile,
+ FileInfo#file_info{mode = Mode}).
+
+
+%% list_dir(Dir) ->
+%% ct:pal("prep/1: ls(~p):~n~p~n~ts",[Dir, file:list_dir(Dir),
+%% begin
+%% {ok,DL} = file:list_dir(Dir),
+%% [[$\n|FN] || FN <- DL]
+%% end]).
+
+
+%%--------------------------------------------------------------------
+do_shell(IO, List) -> do_shell(IO, 0, List).
+
+do_shell(IO, N, [new_prompt|More]) ->
+ do_shell(IO, N+1, More);
+
+do_shell(IO, N, Ops=[{Order,Arg}|More]) ->
+ receive
+ X = <<"\r\n">> ->
+%% ct:pal("Skip newline ~p",[X]),
+ do_shell(IO, N, Ops);
+
+ <<P1,"> ">> when (P1-$0)==N ->
+ do_shell_prompt(IO, N, Order, Arg, More);
+
+ <<P1,P2,"> ">> when (P1-$0)*10 + (P2-$0) == N ->
+ do_shell_prompt(IO, N, Order, Arg, More);
+
+ Err when element(1,Err)==error ->
+ ct:fail("do_shell error: ~p~n",[Err]);
+
+ RecBin when Order==expect ; Order==expect_echo ->
+%% ct:pal("received ~p",[RecBin]),
+ RecStr = string:strip(unicode:characters_to_list(RecBin)),
+ ExpStr = string:strip(Arg),
+ case lists:prefix(ExpStr, RecStr) of
+ true when Order==expect ->
+ ct:pal("Matched ~ts",[RecStr]),
+ do_shell(IO, N, More);
+ true when Order==expect_echo ->
+ ct:pal("Matched echo ~ts",[RecStr]),
+ do_shell(IO, N, More);
+ false ->
+ ct:fail("*** Expected ~p, but got ~p",[string:strip(ExpStr),RecStr])
+ end
+ after 10000 ->
+ case Order of
+ expect -> ct:fail("timeout, expected ~p",[string:strip(Arg)]);
+ type -> ct:fail("timeout, no prompt")
+ end
+ end;
+
+do_shell(_, _, []) ->
+ ok.
+
+
+do_shell_prompt(IO, N, type, Str, More) ->
+%% ct:pal("Matched prompt ~p to trigger sending of next line to server",[N]),
+ IO ! {input, self(), Str++"\r\n"},
+ ct:pal("Promt '~p> ', Sent ~ts",[N,Str++"\r\n"]),
+ do_shell(IO, N, [{expect_echo,Str}|More]); % expect echo of the sent line
+do_shell_prompt(IO, N, Op, Str, More) ->
+%% ct:pal("Matched prompt ~p",[N]),
+ do_shell(IO, N, [{Op,Str}|More]).
+
+%%--------------------------------------------------------------------
diff --git a/lib/ssh/test/ssh_unicode_SUITE_data/sftp.txt b/lib/ssh/test/ssh_unicode_SUITE_data/sftp.txt
new file mode 100644
index 0000000000..3eaaddca21
--- /dev/null
+++ b/lib/ssh/test/ssh_unicode_SUITE_data/sftp.txt
@@ -0,0 +1 @@
+åäöÅÄÖ瑞語
diff --git a/lib/ssh/test/ssh_unicode_SUITE_data/sftp瑞点.txt b/lib/ssh/test/ssh_unicode_SUITE_data/sftp瑞点.txt
new file mode 100644
index 0000000000..3eaaddca21
--- /dev/null
+++ b/lib/ssh/test/ssh_unicode_SUITE_data/sftp瑞点.txt
@@ -0,0 +1 @@
+åäöÅÄÖ瑞語
diff --git a/lib/ssh/test/ssh_unicode_SUITE_data/ssh_host_dsa_key b/lib/ssh/test/ssh_unicode_SUITE_data/ssh_host_dsa_key
new file mode 100644
index 0000000000..51ab6fbd88
--- /dev/null
+++ b/lib/ssh/test/ssh_unicode_SUITE_data/ssh_host_dsa_key
@@ -0,0 +1,13 @@
+-----BEGIN DSA PRIVATE KEY-----
+MIIBuwIBAAKBgQCClaHzE2ul0gKSUxah5W0W8UiJLy4hXngKEqpaUq9SSdVdY2LK
+wVfKH1gt5iuaf1FfzOhsIC9G/GLnjYttXZc92cv/Gfe3gR+s0ni2++MX+T++mE/Q
+diltXv/Hp27PybS67SmiFW7I+RWnT2OKlMPtw2oUuKeztCe5UWjaj/y5FQIVAPLA
+l9RpiU30Z87NRAHY3NTRaqtrAoGANMRxw8UfdtNVR0CrQj3AgPaXOGE4d+G4Gp4X
+skvnCHycSVAjtYxebUkzUzt5Q6f/IabuLUdge3gXrc8BetvrcKbp+XZgM0/Vj2CF
+Ymmy3in6kzGZq7Fw1sZaku6AOU8vLa5woBT2vAcHLLT1bLAzj7viL048T6MfjrOP
+ef8nHvACgYBhDWFQJ1mf99sg92LalVq1dHLmVXb3PTJDfCO/Gz5NFmj9EZbAtdah
+/XcF3DeRF+eEoz48wQF/ExVxSMIhLdL+o+ElpVhlM7Yii+T7dPhkQfEul6zZXu+U
+ykSTXYUbtsfTNRFQGBW2/GfnEc0mnIxfn9v10NEWMzlq5z9wT9P0CgIVAN4wtL5W
+Lv62jKcdskxNyz2NQoBx
+-----END DSA PRIVATE KEY-----
+
diff --git a/lib/ssh/test/ssh_unicode_SUITE_data/ssh_host_dsa_key.pub b/lib/ssh/test/ssh_unicode_SUITE_data/ssh_host_dsa_key.pub
new file mode 100644
index 0000000000..4dbb1305b0
--- /dev/null
+++ b/lib/ssh/test/ssh_unicode_SUITE_data/ssh_host_dsa_key.pub
@@ -0,0 +1,11 @@
+---- BEGIN SSH2 PUBLIC KEY ----
+AAAAB3NzaC1kc3MAAACBAIKVofMTa6XSApJTFqHlbRbxSIkvLiFeeAoSqlpSr1JJ1V1j
+YsrBV8ofWC3mK5p/UV/M6GwgL0b8YueNi21dlz3Zy/8Z97eBH6zSeLb74xf5P76YT9B2
+KW1e/8enbs/JtLrtKaIVbsj5FadPY4qUw+3DahS4p7O0J7lRaNqP/LkVAAAAFQDywJfU
+aYlN9GfOzUQB2NzU0WqrawAAAIA0xHHDxR9201VHQKtCPcCA9pc4YTh34bganheyS+cI
+fJxJUCO1jF5tSTNTO3lDp/8hpu4tR2B7eBetzwF62+twpun5dmAzT9WPYIViabLeKfqT
+MZmrsXDWxlqS7oA5Ty8trnCgFPa8BwcstPVssDOPu+IvTjxPox+Os495/yce8AAAAIBh
+DWFQJ1mf99sg92LalVq1dHLmVXb3PTJDfCO/Gz5NFmj9EZbAtdah/XcF3DeRF+eEoz48
+wQF/ExVxSMIhLdL+o+ElpVhlM7Yii+T7dPhkQfEul6zZXu+UykSTXYUbtsfTNRFQGBW2
+/GfnEc0mnIxfn9v10NEWMzlq5z9wT9P0Cg==
+---- END SSH2 PUBLIC KEY ----
diff --git a/lib/ssh/vsn.mk b/lib/ssh/vsn.mk
index 8186f39888..40ed27d8f5 100644
--- a/lib/ssh/vsn.mk
+++ b/lib/ssh/vsn.mk
@@ -1,5 +1,5 @@
#-*-makefile-*- ; force emacs to enter makefile-mode
-SSH_VSN = 3.0
+SSH_VSN = 3.0.2
APP_VSN = "ssh-$(SSH_VSN)"