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:
parent
d70ac7f7c5
commit
97e1b419a0
2 changed files with 463 additions and 0 deletions
|
@ -21,6 +21,7 @@
|
|||
|
||||
-export([start_link/0, new/1, update/2, match/3, get_max_rate/1]).
|
||||
-export([reload_from_config/0]).
|
||||
-export([read_shaper_rules/2]).
|
||||
-export([validator/1, shaper_rules_validator/0]).
|
||||
%% gen_server callbacks
|
||||
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
|
||||
|
|
462
src/mod_providers.erl
Normal file
462
src/mod_providers.erl
Normal 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=%%|,%%-:
|
Loading…
Add table
Add a link
Reference in a new issue