%%
%% %CopyrightBegin%
%%
%% Copyright Ericsson AB 2004-2022. 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(http_transport).

% Internal application API
-export([
	 start/1, 
	 connect/3, connect/4, 
	 listen/4, listen/5,
	 accept/2, accept/3, 
	 close/2,
         close_tag/1,
	 send/3, 
	 controlling_process/3, 
	 setopts/3, getopts/2, getopts/3, 
	 getstat/2, 
	 peername/2, sockname/2, 
	 resolve/0
	]).
-export([negotiate/3]).
-export([ipv4_name/1, ipv6_name/1]).

-include_lib("inets/src/inets_app/inets_internal.hrl").
-include("http_internal.hrl").


%%%=========================================================================
%%%  Internal application API
%%%=========================================================================

%%-------------------------------------------------------------------------
%% start(SocketType) -> ok | {error, Reason}
%%      SocketType = ip_comm | {ssl, _}  
%%                                   
%% Description: Makes sure ssl is started. 
%%-------------------------------------------------------------------------
start(ip_comm) ->
    ok;
start({ip_comm, _}) ->
    ok;
start({ssl, _}) ->
    do_start_ssl();
start({essl, _}) ->
    do_start_ssl().

do_start_ssl() ->
    try lists:foreach(fun(App) -> 
			      ok = application:ensure_started(App)
		      end,
		      [crypto, asn1, public_key, ssl])
    catch
	_:Reason ->
	    {error, Reason}
    end.
	 

%%-------------------------------------------------------------------------
%% connect(SocketType, Address, Options, Timeout) ->
%%                                            {ok, Socket} | {error, Reason}
%%      SocketType = ip_comm | {ssl, SslConfig}  
%%      Address = {Host, Port}
%%      Options = [option()]
%%      Socket = socket()
%%      option() = ipfamily() | {ip, ip_address()} | {port, integer()}
%%      ipfamily() = inet | inet6 
%%                                   
%% Description: Connects to the Host and Port specified in HTTPRequest.
%%-------------------------------------------------------------------------

connect(SocketType, Address, Opts) ->
    connect(SocketType, Address, Opts, infinity).
connect(ip_comm, {Host, Port}, Opts0, Timeout) ->
    Opts = [binary, {packet, 0}, {active, false}, {reuseaddr, true} | Opts0 ],
    try gen_tcp:connect(Host, Port, Opts, Timeout) of
	{ok, _} = OK ->
	    OK;
	{error, _} = ERROR ->
	    ERROR
    catch 
	exit:{badarg, _} ->
	    {error, {eoptions, Opts}};
	exit:badarg ->
	    {error, {eoptions, Opts}}
    end;

%% Wrapper for backaward compatibillity
connect({ssl, SslConfig}, Address, Opts, Timeout) ->
    connect({?HTTP_DEFAULT_SSL_KIND, SslConfig}, Address, Opts, Timeout);

connect({essl, SslConfig}, {Host, Port}, Opts0, Timeout) -> 
    Opts = [binary, {active, false}, {ssl_imp, new} | Opts0] ++ SslConfig,
    case (catch ssl:connect(Host, Port, Opts, Timeout)) of
	{'EXIT', Reason} ->
	    {error, {eoptions, Reason}};
	{ok, _} = OK ->
	    OK;
	{error, _} = ERROR ->
	    ERROR
    end.


%%-------------------------------------------------------------------------
%% listen(SocketType, Addr, Port, Fd) -> {ok, Socket} | {error, Reason}
%%      SocketType = ip_comm | {ssl, SSLConfig}  
%%      Port = integer() 
%%      Socket = socket()
%%      Fd = undefined | fd()
%%
%% Description: Sets up socket to listen on the port Port on the local
%% host using either gen_tcp or ssl. In the gen_tcp case the port
%% might already have been initiated by a wrapper-program and is
%% given as an Fd that can be retrieved by init:get_argument. The
%% reason for this to enable a HTTP-server not running as root to use
%% port 80.
%%-------------------------------------------------------------------------
listen(ip_comm, Addr, Port, Fd, IpFamily) ->
    listen_ip_comm(Addr, Port, [], Fd, IpFamily);

listen({ip_comm, SockOpts}, Addr, Port, Fd, IpFamily) ->
    listen_ip_comm(Addr, Port, SockOpts, Fd, IpFamily);

listen({essl, SSLConfig}, Addr, Port, Fd, IpFamily) ->
    listen_ssl(Addr, Port, Fd, SSLConfig, IpFamily, []).

listen(ip_comm, Addr, Port, IpFamily) ->
    listen_ip_comm(Addr, Port, [], undefined, IpFamily);

%% Wrapper for backaward compatibillity
listen({ssl, SSLConfig}, Addr, Port, IpFamily) ->
    listen({?HTTP_DEFAULT_SSL_KIND, SSLConfig}, Addr, Port, IpFamily);

listen({essl, SSLConfig}, Addr, Port, IpFamily) ->
    {SSLConfig2, ExtraOpts} = case proplists:get_value(log_alert, SSLConfig, undefined) of
		    undefined ->
			{SSLConfig, []};
		    LogAlert ->
			{proplists:delete(log_alert, SSLConfig), [{log_alert, LogAlert}]}
		end,
    listen_ssl(Addr, Port, undefined, SSLConfig2, IpFamily, ExtraOpts).

listen_ip_comm(Addr, Port, SockOpts, Fd, IpFamily) ->
    case (catch do_listen_ip_comm(Addr, Port, SockOpts, Fd, IpFamily)) of
	{'EXIT', Reason} ->
	    {error, {exit, Reason}};
	Else ->
	    Else
    end.

do_listen_ip_comm(Addr, Port, SockOpts, Fd, IpFamily) ->
    Backlog = proplists:get_value(backlog, SockOpts, 128),
    {NewPort, Opts} = get_socket_info(Addr, Port, Fd,
				      [{backlog, Backlog}, {reuseaddr, true} | SockOpts]),
    Opts2 = [IpFamily | Opts],
    gen_tcp:listen(NewPort, Opts2).

listen_ssl(Addr, Port, Fd, Opts0, IpFamily, ExtraOpts) ->
    Backlog = proplists:get_value(backlog, Opts0, 128),
    {NewPort, SockOpt} = get_socket_info(Addr, Port, Fd, 
					 [{backlog, Backlog}, {reuseaddr, true}]),
    Opts = SockOpt ++ Opts0,
    Opts2 = [IpFamily | Opts],
    ssl:listen(NewPort, Opts2 ++ ExtraOpts).

get_socket_info(Addr, Port, Fd, BaseOpts) ->
    %% The presence of a file descriptor takes precedence
    case Fd of
	undefined ->
	    {Port, sock_opts(Addr, BaseOpts)};
	Fd -> 
	    {0, sock_opts([{fd, Fd} | BaseOpts])}
    end.
	    
%%-------------------------------------------------------------------------
%% accept(SocketType, ListenSocket) -> {ok, Socket} | {error, Reason}
%% accept(SocketType, ListenSocket, Timeout) -> ok | {error, Reason}
%%   SocketType = ip_comm | {ssl, SSLConfig}  
%%   ListenSocket = socket()    
%%   Timeout = infinity | integer() >= 0
%%   Socket = socket()
%%                                   
%% Description: Accepts an incoming connection request on a listen socket,
%% using either gen_tcp or ssl.
%%-------------------------------------------------------------------------
-spec accept(SocketType, ListenSocket) -> {ok, Socket} | {error, Reason} when
      SocketType   :: ip_comm | {ssl, SSLConfig},
      SSLConfig    :: term(),
      ListenSocket :: gen_tcp:socket(),
      Socket       :: gen_tcp:socket(),
      Reason       :: term().
accept(SocketType, ListenSocket) ->
    accept(SocketType, ListenSocket, infinity).

-spec accept(SocketType, ListenSocket, Timeout) -> {ok, Socket} | {error, Reason} when
      SocketType   :: ip_comm | {ssl | essl, SSLConfig},
      SSLConfig    :: term(),
      Timeout      :: timeout(),
      ListenSocket :: gen_tcp:socket(),
      Socket       :: gen_tcp:socket(),
      Reason       :: term().
accept(ip_comm, ListenSocket, Timeout) ->
    gen_tcp:accept(ListenSocket, Timeout);
accept({ip_comm, _}, ListenSocket, Timeout) ->
    gen_tcp:accept(ListenSocket, Timeout);

%% Wrapper for backaward compatibillity
accept({ssl, SSLConfig}, ListenSocket, Timeout) ->
    accept({?HTTP_DEFAULT_SSL_KIND, SSLConfig}, ListenSocket, Timeout);

accept({essl, _SSLConfig}, ListenSocket, Timeout) ->
    ssl:transport_accept(ListenSocket, Timeout).


%%-------------------------------------------------------------------------
%% Description: Assigns a new controlling process to Socket.
%%-------------------------------------------------------------------------
-spec controlling_process(SocketType, Socket, NewOwner) -> Object when
      SocketType :: ip_comm | {ip_comm | ssl | essl, _SSLConfig},
      Socket     :: gen_tcp:socket(),
      NewOwner   :: pid(),
      Object     :: ok | {error, Reason},
      Reason     :: closed | not_owner | badarg | inet:posix().
controlling_process(ip_comm, Socket, NewOwner) ->
    gen_tcp:controlling_process(Socket, NewOwner);
controlling_process({ip_comm, _}, Socket, NewOwner) ->
    gen_tcp:controlling_process(Socket, NewOwner);

%% Wrapper for backaward compatibillity
controlling_process({ssl, SSLConfig}, Socket, NewOwner) ->
    controlling_process({?HTTP_DEFAULT_SSL_KIND, SSLConfig}, Socket, NewOwner);

controlling_process({essl, _}, Socket, NewOwner) ->
    ssl:controlling_process(Socket, NewOwner).


%%-------------------------------------------------------------------------
%% Description: Sets one or more options for a socket, using either
%% gen_tcp or ssl.
%%-------------------------------------------------------------------------
-spec setopts(SocketType, Socket, Options) -> ok | {error, inet:posix()} when
      SocketType :: ip_comm | {ip_comm | ssl | essl, _SSLConfig},
      Socket     :: inet:socket() | ssl:sslsocket(),
      Options    :: [inet:socket_setopt()] |  [gen_tcp:option()].
setopts(ip_comm, Socket, Options) ->
    inet:setopts(Socket, Options);
setopts({ip_comm, _}, Socket, Options) ->
    inet:setopts(Socket, Options);

%% Wrapper for backaward compatibillity
setopts({ssl, SSLConfig}, Socket, Options) ->
    setopts({?HTTP_DEFAULT_SSL_KIND, SSLConfig}, Socket, Options);

setopts({essl, _}, Socket, Options) ->
    (catch ssl:setopts(Socket, Options)).


%%-------------------------------------------------------------------------
%% Description: Gets the values for some options.
%%-------------------------------------------------------------------------
-spec getopts(SocketType, Socket) -> Object when
      SocketType :: ip_comm | {ip_comm | ssl | essl, _SSLConfig},
      Socket     :: ssl:sslsocket() | inet:socket(),
      Object     :: [gen_tcp:option()] | [inet:socket_setopt() | gen_tcp:pktoptions_value()] | [].
getopts(SocketType, Socket) ->
    Opts = [packet, packet_size, recbuf, sndbuf, priority, tos, send_timeout], 
    getopts(SocketType, Socket, Opts).

-spec getopts(SocketType, Socket, Options) -> Object when
      SocketType :: ip_comm | {ip_comm | ssl | essl, _SSLConfig},
      Socket     :: ssl:sslsocket() | inet:socket(),
      Options    :: [gen_tcp:option_name()],
      Object     :: [gen_tcp:option()] | [inet:socket_setopt() | gen_tcp:pktoptions_value()] | [].
getopts({ip_comm, _}, Socket, Options) ->
    getopts(ip_comm, Socket, Options);

getopts(ip_comm, Socket, Options) ->
    case inet:getopts(Socket, Options) of
	{ok, SocketOpts} ->
	    SocketOpts;
	{error, _} -> 
	    []
    end;

%% Wrapper for backaward compatibillity
getopts({ssl, SSLConfig}, Socket, Options) ->
    getopts({?HTTP_DEFAULT_SSL_KIND, SSLConfig}, Socket, Options);

getopts({essl, _}, Socket, Options) ->
    getopts_ssl(Socket, Options).

-spec getopts_ssl(SslSocket, Options) ->
		     [gen_tcp:option()] | [] when
      SslSocket :: ssl:sslsocket(),
      Options :: [gen_tcp:option_name()].
getopts_ssl(Socket, Options) ->
    case ssl:getopts(Socket, Options) of
	{ok, SocketOpts} ->
	    SocketOpts;
	{error, _} -> 
	    []
    end.
    

%%-------------------------------------------------------------------------
%% getstat(SocketType, Socket) -> socket_stats()
%%     SocketType = ip_comm | {ssl, _}
%%     Socket = socket()
%%     socket_stats() = list()
%% Description: Gets the socket stats values for the socket
%%-------------------------------------------------------------------------
getstat(ip_comm = _SocketType, Socket) ->
    case inet:getstat(Socket) of
	{ok, Stats} ->
	    Stats;
	{error, _} ->
	    []
    end;

%% Wrapper for backaward compatibillity
getstat({ssl, SSLConfig}, Socket) ->
    getstat({?HTTP_DEFAULT_SSL_KIND, SSLConfig}, Socket);

getstat({essl, _} = _SocketType, _Socket) ->
    [].


%%-------------------------------------------------------------------------
%% send(RequestOrSocketType, Socket, Message) -> ok | {error, Reason}
%%     SocketType = ip_comm | {ssl, _}
%%     Socket = socket()
%%     Message = list() | binary()                           
%% Description: Sends a packet on a socket, using either gen_tcp or ssl.
%%-------------------------------------------------------------------------
send(ip_comm, Socket, Message) ->
    gen_tcp:send(Socket, Message);
send({ip_comm, _}, Socket, Message) ->
    gen_tcp:send(Socket, Message);

%% Wrapper for backaward compatibillity
send({ssl, SSLConfig}, Socket, Message) ->
    send({?HTTP_DEFAULT_SSL_KIND, SSLConfig}, Socket, Message);

send({essl, _}, Socket, Message) ->
    ssl:send(Socket, Message).

%%-------------------------------------------------------------------------
%% close(SocketType, Socket) -> ok | {error, Reason}
%%     SocketType = ip_comm | {ssl, _}
%%     Socket = socket()  
%%                                   
%% Description: Closes a socket, using either gen_tcp or ssl.
%%-------------------------------------------------------------------------
close(ip_comm, Socket) ->
    gen_tcp:close(Socket);
close({ip_comm, []}, Socket) ->
    gen_tcp:close(Socket);

%% Wrapper for backaward compatibillity
close({ssl, SSLConfig}, Socket) ->
    close({?HTTP_DEFAULT_SSL_KIND, SSLConfig}, Socket);

close({essl, _}, Socket) ->
    ssl:close(Socket).


%%-------------------------------------------------------------------------
%% peername(SocketType, Socket) -> {Port, SockName}
%%     SocketType = ip_comm | {ssl, _}
%%     Socket = socket() 
%%     Port = integer()  (-1 if error occurred)
%%     PeerName = string()
%%                          
%% Description: Returns the address and port for the other end of a
%% connection, usning either gen_tcp or ssl.
%%-------------------------------------------------------------------------
peername(ip_comm, Socket) ->
    do_peername(inet:peername(Socket));
peername({ip_comm, _}, Socket) ->
    do_peername(inet:peername(Socket));

%% Wrapper for backaward compatibillity
peername({ssl, SSLConfig}, Socket) ->
    peername({?HTTP_DEFAULT_SSL_KIND, SSLConfig}, Socket);

peername({essl, _}, Socket) ->
    do_peername(ssl:peername(Socket)).

do_peername({ok, {Addr, Port}}) 
  when is_tuple(Addr) andalso (size(Addr) =:= 4) ->
    PeerName = ipv4_name(Addr), 
    {Port, PeerName};
do_peername({ok, {Addr, Port}}) 
  when is_tuple(Addr) andalso (size(Addr) =:= 8) ->
    PeerName = ipv6_name(Addr), 
    {Port, PeerName};
do_peername({error, _}) ->
    {-1, "unknown"}.


%%-------------------------------------------------------------------------
%% sockname(SocketType, Socket) -> {Port, SockName}
%%     SocketType = ip_comm | {ssl, _}
%%     Socket = socket() 
%%     Port = integer()  (-1 if error occurred)
%%     SockName = string()
%%                          
%% Description: Returns the address and port for the local (our) end 
%% other end of connection, using either gen_tcp or ssl.
%%-------------------------------------------------------------------------
sockname(ip_comm, Socket) ->
    do_sockname(inet:sockname(Socket));
sockname({ip_comm, _}, Socket) ->
    do_sockname(inet:sockname(Socket));
%% Wrapper for backaward compatibillity
sockname({ssl, SSLConfig}, Socket) ->
    sockname({?HTTP_DEFAULT_SSL_KIND, SSLConfig}, Socket);

sockname({essl, _}, Socket) ->
    do_sockname(ssl:sockname(Socket)).

do_sockname({ok, {Addr, Port}}) 
  when is_tuple(Addr) andalso (size(Addr) =:= 4) ->
    SockName = ipv4_name(Addr), 
    {Port, SockName};
do_sockname({ok, {Addr, Port}}) 
  when is_tuple(Addr) andalso (size(Addr) =:= 8) ->
    SockName = ipv6_name(Addr), 
    {Port, SockName};
do_sockname({error, _}) ->
    {-1, "unknown"}.


%%-------------------------------------------------------------------------
%% resolve() -> HostName
%%     HostName = string()
%%     
%% Description: Returns the local hostname. 
%%-------------------------------------------------------------------------
resolve() ->
    {ok, Name} = inet:gethostname(),
    Name.


%%-------------------------------------------------------------------------
%% ipv4_name(Ipv4Addr) -> string()
%% ipv6_name(Ipv6Addr) -> string()
%%     Ipv4Addr = ip4_address()
%%     Ipv6Addr = ip6_address()
%%     
%% Description: Returns the local hostname. 
%%-------------------------------------------------------------------------
ipv4_name({A, B, C, D}) ->
    integer_to_list(A) ++ "." ++
	integer_to_list(B) ++ "." ++
	integer_to_list(C) ++ "." ++
	integer_to_list(D).

ipv6_name({A, B, C, D, E, F, G, H}) ->
    http_util:integer_to_hexlist(A) ++ ":"++ 
	http_util:integer_to_hexlist(B) ++ ":" ++  
	http_util:integer_to_hexlist(C) ++ ":" ++ 
	http_util:integer_to_hexlist(D) ++ ":" ++  
	http_util:integer_to_hexlist(E) ++ ":" ++  
	http_util:integer_to_hexlist(F) ++ ":" ++  
	http_util:integer_to_hexlist(G) ++ ":" ++  
	http_util:integer_to_hexlist(H).


close_tag(ip_comm) ->
    tcp_closed;
close_tag(_) ->
    ssl_closed.

%%%========================================================================
%%% Internal functions
%%%========================================================================

%% -- sock_opts --
%% Address any comes from directive: BindAddress "*"
sock_opts(undefined, Opts) -> 
    sock_opts(Opts);
sock_opts(any = Addr, Opts) -> 
    sock_opts([{ip, Addr} | Opts]);
sock_opts(Addr, Opts) ->
    sock_opts([{ip, Addr} | Opts]).

sock_opts(Opts) ->
    [{packet, 0}, {active, false} | Opts].


%% -- negotiate --
negotiate(ip_comm,_,_) ->
    ok;
negotiate({ip_comm, _},_,_) ->
    ok;
negotiate({ssl, SSLConfig}, Socket, Timeout) ->
    negotiate({?HTTP_DEFAULT_SSL_KIND, SSLConfig}, Socket, Timeout);
negotiate({essl, _}, Socket, Timeout) ->
    negotiate_ssl(Socket, Timeout).

negotiate_ssl(Socket, Timeout) ->
    ssl:handshake(Socket, Timeout). 
