From 2095d3733f0384c332776f078ac820ab6e5a6f84 Mon Sep 17 00:00:00 2001 From: dima Date: Sat, 15 Feb 2014 14:52:04 +0200 Subject: [PATCH] Start fixing for ejabberd v13. - upgrade and handshake implemented according to RFC-6455 - internals updated to use binary data instead of strings --- .gitignore | 2 +- Emakefile | 6 +- README.markdown | 5 +- src/ejabberd_websocket.erl | 468 +++++++++++++++++--------------- src/ejabberd_xmpp_websocket.erl | 121 +++++++-- src/mod_websocket.erl | 131 ++++++++- 6 files changed, 480 insertions(+), 253 deletions(-) diff --git a/.gitignore b/.gitignore index ac8f968..555cc1f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,7 @@ # Compiled source # ################### *.com -*.class +*.beam *.dll *.exe *.o diff --git a/Emakefile b/Emakefile index 462cf73..6fbe5e7 100644 --- a/Emakefile +++ b/Emakefile @@ -1,3 +1,3 @@ -{'src/mod_websocket', [{outdir, "ebin"},{i,"/usr/lib/ejabberd/include"},{i,"/usr/lib/ejabberd/include/web","src"}]}. -{'src/ejabberd_xmpp_websocket', [{outdir, "ebin"},{i,"/usr/lib/ejabberd/include"},{i,"/usr/lib/ejabberd/include/web","src"}]}. -{'src/ejabberd_websocket', [{outdir, "ebin"},{i,"/usr/lib/ejabberd/include"},{i,"/usr/lib/ejabberd/include/web","src"}]}. \ No newline at end of file +{'src/mod_websocket', [{outdir, "ebin"},{i,"/usr/lib/ejabberd/include"}]}. +{'src/ejabberd_xmpp_websocket', [{outdir, "ebin"},{i,"/usr/lib/ejabberd/include"}]}. +{'src/ejabberd_websocket', [{outdir, "ebin"},{i,"/usr/lib/ejabberd/include"}]}. diff --git a/README.markdown b/README.markdown index 065fee6..f250600 100644 --- a/README.markdown +++ b/README.markdown @@ -1,6 +1,3 @@ -> This repo is not maintained and most of the code in here is probably deprecated. - - # Websocket Module for Ejabberd This is a module that adds websocket support for the [ejabberd](http://www.ejabberd.im/) XMPP server. It's a more elegant, modern and faster replacement to Bosh. @@ -17,6 +14,8 @@ It is an implementation of the [XMPP Over Websocket Draft](http://tools.ietf.org ### Install cp ebin/*.beam /path/to/ejabberd/lib/ebin/ +As for ejabberd v.13.12 I have had to install p1_logger (processone/p1_logger in github) as well. It is also possible to install /lager/. I don't know what is that. + ### Configure In the listeners section add the following line: diff --git a/src/ejabberd_websocket.erl b/src/ejabberd_websocket.erl index 8d6e199..126d01c 100644 --- a/src/ejabberd_websocket.erl +++ b/src/ejabberd_websocket.erl @@ -1,9 +1,11 @@ -%%%---------------------------------------------------------------------- +%%---------------------------------------------------------------------- %%% File : ejabberd_websocket.erl %%% Author : Nathan Zorn %%% Purpose : Listener for XMPP over websockets %%%---------------------------------------------------------------------- +%% TODO: downcase all http keys (issue with Sec-Web[Ss]ocket-Key under FF + -module(ejabberd_websocket). -author('nathan.zorn@gmail.com'). @@ -17,6 +19,8 @@ -export([init/2]). %% Includes -include("ejabberd.hrl"). +-define(LAGER, true). +-include("logger.hrl"). -include("jlib.hrl"). -include("ejabberd_websocket.hrl"). %% record used to keep track of listener state @@ -39,6 +43,10 @@ websocket_pid, trail = "" }). + +%% RFC-2616 HTML 1.1 specification +-define(RFC2616_SEPARATORS, "()<>@,;:\\\"/[]?={} \t"). + -define(MAXKEY_LENGTH, 4294967295). %% Supervisor Start start(SockData, Opts) -> @@ -50,29 +58,29 @@ start_link(SockData, Opts) -> init({SockMod, Socket}, Opts) -> TLSEnabled = lists:member(tls, Opts), TLSOpts1 = lists:filter(fun({certfile, _}) -> true; - (_) -> false - end, Opts), + (_) -> false + end, Opts), TLSOpts = [verify_none | TLSOpts1], {SockMod1, Socket1} = - if - TLSEnabled -> - inet:setopts(Socket, [{recbuf, 8192}]), - {ok, TLSSocket} = tls:tcp_to_tls(Socket, TLSOpts), - {tls, TLSSocket}; - true -> - {SockMod, Socket} - end, + if + TLSEnabled -> + inet:setopts(Socket, [{recbuf, 8192}]), + {ok, TLSSocket} = tls:tcp_to_tls(Socket, TLSOpts), + {tls, TLSSocket}; + true -> + {SockMod, Socket} + end, case SockMod1 of - gen_tcp -> - inet:setopts(Socket1, [{packet, http}, {recbuf, 8192}]); - _ -> - ok + gen_tcp -> + inet:setopts(Socket1, [{packet, http}, {recbuf, 8192}]); + _ -> + ok end, RequestHandlers = - case lists:keysearch(request_handlers, 1, Opts) of - {value, {request_handlers, H}} -> H; - false -> [] - end, + case lists:keysearch(request_handlers, 1, Opts) of + {value, {request_handlers, H}} -> H; + false -> [] + end, ?INFO_MSG("started: ~p", [{SockMod1, Socket1}]), State = #state{sockmod = SockMod1, socket = Socket1, @@ -85,187 +93,206 @@ socket_type() -> raw. receive_headers(State) -> - SockMod = State#state.sockmod, - Socket = State#state.socket, - Data = SockMod:recv(Socket, 0, 300000), - ?DEBUG("Data in ~p: headers : ~p",[State, Data]), - case State#state.sockmod of - gen_tcp -> - NewState = process_header(State, Data), - case NewState#state.end_of_request of - true -> - ok; - _ -> - receive_headers(NewState) - end; - _ -> - case Data of - {ok, Binary} -> - ?DEBUG("not gen_tcp, ssl? ~p~n", [Binary]), - {Request, Trail} = parse_request( - State, - State#state.trail ++ - binary_to_list(Binary)), - State1 = State#state{trail = Trail}, - NewState = lists:foldl( - fun(D, S) -> - case S#state.end_of_request of - true -> - S; - _ -> - process_header(S, D) - end - end, State1, Request), - case NewState#state.end_of_request of - true -> - ok; - _ -> - receive_headers(NewState) - end; - Req -> - ?DEBUG("not gen_tcp or ok: ~p~n", [Req]), - ok - end - end. + SockMod = State#state.sockmod, + Socket = State#state.socket, + Data = SockMod:recv(Socket, 0, 300000), + ?WARNING_MSG("Data in ~p: headers : ~p",[State, Data]), + case State#state.sockmod of + gen_tcp -> + NewState = process_header(State, Data), + case NewState#state.end_of_request of + true -> ok; + _ -> receive_headers(NewState) + end; + _ -> + case Data of + {ok, Binary} -> + ?DEBUG("not gen_tcp, ssl? ~p~n", [Binary]), + {Request, Trail} = parse_request(State, + State#state.trail ++ + binary_to_list(Binary)), + State1 = State#state{trail = Trail}, + NewState = lists:foldl(fun(D, S) -> + case S#state.end_of_request of + true -> S; + _ -> process_header(S, D) + end + end, State1, Request), + case NewState#state.end_of_request of + true -> ok; + _ -> receive_headers(NewState) + end; + Req -> + ?DEBUG("not gen_tcp or ok: ~p~n", [Req]), + ok + end + end. process_header(State, Data) -> - case Data of - {ok, {http_request, Method, Uri, Version}} -> - KeepAlive = case Version of - {1, 1} -> - true; - _ -> - false - end, - Path = case Uri of - {absoluteURI, _Scheme, _Host, _Port, P} -> {abs_path, P}; - _ -> Uri - end, - State#state{request_method = Method, - request_version = Version, - request_path = Path, - request_keepalive = KeepAlive}; - {ok, {http_header, _, 'Connection'=Name, _, Conn}} -> - KeepAlive1 = case jlib:tolower(Conn) of - "keep-alive" -> - true; - "close" -> - false; - _ -> - State#state.request_keepalive - end, - State#state{request_keepalive = KeepAlive1, - request_headers=add_header(Name, Conn, State)}; - {ok, {http_header, _, 'Content-Length'=Name, _, SLen}} -> - case catch list_to_integer(SLen) of - Len when is_integer(Len) -> - State#state{request_content_length = Len, - request_headers=add_header(Name, SLen, State)}; - _ -> - State - end; - {ok, {http_header, _, 'Host'=Name, _, Host}} -> - State#state{request_host = Host, - request_headers=add_header(Name, Host, State)}; - {ok, {http_header, _, Name, _, Value}} -> - State#state{request_headers=add_header(Name, Value, State)}; - {ok, http_eoh} when State#state.request_host == undefined -> - ?WARNING_MSG("An HTTP request without 'Host' HTTP header was received.", []), - throw(http_request_no_host_header); - {ok, http_eoh} -> - ?DEBUG("(~w) http query: ~w ~s~n", - [State#state.socket, - State#state.request_method, - element(2, State#state.request_path)]), - Out = process_request(State), - %% Test for web socket - case (Out =/= false) and is_websocket_upgrade(State#state.request_headers) of - true -> - ?DEBUG("Websocket!",[]), - SockMod = State#state.sockmod, - Socket = State#state.socket, - case SockMod of - gen_tcp -> - inet:setopts(Socket, [{packet, raw}]); - _ -> - ok - end, - %% handle hand shake - case handshake(State) of - true -> - case sub_protocol(State#state.request_headers) of - "xmpp" -> - %% send the state back - #state{sockmod = SockMod, - socket = Socket, - request_handlers = State#state.request_handlers}; - _ -> - ?DEBUG("Bad sub protocol",[]), - #state{end_of_request = true, - request_handlers = State#state.request_handlers} - end; - _ -> - ?DEBUG("Bad Handshake",[]), - #state{end_of_request = true, - request_handlers = State#state.request_handlers} - end; - _ -> - ?DEBUG("Regular HTTP",[]), - #state{end_of_request = true, - request_handlers = State#state.request_handlers} - end; - {error, closed} -> - ?ERROR_MSG("Socket closed", [State]), - process_data(State, socket_closed), - #state{end_of_request = true, - request_handlers = State#state.request_handlers}; - {error, timeout} -> - ?DEBUG("Socket recv timed out. Return the same State.",[]), - State; - {ok, HData} -> - PData = case State#state.partial of - <<>> -> - HData; - <> -> - <> - end, - {_Out, Partial, Pid} = case process_data(State, PData) of - {O, P} -> - {O, P, false}; - {Output, Part, ProcId} -> - {Output, Part, ProcId}; - Error -> - {Error, undefined, undefined} - end, - ?DEBUG("C2SPid:~p~n",[Pid]), - case Pid of - false -> - #state{sockmod = State#state.sockmod, - socket = State#state.socket, - partial = Partial, - request_handlers = State#state.request_handlers}; + case Data of + {ok, {http_request, Method, Uri, Version}} -> + KeepAlive = case Version of + {1, 1} -> + true; + _ -> + false + end, + Path = case Uri of + {absoluteURI, _Scheme, _Host, _Port, P} -> {abs_path, P}; + _ -> Uri + end, + State#state{request_method = Method, + request_version = Version, + request_path = Path, + request_keepalive = KeepAlive}; + {ok, {http_header, _, 'Connection'=Name, _, Con}} -> + ConLine = strip_spaces(Con), + ConDifList = string:tokens(ConLine, ?RFC2616_SEPARATORS), %% these can be in different case + ConList = lists:map(fun string:to_lower/1, ConDifList), + KeepAlive1 = case lists:any(fun is_keep_alive/1, ConList) of + true -> true; + _ -> + case lists:any(fun is_close/1, ConList) of + true -> true; + _ -> State#state.request_keepalive + end + end, + State#state{request_keepalive = KeepAlive1, + request_headers=add_header(Name, ConList, State)}; + {ok, {http_header, _, 'Content-Length'=Name, _, SLen}} -> + case catch list_to_integer(SLen) of + Len when is_integer(Len) -> + State#state{request_content_length = Len, + request_headers=add_header(Name, SLen, State)}; + _ -> + State + end; + {ok, {http_header, _, 'Host'=Name, _, Host}} -> + State#state{request_host = Host, + request_headers=add_header(Name, Host, State)}; + {ok, {http_header, _, Name, _, Value}} -> + State#state{request_headers=add_header(Name, Value, State)}; + {ok, http_eoh} when State#state.request_host == undefined -> + ?WARNING_MSG("An HTTP request without 'Host' HTTP header was received.", []), + throw(http_request_no_host_header); + {ok, http_eoh} -> + ?DEBUG("(~w) http query: ~w ~s~n", + [State#state.socket, + State#state.request_method, + element(2, State#state.request_path)]), + Out = process_request(State), + IsUpgrade = is_websocket_upgrade(State#state.request_headers), + %% Test for web socket + case (Out =/= false) and IsUpgrade of + true -> + ?DEBUG("Websocket!",[]), + SockMod = State#state.sockmod, + Socket = State#state.socket, + case SockMod of + gen_tcp -> + inet:setopts(Socket, [{packet, raw}]); + _ -> + ok + end, + %% handle hand shake + case handshake2(State) of + true -> + case sub_protocol(State#state.request_headers) of + "xmpp" -> + %% send the state back + #state{sockmod = SockMod, + socket = Socket, + request_handlers = State#state.request_handlers}; _ -> - #state{sockmod = State#state.sockmod, - socket = State#state.socket, - partial = Partial, - websocket_pid = Pid, - request_handlers = State#state.request_handlers} - end; + ?DEBUG("Bad sub protocol",[]), + #state{end_of_request = true, + request_handlers = State#state.request_handlers} + end; + _ -> + ?DEBUG("Bad Handshake",[]), + #state{end_of_request = true, + request_handlers = State#state.request_handlers} + end; _ -> - ?DEBUG("Not expected: ~p~n",[Data]), - #state{end_of_request = true, - request_handlers = State#state.request_handlers} - end. + ?DEBUG("Regular HTTP",[]), + #state{end_of_request = true, + request_handlers = State#state.request_handlers} + end; + {error, closed} -> + ?ERROR_MSG("Socket closed", [State]), + #state{end_of_request = true, + request_handlers = State#state.request_handlers}; + {error, timeout} -> + ?DEBUG("Socket recv timed out. Return the same State.",[]), + State; + {ok, HData} -> + PData = case State#state.partial of + <<>> -> + HData; + <> -> + <> + end, + {_Out, Partial, Pid} = case process_data(State, PData) of + {O, P} -> + {O, P, false}; + {Output, Part, ProcId} -> + {Output, Part, ProcId}; + Error -> + {Error, undefined, undefined} + end, + ?DEBUG("C2SPid:~p~n",[Pid]), + case Pid of + false -> + #state{sockmod = State#state.sockmod, + socket = State#state.socket, + partial = Partial, + request_handlers = State#state.request_handlers}; + _ -> + #state{sockmod = State#state.sockmod, + socket = State#state.socket, + partial = Partial, + websocket_pid = Pid, + request_handlers = State#state.request_handlers} + end; + _ -> + ?DEBUG("Not expected: ~p~n",[Data]), + #state{end_of_request = true, + request_handlers = State#state.request_handlers} + end. add_header(Name, Value, State) -> [{Name, Value} | State#state.request_headers]. is_websocket_upgrade(RequestHeaders) -> - Connection = {'Connection', "Upgrade"} == lists:keyfind('Connection', 1, - RequestHeaders), - Upgrade = {'Upgrade', "WebSocket"} == lists:keyfind('Upgrade', 1, - RequestHeaders), - Connection and Upgrade. + {'Connection', ConnectionList} = lists:keyfind('Connection', 1, RequestHeaders), + {'Upgrade', Upgrade} = lists:keyfind('Upgrade', 1, RequestHeaders), + lists:any(fun is_upgrade/1, ConnectionList) and (string:to_lower(Upgrade) == "websocket"). + +handshake2(State) -> + SubProto = sub_protocol(State#state.request_headers), + SubProtoHeader = case SubProto of + undefined -> ""; + P -> ["Sec-WebSocket-Protocol: ", P, "\r\n"] + end, + {"Sec-Websocket-Key", SecKey} = lists:keyfind("Sec-Websocket-Key", 1, State#state.request_headers), + Guid = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11", %% TODO: Generate real unique id + WebSocketSec = base64:encode(crypto:hash(sha, SecKey ++ Guid)), + Res = ["HTTP/1.1 101 Switching Protocols\r\n", + "Upgrade: websocket\r\n", + "Connection: Upgrade\r\n", + SubProtoHeader, + ["Sec-WebSocket-Accept: ", WebSocketSec, "\r\n"], + "\r\n" + ], + + ?DEBUG("Respond with:~n~p~n", [Res]), + + case send_text(State, Res) of + ok -> true; + E -> + ?DEBUG("ERROR Sending text:~p~n",[E]), + false + end. handshake(State) -> SockMod = State#state.sockmod, @@ -294,7 +321,7 @@ handshake(State) -> State#state.request_path, SubProto, Sig), - ?DEBUG("Sending handshake response:~p~n",[Res]), + ?WARNING_MSG("Sending handshake response:~p~n",[Res]), %% send response case send_text(State, Res) of ok -> true; @@ -338,10 +365,11 @@ process_data(State, Data) -> {error, _Error} -> undefined end, + ?DEBUG("Incoming data: ~p~n", [Data]), Request = #wsrequest{method = State#state.request_method, path = Path, headers = State#state.request_headers, - data = Data, + data = binary_to_list(Data), fsmref = State#state.websocket_pid, wsocket = Socket, wsockmod = SockMod, @@ -380,7 +408,7 @@ process([], _) -> false; process(RequestHandlers, Request) -> [{HandlerPathPrefix, HandlerModule} | HandlersLeft] = RequestHandlers, - case (lists:prefix(HandlerPathPrefix, Request#wsrequest.path) or + case (lists:prefix([binary_to_list(HandlerPathPrefix)], Request#wsrequest.path) or (HandlerPathPrefix==Request#wsrequest.path)) of true -> ?DEBUG("~p matches ~p", @@ -389,8 +417,8 @@ process(RequestHandlers, Request) -> %% the handler was registered to handle "/test/" and the %% requested path is "/test/foo/bar", the local path is %% ["foo", "bar"] - LocalPath = lists:nthtail(length(HandlerPathPrefix), - Request#wsrequest.path), + LocalPath = "",%%lists:nthtail(length(binary_to_list(HandlerPathPrefix)), + %% Request#wsrequest.path), HandlerModule:process(LocalPath, Request); false -> process(HandlersLeft, Request) @@ -408,7 +436,7 @@ send_text(State, Text) -> end. %% sign data websocket_sign(Part1, Part2, Key3) -> - crypto:md5( <> ). %% verify websocket keys websocket_verify_keys(Key1, Key2) -> @@ -574,22 +602,22 @@ old_hex_to_integer(Hex) -> % The following code is mostly taken from yaws_ssl.erl parse_request(State, Data) -> - case Data of - [] -> - {[], []}; - _ -> - ?DEBUG("GOT ssl data ~p~n", [Data]), - {R, Trail} = case State#state.request_method of - undefined -> - {R1, Trail1} = get_req(Data), - ?DEBUG("Parsed request ~p~n", [R1]), - {[R1], Trail1}; - _ -> - {[], Data} - end, - {H, Trail2} = get_headers(Trail), - {R ++ H, Trail2} - end. + case Data of + [] -> + {[], []}; + _ -> + ?DEBUG("GOT ssl data ~p~n", [Data]), + {R, Trail} = case State#state.request_method of + undefined -> + {R1, Trail1} = get_req(Data), + ?DEBUG("Parsed request ~p~n", [R1]), + {[R1], Trail1}; + _ -> + {[], Data} + end, + {H, Trail2} = get_headers(Trail), + {R ++ H, Trail2} + end. get_req("\r\n\r\n" ++ _) -> bad_request; @@ -694,7 +722,11 @@ get_headers(H, Tail) -> parse_line("Connection:" ++ Con) -> - [{ok, {http_header, undefined, 'Connection', undefined, strip_spaces(Con)}}]; + ?DEBUG("Connection is found in headers with line: ~p~n", [Con]), + ConLine = strip_spaces(Con), + ConDifList = string:tokens(ConLine, ?RFC2616_SEPARATORS), %% these can be in different case + ConList = lists:map(fun string:to_lower/1, ConDifList), + [{ok, {http_header, undefined, 'Connection', undefined, ConList}}]; parse_line("Host:" ++ Con) -> [{ok, {http_header, undefined, 'Host', undefined, strip_spaces(Con)}}]; parse_line("Accept:" ++ Con) -> @@ -743,6 +775,14 @@ parse_line(S) -> [] end. +is_upgrade("upgrade") -> true; +is_upgrade(_) -> false. + +is_keep_alive("keep-alive") -> true; +is_keep_alive(_) -> false. + +is_close("close") -> true; +is_close(_) -> false. is_space($\s) -> true; diff --git a/src/ejabberd_xmpp_websocket.erl b/src/ejabberd_xmpp_websocket.erl index 96e8f19..3a919ec 100644 --- a/src/ejabberd_xmpp_websocket.erl +++ b/src/ejabberd_xmpp_websocket.erl @@ -30,6 +30,8 @@ process_request/5]). -include("ejabberd.hrl"). +-define(LAGER, true). +-include("logger.hrl"). %% Module constants -define(NULL_PEER, {{0, 0, 0, 0}, 0}). @@ -37,9 +39,8 @@ % idle sessions -define(MAX_PAUSE, 120). % may num of sec a client is allowed to pause % the session --define(NS_CLIENT, "jabber:client"). --define(NS_STREAM, "http://etherx.jabber.org/streams"). --define(TEST, 1). +-define(NS_CLIENT, <<"jabber:client">>). +-define(NS_STREAM, <<"http://etherx.jabber.org/streams">>). %% Erlang Records for state -record(wsr, {socket, sockmod, key, out}). @@ -63,10 +64,10 @@ }). start(Host, Sid, Key, IP) -> - Proc = gen_mod:get_module_proc(Host, ejabberd_mod_websocket), + Proc = gen_mod:get_module_proc(list_to_binary(Host), ejabberd_mod_websocket), case catch supervisor:start_child(Proc, [Sid, Key, IP]) of - {ok, Pid} -> {ok, Pid}; - Reason -> + {ok, Pid} -> {ok, Pid}; + Reason -> ?ERROR_MSG("~p~n",[Reason]), {error, "Cannot start XMPP, Websocket session"} end. @@ -241,7 +242,6 @@ handle_sync_event({send_xml, Packet}, _From, StateName, {reply, Reply, StateName, StateData#state{output = Output}}; handle_sync_event({send_xml, Packet}, _From, StateName, StateData) -> Output = [Packet | StateData#state.output], - ?DEBUG("Data from C2S(timer):~p:~p~n",[Output,StateData]), cancel_timer(StateData#state.timer), Timer = set_inactivity_timer(StateData#state.pause, StateData#state.max_inactivity), @@ -365,16 +365,16 @@ terminate(_Reason, _StateName, StateData) -> %% Internal functions %%% stream_start(ParsedPayload) -> - ?DEBUG("~p~n",[ParsedPayload]), + ?DEBUG("Stream started: ~p~n",[ParsedPayload]), case ParsedPayload of {xmlelement, "stream:stream", Attrs, _} -> {"to",Host} = lists:keyfind("to", 1, Attrs), - Sid = sha:sha(term_to_binary({now(), make_ref()})), + Sid = crypto:hash(sha, term_to_binary({now(), make_ref()})), Key = "", {Host, Sid, Key}; {xmlstreamstart, _Name, Attrs} -> {"to",Host} = lists:keyfind("to", 1, Attrs), - Sid = sha:sha(term_to_binary({now(), make_ref()})), + Sid = crypto:hash(sha, term_to_binary({now(), make_ref()})), Key = "", {Host, Sid, Key}; _ -> @@ -408,18 +408,82 @@ send_receiver_reply(Receiver, Reply) -> gen_fsm:reply(Receiver, Reply). %% send data to socket +send_text(StateData, {xmlel, Name, Attributes, Children}) -> + send_text(StateData, list_to_binary(build_xml_element({xmlel, Name, Attributes, Children}))); send_text(StateData, Text) -> ?DEBUG("Send XML on stream = ~p", [Text]), (StateData#state.websocket_sockmod):send(StateData#state.websocket_s, - [0, Text, 255]). + websocket_encode(binary_to_list(Text), no_mask)). + +build_xml_element({xmlel, Name, [], []}) -> + "<" ++ binary_to_list(Name) ++ " />"; +build_xml_element({xmlel, Name, [], Children}) -> + "<" ++ binary_to_list(Name) ++ ">" ++ + build_xml_element({xmllist, Children}) ++ + ""; +build_xml_element({xmlel, Name, Attributes, []}) -> + "<" ++ binary_to_list(Name) ++ build_attributes(Attributes) ++ "/>"; +build_xml_element({xmlel, Name, Attributes, Children}) -> + "<" ++ binary_to_list(Name) ++ build_attributes(Attributes) ++ ">" ++ + build_xml_element({xmllist, Children}) ++ + ""; +build_xml_element({xmllist, []}) -> + ""; +build_xml_element({xmllist, [Element | OtherElements]}) -> + build_xml_element(Element) ++ build_xml_element({xmllist, OtherElements}); +build_xml_element({xmlcdata, CData}) -> + binary_to_list(CData). + %% "". +build_attributes([]) -> + ""; +build_attributes([{AttrName, AttrValue} | Other]) -> + " " ++ binary_to_list(AttrName) ++ "=\"" ++ binary_to_list(AttrValue) ++ "\"" ++ build_attributes(Other). + +binary_len(<>, PayloadLen) -> + if PayloadLen =< 125 -> + << Start:1, PayloadLen:7 >>; + PayloadLen =< 65535 -> + << Start:1, 126:7, PayloadLen:16 >>; + true -> + << Start:1, 127:7, PayloadLen:64 >> + end. + +websocket_encode(Text, no_mask) -> + PayloadLen = string:len(Text), + Textb = list_to_binary(Text), + PayloadLenb = binary_len(<<0:1>>, PayloadLen), + << 1:1, 0:3, 1:4, PayloadLenb/binary, Textb/binary>>; +websocket_encode(Text, mask) -> + websocket_encode(Text, mask, random:uniform((1 bsl 32) - 1)). + +websocket_encode(Text, mask, Mask) -> + PayloadLen = string:len(Text), + Textb = apply_mask(list_to_binary(Text), Mask), + PayloadLenb = binary_len(<<1:1>>, PayloadLen), + << 1:1, 0:3, 1:4, PayloadLenb/binary, Mask:32, Textb/binary>>. + +apply_mask(Data, Mask) -> + apply_mask(Data, Mask, <<>>). +apply_mask(<<>>, _Mask, <<>>) -> + <<>>; +apply_mask(<<>>, _Mask, <>) -> + <>; +apply_mask(<>, Mask, <>) -> + apply_mask(<>, Mask, <>); +apply_mask(<>, Mask, <>) -> + <>; +apply_mask(<>, Mask, <>) -> + <>; +apply_mask(<>, Mask, <>) -> + <>. send_element(StateData, {xmlstreamstart, Name, Attrs}) -> XmlString = streamstart_tobinary({xmlstreamstart, Name, Attrs}), send_text(StateData, XmlString); -send_element(StateData, {xmlstreamend, "stream:stream"}) -> +send_element(StateData, {xmlstreamend, <<"stream:stream">>}) -> send_text(StateData, <<"">>); send_element(StateData, El) -> - send_text(StateData, xml:element_to_binary(El)). + send_text(StateData, El). send_stream_start(C2SPid, Attrs) -> StreamTo = case lists:keyfind("to", 1, Attrs) of @@ -437,17 +501,17 @@ send_stream_start(C2SPid, Attrs) -> gen_fsm:send_event( C2SPid, {xmlstreamstart, "stream:stream", - [{"to", To}, - {"xmlns", ?NS_CLIENT}, - {"xmlns:stream", ?NS_STREAM}]}); + [{<<"to">>, list_to_binary(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}]}) + [{<<"to">>, list_to_binary(To)}, + {<<"xmlns">>, ?NS_CLIENT}, + {<<"version">>, list_to_binary(Version)}, + {<<"xmlns:stream">>, ?NS_STREAM}]}) end. send_data(FsmRef, Req) -> ?DEBUG("session pid:~p~n", [FsmRef]), @@ -526,6 +590,23 @@ attrs_tostring(Str,[X|Rest]) -> %% Tests -ifdef(TEST). -include_lib("eunit/include/eunit.hrl"). +websocket_encode_test() -> + <<129, 5, 72, 101, 108, 108, 111>> = websocket_encode("Hello", no_mask), + <<129, 133, 55, 250, 33, 61, 127, 159, 77, 81, 88>> = websocket_encode("Hello", mask, 939139389). + +build_xml_test() -> + StringXML = "", + ProducedXML = build_xml_element({xmlel, + <<"stream:error">>, + [], + [{xmlel, + <<"invalid-namespace">>, + [{<<"xmlns">>,<<"urn:ietf:params:xml:ns:xmpp-streams">>}], + [{xmlcdata,<<>>}] + }] + }), + StringXML = ProducedXML. + stream_start_end_test() -> false = stream_start_end("stream no xml"), {xmlstreamstart, _X, _Y} = stream_start_end(""), diff --git a/src/mod_websocket.erl b/src/mod_websocket.erl index 167ca17..225ec73 100644 --- a/src/mod_websocket.erl +++ b/src/mod_websocket.erl @@ -20,20 +20,25 @@ ]). -include("ejabberd.hrl"). +-define(LAGER, true). +-include("logger.hrl"). -include("jlib.hrl"). -include("ejabberd_websocket.hrl"). --record(wsdatastate, {legacy=true, - ft=undefined, - flen, - packet= <<>>, - buffer= <<>>, - partial= <<>> +-record(wsdatastate, {legacy=true, %% do we need this? + ft=undefined, %% final segment + opcode, %% opcode 4 bit size + flen=undefined, %% payload length first value (if 126|127 - should use exlen) + exlen=undefined, %% extra payload length, 16 or 64 bits in size + mask=undefined, %% mask used in payload + packet= <<>>, %% exracted packet so fae + buffer= <<>>, %% received data so far + partial= <<>> %% extra bytes }). %%%---------------------------------------------------------------------- %%% API %%%---------------------------------------------------------------------- -process(Path, Req) -> - ?DEBUG("Request data:~p:", [Path, Req]), +process(_Path, Req) -> + ?DEBUG("Request data:~p~n", [Req#wsrequest.data]), %% Validate Origin case validate_origin(Req#wsrequest.headers) of true -> @@ -46,24 +51,37 @@ process(Path, Req) -> Y -> Y end, - ?DEBUG("Origin is valid.",[]), + ?DEBUG("Origin is valid ~p.~n",[Data]), DState = #wsdatastate {legacy=true, buffer=Data, ft=undefined, partial= <<>> }, - case process_data(DState) of + case process_data2(DState) of {<<>>, Part} when is_binary(Part) -> + ?DEBUG("Empty packet~n", []), {<<>>, Part}; {Out, <<>>} when is_binary(Out) -> + ?DEBUG("Request data:~p~n", [Out]), IP = Req#wsrequest.ip, %% websocket frame is finished process request ejabberd_xmpp_websocket:process_request( Req#wsrequest.wsockmod, Req#wsrequest.wsocket, Req#wsrequest.fsmref, - Out, + binary_to_list(Out), IP); - Error -> Error %% pass the errors through + {_Reason, closed} -> + ?DEBUG("Imitating stream close event.", []), + IP = Req#wsrequest.ip, + ejabberd_xmpp_websocket:process_request( + Req#wsrequest.wsockmod, + Req#wsrequest.wsocket, + Req#wsrequest.fsmref, + "", + IP); + Error -> + ?DEBUG("Error occured ~p~n", [Error]), + Error %% pass the errors through end; _ -> ?DEBUG("Invalid Origin in Request: ~p~n",[Req]), @@ -97,6 +115,95 @@ validate_origin([]) -> validate_origin(Headers) -> is_tuple(lists:keyfind("Origin", 1, Headers)). +format_output(DState = #wsdatastate{opcode=1}) -> + {DState#wsdatastate.packet, DState#wsdatastate.partial}; +format_output(DState = #wsdatastate{opcode=8}) -> + {DState#wsdatastate.packet, closed}; +format_output(DState) -> + {DState#wsdatastate.packet, DState#wsdatastate.partial}. + +%% no buffer? +process_data2(DState = #wsdatastate{buffer=undefined}) -> + ?INFO_MSG("Buffer undefined cought", []), + format_output(DState); +%% buffer is empty +process_data2(DState = #wsdatastate{buffer= <<>>}) -> + ?INFO_MSG("Buffer empty cought", []), + format_output(DState); +%% first part of packet - final/or/not packet, reserved bits, opcode (text/bin data), is maske used (should be) +%% and short payload size(up to 125 bytes). +process_data2(DState = #wsdatastate{buffer= <>, + ft=undefined}) -> + warn_not_masked(Mask), + process_data2(DState#wsdatastate{buffer = <>, + ft = (Fin == 1), + opcode = OpCode, + flen = PayLoadLen}); +%% if payload is 16 bytes size +process_data2(DState = #wsdatastate{buffer= <>, + flen=126, + exlen=undefined}) -> + process_data2(DState#wsdatastate{buffer = <>, + flen = ExLen, + exlen = ExLen}); +%% if payload is 64 bytes size +process_data2(DState = #wsdatastate{buffer= <>, + flen=127, + exlen=undefined}) -> + warn_first_bit_should_not_be_one(FirstBit), + process_data2(DState#wsdatastate{buffer = <>, + flen = ExLen, + exlen = ExLen}); +%% exract mask +process_data2(DState = #wsdatastate{buffer = <>, + mask = undefined}) -> + process_data2(DState#wsdatastate{buffer = <>, + mask = Mask}); +%% +process_data2(DState = #wsdatastate{buffer = <>, + packet = <>, + mask = Mask, + flen = Len}) when Len > 3 -> + process_data2(DState#wsdatastate{buffer = <>, + flen = Len - 4, + packet = <>}); +process_data2(DState = #wsdatastate{buffer = <>, + packet = Packet, + mask = Mask, + flen = 3}) -> + process_data2(DState#wsdatastate{buffer = <>, + flen = 0, + packet = <>}); +process_data2(DState = #wsdatastate{buffer = <>, + packet = Packet, + mask = Mask, + flen = 2}) -> + process_data2(DState#wsdatastate{buffer = <>, + flen = 0, + packet = <>}); +process_data2(DState = #wsdatastate{buffer = <>, + packet = Packet, + mask = Mask, + flen = 1}) -> + process_data2(DState#wsdatastate{buffer = <>, + flen = 0, + packet = <>}); +process_data2(DState = #wsdatastate{buffer = <>, + flen = 0}) -> + process_data2(DState#wsdatastate{buffer = <<>>, + partial = <>}). + + + + +%% This basically should be replaced with socket drop routine +warn_not_masked(1) -> ok; +warn_not_masked(_) -> ?WARNING_MSG("Payload was not masked!~n", []). + +%% Same, data size should not be too big. %x0000000000000000-7FFFFFFFFFFFFFFF :) +warn_first_bit_should_not_be_one(0) -> ok; +warn_first_bit_should_not_be_one(_) -> ?WARNING_MSG("64-bit extra payload length should not start with 1!~n", []). + process_data(DState = #wsdatastate{buffer=undefined}) -> {DState#wsdatastate.packet, DState#wsdatastate.partial}; process_data(DState = #wsdatastate{buffer= <<>>}) ->