Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Fetching contributors…

Cannot retrieve contributors at this time

497 lines (410 sloc) 16.134 kb
%%----------------------------------------------------------------------
%%% 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").
-include_lib("kernel/include/inet.hrl").
-behaviour(gen_server).
%% External exports
-export([start_link/0, reopen_logs/0]).
%% gen_server callbacks
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2,
code_change/3]).
%% API
-export([accesslog/6,
setup/2,
authlog/4,
rotate/1,
add_sconf/1,
del_sconf/1]).
%% yaws_logger callbacks
-export([
open_log/3,
close_log/3,
wrap_log/4,
write_log/4
]).
-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,
log_wrap_size = ?WRAP_LOG_SIZE,
copy_errlog,
resolve_hostnames = false}).
%%%----------------------------------------------------------------------
%%% API
%%%----------------------------------------------------------------------
start_link() ->
gen_server:start_link({local, yaws_log}, yaws_log, [], []).
setup(GC, Sconfs) ->
gen_server:call(?MODULE, {setup, GC, Sconfs}, infinity).
add_sconf(SConf) ->
gen_server:call(yaws_log, {soft_add_sc, SConf}, infinity).
del_sconf(SConf) ->
gen_server:call(yaws_log, {soft_del_sc, SConf}, infinity).
accesslog(SConf, Ip, Req, InH, OutH, Time) ->
catch yaws_logger:accesslog(SConf, Ip, Req, InH, OutH, Time).
authlog(SConf, IP, Path, Item) ->
catch yaws_logger:authlog(SConf, IP, Path, Item).
rotate(Res) ->
gen_server:cast(?MODULE, {yaws_hupped, Res}).
%% Useful for embeddded yaws when we don't want yaws to
%% automatically wrap the logs.
reopen_logs() ->
{ok, _GC, SCs} = yaws_api:getconf(),
gen_server:call(?MODULE, {reopen, SCs}).
%%%----------------------------------------------------------------------
%%% Callback functions from yaws_logger
%%%----------------------------------------------------------------------
open_log(ServerName, Type, Dir) ->
FileName = case os:type() of
{win32,_ } ->
lists:map(fun($:) -> $.;
(C ) -> C
end, ServerName);
_ ->
ServerName
end,
A = filename:join([Dir, FileName ++ "." ++ atom_to_list(Type)]),
case file:open(A, [write, raw, append]) of
{ok, Fd} ->
{true, {Fd, A}};
_Err ->
error_logger:format("Cannot open ~p",[A]),
false
end.
close_log(_ServerName, _Type, {Fd, _FileName}) ->
file:close(Fd).
wrap_log(_ServerName, _Type, {Fd, FileName}, LogWrapSize) ->
case wrap_p(FileName, LogWrapSize) of
true ->
file:close(Fd),
Old = [FileName, ".old"],
file:delete(Old),
file:rename(FileName, Old),
{ok, Fd2} = file:open(FileName, [write, raw]),
{Fd2, FileName};
false ->
{Fd, FileName};
enoent ->
%% Logfile disappeared,
error_logger:format("Logfile ~p disappeared - we reopen it",
[FileName]),
file:close(Fd),
{ok, Fd2} = file:open(FileName, [write, raw]),
{Fd2, FileName}
end.
write_log(ServerName, Type, {Fd, _FileName}, Infos) ->
gen_server:cast(yaws_log, {ServerName, Type, Fd, Infos}).
%%%----------------------------------------------------------------------
%%% Callback functions from gen_server
%%%----------------------------------------------------------------------
%%----------------------------------------------------------------------
%% Func: init/1
%% Returns: {ok, State} |
%% {ok, State, Timeout} |
%% ignore |
%% {stop, Reason}
%%----------------------------------------------------------------------
init([]) ->
process_flag(trap_exit, true),
ets:new(yaws_log, [named_table, set, protected, {keypos, 2}]),
{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({setup, GC, Sconfs}, _From, State)
when State#state.running == false ->
Dir = GC#gconf.logdir,
?Debug("setup ~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),
lists:foreach(fun(SC) ->
yaws_logger:open_log(SC, auth, Dir),
yaws_logger:open_log(SC, access, Dir)
end, SCs),
S2 = State#state{running = true,
dir = Dir,
now = fmtnow(),
log_wrap_size = GC#gconf.log_wrap_size,
copy_errlog = Copy,
resolve_hostnames = ?gc_log_has_resolve_hostname(GC)},
yaws:ticker(3000, secs3),
if is_integer(GC#gconf.log_wrap_size) ->
yaws:ticker(10 * 60 * 1000, minute10);
true ->
ok
end,
{reply, ok, S2};
%% We can't ever change logdir, we can however
%% change logging opts for various servers
handle_call({setup, 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
yaws_logger:close_logs(),
%% reopen logfiles
SCs = lists:flatten(Sconfs),
lists:foreach(fun(SC) ->
yaws_logger:open_log(SC, auth, Dir),
yaws_logger:open_log(SC, access, Dir)
end, SCs),
S2 = State#state{running = true,
dir = Dir,
now = fmtnow(),
log_wrap_size = GC#gconf.log_wrap_size,
copy_errlog = Copy,
resolve_hostnames = ?gc_log_has_resolve_hostname(GC)},
if
not is_integer(State#state.log_wrap_size),
is_integer(GC#gconf.log_wrap_size) ->
yaws:ticker(10 * 60 * 1000, minute10);
true ->
ok
end,
{reply, ok, S2};
%% a virt server has been added
handle_call({soft_add_sc, SC}, _From, State) ->
yaws_logger:open_log(SC, auth, State#state.dir),
yaws_logger:open_log(SC, access, State#state.dir),
{reply, ok, State};
%% a virt server has been deleted
handle_call({soft_del_sc, SC}, _From, State) ->
yaws_logger:close_log(SC, auth),
yaws_logger:close_log(SC, access),
{reply, ok, State};
handle_call(state, _From, State) ->
{reply, State, State};
handle_call({reopen, Sconfs}, _From, State) ->
Dir = State#state.dir,
%% close all files
yaws_logger:close_logs(),
%% reopen logfiles
SCs = lists:flatten(Sconfs),
lists:foreach(fun(SC) ->
yaws_logger:open_log(SC, auth, Dir),
yaws_logger:open_log(SC, access, Dir)
end, SCs),
{reply, ok, State}.
%%----------------------------------------------------------------------
%% Func: handle_cast/2
%% Returns: {noreply, State} |
%% {noreply, State, Timeout} |
%% {stop, Reason, State} (terminate/2 is called)
%%----------------------------------------------------------------------
handle_cast({_ServerName, access, Fd, {Ip, Req, InH, OutH, _}}, State) ->
case State#state.running of
true ->
Status = case OutH#outh.status of
undefined -> "-";
S -> integer_to_list(S)
end,
Len = case Req#http_request.method of
'HEAD' ->
"-";
_ ->
case OutH#outh.contlen of
undefined ->
case OutH#outh.act_contlen of
undefined -> "-";
L -> integer_to_list(L)
end;
L ->
integer_to_list(L)
end
end,
Ver = case Req#http_request.version of
{1,0} -> "HTTP/1.0";
{1,1} -> "HTTP/1.1";
{0,9} -> "HTTP/0.9"
end,
Path = yaws_server:safe_decode_path(Req#http_request.path),
Meth = yaws:to_list(Req#http_request.method),
Referer = optional_header(InH#headers.referer),
UserAgent = optional_header(InH#headers.user_agent),
User = case InH#headers.authorization of
{U, _P, _OStr} -> U;
_ -> "-"
end,
Msg = fmt_access_log(State#state.now, fmt_ip(Ip, State), User,
[Meth, $\s, Path, $\s, Ver],
Status, Len, Referer, UserAgent),
file:write(Fd, safe_log_data(Msg)),
{noreply, State};
false ->
{noreply, State}
end;
handle_cast({ServerName, auth, Fd, {Ip, Path, Item}}, State) ->
case State#state.running of
true ->
Host = fmt_ip(Ip, State),
Msg = [Host, " ", 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(Fd, safe_log_data(Msg)),
{noreply, State};
false ->
{noreply,State}
end;
handle_cast({yaws_hupped, _}, State) ->
handle_info(minute10, 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()}};
%% once every 10 minutes, check log sizes
handle_info(minute10, State) ->
yaws_logger:rotate(State#state.log_wrap_size),
case gen_event:call(error_logger, yaws_log_file_h, size, infinity) of
{ok, Size} when State#state.log_wrap_size > 0,
Size > State#state.log_wrap_size ->
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};
handle_info({'EXIT', _, _}, State) ->
{noreply, State}.
wrap_p(Filename, LogWrapSize) ->
case file:read_file_info(Filename) of
{ok, FI} when FI#file_info.size > LogWrapSize, LogWrapSize > 0 ->
true;
{ok, _FI} ->
false;
{error, enoent} ->
enoent;
_ ->
false
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),
yaws_logger:close_logs(),
ok.
%%----------------------------------------------------------------------
%% Func: code_change/3
%% Purpose: Handle upgrade
%% Returns: new State data
%%----------------------------------------------------------------------
code_change(_OldVsn, Data, _Extra) ->
{ok, Data}.
%%%----------------------------------------------------------------------
%%% Internal functions
%%%----------------------------------------------------------------------
optional_header(Item) ->
case Item of
undefined -> "-";
Item -> Item
end.
fmt_access_log(Time, Host, User, Req, Status, Length, Referrer, UserAgent) ->
[Host, " - ", 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, State) when is_tuple(IP) ->
case State#state.resolve_hostnames of
true ->
case catch inet:gethostbyaddr(IP) of
{ok, HE} ->
HE#hostent.h_name;
_ ->
case catch inet_parse:ntoa(IP) of
{'EXIT', _} -> "unknownip";
Addr -> Addr
end
end;
false ->
case catch inet_parse:ntoa(IP) of
{'EXIT', _} -> "unknownip";
Addr -> Addr
end
end;
fmt_ip(unknown, _) ->
"unknownip";
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).
safe_log_data(Elements) ->
[ yaws:to_string(E) || E <- Elements ].
Jump to Line
Something went wrong with that request. Please try again.