diff --git a/src/web/ejabberd_http.erl b/src/web/ejabberd_http.erl index 1018269ab..1a450621a 100644 --- a/src/web/ejabberd_http.erl +++ b/src/web/ejabberd_http.erl @@ -340,7 +340,6 @@ process(Handlers, #ws{} = Ws)-> case (lists:prefix(HandlerPathPrefix, Ws#ws.path) or (HandlerPathPrefix==Ws#ws.path)) of true -> - ?DEBUG("~p matches ~p", [Ws#ws.path, HandlerPathPrefix]), LocalPath = lists:nthtail(length(HandlerPathPrefix), Ws#ws.path), ejabberd_hooks:run(ws_debug, [{LocalPath, Ws}]), Protocol = case lists:keysearch(protocol, 1, HandlerOpts) of @@ -351,9 +350,14 @@ process(Handlers, #ws{} = Ws)-> {value, {origins, O}} -> O; false -> [] end, + Auth = case lists:keysearch(auth, 1, HandlerOpts) of + {value, {auth, A}} -> A; + false -> undefined + end, WS2 = Ws#ws{local_path = LocalPath, protocol=Protocol, - acceptable_origins=Origins}, + acceptable_origins=Origins, + auth_module=Auth}, case ejabberd_websocket:is_acceptable(WS2) of true -> ejabberd_websocket:connect(WS2, HandlerModule); @@ -361,7 +365,6 @@ process(Handlers, #ws{} = Ws)-> process(HandlersLeft, Ws) end; false -> - ?DEBUG("HandlersLeft : ~p ", [HandlersLeft]), process(HandlersLeft, Ws) end; process(Handlers, Request) -> @@ -425,9 +428,10 @@ process_request(#state{request_method = Method, {_, Origin} = lists:keyfind("Origin", 1, RequestHeaders), Ws = #ws{socket = Socket, sockmod = SockMod, - ws_autoexit = true, + ws_autoexit = false, ip = IP, path = LPath, + q = LQuery, vsn = VSN, host = Host, port = Port, @@ -435,7 +439,6 @@ process_request(#state{request_method = Method, headers = RequestHeaders }, process(WebSocketHandlers, Ws), - ?DEBUG("It is a websocket.",[]), none; false -> Request = #request{method = Method, diff --git a/src/web/ejabberd_http.hrl b/src/web/ejabberd_http.hrl index 89c2a55c4..44269da15 100644 --- a/src/web/ejabberd_http.hrl +++ b/src/web/ejabberd_http.hrl @@ -47,6 +47,8 @@ path, % the websocket GET request path headers, % [{Tag, Val}] local_path, + q, protocol, - acceptable_origins + acceptable_origins = [], + auth_module }). \ No newline at end of file diff --git a/src/web/ejabberd_http_bindjson.erl b/src/web/ejabberd_http_bindjson.erl new file mode 100644 index 000000000..61f8779e8 --- /dev/null +++ b/src/web/ejabberd_http_bindjson.erl @@ -0,0 +1,1294 @@ +%%%---------------------------------------------------------------------- +%%% File : ejabberd_http_bindjson.erl +%%% Original Bind Author : Stefan Strigler +%%% Purpose : Implements XMPP over BOSH (XEP-0205) with a JSON Transport +%%% Created : 23 Sep 2010 by Eric Cestari +%%% Modified: may 2009 by Mickael Remond, Alexey Schepin +%%% Id : $Id: ejabberd_http_bind.erl 953 2009-05-07 10:40:40Z alexey $ +%%%---------------------------------------------------------------------- + +-module (ejabberd_http_bindjson). + +-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, + send_xml/2, + sockname/1, + peername/1, + setopts/2, + controlling_process/2, + become_controller/2, + change_controller/2, + custom_receiver/1, + reset_stream/1, + change_shaper/2, + monitor/1, + close/1, + start/4, + handle_session_start/8, + handle_http_put/7, + http_put/7, + http_get/2, + prepare_response/4, + process_request/2]). + +-include("ejabberd.hrl"). +-include("jlib.hrl"). +-include("ejabberd_http.hrl"). +-include("http_bind.hrl"). + +-record(http_bind, {id, pid, to, hold, wait, process_delay, version}). + +-define(NULL_PEER, {{0, 0, 0, 0}, 0}). + +%% http binding request +-record(hbr, {rid, + key, + out}). + +-record(state, {id, + rid = none, + key, + socket, + output = "", + input = queue:new(), + waiting_input = false, + shaper_state, + shaper_timer, + last_receiver, + last_poll, + http_receiver, + wait_timer, + ctime = 0, + timer, + pause=0, + unprocessed_req_list = [], % list of request that have been delayed for proper reordering: {Request, PID} + req_list = [], % list of requests (cache) + max_inactivity, + max_pause, + ip = ?NULL_PEER + }). + +%% Internal request format: +-record(http_put, {rid, + attrs, + payload, + payload_size, + hold, + stream, + ip}). + +%%-define(DBGFSM, true). +-ifdef(DBGFSM). +-define(FSMOPTS, [{debug, [trace]}]). +-else. +-define(FSMOPTS, []). +-endif. + +-define(BOSH_VERSION, "1.8"). +-define(NS_CLIENT, "jabber:client"). +-define(NS_BOSH, "urn:xmpp:xbosh"). +-define(NS_HTTP_BIND, "http://jabber.org/protocol/httpbind"). + +-define(MAX_REQUESTS, 2). % number of simultaneous requests +-define(MIN_POLLING, 2000000). % don't poll faster than that or we will + % shoot you (time in microsec) +-define(MAX_WAIT, 3600). % max num of secs to keep a request on hold +-define(MAX_INACTIVITY, 30000). % msecs to wait before terminating + % idle sessions +-define(MAX_PAUSE, 120). % may num of sec a client is allowed to pause + % the session + +%% Wait 100ms before continue processing, to allow the client provide more related stanzas. +-define(PROCESS_DELAY_DEFAULT, 100). +-define(PROCESS_DELAY_MIN, 0). +-define(PROCESS_DELAY_MAX, 1000). + + +%%%---------------------------------------------------------------------- +%%% API +%%%---------------------------------------------------------------------- +%% TODO: If compile with no supervisor option, start the session without +%% supervisor +start(XMPPDomain, Sid, Key, IP) -> + ?DEBUG("Starting session", []), + case catch supervisor:start_child(ejabberd_http_bind_sup, [Sid, Key, IP]) of + {ok, Pid} -> + {ok, Pid}; + {error, _} = Err -> + case check_bind_module(XMPPDomain) of + false -> + {error, "Cannot start HTTP bind session"}; + true -> + ?ERROR_MSG("Cannot start HTTP bind session: ~p", [Err]), + Err + end; + Exit -> + ?ERROR_MSG("Cannot start HTTP bind session: ~p", [Exit]), + {error, Exit} + end. + +start_link(Sid, Key, IP) -> + gen_fsm:start_link(?MODULE, [Sid, Key, IP], ?FSMOPTS). + +send({http_bind, FsmRef, _IP}, Packet) -> + gen_fsm:sync_send_all_state_event(FsmRef, {send, Packet}). + +send_xml({http_bind, FsmRef, _IP}, Packet) -> + gen_fsm:sync_send_all_state_event(FsmRef, {send_xml, Packet}). + +setopts({http_bind, FsmRef, _IP}, Opts) -> + case lists:member({active, once}, Opts) of + true -> + gen_fsm:send_all_state_event(FsmRef, {activate, self()}); + _ -> + case lists:member({active, false}, Opts) of + true -> + gen_fsm:sync_send_all_state_event( + FsmRef, deactivate_socket); + _ -> + ok + end + end. + +controlling_process(_Socket, _Pid) -> + ok. + +custom_receiver({http_bind, FsmRef, _IP}) -> + {receiver, ?MODULE, FsmRef}. + +become_controller(FsmRef, C2SPid) -> + gen_fsm:send_all_state_event(FsmRef, {become_controller, C2SPid}). + +change_controller({http_bind, FsmRef, _IP}, C2SPid) -> + become_controller(FsmRef, C2SPid). + +reset_stream({http_bind, _FsmRef, _IP}) -> + ok. + +change_shaper({http_bind, FsmRef, _IP}, Shaper) -> + gen_fsm:send_all_state_event(FsmRef, {change_shaper, Shaper}). + +monitor({http_bind, FsmRef, _IP}) -> + erlang:monitor(process, FsmRef). + +close({http_bind, FsmRef, _IP}) -> + catch gen_fsm:sync_send_all_state_event(FsmRef, {stop, close}). + +sockname(_Socket) -> + {ok, ?NULL_PEER}. + +peername({http_bind, _FsmRef, IP}) -> + {ok, IP}. + +%% Entry point for data coming from client through ejabberd HTTP server: +process_request(Data, IP) -> + Opts1 = ejabberd_c2s_config:get_c2s_limits(), + Opts = [{xml_socket, true} | Opts1], + MaxStanzaSize = + case lists:keysearch(max_stanza_size, 1, Opts) of + {value, {_, Size}} -> Size; + _ -> infinity + end, + PayloadSize = iolist_size(Data), + case catch parse_request(Data, PayloadSize, MaxStanzaSize) of + %% No existing session: + {ok, {"", Rid, Attrs, Payload}} -> + case xml:get_attr_s("to",Attrs) of + "" -> + ?DEBUG("Session not created (Improper addressing)", []), + {200, ?HEADER, "{\"body\":{\"type\":\"terminate\" " + "\"condition\":\"improper-addressing\", " + "\"xmlns\":\"" ++ ?NS_HTTP_BIND ++ "\"}}"}; + XmppDomain -> + %% create new session + Sid = make_sid(), + case start(XmppDomain, Sid, "", IP) of + {error, _} -> + {500, ?HEADER,"{\"body\":{\"type\":\"terminate\" " + "\"condition\":\"internal-server-error\", " + "\"xmlns\":\"" ++ ?NS_HTTP_BIND ++ "\",\"$\":\"Internal Server Error\"}}"}; + {ok, Pid} -> + handle_session_start( + Pid, XmppDomain, Sid, Rid, Attrs, + Payload, PayloadSize, IP) + end + end; + %% Existing session + {ok, {Sid, Rid, Attrs, Payload1}} -> + StreamStart = + case xml:get_attr_s("xmpp:restart",Attrs) of + "true" -> + true; + _ -> + false + end, + Payload2 = case xml:get_attr_s("type",Attrs) of + "terminate" -> + %% close stream + Payload1 ++ [{xmlstreamend, "stream:stream"}]; + _ -> + Payload1 + end, + handle_http_put(Sid, Rid, Attrs, Payload2, PayloadSize, + StreamStart, IP); + {size_limit, Sid} -> + case get_session(Sid) of + {error, _} -> + {404, ?HEADER, ""}; + {ok, #http_bind{pid = FsmRef}} -> + gen_fsm:sync_send_all_state_event(FsmRef, {stop, close}), + {200, ?HEADER, "{\"body\": {\"type\"=\"terminate\" " + "\"condition\":\"undefined-condition\" " + "\"xmlns\":\"" ++ ?NS_HTTP_BIND ++ "\", \"$\":\"Request Too Large\"}}"} + end; + _ -> + ?DEBUG("Received bad request: ~p", [Data]), + {400, ?HEADER, ""} + end. + +handle_session_start(Pid, XmppDomain, Sid, Rid, Attrs, + Payload, PayloadSize, IP) -> + ?DEBUG("got pid: ~p", [Pid]), + Wait = case string:to_integer(xml:get_attr_s("wait",Attrs)) of + {error, _} -> + ?MAX_WAIT; + {CWait, _} -> + if + (CWait > ?MAX_WAIT) -> + ?MAX_WAIT; + true -> + CWait + end + end, + Hold = case string:to_integer(xml:get_attr_s("hold",Attrs)) of + {error, _} -> + (?MAX_REQUESTS - 1); + {CHold, _} -> + if + (CHold > (?MAX_REQUESTS - 1)) -> + (?MAX_REQUESTS - 1); + true -> + CHold + end + end, + Pdelay = case string:to_integer(xml:get_attr_s("process-delay",Attrs)) of + {error, _} -> + ?PROCESS_DELAY_DEFAULT; + {CPdelay, _} when + (?PROCESS_DELAY_MIN =< CPdelay) and + (CPdelay =< ?PROCESS_DELAY_MAX) -> + CPdelay; + {CPdelay, _} -> + erlang:max( + erlang:min(CPdelay,?PROCESS_DELAY_MAX), + ?PROCESS_DELAY_MIN) + end, + Version = + case catch list_to_float( + xml:get_attr_s("ver", Attrs)) of + {'EXIT', _} -> 0.0; + V -> V + end, + XmppVersion = xml:get_attr_s("xmpp:version", Attrs), + ?DEBUG("Create session: ~p", [Sid]), + mnesia:async_dirty( + fun() -> + mnesia:write( + #http_bind{id = Sid, + pid = Pid, + to = {XmppDomain, + XmppVersion}, + hold = Hold, + wait = Wait, + process_delay = Pdelay, + version = Version + }) + end), + handle_http_put(Sid, Rid, Attrs, Payload, PayloadSize, true, IP). + +%%%---------------------------------------------------------------------- +%%% Callback functions from gen_fsm +%%%---------------------------------------------------------------------- + +%%---------------------------------------------------------------------- +%% Func: init/1 +%% Returns: {ok, StateName, StateData} | +%% {ok, StateName, StateData, Timeout} | +%% ignore | +%% {stop, StopReason} +%%---------------------------------------------------------------------- +init([Sid, Key, IP]) -> + ?DEBUG("started: ~p", [{Sid, Key, IP}]), + + %% Read c2s options from the first ejabberd_c2s configuration in + %% the config file listen section + %% TODO: We should have different access and shaper values for + %% each connector. The default behaviour should be however to use + %% the default c2s restrictions if not defined for the current + %% connector. + Opts1 = ejabberd_c2s_config:get_c2s_limits(), + Opts = [{xml_socket, true} | Opts1], + + Shaper = none, + ShaperState = shaper:new(Shaper), + Socket = {http_bind, self(), IP}, + ejabberd_socket:start(ejabberd_c2s, ?MODULE, Socket, Opts), + Timer = erlang:start_timer(?MAX_INACTIVITY, self(), []), + {ok, loop, #state{id = Sid, + key = Key, + socket = Socket, + shaper_state = ShaperState, + max_inactivity = ?MAX_INACTIVITY, + max_pause = ?MAX_PAUSE, + timer = Timer}}. + +%%---------------------------------------------------------------------- +%% Func: handle_event/3 +%% Returns: {next_state, NextStateName, NextStateData} | +%% {next_state, NextStateName, NextStateData, Timeout} | +%% {stop, Reason, NewStateData} +%%---------------------------------------------------------------------- +handle_event({become_controller, C2SPid}, StateName, StateData) -> + erlang:monitor(process, C2SPid), + case StateData#state.input of + cancel -> + {next_state, StateName, StateData#state{ + waiting_input = C2SPid}}; + Input -> + lists:foreach( + fun(Event) -> + C2SPid ! Event + end, queue:to_list(Input)), + {next_state, StateName, StateData#state{ + input = queue:new(), + waiting_input = C2SPid}} + end; + +handle_event({change_shaper, Shaper}, StateName, StateData) -> + NewShaperState = shaper:new(Shaper), + {next_state, StateName, StateData#state{shaper_state = NewShaperState}}; +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_xml, Packet}, _From, StateName, + #state{http_receiver = undefined} = StateData) -> + Output = [Packet | StateData#state.output], + Reply = ok, + {reply, Reply, StateName, StateData#state{output = Output}}; +handle_sync_event({send_xml, Packet}, _From, StateName, StateData) -> + Output = [Packet | StateData#state.output], + cancel_timer(StateData#state.timer), + Timer = set_inactivity_timer(StateData#state.pause, + StateData#state.max_inactivity), + HTTPReply = {ok, Output}, + gen_fsm:reply(StateData#state.http_receiver, HTTPReply), + cancel_timer(StateData#state.wait_timer), + Rid = StateData#state.rid, + ReqList = [#hbr{rid = Rid, + key = StateData#state.key, + out = Output + } | + [El || El <- StateData#state.req_list, + El#hbr.rid /= Rid ] + ], + Reply = ok, + {reply, Reply, StateName, + StateData#state{output = [], + http_receiver = undefined, + req_list = ReqList, + wait_timer = undefined, + timer = Timer}}; + +handle_sync_event({stop,close}, _From, _StateName, StateData) -> + Reply = ok, + {stop, normal, Reply, StateData}; +handle_sync_event({stop,stream_closed}, _From, _StateName, StateData) -> + Reply = ok, + {stop, normal, Reply, StateData}; +handle_sync_event(deactivate_socket, _From, StateName, StateData) -> + %% Input = case StateData#state.input of + %% cancel -> + %% queue:new(); + %% Q -> + %% Q + %% end, + {reply, ok, StateName, StateData#state{waiting_input = false}}; +handle_sync_event({stop,Reason}, _From, _StateName, StateData) -> + ?DEBUG("Closing bind session ~p - Reason: ~p", [StateData#state.id, Reason]), + Reply = ok, + {stop, normal, Reply, StateData}; + +%% HTTP PUT: Receive packets from the client +handle_sync_event(#http_put{rid = Rid}, + _From, StateName, StateData) + when StateData#state.shaper_timer /= undefined -> + Pause = + case erlang:read_timer(StateData#state.shaper_timer) of + false -> + 0; + P -> P + end, + Reply = {wait, Pause}, + ?DEBUG("Shaper timer for RID ~p: ~p", [Rid, Reply]), + {reply, Reply, StateName, StateData}; + +handle_sync_event(#http_put{payload_size = PayloadSize} = Request, + _From, StateName, StateData) -> + ?DEBUG("New request: ~p",[Request]), + %% Updating trafic shaper + {NewShaperState, NewShaperTimer} = + update_shaper(StateData#state.shaper_state, PayloadSize), + + handle_http_put_event(Request, StateName, + StateData#state{shaper_state = NewShaperState, + shaper_timer = NewShaperTimer}); + +%% HTTP GET: send packets to the client +handle_sync_event({http_get, Rid, Wait, Hold}, From, StateName, StateData) -> + %% setup timer + send_receiver_reply(StateData#state.http_receiver, {ok, empty}), + cancel_timer(StateData#state.wait_timer), + TNow = tnow(), + if + (Hold > 0) and + (StateData#state.output == []) and + ((TNow - StateData#state.ctime) < (Wait*1000*1000)) and + (StateData#state.rid == Rid) and + (StateData#state.input /= cancel) and + (StateData#state.pause == 0) -> + WaitTimer = erlang:start_timer(Wait * 1000, self(), []), + %% MR: Not sure we should cancel the state timer here. + cancel_timer(StateData#state.timer), + {next_state, StateName, StateData#state{ + http_receiver = From, + wait_timer = WaitTimer, + timer = undefined}}; + (StateData#state.input == cancel) -> + cancel_timer(StateData#state.timer), + Timer = set_inactivity_timer(StateData#state.pause, + StateData#state.max_inactivity), + Reply = {ok, cancel}, + {reply, Reply, StateName, StateData#state{ + input = queue:new(), + http_receiver = undefined, + wait_timer = undefined, + timer = Timer}}; + true -> + cancel_timer(StateData#state.timer), + Timer = set_inactivity_timer(StateData#state.pause, + StateData#state.max_inactivity), + Reply = {ok, StateData#state.output}, + %% save request + ReqList = [#hbr{rid = Rid, + key = StateData#state.key, + out = StateData#state.output + } | + [El || El <- StateData#state.req_list, + El#hbr.rid /= Rid ] + ], + {reply, Reply, StateName, StateData#state{ + output = [], + http_receiver = undefined, + wait_timer = undefined, + timer = Timer, + req_list = ReqList}} + end; + +handle_sync_event(peername, _From, StateName, StateData) -> + Reply = {ok, StateData#state.ip}, + {reply, Reply, StateName, StateData}; + +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} +%%---------------------------------------------------------------------- +%% We reached the max_inactivity timeout: +handle_info({timeout, Timer, _}, _StateName, + #state{id=SID, timer = Timer} = StateData) -> + ?INFO_MSG("Session timeout. Closing the HTTP bind session: ~p", [SID]), + {stop, normal, StateData}; + +handle_info({timeout, WaitTimer, _}, StateName, + #state{wait_timer = WaitTimer} = StateData) -> + if + StateData#state.http_receiver /= undefined -> + cancel_timer(StateData#state.timer), + Timer = set_inactivity_timer(StateData#state.pause, + StateData#state.max_inactivity), + gen_fsm:reply(StateData#state.http_receiver, {ok, empty}), + Rid = StateData#state.rid, + ReqList = [#hbr{rid = Rid, + key = StateData#state.key, + out = [] + } | + [El || El <- StateData#state.req_list, + El#hbr.rid /= Rid ] + ], + {next_state, StateName, + StateData#state{http_receiver = undefined, + req_list = ReqList, + wait_timer = undefined, + timer = Timer}}; + true -> + {next_state, StateName, StateData} + end; + +handle_info({timeout, ShaperTimer, _}, StateName, + #state{shaper_timer = ShaperTimer} = StateData) -> + {next_state, StateName, StateData#state{shaper_timer = undefined}}; + +handle_info({'DOWN', _MRef, process, C2SPid, _}, _StateName, + #state{waiting_input = C2SPid} = 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) -> + ?DEBUG("terminate: Deleting session ~s", [StateData#state.id]), + mnesia:dirty_delete({http_bind, StateData#state.id}), + send_receiver_reply(StateData#state.http_receiver, {ok, terminate}), + case StateData#state.waiting_input of + false -> + ok; + C2SPid -> + gen_fsm:send_event(C2SPid, closed) + end, + ok. + +%%%---------------------------------------------------------------------- +%%% Internal functions +%%%---------------------------------------------------------------------- + +%% PUT / Get processing: +handle_http_put_event(#http_put{rid = Rid, attrs = Attrs, + hold = Hold} = Request, + StateName, StateData) -> + ?DEBUG("New request: ~p",[Request]), + %% Check if Rid valid + RidAllow = rid_allow(StateData#state.rid, Rid, Attrs, Hold, + StateData#state.max_pause), + + %% Check if Rid is in sequence or out of sequence: + case RidAllow of + buffer -> + ?DEBUG("Buffered request: ~p", [Request]), + %% Request is out of sequence: + PendingRequests = StateData#state.unprocessed_req_list, + %% In case an existing RID was already buffered: + Requests = lists:keydelete(Rid, 2, PendingRequests), + ReqList = [#hbr{rid = Rid, + key = StateData#state.key, + out = [] + } | + [El || El <- StateData#state.req_list, + El#hbr.rid > (Rid - 1 - Hold)] + ], + ?DEBUG("reqlist: ~p", [ReqList]), + UnprocessedReqList = [Request | Requests], + cancel_timer(StateData#state.timer), + Timer = set_inactivity_timer(0, StateData#state.max_inactivity), + {reply, buffered, StateName, + StateData#state{unprocessed_req_list = UnprocessedReqList, + req_list = ReqList, + timer = Timer}}; + _ -> + %% Request is in sequence: + process_http_put(Request, StateName, StateData, RidAllow) + end. + +process_http_put(#http_put{rid = Rid, attrs = Attrs, payload = Payload, + hold = Hold, stream = StreamTo, + ip = IP} = Request, + StateName, StateData, RidAllow) -> + ?DEBUG("Actually processing request: ~p", [Request]), + %% Check if key valid + Key = xml:get_attr_s("key", Attrs), + NewKey = xml:get_attr_s("newkey", Attrs), + KeyAllow = + case RidAllow of + repeat -> + true; + false -> + false; + {true, _} -> + case StateData#state.key of + "" -> + true; + OldKey -> + NextKey = sha:sha(Key), + ?DEBUG("Key/OldKey/NextKey: ~s/~s/~s", [Key, OldKey, NextKey]), + if + OldKey == NextKey -> + true; + true -> + ?DEBUG("wrong key: ~s",[Key]), + false + end + end + end, + TNow = tnow(), + LastPoll = if + Payload == [] -> + TNow; + true -> + 0 + end, + if + (Payload == []) and + (Hold == 0) and + (TNow - StateData#state.last_poll < ?MIN_POLLING) -> + Reply = {error, polling_too_frequently}, + {reply, Reply, StateName, StateData}; + KeyAllow -> + case RidAllow of + false -> + Reply = {error, not_exists}, + {reply, Reply, StateName, StateData}; + repeat -> + ?DEBUG("REPEATING ~p", [Rid]), + Reply = case [El#hbr.out || + El <- StateData#state.req_list, + El#hbr.rid == Rid] of + [] -> + {error, not_exists}; + [Out | _XS] -> + {repeat, lists:reverse(Out)} + end, + {reply, Reply, StateName, StateData#state{input = cancel, + last_poll = LastPoll}}; + {true, Pause} -> + SaveKey = if + NewKey == "" -> + Key; + true -> + NewKey + end, + ?DEBUG(" -- SaveKey: ~s~n", [SaveKey]), + + %% save request + ReqList1 = + [El || El <- StateData#state.req_list, + El#hbr.rid > (Rid - 1 - Hold)], + ReqList = + case lists:keymember(Rid, #hbr.rid, ReqList1) of + true -> + ReqList1; + false -> + [#hbr{rid = Rid, + key = StateData#state.key, + out = [] + } | + ReqList1 + ] + end, + ?DEBUG("reqlist: ~p", [ReqList]), + + %% setup next timer + cancel_timer(StateData#state.timer), + Timer = set_inactivity_timer(Pause, + StateData#state.max_inactivity), + case StateData#state.waiting_input of + false -> + Input = + lists:foldl( + fun queue:in/2, + StateData#state.input, Payload), + Reply = ok, + process_buffered_request(Reply, StateName, + StateData#state{input = Input, + rid = Rid, + key = SaveKey, + ctime = TNow, + timer = Timer, + pause = Pause, + last_poll = LastPoll, + req_list = ReqList, + ip = IP + }); + C2SPid -> + case StreamTo of + {To, ""} -> + gen_fsm:send_event( + C2SPid, + {xmlstreamstart, "stream:stream", + [{"to", To}, + {"xmlns", ?NS_CLIENT}, + {"xmlns:stream", ?NS_STREAM}]}); + {To, Version} -> + gen_fsm:send_event( + C2SPid, + {xmlstreamstart, "stream:stream", + [{"to", To}, + {"xmlns", ?NS_CLIENT}, + {"version", Version}, + {"xmlns:stream", ?NS_STREAM}]}); + _ -> + ok + end, + + MaxInactivity = get_max_inactivity(StreamTo, StateData#state.max_inactivity), + MaxPause = get_max_inactivity(StreamTo, StateData#state.max_pause), + + ?DEBUG("really sending now: ~p", [Payload]), + lists:foreach( + fun({xmlstreamend, End}) -> + gen_fsm:send_event( + C2SPid, {xmlstreamend, End}); + (El) -> + gen_fsm:send_event( + C2SPid, {xmlstreamelement, El}) + end, Payload), + Reply = ok, + process_buffered_request(Reply, StateName, + StateData#state{input = queue:new(), + rid = Rid, + key = SaveKey, + ctime = TNow, + timer = Timer, + pause = Pause, + last_poll = LastPoll, + req_list = ReqList, + max_inactivity = MaxInactivity, + max_pause = MaxPause, + ip = IP + }) + end + end; + true -> + Reply = {error, bad_key}, + {reply, Reply, StateName, StateData} + end. + +process_buffered_request(Reply, StateName, StateData) -> + Rid = StateData#state.rid, + Requests = StateData#state.unprocessed_req_list, + case lists:keysearch(Rid+1, 2, Requests) of + {value, Request} -> + ?DEBUG("Processing buffered request: ~p", [Request]), + NewRequests = lists:keydelete(Rid+1, 2, Requests), + handle_http_put_event( + Request, StateName, + StateData#state{unprocessed_req_list = NewRequests}); + _ -> + {reply, Reply, StateName, StateData, hibernate} + end. + +handle_http_put(Sid, Rid, Attrs, Payload, PayloadSize, StreamStart, IP) -> + case http_put(Sid, Rid, Attrs, Payload, PayloadSize, StreamStart, IP) of + {error, not_exists} -> + ?DEBUG("no session associated with sid: ~p", [Sid]), + {404, ?HEADER, ""}; + {{error, Reason}, Sess} -> + ?DEBUG("Error on HTTP put. Reason: ~p", [Reason]), + handle_http_put_error(Reason, Sess); + {{repeat, OutPacket}, Sess} -> + ?DEBUG("http_put said \"repeat!\" ...~nOutPacket: ~p", [OutPacket]), + send_outpacket(Sess, OutPacket); + {{wait, Pause}, _Sess} -> + ?DEBUG("Trafic Shaper: Delaying request ~p", [Rid]), + timer:sleep(Pause), + %{200, ?HEADER, + % xmpp_json:to_json( + % {xmlelement, "body", + % [{"xmlns", ?NS_HTTP_BIND}, + % {"type", "error"}], []})}; + handle_http_put(Sid, Rid, Attrs, Payload, PayloadSize, + StreamStart, IP); + {buffered, _Sess} -> + {200, ?HEADER, "{\"body\":{ \"xmlns\":\""++?NS_HTTP_BIND++"\"}}"}; + {ok, Sess} -> + prepare_response(Sess, Rid, [], StreamStart) + end. + +http_put(Sid, Rid, Attrs, Payload, PayloadSize, StreamStart, IP) -> + ?DEBUG("Looking for session: ~p", [Sid]), + case get_session(Sid) of + {error, _} -> + {error, not_exists}; + {ok, #http_bind{pid = FsmRef, hold=Hold, to={To, StreamVersion}}=Sess}-> + NewStream = + case StreamStart of + true -> + {To, StreamVersion}; + _ -> + "" + end, + {gen_fsm:sync_send_all_state_event( + FsmRef, #http_put{rid = Rid, attrs = Attrs, payload = Payload, + payload_size = PayloadSize, hold = Hold, + stream = NewStream, ip = IP}, 30000), Sess} + end. + +handle_http_put_error(Reason, #http_bind{pid=FsmRef, version=Version}) + when Version >= 0 -> + gen_fsm:sync_send_all_state_event(FsmRef, {stop, {put_error,Reason}}), + case Reason of + not_exists -> + {200, ?HEADER, + mochijson2:encode( + xmpp_json:to_json( + {xmlelement, "body", + [{"xmlns", ?NS_HTTP_BIND}, + {"type", "terminate"}, + {"condition", "item-not-found"}], []}))}; + bad_key -> + {200, ?HEADER, + mochijson2:encode( + xmpp_json:to_json( + {xmlelement, "body", + [{"xmlns", ?NS_HTTP_BIND}, + {"type", "terminate"}, + {"condition", "item-not-found"}], []}))}; + polling_too_frequently -> + {200, ?HEADER, + mochijson2:encode( + xmpp_json:to_json( + {xmlelement, "body", + [{"xmlns", ?NS_HTTP_BIND}, + {"type", "terminate"}, + {"condition", "policy-violation"}], []}))} + end; +handle_http_put_error(Reason, #http_bind{pid=FsmRef}) -> + gen_fsm:sync_send_all_state_event(FsmRef,{stop, {put_error_no_version, Reason}}), + case Reason of + not_exists -> %% bad rid + ?DEBUG("Closing HTTP bind session (Bad rid).", []), + {404, ?HEADER, ""}; + bad_key -> + ?DEBUG("Closing HTTP bind session (Bad key).", []), + {404, ?HEADER, ""}; + polling_too_frequently -> + ?DEBUG("Closing HTTP bind session (User polling too frequently).", []), + {403, ?HEADER, ""} + end. + +%% Control RID ordering +rid_allow(none, _NewRid, _Attrs, _Hold, _MaxPause) -> + %% First request - nothing saved so far + {true, 0}; +rid_allow(OldRid, NewRid, Attrs, Hold, MaxPause) -> + ?DEBUG("Previous rid / New rid: ~p/~p", [OldRid, NewRid]), + if + %% We did not miss any packet, we can process it immediately: + NewRid == OldRid + 1 -> + case catch list_to_integer( + xml:get_attr_s("pause", Attrs)) of + {'EXIT', _} -> + {true, 0}; + Pause1 when Pause1 =< MaxPause -> + ?DEBUG("got pause: ~p", [Pause1]), + {true, Pause1}; + _ -> + {true, 0} + end; + %% We have missed packets, we need to cached it to process it later on: + (OldRid < NewRid) and + (NewRid =< (OldRid + Hold + 1)) -> + buffer; + (NewRid =< OldRid) and + (NewRid > OldRid - Hold - 1) -> + repeat; + true -> + false + end. + +update_shaper(ShaperState, PayloadSize) -> + {NewShaperState, Pause} = shaper:update(ShaperState, PayloadSize), + if + Pause > 0 -> + ShaperTimer = erlang:start_timer(Pause, self(), activate), %% MR: Seems timer is not needed. Activate is not handled + {NewShaperState, ShaperTimer}; + true -> + {NewShaperState, undefined} + end. + +prepare_response(Sess, Rid, OutputEls, StreamStart) -> + receive after Sess#http_bind.process_delay -> ok end, + case catch http_get(Sess, Rid) of + {ok, cancel} -> + %% actually it would be better if we could completely + %% cancel this request, but then we would have to hack + %% ejabberd_http and I'm too lazy now + {200, ?HEADER, "{\"body\": {\"type\":\"error\", \"xmlns\":\""++?NS_HTTP_BIND++"\"/>"}; + {ok, empty} -> + {200, ?HEADER, "{\"body\":{ \"xmlns\":\""++?NS_HTTP_BIND++"\"}}"}; + {ok, terminate} -> + {200, ?HEADER, "{\"body\": {\"type\":\"terminate\", \"xmlns\":\""++?NS_HTTP_BIND++"\"/>"}; + {ok, ROutPacket} -> + OutPacket = lists:reverse(ROutPacket), + ?DEBUG("OutPacket: ~p", [OutputEls++OutPacket]), + prepare_outpacket_response(Sess, Rid, OutputEls++OutPacket, StreamStart); + {'EXIT', {shutdown, _}} -> + {200, ?HEADER, "{\"body\": {\"type\":\"terminate\",\"condition\":\"system-shutdown\", \"xmlns\":\""++ ?NS_HTTP_BIND++"\"}}"}; + {'EXIT', _Reason} -> + {200, ?HEADER, "{\"body\": {\"type\":\"terminate, \"xmlns\":\""++ ?NS_HTTP_BIND++"\"}}"} + end. + +%% Send output payloads on establised sessions +prepare_outpacket_response(Sess, _Rid, OutPacket, false) -> + case catch send_outpacket(Sess, OutPacket) of + {'EXIT', _Reason} -> + {200, ?HEADER, + "{\"body\": {\"type\":\"terminate\", \"xmlns\":\""++ ?NS_HTTP_BIND++"\"}}"}; + SendRes -> + SendRes + end; +%% Handle a new session along with its output payload +prepare_outpacket_response(#http_bind{id=Sid, wait=Wait, + hold=Hold, to=To}=Sess, + Rid, OutPacket, true) -> + case OutPacket of + [{xmlstreamstart, _, OutAttrs} | Els] -> + AuthID = xml:get_attr_s("id", OutAttrs), + From = xml:get_attr_s("from", OutAttrs), + Version = xml:get_attr_s("version", OutAttrs), + OutEls = + case Els of + [] -> + []; + [{xmlstreamelement, + {xmlelement, "stream:features", + StreamAttribs, StreamEls}} + | StreamTail] -> + TypedTail = + [check_default_xmlns(OEl) || + {xmlstreamelement, OEl} <- + StreamTail], + [{xmlelement, "stream:features", + [{"xmlns:stream", + ?NS_STREAM}] ++ + StreamAttribs, StreamEls}] ++ + TypedTail; + StreamTail -> + [check_default_xmlns(OEl) || + {xmlstreamelement, OEl} <- + StreamTail] + end, + case OutEls of + [] -> + prepare_response(Sess, Rid, OutPacket, true); + [{xmlelement, + "stream:error",_,_}] -> + {200, ?HEADER, "{\"body\" : {\"type\":\"terminate\", " + "\"condition\":\"host-unknown\", " + "\"xmlns\"=\""++?NS_HTTP_BIND++"\"}}"}; + _ -> + BOSH_attribs = + [{"authid", AuthID}, + {"xmlns:xmpp", ?NS_BOSH}, + {"xmlns:stream", ?NS_STREAM}] ++ + case OutEls of + [] -> + []; + _ -> + [{"xmpp:version", Version}] + end, + MaxInactivity = get_max_inactivity(To, ?MAX_INACTIVITY), + MaxPause = get_max_pause(To), + {200, ?HEADER, + mochijson2:encode( + xmpp_json:to_json( + {xmlelement,"body", + [{"xmlns", + ?NS_HTTP_BIND}, + {"sid", Sid}, + {"wait", integer_to_list(Wait)}, + {"requests", integer_to_list(Hold+1)}, + {"inactivity", + integer_to_list( + trunc(MaxInactivity/1000))}, + {"maxpause", + integer_to_list(MaxPause)}, + {"polling", + integer_to_list( + trunc(?MIN_POLLING/1000000))}, + {"ver", ?BOSH_VERSION}, + {"from", From}, + {"secure", "true"} %% we're always being secure + ] ++ BOSH_attribs,OutEls}))} + end; + _ -> + {200, ?HEADER, "{\"body\" : {\"type\":\"terminate\", " + "\"condition\":\"internal-server-error\", " + "\"xmlns\"=\""++?NS_HTTP_BIND++"\"}}"} + end. + + +http_get(#http_bind{pid = FsmRef, wait = Wait, hold = Hold}, Rid) -> + gen_fsm:sync_send_all_state_event( + FsmRef, {http_get, Rid, Wait, Hold}, 2 * ?MAX_WAIT * 1000). + +send_outpacket(#http_bind{pid = FsmRef}, OutPacket) -> + case OutPacket of + [] -> + {200, ?HEADER, "{\"body\": {\"xmlns\":\""++?NS_HTTP_BIND++"\"}}"}; + [{xmlstreamend, _}] -> + gen_fsm:sync_send_all_state_event(FsmRef,{stop,stream_closed}), + {200, ?HEADER, "{\"body\": {\"xmlns\":"++?NS_HTTP_BIND++"\"}}"}; + _ -> + %% TODO: We parse to add a default namespace to packet, + %% The spec says adding the jabber:client namespace if + %% mandatory, even if some implementation do not do that + %% change on packets. + %% I think this should be an option to avoid modifying + %% packet in most case. + AllElements = + lists:all(fun({xmlstreamelement, + {xmlelement, "stream:error", _, _}}) -> false; + ({xmlstreamelement, _}) -> true; + (_) -> false + end, OutPacket), + case AllElements of + true -> + TypedEls = [check_default_xmlns(OEl) || + {xmlstreamelement, OEl} <- OutPacket], + Body = mochijson2:encode(xmpp_json:to_json( + {xmlelement,"body", + [{"xmlns", + ?NS_HTTP_BIND}], + TypedEls})), + ?DEBUG(" --- outgoing data --- ~n~s~n --- END --- ~n", + [Body]), + {200, ?HEADER, Body}; + false -> + case OutPacket of + [{xmlstreamstart, _, _} | SEls] -> + OutEls = + case SEls of + [{xmlstreamelement, + {xmlelement, + "stream:features", + StreamAttribs, StreamEls}} | + StreamTail] -> + TypedTail = + [check_default_xmlns(OEl) || + {xmlstreamelement, OEl} <- + StreamTail], + [{xmlelement, + "stream:features", + [{"xmlns:stream", + ?NS_STREAM}] ++ + StreamAttribs, StreamEls}] ++ + TypedTail; + StreamTail -> + [check_default_xmlns(OEl) || + {xmlstreamelement, OEl} <- + StreamTail] + end, + {200, ?HEADER, + mochijson2:encode( + xmpp_json:to_json( + {xmlelement,"body", + [{"xmlns", + ?NS_HTTP_BIND}], + OutEls}))}; + _ -> + SErrCond = + lists:filter( + fun({xmlstreamelement, + {xmlelement, "stream:error", + _, _}}) -> + true; + (_) -> false + end, OutPacket), + StreamErrCond = + case SErrCond of + [] -> + null; + [{xmlstreamelement, + {xmlelement, _, _, _Cond} = + StreamErrorTag} | _] -> + [StreamErrorTag] + end, + gen_fsm:sync_send_all_state_event(FsmRef, + {stop, {stream_error,OutPacket}}), + case StreamErrCond of + null -> + {200, ?HEADER, + "{\"body\" : {\"\"type\"\":\"terminate\", " + "\"condition\":\"internal-server-error\", " + "\"xmlns\"=\""++?NS_HTTP_BIND++"\"}}"}; + _ -> + {200, ?HEADER, + "{\"body\" : {\"\"type\"\":\"terminate\", " + "\"condition\":\"remote-stream-error\", " + "\"xmlns\":\""++?NS_HTTP_BIND++"\", " ++ + "\"xmlns:stream\":\""++?NS_STREAM++"\" \"$\":" ++ + elements_to_string(StreamErrCond) ++ + "}}"} + end + end + end + end. + +parse_request(Data, PayloadSize, MaxStanzaSize) -> + ?DEBUG("--- incoming data --- ~n~p~n --- END --- ", [xmpp_json:from_json(mochijson2:decode(Data))]), + %% MR: I do not think it works if put put several elements in the + %% same body: + case xmpp_json:from_json(mochijson2:decode(Data)) of + {xmlstreamelement,{xmlelement, "body", Attrs, Els}} -> + Xmlns = xml:get_attr_s("xmlns",Attrs), + if + Xmlns /= ?NS_HTTP_BIND -> + {error, bad_request}; + true -> + case catch list_to_integer(xml:get_attr_s("rid", Attrs)) of + {'EXIT', _} -> + {error, bad_request}; + Rid -> + %% I guess this is to remove XMLCDATA: Is it really needed ? + FixedEls = + lists:filter( + fun(I) -> + case I of + {xmlelement, _, _, _} -> + true; + _ -> + false + end + end, Els), + Sid = xml:get_attr_s("sid",Attrs), + if + PayloadSize =< MaxStanzaSize -> + {ok, {Sid, Rid, Attrs, FixedEls}}; + true -> + {size_limit, Sid} + end + end + end; + {xmlstreamelement,{xmlelement, _Name, _Attrs, _Els}} -> + {error, bad_request}; + {error, _Reason} -> + {error, bad_request} + end. + +send_receiver_reply(undefined, _Reply) -> + ok; +send_receiver_reply(Receiver, Reply) -> + gen_fsm:reply(Receiver, Reply). + + +%% Cancel timer and empty message queue. +cancel_timer(undefined) -> + ok; +cancel_timer(Timer) -> + erlang:cancel_timer(Timer), + receive + {timeout, Timer, _} -> + ok + after 0 -> + ok + end. + +%% If client asked for a pause (pause > 0), we apply the pause value +%% as inactivity timer: +set_inactivity_timer(Pause, _MaxInactivity) when Pause > 0 -> + erlang:start_timer(Pause*1000, self(), []); +%% Otherwise, we apply the max_inactivity value as inactivity timer: +set_inactivity_timer(_Pause, MaxInactivity) -> + erlang:start_timer(MaxInactivity, self(), []). + + +%% TODO: Use tail recursion and list reverse ? +elements_to_string([]) -> + []; +elements_to_string([El | Els]) -> + [mochijson2:encode(xmpp_json:to_json(El))|elements_to_string(Els)]. + +%% @spec (To, Default::integer()) -> integer() +%% where To = [] | {Host::string(), Version::string()} +get_max_inactivity({Host, _}, Default) -> + case gen_mod:get_module_opt(Host, mod_http_bind, max_inactivity, undefined) of + Seconds when is_integer(Seconds) -> + Seconds * 1000; + undefined -> + Default + end; +get_max_inactivity(_, Default) -> + Default. + +get_max_pause({Host, _}) -> + gen_mod:get_module_opt(Host, mod_http_bind, max_pause, ?MAX_PAUSE); +get_max_pause(_) -> + ?MAX_PAUSE. + +%% Current time as integer +tnow() -> + {TMegSec, TSec, TMSec} = now(), + (TMegSec * 1000000 + TSec) * 1000000 + TMSec. + +check_default_xmlns({xmlelement, Name, Attrs, Els} = El) -> + case xml:get_tag_attr_s("xmlns", El) of + "" -> {xmlelement, Name, [{"xmlns", ?NS_CLIENT} | Attrs], Els}; + _ -> El + end. + +%% Check that mod_http_bind has been defined in config file. +%% Print a warning in log file if this is not the case. +check_bind_module(XmppDomain) -> + case gen_mod:is_loaded(XmppDomain, mod_http_bind) of + true -> true; + false -> ?ERROR_MSG("You are trying to use BOSH (HTTP Bind), but the module mod_http_bind is not started.~n" + "Check your 'modules' section in your ejabberd configuration file.",[]), + false + end. + +make_sid() -> + sha:sha(term_to_binary({now(), make_ref()})) + ++ "-" ++ ejabberd_cluster:node_id(). + +get_session(SID) -> + case string:tokens(SID, "-") of + [_, NodeID] -> + case ejabberd_cluster:get_node_by_id(NodeID) of + Node when Node == node() -> + case mnesia:dirty_read({http_bind, SID}) of + [] -> + {error, enoent}; + [Session] -> + {ok, Session} + end; + Node -> + case catch rpc:call(Node, mnesia, dirty_read, + [{http_bind, SID}], 5000) of + [Session] -> + {ok, Session}; + _ -> + {error, enoent} + end + end; + _ -> + {error, enoent} + end. diff --git a/src/web/ejabberd_http_ws.erl b/src/web/ejabberd_http_ws.erl index f8ffb3aef..8fb9d56a5 100644 --- a/src/web/ejabberd_http_ws.erl +++ b/src/web/ejabberd_http_ws.erl @@ -155,21 +155,22 @@ handle_sync_event({send, Packet}, _From, StateName, #state{ws = WS} = StateData) true -> list_to_binary(Packet) end, - ?DEBUG("sending on websocket : ~p ", [Packet2]), + %?DEBUG("sending on websocket : ~p ", [Packet2]), WS:send(Packet2), - {reply, ok, StateName, StateData}; + {reply, ok, StateName, StateData}. -handle_sync_event(close, _From, _StateName, StateData) -> - Reply = ok, - {stop, normal, Reply, StateData}. +handle_info(closed, _StateName, StateData) -> + {stop, normal, StateData}; handle_info({browser, Packet}, StateName, StateData)-> + %?DEBUG("Received on websocket : ~p ", [Packet]), + NPacket = unicode:characters_to_binary(Packet,latin1), NewState = case StateData#state.waiting_input of false -> - Input = [StateData#state.input|Packet], + Input = [StateData#state.input|NPacket], StateData#state{input = Input}; {Receiver, _Tag} -> - Receiver ! {tcp, StateData#state.socket,Packet}, + Receiver ! {tcp, StateData#state.socket,NPacket}, cancel_timer(StateData#state.timer), Timer = erlang:start_timer(StateData#state.timeout, self(), []), StateData#state{waiting_input = false, @@ -190,7 +191,15 @@ handle_info(_, StateName, StateData) -> code_change(_OldVsn, StateName, StateData, _Extra) -> {ok, StateName, StateData}. -terminate(_Reason, _StateName, _StateData) -> ok. +terminate(_Reason, _StateName, StateData) -> + case StateData#state.waiting_input of + false -> + ok; + {Receiver,_} -> + ?DEBUG("C2S Pid : ~p", [Receiver]), + Receiver ! {tcp_closed, StateData#state.socket } + end, + ok. cancel_timer(Timer) -> erlang:cancel_timer(Timer), @@ -199,4 +208,4 @@ cancel_timer(Timer) -> ok after 0 -> ok - end. + end. \ No newline at end of file diff --git a/src/web/ejabberd_http_wsjson.erl b/src/web/ejabberd_http_wsjson.erl new file mode 100644 index 000000000..2abd12b3b --- /dev/null +++ b/src/web/ejabberd_http_wsjson.erl @@ -0,0 +1,219 @@ +%%%---------------------------------------------------------------------- +%%% File : ejabberd_websocket.erl +%%% Author : Eric Cestari +%%% Purpose : JSON - XMPP Websocket module support +%%% Created : 09-10-2010 by Eric Cestari +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2010 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_wsjson). +-author('ecestari@process-one.net'). + +-behaviour(gen_fsm). + +% External exports +-export([ + start/1, + start_link/1, + init/1, + handle_event/3, + handle_sync_event/4, + code_change/4, + handle_info/3, + terminate/3, + send_xml/2, + setopts/2, + sockname/1, peername/1, + controlling_process/2, + become_controller/2, + close/1]). + +-include("ejabberd.hrl"). +-include("jlib.hrl"). +-include("ejabberd_http.hrl"). + +-record(state, { + socket, + timeout, + timer, + input = [], + waiting_input = false, %% {ReceiverPid, Tag} + last_receiver, + ws}). + +%-define(DBGFSM, true). + +-ifdef(DBGFSM). +-define(FSMOPTS, [{debug, [trace]}]). +-else. +-define(FSMOPTS, []). +-endif. + +-define(WEBSOCKET_TIMEOUT, 300000). +% +% +%%%%---------------------------------------------------------------------- +%%%% API +%%%%---------------------------------------------------------------------- +start(WS) -> + supervisor:start_child(ejabberd_wsloop_sup, [WS]). + +start_link(WS) -> + gen_fsm:start_link(?MODULE, [WS],?FSMOPTS). + +send_xml({http_ws, FsmRef, _IP}, Packet) -> + gen_fsm:sync_send_all_state_event(FsmRef, {send, Packet}). + +setopts({http_ws, 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_ws, _FsmRef, IP}) -> + {ok, IP}. + +controlling_process(_Socket, _Pid) -> + ok. + +become_controller(FsmRef, C2SPid) -> + gen_fsm:send_all_state_event(FsmRef, {become_controller, C2SPid}). + +close({http_ws, FsmRef, _IP}) -> + catch gen_fsm:sync_send_all_state_event(FsmRef, close). + +%%% Internal + + +init([WS]) -> + %% Read c2s options from the first ejabberd_c2s configuration in + %% the config file listen section + %% TODO: We should have different access and shaper values for + %% each connector. The default behaviour should be however to use + %% the default c2s restrictions if not defined for the current + %% connector. + Opts = [{xml_socket, true}|ejabberd_c2s_config:get_c2s_limits()], + + WSTimeout = case ejabberd_config:get_local_option({websocket_timeout, + ?MYNAME}) of + %% convert seconds of option into milliseconds + Int when is_integer(Int) -> Int*1000; + undefined -> ?WEBSOCKET_TIMEOUT + end, + + Socket = {http_ws, self(), WS:get(ip)}, + ?DEBUG("Client connected through websocket ~p", [Socket]), + ejabberd_socket:start(ejabberd_c2s, ?MODULE, Socket, Opts), + Timer = erlang:start_timer(WSTimeout, self(), []), + {ok, loop, #state{ + socket = Socket, + timeout = WSTimeout, + timer = Timer, + ws = WS}}. + +handle_event({activate, From}, StateName, StateData) -> + case StateData#state.input of + [] -> + {next_state, StateName, + StateData#state{waiting_input = {From, ok}}}; + Input -> + Receiver = From, + lists:reverse(lists:map(fun(Packet)-> + Receiver ! {tcp, StateData#state.socket, [Packet]} + end, Input)), + {next_state, StateName, StateData#state{input = "", + waiting_input = false, + last_receiver = Receiver + }} + end. + +handle_sync_event({send, Packet}, _From, StateName, #state{ws = WS} = StateData) -> + EJson = xmpp_json:to_json(Packet), + Json = mochijson2:encode(EJson), + WS:send(iolist_to_binary(Json)), + {reply, ok, StateName, StateData}; + +handle_sync_event(close, _From, _StateName, StateData) -> + Reply = ok, + {stop, normal, Reply, StateData}. + +handle_info({browser, <<"\n">>}, StateName, StateData)-> + NewState = case StateData#state.waiting_input of + false -> + ok; + {Receiver, _Tag} -> + Receiver ! {tcp, StateData#state.socket,<<"\n">>}, + cancel_timer(StateData#state.timer), + Timer = erlang:start_timer(StateData#state.timeout, self(), []), + StateData#state{waiting_input = false, + last_receiver = Receiver, + timer = Timer} + end, + {next_state, StateName, NewState}; +handle_info({browser, JsonPacket}, StateName, StateData)-> + NewState = case StateData#state.waiting_input of + false -> + EJson = mochijson2:decode(JsonPacket), + Packet = xmpp_json:from_json(EJson), + Input = [Packet | StateData#state.input], + StateData#state{input = Input}; + {Receiver, _Tag} -> + %?DEBUG("Received from browser : ~p", [JsonPacket]), + EJson = mochijson2:decode(JsonPacket), + %?DEBUG("decoded : ~p", [EJson]), + Packet = xmpp_json:from_json(EJson), + %?DEBUG("sending to c2s : ~p", [Packet]), + Receiver ! {tcp, StateData#state.socket,[Packet]}, + cancel_timer(StateData#state.timer), + Timer = erlang:start_timer(StateData#state.timeout, self(), []), + StateData#state{waiting_input = false, + last_receiver = Receiver, + timer = Timer} + end, + {next_state, StateName, NewState}; + + +handle_info({timeout, Timer, _}, _StateName, + #state{timer = Timer} = StateData) -> + {stop, normal, StateData}; + +handle_info(_, StateName, StateData) -> + {next_state, StateName, StateData}. + + +code_change(_OldVsn, StateName, StateData, _Extra) -> + {ok, StateName, StateData}. + +terminate(_Reason, _StateName, _StateData) -> ok. + +cancel_timer(Timer) -> + erlang:cancel_timer(Timer), + receive + {timeout, Timer, _} -> + ok + after 0 -> + ok + end. diff --git a/src/web/ejabberd_websocket.erl b/src/web/ejabberd_websocket.erl index 2b4d41292..3c7b8236e 100644 --- a/src/web/ejabberd_websocket.erl +++ b/src/web/ejabberd_websocket.erl @@ -53,29 +53,30 @@ check(_Path, Headers)-> % If origins are set in configuration, check if it belongs % If origins not set, access is open. is_acceptable(#ws{origin=Origin, protocol=Protocol, - headers = Headers, acceptable_origins = Origins})-> + headers = Headers, acceptable_origins = Origins, auth_module=undefined})-> ClientProtocol = lists:keyfind("Sec-WebSocket-Protocol",1, Headers), - case {(Origin == []) or lists:member(Origin, Origins), ClientProtocol, Protocol } of + case {(Origins == []) or lists:member(Origin, Origins), ClientProtocol, Protocol } of {false, _, _} -> - ?DEBUG("client does not come from authorized origin", []), + ?INFO_MSG("client does not come from authorized origin", []), false; {_, false, _} -> - ?DEBUG("Client did not ask for protocol", []), true; {_, {_, P}, P} -> - ?DEBUG("Protocoles are matching", []), true; - _ -> false - end. - + _ = E-> + ?INFO_MSG("Wrong protocol requested : ~p", [E]), + false + end; +is_acceptable(#ws{local_path=LocalPath, origin=Origin, ip=IP, q=Q, protocol=Protocol, headers = Headers,auth_module=Module})-> + Module:is_acceptable(LocalPath, Q, Origin, Protocol, IP, Headers). % Connect and handshake with Websocket. -connect(#ws{vsn = Vsn, socket = Socket, origin=Origin, host=Host, port=Port, sockmod = SockMod, path = Path, headers = Headers, ws_autoexit = WsAutoExit} = Ws, WsLoop) -> +connect(#ws{vsn = Vsn, socket = Socket, q=Q,origin=Origin, host=Host, port=Port, sockmod = SockMod, path = Path, headers = Headers, ws_autoexit = WsAutoExit} = Ws, WsLoop) -> % build handshake - HandshakeServer = handshake(Vsn, Socket,SockMod, Headers, {Path, Origin, Host, Port}), + HandshakeServer = handshake(Vsn, Socket,SockMod, Headers, {Path, Q, Origin, Host, Port}), % send handshake back - %?DEBUG("building handshake response : ~p", [HandshakeServer]), SockMod:send(Socket, HandshakeServer), + ?DEBUG("Sent handshake response : ~p", [HandshakeServer]), Ws0 = ejabberd_ws:new(Ws#ws{origin = Origin, host = Host}, self()), %?DEBUG("Ws0 : ~p",[Ws0]), % add data to ws record and spawn controlling process @@ -155,7 +156,7 @@ check_headers(Headers, RequiredHeaders) -> % Function: List % Description: Builds the server handshake response. -handshake({'draft-hixie', 0}, Sock,SocketMod, Headers, {Path, Origin, Host, Port}) -> +handshake({'draft-hixie', 0}, Sock,SocketMod, Headers, {Path, Q,Origin, Host, Port}) -> % build data {_, Key1} = lists:keyfind("Sec-Websocket-Key1",1, Headers), {_, Key2} = lists:keyfind("Sec-Websocket-Key2",1, Headers), @@ -175,6 +176,16 @@ handshake({'draft-hixie', 0}, Sock,SocketMod, Headers, {Path, Origin, Host, Port ?ERROR_MSG("tcp error treating data: ~p", [_Other]), <<>> end, + QParams = lists:map( + fun({nokey,[]})-> + none; + ({K, V})-> + K ++ "=" ++ V + end, Q), + QString = case QParams of + [none]-> ""; + QParams-> "?" ++ string:join(QParams, "&") + end, %?DEBUG("got content in body of websocket request: ~p, ~p", [Body,string:join([Host, Path],"/")]), % prepare handhsake response ["HTTP/1.1 101 WebSocket Protocol Handshake\r\n", @@ -183,7 +194,7 @@ handshake({'draft-hixie', 0}, Sock,SocketMod, Headers, {Path, Origin, Host, Port "Sec-WebSocket-Origin: ", Origin, "\r\n", "Sec-WebSocket-Location: ws://", string:join([Host, integer_to_list(Port)],":"), - "/",string:join(Path,"/") , "\r\n\r\n", + "/",string:join(Path,"/"),QString, "\r\n\r\n", build_challenge({'draft-hixie', 0}, {Key1, Key2, Body}) ]; handshake({'draft-hixie', 68}, _Sock,_SocketMod, _Headers, {Path, Origin, Host, Port}) -> @@ -232,7 +243,7 @@ ws_loop(Socket, Buffer, WsHandleLoopPid, SocketMode, WsAutoExit) -> % close websocket and custom controlling loop websocket_close(Socket, WsHandleLoopPid, SocketMode, WsAutoExit); {send, Data} -> - ?DEBUG("sending data to websocket: ~p", [Data]), + %?DEBUG("sending data to websocket: ~p", [Data]), SocketMode:send(Socket, iolist_to_binary([0,Data,255])), ws_loop(Socket, Buffer, WsHandleLoopPid, SocketMode, WsAutoExit); shutdown -> diff --git a/src/web/mochijson2.erl b/src/web/mochijson2.erl new file mode 100644 index 000000000..710ae9bce --- /dev/null +++ b/src/web/mochijson2.erl @@ -0,0 +1,782 @@ +%% @author Bob Ippolito +%% @copyright 2007 Mochi Media, Inc. + +%% @doc Yet another JSON (RFC 4627) library for Erlang. mochijson2 works +%% with binaries as strings, arrays as lists (without an {array, _}) +%% wrapper and it only knows how to decode UTF-8 (and ASCII). + +-module(mochijson2). +-author('bob@mochimedia.com'). +-export([encoder/1, encode/1]). +-export([decoder/1, decode/1]). + +% This is a macro to placate syntax highlighters.. +-define(Q, $\"). +-define(ADV_COL(S, N), S#decoder{offset=N+S#decoder.offset, + column=N+S#decoder.column}). +-define(INC_COL(S), S#decoder{offset=1+S#decoder.offset, + column=1+S#decoder.column}). +-define(INC_LINE(S), S#decoder{offset=1+S#decoder.offset, + column=1, + line=1+S#decoder.line}). +-define(INC_CHAR(S, C), + case C of + $\n -> + S#decoder{column=1, + line=1+S#decoder.line, + offset=1+S#decoder.offset}; + _ -> + S#decoder{column=1+S#decoder.column, + offset=1+S#decoder.offset} + end). +-define(IS_WHITESPACE(C), + (C =:= $\s orelse C =:= $\t orelse C =:= $\r orelse C =:= $\n)). + +%% @type iolist() = [char() | binary() | iolist()] +%% @type iodata() = iolist() | binary() +%% @type json_string() = atom | binary() +%% @type json_number() = integer() | float() +%% @type json_array() = [json_term()] +%% @type json_object() = {struct, [{json_string(), json_term()}]} +%% @type json_iolist() = {json, iolist()} +%% @type json_term() = json_string() | json_number() | json_array() | +%% json_object() | json_iolist() + +-record(encoder, {handler=null, + utf8=false}). + +-record(decoder, {object_hook=null, + offset=0, + line=1, + column=1, + state=null}). + +%% @spec encoder([encoder_option()]) -> function() +%% @doc Create an encoder/1 with the given options. +%% @type encoder_option() = handler_option() | utf8_option() +%% @type utf8_option() = boolean(). Emit unicode as utf8 (default - false) +encoder(Options) -> + State = parse_encoder_options(Options, #encoder{}), + fun (O) -> json_encode(O, State) end. + +%% @spec encode(json_term()) -> iolist() +%% @doc Encode the given as JSON to an iolist. +encode(Any) -> + json_encode(Any, #encoder{}). + +%% @spec decoder([decoder_option()]) -> function() +%% @doc Create a decoder/1 with the given options. +decoder(Options) -> + State = parse_decoder_options(Options, #decoder{}), + fun (O) -> json_decode(O, State) end. + +%% @spec decode(iolist()) -> json_term() +%% @doc Decode the given iolist to Erlang terms. +decode(S) -> + json_decode(S, #decoder{}). + +%% Internal API + +parse_encoder_options([], State) -> + State; +parse_encoder_options([{handler, Handler} | Rest], State) -> + parse_encoder_options(Rest, State#encoder{handler=Handler}); +parse_encoder_options([{utf8, Switch} | Rest], State) -> + parse_encoder_options(Rest, State#encoder{utf8=Switch}). + +parse_decoder_options([], State) -> + State; +parse_decoder_options([{object_hook, Hook} | Rest], State) -> + parse_decoder_options(Rest, State#decoder{object_hook=Hook}). + +json_encode(true, _State) -> + <<"true">>; +json_encode(false, _State) -> + <<"false">>; +json_encode(null, _State) -> + <<"null">>; +json_encode(I, _State) when is_integer(I) andalso I >= -2147483648 andalso I =< 2147483647 -> + %% Anything outside of 32-bit integers should be encoded as a float + integer_to_list(I); +json_encode(I, _State) when is_integer(I) -> + mochinum:digits(float(I)); +json_encode(F, _State) when is_float(F) -> + mochinum:digits(F); +json_encode(S, State) when is_binary(S); is_atom(S) -> + json_encode_string(S, State); +json_encode(Array, State) when is_list(Array) -> + json_encode_array(Array, State); +json_encode({struct, Props}, State) when is_list(Props) -> + json_encode_proplist(Props, State); +json_encode({json, IoList}, _State) -> + IoList; +json_encode(Bad, #encoder{handler=null}) -> + exit({json_encode, {bad_term, Bad}}); +json_encode(Bad, State=#encoder{handler=Handler}) -> + json_encode(Handler(Bad), State). + +json_encode_array([], _State) -> + <<"[]">>; +json_encode_array(L, State) -> + F = fun (O, Acc) -> + [$,, json_encode(O, State) | Acc] + end, + [$, | Acc1] = lists:foldl(F, "[", L), + lists:reverse([$\] | Acc1]). + +json_encode_proplist([], _State) -> + <<"{}">>; +json_encode_proplist(Props, State) -> + F = fun ({K, V}, Acc) -> + KS = json_encode_string(K, State), + VS = json_encode(V, State), + [$,, VS, $:, KS | Acc] + end, + [$, | Acc1] = lists:foldl(F, "{", Props), + lists:reverse([$\} | Acc1]). + +json_encode_string(A, State) when is_atom(A) -> + L = atom_to_list(A), + case json_string_is_safe(L) of + true -> + [?Q, L, ?Q]; + false -> + json_encode_string_unicode(xmerl_ucs:from_utf8(L), State, [?Q]) + end; +json_encode_string(B, State) when is_binary(B) -> + case json_bin_is_safe(B) of + true -> + [?Q, B, ?Q]; + false -> + json_encode_string_unicode(xmerl_ucs:from_utf8(B), State, [?Q]) + end; +json_encode_string(I, _State) when is_integer(I) -> + [?Q, integer_to_list(I), ?Q]; +json_encode_string(L, State) when is_list(L) -> + case json_string_is_safe(L) of + true -> + [?Q, L, ?Q]; + false -> + json_encode_string_unicode(L, State, [?Q]) + end. + +json_string_is_safe([]) -> + true; +json_string_is_safe([C | Rest]) -> + case C of + ?Q -> + false; + $\\ -> + false; + $\b -> + false; + $\f -> + false; + $\n -> + false; + $\r -> + false; + $\t -> + false; + C when C >= 0, C < $\s; C >= 16#7f, C =< 16#10FFFF -> + false; + C when C < 16#7f -> + json_string_is_safe(Rest); + _ -> + false + end. + +json_bin_is_safe(<<>>) -> + true; +json_bin_is_safe(<>) -> + case C of + ?Q -> + false; + $\\ -> + false; + $\b -> + false; + $\f -> + false; + $\n -> + false; + $\r -> + false; + $\t -> + false; + C when C >= 0, C < $\s; C >= 16#7f -> + false; + C when C < 16#7f -> + json_bin_is_safe(Rest) + end. + +json_encode_string_unicode([], _State, Acc) -> + lists:reverse([$\" | Acc]); +json_encode_string_unicode([C | Cs], State, Acc) -> + Acc1 = case C of + ?Q -> + [?Q, $\\ | Acc]; + %% Escaping solidus is only useful when trying to protect + %% against "" injection attacks which are only + %% possible when JSON is inserted into a HTML document + %% in-line. mochijson2 does not protect you from this, so + %% if you do insert directly into HTML then you need to + %% uncomment the following case or escape the output of encode. + %% + %% $/ -> + %% [$/, $\\ | Acc]; + %% + $\\ -> + [$\\, $\\ | Acc]; + $\b -> + [$b, $\\ | Acc]; + $\f -> + [$f, $\\ | Acc]; + $\n -> + [$n, $\\ | Acc]; + $\r -> + [$r, $\\ | Acc]; + $\t -> + [$t, $\\ | Acc]; + C when C >= 0, C < $\s -> + [unihex(C) | Acc]; + C when C >= 16#7f, C =< 16#10FFFF, State#encoder.utf8 -> + [xmerl_ucs:to_utf8(C) | Acc]; + C when C >= 16#7f, C =< 16#10FFFF, not State#encoder.utf8 -> + [unihex(C) | Acc]; + C when C < 16#7f -> + [C | Acc]; + _ -> + exit({json_encode, {bad_char, C}}) + end, + json_encode_string_unicode(Cs, State, Acc1). + +hexdigit(C) when C >= 0, C =< 9 -> + C + $0; +hexdigit(C) when C =< 15 -> + C + $a - 10. + +unihex(C) when C < 16#10000 -> + <> = <>, + Digits = [hexdigit(D) || D <- [D3, D2, D1, D0]], + [$\\, $u | Digits]; +unihex(C) when C =< 16#10FFFF -> + N = C - 16#10000, + S1 = 16#d800 bor ((N bsr 10) band 16#3ff), + S2 = 16#dc00 bor (N band 16#3ff), + [unihex(S1), unihex(S2)]. + +json_decode(L, S) when is_list(L) -> + json_decode(iolist_to_binary(L), S); +json_decode(B, S) -> + {Res, S1} = decode1(B, S), + {eof, _} = tokenize(B, S1#decoder{state=trim}), + Res. + +decode1(B, S=#decoder{state=null}) -> + case tokenize(B, S#decoder{state=any}) of + {{const, C}, S1} -> + {C, S1}; + {start_array, S1} -> + decode_array(B, S1); + {start_object, S1} -> + decode_object(B, S1) + end. + +make_object(V, #decoder{object_hook=null}) -> + V; +make_object(V, #decoder{object_hook=Hook}) -> + Hook(V). + +decode_object(B, S) -> + decode_object(B, S#decoder{state=key}, []). + +decode_object(B, S=#decoder{state=key}, Acc) -> + case tokenize(B, S) of + {end_object, S1} -> + V = make_object({struct, lists:reverse(Acc)}, S1), + {V, S1#decoder{state=null}}; + {{const, K}, S1} -> + {colon, S2} = tokenize(B, S1), + {V, S3} = decode1(B, S2#decoder{state=null}), + decode_object(B, S3#decoder{state=comma}, [{K, V} | Acc]) + end; +decode_object(B, S=#decoder{state=comma}, Acc) -> + case tokenize(B, S) of + {end_object, S1} -> + V = make_object({struct, lists:reverse(Acc)}, S1), + {V, S1#decoder{state=null}}; + {comma, S1} -> + decode_object(B, S1#decoder{state=key}, Acc) + end. + +decode_array(B, S) -> + decode_array(B, S#decoder{state=any}, []). + +decode_array(B, S=#decoder{state=any}, Acc) -> + case tokenize(B, S) of + {end_array, S1} -> + {lists:reverse(Acc), S1#decoder{state=null}}; + {start_array, S1} -> + {Array, S2} = decode_array(B, S1), + decode_array(B, S2#decoder{state=comma}, [Array | Acc]); + {start_object, S1} -> + {Array, S2} = decode_object(B, S1), + decode_array(B, S2#decoder{state=comma}, [Array | Acc]); + {{const, Const}, S1} -> + decode_array(B, S1#decoder{state=comma}, [Const | Acc]) + end; +decode_array(B, S=#decoder{state=comma}, Acc) -> + case tokenize(B, S) of + {end_array, S1} -> + {lists:reverse(Acc), S1#decoder{state=null}}; + {comma, S1} -> + decode_array(B, S1#decoder{state=any}, Acc) + end. + +tokenize_string(B, S=#decoder{offset=O}) -> + case tokenize_string_fast(B, O) of + {escape, O1} -> + Length = O1 - O, + S1 = ?ADV_COL(S, Length), + <<_:O/binary, Head:Length/binary, _/binary>> = B, + tokenize_string(B, S1, lists:reverse(binary_to_list(Head))); + O1 -> + Length = O1 - O, + <<_:O/binary, String:Length/binary, ?Q, _/binary>> = B, + {{const, String}, ?ADV_COL(S, Length + 1)} + end. + +tokenize_string_fast(B, O) -> + case B of + <<_:O/binary, ?Q, _/binary>> -> + O; + <<_:O/binary, $\\, _/binary>> -> + {escape, O}; + <<_:O/binary, C1, _/binary>> when C1 < 128 -> + tokenize_string_fast(B, 1 + O); + <<_:O/binary, C1, C2, _/binary>> when C1 >= 194, C1 =< 223, + C2 >= 128, C2 =< 191 -> + tokenize_string_fast(B, 2 + O); + <<_:O/binary, C1, C2, C3, _/binary>> when C1 >= 224, C1 =< 239, + C2 >= 128, C2 =< 191, + C3 >= 128, C3 =< 191 -> + tokenize_string_fast(B, 3 + O); + <<_:O/binary, C1, C2, C3, C4, _/binary>> when C1 >= 240, C1 =< 244, + C2 >= 128, C2 =< 191, + C3 >= 128, C3 =< 191, + C4 >= 128, C4 =< 191 -> + tokenize_string_fast(B, 4 + O); + _ -> + throw(invalid_utf8) + end. + +tokenize_string(B, S=#decoder{offset=O}, Acc) -> + case B of + <<_:O/binary, ?Q, _/binary>> -> + {{const, iolist_to_binary(lists:reverse(Acc))}, ?INC_COL(S)}; + <<_:O/binary, "\\\"", _/binary>> -> + tokenize_string(B, ?ADV_COL(S, 2), [$\" | Acc]); + <<_:O/binary, "\\\\", _/binary>> -> + tokenize_string(B, ?ADV_COL(S, 2), [$\\ | Acc]); + <<_:O/binary, "\\/", _/binary>> -> + tokenize_string(B, ?ADV_COL(S, 2), [$/ | Acc]); + <<_:O/binary, "\\b", _/binary>> -> + tokenize_string(B, ?ADV_COL(S, 2), [$\b | Acc]); + <<_:O/binary, "\\f", _/binary>> -> + tokenize_string(B, ?ADV_COL(S, 2), [$\f | Acc]); + <<_:O/binary, "\\n", _/binary>> -> + tokenize_string(B, ?ADV_COL(S, 2), [$\n | Acc]); + <<_:O/binary, "\\r", _/binary>> -> + tokenize_string(B, ?ADV_COL(S, 2), [$\r | Acc]); + <<_:O/binary, "\\t", _/binary>> -> + tokenize_string(B, ?ADV_COL(S, 2), [$\t | Acc]); + <<_:O/binary, "\\u", C3, C2, C1, C0, Rest/binary>> -> + C = erlang:list_to_integer([C3, C2, C1, C0], 16), + if C > 16#D7FF, C < 16#DC00 -> + %% coalesce UTF-16 surrogate pair + <<"\\u", D3, D2, D1, D0, _/binary>> = Rest, + D = erlang:list_to_integer([D3,D2,D1,D0], 16), + [CodePoint] = xmerl_ucs:from_utf16be(<>), + Acc1 = lists:reverse(xmerl_ucs:to_utf8(CodePoint), Acc), + tokenize_string(B, ?ADV_COL(S, 12), Acc1); + true -> + Acc1 = lists:reverse(xmerl_ucs:to_utf8(C), Acc), + tokenize_string(B, ?ADV_COL(S, 6), Acc1) + end; + <<_:O/binary, C, _/binary>> -> + tokenize_string(B, ?INC_CHAR(S, C), [C | Acc]) + end. + +tokenize_number(B, S) -> + case tokenize_number(B, sign, S, []) of + {{int, Int}, S1} -> + {{const, list_to_integer(Int)}, S1}; + {{float, Float}, S1} -> + {{const, list_to_float(Float)}, S1} + end. + +tokenize_number(B, sign, S=#decoder{offset=O}, []) -> + case B of + <<_:O/binary, $-, _/binary>> -> + tokenize_number(B, int, ?INC_COL(S), [$-]); + _ -> + tokenize_number(B, int, S, []) + end; +tokenize_number(B, int, S=#decoder{offset=O}, Acc) -> + case B of + <<_:O/binary, $0, _/binary>> -> + tokenize_number(B, frac, ?INC_COL(S), [$0 | Acc]); + <<_:O/binary, C, _/binary>> when C >= $1 andalso C =< $9 -> + tokenize_number(B, int1, ?INC_COL(S), [C | Acc]) + end; +tokenize_number(B, int1, S=#decoder{offset=O}, Acc) -> + case B of + <<_:O/binary, C, _/binary>> when C >= $0 andalso C =< $9 -> + tokenize_number(B, int1, ?INC_COL(S), [C | Acc]); + _ -> + tokenize_number(B, frac, S, Acc) + end; +tokenize_number(B, frac, S=#decoder{offset=O}, Acc) -> + case B of + <<_:O/binary, $., C, _/binary>> when C >= $0, C =< $9 -> + tokenize_number(B, frac1, ?ADV_COL(S, 2), [C, $. | Acc]); + <<_:O/binary, E, _/binary>> when E =:= $e orelse E =:= $E -> + tokenize_number(B, esign, ?INC_COL(S), [$e, $0, $. | Acc]); + _ -> + {{int, lists:reverse(Acc)}, S} + end; +tokenize_number(B, frac1, S=#decoder{offset=O}, Acc) -> + case B of + <<_:O/binary, C, _/binary>> when C >= $0 andalso C =< $9 -> + tokenize_number(B, frac1, ?INC_COL(S), [C | Acc]); + <<_:O/binary, E, _/binary>> when E =:= $e orelse E =:= $E -> + tokenize_number(B, esign, ?INC_COL(S), [$e | Acc]); + _ -> + {{float, lists:reverse(Acc)}, S} + end; +tokenize_number(B, esign, S=#decoder{offset=O}, Acc) -> + case B of + <<_:O/binary, C, _/binary>> when C =:= $- orelse C=:= $+ -> + tokenize_number(B, eint, ?INC_COL(S), [C | Acc]); + _ -> + tokenize_number(B, eint, S, Acc) + end; +tokenize_number(B, eint, S=#decoder{offset=O}, Acc) -> + case B of + <<_:O/binary, C, _/binary>> when C >= $0 andalso C =< $9 -> + tokenize_number(B, eint1, ?INC_COL(S), [C | Acc]) + end; +tokenize_number(B, eint1, S=#decoder{offset=O}, Acc) -> + case B of + <<_:O/binary, C, _/binary>> when C >= $0 andalso C =< $9 -> + tokenize_number(B, eint1, ?INC_COL(S), [C | Acc]); + _ -> + {{float, lists:reverse(Acc)}, S} + end. + +tokenize(B, S=#decoder{offset=O}) -> + case B of + <<_:O/binary, C, _/binary>> when ?IS_WHITESPACE(C) -> + tokenize(B, ?INC_CHAR(S, C)); + <<_:O/binary, "{", _/binary>> -> + {start_object, ?INC_COL(S)}; + <<_:O/binary, "}", _/binary>> -> + {end_object, ?INC_COL(S)}; + <<_:O/binary, "[", _/binary>> -> + {start_array, ?INC_COL(S)}; + <<_:O/binary, "]", _/binary>> -> + {end_array, ?INC_COL(S)}; + <<_:O/binary, ",", _/binary>> -> + {comma, ?INC_COL(S)}; + <<_:O/binary, ":", _/binary>> -> + {colon, ?INC_COL(S)}; + <<_:O/binary, "null", _/binary>> -> + {{const, null}, ?ADV_COL(S, 4)}; + <<_:O/binary, "true", _/binary>> -> + {{const, true}, ?ADV_COL(S, 4)}; + <<_:O/binary, "false", _/binary>> -> + {{const, false}, ?ADV_COL(S, 5)}; + <<_:O/binary, "\"", _/binary>> -> + tokenize_string(B, ?INC_COL(S)); + <<_:O/binary, C, _/binary>> when (C >= $0 andalso C =< $9) + orelse C =:= $- -> + tokenize_number(B, S); + <<_:O/binary>> -> + trim = S#decoder.state, + {eof, S} + end. +%% +%% Tests +%% +-include_lib("eunit/include/eunit.hrl"). +-ifdef(TEST). + + +%% testing constructs borrowed from the Yaws JSON implementation. + +%% Create an object from a list of Key/Value pairs. + +obj_new() -> + {struct, []}. + +is_obj({struct, Props}) -> + F = fun ({K, _}) when is_binary(K) -> true end, + lists:all(F, Props). + +obj_from_list(Props) -> + Obj = {struct, Props}, + ?assert(is_obj(Obj)), + Obj. + +%% Test for equivalence of Erlang terms. +%% Due to arbitrary order of construction, equivalent objects might +%% compare unequal as erlang terms, so we need to carefully recurse +%% through aggregates (tuples and objects). + +equiv({struct, Props1}, {struct, Props2}) -> + equiv_object(Props1, Props2); +equiv(L1, L2) when is_list(L1), is_list(L2) -> + equiv_list(L1, L2); +equiv(N1, N2) when is_number(N1), is_number(N2) -> N1 == N2; +equiv(B1, B2) when is_binary(B1), is_binary(B2) -> B1 == B2; +equiv(A, A) when A =:= true orelse A =:= false orelse A =:= null -> true. + +%% Object representation and traversal order is unknown. +%% Use the sledgehammer and sort property lists. + +equiv_object(Props1, Props2) -> + L1 = lists:keysort(1, Props1), + L2 = lists:keysort(1, Props2), + Pairs = lists:zip(L1, L2), + true = lists:all(fun({{K1, V1}, {K2, V2}}) -> + equiv(K1, K2) and equiv(V1, V2) + end, Pairs). + +%% Recursively compare tuple elements for equivalence. + +equiv_list([], []) -> + true; +equiv_list([V1 | L1], [V2 | L2]) -> + equiv(V1, V2) andalso equiv_list(L1, L2). + +decode_test() -> + [1199344435545.0, 1] = decode(<<"[1199344435545.0,1]">>), + <<16#F0,16#9D,16#9C,16#95>> = decode([34,"\\ud835","\\udf15",34]). + +e2j_vec_test() -> + test_one(e2j_test_vec(utf8), 1). + +test_one([], _N) -> + %% io:format("~p tests passed~n", [N-1]), + ok; +test_one([{E, J} | Rest], N) -> + %% io:format("[~p] ~p ~p~n", [N, E, J]), + true = equiv(E, decode(J)), + true = equiv(E, decode(encode(E))), + test_one(Rest, 1+N). + +e2j_test_vec(utf8) -> + [ + {1, "1"}, + {3.1416, "3.14160"}, %% text representation may truncate, trail zeroes + {-1, "-1"}, + {-3.1416, "-3.14160"}, + {12.0e10, "1.20000e+11"}, + {1.234E+10, "1.23400e+10"}, + {-1.234E-10, "-1.23400e-10"}, + {10.0, "1.0e+01"}, + {123.456, "1.23456E+2"}, + {10.0, "1e1"}, + {<<"foo">>, "\"foo\""}, + {<<"foo", 5, "bar">>, "\"foo\\u0005bar\""}, + {<<"">>, "\"\""}, + {<<"\n\n\n">>, "\"\\n\\n\\n\""}, + {<<"\" \b\f\r\n\t\"">>, "\"\\\" \\b\\f\\r\\n\\t\\\"\""}, + {obj_new(), "{}"}, + {obj_from_list([{<<"foo">>, <<"bar">>}]), "{\"foo\":\"bar\"}"}, + {obj_from_list([{<<"foo">>, <<"bar">>}, {<<"baz">>, 123}]), + "{\"foo\":\"bar\",\"baz\":123}"}, + {[], "[]"}, + {[[]], "[[]]"}, + {[1, <<"foo">>], "[1,\"foo\"]"}, + + %% json array in a json object + {obj_from_list([{<<"foo">>, [123]}]), + "{\"foo\":[123]}"}, + + %% json object in a json object + {obj_from_list([{<<"foo">>, obj_from_list([{<<"bar">>, true}])}]), + "{\"foo\":{\"bar\":true}}"}, + + %% fold evaluation order + {obj_from_list([{<<"foo">>, []}, + {<<"bar">>, obj_from_list([{<<"baz">>, true}])}, + {<<"alice">>, <<"bob">>}]), + "{\"foo\":[],\"bar\":{\"baz\":true},\"alice\":\"bob\"}"}, + + %% json object in a json array + {[-123, <<"foo">>, obj_from_list([{<<"bar">>, []}]), null], + "[-123,\"foo\",{\"bar\":[]},null]"} + ]. + +%% test utf8 encoding +encoder_utf8_test() -> + %% safe conversion case (default) + [34,"\\u0001","\\u0442","\\u0435","\\u0441","\\u0442",34] = + encode(<<1,"\321\202\320\265\321\201\321\202">>), + + %% raw utf8 output (optional) + Enc = mochijson2:encoder([{utf8, true}]), + [34,"\\u0001",[209,130],[208,181],[209,129],[209,130],34] = + Enc(<<1,"\321\202\320\265\321\201\321\202">>). + +input_validation_test() -> + Good = [ + {16#00A3, <>}, %% pound + {16#20AC, <>}, %% euro + {16#10196, <>} %% denarius + ], + lists:foreach(fun({CodePoint, UTF8}) -> + Expect = list_to_binary(xmerl_ucs:to_utf8(CodePoint)), + Expect = decode(UTF8) + end, Good), + + Bad = [ + %% 2nd, 3rd, or 4th byte of a multi-byte sequence w/o leading byte + <>, + %% missing continuations, last byte in each should be 80-BF + <>, + <>, + <>, + %% we don't support code points > 10FFFF per RFC 3629 + <> + ], + lists:foreach( + fun(X) -> + ok = try decode(X) catch invalid_utf8 -> ok end, + %% could be {ucs,{bad_utf8_character_code}} or + %% {json_encode,{bad_char,_}} + {'EXIT', _} = (catch encode(X)) + end, Bad). + +inline_json_test() -> + ?assertEqual(<<"\"iodata iodata\"">>, + iolist_to_binary( + encode({json, [<<"\"iodata">>, " iodata\""]}))), + ?assertEqual({struct, [{<<"key">>, <<"iodata iodata">>}]}, + decode( + encode({struct, + [{key, {json, [<<"\"iodata">>, " iodata\""]}}]}))), + ok. + +big_unicode_test() -> + UTF8Seq = list_to_binary(xmerl_ucs:to_utf8(16#0001d120)), + ?assertEqual( + <<"\"\\ud834\\udd20\"">>, + iolist_to_binary(encode(UTF8Seq))), + ?assertEqual( + UTF8Seq, + decode(iolist_to_binary(encode(UTF8Seq)))), + ok. + +custom_decoder_test() -> + ?assertEqual( + {struct, [{<<"key">>, <<"value">>}]}, + (decoder([]))("{\"key\": \"value\"}")), + F = fun ({struct, [{<<"key">>, <<"value">>}]}) -> win end, + ?assertEqual( + win, + (decoder([{object_hook, F}]))("{\"key\": \"value\"}")), + ok. + +atom_test() -> + %% JSON native atoms + [begin + ?assertEqual(A, decode(atom_to_list(A))), + ?assertEqual(iolist_to_binary(atom_to_list(A)), + iolist_to_binary(encode(A))) + end || A <- [true, false, null]], + %% Atom to string + ?assertEqual( + <<"\"foo\"">>, + iolist_to_binary(encode(foo))), + ?assertEqual( + <<"\"\\ud834\\udd20\"">>, + iolist_to_binary(encode(list_to_atom(xmerl_ucs:to_utf8(16#0001d120))))), + ok. + +key_encode_test() -> + %% Some forms are accepted as keys that would not be strings in other + %% cases + ?assertEqual( + <<"{\"foo\":1}">>, + iolist_to_binary(encode({struct, [{foo, 1}]}))), + ?assertEqual( + <<"{\"foo\":1}">>, + iolist_to_binary(encode({struct, [{<<"foo">>, 1}]}))), + ?assertEqual( + <<"{\"foo\":1}">>, + iolist_to_binary(encode({struct, [{"foo", 1}]}))), + ?assertEqual( + <<"{\"\\ud834\\udd20\":1}">>, + iolist_to_binary( + encode({struct, [{[16#0001d120], 1}]}))), + ?assertEqual( + <<"{\"1\":1}">>, + iolist_to_binary(encode({struct, [{1, 1}]}))), + ok. + +unsafe_chars_test() -> + Chars = "\"\\\b\f\n\r\t", + [begin + ?assertEqual(false, json_string_is_safe([C])), + ?assertEqual(false, json_bin_is_safe(<>)), + ?assertEqual(<>, decode(encode(<>))) + end || C <- Chars], + ?assertEqual( + false, + json_string_is_safe([16#0001d120])), + ?assertEqual( + false, + json_bin_is_safe(list_to_binary(xmerl_ucs:to_utf8(16#0001d120)))), + ?assertEqual( + [16#0001d120], + xmerl_ucs:from_utf8( + binary_to_list( + decode(encode(list_to_atom(xmerl_ucs:to_utf8(16#0001d120))))))), + ?assertEqual( + false, + json_string_is_safe([16#110000])), + ?assertEqual( + false, + json_bin_is_safe(list_to_binary(xmerl_ucs:to_utf8([16#110000])))), + %% solidus can be escaped but isn't unsafe by default + ?assertEqual( + <<"/">>, + decode(<<"\"\\/\"">>)), + ok. + +int_test() -> + ?assertEqual(0, decode("0")), + ?assertEqual(1, decode("1")), + ?assertEqual(11, decode("11")), + ok. + +float_fallback_test() -> + ?assertEqual(<<"-2147483649.0">>, iolist_to_binary(encode(-2147483649))), + ?assertEqual(<<"2147483648.0">>, iolist_to_binary(encode(2147483648))), + ok. + +handler_test() -> + ?assertEqual( + {'EXIT',{json_encode,{bad_term,{}}}}, + catch encode({})), + F = fun ({}) -> [] end, + ?assertEqual( + <<"[]">>, + iolist_to_binary((encoder([{handler, F}]))({}))), + ok. + +-endif. \ No newline at end of file diff --git a/src/web/mod_http_bindjson.erl b/src/web/mod_http_bindjson.erl new file mode 100644 index 000000000..6090ee458 --- /dev/null +++ b/src/web/mod_http_bindjson.erl @@ -0,0 +1,156 @@ +%%%---------------------------------------------------------------------- +%%% File : mod_http_bindjson.erl +%%% Original Author : Stefan Strigler +%%% Purpose : Implementation of XMPP over BOSH (XEP-0206) +%%% Created : Tue Feb 20 13:15:52 CET 2007 +%%% +%%% ejabberd, Copyright (C) 2002-2010 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 +%%% +%%%---------------------------------------------------------------------- + +%%%---------------------------------------------------------------------- +%%% This module acts as a bridge to ejabberd_http_bind which implements +%%% the real stuff, this is to handle the new pluggable architecture for +%%% extending ejabberd's http service. +%%%---------------------------------------------------------------------- +%%% I will probable kill and merge code with the original mod_http_bind +%%% if this feature gains traction. +%%% Eric Cestari + +-module(mod_http_bindjson). +-author('steve@zeank.in-berlin.de'). + +%%-define(ejabberd_debug, true). + +-behaviour(gen_mod). + +-export([ + start/2, + stop/1, + process/2 + ]). + +-include("ejabberd.hrl"). +-include("jlib.hrl"). +-include("ejabberd_http.hrl"). +-include("http_bind.hrl"). + +%% Duplicated from ejabberd_http_bind. +%% TODO: move to hrl file. +-record(http_bind, {id, pid, to, hold, wait, process_delay, version}). + +%%%---------------------------------------------------------------------- +%%% API +%%%---------------------------------------------------------------------- + +process([], #request{method = 'POST', + data = []}) -> + ?DEBUG("Bad Request: no data", []), + {400, ?HEADER, {xmlelement, "h1", [], + [{xmlcdata, "400 Bad Request"}]}}; +process([], #request{method = 'POST', + data = Data, + ip = IP}) -> + ?DEBUG("Incoming data: ~s", [Data]), + %NOTE the whole point of this file is this line. + ejabberd_http_bindjson:process_request(Data, IP); +process([], #request{method = 'GET', + data = []}) -> + {200, ?HEADER, get_human_html_xmlel()}; +process([], #request{method = 'OPTIONS', + data = []}) -> + {200, ?OPTIONS_HEADER, []}; +process(_Path, _Request) -> + ?DEBUG("Bad Request: ~p", [_Request]), + {400, ?HEADER, {xmlelement, "h1", [], + [{xmlcdata, "400 Bad Request"}]}}. + +get_human_html_xmlel() -> + Heading = "ejabberd " ++ atom_to_list(?MODULE), + {xmlelement, "html", [{"xmlns", "http://www.w3.org/1999/xhtml"}], + [{xmlelement, "head", [], + [{xmlelement, "title", [], [{xmlcdata, Heading}]}]}, + {xmlelement, "body", [], + [{xmlelement, "h1", [], [{xmlcdata, Heading}]}, + {xmlelement, "p", [], + [{xmlcdata, "An implementation of "}, + {xmlelement, "a", + [{"href", "http://xmpp.org/extensions/xep-0206.html"}], + [{xmlcdata, "XMPP over BOSH (XEP-0206)"}]}]}, + {xmlelement, "p", [], + [{xmlcdata, "This web page is only informative. " + "To use HTTP-Bind you need a Jabber/XMPP client that supports it."} + ]} + ]}]}. + +%%%---------------------------------------------------------------------- +%%% BEHAVIOUR CALLBACKS +%%%---------------------------------------------------------------------- +start(_Host, _Opts) -> + setup_database(), + HTTPBindSupervisor = + {ejabberd_http_bind_sup, + {ejabberd_tmp_sup, start_link, + [ejabberd_http_bind_sup, ejabberd_http_bind]}, + permanent, + infinity, + supervisor, + [ejabberd_tmp_sup]}, + case supervisor:start_child(ejabberd_sup, HTTPBindSupervisor) of + {ok, _Pid} -> + ok; + {ok, _Pid, _Info} -> + ok; + {error, {already_started, _PidOther}} -> + % mod_http_bind is already started so it will not be started again + ok; + {error, Error} -> + {'EXIT', {start_child_error, Error}} + end. + +stop(_Host) -> + case supervisor:terminate_child(ejabberd_sup, ejabberd_http_bind_sup) of + ok -> + ok; + {error, Error} -> + {'EXIT', {terminate_child_error, Error}} + end. + +setup_database() -> + migrate_database(), + mnesia:create_table(http_bind, + [{ram_copies, [node()]}, + {local_content, true}, + {attributes, record_info(fields, http_bind)}]), + mnesia:add_table_copy(http_bind, node(), ram_copies). + +migrate_database() -> + case catch mnesia:table_info(http_bind, attributes) of + [id, pid, to, hold, wait, process_delay, version] -> + ok; + _ -> + %% Since the stored information is not important, instead + %% of actually migrating data, let's just destroy the table + mnesia:delete_table(http_bind) + end, + case catch mnesia:table_info(http_bind, local_content) of + false -> + mnesia:delete_table(http_bind); + _ -> + ok + end. diff --git a/src/web/simple_ws_check.erl b/src/web/simple_ws_check.erl new file mode 100644 index 000000000..8ef160980 --- /dev/null +++ b/src/web/simple_ws_check.erl @@ -0,0 +1,11 @@ +-module (simple_ws_check). +-export ([is_acceptable/6]). +-include("ejabberd.hrl"). +is_acceptable(["true"]=Path, Q, Origin, Protocol, IP, Headers)-> + ?INFO_MSG("Authorized Websocket ~p with: ~n Q = ~p~n Origin = ~p~n Protocol = ~p~n IP = ~p~n Headers = ~p~n", + [Path, Q, Origin, Protocol, IP, Headers]), + true; +is_acceptable(["false"]=Path, Q, Origin, Protocol, IP, Headers)-> + ?INFO_MSG("Failed Websocket ~p with: ~n Q = ~p~n Origin = ~p~n Protocol = ~p~n IP = ~p~n Headers = ~p~n", + [Path, Q, Origin, Protocol, IP, Headers]), + false. \ No newline at end of file diff --git a/src/web/websocket_test.erl b/src/web/websocket_test.erl index 5928e7d83..b5491bc08 100644 --- a/src/web/websocket_test.erl +++ b/src/web/websocket_test.erl @@ -1,9 +1,10 @@ -module (websocket_test). --export([start/1, loop/1]). +-export([start_link/1, loop/1]). % callback on received websockets data -start(Ws) -> - spawn(?MODULE, loop, [Ws]). +start_link(Ws) -> + Pid = spawn_link(?MODULE, loop, [Ws]), + {ok, Pid}. loop(Ws) -> receive diff --git a/src/web/xmpp_json.erl b/src/web/xmpp_json.erl new file mode 100644 index 000000000..6fae2ab08 --- /dev/null +++ b/src/web/xmpp_json.erl @@ -0,0 +1,362 @@ +%%%---------------------------------------------------------------------- +%%% File : xmpp_json.erl +%%% Author : Eric Cestari +%%% Purpose : Converts {xmlelement,Name, A, Sub} to/from JSON as per protoxep +%%% Created : 09-20-2010 +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2010 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 (xmpp_json). + +-export([to_json/1, from_json/1]). + + + +%%% FROM JSON TO XML + +from_json({struct, [{<<"stream">>, _Attr}]=Elems}) -> + parse_start(Elems); + +from_json({struct, Elems}) -> + {xmlstreamelement, hd(from_json2({struct, Elems}))}. + +from_json2({struct, Elems}) -> + lists:map(fun parse_json/1, Elems). + +parse_start([{BinName, {struct, JAttrs}}]) -> + Name = binary_to_list(BinName), + {FullName, Attrs} = lists:foldl( + fun({<<"xml">>, {struct, XML}}, {N, Attrs}) -> + XmlAttrs = parse_json_special_attrs("xml", XML), + {N, lists:merge(Attrs, XmlAttrs)}; + ({<<"xmlns">>, {struct, XMLNS}}, {N, Attrs}) -> + XmlNsAttrs = parse_json_special_attrs("xmlns", XMLNS), + {N, lists:merge(Attrs, XmlNsAttrs)}; + ({<<"$$">>, BaseNS}, {N, Attrs})-> + {binary_to_list(BaseNS)++":"++N, Attrs}; + ({Key, Value}, {N, Attrs})-> + {N, [{ib2tol(Key), ib2tol(Value)}|Attrs]} + end, {Name, []}, JAttrs), + {xmlstreamstart, FullName, Attrs}. + +parse_json({Name, CData}) when is_binary(CData)-> + {xmlelement, binary_to_list(Name), [], [{xmlcdata, CData}]}; + +parse_json({Name, CDatas}) when is_list(CDatas)-> + lists:map(fun(CData)-> + {xmlelement, binary_to_list(Name), [], [{xmlcdata, CData}]} + end, CDatas); + +parse_json({BinName, {struct, JAttrs}}) -> + Name = binary_to_list(BinName), + {FullName, Attrs, SubEls} = lists:foldl( + fun({<<"$">>, Cdata}, {N, Attrs, _SubEls}) when is_binary(Cdata)-> + {N, Attrs, [{xmlcdata, Cdata}]}; + ({<<"$">>, {struct, Elems}}, {N, Attrs, _SubEls}) -> + SE = lists:map(fun parse_json/1, Elems), + {N, Attrs, lists:flatten(SE)}; % due to 4.2.3.3 + ({<<"xml">>, {struct, XML}}, {N, Attrs, SubEls}) -> + XmlAttrs = parse_json_special_attrs("xml", XML), + {N, lists:merge(Attrs, XmlAttrs), SubEls}; + ({<<"xmlns">>, {struct, XMLNS}}, {N, Attrs, SubEls}) -> + XmlNsAttrs = parse_json_special_attrs("xmlns", XMLNS), + {N, lists:merge(Attrs, XmlNsAttrs), SubEls}; + ({Key, {struct, []}}, {N, Attrs, SubEls})-> + {N, Attrs, [{xmlelement, ib2tol(Key), [], []}|SubEls]}; + ({Key, Value}, {N, Attrs, SubEls})-> + {N, [{binary_to_list(Key), ib2tol(Value)}|Attrs], SubEls} + end, {Name, [], []}, JAttrs), + {xmlelement, FullName, Attrs, SubEls}. + +parse_json_special_attrs(Prefix, XMLNS)-> + lists:reverse(lists:map( + fun({<<"$">>, Value})-> + {Prefix, ib2tol(Value)}; + ({<<"@",NS/binary>>, Value})-> + {Prefix ++ ":"++binary_to_list(NS), ib2tol(Value)} + end, XMLNS)). + +%%% FROM XML TO JSON +to_json({xmlstreamelement, XMLElement})-> + to_json(XMLElement); +to_json({xmlelement, _Name, [], []})-> + {struct, []}; +to_json({xmlelement, Name, [], [{xmlcdata, Cdata}]})-> + {SName, JsonAttrs2} = parse_namespace(Name, []), + {struct, [{SName, Cdata}|JsonAttrs2]}; +to_json({xmlstreamstart, Name, Attrs})-> + JsonAttrs = parse_attrs(Attrs), + {SName, Members2} = parse_namespace(Name, JsonAttrs), + {struct, [{SName, {struct, Members2}}]}; +to_json({xmlelement, Name, Attrs, SubEls})-> + JsonAttrs = parse_attrs(Attrs), + Members = case parse_subels(SubEls) of + [] -> + JsonAttrs; + [Elem] -> + [{<<"$">>,Elem}|JsonAttrs]; + Elems -> + [{<<"$">>,Elems}|JsonAttrs] + end, + {SName, Members2} = parse_namespace(Name, Members), + {struct, [{SName, {struct, Members2}}]}. + +parse_namespace(Name, AttrsList)-> + {l2b(Name), AttrsList}. + +parse_subels([{xmlcdata, Cdata}])-> + l2b(Cdata); +parse_subels([])-> + []; +parse_subels(SubEls)-> + {struct, lists:reverse(lists:foldl( + fun({xmlelement, SName, [], [{xmlcdata, UCdata}]}, Acc)-> + Cdata = l2b(UCdata), + Name = l2b(SName), + case lists:keyfind(Name, 1, Acc) of + {Name, PrevCdata} when is_binary(PrevCdata) -> + Acc1 = lists:keydelete(Name, 1, Acc), + [{Name,[PrevCdata, Cdata]} | Acc1]; + {Name, CDatas} when is_list(CDatas) -> + Acc1 = lists:keydelete(Name, 1, Acc), + [{Name,lists:append(CDatas, [Cdata])} | Acc1]; + _ -> + [{Name, Cdata}| Acc] + end; + ({xmlelement, SName, _, _} = Elem, Acc) -> + E = case to_json(Elem) of %TODO There could be a better way to iterate + {struct, [{_, ToKeep}]} -> ToKeep; + {struct, []} = Empty -> Empty + end, + [{l2b(SName), E}|Acc]; + ({xmlcdata,<<"\n">>}, Acc) -> + Acc + end,[], SubEls))}. + + +parse_attrs(XmlAttrs)-> + {Normal, XMLNS} = lists:foldl( + fun({"xmlns", NS}, {Attrs, XMLNS}) -> + {Attrs,[{<<"$">>, l2b(NS)}| XMLNS]}; + ({"xmlns:" ++ Short, NS}, {Attrs, XMLNS})-> + AttrName = iolist_to_binary([<<"@">>,l2b(Short)]), + {Attrs,[{AttrName, list_to_binary(NS)}| XMLNS]}; + ({"xml:" ++ Short, Val}, {Attrs, XMLNS})-> + % TODO currently tolerates only one xml:* attr per element + AttrName = iolist_to_binary([<<"@">>,l2b(Short)]), + {[{<<"xml">>,{struct, [{AttrName, l2b(Val)}]}}|Attrs], XMLNS}; + ({K, V}, {Attrs, XMLNS})-> + {[{l2b(K), l2b(V)}|Attrs], XMLNS} + end,{[], []}, XmlAttrs), + + case XMLNS of + [{<<"$">>, NS}]-> + [{<<"xmlns">>, NS}|Normal]; + []-> + Normal; + _ -> + [{<<"xmlns">>,{struct, XMLNS} }| Normal] + end. + +l2b(List) when is_list(List) -> list_to_binary(List); +l2b(Bin) when is_binary(Bin) -> Bin. + +ib2tol(Bin) when is_binary(Bin) -> binary_to_list(Bin ); +ib2tol(Integer) when is_integer(Integer) -> integer_to_list(Integer); +ib2tol(List) when is_list(List) -> List. + +%% +%% Tests +%% erlc -DTEST web/xmpp_json.erl && erl -pa web/ -run xmpp_json test -run init stop -noshell +-include_lib("eunit/include/eunit.hrl"). +-ifdef(TEST). + +% 4.2.3.1 Tag with text value +to_text_value_test()-> + In = {xmlstreamelement, {xmlelement, "tag", [], [{xmlcdata, <<"txt-value">>}]}}, + Out = {struct, [{<<"tag">>, <<"txt-value">>}]}, + ?assertEqual(Out, to_json(In)), + ?assertEqual(In, from_json(Out)). + +% 4.2.3.2 Tag with recursive tags +to_tag_with_recursive_tags_test()-> + In = {xmlstreamelement, {xmlelement, "tag", [], + [{xmlelement,"tag2",[], [{xmlcdata, <<"txt-value">>}]}, + {xmlelement,"tag3",[], [ + {xmlelement,"tag4",[], [{xmlcdata, <<"txt2-value">>}]}]}]}}, + Out = {struct, [{<<"tag">>, + {struct, [{<<"$">>, + {struct, [ + {<<"tag2">>,<<"txt-value">>}, + {<<"tag3">>,{struct, [{<<"$">>,{struct, [{<<"tag4">>,<<"txt2-value">>}]}}]}} + ]} + }]} + }] + }, + %io:format("~n~p", [list_to_binary(mochijson2:encode(to_json(In)))]), + io:format("~n~p", [from_json(Out)]), + io:format("~n~p", [to_json(In)]), + ?assertEqual(Out, to_json(In)), + ?assertEqual(In, from_json(Out)). + +% 4.2.3.3 Multiple text value tags as array +multiple_text_value_tags_as_array_test()-> + In = {xmlstreamelement, {xmlelement, "tag", [], [ + {xmlelement,"tag2",[], [ + {xmlcdata, <<"txt-value">>}]}, + {xmlelement,"tag2",[], [ + {xmlcdata, <<"txt-value2">>}]}]}}, + Out = {struct, [{<<"tag">>, + {struct, [{<<"$">>, + {struct, [{<<"tag2">>, + [<<"txt-value">>, <<"txt-value2">>]}]} + }]} + }] + }, + io:format("~p~n", [to_json(In)]), + io:format("~p~n", [from_json(Out)]), + ?assertEqual(Out, to_json(In)), + ?assertEqual(In, from_json(Out)). + +% 4.2.3.4 Tag with attribute, no value +tag_attr_no_value_test() -> + In = {xmlstreamelement, {xmlelement, "tag", [{"attr", "attr-value"}], []}}, + Out = {struct, [{<<"tag">>, {struct, [ + {<<"attr">>,<<"attr-value">>} + ]}}]}, + io:format("~p", [list_to_binary(mochijson2:encode(to_json(In)))]), + io:format("~p", [from_json(Out)]), + ?assertEqual(Out, to_json(In)), + ?assertEqual(In, from_json(Out)). + +% 4.2.3.5 Tag with multiple attributes as array, no value +% Not wellformed XML. + +% 4.2.3.6 Tags as array with unique attributes, no value + + +% 4.2.3.7 Tag with namespace attribute, no value +tag_with_namespace_no_value_test()-> + In = {xmlstreamelement, {xmlelement, "tag", [{"xmlns:ns", "ns-value"}], []}}, + Out = {struct, [{<<"tag">>, {struct, [ + {<<"xmlns">>,{struct, [{<<"@ns">>, <<"ns-value">>}]}} + ]}}]}, + io:format("~p", [list_to_binary(mochijson2:encode(to_json(In)))]), + ?assertEqual(Out, to_json(In)), + ?assertEqual(In, from_json(Out)). + + +% 4.2.3.8 Tag with many attributes to namespace, no value +two_namespaces_tag_no_value_test()-> + In = {xmlstreamelement,{xmlelement, "tag", [{"xmlns:ns", "ns-value"}, + {"xmlns", "root-value"}], []}}, + Out = {struct, [{<<"tag">>, {struct, [ + {<<"xmlns">>,{struct, [ + {<<"$">>, <<"root-value">>}, + {<<"@ns">>, <<"ns-value">>}]}} + ]}}]}, + io:format("~p", [list_to_binary(mochijson2:encode(to_json(In)))]), + ?assertEqual(Out, to_json(In)), + ?assertEqual(In, from_json(Out)). + +% 4.2.3.9 Tag with namespace attribute, no value +% Removed namespace handling. More complex on both sides. +namespaced_tag_no_value_test()-> + In = {xmlstreamelement,{xmlelement, "ns:tag", [{"attr", "attr-value"}], []}}, + Out = {struct, [{<<"ns:tag">>, {struct, [ + {<<"attr">>,<<"attr-value">>} + ]}}]}, + io:format("~p", [list_to_binary(mochijson2:encode(to_json(In)))]), + ?assertEqual(Out, to_json(In)), + ?assertEqual(In, from_json(Out)). + +% 4.2.3.10 Tag with attribute and text value +tag_with_attribute_and_value_test()-> + In = {xmlstreamelement,{xmlelement, "tag", [{"attr", "attr-value"}], + [{xmlcdata, <<"txt-value">>}]}}, + Out = {struct, [{<<"tag">>, {struct, [ + {<<"$">>, <<"txt-value">>}, + {<<"attr">>,<<"attr-value">>} + ]}}]}, + %io:format("~p", [list_to_binary(mochijson2:encode(to_json(In)))]), + ?assertEqual(Out, to_json(In)), + ?assertEqual(In, from_json(Out)). + +% 4.2.3.11 Namespace tag with attribute and text value +% Removed namespace handling. More complex on both sides +namespaced_tag_with_value_test()-> + In = {xmlstreamelement,{xmlelement, "ns:tag", [{"attr", "attr-value"}], [{xmlcdata, <<"txt-value">>}]}}, + Out = {struct, [{<<"ns:tag">>, {struct, [ + {<<"$">>,<<"txt-value">>}, + {<<"attr">>,<<"attr-value">>} + ]}}]}, + io:format("~p", [list_to_binary(mochijson2:encode(to_json(In)))]), + ?assertEqual(Out, to_json(In)), + ?assertEqual(In, from_json(Out)). + +xml_lang_attr_test()-> + In = {xmlstreamelement,{xmlelement, "tag", [{"xml:lang", "en"}], []}}, + Out = {struct, [{<<"tag">>, {struct, [ + {<<"xml">>,{struct,[{<<"@lang">>,<<"en">>}]}} + ]}}]}, + io:format("~p", [list_to_binary(mochijson2:encode(to_json(In)))]), + ?assertEqual(Out, to_json(In)), + ?assertEqual(In, from_json(Out)). + +xmlns_tag_with_value_test()-> + Out = {struct,[{<<"response">>, + {struct,[{<<"$">>,<<"dXNlcm5hbWU9I">>}, + {<<"xmlns">>, <<"urn:ietf:params:xml:ns:xmpp-sasl">>}]}} + ]}, + Out2 = {struct,[{<<"response">>, + {struct,[{<<"xmlns">>, <<"urn:ietf:params:xml:ns:xmpp-sasl">>}, + {<<"$">>,<<"dXNlcm5hbWU9I">>} + ]}} + ]}, + In = {xmlstreamelement,{xmlelement,"response", + [{"xmlns","urn:ietf:params:xml:ns:xmpp-sasl"}], + [{xmlcdata, <<"dXNlcm5hbWU9I">>}]}}, + io:format("~p", [list_to_binary(mochijson2:encode(to_json(In)))]), + ?assertEqual(Out, to_json(In)), + ?assertEqual(In, from_json(Out)), + ?assertEqual(In, from_json(Out2)). + +no_attr_no_value_test()-> + In = {xmlstreamelement, {xmlelement,"failure", + [{"xmlns","urn:ietf:params:xml:ns:xmpp-sasl"}], + [{xmlelement,"not-authorized",[],[]}]}}, + Out = {struct, [{<<"failure">>,{struct, [ + {<<"$">>, {struct, [{<<"not-authorized">>, {struct, []}}]}}, + {<<"xmlns">>, <<"urn:ietf:params:xml:ns:xmpp-sasl">>} + ]}}]}, + io:format("~p", [list_to_binary(mochijson2:encode(to_json(In)))]), + io:format("~p~n", [to_json(In)]), + io:format("~p~n", [from_json(Out)]), + ?assertEqual(Out, to_json(In)), + ?assertEqual(In, from_json(Out)). + +xmlstream_test()-> + In = {xmlstreamstart, "stream", [{"xml:lang", "en"}]}, + Out = {struct, [{<<"stream">>, {struct, [ + {<<"xml">>,{struct,[{<<"@lang">>,<<"en">>}]}} + ]}}]}, + io:format("~p", [list_to_binary(mochijson2:encode(to_json(In)))]), + ?assertEqual(Out, to_json(In)), + ?assertEqual(In, from_json(Out)). +-endif. \ No newline at end of file