%%%------------------------------------------------------------------- %%% File : mod_host_meta.erl %%% Author : Badlop %%% Purpose : Serve host-meta files as described in XEP-0156 %%% Created : 25 March 2022 by Badlop %%% %%% %%% ejabberd, Copyright (C) 2022 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_host_meta). -author('badlop@process-one.net'). -protocol({xep, 156, '1.4.0', '22.05', "complete", ""}). -behaviour(gen_mod). -export([start/2, stop/1, reload/3, process/2, mod_opt_type/1, mod_options/1, depends/2]). -export([mod_doc/0]). -export([get_url/4]). -include("logger.hrl"). -include_lib("xmpp/include/xmpp.hrl"). -include("ejabberd_http.hrl"). -include("ejabberd_web_admin.hrl"). -include("translate.hrl"). %%%---------------------------------------------------------------------- %%% gen_mod callbacks %%%---------------------------------------------------------------------- start(_Host, _Opts) -> report_hostmeta_listener(), ok. stop(_Host) -> ok. reload(_Host, _NewOpts, _OldOpts) -> report_hostmeta_listener(), ok. depends(_Host, _Opts) -> [{mod_bosh, soft}]. %%%---------------------------------------------------------------------- %%% HTTP handlers %%%---------------------------------------------------------------------- process([], #request{method = 'GET', host = Host, path = Path}) -> case lists:last(Path) of <<"host-meta">> -> file_xml(Host); <<"host-meta.json">> -> file_json(Host) end; process(_Path, _Request) -> {404, [], "Not Found"}. %%%---------------------------------------------------------------------- %%% Internal %%%---------------------------------------------------------------------- %% When set to 'auto', it only takes the first valid listener options it finds file_xml(Host) -> BoshList = case get_url(?MODULE, bosh, true, Host) of undefined -> []; BoshUrl -> [?XA(<<"Link">>, [{<<"rel">>, <<"urn:xmpp:alt-connections:xbosh">>}, {<<"href">>, BoshUrl}] )] end, WsList = case get_url(?MODULE, websocket, true, Host) of undefined -> []; WsUrl -> [?XA(<<"Link">>, [{<<"rel">>, <<"urn:xmpp:alt-connections:websocket">>}, {<<"href">>, WsUrl}] )] end, {200, [html, {<<"Content-Type">>, <<"application/xrd+xml">>}, {<<"Access-Control-Allow-Origin">>, <<"*">>}], [<<"\n">>, fxml:element_to_binary( ?XAE(<<"XRD">>, [{<<"xmlns">>,<<"http://docs.oasis-open.org/ns/xri/xrd-1.0">>}], BoshList ++ WsList) )]}. file_json(Host) -> BoshList = case get_url(?MODULE, bosh, true, Host) of undefined -> []; BoshUrl -> [#{rel => <<"urn:xmpp:alt-connections:xbosh">>, href => BoshUrl}] end, WsList = case get_url(?MODULE, websocket, true, Host) of undefined -> []; WsUrl -> [#{rel => <<"urn:xmpp:alt-connections:websocket">>, href => WsUrl}] end, {200, [html, {<<"Content-Type">>, <<"application/json">>}, {<<"Access-Control-Allow-Origin">>, <<"*">>}], [misc:json_encode(#{links => BoshList ++ WsList})]}. get_url(M, bosh, Tls, Host) -> get_url(M, Tls, Host, bosh_service_url, mod_bosh); get_url(M, websocket, Tls, Host) -> get_url(M, Tls, Host, websocket_url, ejabberd_http_ws). get_url(M, Tls, Host, Option, Module) -> case get_url_preliminar(M, Tls, Host, Option, Module) of undefined -> undefined; Url -> misc:expand_keyword(<<"@HOST@">>, Url, Host) end. get_url_preliminar(M, Tls, Host, Option, Module) -> case gen_mod:get_module_opt(Host, M, Option) of undefined -> undefined; auto -> get_auto_url(Tls, Module); <<"auto">> -> get_auto_url(Tls, Module); U when is_binary(U) -> U end. get_auto_url(Tls, Module) -> case find_handler_port_path(Tls, Module) of [] -> undefined; [{ThisTls, Port, Path} | _] -> Protocol = case {ThisTls, Module} of {false, mod_bosh} -> <<"http">>; {true, mod_bosh} -> <<"https">>; {false, ejabberd_http_ws} -> <<"ws">>; {true, ejabberd_http_ws} -> <<"wss">> end, <>))/binary>> end. find_handler_port_path(Tls, Module) -> lists:filtermap( fun({{Port, _, _}, ejabberd_http, #{tls := ThisTls, request_handlers := Handlers}}) when (Tls == any) or (Tls == ThisTls) -> case lists:keyfind(Module, 2, Handlers) of false -> false; {Path, Module} -> {true, {ThisTls, Port, Path}} end; (_) -> false end, ets:tab2list(ejabberd_listener)). report_hostmeta_listener() -> case {find_handler_port_path(false, ?MODULE), find_handler_port_path(true, ?MODULE)} of {[], []} -> ?CRITICAL_MSG("It seems you enabled ~p in 'modules' but forgot to " "add it as a request_handler in an ejabberd_http " "listener.", [?MODULE]); {[_|_], _} -> ?WARNING_MSG("Apparently ~p is enabled in a request_handler in a " "non-encrypted ejabberd_http listener. This is " "disallowed by XEP-0156. Please enable 'tls' in that " "listener, or setup a proxy encryption mechanism.", [?MODULE]); {[], [_|_]} -> ok end. %%%---------------------------------------------------------------------- %%% Options and Doc %%%---------------------------------------------------------------------- mod_opt_type(bosh_service_url) -> econf:either(undefined, econf:binary()); mod_opt_type(websocket_url) -> econf:either(undefined, econf:binary()). mod_options(_) -> [{bosh_service_url, <<"auto">>}, {websocket_url, <<"auto">>}]. mod_doc() -> #{desc => [?T("This module serves small 'host-meta' files as described in " "https://xmpp.org/extensions/xep-0156.html[XEP-0156: Discovering " "Alternative XMPP Connection Methods]."), "", ?T("To use this module, in addition to adding it to the 'modules' " "section, you must also enable it in 'listen' -> 'ejabberd_http' -> " "_`listen-options.md#request_handlers|request_handlers`_."), "", ?T("Notice it only works if ejabberd_http has tls enabled.")], note => "added in 22.05", example => ["listen:", " -", " port: 443", " module: ejabberd_http", " tls: true", " request_handlers:", " /bosh: mod_bosh", " /ws: ejabberd_http_ws", " /.well-known/host-meta: mod_host_meta", " /.well-known/host-meta.json: mod_host_meta", "", "modules:", " mod_bosh: {}", " mod_host_meta:", " bosh_service_url: \"https://@HOST@:5443/bosh\"", " websocket_url: \"wss://@HOST@:5443/ws\""], opts => [{websocket_url, #{value => "undefined | auto | WebSocketURL", desc => ?T("WebSocket URL to announce. " "The keyword '@HOST@' is replaced with the real virtual " "host name. " "If set to 'auto', it will build the URL of the first " "configured WebSocket request handler. " "The default value is 'auto'.")}}, {bosh_service_url, #{value => "undefined | auto | BoshURL", desc => ?T("BOSH service URL to announce. " "The keyword '@HOST@' is replaced with the real " "virtual host name. " "If set to 'auto', it will build the URL of the first " "configured BOSH request handler. " "The default value is 'auto'.")}}] }.