diff --git a/apps/kolab_guam/src/rules/kolab_guam_rule_audit.erl b/apps/kolab_guam/src/rules/kolab_guam_rule_audit.erl index 156a361..d2b7030 100644 --- a/apps/kolab_guam/src/rules/kolab_guam_rule_audit.erl +++ b/apps/kolab_guam/src/rules/kolab_guam_rule_audit.erl @@ -1,95 +1,109 @@ %% Copyright 2021 Apheleia IT AG (http://www.apheleia.ch) %% %% Aaron Seigo (Kolab Systems) %% Christian Mollekopf (Apheleia IT) %% %% This program is free software: you can redistribute it and/or modify %% it under the terms of the GNU General Public License as published by %% the Free Software Foundation, either version 3 of the License, or %% (at your option) any later version. %% %% This program is distributed in the hope that it will be useful, %% but WITHOUT ANY WARRANTY; without even the implied warranty of %% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the %% GNU General Public License for more details. %% %% You should have received a copy of the GNU General Public License %% along with this program. If not, see . -module(kolab_guam_rule_audit). -export([new/1, applies/4, imap_data/3, apply_to_client_message/4, apply_to_server_message/3]). -behavior(kolab_guam_rule). -include("kolab_guam_rule_audit.hrl"). new(_Config) -> #state { }. peername({sslsocket, _, _} = Socket) -> ssl:peername(Socket); peername(Socket) -> inet:peername(Socket). applies(Socket, _Buffer, { _Tag, _Command, _Data }, State) -> {ok, {Ip, _Port}} = peername(Socket), % This command is always immediately active as we expect the LOGIN command at the beginning { true, State#state{ ip = Ip }}. % Unused imap_data(_ResponseToken, _Response, State) -> State. apply_to_client_message(_ImapSession, Buffer, undefined, State) -> { Buffer, State }; % We just buffer the entire command once activated apply_to_client_message(_ImapSession, Buffer, { _Tag, _Command, _Data }, #state{ buffer = LeftOvers, active = true } = State) -> { Buffer, State#state{ buffer = <> }}; % Monitor for the trigger command, otherwise do nothing apply_to_client_message(ImapSession, Buffer, { Tag, Command, Data }, State) -> case is_triggering_command(Command, Data, State) of true -> apply_to_client_message(ImapSession, Buffer, { Tag, Command, Data }, State#state{ active = true, tag = Tag, command = Command }); _ -> { Buffer, State } end. apply_to_server_message(_ImapSession, Buffer, #state{ active = true, tag = Tag, ip = Ip, buffer = FullBuffer, command = Command } = State) -> NewState = case eimap_utils:is_tagged_response(Buffer, Tag) of tagged -> Username = extract_username(FullBuffer, Command), case eimap_utils:check_response_for_failure(Buffer, Tag) of ok -> lager:info("login: ~s from ~s, OK", [Username, inet:ntoa(Ip)]), State#state{ active = false, username = Username }; { no, Reason } -> lager:info("badlogin: ~s from ~s, NO: ~s", [Username, inet:ntoa(Ip), Reason]), State#state{ active = false, username = Username }; { bad, Reason } -> lager:info("badlogin: ~s from ~s, BAD: ~s", [Username, inet:ntoa(Ip), Reason]), State#state{ active = false, username = Username } end; untagged -> State end, { Buffer, NewState }; % Do nothing if not active apply_to_server_message(_ImapSession, Buffer, State) -> { Buffer, State }. %%PRIVATE is_triggering_command(Command, Data, #state{ trigger_commands = TriggerCommands }) -> %% if the command is in the list of trigger commands and the ending is not "" (which means "send me %% the root and separator" according to RFC 3501), then it is treated as a triggering event lists:any(fun(T) -> (Command =:= T) andalso (binary:longest_common_suffix([Data, <<"\"\"">>]) =/= 2) end, TriggerCommands). extract_username(FullBuffer, Command) -> case Command of Command when Command =:= <<"AUTHENTICATE">>; Command =:= <<"authenticate">> -> Lines = binary:split(FullBuffer, <<"\r\n">>, [ global ]), - % We can only handle the LOGIN method - base64:decode(lists:nth(2, Lines)); + FirstLine = lists:nth(1, Lines), + FirstLineParts = binary:split(FirstLine, <<" ">>, [ global ]), + AuthenticateMethod = lists:last(FirstLineParts), + case AuthenticateMethod of + <<"PLAIN">> -> + Decoded = base64:decode(lists:nth(2, Lines)), + % In the form of \0$username\0$password + Split = binary:split(Decoded, <<0>>, [ global ]), + lists:nth(2, Split); + <<"LOGIN">> -> + base64:decode(lists:nth(2, Lines)); + _ -> + %TODO SASL-IR would go here + lager:info("AUTHENTICATE method not implemented ~p", [AuthenticateMethod]), + <<"Not implemented">> + end; <<"LOGIN">> -> List = binary:split(FullBuffer, <<" ">>, [ global ]), lists:nth(3, List) end. diff --git a/apps/kolab_guam/test/kolab_guam_rule_audit_SUITE.erl b/apps/kolab_guam/test/kolab_guam_rule_audit_SUITE.erl index 229d89e..6b4d17b 100644 --- a/apps/kolab_guam/test/kolab_guam_rule_audit_SUITE.erl +++ b/apps/kolab_guam/test/kolab_guam_rule_audit_SUITE.erl @@ -1,111 +1,118 @@ %% Copyright 2015 Kolab Systems AG (http://www.kolabsys.com) %% %% Aaron Seigo (Kolab Systems) %% %% This program is free software: you can redistribute it and/or modify %% it under the terms of the GNU General Public License as published by %% the Free Software Foundation, either version 3 of the License, or %% (at your option) any later version. %% %% This program is distributed in the hope that it will be useful, %% but WITHOUT ANY WARRANTY; without even the implied warranty of %% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the %% GNU General Public License for more details. %% %% You should have received a copy of the GNU General Public License %% along with this program. If not, see . -module(kolab_guam_rule_audit_SUITE). % easier than exporting by name -compile(export_all). % required for common_test to work -include_lib("common_test/include/ct.hrl"). -include("../src/rules/kolab_guam_rule_audit.hrl"). %%%%%%%%%%%%%%%%%%%%%%%%%%% %% common test callbacks %% %%%%%%%%%%%%%%%%%%%%%%%%%%% % Specify a list of all unit test functions all() -> [ kolab_guam_rule_audit_test ]. % required, but can just return Config. this is a suite level setup function. init_per_suite(Config) -> Config. % required, but can just return Config. this is a suite level tear down function. end_per_suite(Config) -> Config. % optional, can do function level setup for all functions, % or for individual functions by matching on TestCase. init_per_testcase(_TestCase, Config) -> Config. % optional, can do function level tear down for all functions, % or for individual functions by matching on TestCase. end_per_testcase(_TestCase, Config) -> Config. kolab_guam_rule_audit_test(_TestConfig) -> %% Data to be fed into the test, one tuple per iteration %% Tuple format: { client_data, server_data, LoginAttemptSucceeded } Data = [ { [<<"a001 LOGIN test1@kolab.org SESAME\r\n">>], <<"a001 OK LOGIN completed">>, true }, { [<<"a001 LOGIN test1@kolab.org SESAME\r\n">>], <<"a001 NO LOGIN completed">>, false }, { [<<"a001 LOGIN test1@kolab.org SESAME\r\n">>], <<"a001 BAD LOGIN completed">>, false }, { [<<"a001 AUTHENTICATE LOGIN\r\n">>, <<"dGVzdDFAa29sYWIub3Jn\r\n">>, <<"V2VsY29tZTJLb2xhYlN5c3RlbXM=\r\n">>], <<"a001 BAD LOGIN completed">>, false }, { [<<"a001 AUTHENTICATE LOGIN\r\n">>, <<"dGVzdDFAa29sYWIub3Jn\r\n">>, <<"V2VsY29tZTJLb2xhYlN5c3RlbXM=\r\n">>], <<"a001 OK LOGIN completed">>, true + }, + % Thunderbird with the authenticate plain mechanism + { + [<<"1 authenticate PLAIN\r\n">>, + <<"AHRlc3QxQGtvbGFiLm9yZwBXZWxjb21lMktvbGFiU3lzdGVtcw==\r\n">>], + <<"1 OK LOGIN completed">>, + true } ], lager:start(), lager:set_loglevel(lager_console_backend, debug), %% setup boilerplate Config = {}, State = kolab_guam_rule_audit:new(Config), ServerConfig = kolab_guam_sup:default_imap_server_config(), { ok, ImapSession } = eimap:start_link(ServerConfig), %% run the dataset through the rule lists:foreach( fun({ ClientDataList, ServerData, LoginAttemptSucceeded }) -> ReadyState = lists:foldl(fun(ClientData, State) -> Split = eimap_utils:split_command_into_components(ClientData), { _, ReadyState } = kolab_guam_rule_audit:apply_to_client_message(ImapSession, ClientData, Split, State), ReadyState end, State, ClientDataList), { _Filtered, NewState } = kolab_guam_rule_audit:apply_to_server_message(ImapSession, ServerData, ReadyState), lager:info("Result ~p", [NewState]), #state{ username = <<"test1@kolab.org">> } = NewState % true = false end, Data).