mirror of
https://github.com/processone/ejabberd.git
synced 2024-10-31 15:21:38 +01:00
705 lines
24 KiB
Erlang
705 lines
24 KiB
Erlang
%%%----------------------------------------------------------------------
|
|
%%% File : mod_applepush_service.erl
|
|
%%% Author : Alexey Shchepin <alexey@process-one.net>
|
|
%%% Purpose : Central push infrastructure
|
|
%%% Created : 5 Jun 2009 by Alexey Shchepin <alexey@process-one.net>
|
|
%%%
|
|
%%% ejabberd, Copyright (C) 2002-2012 ProcessOne
|
|
%%%----------------------------------------------------------------------
|
|
|
|
-module(mod_applepush_service).
|
|
-author('alexey@process-one.net').
|
|
|
|
-behaviour(gen_server).
|
|
-behaviour(gen_mod).
|
|
|
|
%% API
|
|
-export([start_link/2, start/2, stop/1]).
|
|
|
|
%% 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_lib("kernel/include/file.hrl").
|
|
|
|
-record(state, {host = <<"">> :: binary(),
|
|
socket :: ssl:sslsocket(),
|
|
gateway = "" :: string(),
|
|
port = 2195 :: inet:port_number(),
|
|
feedback_socket :: ssl:sslsocket(),
|
|
feedback :: string(),
|
|
feedback_port = 2196 :: inet:port_number(),
|
|
feedback_buf = <<>> :: binary(),
|
|
certfile = "" :: string(),
|
|
certfile_mtime :: file:date_time(),
|
|
failure_script :: string(),
|
|
queue = {0, queue:new()} :: {non_neg_integer(), queue()},
|
|
soundfile = <<"">> :: binary(),
|
|
cmd_id = 0 :: non_neg_integer(),
|
|
cmd_cache = dict:new() :: dict(),
|
|
device_cache = dict:new() :: dict()}).
|
|
|
|
-define(PROCNAME, ejabberd_mod_applepush_service).
|
|
-define(RECONNECT_TIMEOUT, 5000).
|
|
-define(FEEDBACK_RECONNECT_TIMEOUT, 30000).
|
|
-define(HANDSHAKE_TIMEOUT, 60000).
|
|
-define(SSL_TIMEOUT, 5000).
|
|
-define(MAX_QUEUE_SIZE, 1000).
|
|
-define(CACHE_SIZE, 4096).
|
|
-define(MAX_PAYLOAD_SIZE, 255).
|
|
|
|
-define(NS_P1_PUSH, <<"p1:push">>).
|
|
|
|
%%====================================================================
|
|
%% API
|
|
%%====================================================================
|
|
%%--------------------------------------------------------------------
|
|
%% Function: start_link() -> {ok,Pid} | ignore | {error,Error}
|
|
%% Description: Starts the server
|
|
%%--------------------------------------------------------------------
|
|
start_link(Host, Opts) ->
|
|
Proc = gen_mod:get_module_proc(Host, ?PROCNAME),
|
|
gen_server:start_link({local, Proc}, ?MODULE, [Host, Opts], []).
|
|
|
|
start(Host, Opts) ->
|
|
ssl:start(),
|
|
MyHosts = case catch gen_mod:get_opt(
|
|
hosts, Opts,
|
|
fun(L) when is_list(L) ->
|
|
[{iolist_to_binary(H), O} || {H, O}<-L]
|
|
end, []) of
|
|
{'EXIT', _} ->
|
|
[{gen_mod:get_opt_host(Host, Opts,
|
|
<<"applepush.@HOST@">>), Opts}];
|
|
Hs ->
|
|
Hs
|
|
end,
|
|
lists:foreach(
|
|
fun({MyHost, MyOpts}) ->
|
|
Proc = gen_mod:get_module_proc(MyHost, ?PROCNAME),
|
|
ChildSpec =
|
|
{Proc,
|
|
{?MODULE, start_link, [MyHost, MyOpts]},
|
|
transient,
|
|
1000,
|
|
worker,
|
|
[?MODULE]},
|
|
supervisor:start_child(ejabberd_sup, ChildSpec)
|
|
end, MyHosts).
|
|
|
|
stop(Host) ->
|
|
MyHosts = case gen_mod:get_module_opt(
|
|
Host, ?MODULE, hosts,
|
|
fun(Hs) when is_list(Hs) ->
|
|
[iolist_to_binary(H) || {H, _} <- Hs]
|
|
end, []) of
|
|
[] ->
|
|
[gen_mod:get_module_opt_host(
|
|
Host, ?MODULE, <<"applepush.@HOST@">>)];
|
|
Hs ->
|
|
Hs
|
|
end,
|
|
lists:foreach(
|
|
fun(MyHost) ->
|
|
Proc = gen_mod:get_module_proc(MyHost, ?PROCNAME),
|
|
gen_server:call(Proc, stop),
|
|
supervisor:terminate_child(ejabberd_sup, Proc),
|
|
supervisor:delete_child(ejabberd_sup, Proc)
|
|
end, MyHosts).
|
|
|
|
|
|
%%====================================================================
|
|
%% gen_server callbacks
|
|
%%====================================================================
|
|
|
|
%%--------------------------------------------------------------------
|
|
%% Function: init(Args) -> {ok, State} |
|
|
%% {ok, State, Timeout} |
|
|
%% ignore |
|
|
%% {stop, Reason}
|
|
%% Description: Initiates the server
|
|
%%--------------------------------------------------------------------
|
|
init([MyHost, Opts]) ->
|
|
CertFile = gen_mod:get_opt(certfile, Opts,
|
|
fun iolist_to_string/1,
|
|
""),
|
|
SoundFile = gen_mod:get_opt(sound_file, Opts,
|
|
fun iolist_to_binary/1,
|
|
<<"pushalert.wav">>),
|
|
Gateway = gen_mod:get_opt(gateway, Opts,
|
|
fun iolist_to_string/1,
|
|
"gateway.push.apple.com"),
|
|
Feedback = gen_mod:get_opt(feedback, Opts,
|
|
fun iolist_to_string/1,
|
|
undefined),
|
|
Port = gen_mod:get_opt(port, Opts,
|
|
fun(I) when is_integer(I), I>0, I<65536 -> I end,
|
|
2195),
|
|
FeedbackPort = gen_mod:get_opt(feedback_port, Opts,
|
|
fun(I) when is_integer(I), I>0, I<65536 -> I end,
|
|
2196),
|
|
FailureScript = gen_mod:get_opt(failure_script, Opts,
|
|
fun iolist_to_string/1,
|
|
undefined),
|
|
self() ! connect,
|
|
case Feedback of
|
|
undefined ->
|
|
ok;
|
|
_ ->
|
|
self() ! connect_feedback
|
|
end,
|
|
ejabberd_router:register_route(MyHost),
|
|
{ok, #state{host = MyHost,
|
|
gateway = Gateway,
|
|
port = Port,
|
|
feedback = Feedback,
|
|
feedback_port = FeedbackPort,
|
|
certfile = CertFile,
|
|
failure_script = FailureScript,
|
|
queue = {0, queue:new()},
|
|
soundfile = SoundFile}}.
|
|
|
|
%%--------------------------------------------------------------------
|
|
%% Function: %% handle_call(Request, From, State) -> {reply, Reply, State} |
|
|
%% {reply, Reply, State, Timeout} |
|
|
%% {noreply, State} |
|
|
%% {noreply, State, Timeout} |
|
|
%% {stop, Reason, Reply, State} |
|
|
%% {stop, Reason, State}
|
|
%% Description: Handling call messages
|
|
%%--------------------------------------------------------------------
|
|
handle_call(stop, _From, State) ->
|
|
{stop, normal, ok, State}.
|
|
|
|
%%--------------------------------------------------------------------
|
|
%% Function: handle_cast(Msg, State) -> {noreply, State} |
|
|
%% {noreply, State, Timeout} |
|
|
%% {stop, Reason, State}
|
|
%% Description: Handling cast messages
|
|
%%--------------------------------------------------------------------
|
|
handle_cast(_Msg, State) ->
|
|
{noreply, State}.
|
|
|
|
%%--------------------------------------------------------------------
|
|
%% Function: handle_info(Info, State) -> {noreply, State} |
|
|
%% {noreply, State, Timeout} |
|
|
%% {stop, Reason, State}
|
|
%% Description: Handling all non call/cast messages
|
|
%%--------------------------------------------------------------------
|
|
handle_info({route, From, To, Packet}, State) ->
|
|
case catch do_route(From, To, Packet, State) of
|
|
{'EXIT', Reason} ->
|
|
?ERROR_MSG("~p", [Reason]),
|
|
{noreply, State};
|
|
Res ->
|
|
Res
|
|
end;
|
|
handle_info(connect, State) ->
|
|
connect(State);
|
|
handle_info(connect_feedback, #state{certfile_mtime = MTime} = State)
|
|
when MTime /= undefined ->
|
|
erlang:send_after(?FEEDBACK_RECONNECT_TIMEOUT, self(),
|
|
connect_feedback),
|
|
{noreply, State};
|
|
handle_info(connect_feedback, State)
|
|
when State#state.feedback /= undefined,
|
|
State#state.feedback_socket == undefined ->
|
|
Feedback = State#state.feedback,
|
|
FeedbackPort = State#state.feedback_port,
|
|
CertFile = State#state.certfile,
|
|
case ssl:connect(Feedback, FeedbackPort,
|
|
[{certfile, CertFile},
|
|
{active, true},
|
|
binary], ?SSL_TIMEOUT) of
|
|
{ok, Socket} ->
|
|
?INFO_MSG("(~p) Connected to ~p:~p",
|
|
[State#state.host, Feedback, FeedbackPort]),
|
|
{noreply, State#state{feedback_socket = Socket}};
|
|
{error, Reason} ->
|
|
?ERROR_MSG("(~p) Connection to ~p:~p failed: ~p, "
|
|
"retrying after ~p seconds",
|
|
[State#state.host, Feedback, FeedbackPort,
|
|
Reason, ?FEEDBACK_RECONNECT_TIMEOUT div 1000]),
|
|
erlang:send_after(?FEEDBACK_RECONNECT_TIMEOUT, self(),
|
|
connect_feedback),
|
|
{noreply, State}
|
|
end;
|
|
handle_info({ssl, Socket, Packet}, State)
|
|
when Socket == State#state.socket ->
|
|
case Packet of
|
|
<<8, Status, CmdID:32>> when Status /= 0 ->
|
|
case dict:find(CmdID, State#state.cmd_cache) of
|
|
{ok, {JID, _DeviceID}} ->
|
|
?ERROR_MSG("PUSH ERROR for ~p: ~p", [JID, Status]),
|
|
if
|
|
Status == 8 ->
|
|
From = jlib:make_jid(<<"">>, State#state.host, <<"">>),
|
|
ejabberd_router:route(
|
|
From, JID,
|
|
#xmlel{name = <<"message">>, attrs = [],
|
|
children =
|
|
[#xmlel{name = <<"disable">>,
|
|
attrs = [{<<"xmlns">>, ?NS_P1_PUSH},
|
|
{<<"status">>,
|
|
jlib:integer_to_binary(Status)}],
|
|
children = []}]});
|
|
true ->
|
|
ok
|
|
end,
|
|
ok;
|
|
error ->
|
|
?ERROR_MSG("Unknown cmd ID ~p~n", [CmdID]),
|
|
ok
|
|
end;
|
|
_ ->
|
|
?ERROR_MSG("Received unknown packet ~p~n", [Packet])
|
|
end,
|
|
{noreply, State};
|
|
handle_info({ssl, Socket, Packet}, State)
|
|
when Socket == State#state.feedback_socket ->
|
|
?INFO_MSG("(~p) feedback: ~p", [State#state.host, Packet]),
|
|
Buf = <<(State#state.feedback_buf)/binary, Packet/binary>>,
|
|
Buf2 = parse_feedback_buf(Buf, State),
|
|
{noreply, State#state{feedback_buf = Buf2}};
|
|
handle_info({ssl_closed, Socket}, State)
|
|
when Socket == State#state.feedback_socket ->
|
|
ssl:close(Socket),
|
|
erlang:send_after(?FEEDBACK_RECONNECT_TIMEOUT, self(),
|
|
connect_feedback),
|
|
{noreply, State#state{feedback_socket = undefined,
|
|
feedback_buf = <<>>}};
|
|
handle_info(_Info, State) ->
|
|
%io:format("got info: ~p~n", [_Info]),
|
|
{noreply, State}.
|
|
|
|
%%--------------------------------------------------------------------
|
|
%% Function: terminate(Reason, State) -> void()
|
|
%% Description: This function is called by a gen_server when it is about to
|
|
%% terminate. It should be the opposite of Module:init/1 and do any necessary
|
|
%% cleaning up. When it returns, the gen_server terminates with Reason.
|
|
%% The return value is ignored.
|
|
%%--------------------------------------------------------------------
|
|
terminate(_Reason, State) ->
|
|
ejabberd_router:unregister_route(State#state.host),
|
|
ok.
|
|
|
|
%%--------------------------------------------------------------------
|
|
%% Func: code_change(OldVsn, State, Extra) -> {ok, NewState}
|
|
%% Description: Convert process state when code is changed
|
|
%%--------------------------------------------------------------------
|
|
code_change(_OldVsn, State, _Extra) ->
|
|
{ok, State}.
|
|
|
|
%%%----------------------------------------------------------------------
|
|
%%% Internal functions
|
|
%%%----------------------------------------------------------------------
|
|
|
|
do_route(From, To, Packet, State) ->
|
|
#jid{user = User, resource = Resource} = To,
|
|
if
|
|
(User /= <<"">>) or (Resource /= <<"">>) ->
|
|
Err = jlib:make_error_reply(Packet, ?ERR_SERVICE_UNAVAILABLE),
|
|
ejabberd_router:route(To, From, Err),
|
|
{noreply, State};
|
|
true ->
|
|
case Packet of
|
|
#xmlel{name = <<"iq">>} ->
|
|
IQ = jlib:iq_query_info(Packet),
|
|
case IQ of
|
|
#iq{type = get, xmlns = ?NS_DISCO_INFO = XMLNS,
|
|
sub_el = _SubEl, lang = Lang} = IQ ->
|
|
Res = IQ#iq{type = result,
|
|
sub_el =
|
|
[#xmlel{name = <<"query">>,
|
|
attrs = [{<<"xmlns">>, XMLNS}],
|
|
children = iq_disco(Lang)}]},
|
|
ejabberd_router:route(To,
|
|
From,
|
|
jlib:iq_to_xml(Res)),
|
|
{noreply, State};
|
|
#iq{type = get, xmlns = ?NS_DISCO_ITEMS = XMLNS} = IQ ->
|
|
Res = IQ#iq{type = result,
|
|
sub_el =
|
|
[#xmlel{name = <<"query">>,
|
|
attrs = [{<<"xmlns">>, XMLNS}],
|
|
children = []}]},
|
|
ejabberd_router:route(To,
|
|
From,
|
|
jlib:iq_to_xml(Res)),
|
|
{noreply, State};
|
|
_ ->
|
|
Err = jlib:make_error_reply(Packet,
|
|
?ERR_SERVICE_UNAVAILABLE),
|
|
ejabberd_router:route(To, From, Err),
|
|
{noreply, State}
|
|
end;
|
|
#xmlel{name = <<"message">>, children = Els} ->
|
|
case xml:remove_cdata(Els) of
|
|
[#xmlel{name = <<"push">>}] ->
|
|
NewState = handle_message(From, To, Packet, State),
|
|
{noreply, NewState};
|
|
[#xmlel{name = <<"disable">>}] ->
|
|
{noreply, State};
|
|
_ ->
|
|
{noreply, State}
|
|
end;
|
|
_ ->
|
|
{noreply, State}
|
|
end
|
|
end.
|
|
|
|
get_custom_fields(Packet) ->
|
|
case xml:get_subtag(xml:get_subtag(Packet, <<"push">>), <<"custom">>) of
|
|
false -> [];
|
|
#xmlel{name = <<"custom">>, attrs = [], children = Children} ->
|
|
[ {xml:get_tag_attr_s(<<"name">>, C), xml:get_tag_cdata(C)} ||
|
|
C <- xml:remove_cdata(Children) ]
|
|
end.
|
|
|
|
handle_message(From, To, Packet, #state{socket = undefined} = State) ->
|
|
queue_message(From, To, Packet, State);
|
|
handle_message(From, To, Packet, State) ->
|
|
DeviceID =
|
|
xml:get_path_s(Packet,
|
|
[{elem, <<"push">>}, {elem, <<"id">>}, cdata]),
|
|
Msg =
|
|
xml:get_path_s(Packet,
|
|
[{elem, <<"push">>}, {elem, <<"msg">>}, cdata]),
|
|
Badge =
|
|
xml:get_path_s(Packet,
|
|
[{elem, <<"push">>}, {elem, <<"badge">>}, cdata]),
|
|
Sound =
|
|
xml:get_path_s(Packet,
|
|
[{elem, <<"push">>}, {elem, <<"sound">>}, cdata]),
|
|
Sender =
|
|
xml:get_path_s(Packet,
|
|
[{elem, <<"push">>}, {elem, <<"from">>}, cdata]),
|
|
Receiver =
|
|
xml:get_path_s(Packet,
|
|
[{elem, <<"push">>}, {elem, <<"to">>}, cdata]),
|
|
CustomFields = get_custom_fields(Packet),
|
|
Payload = make_payload(State, Msg, Badge, Sound, Sender, CustomFields),
|
|
ID =
|
|
case catch erlang:list_to_integer(binary_to_list(DeviceID), 16) of
|
|
ID1 when is_integer(ID1) ->
|
|
ID1;
|
|
_ ->
|
|
false
|
|
end,
|
|
if
|
|
is_integer(ID) ->
|
|
Command = 1,
|
|
CmdID = State#state.cmd_id,
|
|
{MegaSecs, Secs, _MicroSecs} = now(),
|
|
Expiry = MegaSecs * 1000000 + Secs + 24 * 60 * 60,
|
|
BDeviceID = <<ID:256>>,
|
|
IDLen = size(BDeviceID),
|
|
PayloadLen = size(Payload),
|
|
Notification =
|
|
<<Command:8,
|
|
CmdID:32,
|
|
Expiry:32,
|
|
IDLen:16,
|
|
BDeviceID/binary,
|
|
PayloadLen:16,
|
|
Payload/binary>>,
|
|
?INFO_MSG("(~p) sending notification for ~s~n~p~npayload:~n~s~n"
|
|
"Sender: ~s~n"
|
|
"Receiver: ~s~n"
|
|
"Device ID: ~s~n",
|
|
[State#state.host, erlang:integer_to_list(ID, 16),
|
|
Notification, Payload,
|
|
Sender,
|
|
Receiver, DeviceID]),
|
|
case ssl:send(State#state.socket, Notification) of
|
|
ok ->
|
|
cache(From, ID, State);
|
|
{error, Reason} ->
|
|
?INFO_MSG("(~p) Connection closed: ~p, reconnecting",
|
|
[State#state.host, Reason]),
|
|
ssl:close(State#state.socket),
|
|
self() ! connect,
|
|
queue_message(From, To, Packet,
|
|
State#state{socket = undefined})
|
|
end;
|
|
true ->
|
|
State
|
|
end.
|
|
|
|
make_payload(State, Msg, Badge, Sound, Sender, CustomFields) ->
|
|
Msg2 = json_escape(Msg),
|
|
AlertPayload =
|
|
case Msg2 of
|
|
<<"">> -> <<"">>;
|
|
_ -> <<"\"alert\":\"", Msg2/binary, "\"">>
|
|
end,
|
|
BadgePayload =
|
|
case catch jlib:binary_to_integer(Badge) of
|
|
B when is_integer(B) ->
|
|
<<"\"badge\":", Badge/binary>>;
|
|
_ -> <<"">>
|
|
end,
|
|
SoundPayload =
|
|
case Sound of
|
|
<<"true">> ->
|
|
SoundFile = State#state.soundfile,
|
|
<<"\"sound\":\"", (json_escape(SoundFile))/binary, "\"">>;
|
|
_ -> <<"">>
|
|
end,
|
|
Payloads = lists:filter(
|
|
fun(S) -> S /= <<"">> end,
|
|
[AlertPayload, BadgePayload, SoundPayload]),
|
|
|
|
CustomPayloadFields =
|
|
[<<"\"", (json_escape(Name))/binary, "\":\"",
|
|
(json_escape(Value))/binary, "\"">>
|
|
|| {Name, Value} <- CustomFields] ++
|
|
[<<"\"from\":\"", (json_escape(Sender))/binary, "\"">> || Sender /= <<"">>],
|
|
|
|
Payload1 =
|
|
[<<"{">>,
|
|
str:join(
|
|
[<<"\"aps\":{", (str:join(Payloads, <<",">>))/binary, "}">> | CustomPayloadFields],
|
|
<<",">>),
|
|
<<"}">>],
|
|
Payload = list_to_binary(Payload1),
|
|
PayloadLen = size(Payload),
|
|
if
|
|
PayloadLen > ?MAX_PAYLOAD_SIZE ->
|
|
Delta = PayloadLen - ?MAX_PAYLOAD_SIZE,
|
|
MsgLen = size(Msg),
|
|
if
|
|
MsgLen /= 0 ->
|
|
CutMsg =
|
|
if
|
|
MsgLen > Delta ->
|
|
{CMsg, _} = split_binary(Msg, MsgLen - Delta),
|
|
CMsg;
|
|
true ->
|
|
<<"">>
|
|
end,
|
|
make_payload(State, CutMsg, Badge, Sound, Sender, CustomFields);
|
|
true ->
|
|
Payload2 =
|
|
<<"{\"aps\":{", (str:join(Payloads, <<",">>))/binary, "}}">>,
|
|
Payload2
|
|
end;
|
|
true ->
|
|
Payload
|
|
end.
|
|
|
|
connect(#state{socket = undefined, certfile_mtime = undefined} = State) ->
|
|
Gateway = State#state.gateway,
|
|
Port = State#state.port,
|
|
CertFile = State#state.certfile,
|
|
case ssl:connect(Gateway, Port, [{certfile, CertFile},
|
|
{active, true},
|
|
binary],
|
|
?SSL_TIMEOUT) of
|
|
{ok, Socket} ->
|
|
{noreply, resend_messages(State#state{socket = Socket})};
|
|
{error, Reason} ->
|
|
{Timeout, State2} =
|
|
case Reason of
|
|
esslconnect ->
|
|
MTime = get_mtime(CertFile),
|
|
case State#state.failure_script of
|
|
undefined ->
|
|
ok;
|
|
FailureScript ->
|
|
os:cmd(FailureScript ++ " " ++ Gateway)
|
|
end,
|
|
{?HANDSHAKE_TIMEOUT,
|
|
State#state{certfile_mtime = MTime}};
|
|
_ ->
|
|
{?RECONNECT_TIMEOUT, State}
|
|
end,
|
|
?ERROR_MSG("(~p) Connection to ~p:~p failed: ~p, "
|
|
"retrying after ~p seconds",
|
|
[State2#state.host, Gateway, Port,
|
|
Reason, Timeout div 1000]),
|
|
erlang:send_after(Timeout, self(), connect),
|
|
{noreply, State2}
|
|
end;
|
|
connect(#state{socket = undefined, certfile_mtime = MTime} = State) ->
|
|
CertFile = State#state.certfile,
|
|
case get_mtime(CertFile) of
|
|
MTime ->
|
|
Gateway = State#state.gateway,
|
|
Port = State#state.port,
|
|
Timeout = ?HANDSHAKE_TIMEOUT,
|
|
?ERROR_MSG("(~p) Connection to ~p:~p postponed: "
|
|
"waiting for ~p update, "
|
|
"retrying after ~p seconds",
|
|
[State#state.host, Gateway, Port,
|
|
CertFile, Timeout div 1000]),
|
|
erlang:send_after(Timeout, self(), connect),
|
|
{noreply, State};
|
|
_ ->
|
|
connect(State#state{certfile_mtime = undefined})
|
|
end;
|
|
connect(State) ->
|
|
{noreply, State}.
|
|
|
|
get_mtime(File) ->
|
|
case file:read_file_info(File) of
|
|
{ok, FileInfo} ->
|
|
FileInfo#file_info.mtime;
|
|
{error, _} ->
|
|
no_certfile
|
|
end.
|
|
|
|
bounce_message(From, To, Packet, Reason) ->
|
|
#xmlel{attrs = Attrs} = Packet,
|
|
Type = xml:get_attr_s(<<"type">>, Attrs),
|
|
if Type /= <<"error">>; Type /= <<"result">> ->
|
|
ejabberd_router:route(
|
|
To, From,
|
|
jlib:make_error_reply(
|
|
Packet,
|
|
?ERRT_INTERNAL_SERVER_ERROR(
|
|
xml:get_attr_s(<<"xml:lang">>, Attrs),
|
|
Reason)));
|
|
true ->
|
|
ok
|
|
end.
|
|
|
|
queue_message(From, To, Packet, State) ->
|
|
case State#state.queue of
|
|
{?MAX_QUEUE_SIZE, Queue} ->
|
|
{{value, {From1, To1, Packet1}}, Queue1} = queue:out(Queue),
|
|
bounce_message(From1, To1, Packet1,
|
|
<<"Unable to connect to push service">>),
|
|
Queue2 = queue:in({From, To, Packet}, Queue1),
|
|
State#state{queue = {?MAX_QUEUE_SIZE, Queue2}};
|
|
{Size, Queue} ->
|
|
Queue1 = queue:in({From, To, Packet}, Queue),
|
|
State#state{queue = {Size+1, Queue1}}
|
|
end.
|
|
|
|
resend_messages(#state{queue = {_, Queue}} = State) ->
|
|
lists:foldl(
|
|
fun({From, To, Packet}, AccState) ->
|
|
case catch handle_message(From, To, Packet, AccState) of
|
|
{'EXIT', _} = Err ->
|
|
?ERROR_MSG("error while processing message:~n"
|
|
"** From: ~p~n"
|
|
"** To: ~p~n"
|
|
"** Packet: ~p~n"
|
|
"** Reason: ~p",
|
|
[From, To, Packet, Err]),
|
|
AccState;
|
|
NewAccState ->
|
|
NewAccState
|
|
end
|
|
end, State#state{queue = {0, queue:new()}}, queue:to_list(Queue)).
|
|
|
|
cache(JID, DeviceID, State) ->
|
|
CmdID = State#state.cmd_id,
|
|
Key = CmdID rem ?CACHE_SIZE,
|
|
C1 = State#state.cmd_cache,
|
|
D1 = State#state.device_cache,
|
|
D2 = case dict:find(Key, C1) of
|
|
{ok, {_, OldDeviceID}} ->
|
|
del_device_cache(D1, OldDeviceID);
|
|
error ->
|
|
D1
|
|
end,
|
|
D3 = add_device_cache(D2, DeviceID, JID),
|
|
C2 = dict:store(Key, {JID, DeviceID}, C1),
|
|
State#state{cmd_id = CmdID + 1,
|
|
cmd_cache = C2,
|
|
device_cache = D3}.
|
|
|
|
add_device_cache(DeviceCache, DeviceID, JID) ->
|
|
dict:update(
|
|
DeviceID,
|
|
fun({Counter, _}) -> {Counter + 1, JID} end,
|
|
{1, JID},
|
|
DeviceCache).
|
|
|
|
del_device_cache(DeviceCache, DeviceID) ->
|
|
case dict:find(DeviceID, DeviceCache) of
|
|
{ok, {Counter, JID}} ->
|
|
case Counter of
|
|
1 ->
|
|
dict:erase(DeviceID, DeviceCache);
|
|
_ ->
|
|
dict:store(DeviceID, {Counter - 1, JID}, DeviceCache)
|
|
end;
|
|
error ->
|
|
DeviceCache
|
|
end.
|
|
|
|
json_escape(S) ->
|
|
json_escape(S, <<>>).
|
|
|
|
json_escape(<<>>, Res) ->
|
|
Res;
|
|
json_escape(<<C, S/binary>>, Res) ->
|
|
case C of
|
|
$" -> json_escape(S, <<Res/binary, "\\\"">>);
|
|
$\\ -> json_escape(S, <<Res/binary, "\\\\">>);
|
|
_ when C < 16 ->
|
|
B = list_to_binary(erlang:integer_to_list(C, 16)),
|
|
json_escape(S, <<Res/binary, "\\u000", B/binary>>);
|
|
_ when C < 32 ->
|
|
B = list_to_binary(erlang:integer_to_list(C, 16)),
|
|
json_escape(S, <<Res/binary, "\\u00", B/binary>>);
|
|
_ -> json_escape(S, <<Res/binary, C>>)
|
|
end.
|
|
|
|
|
|
iq_disco(Lang) ->
|
|
[#xmlel{name = <<"identity">>,
|
|
attrs = [{<<"category">>, <<"gateway">>},
|
|
{<<"type">>, <<"apple">>},
|
|
{<<"name">>, translate:translate(
|
|
Lang, <<"Apple Push Service">>)}],
|
|
children = []},
|
|
#xmlel{name = <<"feature">>,
|
|
attrs = [{<<"var">>, ?NS_DISCO_INFO}],
|
|
children = []}].
|
|
|
|
|
|
parse_feedback_buf(Buf, State) ->
|
|
case Buf of
|
|
<<TimeStamp:32, IDLen:16, BDeviceID:IDLen/binary, Rest/binary>> ->
|
|
IDLen8 = IDLen * 8,
|
|
<<DeviceID:IDLen8>> = BDeviceID,
|
|
?INFO_MSG("(~p) received feedback for ~s~n",
|
|
[State#state.host, erlang:integer_to_list(DeviceID, 16)]),
|
|
case dict:find(DeviceID, State#state.device_cache) of
|
|
{ok, {_Counter, JID}} ->
|
|
?INFO_MSG("(~p) sending feedback for ~s to ~s~n",
|
|
[State#state.host,
|
|
erlang:integer_to_list(DeviceID, 16),
|
|
jlib:jid_to_string(JID)]),
|
|
From = jlib:make_jid(<<"">>, State#state.host, <<"">>),
|
|
ejabberd_router:route(
|
|
From, JID,
|
|
#xmlel{name = <<"iq">>,
|
|
attrs = [{<<"id">>, <<"disable">>},
|
|
{<<"type">>, <<"set">>}],
|
|
children =
|
|
[#xmlel{name = <<"disable">>,
|
|
attrs =
|
|
[{<<"xmlns">>, ?NS_P1_PUSH},
|
|
{<<"status">>, <<"feedback">>},
|
|
{<<"ts">>, jlib:integer_to_binary(TimeStamp)},
|
|
{<<"id">>, jlib:integer_to_binary(DeviceID, 16)}],
|
|
children = []}]});
|
|
error ->
|
|
ok
|
|
end,
|
|
parse_feedback_buf(Rest, State);
|
|
_ ->
|
|
Buf
|
|
end.
|
|
|
|
iolist_to_string(S) ->
|
|
binary_to_list(iolist_to_binary(S)).
|