aboutsummaryrefslogtreecommitdiffstats
path: root/lib/ssh
diff options
context:
space:
mode:
Diffstat (limited to 'lib/ssh')
-rw-r--r--lib/ssh/doc/src/notes.xml172
-rw-r--r--lib/ssh/doc/src/ssh.xml37
-rw-r--r--lib/ssh/doc/src/using_ssh.xml4
-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_cli.erl17
-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_file.erl26
-rw-r--r--lib/ssh/src/ssh_io.erl2
-rw-r--r--lib/ssh/src/ssh_message.erl34
-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
27 files changed, 1185 insertions, 126 deletions
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/ssh.xml b/lib/ssh/doc/src/ssh.xml
index 679ef9bc19..5a141ced3c 100644
--- a/lib/ssh/doc/src/ssh.xml
+++ b/lib/ssh/doc/src/ssh.xml
@@ -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/using_ssh.xml b/lib/ssh/doc/src/using_ssh.xml
index 4d73366f5e..9ab71260d3 100644
--- a/lib/ssh/doc/src/using_ssh.xml
+++ b/lib/ssh/doc/src/using_ssh.xml
@@ -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_cli.erl b/lib/ssh/src/ssh_cli.erl
index 2c8e515a14..77453e8fd7 100644
--- a/lib/ssh/src/ssh_cli.erl
+++ b/lib/ssh/src/ssh_cli.erl
@@ -170,10 +170,19 @@ handle_msg({Group, get_unicode_state}, State) ->
{ok, State};
handle_msg({Group, tty_geometry}, #state{group = Group,
- pty = #ssh_pty{width=Width,
- height=Height}
+ pty = Pty
} = State) ->
- Group ! {self(),tty_geometry,{Width,Height}},
+ case Pty of
+ #ssh_pty{width=Width,height=Height} ->
+ Group ! {self(),tty_geometry,{Width,Height}};
+ _ ->
+ %% This is a dirty fix of the problem with the otp ssh:shell
+ %% client. That client will not allocate a tty, but someone
+ %% asks for the tty_geometry just before every erlang prompt.
+ %% If that question is not answered, there is a 2 sec timeout
+ %% Until the prompt is seen by the user at the client side ...
+ Group ! {self(),tty_geometry,{0,0}}
+ end,
{ok,State};
handle_msg({Group, Req}, #state{group = Group, buf = Buf, pty = Pty,
@@ -349,7 +358,7 @@ delete_chars(N, {Buf, BufTail, Col}, Tty) when N > 0 ->
{Buf, NewBufTail, Col}};
delete_chars(N, {Buf, BufTail, Col}, Tty) -> % N < 0
NewBuf = nthtail(-N, Buf),
- NewCol = Col + N,
+ NewCol = case Col + N of V when V >= 0 -> V; _ -> 0 end,
M1 = move_cursor(Col, NewCol, Tty),
M2 = move_cursor(NewCol + length(BufTail) - N, NewCol, Tty),
{[M1, BufTail, lists:duplicate(-N, $ ) | M2],
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_file.erl b/lib/ssh/src/ssh_file.erl
index 21cdedc156..5692138a8a 100644
--- a/lib/ssh/src/ssh_file.erl
+++ b/lib/ssh/src/ssh_file.erl
@@ -65,7 +65,7 @@ is_auth_key(Key, User,Opts) ->
%% Used by client
is_host_key(Key, PeerName, Algorithm, Opts) ->
- case lookup_host_key(PeerName, Algorithm, Opts) of
+ case lookup_host_key(Key, PeerName, Algorithm, Opts) of
{ok, Key} ->
true;
_ ->
@@ -121,9 +121,9 @@ decode_ssh_file(Pem, Password) ->
%% return {ok, Key(s)} or {error, not_found}
%%
-lookup_host_key(Host, Alg, Opts) ->
+lookup_host_key(KeyToMatch, Host, Alg, Opts) ->
Host1 = replace_localhost(Host),
- do_lookup_host_key(Host1, Alg, Opts).
+ do_lookup_host_key(KeyToMatch, Host1, Alg, Opts).
add_host_key(Host, Key, Opts) ->
@@ -204,10 +204,10 @@ replace_localhost("localhost") ->
replace_localhost(Host) ->
Host.
-do_lookup_host_key(Host, Alg, Opts) ->
+do_lookup_host_key(KeyToMatch, Host, Alg, Opts) ->
case file:open(file_name(user, "known_hosts", Opts), [read, binary]) of
{ok, Fd} ->
- Res = lookup_host_key_fd(Fd, Host, Alg),
+ Res = lookup_host_key_fd(Fd, KeyToMatch, Host, Alg),
file:close(Fd),
{ok, Res};
{error, enoent} -> {error, not_found};
@@ -228,16 +228,16 @@ identity_pass_phrase('ssh-rsa') ->
identity_pass_phrase("ssh-rsa") ->
rsa_pass_phrase.
-lookup_host_key_fd(Fd, Host, KeyType) ->
+lookup_host_key_fd(Fd, KeyToMatch, Host, KeyType) ->
case io:get_line(Fd, '') of
eof ->
{error, not_found};
Line ->
case ssh_decode_line(Line, known_hosts) of
[{Key, Attributes}] ->
- handle_host(Fd, Host, proplists:get_value(hostnames, Attributes), Key, KeyType);
+ handle_host(Fd, KeyToMatch, Host, proplists:get_value(hostnames, Attributes), Key, KeyType);
[] ->
- lookup_host_key_fd(Fd, Host, KeyType)
+ lookup_host_key_fd(Fd, KeyToMatch, Host, KeyType)
end
end.
@@ -248,13 +248,13 @@ ssh_decode_line(Line, Type) ->
[]
end.
-handle_host(Fd, Host, HostList, Key, KeyType) ->
+handle_host(Fd, KeyToMatch, Host, HostList, Key, KeyType) ->
Host1 = host_name(Host),
- case lists:member(Host1, HostList) and key_match(Key, KeyType) of
- true ->
+ case lists:member(Host1, HostList) andalso key_match(Key, KeyType) of
+ true when KeyToMatch == Key ->
Key;
- false ->
- lookup_host_key_fd(Fd, Host, KeyType)
+ _ ->
+ lookup_host_key_fd(Fd, KeyToMatch, Host, KeyType)
end.
host_name(Atom) when is_atom(Atom) ->
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 7bd0375521..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,
@@ -315,8 +315,8 @@ decode(<<?BYTE(?SSH_MSG_CHANNEL_DATA), ?UINT32(Recipient), ?UINT32(Len), Data:Le
recipient_channel = Recipient,
data = Data
};
-decode(<<?BYTE(?SSH_MSG_CHANNEL_EXTENDED_DATA), ?UINT32(Recipient),
- ?UINT32(DataType), Data/binary>>) ->
+decode(<<?BYTE(?SSH_MSG_CHANNEL_EXTENDED_DATA), ?UINT32(Recipient),
+ ?UINT32(DataType), ?UINT32(Len), Data:Len/binary>>) ->
#ssh_msg_channel_extended_data{
recipient_channel = Recipient,
data_type_code = DataType,
@@ -380,27 +380,30 @@ decode(<<?BYTE(?SSH_MSG_USERAUTH_BANNER),
language = Lang
};
+decode(<<?BYTE(?SSH_MSG_USERAUTH_INFO_REQUEST), ?UINT32(Len0), Name:Len0/binary,
+ ?UINT32(Len1), Inst:Len1/binary, ?UINT32(Len2), Lang:Len2/binary,
+ ?UINT32(NumPromtps), Data/binary>>) ->
+ #ssh_msg_userauth_info_request{
+ name = Name,
+ instruction = Inst,
+ language_tag = Lang,
+ num_prompts = NumPromtps,
+ data = Data};
+
+%%% Unhandled message, also masked by same 1:st byte value as ?SSH_MSG_USERAUTH_INFO_REQUEST:
decode(<<?BYTE(?SSH_MSG_USERAUTH_PK_OK), ?UINT32(Len), Alg:Len/binary, KeyBlob/binary>>) ->
#ssh_msg_userauth_pk_ok{
algorithm_name = Alg,
key_blob = KeyBlob
};
+%%% Unhandled message, also masked by same 1:st byte value as ?SSH_MSG_USERAUTH_INFO_REQUEST:
decode(<<?BYTE(?SSH_MSG_USERAUTH_PASSWD_CHANGEREQ), ?UINT32(Len0), Prompt:Len0/binary,
?UINT32(Len1), Lang:Len1/binary>>) ->
#ssh_msg_userauth_passwd_changereq{
prompt = Prompt,
languge = Lang
};
-decode(<<?BYTE(?SSH_MSG_USERAUTH_INFO_REQUEST), ?UINT32(Len0), Name:Len0/binary,
- ?UINT32(Len1), Inst:Len1/binary, ?UINT32(Len2), Lang:Len2/binary,
- ?UINT32(NumPromtps), Data/binary>>) ->
- #ssh_msg_userauth_info_request{
- name = Name,
- instruction = Inst,
- language_tag = Lang,
- num_prompts = NumPromtps,
- data = Data};
decode(<<?BYTE(?SSH_MSG_USERAUTH_INFO_RESPONSE), ?UINT32(Num), Data/binary>>) ->
#ssh_msg_userauth_info_response{
@@ -424,8 +427,9 @@ decode(<<?BYTE(?SSH_MSG_KEX_DH_GEX_REQUEST_OLD), ?UINT32(N)>>) ->
#ssh_msg_kex_dh_gex_request_old{
n = N
};
-decode(<<?BYTE(?SSH_MSG_KEX_DH_GEX_GROUP), ?UINT32(Len0), Prime:Len0/big-signed-integer,
- ?UINT32(Len1), Generator:Len1/big-signed-integer>>) ->
+decode(<<?BYTE(?SSH_MSG_KEX_DH_GEX_GROUP),
+ ?UINT32(Len0), Prime:Len0/big-signed-integer-unit:8,
+ ?UINT32(Len1), Generator:Len1/big-signed-integer-unit:8>>) ->
#ssh_msg_kex_dh_gex_group{
p = Prime,
g = Generator
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)"