diff options
-rw-r--r-- | lib/ssh/src/ssh.erl | 321 | ||||
-rw-r--r-- | lib/ssh/src/ssh.hrl | 26 | ||||
-rw-r--r-- | lib/ssh/src/ssh_acceptor.erl | 107 | ||||
-rw-r--r-- | lib/ssh/src/ssh_connection_handler.erl | 10 | ||||
-rw-r--r-- | lib/ssh/src/ssh_options.erl | 61 | ||||
-rw-r--r-- | lib/ssh/src/ssh_sftp.erl | 9 | ||||
-rw-r--r-- | lib/ssh/src/ssh_sftpd.erl | 26 | ||||
-rw-r--r-- | lib/ssh/src/ssh_system_sup.erl | 2 | ||||
-rw-r--r-- | lib/ssh/src/ssh_transport.erl | 43 | ||||
-rw-r--r-- | lib/ssh/test/ssh_sftpd_SUITE.erl | 6 | ||||
-rw-r--r-- | lib/ssh/test/ssh_sftpd_erlclient_SUITE.erl | 2 | ||||
-rw-r--r-- | lib/ssh/test/ssh_sup_SUITE.erl | 111 | ||||
-rw-r--r-- | lib/ssh/test/ssh_test_lib.erl | 3 |
13 files changed, 409 insertions, 318 deletions
diff --git a/lib/ssh/src/ssh.erl b/lib/ssh/src/ssh.erl index 369a00ac40..ff424b738c 100644 --- a/lib/ssh/src/ssh.erl +++ b/lib/ssh/src/ssh.erl @@ -132,13 +132,12 @@ connect(Host, Port, UserOptions, Timeout) when is_integer(Port), SocketOpts = [{active,false} | ?GET_OPT(socket_options,Options)], try Transport:connect(Host, Port, SocketOpts, ConnectionTimeout) of {ok, Socket} -> - Opts = ?PUT_INTERNAL_OPT([{user_pid,self()}, {host,Host}], Options), + Opts = ?PUT_INTERNAL_OPT([{user_pid,self()}, {host,fmt_host(Host)}], Options), ssh_connection_handler:start_connection(client, Socket, Opts, Timeout); {error, Reason} -> {error, Reason} catch exit:{function_clause, _F} -> - io:format('function_clause ~p~n',[_F]), {error, {options, {transport, TransportOpts}}}; exit:badarg -> {error, {options, {socket_options, SocketOpts}}} @@ -183,16 +182,86 @@ daemon(Port) -> daemon(Port, []). -daemon(Port, UserOptions) when is_integer(Port), Port >= 0 -> - daemon(any, Port, UserOptions); - daemon(Socket, UserOptions) when is_port(Socket) -> - daemon(socket, Socket, UserOptions). + try + #{} = Options = ssh_options:handle_options(server, UserOptions), + case valid_socket_to_use(Socket, ?GET_OPT(transport,Options)) of + ok -> + {ok, {IP,Port}} = inet:sockname(Socket), + finalize_start(fmt_host(IP), Port, ?GET_OPT(profile, Options), + ?PUT_INTERNAL_OPT({connected_socket, Socket}, Options), + fun(Opts, DefaultResult) -> + try ssh_acceptor:handle_established_connection( + ?GET_INTERNAL_OPT(address, Opts), + ?GET_INTERNAL_OPT(port, Opts), + Opts, + Socket) + of + {error,Error} -> + {error,Error}; + _ -> + DefaultResult + catch + C:R -> + {error,{could_not_start_connection,{C,R}}} + end + end); + {error,SockError} -> + {error,SockError} + end + catch + throw:bad_fd -> + {error,bad_fd}; + throw:bad_socket -> + {error,bad_socket}; + error:{badmatch,{error,Error}} -> + {error,Error}; + error:Error -> + {error,Error}; + _C:_E -> + {error,{cannot_start_daemon,_C,_E}} + end; +daemon(Port, UserOptions) when 0 =< Port, Port =< 65535 -> + daemon(any, Port, UserOptions). + + +daemon(Host0, Port0, UserOptions0) when 0 =< Port0, Port0 =< 65535 -> + try + {Host1, UserOptions} = handle_daemon_args(Host0, UserOptions0), + #{} = Options0 = ssh_options:handle_options(server, UserOptions), + + {{Host,Port}, ListenSocket} = + open_listen_socket(Host1, Port0, Options0), + + %% Now Host,Port is what to use for the supervisor to register its name, + %% and ListenSocket is for listening on connections. But it is still owned + %% by self()... + + finalize_start(fmt_host(Host), Port, ?GET_OPT(profile, Options0), + ?PUT_INTERNAL_OPT({lsocket,{ListenSocket,self()}}, Options0), + fun(Opts, Result) -> + {_, Callback, _} = ?GET_OPT(transport, Opts), + receive + {request_control, ListenSocket, ReqPid} -> + ok = Callback:controlling_process(ListenSocket, ReqPid), + ReqPid ! {its_yours,ListenSocket}, + Result + end + end) + catch + throw:bad_fd -> + {error,bad_fd}; + throw:bad_socket -> + {error,bad_socket}; + error:{badmatch,{error,Error}} -> + {error,Error}; + error:Error -> + {error,Error}; + _C:_E -> + {error,{cannot_start_daemon,_C,_E}} + end. -daemon(Host0, Port, UserOptions0) -> - {Host, UserOptions} = handle_daemon_args(Host0, UserOptions0), - start_daemon(Host, Port, ssh_options:handle_options(server, UserOptions)). %%-------------------------------------------------------------------- -spec daemon_info(daemon_ref()) -> ok_error( [{atom(), term()}] ). @@ -292,35 +361,62 @@ default_algorithms() -> %%-------------------------------------------------------------------- %%% Internal functions %%-------------------------------------------------------------------- -handle_daemon_args(Host, UserOptions0) -> - case Host of - socket -> - {Host, UserOptions0}; - any -> - {ok, Host0} = inet:gethostname(), - Inet = proplists:get_value(inet, UserOptions0, inet), - {Host0, [Inet | UserOptions0]}; - {_,_,_,_} -> - {Host, [inet, {ip,Host} | UserOptions0]}; - {_,_,_,_,_,_,_,_} -> - {Host, [inet6, {ip,Host} | UserOptions0]}; - _ -> - error(badarg) - end. +handle_daemon_args(HostAddr, Opts) -> + IP = proplists:get_value(ip, Opts), + IPh = case inet:parse_strict_address(HostAddr) of + {ok, IPtuple} -> IPtuple; + {error, einval} when is_tuple(HostAddr), + size(HostAddr)==4 ; size(HostAddr)==6 -> HostAddr; + _ -> undefined + end, + handle_daemon_args(HostAddr, IPh, IP, Opts). + + +%% HostAddr is 'any' +handle_daemon_args(any, undefined, undefined, Opts) -> {any, Opts}; +handle_daemon_args(any, undefined, IP, Opts) -> {IP, Opts}; + +%% HostAddr is 'loopback' or "localhost" +handle_daemon_args(loopback, undefined, {127,_,_,_}=IP, Opts) -> {IP, Opts}; +handle_daemon_args(loopback, undefined, {0,0,0,0,0,0,0,1}=IP, Opts) -> {IP, Opts}; +handle_daemon_args(loopback, undefined, undefined, Opts) -> + IP = case proplists:get_value(inet,Opts) of + true -> {127,0,0,1}; + inet -> {127,0,0,1}; + inet6 -> {0,0,0,0,0,0,0,1}; + _ -> case proplists:get_value(inet6,Opts) of + true -> {0,0,0,0,0,0,0,1}; + _ -> {127,0,0,1} % default if no 'inet' nor 'inet6' + end + end, + {IP, [{ip,IP}|Opts]}; +handle_daemon_args("localhost", IPh, IP, Opts) -> + handle_daemon_args(loopback, IPh, IP, Opts); + +%% HostAddr is ip and no ip-option +handle_daemon_args(_, IP, undefined, Opts) when is_tuple(IP) -> {IP, [{ip,IP}|Opts]}; + +%% HostAddr and ip-option are equal +handle_daemon_args(_, IP, IP, Opts) when is_tuple(IP) -> {IP, Opts}; + +%% HostAddr is ip, but ip-option is different! +handle_daemon_args(_, IPh, IPo, _) when is_tuple(IPh), is_tuple(IPo) -> error({eoption,{ip,IPo}}); + +%% Something else. Whatever it is, it is wrong. +handle_daemon_args(_, _, _, _) -> error(badarg). %%%---------------------------------------------------------------- valid_socket_to_use(Socket, {tcp,_,_}) -> %% Is this tcp-socket a valid socket? - case {is_tcp_socket(Socket), - {ok,[{active,false}]} == inet:getopts(Socket, [active]) - } + try {is_tcp_socket(Socket), + {ok,[{active,false}]} == inet:getopts(Socket, [active]) + } of - {true, true} -> - ok; - {true, false} -> - {error, not_passive_mode}; - _ -> - {error, not_tcp_socket} + {true, true} -> ok; + {true, false} -> {error, not_passive_mode}; + _ -> {error, not_tcp_socket} + catch + _:_ -> {error, bad_socket} end; valid_socket_to_use(_, {L4,_,_}) -> @@ -334,158 +430,51 @@ is_tcp_socket(Socket) -> end. %%%---------------------------------------------------------------- -start_daemon(_, _, {error,Error}) -> - {error,Error}; - -start_daemon(socket, Socket, Options) -> - case valid_socket_to_use(Socket, ?GET_OPT(transport,Options)) of - ok -> - try - do_start_daemon(Socket, Options) - catch - throw:bad_fd -> {error,bad_fd}; - throw:bad_socket -> {error,bad_socket}; - _C:_E -> {error,{cannot_start_daemon,_C,_E}} - end; - {error,SockError} -> - {error,SockError} - end; - -start_daemon(Host, Port, Options) -> - try - do_start_daemon(Host, Port, Options) - catch - throw:bad_fd -> {error,bad_fd}; - throw:bad_socket -> {error,bad_socket}; - _C:_E -> {error,{cannot_start_daemon,_C,_E}} - end. - - -do_start_daemon(Socket, Options) -> - {ok, {IP,Port}} = - try {ok,_} = inet:sockname(Socket) - catch - _:_ -> throw(bad_socket) - end, - Host = fmt_host(IP), - Opts = ?PUT_INTERNAL_OPT([{asocket, Socket}, - {asock_owner,self()}, - {address, Host}, - {port, Port}, - {role, server}], Options), - - Profile = ?GET_OPT(profile, Options), - case ssh_system_sup:system_supervisor(Host, Port, Profile) of - undefined -> - try sshd_sup:start_child(Opts) of - {error, {already_started, _}} -> - {error, eaddrinuse}; - Result = {ok,_} -> - call_ssh_acceptor_handle_connection(Host, Port, Opts, Socket, Result); - Result = {error, _} -> - Result - catch - exit:{noproc, _} -> - {error, ssh_not_started} - end; - Sup -> - AccPid = ssh_system_sup:acceptor_supervisor(Sup), - case ssh_acceptor_sup:start_child(AccPid, Opts) of - {error, {already_started, _}} -> - {error, eaddrinuse}; - {ok, _} -> - call_ssh_acceptor_handle_connection(Host, Port, Opts, Socket, {ok,Sup}); - Other -> - Other - end +open_listen_socket(Host0, Port0, Options0) -> + case ?GET_SOCKET_OPT(fd, Options0) of + undefined -> + {ok,LSock} = ssh_acceptor:listen(Port0, Options0), + {ok,{_,LPort}} = inet:sockname(LSock), + {{Host0,LPort}, LSock}; + + Fd when is_integer(Fd) -> + %% Do gen_tcp:listen with the option {fd,Fd}: + {ok,LSock} = ssh_acceptor:listen(0, Options0), + {ok,{LHost,LPort}} = inet:sockname(LSock), + {{LHost,LPort}, LSock} end. -do_start_daemon(Host0, Port0, Options0) -> - {Host,Port1} = - try - case ?GET_SOCKET_OPT(fd, Options0) of - undefined -> - {Host0,Port0}; - Fd when Port0==0 -> - find_hostport(Fd) - end - catch - _:_ -> throw(bad_fd) - end, - {Port, WaitRequestControl, Options1} = - case Port1 of - 0 -> %% Allocate the socket here to get the port number... - {ok,LSock} = ssh_acceptor:callback_listen(0, Options0), - {ok,{_,LPort}} = inet:sockname(LSock), - {LPort, - LSock, - ?PUT_INTERNAL_OPT({lsocket,{LSock,self()}}, Options0) - }; - _ -> - {Port1, false, Options0} - end, +%%%---------------------------------------------------------------- +finalize_start(Host, Port, Profile, Options0, F) -> Options = ?PUT_INTERNAL_OPT([{address, Host}, {port, Port}, - {role, server}], Options1), - Profile = ?GET_OPT(profile, Options0), + {role, server}], Options0), case ssh_system_sup:system_supervisor(Host, Port, Profile) of undefined -> try sshd_sup:start_child(Options) of {error, {already_started, _}} -> {error, eaddrinuse}; + {error, Error} -> + {error, Error}; Result = {ok,_} -> - sync_request_control(WaitRequestControl, Options), - Result; - Result = {error, _} -> - Result + F(Options, Result) catch exit:{noproc, _} -> {error, ssh_not_started} end; - Sup -> + Sup -> AccPid = ssh_system_sup:acceptor_supervisor(Sup), case ssh_acceptor_sup:start_child(AccPid, Options) of {error, {already_started, _}} -> {error, eaddrinuse}; + {error, Error} -> + {error, Error}; {ok, _} -> - sync_request_control(WaitRequestControl, Options), - {ok, Sup}; - Other -> - Other + F(Options, {ok,Sup}) end end. -call_ssh_acceptor_handle_connection(Host, Port, Options, Socket, DefaultResult) -> - {_, Callback, _} = ?GET_OPT(transport, Options), - try ssh_acceptor:handle_connection(Callback, Host, Port, Options, Socket) - of - {error,Error} -> {error,Error}; - _ -> DefaultResult - catch - C:R -> {error,{could_not_start_connection,{C,R}}} - end. - - -sync_request_control(false, _Options) -> - ok; -sync_request_control(LSock, Options) -> - {_, Callback, _} = ?GET_OPT(transport, Options), - receive - {request_control,LSock,ReqPid} -> - ok = Callback:controlling_process(LSock, ReqPid), - ReqPid ! {its_yours,LSock}, - ok - end. - -find_hostport(Fd) -> - %% Using internal functions inet:open/8 and inet:close/0. - %% Don't try this at home unless you know what you are doing! - {ok,S} = inet:open(Fd, {0,0,0,0}, 0, [], tcp, inet, stream, inet_tcp), - {ok, HostPort} = inet:sockname(S), - ok = inet:close(S), - HostPort. - -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)], ":")). +%%%---------------------------------------------------------------- +fmt_host(any) -> any; +fmt_host(IP) when is_tuple(IP) -> inet:ntoa(IP); +fmt_host(Str) when is_list(Str) -> Str. diff --git a/lib/ssh/src/ssh.hrl b/lib/ssh/src/ssh.hrl index c1ba58ed40..315310f700 100644 --- a/lib/ssh/src/ssh.hrl +++ b/lib/ssh/src/ssh.hrl @@ -75,9 +75,12 @@ %% Option access macros -define(do_get_opt(C,K,O), ssh_options:get_value(C,K,O, ?MODULE,?LINE)). --define(do_get_opt(C,K,O,D), ssh_options:get_value(C,K,O,D,?MODULE,?LINE)). +-define(do_get_opt(C,K,O,D), ssh_options:get_value(C,K,O,?LAZY(D),?MODULE,?LINE)). + +-define(LAZY(D), fun()-> D end). -define(GET_OPT(Key,Opts), ?do_get_opt(user_options, Key,Opts ) ). +-define(GET_OPT(Key,Opts,Def), ?do_get_opt(user_options, Key,Opts,Def) ). -define(GET_INTERNAL_OPT(Key,Opts), ?do_get_opt(internal_options,Key,Opts ) ). -define(GET_INTERNAL_OPT(Key,Opts,Def), ?do_get_opt(internal_options,Key,Opts,Def) ). -define(GET_SOCKET_OPT(Key,Opts), ?do_get_opt(socket_options, Key,Opts ) ). @@ -89,6 +92,10 @@ -define(PUT_INTERNAL_OPT(KeyVal,Opts), ?do_put_opt(internal_options,KeyVal,Opts) ). -define(PUT_SOCKET_OPT(KeyVal,Opts), ?do_put_opt(socket_options, KeyVal,Opts) ). +-define(do_del_opt(C,K,O), ssh_options:delete_key(C,K,O, ?MODULE,?LINE)). +-define(DELETE_INTERNAL_OPT(Key,Opts), ?do_del_opt(internal_options,Key,Opts) ). + + %% Types -type role() :: client | server . -type ok_error(SuccessType) :: {ok, SuccessType} | {error, any()} . @@ -109,12 +116,25 @@ -type double_algs() :: list( {client2serverlist,simple_algs()} | {server2client,simple_algs()} ) | simple_algs() . +-type options() :: #{socket_options := socket_options(), + internal_options := internal_options(), + option_key() => any() + }. + +-type socket_options() :: proplists:proplist(). +-type internal_options() :: #{option_key() => any()}. + +-type option_key() :: atom(). + + %% Records -record(ssh, { - role, %% client | server - peer, %% string version of peer address + role :: client | role(), + peer :: undefined | + {inet:hostname(), + {inet:ip_adress(),inet:port_number()}}, %% string version of peer address c_vsn, %% client version {Major,Minor} s_vsn, %% server version {Major,Minor} diff --git a/lib/ssh/src/ssh_acceptor.erl b/lib/ssh/src/ssh_acceptor.erl index 42be18f2ad..f9e2280212 100644 --- a/lib/ssh/src/ssh_acceptor.erl +++ b/lib/ssh/src/ssh_acceptor.erl @@ -27,8 +27,8 @@ %% Internal application API -export([start_link/4, number_of_connections/1, - callback_listen/2, - handle_connection/5]). + listen/2, + handle_established_connection/4]). %% spawn export -export([acceptor_init/5, acceptor_loop/6]). @@ -42,41 +42,57 @@ start_link(Port, Address, Options, AcceptTimeout) -> Args = [self(), Port, Address, Options, AcceptTimeout], proc_lib:start_link(?MODULE, acceptor_init, Args). +%%%---------------------------------------------------------------- +number_of_connections(SystemSup) -> + length([X || + {R,X,supervisor,[ssh_subsystem_sup]} <- supervisor:which_children(SystemSup), + is_pid(X), + is_reference(R) + ]). + +%%%---------------------------------------------------------------- +listen(Port, Options) -> + {_, Callback, _} = ?GET_OPT(transport, Options), + SockOpts = [{active, false}, {reuseaddr,true} | ?GET_OPT(socket_options, Options)], + case Callback:listen(Port, SockOpts) of + {error, nxdomain} -> + Callback:listen(Port, lists:delete(inet6, SockOpts)); + {error, enetunreach} -> + Callback:listen(Port, lists:delete(inet6, SockOpts)); + {error, eafnosupport} -> + Callback:listen(Port, lists:delete(inet6, SockOpts)); + Other -> + Other + end. + +%%%---------------------------------------------------------------- +handle_established_connection(Address, Port, Options, Socket) -> + {_, Callback, _} = ?GET_OPT(transport, Options), + handle_connection(Callback, Address, Port, Options, Socket). + %%-------------------------------------------------------------------- %%% Internal functions %%-------------------------------------------------------------------- acceptor_init(Parent, Port, Address, Opts, AcceptTimeout) -> - {_, Callback, _} = ?GET_OPT(transport, Opts), try - {LSock0,SockOwner0} = ?GET_INTERNAL_OPT(lsocket, Opts), - true = is_pid(SockOwner0), - {ok,{_,Port}} = inet:sockname(LSock0), - {LSock0, SockOwner0} + ?GET_INTERNAL_OPT(lsocket, Opts) of {LSock, SockOwner} -> - %% Use existing socket - proc_lib:init_ack(Parent, {ok, self()}), - request_ownership(LSock, SockOwner), - acceptor_loop(Callback, Port, Address, Opts, LSock, AcceptTimeout) - catch - error:{badkey,lsocket} -> - %% Open new socket - try - socket_listen(Port, Opts) - of - {ok, ListenSocket} -> + case inet:sockname(LSock) of + {ok,{_,Port}} -> % A usable, open LSock proc_lib:init_ack(Parent, {ok, self()}), - {_, Callback, _} = ?GET_OPT(transport, Opts), - acceptor_loop(Callback, - Port, Address, Opts, ListenSocket, AcceptTimeout); - {error,Error} -> - proc_lib:init_ack(Parent, Error), - {error,Error} - catch - _:_ -> - {error,listen_socket_failed} - end; + request_ownership(LSock, SockOwner), + {_, Callback, _} = ?GET_OPT(transport, Opts), + acceptor_loop(Callback, Port, Address, Opts, LSock, AcceptTimeout); + {error,_} -> % Not open, a restart + {ok,NewLSock} = listen(Port, Opts), + proc_lib:init_ack(Parent, {ok, self()}), + Opts1 = ?DELETE_INTERNAL_OPT(lsocket, Opts), + {_, Callback, _} = ?GET_OPT(transport, Opts1), + acceptor_loop(Callback, Port, Address, Opts1, NewLSock, AcceptTimeout) + end + catch _:_ -> {error,use_existing_socket_failed} end. @@ -88,30 +104,7 @@ request_ownership(LSock, SockOwner) -> {its_yours,LSock} -> ok end. - -socket_listen(Port0, Opts) -> - Port = case ?GET_SOCKET_OPT(fd, Opts) of - undefined -> Port0; - _ -> 0 - end, - callback_listen(Port, Opts). - - -callback_listen(Port, Opts0) -> - {_, Callback, _} = ?GET_OPT(transport, Opts0), - Opts = ?PUT_SOCKET_OPT([{active, false}, {reuseaddr,true}], Opts0), - SockOpts = ?GET_OPT(socket_options, Opts), - case Callback:listen(Port, SockOpts) of - {error, nxdomain} -> - Callback:listen(Port, lists:delete(inet6, SockOpts)); - {error, enetunreach} -> - Callback:listen(Port, lists:delete(inet6, SockOpts)); - {error, eafnosupport} -> - Callback:listen(Port, lists:delete(inet6, SockOpts)); - Other -> - Other - end. - +%%%---------------------------------------------------------------- acceptor_loop(Callback, Port, Address, Opts, ListenSocket, AcceptTimeout) -> case (catch Callback:accept(ListenSocket, AcceptTimeout)) of {ok, Socket} -> @@ -128,6 +121,7 @@ acceptor_loop(Callback, Port, Address, Opts, ListenSocket, AcceptTimeout) -> ListenSocket, AcceptTimeout) end. +%%%---------------------------------------------------------------- handle_connection(Callback, Address, Port, Options, Socket) -> Profile = ?GET_OPT(profile, Options), SystemSup = ssh_system_sup:system_supervisor(Address, Port, Profile), @@ -159,7 +153,7 @@ handle_connection(Callback, Address, Port, Options, Socket) -> {error,max_sessions} end. - +%%%---------------------------------------------------------------- handle_error(timeout) -> ok; @@ -186,10 +180,3 @@ handle_error(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_pid(X), - is_reference(R) - ]). diff --git a/lib/ssh/src/ssh_connection_handler.erl b/lib/ssh/src/ssh_connection_handler.erl index 5a13209ae3..50a29bbb53 100644 --- a/lib/ssh/src/ssh_connection_handler.erl +++ b/lib/ssh/src/ssh_connection_handler.erl @@ -413,14 +413,8 @@ init_connection(server, C = #connection{}, Opts) -> SubSystemSup = proplists:get_value(subsystem_sup, Sups), ConnectionSup = proplists:get_value(connection_sup, Sups), - Shell = ?GET_OPT(shell, Opts), - Exec = ?GET_OPT(exec, Opts), - CliSpec = case ?GET_OPT(ssh_cli, Opts) of - undefined -> {ssh_cli, [Shell]}; - Spec -> Spec - end, - C#connection{cli_spec = CliSpec, - exec = Exec, + C#connection{cli_spec = ?GET_OPT(ssh_cli, Opts, {ssh_cli,[?GET_OPT(shell, Opts)]}), + exec = ?GET_OPT(exec, Opts), system_supervisor = SystemSup, sub_system_supervisor = SubSystemSup, connection_supervisor = ConnectionSup diff --git a/lib/ssh/src/ssh_options.erl b/lib/ssh/src/ssh_options.erl index 55f9c6bdc8..ee3cdbb8a0 100644 --- a/lib/ssh/src/ssh_options.erl +++ b/lib/ssh/src/ssh_options.erl @@ -28,6 +28,7 @@ -export([default/1, get_value/5, get_value/6, put_value/5, + delete_key/5, handle_options/2 ]). @@ -37,16 +38,6 @@ %%%================================================================ %%% Types --type options() :: #{socket_options := socket_options(), - internal_options := internal_options(), - option_key() => any() - }. - --type socket_options() :: proplists:proplist(). --type internal_options() :: #{option_key() => any()}. - --type option_key() :: atom(). - -type option_in() :: proplists:property() | proplists:proplist() . -type option_class() :: internal_options | socket_options | user_options . @@ -75,22 +66,23 @@ get_value(Class, Key, Opts, _CallerMod, _CallerLine) when is_map(Opts) -> user_options -> maps:get(Key, Opts) end; get_value(Class, Key, Opts, _CallerMod, _CallerLine) -> - io:format("*** Bad Opts GET OPT ~p ~p:~p Key=~p,~n Opts=~p~n",[Class,_CallerMod,_CallerLine,Key,Opts]), error({bad_options,Class, Key, Opts, _CallerMod, _CallerLine}). --spec get_value(option_class(), option_key(), options(), any(), +-spec get_value(option_class(), option_key(), options(), fun(() -> any()), atom(), non_neg_integer()) -> any() | no_return(). -get_value(socket_options, Key, Opts, Def, _CallerMod, _CallerLine) when is_map(Opts) -> - proplists:get_value(Key, maps:get(socket_options,Opts), Def); -get_value(Class, Key, Opts, Def, CallerMod, CallerLine) when is_map(Opts) -> +get_value(socket_options, Key, Opts, DefFun, _CallerMod, _CallerLine) when is_map(Opts) -> + proplists:get_value(Key, maps:get(socket_options,Opts), DefFun); +get_value(Class, Key, Opts, DefFun, CallerMod, CallerLine) when is_map(Opts) -> try get_value(Class, Key, Opts, CallerMod, CallerLine) + of + undefined -> DefFun(); + Value -> Value catch - error:{badkey,Key} -> Def + error:{badkey,Key} -> DefFun() end; -get_value(Class, Key, Opts, _Def, _CallerMod, _CallerLine) -> - io:format("*** Bad Opts GET OPT ~p ~p:~p Key=~p,~n Opts=~p~n",[Class,_CallerMod,_CallerLine,Key,Opts]), +get_value(Class, Key, Opts, _DefFun, _CallerMod, _CallerLine) -> error({bad_options,Class, Key, Opts, _CallerMod, _CallerLine}). @@ -136,6 +128,19 @@ put_socket_value(A, SockOpts) when is_atom(A) -> %%%================================================================ %%% +%%% Delete an option +%%% + +-spec delete_key(option_class(), option_key(), options(), + atom(), non_neg_integer()) -> options(). + +delete_key(internal_options, Key, Opts, _CallerMod, _CallerLine) when is_map(Opts) -> + InternalOpts = maps:get(internal_options,Opts), + Opts#{internal_options := maps:remove(Key, InternalOpts)}. + + +%%%================================================================ +%%% %%% Initialize the options %%% @@ -792,18 +797,17 @@ read_moduli_file(D, I, Acc) -> check_silently_accept_hosts(B) when is_boolean(B) -> true; check_silently_accept_hosts(F) when is_function(F,2) -> true; -check_silently_accept_hosts({S,F}) when is_atom(S), - is_function(F,2) -> - lists:member(S, ?SHAs) andalso - lists:member(S, proplists:get_value(hashs,crypto:supports())); -check_silently_accept_hosts({L,F}) when is_list(L), - is_function(F,2) -> - lists:all(fun(S) -> - lists:member(S, ?SHAs) andalso - lists:member(S, proplists:get_value(hashs,crypto:supports())) - end, L); +check_silently_accept_hosts({false,S}) when is_atom(S) -> valid_hash(S); +check_silently_accept_hosts({S,F}) when is_function(F,2) -> valid_hash(S); check_silently_accept_hosts(_) -> false. + +valid_hash(S) -> valid_hash(S, proplists:get_value(hashs,crypto:supports())). + +valid_hash(S, Ss) when is_atom(S) -> lists:member(S, ?SHAs) andalso lists:member(S, Ss); +valid_hash(L, Ss) when is_list(L) -> lists:all(fun(S) -> valid_hash(S,Ss) end, L); +valid_hash(X, _) -> error_in_check(X, "Expect atom or list in fingerprint spec"). + %%%---------------------------------------------------------------- check_preferred_algorithms(Algs) -> try alg_duplicates(Algs, [], []) @@ -871,6 +875,7 @@ handle_pref_alg(Key, Vs, _) -> chk_alg_vs(OptKey, Values, SupportedValues) -> case (Values -- SupportedValues) of [] -> Values; + [none] -> [none]; % for testing only Bad -> error_in_check({OptKey,Bad}, "Unsupported value(s) found") end. diff --git a/lib/ssh/src/ssh_sftp.erl b/lib/ssh/src/ssh_sftp.erl index 140856c8e3..f1f7b57e8d 100644 --- a/lib/ssh/src/ssh_sftp.erl +++ b/lib/ssh/src/ssh_sftp.erl @@ -1063,15 +1063,6 @@ attr_to_info(A) when is_record(A, ssh_xfer_attr) -> gid = A#ssh_xfer_attr.group}. -%% Added workaround for sftp timestam problem. (Timestamps should be -%% in UTC but they where not) . The workaround uses a deprecated -%% function i calandar. This will work as expected most of the time -%% but has problems for the same reason as -%% calendar:local_time_to_universal_time/1. We consider it better that -%% the timestamps work as expected most of the time instead of none of -%% the time. Hopfully the file-api will be updated so that we can -%% solve this problem in a better way in the future. - unix_to_datetime(undefined) -> undefined; unix_to_datetime(UTCSecs) -> diff --git a/lib/ssh/src/ssh_sftpd.erl b/lib/ssh/src/ssh_sftpd.erl index 9352046795..b879116393 100644 --- a/lib/ssh/src/ssh_sftpd.erl +++ b/lib/ssh/src/ssh_sftpd.erl @@ -34,8 +34,7 @@ %%-------------------------------------------------------------------- %% External exports --export([subsystem_spec/1, - listen/1, listen/2, listen/3, stop/1]). +-export([subsystem_spec/1]). -export([init/1, handle_ssh_msg/2, handle_msg/2, terminate/2]). @@ -76,29 +75,6 @@ subsystem_spec(Options) -> {"sftp", {?MODULE, Options}}. -%%% DEPRECATED START %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% - -%%-------------------------------------------------------------------- -%% Function: listen() -> Pid | {error,Error} -%% Description: Starts the server -%%-------------------------------------------------------------------- -listen(Port) -> - listen(any, Port, []). -listen(Port, Options) -> - listen(any, Port, Options). -listen(Addr, Port, Options) -> - SubSystems = [subsystem_spec(Options)], - ssh:daemon(Addr, Port, [{subsystems, SubSystems} |Options]). - -%%-------------------------------------------------------------------- -%% Function: stop(Pid) -> ok -%% Description: Stops the listener -%%-------------------------------------------------------------------- -stop(Pid) -> - ssh:stop_listener(Pid). - - -%%% DEPRECATED END %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %%==================================================================== %% subsystem callbacks diff --git a/lib/ssh/src/ssh_system_sup.erl b/lib/ssh/src/ssh_system_sup.erl index b0bbd3aae5..5a58ef1c44 100644 --- a/lib/ssh/src/ssh_system_sup.erl +++ b/lib/ssh/src/ssh_system_sup.erl @@ -131,7 +131,7 @@ init([Options]) -> RestartStrategy = one_for_one, MaxR = 0, MaxT = 3600, - Children = case ?GET_INTERNAL_OPT(asocket,Options,undefined) of + Children = case ?GET_INTERNAL_OPT(connected_socket,Options,undefined) of undefined -> child_specs(Options); _ -> [] end, diff --git a/lib/ssh/src/ssh_transport.erl b/lib/ssh/src/ssh_transport.erl index 02c995399a..54ea80c727 100644 --- a/lib/ssh/src/ssh_transport.erl +++ b/lib/ssh/src/ssh_transport.erl @@ -200,9 +200,6 @@ is_valid_mac(Mac, Data, #ssh{recv_mac = Algorithm, recv_mac_key = Key, recv_sequence = SeqNum}) -> Mac == mac(Algorithm, Key, SeqNum, Data). -yes_no(Ssh, Prompt) -> - (Ssh#ssh.io_cb):yes_no(Prompt, Ssh). - format_version({Major,Minor}, SoftwareVersion) -> "SSH-" ++ integer_to_list(Major) ++ "." ++ integer_to_list(Minor) ++ "-" ++ SoftwareVersion. @@ -755,16 +752,44 @@ public_algo({#'ECPoint'{},{namedCurve,OID}}) -> accepted_host(Ssh, PeerName, Public, Opts) -> case ?GET_OPT(silently_accept_hosts, Opts) of - F when is_function(F,2) -> + + %% Original option values; User question and no host key fingerprints known. + %% Keep the original question unchanged: + false -> yes == yes_no(Ssh, "New host " ++ PeerName ++ " accept"); + true -> true; + + %% Variant: User question but with host key fingerprint in the question: + {false,Alg} -> + HostKeyAlg = (Ssh#ssh.algorithms)#alg.hkey, + Prompt = io_lib:format("The authenticity of the host can't be established.~n" + "~s host key fingerprint is ~s.~n" + "New host ~p accept", + [fmt_hostkey(HostKeyAlg), + public_key:ssh_hostkey_fingerprint(Alg,Public), + PeerName]), + yes == yes_no(Ssh, Prompt); + + %% Call-back alternatives: A user provided fun is called for the decision: + F when is_function(F,2) -> true == (catch F(PeerName, public_key:ssh_hostkey_fingerprint(Public))); + {DigestAlg,F} when is_function(F,2) -> - true == (catch F(PeerName, public_key:ssh_hostkey_fingerprint(DigestAlg,Public))); - true -> - true; - false -> - yes == yes_no(Ssh, "New host " ++ PeerName ++ " accept") + true == (catch F(PeerName, public_key:ssh_hostkey_fingerprint(DigestAlg,Public))) + end. + +yes_no(Ssh, Prompt) -> + (Ssh#ssh.io_cb):yes_no(Prompt, Ssh#ssh.opts). + + +fmt_hostkey('ssh-rsa') -> "RSA"; +fmt_hostkey('ssh-dss') -> "DSA"; +fmt_hostkey(A) when is_atom(A) -> fmt_hostkey(atom_to_list(A)); +fmt_hostkey("ecdsa"++_) -> "ECDSA"; +fmt_hostkey(X) -> X. + + known_host_key(#ssh{opts = Opts, key_cb = {KeyCb,KeyCbOpts}, peer = {PeerName,_}} = Ssh, Public, Alg) -> UserOpts = ?GET_OPT(user_options, Opts), diff --git a/lib/ssh/test/ssh_sftpd_SUITE.erl b/lib/ssh/test/ssh_sftpd_SUITE.erl index b167f98ac8..673fb54a4f 100644 --- a/lib/ssh/test/ssh_sftpd_SUITE.erl +++ b/lib/ssh/test/ssh_sftpd_SUITE.erl @@ -151,8 +151,8 @@ init_per_testcase(TestCase, Config) -> SubSystems = [ssh_sftpd:subsystem_spec([])], ssh:daemon(0, [{subsystems, SubSystems}|Options]) end, - {ok,Dinf} = ssh:daemon_info(Sftpd), - Port = proplists:get_value(port, Dinf), + + Port = ssh_test_lib:daemon_port(Sftpd), Cm = ssh_test_lib:connect(Port, [{user_dir, ClientUserDir}, @@ -187,7 +187,7 @@ init_per_testcase(TestCase, Config) -> [{sftp, {Cm, Channel}}, {sftpd, Sftpd }| Config]. end_per_testcase(_TestCase, Config) -> - ssh_sftpd:stop(proplists:get_value(sftpd, Config)), + catch ssh:stop_daemon(proplists:get_value(sftpd, Config)), {Cm, Channel} = proplists:get_value(sftp, Config), ssh_connection:close(Cm, Channel), ssh:close(Cm), diff --git a/lib/ssh/test/ssh_sftpd_erlclient_SUITE.erl b/lib/ssh/test/ssh_sftpd_erlclient_SUITE.erl index b4d7eadfa4..9b5d6b5fae 100644 --- a/lib/ssh/test/ssh_sftpd_erlclient_SUITE.erl +++ b/lib/ssh/test/ssh_sftpd_erlclient_SUITE.erl @@ -138,7 +138,7 @@ init_per_testcase(TestCase, Config) -> [{port, Port}, {sftp, {ChannelPid, Connection}}, {sftpd, Sftpd} | NewConfig]. end_per_testcase(_TestCase, Config) -> - catch ssh_sftpd:stop(proplists:get_value(sftpd, Config)), + catch ssh:stop_daemon(proplists:get_value(sftpd, Config)), {Sftp, Connection} = proplists:get_value(sftp, Config), catch ssh_sftp:stop_channel(Sftp), catch ssh:close(Connection), diff --git a/lib/ssh/test/ssh_sup_SUITE.erl b/lib/ssh/test/ssh_sup_SUITE.erl index ff53e1c4c6..fdeb8186a5 100644 --- a/lib/ssh/test/ssh_sup_SUITE.erl +++ b/lib/ssh/test/ssh_sup_SUITE.erl @@ -41,7 +41,8 @@ suite() -> {timetrap,{seconds,100}}]. all() -> - [default_tree, sshc_subtree, sshd_subtree, sshd_subtree_profile]. + [default_tree, sshc_subtree, sshd_subtree, sshd_subtree_profile, + killed_acceptor_restarts]. groups() -> []. @@ -100,6 +101,7 @@ default_tree(Config) when is_list(Config) -> ?wait_match([], supervisor:which_children(sshc_sup)), ?wait_match([], supervisor:which_children(sshd_sup)). +%%------------------------------------------------------------------------- sshc_subtree() -> [{doc, "Make sure the sshc subtree is correct"}]. sshc_subtree(Config) when is_list(Config) -> @@ -128,6 +130,7 @@ sshc_subtree(Config) when is_list(Config) -> ssh:close(Pid2), ?wait_match([], supervisor:which_children(sshc_sup)). +%%------------------------------------------------------------------------- sshd_subtree() -> [{doc, "Make sure the sshd subtree is correct"}]. sshd_subtree(Config) when is_list(Config) -> @@ -149,6 +152,7 @@ sshd_subtree(Config) when is_list(Config) -> ct:sleep(?WAIT_FOR_SHUTDOWN), ?wait_match([], supervisor:which_children(sshd_sup)). +%%------------------------------------------------------------------------- sshd_subtree_profile() -> [{doc, "Make sure the sshd subtree using profile option is correct"}]. sshd_subtree_profile(Config) when is_list(Config) -> @@ -172,19 +176,88 @@ sshd_subtree_profile(Config) when is_list(Config) -> ct:sleep(?WAIT_FOR_SHUTDOWN), ?wait_match([], supervisor:which_children(sshd_sup)). +%%------------------------------------------------------------------------- +killed_acceptor_restarts(Config) -> + Profile = proplists:get_value(profile, Config), + SystemDir = proplists:get_value(data_dir, Config), + UserDir = proplists:get_value(userdir, Config), + {ok, DaemonPid} = ssh:daemon(0, [{system_dir, SystemDir}, + {failfun, fun ssh_test_lib:failfun/2}, + {user_passwords, [{?USER, ?PASSWD}]}, + {profile, Profile}]), + + {ok, DaemonPid2} = ssh:daemon(0, [{system_dir, SystemDir}, + {failfun, fun ssh_test_lib:failfun/2}, + {user_passwords, [{?USER, ?PASSWD}]}, + {profile, Profile}]), + + Port = ssh_test_lib:daemon_port(DaemonPid), + Port2 = ssh_test_lib:daemon_port(DaemonPid2), + true = (Port /= Port2), + + ct:pal("~s",[lists:flatten(ssh_info:string())]), + + {ok,[{AccPid,ListenAddr,Port}]} = acceptor_pid(DaemonPid), + {ok,[{AccPid2,ListenAddr,Port2}]} = acceptor_pid(DaemonPid2), + + true = (AccPid /= AccPid2), + + %% Connect first client and check it is alive: + {ok,C1} = ssh:connect("localhost", Port, [{silently_accept_hosts, true}, + {user_interaction, false}, + {user, ?USER}, + {password, ?PASSWD}, + {user_dir, UserDir}]), + [{client_version,_}] = ssh:connection_info(C1,[client_version]), + %% Make acceptor restart: + exit(AccPid, kill), + + %% Check it is a new acceptor: + {ok,[{AccPid1,ListenAddr,Port}]} = acceptor_pid(DaemonPid), + true = (AccPid /= AccPid1), + true = (AccPid2 /= AccPid1), + + %% Connect second client and check it is alive: + {ok,C2} = ssh:connect("localhost", Port, [{silently_accept_hosts, true}, + {user_interaction, false}, + {user, ?USER}, + {password, ?PASSWD}, + {user_dir, UserDir}]), + [{client_version,_}] = ssh:connection_info(C2,[client_version]), + + ct:pal("~s",[lists:flatten(ssh_info:string())]), + + %% Check first client is still alive: + [{client_version,_}] = ssh:connection_info(C1,[client_version]), + + ok = ssh:stop_daemon(DaemonPid2), + timer:sleep(15000), + [{client_version,_}] = ssh:connection_info(C1,[client_version]), + [{client_version,_}] = ssh:connection_info(C2,[client_version]), + + ok = ssh:stop_daemon(DaemonPid), + timer:sleep(15000), + {error,closed} = ssh:connection_info(C1,[client_version]), + {error,closed} = ssh:connection_info(C2,[client_version]). + +%%------------------------------------------------------------------------- +%% Help functions +%%------------------------------------------------------------------------- check_sshd_system_tree(Daemon, Config) -> Host = proplists:get_value(host, Config), Port = proplists:get_value(port, Config), UserDir = proplists:get_value(userdir, Config), {ok, Client} = ssh:connect(Host, Port, [{silently_accept_hosts, true}, - {user_interaction, false}, - {user, ?USER}, {password, ?PASSWD},{user_dir, UserDir}]), + {user_interaction, false}, + {user, ?USER}, + {password, ?PASSWD}, + {user_dir, UserDir}]), ?wait_match([{_,SubSysSup, supervisor,[ssh_subsystem_sup]}, {{ssh_acceptor_sup,_,_,_}, AccSup, supervisor,[ssh_acceptor_sup]}], supervisor:which_children(Daemon), - [SubSysSup,AccSup]), + [SubSysSup,AccSup]), ?wait_match([{{server,ssh_connection_sup, _,_}, ConnectionSup, supervisor, @@ -208,4 +281,32 @@ check_sshd_system_tree(Daemon, Config) -> ?wait_match([{_, _,worker,[ssh_channel]}], supervisor:which_children(ChannelSup)), ssh:close(Client). - + +acceptor_pid(DaemonPid) -> + Parent = self(), + Pid = spawn(fun() -> + Parent ! {self(), supsearch, + [{AccPid,ListenAddr,Port} + + || {{server,ssh_system_sup,ListenAddr,Port,NS}, + DPid,supervisor, + [ssh_system_sup]} <- supervisor:which_children(sshd_sup), + DPid == DaemonPid, + + {{ssh_acceptor_sup,L1,P1,NS1}, + AccSupPid,supervisor, + [ssh_acceptor_sup]} <- supervisor:which_children(DaemonPid), + L1 == ListenAddr, + P1 == Port, + NS1 == NS1, + + {{ssh_acceptor_sup,L2,P2,NS2}, + AccPid,worker, + [ssh_acceptor]} <- supervisor:which_children(AccSupPid), + L2 == ListenAddr, + P2 == Port, + NS2 == NS]} + end), + receive {Pid, supsearch, L} -> {ok,L} + after 2000 -> timeout + end. diff --git a/lib/ssh/test/ssh_test_lib.erl b/lib/ssh/test/ssh_test_lib.erl index 1673f52821..0ada8233a7 100644 --- a/lib/ssh/test/ssh_test_lib.erl +++ b/lib/ssh/test/ssh_test_lib.erl @@ -64,6 +64,9 @@ daemon(Host, Port, Options) -> Error end. +daemon_port(Pid) -> daemon_port(0, Pid). + + daemon_port(0, Pid) -> {ok,Dinf} = ssh:daemon_info(Pid), proplists:get_value(port, Dinf); daemon_port(Port, _) -> Port. |