1%%----------------------------------------------------------------------
2%%% File    : yaws_log.erl
3%%% Author  : Claes Wikstrom <klacke@hyber.org>
4%%% Purpose :
5%%% Created : 26 Jan 2002 by Claes Wikstrom <klacke@hyber.org>
6%%%----------------------------------------------------------------------
7
8-module(yaws_log).
9-author('klacke@hyber.org').
10-include_lib("kernel/include/file.hrl").
11-include_lib("kernel/include/inet.hrl").
12
13
14-behaviour(gen_server).
15
16%% External exports
17-export([start_link/0, reopen_logs/0]).
18
19%% gen_server callbacks
20-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2,
21         code_change/3]).
22
23%% API
24-export([accesslog/6,
25         setup/2,
26         authlog/4,
27         rotate/1,
28         add_sconf/1,
29         del_sconf/1]).
30
31%% yaws_logger callbacks
32-export([
33         open_log/3,
34         close_log/3,
35         wrap_log/4,
36         write_log/4
37        ]).
38
39
40-include("../include/yaws.hrl").
41-include("../include/yaws_api.hrl").
42-include("yaws_debug.hrl").
43
44%% 1 meg log we wrap
45-define(WRAP_LOG_SIZE, 1000000).
46
47
48-record(state, {running,
49                dir,
50                now,
51                log_wrap_size = ?WRAP_LOG_SIZE,
52                copy_errlog,
53                resolve_hostnames = false}).
54
55
56%%%----------------------------------------------------------------------
57%%% API
58%%%----------------------------------------------------------------------
59start_link() ->
60    gen_server:start_link({local, yaws_log}, yaws_log, [], []).
61
62setup(GC, Sconfs) ->
63    gen_server:call(?MODULE, {setup, GC, Sconfs}, infinity).
64
65add_sconf(SConf) ->
66    gen_server:call(yaws_log, {soft_add_sc, SConf}, infinity).
67
68del_sconf(SConf) ->
69    gen_server:call(yaws_log, {soft_del_sc, SConf}, infinity).
70
71accesslog(SConf, Ip, Req, InH, OutH, Time) ->
72    catch yaws_logger:accesslog(SConf, Ip, Req, InH, OutH, Time).
73
74authlog(SConf, IP, Path, Item) ->
75    catch yaws_logger:authlog(SConf, IP, Path, Item).
76
77rotate(Res) ->
78    gen_server:cast(?MODULE, {yaws_hupped, Res}).
79
80%% Useful for embeddded yaws when we don't want yaws to
81%% automatically wrap the logs.
82reopen_logs() ->
83    {ok, _GC, SCs} = yaws_api:getconf(),
84    gen_server:call(?MODULE, {reopen, SCs}).
85
86
87%%%----------------------------------------------------------------------
88%%% Callback functions from yaws_logger
89%%%----------------------------------------------------------------------
90open_log(ServerName, Type, Dir) ->
91    FileName = case os:type() of
92                   {win32,_ } ->
93                       lists:map(fun($:) -> $.;
94                                    (C ) -> C
95                                 end, ServerName);
96                   _ ->
97                       ServerName
98               end,
99    A = filename:join([Dir, FileName ++ "." ++ atom_to_list(Type)]),
100    case file:open(A, [write, raw, append]) of
101        {ok, Fd} ->
102            {true, {Fd, A}};
103        _Err ->
104            error_logger:format("Cannot open ~p",[A]),
105            false
106    end.
107
108close_log(_ServerName, _Type, {Fd, _FileName}) ->
109    file:close(Fd).
110
111wrap_log(_ServerName, _Type, {Fd, FileName}, LogWrapSize) ->
112    case wrap_p(FileName, LogWrapSize) of
113        true ->
114            file:close(Fd),
115            Old = [FileName, ".old"],
116            file:delete(Old),
117            file:rename(FileName, Old),
118            {ok, Fd2} = file:open(FileName, [write, raw]),
119            {Fd2, FileName};
120        false ->
121            {Fd, FileName};
122        enoent ->
123            %% Logfile disappeared,
124            error_logger:format("Logfile ~p disappeared - we reopen it",
125                                [FileName]),
126            file:close(Fd),
127            {ok, Fd2} = file:open(FileName, [write, raw]),
128            {Fd2, FileName}
129    end.
130
131write_log(ServerName, Type, {Fd, _FileName}, Infos) ->
132    gen_server:cast(yaws_log, {ServerName, Type, Fd, Infos}).
133
134%%%----------------------------------------------------------------------
135%%% Callback functions from gen_server
136%%%----------------------------------------------------------------------
137
138%%----------------------------------------------------------------------
139%% Func: init/1
140%% Returns: {ok, State}          |
141%%          {ok, State, Timeout} |
142%%          ignore               |
143%%          {stop, Reason}
144%%----------------------------------------------------------------------
145init([]) ->
146    process_flag(trap_exit, true),
147    ets:new(yaws_log, [named_table, set, protected, {keypos, 2}]),
148    yaws_dynopts:start_error_logger(),
149    {ok, #state{running = false, now = fmtnow()}}.
150
151%%----------------------------------------------------------------------
152%% Func: handle_call/3
153%% Returns: {reply, Reply, State}          |
154%%          {reply, Reply, State, Timeout} |
155%%          {noreply, State}               |
156%%          {noreply, State, Timeout}      |
157%%          {stop, Reason, Reply, State}   | (terminate/2 is called)
158%%          {stop, Reason, State}            (terminate/2 is called)
159%%----------------------------------------------------------------------
160handle_call({setup, GC, Sconfs}, _From, State)
161  when State#state.running == false ->
162    Dir = GC#gconf.logdir,
163    ?Debug("setup ~s~n", [Dir]),
164    ElogFile = filename:join([Dir, "report.log"]),
165    Copy = if ?gc_has_copy_errlog(GC) ->
166                   gen_event:add_handler(error_logger, yaws_log_file_h,
167                                         ElogFile),
168                   true;
169              true ->
170                   false
171           end,
172    SCs = lists:flatten(Sconfs),
173    lists:foreach(fun(SC) ->
174                          yaws_logger:open_log(SC, auth, Dir),
175                          yaws_logger:open_log(SC, access, Dir)
176                  end, SCs),
177
178    S2 = State#state{running = true,
179                     dir  = Dir,
180                     now = fmtnow(),
181                     log_wrap_size = GC#gconf.log_wrap_size,
182                     copy_errlog = Copy,
183                     resolve_hostnames = ?gc_log_has_resolve_hostname(GC)},
184
185    yaws:ticker(3000, secs3),
186
187    if is_integer(GC#gconf.log_wrap_size) ->
188            yaws:ticker(10 * 60 * 1000, minute10);
189       true ->
190            ok
191    end,
192
193    {reply, ok, S2};
194
195
196
197%% We can't ever change logdir, we can however
198%% change logging opts for various servers
199
200handle_call({setup, GC, Sconfs}, _From, State)
201  when State#state.running == true ->
202
203    Dir = State#state.dir,
204    ElogFile = filename:join([Dir, "report.log"]),
205    Copy = if ?gc_has_copy_errlog(GC), State#state.copy_errlog == false->
206                   gen_event:add_handler(error_logger, yaws_log_file_h,
207                                         ElogFile),
208                   true;
209              ?gc_has_copy_errlog(GC) ->
210                   true;
211              State#state.copy_errlog == true ->
212                   gen_event:delete_handler(error_logger, yaws_log_file_h,
213                                            normal),
214                   false;
215              true ->
216                   false
217           end,
218
219    %% close all files
220    yaws_logger:close_logs(),
221
222    %% reopen logfiles
223    SCs = lists:flatten(Sconfs),
224    lists:foreach(fun(SC) ->
225                          yaws_logger:open_log(SC, auth, Dir),
226                          yaws_logger:open_log(SC, access, Dir)
227                  end, SCs),
228
229    S2 = State#state{running = true,
230                     dir  = Dir,
231                     now = fmtnow(),
232                     log_wrap_size = GC#gconf.log_wrap_size,
233                     copy_errlog = Copy,
234                     resolve_hostnames = ?gc_log_has_resolve_hostname(GC)},
235
236    if
237        not is_integer(State#state.log_wrap_size),
238        is_integer(GC#gconf.log_wrap_size) ->
239            yaws:ticker(10 * 60 * 1000, minute10);
240       true ->
241            ok
242    end,
243    {reply, ok, S2};
244
245
246%% a virt server has been added
247handle_call({soft_add_sc, SC}, _From, State) ->
248    yaws_logger:open_log(SC, auth, State#state.dir),
249    yaws_logger:open_log(SC, access, State#state.dir),
250    {reply, ok, State};
251
252%% a virt server has been deleted
253handle_call({soft_del_sc, SC}, _From, State) ->
254    yaws_logger:close_log(SC, auth),
255    yaws_logger:close_log(SC, access),
256    {reply, ok, State};
257
258
259handle_call(state, _From, State) ->
260    {reply, State, State};
261
262handle_call({reopen, Sconfs}, _From, State) ->
263    Dir = State#state.dir,
264    %% close all files
265    yaws_logger:close_logs(),
266
267    %% reopen logfiles
268    SCs = lists:flatten(Sconfs),
269    lists:foreach(fun(SC) ->
270                          yaws_logger:open_log(SC, auth, Dir),
271                          yaws_logger:open_log(SC, access, Dir)
272                  end, SCs),
273    {reply, ok, State}.
274
275%%----------------------------------------------------------------------
276%% Func: handle_cast/2
277%% Returns: {noreply, State}          |
278%%          {noreply, State, Timeout} |
279%%          {stop, Reason, State}            (terminate/2 is called)
280%%----------------------------------------------------------------------
281handle_cast({_ServerName, access, Fd, {Ip, Req, InH, OutH, _}}, State) ->
282    case State#state.running of
283        true ->
284            Status = case OutH#outh.status of
285                         undefined -> "-";
286                         S         -> integer_to_list(S)
287                     end,
288            Len = case Req#http_request.method of
289                      'HEAD' ->
290                          "-";
291                      _ ->
292                          case OutH#outh.contlen of
293                              undefined ->
294                                  case OutH#outh.act_contlen of
295                                      undefined -> "-";
296                                      L         -> integer_to_list(L)
297                                  end;
298                              L ->
299                                  integer_to_list(L)
300                          end
301                  end,
302            Ver = case Req#http_request.version of
303                      {1,0} -> "HTTP/1.0";
304                      {1,1} -> "HTTP/1.1";
305                      {0,9} -> "HTTP/0.9";
306                      _     -> "HTTP/X.X"
307                  end,
308
309            Path      = yaws_server:safe_path(Req#http_request.path),
310            Meth      = yaws:to_list(Req#http_request.method),
311            Referer   = optional_header(InH#headers.referer),
312            UserAgent = optional_header(InH#headers.user_agent),
313            User      = case InH#headers.authorization of
314                            {U, _P, _OStr} -> U;
315                            _              -> "-"
316                        end,
317
318            Msg = fmt_access_log(State#state.now, fmt_ip(Ip, State), User,
319                                 [Meth, $\s, Path, $\s, Ver],
320                                 Status,  Len, Referer, UserAgent),
321            file:write(Fd, safe_log_data(Msg)),
322            {noreply, State};
323        false ->
324            {noreply, State}
325    end;
326
327handle_cast({ServerName, auth, Fd, {Ip, Path, Item}}, State) ->
328    case State#state.running of
329        true ->
330            Host = fmt_ip(Ip, State),
331            Msg  = [Host, " ", State#state.now, " ", ServerName, " " ,
332                    "\"", Path,"\"",
333                   case Item of
334                       {ok, User}       -> [" OK user=", User];
335                       403              -> [" 403"];
336                       {401, Realm}     -> [" 401 realm=", Realm];
337                       {401, User, PWD} -> [" 401 user=", User, " badpwd=",PWD];
338                       _                -> ""
339                   end, "\n"],
340            file:write(Fd, safe_log_data(Msg)),
341            {noreply, State};
342        false ->
343            {noreply,State}
344    end;
345
346handle_cast({yaws_hupped, _}, State) ->
347    handle_info(minute10, State).
348
349
350%%----------------------------------------------------------------------
351%% Func: handle_info/2
352%% Returns: {noreply, State}          |
353%%          {noreply, State, Timeout} |
354%%          {stop, Reason, State}            (terminate/2 is called)
355%%----------------------------------------------------------------------
356handle_info(secs3, State) ->
357    {noreply, State#state{now = fmtnow()}};
358
359%% once every 10 minutes, check log sizes
360handle_info(minute10, State) ->
361    yaws_logger:rotate(State#state.log_wrap_size),
362
363    case gen_event:call(error_logger, yaws_log_file_h, size, infinity) of
364        {ok, Size} when  State#state.log_wrap_size > 0,
365                       Size > State#state.log_wrap_size ->
366            gen_event:call(error_logger, yaws_log_file_h, wrap, infinity);
367        {error, enoent} ->
368            gen_event:call(error_logger, yaws_log_file_h, reopen, infinity);
369        _ ->
370            ok
371    end,
372    {noreply, State};
373handle_info({'EXIT', _, _}, State) ->
374    {noreply, State}.
375
376
377
378wrap_p(Filename, LogWrapSize) ->
379    case file:read_file_info(Filename) of
380        {ok, FI} when FI#file_info.size > LogWrapSize, LogWrapSize > 0 ->
381            true;
382        {ok, _FI} ->
383            false;
384        {error, enoent} ->
385            enoent;
386        _ ->
387            false
388    end.
389
390
391
392%%----------------------------------------------------------------------
393%% Func: terminate/2
394%% Purpose: Shutdown the server
395%% Returns: any (ignored by gen_server)
396%%----------------------------------------------------------------------
397terminate(_Reason, _State) ->
398    gen_event:delete_handler(error_logger, yaws_log_file_h, normal),
399    yaws_logger:close_logs(),
400    ok.
401
402
403%%----------------------------------------------------------------------
404%% Func: code_change/3
405%% Purpose: Handle upgrade
406%% Returns: new State data
407%%----------------------------------------------------------------------
408code_change(_OldVsn, Data, _Extra) ->
409    {ok, Data}.
410
411%%%----------------------------------------------------------------------
412%%% Internal functions
413%%%----------------------------------------------------------------------
414optional_header(Item) ->
415    case Item of
416        undefined -> "-";
417        Item -> Item
418    end.
419
420fmt_access_log(Time, Host, User, Req, Status,  Length, Referrer, UserAgent) ->
421    [Host, " - ", User, [$\s], Time, [$\s, $\"], no_ctl(Req), [$\",$\s],
422     Status, [$\s], Length, [$\s,$"], Referrer, [$",$\s,$"], UserAgent,
423     [$",$\n]].
424
425
426%% Odd security advisory that only affects webservers where users are
427%% somehow allowed to upload files that later can be downloaded.
428
429no_ctl([H|T]) when H < 32 ->
430    no_ctl(T);
431no_ctl([H|T]) ->
432    [H|no_ctl(T)];
433no_ctl([]) ->
434    [].
435
436
437fmt_ip(IP, State) when is_tuple(IP) ->
438    case State#state.resolve_hostnames of
439        true ->
440            case catch inet:gethostbyaddr(IP) of
441                {ok, HE} ->
442                    HE#hostent.h_name;
443                _ ->
444                    case catch inet_parse:ntoa(IP) of
445                        {'EXIT', _} -> "unknownip";
446                        Addr        -> Addr
447                    end
448            end;
449        false ->
450            case catch inet_parse:ntoa(IP) of
451                {'EXIT', _} -> "unknownip";
452                Addr        -> Addr
453            end
454    end;
455fmt_ip(unknown, _) ->
456    "unknownip";
457fmt_ip(undefined, _) ->
458    "0.0.0.0";
459fmt_ip(HostName, _) ->
460    HostName.
461
462
463fmtnow() ->
464    {{Year, Month, Day}, {Hour, Min, Sec}} =
465        calendar:now_to_local_time(yaws:get_time_tuple()),
466    ["[",fill_zero(Day,2),"/",yaws:month(Month),"/",integer_to_list(Year),":",
467     fill_zero(Hour,2),":",fill_zero(Min,2),":",
468     fill_zero(Sec,2)," ",zone(),"]"].
469
470
471zone() ->
472    Time = erlang:universaltime(),
473    LocalTime = calendar:universal_time_to_local_time(Time),
474    DiffSecs = calendar:datetime_to_gregorian_seconds(LocalTime) -
475        calendar:datetime_to_gregorian_seconds(Time),
476    zone(DiffSecs div 3600, (DiffSecs rem 3600) div 60).
477
478
479
480zone(Hr, Min) when Hr < 0; Min < 0 ->
481    [$-, fill_zero(abs(Hr), 2), fill_zero(abs(Min), 2)];
482zone(Hr, Min) when Hr >= 0, Min >= 0 ->
483    [$+, fill_zero(abs(Hr), 2), fill_zero(abs(Min), 2)].
484
485fill_zero(N, Width) ->
486    left_fill(N, Width, $0).
487
488left_fill(N, Width, Fill) when is_integer(N) ->
489    left_fill(integer_to_list(N), Width, Fill);
490left_fill(N, Width, _Fill) when length(N) >= Width ->
491    N;
492left_fill(N, Width, Fill) ->
493    left_fill([Fill|N], Width, Fill).
494
495safe_log_data(Elements) ->
496    [ yaws:to_string(E) || E <- Elements ].
497
498
499
500