25
1
mirror of https://github.com/processone/ejabberd.git synced 2024-11-24 16:23:40 +01:00
xmpp.chapril.org-ejabberd/src/ejabberd_http_ws.erl

322 lines
12 KiB
Erlang
Raw Normal View History

2015-02-25 10:42:59 +01:00
%%%----------------------------------------------------------------------
%%% 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>
%%%
%%%
2024-01-22 16:40:01 +01:00
%%% ejabberd, Copyright (C) 2002-2024 ProcessOne
2015-02-25 10:42:59 +01:00
%%%
%%% 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.
%%%
2015-02-25 15:19:33 +01:00
%%% 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.,
%%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
2015-02-25 10:42:59 +01:00
%%%
%%%----------------------------------------------------------------------
-module(ejabberd_http_ws).
-author('ecestari@process-one.net').
2018-09-17 10:21:02 +02:00
-behaviour(xmpp_socket).
-behaviour(p1_fsm).
2015-02-25 10:42:59 +01:00
-export([start/1, start_link/1, init/1, handle_event/3,
handle_sync_event/4, code_change/4, handle_info/3,
2015-06-01 14:38:27 +02:00
terminate/3, send_xml/2, setopts/2, sockname/1,
2018-09-17 10:21:02 +02:00
peername/1, controlling_process/2, get_owner/1,
reset_stream/1, close/1, change_shaper/2,
2019-04-23 15:21:06 +02:00
socket_handoff/3, get_transport/1]).
2015-02-25 10:42:59 +01:00
-include("logger.hrl").
-include_lib("xmpp/include/xmpp.hrl").
2015-02-25 10:42:59 +01:00
-include("ejabberd_http.hrl").
-record(state,
{socket :: ws_socket(),
2019-06-14 11:33:26 +02:00
ping_interval :: non_neg_integer(),
2015-02-25 10:42:59 +01:00
ping_timer = make_ref() :: reference(),
2017-02-18 07:36:27 +01:00
pong_expected = false :: boolean(),
2019-06-14 11:33:26 +02:00
timeout :: non_neg_integer(),
2015-02-25 10:42:59 +01:00
timer = make_ref() :: reference(),
input = [] :: list(),
2018-09-17 10:21:02 +02:00
active = false :: boolean(),
c2s_pid :: pid(),
ws :: {#ws{}, pid()}}).
2015-02-25 10:42:59 +01:00
%-define(DBGFSM, true).
-ifdef(DBGFSM).
-define(FSMOPTS, [{debug, [trace]}]).
-else.
-define(FSMOPTS, []).
-endif.
-type ws_socket() :: {http_ws, pid(), {inet:ip_address(), inet:port_number()}}.
-export_type([ws_socket/0]).
start(WS) ->
p1_fsm:start(?MODULE, [WS], ?FSMOPTS).
2015-02-25 10:42:59 +01:00
start_link(WS) ->
p1_fsm:start_link(?MODULE, [WS], ?FSMOPTS).
2015-02-25 10:42:59 +01:00
send_xml({http_ws, FsmRef, _IP}, Packet) ->
case catch p1_fsm:sync_send_all_state_event(FsmRef,
{send_xml, Packet},
15000)
of
{'EXIT', {timeout, _}} -> {error, timeout};
{'EXIT', _} -> {error, einval};
Res -> Res
end.
2015-02-25 10:42:59 +01:00
setopts({http_ws, FsmRef, _IP}, Opts) ->
case lists:member({active, once}, Opts) of
true ->
p1_fsm:send_all_state_event(FsmRef,
2015-02-25 10:42:59 +01:00
{activate, self()});
_ -> ok
end.
sockname(_Socket) -> {ok, {{0, 0, 0, 0}, 0}}.
peername({http_ws, _FsmRef, IP}) -> {ok, IP}.
controlling_process(_Socket, _Pid) -> ok.
close({http_ws, FsmRef, _IP}) ->
catch p1_fsm:sync_send_all_state_event(FsmRef, close).
2015-02-25 10:42:59 +01:00
reset_stream({http_ws, _FsmRef, _IP} = Socket) ->
Socket.
2019-01-30 09:57:17 +01:00
change_shaper({http_ws, FsmRef, _IP}, Shaper) ->
p1_fsm:send_all_state_event(FsmRef, {new_shaper, Shaper}).
2018-09-17 10:21:02 +02:00
get_transport(_Socket) ->
websocket.
get_owner({http_ws, FsmRef, _IP}) ->
FsmRef.
socket_handoff(LocalPath, Request, Opts) ->
ejabberd_websocket:socket_handoff(LocalPath, Request, Opts, ?MODULE, fun get_human_html_xmlel/0).
2015-02-25 10:42:59 +01:00
%%% Internal
init([{#ws{ip = IP, http_opts = HOpts}, _} = WS]) ->
SOpts = lists:filtermap(fun({stream_management, _}) -> true;
({max_ack_queue, _}) -> true;
({ack_timeout, _}) -> true;
({resume_timeout, _}) -> true;
({max_resume_timeout, _}) -> true;
({resend_on_timeout, _}) -> true;
({access, _}) -> true;
(_) -> false
end, HOpts),
Opts = ejabberd_c2s_config:get_c2s_limits() ++ SOpts,
2019-06-14 11:33:26 +02:00
PingInterval = ejabberd_option:websocket_ping_interval(),
WSTimeout = ejabberd_option:websocket_timeout(),
2015-02-25 10:42:59 +01:00
Socket = {http_ws, self(), IP},
?DEBUG("Client connected through websocket ~p",
[Socket]),
case ejabberd_c2s:start(?MODULE, Socket, [{receiver, self()}|Opts]) of
2018-09-17 10:21:02 +02:00
{ok, C2SPid} ->
ejabberd_c2s:accept(C2SPid),
Timer = erlang:start_timer(WSTimeout, self(), []),
{ok, loop,
#state{socket = Socket, timeout = WSTimeout,
timer = Timer, ws = WS, c2s_pid = C2SPid,
ping_interval = PingInterval}};
{error, Reason} ->
{stop, Reason};
ignore ->
ignore
2015-02-25 10:42:59 +01:00
end.
2018-09-17 10:21:02 +02:00
handle_event({activate, From}, StateName, State) ->
State1 = case State#state.input of
[] -> State#state{active = true};
Input ->
lists:foreach(
fun(I) when is_binary(I)->
From ! {tcp, State#state.socket, I};
(I2) ->
From ! {tcp, State#state.socket, [I2]}
end, Input),
State#state{active = false, input = []}
end,
2019-01-30 09:57:17 +01:00
{next_state, StateName, State1#state{c2s_pid = From}};
handle_event({new_shaper, Shaper}, StateName, #state{ws = {_, WsPid}} = StateData) ->
WsPid ! {new_shaper, Shaper},
{next_state, StateName, StateData}.
2018-09-17 10:21:02 +02:00
2015-02-25 10:42:59 +01:00
handle_sync_event({send_xml, Packet}, _From, StateName,
2024-09-02 12:42:55 +02:00
#state{ws = {_, WsPid}} = StateData) ->
SN2 = case Packet of
{xmlstreamstart, _, Attrs} ->
Attrs2 = [{<<"xmlns">>, <<"urn:ietf:params:xml:ns:xmpp-framing">>} |
lists:keydelete(<<"xmlns">>, 1, lists:keydelete(<<"xmlns:stream">>, 1, Attrs))],
route_el(WsPid, #xmlel{name = <<"open">>, attrs = Attrs2}),
StateName;
{xmlstreamend, _} ->
route_el(WsPid, #xmlel{name = <<"close">>,
attrs = [{<<"xmlns">>, <<"urn:ietf:params:xml:ns:xmpp-framing">>}]}),
stream_end_sent;
{xmlstreamraw, <<"\r\n\r\n">>} ->
% cdata ping
StateName;
{xmlstreamelement, #xmlel{name = Name2} = El2} ->
El3 = case Name2 of
<<"stream:", _/binary>> ->
fxml:replace_tag_attr(<<"xmlns:stream">>, ?NS_STREAM, El2);
_ ->
case fxml:get_tag_attr_s(<<"xmlns">>, El2) of
<<"">> ->
fxml:replace_tag_attr(<<"xmlns">>, <<"jabber:client">>, El2);
_ ->
El2
end
end,
route_el(WsPid, El3),
StateName
end,
{reply, ok, SN2, StateData};
handle_sync_event(close, _From, StateName, #state{ws = {_, WsPid}} = StateData)
2024-09-02 12:42:55 +02:00
when StateName /= stream_end_sent ->
Close = #xmlel{name = <<"close">>,
attrs = [{<<"xmlns">>, <<"urn:ietf:params:xml:ns:xmpp-framing">>}]},
2019-06-14 11:33:26 +02:00
route_text(WsPid, fxml:element_to_binary(Close)),
2024-09-02 12:42:55 +02:00
{stop, normal, StateData};
handle_sync_event(close, _From, _StateName, StateData) ->
2015-02-25 10:42:59 +01:00
{stop, normal, StateData}.
handle_info(closed, _StateName, StateData) ->
{stop, normal, StateData};
handle_info({received, Packet}, StateName, StateDataI) ->
{StateData, Parsed} = parse(StateDataI, Packet),
2018-09-17 10:21:02 +02:00
SD = case StateData#state.active of
2015-02-25 10:42:59 +01:00
false ->
2024-09-02 12:42:55 +02:00
Input = StateData#state.input ++ Parsed,
2015-02-25 10:42:59 +01:00
StateData#state{input = Input};
2018-09-17 10:21:02 +02:00
true ->
StateData#state.c2s_pid ! {tcp, StateData#state.socket, Parsed},
setup_timers(StateData#state{active = false})
2015-02-25 10:42:59 +01:00
end,
{next_state, StateName, SD};
handle_info(PingPong, StateName, StateData) when PingPong == ping orelse
PingPong == pong ->
StateData2 = setup_timers(StateData),
{next_state, StateName,
StateData2#state{pong_expected = false}};
handle_info({timeout, Timer, _}, _StateName,
#state{timer = Timer} = StateData) ->
?DEBUG("Closing websocket connection from hitting inactivity timeout", []),
2015-02-25 10:42:59 +01:00
{stop, normal, StateData};
handle_info({timeout, Timer, _}, StateName,
#state{ping_timer = Timer, ws = {_, WsPid}} = StateData) ->
case StateData#state.pong_expected of
false ->
misc:cancel_timer(StateData#state.ping_timer),
2015-02-25 10:42:59 +01:00
PingTimer = erlang:start_timer(StateData#state.ping_interval,
self(), []),
WsPid ! {ping, <<>>},
{next_state, StateName,
StateData#state{ping_timer = PingTimer, pong_expected = true}};
true ->
?DEBUG("Closing websocket connection from missing pongs", []),
2015-02-25 10:42:59 +01:00
{stop, normal, StateData}
end;
handle_info(_, StateName, StateData) ->
{next_state, StateName, StateData}.
code_change(_OldVsn, StateName, StateData, _Extra) ->
{ok, StateName, StateData}.
terminate(_Reason, _StateName, StateData) ->
2018-09-17 10:21:02 +02:00
StateData#state.c2s_pid ! {tcp_closed, StateData#state.socket}.
2015-02-25 10:42:59 +01:00
setup_timers(StateData) ->
misc:cancel_timer(StateData#state.timer),
2015-02-25 10:42:59 +01:00
Timer = erlang:start_timer(StateData#state.timeout,
self(), []),
misc:cancel_timer(StateData#state.ping_timer),
PingTimer = case StateData#state.ping_interval of
0 -> StateData#state.ping_timer;
V -> erlang:start_timer(V, self(), [])
2015-02-25 10:42:59 +01:00
end,
StateData#state{timer = Timer, ping_timer = PingTimer,
pong_expected = false}.
get_human_html_xmlel() ->
Heading = <<"ejabberd ", (misc:atom_to_binary(?MODULE))/binary>>,
2015-02-25 10:42:59 +01:00
#xmlel{name = <<"html">>,
attrs =
[{<<"xmlns">>, <<"http://www.w3.org/1999/xhtml">>}],
children =
[#xmlel{name = <<"head">>, attrs = [],
children =
[#xmlel{name = <<"title">>, attrs = [],
children = [{xmlcdata, Heading}]}]},
#xmlel{name = <<"body">>, attrs = [],
children =
[#xmlel{name = <<"h1">>, attrs = [],
children = [{xmlcdata, Heading}]},
#xmlel{name = <<"p">>, attrs = [],
children =
[{xmlcdata, <<"An implementation of ">>},
#xmlel{name = <<"a">>,
attrs =
[{<<"href">>,
<<"http://tools.ietf.org/html/rfc6455">>}],
children =
[{xmlcdata,
<<"WebSocket protocol">>}]}]},
#xmlel{name = <<"p">>, attrs = [],
children =
[{xmlcdata,
<<"This web page is only informative. To "
"use WebSocket connection you need a Jabber/XMPP "
"client that supports it.">>}]}]}]}.
parse(State, Data) ->
El = fxml_stream:parse_element(Data),
case El of
#xmlel{name = <<"open">>, attrs = Attrs} ->
Attrs2 = [{<<"xmlns:stream">>, ?NS_STREAM}, {<<"xmlns">>, <<"jabber:client">>} |
lists:keydelete(<<"xmlns">>, 1, lists:keydelete(<<"xmlns:stream">>, 1, Attrs))],
{State, [{xmlstreamstart, <<"stream:stream">>, Attrs2}]};
#xmlel{name = <<"close">>} ->
{State, [{xmlstreamend, <<"stream:stream">>}]};
{error, _} ->
{State, [{xmlstreamerror, {4, <<"not well-formed">>}}]};
_ ->
{State, [El]}
2015-02-25 10:42:59 +01:00
end.
2019-06-14 11:33:26 +02:00
-spec route_text(pid(), binary()) -> ok.
route_text(Pid, Data) ->
Pid ! {text_with_reply, Data, self()},
receive
{text_reply, Pid} ->
ok
end.
2024-09-02 12:42:55 +02:00
-spec route_el(pid(), xmlel() | cdata()) -> ok.
route_el(Pid, Data) ->
route_text(Pid, fxml:element_to_binary(Data)).