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,6 +364,29 @@ process_request(#state{request_method = Method,
|
||||
_ ->
|
||||
SockMod:peername(Socket)
|
||||
end,
|
||||
|
||||
%% 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} ->
|
||||
{_, 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,
|
||||
@ -374,15 +397,6 @@ process_request(#state{request_method = Method,
|
||||
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]);
|
||||
|
||||
false ->
|
||||
?DEBUG("It is not a websocket.",[]),
|
||||
case process(RequestHandlers, Request) of
|
||||
El when element(1, El) == xmlelement ->
|
||||
|
@ -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}]
|
||||
}).
|
@ -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
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