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