diff --git a/src/web/ejabberd_http.erl b/src/web/ejabberd_http.erl index 428df4ecd..5365a5b69 100644 --- a/src/web/ejabberd_http.erl +++ b/src/web/ejabberd_http.erl @@ -364,25 +364,39 @@ process_request(#state{request_method = Method, _ -> SockMod:peername(Socket) end, - Request = #request{method = Method, - path = LPath, - q = LQuery, - auth = Auth, - lang = Lang, - host = Host, - port = Port, - tp = TP, - headers = RequestHeaders, - ip = IP}, + %% XXX bard: This previously passed control to %% ejabberd_web:process_get, now passes it to a local %% procedure (process) that handles dispatching based on %% URL path prefix. case ejabberd_websocket:check(Path, RequestHeaders) of - {true, _VSN} -> - ?DEBUG("It is a websocket version : ~p",[_VSN]); - + {true, VSN} -> + {_, Origin} = lists:keyfind("Origin", 1, RequestHeaders), + Ws = #ws{socket = Socket, + sockmod = SockMod, + ws_autoexit = true, + path = Path, + vsn = VSN, + host = Host, + origin = Origin, + headers = RequestHeaders + }, + + + + ?DEBUG("It is a websocket version : ~p",[VSN]), + ejabberd_websocket:connect(Ws, websocket_test); false -> + Request = #request{method = Method, + path = LPath, + q = LQuery, + auth = Auth, + lang = Lang, + host = Host, + port = Port, + tp = TP, + headers = RequestHeaders, + ip = IP}, ?DEBUG("It is not a websocket.",[]), case process(RequestHandlers, Request) of El when element(1, El) == xmlelement -> diff --git a/src/web/ejabberd_http.hrl b/src/web/ejabberd_http.hrl index 8bb96220a..09f8ff832 100644 --- a/src/web/ejabberd_http.hrl +++ b/src/web/ejabberd_http.hrl @@ -32,3 +32,19 @@ tp, % transfer protocol = http | https headers }). + + +% Websocket Request +-record(ws, { + socket, % the socket handling the request + sockmod, % gen_tcp | tls + ws_autoexit, % websocket process is automatically killed: true | false + peer_addr, % peer IP | undefined + peer_port, % peer port | undefined + peer_cert, % undefined | the DER encoded peer certificate that can be decoded with public_key:pkix_decode_cert/2 + vsn, % {Maj,Min} | {'draft-hixie', Ver} + origin, % the originator + host, % the host + path, % the websocket GET request path + headers % [{Tag, Val}] + }). \ No newline at end of file diff --git a/src/web/ejabberd_websocket.erl b/src/web/ejabberd_websocket.erl index 0e1489c85..e18a77860 100644 --- a/src/web/ejabberd_websocket.erl +++ b/src/web/ejabberd_websocket.erl @@ -49,6 +49,29 @@ check(_Path, Headers)-> VsnSupported = [{'draft-hixie', 76}, {'draft-hixie', 68}], % checks check_websockets(VsnSupported, Headers). + +% Connect and handshake with Websocket. +connect(#ws{vsn = Vsn, socket = Socket, origin=Origin, host=Host,sockmod = SockMod, path = Path, headers = Headers, ws_autoexit = WsAutoExit} = Ws, WsLoop) -> + % build handshake + HandshakeServer = handshake(Vsn, Socket,SockMod, Headers, {Path, Origin, Host}), + % send handshake back + ?DEBUG("building handshake response : ~p", [HandshakeServer]), + SockMod:send(Socket, 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 + WsHandleLoopPid = spawn(fun() -> WsLoop:handle(Ws0) end), + erlang:monitor(process, WsHandleLoopPid), + % set opts + case SockMod of + gen_tcp -> + inet:setopts(Socket, [{packet, 0}, {active, true}]); + _ -> + SockMod:setopts(Socket, [{packet, 0}, {active, true}]) + end, + % start listening for incoming data + ws_loop(Socket, none, WsHandleLoopPid, SockMod, WsAutoExit). + check_websockets([], _Headers) -> false; check_websockets([Vsn|T], Headers) -> @@ -64,7 +87,7 @@ check_websocket({'draft-hixie', 76} = Vsn, Headers) -> % set required headers RequiredHeaders = [ {'Upgrade', "WebSocket"}, {'Connection', "Upgrade"}, {'Host', ignore}, {"Origin", ignore}, - {"Sec-WebSocket-Key1", ignore}, {"Sec-WebSocket-Key2", ignore} + {"Sec-Websocket-Key1", ignore}, {"Sec-Websocket-Key2", ignore} ], % check for headers existance case check_headers(Headers, RequiredHeaders) of @@ -95,9 +118,10 @@ check_websocket(_Vsn, _Headers) -> false. % not implemented check_headers(Headers, RequiredHeaders) -> F = fun({Tag, Val}) -> % see if the required Tag is in the Headers - case lists:keysearch(Tag, 1, Headers) of + case lists:keyfind(Tag, 1, Headers) of false -> true; % header not found, keep in list - {value, {Tag, HVal}} -> + {Tag, HVal} -> + ?DEBUG("check: ~p", [{Tag, HVal,Val }]), case Val of ignore -> false; % ignore value -> ok, remove from list HVal -> false; % expected val -> ok, remove from list @@ -114,10 +138,15 @@ check_headers(Headers, RequiredHeaders) -> % Description: Builds the server handshake response. handshake({'draft-hixie', 76}, Sock,SocketMod, Headers, {Path, Origin, Host}) -> % build data - Key1 = lists:keysearch("Sec-WebSocket-Key1",1, Headers), - Key2 = lists:keysearch("Sec-WebSocket-Key2",1, Headers), + {_, Key1} = lists:keyfind("Sec-Websocket-Key1",1, Headers), + {_, Key2} = lists:keyfind("Sec-Websocket-Key2",1, Headers), % handshake needs body of the request, still need to read it [TODO: default recv timeout hard set, will be exported when WS protocol is final] - SocketMod:setopts(Sock, [{packet, raw}, {active, false}]), + case SocketMod of + gen_tcp -> + inet:setopts(Sock, [{packet, raw}, {active, false}]); + _ -> + SocketMod:setopts(Sock, [{packet, raw}, {active, false}]) + end, Body = case SocketMod:recv(Sock, 8, 30*1000) of {ok, Bin} -> Bin; {error, timeout} -> @@ -133,7 +162,7 @@ handshake({'draft-hixie', 76}, Sock,SocketMod, Headers, {Path, Origin, Host}) -> "Upgrade: WebSocket\r\n", "Connection: Upgrade\r\n", "Sec-WebSocket-Origin: ", Origin, "\r\n", - "Sec-WebSocket-Location: ws://", lists:concat([Host, Path]), "\r\n\r\n", + "Sec-WebSocket-Location: ws://", Host, ":5280",Path, "\r\n\r\n", build_challenge({'draft-hixie', 76}, {Key1, Key2, Body}) ]; handshake({'draft-hixie', 68}, _Sock,_SocketMod, _Headers, {Path, Origin, Host}) -> @@ -156,4 +185,73 @@ build_challenge({'draft-hixie', 76}, {Key1, Key2, Key3}) -> Part1 = list_to_integer(Ikey1) div Blank1, Part2 = list_to_integer(Ikey2) div Blank2, Ckey = <>, - erlang:md5(Ckey). \ No newline at end of file + erlang:md5(Ckey). + + +ws_loop(Socket, Buffer, WsHandleLoopPid, SocketMode, WsAutoExit) -> + ?DEBUG("websocket loop", []), + receive + {tcp, Socket, Data} -> + handle_data(Buffer, binary_to_list(Data), Socket, WsHandleLoopPid, SocketMode, WsAutoExit); + {tcp_closed, Socket} -> + ?DEBUG("tcp connection was closed, exit", []), + % close websocket and custom controlling loop + websocket_close(Socket, WsHandleLoopPid, SocketMode, WsAutoExit); + {'DOWN', Ref, process, WsHandleLoopPid, Reason} -> + case Reason of + normal -> + ?DEBUG("linked websocket controlling loop stopped.", []); + _ -> + ?ERROR_MSG("linked websocket controlling loop crashed with reason: ~p", [Reason]) + end, + % demonitor + erlang:demonitor(Ref), + % close websocket and custom controlling loop + websocket_close(Socket, WsHandleLoopPid, SocketMode, WsAutoExit); + {send, Data} -> + ?DEBUG("sending data to websocket: ~p", [Data]), + SocketMode:send(Socket, [0, Data, 255]), + ws_loop(Socket, Buffer, WsHandleLoopPid, SocketMode, WsAutoExit); + shutdown -> + ?DEBUG("shutdown request received, closing websocket with pid ~p", [self()]), + % close websocket and custom controlling loop + websocket_close(Socket, WsHandleLoopPid, SocketMode, WsAutoExit); + _Ignored -> + ?WARNING_MSG("received unexpected message, ignoring: ~p", [_Ignored]), + ws_loop(Socket, Buffer, WsHandleLoopPid, SocketMode, WsAutoExit) + end. + +% Buffering and data handling +handle_data(none, [0|T], Socket, WsHandleLoopPid, SocketMode, WsAutoExit) -> + ?DEBUG("handle_data 1", []), + handle_data([], T, Socket, WsHandleLoopPid, SocketMode, WsAutoExit); + +handle_data(none, [], Socket, WsHandleLoopPid, SocketMode, WsAutoExit) -> + ?DEBUG("handle_data 2", []), + ws_loop(Socket, none, WsHandleLoopPid, SocketMode, WsAutoExit); + +handle_data(L, [255|T], Socket, WsHandleLoopPid, SocketMode, WsAutoExit) -> + ?DEBUG("handle_data 3", []), + WsHandleLoopPid ! {browser, lists:reverse(L)}, + handle_data(none, T, Socket, WsHandleLoopPid, SocketMode, WsAutoExit); + +handle_data(L, [H|T], Socket, WsHandleLoopPid, SocketMode, WsAutoExit) -> + ?DEBUG("handle_data 4, Buffer = ~p", [L]), + handle_data([H|L], T, Socket, WsHandleLoopPid, SocketMode, WsAutoExit); + +handle_data([], L, Socket, WsHandleLoopPid, SocketMode, WsAutoExit) -> + ?DEBUG("handle_data 5", []), + ws_loop(Socket, L, WsHandleLoopPid, SocketMode, WsAutoExit). + +% Close socket and custom handling loop dependency +websocket_close(Socket, WsHandleLoopPid, SocketMode, WsAutoExit) -> + case WsAutoExit of + true -> + % kill custom handling loop process + exit(WsHandleLoopPid, kill); + false -> + % the killing of the custom handling loop process is handled in the loop itself -> send event + WsHandleLoopPid ! closed + end, + % close main socket + SocketMode:close(Socket). diff --git a/src/web/ejabberd_ws.erl b/src/web/ejabberd_ws.erl new file mode 100644 index 000000000..6626dfb67 --- /dev/null +++ b/src/web/ejabberd_ws.erl @@ -0,0 +1,84 @@ +%%%---------------------------------------------------------------------- +%%% File : ejabberd_websocket.erl +%%% Author : Eric Cestari +%%% Purpose : XMPP Websocket support +%%% Created : 09-10-2010 by Eric Cestari +%%% Slightly adapted from : +% ========================================================================================================== +% MISULTIN - Websocket Request +% +% >-|-|-(°> +% +% Copyright (C) 2010, Roberto Ostinelli . +% All rights reserved. +% +% BSD License +% +% Redistribution and use in source and binary forms, with or without modification, are permitted provided +% that the following conditions are met: +% +% * Redistributions of source code must retain the above copyright notice, this list of conditions and the +% following disclaimer. +% * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and +% the following disclaimer in the documentation and/or other materials provided with the distribution. +% * Neither the name of the authors nor the names of its contributors may be used to endorse or promote +% products derived from this software without specific prior written permission. +% +% THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED +% WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +% PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +% ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED +% TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +% HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +% NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +% POSSIBILITY OF SUCH DAMAGE. +% ========================================================================================================== +-module(ejabberd_ws, [Ws, SocketPid]). +-vsn("0.6.1"). + +% API +-export([raw/0, get/1, send/1]). + +% includes +-include("ejabberd_http.hrl"). + + +% ============================ \/ API ====================================================================== + +% Description: Returns raw websocket content. +raw() -> + Ws. + +% Description: Get websocket info. +get(socket) -> + Ws#ws.socket; +get(socket_mode) -> + Ws#ws.sockmod; +get(peer_addr) -> + Ws#ws.peer_addr; +get(peer_port) -> + Ws#ws.peer_port; +get(peer_cert) -> + Ws#ws.peer_cert; +get(vsn) -> + Ws#ws.vsn; +get(origin) -> + Ws#ws.origin; +get(host) -> + Ws#ws.host; +get(path) -> + Ws#ws.path; +get(headers) -> + Ws#ws.headers. + +% send data +send(Data) -> + SocketPid ! {send, Data}. + +% ============================ /\ API ====================================================================== + + + +% ============================ \/ INTERNAL FUNCTIONS ======================================================= + +% ============================ /\ INTERNAL FUNCTIONS ======================================================= diff --git a/src/web/websocket_test.erl b/src/web/websocket_test.erl new file mode 100644 index 000000000..15dedc0b6 --- /dev/null +++ b/src/web/websocket_test.erl @@ -0,0 +1,15 @@ +-module (websocket_test). +-export([handle/1]). + +% callback on received websockets data +handle(Ws) -> + receive + {browser, Data} -> + Ws:send(["received '", Data, "'"]), + handle(Ws); + _Ignore -> + handle(Ws) + after 5000 -> + Ws:send("pushing!"), + handle(Ws) + end.