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