%
%% %CopyrightBegin%
%%
%% Copyright Ericsson AB 2004-2018. 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%
%%

%%

-module(ssh).

-include("ssh.hrl").
-include("ssh_connect.hrl").
-include_lib("public_key/include/public_key.hrl").
-include_lib("kernel/include/file.hrl").
-include_lib("kernel/include/inet.hrl").

-export([start/0, start/1, stop/0,
	 connect/2, connect/3, connect/4,
	 close/1, connection_info/2,
	 channel_info/3,
	 daemon/1, daemon/2, daemon/3,
	 daemon_info/1,
	 default_algorithms/0,
         chk_algos_opts/1,
	 stop_listener/1, stop_listener/2,  stop_listener/3,
	 stop_daemon/1, stop_daemon/2, stop_daemon/3,
	 shell/1, shell/2, shell/3
	]).

%%% "Deprecated" types export:
-export_type([ssh_daemon_ref/0, ssh_connection_ref/0, ssh_channel_id/0]).
-opaque ssh_daemon_ref()     :: daemon_ref().
-opaque ssh_connection_ref() :: connection_ref().
-opaque ssh_channel_id()     :: channel_id().


%%% Type exports
-export_type([daemon_ref/0,
              connection_ref/0,
	      channel_id/0,
              client_options/0, client_option/0,
              daemon_options/0, daemon_option/0,
              common_options/0,
              role/0,
              subsystem_spec/0,
              algs_list/0,
              double_algs/1,
              modify_algs_list/0,
              alg_entry/0,
              kex_alg/0,
              pubkey_alg/0,
              cipher_alg/0,
              mac_alg/0,
              compression_alg/0,
              ip_port/0
	     ]).


-opaque daemon_ref()         :: pid() .
-opaque channel_id()     :: non_neg_integer().
-type connection_ref()       :: pid().  % should be -opaque, but that gives problems

%%--------------------------------------------------------------------
%% Description: Starts the ssh application. Default type
%% is temporary. see application(3)
%%--------------------------------------------------------------------
-spec start() -> ok | {error, term()}.

start() ->
    start(temporary).

-spec start(Type) -> ok | {error, term()} when
      Type :: permanent | transient | temporary .

start(Type) ->
    case application:ensure_all_started(ssh, Type) of
        {ok, _} ->
            ok;
        Other ->
            Other
    end.

%%--------------------------------------------------------------------
%% Description: Stops the ssh application.
%%--------------------------------------------------------------------
-spec stop() -> ok | {error, term()}.

stop() ->
    application:stop(ssh).

%%--------------------------------------------------------------------
%% Description: Starts an ssh connection.
%%--------------------------------------------------------------------
-spec connect(OpenTcpSocket, Options) -> {ok,connection_ref()} | {error,term()} when
      OpenTcpSocket :: open_socket(),
      Options :: client_options().

connect(OpenTcpSocket, Options) when is_port(OpenTcpSocket),
                                     is_list(Options) ->
    connect(OpenTcpSocket, Options, infinity).


-spec connect(open_socket(), client_options(), timeout()) ->
                     {ok,connection_ref()} | {error,term()}
           ; (host(), inet:port_number(), client_options()) ->
                     {ok,connection_ref()} | {error,term()}.

connect(Socket, UserOptions, NegotiationTimeout) when is_port(Socket),
                                                      is_list(UserOptions) ->
    case ssh_options:handle_options(client, UserOptions) of
	{error, Error} ->
	    {error, Error};
	Options ->
            case valid_socket_to_use(Socket, ?GET_OPT(transport,Options)) of
		ok ->
		    {ok, {Host,_Port}} = inet:peername(Socket),
		    Opts = ?PUT_INTERNAL_OPT([{user_pid,self()}, {host,Host}], Options),
		    ssh_connection_handler:start_connection(client, Socket, Opts, NegotiationTimeout);
		{error,SockError} ->
		    {error,SockError}
	    end
    end;

connect(Host, Port, Options) when is_integer(Port),
                                  Port>0,
                                  is_list(Options) ->
    connect(Host, Port, Options, infinity).


-spec connect(Host, Port, Options, NegotiationTimeout) -> {ok,connection_ref()} | {error,term()} when
      Host :: host(),
      Port :: inet:port_number(),
      Options :: client_options(),
      NegotiationTimeout :: timeout().

connect(Host0, Port, UserOptions, Timeout) when is_integer(Port),
                                               Port>0,
                                               is_list(UserOptions) ->
    case ssh_options:handle_options(client, UserOptions) of
	{error, _Reason} = Error ->
	    Error;
        Options ->
	    {_, Transport, _} = TransportOpts = ?GET_OPT(transport, Options),
	    ConnectionTimeout = ?GET_OPT(connect_timeout, Options),
            SocketOpts = [{active,false} | ?GET_OPT(socket_options,Options)],
            Host = mangle_connect_address(Host0, SocketOpts),
	    try Transport:connect(Host, Port, SocketOpts, ConnectionTimeout) of
		{ok, Socket} ->
		    Opts = ?PUT_INTERNAL_OPT([{user_pid,self()}, {host,Host}], Options),
		    ssh_connection_handler:start_connection(client, Socket, Opts, Timeout);
		{error, Reason} ->
		    {error, Reason}
	    catch
		exit:{function_clause, _F} ->
		    {error, {options, {transport, TransportOpts}}};
		exit:badarg ->
		    {error, {options, {socket_options, SocketOpts}}}
	    end
    end.

%%--------------------------------------------------------------------
-spec close(ConnectionRef) -> ok | {error,term()} when
      ConnectionRef :: connection_ref() .
%%
%% Description: Closes an ssh connection.
%%--------------------------------------------------------------------
close(ConnectionRef) ->
    ssh_connection_handler:stop(ConnectionRef).

%%--------------------------------------------------------------------
%% Description: Retrieves information about a connection.
%%--------------------------------------------------------------------
-spec connection_info(ConnectionRef, Keys) ->  ConnectionInfo when
      ConnectionRef :: connection_ref(),
      Keys :: [client_version | server_version | user | peer | sockname],
      ConnectionInfo :: [{client_version, Version}
                         | {server_version, Version}
                         | {user,string()}
                         | {peer, {inet:hostname(), ip_port()}}
                         | {sockname, ip_port()}
                        ],
      Version :: {ProtocolVersion, VersionString::string()},
      ProtocolVersion :: {Major::pos_integer(), Minor::non_neg_integer()} .

connection_info(Connection, Options) ->
    ssh_connection_handler:connection_info(Connection, Options).

%%--------------------------------------------------------------------
-spec channel_info(connection_ref(), channel_id(), [atom()]) -> proplists:proplist().
%%
%% Description: Retrieves information about a connection.
%%--------------------------------------------------------------------
channel_info(ConnectionRef, ChannelId, Options) ->
    ssh_connection_handler:channel_info(ConnectionRef, ChannelId, Options).

%%--------------------------------------------------------------------
%% Description: Starts a server listening for SSH connections
%% on the given port.
%%--------------------------------------------------------------------
-spec daemon(inet:port_number()) ->  {ok,daemon_ref()} | {error,term()}.

daemon(Port) ->
    daemon(Port, []).


-spec daemon(inet:port_number()|open_socket(), daemon_options()) -> {ok,daemon_ref()} | {error,term()}.

daemon(Socket, UserOptions) when is_port(Socket) ->
    try
        #{} = Options = ssh_options:handle_options(server, UserOptions),

        case valid_socket_to_use(Socket, ?GET_OPT(transport,Options)) of
            ok ->
                {ok, {IP,Port}} = inet:sockname(Socket),
                finalize_start(IP, Port, ?GET_OPT(profile, Options),
                               ?PUT_INTERNAL_OPT({connected_socket, Socket}, Options),
                               fun(Opts, DefaultResult) ->
                                       try ssh_acceptor:handle_established_connection(
                                             IP, Port, Opts, Socket)
                                       of
                                           {error,Error} ->
                                               {error,Error};
                                           _ ->
                                               DefaultResult
                                       catch
                                           C:R ->
                                               {error,{could_not_start_connection,{C,R}}}
                                       end
                               end);
            {error,SockError} ->
                {error,SockError}
            end
    catch
        throw:bad_fd ->
            {error,bad_fd};
        throw:bad_socket ->
            {error,bad_socket};
        error:{badmatch,{error,Error}} ->
            {error,Error};
        error:Error ->
            {error,Error};
        _C:_E ->
            {error,{cannot_start_daemon,_C,_E}}
    end;

daemon(Port, UserOptions) when 0 =< Port, Port =< 65535 ->
    daemon(any, Port, UserOptions).


-spec daemon(any | inet:ip_address(), inet:port_number(), daemon_options()) -> {ok,daemon_ref()} | {error,term()}
           ;(socket, open_socket(), daemon_options()) -> {ok,daemon_ref()} | {error,term()}
            .

daemon(Host0, Port0, UserOptions0) when 0 =< Port0, Port0 =< 65535,
                                        Host0 == any ; Host0 == loopback ; is_tuple(Host0) ->
    try
        {Host1, UserOptions} = handle_daemon_args(Host0, UserOptions0),
        #{} = Options0 = ssh_options:handle_options(server, UserOptions),
        {open_listen_socket(Host1, Port0, Options0), Options0}
    of
        {{{Host,Port}, ListenSocket}, Options1} ->
            try
                %% Now Host,Port is what to use for the supervisor to register its name,
                %% and ListenSocket is for listening on connections. But it is still owned
                %% by self()...
                finalize_start(Host, Port, ?GET_OPT(profile, Options1),
                               ?PUT_INTERNAL_OPT({lsocket,{ListenSocket,self()}}, Options1),
                               fun(Opts, Result) ->
                                       {_, Callback, _} = ?GET_OPT(transport, Opts),
                                       receive
                                           {request_control, ListenSocket, ReqPid} ->
                                               ok = Callback:controlling_process(ListenSocket, ReqPid),
                                               ReqPid ! {its_yours,ListenSocket},
                                               Result
                                       end
                               end)
            of
                {error,Err} ->
                    close_listen_socket(ListenSocket, Options1),
                    {error,Err};
                OK ->
                    OK
            catch
                error:Error ->
                    close_listen_socket(ListenSocket, Options1),
                    error(Error);
                exit:Exit ->
                    close_listen_socket(ListenSocket, Options1),
                    exit(Exit)
            end
    catch
        throw:bad_fd ->
            {error,bad_fd};
        throw:bad_socket ->
            {error,bad_socket};
        error:{badmatch,{error,Error}} ->
            {error,Error};
        error:Error ->
            {error,Error};
        _C:_E ->
            {error,{cannot_start_daemon,_C,_E}}
    end;

daemon(_, _, _) ->
    {error, badarg}.

%%--------------------------------------------------------------------
-spec daemon_info(Daemon) -> {ok, DaemonInfo} | {error,term()} when
      Daemon :: daemon_ref(),
      DaemonInfo :: [  {ip, inet:ip_address()}
                       | {port, inet:port_number()}
                       | {profile, term()}
                    ].

daemon_info(Pid) ->
    case catch ssh_system_sup:acceptor_supervisor(Pid) of
	AsupPid when is_pid(AsupPid) ->
	    [{IP,Port,Profile}] =
		[{IP,Prt,Prf} 
                 || {{ssh_acceptor_sup,Hst,Prt,Prf},_Pid,worker,[ssh_acceptor]} 
                        <- supervisor:which_children(AsupPid),
                    IP <- [case inet:parse_strict_address(Hst) of
                               {ok,IP} -> IP;
                               _ -> Hst
                           end]
                ],
	    {ok, [{port,Port},
                  {ip,IP},
                  {profile,Profile}
                 ]};
	_ ->
	    {error,bad_daemon_ref}
    end.

%%--------------------------------------------------------------------
%% Description: Stops the listener, but leaves
%% existing connections started by the listener up and running.
%%--------------------------------------------------------------------
-spec stop_listener(daemon_ref()) -> ok.

stop_listener(SysSup) ->
    ssh_system_sup:stop_listener(SysSup).


-spec stop_listener(inet:ip_address(), inet:port_number()) -> ok.

stop_listener(Address, Port) ->
    stop_listener(Address, Port, ?DEFAULT_PROFILE).


-spec stop_listener(any|inet:ip_address(), inet:port_number(), term()) -> ok.

stop_listener(any, Port, Profile) ->
    map_ip(fun(IP) ->
                   ssh_system_sup:stop_listener(IP, Port, Profile) 
           end, [{0,0,0,0},{0,0,0,0,0,0,0,0}]);
stop_listener(Address, Port, Profile) ->
    map_ip(fun(IP) ->
                   ssh_system_sup:stop_listener(IP, Port, Profile) 
           end, {address,Address}).

%%--------------------------------------------------------------------
%% Description: Stops the listener and all connections started by
%% the listener.
%%--------------------------------------------------------------------
-spec stop_daemon(DaemonRef::daemon_ref()) -> ok.

stop_daemon(SysSup) ->
    ssh_system_sup:stop_system(SysSup).


-spec stop_daemon(inet:ip_address(), inet:port_number()) -> ok.

stop_daemon(Address, Port) ->
    stop_daemon(Address, Port, ?DEFAULT_PROFILE).


-spec stop_daemon(any|inet:ip_address(), inet:port_number(), atom()) -> ok.

stop_daemon(any, Port, Profile) ->
    map_ip(fun(IP) ->
                   ssh_system_sup:stop_system(IP, Port, Profile) 
           end, [{0,0,0,0},{0,0,0,0,0,0,0,0}]);
stop_daemon(Address, Port, Profile) ->
    map_ip(fun(IP) ->
                   ssh_system_sup:stop_system(IP, Port, Profile) 
           end, {address,Address}).

%%--------------------------------------------------------------------
%% Description: Starts an interactive shell to an SSH server on the
%% given <Host>. The function waits for user input,
%% and will not return until the remote shell is ended.(e.g. on
%% exit from the shell)
%%--------------------------------------------------------------------
-spec shell(open_socket() | host()) ->  _.

shell(Socket) when is_port(Socket) ->
    shell(Socket, []);
shell(Host) ->
    shell(Host, ?SSH_DEFAULT_PORT, []).


-spec shell(open_socket() | host(), client_options()) ->  _.

shell(Socket, Options) when is_port(Socket) ->
    start_shell( connect(Socket, Options) );
shell(Host, Options) ->
    shell(Host, ?SSH_DEFAULT_PORT, Options).


-spec shell(Host, Port, Options) -> _ when
      Host :: host(),
      Port :: inet:port_number(),
      Options :: client_options() .

shell(Host, Port, Options) ->
    start_shell( connect(Host, Port, Options) ).



start_shell({ok, ConnectionRef}) ->
    case ssh_connection:session_channel(ConnectionRef, infinity) of
	{ok,ChannelId}  ->
	    success = ssh_connection:ptty_alloc(ConnectionRef, ChannelId, []),
	    Args = [{channel_cb, ssh_shell},
		    {init_args,[ConnectionRef, ChannelId]},
		    {cm, ConnectionRef}, {channel_id, ChannelId}],
	    {ok, State} = ssh_client_channel:init([Args]),
            try
                ssh_client_channel:enter_loop(State)
            catch
                exit:normal ->
                    ok
            end;
	Error ->
	    Error
    end;

start_shell(Error) ->
    Error.

%%--------------------------------------------------------------------
-spec default_algorithms() -> algs_list() .
%%--------------------------------------------------------------------
default_algorithms() ->
    ssh_transport:default_algorithms().

%%--------------------------------------------------------------------
-spec chk_algos_opts(client_options()|daemon_options()) -> internal_options() | {error,term()}.
%%--------------------------------------------------------------------
chk_algos_opts(Opts) ->
    case lists:foldl(
           fun({preferred_algorithms,_}, Acc) -> Acc;
              ({modify_algorithms,_}, Acc) -> Acc;
              (KV, Acc) -> [KV|Acc]
           end, [], Opts)
    of
        [] ->
            case ssh_options:handle_options(client, Opts) of
                M when is_map(M) ->
                    maps:get(preferred_algorithms, M);
                Others ->
                    Others
            end;
        OtherOps ->
            {error, {non_algo_opts_found,OtherOps}}
    end.

%%--------------------------------------------------------------------
%%% Internal functions
%%--------------------------------------------------------------------
%% The handle_daemon_args/2 function basically only sets the ip-option in Opts
%% so that it is correctly set when opening the listening socket.

handle_daemon_args(any, Opts) ->
    case proplists:get_value(ip, Opts) of
        undefined -> {any, Opts};
        IP -> {IP, Opts}
    end;

handle_daemon_args(IPaddr, Opts) when is_tuple(IPaddr) ; IPaddr == loopback ->
    case proplists:get_value(ip, Opts) of
        undefined -> {IPaddr, [{ip,IPaddr}|Opts]};
        IPaddr -> {IPaddr, Opts};
        IP -> {IPaddr, [{ip,IPaddr}|Opts--[{ip,IP}]]} %% Backward compatibility
    end.

%%%----------------------------------------------------------------
valid_socket_to_use(Socket, {tcp,_,_}) ->
    %% Is this tcp-socket a valid socket?
    try {is_tcp_socket(Socket),
         {ok,[{active,false}]} == inet:getopts(Socket, [active])
        }
    of
        {true,  true} -> ok;
        {true, false} -> {error, not_passive_mode};
        _ ->             {error, not_tcp_socket}
    catch
        _:_ ->           {error, bad_socket}
    end;

valid_socket_to_use(_, {L4,_,_}) ->
    {error, {unsupported,L4}}.


is_tcp_socket(Socket) ->
    case inet:getopts(Socket, [delay_send]) of
        {ok,[_]} -> true;
        _ -> false
    end.

%%%----------------------------------------------------------------
open_listen_socket(_Host0, Port0, Options0) ->
    {ok,LSock} =
        case ?GET_SOCKET_OPT(fd, Options0) of
            undefined ->
                ssh_acceptor:listen(Port0, Options0);
            Fd when is_integer(Fd) ->
                %% Do gen_tcp:listen with the option {fd,Fd}:
                ssh_acceptor:listen(0, Options0)
        end,
    {ok,{LHost,LPort}} = inet:sockname(LSock),
    {{LHost,LPort}, LSock}.

%%%----------------------------------------------------------------
close_listen_socket(ListenSocket, Options) ->
    try
        {_, Callback, _} = ?GET_OPT(transport, Options),
        Callback:close(ListenSocket)
    catch
        _C:_E -> ok
    end.

%%%----------------------------------------------------------------
finalize_start(Host, Port, Profile, Options0, F) ->
    try
        %% throws error:Error if no usable hostkey is found
        ssh_connection_handler:available_hkey_algorithms(server, Options0),

        sshd_sup:start_child(Host, Port, Profile, Options0)
    of
        {error, {already_started, _}} ->
            {error, eaddrinuse};
        {error, Error} ->
            {error, Error};
        Result = {ok,_} ->
            F(Options0, Result)
    catch
        error:{shutdown,Err} ->
            {error,Err};
        exit:{noproc, _} ->
            {error, ssh_not_started}
    end.

%%%----------------------------------------------------------------
map_ip(Fun, {address,IP}) when is_tuple(IP) ->
    Fun(IP);
map_ip(Fun, {address,Address}) ->
    IPs = try {ok,#hostent{h_addr_list=IP0s}} = inet:gethostbyname(Address),
               IP0s
          catch
              _:_ -> []
          end,
    map_ip(Fun, IPs);
map_ip(Fun, IPs) ->
    lists:map(Fun, IPs).

%%%----------------------------------------------------------------
mangle_connect_address(A, SockOpts) ->
    mangle_connect_address1(A, proplists:get_value(inet6,SockOpts,false)).

loopback(true) -> {0,0,0,0,0,0,0,1};
loopback(false) ->      {127,0,0,1}.

mangle_connect_address1( loopback,     V6flg) -> loopback(V6flg);
mangle_connect_address1(      any,     V6flg) -> loopback(V6flg);
mangle_connect_address1({0,0,0,0},         _) -> loopback(false);
mangle_connect_address1({0,0,0,0,0,0,0,0}, _) -> loopback(true);
mangle_connect_address1(       IP,     _) when is_tuple(IP) -> IP;
mangle_connect_address1(A, _) ->
    case catch inet:parse_address(A) of
        {ok,         {0,0,0,0}} -> loopback(false);
        {ok, {0,0,0,0,0,0,0,0}} -> loopback(true);
        _ -> A
    end.
