%%----------------------------------------------------------------------
%%% File    : yaws_log.erl
%%% Author  : Claes Wikstrom <klacke@hyber.org>
%%% Purpose : 
%%% Created : 26 Jan 2002 by Claes Wikstrom <klacke@hyber.org>
%%%----------------------------------------------------------------------

-module(yaws_log).
-author('klacke@hyber.org').
-include_lib("kernel/include/file.hrl").


-behaviour(gen_server).

%% External exports
-export([start_link/0]).

%% gen_server callbacks
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, 
         handle_event/2, code_change/3]).
-export([accesslog/5,
         accesslog/7,
         accesslog/8,
         setdir/2,
         open_trace/1,
         trace_traffic/2,
         authlog/4,
         actl_trace/1]).


-include("../include/yaws.hrl").
-include("../include/yaws_api.hrl").
-include("yaws_debug.hrl").

%% 1 meg log we wrap
-define(WRAP_LOG_SIZE, 1000000).


-record(state, {
          running,
          dir,
          now,
          tracefd,
          log_wrap_size = ?WRAP_LOG_SIZE,
          tty_trace = false,
          copy_errlog,
          auth_log,
          alogs =  []}).


-record(alog, {fd,
               servername,
               filename}).


%%%----------------------------------------------------------------------
%%% API
%%%----------------------------------------------------------------------
start_link() ->
    gen_server:start_link({local, yaws_log}, yaws_log, [], []).

accesslog(ServerName, Ip, Req, Status, Length) ->
    accesslog(ServerName, Ip, "-", Req, Status, Length, "-", "-").

accesslog(ServerName, Ip, Req, Status, Length, Referrer, UserAgent) ->
    accesslog(ServerName, Ip, "-", Req, Status, Length, Referrer, UserAgent).

accesslog(ServerName, Ip, User, Req, Status, Length, Referrer, UserAgent) ->
    gen_server:cast(?MODULE, {access, ServerName, Ip, User, Req, 
                              Status, Length, Referrer, UserAgent}).
setdir(GC, Sconfs) ->
    gen_server:call(?MODULE, {setdir, GC, Sconfs}, infinity).

open_trace(What) ->
    gen_server:call(?MODULE, {open_trace, What}, infinity).
trace_traffic(ServerOrClient , Data) ->
    gen_server:cast(?MODULE, {trace, ServerOrClient, Data}).

authlog(ServerName, IP, Path, Item) ->
    gen_server:cast(?MODULE, {auth, ServerName, IP, Path, Item}).

%% from external ctl prog
actl_trace(What) ->
    gen_server:call(?MODULE, {actl_trace, What}, infinity).




%%%----------------------------------------------------------------------
%%% Callback functions from gen_server
%%%----------------------------------------------------------------------

%%----------------------------------------------------------------------
%% Func: init/1
%% Returns: {ok, State}          |
%%          {ok, State, Timeout} |
%%          ignore               |
%%          {stop, Reason}
%%----------------------------------------------------------------------
init([]) ->
    process_flag(trap_exit, true),
    {ok, #state{running = false, now = fmtnow()}}.

%%----------------------------------------------------------------------
%% Func: handle_call/3
%% Returns: {reply, Reply, State}          |
%%          {reply, Reply, State, Timeout} |
%%          {noreply, State}               |
%%          {noreply, State, Timeout}      |
%%          {stop, Reason, Reply, State}   | (terminate/2 is called)
%%          {stop, Reason, State}            (terminate/2 is called)
%%----------------------------------------------------------------------
handle_call({setdir, GC, Sconfs}, _From, State) 
  when State#state.running == false ->
    Dir = GC#gconf.logdir,
    ?Debug("setdir ~s~n", [Dir]),
    ElogFile = filename:join([Dir, "report.log"]),
    Copy = if ?gc_has_copy_errlog(GC) ->
                   gen_event:add_handler(error_logger, yaws_log_file_h, 
                                         ElogFile),
                   true;
              true ->
                   false
           end,
    SCs = lists:flatten(Sconfs),
    L = lists:zf(
          fun(SC) when ?sc_has_access_log(SC)  ->
                  FileName = case os:type() of
                                 {win32,_ } ->
                                     lists:map(fun($:) -> $.;
                                                              (C ) -> C
                                                       end,
                                               SC#sconf.servername);
                                 _ ->
                                     SC#sconf.servername
                             end,
                  A = filename:join([Dir, FileName ++ ".access"]),
                  case file:open(A, [write, raw, append]) of
                      {ok, Fd} ->
                          {true, #alog{servername = SC#sconf.servername,
                                       fd = Fd,
                                       filename = A}};
                      _Err ->
                          error_logger:format("Cannot open ~p~n",[A]),
                          false
                  end;
             (_) ->
                  false
          end, SCs),

    AuthLogFileName = filename:join([Dir, "auth.log"]),
    AuthLog = 
        if ?gc_has_auth_log(GC) ->
                case file:open(AuthLogFileName, [write, raw, append]) of
                    {ok, AFd} ->
                        #alog{fd = AFd,
                              servername = undefined,
                              filename = AuthLogFileName};
                    _Err ->
                        error_logger:format("Cannot open ~s~n", 
                                            [AuthLogFileName]),
                        undefined
                end;
           true ->
                undefined
        end,
    S2 = State#state{running = true,
                     dir  = Dir,
                     now = fmtnow(),
                     log_wrap_size = GC#gconf.log_wrap_size,
                     copy_errlog = Copy,
                     auth_log = AuthLog,
                     alogs = L},

    yaws:ticker(3000, secs3),
    yaws:ticker(10 * 60 * 1000, minute10),

    {reply, ok, S2};



%% We can't ever change logdir, we can however
%% change logging opts for various servers

handle_call({setdir, GC, Sconfs}, _From, State) 
  when State#state.running == true ->

    Dir = State#state.dir,
    ElogFile = filename:join([Dir, "report.log"]),
    Copy = if ?gc_has_copy_errlog(GC), State#state.copy_errlog == false->
                   gen_event:add_handler(error_logger, yaws_log_file_h, 
                                         ElogFile),
                   true;
              ?gc_has_copy_errlog(GC) ->
                   true;
              State#state.copy_errlog == true ->
                   gen_event:delete_handler(error_logger, yaws_log_file_h, 
                                            normal),
                   false;
              true ->
                   false
           end,

    %% close all files
    lists:map(
      fun(AL) ->
              file:close(AL#alog.fd)
      end, State#state.alogs),
    SCs = lists:flatten(Sconfs),

    %% reopen logfiles
    L = lists:zf(fun(SC) -> open_alog(Dir, SC) end,
                 SCs),
    S2 = State#state{running = true,
                     dir  = Dir,
                     now = fmtnow(),
                     copy_errlog = Copy,
                     alogs = L},

    {reply, ok, S2};


%% a virt server has been added
handle_call({soft_add_sc, SC}, _From, State) ->
    case open_alog(State#state.dir, SC) of
        {true, A} ->
            {reply, ok, State#state{alogs = [A|State#state.alogs]}};
        false ->
            {reply, ok, State}
    end;

%% a virt server has been deleted
handle_call({soft_del_sc, SC}, _From, State) ->
    case lists:keysearch(SC#sconf.servername, #alog.servername,
                         State#state.alogs) of
        {value, A} ->
            file:close(A#alog.fd),
            {reply,ok, State#state{alogs = State#state.alogs -- [A]}};
        false ->
            {reply,ok,State}
    end;


handle_call({open_trace, What}, _From, State) ->
    case State#state.tracefd of
        undefined ->
            ok;
        Fd0 ->
            file:close(Fd0)
    end,
    F = lists:concat(["trace.", What]),
    case file:open(filename:join([State#state.dir, F]),[write, raw]) of
        {ok, Fd} ->
            {reply, ok, State#state{tracefd = Fd}};
        Err ->
            {reply,  Err, State}
    end;


handle_call({trace_tty, What}, _From, State) ->
    {reply, ok, State#state{tty_trace = What}};
handle_call(state, _From, State) ->
    {reply, State, State}.


%%----------------------------------------------------------------------
%% Func: handle_cast/2
%% Returns: {noreply, State}          |
%%          {noreply, State, Timeout} |
%%          {stop, Reason, State}            (terminate/2 is called)
%%----------------------------------------------------------------------
handle_cast({access, ServerName, Ip, User, Req, Status, 
             Length, Referrer, UserAgent}, State) ->
    case State#state.running of 
        true ->
            do_alog(ServerName, Ip, User, Req, Status, Length, 
                    Referrer, UserAgent, State),
            {noreply, State};
        false ->
            {noreply, State}
    end;

handle_cast({auth, ServerName, IP, Path, Item}, State) ->
    case State#state.running of 
        true when State#state.auth_log /= undefined ->
            do_auth_log(ServerName, IP, Path, Item, State),
            {noreply, State};
        false ->
            {noreply,State}
    end;

handle_cast({trace, from_server, Data}, State) ->
    Str = ["*** SRV -> CLI *** ", Data],
    file:write(State#state.tracefd, Str),
    tty_trace(Str, State),
    {noreply,  State};
handle_cast({trace, from_client, Data}, State) ->
    Str = ["*** CLI -> SRV *** ", Data],
    file:write(State#state.tracefd, Str),
    tty_trace(Str, State),
    {noreply, State}.


open_alog(Dir, SC) when ?sc_has_access_log(SC) ->
    A = filename:join([Dir, SC#sconf.servername ++ ".access"]),
    case file:open(A, [write, raw, append]) of
        {ok, Fd} ->
            {true, #alog{servername = SC#sconf.servername,
                         fd = Fd,
                         filename = A}};
        _Err ->
            error_logger:format("Cannot open ~p",[A]),
            false
    end;
open_alog(_,_) ->
    false.



do_alog(ServerName, Ip, User, Req, Status, Length, Referrer,
        UserAgent, State) ->
    case lists:keysearch(ServerName, #alog.servername, State#state.alogs) of
        {value, AL} ->
            I = fmt_alog(State#state.now, Ip, User, Req, Status,  Length, 
                         Referrer, UserAgent),
            file:write(AL#alog.fd, I);
        _ ->
            false
    end.

do_auth_log(ServerName, IP, Path, Item, State) ->
    AL = State#state.auth_log,
    IpStr = case catch inet_parse:ntoa(IP) of
                {'EXIT', _} -> "unknownip";
                Val -> Val
            end,
    I = [IpStr,
         " ", State#state.now,
         " ", ServerName, " " ,"\"", Path,"\"",
         case Item of
             {ok, User} ->
                 [" OK user=", User];
             403 ->
                 [" 403"];
             {401, Realm} ->
                 [" 401 realm=", Realm];
             {401, User, PWD} ->
                 [" 401 user=", User, " badpwd=", PWD]
         end, "\n"],
    file:write(AL#alog.fd, I).



tty_trace(Str, State) ->
    case State#state.tty_trace of
        false ->
            ok;
        true ->
            io:format("~s", [binary_to_list(list_to_binary([Str]))])
    end.

%%----------------------------------------------------------------------
%% Func: handle_event/2
%% Returns: 
%%----------------------------------------------------------------------
handle_event({yaws_hupped, _Res}, State) ->
    handle_info(minute10,State),
    {ok, State};

handle_event(_Event, State) ->
    {ok, State}.




%%----------------------------------------------------------------------
%% Func: handle_info/2
%% Returns: {noreply, State}          |
%%          {noreply, State, Timeout} |
%%          {stop, Reason, State}            (terminate/2 is called)
%%----------------------------------------------------------------------
handle_info(secs3, State) ->
    {noreply, State#state{now = fmtnow()}};

handle_info({notify,{yaws_hupped,_}}, State) ->
    handle_info(minute10,State);
%% once every 10 minutes, check log sizes
handle_info(minute10, State) ->
    L = lists:map(
          fun(AL) ->
                  wrap(AL, State)
          end, State#state.alogs),

    Dir = State#state.dir,
    E = filename:join([Dir, "report.log"]),
    case file:read_file_info(E) of
        {ok, FI} when  State#state.log_wrap_size > 0, 
        FI#file_info.size > State#state.log_wrap_size, 
        State#state.copy_errlog == true ->
            gen_event:call(error_logger, yaws_log_file_h, wrap, infinity);
        {error,enoent} ->
            gen_event:call(error_logger, yaws_log_file_h, reopen, infinity);
        _ ->
            ok
    end,
    {noreply, State#state{alogs= L,
                          auth_log = wrap(State#state.auth_log, State)}};
handle_info({'EXIT', _, _}, State) ->
    {noreply, State}.



wrap_p(AL, State) when is_record(AL, alog) ->
    case file:read_file_info(AL#alog.filename) of
        {ok, FI} when FI#file_info.size > State#state.log_wrap_size,
        State#state.log_wrap_size > 0 ->
            true;
        {ok, _FI} ->
            false;
        {error, enoent} ->
            enoent;
        _ ->
            false
    end;
wrap_p(_,_) ->
    false.



wrap(AL, State) ->
    case wrap_p(AL, State) of
        true ->
            file:close(AL#alog.fd),
            Old = [AL#alog.filename, ".old"],
            file:delete(Old),
            file:rename(AL#alog.filename, Old),
            {ok, Fd2} = file:open(AL#alog.filename, [write, raw]),
            error_logger:info_msg("Wrap log ~p",[AL#alog.filename]), 
            AL#alog{fd = Fd2};
        false ->
            AL;
        enoent ->
            %% Logfile disappeared, 
            error_logger:format("Logfile ~p disappeared - we reopen it",
                                [AL#alog.filename]),
	    file:close(AL#alog.fd),
            {ok, Fd2} = file:open(AL#alog.filename, [write, raw]),
            AL#alog{fd = Fd2}
    end.





%%----------------------------------------------------------------------
%% Func: terminate/2
%% Purpose: Shutdown the server
%% Returns: any (ignored by gen_server)
%%----------------------------------------------------------------------
terminate(_Reason, _State) ->
    gen_event:delete_handler(error_logger, yaws_log_file_h, normal),
    ok.


%%----------------------------------------------------------------------
%% Func: code_change/3
%% Purpose: Handle upgrade
%% Returns: new State data
%%----------------------------------------------------------------------
code_change(_OldVsn, Data, _Extra) ->
    {ok, Data}.

%%%----------------------------------------------------------------------
%%% Internal functions
%%%----------------------------------------------------------------------


fmt_alog(Time, Ip, User, Req, Status,  Length, Referrer, UserAgent) ->
    [fmt_ip(Ip), " - ", User, [$\s], Time, [$\s, $"], no_ctl(Req), [$",$\s], 
     Status, [$\s], Length, [$\s,$"], Referrer, [$",$\s,$"], UserAgent, [$",$\n]].


%% Odd security advisory that only affects webservers where users are 
%% somehow allowed to upload files that later can be downloaded.

no_ctl([H|T]) when H < 32 ->
    no_ctl(T);
no_ctl([H|T]) ->
    [H|no_ctl(T)];
no_ctl([]) ->
    [].


fmt_ip(IP) when is_tuple(IP) ->
    inet_parse:ntoa(IP);
fmt_ip(undefined) ->
    "0.0.0.0";
fmt_ip(HostName) ->
    HostName.


fmtnow() ->
    {{Year, Month, Day}, {Hour, Min, Sec}} = 
        calendar:now_to_local_time(now()),
    ["[",fill_zero(Day,2),"/",yaws:month(Month),"/",integer_to_list(Year),":",
     fill_zero(Hour,2),":",fill_zero(Min,2),":",fill_zero(Sec,2)," ",zone(),"]"].
    

zone() ->
    Time = erlang:universaltime(),
    LocalTime = calendar:universal_time_to_local_time(Time),
    DiffSecs = calendar:datetime_to_gregorian_seconds(LocalTime) - 
        calendar:datetime_to_gregorian_seconds(Time),
    zone(DiffSecs div 3600, (DiffSecs rem 3600) div 60).



zone(Hr, Min) when Hr < 0; Min < 0 ->
    [$-, fill_zero(abs(Hr), 2), fill_zero(abs(Min), 2)];
zone(Hr, Min) when Hr >= 0, Min >= 0 ->
    [$+, fill_zero(abs(Hr), 2), fill_zero(abs(Min), 2)].

fill_zero(N, Width) ->
    left_fill(N, Width, $0).

left_fill(N, Width, Fill) when is_integer(N) ->
    left_fill(integer_to_list(N), Width, Fill);
left_fill(N, Width, _Fill) when length(N) >= Width ->
    N;
left_fill(N, Width, Fill) ->
    left_fill([Fill|N], Width, Fill).
