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:
parent
cccbf7de12
commit
c41bdea1f1
@ -364,25 +364,39 @@ process_request(#state{request_method = Method,
|
|||||||
_ ->
|
_ ->
|
||||||
SockMod:peername(Socket)
|
SockMod:peername(Socket)
|
||||||
end,
|
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
|
%% XXX bard: This previously passed control to
|
||||||
%% ejabberd_web:process_get, now passes it to a local
|
%% ejabberd_web:process_get, now passes it to a local
|
||||||
%% procedure (process) that handles dispatching based on
|
%% procedure (process) that handles dispatching based on
|
||||||
%% URL path prefix.
|
%% URL path prefix.
|
||||||
case ejabberd_websocket:check(Path, RequestHeaders) of
|
case ejabberd_websocket:check(Path, RequestHeaders) of
|
||||||
{true, _VSN} ->
|
{true, VSN} ->
|
||||||
?DEBUG("It is a websocket version : ~p",[_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 ->
|
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.",[]),
|
?DEBUG("It is not a websocket.",[]),
|
||||||
case process(RequestHandlers, Request) of
|
case process(RequestHandlers, Request) of
|
||||||
El when element(1, El) == xmlelement ->
|
El when element(1, El) == xmlelement ->
|
||||||
|
@ -32,3 +32,19 @@
|
|||||||
tp, % transfer protocol = http | https
|
tp, % transfer protocol = http | https
|
||||||
headers
|
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}]
|
||||||
|
}).
|
@ -49,6 +49,29 @@ check(_Path, Headers)->
|
|||||||
VsnSupported = [{'draft-hixie', 76}, {'draft-hixie', 68}],
|
VsnSupported = [{'draft-hixie', 76}, {'draft-hixie', 68}],
|
||||||
% checks
|
% checks
|
||||||
check_websockets(VsnSupported, Headers).
|
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([], _Headers) -> false;
|
||||||
check_websockets([Vsn|T], Headers) ->
|
check_websockets([Vsn|T], Headers) ->
|
||||||
@ -64,7 +87,7 @@ check_websocket({'draft-hixie', 76} = Vsn, Headers) ->
|
|||||||
% set required headers
|
% set required headers
|
||||||
RequiredHeaders = [
|
RequiredHeaders = [
|
||||||
{'Upgrade', "WebSocket"}, {'Connection', "Upgrade"}, {'Host', ignore}, {"Origin", ignore},
|
{'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
|
% check for headers existance
|
||||||
case check_headers(Headers, RequiredHeaders) of
|
case check_headers(Headers, RequiredHeaders) of
|
||||||
@ -95,9 +118,10 @@ check_websocket(_Vsn, _Headers) -> false. % not implemented
|
|||||||
check_headers(Headers, RequiredHeaders) ->
|
check_headers(Headers, RequiredHeaders) ->
|
||||||
F = fun({Tag, Val}) ->
|
F = fun({Tag, Val}) ->
|
||||||
% see if the required Tag is in the Headers
|
% 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
|
false -> true; % header not found, keep in list
|
||||||
{value, {Tag, HVal}} ->
|
{Tag, HVal} ->
|
||||||
|
?DEBUG("check: ~p", [{Tag, HVal,Val }]),
|
||||||
case Val of
|
case Val of
|
||||||
ignore -> false; % ignore value -> ok, remove from list
|
ignore -> false; % ignore value -> ok, remove from list
|
||||||
HVal -> false; % expected val -> 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.
|
% Description: Builds the server handshake response.
|
||||||
handshake({'draft-hixie', 76}, Sock,SocketMod, Headers, {Path, Origin, Host}) ->
|
handshake({'draft-hixie', 76}, Sock,SocketMod, Headers, {Path, Origin, Host}) ->
|
||||||
% build data
|
% build data
|
||||||
Key1 = lists:keysearch("Sec-WebSocket-Key1",1, Headers),
|
{_, Key1} = lists:keyfind("Sec-Websocket-Key1",1, Headers),
|
||||||
Key2 = lists:keysearch("Sec-WebSocket-Key2",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]
|
% 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
|
Body = case SocketMod:recv(Sock, 8, 30*1000) of
|
||||||
{ok, Bin} -> Bin;
|
{ok, Bin} -> Bin;
|
||||||
{error, timeout} ->
|
{error, timeout} ->
|
||||||
@ -133,7 +162,7 @@ handshake({'draft-hixie', 76}, Sock,SocketMod, Headers, {Path, Origin, Host}) ->
|
|||||||
"Upgrade: WebSocket\r\n",
|
"Upgrade: WebSocket\r\n",
|
||||||
"Connection: Upgrade\r\n",
|
"Connection: Upgrade\r\n",
|
||||||
"Sec-WebSocket-Origin: ", Origin, "\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})
|
build_challenge({'draft-hixie', 76}, {Key1, Key2, Body})
|
||||||
];
|
];
|
||||||
handshake({'draft-hixie', 68}, _Sock,_SocketMod, _Headers, {Path, Origin, Host}) ->
|
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,
|
Part1 = list_to_integer(Ikey1) div Blank1,
|
||||||
Part2 = list_to_integer(Ikey2) div Blank2,
|
Part2 = list_to_integer(Ikey2) div Blank2,
|
||||||
Ckey = <<Part1:4/big-unsigned-integer-unit:8, Part2:4/big-unsigned-integer-unit:8, Key3/binary>>,
|
Ckey = <<Part1:4/big-unsigned-integer-unit:8, Part2:4/big-unsigned-integer-unit:8, Key3/binary>>,
|
||||||
erlang:md5(Ckey).
|
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
84
src/web/ejabberd_ws.erl
Normal 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 =======================================================
|
15
src/web/websocket_test.erl
Normal file
15
src/web/websocket_test.erl
Normal 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.
|
Loading…
Reference in New Issue
Block a user