%%--------------------------------------------------------------------
%%
%% %CopyrightBegin%
%% 
%% Copyright Ericsson AB 1997-2011. All Rights Reserved.
%% 
%% The contents of this file are subject to the Erlang Public License,
%% Version 1.1, (the "License"); you may not use this file except in
%% compliance with the License. You should have received a copy of the
%% Erlang Public License along with this software. If not, it can be
%% retrieved online at http://www.erlang.org/.
%% 
%% Software distributed under the License is distributed on an "AS IS"
%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
%% the License for the specific language governing rights and limitations
%% under the License.
%% 
%% %CopyrightEnd%
%%
%%
%%-----------------------------------------------------------------
%% File: orber_iiop_net.erl
%% 
%% Description:
%%    This file contains the IIOP communication server
%%
%%-----------------------------------------------------------------
-module(orber_iiop_net).

-behaviour(gen_server).

-include_lib("orber/src/orber_iiop.hrl").

%%-----------------------------------------------------------------
%% External exports
%%-----------------------------------------------------------------
-export([start/1, connect/5, connections/0, 
	 sockname2peername/2, peername2sockname/2,
	 add_connection/5,
	 add/3, remove/1, reconfigure/1, reconfigure/2]).

%%-----------------------------------------------------------------
%% Internal exports
%%-----------------------------------------------------------------
-export([init/1, terminate/2, handle_call/3, 
	 handle_cast/2, handle_info/2, code_change/3]).

%%-----------------------------------------------------------------
%% Server state record and definitions
%%-----------------------------------------------------------------
-define(CONNECTION_DB, orber_iiop_net_db).

-record(state, {ports=[], max_connections, db, counter = 1, queue}).

-record(connection, {pid, socket, type, peerdata, localdata, ref = 0}).

-record(listen, {pid, socket, port, type, ref = 0, options, proxy_options = []}).

%%-----------------------------------------------------------------
%% External interface functions
%%-----------------------------------------------------------------
%%-----------------------------------------------------------------
%% Func: start/1
%%-----------------------------------------------------------------
start(Opts) ->
    gen_server:start_link({local, orber_iiop_net}, orber_iiop_net, Opts, []).

add(IP, normal, Options) ->
    Port = orber_tb:keysearch(iiop_port, Options, orber_env:iiop_port()),
    gen_server:call(orber_iiop_net, {add, IP, normal, Port, Options}, infinity);
add(IP, ssl, Options) ->
    Port = orber_tb:keysearch(iiop_ssl_port, Options, orber_env:iiop_ssl_port()),
    gen_server:call(orber_iiop_net, {add, IP, ssl, Port, Options}, infinity).

remove(Ref) ->
    gen_server:call(orber_iiop_net, {remove, Ref}, infinity).

reconfigure(Options) ->
    lists:foreach(fun(P) -> 
			  P !  {reconfigure, Options}
		  end, 
		  do_select([{#connection{pid = '$1', _='_'}, 
			      [], ['$1']}])).

reconfigure(Options, Ref) ->
    case do_select([{#connection{ref = Ref, pid = '$1', _='_'}, 
		     [], ['$1']}]) of
	[Pid] when is_pid(Pid) ->
	    Pid !  {reconfigure, Options},
	    ok;
	_ ->
	    {error, "No proxy matched the supplied reference"}
    end.

connect(Type, S, AcceptPid, Ref, ProxyOptions) ->
    gen_server:call(orber_iiop_net, {connect, Type, S, AcceptPid, 
				     Ref, ProxyOptions}, infinity).

connections() ->
    do_select([{#connection{peerdata = '$1', _='_'}, [], ['$1']}]).

sockname2peername(SockHost, SockPort) ->
    do_select([{#connection{peerdata = '$1',
			    localdata = {match_type(SockHost), 
					 match_type(SockPort)}, 
			    _='_'}, [], ['$1']}]).


peername2sockname(PeerHost, PeerPort) ->
    do_select([{#connection{peerdata = {match_type(PeerHost), 
					match_type(PeerPort)},
			    localdata = '$1', 
			    _='_'}, [], ['$1']}]).

do_select(Pattern) ->   
    case catch ets:select(?CONNECTION_DB, Pattern) of
	{'EXIT', _What} ->
	    [];
	Result ->
	    Result
    end.

match_type(0) -> 
    %% Wildcard port number
    '_';
match_type("") -> 
    %% Wildcard host
    '_';
match_type(Key) -> 
    %% Wildcard not used.
    Key.
    
add_connection(Socket, Type, PeerData, LocalData, Ref) ->
    ets:insert(?CONNECTION_DB, #connection{pid = self(), socket = Socket, 
					   type = Type, peerdata = PeerData,
					   localdata = LocalData, ref = Ref}).

%%-----------------------------------------------------------------
%% Server functions
%%-----------------------------------------------------------------
%%-----------------------------------------------------------------
%% Func: init/1
%%-----------------------------------------------------------------
init(Options) ->
    process_flag(trap_exit, true),
    {ok, parse_options(Options, 
		       #state{max_connections = orber:iiop_max_in_connections(),
			      db = ets:new(?CONNECTION_DB, [set, public, 
							    named_table,
							    {keypos, 2}]), 
			      queue = queue:new()})}.

%%-----------------------------------------------------------------
%% Func: terminate/1
%%-----------------------------------------------------------------
terminate(_Reason, _State) ->
    ok.

%%-----------------------------------------------------------------
%% Func: parse_options/2
%%-----------------------------------------------------------------
get_options(normal, _Options) ->
    [];
get_options(ssl, Options) ->    
    SSLOpts = 
	case orber_tb:keysearch(ssl_server_options, Options,
				orber_env:ssl_server_options()) of
	    [] ->
		Verify = orber_tb:keysearch(ssl_server_verify, Options, 
					    orber_env:ssl_server_verify()),
		Depth = orber_tb:keysearch(ssl_server_depth, Options, 
					   orber_env:ssl_server_depth()),
		Cert = orber_tb:keysearch(ssl_server_certfile, Options, 
					  orber_env:ssl_server_certfile()),
		CaCert = orber_tb:keysearch(ssl_server_cacertfile, Options, 
					    orber_env:ssl_server_cacertfile()),
		Pwd = orber_tb:keysearch(ssl_server_password, Options, 
					 orber_env:ssl_server_password()),
		Key = orber_tb:keysearch(ssl_server_keyfile, Options, 
					 orber_env:ssl_server_keyfile()),
		Ciphers = orber_tb:keysearch(ssl_server_ciphers, Options, 
					     orber_env:ssl_server_ciphers()),
		Timeout = orber_tb:keysearch(ssl_server_cachetimeout, Options, 
					     orber_env:ssl_server_cachetimeout()),
		KeepAlive = orber_tb:keysearch(ssl_server_cachetimeout, Options, 
					       orber_env:iiop_ssl_in_keepalive()),
		[{verify, Verify},
		 {depth, Depth},
		 {certfile, Cert},
		 {cacertfile, CaCert},
		 {password, Pwd},
		 {keyfile, Key},
		 {ciphers, Ciphers},
		 {cachetimeout, Timeout},
		 {keepalive, KeepAlive}];
	    Opts ->	
		case orber_tb:check_illegal_tcp_options(Opts) of
		    ok -> 
			check_old_ssl_server_options(Options),
			Opts;
		    {error, IllegalOpts} ->
			error_logger:error_report([{application, orber},
						   "TCP options not allowed to set on a connection", 
						   IllegalOpts]),
			error("Illegal TCP option")
		end
	end,
    ssl_server_extra_options(SSLOpts, []).

%%-----------------------------------------------------------------
%% Func: parse_options/2
%%-----------------------------------------------------------------
parse_options([{port, Type, Port} | Rest], State) ->
    Options = get_options(Type, []),
    Options2 = case orber_env:ip_address_variable_defined() of
		   false ->
		       Options;
		   Host ->
		       IPVersion = orber:ip_version(),
		       {ok, IP} = inet:getaddr(Host, IPVersion),
		       [{ip, IP} | Options]
	       end,
    {ok, Listen, NewPort} = orber_socket:listen(Type, Port, Options2, true),
    {ok, Pid} = orber_iiop_socketsup:start_accept(Type, Listen, 0),
    link(Pid),
    ets:insert(?CONNECTION_DB, #listen{pid = Pid, socket = Listen, 
				       port = NewPort, type = Type,
				       options = Options2}),
    parse_options(Rest, State);
parse_options([], State) ->
    State.

ssl_server_extra_options([], Acc) ->
    Acc;
ssl_server_extra_options([{_Type, []}|T], Acc) ->
    ssl_server_extra_options(T, Acc);
ssl_server_extra_options([{_Type, infinity}|T], Acc) ->
    ssl_server_extra_options(T, Acc);
ssl_server_extra_options([{Type, Value}|T], Acc) ->
    ssl_server_extra_options(T, [{Type, Value}|Acc]).

filter_options([], Acc) ->
    Acc;
filter_options([{verify, _}|T], Acc) ->
    filter_options(T, Acc);
filter_options([{depth, _}|T], Acc) ->
    filter_options(T, Acc);
filter_options([{certfile, _}|T], Acc) ->
    filter_options(T, Acc);
filter_options([{cacertfile, _}|T], Acc) ->
    filter_options(T, Acc);
filter_options([{password, _}|T], Acc) ->
    filter_options(T, Acc);
filter_options([{keyfile, _}|T], Acc) ->
    filter_options(T, Acc);
filter_options([{ciphers, _}|T], Acc) ->
    filter_options(T, Acc);
filter_options([{cachetimeout, _}|T], Acc) ->
    filter_options(T, Acc);
filter_options([H|T], Acc) ->
    filter_options(T, [H|Acc]).

%%-----------------------------------------------------------------
%% Func: handle_call/3
%%-----------------------------------------------------------------
handle_call({remove, Ref}, _From, State) ->
    case do_select([{#listen{ref = Ref, pid = '$1', socket = '$2',
			     type = '$3', _='_'}, [], [{{'$1', '$2', '$3'}}]}]) of
	[{Pid, Listen, Type}|_] when is_pid(Pid) ->
	    unlink(Pid),
	    ets:delete(?CONNECTION_DB, Pid),
	    %% Just close the listen socket. Will cause the accept processs 
	    %% to terminate.
	    orber_socket:close(Type, Listen),
	    stop_proxies(do_select([{#connection{ref = Ref, pid = '$1', _='_'}, 
				     [], ['$1']}])),
	    {reply, ok, 
	     State#state{queue = 
			 from_list(
			   lists:keydelete(Pid, 1, 
					   queue:to_list(State#state.queue)))}};
	_ ->
	    {reply, ok, State}
    end;
handle_call({add, IP, Type, Port, AllOptions}, _From, State) ->
    Family = orber_env:ip_version(),
    case inet:getaddr(IP, Family) of
	{ok, IPTuple} ->
	    try [{ip, IPTuple} |get_options(Type, AllOptions)] of
		Options ->
     	            Ref = make_ref(),
		    ProxyOptions = filter_options(AllOptions, []),
	            case orber_socket:listen(Type, Port, Options, false) of
		        {ok, Listen, NewPort} ->
		            {ok, Pid} = orber_iiop_socketsup:start_accept(Type, Listen, Ref,
		       					                  ProxyOptions),
		            link(Pid),
		            ets:insert(?CONNECTION_DB, #listen{pid = Pid, 
						               socket = Listen, 
						               port = NewPort, 
						               type = Type, ref = Ref,
						               options = Options,
						               proxy_options = ProxyOptions}),
		            {reply, {ok, Ref}, State};
	          	Error ->
		            {reply, Error, State}
	            end
            catch
		error:Reason ->
		    {reply, {error, Reason}, State}
	    end;
	Other ->
	    {reply, Other, State}
    end;
handle_call({connect, Type, Socket, _AcceptPid, AccepRef, ProxyOptions}, _From, State) 
  when State#state.max_connections == infinity;
       State#state.max_connections > State#state.counter ->
    case catch access_allowed(Type, Socket, Type) of
	true ->
	    case orber_iiop_insup:start_connection(Type, Socket, 
						   AccepRef, ProxyOptions) of
		{ok, Pid} when is_pid(Pid) ->
		    link(Pid),
		    {reply, {ok, Pid, true}, update_counter(State, 1)};
		Other ->
		    {reply, Other, State}
	    end;
	_ ->
	    {H, P} = orber_socket:peerdata(Type, Socket),
	    orber_tb:info("Blocked connect attempt from ~s - ~p", [H, P]),
	    {reply, denied, State}
    end;
handle_call({connect, Type, Socket, AcceptPid, AccepRef, ProxyOptions}, _From, 
	    #state{queue = Q} = State) ->
    case catch access_allowed(Type, Socket, Type) of
	true ->
	    case orber_iiop_insup:start_connection(Type, Socket, 
						   AccepRef, ProxyOptions) of
		{ok, Pid} when is_pid(Pid) ->
		    link(Pid),
		    Ref = erlang:make_ref(),
		    {reply, {ok, Pid, Ref}, 
		     update_counter(State#state{queue = 
						queue:in({AcceptPid, Ref}, Q)}, 1)};
		Other ->
		    {reply, Other, State}
	    end;
	_ ->
	    {H, P} = orber_socket:peerdata(Type, Socket),
	    orber_tb:info("Blocked connect attempt from ~s - ~p", [H, P]),
	    {reply, denied, State}
    end;
handle_call(_, _, State) ->
    {noreply, State}.

stop_proxies([H|T]) ->
    catch orber_iiop_inproxy:stop(H),
    stop_proxies(T);
stop_proxies([]) ->
    ok.

access_allowed(Type, Socket, Type) ->
    Flags = orber:get_flags(),
    case ?ORB_FLAG_TEST(Flags, ?ORB_ENV_USE_ACL_INCOMING) of
	false ->
	    true;
	true ->
	    SearchFor = 
		case Type of
		    normal ->
			tcp_in;
		    ssl ->
			ssl_in
		end,	    
	    {ok, {Host, Port}} = orber_socket:peername(Type, Socket),
	    case orber_acl:match(Host, SearchFor, true) of
		{true, [], 0} ->
		    true;
		{true, [], Port} ->
		    true;
		{true, [], {Min, Max}} when Port >= Min, Port =< Max ->
		    true;
		{true, Interfaces, 0} ->
		    get_sockethost(Type, Socket),
		    lists:member(get_sockethost(Type, Socket), Interfaces);
		{true, Interfaces, Port} ->
		    lists:member(get_sockethost(Type, Socket), Interfaces);
		{true, Interfaces, {Min, Max}} when Port >= Min, Port =< Max ->
		    lists:member(get_sockethost(Type, Socket), Interfaces);
		_ ->
		    false
	    end
    end.

get_sockethost(Type, Socket) ->
    case orber_socket:peername(Type, Socket) of
	{ok, {Addr, _Port}} ->
	    orber_env:addr2str(Addr);
	_ ->
	    false
    end.
   
%%------------------------------------------------------------
%% Standard gen_server cast handle
%%------------------------------------------------------------
handle_cast(_, State) ->
    {noreply,  State}.

%%------------------------------------------------------------
%% Standard gen_server handles
%%------------------------------------------------------------
handle_info({'EXIT', Pid, _Reason}, State) when is_pid(Pid) ->
    case ets:lookup(?CONNECTION_DB, Pid) of
	[#listen{pid = Pid, socket = Listen, port = Port, type = Type, 
		 ref = Ref, options = Options, proxy_options = POpts}] ->
	    ets:delete(?CONNECTION_DB, Pid),
	    unlink(Pid),
	    NewListen = new_listen_socket(Type, Listen, Port, Options),
	    {ok, NewPid} = orber_iiop_socketsup:start_accept(Type, NewListen, 
							     Ref, POpts),
	    link(NewPid),
	    ets:insert(?CONNECTION_DB, #listen{pid = NewPid, socket = NewListen, 
					       port = Port, type = Type, 
					       ref = Ref, options = Options, 
					       proxy_options = POpts}),
	    %% Remove the connection if it's in the queue.
	    {noreply, 
	     State#state{queue = 
			 from_list(
			   lists:keydelete(Pid, 1, 
					   queue:to_list(State#state.queue)))}};
	[#connection{pid = Pid}] ->
	    ets:delete(?CONNECTION_DB, Pid),
	    unlink(Pid),
	    case queue:out(State#state.queue) of
		{empty, _} ->
		    {noreply, update_counter(State, -1)};
		{{value, {AcceptPid, Ref}}, Q} ->
		    AcceptPid ! {Ref, ok},
		    {noreply, update_counter(State#state{queue = Q}, -1)}
	    end;
	[] ->
	    {noreply, State}
    end;
handle_info(_, State) ->
    {noreply,  State}.

new_listen_socket(normal, ListenFd, _Port, _Options) ->
    ListenFd;
new_listen_socket(ssl, ListenFd, Port, Options) ->
    Generation = orber_env:ssl_generation(),
    if
	Generation > 2 ->
	    ListenFd;
	true ->
	    case is_process_alive(ssl:pid(ListenFd)) of
		true ->
		    ListenFd;
		_ ->
		    {ok, Listen, _NP} = orber_socket:listen(ssl, Port, Options, true),
		    Listen
	    end
    end.

from_list(List) ->
    from_list(List, queue:new()).

from_list([], Q) ->
    Q;
from_list([H|T], Q) ->
    NewQ = queue:in(H, Q),
    from_list(T, NewQ).


%%-----------------------------------------------------------------
%% Func: code_change/3
%%-----------------------------------------------------------------
code_change(_OldVsn, State, _Extra) ->
    {ok, State}.

%%-----------------------------------------------------------------
%% Internal Functions
%%-----------------------------------------------------------------
update_counter(#state{max_connections = infinity} = State, _) ->
    State;
update_counter(State, Value) ->
    State#state{counter = State#state.counter + Value}.


check_old_ssl_server_options(Options) ->
    try
	0 = orber_tb:keysearch(ssl_server_verify, Options, 
			       orber_env:ssl_server_verify()),
    	1 = orber_tb:keysearch(ssl_server_depth, Options, 
			       orber_env:ssl_server_depth()),
     	[] = orber_tb:keysearch(ssl_server_certfile, Options, 
				orber_env:ssl_server_certfile()),
	[] = orber_tb:keysearch(ssl_server_cacertfile, Options, 
				orber_env:ssl_server_cacertfile()),
	[] = orber_tb:keysearch(ssl_server_password, Options, 
				orber_env:ssl_server_password()),
	[] = orber_tb:keysearch(ssl_server_keyfile, Options, 
				orber_env:ssl_server_keyfile()),
	[] = orber_tb:keysearch(ssl_server_ciphers, Options, 
				orber_env:ssl_server_ciphers()),
	infinity = orber_tb:keysearch(ssl_server_cachetimeout, Options, 
				      orber_env:ssl_server_cachetimeout()),
	false = orber_tb:keysearch(iiop_ssl_in_keepalive, Options, 
				   orber_env:iiop_ssl_in_keepalive())
    catch
	_:_ ->
					      io:format("hej\n",[]),
	    error_logger:warning_report([{application, orber},
			 "Ignoring deprecated ssl server options used together with the ssl_server_options"])
    end.
   
