diff --git a/apps/kolab_guam/src/kolab_guam_rule.erl b/apps/kolab_guam/src/kolab_guam_rule.erl --- a/apps/kolab_guam/src/kolab_guam_rule.erl +++ b/apps/kolab_guam/src/kolab_guam_rule.erl @@ -18,9 +18,9 @@ -module(kolab_guam_rule). -callback new(Args :: any()) -> any(). --callback applies(ConnectionDetails :: list(), Buffer :: binary(), State :: any()) -> { true, State :: any() } | +-callback applies(ConnectionDetails :: list(), Buffer :: binary(), SplitBinary :: { Tag :: binary(), Command :: binary(), Data :: binary() }, State :: any()) -> { true, State :: any() } | { false, State :: any() } | { notyet, State :: any() }. --callback apply_to_client_message(ImapSession :: pid(), Command :: binary(), State :: any()) -> { ProcessedCommand :: binary(), State :: any() }. +-callback apply_to_client_message(ImapSession :: pid(), Command :: binary(), SplitBinary :: { Tag :: binary(), Command :: binary(), Data :: binary() }, State :: any()) -> { ProcessedCommand :: binary(), State :: any() }. -callback apply_to_server_message(ImapSession :: pid(), Command :: binary(), State :: any()) -> { ProcessedCommand :: binary(), State :: any() }. -callback imap_data(ResponseToken :: any(), Response :: any(), State :: any()) -> State ::any(). diff --git a/apps/kolab_guam/src/kolab_guam_session.erl b/apps/kolab_guam/src/kolab_guam_session.erl --- a/apps/kolab_guam/src/kolab_guam_session.erl +++ b/apps/kolab_guam/src/kolab_guam_session.erl @@ -27,7 +27,8 @@ %% state record definition -record(state, { listen_socket, socket = undefined, super_pid, tls_config = [], client_implicit_tls = false, client_tls_active = false, server_config = [], - rules_active = [], rules_deciding = [], imap_session = undefined, inflator, deflator }). + rules_active = [], rules_deciding = [], imap_session = undefined, inflator, deflator, buffered_client_data = <<>>, + current_command_split = undefined, command_split_reset_trigger = reset_for_next_client_command }). %% public API start_link(SupervisorPID, ListenSocket, ImapConfig, ImplicitTLS, TLSConfig, Rules) -> gen_server:start_link(?MODULE, [SupervisorPID, ListenSocket, ImapConfig, ImplicitTLS, TLSConfig, Rules], []). @@ -101,7 +102,8 @@ %lager:debug("FROM SERVER: ~s", [Data]), { ModifiedData, CurrentlyActiveRules } = apply_ruleset_serverside(ImapSession, Data, ActiveRules), relay_response(Socket, postprocess_server_data(Deflator, ModifiedData), TLS), - { noreply, State#state{ rules_active = CurrentlyActiveRules } }; + NewSplitCommand = update_split_command_state(ModifiedData, State), + { noreply, State#state{ rules_active = CurrentlyActiveRules, current_command_split = NewSplitCommand } }; handle_info({ 'EXIT', PID, _Reason }, #state { imap_session = PID } = State) -> { stop, normal, State#state{ imap_session = undefined } }; handle_info(Info, State) -> @@ -122,6 +124,30 @@ { ok, State }. %% private API + +%% update_split_command_state updates the split_command being held on to when we get a server response +%% in the case of "transactional" messages (such as authentication) where the client and server enter a bidirectional conversation +%% that is goverened by rules outside the the usual IMAP call/response pattern, we need to wait for the end of the server response +%% since this is relatively expensive due to having to scan the data for the tagged server response, and is not necessary for all other +%% IMAP commands, we shortcircuit when the command does not trigger a "transactional" interaction between client and server, and instead +%% just always reset the split data state at that point +update_split_command_state(Data, #state{ command_split_reset_trigger = reset_on_server_response, current_command_split = CurrentCommandSplit }) -> + case CurrentCommandSplit of + undefined -> undefined; + { Tag, _Command, _Data } -> + case binary:match(Data, <>) of + nomatch -> CurrentCommandSplit; + { 0, _ } -> undefined; + { Start, _ } -> + case binary:at(Data, Start - 1) of + $\n -> undefined; + _ -> CurrentCommandSplit + end + end + end; +update_split_command_state(_Data, _State) -> + undefined. + accept_client(#state{ client_implicit_tls = true, tls_config = TLSConfig, listen_socket = ListenSocket, super_pid = SupervisorPID }) -> AcceptSocket = accept_socket(ListenSocket, SupervisorPID), %% prep for the next listen @@ -150,12 +176,12 @@ close_socket(true, _TLS, Socket) -> ssl:close(Socket); close_socket(_ImplicitTLS, _TLS, Socket) -> gen_tcp:close(Socket). -process_client_data(Socket, Data, #state{ rules_deciding = UndecidedRules, tls_config = TLSConfig, client_tls_active = TLS, rules_active = ActiveRules, socket = Socket, imap_session = ImapSession, inflator = Inflator, deflator = Deflator, server_config = ServerConfig } = State) -> +process_client_data(Socket, Data, #state{ rules_deciding = UndecidedRules, tls_config = TLSConfig, client_tls_active = TLS, rules_active = ActiveRules, socket = Socket, imap_session = ImapSession, inflator = Inflator, deflator = Deflator, server_config = ServerConfig, current_command_split = CurrentCommandSplit } = State) -> %%TODO: multipacket input from clients % TODO: refactor so starttls and compress commands can be made into rules - PreprocessData = preprocess_client_data(Inflator, Data), + PreprocessData = preprocess_client_data(Inflator, Data, State), %lager:info("FROM CLIENT: ~s", [PreprocessData]), - { TLSActive, CurrentSocket, CurrentInflator, CurrentDeflator, CurrentUndecidedRules, CurrentActiveRules } = + { TLSActive, CurrentSocket, CurrentInflator, CurrentDeflator, CurrentUndecidedRules, CurrentActiveRules, DataToBuffer, SplitCommand, SplitResetTrigger } = case check_for_transmission_change_commands(TLS, TLSConfig, PreprocessData, Deflator, Socket) of { socket_upgraded, SSLSocket } -> %% if we have upgraded our socket, then do so to the backend if that hasn't happened auomatically @@ -163,27 +189,39 @@ false -> eimap:starttls(ImapSession, undefined, undefined); _ -> ok end, - { true, SSLSocket, Inflator, Deflator, UndecidedRules, ActiveRules }; + { true, SSLSocket, Inflator, Deflator, UndecidedRules, ActiveRules, <<>>, undefined, undefined }; { compression, NewInflator, NewDeflator } -> eimap:compress(ImapSession), % TODO: make optional - { TLS, Socket, NewInflator, NewDeflator, UndecidedRules, ActiveRules }; + { TLS, Socket, NewInflator, NewDeflator, UndecidedRules, ActiveRules, <<>>, undefined, undefined }; nochange -> %%lager:debug("... now applying rules"), - { ModifiedData, NewUndecidedRules, NewActiveRules } = apply_ruleset_clientside(ImapSession, Socket, PreprocessData, UndecidedRules, ActiveRules), + { ModifiedData, NewSplitCommand, NewSplitResetTrigger, NewUndecidedRules, NewActiveRules, PostAction } = apply_ruleset_clientside(ImapSession, Socket, PreprocessData, CurrentCommandSplit, UndecidedRules, ActiveRules), %%lager:info("The modified data is: ~s", [ModifiedData]), %lager:info("The post-processed data is: ~s", [PostProcessed]), - eimap:passthrough_data(ImapSession, ModifiedData), - { TLS, Socket, Inflator, Deflator, NewUndecidedRules, NewActiveRules} + BufferThisData = + case PostAction of + perform_passthrough -> + eimap:passthrough_data(ImapSession, ModifiedData), + <<>>; + buffer_data -> + Data + end, + { TLS, Socket, Inflator, Deflator, NewUndecidedRules, NewActiveRules, BufferThisData, NewSplitCommand, NewSplitResetTrigger } end, set_socket_active(TLSActive, CurrentSocket), + PrevBuffered = State#state.buffered_client_data, { noreply, State#state{ rules_deciding = CurrentUndecidedRules, rules_active = CurrentActiveRules, socket = CurrentSocket, client_tls_active = TLSActive, - inflator = CurrentInflator, deflator = CurrentDeflator } }. + inflator = CurrentInflator, deflator = CurrentDeflator, + buffered_client_data = <>, + current_command_split = SplitCommand, + command_split_reset_trigger = SplitResetTrigger } }. -preprocess_client_data(undefined, Data) -> - Data; -preprocess_client_data(Z, Data) -> - joined(zlib:inflate(Z, Data), <<>>). +preprocess_client_data(undefined, Data, #state{ buffered_client_data = Buffered }) -> + <>; +preprocess_client_data(Z, Data, #state{ buffered_client_data = Buffered }) -> + Inflated = joined(zlib:inflate(Z, Data), <<>>), + <>. postprocess_server_data(undefined, Data) -> %% we aren't compressing so there is nothing to do @@ -222,27 +260,47 @@ { ModifiedData, ModifiedRuleState } = Module:apply_to_server_message(ImapSession, ServerData, RuleState), apply_next_rule_serverside(ImapSession, ModifiedData, [{ Module, ModifiedRuleState } | ActiveRulesAcc], ActiveRules). -apply_ruleset_clientside(ImapSession, Socket, ClientData, UndecidedRules, CurrentlyActiveRules) -> - { StillUndecided, NewlyActive } = check_undecided(Socket, ClientData, UndecidedRules), +apply_ruleset_clientside(_ImapSession, _Socket, ClientData, _CurrentCommandSplit, [], []) -> + { ClientData, [], [], perform_passthrough, undefined }; +apply_ruleset_clientside(ImapSession, Socket, ClientData, CurrentCommandSplit, UndecidedRules, CurrentlyActiveRules) -> + { PostAction, SplitCommand, SplitResetTrigger } = + case CurrentCommandSplit of + undefined -> + case eimap_utils:split_command_into_components(ClientData) of + { _Tag, <<>>, <<>> } -> { buffer_data, undefined, reset_for_next_client_command }; + { _Tag, Command, _Data } = Split -> { perform_passthrough, Split, when_to_reset_split(Command) } + end; + _ -> { perform_passthrough, CurrentCommandSplit, reset_for_next_client_command } + end, + { StillUndecided, NewlyActive } = check_undecided(Socket, ClientData, SplitCommand, UndecidedRules), ActiveRules = CurrentlyActiveRules ++ NewlyActive, - { ModifiedData, ActiveRulesRun } = apply_next_rule_clientside(ImapSession, ClientData, [], ActiveRules), - { ModifiedData, StillUndecided, ActiveRulesRun }. - -check_undecided(Socket, ClientData, Rules) -> check_next_undecided_rule(Socket, ClientData, Rules, { [], [] }). -check_next_undecided_rule(_Socket, _ClientData, [], Accs) -> Accs; -check_next_undecided_rule(Socket, ClientData, [Rule|Rules], { UndecidedAcc, NewActiveAcc }) -> + %lager:info("Active Rules: ~p", [ActiveRules]), + { ModifiedData, ActiveRulesRun } = apply_next_rule_clientside(ImapSession, ClientData, SplitCommand, [], ActiveRules), + { ModifiedData, SplitCommand, SplitResetTrigger, StillUndecided, ActiveRulesRun, PostAction }. + +when_to_reset_split(<<"AUTHENTICATE">>) -> reset_on_server_response; +when_to_reset_split(<<"authenticate">>) -> reset_on_server_response; +when_to_reset_split(_) -> reset_for_next_client_command. + +check_undecided(Socket, ClientData, undefined, Rules) -> + %% if we do not have a properly split command ... do nothing! + { Rules, [] }; +check_undecided(Socket, ClientData, SplitCommand, Rules) -> check_next_undecided_rule(Socket, ClientData, SplitCommand, Rules, { [], [] }). +check_next_undecided_rule(_Socket, _ClientData, _SplitCommand, [], Accs) -> Accs; +check_next_undecided_rule(Socket, ClientData, SplitCommand, [Rule|Rules], { UndecidedAcc, NewActiveAcc }) -> { Module, RuleState } = Rule, %%lager:debug("Does ~p apply with state ~p? let's find out!", [Module, RuleState]), - check_next_undecided_rule(Socket, ClientData, Rules, applies(Module, Module:applies(Socket, ClientData, RuleState), UndecidedAcc, NewActiveAcc)). + Application = Module:applies(Socket, ClientData, SplitCommand, RuleState), + check_next_undecided_rule(Socket, ClientData, SplitCommand, Rules, applies(Module, Application, UndecidedAcc, NewActiveAcc)). applies(Module, { true, RuleState }, UndecidedAcc, NewActiveAcc) -> { UndecidedAcc, [{ Module, RuleState }|NewActiveAcc] }; applies(_Module, { false, _RuleState }, UndecidedAcc, NewActiveAcc) -> { UndecidedAcc, NewActiveAcc }; applies(Module, { notyet, RuleState }, UndecidedAcc, NewActiveAcc) -> { [{ Module, RuleState }|UndecidedAcc], NewActiveAcc }. -apply_next_rule_clientside(_ImapSession, ClientData, ActiveRulesAcc, []) -> { ClientData, lists:reverse(ActiveRulesAcc) }; -apply_next_rule_clientside(ImapSession, ClientData, ActiveRulesAcc, [{ Module, RuleState }|Rules]) -> - { Data, NewState } = Module:apply_to_client_message(ImapSession, ClientData, RuleState), - apply_next_rule_clientside(ImapSession, Data, [{ Module, NewState } | ActiveRulesAcc], Rules). +apply_next_rule_clientside(_ImapSession, ClientData, _SplitCommand, ActiveRulesAcc, []) -> { ClientData, lists:reverse(ActiveRulesAcc) }; +apply_next_rule_clientside(ImapSession, ClientData, SplitCommand, ActiveRulesAcc, [{ Module, RuleState }|Rules]) -> + { Data, NewState } = Module:apply_to_client_message(ImapSession, ClientData, SplitCommand, RuleState), + apply_next_rule_clientside(ImapSession, Data, SplitCommand, [{ Module, NewState } | ActiveRulesAcc], Rules). relay_response(Socket, Data, false) -> %lager:debug("Sending over non-secure socket ..."), diff --git a/apps/kolab_guam/src/rules/kolab_guam_rule_filter_groupware.erl b/apps/kolab_guam/src/rules/kolab_guam_rule_filter_groupware.erl --- a/apps/kolab_guam/src/rules/kolab_guam_rule_filter_groupware.erl +++ b/apps/kolab_guam/src/rules/kolab_guam_rule_filter_groupware.erl @@ -16,7 +16,7 @@ %% along with this program. If not, see . -module(kolab_guam_rule_filter_groupware). --export([new/1, applies/3, imap_data/3, apply_to_client_message/3, apply_to_server_message/3]). +-export([new/1, applies/4, imap_data/3, apply_to_client_message/4, apply_to_server_message/3]). -behavior(kolab_guam_rule). -record(state, { blacklist = [], tag = <<>>, active = false, last_chunk = <<>>, @@ -24,14 +24,13 @@ new(_Config) -> #state { blacklist = undefined }. -applies(_ConnectionDetails, Buffer, State) -> - { _Tag, Command, Data } = eimap_utils:split_command_into_components(Buffer), - %lager:debug("********** Checking ...~n Command: ~s ~s", [Command, Data]), - { apply_if_id_matches(Command, Data, State), State }. +applies(_ConnectionDetails, _Buffer, { _Tag, Command, Data }, State) -> + Applies = apply_if_id_matches(Command, Data, State), + %lager:debug("********** Checking ...~n Command: ~s ~s, Result ~p", [Command, Data, Applies]), + { Applies, State }. -apply_to_client_message(ImapSession, Buffer, State) -> - { Tag, Command, Data } = eimap_utils:split_command_into_components(Buffer), - { Active, StateTag }= +apply_to_client_message(ImapSession, Buffer, { Tag, Command, Data }, State) -> + { Active, StateTag } = case is_triggering_command(Command, Data, State) of true -> fetch_metadata(ImapSession, State), { true, Tag }; _ -> { false, <<>> }