%% ---
   Excerpted from " Programming Erlang , Second Edition " ,
   published by The Pragmatic Bookshelf .
%%  Copyrights apply to this code. It may not be used to create training material, 
%%  courses, books, articles, and the like. Contact us if you are in doubt.
%%  We make no guarantees that this code is fit for any purpose. 
%%  Visit  for more book information.
%%---
-module(cgi_web_server).

-export([start/0, get_ip_info/0, start_from_shell/1, init/2, handle1/3, terminate/3]).

-define(MODULE_WHITELIST, [ping_pong]).
-define(IP_WHITELIST, [{127,0,0,1}]).
-define(NODE_NAMES, [node()]).

-record(allowed_ip, {ip_address, created}).
-record(ip_connection, {ip_address, number_connections}).

start() ->
    % Initialize Mnesia database if it doesn't already exist
    initialize_mnesia(),
    start(12345).

get_ip_info() ->
  Print out IP information from 
    mnesia:transaction(fun() ->
                               % Print whitelisted IPs
                               Keys = mnesia:foldl(fun(X, Acc) -> [X|Acc] end, [], allowed_ip),
                               io:format("Allowed IPs: ~p~n", [Keys]),

                               % Print IP access records
                               Keys2 = mnesia:foldl(fun(X, Acc) -> [X|Acc] end, [], ip_connection),
                               io:format("IPs Connections: ~p~n", [Keys2])
                       end).

initialize_mnesia() ->
    case mnesia:create_schema(?NODE_NAMES) of
        {error,{_Host,{already_exists,_host}}} ->
            ok;
        ok ->
            ok
    end,

    ok = mnesia:start(),

    DiskCopies = {disc_copies, ?NODE_NAMES},
    ensure_exists(mnesia:create_table(allowed_ip, [{attributes, record_info(fields, allowed_ip)}, DiskCopies])),
    ensure_exists(mnesia:create_table(ip_connection, [{attributes, record_info(fields, ip_connection)}, DiskCopies])),
    case mnesia:wait_for_tables([allowed_ip, ip_connection], 20000) of
        {timeout, _} ->
            io:format("Error! Timeout waiting for tables");
        ok ->
            seed_tables()
    end.

seed_tables() ->
    lists:foreach(fun(IP) ->
                          Result = mnesia:transaction(fun() ->
                                                              mnesia:write(#allowed_ip{
                                                                              ip_address=IP,
                                                                              created=erlang:timestamp()
                                                                             })
                                                      end),
                          io:format("Result ~p~n", [Result])
                  end, ?IP_WHITELIST).

ensure_exists({aborted, {already_exists, _Table}}) -> ok;
ensure_exists({atomic, ok}) -> ok;
ensure_exists(ok) -> ok.

start_from_shell([PortAsAtom]) ->
    PortAsInt = list_to_integer(atom_to_list(PortAsAtom)),
    start(PortAsInt).
%%

start(Port) ->
    {ok, _} = application:ensure_all_started(cowboy),
    Dispatch = cowboy_router:compile(
		 [
  { URIHost , list({URIPath , Handler , Opts } ) }
		  {'_', [{'_', cgi_web_server, #{}}]}
		 ]),
    cowboy:start_clear(cgi_web_server,
		      [{port, Port}],
              #{env => #{dispatch => Dispatch}}
		     ).

init(Req, State) ->
    % Check if request is from whitelisted IP
    {IPAddress, _Port} = cowboy_req:peer(Req),
    io:format("IP Address: ~p~n", [IPAddress]),
    F = fun() ->
                case mnesia:read(allowed_ip, IPAddress, read) of
                    [] -> false;
                    [_] ->
                        % Record request if permitted
                        case mnesia:read(ip_connection, IPAddress, read) of
                            [] ->
                                % Insert a new record
                                IPConn = #ip_connection{ip_address=IPAddress, number_connections=1},
                                mnesia:write(IPConn);
                            [IPConn] ->
                                % Update record
                                NewNumberOfConns = IPConn#ip_connection.number_connections + 1,
                                mnesia:write(IPConn#ip_connection{number_connections=NewNumberOfConns})
                        end,
                        true
                end
        end,

    case mnesia:transaction(F) of
        {_, false} ->
            % Block request if not
            Req2 = cowboy_req:reply(403, #{}, <<"Sorry, your IP is blocked">>, Req),
            {ok, Req2, State};
        {_, true} ->
            % Handle request
            Path = cowboy_req:path(Req),
            handle1(Path, Req, State)
    end.

handle1(<<"/cgi">>, Req, State) ->
    Args = cowboy_req:parse_qs(Req),
    {ok, Bin, Req2} = cowboy_req:read_body(Req),
    Val = case Bin of
              <<>> ->
                  <<>>;
              _ ->
                  io:format("~p", [Bin]),
                  jsx:decode(Bin)
          end,
    Response = call(Args, Val),
    Json = jsx:encode(Response),
    Req3 = cowboy_req:reply(200, #{}, Json, Req2),
    {ok, Req3, State};
handle1(Path, Req, State) ->
    Response = read_file(Path),
    Req1 = cowboy_req:reply(200, #{}, Response, Req),
    {ok, Req1, State}.

terminate(_Reason, _Req, _State) ->
    ok.

% Private functions
call([{<<"mod">>,MB},{<<"func">>,FB}], X) ->
    % Use list_to_existing_atom to prevent invalid parameters from flooding the
    % atom table
    Mod = list_to_existing_atom(binary_to_list(MB)),
    % Check if module is in whitelist
    case lists:member(Mod, ?MODULE_WHITELIST) of
        true ->
            % Use list_to_existing_atom to prevent invalid parameters from
            % flooding the atom table
            Func = list_to_existing_atom(binary_to_list(FB)),
            apply(Mod, Func, [X]);
        _ ->
            % Return an error if module not in whitelist
            [{error, invalid_module}]
    end.

read_file(Path) ->
    % Invoke safe_relative_path on the path from the client to prevent directory
    % traversal attacks
    SafePath = filename:safe_relative_path(Path),
    case file:read_file(SafePath) of
        {ok, Bin} -> Bin;
        _ ->
            % HTML escape path to prevent injection of HTML into the error page
            % markup
	        ["<pre>cannot read:", xmerl_lib:export_text(Path), "</pre>"]
    end.

 ---
  Copyrights apply to this code. It may not be used to create training material, 
  courses, books, articles, and the like. Contact us if you are in doubt.
  We make no guarantees that this code is fit for any purpose. 
  Visit  for more book information.
---
 Initialize Mnesia database if it doesn't already exist
 Print whitelisted IPs
 Print IP access records

 Check if request is from whitelisted IP
 Record request if permitted
 Insert a new record
 Update record
 Block request if not
 Handle request
 Private functions
 Use list_to_existing_atom to prevent invalid parameters from flooding the
 atom table
 Check if module is in whitelist
 Use list_to_existing_atom to prevent invalid parameters from
 flooding the atom table
 Return an error if module not in whitelist
 Invoke safe_relative_path on the path from the client to prevent directory
 traversal attacks
 HTML escape path to prevent injection of HTML into the error page
 markup
   Excerpted from " Programming Erlang , Second Edition " ,
   published by The Pragmatic Bookshelf .
-module(cgi_web_server).

-export([start/0, get_ip_info/0, start_from_shell/1, init/2, handle1/3, terminate/3]).

-define(MODULE_WHITELIST, [ping_pong]).
-define(IP_WHITELIST, [{127,0,0,1}]).
-define(NODE_NAMES, [node()]).

-record(allowed_ip, {ip_address, created}).
-record(ip_connection, {ip_address, number_connections}).

start() ->
    initialize_mnesia(),
    start(12345).

get_ip_info() ->
  Print out IP information from 
    mnesia:transaction(fun() ->
                               Keys = mnesia:foldl(fun(X, Acc) -> [X|Acc] end, [], allowed_ip),
                               io:format("Allowed IPs: ~p~n", [Keys]),

                               Keys2 = mnesia:foldl(fun(X, Acc) -> [X|Acc] end, [], ip_connection),
                               io:format("IPs Connections: ~p~n", [Keys2])
                       end).

initialize_mnesia() ->
    case mnesia:create_schema(?NODE_NAMES) of
        {error,{_Host,{already_exists,_host}}} ->
            ok;
        ok ->
            ok
    end,

    ok = mnesia:start(),

    DiskCopies = {disc_copies, ?NODE_NAMES},
    ensure_exists(mnesia:create_table(allowed_ip, [{attributes, record_info(fields, allowed_ip)}, DiskCopies])),
    ensure_exists(mnesia:create_table(ip_connection, [{attributes, record_info(fields, ip_connection)}, DiskCopies])),
    case mnesia:wait_for_tables([allowed_ip, ip_connection], 20000) of
        {timeout, _} ->
            io:format("Error! Timeout waiting for tables");
        ok ->
            seed_tables()
    end.

seed_tables() ->
    lists:foreach(fun(IP) ->
                          Result = mnesia:transaction(fun() ->
                                                              mnesia:write(#allowed_ip{
                                                                              ip_address=IP,
                                                                              created=erlang:timestamp()
                                                                             })
                                                      end),
                          io:format("Result ~p~n", [Result])
                  end, ?IP_WHITELIST).

ensure_exists({aborted, {already_exists, _Table}}) -> ok;
ensure_exists({atomic, ok}) -> ok;
ensure_exists(ok) -> ok.

start_from_shell([PortAsAtom]) ->
    PortAsInt = list_to_integer(atom_to_list(PortAsAtom)),
    start(PortAsInt).

start(Port) ->
    {ok, _} = application:ensure_all_started(cowboy),
    Dispatch = cowboy_router:compile(
		 [
  { URIHost , list({URIPath , Handler , Opts } ) }
		  {'_', [{'_', cgi_web_server, #{}}]}
		 ]),
    cowboy:start_clear(cgi_web_server,
		      [{port, Port}],
              #{env => #{dispatch => Dispatch}}
		     ).

init(Req, State) ->
    {IPAddress, _Port} = cowboy_req:peer(Req),
    io:format("IP Address: ~p~n", [IPAddress]),
    F = fun() ->
                case mnesia:read(allowed_ip, IPAddress, read) of
                    [] -> false;
                    [_] ->
                        case mnesia:read(ip_connection, IPAddress, read) of
                            [] ->
                                IPConn = #ip_connection{ip_address=IPAddress, number_connections=1},
                                mnesia:write(IPConn);
                            [IPConn] ->
                                NewNumberOfConns = IPConn#ip_connection.number_connections + 1,
                                mnesia:write(IPConn#ip_connection{number_connections=NewNumberOfConns})
                        end,
                        true
                end
        end,

    case mnesia:transaction(F) of
        {_, false} ->
            Req2 = cowboy_req:reply(403, #{}, <<"Sorry, your IP is blocked">>, Req),
            {ok, Req2, State};
        {_, true} ->
            Path = cowboy_req:path(Req),
            handle1(Path, Req, State)
    end.

handle1(<<"/cgi">>, Req, State) ->
    Args = cowboy_req:parse_qs(Req),
    {ok, Bin, Req2} = cowboy_req:read_body(Req),
    Val = case Bin of
              <<>> ->
                  <<>>;
              _ ->
                  io:format("~p", [Bin]),
                  jsx:decode(Bin)
          end,
    Response = call(Args, Val),
    Json = jsx:encode(Response),
    Req3 = cowboy_req:reply(200, #{}, Json, Req2),
    {ok, Req3, State};
handle1(Path, Req, State) ->
    Response = read_file(Path),
    Req1 = cowboy_req:reply(200, #{}, Response, Req),
    {ok, Req1, State}.

terminate(_Reason, _Req, _State) ->
    ok.

call([{<<"mod">>,MB},{<<"func">>,FB}], X) ->
    Mod = list_to_existing_atom(binary_to_list(MB)),
    case lists:member(Mod, ?MODULE_WHITELIST) of
        true ->
            Func = list_to_existing_atom(binary_to_list(FB)),
            apply(Mod, Func, [X]);
        _ ->
            [{error, invalid_module}]
    end.

read_file(Path) ->
    SafePath = filename:safe_relative_path(Path),
    case file:read_file(SafePath) of
        {ok, Bin} -> Bin;
        _ ->
	        ["<pre>cannot read:", xmerl_lib:export_text(Path), "</pre>"]
    end.
