diff options
Diffstat (limited to 'lib/ssh')
36 files changed, 1654 insertions, 1259 deletions
diff --git a/lib/ssh/doc/src/ssh.xml b/lib/ssh/doc/src/ssh.xml index 88d402cf38..368261968d 100644 --- a/lib/ssh/doc/src/ssh.xml +++ b/lib/ssh/doc/src/ssh.xml @@ -379,7 +379,7 @@ on the given port.</fsummary> <type> <v>Port = integer()</v> - <v>HostAddress = ip_address() | any</v> + <v>HostAddress = ip_address() | any | loopback</v> <v>Options = [{Option, Value}]</v> <v>Option = atom()</v> <v>Value = term()</v> @@ -390,6 +390,26 @@ <p>Starts a server listening for SSH connections on the given port. If the <c>Port</c> is 0, a random free port is selected. See <seealso marker="#daemon_info/1">daemon_info/1</seealso> about how to find the selected port number.</p> + + <p>Please note that by historical reasons both the <c>HostAddress</c> argument and the inet socket option + <c>ip</c> set the listening address. This is a source of possible inconsistent settings.</p> + + <p>The rules for handling the two address passing options are:</p> + <list> + <item>if <c>HostAddress</c> is an IP-address, that IP-address is the listening address. + An 'ip'-option will be discarded if present.</item> + + <item>if <c>HostAddress</c> is <c>loopback</c>, the listening address + is <c>loopback</c> and an loopback address will be choosen by the underlying layers. + An 'ip'-option will be discarded if present.</item> + + <item>if <c>HostAddress</c> is <c>any</c> and no 'ip'-option is present, the listening address is + <c>any</c> and the socket will listen to all addresses</item> + + <item>if <c>HostAddress</c> is <c>any</c> and an 'ip'-option is present, the listening address is + set to the value of the 'ip'-option</item> + </list> + <p>Options:</p> <taglist> <tag><c><![CDATA[{inet, inet | inet6}]]></c></tag> diff --git a/lib/ssh/doc/src/ssh_app.xml b/lib/ssh/doc/src/ssh_app.xml index 5f710decc1..515b0639d5 100644 --- a/lib/ssh/doc/src/ssh_app.xml +++ b/lib/ssh/doc/src/ssh_app.xml @@ -109,7 +109,7 @@ </section> <section> <title>Host Keys</title> - <p>RSA and DSA host keys are supported and are + <p>RSA, DSA and ECDSA host keys are supported and are expected to be found in files named <c>ssh_host_rsa_key</c>, <c>ssh_host_dsa_key</c> and <c>ssh_host_ecdsa_key</c>. </p> @@ -160,7 +160,7 @@ <item>ecdsa-sha2-nistp384</item> <item>ecdsa-sha2-nistp521</item> <item>ssh-rsa</item> - <item>(ssh-dss, retired: can be enabled with the <c>preferred_algorithms</c> option)</item> + <item>ssh-dss</item> </list> </item> diff --git a/lib/ssh/src/ssh.erl b/lib/ssh/src/ssh.erl index 369a00ac40..3e80a04b70 100644 --- a/lib/ssh/src/ssh.erl +++ b/lib/ssh/src/ssh.erl @@ -26,6 +26,7 @@ -include("ssh_connect.hrl"). -include_lib("public_key/include/public_key.hrl"). -include_lib("kernel/include/file.hrl"). +-include_lib("kernel/include/inet.hrl"). -export([start/0, start/1, stop/0, connect/2, connect/3, connect/4, @@ -108,7 +109,7 @@ connect(Socket, UserOptions, Timeout) when is_port(Socket), case valid_socket_to_use(Socket, ?GET_OPT(transport,Options)) of ok -> {ok, {Host,_Port}} = inet:sockname(Socket), - Opts = ?PUT_INTERNAL_OPT([{user_pid,self()}, {host,fmt_host(Host)}], Options), + Opts = ?PUT_INTERNAL_OPT([{user_pid,self()}, {host,Host}], Options), ssh_connection_handler:start_connection(client, Socket, Opts, Timeout); {error,SockError} -> {error,SockError} @@ -120,7 +121,7 @@ connect(Host, Port, UserOptions) when is_integer(Port), is_list(UserOptions) -> connect(Host, Port, UserOptions, infinity). -connect(Host, Port, UserOptions, Timeout) when is_integer(Port), +connect(Host0, Port, UserOptions, Timeout) when is_integer(Port), Port>0, is_list(UserOptions) -> case ssh_options:handle_options(client, UserOptions) of @@ -130,6 +131,7 @@ connect(Host, Port, UserOptions, Timeout) when is_integer(Port), {_, Transport, _} = TransportOpts = ?GET_OPT(transport, Options), ConnectionTimeout = ?GET_OPT(connect_timeout, Options), SocketOpts = [{active,false} | ?GET_OPT(socket_options,Options)], + Host = mangle_connect_address(Host0, SocketOpts), try Transport:connect(Host, Port, SocketOpts, ConnectionTimeout) of {ok, Socket} -> Opts = ?PUT_INTERNAL_OPT([{user_pid,self()}, {host,Host}], Options), @@ -138,7 +140,6 @@ connect(Host, Port, UserOptions, Timeout) when is_integer(Port), {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 +184,88 @@ 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(IP, Port, ?GET_OPT(profile, Options), + ?PUT_INTERNAL_OPT({connected_socket, Socket}, Options), + fun(Opts, DefaultResult) -> + try ssh_acceptor:handle_established_connection( + IP, Port, 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, + Host0 == any ; Host0 == loopback ; is_tuple(Host0) -> + 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(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(_, _, _) -> + {error, badarg}. -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()}] ). @@ -200,11 +273,17 @@ daemon(Host0, Port, UserOptions0) -> daemon_info(Pid) -> case catch ssh_system_sup:acceptor_supervisor(Pid) of AsupPid when is_pid(AsupPid) -> - [{ListenAddr,Port,Profile}] = - [{LA,Prt,Prf} || {{ssh_acceptor_sup,LA,Prt,Prf}, - _WorkerPid,worker,[ssh_acceptor]} <- supervisor:which_children(AsupPid)], + [{IP,Port,Profile}] = + [{IP,Prt,Prf} + || {{ssh_acceptor_sup,Hst,Prt,Prf},_Pid,worker,[ssh_acceptor]} + <- supervisor:which_children(AsupPid), + IP <- [case inet:parse_strict_address(Hst) of + {ok,IP} -> IP; + _ -> Hst + end] + ], {ok, [{port,Port}, - {listen_address,ListenAddr}, + {ip,IP}, {profile,Profile} ]}; _ -> @@ -222,8 +301,14 @@ stop_listener(SysSup) -> ssh_system_sup:stop_listener(SysSup). stop_listener(Address, Port) -> stop_listener(Address, Port, ?DEFAULT_PROFILE). +stop_listener(any, Port, Profile) -> + map_ip(fun(IP) -> + ssh_system_sup:stop_listener(IP, Port, Profile) + end, [{0,0,0,0},{0,0,0,0,0,0,0,0}]); stop_listener(Address, Port, Profile) -> - ssh_system_sup:stop_listener(Address, Port, Profile). + map_ip(fun(IP) -> + ssh_system_sup:stop_listener(IP, Port, Profile) + end, {address,Address}). %%-------------------------------------------------------------------- -spec stop_daemon(daemon_ref()) -> ok. @@ -236,9 +321,15 @@ stop_listener(Address, Port, Profile) -> stop_daemon(SysSup) -> ssh_system_sup:stop_system(SysSup). stop_daemon(Address, Port) -> - ssh_system_sup:stop_system(Address, Port, ?DEFAULT_PROFILE). + stop_daemon(Address, Port, ?DEFAULT_PROFILE). +stop_daemon(any, Port, Profile) -> + map_ip(fun(IP) -> + ssh_system_sup:stop_system(IP, Port, Profile) + end, [{0,0,0,0},{0,0,0,0,0,0,0,0}]); stop_daemon(Address, Port, Profile) -> - ssh_system_sup:stop_system(Address, Port, Profile). + map_ip(fun(IP) -> + ssh_system_sup:stop_system(IP, Port, Profile) + end, {address,Address}). %%-------------------------------------------------------------------- -spec shell(inet:socket() | string()) -> _. @@ -292,35 +383,34 @@ 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) +%% The handle_daemon_args/2 function basically only sets the ip-option in Opts +%% so that it is correctly set when opening the listening socket. + +handle_daemon_args(any, Opts) -> + case proplists:get_value(ip, Opts) of + undefined -> {any, Opts}; + IP -> {IP, Opts} + end; + +handle_daemon_args(IPaddr, Opts) when is_tuple(IPaddr) ; IPaddr == loopback -> + case proplists:get_value(ip, Opts) of + undefined -> {IPaddr, [{ip,IPaddr}|Opts]}; + IPaddr -> {IPaddr, Opts}; + IP -> {IPaddr, [{ip,IPaddr}|Opts--[{ip,IP}]]} %% Backward compatibility end. %%%---------------------------------------------------------------- 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 +424,62 @@ 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; +open_listen_socket(_Host0, Port0, Options0) -> + {ok,LSock} = + case ?GET_SOCKET_OPT(fd, Options0) of + undefined -> + ssh_acceptor:listen(Port0, Options0); + Fd when is_integer(Fd) -> + %% Do gen_tcp:listen with the option {fd,Fd}: + ssh_acceptor:listen(0, Options0) + end, + {ok,{LHost,LPort}} = inet:sockname(LSock), + {{LHost,LPort}, LSock}. -start_daemon(Host, Port, Options) -> +%%%---------------------------------------------------------------- +finalize_start(Host, Port, Profile, Options0, F) -> try - do_start_daemon(Host, Port, Options) + sshd_sup:start_child(Host, Port, Profile, Options0) + of + {error, {already_started, _}} -> + {error, eaddrinuse}; + {error, Error} -> + {error, Error}; + Result = {ok,_} -> + F(Options0, Result) catch - throw:bad_fd -> {error,bad_fd}; - throw:bad_socket -> {error,bad_socket}; - _C:_E -> {error,{cannot_start_daemon,_C,_E}} + exit:{noproc, _} -> + {error, ssh_not_started} end. +%%%---------------------------------------------------------------- +map_ip(Fun, {address,IP}) when is_tuple(IP) -> + Fun(IP); +map_ip(Fun, {address,Address}) -> + IPs = try {ok,#hostent{h_addr_list=IP0s}} = inet:gethostbyname(Address), + IP0s + catch + _:_ -> [] + end, + map_ip(Fun, IPs); +map_ip(Fun, IPs) -> + lists:map(Fun, IPs). -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 - 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, - Options = ?PUT_INTERNAL_OPT([{address, Host}, - {port, Port}, - {role, server}], Options1), - Profile = ?GET_OPT(profile, Options0), - case ssh_system_sup:system_supervisor(Host, Port, Profile) of - undefined -> - try sshd_sup:start_child(Options) of - {error, {already_started, _}} -> - {error, eaddrinuse}; - Result = {ok,_} -> - sync_request_control(WaitRequestControl, Options), - 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, Options) of - {error, {already_started, _}} -> - {error, eaddrinuse}; - {ok, _} -> - sync_request_control(WaitRequestControl, Options), - {ok, Sup}; - Other -> - Other - 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 +%%%---------------------------------------------------------------- +mangle_connect_address(A, SockOpts) -> + mangle_connect_address1(A, proplists:get_value(inet6,SockOpts,false)). + +loopback(true) -> {0,0,0,0,0,0,0,1}; +loopback(false) -> {127,0,0,1}. + +mangle_connect_address1( loopback, V6flg) -> loopback(V6flg); +mangle_connect_address1( any, V6flg) -> loopback(V6flg); +mangle_connect_address1({0,0,0,0}, _) -> loopback(false); +mangle_connect_address1({0,0,0,0,0,0,0,0}, _) -> loopback(true); +mangle_connect_address1( IP, _) when is_tuple(IP) -> IP; +mangle_connect_address1(A, _) -> + case catch inet:parse_address(A) of + {ok, {0,0,0,0}} -> loopback(false); + {ok, {0,0,0,0,0,0,0,0}} -> loopback(true); + _ -> A 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)], ":")). diff --git a/lib/ssh/src/ssh.hrl b/lib/ssh/src/ssh.hrl index c1ba58ed40..cf2a359e6c 100644 --- a/lib/ssh/src/ssh.hrl +++ b/lib/ssh/src/ssh.hrl @@ -38,7 +38,6 @@ -define(MAX_RND_PADDING_LEN, 15). -define(SUPPORTED_AUTH_METHODS, "publickey,keyboard-interactive,password"). --define(SUPPORTED_USER_KEYS, ['ssh-rsa','ssh-dss','ecdsa-sha2-nistp256','ecdsa-sha2-nistp384','ecdsa-sha2-nistp521']). -define(FALSE, 0). -define(TRUE, 1). @@ -75,9 +74,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 +91,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 +115,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_address(),inet:port_number()}}, %% string version of peer address c_vsn, %% client version {Major,Minor} s_vsn, %% server version {Major,Minor} @@ -125,6 +144,9 @@ c_keyinit, %% binary payload of kexinit packet s_keyinit, %% binary payload of kexinit packet + send_ext_info, %% May send ext-info to peer + recv_ext_info, %% Expect ext-info from peer + algorithms, %% #alg{} kex, %% key exchange algorithm @@ -178,6 +200,7 @@ userauth_quiet_mode, % boolean() userauth_methods, % list( string() ) eg ["keyboard-interactive", "password"] userauth_supported_methods, % string() eg "keyboard-interactive,password" + userauth_pubkeys, kb_tries_left = 0, % integer(), num tries left for "keyboard-interactive" userauth_preference, available_host_keys, @@ -196,7 +219,9 @@ compress, decompress, c_lng, - s_lng + s_lng, + send_ext_info, + recv_ext_info }). -record(ssh_key, diff --git a/lib/ssh/src/ssh_acceptor.erl b/lib/ssh/src/ssh_acceptor.erl index 42be18f2ad..f7fbd7ccad 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), @@ -135,7 +129,8 @@ handle_connection(Callback, Address, Port, Options, Socket) -> MaxSessions = ?GET_OPT(max_sessions, Options), case number_of_connections(SystemSup) < MaxSessions of true -> - {ok, SubSysSup} = ssh_system_sup:start_subsystem(SystemSup, Options), + {ok, SubSysSup} = + ssh_system_sup:start_subsystem(SystemSup, server, Address, Port, Profile, Options), ConnectionSup = ssh_subsystem_sup:connection_supervisor(SubSysSup), NegTimeout = ?GET_OPT(negotiation_timeout, Options), ssh_connection_handler:start_connection(server, Socket, @@ -159,7 +154,7 @@ handle_connection(Callback, Address, Port, Options, Socket) -> {error,max_sessions} end. - +%%%---------------------------------------------------------------- handle_error(timeout) -> ok; @@ -186,10 +181,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_acceptor_sup.erl b/lib/ssh/src/ssh_acceptor_sup.erl index 77f7826918..26defcfdbd 100644 --- a/lib/ssh/src/ssh_acceptor_sup.erl +++ b/lib/ssh/src/ssh_acceptor_sup.erl @@ -29,7 +29,7 @@ -include("ssh.hrl"). --export([start_link/1, start_child/2, stop_child/4]). +-export([start_link/4, start_child/5, stop_child/4]). %% Supervisor callback -export([init/1]). @@ -41,19 +41,19 @@ %%%========================================================================= %%% API %%%========================================================================= -start_link(Servers) -> - supervisor:start_link(?MODULE, [Servers]). +start_link(Address, Port, Profile, Options) -> + supervisor:start_link(?MODULE, [Address, Port, Profile, Options]). -start_child(AccSup, Options) -> - Spec = child_spec(Options), +start_child(AccSup, Address, Port, Profile, Options) -> + Spec = child_spec(Address, Port, Profile, Options), case supervisor:start_child(AccSup, Spec) of {error, already_present} -> - Address = ?GET_INTERNAL_OPT(address, Options), - Port = ?GET_INTERNAL_OPT(port, Options), - Profile = ?GET_OPT(profile, Options), + %% Is this ever called? stop_child(AccSup, Address, Port, Profile), supervisor:start_child(AccSup, Spec); Reply -> + %% Reply = {ok,SystemSupPid} when the user calls ssh:daemon + %% after having called ssh:stop_listening Reply end. @@ -69,34 +69,29 @@ stop_child(AccSup, Address, Port, Profile) -> %%%========================================================================= %%% Supervisor callback %%%========================================================================= -init([Options]) -> - RestartStrategy = one_for_one, - MaxR = 10, - MaxT = 3600, - Children = [child_spec(Options)], - {ok, {{RestartStrategy, MaxR, MaxT}, Children}}. +init([Address, Port, Profile, Options]) -> + %% Initial start of ssh_acceptor_sup for this port or new start after + %% ssh:stop_daemon + SupFlags = #{strategy => one_for_one, + intensity => 10, + period => 3600 + }, + ChildSpecs = [child_spec(Address, Port, Profile, Options)], + {ok, {SupFlags,ChildSpecs}}. %%%========================================================================= %%% Internal functions %%%========================================================================= -child_spec(Options) -> - Address = ?GET_INTERNAL_OPT(address, Options), - Port = ?GET_INTERNAL_OPT(port, Options), +child_spec(Address, Port, Profile, Options) -> Timeout = ?GET_INTERNAL_OPT(timeout, Options, ?DEFAULT_TIMEOUT), - Profile = ?GET_OPT(profile, Options), - Name = id(Address, Port, Profile), - StartFunc = {ssh_acceptor, start_link, [Port, Address, Options, Timeout]}, - Restart = transient, - Shutdown = brutal_kill, - Modules = [ssh_acceptor], - Type = worker, - {Name, StartFunc, Restart, Shutdown, Type, Modules}. + #{id => id(Address, Port, Profile), + start => {ssh_acceptor, start_link, [Port, Address, Options, Timeout]}, + restart => transient, + shutdown => 5500, %brutal_kill, + type => worker, + modules => [ssh_acceptor] + }. id(Address, Port, Profile) -> - case is_list(Address) of - true -> - {ssh_acceptor_sup, any, Port, Profile}; - false -> - {ssh_acceptor_sup, Address, Port, Profile} - end. + {ssh_acceptor_sup, Address, Port, Profile}. diff --git a/lib/ssh/src/ssh_auth.erl b/lib/ssh/src/ssh_auth.erl index 88c8144063..9eb11a53dc 100644 --- a/lib/ssh/src/ssh_auth.erl +++ b/lib/ssh/src/ssh_auth.erl @@ -136,34 +136,40 @@ keyboard_interactive_msg([#ssh{user = User, Ssh) end. -publickey_msg([Alg, #ssh{user = User, +publickey_msg([SigAlg, #ssh{user = User, session_id = SessionId, service = Service, opts = Opts} = Ssh]) -> - Hash = ssh_transport:sha(Alg), + Hash = ssh_transport:sha(SigAlg), + KeyAlg = key_alg(SigAlg), {KeyCb,KeyCbOpts} = ?GET_OPT(key_cb, Opts), UserOpts = ?GET_OPT(user_options, Opts), - case KeyCb:user_key(Alg, [{key_cb_private,KeyCbOpts}|UserOpts]) of + case KeyCb:user_key(KeyAlg, [{key_cb_private,KeyCbOpts}|UserOpts]) of {ok, PrivKey} -> - StrAlgo = atom_to_list(Alg), - case encode_public_key(StrAlgo, ssh_transport:extract_public_key(PrivKey)) of - not_ok -> - {not_ok, Ssh}; + SigAlgStr = atom_to_list(SigAlg), + try + Key = ssh_transport:extract_public_key(PrivKey), + public_key:ssh_encode(Key, ssh2_pubkey) + of PubKeyBlob -> - SigData = build_sig_data(SessionId, - User, Service, PubKeyBlob, StrAlgo), + SigData = build_sig_data(SessionId, User, Service, + PubKeyBlob, SigAlgStr), Sig = ssh_transport:sign(SigData, Hash, PrivKey), - SigBlob = list_to_binary([?string(StrAlgo), ?binary(Sig)]), + SigBlob = list_to_binary([?string(SigAlgStr), + ?binary(Sig)]), ssh_transport:ssh_packet( #ssh_msg_userauth_request{user = User, service = Service, method = "publickey", data = [?TRUE, - ?string(StrAlgo), + ?string(SigAlgStr), ?binary(PubKeyBlob), ?binary(SigBlob)]}, Ssh) - end; + catch + _:_ -> + {not_ok, Ssh} + end; _Error -> {not_ok, Ssh} end. @@ -175,6 +181,7 @@ service_request_msg(Ssh) -> %%%---------------------------------------------------------------- init_userauth_request_msg(#ssh{opts = Opts} = Ssh) -> + %% Client side case ?GET_OPT(user, Opts) of undefined -> ErrStr = "Could not determine the users name", @@ -183,25 +190,16 @@ init_userauth_request_msg(#ssh{opts = Opts} = Ssh) -> description = ErrStr}); User -> - Msg = #ssh_msg_userauth_request{user = User, - service = "ssh-connection", - method = "none", - data = <<>>}, - Algs0 = ?GET_OPT(pref_public_key_algs, Opts), - %% The following line is not strictly correct. The call returns the - %% supported HOST key types while we are interested in USER keys. However, - %% they "happens" to be the same (for now). This could change.... - %% There is no danger as long as the set of user keys is a subset of the set - %% of host keys. - CryptoSupported = ssh_transport:supported_algorithms(public_key), - Algs = [A || A <- Algs0, - lists:member(A, CryptoSupported)], - - Prefs = method_preference(Algs), - ssh_transport:ssh_packet(Msg, Ssh#ssh{user = User, - userauth_preference = Prefs, - userauth_methods = none, - service = "ssh-connection"}) + ssh_transport:ssh_packet( + #ssh_msg_userauth_request{user = User, + service = "ssh-connection", + method = "none", + data = <<>>}, + Ssh#ssh{user = User, + userauth_preference = method_preference(Ssh#ssh.userauth_pubkeys), + userauth_methods = none, + service = "ssh-connection"} + ) end. %%%---------------------------------------------------------------- @@ -272,8 +270,7 @@ handle_userauth_request(#ssh_msg_userauth_request{user = User, #ssh{opts = Opts, userauth_supported_methods = Methods} = Ssh) -> - case pre_verify_sig(User, binary_to_list(BAlg), - KeyBlob, Opts) of + case pre_verify_sig(User, KeyBlob, Opts) of true -> {not_authorized, {User, undefined}, ssh_transport:ssh_packet( @@ -299,8 +296,7 @@ handle_userauth_request(#ssh_msg_userauth_request{user = User, userauth_supported_methods = Methods} = Ssh) -> case verify_sig(SessionId, User, "ssh-connection", - binary_to_list(BAlg), - KeyBlob, SigWLen, Opts) of + BAlg, KeyBlob, SigWLen, Opts) of true -> {authorized, User, ssh_transport:ssh_packet( @@ -453,14 +449,14 @@ handle_userauth_info_response(#ssh_msg_userauth_info_response{}, %%-------------------------------------------------------------------- %%% Internal functions %%-------------------------------------------------------------------- -method_preference(Algs) -> - lists:foldr(fun(A, Acc) -> - [{"publickey", ?MODULE, publickey_msg, [A]} | Acc] - end, - [{"password", ?MODULE, password_msg, []}, - {"keyboard-interactive", ?MODULE, keyboard_interactive_msg, []} - ], - Algs). +method_preference(SigKeyAlgs) -> + %% PubKeyAlgs: List of user (client) public key algorithms to try to use. + %% All of the acceptable algorithms is the default values. + PubKeyDefs = [{"publickey", ?MODULE, publickey_msg, [A]} || A <- SigKeyAlgs], + NonPKmethods = [{"password", ?MODULE, password_msg, []}, + {"keyboard-interactive", ?MODULE, keyboard_interactive_msg, []} + ], + PubKeyDefs ++ NonPKmethods. check_password(User, Password, Opts, Ssh) -> case ?GET_OPT(pwdfun, Opts) of @@ -499,9 +495,9 @@ get_password_option(Opts, User) -> false -> ?GET_OPT(password, Opts) end. -pre_verify_sig(User, Alg, KeyBlob, Opts) -> +pre_verify_sig(User, KeyBlob, Opts) -> try - {ok, Key} = decode_public_key_v2(KeyBlob, Alg), + Key = public_key:ssh_decode(KeyBlob, ssh2_pubkey), % or exception {KeyCb,KeyCbOpts} = ?GET_OPT(key_cb, Opts), UserOpts = ?GET_OPT(user_options, Opts), KeyCb:is_auth_key(Key, User, [{key_cb_private,KeyCbOpts}|UserOpts]) @@ -510,23 +506,18 @@ pre_verify_sig(User, Alg, KeyBlob, Opts) -> false end. -verify_sig(SessionId, User, Service, Alg, KeyBlob, SigWLen, Opts) -> +verify_sig(SessionId, User, Service, AlgBin, KeyBlob, SigWLen, Opts) -> try - {ok, Key} = decode_public_key_v2(KeyBlob, Alg), - + Alg = binary_to_list(AlgBin), {KeyCb,KeyCbOpts} = ?GET_OPT(key_cb, Opts), UserOpts = ?GET_OPT(user_options, Opts), - case KeyCb:is_auth_key(Key, User, [{key_cb_private,KeyCbOpts}|UserOpts]) of - true -> - PlainText = build_sig_data(SessionId, User, - Service, KeyBlob, Alg), - <<?UINT32(AlgSigLen), AlgSig:AlgSigLen/binary>> = SigWLen, - <<?UINT32(AlgLen), _Alg:AlgLen/binary, - ?UINT32(SigLen), Sig:SigLen/binary>> = AlgSig, - ssh_transport:verify(PlainText, ssh_transport:sha(list_to_atom(Alg)), Sig, Key); - false -> - false - end + Key = public_key:ssh_decode(KeyBlob, ssh2_pubkey), % or exception + true = KeyCb:is_auth_key(Key, User, [{key_cb_private,KeyCbOpts}|UserOpts]), + PlainText = build_sig_data(SessionId, User, Service, KeyBlob, Alg), + <<?UINT32(AlgSigLen), AlgSig:AlgSigLen/binary>> = SigWLen, + <<?UINT32(AlgLen), _Alg:AlgLen/binary, + ?UINT32(SigLen), Sig:SigLen/binary>> = AlgSig, + ssh_transport:verify(PlainText, ssh_transport:sha(Alg), Sig, Key) catch _:_ -> false @@ -598,18 +589,7 @@ keyboard_interact_fun(KbdInteractFun, Name, Instr, PromptInfos, NumPrompts) -> language = "en"}}) end. -decode_public_key_v2(Bin, _Type) -> - try - public_key:ssh_decode(Bin, ssh2_pubkey) - of - Key -> {ok, Key} - catch - _:_ -> {error, bad_format} - end. -encode_public_key(_Alg, Key) -> - try - public_key:ssh_encode(Key, ssh2_pubkey) - catch - _:_ -> not_ok - end. +key_alg('rsa-sha2-256') -> 'ssh-rsa'; +key_alg('rsa-sha2-512') -> 'ssh-rsa'; +key_alg(Alg) -> Alg. diff --git a/lib/ssh/src/ssh_connection_handler.erl b/lib/ssh/src/ssh_connection_handler.erl index 15a05a1b85..74e14a233f 100644 --- a/lib/ssh/src/ssh_connection_handler.erl +++ b/lib/ssh/src/ssh_connection_handler.erl @@ -60,7 +60,7 @@ ]). %%% Behaviour callbacks --export([callback_mode/0, handle_event/4, terminate/3, +-export([init/1, callback_mode/0, handle_event/4, terminate/3, format_status/2, code_change/4]). %%% Exports not intended to be used :). They are used for spawning and tests @@ -80,7 +80,11 @@ ) -> {ok, pid()}. %% . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . start_link(Role, Socket, Options) -> - {ok, proc_lib:spawn_link(?MODULE, init_connection_handler, [Role, Socket, Options])}. + {ok, proc_lib:spawn_opt(?MODULE, + init_connection_handler, + [Role, Socket, Options], + [link, {message_queue_data,off_heap}] + )}. %%-------------------------------------------------------------------- @@ -333,8 +337,7 @@ renegotiate_data(ConnectionHandler) -> transport_protocol :: atom(), % ex: tcp transport_cb :: atom(), % ex: gen_tcp transport_close_tag :: atom(), % ex: tcp_closed - ssh_params :: #ssh{} - | undefined, + ssh_params :: #ssh{}, socket :: inet:socket(), decrypted_data_buffer = <<>> :: binary(), encrypted_data_buffer = <<>> :: binary(), @@ -358,77 +361,78 @@ renegotiate_data(ConnectionHandler) -> ) -> no_return(). %% . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . init_connection_handler(Role, Socket, Opts) -> - process_flag(trap_exit, true), - S0 = init_process_state(Role, Socket, Opts), - try - {Protocol, Callback, CloseTag} = ?GET_OPT(transport, Opts), - S0#data{ssh_params = init_ssh_record(Role, Socket, Opts), - transport_protocol = Protocol, - transport_cb = Callback, - transport_close_tag = CloseTag - } - of - S -> - gen_statem:enter_loop(?MODULE, - [], %%[{debug,[trace,log,statistics,debug]} || Role==server], - {hello,Role}, - S) - catch - _:Error -> - gen_statem:enter_loop(?MODULE, - [], - {init_error,Error}, - S0) - end. - - -init_process_state(Role, Socket, Opts) -> - D = #data{connection_state = - C = #connection{channel_cache = ssh_channel:cache_create(), - channel_id_seed = 0, - port_bindings = [], - requests = [], - options = Opts}, - starter = ?GET_INTERNAL_OPT(user_pid, Opts), - socket = Socket, - opts = Opts - }, - case Role of - client -> - %% Start the renegotiation timers - timer:apply_after(?REKEY_TIMOUT, gen_statem, cast, [self(), renegotiate]), - timer:apply_after(?REKEY_DATA_TIMOUT, gen_statem, cast, [self(), data_size]), - cache_init_idle_timer(D); - server -> - cache_init_idle_timer( - D#data{connection_state = init_connection(Role, C, Opts)} - ) + case init([Role, Socket, Opts]) of + {ok, StartState, D} -> + process_flag(trap_exit, true), + gen_statem:enter_loop(?MODULE, + [], %%[{debug,[trace,log,statistics,debug]} ], %% [] + StartState, + D); + + {stop, enotconn} -> + %% Handles the abnormal sequence: + %% SYN-> + %% <-SYNACK + %% ACK-> + %% RST-> + exit({shutdown, "TCP connection to server was prematurely closed by the client"}); + + {stop, OtherError} -> + exit({shutdown, {init,OtherError}}) end. -init_connection(server, C = #connection{}, Opts) -> - Sups = ?GET_INTERNAL_OPT(supervisors, Opts), - SystemSup = proplists:get_value(system_sup, Sups), - SubSystemSup = proplists:get_value(subsystem_sup, Sups), - ConnectionSup = proplists:get_value(connection_sup, Sups), +init([Role,Socket,Opts]) -> + case inet:peername(Socket) of + {ok, PeerAddr} -> + {Protocol, Callback, CloseTag} = ?GET_OPT(transport, Opts), + C = #connection{channel_cache = ssh_channel:cache_create(), + channel_id_seed = 0, + port_bindings = [], + requests = [], + options = Opts}, + D0 = #data{starter = ?GET_INTERNAL_OPT(user_pid, Opts), + connection_state = C, + socket = Socket, + transport_protocol = Protocol, + transport_cb = Callback, + transport_close_tag = CloseTag, + ssh_params = init_ssh_record(Role, Socket, PeerAddr, Opts), + opts = Opts + }, + D = case Role of + client -> + %% Start the renegotiation timers + timer:apply_after(?REKEY_TIMOUT, gen_statem, cast, [self(), renegotiate]), + timer:apply_after(?REKEY_DATA_TIMOUT, gen_statem, cast, [self(), data_size]), + cache_init_idle_timer(D0); + server -> + Sups = ?GET_INTERNAL_OPT(supervisors, Opts), + cache_init_idle_timer( + D0#data{connection_state = + C#connection{cli_spec = ?GET_OPT(ssh_cli, Opts, {ssh_cli,[?GET_OPT(shell, Opts)]}), + exec = ?GET_OPT(exec, Opts), + system_supervisor = proplists:get_value(system_sup, Sups), + sub_system_supervisor = proplists:get_value(subsystem_sup, Sups), + connection_supervisor = proplists:get_value(connection_sup, Sups) + }}) + end, + {ok, {hello,Role}, D}; + + {error,Error} -> + {stop, Error} + end. - 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, - system_supervisor = SystemSup, - sub_system_supervisor = SubSystemSup, - connection_supervisor = ConnectionSup - }. init_ssh_record(Role, Socket, Opts) -> - {ok, PeerAddr} = inet:peername(Socket), + %% Export of this internal function is + %% intended for low-level protocol test suites + {ok,PeerAddr} = inet:peername(Socket), + init_ssh_record(Role, Socket, PeerAddr, Opts). + +init_ssh_record(Role, _Socket, PeerAddr, Opts) -> KeyCb = ?GET_OPT(key_cb, Opts), AuthMethods = case Role of @@ -446,13 +450,21 @@ init_ssh_record(Role, Socket, Opts) -> {Vsn, Version} = ssh_transport:versions(Role, Opts), case Role of client -> - PeerName = ?GET_INTERNAL_OPT(host, Opts), + PeerName = case ?GET_INTERNAL_OPT(host, Opts) of + PeerIP when is_tuple(PeerIP) -> + inet_parse:ntoa(PeerIP); + PeerName0 when is_atom(PeerName0) -> + atom_to_list(PeerName0); + PeerName0 when is_list(PeerName0) -> + PeerName0 + end, S0#ssh{c_vsn = Vsn, c_version = Version, io_cb = case ?GET_OPT(user_interaction, Opts) of true -> ssh_io; false -> ssh_no_io end, + userauth_pubkeys = ?GET_OPT(pref_public_key_algs, Opts), userauth_quiet_mode = ?GET_OPT(quiet_mode, Opts), peer = {PeerName, PeerAddr} }; @@ -478,49 +490,48 @@ init_ssh_record(Role, Socket, Opts) -> -type renegotiate_flag() :: init | renegotiate. -type state_name() :: - {init_error,any()} - | {hello, role()} - | {kexinit, role(), renegotiate_flag()} - | {key_exchange, role(), renegotiate_flag()} - | {key_exchange_dh_gex_init, server, renegotiate_flag()} + {hello, role() } + | {kexinit, role(), renegotiate_flag()} + | {key_exchange, role(), renegotiate_flag()} + | {key_exchange_dh_gex_init, server, renegotiate_flag()} | {key_exchange_dh_gex_reply, client, renegotiate_flag()} - | {new_keys, role()} - | {service_request, role()} - | {userauth, role()} - | {userauth_keyboard_interactive, role()} - | {connected, role()} + | {new_keys, role(), renegotiate_flag()} + | {ext_info, role(), renegotiate_flag()} + | {service_request, role() } + | {userauth, role() } + | {userauth_keyboard_interactive, role() } + | {userauth_keyboard_interactive_extra, server } + | {userauth_keyboard_interactive_info_response, client } + | {connected, role() } . --type handle_event_result() :: gen_statem:handle_event_result(). +%% The state names must fulfill some rules regarding +%% where the role() and the renegotiate_flag() is placed: + +-spec role(state_name()) -> role(). +role({_,Role}) -> Role; +role({_,Role,_}) -> Role. + +-spec renegotiation(state_name()) -> boolean(). +renegotiation({_,_,ReNeg}) -> ReNeg == renegotiation; +renegotiation(_) -> false. + + +-define(CONNECTED(StateName), + (element(1,StateName) == connected orelse + element(1,StateName) == ext_info ) ). -spec handle_event(gen_statem:event_type(), event_content(), state_name(), #data{} - ) -> handle_event_result(). + ) -> gen_statem:event_handler_result(state_name()) . %% . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . -%%% ######## Error in the initialisation #### - callback_mode() -> handle_event_function. -handle_event(_, _Event, {init_error,Error}, _) -> - case Error of - {badmatch,{error,enotconn}} -> - %% Handles the abnormal sequence: - %% SYN-> - %% <-SYNACK - %% ACK-> - %% RST-> - {stop, {shutdown,"TCP connenction to server was prematurely closed by the client"}}; - - OtherError -> - {stop, {shutdown,{init,OtherError}}} - end; - - %%% ######## {hello, client|server} #### %% The very first event that is sent when the we are set as controlling process of Socket handle_event(_, socket_control, {hello,_}, D) -> @@ -598,13 +609,17 @@ handle_event(_, {#ssh_msg_kexinit{}=Kex, Payload}, {kexinit,Role,ReNeg}, handle_event(_, #ssh_msg_kexdh_init{} = Msg, {key_exchange,server,ReNeg}, D) -> {ok, KexdhReply, Ssh1} = ssh_transport:handle_kexdh_init(Msg, D#data.ssh_params), send_bytes(KexdhReply, D), - {ok, NewKeys, Ssh} = ssh_transport:new_keys_message(Ssh1), + {ok, NewKeys, Ssh2} = ssh_transport:new_keys_message(Ssh1), send_bytes(NewKeys, D), + {ok, ExtInfo, Ssh} = ssh_transport:ext_info_message(Ssh2), + send_bytes(ExtInfo, D), {next_state, {new_keys,server,ReNeg}, D#data{ssh_params=Ssh}}; handle_event(_, #ssh_msg_kexdh_reply{} = Msg, {key_exchange,client,ReNeg}, D) -> - {ok, NewKeys, Ssh} = ssh_transport:handle_kexdh_reply(Msg, D#data.ssh_params), + {ok, NewKeys, Ssh1} = ssh_transport:handle_kexdh_reply(Msg, D#data.ssh_params), send_bytes(NewKeys, D), + {ok, ExtInfo, Ssh} = ssh_transport:ext_info_message(Ssh1), + send_bytes(ExtInfo, D), {next_state, {new_keys,client,ReNeg}, D#data{ssh_params=Ssh}}; %%%---- diffie-hellman group exchange @@ -629,13 +644,17 @@ handle_event(_, #ssh_msg_kex_dh_gex_group{} = Msg, {key_exchange,client,ReNeg}, handle_event(_, #ssh_msg_kex_ecdh_init{} = Msg, {key_exchange,server,ReNeg}, D) -> {ok, KexEcdhReply, Ssh1} = ssh_transport:handle_kex_ecdh_init(Msg, D#data.ssh_params), send_bytes(KexEcdhReply, D), - {ok, NewKeys, Ssh} = ssh_transport:new_keys_message(Ssh1), + {ok, NewKeys, Ssh2} = ssh_transport:new_keys_message(Ssh1), send_bytes(NewKeys, D), + {ok, ExtInfo, Ssh} = ssh_transport:ext_info_message(Ssh2), + send_bytes(ExtInfo, D), {next_state, {new_keys,server,ReNeg}, D#data{ssh_params=Ssh}}; handle_event(_, #ssh_msg_kex_ecdh_reply{} = Msg, {key_exchange,client,ReNeg}, D) -> - {ok, NewKeys, Ssh} = ssh_transport:handle_kex_ecdh_reply(Msg, D#data.ssh_params), + {ok, NewKeys, Ssh1} = ssh_transport:handle_kex_ecdh_reply(Msg, D#data.ssh_params), send_bytes(NewKeys, D), + {ok, ExtInfo, Ssh} = ssh_transport:ext_info_message(Ssh1), + send_bytes(ExtInfo, D), {next_state, {new_keys,client,ReNeg}, D#data{ssh_params=Ssh}}; @@ -644,8 +663,10 @@ handle_event(_, #ssh_msg_kex_ecdh_reply{} = Msg, {key_exchange,client,ReNeg}, D) handle_event(_, #ssh_msg_kex_dh_gex_init{} = Msg, {key_exchange_dh_gex_init,server,ReNeg}, D) -> {ok, KexGexReply, Ssh1} = ssh_transport:handle_kex_dh_gex_init(Msg, D#data.ssh_params), send_bytes(KexGexReply, D), - {ok, NewKeys, Ssh} = ssh_transport:new_keys_message(Ssh1), + {ok, NewKeys, Ssh2} = ssh_transport:new_keys_message(Ssh1), send_bytes(NewKeys, D), + {ok, ExtInfo, Ssh} = ssh_transport:ext_info_message(Ssh2), + send_bytes(ExtInfo, D), {next_state, {new_keys,server,ReNeg}, D#data{ssh_params=Ssh}}; @@ -654,30 +675,60 @@ handle_event(_, #ssh_msg_kex_dh_gex_init{} = Msg, {key_exchange_dh_gex_init,serv handle_event(_, #ssh_msg_kex_dh_gex_reply{} = Msg, {key_exchange_dh_gex_reply,client,ReNeg}, D) -> {ok, NewKeys, Ssh1} = ssh_transport:handle_kex_dh_gex_reply(Msg, D#data.ssh_params), send_bytes(NewKeys, D), - {next_state, {new_keys,client,ReNeg}, D#data{ssh_params=Ssh1}}; + {ok, ExtInfo, Ssh} = ssh_transport:ext_info_message(Ssh1), + send_bytes(ExtInfo, D), + {next_state, {new_keys,client,ReNeg}, D#data{ssh_params=Ssh}}; %%% ######## {new_keys, client|server} #### %% First key exchange round: -handle_event(_, #ssh_msg_newkeys{} = Msg, {new_keys,Role,init}, D) -> +handle_event(_, #ssh_msg_newkeys{} = Msg, {new_keys,client,init}, D) -> {ok, Ssh1} = ssh_transport:handle_new_keys(Msg, D#data.ssh_params), - Ssh = case Role of - client -> - {MsgReq, Ssh2} = ssh_auth:service_request_msg(Ssh1), - send_bytes(MsgReq, D), - Ssh2; - server -> - Ssh1 - end, - {next_state, {service_request,Role}, D#data{ssh_params=Ssh}}; + %% {ok, ExtInfo, Ssh2} = ssh_transport:ext_info_message(Ssh1), + %% send_bytes(ExtInfo, D), + {MsgReq, Ssh} = ssh_auth:service_request_msg(Ssh1), + send_bytes(MsgReq, D), + {next_state, {ext_info,client,init}, D#data{ssh_params=Ssh}}; + +handle_event(_, #ssh_msg_newkeys{} = Msg, {new_keys,server,init}, D) -> + {ok, Ssh} = ssh_transport:handle_new_keys(Msg, D#data.ssh_params), + %% {ok, ExtInfo, Ssh} = ssh_transport:ext_info_message(Ssh1), + %% send_bytes(ExtInfo, D), + {next_state, {ext_info,server,init}, D#data{ssh_params=Ssh}}; %% Subsequent key exchange rounds (renegotiation): handle_event(_, #ssh_msg_newkeys{} = Msg, {new_keys,Role,renegotiate}, D) -> {ok, Ssh} = ssh_transport:handle_new_keys(Msg, D#data.ssh_params), - {next_state, {connected,Role}, D#data{ssh_params=Ssh}}; + %% {ok, ExtInfo, Ssh} = ssh_transport:ext_info_message(Ssh1), + %% send_bytes(ExtInfo, D), + {next_state, {ext_info,Role,renegotiate}, D#data{ssh_params=Ssh}}; + -%%% ######## {service_request, client|server} +%%% ######## {ext_info, client|server, init|renegotiate} #### + +handle_event(_, #ssh_msg_ext_info{}=Msg, {ext_info,Role,init}, D0) -> + D = handle_ssh_msg_ext_info(Msg, D0), + {next_state, {service_request,Role}, D}; + +handle_event(_, #ssh_msg_ext_info{}=Msg, {ext_info,Role,renegotiate}, D0) -> + D = handle_ssh_msg_ext_info(Msg, D0), + {next_state, {connected,Role}, D}; + +handle_event(_, #ssh_msg_newkeys{}=Msg, {ext_info,_Role,renegotiate}, D) -> + {ok, Ssh} = ssh_transport:handle_new_keys(Msg, D#data.ssh_params), + {keep_state, D#data{ssh_params = Ssh}}; + + +handle_event(internal, Msg, {ext_info,Role,init}, D) when is_tuple(Msg) -> + %% If something else arrives, goto next state and handle the event in that one + {next_state, {service_request,Role}, D, [postpone]}; + +handle_event(internal, Msg, {ext_info,Role,_ReNegFlag}, D) when is_tuple(Msg) -> + %% If something else arrives, goto next state and handle the event in that one + {next_state, {connected,Role}, D, [postpone]}; + +%%% ######## {service_request, client|server} #### handle_event(_, Msg = #ssh_msg_service_request{name=ServiceName}, StateName = {service_request,server}, D) -> case ServiceName of @@ -756,6 +807,11 @@ handle_event(_, end; %%---- userauth success to client +handle_event(_, #ssh_msg_ext_info{}=Msg, {userauth,client}, D0) -> + %% FIXME: need new state to receive this msg! + D = handle_ssh_msg_ext_info(Msg, D0), + {keep_state, D}; + handle_event(_, #ssh_msg_userauth_success{}, {userauth,client}, D=#data{ssh_params = Ssh}) -> D#data.starter ! ssh_connected, {next_state, {connected,client}, D#data{ssh_params=Ssh#ssh{authenticated = true}}}; @@ -810,7 +866,7 @@ handle_event(_, #ssh_msg_userauth_info_request{} = Msg, {userauth_keyboard_inter send_bytes(Reply, D), {next_state, {userauth_keyboard_interactive_info_response,client}, D#data{ssh_params = Ssh}}; not_ok -> - {next_state, {userauth,client}, D, [{next_event, internal, Msg}]} + {next_state, {userauth,client}, D, [postpone]} end; handle_event(_, #ssh_msg_userauth_info_response{} = Msg, {userauth_keyboard_interactive, server}, D) -> @@ -839,14 +895,14 @@ handle_event(_, #ssh_msg_userauth_info_response{} = Msg, {userauth_keyboard_inte {next_state, {connected,server}, D#data{auth_user = User, ssh_params = Ssh#ssh{authenticated = true}}}; -handle_event(_, Msg = #ssh_msg_userauth_failure{}, {userauth_keyboard_interactive, client}, +handle_event(_, #ssh_msg_userauth_failure{}, {userauth_keyboard_interactive, client}, #data{ssh_params = Ssh0} = D0) -> Prefs = [{Method,M,F,A} || {Method,M,F,A} <- Ssh0#ssh.userauth_preference, Method =/= "keyboard-interactive"], D = D0#data{ssh_params = Ssh0#ssh{userauth_preference=Prefs}}, - {next_state, {userauth,client}, D, [{next_event, internal, Msg}]}; + {next_state, {userauth,client}, D, [postpone]}; -handle_event(_, Msg=#ssh_msg_userauth_failure{}, {userauth_keyboard_interactive_info_response, client}, +handle_event(_, #ssh_msg_userauth_failure{}, {userauth_keyboard_interactive_info_response, client}, #data{ssh_params = Ssh0} = D0) -> Opts = Ssh0#ssh.opts, D = case ?GET_OPT(password, Opts) of @@ -856,23 +912,28 @@ handle_event(_, Msg=#ssh_msg_userauth_failure{}, {userauth_keyboard_interactive_ D0#data{ssh_params = Ssh0#ssh{opts = ?PUT_OPT({password,not_ok}, Opts)}} % FIXME:intermodule dependency end, - {next_state, {userauth,client}, D, [{next_event, internal, Msg}]}; + {next_state, {userauth,client}, D, [postpone]}; + +handle_event(_, #ssh_msg_ext_info{}=Msg, {userauth_keyboard_interactive_info_response, client}, D0) -> + %% FIXME: need new state to receive this msg! + D = handle_ssh_msg_ext_info(Msg, D0), + {keep_state, D}; -handle_event(_, Msg=#ssh_msg_userauth_success{}, {userauth_keyboard_interactive_info_response, client}, D) -> - {next_state, {userauth,client}, D, [{next_event, internal, Msg}]}; +handle_event(_, #ssh_msg_userauth_success{}, {userauth_keyboard_interactive_info_response, client}, D) -> + {next_state, {userauth,client}, D, [postpone]}; -handle_event(_, Msg=#ssh_msg_userauth_info_request{}, {userauth_keyboard_interactive_info_response, client}, D) -> - {next_state, {userauth_keyboard_interactive,client}, D, [{next_event, internal, Msg}]}; +handle_event(_, #ssh_msg_userauth_info_request{}, {userauth_keyboard_interactive_info_response, client}, D) -> + {next_state, {userauth_keyboard_interactive,client}, D, [postpone]}; %%% ######## {connected, client|server} #### -handle_event(_, {#ssh_msg_kexinit{},_} = Event, {connected,Role}, D0) -> +handle_event(_, {#ssh_msg_kexinit{},_}, {connected,Role}, D0) -> {KeyInitMsg, SshPacket, Ssh} = ssh_transport:key_exchange_init_msg(D0#data.ssh_params), D = D0#data{ssh_params = Ssh, key_exchange_init_msg = KeyInitMsg}, send_bytes(SshPacket, D), - {next_state, {kexinit,Role,renegotiate}, D, [{next_event, internal, Event}]}; + {next_state, {kexinit,Role,renegotiate}, D, [postpone]}; handle_event(_, #ssh_msg_disconnect{description=Desc} = Msg, StateName, D0) -> {disconnect, _, {{replies,Replies}, _}} = @@ -976,12 +1037,10 @@ handle_event(cast, data_size, _, _) -> -handle_event(cast, _, StateName, _) when StateName /= {connected,server}, - StateName /= {connected,client} -> +handle_event(cast, _, StateName, _) when not ?CONNECTED(StateName) -> {keep_state_and_data, [postpone]}; - -handle_event(cast, {adjust_window,ChannelId,Bytes}, {connected,_}, D) -> +handle_event(cast, {adjust_window,ChannelId,Bytes}, StateName, D) when ?CONNECTED(StateName) -> case ssh_channel:cache_lookup(cache(D), ChannelId) of #channel{recv_window_size = WinSize, recv_window_pending = Pending, @@ -1007,7 +1066,7 @@ handle_event(cast, {adjust_window,ChannelId,Bytes}, {connected,_}, D) -> keep_state_and_data end; -handle_event(cast, {reply_request,success,ChannelId}, {connected,_}, D) -> +handle_event(cast, {reply_request,success,ChannelId}, StateName, D) when ?CONNECTED(StateName) -> case ssh_channel:cache_lookup(cache(D), ChannelId) of #channel{remote_id = RemoteId} -> Msg = ssh_connection:channel_success_msg(RemoteId), @@ -1018,13 +1077,13 @@ handle_event(cast, {reply_request,success,ChannelId}, {connected,_}, D) -> keep_state_and_data end; -handle_event(cast, {request,ChannelPid, ChannelId, Type, Data}, {connected,_}, D) -> +handle_event(cast, {request,ChannelPid, ChannelId, Type, Data}, StateName, D) when ?CONNECTED(StateName) -> {keep_state, handle_request(ChannelPid, ChannelId, Type, Data, false, none, D)}; -handle_event(cast, {request,ChannelId,Type,Data}, {connected,_}, D) -> +handle_event(cast, {request,ChannelId,Type,Data}, StateName, D) when ?CONNECTED(StateName) -> {keep_state, handle_request(ChannelId, Type, Data, false, none, D)}; -handle_event(cast, {unknown,Data}, {connected,_}, D) -> +handle_event(cast, {unknown,Data}, StateName, D) when ?CONNECTED(StateName) -> Msg = #ssh_msg_unimplemented{sequence = Data}, {keep_state, send_msg(Msg,D)}; @@ -1085,30 +1144,34 @@ handle_event({call,From}, stop, StateName, D0) -> {Repls,D} = send_replies(Replies, D0), {stop_and_reply, normal, [{reply,From,ok}|Repls], D#data{connection_state=Connection}}; -handle_event({call,_}, _, StateName, _) when StateName /= {connected,server}, - StateName /= {connected,client} -> + +handle_event({call,_}, _, StateName, _) when not ?CONNECTED(StateName) -> {keep_state_and_data, [postpone]}; -handle_event({call,From}, {request, ChannelPid, ChannelId, Type, Data, Timeout}, {connected,_}, D0) -> +handle_event({call,From}, {request, ChannelPid, ChannelId, Type, Data, Timeout}, StateName, D0) + when ?CONNECTED(StateName) -> D = handle_request(ChannelPid, ChannelId, Type, Data, true, From, D0), %% Note reply to channel will happen later when reply is recived from peer on the socket start_channel_request_timer(ChannelId, From, Timeout), {keep_state, cache_request_idle_timer_check(D)}; -handle_event({call,From}, {request, ChannelId, Type, Data, Timeout}, {connected,_}, D0) -> +handle_event({call,From}, {request, ChannelId, Type, Data, Timeout}, StateName, D0) + when ?CONNECTED(StateName) -> D = handle_request(ChannelId, Type, Data, true, From, D0), %% Note reply to channel will happen later when reply is recived from peer on the socket start_channel_request_timer(ChannelId, From, Timeout), {keep_state, cache_request_idle_timer_check(D)}; -handle_event({call,From}, {data, ChannelId, Type, Data, Timeout}, {connected,_}, D0) -> +handle_event({call,From}, {data, ChannelId, Type, Data, Timeout}, StateName, D0) + when ?CONNECTED(StateName) -> {{replies, Replies}, Connection} = ssh_connection:channel_data(ChannelId, Type, Data, D0#data.connection_state, From), {Repls,D} = send_replies(Replies, D0#data{connection_state = Connection}), start_channel_request_timer(ChannelId, From, Timeout), % FIXME: No message exchange so why? {keep_state, D, Repls}; -handle_event({call,From}, {eof, ChannelId}, {connected,_}, D0) -> +handle_event({call,From}, {eof, ChannelId}, StateName, D0) + when ?CONNECTED(StateName) -> case ssh_channel:cache_lookup(cache(D0), ChannelId) of #channel{remote_id = Id, sent_close = false} -> D = send_msg(ssh_connection:channel_eof_msg(Id), D0), @@ -1119,8 +1182,8 @@ handle_event({call,From}, {eof, ChannelId}, {connected,_}, D0) -> handle_event({call,From}, {open, ChannelPid, Type, InitialWindowSize, MaxPacketSize, Data, Timeout}, - {connected,_}, - D0) -> + StateName, + D0) when ?CONNECTED(StateName) -> erlang:monitor(process, ChannelPid), {ChannelId, D1} = new_channel_id(D0), D2 = send_msg(ssh_connection:channel_open_msg(Type, ChannelId, @@ -1140,7 +1203,8 @@ handle_event({call,From}, start_channel_request_timer(ChannelId, From, Timeout), {keep_state, cache_cancel_idle_timer(D)}; -handle_event({call,From}, {send_window, ChannelId}, {connected,_}, D) -> +handle_event({call,From}, {send_window, ChannelId}, StateName, D) + when ?CONNECTED(StateName) -> Reply = case ssh_channel:cache_lookup(cache(D), ChannelId) of #channel{send_window_size = WinSize, send_packet_size = Packsize} -> @@ -1150,7 +1214,8 @@ handle_event({call,From}, {send_window, ChannelId}, {connected,_}, D) -> end, {keep_state_and_data, [{reply,From,Reply}]}; -handle_event({call,From}, {recv_window, ChannelId}, {connected,_}, D) -> +handle_event({call,From}, {recv_window, ChannelId}, StateName, D) + when ?CONNECTED(StateName) -> Reply = case ssh_channel:cache_lookup(cache(D), ChannelId) of #channel{recv_window_size = WinSize, recv_packet_size = Packsize} -> @@ -1160,7 +1225,8 @@ handle_event({call,From}, {recv_window, ChannelId}, {connected,_}, D) -> end, {keep_state_and_data, [{reply,From,Reply}]}; -handle_event({call,From}, {close, ChannelId}, {connected,_}, D0) -> +handle_event({call,From}, {close, ChannelId}, StateName, D0) + when ?CONNECTED(StateName) -> case ssh_channel:cache_lookup(cache(D0), ChannelId) of #channel{remote_id = Id} = Channel -> D1 = send_msg(ssh_connection:channel_close_msg(Id), D0), @@ -1285,7 +1351,6 @@ handle_event(info, {'EXIT', _Sup, Reason}, _, _) -> {stop, {shutdown, Reason}}; handle_event(info, check_cache, _, D) -> -ct:pal("check_cache",[]), {keep_state, cache_check_set_idle_timer(D)}; handle_event(info, UnexpectedMessage, StateName, D = #data{ssh_params = Ssh}) -> @@ -1329,11 +1394,16 @@ handle_event(info, UnexpectedMessage, StateName, D = #data{ssh_params = Ssh}) -> handle_event(internal, {disconnect,Msg,_Reason}, StateName, D) -> disconnect(Msg, StateName, D); +handle_event(_Type, _Msg, {ext_info,Role,_ReNegFlag}, D) -> + %% If something else arrives, goto next state and handle the event in that one + {next_state, {connected,Role}, D, [postpone]}; + handle_event(Type, Ev, StateName, D) -> Descr = case catch atom_to_list(element(1,Ev)) of "ssh_msg_" ++_ when Type==internal -> - "Message in wrong state"; +%% "Message in wrong state"; + lists:flatten(io_lib:format("Message ~p in wrong state (~p)", [element(1,Ev), StateName])); _ -> "Internal error" end, @@ -1473,16 +1543,6 @@ peer_role(client) -> server; peer_role(server) -> client. %%-------------------------------------------------------------------- -%% StateName to Role -role({_,Role}) -> Role; -role({_,Role,_}) -> Role. - -%%-------------------------------------------------------------------- -%% Check the StateName to see if we are in the renegotiation phase -renegotiation({_,_,ReNeg}) -> ReNeg == renegotiation; -renegotiation(_) -> false. - -%%-------------------------------------------------------------------- supported_host_keys(client, _, Options) -> try find_sup_hkeys(Options) @@ -1518,14 +1578,19 @@ find_sup_hkeys(Options) -> %% Alg :: atom() available_host_key({KeyCb,KeyCbOpts}, Alg, Opts) -> UserOpts = ?GET_OPT(user_options, Opts), - element(1, - catch KeyCb:host_key(Alg, [{key_cb_private,KeyCbOpts}|UserOpts])) == ok. + case KeyCb:host_key(Alg, [{key_cb_private,KeyCbOpts}|UserOpts]) of + {ok,_} -> true; + _ -> false + end. + send_msg(Msg, State=#data{ssh_params=Ssh0}) when is_tuple(Msg) -> {Bytes, Ssh} = ssh_transport:ssh_packet(Msg, Ssh0), send_bytes(Bytes, State), State#data{ssh_params=Ssh}. +send_bytes("", _D) -> + ok; send_bytes(Bytes, #data{socket = Socket, transport_cb = Transport}) -> _ = Transport:send(Socket, Bytes), ok. @@ -1632,6 +1697,29 @@ cache(#data{connection_state=C}) -> C#connection.channel_cache. %%%---------------------------------------------------------------- +handle_ssh_msg_ext_info(#ssh_msg_ext_info{}, D=#data{ssh_params = #ssh{recv_ext_info=false}} ) -> + % The peer sent this although we didn't allow it! + D; + +handle_ssh_msg_ext_info(#ssh_msg_ext_info{data=Data}, D0) -> + lists:foldl(fun ext_info/2, D0, Data). + + +ext_info({"server-sig-algs",SigAlgs}, D0 = #data{ssh_params=#ssh{role=client}=Ssh0}) -> + %% Make strings to eliminate risk of beeing bombed with odd strings that fills the atom table: + SupportedAlgs = lists:map(fun erlang:atom_to_list/1, ssh_transport:supported_algorithms(public_key)), + Ssh = Ssh0#ssh{userauth_pubkeys = + [list_to_atom(SigAlg) || SigAlg <- string:tokens(SigAlgs,","), + %% length of SigAlg is implicitly checked by member: + lists:member(SigAlg, SupportedAlgs) + ]}, + D0#data{ssh_params = Ssh}; + +ext_info(_, D0) -> + %% Not implemented + D0. + +%%%---------------------------------------------------------------- handle_request(ChannelPid, ChannelId, Type, Data, WantReply, From, D) -> case ssh_channel:cache_lookup(cache(D), ChannelId) of #channel{remote_id = Id} = Channel -> @@ -1921,12 +2009,14 @@ handshake(Pid, Ref, Timeout) -> end. update_inet_buffers(Socket) -> - {ok, BufSzs0} = inet:getopts(Socket, [sndbuf,recbuf]), - MinVal = 655360, - case - [{Tag,MinVal} || {Tag,Val} <- BufSzs0, - Val < MinVal] + try + {ok, BufSzs0} = inet:getopts(Socket, [sndbuf,recbuf]), + MinVal = 655360, + [{Tag,MinVal} || {Tag,Val} <- BufSzs0, + Val < MinVal] of [] -> ok; NewOpts -> inet:setopts(Socket, NewOpts) + catch + _:_ -> ok end. diff --git a/lib/ssh/src/ssh_connection_sup.erl b/lib/ssh/src/ssh_connection_sup.erl index 0f54053f52..fad796f196 100644 --- a/lib/ssh/src/ssh_connection_sup.erl +++ b/lib/ssh/src/ssh_connection_sup.erl @@ -45,19 +45,17 @@ start_child(Sup, Args) -> %%%========================================================================= %%% Supervisor callback %%%========================================================================= --spec init( [term()] ) -> {ok,{supervisor:sup_flags(),[supervisor:child_spec()]}} | ignore . - init(_) -> - RestartStrategy = simple_one_for_one, - MaxR = 0, - MaxT = 3600, - - Name = undefined, % As simple_one_for_one is used. - StartFunc = {ssh_connection_handler, start_link, []}, - Restart = temporary, % E.g. should not be restarted - Shutdown = 4000, - Modules = [ssh_connection_handler], - Type = worker, - - ChildSpec = {Name, StartFunc, Restart, Shutdown, Type, Modules}, - {ok, {{RestartStrategy, MaxR, MaxT}, [ChildSpec]}}. + SupFlags = #{strategy => simple_one_for_one, + intensity => 0, + period => 3600 + }, + ChildSpecs = [#{id => undefined, % As simple_one_for_one is used. + start => {ssh_connection_handler, start_link, []}, + restart => temporary, + shutdown => 4000, + type => worker, + modules => [ssh_connection_handler] + } + ], + {ok, {SupFlags,ChildSpecs}}. diff --git a/lib/ssh/src/ssh_dbg.erl b/lib/ssh/src/ssh_dbg.erl index 0345bbdea7..9431bf1817 100644 --- a/lib/ssh/src/ssh_dbg.erl +++ b/lib/ssh/src/ssh_dbg.erl @@ -56,7 +56,7 @@ messages(Write, MangleArg) when is_function(Write,2), dbg_ssh_messages() -> dbg:tp(ssh_message,encode,1, x), dbg:tp(ssh_message,decode,1, x), - dbg:tpl(ssh_transport,select_algorithm,3, x), + dbg:tpl(ssh_transport,select_algorithm,4, x), dbg:tp(ssh_transport,hello_version_msg,1, x), dbg:tp(ssh_transport,handle_hello_version,1, x). @@ -77,7 +77,7 @@ msg_formater({trace_ts,Pid,return_from,{ssh_message,decode,1},Msg,TS}, D) -> msg_formater({trace_ts,_Pid,call,{ssh_transport,select_algorithm,_},_TS}, D) -> D; -msg_formater({trace_ts,Pid,return_from,{ssh_transport,select_algorithm,3},{ok,Alg},TS}, D) -> +msg_formater({trace_ts,Pid,return_from,{ssh_transport,select_algorithm,_},{ok,Alg},TS}, D) -> fmt("~n~s ~p ALGORITHMS~n~s~n", [ts(TS),Pid, wr_record(Alg)], D); msg_formater({trace_ts,_Pid,call,{ssh_transport,hello_version_msg,_},_TS}, D) -> @@ -160,6 +160,7 @@ shrink_bin(X) -> X. ?wr_record(ssh_msg_kexdh_init); ?wr_record(ssh_msg_kexdh_reply); ?wr_record(ssh_msg_newkeys); +?wr_record(ssh_msg_ext_info); ?wr_record(ssh_msg_kex_dh_gex_request); ?wr_record(ssh_msg_kex_dh_gex_request_old); ?wr_record(ssh_msg_kex_dh_gex_group); diff --git a/lib/ssh/src/ssh_file.erl b/lib/ssh/src/ssh_file.erl index 898b4cc5c4..4498c70d34 100644 --- a/lib/ssh/src/ssh_file.erl +++ b/lib/ssh/src/ssh_file.erl @@ -75,17 +75,12 @@ host_key(Algorithm, Opts) -> Password = proplists:get_value(identity_pass_phrase(Algorithm), Opts, ignore), case decode(File, Password) of {ok,Key} -> - case {Key,Algorithm} of - {#'RSAPrivateKey'{}, 'ssh-rsa'} -> {ok,Key}; - {#'DSAPrivateKey'{}, 'ssh-dss'} -> {ok,Key}; - {#'ECPrivateKey'{parameters = {namedCurve, ?'secp256r1'}}, 'ecdsa-sha2-nistp256'} -> {ok,Key}; - {#'ECPrivateKey'{parameters = {namedCurve, ?'secp384r1'}}, 'ecdsa-sha2-nistp384'} -> {ok,Key}; - {#'ECPrivateKey'{parameters = {namedCurve, ?'secp521r1'}}, 'ecdsa-sha2-nistp521'} -> {ok,Key}; - _ -> - {error,bad_keytype_in_file} + case ssh_transport:valid_key_sha_alg(Key,Algorithm) of + true -> {ok,Key}; + false -> {error,bad_keytype_in_file} end; - Other -> - Other + {error,DecodeError} -> + {error,DecodeError} end. is_auth_key(Key, User,Opts) -> @@ -115,6 +110,9 @@ user_key(Algorithm, Opts) -> %% Internal functions %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% file_base_name('ssh-rsa' ) -> "ssh_host_rsa_key"; +file_base_name('rsa-sha2-256' ) -> "ssh_host_rsa_key"; +file_base_name('rsa-sha2-384' ) -> "ssh_host_rsa_key"; +file_base_name('rsa-sha2-512' ) -> "ssh_host_rsa_key"; file_base_name('ssh-dss' ) -> "ssh_host_dsa_key"; file_base_name('ecdsa-sha2-nistp256') -> "ssh_host_ecdsa_key"; file_base_name('ecdsa-sha2-nistp384') -> "ssh_host_ecdsa_key"; @@ -221,6 +219,8 @@ file_name(Type, Name, Opts) -> %% in: "host" out: "host,1.2.3.4. +add_ip(IP) when is_tuple(IP) -> + ssh_connection:encode_ip(IP); add_ip(Host) -> case inet:getaddr(Host, inet) of {ok, Addr} -> @@ -251,12 +251,18 @@ do_lookup_host_key(KeyToMatch, Host, Alg, Opts) -> identity_key_filename('ssh-dss' ) -> "id_dsa"; identity_key_filename('ssh-rsa' ) -> "id_rsa"; +identity_key_filename('rsa-sha2-256' ) -> "id_rsa"; +identity_key_filename('rsa-sha2-384' ) -> "id_rsa"; +identity_key_filename('rsa-sha2-512' ) -> "id_rsa"; identity_key_filename('ecdsa-sha2-nistp256') -> "id_ecdsa"; identity_key_filename('ecdsa-sha2-nistp384') -> "id_ecdsa"; identity_key_filename('ecdsa-sha2-nistp521') -> "id_ecdsa". identity_pass_phrase("ssh-dss" ) -> dsa_pass_phrase; identity_pass_phrase("ssh-rsa" ) -> rsa_pass_phrase; +identity_pass_phrase("rsa-sha2-256" ) -> rsa_pass_phrase; +identity_pass_phrase("rsa-sha2-384" ) -> rsa_pass_phrase; +identity_pass_phrase("rsa-sha2-512" ) -> rsa_pass_phrase; identity_pass_phrase("ecdsa-sha2-"++_) -> ecdsa_pass_phrase; identity_pass_phrase(P) when is_atom(P) -> identity_pass_phrase(atom_to_list(P)). diff --git a/lib/ssh/src/ssh_message.erl b/lib/ssh/src/ssh_message.erl index 562f040477..21c0eabcd3 100644 --- a/lib/ssh/src/ssh_message.erl +++ b/lib/ssh/src/ssh_message.erl @@ -215,6 +215,16 @@ encode(#ssh_msg_service_accept{ }) -> <<?Ebyte(?SSH_MSG_SERVICE_ACCEPT), ?Estring_utf8(Service)>>; +encode(#ssh_msg_ext_info{ + nr_extensions = N, + data = Data + }) -> + lists:foldl(fun({ExtName,ExtVal}, Acc) -> + <<Acc/binary, ?Estring(ExtName), ?Estring(ExtVal)>> + end, + <<?Ebyte(?SSH_MSG_EXT_INFO), ?Euint32(N)>>, + Data); + encode(#ssh_msg_newkeys{}) -> <<?Ebyte(?SSH_MSG_NEWKEYS)>>; @@ -435,6 +445,18 @@ decode(<<?BYTE(?SSH_MSG_USERAUTH_INFO_RESPONSE), ?UINT32(Num), Data/binary>>) -> num_responses = Num, data = Data}; +decode(<<?BYTE(?SSH_MSG_EXT_INFO), ?UINT32(N), BinData/binary>>) -> + Data = bin_foldr( + fun(Bin,Acc) when length(Acc) == N -> + {Bin,Acc}; + (<<?DEC_BIN(V0,__0), ?DEC_BIN(V1,__1), Rest/binary>>, Acc) -> + {Rest,[{binary_to_list(V0),binary_to_list(V1)}|Acc]} + end, [], BinData), + #ssh_msg_ext_info{ + nr_extensions = N, + data = Data + }; + %%% Keyexchange messages decode(<<?BYTE(?SSH_MSG_KEXINIT), Cookie:128, Data/binary>>) -> decode_kex_init(Data, [Cookie, ssh_msg_kexinit], 10); @@ -537,17 +559,28 @@ decode(<<?BYTE(?SSH_MSG_DEBUG), ?BYTE(Bool), ?DEC_BIN(Msg,__0), ?DEC_BIN(Lang,__ %%% Helper functions %%% +bin_foldr(Fun, Acc, Bin) -> + lists:reverse(bin_foldl(Fun, Acc, Bin)). + +bin_foldl(_, Acc, <<>>) -> Acc; +bin_foldl(Fun, Acc0, Bin0) -> + {Bin,Acc} = Fun(Bin0,Acc0), + bin_foldl(Fun, Acc, Bin). + +%%%---------------------------------------------------------------- decode_keyboard_interactive_prompts(<<>>, Acc) -> lists:reverse(Acc); decode_keyboard_interactive_prompts(<<?DEC_BIN(Prompt,__0), ?BYTE(Bool), Bin/binary>>, Acc) -> decode_keyboard_interactive_prompts(Bin, [{Prompt, erl_boolean(Bool)} | Acc]). +%%%---------------------------------------------------------------- erl_boolean(0) -> false; erl_boolean(1) -> true. +%%%---------------------------------------------------------------- decode_kex_init(<<?BYTE(Bool), ?UINT32(X)>>, Acc, 0) -> list_to_tuple(lists:reverse([X, erl_boolean(Bool) | Acc])); decode_kex_init(<<?BYTE(Bool)>>, Acc, 0) -> @@ -569,11 +602,22 @@ decode_signature(<<?DEC_BIN(_Alg,__0), ?UINT32(_), Signature/binary>>) -> Signature. -encode_signature(#'RSAPublicKey'{}, Signature) -> - <<?Ebinary(<<"ssh-rsa">>), ?Ebinary(Signature)>>; -encode_signature({_, #'Dss-Parms'{}}, Signature) -> +encode_signature({#'RSAPublicKey'{},Sign}, Signature) -> + SignName = list_to_binary(atom_to_list(Sign)), + <<?Ebinary(SignName), ?Ebinary(Signature)>>; +encode_signature({{_, #'Dss-Parms'{}},_}, Signature) -> <<?Ebinary(<<"ssh-dss">>), ?Ebinary(Signature)>>; -encode_signature({#'ECPoint'{}, {namedCurve,OID}}, Signature) -> +encode_signature({{#'ECPoint'{}, {namedCurve,OID}},_}, Signature) -> CurveName = public_key:oid2ssh_curvename(OID), <<?Ebinary(<<"ecdsa-sha2-",CurveName/binary>>), ?Ebinary(Signature)>>. +%% encode_signature(#'RSAPublicKey'{}, Signature) -> +%% SignName = <<"ssh-rsa">>, +%% <<?Ebinary(SignName), ?Ebinary(Signature)>>; +%% encode_signature({_, #'Dss-Parms'{}}, Signature) -> +%% <<?Ebinary(<<"ssh-dss">>), ?Ebinary(Signature)>>; +%% encode_signature({#'ECPoint'{}, {namedCurve,OID}}, Signature) -> +%% CurveName = public_key:oid2ssh_curvename(OID), +%% <<?Ebinary(<<"ecdsa-sha2-",CurveName/binary>>), ?Ebinary(Signature)>>. + + diff --git a/lib/ssh/src/ssh_options.erl b/lib/ssh/src/ssh_options.erl index 55f9c6bdc8..0886d5b34d 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 %%% @@ -432,9 +437,7 @@ default(client) -> {pref_public_key_algs, def} => #{default => - %% Get dynamically supported keys in the order of the ?SUPPORTED_USER_KEYS - [A || A <- ?SUPPORTED_USER_KEYS, - lists:member(A, ssh_transport:supported_algorithms(public_key))], + ssh_transport:supported_algorithms(public_key), chk => fun check_pref_public_key_algs/1, class => @@ -609,11 +612,23 @@ default(common) -> }, {max_random_length_padding, def} => - #{default => ?MAX_RND_PADDING_LEN, - chk => fun check_non_neg_integer/1, - class => user_options - } - }. + #{default => ?MAX_RND_PADDING_LEN, + chk => fun check_non_neg_integer/1, + class => user_options + }, + + {send_ext_info, def} => + #{default => true, + chk => fun erlang:is_boolean/1, + class => user_options + }, + + {recv_ext_info, def} => + #{default => true, + chk => fun erlang:is_boolean/1, + class => user_options + } + }. %%%================================================================ @@ -653,20 +668,8 @@ check_pref_public_key_algs(V) -> PKs = ssh_transport:supported_algorithms(public_key), CHK = fun(A, Ack) -> case lists:member(A, PKs) of - true -> - [A|Ack]; - false -> - %% Check with the documented options, that is, - %% the one we can handle - case lists:member(A,?SUPPORTED_USER_KEYS) of - false -> - %% An algorithm ssh never can handle - error_in_check(A, "Not supported public key"); - true -> - %% An algorithm ssh can handle, but not in - %% this very call - Ack - end + true -> [A|Ack]; + false -> error_in_check(A, "Not supported public key") end end, case lists:foldr( @@ -792,18 +795,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 +873,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_subsystem_sup.erl b/lib/ssh/src/ssh_subsystem_sup.erl index cf82db458f..cf409ade6b 100644 --- a/lib/ssh/src/ssh_subsystem_sup.erl +++ b/lib/ssh/src/ssh_subsystem_sup.erl @@ -28,7 +28,7 @@ -include("ssh.hrl"). --export([start_link/1, +-export([start_link/5, connection_supervisor/1, channel_supervisor/1 ]). @@ -39,8 +39,8 @@ %%%========================================================================= %%% API %%%========================================================================= -start_link(Options) -> - supervisor:start_link(?MODULE, [Options]). +start_link(Role, Address, Port, Profile, Options) -> + supervisor:start_link(?MODULE, [Role, Address, Port, Profile, Options]). connection_supervisor(SupPid) -> Children = supervisor:which_children(SupPid), @@ -53,49 +53,40 @@ channel_supervisor(SupPid) -> %%%========================================================================= %%% Supervisor callback %%%========================================================================= --spec init( [term()] ) -> {ok,{supervisor:sup_flags(),[supervisor:child_spec()]}} | ignore . - -init([Options]) -> - RestartStrategy = one_for_all, - MaxR = 0, - MaxT = 3600, - Children = child_specs(Options), - {ok, {{RestartStrategy, MaxR, MaxT}, Children}}. +init([Role, Address, Port, Profile, Options]) -> + SupFlags = #{strategy => one_for_all, + intensity => 0, + period => 3600 + }, + ChildSpecs = child_specs(Role, Address, Port, Profile, Options), + {ok, {SupFlags,ChildSpecs}}. %%%========================================================================= %%% Internal functions %%%========================================================================= -child_specs(Options) -> - case ?GET_INTERNAL_OPT(role, Options) of - client -> - []; - server -> - [ssh_channel_child_spec(Options), ssh_connectinon_child_spec(Options)] - end. +child_specs(client, _Address, _Port, _Profile, _Options) -> + []; +child_specs(server, Address, Port, Profile, Options) -> + [ssh_channel_child_spec(server, Address, Port, Profile, Options), + ssh_connection_child_spec(server, Address, Port, Profile, Options)]. -ssh_connectinon_child_spec(Options) -> - Address = ?GET_INTERNAL_OPT(address, Options), - Port = ?GET_INTERNAL_OPT(port, Options), - Role = ?GET_INTERNAL_OPT(role, Options), - Name = id(Role, ssh_connection_sup, Address, Port), - StartFunc = {ssh_connection_sup, start_link, [Options]}, - Restart = temporary, - Shutdown = 5000, - Modules = [ssh_connection_sup], - Type = supervisor, - {Name, StartFunc, Restart, Shutdown, Type, Modules}. - -ssh_channel_child_spec(Options) -> - Address = ?GET_INTERNAL_OPT(address, Options), - Port = ?GET_INTERNAL_OPT(port, Options), - Role = ?GET_INTERNAL_OPT(role, Options), - Name = id(Role, ssh_channel_sup, Address, Port), - StartFunc = {ssh_channel_sup, start_link, [Options]}, - Restart = temporary, - Shutdown = infinity, - Modules = [ssh_channel_sup], - Type = supervisor, - {Name, StartFunc, Restart, Shutdown, Type, Modules}. +ssh_connection_child_spec(Role, Address, Port, _Profile, Options) -> + #{id => id(Role, ssh_connection_sup, Address, Port), + start => {ssh_connection_sup, start_link, [Options]}, + restart => temporary, + shutdown => 5000, + type => supervisor, + modules => [ssh_connection_sup] + }. + +ssh_channel_child_spec(Role, Address, Port, _Profile, Options) -> + #{id => id(Role, ssh_channel_sup, Address, Port), + start => {ssh_channel_sup, start_link, [Options]}, + restart => temporary, + shutdown => infinity, + type => supervisor, + modules => [ssh_channel_sup] + }. id(Role, Sup, Address, Port) -> {Role, Sup, Address, Port}. diff --git a/lib/ssh/src/ssh_sup.erl b/lib/ssh/src/ssh_sup.erl index 8b57387589..26574763e4 100644 --- a/lib/ssh/src/ssh_sup.erl +++ b/lib/ssh/src/ssh_sup.erl @@ -31,63 +31,20 @@ %%%========================================================================= %%% Supervisor callback %%%========================================================================= --spec init( [term()] ) -> {ok,{supervisor:sup_flags(),[supervisor:child_spec()]}} | ignore . - -init([]) -> - SupFlags = {one_for_one, 10, 3600}, - Children = children(), - {ok, {SupFlags, Children}}. - -%%%========================================================================= -%%% Internal functions -%%%========================================================================= -get_services() -> - case (catch application:get_env(ssh, services)) of - {ok, Services} -> - Services; - _ -> - [] - end. - -children() -> - Services = get_services(), - Clients = [Service || Service <- Services, is_client(Service)], - Servers = [Service || Service <- Services, is_server(Service)], - - [server_child_spec(Servers), client_child_spec(Clients)]. - -server_child_spec(Servers) -> - Name = sshd_sup, - StartFunc = {sshd_sup, start_link, [Servers]}, - Restart = permanent, - Shutdown = infinity, - Modules = [sshd_sup], - Type = supervisor, - {Name, StartFunc, Restart, Shutdown, Type, Modules}. - -client_child_spec(Clients) -> - Name = sshc_sup, - StartFunc = {sshc_sup, start_link, [Clients]}, - Restart = permanent, - Shutdown = infinity, - Modules = [sshc_sup], - Type = supervisor, - {Name, StartFunc, Restart, Shutdown, Type, Modules}. - -is_server({sftpd, _}) -> - true; -is_server({shelld, _}) -> - true; -is_server(_) -> - false. - -is_client({sftpc, _}) -> - true; -is_client({shellc, _}) -> - true; -is_client(_) -> - false. - - - +init(_) -> + SupFlags = #{strategy => one_for_one, + intensity => 10, + period => 3600 + }, + ChildSpecs = [#{id => Module, + start => {Module, start_link, []}, + restart => permanent, + shutdown => 4000, %brutal_kill, + type => supervisor, + modules => [Module] + } + || Module <- [sshd_sup, + sshc_sup] + ], + {ok, {SupFlags,ChildSpecs}}. diff --git a/lib/ssh/src/ssh_system_sup.erl b/lib/ssh/src/ssh_system_sup.erl index b0bbd3aae5..84b4cd3241 100644 --- a/lib/ssh/src/ssh_system_sup.erl +++ b/lib/ssh/src/ssh_system_sup.erl @@ -21,7 +21,7 @@ %% %%---------------------------------------------------------------------- %% Purpose: The ssh server instance supervisor, an instans of this supervisor -%% exists for every ip-address and port combination, hangs under +%% exists for every ip-address and port combination, hangs under %% sshd_sup. %%---------------------------------------------------------------------- @@ -31,64 +31,103 @@ -include("ssh.hrl"). --export([start_link/1, stop_listener/1, +-export([start_link/4, stop_listener/1, stop_listener/3, stop_system/1, stop_system/3, system_supervisor/3, - subsystem_supervisor/1, channel_supervisor/1, - connection_supervisor/1, - acceptor_supervisor/1, start_subsystem/2, restart_subsystem/3, - restart_acceptor/3, stop_subsystem/2]). + subsystem_supervisor/1, channel_supervisor/1, + connection_supervisor/1, + acceptor_supervisor/1, start_subsystem/6, + stop_subsystem/2]). %% Supervisor callback -export([init/1]). %%%========================================================================= -%%% Internal API +%%% API %%%========================================================================= -start_link(Options) -> - Address = ?GET_INTERNAL_OPT(address, Options), - Port = ?GET_INTERNAL_OPT(port, Options), - Profile = ?GET_OPT(profile, Options), +start_link(Address, Port, Profile, Options) -> Name = make_name(Address, Port, Profile), - supervisor:start_link({local, Name}, ?MODULE, [Options]). + supervisor:start_link({local, Name}, ?MODULE, [Address, Port, Profile, Options]). -stop_listener(SysSup) -> - stop_acceptor(SysSup). +%%%========================================================================= +%%% Supervisor callback +%%%========================================================================= +init([Address, Port, Profile, Options]) -> + SupFlags = #{strategy => one_for_one, + intensity => 0, + period => 3600 + }, + ChildSpecs = + case ?GET_INTERNAL_OPT(connected_socket,Options,undefined) of + undefined -> + [#{id => id(ssh_acceptor_sup, Address, Port, Profile), + start => {ssh_acceptor_sup, start_link, [Address, Port, Profile, Options]}, + restart => transient, + shutdown => infinity, + type => supervisor, + modules => [ssh_acceptor_sup] + }]; + _ -> + [] + end, + {ok, {SupFlags,ChildSpecs}}. + +%%%========================================================================= +%%% Service API +%%%========================================================================= +stop_listener(SystemSup) -> + {Name, AcceptorSup, _, _} = lookup(ssh_acceptor_sup, SystemSup), + case supervisor:terminate_child(AcceptorSup, Name) of + ok -> + supervisor:delete_child(AcceptorSup, Name); + Error -> + Error + end. stop_listener(Address, Port, Profile) -> - Name = make_name(Address, Port, Profile), - stop_acceptor(whereis(Name)). - + stop_listener( + system_supervisor(Address, Port, Profile)). + + stop_system(SysSup) -> - Name = sshd_sup:system_name(SysSup), - spawn(fun() -> sshd_sup:stop_child(Name) end), + spawn(fun() -> sshd_sup:stop_child(SysSup) end), ok. -stop_system(Address, Port, Profile) -> +stop_system(Address, Port, Profile) -> spawn(fun() -> sshd_sup:stop_child(Address, Port, Profile) end), ok. + system_supervisor(Address, Port, Profile) -> Name = make_name(Address, Port, Profile), whereis(Name). subsystem_supervisor(SystemSup) -> - ssh_subsystem_sup(supervisor:which_children(SystemSup)). + {_, Child, _, _} = lookup(ssh_subsystem_sup, SystemSup), + Child. channel_supervisor(SystemSup) -> - SubSysSup = ssh_subsystem_sup(supervisor:which_children(SystemSup)), - ssh_subsystem_sup:channel_supervisor(SubSysSup). + ssh_subsystem_sup:channel_supervisor( + subsystem_supervisor(SystemSup)). connection_supervisor(SystemSup) -> - SubSysSup = ssh_subsystem_sup(supervisor:which_children(SystemSup)), - ssh_subsystem_sup:connection_supervisor(SubSysSup). + ssh_subsystem_sup:connection_supervisor( + subsystem_supervisor(SystemSup)). acceptor_supervisor(SystemSup) -> - ssh_acceptor_sup(supervisor:which_children(SystemSup)). + {_, Child, _, _} = lookup(ssh_acceptor_sup, SystemSup), + Child. -start_subsystem(SystemSup, Options) -> - Spec = ssh_subsystem_child_spec(Options), - supervisor:start_child(SystemSup, Spec). + +start_subsystem(SystemSup, Role, Address, Port, Profile, Options) -> + SubsystemSpec = + #{id => make_ref(), + start => {ssh_subsystem_sup, start_link, [Role, Address, Port, Profile, Options]}, + restart => temporary, + shutdown => infinity, + type => supervisor, + modules => [ssh_subsystem_sup]}, + supervisor:start_child(SystemSup, SubsystemSpec). stop_subsystem(SystemSup, SubSys) -> case catch lists:keyfind(SubSys, 2, supervisor:which_children(SystemSup)) of @@ -106,100 +145,21 @@ stop_subsystem(SystemSup, SubSys) -> ok end. - -restart_subsystem(Address, Port, Profile) -> - SysSupName = make_name(Address, Port, Profile), - SubSysName = id(ssh_subsystem_sup, Address, Port, Profile), - case supervisor:terminate_child(SysSupName, SubSysName) of - ok -> - supervisor:restart_child(SysSupName, SubSysName); - Error -> - Error - end. - -restart_acceptor(Address, Port, Profile) -> - SysSupName = make_name(Address, Port, Profile), - AcceptorName = id(ssh_acceptor_sup, Address, Port, Profile), - supervisor:restart_child(SysSupName, AcceptorName). - -%%%========================================================================= -%%% Supervisor callback -%%%========================================================================= --spec init( [term()] ) -> {ok,{supervisor:sup_flags(),[supervisor:child_spec()]}} | ignore . - -init([Options]) -> - RestartStrategy = one_for_one, - MaxR = 0, - MaxT = 3600, - Children = case ?GET_INTERNAL_OPT(asocket,Options,undefined) of - undefined -> child_specs(Options); - _ -> [] - end, - {ok, {{RestartStrategy, MaxR, MaxT}, Children}}. - %%%========================================================================= %%% Internal functions %%%========================================================================= -child_specs(Options) -> - [ssh_acceptor_child_spec(Options)]. - -ssh_acceptor_child_spec(Options) -> - Address = ?GET_INTERNAL_OPT(address, Options), - Port = ?GET_INTERNAL_OPT(port, Options), - Profile = ?GET_OPT(profile, Options), - Name = id(ssh_acceptor_sup, Address, Port, Profile), - StartFunc = {ssh_acceptor_sup, start_link, [Options]}, - Restart = transient, - Shutdown = infinity, - Modules = [ssh_acceptor_sup], - Type = supervisor, - {Name, StartFunc, Restart, Shutdown, Type, Modules}. - -ssh_subsystem_child_spec(Options) -> - Name = make_ref(), - StartFunc = {ssh_subsystem_sup, start_link, [Options]}, - Restart = temporary, - Shutdown = infinity, - Modules = [ssh_subsystem_sup], - Type = supervisor, - {Name, StartFunc, Restart, Shutdown, Type, Modules}. - - id(Sup, Address, Port, Profile) -> - case is_list(Address) of - true -> - {Sup, any, Port, Profile}; - false -> - {Sup, Address, Port, Profile} - end. + {Sup, Address, Port, Profile}. make_name(Address, Port, Profile) -> - case is_list(Address) of - true -> - list_to_atom(lists:flatten(io_lib:format("ssh_system_~p_~p_~p_sup", - [any, Port, Profile]))); - false -> - list_to_atom(lists:flatten(io_lib:format("ssh_system_~p_~p_~p_sup", - [Address, Port, Profile]))) - end. + list_to_atom(lists:flatten(io_lib:format("ssh_system_~s_~p_~p_sup", [fmt_host(Address), Port, Profile]))). -ssh_subsystem_sup([{_, Child, _, [ssh_subsystem_sup]} | _]) -> - Child; -ssh_subsystem_sup([_ | Rest]) -> - ssh_subsystem_sup(Rest). +fmt_host(IP) when is_tuple(IP) -> inet:ntoa(IP); +fmt_host(A) when is_atom(A) -> A; +fmt_host(S) when is_list(S) -> S. -ssh_acceptor_sup([{_, Child, _, [ssh_acceptor_sup]} | _]) -> - Child; -ssh_acceptor_sup([_ | Rest]) -> - ssh_acceptor_sup(Rest). -stop_acceptor(Sup) -> - [{Name, AcceptorSup}] = - [{SupName, ASup} || {SupName, ASup, _, [ssh_acceptor_sup]} <- - supervisor:which_children(Sup)], - case supervisor:terminate_child(AcceptorSup, Name) of - ok -> - supervisor:delete_child(AcceptorSup, Name); - Error -> - Error - end. +lookup(SupModule, SystemSup) -> + lists:keyfind([SupModule], 4, + supervisor:which_children(SystemSup)). + diff --git a/lib/ssh/src/ssh_transport.erl b/lib/ssh/src/ssh_transport.erl index 02c995399a..7c7dda7a1e 100644 --- a/lib/ssh/src/ssh_transport.erl +++ b/lib/ssh/src/ssh_transport.erl @@ -38,6 +38,7 @@ handle_hello_version/1, key_exchange_init_msg/1, key_init/3, new_keys_message/1, + ext_info_message/1, handle_kexinit_msg/3, handle_kexdh_init/2, handle_kex_dh_gex_group/2, handle_kex_dh_gex_init/2, handle_kex_dh_gex_reply/2, handle_new_keys/2, handle_kex_dh_gex_request/2, @@ -47,6 +48,7 @@ parallell_gen_key/1, extract_public_key/1, ssh_packet/2, pack/2, + valid_key_sha_alg/2, sha/1, sign/3, verify/4]). %%% For test suites @@ -90,6 +92,10 @@ default_algorithms(cipher) -> default_algorithms(mac) -> supported_algorithms(mac, same(['AEAD_AES_128_GCM', 'AEAD_AES_256_GCM'])); +default_algorithms(public_key) -> + supported_algorithms(public_key, ['rsa-sha2-256', + 'rsa-sha2-384', + 'rsa-sha2-512']); default_algorithms(Alg) -> supported_algorithms(Alg, []). @@ -116,6 +122,9 @@ supported_algorithms(public_key) -> {'ecdsa-sha2-nistp384', [{public_keys,ecdsa}, {hashs,sha384}, {ec_curve,secp384r1}]}, {'ecdsa-sha2-nistp521', [{public_keys,ecdsa}, {hashs,sha512}, {ec_curve,secp521r1}]}, {'ecdsa-sha2-nistp256', [{public_keys,ecdsa}, {hashs,sha256}, {ec_curve,secp256r1}]}, + {'rsa-sha2-256', [{public_keys,rsa}, {hashs,sha256} ]}, + {'rsa-sha2-384', [{public_keys,rsa}, {hashs,sha384} ]}, + {'rsa-sha2-512', [{public_keys,rsa}, {hashs,sha512} ]}, {'ssh-rsa', [{public_keys,rsa}, {hashs,sha} ]}, {'ssh-dss', [{public_keys,dss}, {hashs,sha} ]} % Gone in OpenSSH 7.3.p1 ]); @@ -181,7 +190,7 @@ ssh_vsn() -> end. random_id(Nlo, Nup) -> - [crypto:rand_uniform($a,$z+1) || _<- lists:duplicate(crypto:rand_uniform(Nlo,Nup+1),x) ]. + [$a + rand:uniform($z-$a+1) - 1 || _<- lists:duplicate(Nlo + rand:uniform(Nup-Nlo+1) - 1, x)]. hello_version_msg(Data) -> [Data,"\r\n"]. @@ -200,9 +209,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. @@ -233,7 +239,7 @@ key_exchange_init_msg(Ssh0) -> kex_init(#ssh{role = Role, opts = Opts, available_host_keys = HostKeyAlgs}) -> Random = ssh_bits:random(16), PrefAlgs = ?GET_OPT(preferred_algorithms, Opts), - kexinit_message(Role, Random, PrefAlgs, HostKeyAlgs). + kexinit_message(Role, Random, PrefAlgs, HostKeyAlgs, Opts). key_init(client, Ssh, Value) -> Ssh#ssh{c_keyinit = Value}; @@ -241,10 +247,11 @@ key_init(server, Ssh, Value) -> Ssh#ssh{s_keyinit = Value}. -kexinit_message(_Role, Random, Algs, HostKeyAlgs) -> +kexinit_message(Role, Random, Algs, HostKeyAlgs, Opts) -> #ssh_msg_kexinit{ cookie = Random, - kex_algorithms = to_strings( get_algs(kex,Algs) ), + kex_algorithms = to_strings( get_algs(kex,Algs) ) + ++ kex_ext_info(Role,Opts), server_host_key_algorithms = HostKeyAlgs, encryption_algorithms_client_to_server = c2s(cipher,Algs), encryption_algorithms_server_to_client = s2c(cipher,Algs), @@ -266,39 +273,42 @@ get_algs(Key, Algs) -> proplists:get_value(Key, Algs, default_algorithms(Key)). to_strings(L) -> lists:map(fun erlang:atom_to_list/1, L). new_keys_message(Ssh0) -> - {SshPacket, Ssh} = - ssh_packet(#ssh_msg_newkeys{}, Ssh0), + {SshPacket, Ssh1} = ssh_packet(#ssh_msg_newkeys{}, Ssh0), + Ssh = install_alg(snd, Ssh1), {ok, SshPacket, Ssh}. handle_kexinit_msg(#ssh_msg_kexinit{} = CounterPart, #ssh_msg_kexinit{} = Own, - #ssh{role = client} = Ssh0) -> - {ok, Algoritms} = select_algorithm(client, Own, CounterPart), - case verify_algorithm(Algoritms) of - true -> - key_exchange_first_msg(Algoritms#alg.kex, - Ssh0#ssh{algorithms = Algoritms}); - {false,Alg} -> - %% TODO: Correct code? - ssh_connection_handler:disconnect( - #ssh_msg_disconnect{code = ?SSH_DISCONNECT_KEY_EXCHANGE_FAILED, - description = "Selection of key exchange algorithm failed: " - ++ Alg - }) + #ssh{role = client} = Ssh) -> + try + {ok, Algorithms} = select_algorithm(client, Own, CounterPart, Ssh#ssh.opts), + true = verify_algorithm(Algorithms), + Algorithms + of + Algos -> + key_exchange_first_msg(Algos#alg.kex, + Ssh#ssh{algorithms = Algos}) + catch + _:_ -> + ssh_connection_handler:disconnect( + #ssh_msg_disconnect{code = ?SSH_DISCONNECT_KEY_EXCHANGE_FAILED, + description = "Selection of key exchange algorithm failed"}) end; handle_kexinit_msg(#ssh_msg_kexinit{} = CounterPart, #ssh_msg_kexinit{} = Own, - #ssh{role = server} = Ssh) -> - {ok, Algoritms} = select_algorithm(server, CounterPart, Own), - case verify_algorithm(Algoritms) of - true -> - {ok, Ssh#ssh{algorithms = Algoritms}}; - {false,Alg} -> - ssh_connection_handler:disconnect( - #ssh_msg_disconnect{code = ?SSH_DISCONNECT_KEY_EXCHANGE_FAILED, - description = "Selection of key exchange algorithm failed: " - ++ Alg - }) + #ssh{role = server} = Ssh) -> + try + {ok, Algorithms} = select_algorithm(server, CounterPart, Own, Ssh#ssh.opts), + true = verify_algorithm(Algorithms), + Algorithms + of + Algos -> + {ok, Ssh#ssh{algorithms = Algos}} + catch + _:_ -> + ssh_connection_handler:disconnect( + #ssh_msg_disconnect{code = ?SSH_DISCONNECT_KEY_EXCHANGE_FAILED, + description = "Selection of key exchange algorithm failed"}) end. @@ -311,6 +321,8 @@ verify_algorithm(#alg{decrypt = undefined}) -> {false, "decrypt"}; verify_algorithm(#alg{compress = undefined}) -> {false, "compress"}; verify_algorithm(#alg{decompress = undefined}) -> {false, "decompress"}; verify_algorithm(#alg{kex = Kex}) -> + %% This also catches the error if 'ext-info-s' or 'ext-info-c' is selected. + %% (draft-ietf-curdle-ssh-ext-info-04 2.2) case lists:member(Kex, supported_algorithms(kex)) of true -> true; false -> {false, "kex"} @@ -373,7 +385,8 @@ key_exchange_first_msg(Kex, Ssh0) when Kex == 'ecdh-sha2-nistp256' ; %%% diffie-hellman-group18-sha512 %%% handle_kexdh_init(#ssh_msg_kexdh_init{e = E}, - Ssh0 = #ssh{algorithms = #alg{kex=Kex} = Algs}) -> + Ssh0 = #ssh{algorithms = #alg{kex=Kex, + hkey=SignAlg} = Algs}) -> %% server {G, P} = dh_group(Kex), if @@ -381,12 +394,12 @@ handle_kexdh_init(#ssh_msg_kexdh_init{e = E}, Sz = dh_bits(Algs), {Public, Private} = generate_key(dh, [P,G,2*Sz]), K = compute_key(dh, E, Private, [P,G]), - MyPrivHostKey = get_host_key(Ssh0), + MyPrivHostKey = get_host_key(Ssh0, SignAlg), MyPubHostKey = extract_public_key(MyPrivHostKey), - H = kex_h(Ssh0, MyPubHostKey, E, Public, K), - H_SIG = sign_host_key(Ssh0, MyPrivHostKey, H), + H = kex_hash(Ssh0, MyPubHostKey, SignAlg, sha(Kex), {E,Public,K}), + H_SIG = sign(H, sha(SignAlg), MyPrivHostKey), {SshPacket, Ssh1} = - ssh_packet(#ssh_msg_kexdh_reply{public_host_key = MyPubHostKey, + ssh_packet(#ssh_msg_kexdh_reply{public_host_key = {MyPubHostKey,SignAlg}, f = Public, h_sig = H_SIG }, Ssh0), @@ -407,19 +420,20 @@ handle_kexdh_init(#ssh_msg_kexdh_init{e = E}, handle_kexdh_reply(#ssh_msg_kexdh_reply{public_host_key = PeerPubHostKey, f = F, h_sig = H_SIG}, - #ssh{keyex_key = {{Private, Public}, {G, P}}} = Ssh0) -> + #ssh{keyex_key = {{Private, Public}, {G, P}}, + algorithms = #alg{kex=Kex, + hkey=SignAlg}} = Ssh0) -> %% client if 1=<F, F=<(P-1)-> K = compute_key(dh, F, Private, [P,G]), - H = kex_h(Ssh0, PeerPubHostKey, Public, F, K), - + H = kex_hash(Ssh0, PeerPubHostKey, SignAlg, sha(Kex), {Public,F,K}), case verify_host_key(Ssh0, PeerPubHostKey, H, H_SIG) of ok -> {SshPacket, Ssh} = ssh_packet(#ssh_msg_newkeys{}, Ssh0), - {ok, SshPacket, Ssh#ssh{shared_secret = ssh_bits:mpint(K), - exchanged_hash = H, - session_id = sid(Ssh, H)}}; + {ok, SshPacket, install_alg(snd, Ssh#ssh{shared_secret = ssh_bits:mpint(K), + exchanged_hash = H, + session_id = sid(Ssh, H)})}; Error -> ssh_connection_handler:disconnect( #ssh_msg_disconnect{ @@ -489,7 +503,7 @@ handle_kex_dh_gex_request(#ssh_msg_kex_dh_gex_request_old{n = NBits}, ssh_packet(#ssh_msg_kex_dh_gex_group{p = P, g = G}, Ssh0), {ok, SshPacket, Ssh#ssh{keyex_key = {x, {G, P}}, - keyex_info = {-1, -1, NBits} % flag for kex_h hash calc + keyex_info = {-1, -1, NBits} % flag for kex_hash calc }}; {error,_} -> ssh_connection_handler:disconnect( @@ -535,20 +549,21 @@ handle_kex_dh_gex_group(#ssh_msg_kex_dh_gex_group{p = P, g = G}, Ssh0) -> handle_kex_dh_gex_init(#ssh_msg_kex_dh_gex_init{e = E}, #ssh{keyex_key = {{Private, Public}, {G, P}}, - keyex_info = {Min, Max, NBits}} = - Ssh0) -> + keyex_info = {Min, Max, NBits}, + algorithms = #alg{kex=Kex, + hkey=SignAlg}} = Ssh0) -> %% server if 1=<E, E=<(P-1) -> K = compute_key(dh, E, Private, [P,G]), if 1<K, K<(P-1) -> - MyPrivHostKey = get_host_key(Ssh0), + MyPrivHostKey = get_host_key(Ssh0, SignAlg), MyPubHostKey = extract_public_key(MyPrivHostKey), - H = kex_h(Ssh0, MyPubHostKey, Min, NBits, Max, P, G, E, Public, K), - H_SIG = sign_host_key(Ssh0, MyPrivHostKey, H), + H = kex_hash(Ssh0, MyPubHostKey, SignAlg, sha(Kex), {Min,NBits,Max,P,G,E,Public,K}), + H_SIG = sign(H, sha(SignAlg), MyPrivHostKey), {SshPacket, Ssh} = - ssh_packet(#ssh_msg_kex_dh_gex_reply{public_host_key = MyPubHostKey, + ssh_packet(#ssh_msg_kex_dh_gex_reply{public_host_key = {MyPubHostKey,SignAlg}, f = Public, h_sig = H_SIG}, Ssh0), {ok, SshPacket, Ssh#ssh{shared_secret = ssh_bits:mpint(K), @@ -574,7 +589,9 @@ handle_kex_dh_gex_reply(#ssh_msg_kex_dh_gex_reply{public_host_key = PeerPubHostK f = F, h_sig = H_SIG}, #ssh{keyex_key = {{Private, Public}, {G, P}}, - keyex_info = {Min, Max, NBits}} = + keyex_info = {Min, Max, NBits}, + algorithms = #alg{kex=Kex, + hkey=SignAlg}} = Ssh0) -> %% client if @@ -582,14 +599,13 @@ handle_kex_dh_gex_reply(#ssh_msg_kex_dh_gex_reply{public_host_key = PeerPubHostK K = compute_key(dh, F, Private, [P,G]), if 1<K, K<(P-1) -> - H = kex_h(Ssh0, PeerPubHostKey, Min, NBits, Max, P, G, Public, F, K), - + H = kex_hash(Ssh0, PeerPubHostKey, SignAlg, sha(Kex), {Min,NBits,Max,P,G,Public,F,K}), case verify_host_key(Ssh0, PeerPubHostKey, H, H_SIG) of ok -> {SshPacket, Ssh} = ssh_packet(#ssh_msg_newkeys{}, Ssh0), - {ok, SshPacket, Ssh#ssh{shared_secret = ssh_bits:mpint(K), - exchanged_hash = H, - session_id = sid(Ssh, H)}}; + {ok, SshPacket, install_alg(snd, Ssh#ssh{shared_secret = ssh_bits:mpint(K), + exchanged_hash = H, + session_id = sid(Ssh, H)})}; _Error -> ssh_connection_handler:disconnect( #ssh_msg_disconnect{ @@ -619,7 +635,8 @@ handle_kex_dh_gex_reply(#ssh_msg_kex_dh_gex_reply{public_host_key = PeerPubHostK %%% diffie-hellman-ecdh-sha2-* %%% handle_kex_ecdh_init(#ssh_msg_kex_ecdh_init{q_c = PeerPublic}, - Ssh0 = #ssh{algorithms = #alg{kex=Kex}}) -> + Ssh0 = #ssh{algorithms = #alg{kex=Kex, + hkey=SignAlg}}) -> %% at server Curve = ecdh_curve(Kex), {MyPublic, MyPrivate} = generate_key(ecdh, Curve), @@ -627,12 +644,12 @@ handle_kex_ecdh_init(#ssh_msg_kex_ecdh_init{q_c = PeerPublic}, compute_key(ecdh, PeerPublic, MyPrivate, Curve) of K -> - MyPrivHostKey = get_host_key(Ssh0), + MyPrivHostKey = get_host_key(Ssh0, SignAlg), MyPubHostKey = extract_public_key(MyPrivHostKey), - H = kex_h(Ssh0, Curve, MyPubHostKey, PeerPublic, MyPublic, K), - H_SIG = sign_host_key(Ssh0, MyPrivHostKey, H), + H = kex_hash(Ssh0, MyPubHostKey, SignAlg, sha(Curve), {PeerPublic, MyPublic, K}), + H_SIG = sign(H, sha(SignAlg), MyPrivHostKey), {SshPacket, Ssh1} = - ssh_packet(#ssh_msg_kex_ecdh_reply{public_host_key = MyPubHostKey, + ssh_packet(#ssh_msg_kex_ecdh_reply{public_host_key = {MyPubHostKey,SignAlg}, q_s = MyPublic, h_sig = H_SIG}, Ssh0), @@ -652,20 +669,21 @@ handle_kex_ecdh_init(#ssh_msg_kex_ecdh_init{q_c = PeerPublic}, handle_kex_ecdh_reply(#ssh_msg_kex_ecdh_reply{public_host_key = PeerPubHostKey, q_s = PeerPublic, h_sig = H_SIG}, - #ssh{keyex_key = {{MyPublic,MyPrivate}, Curve}} = Ssh0 + #ssh{keyex_key = {{MyPublic,MyPrivate}, Curve}, + algorithms = #alg{hkey=SignAlg}} = Ssh0 ) -> %% at client try compute_key(ecdh, PeerPublic, MyPrivate, Curve) of K -> - H = kex_h(Ssh0, Curve, PeerPubHostKey, MyPublic, PeerPublic, K), + H = kex_hash(Ssh0, PeerPubHostKey, SignAlg, sha(Curve), {MyPublic,PeerPublic,K}), case verify_host_key(Ssh0, PeerPubHostKey, H, H_SIG) of ok -> {SshPacket, Ssh} = ssh_packet(#ssh_msg_newkeys{}, Ssh0), - {ok, SshPacket, Ssh#ssh{shared_secret = ssh_bits:mpint(K), - exchanged_hash = H, - session_id = sid(Ssh, H)}}; + {ok, SshPacket, install_alg(snd, Ssh#ssh{shared_secret = ssh_bits:mpint(K), + exchanged_hash = H, + session_id = sid(Ssh, H)})}; Error -> ssh_connection_handler:disconnect( #ssh_msg_disconnect{ @@ -685,7 +703,7 @@ handle_kex_ecdh_reply(#ssh_msg_kex_ecdh_reply{public_host_key = PeerPubHostKey, %%%---------------------------------------------------------------- handle_new_keys(#ssh_msg_newkeys{}, Ssh0) -> - try install_alg(Ssh0) of + try install_alg(rcv, Ssh0) of #ssh{} = Ssh -> {ok, Ssh} catch @@ -696,34 +714,49 @@ handle_new_keys(#ssh_msg_newkeys{}, Ssh0) -> }) end. + +%%%---------------------------------------------------------------- +kex_ext_info(Role, Opts) -> + case ?GET_OPT(recv_ext_info,Opts) of + true when Role==client -> ["ext-info-c"]; + true when Role==server -> ["ext-info-s"]; + false -> [] + end. + +ext_info_message(#ssh{role=client, + send_ext_info=true} = Ssh0) -> + %% FIXME: no extensions implemented + {ok, "", Ssh0}; + +ext_info_message(#ssh{role=server, + send_ext_info=true} = Ssh0) -> + AlgsList = lists:map(fun erlang:atom_to_list/1, + ssh_transport:default_algorithms(public_key)), + Msg = #ssh_msg_ext_info{nr_extensions = 1, + data = [{"server-sig-algs", string:join(AlgsList,",")}] + }, + {SshPacket, Ssh} = ssh_packet(Msg, Ssh0), + {ok, SshPacket, Ssh}; + +ext_info_message(Ssh0) -> + {ok, "", Ssh0}. % "" means: 'do not send' + +%%%---------------------------------------------------------------- %% select session id -sid(#ssh{session_id = undefined}, H) -> - H; -sid(#ssh{session_id = Id}, _) -> - Id. +sid(#ssh{session_id = undefined}, H) -> H; +sid(#ssh{session_id = Id}, _) -> Id. %% %% The host key should be read from storage %% -get_host_key(SSH) -> - #ssh{key_cb = {KeyCb,KeyCbOpts}, opts = Opts, algorithms = ALG} = SSH, +get_host_key(SSH, SignAlg) -> + #ssh{key_cb = {KeyCb,KeyCbOpts}, opts = Opts} = SSH, UserOpts = ?GET_OPT(user_options, Opts), - case KeyCb:host_key(ALG#alg.hkey, [{key_cb_private,KeyCbOpts}|UserOpts]) of - {ok, #'RSAPrivateKey'{} = Key} -> Key; - {ok, #'DSAPrivateKey'{} = Key} -> Key; - {ok, #'ECPrivateKey'{} = Key} -> Key; - Result -> - exit({error, {Result, unsupported_key_type}}) + case KeyCb:host_key(SignAlg, [{key_cb_private,KeyCbOpts}|UserOpts]) of + {ok, PrivHostKey} -> PrivHostKey; + Result -> exit({error, {Result, unsupported_key_type}}) end. -sign_host_key(_Ssh, PrivateKey, H) -> - sign(H, sign_host_key_sha(PrivateKey), PrivateKey). - -sign_host_key_sha(#'ECPrivateKey'{parameters = {namedCurve,OID}}) -> sha(OID); -sign_host_key_sha(#'RSAPrivateKey'{}) -> sha; -sign_host_key_sha(#'DSAPrivateKey'{}) -> sha. - - extract_public_key(#'RSAPrivateKey'{modulus = N, publicExponent = E}) -> #'RSAPublicKey'{modulus = N, publicExponent = E}; extract_public_key(#'DSAPrivateKey'{y = Y, p = P, q = Q, g = G}) -> @@ -733,8 +766,8 @@ extract_public_key(#'ECPrivateKey'{parameters = {namedCurve,OID}, {#'ECPoint'{point=Q}, {namedCurve,OID}}. -verify_host_key(SSH, PublicKey, Digest, Signature) -> - case verify(Digest, host_key_sha(PublicKey), Signature, PublicKey) of +verify_host_key(#ssh{algorithms=Alg}=SSH, PublicKey, Digest, Signature) -> + case verify(Digest, sha(Alg#alg.hkey), Signature, PublicKey) of false -> {error, bad_signature}; true -> @@ -742,29 +775,47 @@ verify_host_key(SSH, PublicKey, Digest, Signature) -> end. -host_key_sha(#'RSAPublicKey'{}) -> sha; -host_key_sha({_, #'Dss-Parms'{}}) -> sha; -host_key_sha({#'ECPoint'{},{namedCurve,OID}}) -> sha(OID). - -public_algo(#'RSAPublicKey'{}) -> 'ssh-rsa'; -public_algo({_, #'Dss-Parms'{}}) -> 'ssh-dss'; -public_algo({#'ECPoint'{},{namedCurve,OID}}) -> - Curve = public_key:oid2ssh_curvename(OID), - list_to_atom("ecdsa-sha2-" ++ binary_to_list(Curve)). - 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), @@ -787,7 +838,7 @@ known_host_key(#ssh{opts = Opts, key_cb = {KeyCb,KeyCbOpts}, peer = {PeerName,_} %% %% The first algorithm in each list MUST be the preferred (guessed) %% algorithm. Each string MUST contain at least one algorithm name. -select_algorithm(Role, Client, Server) -> +select_algorithm(Role, Client, Server, Opts) -> {Encrypt0, Decrypt0} = select_encrypt_decrypt(Role, Client, Server), {SendMac0, RecvMac0} = select_send_recv_mac(Role, Client, Server), @@ -812,17 +863,34 @@ select_algorithm(Role, Client, Server) -> Kex = select(Client#ssh_msg_kexinit.kex_algorithms, Server#ssh_msg_kexinit.kex_algorithms), - Alg = #alg{kex = Kex, - hkey = HK, - encrypt = Encrypt, - decrypt = Decrypt, - send_mac = SendMac, - recv_mac = RecvMac, - compress = Compression, - decompress = Decompression, - c_lng = C_Lng, - s_lng = S_Lng}, - {ok, Alg}. + SendExtInfo = + %% To send we must have that option enabled and ... + ?GET_OPT(send_ext_info,Opts) andalso + %% ... the peer must have told us to send: + case Role of + server -> lists:member("ext-info-c", Client#ssh_msg_kexinit.kex_algorithms); + client -> lists:member("ext-info-s", Server#ssh_msg_kexinit.kex_algorithms) + end, + + RecvExtInfo = + %% The peer should not send unless told so by us (which is + %% guided by an option). + %% (However a malicious peer could send anyway, so we must be prepared) + ?GET_OPT(recv_ext_info,Opts), + + {ok, #alg{kex = Kex, + hkey = HK, + encrypt = Encrypt, + decrypt = Decrypt, + send_mac = SendMac, + recv_mac = RecvMac, + compress = Compression, + decompress = Decompression, + c_lng = C_Lng, + s_lng = S_Lng, + send_ext_info = SendExtInfo, + recv_ext_info = RecvExtInfo + }}. %%% It is an agreed problem with RFC 5674 that if the selection is @@ -903,45 +971,66 @@ select_compression_decompression(server, Client, Server) -> Server#ssh_msg_kexinit.compression_algorithms_server_to_client), {Compression, Decompression}. -install_alg(SSH) -> - SSH1 = alg_final(SSH), - SSH2 = alg_setup(SSH1), - alg_init(SSH2). +%% DIr = rcv | snd +install_alg(Dir, SSH) -> + SSH1 = alg_final(Dir, SSH), + SSH2 = alg_setup(Dir, SSH1), + alg_init(Dir, SSH2). -alg_setup(SSH) -> +alg_setup(snd, SSH) -> ALG = SSH#ssh.algorithms, SSH#ssh{kex = ALG#alg.kex, hkey = ALG#alg.hkey, encrypt = ALG#alg.encrypt, - decrypt = ALG#alg.decrypt, send_mac = ALG#alg.send_mac, send_mac_size = mac_digest_size(ALG#alg.send_mac), + compress = ALG#alg.compress, + c_lng = ALG#alg.c_lng, + s_lng = ALG#alg.s_lng, + send_ext_info = ALG#alg.send_ext_info, + recv_ext_info = ALG#alg.recv_ext_info + }; + +alg_setup(rcv, SSH) -> + ALG = SSH#ssh.algorithms, + SSH#ssh{kex = ALG#alg.kex, + hkey = ALG#alg.hkey, + decrypt = ALG#alg.decrypt, recv_mac = ALG#alg.recv_mac, recv_mac_size = mac_digest_size(ALG#alg.recv_mac), - compress = ALG#alg.compress, decompress = ALG#alg.decompress, c_lng = ALG#alg.c_lng, s_lng = ALG#alg.s_lng, - algorithms = undefined + send_ext_info = ALG#alg.send_ext_info, + recv_ext_info = ALG#alg.recv_ext_info }. -alg_init(SSH0) -> + +alg_init(snd, SSH0) -> {ok,SSH1} = send_mac_init(SSH0), - {ok,SSH2} = recv_mac_init(SSH1), - {ok,SSH3} = encrypt_init(SSH2), - {ok,SSH4} = decrypt_init(SSH3), - {ok,SSH5} = compress_init(SSH4), - {ok,SSH6} = decompress_init(SSH5), - SSH6. - -alg_final(SSH0) -> + {ok,SSH2} = encrypt_init(SSH1), + {ok,SSH3} = compress_init(SSH2), + SSH3; + +alg_init(rcv, SSH0) -> + {ok,SSH1} = recv_mac_init(SSH0), + {ok,SSH2} = decrypt_init(SSH1), + {ok,SSH3} = decompress_init(SSH2), + SSH3. + + +alg_final(snd, SSH0) -> {ok,SSH1} = send_mac_final(SSH0), - {ok,SSH2} = recv_mac_final(SSH1), - {ok,SSH3} = encrypt_final(SSH2), - {ok,SSH4} = decrypt_final(SSH3), - {ok,SSH5} = compress_final(SSH4), - {ok,SSH6} = decompress_final(SSH5), - SSH6. + {ok,SSH2} = encrypt_final(SSH1), + {ok,SSH3} = compress_final(SSH2), + SSH3; + +alg_final(rcv, SSH0) -> + {ok,SSH1} = recv_mac_final(SSH0), + {ok,SSH2} = decrypt_final(SSH1), + {ok,SSH3} = decompress_final(SSH2), + SSH3. + select_all(CL, SL) when length(CL) + length(SL) < ?MAX_NUM_ALGORITHMS -> A = CL -- SL, %% algortihms only used by client @@ -1016,7 +1105,7 @@ padding_length(Size, #ssh{encrypt_block_size = BlockSize, end, PadBlockSize = max(BlockSize,4), MaxExtraBlocks = (max(RandomLengthPadding,MinPaddingLen) - MinPaddingLen) div PadBlockSize, - ExtraPaddingLen = try crypto:rand_uniform(0,MaxExtraBlocks)*PadBlockSize + ExtraPaddingLen = try (rand:uniform(MaxExtraBlocks+1) - 1) * PadBlockSize catch _:_ -> 0 end, MinPaddingLen + ExtraPaddingLen. @@ -1105,29 +1194,37 @@ payload(<<PacketLen:32, PaddingLen:8, PayloadAndPadding/binary>>) -> <<Payload:PayloadLen/binary, _/binary>> = PayloadAndPadding, Payload. -sign(SigData, Hash, #'DSAPrivateKey'{} = Key) -> - DerSignature = public_key:sign(SigData, Hash, Key), +sign(SigData, HashAlg, #'DSAPrivateKey'{} = Key) -> + DerSignature = public_key:sign(SigData, HashAlg, Key), #'Dss-Sig-Value'{r = R, s = S} = public_key:der_decode('Dss-Sig-Value', DerSignature), <<R:160/big-unsigned-integer, S:160/big-unsigned-integer>>; -sign(SigData, Hash, Key = #'ECPrivateKey'{}) -> - DerEncodedSign = public_key:sign(SigData, Hash, Key), +sign(SigData, HashAlg, Key = #'ECPrivateKey'{}) -> + DerEncodedSign = public_key:sign(SigData, HashAlg, Key), #'ECDSA-Sig-Value'{r=R, s=S} = public_key:der_decode('ECDSA-Sig-Value', DerEncodedSign), <<?Empint(R),?Empint(S)>>; -sign(SigData, Hash, Key) -> - public_key:sign(SigData, Hash, Key). - -verify(PlainText, Hash, Sig, {_, #'Dss-Parms'{}} = Key) -> - <<R:160/big-unsigned-integer, S:160/big-unsigned-integer>> = Sig, - Signature = public_key:der_encode('Dss-Sig-Value', #'Dss-Sig-Value'{r = R, s = S}), - public_key:verify(PlainText, Hash, Signature, Key); -verify(PlainText, Hash, Sig, {#'ECPoint'{},_} = Key) -> - <<?UINT32(Rlen),R:Rlen/big-signed-integer-unit:8, - ?UINT32(Slen),S:Slen/big-signed-integer-unit:8>> = Sig, - Sval = #'ECDSA-Sig-Value'{r=R, s=S}, - DerEncodedSig = public_key:der_encode('ECDSA-Sig-Value',Sval), - public_key:verify(PlainText, Hash, DerEncodedSig, Key); -verify(PlainText, Hash, Sig, Key) -> - public_key:verify(PlainText, Hash, Sig, Key). +sign(SigData, HashAlg, Key) -> + public_key:sign(SigData, HashAlg, Key). + +verify(PlainText, HashAlg, Sig, {_, #'Dss-Parms'{}} = Key) -> + case Sig of + <<R:160/big-unsigned-integer, S:160/big-unsigned-integer>> -> + Signature = public_key:der_encode('Dss-Sig-Value', #'Dss-Sig-Value'{r = R, s = S}), + public_key:verify(PlainText, HashAlg, Signature, Key); + _ -> + false + end; +verify(PlainText, HashAlg, Sig, {#'ECPoint'{},_} = Key) -> + case Sig of + <<?UINT32(Rlen),R:Rlen/big-signed-integer-unit:8, + ?UINT32(Slen),S:Slen/big-signed-integer-unit:8>> -> + Sval = #'ECDSA-Sig-Value'{r=R, s=S}, + DerEncodedSig = public_key:der_encode('ECDSA-Sig-Value',Sval), + public_key:verify(PlainText, HashAlg, DerEncodedSig, Key); + _ -> + false + end; +verify(PlainText, HashAlg, Sig, Key) -> + public_key:verify(PlainText, HashAlg, Sig, Key). %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% @@ -1634,39 +1731,63 @@ hash(K, H, Ki, N, HashAlg) -> hash(K, H, <<Ki/binary, Kj/binary>>, N-128, HashAlg). %%%---------------------------------------------------------------- -kex_h(SSH, Key, E, F, K) -> - KeyBin = public_key:ssh_encode(Key, ssh2_pubkey), - L = <<?Estring(SSH#ssh.c_version), ?Estring(SSH#ssh.s_version), - ?Ebinary(SSH#ssh.c_keyinit), ?Ebinary(SSH#ssh.s_keyinit), ?Ebinary(KeyBin), - ?Empint(E), ?Empint(F), ?Empint(K)>>, - crypto:hash(sha((SSH#ssh.algorithms)#alg.kex), L). - -kex_h(SSH, Curve, Key, Q_c, Q_s, K) -> - KeyBin = public_key:ssh_encode(Key, ssh2_pubkey), - L = <<?Estring(SSH#ssh.c_version), ?Estring(SSH#ssh.s_version), - ?Ebinary(SSH#ssh.c_keyinit), ?Ebinary(SSH#ssh.s_keyinit), ?Ebinary(KeyBin), - ?Empint(Q_c), ?Empint(Q_s), ?Empint(K)>>, - crypto:hash(sha(Curve), L). - -kex_h(SSH, Key, Min, NBits, Max, Prime, Gen, E, F, K) -> - KeyBin = public_key:ssh_encode(Key, ssh2_pubkey), - L = if Min==-1; Max==-1 -> - %% flag from 'ssh_msg_kex_dh_gex_request_old' - %% It was like this before that message was supported, - %% why? - <<?Estring(SSH#ssh.c_version), ?Estring(SSH#ssh.s_version), - ?Ebinary(SSH#ssh.c_keyinit), ?Ebinary(SSH#ssh.s_keyinit), ?Ebinary(KeyBin), - ?Empint(E), ?Empint(F), ?Empint(K)>>; - true -> - <<?Estring(SSH#ssh.c_version), ?Estring(SSH#ssh.s_version), - ?Ebinary(SSH#ssh.c_keyinit), ?Ebinary(SSH#ssh.s_keyinit), ?Ebinary(KeyBin), - ?Euint32(Min), ?Euint32(NBits), ?Euint32(Max), - ?Empint(Prime), ?Empint(Gen), ?Empint(E), ?Empint(F), ?Empint(K)>> - end, - crypto:hash(sha((SSH#ssh.algorithms)#alg.kex), L). - +kex_hash(SSH, Key, SignAlg, HashAlg, Args) -> + crypto:hash(HashAlg, kex_plaintext(SSH,Key,SignAlg,Args)). + +kex_plaintext(SSH, Key, SignAlg, Args) -> + EncodedKey = public_key:ssh_encode({Key,SignAlg}, ssh2_pubkey), + <<?Estring(SSH#ssh.c_version), ?Estring(SSH#ssh.s_version), + ?Ebinary(SSH#ssh.c_keyinit), ?Ebinary(SSH#ssh.s_keyinit), + ?Ebinary(EncodedKey), + (kex_alg_dependent(Args))/binary>>. + +kex_alg_dependent({E, F, K}) -> + %% diffie-hellman and ec diffie-hellman (with E = Q_c, F = Q_s) + <<?Empint(E), ?Empint(F), ?Empint(K)>>; + +kex_alg_dependent({-1, _, -1, _, _, E, F, K}) -> + %% ssh_msg_kex_dh_gex_request_old + <<?Empint(E), ?Empint(F), ?Empint(K)>>; + +kex_alg_dependent({Min, NBits, Max, Prime, Gen, E, F, K}) -> + %% diffie-hellman group exchange + <<?Euint32(Min), ?Euint32(NBits), ?Euint32(Max), + ?Empint(Prime), ?Empint(Gen), ?Empint(E), ?Empint(F), ?Empint(K)>>. + +%%%---------------------------------------------------------------- + +valid_key_sha_alg(#'RSAPublicKey'{}, 'rsa-sha2-512') -> true; +valid_key_sha_alg(#'RSAPublicKey'{}, 'rsa-sha2-384') -> true; +valid_key_sha_alg(#'RSAPublicKey'{}, 'rsa-sha2-256') -> true; +valid_key_sha_alg(#'RSAPublicKey'{}, 'ssh-rsa' ) -> true; + +valid_key_sha_alg(#'RSAPrivateKey'{}, 'rsa-sha2-512') -> true; +valid_key_sha_alg(#'RSAPrivateKey'{}, 'rsa-sha2-384') -> true; +valid_key_sha_alg(#'RSAPrivateKey'{}, 'rsa-sha2-256') -> true; +valid_key_sha_alg(#'RSAPrivateKey'{}, 'ssh-rsa' ) -> true; + +valid_key_sha_alg({_, #'Dss-Parms'{}}, 'ssh-dss') -> true; +valid_key_sha_alg(#'DSAPrivateKey'{}, 'ssh-dss') -> true; + +valid_key_sha_alg({#'ECPoint'{},{namedCurve,OID}}, Alg) -> sha(OID) == sha(Alg); +valid_key_sha_alg(#'ECPrivateKey'{parameters = {namedCurve,OID}}, Alg) -> sha(OID) == sha(Alg); +valid_key_sha_alg(_, _) -> false. + + + +public_algo(#'RSAPublicKey'{}) -> 'ssh-rsa'; % FIXME: Not right with draft-curdle-rsa-sha2 +public_algo({_, #'Dss-Parms'{}}) -> 'ssh-dss'; +public_algo({#'ECPoint'{},{namedCurve,OID}}) -> + Curve = public_key:oid2ssh_curvename(OID), + list_to_atom("ecdsa-sha2-" ++ binary_to_list(Curve)). + + + sha('ssh-rsa') -> sha; +sha('rsa-sha2-256') -> sha256; +sha('rsa-sha2-384') -> sha384; +sha('rsa-sha2-512') -> sha512; sha('ssh-dss') -> sha; sha('ecdsa-sha2-nistp256') -> sha(secp256r1); sha('ecdsa-sha2-nistp384') -> sha(secp384r1); @@ -1686,7 +1807,8 @@ sha(?'secp384r1') -> sha(secp384r1); sha(?'secp521r1') -> sha(secp521r1); sha('ecdh-sha2-nistp256') -> sha(secp256r1); sha('ecdh-sha2-nistp384') -> sha(secp384r1); -sha('ecdh-sha2-nistp521') -> sha(secp521r1). +sha('ecdh-sha2-nistp521') -> sha(secp521r1); +sha(Str) when is_list(Str), length(Str)<50 -> sha(list_to_atom(Str)). mac_key_bytes('hmac-sha1') -> 20; diff --git a/lib/ssh/src/ssh_transport.hrl b/lib/ssh/src/ssh_transport.hrl index 19b3f5c437..faae6008f2 100644 --- a/lib/ssh/src/ssh_transport.hrl +++ b/lib/ssh/src/ssh_transport.hrl @@ -48,6 +48,7 @@ -define(SSH_MSG_DEBUG, 4). -define(SSH_MSG_SERVICE_REQUEST, 5). -define(SSH_MSG_SERVICE_ACCEPT, 6). +-define(SSH_MSG_EXT_INFO, 7). -define(SSH_MSG_KEXINIT, 20). -define(SSH_MSG_NEWKEYS, 21). @@ -88,6 +89,20 @@ name %% string }). +-record(ssh_msg_ext_info, + { + nr_extensions, %% uint32 + + %% repeat the following 2 fields "nr-extensions" times: + %% string extension-name + %% string extension-value + + data %% [{extension-name, %% string + %% extension-value}, %% string + %% ... + %% ] + }). + -record(ssh_msg_kexinit, { cookie, %% random(16) diff --git a/lib/ssh/src/sshc_sup.erl b/lib/ssh/src/sshc_sup.erl index 15858f36e1..c71b81dc6d 100644 --- a/lib/ssh/src/sshc_sup.erl +++ b/lib/ssh/src/sshc_sup.erl @@ -27,23 +27,25 @@ -behaviour(supervisor). --export([start_link/1, start_child/1, stop_child/1]). +-export([start_link/0, start_child/1, stop_child/1]). %% Supervisor callback -export([init/1]). +-define(SSHC_SUP, ?MODULE). + %%%========================================================================= %%% API %%%========================================================================= -start_link(Args) -> - supervisor:start_link({local, ?MODULE}, ?MODULE, [Args]). +start_link() -> + supervisor:start_link({local,?SSHC_SUP}, ?MODULE, []). start_child(Args) -> supervisor:start_child(?MODULE, Args). stop_child(Client) -> spawn(fun() -> - ClientSup = whereis(?MODULE), + ClientSup = whereis(?SSHC_SUP), supervisor:terminate_child(ClientSup, Client) end), ok. @@ -51,22 +53,17 @@ stop_child(Client) -> %%%========================================================================= %%% Supervisor callback %%%========================================================================= --spec init( [term()] ) -> {ok,{supervisor:sup_flags(),[supervisor:child_spec()]}} | ignore . - -init(Args) -> - RestartStrategy = simple_one_for_one, - MaxR = 0, - MaxT = 3600, - {ok, {{RestartStrategy, MaxR, MaxT}, [child_spec(Args)]}}. - -%%%========================================================================= -%%% Internal functions -%%%========================================================================= -child_spec(_) -> - Name = undefined, % As simple_one_for_one is used. - StartFunc = {ssh_connection_handler, start_link, []}, - Restart = temporary, - Shutdown = 4000, - Modules = [ssh_connection_handler], - Type = worker, - {Name, StartFunc, Restart, Shutdown, Type, Modules}. +init(_) -> + SupFlags = #{strategy => simple_one_for_one, + intensity => 0, + period => 3600 + }, + ChildSpecs = [#{id => undefined, % As simple_one_for_one is used. + start => {ssh_connection_handler, start_link, []}, + restart => temporary, + shutdown => 4000, + type => worker, + modules => [ssh_connection_handler] + } + ], + {ok, {SupFlags,ChildSpecs}}. diff --git a/lib/ssh/src/sshd_sup.erl b/lib/ssh/src/sshd_sup.erl index 14f1937abd..449ba20d02 100644 --- a/lib/ssh/src/sshd_sup.erl +++ b/lib/ssh/src/sshd_sup.erl @@ -19,7 +19,7 @@ %% %% %%---------------------------------------------------------------------- -%% Purpose: The top supervisor for ssh servers hangs under +%% Purpose: The top supervisor for ssh servers hangs under %% ssh_sup. %%---------------------------------------------------------------------- @@ -29,90 +29,79 @@ -include("ssh.hrl"). --export([start_link/1, start_child/1, stop_child/1, - stop_child/3, system_name/1]). +-export([start_link/0, + start_child/4, + stop_child/1, + stop_child/3 +]). %% Supervisor callback -export([init/1]). +-define(SSHD_SUP, ?MODULE). + %%%========================================================================= %%% API %%%========================================================================= -start_link(Servers) -> - supervisor:start_link({local, ?MODULE}, ?MODULE, [Servers]). +start_link() -> + %% No children are start now. We wait until the user calls ssh:daemon + %% and uses start_child/4 to create the children + supervisor:start_link({local,?SSHD_SUP}, ?MODULE, []). -start_child(Options) -> - Address = ?GET_INTERNAL_OPT(address, Options), - Port = ?GET_INTERNAL_OPT(port, Options), - Profile = ?GET_OPT(profile, Options), +start_child(Address, Port, Profile, Options) -> case ssh_system_sup:system_supervisor(Address, Port, Profile) of undefined -> - Spec = child_spec(Address, Port, Options), - case supervisor:start_child(?MODULE, Spec) of - {error, already_present} -> - Name = id(Address, Port, Profile), - supervisor:delete_child(?MODULE, Name), - supervisor:start_child(?MODULE, Spec); - Reply -> - Reply - end; + %% Here we start listening on a new Host/Port/Profile + Spec = child_spec(Address, Port, Profile, Options), + supervisor:start_child(?SSHD_SUP, Spec); Pid -> + %% Here we resume listening on a new Host/Port/Profile after + %% haveing stopped listening to he same with ssh:stop_listen(Pid) AccPid = ssh_system_sup:acceptor_supervisor(Pid), - ssh_acceptor_sup:start_child(AccPid, Options) + ssh_acceptor_sup:start_child(AccPid, Address, Port, Profile, Options), + {ok,Pid} end. -stop_child(Name) -> - supervisor:terminate_child(?MODULE, Name). +stop_child(ChildId) when is_tuple(ChildId) -> + supervisor:terminate_child(?SSHD_SUP, ChildId); +stop_child(ChildPid) when is_pid(ChildPid)-> + stop_child(system_name(ChildPid)). -stop_child(Address, Port, Profile) -> - Name = id(Address, Port, Profile), - stop_child(Name). -system_name(SysSup) -> - Children = supervisor:which_children(sshd_sup), - system_name(SysSup, Children). +stop_child(Address, Port, Profile) -> + Id = id(Address, Port, Profile), + stop_child(Id). %%%========================================================================= %%% Supervisor callback %%%========================================================================= --spec init( [term()] ) -> {ok,{supervisor:sup_flags(),[supervisor:child_spec()]}} | ignore . - -init([Servers]) -> - RestartStrategy = one_for_one, - MaxR = 10, - MaxT = 3600, - Fun = fun(ServerOpts) -> - Address = ?GET_INTERNAL_OPT(address, ServerOpts), - Port = ?GET_INTERNAL_OPT(port, ServerOpts), - child_spec(Address, Port, ServerOpts) - end, - Children = lists:map(Fun, Servers), - {ok, {{RestartStrategy, MaxR, MaxT}, Children}}. +init(_) -> + SupFlags = #{strategy => one_for_one, + intensity => 10, + period => 3600 + }, + ChildSpecs = [ + ], + {ok, {SupFlags,ChildSpecs}}. %%%========================================================================= %%% Internal functions %%%========================================================================= -child_spec(Address, Port, Options) -> - Profile = ?GET_OPT(profile, Options), - Name = id(Address, Port,Profile), - StartFunc = {ssh_system_sup, start_link, [Options]}, - Restart = temporary, - Shutdown = infinity, - Modules = [ssh_system_sup], - Type = supervisor, - {Name, StartFunc, Restart, Shutdown, Type, Modules}. +child_spec(Address, Port, Profile, Options) -> + #{id => id(Address, Port, Profile), + start => {ssh_system_sup, start_link, [Address, Port, Profile, Options]}, + restart => temporary, + shutdown => infinity, + type => supervisor, + modules => [ssh_system_sup] + }. id(Address, Port, Profile) -> - case is_list(Address) of - true -> - {server, ssh_system_sup, any, Port, Profile}; - false -> - {server, ssh_system_sup, Address, Port, Profile} + {server, ssh_system_sup, Address, Port, Profile}. + +system_name(SysSup) -> + case lists:keyfind(SysSup, 2, supervisor:which_children(?SSHD_SUP)) of + {Name, SysSup, _, _} -> Name; + false -> undefind end. -system_name([], _ ) -> - undefined; -system_name(SysSup, [{Name, SysSup, _, _} | _]) -> - Name; -system_name(SysSup, [_ | Rest]) -> - system_name(SysSup, Rest). diff --git a/lib/ssh/test/ssh_algorithms_SUITE.erl b/lib/ssh/test/ssh_algorithms_SUITE.erl index 6f75d83c4a..2990d1e02a 100644 --- a/lib/ssh/test/ssh_algorithms_SUITE.erl +++ b/lib/ssh/test/ssh_algorithms_SUITE.erl @@ -235,13 +235,12 @@ sshc_simple_exec_os_cmd(Config) -> Parent = self(), Client = spawn( fun() -> - Cmd = lists:concat(["ssh -p ",Port, - " -C" - " -o UserKnownHostsFile=",KnownHosts, - " -o StrictHostKeyChecking=no" - " ",Host," 1+1."]), - Result = os:cmd(Cmd), - ct:log("~p~n = ~p",[Cmd, Result]), + Result = ssh_test_lib:open_sshc(Host, Port, + [" -C" + " -o UserKnownHostsFile=",KnownHosts, + " -o StrictHostKeyChecking=no" + ], + " 1+1."), Parent ! {result, self(), Result, "2"} end), receive diff --git a/lib/ssh/test/ssh_basic_SUITE.erl b/lib/ssh/test/ssh_basic_SUITE.erl index a9b6be222e..c271ff49ef 100644 --- a/lib/ssh/test/ssh_basic_SUITE.erl +++ b/lib/ssh/test/ssh_basic_SUITE.erl @@ -651,6 +651,7 @@ exec_key_differs_fail(Config) when is_list(Config) -> IO = ssh_test_lib:start_io_server(), ssh_test_lib:start_shell(Port, IO, [{user_dir,UserDir}, + {recv_ext_info, false}, {preferred_algorithms,[{public_key,['ssh-rsa']}]}, {pref_public_key_algs,['ssh-dss']}]), receive @@ -742,7 +743,8 @@ known_hosts(Config) when is_list(Config) -> Lines = string:tokens(binary_to_list(Binary), "\n"), [Line] = Lines, [HostAndIp, Alg, _KeyData] = string:tokens(Line, " "), - [Host, _Ip] = string:tokens(HostAndIp, ","), + [StoredHost, _Ip] = string:tokens(HostAndIp, ","), + true = ssh_test_lib:match_ip(StoredHost, Host), "ssh-" ++ _ = Alg, ssh:stop_daemon(Pid). %%-------------------------------------------------------------------- @@ -1365,13 +1367,25 @@ new_do_shell(IO, N, Ops=[{Order,Arg}|More]) -> ct:log("Skip newline ~p",[_X]), new_do_shell(IO, N, Ops); - <<Pfx:PfxSize/binary,P1,"> ">> when (P1-$0)==N -> + <<P1,"> ">> when (P1-$0)==N -> + new_do_shell_prompt(IO, N, Order, Arg, More); + <<"(",Pfx:PfxSize/binary,")",P1,"> ">> when (P1-$0)==N -> + new_do_shell_prompt(IO, N, Order, Arg, More); + <<"('",Pfx:PfxSize/binary,"')",P1,"> ">> when (P1-$0)==N -> new_do_shell_prompt(IO, N, Order, Arg, More); - <<Pfx:PfxSize/binary,P1,P2,"> ">> when (P1-$0)*10 + (P2-$0) == N -> + <<P1,P2,"> ">> when (P1-$0)*10 + (P2-$0) == N -> + new_do_shell_prompt(IO, N, Order, Arg, More); + <<"(",Pfx:PfxSize/binary,")",P1,P2,"> ">> when (P1-$0)*10 + (P2-$0) == N -> + new_do_shell_prompt(IO, N, Order, Arg, More); + <<"('",Pfx:PfxSize/binary,"')",P1,P2,"> ">> when (P1-$0)*10 + (P2-$0) == N -> new_do_shell_prompt(IO, N, Order, Arg, More); - <<Pfx:PfxSize/binary,P1,P2,P3,"> ">> when (P1-$0)*100 + (P2-$0)*10 + (P3-$0) == N -> + <<P1,P2,P3,"> ">> when (P1-$0)*100 + (P2-$0)*10 + (P3-$0) == N -> + new_do_shell_prompt(IO, N, Order, Arg, More); + <<"(",Pfx:PfxSize/binary,")",P1,P2,P3,"> ">> when (P1-$0)*100 + (P2-$0)*10 + (P3-$0) == N -> + new_do_shell_prompt(IO, N, Order, Arg, More); + <<"('",Pfx:PfxSize/binary,"')",P1,P2,P3,"> ">> when (P1-$0)*100 + (P2-$0)*10 + (P3-$0) == N -> new_do_shell_prompt(IO, N, Order, Arg, More); Err when element(1,Err)==error -> @@ -1407,7 +1421,7 @@ prompt_prefix() -> case node() of nonode@nohost -> <<>>; Node -> list_to_binary( - lists:concat(["(",Node,")"])) + atom_to_list(Node)) end. diff --git a/lib/ssh/test/ssh_bench_SUITE.erl b/lib/ssh/test/ssh_bench_SUITE.erl index ac52bb7e28..317e50ed1d 100644 --- a/lib/ssh/test/ssh_bench_SUITE.erl +++ b/lib/ssh/test/ssh_bench_SUITE.erl @@ -98,7 +98,7 @@ end_per_testcase(_Func, _Conf) -> connect(Config) -> KexAlgs = proplists:get_value(kex, ssh:default_algorithms()), - ct:pal("KexAlgs = ~p",[KexAlgs]), + ct:log("KexAlgs = ~p",[KexAlgs]), lists:foreach( fun(KexAlg) -> PrefAlgs = preferred_algorithms(KexAlg), @@ -242,11 +242,11 @@ median(Data) when is_list(Data) -> 1 -> lists:nth(N div 2 + 1, SortedData) end, - ct:pal("median(~p) = ~p",[SortedData,Median]), + ct:log("median(~p) = ~p",[SortedData,Median]), Median. report(Data) -> - ct:pal("EventData = ~p",[Data]), + ct:log("EventData = ~p",[Data]), ct_event:notify(#event{name = benchmark_data, data = Data}). diff --git a/lib/ssh/test/ssh_connection_SUITE.erl b/lib/ssh/test/ssh_connection_SUITE.erl index 2819a4dbd9..b911cf0e9e 100644 --- a/lib/ssh/test/ssh_connection_SUITE.erl +++ b/lib/ssh/test/ssh_connection_SUITE.erl @@ -89,7 +89,7 @@ end_per_suite(Config) -> %%-------------------------------------------------------------------- init_per_group(openssh, Config) -> - case gen_tcp:connect("localhost", 22, []) of + case ssh_test_lib:gen_tcp_connect("localhost", 22, []) of {error,econnrefused} -> {skip,"No openssh deamon"}; {ok, Socket} -> @@ -126,7 +126,7 @@ simple_exec(Config) when is_list(Config) -> simple_exec_sock(_Config) -> - {ok, Sock} = gen_tcp:connect("localhost", ?SSH_DEFAULT_PORT, [{active,false}]), + {ok, Sock} = ssh_test_lib: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). @@ -179,13 +179,13 @@ daemon_sock_not_tcp(_Config) -> %%-------------------------------------------------------------------- connect_sock_not_passive(_Config) -> - {ok,Sock} = gen_tcp:connect("localhost", ?SSH_DEFAULT_PORT, []), + {ok,Sock} = ssh_test_lib:gen_tcp_connect("localhost", ?SSH_DEFAULT_PORT, []), {error, not_passive_mode} = ssh:connect(Sock, []), gen_tcp:close(Sock). %%-------------------------------------------------------------------- daemon_sock_not_passive(_Config) -> - {ok,Sock} = gen_tcp:connect("localhost", ?SSH_DEFAULT_PORT, []), + {ok,Sock} = ssh_test_lib:gen_tcp_connect("localhost", ?SSH_DEFAULT_PORT, []), {error, not_passive_mode} = ssh:daemon(Sock), gen_tcp:close(Sock). @@ -585,12 +585,13 @@ start_shell_sock_exec_fun(Config) when is_list(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}]), + {Pid, HostD, Port} = ssh_test_lib:daemon([{system_dir, SysDir}, + {user_dir, UserDir}, + {password, "morot"}, + {exec, fun ssh_exec/1}]), + Host = ssh_test_lib:ntoa(ssh_test_lib:mangle_connect_address(HostD)), - {ok, Sock} = gen_tcp:connect(Host, Port, [{active,false}]), + {ok, Sock} = ssh_test_lib:gen_tcp_connect(Host, Port, [{active,false}]), {ok,ConnectionRef} = ssh:connect(Sock, [{silently_accept_hosts, true}, {user, "foo"}, {password, "morot"}, @@ -623,7 +624,7 @@ start_shell_sock_daemon_exec(Config) -> {ok,{_IP,Port}} = inet:sockname(Sl), % _IP is likely to be {0,0,0,0}. Win don't like... spawn_link(fun() -> - {ok,Ss} = gen_tcp:connect("localhost", Port, [{active,false}]), + {ok,Ss} = ssh_test_lib:gen_tcp_connect("localhost", Port, [{active,false}]), {ok, _Pid} = ssh:daemon(Ss, [{system_dir, SysDir}, {user_dir, UserDir}, {password, "morot"}, @@ -658,10 +659,10 @@ gracefull_invalid_version(Config) when is_list(Config) -> SysDir = proplists:get_value(data_dir, Config), {_Pid, Host, Port} = ssh_test_lib:daemon([{system_dir, SysDir}, - {user_dir, UserDir}, - {password, "morot"}]), + {user_dir, UserDir}, + {password, "morot"}]), - {ok, S} = gen_tcp:connect(Host, Port, []), + {ok, S} = ssh_test_lib:gen_tcp_connect(Host, Port, []), ok = gen_tcp:send(S, ["SSH-8.-1","\r\n"]), receive Verstring -> @@ -680,10 +681,10 @@ gracefull_invalid_start(Config) when is_list(Config) -> 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"}]), + {user_dir, UserDir}, + {password, "morot"}]), - {ok, S} = gen_tcp:connect(Host, Port, []), + {ok, S} = ssh_test_lib:gen_tcp_connect(Host, Port, []), ok = gen_tcp:send(S, ["foobar","\r\n"]), receive Verstring -> @@ -702,10 +703,10 @@ gracefull_invalid_long_start(Config) when is_list(Config) -> 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"}]), + {user_dir, UserDir}, + {password, "morot"}]), - {ok, S} = gen_tcp:connect(Host, Port, []), + {ok, S} = ssh_test_lib:gen_tcp_connect(Host, Port, []), ok = gen_tcp:send(S, [lists:duplicate(257, $a), "\r\n"]), receive Verstring -> @@ -725,10 +726,10 @@ gracefull_invalid_long_start_no_nl(Config) when is_list(Config) -> 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"}]), + {user_dir, UserDir}, + {password, "morot"}]), - {ok, S} = gen_tcp:connect(Host, Port, []), + {ok, S} = ssh_test_lib:gen_tcp_connect(Host, Port, []), ok = gen_tcp:send(S, [lists:duplicate(257, $a), "\r\n"]), receive Verstring -> @@ -779,22 +780,21 @@ stop_listener(Config) when is_list(Config) -> ct:fail("Exec Timeout") end, - {ok, HostAddr} = inet:getaddr(Host, inet), - case ssh_test_lib:daemon(HostAddr, Port, [{system_dir, SysDir}, - {user_dir, UserDir}, - {password, "potatis"}, - {exec, fun ssh_exec/1}]) of - {Pid1, HostAddr, Port} -> + case ssh_test_lib:daemon(Port, [{system_dir, SysDir}, + {user_dir, UserDir}, + {password, "potatis"}, + {exec, fun ssh_exec/1}]) of + {Pid1, Host, Port} -> ConnectionRef1 = ssh_test_lib:connect(Host, Port, [{silently_accept_hosts, true}, {user, "foo"}, {password, "potatis"}, {user_interaction, true}, {user_dir, UserDir}]), {error, _} = ssh:connect(Host, Port, [{silently_accept_hosts, true}, - {user, "foo"}, - {password, "morot"}, - {user_interaction, true}, - {user_dir, UserDir}]), + {user, "foo"}, + {password, "morot"}, + {user_interaction, true}, + {user_dir, UserDir}]), ssh:close(ConnectionRef0), ssh:close(ConnectionRef1), ssh:stop_daemon(Pid0), diff --git a/lib/ssh/test/ssh_options_SUITE.erl b/lib/ssh/test/ssh_options_SUITE.erl index 758c20e2b8..344a042d79 100644 --- a/lib/ssh/test/ssh_options_SUITE.erl +++ b/lib/ssh/test/ssh_options_SUITE.erl @@ -868,13 +868,13 @@ really_do_hostkey_fingerprint_check(Config, HashAlg) -> ct:log("Fingerprints(~p) = ~p",[HashAlg,FPs]), %% Start daemon with the public keys that we got fingerprints from - {Pid, Host, Port} = ssh_test_lib:daemon([{system_dir, SysDir}, + {Pid, Host0, Port} = ssh_test_lib:daemon([{system_dir, SysDir}, {user_dir, UserDirServer}, {password, "morot"}]), - + Host = ssh_test_lib:ntoa(Host0), FP_check_fun = fun(PeerName, FP) -> - ct:pal("PeerName = ~p, FP = ~p",[PeerName,FP]), - HostCheck = (Host == PeerName), + ct:log("PeerName = ~p, FP = ~p",[PeerName,FP]), + HostCheck = ssh_test_lib:match_ip(Host, PeerName), FPCheck = if is_atom(HashAlg) -> lists:member(FP, FPs); is_list(HashAlg) -> lists:all(fun(FP1) -> lists:member(FP1,FPs) end, @@ -1052,20 +1052,20 @@ id_string_random_client(Config) -> %%-------------------------------------------------------------------- id_string_no_opt_server(Config) -> {_Server, Host, Port} = ssh_test_lib:std_daemon(Config, []), - {ok,S1}=gen_tcp:connect(Host,Port,[{active,false},{packet,line}]), + {ok,S1}=ssh_test_lib:gen_tcp_connect(Host,Port,[{active,false},{packet,line}]), {ok,"SSH-2.0-Erlang/"++Vsn} = gen_tcp:recv(S1, 0, 2000), true = expected_ssh_vsn(Vsn). %%-------------------------------------------------------------------- id_string_own_string_server(Config) -> {_Server, Host, Port} = ssh_test_lib:std_daemon(Config, [{id_string,"Olle"}]), - {ok,S1}=gen_tcp:connect(Host,Port,[{active,false},{packet,line}]), + {ok,S1}=ssh_test_lib:gen_tcp_connect(Host,Port,[{active,false},{packet,line}]), {ok,"SSH-2.0-Olle\r\n"} = gen_tcp:recv(S1, 0, 2000). %%-------------------------------------------------------------------- id_string_random_server(Config) -> {_Server, Host, Port} = ssh_test_lib:std_daemon(Config, [{id_string,random}]), - {ok,S1}=gen_tcp:connect(Host,Port,[{active,false},{packet,line}]), + {ok,S1}=ssh_test_lib:gen_tcp_connect(Host,Port,[{active,false},{packet,line}]), {ok,"SSH-2.0-"++Rnd} = gen_tcp:recv(S1, 0, 2000), case Rnd of "Erlang"++_ -> ct:log("Id=~p",[Rnd]), @@ -1086,11 +1086,11 @@ ssh_connect_negtimeout(Config, Parallel) -> ct:log("Parallel: ~p",[Parallel]), {_Pid, Host, Port} = ssh_test_lib:daemon([{system_dir, SystemDir},{user_dir, UserDir}, - {parallel_login, Parallel}, - {negotiation_timeout, NegTimeOut}, - {failfun, fun ssh_test_lib:failfun/2}]), - - {ok,Socket} = gen_tcp:connect(Host, Port, []), + {parallel_login, Parallel}, + {negotiation_timeout, NegTimeOut}, + {failfun, fun ssh_test_lib:failfun/2}]), + + {ok,Socket} = ssh_test_lib:gen_tcp_connect(Host, Port, []), Factor = 2, ct:log("And now sleeping ~p*NegTimeOut (~p ms)...", [Factor, round(Factor * NegTimeOut)]), diff --git a/lib/ssh/test/ssh_protocol_SUITE.erl b/lib/ssh/test/ssh_protocol_SUITE.erl index 2c4fa8be88..9e7d1a5fa3 100644 --- a/lib/ssh/test/ssh_protocol_SUITE.erl +++ b/lib/ssh/test/ssh_protocol_SUITE.erl @@ -752,6 +752,7 @@ connect_and_kex(Config, InitialState) -> {cipher,?DEFAULT_CIPHERS} ]}, {silently_accept_hosts, true}, + {recv_ext_info, false}, {user_dir, user_dir(Config)}, {user_interaction, false}]}, receive_hello, diff --git a/lib/ssh/test/ssh_relay.erl b/lib/ssh/test/ssh_relay.erl index 28000fbb97..763130358b 100644 --- a/lib/ssh/test/ssh_relay.erl +++ b/lib/ssh/test/ssh_relay.erl @@ -131,7 +131,8 @@ init([ListenAddr, ListenPort, PeerAddr, PeerPort | _Options]) -> S = #state{local_addr = ListenAddr, local_port = ListenPort, lpid = LPid, - peer_addr = PeerAddr, + peer_addr = ssh_test_lib:ntoa( + ssh_test_lib:mangle_connect_address(PeerAddr)), peer_port = PeerPort }, {ok, S}; @@ -241,11 +242,11 @@ handle_info(stop, State) -> {stop, normal, State}; handle_info({'DOWN', _Ref, _process, LPid, Reason}, S) when S#state.lpid == LPid -> - io:format("Acceptor has finished: ~p~n", [Reason]), + io:format("Acceptor in ~p has finished: ~p~n", [?MODULE,Reason]), {noreply, S}; handle_info(_Info, State) -> - io:format("Unhandled info: ~p~n", [_Info]), + io:format("~p:~p Unhandled info: ~p~n", [?MODULE,?LINE,_Info]), {noreply, State}. %%-------------------------------------------------------------------- diff --git a/lib/ssh/test/ssh_sftp_SUITE.erl b/lib/ssh/test/ssh_sftp_SUITE.erl index acf76157a2..7efeb3a0ad 100644 --- a/lib/ssh/test/ssh_sftp_SUITE.erl +++ b/lib/ssh/test/ssh_sftp_SUITE.erl @@ -660,7 +660,7 @@ start_channel_sock(Config) -> {Host,Port} = proplists:get_value(peer, Config), %% Get a tcp socket - {ok, Sock} = gen_tcp:connect(Host, Port, [{active,false}]), + {ok, Sock} = ssh_test_lib:gen_tcp_connect(Host, Port, [{active,false}]), %% and open one channel on one new Connection {ok, ChPid1, Conn} = ssh_sftp:start_channel(Sock, Opts), diff --git a/lib/ssh/test/ssh_sftpd_SUITE.erl b/lib/ssh/test/ssh_sftpd_SUITE.erl index b167f98ac8..379c0bcb0a 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), @@ -705,10 +705,10 @@ try_access(Path, Cm, Channel, ReqId) -> {ok, <<?SSH_FXP_STATUS, ?UINT32(ReqId), ?UINT32(Code), Rest/binary>>, <<>>} -> case Code of ?SSH_FX_FILE_IS_A_DIRECTORY -> - ct:pal("Got the expected SSH_FX_FILE_IS_A_DIRECTORY status",[]), + ct:log("Got the expected SSH_FX_FILE_IS_A_DIRECTORY status",[]), ok; ?SSH_FX_FAILURE -> - ct:pal("Got the expected SSH_FX_FAILURE status",[]), + ct:log("Got the expected SSH_FX_FAILURE status",[]), ok; _ -> case Rest of 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..dd7c4b1473 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,27 +130,31 @@ 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) -> HostIP = proplists:get_value(host_ip, Config), Port = proplists:get_value(port, Config), SystemDir = proplists:get_value(data_dir, Config), - ssh:daemon(HostIP, Port, [{system_dir, SystemDir}, - {failfun, fun ssh_test_lib:failfun/2}, - {user_passwords, - [{?USER, ?PASSWD}]}]), + {ok,Daemon} = ssh:daemon(HostIP, Port, [{system_dir, SystemDir}, + {failfun, fun ssh_test_lib:failfun/2}, + {user_passwords, + [{?USER, ?PASSWD}]}]), - ?wait_match([{{server,ssh_system_sup, HostIP, Port, ?DEFAULT_PROFILE}, + ct:log("Expect HostIP=~p, Port=~p, Daemon=~p",[HostIP,Port,Daemon]), + ?wait_match([{{server,ssh_system_sup, ListenIP, Port, ?DEFAULT_PROFILE}, Daemon, supervisor, [ssh_system_sup]}], supervisor:which_children(sshd_sup), - Daemon), + [ListenIP,Daemon]), + true = ssh_test_lib:match_ip(HostIP, ListenIP), check_sshd_system_tree(Daemon, Config), ssh:stop_daemon(HostIP, Port), 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) -> @@ -157,34 +163,105 @@ sshd_subtree_profile(Config) when is_list(Config) -> Profile = proplists:get_value(profile, Config), SystemDir = proplists:get_value(data_dir, Config), - {ok, _} = ssh:daemon(HostIP, Port, [{system_dir, SystemDir}, - {failfun, fun ssh_test_lib:failfun/2}, - {user_passwords, - [{?USER, ?PASSWD}]}, - {profile, Profile}]), - ?wait_match([{{server,ssh_system_sup, HostIP,Port,Profile}, + {ok, Daemon} = ssh:daemon(HostIP, Port, [{system_dir, SystemDir}, + {failfun, fun ssh_test_lib:failfun/2}, + {user_passwords, + [{?USER, ?PASSWD}]}, + {profile, Profile}]), + ct:log("Expect HostIP=~p, Port=~p, Profile=~p, Daemon=~p",[HostIP,Port,Profile,Daemon]), + ?wait_match([{{server,ssh_system_sup, ListenIP,Port,Profile}, Daemon, supervisor, [ssh_system_sup]}], supervisor:which_children(sshd_sup), - Daemon), + [ListenIP,Daemon]), + true = ssh_test_lib:match_ip(HostIP, ListenIP), check_sshd_system_tree(Daemon, Config), ssh:stop_daemon(HostIP, Port, Profile), 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 +285,33 @@ 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..ded47ca4f6 100644 --- a/lib/ssh/test/ssh_test_lib.erl +++ b/lib/ssh/test/ssh_test_lib.erl @@ -32,15 +32,19 @@ -define(TIMEOUT, 50000). +%%%---------------------------------------------------------------- connect(Port, Options) when is_integer(Port) -> connect(hostname(), Port, Options). connect(any, Port, Options) -> connect(hostname(), Port, Options); connect(Host, Port, Options) -> - {ok, ConnectionRef} = ssh:connect(Host, Port, Options), + R = ssh:connect(Host, Port, Options), + ct:log("~p:~p ssh:connect(~p, ~p, ~p)~n -> ~p",[?MODULE,?LINE,Host, Port, Options, R]), + {ok, ConnectionRef} = R, ConnectionRef. +%%%---------------------------------------------------------------- daemon(Options) -> daemon(any, 0, Options). @@ -53,23 +57,57 @@ daemon(Host, Options) -> daemon(Host, Port, Options) -> ct:log("~p:~p Calling ssh:daemon(~p, ~p, ~p)",[?MODULE,?LINE,Host,Port,Options]), case ssh:daemon(Host, Port, Options) of - {ok, Pid} when Host == any -> - ct:log("ssh:daemon ok (1)",[]), - {Pid, hostname(), daemon_port(Port,Pid)}; {ok, Pid} -> - ct:log("ssh:daemon ok (2)",[]), - {Pid, Host, daemon_port(Port,Pid)}; + {ok,L} = ssh:daemon_info(Pid), + ListenPort = proplists:get_value(port, L), + ListenIP = proplists:get_value(ip, L), + {Pid, ListenIP, ListenPort}; Error -> ct:log("ssh:daemon error ~p",[Error]), 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. - +%%%---------------------------------------------------------------- +gen_tcp_connect(Host0, Port, Options) -> + Host = ssh_test_lib:ntoa(ssh_test_lib:mangle_connect_address(Host0)), + ct:log("~p:~p gen_tcp:connect(~p, ~p, ~p)~nHost0 = ~p", + [?MODULE,?LINE, Host, Port, Options, Host0]), + Result = gen_tcp:connect(Host, Port, Options), + ct:log("~p:~p Result = ~p", [?MODULE,?LINE, Result]), + Result. + +%%%---------------------------------------------------------------- +open_sshc(Host0, Port, OptStr) -> + open_sshc(Host0, Port, OptStr, ""). + +open_sshc(Host0, Port, OptStr, ExecStr) -> + Cmd = open_sshc_cmd(Host0, Port, OptStr, ExecStr), + Result = os:cmd(Cmd), + ct:log("~p:~p Result = ~p", [?MODULE,?LINE, Result]), + Result. + +open_sshc_cmd(Host, Port, OptStr) -> + open_sshc_cmd(Host, Port, OptStr, ""). + +open_sshc_cmd(Host0, Port, OptStr, ExecStr) -> + Host = ssh_test_lib:ntoa(ssh_test_lib:mangle_connect_address(Host0)), + Cmd = lists:flatten(["ssh -p ", integer_to_list(Port), + " ", OptStr, + " ", Host, + " ", ExecStr]), + ct:log("~p:~p OpenSSH Cmd = ~p", [?MODULE,?LINE, Cmd]), + Cmd. + +%%%---------------------------------------------------------------- std_daemon(Config, ExtraOpts) -> PrivDir = proplists:get_value(priv_dir, Config), UserDir = filename:join(PrivDir, nopubkey), % to make sure we don't use public-key-auth @@ -85,6 +123,7 @@ std_daemon1(Config, ExtraOpts) -> {failfun, fun ssh_test_lib:failfun/2} | ExtraOpts]). +%%%---------------------------------------------------------------- std_connect(Config, Host, Port, ExtraOpts) -> UserDir = proplists:get_value(priv_dir, Config), _ConnectionRef = @@ -95,6 +134,7 @@ std_connect(Config, Host, Port, ExtraOpts) -> {user_interaction, false} | ExtraOpts]). +%%%---------------------------------------------------------------- std_simple_sftp(Host, Port, Config) -> std_simple_sftp(Host, Port, Config, []). @@ -109,6 +149,7 @@ std_simple_sftp(Host, Port, Config, Opts) -> ok = ssh:close(ConnectionRef), Data == ReadData. +%%%---------------------------------------------------------------- std_simple_exec(Host, Port, Config) -> std_simple_exec(Host, Port, Config, []). @@ -135,6 +176,7 @@ std_simple_exec(Host, Port, Config, Opts) -> ct:fail(ExecResult) end. +%%%---------------------------------------------------------------- start_shell(Port, IOServer) -> start_shell(Port, IOServer, []). @@ -149,6 +191,7 @@ start_shell(Port, IOServer, ExtraOptions) -> end). +%%%---------------------------------------------------------------- start_io_server() -> spawn_link(?MODULE, init_io_server, [self()]). @@ -207,8 +250,7 @@ reply(TestCase, Result) -> %%ct:log("reply ~p sending ~p ! ~p",[self(), TestCase, Result]), TestCase ! Result. - - +%%%---------------------------------------------------------------- rcv_expected(Expect, SshPort, Timeout) -> receive {SshPort, Recvd} when is_function(Expect) -> @@ -817,8 +859,9 @@ get_kex_init(Conn) -> get_kex_init(Conn, Ref, TRef) -> %% First, validate the key exchange is complete (StateName == connected) - case sys:get_state(Conn) of - {{connected,_}, S} -> + {State, S} = sys:get_state(Conn), + case expected_state(State) of + true -> timer:cancel(TRef), %% Next, walk through the elements of the #state record looking %% for the #ssh_msg_kexinit record. This method is robust against @@ -832,8 +875,8 @@ get_kex_init(Conn, Ref, TRef) -> KexInit end; - {OtherState, S} -> - ct:log("Not in 'connected' state: ~p",[OtherState]), + false -> + ct:log("Not in 'connected' state: ~p",[State]), receive {reneg_timeout,Ref} -> ct:log("S = ~p", [S]), @@ -845,6 +888,10 @@ get_kex_init(Conn, Ref, TRef) -> end end. +expected_state({ext_info,_,_}) -> true; +expected_state({connected,_}) -> true; +expected_state(_) -> false. + %%%---------------------------------------------------------------- %%% Return a string with N random characters %%% @@ -862,3 +909,73 @@ create_random_dir(Config) -> %% The likelyhood of always generating an existing file name is low create_random_dir(Config) end. + +%%%---------------------------------------------------------------- +match_ip(A, B) -> + R = match_ip0(A,B) orelse match_ip0(B,A), + ct:log("match_ip(~p, ~p) -> ~p",[A, B, R]), + R. + +match_ip0(A, A) -> + true; +match_ip0(any, _) -> + true; +match_ip0(A, B) -> + case match_ip1(A, B) of + true -> + true; + false when is_list(A) -> + case inet:parse_address(A) of + {ok,IPa} -> match_ip0(IPa, B); + _ -> false + end; + false when is_list(B) -> + case inet:parse_address(B) of + {ok,IPb} -> match_ip0(A, IPb); + _ -> false + end; + false -> + false + end. + +match_ip1(any, _) -> true; +match_ip1(loopback, {127,_,_,_}) -> true; +match_ip1({0,0,0,0}, {127,_,_,_}) -> true; +match_ip1(loopback, {0,0,0,0,0,0,0,1}) -> true; +match_ip1({0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,1}) -> true; +match_ip1(_, _) -> false. + +%%%---------------------------------------------------------------- +mangle_connect_address(A) -> + mangle_connect_address(A, []). + +mangle_connect_address(A, SockOpts) -> + mangle_connect_address1(A, proplists:get_value(inet6,SockOpts,false)). + +loopback(true) -> {0,0,0,0,0,0,0,1}; +loopback(false) -> {127,0,0,1}. + +mangle_connect_address1( loopback, V6flg) -> loopback(V6flg); +mangle_connect_address1( any, V6flg) -> loopback(V6flg); +mangle_connect_address1({0,0,0,0}, _) -> loopback(false); +mangle_connect_address1({0,0,0,0,0,0,0,0}, _) -> loopback(true); +mangle_connect_address1( IP, _) when is_tuple(IP) -> IP; +mangle_connect_address1(A, _) -> + case catch inet:parse_address(A) of + {ok, {0,0,0,0}} -> loopback(false); + {ok, {0,0,0,0,0,0,0,0}} -> loopback(true); + _ -> A + end. + +%%%---------------------------------------------------------------- +ntoa(A) -> + try inet:ntoa(A) + of + {error,_} when is_atom(A) -> atom_to_list(A); + {error,_} when is_list(A) -> A; + S when is_list(S) -> S + catch + _:_ when is_atom(A) -> atom_to_list(A); + _:_ when is_list(A) -> A + end. + diff --git a/lib/ssh/test/ssh_to_openssh_SUITE.erl b/lib/ssh/test/ssh_to_openssh_SUITE.erl index 7eda009552..6b3055ebab 100644 --- a/lib/ssh/test/ssh_to_openssh_SUITE.erl +++ b/lib/ssh/test/ssh_to_openssh_SUITE.erl @@ -153,7 +153,7 @@ erlang_shell_client_openssh_server(Config) when is_list(Config) -> IO = ssh_test_lib:start_io_server(), Shell = ssh_test_lib:start_shell(?SSH_DEFAULT_PORT, IO), IO ! {input, self(), "echo Hej\n"}, - receive_data("Hej"), + receive_data("Hej", undefined), IO ! {input, self(), "exit\n"}, receive_logout(), receive_normal_exit(Shell). @@ -376,18 +376,18 @@ erlang_server_openssh_client_public_key_rsa(Config) when is_list(Config) -> erlang_server_openssh_client_public_key_X(Config, ssh_rsa). -erlang_server_openssh_client_public_key_X(Config, PubKeyAlg) -> +erlang_server_openssh_client_public_key_X(Config, _PubKeyAlg) -> SystemDir = proplists:get_value(data_dir, Config), PrivDir = proplists:get_value(priv_dir, Config), KnownHosts = filename:join(PrivDir, "known_hosts"), {Pid, Host, Port} = ssh_test_lib:daemon([{system_dir, SystemDir}, {failfun, fun ssh_test_lib:failfun/2}]), - ct:sleep(500), - Cmd = "ssh -p " ++ integer_to_list(Port) ++ - " -o UserKnownHostsFile=" ++ KnownHosts ++ - " " ++ Host ++ " 1+1.", + Cmd = ssh_test_lib:open_sshc_cmd(Host, Port, + [" -o UserKnownHostsFile=", KnownHosts, + " -o StrictHostKeyChecking=no"], + "1+1."), OpenSsh = ssh_test_lib:open_port({spawn, Cmd}), ssh_test_lib:rcv_expected({data,<<"2\n">>}, OpenSsh, ?TIMEOUT), ssh:stop_daemon(Pid). @@ -395,13 +395,13 @@ erlang_server_openssh_client_public_key_X(Config, PubKeyAlg) -> %%-------------------------------------------------------------------- %% Test that the Erlang/OTP server can renegotiate with openSSH erlang_server_openssh_client_renegotiate(Config) -> - PubKeyAlg = ssh_rsa, + _PubKeyAlg = ssh_rsa, SystemDir = proplists:get_value(data_dir, Config), PrivDir = proplists:get_value(priv_dir, Config), KnownHosts = filename:join(PrivDir, "known_hosts"), {Pid, Host, Port} = ssh_test_lib:daemon([{system_dir, SystemDir}, - {failfun, fun ssh_test_lib:failfun/2}]), + {failfun, fun ssh_test_lib:failfun/2}]), ct:sleep(500), RenegLimitK = 3, @@ -409,11 +409,13 @@ erlang_server_openssh_client_renegotiate(Config) -> Data = lists:duplicate(trunc(1.1*RenegLimitK*1024), $a), ok = file:write_file(DataFile, Data), - Cmd = "ssh -p " ++ integer_to_list(Port) ++ - " -o UserKnownHostsFile=" ++ KnownHosts ++ - " -o RekeyLimit=" ++ integer_to_list(RenegLimitK) ++"K" ++ - " " ++ Host ++ " < " ++ DataFile, - OpenSsh = ssh_test_lib:open_port({spawn, Cmd}), + Cmd = ssh_test_lib:open_sshc_cmd(Host, Port, + [" -o UserKnownHostsFile=", KnownHosts, + " -o StrictHostKeyChecking=no", + " -o RekeyLimit=",integer_to_list(RenegLimitK),"K"]), + + + OpenSsh = ssh_test_lib:open_port({spawn, Cmd++" < "++DataFile}), Expect = fun({data,R}) -> try @@ -449,7 +451,6 @@ erlang_server_openssh_client_renegotiate(Config) -> %%-------------------------------------------------------------------- erlang_client_openssh_server_renegotiate(_Config) -> process_flag(trap_exit, true), - IO = ssh_test_lib:start_io_server(), Ref = make_ref(), Parent = self(), @@ -462,7 +463,7 @@ erlang_client_openssh_server_renegotiate(_Config) -> {silently_accept_hosts,true}], group_leader(IO, self()), {ok, ConnRef} = ssh:connect(Host, ?SSH_DEFAULT_PORT, Options), - ct:pal("Parent = ~p, IO = ~p, Shell = ~p, ConnRef = ~p~n",[Parent, IO, self(), ConnRef]), + ct:log("Parent = ~p, IO = ~p, Shell = ~p, ConnRef = ~p~n",[Parent, IO, self(), ConnRef]), case ssh_connection:session_channel(ConnRef, infinity) of {ok,ChannelId} -> success = ssh_connection:ptty_alloc(ConnRef, ChannelId, []), @@ -485,11 +486,11 @@ erlang_client_openssh_server_renegotiate(_Config) -> ct:fail("Error=~p",[Error]); {ok, Ref, ConnectionRef} -> IO ! {input, self(), "echo Hej1\n"}, - receive_data("Hej1"), + receive_data("Hej1", ConnectionRef), Kex1 = ssh_test_lib:get_kex_init(ConnectionRef), ssh_connection_handler:renegotiate(ConnectionRef), IO ! {input, self(), "echo Hej2\n"}, - receive_data("Hej2"), + receive_data("Hej2", ConnectionRef), Kex2 = ssh_test_lib:get_kex_init(ConnectionRef), IO ! {input, self(), "exit\n"}, receive_logout(), @@ -552,23 +553,29 @@ erlang_client_openssh_server_nonexistent_subsystem(Config) when is_list(Config) %%-------------------------------------------------------------------- %%% Internal functions ----------------------------------------------- %%-------------------------------------------------------------------- -receive_data(Data) -> +receive_data(Data, Conn) -> receive Info when is_binary(Info) -> Lines = string:tokens(binary_to_list(Info), "\r\n "), case lists:member(Data, Lines) of true -> - ct:log("Expected result found in lines: ~p~n", [Lines]), + ct:log("Expected result ~p found in lines: ~p~n", [Data,Lines]), ok; false -> ct:log("Extra info: ~p~n", [Info]), - receive_data(Data) + receive_data(Data, Conn) end; Other -> ct:log("Unexpected: ~p",[Other]), - receive_data(Data) - after - 30000 -> ct:fail("timeout ~p:~p",[?MODULE,?LINE]) + receive_data(Data, Conn) + after + 30000 -> + {State, _} = case Conn of + undefined -> {'??','??'}; + _ -> sys:get_state(Conn) + end, + ct:log("timeout ~p:~p~nExpect ~p~nState = ~p",[?MODULE,?LINE,Data,State]), + ct:fail("timeout ~p:~p",[?MODULE,?LINE]) end. receive_logout() -> diff --git a/lib/ssh/test/ssh_trpt_test_lib.erl b/lib/ssh/test/ssh_trpt_test_lib.erl index 261239c152..781889ddd1 100644 --- a/lib/ssh/test/ssh_trpt_test_lib.erl +++ b/lib/ssh/test/ssh_trpt_test_lib.erl @@ -314,8 +314,7 @@ mangle_opts(Options) -> lists:keydelete(K,1,Opts) end, Options, SysOpts). -host({0,0,0,0}) -> "localhost"; -host(H) -> H. +host(H) -> ssh_test_lib:ntoa(ssh_test_lib:mangle_connect_address(H)). %%%---------------------------------------------------------------- send(S=#s{ssh=C}, hello) -> @@ -398,6 +397,12 @@ send(S0, {special,Msg,PacketFun}) when is_tuple(Msg), send_bytes(Packet, S#s{ssh = C, %%inc_send_seq_num(C), return_value = Msg}); +send(S0, #ssh_msg_newkeys{} = Msg) -> + S = opt(print_messages, S0, + fun(X) when X==true;X==detail -> {"Send~n~s~n",[format_msg(Msg)]} end), + {ok, Packet, C} = ssh_transport:new_keys_message(S#s.ssh), + send_bytes(Packet, S#s{ssh = C}); + send(S0, Msg) when is_tuple(Msg) -> S = opt(print_messages, S0, fun(X) when X==true;X==detail -> {"Send~n~s~n",[format_msg(Msg)]} end), @@ -456,7 +461,10 @@ recv(S0 = #s{}) -> }; #ssh_msg_kexdh_reply{} -> {ok, _NewKeys, C} = ssh_transport:handle_kexdh_reply(PeerMsg, S#s.ssh), - S#s{ssh=C#ssh{send_sequence=S#s.ssh#ssh.send_sequence}}; % Back the number + S#s{ssh = (S#s.ssh)#ssh{shared_secret = C#ssh.shared_secret, + exchanged_hash = C#ssh.exchanged_hash, + session_id = C#ssh.session_id}}; + %%%S#s{ssh=C#ssh{send_sequence=S#s.ssh#ssh.send_sequence}}; % Back the number #ssh_msg_newkeys{} -> {ok, C} = ssh_transport:handle_new_keys(PeerMsg, S#s.ssh), S#s{ssh=C}; |