%% %% %CopyrightBegin% %% %% Copyright Ericsson AB 2008-2017. All Rights Reserved. %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. %% You may obtain a copy of the License at %% %% http://www.apache.org/licenses/LICENSE-2.0 %% %% Unless required by applicable law or agreed to in writing, software %% distributed under the License is distributed on an "AS IS" BASIS, %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. %% See the License for the specific language governing permissions and %% limitations under the License. %% %% %CopyrightEnd% %% %% %%---------------------------------------------------------------------- %% Purpose: Handles an ssh connection, e.i. both the %% setup SSH Transport Layer Protocol (RFC 4253), Authentication %% Protocol (RFC 4252) and SSH connection Protocol (RFC 4255) %% Details of the different protocols are %% implemented in ssh_transport.erl, ssh_auth.erl and ssh_connection.erl %% ---------------------------------------------------------------------- -module(ssh_connection_handler). -behaviour(gen_statem). -include("ssh.hrl"). -include("ssh_transport.hrl"). -include("ssh_auth.hrl"). -include("ssh_connect.hrl"). %%==================================================================== %%% Exports %%==================================================================== %%% Start and stop -export([start_link/3, stop/1 ]). %%% Internal application API -export([start_connection/4, open_channel/6, request/6, request/7, reply_request/3, send/5, send_eof/2, info/1, info/2, connection_info/2, channel_info/3, adjust_window/3, close/2, disconnect/1, disconnect/2, get_print_info/1 ]). %%% Behaviour callbacks -export([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 -export([init_connection_handler/3, % proc_lib:spawn needs this init_ssh_record/3, % Export of this internal function % intended for low-level protocol test suites renegotiate/1, renegotiate_data/1 % Export intended for test cases ]). %%==================================================================== %% Start / stop %%==================================================================== %%-------------------------------------------------------------------- -spec start_link(role(), inet:socket(), proplists:proplist() ) -> {ok, pid()}. %% . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . start_link(Role, Socket, Options) -> {ok, proc_lib:spawn_link(?MODULE, init_connection_handler, [Role, Socket, Options])}. %%-------------------------------------------------------------------- -spec stop(connection_ref() ) -> ok | {error, term()}. %% . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . stop(ConnectionHandler)-> case call(ConnectionHandler, stop) of {error, closed} -> ok; Other -> Other end. %%==================================================================== %% Internal application API %%==================================================================== -define(DefaultTransport, {tcp, gen_tcp, tcp_closed} ). %%-------------------------------------------------------------------- -spec start_connection(role(), inet:socket(), proplists:proplist(), timeout() ) -> {ok, connection_ref()} | {error, term()}. %% . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . start_connection(client = Role, Socket, Options, Timeout) -> try {ok, Pid} = sshc_sup:start_child([Role, Socket, Options]), ok = socket_control(Socket, Pid, Options), handshake(Pid, erlang:monitor(process,Pid), Timeout) catch exit:{noproc, _} -> {error, ssh_not_started}; _:Error -> {error, Error} end; start_connection(server = Role, Socket, Options, Timeout) -> SSH_Opts = proplists:get_value(ssh_opts, Options, []), try case proplists:get_value(parallel_login, SSH_Opts, false) of true -> HandshakerPid = spawn_link(fun() -> receive {do_handshake, Pid} -> handshake(Pid, erlang:monitor(process,Pid), Timeout) end end), ChildPid = start_the_connection_child(HandshakerPid, Role, Socket, Options), HandshakerPid ! {do_handshake, ChildPid}; false -> ChildPid = start_the_connection_child(self(), Role, Socket, Options), handshake(ChildPid, erlang:monitor(process,ChildPid), Timeout) end catch exit:{noproc, _} -> {error, ssh_not_started}; _:Error -> {error, Error} end. %%-------------------------------------------------------------------- %%% Some other module has decided to disconnect. -spec disconnect(#ssh_msg_disconnect{}) -> no_return(). -spec disconnect(#ssh_msg_disconnect{}, iodata()) -> no_return(). %% . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . disconnect(Msg = #ssh_msg_disconnect{}) -> throw({keep_state_and_data, [{next_event, internal, {disconnect, Msg, Msg#ssh_msg_disconnect.description}}]}). disconnect(Msg = #ssh_msg_disconnect{}, ExtraInfo) -> throw({keep_state_and_data, [{next_event, internal, {disconnect, Msg, {Msg#ssh_msg_disconnect.description,ExtraInfo}}}]}). %%-------------------------------------------------------------------- -spec open_channel(connection_ref(), string(), iodata(), pos_integer(), pos_integer(), timeout() ) -> {open, channel_id()} | {error, term()}. %% . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . open_channel(ConnectionHandler, ChannelType, ChannelSpecificData, InitialWindowSize, MaxPacketSize, Timeout) -> call(ConnectionHandler, {open, self(), ChannelType, InitialWindowSize, MaxPacketSize, ChannelSpecificData, Timeout}). %%-------------------------------------------------------------------- -spec request(connection_ref(), pid(), channel_id(), string(), boolean(), iodata(), timeout() ) -> success | failure | ok | {error,timeout}. -spec request(connection_ref(), channel_id(), string(), boolean(), iodata(), timeout() ) -> success | failure | ok | {error,timeout}. %% . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . request(ConnectionHandler, ChannelPid, ChannelId, Type, true, Data, Timeout) -> call(ConnectionHandler, {request, ChannelPid, ChannelId, Type, Data, Timeout}); request(ConnectionHandler, ChannelPid, ChannelId, Type, false, Data, _) -> cast(ConnectionHandler, {request, ChannelPid, ChannelId, Type, Data}). request(ConnectionHandler, ChannelId, Type, true, Data, Timeout) -> call(ConnectionHandler, {request, ChannelId, Type, Data, Timeout}); request(ConnectionHandler, ChannelId, Type, false, Data, _) -> cast(ConnectionHandler, {request, ChannelId, Type, Data}). %%-------------------------------------------------------------------- -spec reply_request(connection_ref(), success | failure, channel_id() ) -> ok. %% . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . reply_request(ConnectionHandler, Status, ChannelId) -> cast(ConnectionHandler, {reply_request, Status, ChannelId}). %%-------------------------------------------------------------------- -spec send(connection_ref(), channel_id(), non_neg_integer(), iodata(), timeout() ) -> ok | {error, timeout|closed}. %% . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . send(ConnectionHandler, ChannelId, Type, Data, Timeout) -> call(ConnectionHandler, {data, ChannelId, Type, Data, Timeout}). %%-------------------------------------------------------------------- -spec send_eof(connection_ref(), channel_id() ) -> ok | {error,closed}. %% . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . send_eof(ConnectionHandler, ChannelId) -> call(ConnectionHandler, {eof, ChannelId}). %%-------------------------------------------------------------------- -spec info(connection_ref() ) -> {ok, [#channel{}]} . -spec info(connection_ref(), pid() | all ) -> {ok, [#channel{}]} . %% . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . info(ConnectionHandler) -> info(ConnectionHandler, all). info(ConnectionHandler, ChannelProcess) -> call(ConnectionHandler, {info, ChannelProcess}). %%-------------------------------------------------------------------- -type local_sock_info() :: {inet:ip_address(), non_neg_integer()} | string(). -type peer_sock_info() :: {inet:ip_address(), non_neg_integer()} | string(). -type state_info() :: iolist(). -spec get_print_info(connection_ref() ) -> {{local_sock_info(), peer_sock_info()}, state_info() }. %% . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . get_print_info(ConnectionHandler) -> call(ConnectionHandler, get_print_info, 1000). %%-------------------------------------------------------------------- -spec connection_info(connection_ref(), [atom()] ) -> proplists:proplist(). %% . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . connection_info(ConnectionHandler, Options) -> call(ConnectionHandler, {connection_info, Options}). %%-------------------------------------------------------------------- -spec channel_info(connection_ref(), channel_id(), [atom()] ) -> proplists:proplist(). %% . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . channel_info(ConnectionHandler, ChannelId, Options) -> call(ConnectionHandler, {channel_info, ChannelId, Options}). %%-------------------------------------------------------------------- -spec adjust_window(connection_ref(), channel_id(), integer() ) -> ok. %% . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . adjust_window(ConnectionHandler, Channel, Bytes) -> cast(ConnectionHandler, {adjust_window, Channel, Bytes}). %%-------------------------------------------------------------------- -spec close(connection_ref(), channel_id() ) -> ok. %% . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . close(ConnectionHandler, ChannelId) -> case call(ConnectionHandler, {close, ChannelId}) of ok -> ok; {error, closed} -> ok end. %%==================================================================== %% Test support %%==================================================================== %%-------------------------------------------------------------------- -spec renegotiate(connection_ref() ) -> ok. %% . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . renegotiate(ConnectionHandler) -> cast(ConnectionHandler, renegotiate). %%-------------------------------------------------------------------- -spec renegotiate_data(connection_ref() ) -> ok. %% . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . renegotiate_data(ConnectionHandler) -> cast(ConnectionHandler, data_size). %%==================================================================== %% Internal process state %%==================================================================== -record(data, { starter :: pid(), auth_user :: string() | undefined, connection_state :: #connection{}, latest_channel_id = 0 :: non_neg_integer(), idle_timer_ref :: undefined | infinity | reference(), idle_timer_value = infinity :: infinity | pos_integer(), transport_protocol :: atom(), % ex: tcp transport_cb :: atom(), % ex: gen_tcp transport_close_tag :: atom(), % ex: tcp_closed ssh_params :: #ssh{} | undefined, socket :: inet:socket(), decrypted_data_buffer = <<>> :: binary(), encrypted_data_buffer = <<>> :: binary(), undecrypted_packet_length :: undefined | non_neg_integer(), key_exchange_init_msg :: #ssh_msg_kexinit{} | undefined, last_size_rekey = 0 :: non_neg_integer(), event_queue = [] :: list(), opts :: proplists:proplist(), inet_initial_recbuf_size :: pos_integer() | undefined }). %%==================================================================== %% Intitialisation %%==================================================================== %%-------------------------------------------------------------------- -spec init_connection_handler(role(), inet:socket(), proplists:proplist() ) -> no_return(). %% . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . init_connection_handler(Role, Socket, Opts) -> process_flag(trap_exit, true), S0 = init_process_state(Role, Socket, Opts), try {Protocol, Callback, CloseTag} = proplists:get_value(transport, Opts, ?DefaultTransport), 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 = proplists:get_value(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 -> D#data{connection_state = init_connection(Role, C, Opts)} end. init_connection(server, C = #connection{}, Opts) -> Sups = proplists:get_value(supervisors, Opts), SystemSup = proplists:get_value(system_sup, Sups), SubSystemSup = proplists:get_value(subsystem_sup, Sups), ConnectionSup = proplists:get_value(connection_sup, Sups), Shell = proplists:get_value(shell, Opts), Exec = proplists:get_value(exec, Opts), CliSpec = proplists:get_value(ssh_cli, Opts, {ssh_cli, [Shell]}), 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), KeyCb = proplists:get_value(key_cb, Opts, ssh_file), AuthMethods = proplists:get_value(auth_methods, Opts, case Role of server -> ?SUPPORTED_AUTH_METHODS; client -> undefined end), S0 = #ssh{role = Role, key_cb = KeyCb, opts = Opts, userauth_supported_methods = AuthMethods, available_host_keys = supported_host_keys(Role, KeyCb, Opts), random_length_padding = proplists:get_value(max_random_length_padding, Opts, (#ssh{})#ssh.random_length_padding) }, {Vsn, Version} = ssh_transport:versions(Role, Opts), case Role of client -> PeerName = proplists:get_value(host, Opts), S0#ssh{c_vsn = Vsn, c_version = Version, io_cb = case proplists:get_value(user_interaction, Opts, true) of true -> ssh_io; false -> ssh_no_io end, userauth_quiet_mode = proplists:get_value(quiet_mode, Opts, false), peer = {PeerName, PeerAddr} }; server -> S0#ssh{s_vsn = Vsn, s_version = Version, io_cb = proplists:get_value(io_cb, Opts, ssh_io), userauth_methods = string:tokens(AuthMethods, ","), kb_tries_left = 3, peer = {undefined, PeerAddr} } end. %%==================================================================== %% gen_statem callbacks %%==================================================================== %%-------------------------------------------------------------------- -type event_content() :: any(). -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()} | {key_exchange_dh_gex_reply, client, renegotiate_flag()} | {new_keys, role()} | {service_request, role()} | {userauth, role()} | {userauth_keyboard_interactive, role()} | {connected, role()} . -type handle_event_result() :: gen_statem:handle_event_result(). -spec handle_event(gen_statem:event_type(), event_content(), state_name(), #data{} ) -> handle_event_result(). %% . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . %%% ######## 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) -> VsnMsg = ssh_transport:hello_version_msg(string_version(D#data.ssh_params)), send_bytes(VsnMsg, D), case inet:getopts(Socket=D#data.socket, [recbuf]) of {ok, [{recbuf,Size}]} -> %% Set the socket to the hello text line handling mode: inet:setopts(Socket, [{packet, line}, {active, once}, % Expecting the version string which might % be max ?MAX_PROTO_VERSION bytes: {recbuf, ?MAX_PROTO_VERSION}, {nodelay,true}]), {keep_state, D#data{inet_initial_recbuf_size=Size}}; Other -> {stop, {shutdown,{unexpected_getopts_return, Other}}} end; handle_event(_, {info_line,_Line}, {hello,Role}, D) -> case Role of client -> %% The server may send info lines to the client before the version_exchange %% RFC4253/4.2 inet:setopts(D#data.socket, [{active, once}]), keep_state_and_data; server -> %% But the client may NOT send them to the server. Openssh answers with cleartext, %% and so do we send_bytes("Protocol mismatch.", D), {stop, {shutdown,"Protocol mismatch in version exchange. Client sent info lines."}} end; handle_event(_, {version_exchange,Version}, {hello,Role}, D) -> {NumVsn, StrVsn} = ssh_transport:handle_hello_version(Version), case handle_version(NumVsn, StrVsn, D#data.ssh_params) of {ok, Ssh1} -> %% Since the hello part is finnished correctly, we set the %% socket to the packet handling mode (including recbuf size): inet:setopts(D#data.socket, [{packet,0}, {mode,binary}, {active, once}, {recbuf, D#data.inet_initial_recbuf_size}]), {KeyInitMsg, SshPacket, Ssh} = ssh_transport:key_exchange_init_msg(Ssh1), send_bytes(SshPacket, D), {next_state, {kexinit,Role,init}, D#data{ssh_params = Ssh, key_exchange_init_msg = KeyInitMsg}}; not_supported -> disconnect( #ssh_msg_disconnect{code = ?SSH_DISCONNECT_PROTOCOL_VERSION_NOT_SUPPORTED, description = ["Protocol version ",StrVsn," not supported"]}, {next_state, {hello,Role}, D}) end; %%% ######## {kexinit, client|server, init|renegotiate} #### handle_event(_, {#ssh_msg_kexinit{}=Kex, Payload}, {kexinit,Role,ReNeg}, D = #data{key_exchange_init_msg = OwnKex}) -> Ssh1 = ssh_transport:key_init(peer_role(Role), D#data.ssh_params, Payload), Ssh = case ssh_transport:handle_kexinit_msg(Kex, OwnKex, Ssh1) of {ok, NextKexMsg, Ssh2} when Role==client -> send_bytes(NextKexMsg, D), Ssh2; {ok, Ssh2} when Role==server -> Ssh2 end, {next_state, {key_exchange,Role,ReNeg}, D#data{ssh_params=Ssh}}; %%% ######## {key_exchange, client|server, init|renegotiate} #### %%%---- diffie-hellman 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), send_bytes(NewKeys, 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), send_bytes(NewKeys, D), {next_state, {new_keys,client,ReNeg}, D#data{ssh_params=Ssh}}; %%%---- diffie-hellman group exchange handle_event(_, #ssh_msg_kex_dh_gex_request{} = Msg, {key_exchange,server,ReNeg}, D) -> {ok, GexGroup, Ssh1} = ssh_transport:handle_kex_dh_gex_request(Msg, D#data.ssh_params), send_bytes(GexGroup, D), Ssh = ssh_transport:parallell_gen_key(Ssh1), {next_state, {key_exchange_dh_gex_init,server,ReNeg}, D#data{ssh_params=Ssh}}; handle_event(_, #ssh_msg_kex_dh_gex_request_old{} = Msg, {key_exchange,server,ReNeg}, D) -> {ok, GexGroup, Ssh1} = ssh_transport:handle_kex_dh_gex_request(Msg, D#data.ssh_params), send_bytes(GexGroup, D), Ssh = ssh_transport:parallell_gen_key(Ssh1), {next_state, {key_exchange_dh_gex_init,server,ReNeg}, D#data{ssh_params=Ssh}}; handle_event(_, #ssh_msg_kex_dh_gex_group{} = Msg, {key_exchange,client,ReNeg}, D) -> {ok, KexGexInit, Ssh} = ssh_transport:handle_kex_dh_gex_group(Msg, D#data.ssh_params), send_bytes(KexGexInit, D), {next_state, {key_exchange_dh_gex_reply,client,ReNeg}, D#data{ssh_params=Ssh}}; %%%---- elliptic curve diffie-hellman 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), send_bytes(NewKeys, 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), send_bytes(NewKeys, D), {next_state, {new_keys,client,ReNeg}, D#data{ssh_params=Ssh}}; %%% ######## {key_exchange_dh_gex_init, server, init|renegotiate} #### 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), send_bytes(NewKeys, D), {next_state, {new_keys,server,ReNeg}, D#data{ssh_params=Ssh}}; %%% ######## {key_exchange_dh_gex_reply, client, init|renegotiate} #### 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}}; %%% ######## {new_keys, client|server} #### %% First key exchange round: handle_event(_, #ssh_msg_newkeys{} = Msg, {new_keys,Role,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}}; %% 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}}; %%% ######## {service_request, client|server} handle_event(_, Msg = #ssh_msg_service_request{name=ServiceName}, StateName = {service_request,server}, D) -> case ServiceName of "ssh-userauth" -> Ssh0 = #ssh{session_id=SessionId} = D#data.ssh_params, {ok, {Reply, Ssh}} = ssh_auth:handle_userauth_request(Msg, SessionId, Ssh0), send_bytes(Reply, D), {next_state, {userauth,server}, D#data{ssh_params = Ssh}}; _ -> disconnect(#ssh_msg_disconnect{code = ?SSH_DISCONNECT_SERVICE_NOT_AVAILABLE, description = "Unknown service"}, StateName, D) end; handle_event(_, #ssh_msg_service_accept{name = "ssh-userauth"}, {service_request,client}, #data{ssh_params = #ssh{service="ssh-userauth"} = Ssh0} = State) -> {Msg, Ssh} = ssh_auth:init_userauth_request_msg(Ssh0), send_bytes(Msg, State), {next_state, {userauth,client}, State#data{auth_user = Ssh#ssh.user, ssh_params = Ssh}}; %%% ######## {userauth, client|server} #### %%---- userauth request to server handle_event(_, Msg = #ssh_msg_userauth_request{service = ServiceName, method = Method}, StateName = {userauth,server}, D = #data{ssh_params=Ssh0}) -> case {ServiceName, Ssh0#ssh.service, Method} of {"ssh-connection", "ssh-connection", "none"} -> %% Probably the very first userauth_request but we deny unauthorized login {not_authorized, _, {Reply,Ssh}} = ssh_auth:handle_userauth_request(Msg, Ssh0#ssh.session_id, Ssh0), send_bytes(Reply, D), {keep_state, D#data{ssh_params = Ssh}}; {"ssh-connection", "ssh-connection", Method} -> %% Userauth request with a method like "password" or so case lists:member(Method, Ssh0#ssh.userauth_methods) of true -> %% Yepp! we support this method case ssh_auth:handle_userauth_request(Msg, Ssh0#ssh.session_id, Ssh0) of {authorized, User, {Reply, Ssh}} -> send_bytes(Reply, D), D#data.starter ! ssh_connected, connected_fun(User, Method, D), {next_state, {connected,server}, D#data{auth_user = User, ssh_params = Ssh#ssh{authenticated = true}}}; {not_authorized, {User, Reason}, {Reply, Ssh}} when Method == "keyboard-interactive" -> retry_fun(User, Reason, D), send_bytes(Reply, D), {next_state, {userauth_keyboard_interactive,server}, D#data{ssh_params = Ssh}}; {not_authorized, {User, Reason}, {Reply, Ssh}} -> retry_fun(User, Reason, D), send_bytes(Reply, D), {keep_state, D#data{ssh_params = Ssh}} end; false -> %% No we do not support this method (=/= none) %% At least one non-erlang client does like this. Retry as the next event {keep_state_and_data, [{next_event, internal, Msg#ssh_msg_userauth_request{method="none"}}] } end; %% {"ssh-connection", Expected, Method} when Expected =/= ServiceName -> Do what? %% {ServiceName, Expected, Method} when Expected =/= ServiceName -> Do what? {ServiceName, _, _} when ServiceName =/= "ssh-connection" -> disconnect(#ssh_msg_disconnect{code = ?SSH_DISCONNECT_SERVICE_NOT_AVAILABLE, description = "Unknown service"}, StateName, D) end; %%---- userauth success to client 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}}}; %%---- userauth failure response to client handle_event(_, #ssh_msg_userauth_failure{}, {userauth,client}=StateName, D = #data{ssh_params = #ssh{userauth_methods = []}}) -> Msg = #ssh_msg_disconnect{code = ?SSH_DISCONNECT_NO_MORE_AUTH_METHODS_AVAILABLE, description = "Unable to connect using the available" " authentication methods"}, disconnect(Msg, StateName, D); handle_event(_, #ssh_msg_userauth_failure{authentications = Methods}, StateName={userauth,client}, D = #data{ssh_params = Ssh0}) -> %% The prefered authentication method failed try next method Ssh1 = case Ssh0#ssh.userauth_methods of none -> %% Server tells us which authentication methods that are allowed Ssh0#ssh{userauth_methods = string:tokens(Methods, ",")}; _ -> %% We already know... Ssh0 end, case ssh_auth:userauth_request_msg(Ssh1) of {disconnect, DisconnectMsg, {Msg, Ssh}} -> send_bytes(Msg, D), disconnect(DisconnectMsg, StateName, D#data{ssh_params = Ssh}); {"keyboard-interactive", {Msg, Ssh}} -> send_bytes(Msg, D), {next_state, {userauth_keyboard_interactive,client}, D#data{ssh_params = Ssh}}; {_Method, {Msg, Ssh}} -> send_bytes(Msg, D), {keep_state, D#data{ssh_params = Ssh}} end; %%---- banner to client handle_event(_, #ssh_msg_userauth_banner{message = Msg}, {userauth,client}, D) -> case D#data.ssh_params#ssh.userauth_quiet_mode of false -> io:format("~s", [Msg]); true -> ok end, keep_state_and_data; %%% ######## {userauth_keyboard_interactive, client|server} handle_event(_, #ssh_msg_userauth_info_request{} = Msg, {userauth_keyboard_interactive, client}, #data{ssh_params = Ssh0} = D) -> case ssh_auth:handle_userauth_info_request(Msg, Ssh0) of {ok, {Reply, Ssh}} -> 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}]} end; handle_event(_, #ssh_msg_userauth_info_response{} = Msg, {userauth_keyboard_interactive, server}, D) -> case ssh_auth:handle_userauth_info_response(Msg, D#data.ssh_params) of {authorized, User, {Reply, Ssh}} -> send_bytes(Reply, D), D#data.starter ! ssh_connected, connected_fun(User, "keyboard-interactive", D), {next_state, {connected,server}, D#data{auth_user = User, ssh_params = Ssh#ssh{authenticated = true}}}; {not_authorized, {User, Reason}, {Reply, Ssh}} -> retry_fun(User, Reason, D), send_bytes(Reply, D), {next_state, {userauth,server}, D#data{ssh_params = Ssh}}; {authorized_but_one_more, _User, {Reply, Ssh}} -> send_bytes(Reply, D), {next_state, {userauth_keyboard_interactive_extra,server}, D#data{ssh_params = Ssh}} end; handle_event(_, #ssh_msg_userauth_info_response{} = Msg, {userauth_keyboard_interactive_extra, server}, D) -> {authorized, User, {Reply, Ssh}} = ssh_auth:handle_userauth_info_response({extra,Msg}, D#data.ssh_params), send_bytes(Reply, D), D#data.starter ! ssh_connected, connected_fun(User, "keyboard-interactive", D), {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}, #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}]}; handle_event(_, Msg=#ssh_msg_userauth_failure{}, {userauth_keyboard_interactive_info_response, client}, #data{ssh_params = Ssh0} = D0) -> Opts = Ssh0#ssh.opts, D = case proplists:get_value(password, Opts) of undefined -> D0; _ -> D0#data{ssh_params = Ssh0#ssh{opts = lists:keyreplace(password,1,Opts, {password,not_ok})}} % FIXME:intermodule dependency end, {next_state, {userauth,client}, D, [{next_event, internal, Msg}]}; 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(_, Msg=#ssh_msg_userauth_info_request{}, {userauth_keyboard_interactive_info_response, client}, D) -> {next_state, {userauth_keyboard_interactive,client}, D, [{next_event, internal, Msg}]}; %%% ######## {connected, client|server} #### handle_event(_, {#ssh_msg_kexinit{},_} = Event, {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}]}; handle_event(_, #ssh_msg_disconnect{description=Desc} = Msg, StateName, D0) -> {disconnect, _, {{replies,Replies}, _}} = ssh_connection:handle_msg(Msg, D0#data.connection_state, role(StateName)), {Actions,D} = send_replies(Replies, D0), disconnect_fun(Desc, D), {stop_and_reply, {shutdown,Desc}, Actions, D}; handle_event(_, #ssh_msg_ignore{}, _, _) -> keep_state_and_data; handle_event(_, #ssh_msg_unimplemented{}, _, _) -> keep_state_and_data; handle_event(_, #ssh_msg_debug{} = Msg, _, D) -> debug_fun(Msg, D), keep_state_and_data; handle_event(internal, Msg=#ssh_msg_global_request{}, StateName, D) -> handle_connection_msg(Msg, StateName, D); handle_event(internal, Msg=#ssh_msg_request_success{}, StateName, D) -> handle_connection_msg(Msg, StateName, D); handle_event(internal, Msg=#ssh_msg_request_failure{}, StateName, D) -> handle_connection_msg(Msg, StateName, D); handle_event(internal, Msg=#ssh_msg_channel_open{}, StateName, D) -> handle_connection_msg(Msg, StateName, D); handle_event(internal, Msg=#ssh_msg_channel_open_confirmation{}, StateName, D) -> handle_connection_msg(Msg, StateName, D); handle_event(internal, Msg=#ssh_msg_channel_open_failure{}, StateName, D) -> handle_connection_msg(Msg, StateName, D); handle_event(internal, Msg=#ssh_msg_channel_window_adjust{}, StateName, D) -> handle_connection_msg(Msg, StateName, D); handle_event(internal, Msg=#ssh_msg_channel_data{}, StateName, D) -> handle_connection_msg(Msg, StateName, D); handle_event(internal, Msg=#ssh_msg_channel_extended_data{}, StateName, D) -> handle_connection_msg(Msg, StateName, D); handle_event(internal, Msg=#ssh_msg_channel_eof{}, StateName, D) -> handle_connection_msg(Msg, StateName, D); handle_event(internal, Msg=#ssh_msg_channel_close{}, StateName, D) -> handle_connection_msg(Msg, StateName, D); handle_event(internal, Msg=#ssh_msg_channel_request{}, StateName, D) -> handle_connection_msg(Msg, StateName, D); handle_event(internal, Msg=#ssh_msg_channel_success{}, StateName, D) -> update_inet_buffers(D#data.socket), handle_connection_msg(Msg, StateName, D); handle_event(internal, Msg=#ssh_msg_channel_failure{}, StateName, D) -> handle_connection_msg(Msg, StateName, D); handle_event(cast, renegotiate, {connected,Role}, D) -> {KeyInitMsg, SshPacket, Ssh} = ssh_transport:key_exchange_init_msg(D#data.ssh_params), send_bytes(SshPacket, D), timer:apply_after(?REKEY_TIMOUT, gen_statem, cast, [self(), renegotiate]), {next_state, {kexinit,Role,renegotiate}, D#data{ssh_params = Ssh, key_exchange_init_msg = KeyInitMsg}}; handle_event(cast, renegotiate, _, _) -> %% Already in key-exchange so safe to ignore timer:apply_after(?REKEY_TIMOUT, gen_statem, cast, [self(), renegotiate]), % FIXME: not here in original keep_state_and_data; %% Rekey due to sent data limit reached? handle_event(cast, data_size, {connected,Role}, D) -> {ok, [{send_oct,Sent0}]} = inet:getstat(D#data.socket, [send_oct]), Sent = Sent0 - D#data.last_size_rekey, MaxSent = proplists:get_value(rekey_limit, D#data.opts, 1024000000), timer:apply_after(?REKEY_DATA_TIMOUT, gen_statem, cast, [self(), data_size]), case Sent >= MaxSent of true -> {KeyInitMsg, SshPacket, Ssh} = ssh_transport:key_exchange_init_msg(D#data.ssh_params), send_bytes(SshPacket, D), {next_state, {kexinit,Role,renegotiate}, D#data{ssh_params = Ssh, key_exchange_init_msg = KeyInitMsg, last_size_rekey = Sent0}}; _ -> keep_state_and_data end; handle_event(cast, data_size, _, _) -> %% Already in key-exchange so safe to ignore timer:apply_after(?REKEY_DATA_TIMOUT, gen_statem, cast, [self(), data_size]), % FIXME: not here in original keep_state_and_data; handle_event(cast, _, StateName, _) when StateName /= {connected,server}, StateName /= {connected,client} -> {keep_state_and_data, [postpone]}; handle_event(cast, {adjust_window,ChannelId,Bytes}, {connected,_}, D) -> case ssh_channel:cache_lookup(cache(D), ChannelId) of #channel{recv_window_size = WinSize, recv_window_pending = Pending, recv_packet_size = PktSize} = Channel when (WinSize-Bytes) >= 2*PktSize -> %% The peer can send at least two more *full* packet, no hurry. ssh_channel:cache_update(cache(D), Channel#channel{recv_window_pending = Pending + Bytes}), keep_state_and_data; #channel{recv_window_size = WinSize, recv_window_pending = Pending, remote_id = Id} = Channel -> %% Now we have to update the window - we can't receive so many more pkts ssh_channel:cache_update(cache(D), Channel#channel{recv_window_size = WinSize + Bytes + Pending, recv_window_pending = 0}), Msg = ssh_connection:channel_adjust_window_msg(Id, Bytes + Pending), {keep_state, send_msg(Msg,D)}; undefined -> keep_state_and_data end; handle_event(cast, {reply_request,success,ChannelId}, {connected,_}, D) -> case ssh_channel:cache_lookup(cache(D), ChannelId) of #channel{remote_id = RemoteId} -> Msg = ssh_connection:channel_success_msg(RemoteId), update_inet_buffers(D#data.socket), {keep_state, send_msg(Msg,D)}; undefined -> keep_state_and_data end; handle_event(cast, {request,ChannelPid, ChannelId, Type, Data}, {connected,_}, D) -> {keep_state, handle_request(ChannelPid, ChannelId, Type, Data, false, none, D)}; handle_event(cast, {request,ChannelId,Type,Data}, {connected,_}, D) -> {keep_state, handle_request(ChannelId, Type, Data, false, none, D)}; handle_event(cast, {unknown,Data}, {connected,_}, D) -> Msg = #ssh_msg_unimplemented{sequence = Data}, {keep_state, send_msg(Msg,D)}; %%% Previously handle_sync_event began here handle_event({call,From}, get_print_info, StateName, D) -> Reply = try {inet:sockname(D#data.socket), inet:peername(D#data.socket) } of {{ok,Local}, {ok,Remote}} -> {{Local,Remote},io_lib:format("statename=~p",[StateName])}; _ -> {{"-",0},"-"} catch _:_ -> {{"?",0},"?"} end, {keep_state_and_data, [{reply,From,Reply}]}; handle_event({call,From}, {connection_info, Options}, _, D) -> Info = fold_keys(Options, fun conn_info/2, D), {keep_state_and_data, [{reply,From,Info}]}; handle_event({call,From}, {channel_info,ChannelId,Options}, _, D) -> case ssh_channel:cache_lookup(cache(D), ChannelId) of #channel{} = Channel -> Info = fold_keys(Options, fun chann_info/2, Channel), {keep_state_and_data, [{reply,From,Info}]}; undefined -> {keep_state_and_data, [{reply,From,[]}]} end; handle_event({call,From}, {info, all}, _, D) -> Result = ssh_channel:cache_foldl(fun(Channel, Acc) -> [Channel | Acc] end, [], cache(D)), {keep_state_and_data, [{reply, From, {ok,Result}}]}; handle_event({call,From}, {info, ChannelPid}, _, D) -> Result = ssh_channel:cache_foldl( fun(Channel, Acc) when Channel#channel.user == ChannelPid -> [Channel | Acc]; (_, Acc) -> Acc end, [], cache(D)), {keep_state_and_data, [{reply, From, {ok,Result}}]}; handle_event({call,From}, stop, StateName, D0) -> {disconnect, _Reason, {{replies, Replies}, Connection}} = ssh_connection:handle_msg(#ssh_msg_disconnect{code = ?SSH_DISCONNECT_BY_APPLICATION, description = "User closed down connection"}, D0#data.connection_state, role(StateName)), {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} -> {keep_state_and_data, [postpone]}; handle_event({call,From}, {request, ChannelPid, ChannelId, Type, Data, Timeout}, {connected,_}, D0) -> 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) -> 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) -> {{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) -> 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), {keep_state, D, [{reply,From,ok}]}; _ -> {keep_state, D0, [{reply,From,{error,closed}}]} end; handle_event({call,From}, {open, ChannelPid, Type, InitialWindowSize, MaxPacketSize, Data, Timeout}, {connected,_}, D0) -> erlang:monitor(process, ChannelPid), {ChannelId, D1} = new_channel_id(D0), D2 = send_msg(ssh_connection:channel_open_msg(Type, ChannelId, InitialWindowSize, MaxPacketSize, Data), D1), ssh_channel:cache_update(cache(D2), #channel{type = Type, sys = "none", user = ChannelPid, local_id = ChannelId, recv_window_size = InitialWindowSize, recv_packet_size = MaxPacketSize, send_buf = queue:new() }), D = add_request(true, ChannelId, From, D2), start_channel_request_timer(ChannelId, From, Timeout), {keep_state, cache_cancel_idle_timer(D)}; handle_event({call,From}, {send_window, ChannelId}, {connected,_}, D) -> Reply = case ssh_channel:cache_lookup(cache(D), ChannelId) of #channel{send_window_size = WinSize, send_packet_size = Packsize} -> {ok, {WinSize, Packsize}}; undefined -> {error, einval} end, {keep_state_and_data, [{reply,From,Reply}]}; handle_event({call,From}, {recv_window, ChannelId}, {connected,_}, D) -> Reply = case ssh_channel:cache_lookup(cache(D), ChannelId) of #channel{recv_window_size = WinSize, recv_packet_size = Packsize} -> {ok, {WinSize, Packsize}}; undefined -> {error, einval} end, {keep_state_and_data, [{reply,From,Reply}]}; handle_event({call,From}, {close, ChannelId}, {connected,_}, D0) -> case ssh_channel:cache_lookup(cache(D0), ChannelId) of #channel{remote_id = Id} = Channel -> D1 = send_msg(ssh_connection:channel_close_msg(Id), D0), ssh_channel:cache_update(cache(D1), Channel#channel{sent_close = true}), {keep_state, cache_request_idle_timer_check(D1), [{reply,From,ok}]}; undefined -> {keep_state_and_data, [{reply,From,ok}]} end; %%===== Reception of encrypted bytes, decryption and framing handle_event(info, {Proto, Sock, Info}, {hello,_}, #data{socket = Sock, transport_protocol = Proto}) -> case Info of "SSH-" ++ _ -> {keep_state_and_data, [{next_event, internal, {version_exchange,Info}}]}; _ -> {keep_state_and_data, [{next_event, internal, {info_line,Info}}]} end; handle_event(info, {Proto, Sock, NewData}, StateName, D0 = #data{socket = Sock, transport_protocol = Proto}) -> try ssh_transport:handle_packet_part( D0#data.decrypted_data_buffer, <<(D0#data.encrypted_data_buffer)/binary, NewData/binary>>, D0#data.undecrypted_packet_length, D0#data.ssh_params) of {packet_decrypted, DecryptedBytes, EncryptedDataRest, Ssh1} -> D = D0#data{ssh_params = Ssh1#ssh{recv_sequence = ssh_transport:next_seqnum(Ssh1#ssh.recv_sequence)}, decrypted_data_buffer = <<>>, undecrypted_packet_length = undefined, encrypted_data_buffer = EncryptedDataRest}, try ssh_message:decode(set_kex_overload_prefix(DecryptedBytes,D)) of Msg = #ssh_msg_kexinit{} -> {keep_state, D, [{next_event, internal, prepare_next_packet}, {next_event, internal, {Msg,DecryptedBytes}} ]}; Msg -> {keep_state, D, [{next_event, internal, prepare_next_packet}, {next_event, internal, Msg} ]} catch _C:_E -> disconnect(#ssh_msg_disconnect{code = ?SSH_DISCONNECT_PROTOCOL_ERROR, description = "Bad packet"}, StateName, D) end; {get_more, DecryptedBytes, EncryptedDataRest, RemainingSshPacketLen, Ssh1} -> %% Here we know that there are not enough bytes in %% EncryptedDataRest to use. We must wait for more. inet:setopts(Sock, [{active, once}]), {keep_state, D0#data{encrypted_data_buffer = EncryptedDataRest, decrypted_data_buffer = DecryptedBytes, undecrypted_packet_length = RemainingSshPacketLen, ssh_params = Ssh1}}; {bad_mac, Ssh1} -> disconnect(#ssh_msg_disconnect{code = ?SSH_DISCONNECT_PROTOCOL_ERROR, description = "Bad packet"}, StateName, D0#data{ssh_params=Ssh1}); {error, {exceeds_max_size,_PacketLen}} -> disconnect(#ssh_msg_disconnect{code = ?SSH_DISCONNECT_PROTOCOL_ERROR, description = "Bad packet"}, StateName, D0) catch _C:_E -> disconnect(#ssh_msg_disconnect{code = ?SSH_DISCONNECT_PROTOCOL_ERROR, description = "Bad packet"}, StateName, D0) end; %%%==== handle_event(internal, prepare_next_packet, _, D) -> Enough = erlang:max(8, D#data.ssh_params#ssh.decrypt_block_size), case size(D#data.encrypted_data_buffer) of Sz when Sz >= Enough -> self() ! {D#data.transport_protocol, D#data.socket, <<>>}; _ -> ok end, inet:setopts(D#data.socket, [{active, once}]), keep_state_and_data; handle_event(info, {CloseTag,Socket}, StateName, D = #data{socket = Socket, transport_close_tag = CloseTag}) -> %% Simulate a disconnect from the peer handle_event(info, #ssh_msg_disconnect{code = ?SSH_DISCONNECT_BY_APPLICATION, description = "Connection closed"}, StateName, D); handle_event(info, {timeout, {_, From} = Request}, _, #data{connection_state = #connection{requests = Requests} = C0} = D) -> case lists:member(Request, Requests) of true -> %% A channel request is not answered in time. Answer {error,timeout} %% to the caller C = C0#connection{requests = lists:delete(Request, Requests)}, {keep_state, D#data{connection_state=C}, [{reply,From,{error,timeout}}]}; false -> %% The request is answered - just ignore the timeout keep_state_and_data end; %%% Handle that ssh channels user process goes down handle_event(info, {'DOWN', _Ref, process, ChannelPid, _Reason}, _, D0) -> {{replies, Replies}, D1} = handle_channel_down(ChannelPid, D0), {Repls, D} = send_replies(Replies, D1), {keep_state, D, Repls}; %%% So that terminate will be run when supervisor is shutdown handle_event(info, {'EXIT', _Sup, Reason}, _, _) -> {stop, {shutdown, Reason}}; handle_event(info, check_cache, _, D) -> {keep_state, cache_check_set_idle_timer(D)}; handle_event(info, UnexpectedMessage, StateName, D = #data{ssh_params = Ssh}) -> case unexpected_fun(UnexpectedMessage, D) of report -> Msg = lists:flatten( io_lib:format( "Unexpected message '~p' received in state '~p'\n" "Role: ~p\n" "Peer: ~p\n" "Local Address: ~p\n", [UnexpectedMessage, StateName, Ssh#ssh.role, Ssh#ssh.peer, proplists:get_value(address, Ssh#ssh.opts)])), error_logger:info_report(Msg), keep_state_and_data; skip -> keep_state_and_data; Other -> Msg = lists:flatten( io_lib:format("Call to fun in 'unexpectedfun' failed:~n" "Return: ~p\n" "Message: ~p\n" "Role: ~p\n" "Peer: ~p\n" "Local Address: ~p\n", [Other, UnexpectedMessage, Ssh#ssh.role, element(2,Ssh#ssh.peer), proplists:get_value(address, Ssh#ssh.opts)] )), error_logger:error_report(Msg), keep_state_and_data end; handle_event(internal, {disconnect,Msg,_Reason}, StateName, D) -> disconnect(Msg, StateName, D); 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"; _ -> "Internal error" end, disconnect(#ssh_msg_disconnect{code = ?SSH_DISCONNECT_PROTOCOL_ERROR, description = Descr}, StateName, D). %%-------------------------------------------------------------------- -spec terminate(any(), state_name(), #data{} ) -> finalize_termination_result() . %% . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . terminate(normal, StateName, State) -> finalize_termination(StateName, State); terminate({shutdown,{init,Reason}}, StateName, State) -> error_logger:info_report(io_lib:format("Erlang ssh in connection handler init: ~p~n",[Reason])), finalize_termination(StateName, State); terminate(shutdown, StateName, State0) -> %% Terminated by supervisor State = send_msg(#ssh_msg_disconnect{code = ?SSH_DISCONNECT_BY_APPLICATION, description = "Application shutdown"}, State0), finalize_termination(StateName, State); %% terminate({shutdown,Msg}, StateName, State0) when is_record(Msg,ssh_msg_disconnect)-> %% State = send_msg(Msg, State0), %% finalize_termination(StateName, Msg, State); terminate({shutdown,_R}, StateName, State) -> finalize_termination(StateName, State); terminate(Reason, StateName, State0) -> %% Others, e.g undef, {badmatch,_} log_error(Reason), State = send_msg(#ssh_msg_disconnect{code = ?SSH_DISCONNECT_BY_APPLICATION, description = "Internal error"}, State0), finalize_termination(StateName, State). %%-------------------------------------------------------------------- %% . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . format_status(normal, [_, _StateName, D]) -> [{data, [{"State", D}]}]; format_status(terminate, [_, _StateName, D]) -> DataPropList0 = fmt_stat_rec(record_info(fields, data), D, [decrypted_data_buffer, encrypted_data_buffer, key_exchange_init_msg, user_passwords, opts, inet_initial_recbuf_size]), SshPropList = fmt_stat_rec(record_info(fields, ssh), D#data.ssh_params, [c_keyinit, s_keyinit, send_mac_key, send_mac_size, recv_mac_key, recv_mac_size, encrypt_keys, encrypt_ctx, decrypt_keys, decrypt_ctx, compress_ctx, decompress_ctx, shared_secret, exchanged_hash, session_id, keyex_key, keyex_info, available_host_keys]), DataPropList = lists:keyreplace(ssh_params, 1, DataPropList0, {ssh_params,SshPropList}), [{data, [{"State", DataPropList}]}]. fmt_stat_rec(FieldNames, Rec, Exclude) -> Values = tl(tuple_to_list(Rec)), [P || {K,_} = P <- lists:zip(FieldNames, Values), not lists:member(K, Exclude)]. %%-------------------------------------------------------------------- -spec code_change(term() | {down,term()}, state_name(), #data{}, term() ) -> {ok, state_name(), #data{}}. %% . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . code_change(_OldVsn, StateName, State, _Extra) -> {ok, StateName, State}. %%==================================================================== %% Internal functions %%==================================================================== %%-------------------------------------------------------------------- %% Starting start_the_connection_child(UserPid, Role, Socket, Options) -> Sups = proplists:get_value(supervisors, Options), ConnectionSup = proplists:get_value(connection_sup, Sups), Opts = [{supervisors, Sups}, {user_pid, UserPid} | proplists:get_value(ssh_opts, Options, [])], {ok, Pid} = ssh_connection_sup:start_child(ConnectionSup, [Role, Socket, Opts]), ok = socket_control(Socket, Pid, Options), Pid. %%-------------------------------------------------------------------- %% Stopping -type finalize_termination_result() :: ok . finalize_termination(_StateName, #data{transport_cb = Transport, connection_state = Connection, socket = Socket}) -> case Connection of #connection{system_supervisor = SysSup, sub_system_supervisor = SubSysSup} when is_pid(SubSysSup) -> ssh_system_sup:stop_subsystem(SysSup, SubSysSup); _ -> do_nothing end, (catch Transport:close(Socket)), ok. %%-------------------------------------------------------------------- %% "Invert" the Role 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 case proplists:get_value(public_key, proplists:get_value(preferred_algorithms,Options,[]) ) of undefined -> ssh_transport:default_algorithms(public_key); L -> L -- (L--ssh_transport:default_algorithms(public_key)) end of [] -> {stop, {shutdown, "No public key algs"}}; Algs -> [atom_to_list(A) || A<-Algs] catch exit:Reason -> {stop, {shutdown, Reason}} end; supported_host_keys(server, KeyCb, Options) -> [atom_to_list(A) || A <- proplists:get_value(public_key, proplists:get_value(preferred_algorithms,Options,[]), ssh_transport:default_algorithms(public_key) ), available_host_key(KeyCb, A, Options) ]. %% Alg :: atom() available_host_key(KeyCb, Alg, Opts) -> element(1, catch KeyCb:host_key(Alg, Opts)) == ok. 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(Bytes, #data{socket = Socket, transport_cb = Transport}) -> _ = Transport:send(Socket, Bytes), ok. handle_version({2, 0} = NumVsn, StrVsn, Ssh0) -> Ssh = counterpart_versions(NumVsn, StrVsn, Ssh0), {ok, Ssh}; handle_version(_,_,_) -> not_supported. string_version(#ssh{role = client, c_version = Vsn}) -> Vsn; string_version(#ssh{role = server, s_version = Vsn}) -> Vsn. cast(FsmPid, Event) -> gen_statem:cast(FsmPid, Event). call(FsmPid, Event) -> call(FsmPid, Event, infinity). call(FsmPid, Event, Timeout) -> try gen_statem:call(FsmPid, Event, Timeout) of {closed, _R} -> {error, closed}; {killed, _R} -> {error, closed}; Result -> Result catch exit:{noproc, _R} -> {error, closed}; exit:{normal, _R} -> {error, closed}; exit:{{shutdown, _R},_} -> {error, closed} end. handle_connection_msg(Msg, StateName, D0 = #data{starter = User, connection_state = Connection0, event_queue = Qev0}) -> Renegotiation = renegotiation(StateName), Role = role(StateName), try ssh_connection:handle_msg(Msg, Connection0, Role) of {{replies, Replies}, Connection} -> {Repls, D} = case StateName of {connected,_} -> send_replies(Replies, D0#data{connection_state=Connection}); _ -> {ConnReplies, NonConnReplies} = lists:splitwith(fun not_connected_filter/1, Replies), send_replies(NonConnReplies, D0#data{event_queue = Qev0 ++ ConnReplies}) end, {keep_state, D, Repls}; {noreply, Connection} -> {keep_state, D0#data{connection_state = Connection}}; {disconnect, Reason0, {{replies, Replies}, Connection}} -> {Repls, D} = send_replies(Replies, D0#data{connection_state = Connection}), case {Reason0,Role} of {{_, Reason}, client} when ((StateName =/= {connected,client}) and (not Renegotiation)) -> User ! {self(), not_connected, Reason}; _ -> ok end, {stop_and_reply, {shutdown,normal}, Repls, D#data{connection_state = Connection}} catch _:Error -> {disconnect, _Reason, {{replies, Replies}, Connection}} = ssh_connection:handle_msg( #ssh_msg_disconnect{code = ?SSH_DISCONNECT_BY_APPLICATION, description = "Internal error"}, Connection0, Role), {Repls, D} = send_replies(Replies, D0#data{connection_state = Connection}), {stop_and_reply, {shutdown,Error}, Repls, D#data{connection_state = Connection}} end. set_kex_overload_prefix(Msg = <>, #data{ssh_params=SshParams}) when Op == 30; Op == 31 -> case catch atom_to_list(kex(SshParams)) of "ecdh-sha2-" ++ _ -> <<"ecdh",Msg/binary>>; "diffie-hellman-group-exchange-" ++ _ -> <<"dh_gex",Msg/binary>>; "diffie-hellman-group" ++ _ -> <<"dh",Msg/binary>>; _ -> Msg end; set_kex_overload_prefix(Msg, _) -> Msg. kex(#ssh{algorithms=#alg{kex=Kex}}) -> Kex; kex(_) -> undefined. cache(#data{connection_state=C}) -> C#connection.channel_cache. %%%---------------------------------------------------------------- handle_request(ChannelPid, ChannelId, Type, Data, WantReply, From, D) -> case ssh_channel:cache_lookup(cache(D), ChannelId) of #channel{remote_id = Id} = Channel -> update_sys(cache(D), Channel, Type, ChannelPid), send_msg(ssh_connection:channel_request_msg(Id, Type, WantReply, Data), add_request(WantReply, ChannelId, From, D)); undefined -> D end. handle_request(ChannelId, Type, Data, WantReply, From, D) -> case ssh_channel:cache_lookup(cache(D), ChannelId) of #channel{remote_id = Id} -> send_msg(ssh_connection:channel_request_msg(Id, Type, WantReply, Data), add_request(WantReply, ChannelId, From, D)); undefined -> D end. %%%---------------------------------------------------------------- handle_channel_down(ChannelPid, D) -> ssh_channel:cache_foldl( fun(Channel, Acc) when Channel#channel.user == ChannelPid -> ssh_channel:cache_delete(cache(D), Channel#channel.local_id), Acc; (_,Acc) -> Acc end, [], cache(D)), {{replies, []}, cache_check_set_idle_timer(D)}. update_sys(Cache, Channel, Type, ChannelPid) -> ssh_channel:cache_update(Cache, Channel#channel{sys = Type, user = ChannelPid}). add_request(false, _ChannelId, _From, State) -> State; add_request(true, ChannelId, From, #data{connection_state = #connection{requests = Requests0} = Connection} = State) -> Requests = [{ChannelId, From} | Requests0], State#data{connection_state = Connection#connection{requests = Requests}}. new_channel_id(#data{connection_state = #connection{channel_id_seed = Id} = Connection} = State) -> {Id, State#data{connection_state = Connection#connection{channel_id_seed = Id + 1}}}. %%%---------------------------------------------------------------- %% %%% This server/client has decided to disconnect via the state machine: disconnect(Msg=#ssh_msg_disconnect{description=Description}, _StateName, State0) -> State = send_msg(Msg, State0), disconnect_fun(Description, State), {stop, {shutdown,Description}, State}. %%%---------------------------------------------------------------- counterpart_versions(NumVsn, StrVsn, #ssh{role = server} = Ssh) -> Ssh#ssh{c_vsn = NumVsn , c_version = StrVsn}; counterpart_versions(NumVsn, StrVsn, #ssh{role = client} = Ssh) -> Ssh#ssh{s_vsn = NumVsn , s_version = StrVsn}. %%%---------------------------------------------------------------- conn_info(client_version, #data{ssh_params=S}) -> {S#ssh.c_vsn, S#ssh.c_version}; conn_info(server_version, #data{ssh_params=S}) -> {S#ssh.s_vsn, S#ssh.s_version}; conn_info(peer, #data{ssh_params=S}) -> S#ssh.peer; conn_info(user, D) -> D#data.auth_user; conn_info(sockname, D) -> {ok, SockName} = inet:sockname(D#data.socket), SockName; %% dbg options ( = not documented): conn_info(socket, D) -> D#data.socket; conn_info(chan_ids, D) -> ssh_channel:cache_foldl(fun(#channel{local_id=Id}, Acc) -> [Id | Acc] end, [], cache(D)). %%%---------------------------------------------------------------- chann_info(recv_window, C) -> {{win_size, C#channel.recv_window_size}, {packet_size, C#channel.recv_packet_size}}; chann_info(send_window, C) -> {{win_size, C#channel.send_window_size}, {packet_size, C#channel.send_packet_size}}; %% dbg options ( = not documented): chann_info(pid, C) -> C#channel.user. %%%---------------------------------------------------------------- %% Assisting meta function for the *_info functions fold_keys(Keys, Fun, Extra) -> lists:foldr(fun(Key, Acc) -> try Fun(Key, Extra) of Value -> [{Key,Value}|Acc] catch _:_ -> Acc end end, [], Keys). %%%---------------------------------------------------------------- log_error(Reason) -> Report = io_lib:format("Erlang ssh connection handler failed with reason:~n" " ~p~n" "Stacktrace:~n" " ~p~n", [Reason, erlang:get_stacktrace()]), error_logger:error_report(Report). %%%---------------------------------------------------------------- not_connected_filter({connection_reply, _Data}) -> true; not_connected_filter(_) -> false. %%%---------------------------------------------------------------- send_replies(Repls, State) -> lists:foldl(fun get_repl/2, {[],State}, Repls). get_repl({connection_reply,Msg}, {CallRepls,S}) -> if is_record(Msg, ssh_msg_channel_success) -> update_inet_buffers(S#data.socket); true -> ok end, {CallRepls, send_msg(Msg,S)}; get_repl({channel_data,undefined,_Data}, Acc) -> Acc; get_repl({channel_data,Pid,Data}, Acc) -> Pid ! {ssh_cm, self(), Data}, Acc; get_repl({channel_request_reply,From,Data}, {CallRepls,S}) -> {[{reply,From,Data}|CallRepls], S}; get_repl({flow_control,Cache,Channel,From,Msg}, {CallRepls,S}) -> ssh_channel:cache_update(Cache, Channel#channel{flow_control = undefined}), {[{reply,From,Msg}|CallRepls], S}; get_repl({flow_control,From,Msg}, {CallRepls,S}) -> {[{reply,From,Msg}|CallRepls], S}; get_repl(noreply, Acc) -> Acc; get_repl(X, Acc) -> exit({get_repl,X,Acc}). %%%---------------------------------------------------------------- disconnect_fun({disconnect,Msg}, D) -> disconnect_fun(Msg, D); disconnect_fun(Reason, #data{opts=Opts}) -> case proplists:get_value(disconnectfun, Opts) of undefined -> ok; Fun -> catch Fun(Reason) end. unexpected_fun(UnexpectedMessage, #data{opts = Opts, ssh_params = #ssh{peer = {_,Peer} } } ) -> case proplists:get_value(unexpectedfun, Opts) of undefined -> report; Fun -> catch Fun(UnexpectedMessage, Peer) end. debug_fun(#ssh_msg_debug{always_display = Display, message = DbgMsg, language = Lang}, #data{opts = Opts}) -> case proplists:get_value(ssh_msg_debug_fun, Opts) of undefined -> ok; Fun -> catch Fun(self(), Display, DbgMsg, Lang) end. connected_fun(User, Method, #data{ssh_params = #ssh{peer = {_,Peer}}, opts = Opts}) -> case proplists:get_value(connectfun, Opts) of undefined -> ok; Fun -> catch Fun(User, Peer, Method) end. retry_fun(_, undefined, _) -> ok; retry_fun(User, Reason, #data{ssh_params = #ssh{opts = Opts, peer = {_,Peer} }}) -> {Tag,Info} = case Reason of {error, Error} -> {failfun, Error}; _ -> {infofun, Reason} end, Fun = proplists:get_value(Tag, Opts, fun(_,_)-> ok end), try erlang:fun_info(Fun, arity) of {arity, 2} -> %% Backwards compatible catch Fun(User, Info); {arity, 3} -> catch Fun(User, Peer, Info); _ -> ok catch _:_ -> ok end. %%%---------------------------------------------------------------- %%% Cache idle timer that closes the connection if there are no %%% channels open for a while. cache_init_idle_timer(D) -> case proplists:get_value(idle_time, D#data.opts, infinity) of infinity -> D#data{idle_timer_value = infinity, idle_timer_ref = infinity % A flag used later... }; IdleTime -> %% We dont want to set the timeout on first connect D#data{idle_timer_value = IdleTime} end. cache_check_set_idle_timer(D = #data{idle_timer_ref = undefined, idle_timer_value = IdleTime}) -> %% No timer set - shall we set one? case ssh_channel:cache_info(num_entries, cache(D)) of 0 when IdleTime == infinity -> %% No. Meaningless to set a timer that fires in an infinite time... D; 0 -> %% Yes, we'll set one since the cache is empty and it should not %% be that for a specified time D#data{idle_timer_ref = erlang:send_after(IdleTime, self(), {'EXIT',[],"Timeout"})}; _ -> %% No - there are entries in the cache D end; cache_check_set_idle_timer(D) -> %% There is already a timer set or the timeout time is infinite D. cache_cancel_idle_timer(D) -> case D#data.idle_timer_ref of infinity -> %% The timer is not activated D; undefined -> %% The timer is already cancelled D; TimerRef -> %% The timer is active erlang:cancel_timer(TimerRef), D#data{idle_timer_ref = undefined} end. cache_request_idle_timer_check(D = #data{idle_timer_value = infinity}) -> D; cache_request_idle_timer_check(D = #data{idle_timer_value = IdleTime}) -> erlang:send_after(IdleTime, self(), check_cache), D. %%%---------------------------------------------------------------- start_channel_request_timer(_,_, infinity) -> ok; start_channel_request_timer(Channel, From, Time) -> erlang:send_after(Time, self(), {timeout, {Channel, From}}). %%%---------------------------------------------------------------- %%% Connection start and initalization helpers socket_control(Socket, Pid, Options) -> {_, TransportCallback, _} = % For example {_,gen_tcp,_} proplists:get_value(transport, Options, ?DefaultTransport), case TransportCallback:controlling_process(Socket, Pid) of ok -> gen_statem:cast(Pid, socket_control); {error, Reason} -> {error, Reason} end. handshake(Pid, Ref, Timeout) -> receive ssh_connected -> erlang:demonitor(Ref), {ok, Pid}; {Pid, not_connected, Reason} -> {error, Reason}; {Pid, user_password} -> Pass = io:get_password(), Pid ! Pass, handshake(Pid, Ref, Timeout); {Pid, question} -> Answer = io:get_line(""), Pid ! Answer, handshake(Pid, Ref, Timeout); {'DOWN', _, process, Pid, {shutdown, Reason}} -> {error, Reason}; {'DOWN', _, process, Pid, Reason} -> {error, Reason} after Timeout -> stop(Pid), {error, timeout} end. update_inet_buffers(Socket) -> {ok, BufSzs0} = inet:getopts(Socket, [sndbuf,recbuf]), MinVal = 655360, case [{Tag,MinVal} || {Tag,Val} <- BufSzs0, Val < MinVal] of [] -> ok; NewOpts -> inet:setopts(Socket, NewOpts) end.