aboutsummaryrefslogtreecommitdiff
path: root/src/ejabberd_http_poll.erl
diff options
context:
space:
mode:
Diffstat (limited to 'src/ejabberd_http_poll.erl')
-rw-r--r--src/ejabberd_http_poll.erl429
1 files changed, 429 insertions, 0 deletions
diff --git a/src/ejabberd_http_poll.erl b/src/ejabberd_http_poll.erl
new file mode 100644
index 000000000..bc974da25
--- /dev/null
+++ b/src/ejabberd_http_poll.erl
@@ -0,0 +1,429 @@
+%%%----------------------------------------------------------------------
+%%% File : ejabberd_http_poll.erl
+%%% Author : Alexey Shchepin <alexey@process-one.net>
+%%% Purpose : HTTP Polling support (XEP-0025)
+%%% Created : 4 Mar 2004 by Alexey Shchepin <alexey@process-one.net>
+%%%
+%%%
+%%% ejabberd, Copyright (C) 2002-2013 ProcessOne
+%%%
+%%% 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 2 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, write to the Free Software
+%%% Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
+%%% 02111-1307 USA
+%%%
+%%%----------------------------------------------------------------------
+
+-module(ejabberd_http_poll).
+
+-author('alexey@process-one.net').
+
+-behaviour(gen_fsm).
+
+%% External exports
+-export([start_link/3, init/1, handle_event/3,
+ handle_sync_event/4, code_change/4, handle_info/3,
+ terminate/3, send/2, setopts/2, sockname/1, peername/1,
+ controlling_process/2, close/1, process/2]).
+
+-include("ejabberd.hrl").
+-include("logger.hrl").
+
+-include("jlib.hrl").
+
+-include("ejabberd_http.hrl").
+
+-record(http_poll, {id :: pid() | binary(), pid :: pid()}).
+
+-type poll_socket() :: #http_poll{}.
+-export_type([poll_socket/0]).
+
+-record(state,
+ {id, key, socket, output = <<"">>, input = <<"">>,
+ waiting_input = false, last_receiver, http_poll_timeout,
+ timer}).
+
+%-define(DBGFSM, true).
+
+-ifdef(DBGFSM).
+
+-define(FSMOPTS, [{debug, [trace]}]).
+
+-else.
+
+-define(FSMOPTS, []).
+
+-endif.
+
+-define(HTTP_POLL_TIMEOUT, 300).
+
+-define(CT,
+ {<<"Content-Type">>, <<"text/xml; charset=utf-8">>}).
+
+-define(BAD_REQUEST,
+ [?CT, {<<"Set-Cookie">>, <<"ID=-3:0; expires=-1">>}]).
+
+%%%----------------------------------------------------------------------
+%%% API
+%%%----------------------------------------------------------------------
+start(ID, Key, IP) ->
+ mnesia:create_table(http_poll,
+ [{ram_copies, [node()]},
+ {attributes, record_info(fields, http_poll)}]),
+ supervisor:start_child(ejabberd_http_poll_sup, [ID, Key, IP]).
+
+start_link(ID, Key, IP) ->
+ gen_fsm:start_link(?MODULE, [ID, Key, IP], ?FSMOPTS).
+
+send({http_poll, FsmRef, _IP}, Packet) ->
+ gen_fsm:sync_send_all_state_event(FsmRef,
+ {send, Packet}).
+
+setopts({http_poll, FsmRef, _IP}, Opts) ->
+ case lists:member({active, once}, Opts) of
+ true ->
+ gen_fsm:send_all_state_event(FsmRef,
+ {activate, self()});
+ _ -> ok
+ end.
+
+sockname(_Socket) -> {ok, {{0, 0, 0, 0}, 0}}.
+
+peername({http_poll, _FsmRef, IP}) -> {ok, IP}.
+
+controlling_process(_Socket, _Pid) -> ok.
+
+close({http_poll, FsmRef, _IP}) ->
+ catch gen_fsm:sync_send_all_state_event(FsmRef, close).
+
+process([],
+ #request{data = Data, ip = IP} = _Request) ->
+ case catch parse_request(Data) of
+ {ok, ID1, Key, NewKey, Packet} ->
+ ID = if
+ (ID1 == <<"0">>) or (ID1 == <<"mobile">>) ->
+ NewID = sha:sha(term_to_binary({now(), make_ref()})),
+ {ok, Pid} = start(NewID, <<"">>, IP),
+ mnesia:transaction(
+ fun() ->
+ mnesia:write(#http_poll{id = NewID, pid = Pid})
+ end),
+ NewID;
+ true ->
+ ID1
+ end,
+ case http_put(ID, Key, NewKey, Packet) of
+ {error, not_exists} ->
+ {200, ?BAD_REQUEST, <<"">>};
+ {error, bad_key} ->
+ {200, ?BAD_REQUEST, <<"">>};
+ ok ->
+ receive
+ after 100 -> ok
+ end,
+ case http_get(ID) of
+ {error, not_exists} ->
+ {200, ?BAD_REQUEST, <<"">>};
+ {ok, OutPacket} ->
+ if
+ ID == ID1 ->
+ Cookie = <<"ID=", ID/binary, "; expires=-1">>,
+ {200, [?CT, {<<"Set-Cookie">>, Cookie}],
+ OutPacket};
+ ID1 == <<"mobile">> ->
+ {200, [?CT], [ID, $\n, OutPacket]};
+ true ->
+ Cookie = <<"ID=", ID/binary, "; expires=-1">>,
+ {200, [?CT, {<<"Set-Cookie">>, Cookie}],
+ OutPacket}
+ end
+ end
+ end;
+ _ ->
+ HumanHTMLxmlel = get_human_html_xmlel(),
+ {200, [?CT, {<<"Set-Cookie">>, <<"ID=-2:0; expires=-1">>}], HumanHTMLxmlel}
+ end;
+process(_, _Request) ->
+ {400, [],
+ #xmlel{name = <<"h1">>, attrs = [],
+ children = [{xmlcdata, <<"400 Bad Request">>}]}}.
+
+%% Code copied from mod_http_bind.erl and customized
+get_human_html_xmlel() ->
+ Heading = <<"ejabberd ",
+ (iolist_to_binary(atom_to_list(?MODULE)))/binary>>,
+ #xmlel{name = <<"html">>,
+ attrs =
+ [{<<"xmlns">>, <<"http://www.w3.org/1999/xhtml">>}],
+ children =
+ [#xmlel{name = <<"head">>, attrs = [],
+ children =
+ [#xmlel{name = <<"title">>, attrs = [],
+ children = [{xmlcdata, Heading}]}]},
+ #xmlel{name = <<"body">>, attrs = [],
+ children =
+ [#xmlel{name = <<"h1">>, attrs = [],
+ children = [{xmlcdata, Heading}]},
+ #xmlel{name = <<"p">>, attrs = [],
+ children =
+ [{xmlcdata, <<"An implementation of ">>},
+ #xmlel{name = <<"a">>,
+ attrs =
+ [{<<"href">>,
+ <<"http://xmpp.org/extensions/xep-0025.html">>}],
+ children =
+ [{xmlcdata,
+ <<"Jabber HTTP Polling (XEP-0025)">>}]}]},
+ #xmlel{name = <<"p">>, attrs = [],
+ children =
+ [{xmlcdata,
+ <<"This web page is only informative. To "
+ "use HTTP-Poll you need a Jabber/XMPP "
+ "client that supports it.">>}]}]}]}.
+
+%%%----------------------------------------------------------------------
+%%% Callback functions from gen_fsm
+%%%----------------------------------------------------------------------
+
+%%----------------------------------------------------------------------
+%% Func: init/1
+%% Returns: {ok, StateName, StateData} |
+%% {ok, StateName, StateData, Timeout} |
+%% ignore |
+%% {stop, StopReason}
+%%----------------------------------------------------------------------
+init([ID, Key, IP]) ->
+ ?INFO_MSG("started: ~p", [{ID, Key, IP}]),
+ Opts = ejabberd_c2s_config:get_c2s_limits(),
+ HTTPPollTimeout = ejabberd_config:get_local_option(
+ {http_poll_timeout, ?MYNAME},
+ fun(I) when is_integer(I), I>0 -> I end,
+ ?HTTP_POLL_TIMEOUT) * 1000,
+ Socket = {http_poll, self(), IP},
+ ejabberd_socket:start(ejabberd_c2s, ?MODULE, Socket,
+ Opts),
+ Timer = erlang:start_timer(HTTPPollTimeout, self(), []),
+ {ok, loop,
+ #state{id = ID, key = Key, socket = Socket,
+ http_poll_timeout = HTTPPollTimeout, timer = Timer}}.
+
+%%----------------------------------------------------------------------
+%% Func: StateName/2
+%% Returns: {next_state, NextStateName, NextStateData} |
+%% {next_state, NextStateName, NextStateData, Timeout} |
+%% {stop, Reason, NewStateData}
+%% {stop, Reason, NewStateData}
+%%----------------------------------------------------------------------
+
+%%----------------------------------------------------------------------
+%% Func: StateName/3
+%% Returns: {next_state, NextStateName, NextStateData} |
+%% {next_state, NextStateName, NextStateData, Timeout} |
+%% {reply, Reply, NextStateName, NextStateData} |
+%% {reply, Reply, NextStateName, NextStateData, Timeout} |
+%% {stop, Reason, NewStateData} |
+%% {stop, Reason, Reply, NewStateData}
+%% {stop, Reason, Reply, NewStateData}
+%%----------------------------------------------------------------------
+%state_name(Event, From, StateData) ->
+% Reply = ok,
+% {reply, Reply, state_name, StateData}.
+
+%%----------------------------------------------------------------------
+%% Func: handle_event/3
+%% Returns: {next_state, NextStateName, NextStateData} |
+%% {next_state, NextStateName, NextStateData, Timeout} |
+%% {stop, Reason, NewStateData}
+%%----------------------------------------------------------------------
+handle_event({activate, From}, StateName, StateData) ->
+ case StateData#state.input of
+ <<"">> ->
+ {next_state, StateName,
+ StateData#state{waiting_input = {From, ok}}};
+ Input ->
+ Receiver = From,
+ Receiver !
+ {tcp, StateData#state.socket, iolist_to_binary(Input)},
+ {next_state, StateName,
+ StateData#state{input = <<"">>, waiting_input = false,
+ last_receiver = Receiver}}
+ end;
+handle_event(_Event, StateName, StateData) ->
+ {next_state, StateName, StateData}.
+
+%%----------------------------------------------------------------------
+%% Func: handle_sync_event/4
+%% Returns: {next_state, NextStateName, NextStateData} |
+%% {next_state, NextStateName, NextStateData, Timeout} |
+%% {reply, Reply, NextStateName, NextStateData} |
+%% {reply, Reply, NextStateName, NextStateData, Timeout} |
+%% {stop, Reason, NewStateData} |
+%% {stop, Reason, Reply, NewStateData}
+%%----------------------------------------------------------------------
+handle_sync_event({send, Packet}, _From, StateName,
+ StateData) ->
+ Packet2 = if is_binary(Packet) -> (Packet);
+ true -> Packet
+ end,
+ Output = StateData#state.output ++
+ [lists:flatten(Packet2)],
+ Reply = ok,
+ {reply, Reply, StateName,
+ StateData#state{output = Output}};
+handle_sync_event(stop, _From, _StateName, StateData) ->
+ Reply = ok, {stop, normal, Reply, StateData};
+handle_sync_event({http_put, Key, NewKey, Packet},
+ _From, StateName, StateData) ->
+ Allow = case StateData#state.key of
+ <<"">> -> true;
+ OldKey ->
+ NextKey = jlib:encode_base64((crypto:sha(Key))),
+ if OldKey == NextKey -> true;
+ true -> false
+ end
+ end,
+ if Allow ->
+ case StateData#state.waiting_input of
+ false ->
+ Input = [StateData#state.input | Packet],
+ Reply = ok,
+ {reply, Reply, StateName,
+ StateData#state{input = Input, key = NewKey}};
+ {Receiver, _Tag} ->
+ Receiver !
+ {tcp, StateData#state.socket, iolist_to_binary(Packet)},
+ cancel_timer(StateData#state.timer),
+ Timer =
+ erlang:start_timer(StateData#state.http_poll_timeout,
+ self(), []),
+ Reply = ok,
+ {reply, Reply, StateName,
+ StateData#state{waiting_input = false,
+ last_receiver = Receiver, key = NewKey,
+ timer = Timer}}
+ end;
+ true ->
+ Reply = {error, bad_key},
+ {reply, Reply, StateName, StateData}
+ end;
+handle_sync_event(http_get, _From, StateName,
+ StateData) ->
+ Reply = {ok, StateData#state.output},
+ {reply, Reply, StateName,
+ StateData#state{output = <<"">>}};
+handle_sync_event(_Event, _From, StateName,
+ StateData) ->
+ Reply = ok, {reply, Reply, StateName, StateData}.
+
+code_change(_OldVsn, StateName, StateData, _Extra) ->
+ {ok, StateName, StateData}.
+
+%%----------------------------------------------------------------------
+%% Func: handle_info/3
+%% Returns: {next_state, NextStateName, NextStateData} |
+%% {next_state, NextStateName, NextStateData, Timeout} |
+%% {stop, Reason, NewStateData}
+%%----------------------------------------------------------------------
+handle_info({timeout, Timer, _}, _StateName,
+ #state{timer = Timer} = StateData) ->
+ {stop, normal, StateData};
+handle_info(_, StateName, StateData) ->
+ {next_state, StateName, StateData}.
+
+%%----------------------------------------------------------------------
+%% Func: terminate/3
+%% Purpose: Shutdown the fsm
+%% Returns: any
+%%----------------------------------------------------------------------
+terminate(_Reason, _StateName, StateData) ->
+ mnesia:transaction(
+ fun() ->
+ mnesia:delete({http_poll, StateData#state.id})
+ end),
+ case StateData#state.waiting_input of
+ false ->
+ case StateData#state.last_receiver of
+ undefined -> ok;
+ Receiver ->
+ Receiver ! {tcp_closed, StateData#state.socket}
+ end;
+ {Receiver, _Tag} ->
+ Receiver ! {tcp_closed, StateData#state.socket}
+ end,
+ catch resend_messages(StateData#state.output),
+ ok.
+
+%%%----------------------------------------------------------------------
+%%% Internal functions
+%%%----------------------------------------------------------------------
+
+http_put(ID, Key, NewKey, Packet) ->
+ case mnesia:dirty_read({http_poll, ID}) of
+ [] ->
+ {error, not_exists};
+ [#http_poll{pid = FsmRef}] ->
+ gen_fsm:sync_send_all_state_event(
+ FsmRef, {http_put, Key, NewKey, Packet})
+ end.
+
+http_get(ID) ->
+ case mnesia:dirty_read({http_poll, ID}) of
+ [] ->
+ {error, not_exists};
+ [#http_poll{pid = FsmRef}] ->
+ gen_fsm:sync_send_all_state_event(FsmRef, http_get)
+ end.
+
+parse_request(Data) ->
+ Comma = str:chr(Data, $,),
+ Header = str:substr(Data, 1, Comma - 1),
+ Packet = str:substr(Data, Comma + 1, byte_size(Data)),
+ {ID, Key, NewKey} = case str:tokens(Header, <<";">>) of
+ [ID1] -> {ID1, <<"">>, <<"">>};
+ [ID1, Key1] -> {ID1, Key1, Key1};
+ [ID1, Key1, NewKey1] -> {ID1, Key1, NewKey1}
+ end,
+ {ok, ID, Key, NewKey, Packet}.
+
+cancel_timer(Timer) ->
+ erlang:cancel_timer(Timer),
+ receive {timeout, Timer, _} -> ok after 0 -> ok end.
+
+%% Resend the polled messages
+resend_messages(Messages) ->
+%% This function is used to resend messages that have been polled but not
+%% delivered.
+ lists:foreach(fun (Packet) -> resend_message(Packet)
+ end,
+ Messages).
+
+resend_message(Packet) ->
+ #xmlel{name = Name} = ParsedPacket =
+ xml_stream:parse_element(Packet),
+ if Name == <<"iq">>;
+ Name == <<"message">>;
+ Name == <<"presence">> ->
+ From = get_jid(<<"from">>, ParsedPacket),
+ To = get_jid(<<"to">>, ParsedPacket),
+ ?DEBUG("Resend ~p ~p ~p~n", [From, To, ParsedPacket]),
+ ejabberd_router:route(From, To, ParsedPacket);
+ true -> ok
+ end.
+
+%% Type can be "from" or "to"
+%% Parsed packet is a parsed Jabber packet.
+get_jid(Type, ParsedPacket) ->
+ case xml:get_tag_attr(Type, ParsedPacket) of
+ {value, StringJid} -> jlib:string_to_jid(StringJid);
+ false -> jlib:make_jid(<<"">>, <<"">>, <<"">>)
+ end.