mirror of
https://github.com/processone/ejabberd.git
synced 2024-11-24 16:23:40 +01:00
430 lines
14 KiB
Erlang
430 lines
14 KiB
Erlang
%%%-------------------------------------------------------------------
|
|
%%% File : mod_bosh.erl
|
|
%%% Author : Evgeniy Khramtsov <ekhramtsov@process-one.net>
|
|
%%% Purpose : This module acts as a bridge to ejabberd_bosh which implements
|
|
%%% the real stuff, this is to handle the new pluggable architecture
|
|
%%% for extending ejabberd's http service.
|
|
%%% Created : 20 Jul 2011 by Evgeniy Khramtsov <ekhramtsov@process-one.net>
|
|
%%%
|
|
%%%
|
|
%%% ejabberd, Copyright (C) 2002-2024 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.,
|
|
%%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
|
%%%
|
|
%%%-------------------------------------------------------------------
|
|
-module(mod_bosh).
|
|
|
|
-author('steve@zeank.in-berlin.de').
|
|
|
|
%%-define(ejabberd_debug, true).
|
|
|
|
-behaviour(gen_mod).
|
|
|
|
-export([start_link/0]).
|
|
-export([start/2, stop/1, reload/3, process/2, open_session/2,
|
|
close_session/1, find_session/1, clean_cache/1]).
|
|
|
|
-export([depends/2, mod_opt_type/1, mod_options/1, mod_doc/0]).
|
|
|
|
-include("logger.hrl").
|
|
-include_lib("stdlib/include/ms_transform.hrl").
|
|
-include_lib("xmpp/include/xmpp.hrl").
|
|
-include("ejabberd_http.hrl").
|
|
-include("bosh.hrl").
|
|
-include("translate.hrl").
|
|
|
|
-callback init() -> any().
|
|
-callback open_session(binary(), pid()) -> ok | {error, any()}.
|
|
-callback close_session(binary()) -> ok | {error, any()}.
|
|
-callback find_session(binary()) -> {ok, pid()} | {error, any()}.
|
|
-callback use_cache() -> boolean().
|
|
-callback cache_nodes() -> [node()].
|
|
|
|
-optional_callbacks([use_cache/0, cache_nodes/0]).
|
|
|
|
%%%----------------------------------------------------------------------
|
|
%%% API
|
|
%%%----------------------------------------------------------------------
|
|
start_link() ->
|
|
gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
|
|
|
|
process([], #request{method = 'POST', data = <<>>}) ->
|
|
?DEBUG("Bad Request: no data", []),
|
|
{400, ?HEADER(?CT_XML),
|
|
#xmlel{name = <<"h1">>, attrs = [],
|
|
children = [{xmlcdata, <<"400 Bad Request">>}]}};
|
|
process([],
|
|
#request{method = 'POST', data = Data, ip = IP, headers = Hdrs}) ->
|
|
?DEBUG("Incoming data: ~p", [Data]),
|
|
Type = get_type(Hdrs),
|
|
ejabberd_bosh:process_request(Data, IP, Type);
|
|
process([], #request{method = 'GET', data = <<>>}) ->
|
|
{200, ?HEADER(?CT_XML), get_human_html_xmlel()};
|
|
process([], #request{method = 'OPTIONS', data = <<>>}) ->
|
|
{200, ?OPTIONS_HEADER, []};
|
|
process(_Path, _Request) ->
|
|
?DEBUG("Bad Request: ~p", [_Request]),
|
|
{400, ?HEADER(?CT_XML),
|
|
#xmlel{name = <<"h1">>, attrs = [],
|
|
children = [{xmlcdata, <<"400 Bad Request">>}]}}.
|
|
|
|
-spec open_session(binary(), pid()) -> ok | {error, any()}.
|
|
open_session(SID, Pid) ->
|
|
Mod = gen_mod:ram_db_mod(global, ?MODULE),
|
|
case Mod:open_session(SID, Pid) of
|
|
ok ->
|
|
delete_cache(Mod, SID);
|
|
{error, _} = Err ->
|
|
Err
|
|
end.
|
|
|
|
-spec close_session(binary()) -> ok.
|
|
close_session(SID) ->
|
|
Mod = gen_mod:ram_db_mod(global, ?MODULE),
|
|
Mod:close_session(SID),
|
|
delete_cache(Mod, SID).
|
|
|
|
-spec find_session(binary()) -> {ok, pid()} | error.
|
|
find_session(SID) ->
|
|
Mod = gen_mod:ram_db_mod(global, ?MODULE),
|
|
case use_cache(Mod) of
|
|
true ->
|
|
ets_cache:lookup(
|
|
?BOSH_CACHE, SID,
|
|
fun() ->
|
|
case Mod:find_session(SID) of
|
|
{ok, Pid} -> {ok, Pid};
|
|
{error, _} -> error
|
|
end
|
|
end);
|
|
false ->
|
|
case Mod:find_session(SID) of
|
|
{ok, Pid} -> {ok, Pid};
|
|
{error, _} -> error
|
|
end
|
|
end.
|
|
|
|
start(Host, _Opts) ->
|
|
Mod = gen_mod:ram_db_mod(Host, ?MODULE),
|
|
init_cache(Host, Mod),
|
|
Mod:init(),
|
|
clean_cache(),
|
|
TmpSup = gen_mod:get_module_proc(Host, ?MODULE),
|
|
TmpSupSpec = {TmpSup,
|
|
{ejabberd_tmp_sup, start_link, [TmpSup, ejabberd_bosh]},
|
|
permanent, infinity, supervisor, [ejabberd_tmp_sup]},
|
|
supervisor:start_child(ejabberd_gen_mod_sup, TmpSupSpec).
|
|
|
|
stop(Host) ->
|
|
TmpSup = gen_mod:get_module_proc(Host, ?MODULE),
|
|
supervisor:terminate_child(ejabberd_gen_mod_sup, TmpSup),
|
|
supervisor:delete_child(ejabberd_gen_mod_sup, TmpSup).
|
|
|
|
reload(Host, _NewOpts, _OldOpts) ->
|
|
Mod = gen_mod:ram_db_mod(global, ?MODULE),
|
|
init_cache(Host, Mod),
|
|
Mod:init(),
|
|
ok.
|
|
|
|
%%%===================================================================
|
|
%%% Internal functions
|
|
%%%===================================================================
|
|
get_type(Hdrs) ->
|
|
try
|
|
{_, S} = lists:keyfind('Content-Type', 1, Hdrs),
|
|
[T|_] = str:tokens(S, <<";">>),
|
|
[_, <<"json">>] = str:tokens(T, <<"/">>),
|
|
json
|
|
catch _:_ ->
|
|
xml
|
|
end.
|
|
|
|
depends(_Host, _Opts) ->
|
|
[].
|
|
|
|
-ifdef(OTP_BELOW_27).
|
|
mod_opt_type_json() ->
|
|
econf:and_then(
|
|
econf:bool(),
|
|
fun(false) -> false;
|
|
(true) ->
|
|
ejabberd:start_app(jiffy),
|
|
true
|
|
end).
|
|
-else.
|
|
mod_opt_type_json() ->
|
|
econf:bool().
|
|
-endif.
|
|
|
|
mod_opt_type(json) ->
|
|
mod_opt_type_json();
|
|
mod_opt_type(max_concat) ->
|
|
econf:pos_int(unlimited);
|
|
mod_opt_type(max_inactivity) ->
|
|
econf:timeout(second);
|
|
mod_opt_type(max_pause) ->
|
|
econf:timeout(second);
|
|
mod_opt_type(prebind) ->
|
|
econf:bool();
|
|
mod_opt_type(queue_type) ->
|
|
econf:queue_type();
|
|
mod_opt_type(ram_db_type) ->
|
|
econf:db_type(?MODULE);
|
|
mod_opt_type(use_cache) ->
|
|
econf:bool();
|
|
mod_opt_type(cache_size) ->
|
|
econf:pos_int(infinity);
|
|
mod_opt_type(cache_missed) ->
|
|
econf:bool();
|
|
mod_opt_type(cache_life_time) ->
|
|
econf:timeout(second, infinity).
|
|
|
|
-spec mod_options(binary()) -> [{json, boolean()} |
|
|
{atom(), term()}].
|
|
mod_options(Host) ->
|
|
[{json, false},
|
|
{max_concat, unlimited},
|
|
{max_inactivity, timer:seconds(30)},
|
|
{max_pause, timer:seconds(120)},
|
|
{prebind, false},
|
|
{ram_db_type, ejabberd_config:default_ram_db(Host, ?MODULE)},
|
|
{queue_type, ejabberd_option:queue_type(Host)},
|
|
{use_cache, ejabberd_option:use_cache(Host)},
|
|
{cache_size, ejabberd_option:cache_size(Host)},
|
|
{cache_missed, ejabberd_option:cache_missed(Host)},
|
|
{cache_life_time, ejabberd_option:cache_life_time(Host)}].
|
|
|
|
mod_doc() ->
|
|
#{desc =>
|
|
?T("This module implements XMPP over BOSH as defined in "
|
|
"https://xmpp.org/extensions/xep-0124.html[XEP-0124] and "
|
|
"https://xmpp.org/extensions/xep-0206.html[XEP-0206]. BOSH "
|
|
"stands for Bidirectional-streams Over Synchronous HTTP. "
|
|
"It makes it possible to simulate long lived connections "
|
|
"required by XMPP over the HTTP protocol. In practice, "
|
|
"this module makes it possible to use XMPP in a browser without "
|
|
"WebSocket support and more generally to have a way to use "
|
|
"XMPP while having to get through an HTTP proxy."),
|
|
opts =>
|
|
[{json,
|
|
#{value => "true | false",
|
|
desc => ?T("This option has no effect.")}},
|
|
{max_concat,
|
|
#{value => "pos_integer() | infinity",
|
|
desc =>
|
|
?T("This option limits the number of stanzas that the server "
|
|
"will send in a single bosh request. "
|
|
"The default value is 'unlimited'.")}},
|
|
{max_inactivity,
|
|
#{value => "timeout()",
|
|
desc =>
|
|
?T("The option defines the maximum inactivity period. "
|
|
"The default value is '30' seconds.")}},
|
|
{max_pause,
|
|
#{value => "pos_integer()",
|
|
desc =>
|
|
?T("Indicate the maximum length of a temporary session pause "
|
|
"(in seconds) that a client can request. "
|
|
"The default value is '120'.")}},
|
|
{prebind,
|
|
#{value => "true | false",
|
|
desc =>
|
|
?T("If enabled, the client can create the session without "
|
|
"going through authentication. Basically, it creates a "
|
|
"new session with anonymous authentication. "
|
|
"The default value is 'false'.")}},
|
|
{queue_type,
|
|
#{value => "ram | file",
|
|
desc =>
|
|
?T("Same as top-level _`queue_type`_ option, but applied to this module only.")}},
|
|
{ram_db_type,
|
|
#{value => "mnesia | sql | redis",
|
|
desc =>
|
|
?T("Same as top-level _`default_ram_db`_ option, "
|
|
"but applied to this module only.")}},
|
|
{use_cache,
|
|
#{value => "true | false",
|
|
desc =>
|
|
?T("Same as top-level _`use_cache`_ option, but applied to this module only.")}},
|
|
{cache_size,
|
|
#{value => "pos_integer() | infinity",
|
|
desc =>
|
|
?T("Same as top-level _`cache_size`_ option, but applied to this module only.")}},
|
|
{cache_missed,
|
|
#{value => "true | false",
|
|
desc =>
|
|
?T("Same as top-level _`cache_missed`_ option, but applied to this module only.")}},
|
|
{cache_life_time,
|
|
#{value => "timeout()",
|
|
desc =>
|
|
?T("Same as top-level _`cache_life_time`_ option, but applied to this module only.")}}],
|
|
example =>
|
|
["listen:",
|
|
" -",
|
|
" port: 5222",
|
|
" module: ejabberd_c2s",
|
|
" -",
|
|
" port: 5443",
|
|
" module: ejabberd_http",
|
|
" request_handlers:",
|
|
" /bosh: mod_bosh",
|
|
"",
|
|
"modules:",
|
|
" mod_bosh: {}"]}.
|
|
|
|
%%%----------------------------------------------------------------------
|
|
%%% Cache stuff
|
|
%%%----------------------------------------------------------------------
|
|
-spec init_cache(binary(), module()) -> ok.
|
|
init_cache(Host, Mod) ->
|
|
case use_cache(Mod, Host) of
|
|
true ->
|
|
ets_cache:new(?BOSH_CACHE, cache_opts(Host));
|
|
false ->
|
|
ets_cache:delete(?BOSH_CACHE)
|
|
end.
|
|
|
|
-spec use_cache(module()) -> boolean().
|
|
use_cache(Mod) ->
|
|
use_cache(Mod, global).
|
|
|
|
-spec use_cache(module(), global | binary()) -> boolean().
|
|
use_cache(Mod, Host) ->
|
|
case erlang:function_exported(Mod, use_cache, 0) of
|
|
true -> Mod:use_cache();
|
|
false -> mod_bosh_opt:use_cache(Host)
|
|
end.
|
|
|
|
-spec cache_nodes(module()) -> [node()].
|
|
cache_nodes(Mod) ->
|
|
case erlang:function_exported(Mod, cache_nodes, 0) of
|
|
true -> Mod:cache_nodes();
|
|
false -> ejabberd_cluster:get_nodes()
|
|
end.
|
|
|
|
-spec delete_cache(module(), binary()) -> ok.
|
|
delete_cache(Mod, SID) ->
|
|
case use_cache(Mod) of
|
|
true ->
|
|
ets_cache:delete(?BOSH_CACHE, SID, cache_nodes(Mod));
|
|
false ->
|
|
ok
|
|
end.
|
|
|
|
-spec cache_opts(binary()) -> [proplists:property()].
|
|
cache_opts(Host) ->
|
|
MaxSize = mod_bosh_opt:cache_size(Host),
|
|
CacheMissed = mod_bosh_opt:cache_missed(Host),
|
|
LifeTime = mod_bosh_opt:cache_life_time(Host),
|
|
[{max_size, MaxSize}, {cache_missed, CacheMissed}, {life_time, LifeTime}].
|
|
|
|
-spec clean_cache(node()) -> non_neg_integer().
|
|
clean_cache(Node) ->
|
|
ets_cache:filter(
|
|
?BOSH_CACHE,
|
|
fun(_, error) ->
|
|
false;
|
|
(_, {ok, Pid}) ->
|
|
node(Pid) /= Node
|
|
end).
|
|
|
|
-spec clean_cache() -> ok.
|
|
clean_cache() ->
|
|
ejabberd_cluster:eval_everywhere(?MODULE, clean_cache, [node()]).
|
|
|
|
%%%----------------------------------------------------------------------
|
|
%%% Help Web Page
|
|
%%%----------------------------------------------------------------------
|
|
|
|
get_human_html_xmlel() ->
|
|
Heading = <<"ejabberd ",
|
|
(iolist_to_binary(atom_to_list(?MODULE)))/binary>>,
|
|
#xmlel{name = <<"html">>,
|
|
attrs =
|
|
[{<<"xmlns">>, <<"http://www.w3.org/1999/xhtml">>}],
|
|
children =
|
|
[#xmlel{name = <<"head">>,
|
|
children =
|
|
[#xmlel{name = <<"title">>,
|
|
children = [{xmlcdata, Heading}]},
|
|
#xmlel{name = <<"style">>,
|
|
children = [{xmlcdata, get_style_cdata()}]}]},
|
|
#xmlel{name = <<"body">>,
|
|
children =
|
|
[#xmlel{name = <<"div">>,
|
|
attrs = [{<<"class">>, <<"container">>}],
|
|
children = get_container_children(Heading)}]}]}.
|
|
|
|
get_container_children(Heading) ->
|
|
[#xmlel{name = <<"div">>,
|
|
attrs = [{<<"class">>, <<"section">>}],
|
|
children =
|
|
[#xmlel{name = <<"div">>,
|
|
attrs = [{<<"class">>, <<"block">>}],
|
|
children =
|
|
[#xmlel{name = <<"a">>,
|
|
attrs = [{<<"href">>, <<"https://www.ejabberd.im">>}],
|
|
children =
|
|
[#xmlel{name = <<"img">>,
|
|
attrs = [{<<"height">>, <<"32">>},
|
|
{<<"src">>, get_image_src()}]}]}]}]},
|
|
#xmlel{name = <<"div">>,
|
|
attrs = [{<<"class">>, <<"white section">>}],
|
|
children =
|
|
[#xmlel{name = <<"div">>,
|
|
attrs = [{<<"class">>, <<"block">>}],
|
|
children =
|
|
[#xmlel{name = <<"h1">>, children = [{xmlcdata, Heading}]},
|
|
#xmlel{name = <<"p">>, children =
|
|
[{xmlcdata, <<"An implementation of ">>},
|
|
#xmlel{name = <<"a">>,
|
|
attrs = [{<<"href">>, <<"http://xmpp.org/extensions/xep-0206.html">>}],
|
|
children = [{xmlcdata, <<"XMPP over BOSH (XEP-0206)">>}]}]},
|
|
#xmlel{name = <<"p">>, children =
|
|
[{xmlcdata, <<"This web page is only informative. To "
|
|
"use HTTP-Bind you need a Jabber/XMPP "
|
|
"client that supports it.">>}]}]}]},
|
|
#xmlel{name = <<"div">>,
|
|
attrs = [{<<"class">>, <<"section">>}],
|
|
children =
|
|
[#xmlel{name = <<"div">>,
|
|
attrs = [{<<"class">>, <<"block">>}],
|
|
children =
|
|
[#xmlel{name = <<"a">>,
|
|
attrs = [{<<"href">>, <<"https://www.ejabberd.im">>},
|
|
{<<"title">>, <<"ejabberd XMPP server">>}],
|
|
children = [{xmlcdata, <<"ejabberd">>}]},
|
|
{xmlcdata, <<" is maintained by ">>},
|
|
#xmlel{name = <<"a">>,
|
|
attrs = [{<<"href">>, <<"https://www.process-one.net">>},
|
|
{<<"title">>, <<"ProcessOne - Leader in Instant Messaging and Push Solutions">>}],
|
|
children = [{xmlcdata, <<"ProcessOne">>}]} ]}]}
|
|
].
|
|
|
|
get_style_cdata() ->
|
|
case misc:read_css("bosh.css") of
|
|
{ok, Data} -> Data;
|
|
{error, _} -> <<>>
|
|
end.
|
|
|
|
get_image_src() ->
|
|
case misc:read_img("bosh-logo.png") of
|
|
{ok, Img} ->
|
|
B64Img = base64:encode(Img),
|
|
<<"data:image/png;base64,", B64Img/binary>>;
|
|
{error, _} ->
|
|
<<>>
|
|
end.
|