From 84051a76ee4c07f7453ba2bf24fe32c8cf8c7b48 Mon Sep 17 00:00:00 2001 From: Hans Nilsson Date: Thu, 26 May 2016 15:33:18 +0200 Subject: ssh: ssh:connect, ssh:shell and ssh_sftp:start_subsystem supports client tcp-socket as input --- lib/ssh/doc/src/ssh.xml | 8 +++- lib/ssh/doc/src/ssh_sftp.xml | 14 ++++-- lib/ssh/src/ssh.erl | 85 +++++++++++++++++++++++++++-------- lib/ssh/src/ssh_sftp.erl | 23 ++++++++++ lib/ssh/test/ssh_connection_SUITE.erl | 64 ++++++++++++++++++++++++++ lib/ssh/test/ssh_sftp_SUITE.erl | 55 ++++++++++++++++++++++- 6 files changed, 224 insertions(+), 25 deletions(-) diff --git a/lib/ssh/doc/src/ssh.xml b/lib/ssh/doc/src/ssh.xml index ff2d6e082a..bd330e479f 100644 --- a/lib/ssh/doc/src/ssh.xml +++ b/lib/ssh/doc/src/ssh.xml @@ -124,9 +124,11 @@ + connect(TcpSocket, Options) -> + connect(TcpSocket, Options, Timeout) -> connect(Host, Port, Options) -> - connect(Host, Port, Options, Timeout) -> {ok, - ssh_connection_ref()} | {error, Reason} + connect(Host, Port, Options, Timeout) -> + {ok, ssh_connection_ref()} | {error, Reason} Connects to an SSH server. Host = string() @@ -137,6 +139,8 @@ Timeout = infinity | integer() Negotiation time-out in milli-seconds. The default value is infinity. For connection time-out, use option {connect_timeout, timeout()}. + TcpSocket = port() + The socket is supposed to be from gen_tcp:connect with option {active,false}

Connects to an SSH server. No channel is started. This is done diff --git a/lib/ssh/doc/src/ssh_sftp.xml b/lib/ssh/doc/src/ssh_sftp.xml index 071d46ec57..67531b7d99 100644 --- a/lib/ssh/doc/src/ssh_sftp.xml +++ b/lib/ssh/doc/src/ssh_sftp.xml @@ -526,16 +526,24 @@ + start_channel(TcpSocket) -> + start_channel(TcpSocket, Options) -> + {ok, Pid, ConnectionRef} | {error, reason()|term()} + start_channel(ConnectionRef) -> - start_channel(ConnectionRef, Options) -> + start_channel(ConnectionRef, Options) -> + {ok, Pid} | {error, reason()|term()} + start_channel(Host, Options) -> - start_channel(Host, Port, Options) -> {ok, Pid} | {ok, Pid, ConnectionRef} | - {error, reason()|term()} + start_channel(Host, Port, Options) -> + {ok, Pid, ConnectionRef} | {error, reason()|term()} Starts an SFTP client. Host = string() ConnectionRef = ssh_connection_ref() Port = integer() + TcpSocket = port() + The socket is supposed to be from gen_tcp:connect with option {active,false} Options = [{Option, Value}] diff --git a/lib/ssh/src/ssh.erl b/lib/ssh/src/ssh.erl index 09b07b7a2a..50dfe55798 100644 --- a/lib/ssh/src/ssh.erl +++ b/lib/ssh/src/ssh.erl @@ -27,7 +27,9 @@ -include_lib("public_key/include/public_key.hrl"). -include_lib("kernel/include/file.hrl"). --export([start/0, start/1, stop/0, connect/3, connect/4, close/1, connection_info/2, +-export([start/0, start/1, stop/0, + connect/2, connect/3, connect/4, + close/1, connection_info/2, channel_info/3, daemon/1, daemon/2, daemon/3, daemon_info/1, @@ -70,13 +72,46 @@ stop() -> application:stop(ssh). %%-------------------------------------------------------------------- --spec connect(string(), integer(), proplists:proplist()) -> {ok, pid()} | {error, term()}. +-spec connect(port(), proplists:proplist()) -> {ok, pid()} | {error, term()}. + +-spec connect(port(), proplists:proplist(), timeout()) -> {ok, pid()} | {error, term()} + ; (string(), integer(), proplists:proplist()) -> {ok, pid()} | {error, term()}. + -spec connect(string(), integer(), proplists:proplist(), timeout()) -> {ok, pid()} | {error, term()}. %% %% Description: Starts an ssh connection. %%-------------------------------------------------------------------- -connect(Host, Port, Options) -> +connect(Socket, Options) -> + connect(Socket, Options, infinity). + +connect(Socket, Options, Timeout) when is_port(Socket) -> + case handle_options(Options) of + {error, _Reason} = Error -> + Error; + {_SocketOptions, SshOptions} -> + case proplists:get_value(transport, Options, {tcp, gen_tcp, tcp_closed}) of + {tcp,_,_} -> + %% Is the socket a valid tcp socket? + case {{ok,[]} =/= inet:getopts(Socket, [delay_send]), + {ok,[{active,false}]} == inet:getopts(Socket, [active]) + } + of + {true, true} -> + {ok, {Host,_Port}} = inet:sockname(Socket), + Opts = [{user_pid,self()}, {host,fmt_host(Host)} | SshOptions], + ssh_connection_handler:start_connection(client, Socket, Opts, Timeout); + {true, false} -> + {error, not_passive_mode}; + _ -> + {error, not_tcp_socket} + end; + {L4,_,_} -> + {error, {unsupported,L4}} + end + end; +connect(Host, Port, Options) when is_integer(Port), Port>0 -> connect(Host, Port, Options, infinity). + connect(Host, Port, Options, Timeout) -> case handle_options(Options) of {error, _Reason} = Error -> @@ -199,8 +234,8 @@ stop_daemon(Address, Port) -> stop_daemon(Address, Port, Profile) -> ssh_system_sup:stop_system(Address, Port, Profile). %%-------------------------------------------------------------------- --spec shell(string()) -> _. --spec shell(string(), proplists:proplist()) -> _. +-spec shell(port() | string()) -> _. +-spec shell(port() | string(), proplists:proplist()) -> _. -spec shell(string(), integer(), proplists:proplist()) -> _. %% Host = string() @@ -212,27 +247,34 @@ stop_daemon(Address, Port, Profile) -> %% and will not return until the remote shell is ended.(e.g. on %% exit from the shell) %%-------------------------------------------------------------------- +shell(Socket) when is_port(Socket) -> + shell(Socket, []); shell(Host) -> shell(Host, ?SSH_DEFAULT_PORT, []). + +shell(Socket, Options) when is_port(Socket) -> + start_shell( connect(Socket, Options) ); shell(Host, Options) -> shell(Host, ?SSH_DEFAULT_PORT, Options). + shell(Host, Port, Options) -> - case connect(Host, Port, Options) of - {ok, ConnectionRef} -> - case ssh_connection:session_channel(ConnectionRef, infinity) of - {ok,ChannelId} -> - success = ssh_connection:ptty_alloc(ConnectionRef, ChannelId, []), - Args = [{channel_cb, ssh_shell}, - {init_args,[ConnectionRef, ChannelId]}, - {cm, ConnectionRef}, {channel_id, ChannelId}], - {ok, State} = ssh_channel:init([Args]), - ssh_channel:enter_loop(State); - Error -> - Error - end; + start_shell( connect(Host, Port, Options) ). + + +start_shell({ok, ConnectionRef}) -> + case ssh_connection:session_channel(ConnectionRef, infinity) of + {ok,ChannelId} -> + success = ssh_connection:ptty_alloc(ConnectionRef, ChannelId, []), + Args = [{channel_cb, ssh_shell}, + {init_args,[ConnectionRef, ChannelId]}, + {cm, ConnectionRef}, {channel_id, ChannelId}], + {ok, State} = ssh_channel:init([Args]), + ssh_channel:enter_loop(State); Error -> Error - end. + end; +start_shell(Error) -> + Error. %%-------------------------------------------------------------------- %%-------------------------------------------------------------------- @@ -835,3 +877,8 @@ handle_user_pref_pubkey_algs([H|T], Acc) -> false -> false end. + +fmt_host({A,B,C,D}) -> + lists:concat([A,".",B,".",C,".",D]); +fmt_host(T={_,_,_,_,_,_,_,_}) -> + lists:flatten(string:join([io_lib:format("~.16B",[A]) || A <- tuple_to_list(T)], ":")). diff --git a/lib/ssh/src/ssh_sftp.erl b/lib/ssh/src/ssh_sftp.erl index b03652a136..afc2fb88ff 100644 --- a/lib/ssh/src/ssh_sftp.erl +++ b/lib/ssh/src/ssh_sftp.erl @@ -95,8 +95,31 @@ %%==================================================================== start_channel(Cm) when is_pid(Cm) -> start_channel(Cm, []); +start_channel(Socket) when is_port(Socket) -> + start_channel(Socket, []); start_channel(Host) when is_list(Host) -> start_channel(Host, []). + +start_channel(Socket, Options) when is_port(Socket) -> + Timeout = + %% A mixture of ssh:connect and ssh_sftp:start_channel: + case proplists:get_value(connect_timeout, Options, undefined) of + undefined -> + proplists:get_value(timeout, Options, infinity); + TO -> + TO + end, + case ssh:connect(Socket, Options, Timeout) of + {ok,Cm} -> + case start_channel(Cm, Options) of + {ok, Pid} -> + {ok, Pid, Cm}; + Error -> + Error + end; + Error -> + Error + end; start_channel(Cm, Opts) when is_pid(Cm) -> Timeout = proplists:get_value(timeout, Opts, infinity), {_, ChanOpts, SftpOpts} = handle_options(Opts, [], [], []), diff --git a/lib/ssh/test/ssh_connection_SUITE.erl b/lib/ssh/test/ssh_connection_SUITE.erl index 0f757a0322..97dcb8570d 100644 --- a/lib/ssh/test/ssh_connection_SUITE.erl +++ b/lib/ssh/test/ssh_connection_SUITE.erl @@ -47,6 +47,7 @@ all() -> start_shell, start_shell_exec, start_shell_exec_fun, + start_shell_sock_exec_fun, gracefull_invalid_version, gracefull_invalid_start, gracefull_invalid_long_start, @@ -60,6 +61,9 @@ groups() -> payload() -> [simple_exec, + simple_exec_sock, + connect_sock_not_tcp, + connect_sock_not_passive, small_cat, big_cat, send_after_exit]. @@ -111,6 +115,18 @@ simple_exec() -> simple_exec(Config) when is_list(Config) -> ConnectionRef = ssh_test_lib:connect(?SSH_DEFAULT_PORT, [{silently_accept_hosts, true}, {user_interaction, false}]), + do_simple_exec(ConnectionRef). + + +simple_exec_sock(Config) -> + {ok, Sock} = gen_tcp:connect("localhost", ?SSH_DEFAULT_PORT, [{active,false}]), + {ok, ConnectionRef} = ssh:connect(Sock, [{silently_accept_hosts, true}, + {user_interaction, false}]), + do_simple_exec(ConnectionRef). + + + +do_simple_exec(ConnectionRef) -> {ok, ChannelId0} = ssh_connection:session_channel(ConnectionRef, infinity), success = ssh_connection:exec(ConnectionRef, ChannelId0, "echo testing", infinity), @@ -142,6 +158,18 @@ simple_exec(Config) when is_list(Config) -> 10000 -> ct:fail("timeout ~p:~p",[?MODULE,?LINE]) end. +%%-------------------------------------------------------------------- +connect_sock_not_tcp(Config) -> + {ok,Sock} = gen_udp:open(0, []), + {error, not_tcp_socket} = ssh:connect(Sock, []), + gen_udp:close(Sock). + +%%-------------------------------------------------------------------- +connect_sock_not_passive(Config) -> + {ok,Sock} = gen_tcp:connect("localhost", ?SSH_DEFAULT_PORT, []), + {error, not_passive_mode} = ssh:connect(Sock, []), + gen_tcp:close(Sock). + %%-------------------------------------------------------------------- small_cat() -> [{doc, "Use 'cat' to echo small data block back to us."}]. @@ -455,6 +483,42 @@ start_shell_exec_fun(Config) when is_list(Config) -> ssh:close(ConnectionRef), ssh:stop_daemon(Pid). +%%-------------------------------------------------------------------- +start_shell_sock_exec_fun() -> + [{doc, "start shell on tcp-socket to exec command"}]. + +start_shell_sock_exec_fun(Config) when is_list(Config) -> + PrivDir = proplists:get_value(priv_dir, Config), + UserDir = filename:join(PrivDir, nopubkey), % to make sure we don't use public-key-auth + file:make_dir(UserDir), + SysDir = proplists:get_value(data_dir, Config), + {Pid, Host, Port} = ssh_test_lib:daemon([{system_dir, SysDir}, + {user_dir, UserDir}, + {password, "morot"}, + {exec, fun ssh_exec/1}]), + + {ok, Sock} = gen_tcp:connect(Host, Port, [{active,false}]), + {ok,ConnectionRef} = ssh:connect(Sock, [{silently_accept_hosts, true}, + {user, "foo"}, + {password, "morot"}, + {user_interaction, true}, + {user_dir, UserDir}]), + + {ok, ChannelId0} = ssh_connection:session_channel(ConnectionRef, infinity), + + success = ssh_connection:exec(ConnectionRef, ChannelId0, + "testing", infinity), + + receive + {ssh_cm, ConnectionRef, {data, _ChannelId, 0, <<"testing\r\n">>}} -> + ok + after 5000 -> + ct:fail("Exec Timeout") + end, + + ssh:close(ConnectionRef), + ssh:stop_daemon(Pid). + %%-------------------------------------------------------------------- gracefull_invalid_version(Config) when is_list(Config) -> diff --git a/lib/ssh/test/ssh_sftp_SUITE.erl b/lib/ssh/test/ssh_sftp_SUITE.erl index 26fe0935e1..46db85c1be 100644 --- a/lib/ssh/test/ssh_sftp_SUITE.erl +++ b/lib/ssh/test/ssh_sftp_SUITE.erl @@ -86,7 +86,8 @@ groups() -> write_file, write_file_iolist, write_big_file, sftp_read_big_file, rename_file, mk_rm_dir, remove_file, links, retrieve_attributes, set_attributes, async_read, - async_write, position, pos_read, pos_write + async_write, position, pos_read, pos_write, + start_channel_sock ]} ]. @@ -624,6 +625,58 @@ pos_write(Config) when is_list(Config) -> NewData1 = list_to_binary("Bye, see you tomorrow!"), {ok, NewData1} = ssh_sftp:read_file(Sftp, FileName). +%%-------------------------------------------------------------------- +start_channel_sock(Config) -> + LoginOpts = + case proplists:get_value(group,Config) of + erlang_server -> + [{user, proplists:get_value(user, Config)}, + {password, proplists:get_value(passwd, Config)}]; + openssh_server -> + [] % Use public key + end, + + Opts = [{user_interaction, false}, + {silently_accept_hosts, true} + | LoginOpts], + + {Host,Port} = proplists:get_value(peer, Config), + + %% Get a tcp socket + {ok, Sock} = gen_tcp:connect(Host, Port, [{active,false}]), + + %% and open one channel on one new Connection + {ok, ChPid1, Conn} = ssh_sftp:start_channel(Sock, Opts), + + %% Test that the channel is usable + FileName = proplists:get_value(filename, Config), + ok = open_close_file(ChPid1, FileName, [read]), + ok = open_close_file(ChPid1, FileName, [write]), + + %% Try to open a second channel on the Connection + {ok, ChPid2} = ssh_sftp:start_channel(Conn, Opts), + ok = open_close_file(ChPid1, FileName, [read]), + ok = open_close_file(ChPid2, FileName, [read]), + + %% Test that the second channel still works after closing the first one + ok = ssh_sftp:stop_channel(ChPid1), + ok = open_close_file(ChPid2, FileName, [write]), + + %% Test the Connection survives that all channels are closed + ok = ssh_sftp:stop_channel(ChPid2), + {ok, ChPid3} = ssh_sftp:start_channel(Conn, Opts), + ok = open_close_file(ChPid3, FileName, [write]), + + %% Test that a closed channel really is closed + {error, closed} = ssh_sftp:open(ChPid2, FileName, [write]), + ok = ssh_sftp:stop_channel(ChPid3), + + %% Test that the socket is closed when the Connection closes + ok = ssh:close(Conn), + {error,einval} = inet:getopts(Sock, [active]), + + ok. + %%-------------------------------------------------------------------- sftp_nonexistent_subsystem() -> [{doc, "Try to execute sftp subsystem on a server that does not support it"}]. -- cgit v1.2.3