1%%
2%% %CopyrightBegin%
3%%
4%% Copyright Ericsson AB 2001-2016. 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%% Security Audit Functionality
22
23%%
24%% The gen_server code.
25%%
26%% A gen_server is needed in this module to take care of shared access to the
27%% data file used to store failed and successful authentications aswell as
28%% user blocks.
29%%
30%% The storage model is a write-through model with both an ets and a dets
31%% table. Writes are done to both the ets and then the dets table, but reads
32%% are only done from the ets table.
33%%
34%% This approach also enables parallelism when using dets by returning the
35%% same dets table identifier when opening several files with the same
36%% physical location.
37%%
38%% NOTE: This could be implemented using a single dets table, as it is
39%%       possible to open a dets file with the ram_file flag, but this
40%%       would require periodical sync's to disk, and it would be hard
41%%       to decide when such an operation should occur.
42%%
43
44
45-module(mod_security_server).
46
47-include("httpd.hrl").
48-include("httpd_internal.hrl").
49
50-behaviour(gen_server).
51
52
53%% User API exports (called via mod_security)
54-export([list_blocked_users/2, list_blocked_users/3,
55	 block_user/5,
56	 unblock_user/3, unblock_user/4,
57	 list_auth_users/2, list_auth_users/3]).
58
59%% Internal exports (for mod_security only)
60-export([start/3, stop/2, stop/3,
61	 new_table/4, delete_tables/3,
62	 store_failed_auth/6, store_successful_auth/5,
63	 check_blocked_user/6]).
64
65%% gen_server exports
66-export([start_link/3, init/1,
67	 handle_info/2, handle_call/3, handle_cast/2,
68	 terminate/2,
69	 code_change/3]).
70
71%%====================================================================
72%% Internal application API
73%%====================================================================
74
75%% NOTE: This is called by httpd_misc_sup when the process is started
76start_link(Addr, Port, Profile) ->
77    Name = make_name(Addr, Port, Profile),
78    gen_server:start_link({local, Name}, ?MODULE, [], [{timeout, infinity}]).
79
80%% Called  by the mod_security module.
81start(Addr, Port, Profile) ->
82    Name = make_name(Addr, Port, Profile),
83    case whereis(Name) of
84	undefined ->
85	   httpd_misc_sup:start_sec_server(Addr, Port, Profile);
86	_ -> %% Already started...
87	    ok
88    end.
89
90stop(Port, Profile) ->
91    stop(undefined, Port, Profile).
92stop(Addr, Port, Profile) ->
93    Name = make_name(Addr, Port, Profile),
94    case whereis(Name) of
95	undefined ->
96	    ok;
97	_ ->
98	    httpd_misc_sup:stop_sec_server(Addr, Port, Profile)
99    end.
100
101addr(undefined) ->
102    any;
103addr(Addr) ->
104    Addr.
105
106list_blocked_users(Addr, Port) ->
107    list_blocked_users(Addr, Port, ?DEFAULT_PROFILE).
108list_blocked_users(Addr, Port, Profile) when is_atom(Profile)->
109    Name = make_name(Addr, Port, Profile),
110    Req  = {list_blocked_users, addr(Addr), Port, Profile,'_'},
111    call(Name, Req);
112list_blocked_users(Addr, Port, Dir) ->
113    list_blocked_users(Addr, Port, ?DEFAULT_PROFILE, Dir).
114list_blocked_users(Addr, Port, Profile, Dir) ->
115    Name = make_name(Addr, Port, Profile),
116    Req  = {list_blocked_users, addr(Addr), Port, Profile, Dir},
117    call(Name, Req).
118
119block_user(User, Addr, Port, Dir, Time) ->
120    block_user(User, Addr, Port, ?DEFAULT_PROFILE, Dir, Time).
121block_user(User, Addr, Port, Profile, Dir, Time) ->
122    Name = make_name(Addr, Port, Profile),
123    Req  = {block_user, User, addr(Addr), Port, Profile, Dir, Time},
124    call(Name, Req).
125
126unblock_user(User, Addr, Port) ->
127    unblock_user(User, Addr, Port, ?DEFAULT_PROFILE).
128unblock_user(User, Addr, Port, Profile) when is_atom(Profile)->
129    Name = make_name(Addr, Port, Profile),
130    Req  = {unblock_user, User, addr(Addr), Port, Profile, '_'},
131    call(Name, Req);
132unblock_user(User, Addr, Port, Dir) ->
133    unblock_user(User, Addr, Port, ?DEFAULT_PROFILE, Dir).
134unblock_user(User, Addr, Port, Profile, Dir) ->
135    Name = make_name(Addr, Port, Profile),
136    Req  = {unblock_user, User, addr(Addr), Port, Profile, Dir},
137    call(Name, Req).
138
139list_auth_users(Addr, Port) ->
140    list_auth_users(Addr, Port, ?DEFAULT_PROFILE).
141list_auth_users(Addr, Port, Profile) when is_atom(Profile) ->
142    Name = make_name(Addr, Port, Profile),
143    Req  = {list_auth_users, addr(Addr), Port, Profile, '_'},
144    call(Name, Req);
145list_auth_users(Addr, Port, Dir) ->
146    list_auth_users(Addr, Port, ?DEFAULT_PROFILE, Dir).
147list_auth_users(Addr, Port, Profile, Dir) ->
148    Name = make_name(Addr,Port, Profile),
149    Req  = {list_auth_users, addr(Addr), Port, Profile, Dir},
150    call(Name, Req).
151
152new_table(Addr, Port, Profile, TabName) ->
153    Name = make_name(Addr,Port, Profile),
154    Req  = {new_table, addr(Addr), Port, Profile, TabName},
155    call(Name, Req).
156
157delete_tables(Addr, Port, Profile) ->
158    Name = make_name(Addr, Port, Profile),
159    case whereis(Name) of
160	undefined ->
161	    ok;
162	_ ->
163	    call(Name, delete_tables)
164    end.
165
166store_failed_auth(Info, Addr, Port, Profile, DecodedString, SDirData) ->
167    Name = make_name(Addr, Port, Profile),
168    Msg  = {store_failed_auth, Profile, [Info,DecodedString,SDirData]},
169    cast(Name, Msg).
170
171store_successful_auth(Addr, Port, Profile, User, SDirData) ->
172    Name = make_name(Addr,Port, Profile),
173    Msg  = {store_successful_auth, [User,Addr,Port, Profile, SDirData]},
174    cast(Name, Msg).
175
176check_blocked_user(Info, User, SDirData, Addr, Port, Profile) ->
177    Name = make_name(Addr, Port, Profile),
178    Req  = {check_blocked_user, Profile, [Info, User, SDirData]},
179    call(Name, Req).
180
181%%====================================================================
182%% Behavior call backs
183%%====================================================================
184init(_) ->
185    process_flag(trap_exit, true),
186    {ok, []}.
187
188handle_call(stop, _From, _Tables) ->
189    {stop, normal, ok, []};
190
191handle_call({block_user, User, Addr, Port, Profile, Dir, Time}, _From, Tables) ->
192    Ret = block_user_int(User, Addr, Port, Profile, Dir, Time),
193    {reply, Ret, Tables};
194
195handle_call({list_blocked_users, Addr, Port, Profile, Dir}, _From, Tables) ->
196    Blocked = list_blocked(Tables, Addr, Port, Profile, Dir, []),
197    {reply, Blocked, Tables};
198
199handle_call({unblock_user, User, Addr, Port, Profile, Dir}, _From, Tables) ->
200    Ret = unblock_user_int(User, Addr, Port, Profile,Dir),
201    {reply, Ret, Tables};
202
203handle_call({list_auth_users, Addr, Port, Profile, Dir}, _From, Tables) ->
204    Auth = list_auth(Tables, Addr, Port, Profile, Dir, []),
205    {reply, Auth, Tables};
206
207handle_call({new_table, Addr, Port, Profile, Name}, _From, Tables) ->
208    case lists:keysearch(Name, 1, Tables) of
209	{value, {Name, {Ets, Dets}}} ->
210	    {reply, {ok, {Ets, Dets}}, Tables};
211	false ->
212	    TName = make_name(Addr,Port, Profile, length(Tables)),
213	    case dets:open_file(TName, [{type, bag}, {file, Name},
214					{repair, true},
215					{access, read_write}]) of
216		{ok, DFile} ->
217		    ETS = ets:new(TName, [bag, private]),
218		    sync_dets_to_ets(DFile, ETS),
219		    NewTables = [{Name, {ETS, DFile}}|Tables],
220		    {reply, {ok, {ETS, DFile}}, NewTables};
221		{error, Err} ->
222		    {reply, {error, {create_dets, Err}}, Tables}
223	    end
224    end;
225
226handle_call(delete_tables, _From, Tables) ->
227    lists:foreach(fun({_Name, {ETS, DETS}}) ->
228			  dets:close(DETS),
229			  ets:delete(ETS)
230		  end, Tables),
231    {reply, ok, []};
232
233handle_call({check_blocked_user, Profile, [Info, User, SDirData]}, _From, Tables) ->
234    {ETS, DETS} = proplists:get_value(data_file, SDirData),
235    Dir = proplists:get_value(path, SDirData),
236    Addr = proplists:get_value(bind_address, SDirData),
237    Port = proplists:get_value(port, SDirData),
238    CBModule =
239	proplists:get_value(callback_module, SDirData, no_module_at_all),
240    Ret =
241	check_blocked_user(Info, User, Dir, Addr, Port, Profile, ETS, DETS, CBModule),
242    {reply, Ret, Tables};
243
244handle_call(_Request,_From,Tables) ->
245    {reply,ok,Tables}.
246
247handle_cast({store_failed_auth, _,[_, _, []]}, Tables) ->
248    %% Some other authentication scheme than mod_auth (example mod_htacess)
249    %% was the source for the authentication failure so we should ignor it!
250    {noreply, Tables};
251handle_cast({store_failed_auth, Profile, [Info, DecodedString, SDirData]}, Tables) ->
252    {ETS, DETS} = proplists:get_value(data_file, SDirData),
253    Dir  = proplists:get_value(path, SDirData),
254    Addr = proplists:get_value(bind_address, SDirData),
255    Port = proplists:get_value(port, SDirData),
256    {ok, [User,Password]} = httpd_util:split(DecodedString,":",2),
257    Seconds = universal_time(),
258    Key = {User, Dir, Addr, Port, Profile},
259    %% Event
260    CBModule = proplists:get_value(callback_module,
261				     SDirData, no_module_at_all),
262    auth_fail_event(CBModule,Addr,Port,Dir,User,Password),
263
264    %% Find out if any of this user's other failed logins are too old to keep..
265    case ets:match_object(ETS, {failed, {Key, '_', '_'}}) of
266	[] ->
267	    no;
268	List ->
269	    ExpireTime = proplists:get_value(fail_expire_time,
270					     SDirData, 30)*60,
271	    lists:map(fun({failed, {TheKey, LS, Gen}}) ->
272			      Diff = Seconds-LS,
273			      if
274				  Diff > ExpireTime ->
275				      ets:match_delete(ETS,
276						       {failed,
277							{TheKey, LS, Gen}}),
278				      dets:match_delete(DETS,
279							{failed,
280							 {TheKey, LS, Gen}});
281				  true ->
282				      ok
283			      end
284		      end,
285		      List)
286    end,
287
288    %% Insert the new failure..
289    Generation = length(ets:match_object(ETS, {failed, {Key, '_', '_'}})),
290    ets:insert(ETS, {failed, {Key, Seconds, Generation}}),
291    dets:insert(DETS, {failed, {Key, Seconds, Generation}}),
292
293    %% See if we should block this user..
294    MaxRetries = proplists:get_value(max_retries, SDirData, 3),
295    BlockTime = proplists:get_value(block_time, SDirData, 60),
296    case ets:match_object(ETS, {failed, {Key, '_', '_'}}) of
297	List1 when length(List1) >= MaxRetries ->
298	    %% Block this user until Future
299	    Future = Seconds+BlockTime*60,
300	    Reason = io_lib:format("Blocking user ~s from dir ~s "
301				   "for ~p minutes",
302				   [User, Dir, BlockTime]),
303	    mod_log:security_log(Info, lists:flatten(Reason)),
304
305	    %% Event
306	    user_block_event(CBModule,Addr,Port,Dir,User),
307
308	    ets:match_delete(ETS,{blocked_user,
309				  {User, Addr, Port, Dir, '$1'}}),
310	    dets:match_delete(DETS, {blocked_user,
311				     {User, Addr, Port, Dir, '$1'}}),
312	    BlockRecord = {blocked_user,
313			   {User, Addr, Port, Profile, Dir, Future}},
314	    ets:insert(ETS, BlockRecord),
315	    dets:insert(DETS, BlockRecord),
316	    %% Remove previous failed requests.
317	    ets:match_delete(ETS, {failed, {Key, '_', '_'}}),
318	    dets:match_delete(DETS, {failed, {Key, '_', '_'}});
319	_ ->
320	    no
321    end,
322    {noreply, Tables};
323
324handle_cast({store_successful_auth, [User, Addr, Port, Profile, SDirData]}, Tables) ->
325    {ETS, DETS} = proplists:get_value(data_file, SDirData),
326    AuthTimeOut = proplists:get_value(auth_timeout, SDirData, 30),
327    Dir = proplists:get_value(path, SDirData),
328    Key = {User, Dir, Addr, Port, Profile},
329
330    %% Remove failed entries for this Key
331    dets:match_delete(DETS, {failed, {Key, '_', '_'}}),
332    ets:match_delete(ETS, {failed, {Key, '_', '_'}}),
333
334    %% Keep track of when the last successful login took place.
335    Seconds = universal_time()+AuthTimeOut,
336    ets:match_delete(ETS, {success, {Key, '_'}}),
337    dets:match_delete(DETS, {success, {Key, '_'}}),
338    ets:insert(ETS, {success, {Key, Seconds}}),
339    dets:insert(DETS, {success, {Key, Seconds}}),
340    {noreply, Tables};
341
342handle_cast(Req, Tables) ->
343    error_msg("security server got unknown cast: ~p",[Req]),
344    {noreply, Tables}.
345
346handle_info(_Info, State) ->
347    {noreply, State}.
348
349terminate(_Reason, _Tables) ->
350    ok.
351
352code_change(_, State, _Extra) ->
353    {ok, State}.
354
355%%--------------------------------------------------------------------
356%%% Internal functions
357%%--------------------------------------------------------------------
358
359%% block_user_int/5
360block_user_int(User, Addr, Port, Profile, Dir, Time) ->
361    Dirs = httpd_manager:config_match(Addr, Port, Profile,
362				      {security_directory, {'_', '_'}}),
363    case find_dirdata(Dirs, Dir) of
364	{ok, DirData, {ETS, DETS}} ->
365	    Time1 =
366		case Time of
367		    infinity ->
368			99999999999999999999999999999;
369		    _ ->
370			Time
371		end,
372	    Future = universal_time()+Time1,
373	    ets:match_delete(ETS, {blocked_user, {User,Addr,Port,Profile, Dir,'_'}}),
374	    dets:match_delete(DETS, {blocked_user,
375				     {User,Addr,Port,Profile, Dir,'_'}}),
376	    ets:insert(ETS, {blocked_user, {User,Addr,Port, Profile, Dir,Future}}),
377	    dets:insert(DETS, {blocked_user, {User,Addr,Port,Profile, Dir,Future}}),
378	    CBModule = proplists:get_value(callback_module, DirData,
379					     no_module_at_all),
380	    user_block_event(CBModule,Addr,Port,Dir,User),
381	    true;
382	_ ->
383	    {error, no_such_directory}
384    end.
385
386find_dirdata([], _Dir) ->
387    false;
388find_dirdata([{security_directory, {_, DirData}}|SDirs], Dir) ->
389    case lists:keysearch(path, 1, DirData) of
390	{value, {path, Dir}} ->
391	    {value, {data_file, {ETS, DETS}}} =
392		lists:keysearch(data_file, 1, DirData),
393	    {ok, DirData, {ETS, DETS}};
394	_ ->
395	    find_dirdata(SDirs, Dir)
396    end.
397
398unblock_user_int(User, Addr, Port, Profile, Dir) ->
399    Dirs = httpd_manager:config_match(Addr, Port, Profile,
400				      {security_directory, {'_', '_'}}),
401    case find_dirdata(Dirs, Dir) of
402	{ok, DirData, {ETS, DETS}} ->
403	    case ets:match_object(ETS,
404				  {blocked_user,{User,Addr,Port,Profile,Dir,'_'}}) of
405		[] ->
406		    {error, not_blocked};
407		_Objects ->
408		    ets:match_delete(ETS, {blocked_user,
409					   {User, Addr, Port, Profile, Dir, '_'}}),
410		    dets:match_delete(DETS, {blocked_user,
411					     {User, Addr, Port, Profile, Dir, '_'}}),
412	       	    CBModule = proplists:get_value(callback_module,
413						     DirData,
414						     no_module_at_all),
415		    user_unblock_event(CBModule,Addr,Port,Dir,User),
416		    true
417	    end;
418	_ ->
419	    {error, no_such_directory}
420    end.
421
422list_auth([], _, _, _, _, Acc) ->
423    Acc;
424list_auth([{_Name, {ETS, DETS}}|Tables], Addr, Port, Profile, Dir, Acc) ->
425    case ets:match_object(ETS, {success, {{'_', Dir, Addr, Port, Profile}, '_'}}) of
426	[] ->
427	    list_auth(Tables, Addr, Port, Profile, Dir, Acc);
428	List ->
429	    TN = universal_time(),
430	    NewAcc = lists:foldr(fun({success,{{U,Ad,P, Pr,D},T}},Ac) ->
431					 if
432					     T-TN > 0 ->
433						 [U|Ac];
434					     true ->
435						 Rec = {success,
436							{{U,Ad,P,Pr,D},T}},
437						 ets:match_delete(ETS,Rec),
438						 dets:match_delete(DETS,Rec),
439						 Ac
440					 end
441				 end,
442				 Acc, List),
443	    list_auth(Tables, Addr, Port, Profile, Dir, NewAcc)
444    end.
445
446list_blocked([], _, _, _, _, Acc) ->
447    TN = universal_time(),
448    lists:foldl(fun({U,Ad,P,Pr,D,T}, Ac) ->
449			if
450			    T-TN > 0 ->
451				[{U,Ad,P, Pr,D,local_time(T)}|Ac];
452			    true ->
453				Ac
454			end
455		end,
456		[], Acc);
457list_blocked([{_Name, {ETS, _DETS}}|Tables], Addr, Port, Profile, Dir, Acc) ->
458    List = ets:match_object(ETS, {blocked_user,
459				  {'_',Addr,Port,Profile, Dir,'_'}}),
460
461    NewBlocked = lists:foldl(fun({blocked_user, X}, A) ->
462				     [X|A] end, Acc, List),
463
464    list_blocked(Tables, Addr, Port, Profile, Dir, NewBlocked).
465
466
467%% Reads dets-table DETS and syncronizes it with the ets-table ETS.
468%%
469sync_dets_to_ets(DETS, ETS) ->
470    dets:traverse(DETS, fun(X) ->
471				ets:insert(ETS, X),
472				continue
473			end).
474
475%% Check if a specific user is blocked from access.
476%%
477%% The sideeffect of this routine is that it unblocks also other users
478%% whos blocking time has expired. This to keep the tables as small
479%% as possible.
480%%
481check_blocked_user(Info, User, Dir, Addr, Port, Profile, ETS, DETS, CBModule) ->
482    TN = universal_time(),
483    BlockList = ets:match_object(ETS, {blocked_user, {User, '_', '_', '_', '_', '_'}}),
484    Blocked = lists:foldl(fun({blocked_user, X}, A) ->
485				  [X|A] end, [], BlockList),
486    check_blocked_user(Info,User,Dir,
487		       Addr,Port, Profile, ETS,DETS,TN,Blocked,CBModule).
488
489check_blocked_user(_Info, _, _, _, _, _, _, _, _,[], _CBModule) ->
490    false;
491check_blocked_user(Info, User, Dir, Addr, Port, Profile, ETS, DETS, TN,
492		   [{User,Addr,Port,Profile, Dir,T}| _], CBModule) ->
493    TD = T-TN,
494    if
495	TD =< 0 ->
496	    %% Blocking has expired, remove and grant access.
497	    unblock_user(Info, User, Dir, Addr, Port, Profile, ETS, DETS, CBModule),
498	    false;
499	true ->
500	    true
501    end;
502check_blocked_user(Info, User, Dir, Addr, Port, Profile, ETS, DETS, TN,
503		   [{OUser,ODir,OAddr,OPort, OProfile, T}|Ls], CBModule) ->
504    TD = T-TN,
505    if
506	TD =< 0 ->
507	    %% Blocking has expired, remove.
508	    unblock_user(Info, OUser, ODir, OAddr, OPort, OProfile,
509			 ETS, DETS, CBModule);
510	true ->
511	    true
512    end,
513    check_blocked_user(Info, User, Dir, Addr, Port, Profile, ETS, DETS,
514		       TN, Ls, CBModule).
515
516unblock_user(Info, User, Dir, Addr, Port, Profile, ETS, DETS, CBModule) ->
517    Reason =
518	io_lib:format("User ~s was removed from the block list for dir ~s",
519			 [User, Dir]),
520    mod_log:security_log(Info, lists:flatten(Reason)),
521    user_unblock_event(CBModule,Addr,Port,Dir,User),
522    dets:match_delete(DETS, {blocked_user, {User, Addr, Port, Profile, Dir, '_'}}),
523    ets:match_delete(ETS, {blocked_user, {User, Addr, Port, Profile, Dir, '_'}}).
524
525make_name(Addr,Port, Profile) ->
526    httpd_util:make_name(?MODULE_STRING, Addr, Port, Profile).
527
528make_name(Addr,Port, Profile, Num) ->
529    httpd_util:make_name(?MODULE_STRING, Addr,Port,
530			 atom_to_list(Profile) ++ "__" ++ integer_to_list(Num)).
531
532auth_fail_event(Mod,Addr,Port,Dir,User,Passwd) ->
533    event(auth_fail,Mod,Addr,Port,Dir,[{user,User},{password,Passwd}]).
534
535user_block_event(Mod,Addr,Port,Dir,User) ->
536    event(user_block,Mod,Addr,Port,Dir,[{user,User}]).
537
538user_unblock_event(Mod,Addr,Port,Dir,User) ->
539    event(user_unblock,Mod,Addr,Port,Dir,[{user,User}]).
540
541event(Event, Mod, undefined, Port, Dir, Info) ->
542    (catch Mod:event(Event,Port,Dir,Info));
543event(Event, Mod, any, Port, Dir, Info) ->
544    (catch Mod:event(Event,Port,Dir,Info));
545event(Event, Mod, Addr, Port, Dir, Info) ->
546    (catch Mod:event(Event,Addr,Port,Dir,Info)).
547
548universal_time() ->
549    calendar:datetime_to_gregorian_seconds(calendar:universal_time()).
550
551local_time(T) ->
552    calendar:universal_time_to_local_time(
553      calendar:gregorian_seconds_to_datetime(T)).
554
555error_msg(F, A) ->
556    error_logger:error_msg(F, A).
557
558call(Name, Req) ->
559    case (catch gen_server:call(Name, Req)) of
560        {'EXIT', Reason} ->
561            {error, Reason};
562        Reply ->
563            Reply
564    end.
565
566cast(Name, Msg) ->
567    case (catch gen_server:cast(Name, Msg)) of
568        {'EXIT', Reason} ->
569            {error, Reason};
570        Result ->
571            Result
572    end.
573