25
1
mirror of https://github.com/processone/ejabberd.git synced 2024-12-24 17:29:28 +01:00

[TECH-1511] preliminary XMPP support via websockets

This commit is contained in:
Eric Cestari 2010-09-13 17:26:24 +02:00
parent c8567f1de2
commit b0a81778af
6 changed files with 238 additions and 24 deletions

View File

@ -75,7 +75,7 @@ update_reg_users_counter_table(Server) ->
mnesia:sync_dirty(F).
plain_password_required() ->
false.
true.
check_password(User, Server, Password) ->
LUser = jlib:nodeprep(User),

View File

@ -1851,6 +1851,7 @@ get_conn_type(StateData) ->
end
end;
ejabberd_http_poll -> http_poll;
ejabberd_http_ws -> http_ws;
ejabberd_http_bind -> http_bind;
_ -> unknown
end.

View File

@ -146,6 +146,14 @@ init([]) ->
infinity,
supervisor,
[ejabberd_tmp_sup]},
WSLoopSupervisor =
{ejabberd_wsloop_sup,
{ejabberd_tmp_sup, start_link,
[ejabberd_wsloop_sup, ejabberd_wsloop]},
permanent,
infinity,
supervisor,
[ejabberd_tmp_sup]},
FrontendSocketSupervisor =
{ejabberd_frontend_socket_sup,
{ejabberd_tmp_sup, start_link,

View File

@ -409,9 +409,7 @@ process_request(#state{request_method = Method,
origin = Origin,
headers = RequestHeaders
},
?DEBUG("WS: ~p/~p~n", [WebSocketHandlers, Path]),
?DEBUG("It is a websocket version : ~p",[VSN]),
process(WebSocketHandlers, Ws),
process(WebSocketHandlers, Ws),
none;
false ->
Request = #request{method = Method,

View File

@ -0,0 +1,206 @@
%%%----------------------------------------------------------------------
%%% 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>
%%%
%%%
%%% ejabberd, Copyright (C) 2002-2010 ProcessOne
%%%
%%% This program is free software; you can redistribute it and/or
%%% modify it under the terms of the GNU General Public License as
%%% published by the Free Software Foundation; either version 2 of the
%%% License, or (at your option) any later version.
%%%
%%% This program is distributed in the hope that it will be useful,
%%% but WITHOUT ANY WARRANTY; without even the implied warranty of
%%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
%%% General Public License for more details.
%%%
%%% You should have received a copy of the GNU General Public License
%%% along with this program; if not, write to the Free Software
%%% Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
%%% 02111-1307 USA
%%%
%%%----------------------------------------------------------------------
-module (ejabberd_http_ws).
-author('ecestari@process-one.net').
-behaviour(gen_fsm).
% External exports
-export([
start/1,
start_link/1,
init/1,
handle_event/3,
handle_sync_event/4,
code_change/4,
handle_info/3,
terminate/3,
send/2,
setopts/2,
sockname/1, peername/1,
controlling_process/2,
close/1]).
-include("ejabberd.hrl").
-include("jlib.hrl").
-include("ejabberd_http.hrl").
-record(state, {
socket,
timeout,
timer,
input = "",
waiting_input = false, %% {ReceiverPid, Tag}
last_receiver,
ws}).
%-define(DBGFSM, true).
-ifdef(DBGFSM).
-define(FSMOPTS, [{debug, [trace]}]).
-else.
-define(FSMOPTS, []).
-endif.
-define(WEBSOCKET_TIMEOUT, 300000).
%
%
%%%%----------------------------------------------------------------------
%%%% API
%%%%----------------------------------------------------------------------
start(WS) ->
supervisor:start_child(ejabberd_wsloop_sup, [WS]).
start_link(WS) ->
gen_fsm:start_link(?MODULE, [WS],[{debug, [trace]}]).
send({http_ws, FsmRef, _IP}, Packet) ->
gen_fsm:sync_send_all_state_event(FsmRef, {send, Packet}).
setopts({http_ws, FsmRef, _IP}, Opts) ->
case lists:member({active, once}, Opts) of
true ->
gen_fsm:send_all_state_event(FsmRef, {activate, self()});
_ ->
ok
end.
sockname(_Socket) ->
{ok, {{0, 0, 0, 0}, 0}}.
peername({http_ws, _FsmRef, IP}) ->
{ok, IP}.
controlling_process(_Socket, _Pid) ->
ok.
become_controller(FsmRef, C2SPid) ->
gen_fsm:send_all_state_event(FsmRef, {become_controller, C2SPid}).
close({http_ws, FsmRef, _IP}) ->
catch gen_fsm:sync_send_all_state_event(FsmRef, close).
%%% Internal
init([WS]) ->
?INFO_MSG("started: ~p", [WS]),
%% Read c2s options from the first ejabberd_c2s configuration in
%% the config file listen section
%% TODO: We should have different access and shaper values for
%% each connector. The default behaviour should be however to use
%% the default c2s restrictions if not defined for the current
%% connector.
Opts = ejabberd_c2s_config:get_c2s_limits(),
WSTimeout = case ejabberd_config:get_local_option({websocket_timeout,
?MYNAME}) of
%% convert seconds of option into milliseconds
Int when is_integer(Int) -> Int*1000;
undefined -> ?WEBSOCKET_TIMEOUT
end,
Socket = {http_ws, self(), {{127,0,0,1}, 5280}}, %FIXME
ejabberd_socket:start(ejabberd_c2s, ?MODULE, Socket, Opts),
Timer = erlang:start_timer(WSTimeout, self(), []),
{ok, loop, #state{
socket = Socket,
timeout = WSTimeout,
timer = Timer,
ws = WS}}.
handle_event({activate, From}, StateName, StateData) ->
case StateData#state.input of
"" ->
{next_state, StateName,
StateData#state{waiting_input = {From, ok}}};
Input ->
Receiver = From,
Receiver ! {tcp, StateData#state.socket, list_to_binary(Input)},
{next_state, StateName, StateData#state{input = "",
waiting_input = false,
last_receiver = Receiver
}}
end.
handle_sync_event({send, Packet}, _From, StateName, #state{ws = WS} = StateData) ->
Packet2 = if
is_binary(Packet) ->
binary_to_list(Packet);
true ->
Packet
end,
?DEBUG("sending on websocket : ~p ", [Packet2]),
WS:send(lists:flatten(Packet2)),
{reply, ok, StateName, StateData};
handle_sync_event(close, _From, _StateName, StateData) ->
Reply = ok,
{stop, normal, Reply, StateData}.
handle_info({browser, Packet}, StateName, StateData)->
case StateData#state.waiting_input of
false ->
Input = [StateData#state.input|Packet],
Reply = ok,
{reply, Reply, StateName, StateData#state{input = Input}};
{Receiver, _Tag} ->
Receiver ! {tcp, StateData#state.socket,
list_to_binary(Packet)},
cancel_timer(StateData#state.timer),
Timer = erlang:start_timer(StateData#state.timeout, self(), []),
Reply = ok,
{reply, Reply, StateName,
StateData#state{waiting_input = false,
last_receiver = Receiver,
timer = Timer}}
end,
{next_state, StateName, StateData};
handle_info({timeout, Timer, _}, _StateName,
#state{timer = Timer} = StateData) ->
{stop, normal, StateData};
handle_info(_, StateName, StateData) ->
{next_state, StateName, StateData}.
code_change(_OldVsn, StateName, StateData, _Extra) ->
{ok, StateName, StateData}.
terminate(_Reason, _StateName, _StateData) -> ok.
cancel_timer(Timer) ->
erlang:cancel_timer(Timer),
receive
{timeout, Timer, _} ->
ok
after 0 ->
ok
end.

View File

@ -44,7 +44,7 @@
-include("ejabberd_http.hrl").
check(_Path, Headers)->
?DEBUG("testing for a websocket request path: ~p headers: ~p", [_Path, Headers]),
%?DEBUG("testing for a websocket request path: ~p headers: ~p", [_Path, Headers]),
% set supported websocket protocols, order does matter
VsnSupported = [{'draft-hixie', 76}, {'draft-hixie', 68}],
% checks
@ -55,12 +55,12 @@ connect(#ws{vsn = Vsn, socket = Socket, origin=Origin, host=Host, port=Port, soc
% build handshake
HandshakeServer = handshake(Vsn, Socket,SockMod, Headers, {Path, Origin, Host, Port}),
% send handshake back
?DEBUG("building handshake response : ~p", [HandshakeServer]),
%?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]),
%?DEBUG("Ws0 : ~p",[Ws0]),
% add data to ws record and spawn controlling process
WsHandleLoopPid = WsLoop:start(Ws0),
{ok, WsHandleLoopPid} = WsLoop:start_link(Ws0),
erlang:monitor(process, WsHandleLoopPid),
% set opts
case SockMod of
@ -83,7 +83,7 @@ check_websockets([Vsn|T], Headers) ->
% Function: {true, Vsn} | false
% Description: Check if the incoming request is a websocket request.
check_websocket({'draft-hixie', 76} = Vsn, Headers) ->
?DEBUG("testing for websocket protocol ~p", [Vsn]),
%?DEBUG("testing for websocket protocol ~p", [Vsn]),
% set required headers
RequiredHeaders = [
{'Upgrade', "WebSocket"}, {'Connection', "Upgrade"}, {'Host', ignore}, {"Origin", ignore},
@ -95,11 +95,11 @@ check_websocket({'draft-hixie', 76} = Vsn, Headers) ->
% return
{true, Vsn};
_RemainingHeaders ->
?DEBUG("not protocol ~p, remaining headers: ~p", [Vsn, _RemainingHeaders]),
%?DEBUG("not protocol ~p, remaining headers: ~p", [Vsn, _RemainingHeaders]),
false
end;
check_websocket({'draft-hixie', 68} = Vsn, Headers) ->
?DEBUG("testing for websocket protocol ~p", [Vsn]),
%?DEBUG("testing for websocket protocol ~p", [Vsn]),
% set required headers
RequiredHeaders = [
{'Upgrade', "WebSocket"}, {'Connection', "Upgrade"}, {'Host', ignore}, {"Origin", ignore}
@ -108,7 +108,7 @@ check_websocket({'draft-hixie', 68} = Vsn, Headers) ->
case check_headers(Headers, RequiredHeaders) of
true -> {true, Vsn};
_RemainingHeaders ->
?DEBUG("not protocol ~p, remaining headers: ~p", [Vsn, _RemainingHeaders]),
%?DEBUG("not protocol ~p, remaining headers: ~p", [Vsn, _RemainingHeaders]),
false
end;
check_websocket(_Vsn, _Headers) -> false. % not implemented
@ -121,7 +121,7 @@ check_headers(Headers, RequiredHeaders) ->
case lists:keyfind(Tag, 1, Headers) of
false -> true; % header not found, keep in list
{Tag, HVal} ->
?DEBUG("check: ~p", [{Tag, HVal,Val }]),
%?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
@ -156,7 +156,7 @@ handshake({'draft-hixie', 76}, Sock,SocketMod, Headers, {Path, Origin, Host, Por
?ERROR_MSG("tcp error treating data: ~p", [_Other]),
<<>>
end,
?DEBUG("got content in body of websocket request: ~p, ~p", [Body,string:join([Host, Path],"/")]),
%?DEBUG("got content in body of websocket request: ~p, ~p", [Body,string:join([Host, Path],"/")]),
% prepare handhsake response
["HTTP/1.1 101 WebSocket Protocol Handshake\r\n",
"Upgrade: WebSocket\r\n",
@ -193,18 +193,19 @@ build_challenge({'draft-hixie', 76}, {Key1, Key2, Key3}) ->
ws_loop(Socket, Buffer, WsHandleLoopPid, SocketMode, WsAutoExit) ->
?DEBUG("websocket loop", []),
%?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", []),
%?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.", []);
%?DEBUG("linked websocket controlling loop stopped.", []);
ok;
_ ->
?ERROR_MSG("linked websocket controlling loop crashed with reason: ~p", [Reason])
end,
@ -213,11 +214,11 @@ ws_loop(Socket, Buffer, WsHandleLoopPid, SocketMode, WsAutoExit) ->
% close websocket and custom controlling loop
websocket_close(Socket, WsHandleLoopPid, SocketMode, WsAutoExit);
{send, Data} ->
?DEBUG("sending data to websocket: ~p", [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()]),
%?DEBUG("shutdown request received, closing websocket with pid ~p", [self()]),
% close websocket and custom controlling loop
websocket_close(Socket, WsHandleLoopPid, SocketMode, WsAutoExit);
_Ignored ->
@ -227,24 +228,24 @@ ws_loop(Socket, Buffer, WsHandleLoopPid, SocketMode, WsAutoExit) ->
% Buffering and data handling
handle_data(none, [0|T], Socket, WsHandleLoopPid, SocketMode, WsAutoExit) ->
?DEBUG("handle_data 1", []),
%?DEBUG("handle_data 1", []),
handle_data([], T, Socket, WsHandleLoopPid, SocketMode, WsAutoExit);
handle_data(none, [], Socket, WsHandleLoopPid, SocketMode, WsAutoExit) ->
?DEBUG("handle_data 2", []),
%?DEBUG("handle_data 2", []),
ws_loop(Socket, none, WsHandleLoopPid, SocketMode, WsAutoExit);
handle_data(L, [255|T], Socket, WsHandleLoopPid, SocketMode, WsAutoExit) ->
?DEBUG("handle_data 3", []),
%?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]),
%?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", []),
%?DEBUG("handle_data 5", []),
ws_loop(Socket, L, WsHandleLoopPid, SocketMode, WsAutoExit).
% Close socket and custom handling loop dependency