%%
%% %CopyrightBegin%
%%
%% Copyright Ericsson AB 2008-2016. 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
%%====================================================================
-export([start_link/3,
stop/1
]).
%%% Internal application API
-export([start_connection/4,
open_channel/6,
request/6, request/7,
reply_request/3,
global_request/4,
send/5,
send_eof/2,
info/1, info/2,
connection_info/2,
channel_info/3,
adjust_window/3, close/2, renegotiate/1, renegotiate_data/1,
disconnect/1, disconnect/2,
get_print_info/1
]).
%%% gen_statem callbacks
-export([init/1, handle_event/4, terminate/3, format_status/2, code_change/4]).
%%====================================================================
%% Process state
%%====================================================================
-record(state, {
starter,
auth_user,
connection_state,
latest_channel_id = 0,
idle_timer_ref,
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{}
last_size_rekey = 0,
event_queue = [],
connection_queue,
address,
port,
opts,
recbuf
}).
%%====================================================================
%% Start / stop
%%====================================================================
%%--------------------------------------------------------------------
-spec start_link(role(),
inet:socket(),
proplists:proplist()
) -> {ok, pid()}.
%% . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
start_link(Role, Socket, Options) ->
{ok, proc_lib:spawn_link(?MODULE, init, [[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
%%====================================================================
%%--------------------------------------------------------------------
-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]),
{_, Callback, _} =
proplists:get_value(transport, Options, {tcp, gen_tcp, tcp_closed}),
ok = socket_control(Socket, Pid, Callback),
Ref = erlang:monitor(process, Pid),
handshake(Pid, Ref, 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(),
binary(),
pos_integer(),
pos_integer(),
timeout()
) -> {ok, 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 global_request(connection_ref(),
string(),
boolean(),
iolist()
) -> ok | error.
%% . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
global_request(ConnectionHandler, Type, true = Reply, Data) ->
case call(ConnectionHandler, {global_request, self(), Type, Reply, Data}) of
{ssh_cm, ConnectionHandler, {success, _}} ->
ok;
{ssh_cm, ConnectionHandler, {failure, _}} ->
error
end;
global_request(ConnectionHandler, Type, false = Reply, Data) ->
cast(ConnectionHandler, {global_request, self(), Type, Reply, Data}).
%%--------------------------------------------------------------------
-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()
) -> [ #channel{} ].
-spec info(connection_ref(),
pid()
) -> [ #channel{} ].
%% . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
info(ConnectionHandler) ->
info(ConnectionHandler, {info, 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 renegotiate(connection_ref()
) -> ok.
%% . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
renegotiate(ConnectionHandler) ->
cast(ConnectionHandler, renegotiate).
%%--------------------------------------------------------------------
-spec renegotiate_data(connection_ref()
) -> ok.
%% . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
renegotiate_data(ConnectionHandler) ->
cast(ConnectionHandler, data_size).
%%--------------------------------------------------------------------
-spec close(connection_ref(),
channel_id()
) -> ok.
%% . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
close(ConnectionHandler, ChannelId) ->
case call(ConnectionHandler, {close, ChannelId}) of
ok ->
ok;
{error, closed} ->
ok
end.
%%====================================================================
%% gen_statem callbacks
%%====================================================================
%%--------------------------------------------------------------------
%% . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
init([Role, Socket, SshOpts]) ->
process_flag(trap_exit, true),
{NumVsn, StrVsn} = ssh_transport:versions(Role, SshOpts),
{Protocol, Callback, CloseTag} =
proplists:get_value(transport, SshOpts, {tcp, gen_tcp, tcp_closed}),
Cache = ssh_channel:cache_create(),
State =
init_role(Role,
#state{
connection_state = #connection{channel_cache = Cache,
channel_id_seed = 0,
port_bindings = [],
requests = [],
options = SshOpts},
socket = Socket,
decoded_data_buffer = <<>>,
encoded_data_buffer = <<>>,
transport_protocol = Protocol,
transport_cb = Callback,
transport_close_tag = CloseTag,
opts = SshOpts
}),
try init_ssh_record(Role, NumVsn, StrVsn, SshOpts, Socket) of
Ssh ->
gen_statem:enter_loop(?MODULE,
[], %%[{debug,[trace,log,statistics,debug]} || Role==server],
handle_event_function,
{hello,Role},
State#state{ssh_params = Ssh},
[])
catch
_:Error ->
gen_statem:enter_loop(?MODULE,
[],
handle_event_function,
{init_error,Error},
State,
[])
end.
%%--------------------------------------------------------------------
%% . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
%%% ######## Error in the initialiasation ####
handle_event(_, _Event, {init_error,{badmatch,{error,enotconn}}}, _State) ->
%% Handles the abnormal sequence:
%% SYN->
%% <-SYNACK
%% ACK->
%% RST->
{stop, {shutdown,"TCP connenction to server was prematurely closed by the client"}};
handle_event(_, _Event, {init_error,OtherError}, _State) ->
{stop, {shutdown,{init,OtherError}}};
%%% ######## {hello, client|server} ####
handle_event(_, socket_control, StateName={hello,_}, S=#state{socket=Socket,
ssh_params=Ssh}) ->
VsnMsg = ssh_transport:hello_version_msg(string_version(Ssh)),
send_bytes(VsnMsg, S),
case getopt(recbuf, Socket) of
{ok, Size} ->
inet:setopts(Socket, [{packet, line}, {active, once}, {recbuf, ?MAX_PROTO_VERSION}, {nodelay,true}]),
{next_state, StateName, S#state{recbuf=Size}};
{error, Reason} ->
{stop, {shutdown,Reason}}
end;
handle_event(_, {info_line,_Line}, StateName={hello,client}, S=#state{socket=Socket}) ->
%% The server may send info lines before the version_exchange
inet:setopts(Socket, [{active, once}]),
{next_state, StateName, S};
handle_event(_, {info_line,_Line}, {hello,server}, S) ->
%% as openssh
send_bytes("Protocol mismatch.", S),
{stop, {shutdown,"Protocol mismatch in version exchange."}};
handle_event(_, {version_exchange,Version}, {hello,Role}, S=#state{ssh_params = Ssh0,
socket = Socket,
recbuf = Size}) ->
{NumVsn, StrVsn} = ssh_transport:handle_hello_version(Version),
case handle_version(NumVsn, StrVsn, Ssh0) of
{ok, Ssh1} ->
inet:setopts(Socket, [{packet,0}, {mode,binary}, {active, once}, {recbuf, Size}]),
{KeyInitMsg, SshPacket, Ssh} = ssh_transport:key_exchange_init_msg(Ssh1),
send_bytes(SshPacket, S),
{next_state, {kexinit,Role,init}, S#state{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}, S})
end;
%%% ######## {kexinit, client|server, init|renegotiate} ####
handle_event(_, {#ssh_msg_kexinit{} = Kex, Payload}, {kexinit,client,ReNeg},
S = #state{ssh_params = Ssh0,
key_exchange_init_msg = OwnKex}) ->
Ssh1 = ssh_transport:key_init(server, Ssh0, Payload), % Yes, *server*
{ok, NextKexMsg, Ssh} = ssh_transport:handle_kexinit_msg(Kex, OwnKex, Ssh1),
send_bytes(NextKexMsg, S),
{next_state, {key_exchange,client,ReNeg}, S#state{ssh_params = Ssh}};
handle_event(_, {#ssh_msg_kexinit{} = Kex, Payload}, {kexinit,server,ReNeg},
S = #state{ssh_params = Ssh0,
key_exchange_init_msg = OwnKex}) ->
Ssh1 = ssh_transport:key_init(client, Ssh0, Payload), % Yes, *client*
{ok, Ssh} = ssh_transport:handle_kexinit_msg(Kex, OwnKex, Ssh1),
{next_state, {key_exchange,server,ReNeg}, S#state{ssh_params = Ssh}};
%%% ######## {key_exchange, client|server, init|renegotiate} ####
handle_event(_, #ssh_msg_kexdh_init{} = Msg, {key_exchange,server,ReNeg},
S = #state{ssh_params = Ssh0}) ->
{ok, KexdhReply, Ssh1} = ssh_transport:handle_kexdh_init(Msg, Ssh0),
send_bytes(KexdhReply, S),
{ok, NewKeys, Ssh} = ssh_transport:new_keys_message(Ssh1),
send_bytes(NewKeys, S),
{next_state, {new_keys,server,ReNeg}, S#state{ssh_params = Ssh}};
handle_event(_, #ssh_msg_kexdh_reply{} = Msg, {key_exchange,client,ReNeg},
#state{ssh_params=Ssh0} = State) ->
{ok, NewKeys, Ssh} = ssh_transport:handle_kexdh_reply(Msg, Ssh0),
send_bytes(NewKeys, State),
{next_state, {new_keys,client,ReNeg}, State#state{ssh_params = Ssh}};
handle_event(_, #ssh_msg_kex_dh_gex_request{} = Msg, {key_exchange,server,ReNeg},
#state{ssh_params=Ssh0} = State) ->
{ok, GexGroup, Ssh} = ssh_transport:handle_kex_dh_gex_request(Msg, Ssh0),
send_bytes(GexGroup, State),
{next_state, {key_exchange_dh_gex_init,server,ReNeg}, State#state{ssh_params = Ssh}};
handle_event(_, #ssh_msg_kex_dh_gex_request_old{} = Msg, {key_exchange,server,ReNeg},
#state{ssh_params=Ssh0} = State) ->
{ok, GexGroup, Ssh} = ssh_transport:handle_kex_dh_gex_request(Msg, Ssh0),
send_bytes(GexGroup, State),
{next_state, {key_exchange_dh_gex_init,server,ReNeg}, State#state{ssh_params = Ssh}};
handle_event(_, #ssh_msg_kex_dh_gex_group{} = Msg, {key_exchange,client,ReNeg},
#state{ssh_params=Ssh0} = State) ->
{ok, KexGexInit, Ssh} = ssh_transport:handle_kex_dh_gex_group(Msg, Ssh0),
send_bytes(KexGexInit, State),
{next_state, {key_exchange_dh_gex_reply,client,ReNeg}, State#state{ssh_params = Ssh}};
handle_event(_, #ssh_msg_kex_ecdh_init{} = Msg, {key_exchange,server,ReNeg},
#state{ssh_params=Ssh0} = State) ->
{ok, KexEcdhReply, Ssh1} = ssh_transport:handle_kex_ecdh_init(Msg, Ssh0),
send_bytes(KexEcdhReply, State),
{ok, NewKeys, Ssh} = ssh_transport:new_keys_message(Ssh1),
send_bytes(NewKeys, State),
{next_state, {new_keys,server,ReNeg}, State#state{ssh_params = Ssh}};
handle_event(_, #ssh_msg_kex_ecdh_reply{} = Msg, {key_exchange,client,ReNeg},
#state{ssh_params=Ssh0} = State) ->
{ok, NewKeys, Ssh} = ssh_transport:handle_kex_ecdh_reply(Msg, Ssh0),
send_bytes(NewKeys, State),
{next_state, {new_keys,client,ReNeg}, State#state{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},
#state{ssh_params=Ssh0} = State) ->
{ok, KexGexReply, Ssh1} = ssh_transport:handle_kex_dh_gex_init(Msg, Ssh0),
send_bytes(KexGexReply, State),
{ok, NewKeys, Ssh} = ssh_transport:new_keys_message(Ssh1),
send_bytes(NewKeys, State),
{next_state, {new_keys,server,ReNeg}, State#state{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},
#state{ssh_params=Ssh0} = State) ->
{ok, NewKeys, Ssh1} = ssh_transport:handle_kex_dh_gex_reply(Msg, Ssh0),
send_bytes(NewKeys, State),
{next_state, {new_keys,client,ReNeg}, State#state{ssh_params = Ssh1}};
%%% ######## {new_keys, client|server} ####
handle_event(_, #ssh_msg_newkeys{} = Msg, {new_keys,client,init},
#state{ssh_params = Ssh0} = State) ->
{ok, Ssh1} = ssh_transport:handle_new_keys(Msg, Ssh0),
{MsgReq, Ssh} = ssh_auth:service_request_msg(Ssh1),
send_bytes(MsgReq, State),
{next_state, {service_request,client}, State#state{ssh_params=Ssh}};
handle_event(_, #ssh_msg_newkeys{} = Msg, {new_keys,server,init},
S = #state{ssh_params = Ssh0}) ->
{ok, Ssh} = ssh_transport:handle_new_keys(Msg, Ssh0),
{next_state, {service_request,server}, S#state{ssh_params = Ssh}};
handle_event(_, #ssh_msg_newkeys{}, {new_keys,Role,renegotiate}, S) ->
{next_state, {connected,Role}, S};
%%% ######## {service_request, client|server}
handle_event(_, #ssh_msg_service_request{name = "ssh-userauth"} = Msg, {service_request,server},
#state{ssh_params = #ssh{session_id=SessionId} = Ssh0} = State) ->
{ok, {Reply, Ssh}} = ssh_auth:handle_userauth_request(Msg, SessionId, Ssh0),
send_bytes(Reply, State),
{next_state, {userauth,server}, State#state{ssh_params = Ssh}};
handle_event(_, #ssh_msg_service_request{}, {service_request,server}=StateName, State) ->
Msg = #ssh_msg_disconnect{code = ?SSH_DISCONNECT_SERVICE_NOT_AVAILABLE,
description = "Unknown service"},
disconnect(Msg, StateName, State);
handle_event(_, #ssh_msg_service_accept{name = "ssh-userauth"}, {service_request,client},
#state{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#state{auth_user = Ssh#ssh.user, ssh_params = Ssh}};
%%% ######## {userauth, client|server} ####
handle_event(_, #ssh_msg_userauth_request{service = "ssh-connection",
method = "none"} = Msg, StateName={userauth,server},
#state{ssh_params = #ssh{session_id = SessionId,
service = "ssh-connection"} = Ssh0
} = State) ->
{not_authorized, {_User, _Reason}, {Reply, Ssh}} =
ssh_auth:handle_userauth_request(Msg, SessionId, Ssh0),
send_bytes(Reply, State),
{next_state, StateName, State#state{ssh_params = Ssh}};
handle_event(_, #ssh_msg_userauth_request{service = "ssh-connection",
method = Method} = Msg, StateName={userauth,server},
#state{ssh_params = #ssh{session_id = SessionId,
service = "ssh-connection",
peer = {_, Address}} = Ssh0,
opts = Opts, starter = Pid} = State) ->
case lists:member(Method, Ssh0#ssh.userauth_methods) of
true ->
case ssh_auth:handle_userauth_request(Msg, SessionId, Ssh0) of
{authorized, User, {Reply, Ssh}} ->
send_bytes(Reply, State),
Pid ! ssh_connected,
connected_fun(User, Address, Method, Opts),
{next_state, {connected,server},
State#state{auth_user = User, ssh_params = Ssh#ssh{authenticated = true}}};
{not_authorized, {User, Reason}, {Reply, Ssh}} when Method == "keyboard-interactive" ->
retry_fun(User, Address, Reason, Opts),
send_bytes(Reply, State),
{next_state, {userauth_keyboard_interactive,server}, State#state{ssh_params = Ssh}};
{not_authorized, {User, Reason}, {Reply, Ssh}} ->
retry_fun(User, Address, Reason, Opts),
send_bytes(Reply, State),
{next_state, StateName, State#state{ssh_params = Ssh}}
end;
false ->
%% At least one non-erlang client does like this. Retry as the next event
{next_state, StateName, State,
[{next_event, internal, Msg#ssh_msg_userauth_request{method="none"}}]
}
end;
handle_event(_, #ssh_msg_userauth_request{service = Service}, {userauth,server}=StateName, State)
when Service =/= "ssh-connection" ->
Msg = #ssh_msg_disconnect{code = ?SSH_DISCONNECT_SERVICE_NOT_AVAILABLE,
description = "Unknown service"},
disconnect(Msg, StateName, State);
handle_event(_, #ssh_msg_userauth_success{}, {userauth,client}, #state{ssh_params = Ssh,
starter = Pid} = State) ->
Pid ! ssh_connected,
{next_state, {connected,client}, State#state{ssh_params=Ssh#ssh{authenticated = true}}};
handle_event(_, #ssh_msg_userauth_failure{}, {userauth,client}=StateName,
#state{ssh_params = #ssh{userauth_methods = []}} = State) ->
Msg = #ssh_msg_disconnect{code = ?SSH_DISCONNECT_NO_MORE_AUTH_METHODS_AVAILABLE,
description = "Unable to connect using the available"
" authentication methods"},
disconnect(Msg, StateName, State);
handle_event(_, #ssh_msg_userauth_failure{authentications = Methods}, StateName={userauth,client},
#state{ssh_params = Ssh0 = #ssh{userauth_methods=AuthMthds}} = State) ->
%% The prefered authentication method failed try next method
Ssh1 = case AuthMthds 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, State),
disconnect(DisconnectMsg, StateName, State#state{ssh_params = Ssh});
{"keyboard-interactive", {Msg, Ssh}} ->
send_bytes(Msg, State),
{next_state, {userauth_keyboard_interactive,client}, State#state{ssh_params = Ssh}};
{_Method, {Msg, Ssh}} ->
send_bytes(Msg, State),
{next_state, StateName, State#state{ssh_params = Ssh}}
end;
handle_event(_, #ssh_msg_userauth_banner{}, StateName={userauth,client},
#state{ssh_params = #ssh{userauth_quiet_mode=true}} = State) ->
{next_state, StateName, State};
handle_event(_, #ssh_msg_userauth_banner{message = Msg}, StateName={userauth,client},
#state{ssh_params = #ssh{userauth_quiet_mode=false}} = State) ->
io:format("~s", [Msg]),
{next_state, StateName, State};
%%% ######## {userauth_keyboard_interactive, client|server}
handle_event(_, #ssh_msg_userauth_info_request{} = Msg, {userauth_keyboard_interactive, client},
#state{ssh_params = #ssh{io_cb=IoCb} = Ssh0} = State) ->
{ok, {Reply, Ssh}} = ssh_auth:handle_userauth_info_request(Msg, IoCb, Ssh0),
send_bytes(Reply, State),
{next_state, {userauth_keyboard_interactive_info_response,client}, State#state{ssh_params = Ssh}};
handle_event(_, #ssh_msg_userauth_info_response{} = Msg, {userauth_keyboard_interactive, server},
#state{ssh_params = #ssh{peer = {_,Address}} = Ssh0,
opts = Opts,
starter = Pid} = State) ->
case ssh_auth:handle_userauth_info_response(Msg, Ssh0) of
{authorized, User, {Reply, Ssh}} ->
send_bytes(Reply, State),
Pid ! ssh_connected,
connected_fun(User, Address, "keyboard-interactive", Opts),
{next_state, {connected,server}, State#state{auth_user = User,
ssh_params = Ssh#ssh{authenticated = true}}};
{not_authorized, {User, Reason}, {Reply, Ssh}} ->
retry_fun(User, Address, Reason, Opts),
send_bytes(Reply, State),
{next_state, {userauth,server}, State#state{ssh_params = Ssh}}
end;
handle_event(_, Msg = #ssh_msg_userauth_failure{}, {userauth_keyboard_interactive, client},
#state{ssh_params = Ssh0 = #ssh{userauth_preference=Prefs0}} = State) ->
Prefs = [{Method,M,F,A} || {Method,M,F,A} <- Prefs0,
Method =/= "keyboard-interactive"],
{next_state, {userauth,client},
State#state{ssh_params = Ssh0#ssh{userauth_preference=Prefs}},
[{next_event, internal, Msg}]};
handle_event(_, Msg=#ssh_msg_userauth_failure{}, {userauth_keyboard_interactive_info_response, client}, S) ->
{next_state, {userauth,client}, S, [{next_event, internal, Msg}]};
handle_event(_, Msg=#ssh_msg_userauth_success{}, {userauth_keyboard_interactive_info_response, client}, S) ->
{next_state, {userauth,client}, S, [{next_event, internal, Msg}]};
handle_event(_, Msg=#ssh_msg_userauth_info_request{}, {userauth_keyboard_interactive_info_response, client}, S) ->
{next_state, {userauth_keyboard_interactive,client}, S, [{next_event, internal, Msg}]};
%%% ######## {connected, client|server} ####
handle_event(_, {#ssh_msg_kexinit{},_} = Event, {connected,Role}, #state{ssh_params = Ssh0} = State0) ->
{KeyInitMsg, SshPacket, Ssh} = ssh_transport:key_exchange_init_msg(Ssh0),
State = State0#state{ssh_params = Ssh,
key_exchange_init_msg = KeyInitMsg},
send_bytes(SshPacket, State),
{next_state, {kexinit,Role,renegotiate}, State, [{next_event, internal, Event}]};
handle_event(_, #ssh_msg_disconnect{description=Desc} = Msg, StateName,
State0 = #state{connection_state = Connection0}) ->
{disconnect, _, {{replies, Replies}, _Connection}} =
ssh_connection:handle_msg(Msg, Connection0, role(StateName)),
{Repls,State} = send_replies(Replies, State0),
disconnect_fun(Desc, State#state.opts),
{stop_and_reply, {shutdown,Desc}, Repls, State};
handle_event(_, #ssh_msg_ignore{}, StateName, State) ->
{next_state, StateName, State};
handle_event(_, #ssh_msg_debug{always_display = Display,
message = DbgMsg,
language = Lang}, StateName, #state{opts = Opts} = State) ->
F = proplists:get_value(ssh_msg_debug_fun, Opts,
fun(_ConnRef, _AlwaysDisplay, _Msg, _Language) -> ok end
),
catch F(self(), Display, DbgMsg, Lang),
{next_state, StateName, State};
handle_event(_, #ssh_msg_unimplemented{}, StateName, State) ->
{next_state, StateName, State};
handle_event(internal, Msg=#ssh_msg_global_request{}, StateName, State) ->
handle_connection_msg(Msg, StateName, State);
handle_event(internal, Msg=#ssh_msg_request_success{}, StateName, State) ->
handle_connection_msg(Msg, StateName, State);
handle_event(internal, Msg=#ssh_msg_request_failure{}, StateName, State) ->
handle_connection_msg(Msg, StateName, State);
handle_event(internal, Msg=#ssh_msg_channel_open{}, StateName, State) ->
handle_connection_msg(Msg, StateName, State);
handle_event(internal, Msg=#ssh_msg_channel_open_confirmation{}, StateName, State) ->
handle_connection_msg(Msg, StateName, State);
handle_event(internal, Msg=#ssh_msg_channel_open_failure{}, StateName, State) ->
handle_connection_msg(Msg, StateName, State);
handle_event(internal, Msg=#ssh_msg_channel_window_adjust{}, StateName, State) ->
handle_connection_msg(Msg, StateName, State);
handle_event(internal, Msg=#ssh_msg_channel_data{}, StateName, State) ->
handle_connection_msg(Msg, StateName, State);
handle_event(internal, Msg=#ssh_msg_channel_extended_data{}, StateName, State) ->
handle_connection_msg(Msg, StateName, State);
handle_event(internal, Msg=#ssh_msg_channel_eof{}, StateName, State) ->
handle_connection_msg(Msg, StateName, State);
handle_event(internal, Msg=#ssh_msg_channel_close{}, StateName, State) ->
handle_connection_msg(Msg, StateName, State);
handle_event(internal, Msg=#ssh_msg_channel_request{}, StateName, State) ->
handle_connection_msg(Msg, StateName, State);
handle_event(internal, Msg=#ssh_msg_channel_success{}, StateName, State) ->
handle_connection_msg(Msg, StateName, State);
handle_event(internal, Msg=#ssh_msg_channel_failure{}, StateName, State) ->
handle_connection_msg(Msg, StateName, State);
handle_event(cast, renegotiate, {connected,Role}, #state{ssh_params=Ssh0} = State) ->
{KeyInitMsg, SshPacket, Ssh} = ssh_transport:key_exchange_init_msg(Ssh0),
send_bytes(SshPacket, State),
%%% FIXME: timer
timer:apply_after(?REKEY_TIMOUT, gen_statem, cast, [self(), renegotiate]),
{next_state, {kexinit,Role,renegotiate}, State#state{ssh_params = Ssh,
key_exchange_init_msg = KeyInitMsg}};
handle_event(cast, renegotiate, StateName, State) ->
%% Already in key-exchange so safe to ignore
{next_state, StateName, State};
%% Rekey due to sent data limit reached?
handle_event(cast, data_size, {connected,Role}, #state{ssh_params=Ssh0} = State) ->
{ok, [{send_oct,Sent0}]} = inet:getstat(State#state.socket, [send_oct]),
Sent = Sent0 - State#state.last_size_rekey,
MaxSent = proplists:get_value(rekey_limit, State#state.opts, 1024000000),
%%% FIXME: timer
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(Ssh0),
send_bytes(SshPacket, State),
{next_state, {kexinit,Role,renegotiate}, State#state{ssh_params = Ssh,
key_exchange_init_msg = KeyInitMsg,
last_size_rekey = Sent0}};
_ ->
{next_state, {connected,Role}, State}
end;
handle_event(cast, data_size, StateName, State) ->
%% Already in key-exchange so safe to ignore
{next_state, StateName, State};
handle_event(cast, _, StateName, State) when StateName /= {connected,server},
StateName /= {connected,client} ->
{next_state, StateName, State, [postpone]};
handle_event(cast, {adjust_window,ChannelId,Bytes}, StateName={connected,_Role},
#state{connection_state =
#connection{channel_cache = Cache}} = State0) ->
case ssh_channel:cache_lookup(Cache, 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,
Channel#channel{recv_window_pending = Pending + Bytes}),
{next_state, StateName, State0};
#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,
Channel#channel{recv_window_size =
WinSize + Bytes + Pending,
recv_window_pending = 0}),
Msg = ssh_connection:channel_adjust_window_msg(Id, Bytes + Pending),
{next_state, StateName, send_msg(Msg,State0)};
undefined ->
{next_state, StateName, State0}
end;
handle_event(cast, {reply_request,success,ChannelId}, StateName={connected,_},
#state{connection_state =
#connection{channel_cache = Cache}} = State0) ->
case ssh_channel:cache_lookup(Cache, ChannelId) of
#channel{remote_id = RemoteId} ->
Msg = ssh_connection:channel_success_msg(RemoteId),
{next_state, StateName, send_msg(Msg,State0)};
undefined ->
{next_state, StateName, State0}
end;
handle_event(cast, {request,ChannelPid,ChannelId,Type,Data}, StateName={connected,_}, State0) ->
State = handle_request(ChannelPid, ChannelId, Type, Data, false, none, State0),
{next_state, StateName, State};
handle_event(cast, {request,ChannelId,Type,Data}, StateName={connected,_}, State0) ->
State = handle_request(ChannelId, Type, Data, false, none, State0),
{next_state, StateName, State};
handle_event(cast, {unknown,Data}, StateName={connected,_}, State) ->
Msg = #ssh_msg_unimplemented{sequence = Data},
{next_state, StateName, send_msg(Msg,State)};
%%% Previously handle_sync_event began here
handle_event({call,From}, get_print_info, StateName, State) ->
Reply =
try
{inet:sockname(State#state.socket),
inet:peername(State#state.socket)
}
of
{{ok,Local}, {ok,Remote}} -> {{Local,Remote},io_lib:format("statename=~p",[StateName])};
_ -> {{"-",0},"-"}
catch
_:_ -> {{"?",0},"?"}
end,
{next_state, StateName, State, [{reply,From,Reply}]};
handle_event({call,From}, {connection_info, Options}, StateName, State) ->
Info = ssh_info(Options, State, []),
{next_state, StateName, State, [{reply,From,Info}]};
handle_event({call,From}, {channel_info,ChannelId,Options}, StateName,
State=#state{connection_state = #connection{channel_cache = Cache}}) ->
case ssh_channel:cache_lookup(Cache, ChannelId) of
#channel{} = Channel ->
Info = ssh_channel_info(Options, Channel, []),
{next_state, StateName, State, [{reply,From,Info}]};
undefined ->
{next_state, StateName, State, [{reply,From,[]}]}
end;
handle_event({call,From}, {info, ChannelPid}, StateName, State = #state{connection_state =
#connection{channel_cache = Cache}}) ->
Result = ssh_channel:cache_foldl(
fun(Channel, Acc) when ChannelPid == all;
Channel#channel.user == ChannelPid ->
[Channel | Acc];
(_, Acc) ->
Acc
end, [], Cache),
{next_state, StateName, State, [{reply, From, {ok,Result}}]};
handle_event({call,From}, stop, StateName, #state{connection_state = Connection0} = State0) ->
{disconnect, _Reason, {{replies, Replies}, Connection}} =
ssh_connection:handle_msg(#ssh_msg_disconnect{code = ?SSH_DISCONNECT_BY_APPLICATION,
description = "User closed down connection"},
Connection0, role(StateName)),
{Repls,State} = send_replies(Replies, State0),
{stop_and_reply, normal, [{reply,From,ok}|Repls], State#state{connection_state=Connection}};
handle_event({call,_}, _, StateName, State) when StateName /= {connected,server},
StateName /= {connected,client} ->
{next_state, StateName, State, [postpone]};
handle_event({call,From}, {request, ChannelPid, ChannelId, Type, Data, Timeout}, StateName={connected,_}, State0) ->
State = handle_request(ChannelPid, ChannelId, Type, Data, true, From, State0),
%% Note reply to channel will happen later when reply is recived from peer on the socket
start_timeout(ChannelId, From, Timeout),
handle_idle_timeout(State),
{next_state, StateName, State};
handle_event({call,From}, {request, ChannelId, Type, Data, Timeout}, StateName={connected,_}, State0) ->
State = handle_request(ChannelId, Type, Data, true, From, State0),
%% Note reply to channel will happen later when reply is recived from peer on the socket
start_timeout(ChannelId, From, Timeout),
handle_idle_timeout(State),
{next_state, StateName, State};
handle_event({call,From}, {global_request, Pid, _, _, _} = Request, StateName={connected,_},
#state{connection_state = #connection{channel_cache = Cache}} = State0) ->
State1 = handle_global_request(Request, State0),
Channel = ssh_channel:cache_find(Pid, Cache),
State = add_request(true, Channel#channel.local_id, From, State1),
{next_state, StateName, State};
handle_event({call,From}, {data, ChannelId, Type, Data, Timeout}, StateName={connected,_},
#state{connection_state = #connection{channel_cache=_Cache} = Connection0} = State0) ->
case ssh_connection:channel_data(ChannelId, Type, Data, Connection0, From) of
{{replies, Replies}, Connection} ->
{Repls,State} = send_replies(Replies, State0#state{connection_state = Connection}),
start_timeout(ChannelId, From, Timeout),
{next_state, StateName, State, Repls};
{noreply, Connection} ->
start_timeout(ChannelId, From, Timeout),
{next_state, StateName, State0#state{connection_state = Connection}}
end;
handle_event({call,From}, {eof, ChannelId}, StateName={connected,_},
#state{connection_state = #connection{channel_cache=Cache}} = State0) ->
case ssh_channel:cache_lookup(Cache, ChannelId) of
#channel{remote_id = Id, sent_close = false} ->
State = send_msg(ssh_connection:channel_eof_msg(Id), State0),
{next_state, StateName, State, [{reply,From,ok}]};
_ ->
{next_state, StateName, State0, [{reply,From,{error,closed}}]}
end;
handle_event({call,From},
{open, ChannelPid, Type, InitialWindowSize, MaxPacketSize, Data, Timeout},
StateName = {connected,_},
#state{connection_state = #connection{channel_cache = Cache}} = State0) ->
erlang:monitor(process, ChannelPid),
{ChannelId, State1} = new_channel_id(State0),
Msg = ssh_connection:channel_open_msg(Type, ChannelId,
InitialWindowSize,
MaxPacketSize, Data),
State2 = send_msg(Msg, State1),
Channel = #channel{type = Type,
sys = "none",
user = ChannelPid,
local_id = ChannelId,
recv_window_size = InitialWindowSize,
recv_packet_size = MaxPacketSize,
send_buf = queue:new()
},
ssh_channel:cache_update(Cache, Channel),
State = add_request(true, ChannelId, From, State2),
start_timeout(ChannelId, From, Timeout),
{next_state, StateName, remove_timer_ref(State)};
handle_event({call,From}, {send_window, ChannelId}, StateName={connected,_},
#state{connection_state = #connection{channel_cache = Cache}} = State) ->
Reply = case ssh_channel:cache_lookup(Cache, ChannelId) of
#channel{send_window_size = WinSize,
send_packet_size = Packsize} ->
{ok, {WinSize, Packsize}};
undefined ->
{error, einval}
end,
{next_state, StateName, State, [{reply,From,Reply}]};
handle_event({call,From}, {recv_window, ChannelId}, StateName={connected,_},
#state{connection_state = #connection{channel_cache = Cache}} = State) ->
Reply = case ssh_channel:cache_lookup(Cache, ChannelId) of
#channel{recv_window_size = WinSize,
recv_packet_size = Packsize} ->
{ok, {WinSize, Packsize}};
undefined ->
{error, einval}
end,
{next_state, StateName, State, [{reply,From,Reply}]};
handle_event({call,From}, {close, ChannelId}, StateName={connected,_},
#state{connection_state =
#connection{channel_cache = Cache}} = State0) ->
case ssh_channel:cache_lookup(Cache, ChannelId) of
#channel{remote_id = Id} = Channel ->
State1 = send_msg(ssh_connection:channel_close_msg(Id), State0),
ssh_channel:cache_update(Cache, Channel#channel{sent_close = true}),
handle_idle_timeout(State1),
{next_state, StateName, State1, [{reply,From,ok}]};
undefined ->
{next_state, StateName, State0, [{reply,From,ok}]}
end;
handle_event(info, {Protocol, Socket, "SSH-" ++ _ = Version}, StateName={hello,_},
State=#state{socket = Socket,
transport_protocol = Protocol}) ->
{next_state, StateName, State, [{next_event, internal, {version_exchange,Version}}]};
handle_event(info, {Protocol, Socket, Info}, StateName={hello,_},
State=#state{socket = Socket,
transport_protocol = Protocol}) ->
{next_state, StateName, State, [{next_event, internal, {info_line,Info}}]};
handle_event(info, {Protocol, Socket, Data}, StateName, State0 =
#state{socket = Socket,
transport_protocol = Protocol,
decoded_data_buffer = DecData0,
encoded_data_buffer = EncData0,
undecoded_packet_length = RemainingSshPacketLen0,
ssh_params = Ssh0}) ->
Encoded = <<EncData0/binary, Data/binary>>,
try ssh_transport:handle_packet_part(DecData0, Encoded, RemainingSshPacketLen0, Ssh0)
of
{decoded, Bytes, EncDataRest, Ssh1} ->
State = State0#state{ssh_params =
Ssh1#ssh{recv_sequence = ssh_transport:next_seqnum(Ssh1#ssh.recv_sequence)},
decoded_data_buffer = <<>>,
undecoded_packet_length = undefined,
encoded_data_buffer = EncDataRest},
try
ssh_message:decode(set_prefix_if_trouble(Bytes,State))
of
Msg = #ssh_msg_kexinit{} ->
{next_state, StateName, State, [{next_event, internal, {Msg,Bytes}},
{next_event, internal, prepare_next_packet}
]};
Msg ->
{next_state, StateName, State, [{next_event, internal, Msg},
{next_event, internal, prepare_next_packet}
]}
catch
_C:_E ->
DisconnectMsg =
#ssh_msg_disconnect{code = ?SSH_DISCONNECT_PROTOCOL_ERROR,
description = "Encountered unexpected input"},
disconnect(DisconnectMsg, StateName, State)
end;
{get_more, DecBytes, EncDataRest, RemainingSshPacketLen, Ssh1} ->
%% Here we know that there are not enough bytes in EncDataRest to use. Must wait.
inet:setopts(Socket, [{active, once}]),
{next_state, StateName, State0#state{encoded_data_buffer = EncDataRest,
decoded_data_buffer = DecBytes,
undecoded_packet_length = RemainingSshPacketLen,
ssh_params = Ssh1}};
{bad_mac, Ssh1} ->
DisconnectMsg =
#ssh_msg_disconnect{code = ?SSH_DISCONNECT_PROTOCOL_ERROR,
description = "Bad mac"},
disconnect(DisconnectMsg, StateName, State0#state{ssh_params=Ssh1});
{error, {exceeds_max_size,PacketLen}} ->
DisconnectMsg =
#ssh_msg_disconnect{code = ?SSH_DISCONNECT_PROTOCOL_ERROR,
description = "Bad packet length "
++ integer_to_list(PacketLen)},
disconnect(DisconnectMsg, StateName, State0)
catch
_C:_E ->
DisconnectMsg =
#ssh_msg_disconnect{code = ?SSH_DISCONNECT_PROTOCOL_ERROR,
description = "Bad packet"},
disconnect(DisconnectMsg, StateName, State0)
end;
handle_event(internal, prepare_next_packet, StateName, State) ->
Enough = erlang:max(8, State#state.ssh_params#ssh.decrypt_block_size),
case size(State#state.encoded_data_buffer) of
Sz when Sz >= Enough ->
self() ! {State#state.transport_protocol, State#state.socket, <<>>};
_ ->
inet:setopts(State#state.socket, [{active, once}])
end,
{next_state, StateName, State};
handle_event(info, {CloseTag,Socket}, StateName,
State=#state{socket = Socket,
transport_close_tag = CloseTag}) ->
DisconnectMsg =
#ssh_msg_disconnect{code = ?SSH_DISCONNECT_BY_APPLICATION,
description = "Connection closed"},
disconnect(DisconnectMsg, StateName, State);
handle_event(info, {timeout, {_, From} = Request}, StateName,
#state{connection_state = #connection{requests = Requests} = Connection} = State) ->
case lists:member(Request, Requests) of
true ->
{next_state, StateName,
State#state{connection_state =
Connection#connection{requests =
lists:delete(Request, Requests)}},
[{reply,From,{error,timeout}}]};
false ->
{next_state, StateName, State}
end;
%%% Handle that ssh channels user process goes down
handle_event(info, {'DOWN', _Ref, process, ChannelPid, _Reason}, StateName, State0) ->
{{replies, Replies}, State1} = handle_channel_down(ChannelPid, State0),
{Repls, State} = send_replies(Replies, State1),
{next_state, StateName, State, 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, _ , _}, StateName,
#state{connection_state = #connection{channel_cache=Cache}} = State) ->
{next_state, StateName, check_cache(State, Cache)};
handle_event(info, UnexpectedMessage, StateName,
State = #state{opts = Opts,
ssh_params = SshParams}) ->
case unexpected_fun(UnexpectedMessage, Opts, SshParams) 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,
SshParams#ssh.role, SshParams#ssh.peer,
proplists:get_value(address, SshParams#ssh.opts)])),
error_logger:info_report(Msg),
{next_state, StateName, State};
skip ->
{next_state, StateName, State};
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,
SshParams#ssh.role,
element(2,SshParams#ssh.peer),
proplists:get_value(address, SshParams#ssh.opts)]
)),
error_logger:error_report(Msg),
{next_state, StateName, State}
end;
handle_event(internal, {disconnect,Msg,_Reason}, StateName, State) ->
disconnect(Msg, StateName, State);
handle_event(Type, Ev, StateName, State) ->
case catch atom_to_list(element(1,Ev)) of
"ssh_msg_" ++_ when Type==internal ->
Msg = #ssh_msg_disconnect{code = ?SSH_DISCONNECT_PROTOCOL_ERROR,
description = "Message in wrong state"},
disconnect(Msg, StateName, State);
_ ->
Msg = #ssh_msg_disconnect{code = ?SSH_DISCONNECT_PROTOCOL_ERROR,
description = "Internal error"},
disconnect(Msg, StateName, State)
end.
%%--------------------------------------------------------------------
%% . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
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),
timer:sleep(400), %% FIXME!!! gen_tcp:shutdown instead
finalize_termination(StateName, State);
%% terminate({shutdown,Msg}, StateName, State0) when is_record(Msg,ssh_msg_disconnect)->
%% State = send_msg(Msg, State0),
%% timer:sleep(400), %% FIXME!!! gen_tcp:shutdown instead
%% 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, State]) ->
[{data, [{"State", State}]}];
format_status(terminate, [_, _StateName, State]) ->
SshParams0 = (State#state.ssh_params),
SshParams = SshParams0#ssh{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 = "***"},
[{data, [{"State", State#state{decoded_data_buffer = "***",
encoded_data_buffer = "***",
key_exchange_init_msg = "***",
opts = "***",
recbuf = "***",
ssh_params = SshParams
}}]}].
%%--------------------------------------------------------------------
%% . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
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]),
{_, Callback, _} = proplists:get_value(transport, Options, {tcp, gen_tcp, tcp_closed}),
socket_control(Socket, Pid, Callback),
Pid.
init_role(client, #state{opts = Opts} = State0) ->
Pid = proplists:get_value(user_pid, Opts),
TimerRef = get_idle_time(Opts),
timer:apply_after(?REKEY_TIMOUT, gen_statem, cast, [self(), renegotiate]),
timer:apply_after(?REKEY_DATA_TIMOUT, gen_statem, cast,
[self(), data_size]),
State0#state{starter = Pid,
idle_timer_ref = TimerRef};
init_role(server, #state{opts = Opts, connection_state = Connection} = State) ->
Sups = proplists:get_value(supervisors, Opts),
Pid = proplists:get_value(user_pid, 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]}),
State#state{starter = Pid,
connection_state = Connection#connection{
cli_spec = CliSpec,
exec = Exec,
system_supervisor = SystemSup,
sub_system_supervisor = SubSystemSup,
connection_supervisor = ConnectionSup
}}.
%% init_ssh_record(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),
%% random_length_padding = proplists:get_value(max_random_length_padding,
%% Options,
%% (#ssh{})#ssh.random_length_padding)
%% };
%% init_ssh_record(server = Role, Vsn, Version, Options, Socket) ->
%% AuthMethods = proplists:get_value(auth_methods, Options,
%% ?SUPPORTED_AUTH_METHODS),
%% AuthMethodsAsList = string:tokens(AuthMethods, ","),
%% {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,
%% userauth_methods = AuthMethodsAsList,
%% kb_tries_left = 3,
%% peer = {undefined, PeerAddr},
%% available_host_keys = supported_host_keys(Role, KeyCb, Options),
%% random_length_padding = proplists:get_value(max_random_length_padding,
%% Options,
%% (#ssh{})#ssh.random_length_padding)
%% }.
init_ssh_record(Role, Vsn, Version, Options, Socket) ->
{ok, PeerAddr} = inet:peername(Socket),
KeyCb = proplists:get_value(key_cb, Options, ssh_file),
AuthMethods = proplists:get_value(auth_methods, Options, ?SUPPORTED_AUTH_METHODS),
S0 = #ssh{role = Role,
key_cb = KeyCb,
opts = Options,
userauth_supported_methods = AuthMethods,
available_host_keys = supported_host_keys(Role, KeyCb, Options),
random_length_padding = proplists:get_value(max_random_length_padding,
Options,
(#ssh{})#ssh.random_length_padding)
},
case Role of
client ->
PeerName = proplists:get_value(host, Options),
S0#ssh{c_vsn = Vsn,
c_version = Version,
io_cb = case proplists:get_value(user_interaction, Options, true) of
true -> ssh_io;
false -> ssh_no_io
end,
userauth_quiet_mode = proplists:get_value(quiet_mode, Options, false),
peer = {PeerName, PeerAddr}
};
server ->
S0#ssh{s_vsn = Vsn,
s_version = Version,
io_cb = proplists:get_value(io_cb, Options, ssh_io),
userauth_methods = string:tokens(AuthMethods, ","),
kb_tries_left = 3,
peer = {undefined, PeerAddr}
}
end.
%%--------------------------------------------------------------------
%% Stopping
finalize_termination(_StateName, #state{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.
%% StateName to Role
role({_,Role}) -> Role;
role({_,Role,_}) -> Role.
renegotiation({_,_,ReNeg}) -> ReNeg == renegotiation;
renegotiation(_) -> false.
get_idle_time(SshOptions) ->
case proplists:get_value(idle_time, SshOptions) of
infinity ->
infinity;
_IdleTime -> %% We dont want to set the timeout on first connect
undefined
end.
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=#state{ssh_params=Ssh0}) when is_tuple(Msg) ->
{Bytes, Ssh} = ssh_transport:ssh_packet(Msg, Ssh0),
send_bytes(Bytes, State),
State#state{ssh_params=Ssh}.
send_bytes(Bytes, #state{socket = Socket, transport_cb = Transport}) ->
Transport:send(Socket, Bytes).
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, State0 =
#state{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} ->
case StateName of
{connected,_} ->
{Repls, State} = send_replies(Replies,
State0#state{connection_state=Connection}),
{next_state, StateName, State, Repls};
_ ->
{ConnReplies, Replies} =
lists:splitwith(fun not_connected_filter/1, Replies),
{Repls, State} = send_replies(Replies,
State0#state{event_queue = Qev0 ++ ConnReplies}),
{next_state, StateName, State, Repls}
end;
{noreply, Connection} ->
{next_state, StateName, State0#state{connection_state = Connection}};
{disconnect, Reason0, {{replies, Replies}, Connection}} ->
{Repls,State} = send_replies(Replies, State0#state{connection_state = Connection}),
case {Reason0,Role} of
{{_, Reason}, client} when ((StateName =/= {connected,client}) and (not Renegotiation)) ->
User ! {self(), not_connected, Reason};
_ ->
ok
end,
{stop, {shutdown,normal}, Repls, State#state{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,State} = send_replies(Replies, State0#state{connection_state = Connection}),
{stop, {shutdown,Error}, Repls, State#state{connection_state = Connection}}
end.
set_prefix_if_trouble(Msg = <<?BYTE(Op),_/binary>>, #state{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_prefix_if_trouble(Msg, _) ->
Msg.
kex(#ssh{algorithms=#alg{kex=Kex}}) -> Kex;
kex(_) -> undefined.
%%%----------------------------------------------------------------
handle_request(ChannelPid, ChannelId, Type, Data, WantReply, From,
#state{connection_state =
#connection{channel_cache = Cache}} = State0) ->
case ssh_channel:cache_lookup(Cache, ChannelId) of
#channel{remote_id = Id} = Channel ->
update_sys(Cache, Channel, Type, ChannelPid),
Msg = ssh_connection:channel_request_msg(Id, Type,
WantReply, Data),
send_msg(Msg, add_request(WantReply, ChannelId, From, State0));
undefined ->
State0
end.
handle_request(ChannelId, Type, Data, WantReply, From,
#state{connection_state =
#connection{channel_cache = Cache}} = State0) ->
case ssh_channel:cache_lookup(Cache, ChannelId) of
#channel{remote_id = Id} ->
Msg = ssh_connection:channel_request_msg(Id, Type,
WantReply, Data),
send_msg(Msg, add_request(WantReply, ChannelId, From, State0));
undefined ->
State0
end.
%%%----------------------------------------------------------------
handle_global_request({global_request, ChannelPid,
"tcpip-forward" = Type, WantReply,
<<?UINT32(IPLen),
IP:IPLen/binary, ?UINT32(Port)>> = Data},
#state{connection_state =
#connection{channel_cache = Cache}
= Connection0} = State) ->
ssh_channel:cache_update(Cache, #channel{user = ChannelPid,
type = "forwarded-tcpip",
sys = none}),
Connection = ssh_connection:bind(IP, Port, ChannelPid, Connection0),
Msg = ssh_connection:global_request_msg(Type, WantReply, Data),
send_msg(Msg, State#state{connection_state = Connection});
handle_global_request({global_request, _Pid, "cancel-tcpip-forward" = Type,
WantReply, <<?UINT32(IPLen),
IP:IPLen/binary, ?UINT32(Port)>> = Data},
#state{connection_state = Connection0} = State) ->
Connection = ssh_connection:unbind(IP, Port, Connection0),
Msg = ssh_connection:global_request_msg(Type, WantReply, Data),
send_msg(Msg, State#state{connection_state = Connection});
handle_global_request({global_request, _, "cancel-tcpip-forward" = Type,
WantReply, Data}, State) ->
Msg = ssh_connection:global_request_msg(Type, WantReply, Data),
send_msg(Msg, State).
%%%----------------------------------------------------------------
handle_idle_timeout(#state{opts = Opts}) ->
case proplists:get_value(idle_time, Opts, infinity) of
infinity ->
ok;
IdleTime ->
erlang:send_after(IdleTime, self(), {check_cache, [], []})
end.
handle_channel_down(ChannelPid, #state{connection_state =
#connection{channel_cache = Cache}} =
State) ->
ssh_channel:cache_foldl(
fun(Channel, Acc) when Channel#channel.user == ChannelPid ->
ssh_channel:cache_delete(Cache,
Channel#channel.local_id),
Acc;
(_,Acc) ->
Acc
end, [], Cache),
{{replies, []}, check_cache(State, Cache)}.
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, #state{connection_state =
#connection{requests = Requests0} =
Connection} = State) ->
Requests = [{ChannelId, From} | Requests0],
State#state{connection_state = Connection#connection{requests = Requests}}.
new_channel_id(#state{connection_state = #connection{channel_id_seed = Id} =
Connection}
= State) ->
{Id, State#state{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#state.opts),
timer:sleep(400),
{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}.
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, PeerAddr, {error, Reason}, Opts) ->
case proplists:get_value(failfun, Opts) of
undefined ->
ok;
Fun ->
do_retry_fun(Fun, User, PeerAddr, Reason)
end;
retry_fun(User, PeerAddr, Reason, Opts) ->
case proplists:get_value(infofun, Opts) of
undefined ->
ok;
Fun ->
do_retry_fun(Fun, User, PeerAddr, Reason)
end.
do_retry_fun(Fun, User, PeerAddr, Reason) ->
case erlang:fun_info(Fun, arity) of
{arity, 2} -> %% Backwards compatible
catch Fun(User, Reason);
{arity, 3} ->
catch Fun(User, PeerAddr, Reason)
end.
ssh_info([], _State, Acc) ->
Acc;
ssh_info([client_version | Rest], #state{ssh_params = #ssh{c_vsn = IntVsn,
c_version = StringVsn}} = State, Acc) ->
ssh_info(Rest, State, [{client_version, {IntVsn, StringVsn}} | Acc]);
ssh_info([server_version | Rest], #state{ssh_params =#ssh{s_vsn = IntVsn,
s_version = StringVsn}} = State, Acc) ->
ssh_info(Rest, State, [{server_version, {IntVsn, StringVsn}} | Acc]);
ssh_info([peer | Rest], #state{ssh_params = #ssh{peer = Peer}} = State, Acc) ->
ssh_info(Rest, State, [{peer, Peer} | Acc]);
ssh_info([sockname | Rest], #state{socket = Socket} = State, Acc) ->
{ok, SockName} = inet:sockname(Socket),
ssh_info(Rest, State, [{sockname, SockName}|Acc]);
ssh_info([user | Rest], #state{auth_user = User} = State, Acc) ->
ssh_info(Rest, State, [{user, User}|Acc]);
ssh_info([ _ | Rest], State, Acc) ->
ssh_info(Rest, State, Acc).
ssh_channel_info([], _, Acc) ->
Acc;
ssh_channel_info([recv_window | Rest], #channel{recv_window_size = WinSize,
recv_packet_size = Packsize
} = Channel, Acc) ->
ssh_channel_info(Rest, Channel, [{recv_window, {{win_size, WinSize},
{packet_size, Packsize}}} | Acc]);
ssh_channel_info([send_window | Rest], #channel{send_window_size = WinSize,
send_packet_size = Packsize
} = Channel, Acc) ->
ssh_channel_info(Rest, Channel, [{send_window, {{win_size, WinSize},
{packet_size, Packsize}}} | Acc]);
ssh_channel_info([ _ | Rest], Channel, Acc) ->
ssh_channel_info(Rest, Channel, Acc).
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}) ->
{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}, Opts) ->
disconnect_fun(Msg, Opts);
disconnect_fun(_, undefined) ->
ok;
disconnect_fun(Reason, Opts) ->
case proplists:get_value(disconnectfun, Opts) of
undefined ->
ok;
Fun ->
catch Fun(Reason)
end.
unexpected_fun(UnexpectedMessage, Opts, #ssh{peer={_,Peer}}) ->
case proplists:get_value(unexpectedfun, Opts) of
undefined ->
report;
Fun ->
catch Fun(UnexpectedMessage, Peer)
end.
check_cache(#state{opts = Opts} = State, Cache) ->
%% Check the number of entries in Cache
case proplists:get_value(size, ets:info(Cache)) of
0 ->
case proplists:get_value(idle_time, Opts, infinity) of
infinity ->
State;
Time ->
handle_idle_timer(Time, State)
end;
_ ->
State
end.
handle_idle_timer(Time, #state{idle_timer_ref = undefined} = State) ->
TimerRef = erlang:send_after(Time, self(), {'EXIT', [], "Timeout"}),
State#state{idle_timer_ref=TimerRef};
handle_idle_timer(_, State) ->
State.
remove_timer_ref(State) ->
case State#state.idle_timer_ref of
infinity -> %% If the timer is not activated
State;
undefined -> %% If we already has cancelled the timer
State;
TimerRef -> %% Timer is active
erlang:cancel_timer(TimerRef),
State#state{idle_timer_ref = undefined}
end.
socket_control(Socket, Pid, Transport) ->
case Transport: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.
start_timeout(_,_, infinity) ->
ok;
start_timeout(Channel, From, Time) ->
erlang:send_after(Time, self(), {timeout, {Channel, From}}).
getopt(Opt, Socket) ->
case inet:getopts(Socket, [Opt]) of
{ok, [{Opt, Value}]} ->
{ok, Value};
Other ->
{error, {unexpected_getopts_return, Other}}
end.