diff --git a/app.config b/app.config index 2415e9d..292e87d 100644 --- a/app.config +++ b/app.config @@ -1,51 +1,65 @@ %% -*- tab-width: 4;erlang-indent-level: 4;indent-tabs-mode: nil-*- [ { kolab_guam, [ { imap_servers, [ { default, [ { host, "192.168.56.101" }, { port, 143 }, - { tls, false } + { tls, no } + ] + }, + { kolabsys , [ + { host, "imap.kolabsys.com" }, + { port, 143 }, + { tls, starttls } ] } ] }, { listeners, [ { default, [ { port, 1143 }, { imap_server, default }, { rules, [ { filter_groupware, [] } ] }, { tls_config, [ { certfile, "/etc/ssl/sample.pem" } ] } ] }, { default_tls, [ { port, 1993 }, { imap_server, default }, { rules, [ { filter_groupware, [] } ] }, { implicit_tls, true }, { tls_config, [ { certfile, "/etc/ssl/sample.cert" }, { keyfile, "/etc/ssl/sample.key" } ] } ] + }, + { kolabsys, [ + { port, 1994 }, + { imap_server, kolabsys }, + { rules, [ + ] + } + ] } ] } ] }, { lager, [ { handlers, [ { lager_console_backend, debug }, { lager_file_backend, [ { file, "log/error.log"}, { level, error } ] }, { lager_file_backend, [ { file, "log/console.log"}, { level, debug } ] } ] } ] } ]. diff --git a/apps/kolab_guam/src/kolab_guam_session.erl b/apps/kolab_guam/src/kolab_guam_session.erl index 0cac0d7..1fa49de 100644 --- a/apps/kolab_guam/src/kolab_guam_session.erl +++ b/apps/kolab_guam/src/kolab_guam_session.erl @@ -1,255 +1,243 @@ %% 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_session). -behaviour(gen_server). -include_lib("eimap/src/eimap.hrl"). %% API -export([ start_link/6 ]). %% gen_server callbacks -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). %% state record definition -record(state, { socket, super_pid, tls_config = [], client_implicit_tls = false, client_tls_active = false, server_config = #eimap_server_config{}, - rules_active = [], rules_deciding = [], imap_session, inflator, deflator, server_inflator, server_deflator }). + rules_active = [], rules_deciding = [], imap_session, inflator, deflator }). %% public API start_link(SupervisorPID, ListenSocket, ImapConfig, ImplicitTLS, TLSConfig, Rules) -> gen_server:start_link(?MODULE, [SupervisorPID, ListenSocket, ImapConfig, ImplicitTLS, TLSConfig, Rules], []). %% gen_server API init([SupervisorPID, ListenSocket, ImapConfig, ImplicitTLS, TLSConfig, Rules]) -> %% accepting a connection is blocking .. so do it async %% lager:debug("Starting a session handler on socket ~p for listener ~p", [ListenSocket, SupervisorPID]), ActiveRules = init_rules(Rules), gen_server:cast(self(), accept), %% lager:debug("Rules are ~p from ~p", [ActiveRules, Rules]), { ok, #state{ socket = ListenSocket, super_pid = SupervisorPID, client_implicit_tls = ImplicitTLS, tls_config = TLSConfig, server_config = ImapConfig, rules_deciding = ActiveRules } }. handle_call(_Request, _From, State) -> { reply, ok, State }. handle_cast(accept, State = #state{ socket = ListenSocket, server_config = ServerConfig }) -> { ok, AcceptSocket, TLSActive } = accept_client(ListenSocket, State), { ok, ImapSession } = eimap:start_link(ServerConfig), eimap:start_passthrough(ImapSession, self()), eimap:connect(ImapSession), { noreply, State#state{ socket = AcceptSocket, imap_session = ImapSession, client_tls_active = TLSActive } }; handle_cast(_Msg, State) -> { noreply, State }. handle_info({ tcp_closed, _Socket }, State) -> %lager:debug("Client closed socket"), { stop, normal, State }; handle_info({ tcp_error, _Socket, _Error }, State) -> %lager:debug("Socket error"), { stop, normal, State }; handle_info({ tcp, Socket, Data }, #state{ client_tls_active = false } = State) -> %lager:debug("Data coming in from client over TCP"), process_client_data(Socket, Data, State); handle_info({ ssl, Socket, Data }, State) -> %lager:debug("Data coming in from client over SSL, ~p", [Data]), process_client_data(Socket, Data, State); -handle_info({ imap_server_response, Data }, #state{ socket = Socket, client_tls_active = TLS, server_inflator = Inflator, server_deflator = Deflator, rules_active = ActiveRules } = State) -> - { ProcessedData, NewState } = preprocess_server_data(Inflator, Data, State), - { ModifiedData, NewActiveRules } = apply_ruleset_serverside(ProcessedData, ActiveRules), +handle_info({ imap_server_response, Data }, #state{ socket = Socket, client_tls_active = TLS, deflator = Deflator, rules_active = ActiveRules } = State) -> %lager:debug("FROM SERVER: ~s", [Data]), + { ModifiedData, CurrentlyActiveRules } = apply_ruleset_serverside(Data, ActiveRules), relay_response(Socket, postprocess_server_data(Deflator, ModifiedData), TLS), - { noreply, NewState#state{ rules_active = NewActiveRules } }; + { noreply, State#state{ rules_active = CurrentlyActiveRules } }; handle_info(Info, State) -> lager:debug("Received unexpected info... ~p", [Info]), { noreply, State }. -terminate(_Reason, #state{ inflator = Inflator, deflator = Deflator, server_inflator = ServerInflator, server_deflator = ServerDeflator }) -> +terminate(_Reason, #state{ inflator = Inflator, deflator = Deflator, socket = Socket, client_tls_active = TLS }) -> %lager:debug("Termination!~p", [self()]), close_zlib_handle(Inflator), close_zlib_handle(Deflator), - close_zlib_handle(ServerInflator), - close_zlib_handle(ServerDeflator), + close_socket(TLS, Socket), ok. code_change(_OldVsn, State, _Extra) -> { ok, State }. %% private API accept_client(ListenSocket, #state{ client_implicit_tls = true, super_pid = SupervisorPID }) -> { ok, AcceptSocket } = ssl:transport_accept(ListenSocket), ok = ssl:ssl_accept(AcceptSocket), ok = ssl:setopts(AcceptSocket, [{ active, once }, { mode, binary }]), %% start a new accepting process to replace this one, which is now i use supervisor:start_child(SupervisorPID, []), ok = ssl:setopts(ListenSocket, [{ active, once }, { mode, binary }]), % lager:info("~p All done!", [self()]), { ok, AcceptSocket, true }; accept_client(ListenSocket, #state{ super_pid = SupervisorPID }) -> { ok, AcceptSocket } = gen_tcp:accept(ListenSocket), ok = inet:setopts(AcceptSocket, [{ active, once }, { mode, binary }]), %% start a new accepting process to replace this one, which is now i use supervisor:start_child(SupervisorPID, []), ok = inet:setopts(ListenSocket, [{ active, once }]), { ok, AcceptSocket, false }. close_zlib_handle(undefined) -> ok; close_zlib_handle(Z) -> zlib:close(Z). -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_deflator = ServerDeflator } = State) -> +close_socket(_TLS, undefined) -> ok; +close_socket(true, Socket) -> ssl:close(Socket); +close_socket(_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 } = State) -> %%TODO: multipacket input from clients + % TODO: refactor so starttls and compress commands can be made into rules PreprocessData = preprocess_client_data(Inflator, Data), %lager:info("FROM CLIENT: ~s", [PreprocessData]), - %%lager:debug("... now applying rules"), - { ModifiedData, NewUndecidedRules, NewActiveRules } = apply_ruleset_clientside(Socket, PreprocessData, UndecidedRules, ActiveRules), - %%lager:info("The modified data is: ~s", [ModifiedData]), - { TLSActive, CurrentSocket } = - case check_tls_state(TLS, TLSConfig, PreprocessData, ServerDeflator, Socket) of - { upgraded, SSLSocket } -> { true, SSLSocket }; - _ -> - PostProcessed = postprocess_client_data(Deflator, ModifiedData), + { TLSActive, CurrentSocket, CurrentInflator, CurrentDeflator, CurrentUndecidedRules, CurrentActiveRules } = + case check_for_transmission_change_commands(TLS, TLSConfig, PreprocessData, Deflator, Socket) of + { socket_upgraded, SSLSocket } -> { true, SSLSocket, Inflator, Deflator, UndecidedRules, ActiveRules }; + { compression, NewInflator, NewDeflator } -> { TLS, Socket, NewInflator, NewDeflator, UndecidedRules, ActiveRules }; + nochange -> + %%lager:debug("... now applying rules"), + { ModifiedData, NewUndecidedRules, NewActiveRules } = apply_ruleset_clientside(Socket, PreprocessData, UndecidedRules, ActiveRules), + %%lager:info("The modified data is: ~s", [ModifiedData]), %lager:info("The post-processed data is: ~s", [PostProcessed]), - eimap:passthrough_data(ImapSession, PostProcessed), - { TLS, Socket } + eimap:passthrough_data(ImapSession, ModifiedData), + { TLS, Socket, Inflator, Deflator, NewUndecidedRules, NewActiveRules} end, set_socket_active(TLSActive, CurrentSocket), - { noreply, State#state{ rules_deciding = NewUndecidedRules, rules_active = NewActiveRules, socket = CurrentSocket, client_tls_active = TLSActive } }. + { noreply, State#state{ rules_deciding = CurrentUndecidedRules, rules_active = CurrentActiveRules, + socket = CurrentSocket, client_tls_active = TLSActive, + inflator = CurrentInflator, deflator = CurrentDeflator } }. preprocess_client_data(undefined, Data) -> Data; preprocess_client_data(Z, Data) -> - inflate(Z, Data). - -postprocess_client_data(undefined, Data) -> - %% we aren't compressing so there is nothing to do - Data; -postprocess_client_data(Z, Data) -> - %% compression is happening .. but eimap does not yet support that <-- TODO - deflate(Z, Data). - -preprocess_server_data(undefined, Data, State) -> - case binary:match(Data, <<"OK DEFLATE active">>, [ { scope, { 0, min(byte_size(Data), 30) } } ]) of - nomatch -> - %% we aren't compressing so there is nothing to do - { Data, State }; - _ -> - %% create an inflate/deflate pair for use with the client - Inflator = zlib:open(), - ok = zlib:inflateInit(Inflator, -15), - Deflator = zlib:open(), - ok = zlib:deflateInit(Deflator, 1, deflated, -15, 8, default), - ServerInflator = zlib:open(), - - %% create an inflate/defate pair for use with the server - ok = zlib:inflateInit(ServerInflator, -15), - ServerDeflator = zlib:open(), - ok = zlib:deflateInit(ServerDeflator, 1, deflated, -15, 8, default), - - %% return data with State modified to include the zlib sessions - { Data, State#state{ inflator = Inflator, deflator = Deflator, server_inflator = ServerInflator, server_deflator = ServerDeflator } } - end; -preprocess_server_data(Z, Data, State) -> - { inflate(Z, Data), State }. + joined(zlib:inflate(Z, Data), <<>>). postprocess_server_data(undefined, Data) -> %% we aren't compressing so there is nothing to do Data; postprocess_server_data(Z, Data) -> %% compression is happening .. but eimap does not yet support that <-- TODO - deflate(Z, Data). - -deflate(Z, Data) -> - [Deflated] = zlib:deflate(Z, Data, sync), - Deflated. + joined(zlib:deflate(Z, Data, sync), <<>>). -inflate(Z, Data) -> - [Inflated] = zlib:inflate(Z, Data), - Inflated. +joined([], Binary) -> Binary; +joined([H|Rest], Binary) -> joined(Rest, <>). init_rules(RuleConfig) -> init_rule(RuleConfig, []). init_rule([], Acc) -> Acc; init_rule([{ RuleName, Config }|RuleConfig], Acc) -> Module = full_rule_name(RuleName), %% we try to new the module, but if something goes wrong, e.g. it does not exist, %% then we skip this config block because it is BROKEN try Module:new(Config) of ModuleState -> init_rule(RuleConfig, [{ Module, ModuleState }|Acc]) catch Type:Error -> lager:warning("Could not create rule for ~p due to failure: ~p ~p", [RuleName, Type, Error]), init_rule(RuleConfig, Acc) end; init_rule([_|RuleConfig], Acc) -> init_rule(RuleConfig, Acc). full_rule_name(Module) when is_atom(Module) -> list_to_atom("kolab_guam_rule_" ++ atom_to_list(Module)). apply_ruleset_serverside(ServerData, CurrentlyActiveRules) -> apply_next_rule_serverside(ServerData, [], CurrentlyActiveRules). apply_next_rule_serverside(ServerData, ActiveRulesAcc, []) -> { ServerData, lists:reverse(ActiveRulesAcc) }; apply_next_rule_serverside(ServerData, ActiveRulesAcc, [{ Module, RuleState } | ActiveRules]) -> %TODO: allow rules to remove themselves from the action during serverside processing? { ModifiedData, ModifiedRuleState } = Module:apply_to_server_message(ServerData, RuleState), apply_next_rule_serverside(ModifiedData, [{ Module, ModifiedRuleState } | ActiveRulesAcc], ActiveRules). apply_ruleset_clientside(Socket, ClientData, UndecidedRules, CurrentlyActiveRules) -> { StillUndecided, NewlyActive } = check_undecided(Socket, ClientData, UndecidedRules), ActiveRules = CurrentlyActiveRules ++ NewlyActive, { ModifiedData, ActiveRulesRun } = apply_next_rule_clientside(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 }) -> %%TODO: allow RuleState to be change here { 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)). 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(ClientData, ActiveRulesAcc, []) -> { ClientData, lists:reverse(ActiveRulesAcc) }; apply_next_rule_clientside(ClientData, ActiveRulesAcc, [{ Module, RuleState }|Rules]) -> %%FIXME: new state (if any) is not being applied { Data, NewState } = Module:apply_to_client_message(ClientData, RuleState), apply_next_rule_clientside(Data, [{ Module, NewState } | ActiveRulesAcc], Rules). relay_response(Socket, Data, false) -> %lager:debug("Sending over non-secure socket ..."), gen_tcp:send(Socket, Data); relay_response(Socket, Data, _TLS) -> %lager:debug("Sending over TLS!"), ssl:send(Socket, Data). -check_tls_state(true, _TLSConfig, _Buffer, _Deflator, _Socket) -> - nochange; -check_tls_state(false, TLSConfig, Buffer, Deflator, Socket) -> +check_for_transmission_change_commands(TLS, TLSConfig, Buffer, Deflator, Socket) -> {Tag, Command, _Data } = eimap_utils:split_command_into_components(Buffer), - case Command =:= <<"STARTTLS">> of - true -> - Response = <>, - relay_response(Socket, postprocess_server_data(Deflator, Response), false), - { ok, SSLSocket } = ssl:ssl_accept(Socket, TLSConfig), - { upgraded, SSLSocket }; - _ -> nochange + case check_tls_state(TLS, TLSConfig, Command, Deflator, Socket, Tag) of + nochange -> check_compress_request(Deflator, Command, Socket, TLS, Tag); + Response -> Response end. +check_tls_state(false, TLSConfig, <<"STARTTLS">>, Deflator, Socket, Tag) -> start_client_tls(TLSConfig, Deflator, Socket, Tag); +check_tls_state(false, TLSConfig, <<"starttls">>, Deflator, Socket, Tag) -> start_client_tls(TLSConfig, Deflator, Socket, Tag); +check_tls_state(_TLS, _TLSConfig, _Buffer, _Deflator, _Socket, _Tag) -> nochange. + +start_client_tls(TLSConfig, Deflator, Socket, Tag) -> + Response = <>, + relay_response(Socket, postprocess_server_data(Deflator, Response), false), + { ok, SSLSocket } = ssl:ssl_accept(Socket, TLSConfig), + { socket_upgraded, SSLSocket }. + +check_compress_request(undefined, <<"COMPRESS">>, Socket, TLS, Tag) -> start_client_compression(Socket, TLS, Tag); +check_compress_request(undefined, <<"compress">>, Socket, TLS, Tag) -> start_client_compression(Socket, TLS, Tag); +check_compress_request(_Deflator, _Command, _Socket, _TLS, _Tag) -> nochange. + +start_client_compression(Socket, TLS, Tag) -> + Response = <>, + relay_response(Socket, postprocess_server_data(undefined, Response), TLS), + %% create an inflate/deflate pair for use with the client + Inflator = zlib:open(), + ok = zlib:inflateInit(Inflator, -15), + Deflator = zlib:open(), + ok = zlib:deflateInit(Deflator, 1, deflated, -15, 8, default), + { compression, Inflator, Deflator }. + + set_socket_active(true, Socket) -> ssl:setopts(Socket, [{ active, once }]); set_socket_active(_, Socket) -> inet:setopts(Socket, [{ active, once }]).