25
1
mirror of https://github.com/processone/ejabberd.git synced 2024-10-01 14:44:07 +02:00

[TECH-1511] rough support for websockets

This commit is contained in:
Eric Cestari 2010-09-10 17:14:58 +02:00
parent cccbf7de12
commit c41bdea1f1
5 changed files with 248 additions and 21 deletions

View File

@ -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 ->

View File

@ -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}]
}).

View File

@ -50,6 +50,29 @@ check(_Path, Headers)->
% 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) ->
case check_websocket(Vsn, Headers) of
@ -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}) ->
@ -157,3 +186,72 @@ build_challenge({'draft-hixie', 76}, {Key1, Key2, Key3}) ->
Part2 = list_to_integer(Ikey2) div Blank2,
Ckey = <<Part1:4/big-unsigned-integer-unit:8, Part2:4/big-unsigned-integer-unit:8, Key3/binary>>,
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).

84
src/web/ejabberd_ws.erl Normal file
View File

@ -0,0 +1,84 @@
%%%----------------------------------------------------------------------
%%% File : ejabberd_websocket.erl
%%% Author : Eric Cestari <ecestari@process-one.net>
%%% Purpose : XMPP Websocket support
%%% Created : 09-10-2010 by Eric Cestari <ecestari@process-one.net>
%%% Slightly adapted from :
% ==========================================================================================================
% MISULTIN - Websocket Request
%
% >-|-|-(°>
%
% Copyright (C) 2010, Roberto Ostinelli <roberto@ostinelli.net>.
% 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 =======================================================

View File

@ -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.