1
0
Fork 0
mirror of https://github.com/processone/ejabberd synced 2025-10-03 09:49:18 +02:00

mod_providers: New module to serve easily XMPP Providers files

This commit is contained in:
Badlop 2025-08-07 11:13:15 +02:00
parent d70ac7f7c5
commit 97e1b419a0
2 changed files with 463 additions and 0 deletions

View file

@ -21,6 +21,7 @@
-export([start_link/0, new/1, update/2, match/3, get_max_rate/1]). -export([start_link/0, new/1, update/2, match/3, get_max_rate/1]).
-export([reload_from_config/0]). -export([reload_from_config/0]).
-export([read_shaper_rules/2]).
-export([validator/1, shaper_rules_validator/0]). -export([validator/1, shaper_rules_validator/0]).
%% gen_server callbacks %% gen_server callbacks
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, -export([init/1, handle_call/3, handle_cast/2, handle_info/2,

462
src/mod_providers.erl Normal file
View file

@ -0,0 +1,462 @@
%%%-------------------------------------------------------------------
%%% File : mod_providers.erl
%%% Author : Badlop <badlop@process-one.net>
%%% Purpose : Serve xmpp-provider-v2.json files as described by XMPP Providers
%%% Created : 7 August 2025 by Badlop <badlop@process-one.net>
%%%
%%%
%%% ejabberd, Copyright (C) 2025 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.
%%%
%%%----------------------------------------------------------------------
%%--------------------------------------------------------------------
%%| Definitions
%% This module is based in mod_host_meta.erl
%% @format-begin
-module(mod_providers).
-author('badlop@process-one.net').
-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]).
-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) ->
[].
%%--------------------------------------------------------------------
%%| HTTP handlers
process([],
#request{method = 'GET',
host = Host,
path = Path}) ->
case lists:last(Path) of
<<"xmpp-provider-v2.json">> ->
file_json(Host)
end;
process(_Path, _Request) ->
{404, [], "Not Found"}.
%%--------------------------------------------------------------------
%%| JSON
file_json(Host) ->
Content =
#{website => build_urls(Host, website),
alternativeJids => gen_mod:get_module_opt(Host, ?MODULE, alternativeJids),
busFactor => gen_mod:get_module_opt(Host, ?MODULE, busFactor),
organization => gen_mod:get_module_opt(Host, ?MODULE, organization),
passwordReset => get_password_url(Host),
serverTesting => gen_mod:get_module_opt(Host, ?MODULE, serverTesting),
maximumHttpFileUploadTotalSize => get_upload_size(Host),
maximumHttpFileUploadStorageTime => get_upload_time(Host),
maximumMessageArchiveManagementStorageTime =>
gen_mod:get_module_opt(Host, ?MODULE, maximumMessageArchiveManagementStorageTime),
professionalHosting => gen_mod:get_module_opt(Host, ?MODULE, professionalHosting),
freeOfCharge => gen_mod:get_module_opt(Host, ?MODULE, freeOfCharge),
legalNotice => build_urls(Host, legalNotice),
serverLocations => gen_mod:get_module_opt(Host, ?MODULE, serverLocations),
since => gen_mod:get_module_opt(Host, ?MODULE, since)},
{200,
[html,
{<<"Content-Type">>, <<"application/json">>},
{<<"Access-Control-Allow-Origin">>, <<"*">>}],
[misc:json_encode(Content)]}.
%%--------------------------------------------------------------------
%%| Upload Size
get_upload_size(Host) ->
case gen_mod:get_module_opt(Host, ?MODULE, maximumHttpFileUploadTotalSize) of
default_value ->
get_upload_size_mhuq(Host);
I when is_integer(I) ->
I
end.
get_upload_size_mhuq(Host) ->
case gen_mod:is_loaded(Host, mod_http_upload_quota) of
true ->
Access = gen_mod:get_module_opt(Host, mod_http_upload_quota, access_hard_quota),
Rules = ejabberd_shaper:read_shaper_rules(Access, Host),
get_upload_size_rules(Rules);
false ->
0
end.
get_upload_size_rules(Rules) ->
case lists:keysearch([{acl, all}], 2, Rules) of
{value, {Size, _}} ->
Size;
false ->
0
end.
%%--------------------------------------------------------------------
%%| Upload Time
get_upload_time(Host) ->
case gen_mod:get_module_opt(Host, ?MODULE, maximumHttpFileUploadStorageTime) of
default_value ->
get_upload_time_mhuq(Host);
I when is_integer(I) ->
I
end.
get_upload_time_mhuq(Host) ->
case gen_mod:is_loaded(Host, mod_http_upload_quota) of
true ->
case gen_mod:get_module_opt(Host, mod_http_upload_quota, max_days) of
infinity ->
0;
I when is_integer(I) ->
I
end;
false ->
0
end.
%%--------------------------------------------------------------------
%%| Password URL
get_password_url(Host) ->
build_urls(Host, get_password_url2(Host)).
get_password_url2(Host) ->
case gen_mod:get_module_opt(Host, ?MODULE, passwordReset) of
default_value ->
get_password_url3(Host);
U when is_binary(U) ->
U
end.
get_password_url3(Host) ->
case find_handler_port_path2(any, mod_register_web) of
[] ->
<<"">>;
[{ThisTls, Port, Path} | _] ->
Protocol =
case ThisTls of
false ->
<<"http">>;
true ->
<<"https">>
end,
<<Protocol/binary,
"://",
Host/binary,
":",
(integer_to_binary(Port))/binary,
"/",
(str:join(Path, <<"/">>))/binary,
"/">>
end.
%% TODO Ya hay otra funciona como esta
find_handler_port_path2(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)).
%%--------------------------------------------------------------------
%%| Build URLs
build_urls(Host, Option) when is_atom(Option) ->
build_urls(Host, gen_mod:get_module_opt(Host, ?MODULE, Option));
build_urls(_Host, <<"">>) ->
#{};
build_urls(Host, Url) when not is_atom(Url) ->
Languages = gen_mod:get_module_opt(Host, ?MODULE, languages),
maps:from_list([{L, misc:expand_keyword(<<"@LANGUAGE_URL@">>, Url, L)}
|| L <- Languages]).
find_handler_port_path(Tls, Module) ->
lists:filtermap(fun ({{Port, _, _},
ejabberd_http,
#{tls := ThisTls, request_handlers := Handlers}})
when is_integer(Port) and ((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. If this is "
"not desired, enable 'tls' in that "
"listener, or setup a proxy encryption mechanism.",
[?MODULE]);
{[], [_ | _]} ->
ok
end.
%%--------------------------------------------------------------------
%%| Options
mod_opt_type(languages) ->
econf:list(
econf:binary());
mod_opt_type(website) ->
econf:binary();
mod_opt_type(alternativeJids) ->
econf:list(
econf:domain(), [unique]);
mod_opt_type(busFactor) ->
econf:int();
mod_opt_type(organization) ->
econf:enum([company,
'commercial person',
'private person',
governmental,
'non-governmental']);
mod_opt_type(passwordReset) ->
econf:binary();
mod_opt_type(serverTesting) ->
econf:bool();
mod_opt_type(maximumHttpFileUploadTotalSize) ->
econf:int();
mod_opt_type(maximumHttpFileUploadStorageTime) ->
econf:int();
mod_opt_type(maximumMessageArchiveManagementStorageTime) ->
econf:int();
mod_opt_type(professionalHosting) ->
econf:bool();
mod_opt_type(freeOfCharge) ->
econf:bool();
mod_opt_type(legalNotice) ->
econf:binary();
mod_opt_type(serverLocations) ->
econf:list(
econf:binary());
mod_opt_type(since) ->
econf:binary().
mod_options(Host) ->
[{languages, [ejabberd_option:language(Host)]},
{website, <<"">>},
{alternativeJids, []},
{busFactor, -1},
{organization, ''},
{passwordReset, default_value},
{serverTesting, false},
{maximumHttpFileUploadTotalSize, default_value},
{maximumHttpFileUploadStorageTime, default_value},
{maximumMessageArchiveManagementStorageTime, 0},
{professionalHosting, false},
{freeOfCharge, false},
{legalNotice, <<"">>},
{serverLocations, []},
{since, <<"">>}].
%%--------------------------------------------------------------------
%%| Doc
mod_doc() ->
#{desc =>
[?T("This module serves JSON provider files API v2 as described by "
"https://providers.xmpp.net/provider-file-generator/[XMPP Providers]."),
"",
?T("It attempts to fill some properties gathering values automatically from your existing ejabberd configuration. Try enabling the module, check what values are displayed, and then customize using the options."),
"",
?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`_. "
"Notice you should set in _`listen.md#ejabberd_http|ejabberd_http`_ "
"the option _`listen-options.md#tls|tls`_ enabled.")],
note => "added in 25.xx",
opts =>
[{languages,
#{value => "[string()]",
desc =>
?T("List of language codes that your pages are available. "
"Some options define URL where the keyword '@LANGUAGE_URL@' "
"will be replaced with each of those language codes. "
"The default value is a list with the language set in the "
"option _`language`_, for example: '[en]'.")}},
{website,
#{value => "string()",
desc =>
?T("Provider website. "
"The keyword '@LANGUAGE_URL@' is replaced with each language. "
"The default value is '\"\"'.")}},
{alternativeJids,
#{value => "[string()]",
desc =>
?T("List of JIDs (XMPP server domains) a provider offers for "
"registration other than its main JID. "
"The default value is '[]'.")}},
{busFactor,
#{value => "integer()",
desc =>
?T("Bus factor of the XMPP service (i.e., the minimum number of "
"team members that the service could not survive losing) or '-1' for n/a. "
"The default value is '-1'.")}},
{organization,
#{value => "string()",
desc =>
?T("Type of organization providing the XMPP service. "
"Allowed values are: 'company', '\"commercial person\"', '\"private person\"', "
"'governmental', '\"non-governmental\"' or '\"\"'. "
"The default value is '\"\"'.")}},
{passwordReset,
#{value => "string()",
desc =>
?T("Password reset web page (per language) used for an automatic password reset "
"(e.g., via email) or describing how to manually reset a password "
"(e.g., by contacting the provider). "
"The keyword '@LANGUAGE_URL@' is replaced with each language. "
"The default value is an URL built automatically "
"if _`mod_register_web`_ is configured as a 'request_handler', "
"or '\"\"' otherwise.")}},
{serverTesting,
#{value => "true | false",
desc =>
?T("Whether tests against the provider's server are allowed "
"(e.g., certificate checks and uptime monitoring). "
"The default value is 'false'.")}},
{maximumHttpFileUploadTotalSize,
#{value => "integer()",
desc =>
?T("Maximum size of all shared files in total per user (number in megabytes (MB), "
"'0' for no limit or '-1' for less than 1 MB). "
"Attention: MB is used instead of MiB (e.g., 104,857,600 bytes = 100 MiB ≈ 104 MB). "
"This property is not about the maximum size of each shared file, "
"which is already retrieved via XMPP. "
"The default value is the value of the shaper value "
"of option 'access_hard_quota' "
"from module _`mod_http_upload_quota`_, or '0' otherwise.")}},
{maximumHttpFileUploadStorageTime,
#{value => "integer()",
desc =>
?T("Maximum storage duration of each shared file "
"(number in days, '0' for no limit or '-1' for less than 1 day). "
"The default value is the same as option 'max_days' "
"from module _`mod_http_upload_quota`_, or '0' otherwise.")}},
{maximumMessageArchiveManagementStorageTime,
#{value => "integer()",
desc =>
?T("Maximum storage duration of each exchanged message "
"(number in days, '0' for no limit or '-1' for less than 1 day). "
"The default value is '0'.")}},
{professionalHosting,
#{value => "true | false",
desc =>
?T("Whether the XMPP server is hosted with good internet connection speed, "
"uninterruptible power supply, access protection and regular backups. "
"The default value is 'false'.")}},
{freeOfCharge,
#{value => "true | false",
desc =>
?T("Whether the XMPP service can be used for free. "
"The default value is 'false'.")}},
{legalNotice,
#{value => "string()",
desc =>
?T("Legal notice web page (per language). "
"The keyword '@LANGUAGE_URL@' is replaced with each language. "
"The default value is '\"\"'.")}},
{serverLocations,
#{value => "[string()]",
desc =>
?T("List of language codes of Server/Backup locations. "
"The default value is an empty list: '[]'.")}},
{since,
#{value => "string()",
desc =>
?T("Date since the XMPP service is available. "
"The default value is an empty string: '\"\"'.")}}],
example =>
["listen:",
" -",
" port: 443",
" module: ejabberd_http",
" tls: true",
" request_handlers:",
" /.well-known/xmpp-provider-v2.json: mod_providers",
"",
"modules:",
" mod_providers:",
" alternativeJids: [\"example1.com\", \"example2.com\"]",
" busFactor: 1",
" freeOfCharge: true",
" languages: [ag, ao, bg, en]",
" legalNotice: \"http://@HOST@/legal/@LANGUAGE_URL@/\"",
" maximumHttpFileUploadStorageTime: 0",
" maximumHttpFileUploadTotalSize: 0",
" maximumMessageArchiveManagementStorageTime: 0",
" organization: \"non-governmental\"",
" passwordReset: \"http://@HOST@/reset/@LANGUAGE_URL@/\"",
" professionalHosting: true",
" serverLocations: [ao, bg]",
" serverTesting: true",
" since: \"2025-12-31\"",
" website: \"http://@HOST@/website/@LANGUAGE_URL@/\""]}.
%%--------------------------------------------------------------------
%%| vim: set foldmethod=marker foldmarker=%%|,%%-: