%% %% %CopyrightBegin% %% %% Copyright Ericsson AB 2008-2013. All Rights Reserved. %% %% The contents of this file are subject to the Erlang Public License, %% Version 1.1, (the "License"); you may not use this file except in %% compliance with the License. You should have received a copy of the %% Erlang Public License along with this software. If not, it can be %% retrieved online at http://www.erlang.org/. %% %% Software distributed under the License is distributed on an "AS IS" %% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See %% the License for the specific language governing rights and limitations %% under the License. %% %% %CopyrightEnd% %% %% %%---------------------------------------------------------------------- %% Purpose: Handles the setup of an ssh connection, e.i. both the %% setup SSH Transport Layer Protocol (RFC 4253) and Authentication %% Protocol (RFC 4252). Details of the different protocols are %% implemented in ssh_transport.erl, ssh_auth.erl %% ---------------------------------------------------------------------- -module(ssh_connection_handler). -behaviour(gen_fsm). -include("ssh.hrl"). -include("ssh_transport.hrl"). -include("ssh_auth.hrl"). -include("ssh_connect.hrl"). -export([start_link/4, send/2, renegotiate/1, send_event/2, connection_info/3, peer_address/1, renegotiate_data/1]). %% gen_fsm callbacks -export([hello/2, kexinit/2, key_exchange/2, new_keys/2, userauth/2, connected/2]). -export([init/1, handle_event/3, handle_sync_event/4, handle_info/3, terminate/3, code_change/4]). %% spawn export -export([ssh_info_handler/3]). -record(state, { transport_protocol, % ex: tcp transport_cb, transport_close_tag, ssh_params, % #ssh{} - from ssh.hrl socket, % socket() decoded_data_buffer, % binary() encoded_data_buffer, % binary() undecoded_packet_length, % integer() key_exchange_init_msg, % #ssh_msg_kexinit{} renegotiate = false, % boolean() manager, % pid() connection_queue, address, port, opts }). -define(DBG_MESSAGE, true). %%==================================================================== %% Internal application API %%==================================================================== %%-------------------------------------------------------------------- %% Function: start_link() -> ok,Pid} | ignore | {error,Error} %% Description:Creates a gen_fsm process which calls Module:init/1 to %% initialize. To ensure a synchronized start-up procedure, this function %% does not return until Module:init/1 has returned. %%-------------------------------------------------------------------- start_link(Role, Manager, Socket, Options) -> gen_fsm:start_link(?MODULE, [Role, Manager, Socket, Options], []). send(ConnectionHandler, Data) -> send_all_state_event(ConnectionHandler, {send, Data}). renegotiate(ConnectionHandler) -> send_all_state_event(ConnectionHandler, renegotiate). renegotiate_data(ConnectionHandler) -> send_all_state_event(ConnectionHandler, data_size). connection_info(ConnectionHandler, From, Options) -> send_all_state_event(ConnectionHandler, {info, From, Options}). %% Replaced with option to connection_info/3. For now keep %% for backwards compatibility peer_address(ConnectionHandler) -> sync_send_all_state_event(ConnectionHandler, peer_address). %%==================================================================== %% gen_fsm callbacks %%==================================================================== %%-------------------------------------------------------------------- %% Function: init(Args) -> {ok, StateName, State} | %% {ok, StateName, State, Timeout} | %% ignore | %% {stop, StopReason} %% Description:Whenever a gen_fsm is started using gen_fsm:start/[3,4] or %% gen_fsm:start_link/3,4, this function is called by the new process to %% initialize. %%-------------------------------------------------------------------- init([Role, Manager, Socket, SshOpts]) -> process_flag(trap_exit, true), {NumVsn, StrVsn} = ssh_transport:versions(Role, SshOpts), ssh_bits:install_messages(ssh_transport:transport_messages(NumVsn)), {Protocol, Callback, CloseTag} = proplists:get_value(transport, SshOpts, {tcp, gen_tcp, tcp_closed}), try init_ssh(Role, NumVsn, StrVsn, SshOpts, Socket) of Ssh -> {ok, hello, #state{ssh_params = Ssh#ssh{send_sequence = 0, recv_sequence = 0}, socket = Socket, decoded_data_buffer = <<>>, encoded_data_buffer = <<>>, transport_protocol = Protocol, transport_cb = Callback, transport_close_tag = CloseTag, manager = Manager, opts = SshOpts }} catch exit:Reason -> {stop, {shutdown, Reason}} end. %%-------------------------------------------------------------------- %% Function: %% state_name(Event, State) -> {next_state, NextStateName, NextState}| %% {next_state, NextStateName, %% NextState, Timeout} | %% {stop, Reason, NewState} %% Description:There should be one instance of this function for each possible %% state name. Whenever a gen_fsm receives an event sent using %% gen_fsm:send_event/2, the instance of this function with the same name as %% the current state name StateName is called to handle the event. It is also %% called if a timeout occurs. %%-------------------------------------------------------------------- hello(socket_control, #state{socket = Socket, ssh_params = Ssh} = State) -> VsnMsg = ssh_transport:hello_version_msg(string_version(Ssh)), send_msg(VsnMsg, State), inet:setopts(Socket, [{packet, line}]), {next_state, hello, next_packet(State)}; hello({info_line, _Line}, State) -> {next_state, hello, next_packet(State)}; hello({version_exchange, Version}, #state{ssh_params = Ssh0, socket = Socket} = State) -> {NumVsn, StrVsn} = ssh_transport:handle_hello_version(Version), case handle_version(NumVsn, StrVsn, Ssh0) of {ok, Ssh1} -> inet:setopts(Socket, [{packet,0}, {mode,binary}]), {KeyInitMsg, SshPacket, Ssh} = ssh_transport:key_exchange_init_msg(Ssh1), send_msg(SshPacket, State), {next_state, kexinit, next_packet(State#state{ssh_params = Ssh, key_exchange_init_msg = KeyInitMsg})}; not_supported -> DisconnectMsg = #ssh_msg_disconnect{code = ?SSH_DISCONNECT_PROTOCOL_VERSION_NOT_SUPPORTED, description = "Protocol version " ++ StrVsn ++ " not supported", language = "en"}, handle_disconnect(DisconnectMsg, State) end. kexinit({#ssh_msg_kexinit{} = Kex, Payload}, #state{ssh_params = #ssh{role = Role} = Ssh0, key_exchange_init_msg = OwnKex} = State) -> Ssh1 = ssh_transport:key_init(opposite_role(Role), Ssh0, Payload), try ssh_transport:handle_kexinit_msg(Kex, OwnKex, Ssh1) of {ok, NextKexMsg, Ssh} when Role == client -> send_msg(NextKexMsg, State), {next_state, key_exchange, next_packet(State#state{ssh_params = Ssh})}; {ok, Ssh} when Role == server -> {next_state, key_exchange, next_packet(State#state{ssh_params = Ssh})} catch #ssh_msg_disconnect{} = DisconnectMsg -> handle_disconnect(DisconnectMsg, State); _:Error -> Desc = log_error(Error), handle_disconnect(#ssh_msg_disconnect{code = ?SSH_DISCONNECT_KEY_EXCHANGE_FAILED, description = Desc, language = "en"}, State) end. key_exchange(#ssh_msg_kexdh_init{} = Msg, #state{ssh_params = #ssh{role = server} =Ssh0} = State) -> try ssh_transport:handle_kexdh_init(Msg, Ssh0) of {ok, KexdhReply, Ssh1} -> send_msg(KexdhReply, State), {ok, NewKeys, Ssh} = ssh_transport:new_keys_message(Ssh1), send_msg(NewKeys, State), {next_state, new_keys, next_packet(State#state{ssh_params = Ssh})} catch #ssh_msg_disconnect{} = DisconnectMsg -> handle_disconnect(DisconnectMsg, State); _:Error -> Desc = log_error(Error), handle_disconnect(#ssh_msg_disconnect{code = ?SSH_DISCONNECT_KEY_EXCHANGE_FAILED, description = Desc, language = "en"}, State) end; key_exchange(#ssh_msg_kexdh_reply{} = Msg, #state{ssh_params = #ssh{role = client} = Ssh0} = State) -> try ssh_transport:handle_kexdh_reply(Msg, Ssh0) of {ok, NewKeys, Ssh} -> send_msg(NewKeys, State), {next_state, new_keys, next_packet(State#state{ssh_params = Ssh})} catch #ssh_msg_disconnect{} = DisconnectMsg -> handle_disconnect(DisconnectMsg, State); _:Error -> Desc = log_error(Error), handle_disconnect(#ssh_msg_disconnect{code = ?SSH_DISCONNECT_KEY_EXCHANGE_FAILED, description = Desc, language = "en"}, State) end; key_exchange(#ssh_msg_kex_dh_gex_group{} = Msg, #state{ssh_params = #ssh{role = server} = Ssh0} = State) -> try ssh_transport:handle_kex_dh_gex_group(Msg, Ssh0) of {ok, NextKexMsg, Ssh1} -> send_msg(NextKexMsg, State), {ok, NewKeys, Ssh} = ssh_transport:new_keys_message(Ssh1), send_msg(NewKeys, State), {next_state, new_keys, next_packet(State#state{ssh_params = Ssh})} catch #ssh_msg_disconnect{} = DisconnectMsg -> handle_disconnect(DisconnectMsg, State); _:Error -> Desc = log_error(Error), handle_disconnect(#ssh_msg_disconnect{code = ?SSH_DISCONNECT_KEY_EXCHANGE_FAILED, description = Desc, language = "en"}, State) end; key_exchange(#ssh_msg_kex_dh_gex_request{} = Msg, #state{ssh_params = #ssh{role = client} = Ssh0} = State) -> try ssh_transport:handle_kex_dh_gex_request(Msg, Ssh0) of {ok, NextKexMsg, Ssh} -> send_msg(NextKexMsg, State), {next_state, new_keys, next_packet(State#state{ssh_params = Ssh})} catch #ssh_msg_disconnect{} = DisconnectMsg -> handle_disconnect(DisconnectMsg, State); _:Error -> Desc = log_error(Error), handle_disconnect(#ssh_msg_disconnect{code = ?SSH_DISCONNECT_KEY_EXCHANGE_FAILED, description = Desc, language = "en"}, State) end; key_exchange(#ssh_msg_kex_dh_gex_reply{} = Msg, #state{ssh_params = #ssh{role = client} = Ssh0} = State) -> try ssh_transport:handle_kex_dh_gex_reply(Msg, Ssh0) of {ok, NewKeys, Ssh} -> send_msg(NewKeys, State), {next_state, new_keys, next_packet(State#state{ssh_params = Ssh})} catch #ssh_msg_disconnect{} = DisconnectMsg -> handle_disconnect(DisconnectMsg, State); _:Error -> Desc = log_error(Error), handle_disconnect(#ssh_msg_disconnect{code = ?SSH_DISCONNECT_KEY_EXCHANGE_FAILED, description = Desc, language = "en"}, State) end. new_keys(#ssh_msg_newkeys{} = Msg, #state{ssh_params = Ssh0} = State0) -> try ssh_transport:handle_new_keys(Msg, Ssh0) of {ok, Ssh} -> {NextStateName, State} = after_new_keys(State0#state{ssh_params = Ssh}), {next_state, NextStateName, next_packet(State)} catch #ssh_msg_disconnect{} = DisconnectMsg -> handle_disconnect(DisconnectMsg, State0); _:Error -> Desc = log_error(Error), handle_disconnect(#ssh_msg_disconnect{code = ?SSH_DISCONNECT_KEY_EXCHANGE_FAILED, description = Desc, language = "en"}, State0) end. userauth(#ssh_msg_service_request{name = "ssh-userauth"} = Msg, #state{ssh_params = #ssh{role = server, session_id = SessionId} = Ssh0} = State) -> ssh_bits:install_messages(ssh_auth:userauth_messages()), try ssh_auth:handle_userauth_request(Msg, SessionId, Ssh0) of {ok, {Reply, Ssh}} -> send_msg(Reply, State), {next_state, userauth, next_packet(State#state{ssh_params = Ssh})} catch #ssh_msg_disconnect{} = DisconnectMsg -> handle_disconnect(DisconnectMsg, State); _:Error -> Desc = log_error(Error), handle_disconnect(#ssh_msg_disconnect{code = ?SSH_DISCONNECT_SERVICE_NOT_AVAILABLE, description = Desc, language = "en"}, State) end; userauth(#ssh_msg_service_accept{name = "ssh-userauth"}, #state{ssh_params = #ssh{role = client, service = "ssh-userauth"} = Ssh0} = State) -> {Msg, Ssh} = ssh_auth:init_userauth_request_msg(Ssh0), send_msg(Msg, State), {next_state, userauth, next_packet(State#state{ssh_params = Ssh})}; userauth(#ssh_msg_userauth_request{service = "ssh-connection", method = "none"} = Msg, #state{ssh_params = #ssh{session_id = SessionId, role = server, service = "ssh-connection"} = Ssh0 } = State) -> try ssh_auth:handle_userauth_request(Msg, SessionId, Ssh0) of {not_authorized, {_User, _Reason}, {Reply, Ssh}} -> send_msg(Reply, State), {next_state, userauth, next_packet(State#state{ssh_params = Ssh})} catch #ssh_msg_disconnect{} = DisconnectMsg -> handle_disconnect(DisconnectMsg, State); _:Error -> Desc = log_error(Error), handle_disconnect(#ssh_msg_disconnect{code = ?SSH_DISCONNECT_SERVICE_NOT_AVAILABLE, description = Desc, language = "en"}, State) end; userauth(#ssh_msg_userauth_request{service = "ssh-connection", method = Method} = Msg, #state{ssh_params = #ssh{session_id = SessionId, role = server, service = "ssh-connection", peer = {_, Address}} = Ssh0, opts = Opts, manager = Pid} = State) -> try ssh_auth:handle_userauth_request(Msg, SessionId, Ssh0) of {authorized, User, {Reply, Ssh}} -> send_msg(Reply, State), ssh_userreg:register_user(User, Pid), Pid ! ssh_connected, connected_fun(User, Address, Method, Opts), {next_state, connected, next_packet(State#state{ssh_params = Ssh})}; {not_authorized, {User, Reason}, {Reply, Ssh}} -> retry_fun(User, Reason, Opts), send_msg(Reply, State), {next_state, userauth, next_packet(State#state{ssh_params = Ssh})} catch #ssh_msg_disconnect{} = DisconnectMsg -> handle_disconnect(DisconnectMsg, State); _:Error -> Desc = log_error(Error), handle_disconnect(#ssh_msg_disconnect{code = ?SSH_DISCONNECT_SERVICE_NOT_AVAILABLE, description = Desc, language = "en"}, State) end; userauth(#ssh_msg_userauth_info_request{} = Msg, #state{ssh_params = #ssh{role = client, io_cb = IoCb} = Ssh0} = State) -> try ssh_auth:handle_userauth_info_request(Msg, IoCb, Ssh0) of {ok, {Reply, Ssh}} -> send_msg(Reply, State), {next_state, userauth, next_packet(State#state{ssh_params = Ssh})} catch #ssh_msg_disconnect{} = DisconnectMsg -> handle_disconnect(DisconnectMsg, State); _:Error -> Desc = log_error(Error), handle_disconnect(#ssh_msg_disconnect{code = ?SSH_DISCONNECT_SERVICE_NOT_AVAILABLE, description = Desc, language = "en"}, State) end; userauth(#ssh_msg_userauth_info_response{} = Msg, #state{ssh_params = #ssh{role = server} = Ssh0} = State) -> try ssh_auth:handle_userauth_info_response(Msg, Ssh0) of {ok, {Reply, Ssh}} -> send_msg(Reply, State), {next_state, userauth, next_packet(State#state{ssh_params = Ssh})} catch #ssh_msg_disconnect{} = DisconnectMsg -> handle_disconnect(DisconnectMsg, State); _:Error -> Desc = log_error(Error), handle_disconnect(#ssh_msg_disconnect{code = ?SSH_DISCONNECT_SERVICE_NOT_AVAILABLE, description = Desc, language = "en"}, State) end; userauth(#ssh_msg_userauth_success{}, #state{ssh_params = #ssh{role = client}, manager = Pid} = State) -> Pid ! ssh_connected, {next_state, connected, next_packet(State)}; userauth(#ssh_msg_userauth_failure{}, #state{ssh_params = #ssh{role = client, userauth_methods = []}} = State) -> Msg = #ssh_msg_disconnect{code = ?SSH_DISCONNECT_NO_MORE_AUTH_METHODS_AVAILABLE, description = "Unable to connect using the available" " authentication methods", language = "en"}, handle_disconnect(Msg, State); %% Server tells us which authentication methods that are allowed userauth(#ssh_msg_userauth_failure{authentications = Methodes}, #state{ssh_params = #ssh{role = client, userauth_methods = none} = Ssh0} = State) -> AuthMethods = string:tokens(Methodes, ","), Ssh1 = Ssh0#ssh{userauth_methods = AuthMethods}, case ssh_auth:userauth_request_msg(Ssh1) of {disconnect, DisconnectMsg, {Msg, Ssh}} -> send_msg(Msg, State), handle_disconnect(DisconnectMsg, State#state{ssh_params = Ssh}); {Msg, Ssh} -> send_msg(Msg, State), {next_state, userauth, next_packet(State#state{ssh_params = Ssh})} end; %% The prefered authentication method failed try next method userauth(#ssh_msg_userauth_failure{}, #state{ssh_params = #ssh{role = client} = Ssh0} = State) -> case ssh_auth:userauth_request_msg(Ssh0) of {disconnect, DisconnectMsg,{Msg, Ssh}} -> send_msg(Msg, State), handle_disconnect(DisconnectMsg, State#state{ssh_params = Ssh}); {Msg, Ssh} -> send_msg(Msg, State), {next_state, userauth, next_packet(State#state{ssh_params = Ssh})} end; userauth(#ssh_msg_userauth_banner{}, #state{ssh_params = #ssh{userauth_quiet_mode = true, role = client}} = State) -> {next_state, userauth, next_packet(State)}; userauth(#ssh_msg_userauth_banner{message = Msg}, #state{ssh_params = #ssh{userauth_quiet_mode = false, role = client}} = State) -> io:format("~s", [Msg]), {next_state, userauth, next_packet(State)}. connected({#ssh_msg_kexinit{}, _Payload} = Event, State) -> kexinit(Event, State#state{renegotiate = true}). %%-------------------------------------------------------------------- %% Function: %% handle_event(Event, StateName, State) -> {next_state, NextStateName, %% NextState} | %% {next_state, NextStateName, %% NextState, Timeout} | %% {stop, Reason, NewState} %% Description: Whenever a gen_fsm receives an event sent using %% gen_fsm:send_all_state_event/2, this function is called to handle %% the event. %%-------------------------------------------------------------------- handle_event({send, Data}, StateName, #state{ssh_params = Ssh0} = State) -> {Packet, Ssh} = ssh_transport:pack(Data, Ssh0), send_msg(Packet, State), {next_state, StateName, next_packet(State#state{ssh_params = Ssh})}; handle_event(#ssh_msg_disconnect{} = Msg, _StateName, #state{manager = Pid} = State) -> (catch ssh_connection_manager:event(Pid, Msg)), {stop, normal, State}; handle_event(#ssh_msg_ignore{}, StateName, State) -> {next_state, StateName, next_packet(State)}; handle_event(#ssh_msg_debug{always_display = true, message = DbgMsg}, StateName, State) -> io:format("DEBUG: ~p\n", [DbgMsg]), {next_state, StateName, next_packet(State)}; handle_event(#ssh_msg_debug{}, StateName, State) -> {next_state, StateName, next_packet(State)}; handle_event(#ssh_msg_unimplemented{}, StateName, State) -> {next_state, StateName, next_packet(State)}; handle_event(renegotiate, connected, #state{ssh_params = Ssh0} = State) -> {KeyInitMsg, SshPacket, Ssh} = ssh_transport:key_exchange_init_msg(Ssh0), send_msg(SshPacket, State), {next_state, connected, next_packet(State#state{ssh_params = Ssh, key_exchange_init_msg = KeyInitMsg, renegotiate = true})}; handle_event(renegotiate, StateName, State) -> %% Allready in keyexcahange so ignore {next_state, StateName, State}; handle_event({info, From, Options}, StateName, #state{ssh_params = Ssh} = State) -> spawn(?MODULE, ssh_info_handler, [Options, Ssh, From]), {next_state, StateName, State}; handle_event(data_size, connected, #state{ssh_params = Ssh0} = State) -> Sent = inet:getstat(State#state.socket, [send_oct]), MaxSent = proplists:get_value(rekey_limit, State#state.opts, 1024000000), case Sent >= MaxSent of true -> {KeyInitMsg, SshPacket, Ssh} = ssh_transport:key_exchange_init_msg(Ssh0), send_msg(SshPacket, State), {next_state, connected, next_packet(State#state{ssh_params = Ssh, key_exchange_init_msg = KeyInitMsg, renegotiate = true})}; _ -> {next_state, connected, next_packet(State)} end; handle_event(data_size, StateName, State) -> {next_state, StateName, State}; handle_event({unknown, Data}, StateName, State) -> Msg = #ssh_msg_unimplemented{sequence = Data}, send_msg(Msg, State), {next_state, StateName, next_packet(State)}. %%-------------------------------------------------------------------- %% Function: %% handle_sync_event(Event, From, StateName, %% State) -> {next_state, NextStateName, NextState} | %% {next_state, NextStateName, NextState, %% Timeout} | %% {reply, Reply, NextStateName, NextState}| %% {reply, Reply, NextStateName, NextState, %% Timeout} | %% {stop, Reason, NewState} | %% {stop, Reason, Reply, NewState} %% Description: Whenever a gen_fsm receives an event sent using %% gen_fsm:sync_send_all_state_event/2,3, this function is called to handle %% the event. %%-------------------------------------------------------------------- %% Replaced with option to connection_info/3. For now keep %% for backwards compatibility handle_sync_event(peer_address, _From, StateName, #state{ssh_params = #ssh{peer = {_, Address}}} = State) -> {reply, {ok, Address}, StateName, State}. %%-------------------------------------------------------------------- %% Function: %% handle_info(Info,StateName,State)-> {next_state, NextStateName, NextState}| %% {next_state, NextStateName, NextState, %% Timeout} | %% {stop, Reason, NewState} %% Description: This function is called by a gen_fsm when it receives any %% other message than a synchronous or asynchronous event %% (or a system message). %%-------------------------------------------------------------------- handle_info({Protocol, Socket, "SSH-" ++ _ = Version}, hello, #state{socket = Socket, transport_protocol = Protocol} = State ) -> event({version_exchange, Version}, hello, State); handle_info({Protocol, Socket, Info}, hello, #state{socket = Socket, transport_protocol = Protocol} = State) -> event({info_line, Info}, hello, State); handle_info({Protocol, Socket, Data}, Statename, #state{socket = Socket, transport_protocol = Protocol, ssh_params = #ssh{decrypt_block_size = BlockSize, recv_mac_size = MacSize} = Ssh0, decoded_data_buffer = <<>>, encoded_data_buffer = EncData0} = State0) -> %% Implementations SHOULD decrypt the length after receiving the %% first 8 (or cipher block size, whichever is larger) bytes of a %% packet. (RFC 4253: Section 6 - Binary Packet Protocol) case size(EncData0) + size(Data) >= erlang:max(8, BlockSize) of true -> {Ssh, SshPacketLen, DecData, EncData} = ssh_transport:decrypt_first_block(<<EncData0/binary, Data/binary>>, Ssh0), case SshPacketLen > ?SSH_MAX_PACKET_SIZE of true -> DisconnectMsg = #ssh_msg_disconnect{code = ?SSH_DISCONNECT_PROTOCOL_ERROR, description = "Bad packet length " ++ integer_to_list(SshPacketLen), language = "en"}, handle_disconnect(DisconnectMsg, State0); false -> RemainingSshPacketLen = (SshPacketLen + ?SSH_LENGHT_INDICATOR_SIZE) - BlockSize + MacSize, State = State0#state{ssh_params = Ssh}, handle_ssh_packet_data(RemainingSshPacketLen, DecData, EncData, Statename, State) end; false -> {next_state, Statename, next_packet(State0#state{encoded_data_buffer = <<EncData0/binary, Data/binary>>})} end; handle_info({Protocol, Socket, Data}, Statename, #state{socket = Socket, transport_protocol = Protocol, decoded_data_buffer = DecData, encoded_data_buffer = EncData, undecoded_packet_length = Len} = State) when is_integer(Len) -> handle_ssh_packet_data(Len, DecData, <<EncData/binary, Data/binary>>, Statename, State); handle_info({CloseTag, _Socket}, _StateName, #state{transport_close_tag = CloseTag, ssh_params = #ssh{role = _Role, opts = _Opts}} = State) -> DisconnectMsg = #ssh_msg_disconnect{code = ?SSH_DISCONNECT_CONNECTION_LOST, description = "Connection Lost", language = "en"}, {stop, {shutdown, DisconnectMsg}, State}; %%% So that terminate will be run when supervisor is shutdown handle_info({'EXIT', _Sup, Reason}, _StateName, State) -> {stop, Reason, State}; handle_info(UnexpectedMessage, StateName, #state{ssh_params = SshParams} = State) -> 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, SshParams#ssh.role, SshParams#ssh.peer, proplists:get_value(address, SshParams#ssh.opts)])), error_logger:info_report(Msg), {next_state, StateName, State}. %%-------------------------------------------------------------------- %% Function: terminate(Reason, StateName, State) -> void() %% Description:This function is called by a gen_fsm when it is about %% to terminate. It should be the opposite of Module:init/1 and do any %% necessary cleaning up. When it returns, the gen_fsm terminates with %% Reason. The return value is ignored. %%-------------------------------------------------------------------- terminate(normal, _, #state{transport_cb = Transport, socket = Socket, manager = Pid}) -> (catch ssh_userreg:delete_user(Pid)), (catch Transport:close(Socket)), ok; %% Terminated as manager terminated terminate(shutdown, StateName, #state{ssh_params = Ssh0} = State) -> DisconnectMsg = #ssh_msg_disconnect{code = ?SSH_DISCONNECT_BY_APPLICATION, description = "Application shutdown", language = "en"}, {SshPacket, Ssh} = ssh_transport:ssh_packet(DisconnectMsg, Ssh0), send_msg(SshPacket, State), terminate(normal, StateName, State#state{ssh_params = Ssh}); terminate({shutdown, #ssh_msg_disconnect{} = Msg}, StateName, #state{ssh_params = Ssh0, manager = Pid} = State) -> {SshPacket, Ssh} = ssh_transport:ssh_packet(Msg, Ssh0), send_msg(SshPacket, State), ssh_connection_manager:event(Pid, Msg), terminate(normal, StateName, State#state{ssh_params = Ssh}); terminate(Reason, StateName, #state{ssh_params = Ssh0, manager = Pid} = State) -> log_error(Reason), DisconnectMsg = #ssh_msg_disconnect{code = ?SSH_DISCONNECT_BY_APPLICATION, description = "Internal error", language = "en"}, {SshPacket, Ssh} = ssh_transport:ssh_packet(DisconnectMsg, Ssh0), ssh_connection_manager:event(Pid, DisconnectMsg), send_msg(SshPacket, State), terminate(normal, StateName, State#state{ssh_params = Ssh}). %%-------------------------------------------------------------------- %% Function: %% code_change(OldVsn, StateName, State, Extra) -> {ok, StateName, NewState} %% Description: Convert process state when code is changed %%-------------------------------------------------------------------- code_change(_OldVsn, StateName, State, _Extra) -> {ok, StateName, State}. %%-------------------------------------------------------------------- %%% Internal functions %%-------------------------------------------------------------------- init_ssh(client = Role, Vsn, Version, Options, Socket) -> IOCb = case proplists:get_value(user_interaction, Options, true) of true -> ssh_io; false -> ssh_no_io end, AuthMethods = proplists:get_value(auth_methods, Options, ?SUPPORTED_AUTH_METHODS), {ok, PeerAddr} = inet:peername(Socket), PeerName = proplists:get_value(host, Options), KeyCb = proplists:get_value(key_cb, Options, ssh_file), #ssh{role = Role, c_vsn = Vsn, c_version = Version, key_cb = KeyCb, io_cb = IOCb, userauth_quiet_mode = proplists:get_value(quiet_mode, Options, false), opts = Options, userauth_supported_methods = AuthMethods, peer = {PeerName, PeerAddr}, available_host_keys = supported_host_keys(Role, KeyCb, Options) }; init_ssh(server = Role, Vsn, Version, Options, Socket) -> AuthMethods = proplists:get_value(auth_methods, Options, ?SUPPORTED_AUTH_METHODS), {ok, PeerAddr} = inet:peername(Socket), KeyCb = proplists:get_value(key_cb, Options, ssh_file), #ssh{role = Role, s_vsn = Vsn, s_version = Version, key_cb = KeyCb, io_cb = proplists:get_value(io_cb, Options, ssh_io), opts = Options, userauth_supported_methods = AuthMethods, peer = {undefined, PeerAddr}, available_host_keys = supported_host_keys(Role, KeyCb, Options) }. supported_host_keys(client, _, Options) -> try case extract_algs(proplists:get_value(pref_public_key_algs, Options, false), []) of false -> ["ssh-rsa", "ssh-dss"]; Algs -> Algs end catch exit:Reason -> {stop, {shutdown, Reason}} end; supported_host_keys(server, KeyCb, Options) -> lists:foldl(fun(Type, Acc) -> case available_host_key(KeyCb, Type, Options) of {error, _} -> Acc; Alg -> [Alg | Acc] end end, [], %% Prefered alg last so no need to reverse ["ssh-dss", "ssh-rsa"]). extract_algs(false, _) -> false; extract_algs([],[]) -> false; extract_algs([], NewList) -> lists:reverse(NewList); extract_algs([H|T], NewList) -> case H of 'ssh-dss' -> extract_algs(T, ["ssh-dss"|NewList]); 'ssh-rsa' -> extract_algs(T, ["ssh-rsa"|NewList]) end. available_host_key(KeyCb, "ssh-dss"= Alg, Opts) -> case KeyCb:host_key('ssh-dss', Opts) of {ok, _} -> Alg; Other -> Other end; available_host_key(KeyCb, "ssh-rsa" = Alg, Opts) -> case KeyCb:host_key('ssh-rsa', Opts) of {ok, _} -> Alg; Other -> Other end. send_msg(Msg, #state{socket = Socket, transport_cb = Transport}) -> Transport:send(Socket, Msg). 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. send_event(FsmPid, Event) -> gen_fsm:send_event(FsmPid, Event). send_all_state_event(FsmPid, Event) -> gen_fsm:send_all_state_event(FsmPid, Event). sync_send_all_state_event(FsmPid, Event) -> gen_fsm:sync_send_all_state_event(FsmPid, Event). %% simulate send_all_state_event(self(), Event) event(#ssh_msg_disconnect{} = Event, StateName, State) -> handle_event(Event, StateName, State); event(#ssh_msg_ignore{} = Event, StateName, State) -> handle_event(Event, StateName, State); event(#ssh_msg_debug{} = Event, StateName, State) -> handle_event(Event, StateName, State); event(#ssh_msg_unimplemented{} = Event, StateName, State) -> handle_event(Event, StateName, State); %% simulate send_event(self(), Event) event(Event, StateName, State) -> ?MODULE:StateName(Event, State). generate_event(<<?BYTE(Byte), _/binary>> = Msg, StateName, #state{manager = Pid} = State0, EncData) when Byte == ?SSH_MSG_GLOBAL_REQUEST; Byte == ?SSH_MSG_REQUEST_SUCCESS; Byte == ?SSH_MSG_REQUEST_FAILURE; Byte == ?SSH_MSG_CHANNEL_OPEN; Byte == ?SSH_MSG_CHANNEL_OPEN_CONFIRMATION; Byte == ?SSH_MSG_CHANNEL_OPEN_FAILURE; Byte == ?SSH_MSG_CHANNEL_WINDOW_ADJUST; Byte == ?SSH_MSG_CHANNEL_DATA; Byte == ?SSH_MSG_CHANNEL_EXTENDED_DATA; Byte == ?SSH_MSG_CHANNEL_EOF; Byte == ?SSH_MSG_CHANNEL_CLOSE; Byte == ?SSH_MSG_CHANNEL_REQUEST; Byte == ?SSH_MSG_CHANNEL_SUCCESS; Byte == ?SSH_MSG_CHANNEL_FAILURE -> try ssh_connection_manager:event(Pid, Msg), State = generate_event_new_state(State0, EncData), next_packet(State), {next_state, StateName, State} catch exit:{noproc, Reason} -> {stop, {shutdown, Reason}, State0} end; generate_event(Msg, StateName, State0, EncData) -> Event = ssh_bits:decode(Msg), State = generate_event_new_state(State0, EncData), case Event of #ssh_msg_kexinit{} -> %% We need payload for verification later. event({Event, Msg}, StateName, State); _ -> event(Event, StateName, State) end. generate_event_new_state(#state{ssh_params = #ssh{recv_sequence = SeqNum0} = Ssh} = State, EncData) -> SeqNum = ssh_transport:next_seqnum(SeqNum0), State#state{ssh_params = Ssh#ssh{recv_sequence = SeqNum}, decoded_data_buffer = <<>>, encoded_data_buffer = EncData, undecoded_packet_length = undefined}. next_packet(#state{decoded_data_buffer = <<>>, encoded_data_buffer = Buff, ssh_params = #ssh{decrypt_block_size = BlockSize}, socket = Socket, transport_protocol = Protocol} = State) when Buff =/= <<>> -> case size(Buff) >= erlang:max(8, BlockSize) of true -> %% Enough data from the next packet has been received to %% decode the length indicator, fake a socket-recive %% message so that the data will be processed self() ! {Protocol, Socket, <<>>}; false -> inet:setopts(Socket, [{active, once}]) end, State; next_packet(#state{socket = Socket} = State) -> inet:setopts(Socket, [{active, once}]), State. after_new_keys(#state{renegotiate = true} = State) -> {connected, State#state{renegotiate = false}}; after_new_keys(#state{renegotiate = false, ssh_params = #ssh{role = client} = Ssh0} = State) -> ssh_bits:install_messages(ssh_auth:userauth_messages()), {Msg, Ssh} = ssh_auth:service_request_msg(Ssh0), send_msg(Msg, State), {userauth, State#state{ssh_params = Ssh}}; after_new_keys(#state{renegotiate = false, ssh_params = #ssh{role = server}} = State) -> {userauth, State}. handle_ssh_packet_data(RemainingSshPacketLen, DecData, EncData, StateName, State) -> EncSize = size(EncData), case RemainingSshPacketLen > EncSize of true -> {next_state, StateName, next_packet(State#state{decoded_data_buffer = DecData, encoded_data_buffer = EncData, undecoded_packet_length = RemainingSshPacketLen})}; false -> handle_ssh_packet(RemainingSshPacketLen, StateName, State#state{decoded_data_buffer = DecData, encoded_data_buffer = EncData}) end. handle_ssh_packet(Length, StateName, #state{decoded_data_buffer = DecData0, encoded_data_buffer = EncData0, ssh_params = Ssh0, transport_protocol = _Protocol, socket = _Socket} = State0) -> {Ssh1, DecData, EncData, Mac} = ssh_transport:unpack(EncData0, Length, Ssh0), SshPacket = <<DecData0/binary, DecData/binary>>, case ssh_transport:is_valid_mac(Mac, SshPacket, Ssh1) of true -> PacketData = ssh_transport:msg_data(SshPacket), {Ssh1, Msg} = ssh_transport:decompress(Ssh1, PacketData), generate_event(Msg, StateName, State0#state{ssh_params = Ssh1, %% Important to be set for %% next_packet decoded_data_buffer = <<>>}, EncData); false -> DisconnectMsg = #ssh_msg_disconnect{code = ?SSH_DISCONNECT_PROTOCOL_ERROR, description = "Bad mac", language = "en"}, handle_disconnect(DisconnectMsg, State0) end. handle_disconnect(#ssh_msg_disconnect{} = Msg, State) -> {stop, {shutdown, Msg}, 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}. opposite_role(client) -> server; opposite_role(server) -> client. connected_fun(User, PeerAddr, Method, Opts) -> case proplists:get_value(connectfun, Opts) of undefined -> ok; Fun -> catch Fun(User, PeerAddr, Method) end. retry_fun(_, undefined, _) -> ok; retry_fun(User, {error, Reason}, Opts) -> case proplists:get_value(failfun, Opts) of undefined -> ok; Fun -> catch Fun(User, Reason) end; retry_fun(User, Reason, Opts) -> case proplists:get_value(infofun, Opts) of undefined -> ok; Fun -> catch Fun(User, Reason) end. ssh_info_handler(Options, Ssh, From) -> Info = ssh_info(Options, Ssh, []), ssh_connection_manager:send_msg({channel_requst_reply, From, Info}). ssh_info([], _, Acc) -> Acc; ssh_info([client_version | Rest], #ssh{c_vsn = IntVsn, c_version = StringVsn} = SshParams, Acc) -> ssh_info(Rest, SshParams, [{client_version, {IntVsn, StringVsn}} | Acc]); ssh_info([server_version | Rest], #ssh{s_vsn = IntVsn, s_version = StringVsn} = SshParams, Acc) -> ssh_info(Rest, SshParams, [{server_version, {IntVsn, StringVsn}} | Acc]); ssh_info([peer | Rest], #ssh{peer = Peer} = SshParams, Acc) -> ssh_info(Rest, SshParams, [{peer, Peer} | Acc]); ssh_info([ _ | Rest], SshParams, Acc) -> ssh_info(Rest, SshParams, Acc). log_error(Reason) -> Report = io_lib:format("Erlang ssh connection handler failed with reason: " "~p ~n, Stacktace: ~p ~n" "please report this to erlang-bugs@erlang.org \n", [Reason, erlang:get_stacktrace()]), error_logger:error_report(Report), "Internal error".