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:
parent
c8567f1de2
commit
b0a81778af
@ -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),
|
||||
|
@ -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.
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
206
src/web/ejabberd_http_ws.erl
Normal file
206
src/web/ejabberd_http_ws.erl
Normal 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.
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user