%% ``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 via the world wide web 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.
%% 
%% The Initial Developer of the Original Code is Ericsson Utvecklings AB.
%% Portions created by Ericsson are Copyright 1999, Ericsson Utvecklings
%% AB. All Rights Reserved.''
%% 
%%     $Id: mod_security.erl,v 1.1 2008/12/17 09:53:35 mikpe Exp $
%%
-module(mod_security).

%% Security Audit Functionality

%% User API exports
-export([list_blocked_users/1, list_blocked_users/2, list_blocked_users/3, 
	 block_user/4, block_user/5, 
	 unblock_user/2, unblock_user/3, unblock_user/4,
	 list_auth_users/1, list_auth_users/2, list_auth_users/3]).

%% module API exports
-export([do/1, load/2, store/2, remove/1]).

-include("httpd.hrl").

-define(VMODULE,"SEC").
-include("httpd_verbosity.hrl").


%% do/1
do(Info) ->
    ?vdebug("~n   do with ~n   Info: ~p",[Info]),
    %% Check and see if any user has been authorized.
    case httpd_util:key1search(Info#mod.data,remote_user,not_defined_user) of
	not_defined_user ->
	    %% No user has been authorized.
	    case httpd_util:key1search(Info#mod.data, status) of
		%% A status code has been generated!
		{401, PhraseArgs, Reason} ->
		    case httpd_util:key1search(Info#mod.parsed_header,
					       "authorization") of
			undefined ->
			    %% Not an authorization attempt (server just replied to
			    %% challenge for authentication)
			    {proceed, Info#mod.data};
			[$B,$a,$s,$i,$c,$ |EncodedString] ->
			    %% Someone tried to authenticate, and obviously failed!
			    ?vlog("~n   Authentication failed: ~s",
				  [EncodedString]),
			    report_failed(Info, EncodedString,"Failed authentication"),
			    take_failed_action(Info, EncodedString),
			    {proceed, Info#mod.data}
		    end;
		_ ->
		    {proceed, Info#mod.data}
	    end;
	User ->
	    %% A user has been authenticated, now is he blocked ?
	    ?vtrace("user '~p' authentication",[User]),
	    Path = mod_alias:path(Info#mod.data,
				  Info#mod.config_db,
				  Info#mod.request_uri),
	    {Dir, SDirData} = secretp(Path, Info#mod.config_db),
	    Addr = httpd_util:lookup(Info#mod.config_db, bind_address),
	    Port = httpd_util:lookup(Info#mod.config_db, port),
	    DF   = httpd_util:key1search(SDirData, data_file),
	    case mod_security_server:check_blocked_user(Info, User, 
							SDirData, 
							Addr, Port) of
		true ->
		    ?vtrace("user blocked",[]),
		    report_failed(Info,httpd_util:decode_base64(User) ,"User Blocked"),
		    {proceed, [{status, {403, Info#mod.request_uri, ""}}|Info#mod.data]};
		false ->
		    ?vtrace("user not blocked",[]),
		    EncodedUser=httpd_util:decode_base64(User),
		    report_failed(Info, EncodedUser,"Authentication Succedded"),
		    mod_security_server:store_successful_auth(Addr, Port, 
							      User, SDirData),
		    {proceed, Info#mod.data}
	    end
    end.



report_failed(Info, EncodedString,Event) ->
    Request = Info#mod.request_line,
    Decoded = httpd_util:decode_base64(EncodedString),
    {PortNumber,RemoteHost}=(Info#mod.init_data)#init_data.peername,
    String = RemoteHost++" : " ++ Event ++ " : "++Request++" : "++Decoded,
    mod_disk_log:security_log(Info,String),
    mod_log:security_log(Info, String).

take_failed_action(Info, EncodedString) ->
    Path = mod_alias:path(Info#mod.data,Info#mod.config_db, Info#mod.request_uri),
    {Dir, SDirData} = secretp(Path, Info#mod.config_db),
    Addr = httpd_util:lookup(Info#mod.config_db, bind_address),
    Port = httpd_util:lookup(Info#mod.config_db, port),
    DecodedString = httpd_util:decode_base64(EncodedString),
    mod_security_server:store_failed_auth(Info, Addr, Port, 
					  DecodedString, SDirData).

secretp(Path, ConfigDB) ->
    Directories = ets:match(ConfigDB,{directory,'$1','_'}),
    case secret_path(Path, Directories) of
	{yes, Directory} ->
	    SDirs0 = httpd_util:multi_lookup(ConfigDB, security_directory),
	    SDir = lists:filter(fun(X) ->
					lists:member({path, Directory}, X)
				end, SDirs0),
	    {Directory, lists:flatten(SDir)};
	no ->
	    error_report({internal_error_secretp, ?MODULE}),
	    {[], []}
    end.

secret_path(Path,Directories) ->
    secret_path(Path, httpd_util:uniq(lists:sort(Directories)), to_be_found).

secret_path(Path, [], to_be_found) ->
    no;
secret_path(Path, [], Directory) ->
    {yes, Directory};
secret_path(Path, [[NewDirectory]|Rest], Directory) ->
    case regexp:match(Path, NewDirectory) of
	{match, _, _} when Directory == to_be_found ->
	    secret_path(Path, Rest, NewDirectory);
	{match, _, Length} when Length > length(Directory)->
	    secret_path(Path, Rest, NewDirectory);
	{match, _, Length} ->
	    secret_path(Path, Rest, Directory);
	nomatch ->
	    secret_path(Path, Rest, Directory)
    end.


load([$<,$D,$i,$r,$e,$c,$t,$o,$r,$y,$ |Directory],[]) ->
    Dir = httpd_conf:custom_clean(Directory,"",">"),
    {ok, [{security_directory, Dir, [{path, Dir}]}]};
load(eof,[{security_directory,Directory, DirData}|_]) ->
    {error, ?NICE("Premature end-of-file in "++Directory)};
load([$S,$e,$c,$u,$r,$i,$t,$y,$D,$a,$t,$a,$F,$i,$l,$e,$ |FileName],
     [{security_directory, Dir, DirData}]) ->
    File = httpd_conf:clean(FileName),
    {ok, [{security_directory, Dir, [{data_file, File}|DirData]}]};
load([$S,$e,$c,$u,$r,$i,$t,$y,$C,$a,$l,$l,$b,$a,$c,$k,$M,$o,$d,$u,$l,$e,$ |ModuleName],
     [{security_directory, Dir, DirData}]) ->
    Mod = list_to_atom(httpd_conf:clean(ModuleName)),
    {ok, [{security_directory, Dir, [{callback_module, Mod}|DirData]}]};
load([$S,$e,$c,$u,$r,$i,$t,$y,$M,$a,$x,$R,$e,$t,$r,$i,$e,$s,$ |Retries],
     [{security_directory, Dir, DirData}]) ->
    MaxRetries = httpd_conf:clean(Retries),
    load_return_int_tag("SecurityMaxRetries", max_retries, 
			httpd_conf:clean(Retries), Dir, DirData);
load([$S,$e,$c,$u,$r,$i,$t,$y,$B,$l,$o,$c,$k,$T,$i,$m,$e,$ |Time],
     [{security_directory, Dir, DirData}]) ->
    load_return_int_tag("SecurityBlockTime", block_time,
			httpd_conf:clean(Time), Dir, DirData);
load([$S,$e,$c,$u,$r,$i,$t,$y,$F,$a,$i,$l,$E,$x,$p,$i,$r,$e,$T,$i,$m,$e,$ |Time],
     [{security_directory, Dir, DirData}]) ->
    load_return_int_tag("SecurityFailExpireTime", fail_expire_time,
			httpd_conf:clean(Time), Dir, DirData);
load([$S,$e,$c,$u,$r,$i,$t,$y,$A,$u,$t,$h,$T,$i,$m,$e,$o,$u,$t,$ |Time0],
     [{security_directory, Dir, DirData}]) ->
    Time = httpd_conf:clean(Time0),
    load_return_int_tag("SecurityAuthTimeout", auth_timeout,
			httpd_conf:clean(Time), Dir, DirData);
load([$A,$u,$t,$h,$N,$a,$m,$e,$ |Name0],
     [{security_directory, Dir, DirData}]) ->
    Name = httpd_conf:clean(Name0),
    {ok, [{security_directory, Dir, [{auth_name, Name}|DirData]}]};
load("</Directory>",[{security_directory,Directory, DirData}]) ->
    {ok, [], {security_directory, Directory, DirData}}.

load_return_int_tag(Name, Atom, Time, Dir, DirData) ->
    case Time of
	"infinity" ->
	    {ok, [{security_directory, Dir, [{Atom, 99999999999999999999999999999}|DirData]}]};
	Int ->
	    case catch list_to_integer(Time) of
		{'EXIT', _} ->
		    {error, Time++" is an invalid "++Name};
		Val ->
		    {ok, [{security_directory, Dir, [{Atom, Val}|DirData]}]}
	    end
    end.

store({security_directory, Dir0, DirData}, ConfigList) ->
    ?CDEBUG("store(security_directory) -> ~n"
	    "      Dir0:       ~p~n"
	    "      DirData:    ~p",
	    [Dir0, DirData]),
    Addr = httpd_util:key1search(ConfigList, bind_address),
    Port = httpd_util:key1search(ConfigList, port),
    mod_security_server:start(Addr, Port),
    SR = httpd_util:key1search(ConfigList, server_root),
    Dir = 
	case filename:pathtype(Dir0) of
	    relative ->
		filename:join(SR, Dir0);
	    _ ->
		Dir0
	end,
    case httpd_util:key1search(DirData, data_file, no_data_file) of
	no_data_file ->
	    {error, no_security_data_file};
	DataFile0 ->
	    DataFile = 
		case filename:pathtype(DataFile0) of
		    relative ->
			filename:join(SR, DataFile0);
		    _ ->
			DataFile0
		end,
	    case mod_security_server:new_table(Addr, Port, DataFile) of
		{ok, TwoTables} ->
		    NewDirData0 = lists:keyreplace(data_file, 1, DirData, 
						   {data_file, TwoTables}),
		    NewDirData1 = case Addr of
				      undefined ->
					  [{port,Port}|NewDirData0];
				      _ ->
					  [{port,Port},{bind_address,Addr}|
					   NewDirData0]
				  end,
		    {ok, {security_directory,NewDirData1}};
		{error, Err} ->
		    {error, {{open_data_file, DataFile}, Err}}
	    end
    end.


remove(ConfigDB) ->
    Addr = case ets:lookup(ConfigDB, bind_address) of
	       [] -> 
		   undefined;
	       [{bind_address, Address}] ->
		   Address
	   end,
    [{port, Port}] = ets:lookup(ConfigDB, port),
    mod_security_server:delete_tables(Addr, Port),
    mod_security_server:stop(Addr, Port).
    

%%
%% User API
%%

%% list_blocked_users

list_blocked_users(Port) ->
    list_blocked_users(undefined, Port).

list_blocked_users(Port, Dir) when integer(Port) ->
    list_blocked_users(undefined,Port,Dir);
list_blocked_users(Addr, Port) when integer(Port) ->
    mod_security_server:list_blocked_users(Addr, Port).

list_blocked_users(Addr, Port, Dir) ->
    mod_security_server:list_blocked_users(Addr, Port, Dir).


%% block_user

block_user(User, Port, Dir, Time) ->
    block_user(User, undefined, Port, Dir, Time).
block_user(User, Addr, Port, Dir, Time) ->
    mod_security_server:block_user(User, Addr, Port, Dir, Time).


%% unblock_user

unblock_user(User, Port) ->
    unblock_user(User, undefined, Port).

unblock_user(User, Port, Dir) when integer(Port) ->
    unblock_user(User, undefined, Port, Dir);
unblock_user(User, Addr, Port) when integer(Port) ->
    mod_security_server:unblock_user(User, Addr, Port).

unblock_user(User, Addr, Port, Dir) ->
    mod_security_server:unblock_user(User, Addr, Port, Dir).


%% list_auth_users

list_auth_users(Port) ->
    list_auth_users(undefined,Port).

list_auth_users(Port, Dir) when integer(Port) ->
    list_auth_users(undefined, Port, Dir);
list_auth_users(Addr, Port) when integer(Port) ->
    mod_security_server:list_auth_users(Addr, Port).

list_auth_users(Addr, Port, Dir) ->
    mod_security_server:list_auth_users(Addr, Port, Dir).
    

error_report(M) ->
    error_logger:error_report(M).
