1
%% ``The contents of this file are subject to the Erlang Public License,
2
%% Version 1.1, (the "License"); you may not use this file except in
3
%% compliance with the License. You should have received a copy of the
4
%% Erlang Public License along with this software. If not, it can be
5
%% retrieved via the world wide web at http://www.erlang.org/.
7
%% Software distributed under the License is distributed on an "AS IS"
8
%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
9
%% the License for the specific language governing rights and limitations
12
%% The Initial Developer of the Original Code is Ericsson Utvecklings AB.
13
%% Portions created by Ericsson are Copyright 1999, Ericsson Utvecklings
14
%% AB. All Rights Reserved.''
16
%% $Id: mod_security_server.erl,v 1.1 2008/12/17 09:53:36 mikpe Exp $
18
%% Security Audit Functionality
21
%% The gen_server code.
23
%% A gen_server is needed in this module to take care of shared access to the
24
%% data file used to store failed and successful authentications aswell as
27
%% The storage model is a write-through model with both an ets and a dets
28
%% table. Writes are done to both the ets and then the dets table, but reads
29
%% are only done from the ets table.
31
%% This approach also enables parallelism when using dets by returning the
32
%% same dets table identifier when opening several files with the same
35
%% NOTE: This could be implemented using a single dets table, as it is
36
%% possible to open a dets file with the ram_file flag, but this
37
%% would require periodical sync's to disk, and it would be hard
38
%% to decide when such an operation should occur.
42
-module(mod_security_server).
44
-include("httpd.hrl").
45
-include("httpd_verbosity.hrl").
48
-behaviour(gen_server).
51
%% User API exports (called via mod_security)
52
-export([list_blocked_users/2, list_blocked_users/3,
54
unblock_user/3, unblock_user/4,
55
list_auth_users/2, list_auth_users/3]).
57
%% Internal exports (for mod_security only)
58
-export([start/2, stop/1, stop/2,
59
new_table/3, delete_tables/2,
60
store_failed_auth/5, store_successful_auth/4,
61
check_blocked_user/5]).
64
-export([start_link/3,
66
handle_info/2, handle_call/3, handle_cast/2,
70
-export([verbosity/3]).
73
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
77
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
81
%% NOTE: This is called by httpd_misc_sup when the process is started
84
start_link(Addr, Port, Verbosity) ->
85
?vtrace("start_link -> entry with"
87
"~n Port: ~p", [Addr, Port]),
88
Name = make_name(Addr, Port),
89
gen_server:start_link({local, Name}, ?MODULE, [Verbosity],
90
[{timeout, infinity}]).
94
%% Called by the mod_security module.
97
Name = make_name(Addr, Port),
100
Verbosity = get(security_verbosity),
101
case httpd_misc_sup:start_sec_server(Addr, Port, Verbosity) of
103
put(security_server, Pid),
106
exit({failed_start_security_server, Error})
108
_ -> %% Already started...
116
stop(undefined, Port).
118
Name = make_name(Addr, Port),
119
case whereis(Name) of
123
httpd_misc_sup:stop_sec_server(Addr, Port)
129
verbosity(Addr, Port, Verbosity) ->
130
Name = make_name(Addr, Port),
131
Req = {verbosity, Verbosity},
135
%% list_blocked_users
137
list_blocked_users(Addr, Port) ->
138
Name = make_name(Addr,Port),
139
Req = {list_blocked_users, Addr, Port, '_'},
142
list_blocked_users(Addr, Port, Dir) ->
143
Name = make_name(Addr, Port),
144
Req = {list_blocked_users, Addr, Port, Dir},
150
block_user(User, Addr, Port, Dir, Time) ->
151
Name = make_name(Addr, Port),
152
Req = {block_user, User, Addr, Port, Dir, Time},
158
unblock_user(User, Addr, Port) ->
159
Name = make_name(Addr, Port),
160
Req = {unblock_user, User, Addr, Port, '_'},
163
unblock_user(User, Addr, Port, Dir) ->
164
Name = make_name(Addr, Port),
165
Req = {unblock_user, User, Addr, Port, Dir},
171
list_auth_users(Addr, Port) ->
172
Name = make_name(Addr, Port),
173
Req = {list_auth_users, Addr, Port, '_'},
176
list_auth_users(Addr, Port, Dir) ->
177
Name = make_name(Addr,Port),
178
Req = {list_auth_users, Addr, Port, Dir},
184
new_table(Addr, Port, TabName) ->
185
Name = make_name(Addr,Port),
186
Req = {new_table, Addr, Port, TabName},
192
delete_tables(Addr, Port) ->
193
Name = make_name(Addr, Port),
194
case whereis(Name) of
198
call(Name, delete_tables)
204
store_failed_auth(Info, Addr, Port, DecodedString, SDirData) ->
205
Name = make_name(Addr,Port),
206
Msg = {store_failed_auth,[Info,DecodedString,SDirData]},
210
%% store_successful_auth
212
store_successful_auth(Addr, Port, User, SDirData) ->
213
Name = make_name(Addr,Port),
214
Msg = {store_successful_auth, [User,Addr,Port,SDirData]},
218
%% check_blocked_user
220
check_blocked_user(Info, User, SDirData, Addr, Port) ->
221
Name = make_name(Addr, Port),
222
Req = {check_blocked_user, [Info, User, SDirData]},
226
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
228
%% Server call-back functions %%
230
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
235
init([?default_verbosity]);
237
?DEBUG("init -> entry with Verbosity: ~p",[Verbosity]),
238
process_flag(trap_exit, true),
240
put(verbosity, Verbosity),
241
?vlog("starting",[]),
247
handle_call(stop, _From, Tables) ->
249
{stop, normal, ok, []};
252
handle_call({verbosity,Verbosity}, _From, Tables) ->
253
?vlog("set verbosity to ~p",[Verbosity]),
254
OldVerbosity = get(verbosity),
255
put(verbosity,Verbosity),
256
?vdebug("old verbosity: ~p",[OldVerbosity]),
257
{reply,OldVerbosity,Tables};
260
handle_call({block_user, User, Addr, Port, Dir, Time}, _From, Tables) ->
261
?vlog("block user '~p' for ~p",[User,Dir]),
262
Ret = block_user_int({User, Addr, Port, Dir, Time}),
263
?vdebug("block user result: ~p",[Ret]),
264
{reply, Ret, Tables};
267
handle_call({list_blocked_users, Addr, Port, Dir}, _From, Tables) ->
268
?vlog("list blocked users for ~p",[Dir]),
269
Blocked = list_blocked(Tables, Addr, Port, Dir, []),
270
?vdebug("list blocked users: ~p",[Blocked]),
271
{reply, Blocked, Tables};
274
handle_call({unblock_user, User, Addr, Port, Dir}, _From, Tables) ->
275
?vlog("unblock user '~p' for ~p",[User,Dir]),
276
Ret = unblock_user_int({User, Addr, Port, Dir}),
277
?vdebug("unblock user result: ~p",[Ret]),
278
{reply, Ret, Tables};
281
handle_call({list_auth_users, Addr, Port, Dir}, _From, Tables) ->
282
?vlog("list auth users for ~p",[Dir]),
283
Auth = list_auth(Tables, Addr, Port, Dir, []),
284
?vdebug("list auth users result: ~p",[Auth]),
285
{reply, Auth, Tables};
288
handle_call({new_table, Addr, Port, Name}, _From, Tables) ->
289
case lists:keysearch(Name, 1, Tables) of
290
{value, {Name, {Ets, Dets}}} ->
291
?DEBUG("handle_call(new_table) -> we already have this table: ~p",
293
?vdebug("new table; we already have this one: ~p",[Name]),
294
{reply, {ok, {Ets, Dets}}, Tables};
296
?LOG("handle_call(new_table) -> new_table: Name = ~p",[Name]),
297
?vlog("new table: ~p",[Name]),
298
TName = make_name(Addr,Port,length(Tables)),
299
?DEBUG("handle_call(new_table) -> TName: ~p",[TName]),
300
?vdebug("new table: ~p",[TName]),
301
case dets:open_file(TName, [{type, bag}, {file, Name},
303
{access, read_write}]) of
305
ETS = ets:new(TName, [bag, private]),
306
sync_dets_to_ets(DFile, ETS),
307
NewTables = [{Name, {ETS, DFile}}|Tables],
308
?DEBUG("handle_call(new_table) -> ~n"
309
" NewTables: ~p",[NewTables]),
310
?vtrace("new tables: ~p",[NewTables]),
311
{reply, {ok, {ETS, DFile}}, NewTables};
313
?LOG("handle_call -> Err: ~p",[Err]),
314
?vinfo("failed open dets file: ~p",[Err]),
315
{reply, {error, {create_dets, Err}}, Tables}
319
handle_call(delete_tables, _From, Tables) ->
320
?vlog("delete tables",[]),
321
lists:foreach(fun({Name, {ETS, DETS}}) ->
327
handle_call({check_blocked_user, [Info, User, SDirData]}, _From, Tables) ->
328
?vlog("check blocked user '~p'",[User]),
329
{ETS, DETS} = httpd_util:key1search(SDirData, data_file),
330
Dir = httpd_util:key1search(SDirData, path),
331
Addr = httpd_util:key1search(SDirData, bind_address),
332
Port = httpd_util:key1search(SDirData, port),
333
CBModule = httpd_util:key1search(SDirData, callback_module, no_module_at_all),
334
?vdebug("call back module: ~p",[CBModule]),
335
Ret = check_blocked_user(Info, User, Dir, Addr, Port, ETS, DETS, CBModule),
336
?vdebug("check result: ~p",[Ret]),
337
{reply, Ret, Tables};
338
handle_call(Request,From,Tables) ->
339
?vinfo("~n unknown call '~p' from ~p",[Request,From]),
345
handle_cast({store_failed_auth, [Info, DecodedString, SDirData]}, Tables) ->
346
?vlog("store failed auth",[]),
347
{ETS, DETS} = httpd_util:key1search(SDirData, data_file),
348
Dir = httpd_util:key1search(SDirData, path),
349
Addr = httpd_util:key1search(SDirData, bind_address),
350
Port = httpd_util:key1search(SDirData, port),
351
{ok, [User,Password]} = httpd_util:split(DecodedString,":",2),
352
?vdebug("user '~p' and password '~p'",[User,Password]),
353
Seconds = universal_time(),
354
Key = {User, Dir, Addr, Port},
357
CBModule = httpd_util:key1search(SDirData, callback_module, no_module_at_all),
358
?vtrace("call back module: ~p",[CBModule]),
359
auth_fail_event(CBModule,Addr,Port,Dir,User,Password),
361
%% Find out if any of this user's other failed logins are too old to keep..
362
?vtrace("remove old login failures",[]),
363
case ets:match_object(ETS, {failed, {Key, '_', '_'}}) of
365
?vtrace("no old login failures",[]),
367
List when list(List) ->
368
?vtrace("~p old login failures",[length(List)]),
369
ExpireTime = httpd_util:key1search(SDirData, fail_expire_time, 30)*60,
370
?vtrace("expire time ~p",[ExpireTime]),
371
lists:map(fun({failed, {TheKey, LS, Gen}}) ->
375
?vtrace("~n '~p' is to old to keep: ~p",
377
ets:match_delete(ETS, {failed, {TheKey, LS, Gen}}),
378
dets:match_delete(DETS, {failed, {TheKey, LS, Gen}});
380
?vtrace("~n '~p' is not old enough: ~p",
387
?vlog("~n unknown login failure search resuylt: ~p",[O]),
391
%% Insert the new failure..
392
Generation = length(ets:match_object(ETS, {failed, {Key, '_', '_'}})),
393
?vtrace("insert ('~p') new login failure: ~p",[Key,Generation]),
394
ets:insert(ETS, {failed, {Key, Seconds, Generation}}),
395
dets:insert(DETS, {failed, {Key, Seconds, Generation}}),
397
%% See if we should block this user..
398
MaxRetries = httpd_util:key1search(SDirData, max_retries, 3),
399
BlockTime = httpd_util:key1search(SDirData, block_time, 60),
400
?vtrace("~n Max retries ~p, block time ~p",[MaxRetries,BlockTime]),
401
case ets:match_object(ETS, {failed, {Key, '_', '_'}}) of
403
?vtrace("~n ~p tries so far",[length(List1)]),
405
length(List1) >= MaxRetries ->
406
%% Block this user until Future
407
?vtrace("block user '~p'",[User]),
408
Future = Seconds+BlockTime*60,
409
?vtrace("future: ~p",[Future]),
410
Reason = io_lib:format("Blocking user ~s from dir ~s "
412
[User, Dir, BlockTime]),
413
mod_log:security_log(Info, lists:flatten(Reason)),
416
user_block_event(CBModule,Addr,Port,Dir,User),
418
ets:match_delete(ETS,{blocked_user,
419
{User, Addr, Port, Dir, '$1'}}),
420
dets:match_delete(DETS, {blocked_user,
421
{User, Addr, Port, Dir, '$1'}}),
422
BlockRecord = {blocked_user,
423
{User, Addr, Port, Dir, Future}},
424
ets:insert(ETS, BlockRecord),
425
dets:insert(DETS, BlockRecord),
426
%% Remove previous failed requests.
427
ets:match_delete(ETS, {failed, {Key, '_', '_'}}),
428
dets:match_delete(DETS, {failed, {Key, '_', '_'}});
430
?vtrace("still some tries to go",[]),
438
handle_cast({store_successful_auth, [User, Addr, Port, SDirData]}, Tables) ->
439
?vlog("store successfull auth",[]),
440
{ETS, DETS} = httpd_util:key1search(SDirData, data_file),
441
AuthTimeOut = httpd_util:key1search(SDirData, auth_timeout, 30),
442
Dir = httpd_util:key1search(SDirData, path),
443
Key = {User, Dir, Addr, Port},
445
%% Remove failed entries for this Key
446
dets:match_delete(DETS, {failed, {Key, '_', '_'}}),
447
ets:match_delete(ETS, {failed, {Key, '_', '_'}}),
449
%% Keep track of when the last successful login took place.
450
Seconds = universal_time()+AuthTimeOut,
451
ets:match_delete(ETS, {success, {Key, '_'}}),
452
dets:match_delete(DETS, {success, {Key, '_'}}),
453
ets:insert(ETS, {success, {Key, Seconds}}),
454
dets:insert(DETS, {success, {Key, Seconds}}),
457
handle_cast(Req, Tables) ->
458
?vinfo("~n unknown cast '~p'",[Req]),
459
error_msg("security server got unknown cast: ~p",[Req]),
465
handle_info(Info, State) ->
466
?vinfo("~n unknown info '~p'",[Info]),
472
terminate(Reason, _Tables) ->
473
?vlog("~n Terminating for reason: ~p",[Reason]),
477
%% code_change({down, ToVsn}, State, Extra)
479
code_change({down, _}, State, _Extra) ->
480
?vlog("downgrade", []),
484
%% code_change(FromVsn, State, Extra)
486
code_change(_, State, Extra) ->
487
?vlog("upgrade", []),
494
block_user_int({User, Addr, Port, Dir, Time}) ->
495
Dirs = httpd_manager:config_match(Addr, Port, {security_directory, '_'}),
496
?vtrace("block '~p' for ~p during ~p",[User,Dir,Time]),
497
case find_dirdata(Dirs, Dir) of
498
{ok, DirData, {ETS, DETS}} ->
502
99999999999999999999999999999;
506
Future = universal_time()+Time1,
507
ets:match_delete(ETS, {blocked_user, {User,Addr,Port,Dir,'_'}}),
508
dets:match_delete(DETS, {blocked_user, {User,Addr,Port,Dir,'_'}}),
509
ets:insert(ETS, {blocked_user, {User,Addr,Port,Dir,Future}}),
510
dets:insert(DETS, {blocked_user, {User,Addr,Port,Dir,Future}}),
511
CBModule = httpd_util:key1search(DirData, callback_module,
513
?vtrace("call back module ~p",[CBModule]),
514
user_block_event(CBModule,Addr,Port,Dir,User),
517
{error, no_such_directory}
521
find_dirdata([], _Dir) ->
523
find_dirdata([{security_directory, DirData}|SDirs], Dir) ->
524
case lists:keysearch(path, 1, DirData) of
525
{value, {path, Dir}} ->
526
{value, {data_file, {ETS, DETS}}} =
527
lists:keysearch(data_file, 1, DirData),
528
{ok, DirData, {ETS, DETS}};
530
find_dirdata(SDirs, Dir)
533
%% unblock_user_int/2
535
unblock_user_int({User, Addr, Port, Dir}) ->
536
?vtrace("unblock user '~p' for ~p",[User,Dir]),
537
Dirs = httpd_manager:config_match(Addr, Port, {security_directory, '_'}),
538
?vtrace("~n dirs: ~p",[Dirs]),
539
case find_dirdata(Dirs, Dir) of
540
{ok, DirData, {ETS, DETS}} ->
541
case ets:match_object(ETS,{blocked_user,{User,Addr,Port,Dir,'_'}}) of
543
?vtrace("not blocked",[]),
544
{error, not_blocked};
546
ets:match_delete(ETS, {blocked_user,
547
{User, Addr, Port, Dir, '_'}}),
548
dets:match_delete(DETS, {blocked_user,
549
{User, Addr, Port, Dir, '_'}}),
550
CBModule = httpd_util:key1search(DirData, callback_module,
552
user_unblock_event(CBModule,Addr,Port,Dir,User),
556
?vlog("~n cannot unblock: no such directory '~p'",[Dir]),
557
{error, no_such_directory}
564
list_auth([], _Addr, _Port, Dir, Acc) ->
566
list_auth([{Name, {ETS, DETS}}|Tables], Addr, Port, Dir, Acc) ->
567
case ets:match_object(ETS, {success, {{'_', Dir, Addr, Port}, '_'}}) of
569
list_auth(Tables, Addr, Port, Dir, Acc);
570
List when list(List) ->
571
TN = universal_time(),
572
NewAcc = lists:foldr(fun({success,{{U,Ad,P,D},T}},Ac) ->
577
Rec = {success,{{U,Ad,P,D},T}},
578
ets:match_delete(ETS,Rec),
579
dets:match_delete(DETS,Rec),
584
list_auth(Tables, Addr, Port, Dir, NewAcc);
586
list_auth(Tables, Addr, Port, Dir, Acc)
592
list_blocked([], Addr, Port, Dir, Acc) ->
593
TN = universal_time(),
594
lists:foldl(fun({U,Ad,P,D,T}, Ac) ->
597
[{U,Ad,P,D,local_time(T)}|Ac];
603
list_blocked([{Name, {ETS, DETS}}|Tables], Addr, Port, Dir, Acc) ->
605
case ets:match_object(ETS, {blocked_user, {'_',Addr,Port,Dir,'_'}}) of
606
List when list(List) ->
607
lists:foldl(fun({blocked_user, X}, A) -> [X|A] end, Acc, List);
611
list_blocked(Tables, Addr, Port, Dir, NewBlocked).
615
%% sync_dets_to_ets/2
617
%% Reads dets-table DETS and syncronizes it with the ets-table ETS.
619
sync_dets_to_ets(DETS, ETS) ->
620
dets:traverse(DETS, fun(X) ->
626
%% check_blocked_user/7 -> true | false
628
%% Check if a specific user is blocked from access.
630
%% The sideeffect of this routine is that it unblocks also other users
631
%% whos blocking time has expired. This to keep the tables as small
634
check_blocked_user(Info, User, Dir, Addr, Port, ETS, DETS, CBModule) ->
635
TN = universal_time(),
636
case ets:match_object(ETS, {blocked_user, {User, '_', '_', '_', '_'}}) of
637
List when list(List) ->
638
Blocked = lists:foldl(fun({blocked_user, X}, A) ->
639
[X|A] end, [], List),
640
check_blocked_user(Info,User,Dir,Addr,Port,ETS,DETS,TN,Blocked,CBModule);
644
check_blocked_user(Info, User, Dir, Addr, Port, ETS, DETS, TN, [], CBModule) ->
646
check_blocked_user(Info, User, Dir, Addr, Port, ETS, DETS, TN,
647
[{User,Addr,Port,Dir,T}|Ls], CBModule) ->
651
%% Blocking has expired, remove and grant access.
652
unblock_user(Info, User, Dir, Addr, Port, ETS, DETS, CBModule),
657
check_blocked_user(Info, User, Dir, Addr, Port, ETS, DETS, TN,
658
[{OUser,ODir,OAddr,OPort,T}|Ls], CBModule) ->
662
%% Blocking has expired, remove.
663
unblock_user(Info, OUser, ODir, OAddr, OPort, ETS, DETS, CBModule);
667
check_blocked_user(Info, User, Dir, Addr, Port, ETS, DETS, TN, Ls, CBModule).
669
unblock_user(Info, User, Dir, Addr, Port, ETS, DETS, CBModule) ->
670
Reason=io_lib:format("User ~s was removed from the block list for dir ~s",
672
mod_log:security_log(Info, lists:flatten(Reason)),
673
user_unblock_event(CBModule,Addr,Port,Dir,User),
674
dets:match_delete(DETS, {blocked_user, {User, Addr, Port, Dir, '_'}}),
675
ets:match_delete(ETS, {blocked_user, {User, Addr, Port, Dir, '_'}}).
678
make_name(Addr,Port) ->
679
httpd_util:make_name("httpd_security",Addr,Port).
681
make_name(Addr,Port,Num) ->
682
httpd_util:make_name("httpd_security",Addr,Port,
683
"__" ++ integer_to_list(Num)).
686
auth_fail_event(Mod,Addr,Port,Dir,User,Passwd) ->
687
event(auth_fail,Mod,Addr,Port,Dir,[{user,User},{password,Passwd}]).
689
user_block_event(Mod,Addr,Port,Dir,User) ->
690
event(user_block,Mod,Addr,Port,Dir,[{user,User}]).
692
user_unblock_event(Mod,Addr,Port,Dir,User) ->
693
event(user_unblock,Mod,Addr,Port,Dir,[{user,User}]).
695
event(Event,Mod,undefined,Port,Dir,Info) ->
696
(catch Mod:event(Event,Port,Dir,Info));
697
event(Event,Mod,Addr,Port,Dir,Info) ->
698
(catch Mod:event(Event,Addr,Port,Dir,Info)).
701
calendar:datetime_to_gregorian_seconds(calendar:universal_time()).
704
calendar:universal_time_to_local_time(
705
calendar:gregorian_seconds_to_datetime(T)).
709
error_logger:error_msg(F, A).
713
case (catch gen_server:call(Name, Req)) of
722
case (catch gen_server:cast(Name, Msg)) of