1%%
2%% %CopyrightBegin%
3%%
4%% Copyright Ericsson AB 1997-2018. All Rights Reserved.
5%%
6%% Licensed under the Apache License, Version 2.0 (the "License");
7%% you may not use this file except in compliance with the License.
8%% You may obtain a copy of the License at
9%%
10%%     http://www.apache.org/licenses/LICENSE-2.0
11%%
12%% Unless required by applicable law or agreed to in writing, software
13%% distributed under the License is distributed on an "AS IS" BASIS,
14%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15%% See the License for the specific language governing permissions and
16%% limitations under the License.
17%%
18%% %CopyrightEnd%
19%%
20%%
21-module(mod_disk_log).
22
23%% Application internal API
24-export([error_log/2, report_error/2, security_log/2]).
25
26%% Callback API
27-export([do/1, load/2, store/2, remove/1]).
28
29-define(VMODULE,"DISK_LOG").
30
31-include("httpd.hrl").
32-include("httpd_internal.hrl").
33
34%%%=========================================================================
35%%%  API
36%%%=========================================================================
37
38%% security_log
39security_log(#mod{config_db = ConfigDb} = Info, Event) ->
40    Format = get_log_format(ConfigDb),
41    Date = httpd_util:custom_date(),
42    case httpd_log:security_entry(security_disk_log, no_security_log,
43				  Info, Date, Event) of
44	no_security_log ->
45	    ok;
46	{Log, Entry} ->
47	    write(Log, Entry, Format)
48    end.
49
50report_error(ConfigDB, Error) ->
51    Format = get_log_format(ConfigDB),
52    Date = httpd_util:custom_date(),
53    case httpd_log:error_report_entry(error_disk_log, no_error_log, ConfigDB,
54				      Date, Error) of
55	no_error_log ->
56	    ok;
57	{Log, Entry} ->
58	    write(Log, Entry, Format)
59    end.
60
61error_log(Info, Reason) ->
62    Date = httpd_util:custom_date(),
63    error_log(Info, Date, Reason).
64
65error_log(#mod{config_db = ConfigDB} = Info, Date, Reason) ->
66    Format = get_log_format(ConfigDB),
67     case httpd_log:error_entry(error_disk_log, no_error_log,
68			       Info, Date, Reason) of
69	no_error_log ->
70	    ok;
71	{Log, Entry} ->
72	     write(Log, Entry, Format)
73    end.
74
75%%%=========================================================================
76%%%  CALLBACK API
77%%%=========================================================================
78%%--------------------------------------------------------------------------
79%% do(ModData) -> {proceed, OldData} | {proceed, NewData} | {break, NewData}
80%%                | done
81%%     ModData = #mod{}
82%%
83%% Description:  See httpd(3) ESWAPI CALLBACK FUNCTIONS
84%%-------------------------------------------------------------------------
85do(Info) ->
86    AuthUser  = auth_user(Info#mod.data),
87    Date      = httpd_util:custom_date(),
88    log_internal_info(Info,Date,Info#mod.data),
89    LogFormat = get_log_format(Info#mod.config_db),
90    case proplists:get_value(status, Info#mod.data) of
91	%% A status code has been generated!
92	{StatusCode, _PhraseArgs, Reason} ->
93	    transfer_log(Info, "-", AuthUser, Date, StatusCode, 0,
94			 LogFormat),
95	    if
96		StatusCode >= 400 ->
97		    error_log(Info, Date, Reason);
98		true ->
99		    not_an_error
100	    end,
101	    {proceed,Info#mod.data};
102	%% No status code has been generated!
103	undefined ->
104	    case proplists:get_value(response, Info#mod.data) of
105		{already_sent,StatusCode,Size} ->
106		    transfer_log(Info, "-", AuthUser, Date, StatusCode,
107				 Size, LogFormat),
108		    {proceed,Info#mod.data};
109
110		{response, Head, _Body} ->
111		    Size = proplists:get_value(content_length, Head, 0),
112		    Code = proplists:get_value(code, Head, 200),
113		    transfer_log(Info, "-", AuthUser, Date, Code,
114				 Size, LogFormat),
115		    {proceed,Info#mod.data};
116
117		{_StatusCode, Response} ->
118		    transfer_log(Info, "-", AuthUser, Date, 200,
119				 httpd_util:flatlength(Response), LogFormat),
120		    {proceed,Info#mod.data};
121		undefined ->
122		    transfer_log(Info, "-", AuthUser, Date, 200,
123				 0, LogFormat),
124		    {proceed,Info#mod.data}
125	    end
126    end.
127
128%%--------------------------------------------------------------------------
129%% load(Line, Context) ->  eof | ok | {ok, NewContext} |
130%%                     {ok, NewContext, Directive} |
131%%                     {ok, NewContext, DirectiveList} | {error, Reason}
132%% Line = string()
133%% Context = NewContext = DirectiveList = [Directive]
134%% Directive = {DirectiveKey , DirectiveValue}
135%% DirectiveKey = DirectiveValue = term()
136%% Reason = term()
137%%
138%% Description: See httpd(3) ESWAPI CALLBACK FUNCTIONS
139%%-------------------------------------------------------------------------
140load("TransferDiskLogSize " ++ TransferDiskLogSize, []) ->
141    try re:split(TransferDiskLogSize, " ",  [{return, list}]) of
142	[MaxBytes, MaxFiles] ->
143	    case make_integer(MaxBytes) of
144		{ok,MaxBytesInteger} ->
145		    case make_integer(MaxFiles) of
146			{ok,MaxFilesInteger} ->
147			    {ok,[],{transfer_disk_log_size,
148				    {MaxBytesInteger,MaxFilesInteger}}};
149			{error,_} ->
150			    {error,
151			     ?NICE(string:strip(TransferDiskLogSize)++
152				   " is an invalid TransferDiskLogSize")}
153		    end;
154		_ ->
155		    {error,?NICE(string:strip(TransferDiskLogSize)++
156				     " is an invalid TransferDiskLogSize")}
157	    end
158    catch _:_ ->
159	    {error,?NICE(string:strip(TransferDiskLogSize) ++
160			     " is an invalid TransferDiskLogSize")}
161    end;
162load("TransferDiskLog " ++ TransferDiskLog,[]) ->
163    {ok,[],{transfer_disk_log,string:strip(TransferDiskLog)}};
164
165load("ErrorDiskLogSize " ++  ErrorDiskLogSize, []) ->
166    try re:split(ErrorDiskLogSize," ", [{return, list}]) of
167	[MaxBytes,MaxFiles] ->
168	    case make_integer(MaxBytes) of
169		{ok,MaxBytesInteger} ->
170		    case make_integer(MaxFiles) of
171			{ok,MaxFilesInteger} ->
172			    {ok,[],{error_disk_log_size,
173				    {MaxBytesInteger,MaxFilesInteger}}};
174			{error,_} ->
175			    {error,?NICE(string:strip(ErrorDiskLogSize)++
176					 " is an invalid ErrorDiskLogSize")}
177		    end;
178		{error,_} ->
179		    {error,?NICE(string:strip(ErrorDiskLogSize)++
180				 " is an invalid ErrorDiskLogSize")}
181	    end
182    catch _:_ ->
183	    {error,?NICE(string:strip(ErrorDiskLogSize) ++
184			     " is an invalid TransferDiskLogSize")}
185    end;
186load("ErrorDiskLog " ++ ErrorDiskLog, []) ->
187    {ok, [], {error_disk_log, string:strip(ErrorDiskLog)}};
188
189load("SecurityDiskLogSize " ++ SecurityDiskLogSize, []) ->
190    try re:split(SecurityDiskLogSize, " ", [{return, list}]) of
191	[MaxBytes, MaxFiles] ->
192	    case make_integer(MaxBytes) of
193		{ok, MaxBytesInteger} ->
194		    case make_integer(MaxFiles) of
195			{ok, MaxFilesInteger} ->
196			    {ok, [], {security_disk_log_size,
197				      {MaxBytesInteger, MaxFilesInteger}}};
198			{error,_} ->
199			    {error,
200			     ?NICE(string:strip(SecurityDiskLogSize) ++
201				   " is an invalid SecurityDiskLogSize")}
202		    end;
203		{error, _} ->
204		    {error, ?NICE(string:strip(SecurityDiskLogSize) ++
205				  " is an invalid SecurityDiskLogSize")}
206	    end
207    catch _:_ ->
208	    {error,?NICE(string:strip(SecurityDiskLogSize) ++
209			     " is an invalid SecurityDiskLogSize")}
210    end;
211load("SecurityDiskLog " ++ SecurityDiskLog, []) ->
212    {ok, [], {security_disk_log, string:strip(SecurityDiskLog)}};
213
214load("DiskLogFormat " ++ Format, []) ->
215    case string:strip(Format) of
216	"internal" ->
217	    {ok, [], {disk_log_format,internal}};
218	"external" ->
219	    {ok, [], {disk_log_format,external}};
220	_Default ->
221	    {ok, [], {disk_log_format,external}}
222    end.
223
224%%--------------------------------------------------------------------------
225%% store(Directive, DirectiveList) -> {ok, NewDirective} |
226%%                                    {ok, [NewDirective]} |
227%%                                    {error, Reason}
228%% Directive = {DirectiveKey , DirectiveValue}
229%% DirectiveKey = DirectiveValue = term()
230%% Reason = term()
231%%
232%% Description: See httpd(3) ESWAPI CALLBACK FUNCTIONS
233%%-------------------------------------------------------------------------
234store({transfer_disk_log,TransferDiskLog}, ConfigList)
235  when is_list(TransferDiskLog) ->
236    case create_disk_log(TransferDiskLog,
237			 transfer_disk_log_size, ConfigList) of
238	{ok,TransferDB} ->
239	    {ok,{transfer_disk_log,TransferDB}};
240	{error,Reason} ->
241	    {error,Reason}
242    end;
243store({transfer_disk_log,TransferLog}, _) ->
244    {error, {wrong_type, {transfer_disk_log, TransferLog}}};
245store({security_disk_log,SecurityDiskLog},ConfigList)
246  when is_list(SecurityDiskLog) ->
247    case create_disk_log(SecurityDiskLog,
248			 security_disk_log_size, ConfigList) of
249	{ok,SecurityDB} ->
250	    {ok,{security_disk_log,SecurityDB}};
251	{error,Reason} ->
252	    {error,Reason}
253    end;
254store({security_disk_log, SecurityLog}, _) ->
255    {error, {wrong_type, {security_disk_log, SecurityLog}}};
256
257store({error_disk_log,ErrorDiskLog},ConfigList) when is_list(ErrorDiskLog) ->
258    case create_disk_log(ErrorDiskLog, error_disk_log_size, ConfigList) of
259	{ok,ErrorDB} ->
260	    {ok,{error_disk_log,ErrorDB}};
261	{error,Reason} ->
262	    {error,Reason}
263    end;
264store({error_disk_log,ErrorLog}, _) ->
265    {error, {wrong_type, {error_disk_log, ErrorLog}}};
266store({transfer_disk_log_size, {ByteInt, FileInt}} = Conf, _)
267  when is_integer(ByteInt), is_integer(FileInt)->
268    {ok, Conf};
269store({transfer_disk_log_size, Value}, _) ->
270    {error, {wrong_type, {transfer_disk_log_size, Value}}};
271store({error_disk_log_size, {ByteInt, FileInt}} = Conf, _)
272  when is_integer(ByteInt), is_integer(FileInt)->
273    {ok, Conf};
274store({error_disk_log_size, Value}, _) ->
275    {error, {wrong_type, {error_disk_log_size, Value}}};
276store({security_disk_log_size, {ByteInt, FileInt}} = Conf, _)
277  when is_integer(ByteInt), is_integer(FileInt)->
278    {ok, Conf};
279store({security_disk_log_size, Value}, _) ->
280    {error, {wrong_type, {security_disk_log_size, Value}}};
281store({disk_log_format, Value} = Conf, _) when Value == internal;
282					Value == external ->
283    {ok, Conf};
284store({disk_log_format, Value}, _) ->
285    {error, {wrong_type, {disk_log_format, Value}}}.
286
287%%--------------------------------------------------------------------------
288%% remove(ConfigDb) -> _
289%%
290%% Description: See httpd(3) ESWAPI CALLBACK FUNCTIONS
291%%-------------------------------------------------------------------------
292remove(ConfigDB) ->
293    lists:foreach(fun([DiskLog]) -> close(DiskLog) end,
294		  ets:match(ConfigDB,{transfer_disk_log,'$1'})),
295    lists:foreach(fun([DiskLog]) -> close(DiskLog) end,
296		  ets:match(ConfigDB,{error_disk_log,'$1'})),
297    lists:foreach(fun([DiskLog]) -> close(DiskLog) end,
298		  ets:match(ConfigDB,{security_disk_log,'$1'})),
299    ok.
300
301%%%========================================================================
302%%% Internal functions
303%%%========================================================================
304
305%% transfer_log
306transfer_log(Info, RFC931, AuthUser, Date, StatusCode, Bytes, Format) ->
307    case httpd_log:access_entry(transfer_disk_log, no_transfer_log,
308				Info, RFC931, AuthUser, Date, StatusCode,
309				Bytes) of
310	no_transfer_log ->
311	    ok;
312	{Log, Entry} ->
313    	    write(Log, Entry, Format)
314    end.
315
316
317get_log_format(ConfigDB)->
318    httpd_util:lookup(ConfigDB,disk_log_format,external).
319
320%%----------------------------------------------------------------------
321%% Open or creates the disklogs
322%%----------------------------------------------------------------------
323log_size(ConfigList, Tag) ->
324    proplists:get_value(Tag, ConfigList, {500*1024,8}).
325
326create_disk_log(LogFile, SizeTag, ConfigList) ->
327    Filename = string:strip(LogFile),
328    {MaxBytes, MaxFiles} = log_size(ConfigList, SizeTag),
329    case filename:pathtype(Filename) of
330	absolute ->
331	    create_disk_log(Filename, MaxBytes, MaxFiles, ConfigList);
332	volumerelative ->
333	    create_disk_log(Filename, MaxBytes, MaxFiles, ConfigList);
334	relative ->
335	    case proplists:get_value(server_root,ConfigList) of
336		undefined ->
337		    {error,
338		     ?NICE(Filename++
339			   " is an invalid ErrorLog beacuse ServerRoot "
340			   "is not defined")};
341		ServerRoot ->
342		    AbsoluteFilename = filename:join(ServerRoot,Filename),
343		    create_disk_log(AbsoluteFilename, MaxBytes, MaxFiles,
344				     ConfigList)
345	    end
346    end.
347
348create_disk_log(Filename, MaxBytes, MaxFiles, ConfigList) ->
349    Format = proplists:get_value(disk_log_format, ConfigList, external),
350    open(Filename, MaxBytes, MaxFiles, Format).
351
352
353%%----------------------------------------------------------------------
354%% Function:    open/4
355%% Description: Open a disk log file.
356%% Control which format the disk log will be in. The external file
357%% format is used as default since that format was used by older
358%% implementations of inets.
359%%
360%% When the internal disk log format is used, we will do some extra
361%% controls. If the files are valid, try to repair them and if
362%% thats not possible, truncate.
363%%----------------------------------------------------------------------
364
365open(Filename, MaxBytes, MaxFiles, internal) ->
366    Opt0 = {format, internal},
367    Opts1 = [Opt0, {repair, true}],
368    Opts2 = [Opt0, {repair, truncate}],
369    open1(Filename, MaxBytes, MaxFiles, Opts1, Opts2);
370open(Filename, MaxBytes, MaxFiles, _) ->
371    Opts = [{format, external}],
372    open1(Filename, MaxBytes, MaxFiles, Opts, Opts).
373
374open1(Filename, MaxBytes, MaxFiles, Opts1, Opts2) ->
375    Opts0 = [{name, Filename}, {file, Filename}, {type, wrap}],
376    case open2(Opts0 ++ Opts1, Opts0 ++ Opts2, {MaxBytes, MaxFiles}) of
377        {ok, LogDB} ->
378            {ok, LogDB};
379        {repaired, LogDB, {recovered, _}, {badbytes, _}} ->
380            {ok, LogDB};
381        {error, Reason} ->
382            {error,
383             ?NICE("Can't create " ++ Filename ++
384                   lists:flatten(io_lib:format(", ~p",[Reason])))};
385        _ ->
386            {error, ?NICE("Can't create "++Filename)}
387    end.
388
389open2(Opts1, Opts2, Size) ->
390    case disk_log:open(Opts1) of
391        {error, {badarg, size}} ->
392            %% File did not exist, add the size option and try again
393            disk_log:open([{size, Size} | Opts1]);
394        {error, {Reason, _}} when
395                Reason == not_a_log_file;
396                Reason == invalid_index_file ->
397            %% File was corrupt, add the truncate option and try again
398            disk_log:open([{size, Size} | Opts2]);
399        Else ->
400            Else
401    end.
402
403
404%%----------------------------------------------------------------------
405%% Actually writes the entry to the disk_log. If the log is an
406%% internal disk_log write it with log otherwise with blog.
407%%----------------------------------------------------------------------
408write(Log, Entry, internal) ->
409    disk_log:log(Log, list_to_binary(Entry));
410
411write(Log, Entry, _) ->
412    disk_log:blog(Log, list_to_binary(Entry)).
413
414%% Close the log file
415close(Log) ->
416    disk_log:close(Log).
417
418auth_user(Data) ->
419    case proplists:get_value(remote_user,Data) of
420	undefined ->
421	    "-";
422	RemoteUser ->
423	    RemoteUser
424    end.
425%% log_internal_info
426
427log_internal_info(_, _,[]) ->
428    ok;
429log_internal_info(Info,Date,[{internal_info,Reason}|Rest]) ->
430    error_log(Info,Date,Reason),
431    log_internal_info(Info,Date,Rest);
432log_internal_info(Info,Date,[_|Rest]) ->
433    log_internal_info(Info,Date,Rest).
434
435make_integer(List) ->
436    try list_to_integer(List) of
437	N ->
438	    {ok, N}
439    catch
440	_:_ ->
441	    {error, {badarg, list_to_integer, List}}
442    end.
443