aboutsummaryrefslogtreecommitdiff
path: root/src/mod_c2s_debug.erl
diff options
context:
space:
mode:
Diffstat (limited to 'src/mod_c2s_debug.erl')
-rw-r--r--src/mod_c2s_debug.erl205
1 files changed, 205 insertions, 0 deletions
diff --git a/src/mod_c2s_debug.erl b/src/mod_c2s_debug.erl
new file mode 100644
index 000000000..41c6e9dcd
--- /dev/null
+++ b/src/mod_c2s_debug.erl
@@ -0,0 +1,205 @@
+%% Usage:
+%% In config file:
+%% {mod_c2s_debug, [{logdir, "/tmp/xmpplogs"}]},
+%% It is possible to limit to a specific jid with option:
+%% {users, ["test@localhost"]}
+%% Warning: Only works with a single JID for now.
+%%
+%% Start from Erlang shell:
+%% mod_c2s_debug:start("localhost", []).
+%% mod_c2s_debug:stop("localhost").
+%%
+%% Warning: Only one module for the debug handler can be defined.
+-module(mod_c2s_debug).
+-author('mremond@process-one.net').
+
+-behaviour(gen_mod).
+-behavior(gen_server).
+
+-export([start/2, start_link/2, stop/1,
+ debug_start/3, debug_stop/2, log_packet/4, log_packet/5]).
+%% gen_server callbacks
+-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
+ terminate/2, code_change/3]).
+
+-include("ejabberd.hrl").
+-include("jlib.hrl").
+-include("ejabberd_c2s.hrl").
+
+-record(modstate, {host, logdir, pid, iodevice, user}).
+-record(clientinfo, {pid, jid, auth_module, ip}).
+
+-define(SUPERVISOR, ejabberd_sup).
+-define(PROCNAME, c2s_debug).
+
+%%====================================================================
+%% gen_mod callbacks
+%%====================================================================
+start(Host, Opts) ->
+ Proc = gen_mod:get_module_proc(Host, ?PROCNAME),
+ Spec = {Proc, {?MODULE, start_link, [Host, Opts]},
+ transient, 2000, worker, [?MODULE]},
+ supervisor:start_child(?SUPERVISOR, Spec).
+
+stop(Host) ->
+ Proc = gen_mod:get_module_proc(Host, ?PROCNAME),
+ gen_server:call(Proc, stop),
+ supervisor:delete_child(?SUPERVISOR, Proc).
+
+start_link(Host, Opts) ->
+ Proc = gen_mod:get_module_proc(Host, ?PROCNAME),
+ gen_server:start_link({local, Proc}, ?MODULE, [Host, Opts], []).
+
+%%====================================================================
+%% Hooks
+%%====================================================================
+
+%% Debug handled by another module... Do nothing:
+debug_start(_Status, Pid, C2SState) ->
+ Host = C2SState#state.server,
+ Proc = gen_mod:get_module_proc(Host, ?PROCNAME),
+
+ JID = jlib:jid_to_string(C2SState#state.jid),
+ AuthModule = C2SState#state.auth_module,
+ IP = C2SState#state.ip,
+ ClientInfo = #clientinfo{pid = Pid, jid = JID, auth_module = AuthModule, ip = IP},
+
+ gen_server:call(Proc, {debug_start, ClientInfo}).
+
+debug_stop(Pid, C2SState) ->
+ Host = C2SState#state.server,
+ Proc = gen_mod:get_module_proc(Host, ?PROCNAME),
+ gen_server:cast(Proc, {debug_stop, Pid}).
+
+log_packet(false, _FromJID, _ToJID, _Packet) ->
+ ok;
+log_packet(true, FromJID, ToJID, Packet) ->
+ Host = FromJID#jid.lserver,
+ Proc = gen_mod:get_module_proc(Host, ?PROCNAME),
+ gen_server:cast(Proc, {addlog, {"Send", FromJID, ToJID, Packet}}).
+log_packet(false, _JID, _FromJID, _ToJID, _Packet) ->
+ ok;
+log_packet(true, JID, FromJID, ToJID, Packet) ->
+ Host = JID#jid.lserver,
+ Proc = gen_mod:get_module_proc(Host, ?PROCNAME),
+ gen_server:cast(Proc, {addlog, {"Receive", FromJID, ToJID, Packet}}).
+
+%%====================================================================
+%% gen_server callbacks
+%%====================================================================
+init([Host, Opts]) ->
+ ?INFO_MSG("Starting c2s debug module for: ~p", [Host]),
+ MyHost = gen_mod:get_opt_host(Host, Opts, "c2s_debug.@HOST@"),
+ ejabberd_hooks:add(c2s_debug_start_hook, Host,
+ ?MODULE, debug_start, 50),
+ ejabberd_hooks:add(c2s_debug_stop_hook, Host,
+ ?MODULE, debug_stop, 50),
+ ejabberd_hooks:add(user_send_packet, Host, ?MODULE, log_packet, 50),
+ ejabberd_hooks:add(user_receive_packet, Host, ?MODULE, log_packet, 50),
+
+ Logdir = gen_mod:get_opt(logdir, Opts, "/tmp/xmpplogs/"),
+ %% TODO: We currently support only one user. Support multiple users
+ SJID = case gen_mod:get_opt(users, Opts, undefined) of
+ undefined ->
+ undefined;
+ [User1|_] ->
+ User1
+ end,
+ make_dir_rec(Logdir),
+ {ok, #modstate{host = MyHost, logdir = Logdir, user = jlib:string_to_jid(SJID)}}.
+
+terminate(_Reason, #modstate{host = Host}) ->
+ ?INFO_MSG("Stopping c2s debug module for: ~s", [Host]),
+ ejabberd_hooks:delete(c2s_debug_start_hook, Host,
+ ?MODULE, debug_start, 50),
+ ejabberd_hooks:delete(c2s_debug_stop_hook, Host,
+ ?MODULE, debug_stop, 50),
+ ejabberd_hooks:delete(user_send_packet, Host, ?MODULE, log_packet, 50).
+
+%% No specific user: Select the first new user to connect
+handle_call({debug_start, ClientInfo}, _From, #modstate{pid=undefined, user=undefined} = State) ->
+ Pid = ClientInfo#clientinfo.pid,
+ ?INFO_MSG("Debug started for PID:~p", [Pid]),
+
+ JID = ClientInfo#clientinfo.jid,
+ AuthModule = ClientInfo#clientinfo.auth_module,
+ IP = ClientInfo#clientinfo.ip,
+
+ {ok, IOD} = file:open(filename(State#modstate.logdir), [append]),
+ Line = io_lib:format("~s - Session open~nJID: ~s~nAuthModule: ~p~nIP: ~p~n",
+ [timestamp(), JID, AuthModule, IP]),
+ file:write(IOD, Line),
+
+ {reply, true, State#modstate{pid = Pid, iodevice = IOD}};
+%% Targeting a specific user
+handle_call({debug_start, ClientInfo}, _From, #modstate{pid=undefined, user=JID} = State) ->
+ ClientJID = ClientInfo#clientinfo.jid,
+ case jlib:jid_remove_resource(jlib:string_to_jid(ClientJID)) of
+ JID ->
+ Pid = ClientInfo#clientinfo.pid,
+ ?INFO_MSG("Debug started for PID:~p", [Pid]),
+ AuthModule = ClientInfo#clientinfo.auth_module,
+ IP = ClientInfo#clientinfo.ip,
+
+ {ok, IOD} = file:open(filename(State#modstate.logdir), [append]),
+ Line = io_lib:format("~s - Session open~nJID: ~s~nAuthModule: ~p~nIP: ~p~n",
+ [timestamp(), ClientJID, AuthModule, IP]),
+ file:write(IOD, Line),
+ {reply, true, State#modstate{pid = Pid, iodevice = IOD}};
+ _ ->
+ {reply, false, State}
+ end;
+handle_call({debug_start, _ClientInfo}, _From, State) ->
+ {reply, false, State};
+handle_call(stop, _From, State) ->
+ {stop, normal, ok, State};
+handle_call(_Req, _From, State) ->
+ {reply, {error, badarg}, State}.
+
+handle_cast({addlog, _}, #modstate{iodevice=undefined} = State) ->
+ {noreply, State};
+handle_cast({addlog, {Direction, FromJID, ToJID, Packet}}, #modstate{iodevice=IOD} = State) ->
+ LogEntry = io_lib:format("=====~n~s - ~s~nFrom: ~s~nTo: ~s~n~s~n", [timestamp(), Direction,
+ jlib:jid_to_string(FromJID),
+ jlib:jid_to_string(ToJID),
+ xml:element_to_string(Packet)]),
+ file:write(IOD, LogEntry),
+ {noreply, State};
+handle_cast({debug_stop, Pid}, #modstate{pid=Pid, iodevice=IOD} = State) ->
+ Line = io_lib:format("=====~n~s - Session closed~n",
+ [timestamp()]),
+ file:write(IOD, Line),
+
+ file:close(IOD),
+ {noreply, State#modstate{pid = undefined, iodevice=undefined}};
+handle_cast(_Msg, State) ->
+ {noreply, State}.
+
+handle_info(_Info, State) ->
+ {noreply, State}.
+
+code_change(_OldVsn, State, _Extra) ->
+ {ok, State}.
+
+%% Generate filename
+filename(LogDir) ->
+ Filename = lists:flatten(timestamp()) ++ "-c2s.log",
+ filename:join([LogDir, Filename]).
+
+%% Generate timestamp
+timestamp() ->
+ {Y,Mo,D} = erlang:date(),
+ {H,Mi,S} = erlang:time(),
+ io_lib:format("~4.4.0w~2.2.0w~2.2.0w-~2.2.0w~2.2.0w~2.2.0w", [Y,Mo,D,H,Mi,S]).
+
+%% Create dir recusively
+make_dir_rec(Dir) ->
+ case file:read_file_info(Dir) of
+ {ok, _} ->
+ ok;
+ {error, enoent} ->
+ DirS = filename:split(Dir),
+ DirR = lists:sublist(DirS, length(DirS)-1),
+ make_dir_rec(filename:join(DirR)),
+ file:make_dir(Dir)
+ end.