ejabberd-24.12/ 0000775 0001750 0001750 00000000000 14730775155 013656 5 ustar debalance debalance ejabberd-24.12/rebar.config 0000664 0001750 0001750 00000035072 14730775155 016147 0 ustar debalance debalance %%%----------------------------------------------------------------------
%%%
%%% 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.
%%%
%%%----------------------------------------------------------------------
%%%
%%% Dependencies
%%%
{deps, [{if_not_rebar3,
{if_version_below, "24",
{base64url, "~> 1.0", {git, "https://github.com/dvv/base64url", {tag, "1.0.1"}}}
}},
{cache_tab, "~> 1.0.30", {git, "https://github.com/processone/cache_tab", {tag, "1.0.31"}}},
{eimp, "~> 1.0.22", {git, "https://github.com/processone/eimp", {tag, "1.0.23"}}},
{if_var_true, pam,
{epam, "~> 1.0.14", {git, "https://github.com/processone/epam", {tag, "1.0.14"}}}},
{if_var_true, redis,
{if_not_rebar3,
{eredis, "~> 1.2.0", {git, "https://github.com/wooga/eredis/", {tag, "v1.2.0"}}}
}},
{if_var_true, redis,
{if_rebar3,
{if_version_below, "21",
{eredis, "1.2.0", {git, "https://github.com/wooga/eredis/", {tag, "v1.2.0"}}},
{eredis, "~> 1.7.1", {git, "https://github.com/Nordix/eredis/", {tag, "v1.7.1"}}}
}}},
{if_var_true, sip,
{esip, "~> 1.0.52", {git, "https://github.com/processone/esip", {tag, "1.0.56"}}}},
{if_var_true, zlib,
{ezlib, "~> 1.0.12", {git, "https://github.com/processone/ezlib", {tag, "1.0.13"}}}},
{fast_tls, "~> 1.1.19", {git, "https://github.com/processone/fast_tls", {tag, "1.1.22"}}},
{fast_xml, ".*", {git, "https://github.com/processone/fast_xml", {tag, "1.1.55"}}},
{fast_yaml, "~> 1.0.36", {git, "https://github.com/processone/fast_yaml", {tag, "1.0.37"}}},
{idna, "~> 6.0", {git, "https://github.com/benoitc/erlang-idna", {tag, "6.0.0"}}},
{if_version_below, "27",
{jiffy, "~> 1.1.1", {git, "https://github.com/davisp/jiffy", {tag, "1.1.1"}}}
},
{if_version_above, "23",
{jose, "~> 1.11.10", {git, "https://github.com/potatosalad/erlang-jose", {tag, "1.11.10"}}},
{jose, "1.11.1", {git, "https://github.com/potatosalad/erlang-jose", {tag, "1.11.1"}}}
},
{if_version_below, "22",
{lager, "~> 3.9.1", {git, "https://github.com/erlang-lager/lager", {tag, "3.9.1"}}}
},
{if_var_true, lua,
{if_version_below, "21",
{luerl, "1.0.0", {git, "https://github.com/rvirding/luerl", {tag, "1.0"}}},
{luerl, "~> 1.2.0", {git, "https://github.com/rvirding/luerl", {tag, "1.2"}}}
}},
{mqtree, "~> 1.0.16", {git, "https://github.com/processone/mqtree", {tag, "1.0.17"}}},
{p1_acme, "~> 1.0.23", {git, "https://github.com/processone/p1_acme", {tag, "1.0.25"}}},
{if_var_true, mysql,
{p1_mysql, "~> 1.0.24", {git, "https://github.com/processone/p1_mysql", {tag, "1.0.25"}}}},
{p1_oauth2, "~> 0.6.14", {git, "https://github.com/processone/p1_oauth2", {tag, "0.6.14"}}},
{if_var_true, pgsql,
{p1_pgsql, "~> 1.1.26", {git, "https://github.com/processone/p1_pgsql", {tag, "1.1.29"}}}},
{p1_utils, "~> 1.0.25", {git, "https://github.com/processone/p1_utils", {tag, "1.0.26"}}},
{pkix, "~> 1.0.10", {git, "https://github.com/processone/pkix", {tag, "1.0.10"}}},
{if_var_true, sqlite,
{sqlite3, "~> 1.1.14", {git, "https://github.com/processone/erlang-sqlite3", {tag, "1.1.15"}}}},
{stringprep, "~> 1.0.29", {git, "https://github.com/processone/stringprep", {tag, "1.0.30"}}},
{if_var_true, stun,
{stun, "~> 1.2.12", {git, "https://github.com/processone/stun", {tag, "1.2.15"}}}},
{xmpp, "~> 1.9.1", {git, "https://github.com/processone/xmpp", {tag, "1.9.1"}}},
{yconf, "~> 1.0.17", {git, "https://github.com/processone/yconf", {tag, "1.0.17"}}}
]}.
{gitonly_deps, [ejabberd_po]}.
{if_var_true, latest_deps,
{floating_deps, [cache_tab,
eimp,
epam,
esip,
ezlib,
fast_tls,
fast_xml,
fast_yaml,
mqtree,
p1_acme,
p1_mysql,
p1_oauth2,
p1_pgsql,
p1_utils,
pkix,
sqlite3,
stringprep,
stun,
xmpp,
yconf]}}.
%%%
%%% Compile
%%%
{recursive_cmds, ['configure-deps']}.
{post_hook_configure, [{"eimp", []},
{if_var_true, pam, {"epam", []}},
{if_var_true, sip, {"esip", []}},
{if_var_true, zlib, {"ezlib", []}},
{"fast_tls", []},
{"fast_xml", [{if_var_true, full_xml, "--enable-full-xml"}]},
{"fast_yaml", []},
{"stringprep", []}]}.
{erl_first_files, ["src/ejabberd_sql_pt.erl", "src/ejabberd_config.erl",
"src/gen_mod.erl", "src/mod_muc_room.erl",
"src/mod_push.erl", "src/xmpp_socket.erl"]}.
{erl_opts, [nowarn_deprecated_function,
{i, "include"},
{if_version_above, "20", {d, 'DEPRECATED_GET_STACKTRACE'}},
{if_version_above, "20", {d, 'HAVE_ERL_ERROR'}},
{if_version_above, "20", {d, 'HAVE_URI_STRING'}},
{if_version_below, "21", {d, 'USE_OLD_HTTP_URI'}},
{if_version_below, "22", {d, 'LAGER'}},
{if_version_below, "21", {d, 'NO_CUSTOMIZE_HOSTNAME_CHECK'}},
{if_version_below, "23", {d, 'USE_OLD_CRYPTO_HMAC'}},
{if_version_below, "23", {d, 'USE_OLD_PG2'}},
{if_version_below, "24", {d, 'COMPILER_REPORTS_ONLY_LINES'}},
{if_version_below, "24", {d, 'SYSTOOLS_APP_DEF_WITHOUT_OPTIONAL'}},
{if_version_below, "24", {d, 'OTP_BELOW_24'}},
{if_version_below, "25", {d, 'OTP_BELOW_25'}},
{if_version_below, "26", {d, 'OTP_BELOW_26'}},
{if_version_below, "27", {d, 'OTP_BELOW_27'}},
{if_var_false, debug, no_debug_info},
{if_var_true, debug, debug_info},
{if_var_true, elixir, {d, 'ELIXIR_ENABLED'}},
{if_var_true, new_sql_schema, {d, 'NEW_SQL_SCHEMA'}},
{if_var_true, roster_gateway_workaround, {d, 'ROSTER_GATEWAY_WORKAROUND'}},
{if_var_true, sip, {d, 'SIP'}},
{if_var_true, stun, {d, 'STUN'}},
{if_type_exported, {odbc, {opaque, connection_reference, 0}}, {d, 'ODBC_HAS_TYPES'}},
{src_dirs, [src,
{if_rebar3, sql},
{if_var_true, tools, tools}]}]}.
{if_rebar3, {plugins, [{if_version_below, "21", {rebar3_hex, "7.0.7"}},
{if_version_above, "20", {rebar3_hex, "~> 7.0.8"}},
{provider_asn1, "0.2.0"},
%% Protocol consolidation doesn't work correctly in upstream rebar_mix, see
%% https://github.com/Supersonido/rebar_mix/issues/27#issuecomment-894873335
%% Let's use this fixed rebar_mix fork, see its PR:
%% https://github.com/Supersonido/rebar_mix/pull/31
{if_var_true, elixir, {rebar_mix, ".*",
{git, "https://github.com/bsanyi/rebar_mix.git",
{branch, "consolidation_fix"}}}
}]}}.
{if_rebar3, {project_plugins, [configure_deps,
{if_var_true, tools, rebar3_format},
{if_var_true, tools, rebar3_lint}
]}}.
{if_not_rebar3, {plugins, [
deps_erl_opts, override_deps_versions2, override_opts, configure_deps
]}}.
{if_rebar3, {if_var_true, elixir,
{provider_hooks, [
{post, [{compile, {mix, consolidate_protocols}}]}
]}}}.
%% Compiling Jose 1.11.10 with Erlang/OTP 27.0 throws warnings on public_key deprecated functions
{if_rebar3, {overrides, [{del, jose, [{erl_opts, [warnings_as_errors]}]}]}}.
{sub_dirs, ["rel"]}.
{keep_build_info, true}.
%%%
%%% Test
%%%
{xref_warnings, false}.
{if_rebar3,
{xref_checks,
[deprecated_function_calls, deprecated_functions, locals_not_used,
undefined_function_calls, undefined_functions]}
}.
{if_not_rebar3,
{xref_checks,
[deprecated_function_calls, deprecated_functions,
undefined_function_calls, undefined_functions]}
}.
{xref_exclusions, [
"(\"gen_transport\":_/_)",
"(\"eprof\":_/_)",
{if_var_false, elixir, "(\"Elixir.*\":_/_)"},
{if_var_false, http, "(\"lhttpc\":_/_)"},
{if_var_false, mysql, "(\".*mysql.*\":_/_)"},
{if_var_false, odbc, "(\"odbc\":_/_)"},
{if_var_false, pam, "(\"epam\":_/_)"},
{if_var_false, pgsql, "(\".*pgsql.*\":_/_)"},
{if_var_false, redis, "(\"eredis\":_/_)"},
{if_var_false, sqlite, "(\"sqlite3\":_/_)"},
{if_var_false, zlib, "(\"ezlib\":_/_)"}]}.
{xref_ignores, [{eldap_filter_yecc, return_error, 2},
{http_uri, encode, 1}]}.
{eunit_compile_opts, [{i, "tools"},
{i, "include"}]}.
{dialyzer, [{get_warnings, false}, % Show warnings of dependencies
{if_version_above, "25",
{plt_extra_apps,
[asn1, odbc, public_key, stdlib, syntax_tools,
idna, jose,
cache_tab, eimp, fast_tls, fast_xml, fast_yaml,
mqtree, p1_acme, p1_oauth2, p1_utils, pkix,
stringprep, xmpp, yconf,
{if_version_below, "27", jiffy},
{if_var_true, pam, epam},
{if_var_true, redis, eredis},
{if_var_true, sip, esip},
{if_var_true, zlib, ezlib},
{if_var_true, lua, luerl},
{if_var_true, mysql, p1_mysql},
{if_var_true, pgsql, p1_pgsql},
{if_var_true, stun, stun},
{if_var_true, sqlite, sqlite3}]},
{plt_extra_apps, % For Erlang/OTP 25 and older
[cache_tab, eimp, fast_tls, fast_xml, fast_yaml,
mqtree, p1_acme, p1_oauth2, p1_utils, pkix, stringprep, xmpp, yconf,
{if_var_true, pam, epam},
{if_var_true, redis, eredis},
{if_var_true, sip, esip},
{if_var_true, zlib, ezlib},
{if_var_true, lua, luerl},
{if_var_true, mysql, p1_mysql},
{if_var_true, pgsql, p1_pgsql},
{if_var_true, stun, stun},
{if_var_true, sqlite, sqlite3}]}
} ]}.
{ct_opts, [{keep_logs, 20}]}.
{cover_enabled, true}.
{cover_export_enabled, true}.
{coveralls_coverdata, "_build/test/cover/ct.coverdata"}.
{coveralls_service_name, "github"}.
%%%
%%% OTP Release
%%%
{relx, [{release, {ejabberd, {cmd, "grep {vsn, vars.config | sed 's|{vsn, \"||;s|\"}.||' | tr -d '\012'"}},
[ejabberd]},
{sys_config, "./rel/sys.config"},
{vm_args, "./rel/vm.args"},
{overlay_vars, "vars.config"},
{overlay, [{mkdir, "logs"},
{mkdir, "database"},
{mkdir, "conf"},
{copy, "rel/files/erl", "erts-\{\{erts_vsn\}\}/bin/erl"},
{template, "ejabberdctl.template", "bin/ejabberdctl"},
{copy, "_build/default/lib/ejabberd/ebin/Elixir.*", "lib/ejabberd-{{release_version}}/ebin/"},
{copy, "{{base_dir}}/consolidated/*", "lib/ejabberd-{{release_version}}/ebin/"},
{copy, "rel/overlays/iex", "releases/{{release_version}}/"},
{if_var_true, elixir,
{template, "rel/overlays/elixir", "releases/{{release_version}}/elixir"}
},
{copy, "inetrc", "conf/inetrc"},
{copy, "tools/captcha*.sh", "lib/ejabberd-\{\{release_version\}\}/priv/bin/"},
{copy, "rel/files/install_upgrade.escript", "bin/install_upgrade.escript"}]}
]}.
{profiles, [{prod, [{relx, [{debug_info, strip},
{dev_mode, false},
{include_erts, true},
{include_src, true},
{generate_start_script, false},
{overlay, [{copy, "sql/*", "lib/ejabberd-\{\{release_version\}\}/priv/sql/"},
{copy, "ejabberdctl.cfg.example", "conf/ejabberdctl.cfg"},
{copy, "ejabberd.yml.example", "conf/ejabberd.yml"}]}]}]},
{dev, [{post_hooks, [{release, "rel/setup-dev.sh rebar3"}]},
{deps, [{if_version_above, "20", sync}]},
{relx, [{debug_info, keep},
{dev_mode, true},
{include_erts, true},
{include_src, false},
{generate_start_script, true},
{extended_start_script, true},
{overlay, [{copy, "ejabberdctl.cfg.example", "conf/ejabberdctl.cfg.example"},
{copy, "ejabberd.yml.example", "conf/ejabberd.yml.example"},
{copy, "test/ejabberd_SUITE_data/ca.pem", "conf/"},
{copy, "test/ejabberd_SUITE_data/cert.pem", "conf/"}]}]}]},
{translations, [{deps, [{ejabberd_po, ".*", {git, "https://github.com/processone/ejabberd-po", {branch, "main"}}}]}]},
{test, [{erl_opts, [nowarn_export_all]}]}]}.
{alias, [{relive, [{shell, "--apps ejabberd \
--config rel/relive.config \
--eval sync:go(). \
--script rel/relive.escript \
--name ejabberd@localhost"}]}
]}.
%% Local Variables:
%% mode: erlang
%% End:
%% vim: set filetype=erlang tabstop=8:
ejabberd-24.12/SECURITY.md 0000664 0001750 0001750 00000004070 14730775155 015450 0 ustar debalance debalance # Security Policy
## Supported Versions
We recommend that all users always use the latest version of ejabberd.
To ensure the best experience and security, upgrade to the latest version available on [this repo](https://github.com/processone/ejabberd).
## Reporting a Vulnerability
### Private Reporting
**Preferred Method**: Use GitHub's private vulnerability reporting system by clicking the "Report a Vulnerability" button in the [Security tab of this repository](https://github.com/processone/ejabberd/security). This ensures your report is securely transmitted and tracked.
**Alternative**: If you cannot use the GitHub system, send an email to **`contact@process-one.net`** with the following details:
- A clear description of the vulnerability.
- Steps to reproduce the issue.
- Any potential impact or exploitation scenarios.
### Response Time
We aim to acknowledge receipt of your report within 72 hours. You can expect regular updates on the status of your report.
### Resolution
If the vulnerability is confirmed, we will work on a patch or mitigation strategy.
We will notify you once the issue is resolved and coordinate a public disclosure if needed.
### Acknowledgements
We value and appreciate the contributions of security researchers and community members.
If you wish, we are happy to acknowledge your efforts publicly by listing your name (or alias) below in this document.
Please let us know if you would like to be recognized when reporting the vulnerability.
## Public Discussion
For general inquiries or discussions about the project’s security, feel free to chat with us here:
- XMPP room: `ejabberd@conference.process-one.net`
- [GitHub Discussions](https://github.com/processone/ejabberd/discussions)
However, please note that if the issue is **critical** or potentially exploitable, **do not share it publicly**. Instead, we strongly recommend you contact the maintainers directly via the private reporting methods outlined above to ensure a secure and timely response.
Thank you for helping us improve the security of ejabberd!
ejabberd-24.12/tools/ 0000775 0001750 0001750 00000000000 14730775155 015016 5 ustar debalance debalance ejabberd-24.12/tools/jhbtest.pl 0000775 0001750 0001750 00000027552 14730775155 017034 0 ustar debalance debalance #!/usr/bin/perl -w
use strict; # har har
use constant ERR => 0;
use constant WARN => 1;
use constant INFO => 2;
use constant DEBUG => 3;
use constant RID => 31974;
### ### ### conf ### ### ###
my $BASE_ADDRESS = "http://localhost:5280/http-bind/";
my $JABBER_SERVER = "localhost";
my $RID = RID;
my $WAIT = 60;
my $USER = "tester";
my $UPW = "mysecret";
my $DEBUG = INFO;
### ### ### END conf ### ### ###
# create an agent we can use for our requests
use LWP::UserAgent;
my $ua = new LWP::UserAgent();
# create a tree parser to parse response content
use XML::Parser;
my $p = new XML::Parser(Style => 'Tree');
### ### ### subs ### ### ###
sub doSend() {
my $content = shift;
# create a request
my $req = new HTTP::Request(POST => $BASE_ADDRESS);
$req->content_type('text/xml; charset=utf-8');
$req->content($content);
debug(DEBUG,"<< Request\n".$req->as_string."<< END Request");
# send request
my $res = $ua->request($req);
debug(DEBUG,">> Response\n" . $res->as_string .">> END Response");
return $res;
}
# getChildEls
# used to strip enclosing body element
# PARAMS: @tree - tree style array from XML::Parser
# RETURN: @children - child elements of top level element
sub getChildEls
{
my $t = $_[0];
shift @{$t->[1]};
return @{$t->[1]};
}
sub debug
{
my $lvl = shift;
my $msg = shift;
return if ($DEBUG < $lvl);
my $prefix = "[";
$prefix .= "ERROR" if ($lvl == ERR);
$prefix .= "WARNING" if ($lvl == WARN);
$prefix .= "INFO" if ($lvl == INFO);
$prefix .= "DEBUG" if ($lvl == DEBUG);
$prefix .= "] ";
$msg =~ s/\n/\n$prefix/g;
print STDERR $prefix . $msg . "\n";
}
### ### ### main ### ### ###
$| = 1; # set streaming output
# no body
print "Sending some 'foo': ";
my $res = &doSend("foo");
if ($res->code == 400) {
print "OK.\n";
} else {
print "Failed!\n";
print $res->as_string, "\n";
}
# no body
print "Sending some ' ': ";
$res = &doSend(" ");
if ($res->code == 400) {
print "OK.\n";
} else {
print "Failed!\n";
print $res->as_string, "\n";
}
# empty body
print "Sending empty body: ";
$res = &doSend("
content =~/as_string, "\n";
}
# sending empty 'to' attribute
print "Empty 'to' attribute at session creation request: ";
$res = &doSend("content =~/as_string, "\n";
}
# forget to send a rid
print "Missing 'rid' attribute at session creation request: ";
$res = &doSend("content =~/as_string, "\n";
}
# trying to connect to non-existent jabber server
print "Connecting to non-existent jabber server: ";
$res = &doSend("content =~/content =~/as_string, "\n";
}
# connection to foreign server
#print "Connecting to foreign jabber server: ";
#$res = &doSend("content =~/as_string, "\n";
#}
my %sess;
sub getSess
{
$sess{rid} = RID; # a rid to start
$res = &doSend("$USER ");
my @els = (&getChildEls($p->parse($res->content)));
unless ($els[0] eq 'iq' && $els[1]->[0]->{'type'} eq 'result') {
debug(ERR, $res->content);
return 0;
}
# send auth
$sess{rid}++;
$res = &doSend("$USER test $UPW ");
@els = (&getChildEls($p->parse($res->content)));
unless ($els[0] eq 'iq' && $els[1]->[0]->{'type'} eq 'result') {
debug(ERR, $res->content);
return 0;
}
return 1;
}
print "Authenticating: ";
if (&doAuth()) {
print "OK.\n";
} else {
print "FAILED!\n";
debug(ERR, "Aborting.");
exit(1);
}
sub doPoll
{
$sess{rid}++;
return &doSend(" ");
debug(INFO, $res->content);
# send presence
print "sending presence\n";
$sess{rid}++;
$res = &doSend(" ");
debug(INFO, $res->content);
# sending bullshit
print "sending bullshit\n";
$sess{rid}++;
$res = &doSend("sending bullshit");
debug(INFO, $res->content);
# send presence
print "sending xa presence\n";
$sess{rid}++;
$res = &doSend("xa ");
debug(INFO, $res->content);
# disconnect
sleep 3;
print "logout\n";
$sess{rid}++;
$res = &doSend(" ");
debug(INFO, $res->content);
print "Checking if session terminated: ";
$res = &doPoll();
if ($res->code != 404) {
print "FAILED!\n";
debug(ERR, "Aborting.");
exit(1);
}
print "OK.\n";
ejabberd-24.12/tools/check_xep_versions.sh 0000775 0001750 0001750 00000001312 14730775155 021233 0 ustar debalance debalance #!/bin/bash
check_xep()
{
xep=xep-$1
int=$(echo $1 | sed 's/^0*//')
[ -f $BASE/doc/$xep ] || curl -s -o $BASE/doc/$xep https://xmpp.org/extensions/$xep.html
title=$(sed '//!d;s/.*\(.*\)<\/title>.*/\1/' $BASE/doc/$xep)
vsn=$(grep -A1 Version $BASE/doc/$xep | sed '//!d;q' | sed 's/.*>\(.*\)<.*/\1/')
imp=$(grep "{xep, $int," $BASE/src/* | sed "s/.*src\/\(.*\).erl.*[0-9], '\([0-9.-]*\)'.*/\1 \2/")
[ "$imp" == "" ] && imp="NA 0.0"
echo "$title;$vsn;${imp/ /;}"
}
[ $# -eq 1 ] && BASE="$1" || BASE="$PWD"
[ -d $BASE/doc ] || mkdir $BASE/doc
for x_num in $(grep "{xep" $BASE/src/* | sed "s/,//" | awk '{printf("%04d\n", $2)}' | sort -u)
do
check_xep $x_num
done
ejabberd-24.12/tools/captcha.sh 0000775 0001750 0001750 00000005106 14730775155 016762 0 ustar debalance debalance #!/bin/sh
# This script is an example captcha script.
# It takes the text to recognize in the captcha image as a parameter.
# It return the image binary as a result. ejabberd support PNG, JPEG and GIF.
# The whole idea of the captcha script is to let server admins adapt it to
# their own needs. The goal is to be able to make the captcha generation as
# unique as possible, to make the captcha challenge difficult to bypass by
# a bot.
# Server admins are thus supposed to write and use their own captcha generators.
# This script relies on ImageMagick.
# It is NOT compliant with ImageMagick forks like GraphicsMagick.
INPUT=$1
for n in $(od -A n -t u2 -N 48 /dev/urandom); do RL="$RL$n "; done
get_random ()
{
R=${RL%% *}
RL=${RL#* }
}
get_random
WAVE1_AMPLITUDE=$((2 + R % 5))
get_random
WAVE1_LENGTH=$((50 + R % 25))
get_random
WAVE2_AMPLITUDE=$((2 + R % 5))
get_random
WAVE2_LENGTH=$((50 + R % 25))
get_random
WAVE3_AMPLITUDE=$((2 + R % 5))
get_random
WAVE3_LENGTH=$((50 + R % 25))
get_random
W1_LINE_START_Y=$((10 + R % 40))
get_random
W1_LINE_STOP_Y=$((10 + R % 40))
get_random
W2_LINE_START_Y=$((10 + R % 40))
get_random
W2_LINE_STOP_Y=$((10 + R % 40))
get_random
W3_LINE_START_Y=$((10 + R % 40))
get_random
W3_LINE_STOP_Y=$((10 + R % 40))
get_random
B1_LINE_START_Y=$((R % 40))
get_random
B1_LINE_STOP_Y=$((R % 40))
get_random
B2_LINE_START_Y=$((R % 40))
get_random
B2_LINE_STOP_Y=$((R % 40))
#B3_LINE_START_Y=$((R % 40))
#B3_LINE_STOP_Y=$((R % 40))
get_random
B1_LINE_START_X=$((R % 20))
get_random
B1_LINE_STOP_X=$((100 + R % 40))
get_random
B2_LINE_START_X=$((R % 20))
get_random
B2_LINE_STOP_X=$((100 + R % 40))
#B3_LINE_START_X=$((R % 20))
#B3_LINE_STOP_X=$((100 + R % 40))
get_random
ROLL_X=$((R % 40))
convert -size 180x60 xc:none -pointsize 40 \
\( -clone 0 -fill white \
-stroke black -strokewidth 4 -annotate +0+40 "$INPUT" \
-stroke white -strokewidth 2 -annotate +0+40 "$INPUT" \
-roll +$ROLL_X+0 \
-wave "$WAVE1_AMPLITUDE"x"$WAVE1_LENGTH" \
-roll -$ROLL_X+0 \) \
\( -clone 0 -stroke black \
-strokewidth 1 -draw \
"line $B1_LINE_START_X,$B1_LINE_START_Y $B1_LINE_STOP_X,$B1_LINE_STOP_Y" \
-strokewidth 1 -draw \
"line $B2_LINE_START_X,$B2_LINE_START_Y $B2_LINE_STOP_X,$B2_LINE_STOP_Y" \
-wave "$WAVE2_AMPLITUDE"x"$WAVE2_LENGTH" \) \
\( -clone 0 -stroke white \
-strokewidth 2 -draw "line 0,$W1_LINE_START_Y 140,$W1_LINE_STOP_Y" \
-strokewidth 2 -draw "line 0,$W2_LINE_START_Y 140,$W2_LINE_STOP_Y" \
-strokewidth 2 -draw "line 0,$W3_LINE_START_Y 140,$W3_LINE_STOP_Y" \
-wave "$WAVE3_AMPLITUDE"x"$WAVE3_LENGTH" \) \
-flatten -crop 140x60 +repage -quality 90 -depth 8 png:-
ejabberd-24.12/tools/hook_deps.sh 0000775 0001750 0001750 00000032174 14730775155 017337 0 ustar debalance debalance #!/usr/bin/env escript
%% -*- erlang -*-
-record(state, {run_hooks = #{},
run_fold_hooks = #{},
hooked_funs = {#{}, #{}},
iq_handlers = {#{}, #{}},
exports = #{},
module :: module(),
file :: filename:filename()}).
main(Paths) ->
State =
fold_beams(
fun(File0, Tree, X, Acc0) ->
BareName = filename:rootname(filename:basename(File0)),
Mod = list_to_atom(BareName),
File = BareName ++ ".erl",
Exports = maps:put(Mod, X, Acc0#state.exports),
Acc1 = Acc0#state{file = File, module = Mod, exports = Exports},
erl_syntax_lib:fold(
fun(Form, Acc) ->
case erl_syntax:type(Form) of
application ->
case erl_syntax_lib:analyze_application(Form) of
{ejabberd_hooks, {run, N}} when N == 2; N == 3 ->
collect_run_hook(Form, Acc);
{ejabberd_hooks, {run_fold, N}} when N == 3; N == 4 ->
collect_run_fold_hook(Form, Acc);
{ejabberd_hooks, {add, N}} when N == 4; N == 5 ->
collect_run_fun(Form, add, Acc);
{ejabberd_hooks, {delete, N}} when N == 4; N == 5 ->
collect_run_fun(Form, delete, Acc);
{gen_iq_handler, {add_iq_handler, 5}} ->
collect_iq_handler(Form, add, Acc);
{gen_iq_handler, {remove_iq_handler, 3}} ->
collect_iq_handler(Form, delete, Acc);
_ ->
Acc
end;
_ ->
Acc
end
end, Acc1, Tree)
end, #state{}, Paths),
check_hooks_arity(State#state.run_hooks),
check_hooks_arity(State#state.run_fold_hooks),
check_iq_handlers_export(State#state.iq_handlers, State#state.exports),
analyze_iq_handlers(State#state.iq_handlers),
analyze_hooks(State#state.hooked_funs),
RunDeps = build_deps(State#state.run_hooks, State#state.hooked_funs),
RunFoldDeps = build_deps(State#state.run_fold_hooks, State#state.hooked_funs),
emit_module(RunDeps, RunFoldDeps, hooks_type_test).
collect_run_hook(Form, State) ->
[Hook|Tail] = erl_syntax:application_arguments(Form),
case atom_value(Hook, State) of
undefined ->
State;
HookName ->
Args = case Tail of
[_Host, Args0] -> Args0;
[Args0] ->
Args0
end,
Arity = erl_syntax:list_length(Args),
Hooks = maps:put({HookName, Arity},
{State#state.file, erl_syntax:get_pos(Hook)},
State#state.run_hooks),
State#state{run_hooks = Hooks}
end.
collect_run_fold_hook(Form, State) ->
[Hook|Tail] = erl_syntax:application_arguments(Form),
case atom_value(Hook, State) of
undefined ->
State;
HookName ->
Args = case Tail of
[_Host, _Val, Args0] -> Args0;
[_Val, Args0] -> Args0
end,
Arity = erl_syntax:list_length(Args) + 1,
Hooks = maps:put({HookName, Arity},
{State#state.file, erl_syntax:get_pos(Form)},
State#state.run_fold_hooks),
State#state{run_fold_hooks = Hooks}
end.
collect_run_fun(Form, Action, State) ->
[Hook|Tail] = erl_syntax:application_arguments(Form),
case atom_value(Hook, State) of
undefined ->
State;
HookName ->
{Module, Fun, Seq} = case Tail of
[_Host, M, F, S] ->
{M, F, S};
[M, F, S] ->
{M, F, S}
end,
ModName = module_name(Module, State),
FunName = atom_value(Fun, State),
SeqInt = integer_value(Seq, State),
if ModName /= undefined, FunName /= undefined, SeqInt /= undefined ->
Pos = case Action of
add -> 1;
delete -> 2
end,
Funs = maps_append(
HookName,
{ModName, FunName, SeqInt,
{State#state.file, erl_syntax:get_pos(Form)}},
element(Pos, State#state.hooked_funs)),
Hooked = setelement(Pos, State#state.hooked_funs, Funs),
State#state{hooked_funs = Hooked};
true ->
State
end
end.
collect_iq_handler(Form, add, #state{iq_handlers = {Add, Del}} = State) ->
[Component, _Host, Namespace, Module, Function] = erl_syntax:application_arguments(Form),
Mod = module_name(Module, State),
Fun = atom_value(Function, State),
Comp = atom_value(Component, State),
NS = binary_value(Namespace, State),
if Mod /= undefined, Fun /= undefined, Comp /= undefined, NS /= undefined ->
Handlers = maps_append(
{Comp, NS},
{Mod, Fun,
{State#state.file, erl_syntax:get_pos(Form)}},
Add),
State#state{iq_handlers = {Handlers, Del}};
true ->
State
end;
collect_iq_handler(Form, delete, #state{iq_handlers = {Add, Del}} = State) ->
[Component, _Host, Namespace] = erl_syntax:application_arguments(Form),
Comp = atom_value(Component, State),
NS = binary_value(Namespace, State),
if Comp /= undefined, NS /= undefined ->
Handlers = maps_append(
{Comp, NS},
{State#state.file, erl_syntax:get_pos(Form)},
Del),
State#state{iq_handlers = {Add, Handlers}};
true ->
State
end.
check_hooks_arity(Hooks) ->
maps:fold(
fun({Hook, Arity}, _, M) ->
case maps:is_key(Hook, M) of
true ->
err("Error: hook ~s is called with different "
"number of arguments~n", [Hook]);
false ->
maps:put(Hook, Arity, M)
end
end, #{}, Hooks).
check_iq_handlers_export({HookedFuns, _}, Exports) ->
maps:map(
fun(_, Funs) ->
lists:foreach(
fun({Mod, Fun, {File, FileNo}}) ->
case is_exported(Mod, Fun, 1, Exports) of
true -> ok;
false ->
err("~s:~p: Error: "
"iq handler is registered on unexported function: "
"~s:~s/1~n", [File, FileNo, Mod, Fun])
end
end, Funs)
end, HookedFuns).
analyze_iq_handlers({Add, Del}) ->
maps:map(
fun(Handler, Funs) ->
lists:foreach(
fun({_, _, {File, FileNo}}) ->
case maps:is_key(Handler, Del) of
true -> ok;
false ->
err("~s:~p: Error: "
"iq handler is added but not removed~n",
[File, FileNo])
end
end, Funs)
end, Add),
maps:map(
fun(Handler, Meta) ->
lists:foreach(
fun({File, FileNo}) ->
case maps:is_key(Handler, Add) of
true -> ok;
false ->
err("~s:~p: Error: "
"iq handler is removed but not added~n",
[File, FileNo])
end
end, Meta)
end, Del).
analyze_hooks({Add, Del}) ->
Del1 = maps:fold(
fun(Hook, Funs, D) ->
lists:foldl(
fun({Mod, Fun, Seq, {File, FileNo}}, D1) ->
maps:put({Hook, Mod, Fun, Seq}, {File, FileNo}, D1)
end, D, Funs)
end, #{}, Del),
Add1 = maps:fold(
fun(Hook, Funs, D) ->
lists:foldl(
fun({Mod, Fun, Seq, {File, FileNo}}, D1) ->
maps:put({Hook, Mod, Fun, Seq}, {File, FileNo}, D1)
end, D, Funs)
end, #{}, Add),
lists:foreach(
fun({{Hook, Mod, Fun, _} = Key, {File, FileNo}}) ->
case maps:is_key(Key, Del1) of
true -> ok;
false ->
err("~s:~p: Error: "
"hook ~s->~s->~s is added but was never deleted~n",
[File, FileNo, Hook, Mod, Fun])
end
end, maps:to_list(Add1)),
lists:foreach(
fun({{Hook, Mod, Fun, _} = Key, {File, FileNo}}) ->
case maps:is_key(Key, Add1) of
true -> ok;
false ->
err("~s:~p: Error: "
"hook ~s->~s->~s is deleted but was never added~n",
[File, FileNo, Hook, Mod, Fun])
end
end, maps:to_list(Del1)).
build_deps(Hooks, {HookedFuns, _}) ->
maps:fold(
fun({Hook, Arity}, Meta, Deps) ->
case maps:find(Hook, HookedFuns) of
{ok, Funs} ->
ExportedFuns =
lists:map(
fun({M, F, Seq, FunMeta}) ->
{{M, F, Arity}, Seq, FunMeta}
end, Funs),
maps_append_list({Hook, Arity, Meta}, ExportedFuns, Deps);
error ->
maps_append_list({Hook, Arity, Meta}, [], Deps)
end
end, #{}, Hooks).
module_name(Form, State) ->
try
Name = erl_syntax:macro_name(Form),
'MODULE' = erl_syntax:variable_name(Name),
State#state.module
catch _:_ ->
atom_value(Form, State)
end.
atom_value(Form, State) ->
case erl_syntax:type(Form) of
atom ->
erl_syntax:atom_value(Form);
_ ->
warn_type(Form, State, "not an atom"),
undefined
end.
integer_value(Form, State) ->
case erl_syntax:type(Form) of
integer ->
erl_syntax:integer_value(Form);
_ ->
warn_type(Form, State, "not an integer"),
undefined
end.
binary_value(Form, State) ->
try erl_syntax:concrete(Form) of
Binary when is_binary(Binary) ->
Binary;
_ ->
warn_type(Form, State, "not a binary"),
undefined
catch _:_ ->
warn_type(Form, State, "not a binary"),
undefined
end.
is_exported(Mod, Fun, Arity, Exports) ->
try maps:get(Mod, Exports) of
L -> lists:member({Fun, Arity}, L)
catch _:{badkey, _} -> false
end.
warn_type({var, _, 'Type'}, #state{module = mod_delegation}, "not an atom") ->
ok;
warn_type({var, _, 'NS'}, #state{module = mod_delegation}, "not a binary") ->
ok;
warn_type({var, _, _}, #state{module = gen_mod}, _) ->
ok;
warn_type(Form, State, Warning) ->
log("~s:~p: Warning: " ++ Warning ++ ": ~s~n",
[State#state.file,
erl_syntax:get_pos(Form),
erl_prettypr:format(Form)]).
emit_module(RunDeps, RunFoldDeps, Module) ->
File = filename:join(["src", Module]) ++ ".erl",
try
{ok, Fd} = file:open(File, [write]),
write(Fd,
"%% Generated automatically~n"
"%% DO NOT EDIT: run `make hooks` instead~n~n", []),
write(Fd, "-module(~s).~n", [Module]),
write(Fd, "-compile(nowarn_unused_vars).~n", []),
write(Fd, "-dialyzer(no_return).~n~n", []),
emit_export(Fd, RunDeps, "run hooks"),
emit_export(Fd, RunFoldDeps, "run_fold hooks"),
emit_run_hooks(Fd, RunDeps),
emit_run_fold_hooks(Fd, RunFoldDeps),
file:close(Fd),
log("Module written to ~s~n", [File])
catch _:{badmatch, {error, Reason}} ->
err("Error: writing to ~s failed: ~s", [File, file:format_error(Reason)])
end.
emit_run_hooks(Fd, Deps) ->
DepsList = lists:sort(maps:to_list(Deps)),
lists:foreach(
fun({{Hook, Arity, {File, LineNo}}, Funs}) ->
write(Fd, "%% called at ~s:~p~n", [File, LineNo]),
Args = string:join(
[[N] || N <- lists:sublist(lists:seq($A, $Z), Arity)],
", "),
write(Fd, "~s(~s) ->~n ", [Hook, Args]),
Calls = [io_lib:format("_ = ~s:~s(~s)", [Mod, Fun, Args])
|| {{Mod, Fun, _}, _Seq, _} <- lists:keysort(2, Funs)],
write(Fd, "~s.~n~n",
[string:join(Calls ++ ["ok"], ",\n ")])
end, DepsList).
emit_run_fold_hooks(Fd, Deps) ->
DepsList = lists:sort(maps:to_list(Deps)),
lists:foreach(
fun({{Hook, Arity, {File, LineNo}}, []}) ->
write(Fd, "%% called at ~s:~p~n", [File, LineNo]),
Args = ["Acc"|lists:duplicate(Arity - 1, "_")],
write(Fd, "~s(~s) -> Acc.~n~n", [Hook, string:join(Args, ", ")]);
({{Hook, Arity, {File, LineNo}}, Funs}) ->
write(Fd, "%% called at ~s:~p~n", [File, LineNo]),
Args = [[N] || N <- lists:sublist(lists:seq($A, $Z), Arity - 1)],
write(Fd, "~s(~s) ->~n ", [Hook, string:join(["Acc0"|Args], ", ")]),
{Calls, _} = lists:mapfoldl(
fun({{Mod, Fun, _}, _Seq, _}, N) ->
Args1 = ["Acc" ++ integer_to_list(N)|Args],
{io_lib:format("Acc~p = ~s:~s(~s)",
[N+1, Mod, Fun,
string:join(Args1, ", ")]),
N + 1}
end, 0, lists:keysort(2, Funs)),
write(Fd, "~s,~n", [string:join(Calls, ",\n ")]),
write(Fd, " Acc~p.~n~n", [length(Funs)])
end, DepsList).
emit_export(Fd, Deps, Comment) ->
DepsList = lists:sort(maps:to_list(Deps)),
Exports = lists:map(
fun({{Hook, Arity, _}, _}) ->
io_lib:format("~s/~p", [Hook, Arity])
end, DepsList),
write(Fd, "%% ~s~n-export([~s]).~n~n",
[Comment, string:join(Exports, ",\n ")]).
fold_beams(Fun, State, Paths) ->
Paths1 = fold_paths(Paths),
Total = length(Paths1),
{_, State1} =
lists:foldl(
fun(File, {I, Acc}) ->
io:format("Progress: ~B% (~B/~B)\r",
[round(I*100/Total), I, Total]),
case is_elixir_beam(File) of
true -> {I+1, Acc};
false ->
{AbsCode, Exports} = get_code_from_beam(File),
Acc2 = lists:foldl(
fun(Form, Acc1) ->
Fun(File, Form, Exports, Acc1)
end, Acc, AbsCode),
{I+1, Acc2}
end
end, {0, State}, Paths1),
State1.
fold_paths(Paths) ->
lists:flatmap(
fun(Path) ->
case filelib:is_dir(Path) of
true ->
lists:reverse(
filelib:fold_files(
Path, ".+\.beam\$", false,
fun(File, Acc) ->
[File|Acc]
end, []));
false ->
[Path]
end
end, Paths).
is_elixir_beam(File) ->
case filename:basename(File) of
"Elixir" ++ _ -> true;
_ -> false
end.
get_code_from_beam(File) ->
case beam_lib:chunks(File, [abstract_code, exports]) of
{ok, {_, [{abstract_code, {raw_abstract_v1, Forms}}, {exports, X}]}} ->
{Forms, X};
_ ->
err("No abstract code found in ~s~n", [File])
end.
log(Format, Args) ->
io:format(standard_io, Format, Args).
err(Format, Args) ->
io:format(standard_error, Format, Args),
halt(1).
write(Fd, Format, Args) ->
file:write(Fd, io_lib:format(Format, Args)).
maps_append(K, V, M) ->
maps_append_list(K, [V], M).
maps_append_list(K, L1, M) ->
L2 = maps:get(K, M, []),
maps:put(K, L2 ++ L1, M).
ejabberd-24.12/tools/make-installers 0000775 0001750 0001750 00000021401 14730775155 020035 0 ustar debalance debalance #!/bin/sh
# Build installers for Linux/x64 and Linux/arm64.
#
# Author: Holger Weiss .
#
# Copyright (c) 2022 ProcessOne, SARL.
# All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
set -e
set -u
myself=${0##*/}
architectures='x64 arm64'
iteration=1
usage()
{
echo >&2 "Usage: $myself [-i ]"
exit 2
}
while getopts i: opt
do
case $opt in
i)
iteration="$OPTARG"
;;
\?)
usage
;;
esac
done
shift $((OPTIND - 1))
if ! [ -e 'mix.exs' ] || ! [ -e "tools/$myself" ]
then
echo >&2 "Please call this script from the repository's root directory."
exit 2
elif [ $# -ne 0 ]
then
usage
fi
if type 'makeself' >'/dev/null'
then makeself='makeself'
elif type 'makeself.sh' >'/dev/null'
then makeself='makeself.sh'
else
echo >&2 'This script requires makeself: https://makeself.io'
exit 1
fi
rel_name='ejabberd'
rel_vsn=$(git describe --tags | sed -e 's/-g.*//' -e 's/-/./' | tr -d '[:space:]')
home_url='https://www.ejabberd.im'
doc_url='https://docs.ejabberd.im'
upgrade_url="$doc_url/admin/upgrade/#specific-version-upgrade-notes"
admin_url="$doc_url/admin/install/next-steps/#administration-account"
default_code_dir="/opt/$rel_name-$rel_vsn"
default_data_dir="/opt/$rel_name"
tmp_dir=$(mktemp -d "/tmp/.$rel_name.XXXXXX")
trap 'rm -rf "$tmp_dir"' INT TERM EXIT
umask 022
create_help_file()
{
local file="$1"
cat >"$file" <<-EOF
This is the $rel_name $rel_vsn-$iteration installer for linux-gnu-$arch
Visit:
$home_url
ejabberd documentation site:
$doc_url
EOF
}
create_setup_script()
{
local dir="$1"
cat >"$dir/setup" <<-EOF
#!/bin/sh
set -e
set -u
export PATH='/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin'
user_agrees()
{
local question="\$*"
if [ -t 0 ]
then
read -p "\$question (y/n) [n] " response
case "\$response" in
[Yy]|[Yy][Ee][Ss])
return 0
;;
[Nn]|[Nn][Oo]|'')
return 1
;;
*)
echo 'Please respond with "yes" or "no".'
user_agrees "\$question"
;;
esac
else # Assume 'yes' if not running interactively.
return 0
fi
}
if [ \$(id -u) = 0 ]
then
is_superuser=true
else
is_superuser=false
echo "Running without superuser privileges (installer wasn't invoked"
echo 'with "sudo"), cannot perform system-wide installation this way.'
if ! user_agrees 'Continue anyway?'
then
echo 'Aborting installation.'
exit 1
fi
fi
if [ \$is_superuser = true ]
then
code_dir='$default_code_dir'
data_dir='$default_data_dir'
user_name='$rel_name'
group_name='$rel_name'
elif user_agrees "Install $rel_name below \$HOME/opt?"
then
code_dir="\$HOME/opt/$rel_name-$rel_vsn"
data_dir="\$HOME/opt/$rel_name"
user_name="\$(id -u -n)"
group_name="\$(id -g -n)"
else
read -p 'Installation prefix: ' prefix
if printf '%s' "\$prefix" | grep -q '^/'
then
code_dir="\$prefix/$rel_name-$rel_vsn"
data_dir="\$prefix/$rel_name"
user_name="\$(id -u -n)"
group_name="\$(id -g -n)"
else
echo >&2 'Prefix must be specified as an absolute path.'
echo >&2 'Aborting installation.'
exit 1
fi
fi
prefix="\$(dirname "\$code_dir")"
conf_dir="\$data_dir/conf"
pem_file="\$conf_dir/server.pem"
uninstall_file="\$code_dir/uninstall.txt"
if [ -e '/run/systemd/system' ]
then is_systemd=true
else is_systemd=false
fi
if [ -e "\$data_dir" ]
then is_upgrade=true
else is_upgrade=false
fi
if id -u "\$user_name" >'/dev/null' 2>&1
then user_exists=true
else user_exists=false
fi
echo
echo 'The following installation paths will be used:'
echo "- \$code_dir"
if [ \$is_upgrade = true ]
then echo "- \$data_dir (existing files won't be modified)"
else echo "- \$data_dir (for configuration, database, and log files)"
fi
if [ \$is_superuser = true ]
then
if [ \$is_systemd = true ]
then
echo '- /etc/systemd/system/$rel_name.service'
if [ \$is_upgrade = false ]
then echo 'The $rel_name service is going to be enabled and started.'
fi
fi
if [ \$user_exists = false ]
then echo 'The $rel_name user is going to be created.'
fi
fi
if ! user_agrees 'Install $rel_name $rel_vsn now?'
then
echo 'Aborting installation.'
exit 1
fi
echo
if [ \$user_exists = false ] && [ \$is_superuser = true ]
then useradd -r -d "\$data_dir" "\$user_name"
fi
host=\$(hostname --fqdn 2>'/dev/null' || :)
if [ -z "\$host" ]
then host='localhost'
fi
mkdir -p "\$prefix"
tar -cf - '$rel_name' | tar --skip-old-files -C "\$prefix" -xf -
tar -cf - '$rel_name-$rel_vsn' | tar -C "\$prefix" -xf -
if [ \$is_superuser = true ]
then
if [ \$is_upgrade = false ]
then chown -R -h "\$user_name:\$group_name" "\$data_dir"
fi
chown -R -h "\$(id -u -n):\$group_name" "\$code_dir"
chmod -R g+rX "\$code_dir"
chmod '4750' "\$code_dir/lib/epam-"*'/priv/bin/epam'
else
sed -i "s/^INSTALLUSER=.*/INSTALLUSER=\"\$user_name\"/" \
"\$code_dir/bin/${rel_name}ctl"
sed -i "s/^USER=.*/USER=\$user_name/" \
"\$code_dir/bin/$rel_name.init"
sed -i \
-e "s/^User=.*/User=\$user_name/" \
-e "s/^Group=.*/Group=\$group_name/" \
"\$code_dir/bin/$rel_name.service"
fi
if [ "\$code_dir" != '$default_code_dir' ]
then
sed -i "s|$default_code_dir|\$code_dir|g" \
"\$code_dir/bin/$rel_name.init" \
"\$code_dir/bin/$rel_name.service"
fi
if [ "\$data_dir" != '$default_data_dir' ]
then
sed -i "s|$default_data_dir|\$data_dir|g" \
"\$code_dir/bin/${rel_name}ctl" \
"\$data_dir/conf/$rel_name.yml" \
"\$data_dir/conf/${rel_name}ctl.cfg"
fi
if [ \$is_upgrade = false ]
then
sed -i "s/ - localhost$/ - \$host/" "\$conf_dir/$rel_name.yml"
openssl req -x509 \
-batch \
-nodes \
-newkey rsa:4096 \
-keyout "\$pem_file" \
-out "\$pem_file" \
-days 3650 \
-subj "/CN=\$host" >'/dev/null' 2>&1 || :
if ! [ -e "\$pem_file" ]
then
echo 'Failed to create a TLS certificate for $rel_name.' >&2
elif [ \$is_superuser = true ]
then
chown "\$user_name:\$group_name" "\$pem_file"
fi
fi
case \$is_systemd,\$is_superuser in
true,true)
cp "\$code_dir/bin/$rel_name.service" '/etc/systemd/system/'
systemctl -q daemon-reload
if [ \$is_upgrade = false ]
then systemctl -q --now enable '$rel_name'
fi
;;
true,false)
echo 'You might want to install a systemd unit (see the'
echo "\$code_dir/bin directory for an example)."
;;
false,*)
echo 'You might want to install an init script (see the'
echo "\$code_dir/bin directory for an example)."
;;
esac
echo
echo '$rel_name $rel_vsn has been installed successfully.'
echo
cat >"\$uninstall_file" <<-_EOF
# To uninstall $rel_name, first remove the service. If you're using systemd:
systemctl --now disable $rel_name
rm -f /etc/systemd/system/$rel_name.service
# Remove the binary files:
rm -rf \$code_dir
# If you want to remove your configuration, database and logs:
rm -rf \$data_dir
_EOF
if [ \$is_superuser = true ]
then
cat >>"\$uninstall_file" <<-_EOF
# To remove the user running $rel_name:
userdel \$user_name
_EOF
fi
if [ \$is_upgrade = false ]
then
if [ \$is_systemd = true ] && [ \$is_superuser = true ]
then
echo 'Now you can check $rel_name is running correctly:'
echo ' systemctl status $rel_name'
echo
fi
echo 'Next you may want to edit $rel_name.yml to set up hosts,'
echo 'register an account and grant it admin rigts, see:'
echo
echo '$admin_url'
else
echo 'Please check the following web site for upgrade notes:'
echo
echo '$upgrade_url'
echo
if [ \$is_systemd = true ] && [ \$is_superuser = true ]
then
echo 'If everything looks fine, restart the $rel_name service:'
echo ' systemctl restart $rel_name'
else
echo 'If everything looks fine, restart the $rel_name service.'
fi
fi
EOF
chmod +x "$dir/setup"
}
for arch in $architectures
do
tar_name="$rel_name-$rel_vsn-linux-gnu-$arch.tar.gz"
installer_name="$rel_name-$rel_vsn-$iteration-linux-$arch.run"
test -e "$tar_name" || tools/make-binaries
echo "$myself: Putting together installer for $arch ..."
tar -C "$tmp_dir" -xzpf "$tar_name"
create_help_file "$tmp_dir/help.txt"
create_setup_script "$tmp_dir"
"$makeself" --help-header "$tmp_dir/help.txt" \
"$tmp_dir" "$installer_name" "$rel_name $rel_vsn" './setup'
find "$tmp_dir" -mindepth 1 -delete
done
echo "$myself: Created installers successfully."
ejabberd-24.12/tools/prepare-tr.sh 0000775 0001750 0001750 00000006130 14730775155 017436 0 ustar debalance debalance #!/bin/bash
# Frontend for ejabberd's extract-tr.sh
# How to create template files for a new language:
# NEWLANG=zh
# cp priv/msgs/ejabberd.pot priv/msgs/$NEWLANG.po
# echo \{\"\",\"\"\}. > priv/msgs/$NEWLANG.msg
# make translations
extract_lang_src2pot ()
{
./tools/extract-tr.sh src $DEPS_DIR/xmpp/src > $PO_DIR/ejabberd.pot
}
extract_lang_popot2po ()
{
LANG_CODE=$1
PO_PATH=$PO_DIR/$LANG_CODE.po
POT_PATH=$PO_DIR/$PROJECT.pot
msgmerge $PO_PATH $POT_PATH >$PO_PATH.translate 2>>$LOG
mv $PO_PATH.translate $PO_PATH
}
extract_lang_po2msg ()
{
LANG_CODE=$1
PO_PATH=$LANG_CODE.po
MS_PATH=$PO_PATH.ms
MSGID_PATH=$PO_PATH.msgid
MSGSTR_PATH=$PO_PATH.msgstr
MSGS_PATH=$LANG_CODE.msg
cd $PO_DIR || exit
# Check PO has correct ~
# Let's convert to C format so we can use msgfmt
PO_TEMP=$LANG_CODE.po.temp
cat $PO_PATH | sed 's/%/perc/g' | sed 's/~/%/g' | sed 's/#:.*/#, c-format/g' >$PO_TEMP
msgfmt $PO_TEMP --check-format
result=$?
rm $PO_TEMP
if [ $result -ne 0 ] ; then
exit 1
fi
msgattrib $PO_PATH --translated --no-fuzzy --no-obsolete --no-location --no-wrap | grep "^msg" | tail --lines=+3 >$MS_PATH
grep "^msgid" $PO_PATH.ms | sed 's/^msgid //g' >$MSGID_PATH
grep "^msgstr" $PO_PATH.ms | sed 's/^msgstr //g' >$MSGSTR_PATH
{
echo "%% Generated automatically"
echo "%% DO NOT EDIT: run \`make translations\` instead"
echo "%% To improve translations please read:"
echo "%% https://docs.ejabberd.im/developer/extending-ejabberd/localization/"
echo ""
} >>$MSGS_PATH
paste $MSGID_PATH $MSGSTR_PATH --delimiter=, | awk '{print "{" $0 "}."}' | sort -g >>$MSGS_PATH
rm $MS_PATH
rm $MSGID_PATH
rm $MSGSTR_PATH
mv $MSGS_PATH $MSGS_DIR
}
extract_lang_updateall ()
{
echo ""
echo "Generating POT..."
extract_lang_src2pot
cd $MSGS_DIR || exit
echo ""
echo "File Missing (fuzzy) Language Last translator"
echo "---- ------- ------- -------- ---------------"
for i in *.msg ; do
LANG_CODE=${i%.msg}
printf "%s" "$LANG_CODE" | awk '{printf "%-6s", $1 }'
PO=$PO_DIR/$LANG_CODE.po
extract_lang_popot2po $LANG_CODE
extract_lang_po2msg $LANG_CODE
MISSING=$(msgfmt --statistics $PO 2>&1 | awk '{printf "%5s", $4+$7 }')
printf " %s" "$MISSING"
FUZZY=$(msgfmt --statistics $PO 2>&1 | awk '{printf "%7s", $4 }')
printf " %s" "$FUZZY"
LANGUAGE=$(grep "X-Language:" $PO | sed 's/\"X-Language: //g' | sed 's/\\n\"//g' | awk '{printf "%-12s", $1}')
printf " %s" "$LANGUAGE"
LASTAUTH=$(grep "Last-Translator" $PO | sed 's/\"Last-Translator: //g' | sed 's/\\n\"//g')
echo " $LASTAUTH"
done
echo ""
rm messages.mo
grep -v " done" $LOG
rm $LOG
cd ..
}
EJA_DIR=$(pwd)
PROJECT=ejabberd
DEPS_DIR=$1
MSGS_DIR=$EJA_DIR/priv/msgs
LOG=/tmp/ejabberd-translate-errors.log
PO_DIR=$EJA_DIR/$DEPS_DIR/ejabberd_po/src/
if [ ! -f $EJA_DIR/$DEPS_DIR/ejabberd_po/src/ejabberd.pot ]; then
echo "Couldn't find the required ejabberd_po repository in"
echo " $PO_DIR"
echo "Run: ./configure --enable-tools; ./rebar get-deps"
exit 1
fi
echo "Using PO files from $PO_DIR."
extract_lang_updateall
ejabberd-24.12/tools/opt_types.sh 0000775 0001750 0001750 00000042126 14730775155 017410 0 ustar debalance debalance #!/usr/bin/env escript
%% -*- erlang -*-
-compile([nowarn_unused_function]).
-record(state, {g_opts = #{} :: map(),
m_opts = #{} :: map(),
globals = [] :: [atom()],
defaults = #{} :: map(),
mod_defaults = #{} :: map(),
specs = #{} :: map(),
mod_specs = #{} :: map()}).
main([Mod|Paths]) ->
State = fold_beams(
fun(File, Form, StateAcc) ->
append(Form, File, StateAcc)
end, #state{}, Paths),
emit_modules(map_to_specs(State#state.m_opts,
State#state.mod_defaults,
State#state.mod_specs)),
emit_config(Mod,
map_to_specs(State#state.g_opts,
State#state.defaults,
State#state.specs),
State#state.globals).
emit_config(Mod, Specs, Globals) ->
File = filename:join("src", Mod ++ ".erl"),
case file:open(File, [write]) of
{ok, Fd} ->
emit_header(Fd, Mod, Specs, Globals),
emit_funs(Fd, Mod, Specs, Globals);
{error, Reason} ->
err("Failed to open file ~s for writing: ~s",
[File, file:format_error(Reason)])
end.
emit_modules(Specs) ->
M = lists:foldl(
fun({{Mod, Opt}, Spec}, Acc) ->
Opts = maps:get(Mod, Acc, []),
Opts1 = [{Opt, Spec}|Opts],
maps:put(Mod, Opts1, Acc)
end, #{}, Specs),
maps:fold(
fun(Mod, OptSpecs, _) ->
ModS = atom_to_list(Mod) ++ "_opt",
File = filename:join("src", ModS ++ ".erl"),
case file:open(File, [write]) of
{ok, Fd} ->
OptSpecs1 = lists:reverse(OptSpecs),
emit_header(Fd, ModS, OptSpecs1),
emit_funs(Fd, Mod, OptSpecs1);
{error, Reason} ->
err("Failed to open file ~s for writing: ~s",
[File, file:format_error(Reason)])
end
end, ok, M).
emit_header(Fd, Mod, Specs, Globals) ->
log(Fd, comment(), []),
log(Fd, "-module(~s).~n", [Mod]),
lists:foreach(
fun({{_, Opt}, _}) ->
case lists:member(Opt, Globals) of
true ->
log(Fd, "-export([~s/0]).", [Opt]);
false ->
log(Fd, "-export([~s/0, ~s/1]).", [Opt, Opt])
end
end, Specs),
log(Fd, "", []).
emit_header(Fd, Mod, Specs) ->
log(Fd, comment(), []),
log(Fd, "-module(~s).~n", [Mod]),
lists:foreach(
fun({Opt, _}) ->
log(Fd, "-export([~s/1]).", [Opt])
end, Specs),
log(Fd, "", []).
emit_funs(Fd, _Mod, Specs, Globals) ->
lists:foreach(
fun({{_, Opt}, Type}) ->
SType = t_to_string(Type),
case lists:member(Opt, Globals) of
true ->
log(Fd,
"-spec ~s() -> ~s.~n"
"~s() ->~n"
" ejabberd_config:get_option({~s, global}).~n",
[Opt, SType, Opt, Opt]);
false ->
log(Fd,
"-spec ~s() -> ~s.~n"
"~s() ->~n"
" ~s(global).~n"
"-spec ~s(global | binary()) -> ~s.~n"
"~s(Host) ->~n"
" ejabberd_config:get_option({~s, Host}).~n",
[Opt, SType, Opt, Opt, Opt, SType, Opt, Opt])
end
end, Specs).
emit_funs(Fd, Mod, Specs) ->
lists:foreach(
fun({Opt, Type}) ->
Mod2 = strip_db_type(Mod),
log(Fd,
"-spec ~s(gen_mod:opts() | global | binary()) -> ~s.~n"
"~s(Opts) when is_map(Opts) ->~n"
" gen_mod:get_opt(~s, Opts);~n"
"~s(Host) ->~n"
" gen_mod:get_module_opt(Host, ~s, ~s).~n",
[Opt, t_to_string(Type), Opt, Opt, Opt, Mod2, Opt])
end, Specs).
strip_db_type(mod_vcard_ldap) ->
mod_vcard;
strip_db_type(mod_vcard_mnesia) ->
mod_vcard;
strip_db_type(Mod) ->
Mod.
append({globals, Form}, _File, State) ->
[Clause] = erl_syntax:function_clauses(Form),
Body = lists:last(erl_syntax:clause_body(Clause)),
Gs = lists:map(fun erl_syntax:atom_value/1,
erl_syntax:list_elements(Body)),
Globals = State#state.globals ++ Gs,
State#state{globals = Globals};
append({Index, Form}, File, State) when Index == #state.defaults;
Index == #state.mod_defaults ->
Mod = module(File),
[Clause] = erl_syntax:function_clauses(Form),
Body = lists:last(erl_syntax:clause_body(Clause)),
case erl_syntax:is_proper_list(Body) of
true ->
Opts = lists:foldl(
fun(E, M) ->
try
[E1, E2|_] = erl_syntax:tuple_elements(E),
Name = erl_syntax:atom_value(E1),
Val = erl_syntax:concrete(E2),
maps:put({Mod, Name}, Val, M)
catch _:_ ->
M
end
end, element(Index, State), erl_syntax:list_elements(Body)),
setelement(Index, State, Opts);
false ->
warn("~s: improper list", [format_file(File, Body)]),
State
end;
append({Index, Form}, File, State) when Index == #state.specs;
Index == #state.mod_specs ->
Specs = element(Index, State),
Mod = module(File),
try
{type, _, 'fun', Form1} = Form,
{type, _, list, Form2} = lists:last(Form1),
Tuples = case Form2 of
[{type, _, union, Form3}] -> Form3;
_ -> Form2
end,
Specs1 = lists:foldl(
fun({type, _, tuple, [{atom, _, Atom}, Form5]}, Acc) ->
maps:put({Mod, Atom}, Form5, Acc);
(_, Acc) ->
Acc
end, Specs, Tuples),
setelement(Index, State, Specs1)
catch _:_ ->
warn("~s: unsupported type spec", [format_file(File, Form)]),
State
end;
append({Type, Form}, File, State) when Type == opt_type; Type == mod_opt_type ->
Clauses = erl_syntax:function_clauses(Form),
Mod = module(File),
lists:foldl(
fun(Clause, StateAcc) ->
[Arg] = erl_syntax:clause_patterns(Clause),
Body = lists:last(erl_syntax:clause_body(Clause)),
case erl_syntax:type(Arg) of
atom ->
Name = erl_syntax:atom_value(Arg),
case Type of
opt_type ->
GOpts = StateAcc#state.g_opts,
State#state{
g_opts = append_body({Mod, Name}, Body, GOpts)};
mod_opt_type ->
MOpts = StateAcc#state.m_opts,
State#state{
m_opts = append_body({Mod, Name}, Body, MOpts)}
end;
T ->
warn("~s: unexpected option name: ~s",
[format_file(File, Arg), T]),
StateAcc
end
end, State, Clauses).
append_body(Name, Body, Map) ->
maps:put(Name, Body, Map).
map_to_specs(Map, Defaults, Specs) ->
lists:keysort(
1, maps:fold(
fun({Mod, Opt} = Key, Val, Acc) ->
S1 = type_with_default(Key, Val, Defaults),
S2 = case t_is_any(S1) of
true ->
try maps:get(Key, Specs)
catch _:{badkey, _} ->
warn("Cannot derive type for ~s->~s", [Mod, Opt]),
S1
end;
false ->
S1
end,
[{Key, S2}|Acc]
end, [], Map)).
type_with_default({Mod, _} = Key, Val, Defaults) ->
S = try spec(Mod, Val)
catch throw:unknown -> erl_types:t_any()
end,
case t_is_any(S) of
true ->
S;
false ->
try maps:get(Key, Defaults) of
T ->
erl_types:t_sup(
[S, erl_types:t_from_term(T)])
catch _:{badkey, _} ->
S
end
end.
spec(Mod, Form) ->
case erl_syntax:type(Form) of
application ->
case erl_syntax_lib:analyze_application(Form) of
{M, {Fun, Arity}} when M == econf;
M == yconf ->
Args = erl_syntax:application_arguments(Form),
spec(Fun, Arity, Args, Mod);
_ ->
t_unknown(Mod)
end;
_ ->
t_unknown(Mod)
end.
spec(pos_int, 0, _, _) ->
erl_types:t_pos_integer();
spec(pos_int, 1, [Inf], _) ->
erl_types:t_sup(
erl_types:t_pos_integer(),
erl_types:t_atom(erl_syntax:atom_value(Inf)));
spec(non_neg_int, 0, _, _) ->
erl_types:t_non_neg_integer();
spec(non_neg_int, 1, [Inf], _) ->
erl_types:t_sup(
erl_types:t_non_neg_integer(),
erl_types:t_atom(erl_syntax:atom_value(Inf)));
spec(int, 0, _, _) ->
erl_types:t_integer();
spec(int, 2, [Min, Max], _) ->
erl_types:t_from_range(
erl_syntax:integer_value(Min),
erl_syntax:integer_value(Max));
spec(number, 1, _, _) ->
erl_types:t_number();
spec(octal, 0, _, _) ->
erl_types:t_non_neg_integer();
spec(binary, A, _, _) when A == 0; A == 1; A == 2 ->
erl_types:t_binary();
spec(enum, 1, [L], _) ->
try
Els = erl_syntax:list_elements(L),
Atoms = lists:map(
fun(A) ->
erl_types:t_atom(
erl_syntax:atom_value(A))
end, Els),
erl_types:t_sup(Atoms)
catch _:_ ->
erl_types:t_binary()
end;
spec(bool, 0, _, _) ->
erl_types:t_boolean();
spec(atom, 0, _, _) ->
erl_types:t_atom();
spec(string, A, _, _) when A == 0; A == 1; A == 2 ->
erl_types:t_string();
spec(any, 0, _, Mod) ->
t_unknown(Mod);
spec(url, A, _, _) when A == 0; A == 1 ->
erl_types:t_binary();
spec(file, A, _, _) when A == 0; A == 1 ->
erl_types:t_binary();
spec(directory, A, _, _) when A == 0; A == 1 ->
erl_types:t_binary();
spec(ip, 0, _, _) ->
t_remote(inet, ip_address);
spec(ipv4, 0, _, _) ->
t_remote(inet, ip4_address);
spec(ipv6, 0, _, _) ->
t_remote(inet, ip6_address);
spec(ip_mask, 0, _, _) ->
erl_types:t_sup(
erl_types:t_tuple(
[t_remote(inet, ip4_address), erl_types:t_from_range(0, 32)]),
erl_types:t_tuple(
[t_remote(inet, ip6_address), erl_types:t_from_range(0, 128)]));
spec(port, 0, _, _) ->
erl_types:t_from_range(1, 65535);
spec(re, A, _, _) when A == 0; A == 1 ->
t_remote(misc, re_mp);
spec(glob, A, _, _) when A == 0; A == 1 ->
t_remote(misc, re_mp);
spec(path, 0, _, _) ->
erl_types:t_binary();
spec(binary_sep, 1, _, _) ->
erl_types:t_list(erl_types:t_binary());
spec(beam, A, _, _) when A == 0; A == 1 ->
erl_types:t_module();
spec(timeout, 1, _, _) ->
erl_types:t_pos_integer();
spec(timeout, 2, [_, Inf], _) ->
erl_types:t_sup(
erl_types:t_pos_integer(),
erl_types:t_atom(erl_syntax:atom_value(Inf)));
spec(non_empty, 1, [Form], Mod) ->
S = spec(Mod, Form),
case erl_types:t_is_list(S) of
true ->
erl_types:t_nonempty_list(
erl_types:t_list_elements(S));
false ->
S
end;
spec(unique, 1, [Form], Mod) ->
spec(Mod, Form);
spec(acl, 0, _, _) ->
t_remote(acl, acl);
spec(shaper, 0, _, _) ->
erl_types:t_sup(
[erl_types:t_atom(),
erl_types:t_list(t_remote(ejabberd_shaper, shaper_rule))]);
spec(url_or_file, 0, _, _) ->
erl_types:t_tuple(
[erl_types:t_sup([erl_types:t_atom(file),
erl_types:t_atom(url)]),
erl_types:t_binary()]);
spec(lang, 0, _, _) ->
erl_types:t_binary();
spec(pem, 0, _, _) ->
erl_types:t_binary();
spec(jid, 0, _, _) ->
t_remote(jid, jid);
spec(domain, 0, _, _) ->
erl_types:t_binary();
spec(db_type, 1, _, _) ->
erl_types:t_atom();
spec(queue_type, 0, _, _) ->
erl_types:t_sup([erl_types:t_atom(ram),
erl_types:t_atom(file)]);
spec(ldap_filter, 0, _, _) ->
erl_types:t_binary();
spec(sip_uri, 0, _, _) ->
t_remote(esip, uri);
spec(Fun, A, [Form|_], Mod) when (A == 1 orelse A == 2) andalso
(Fun == list orelse Fun == list_or_single) ->
erl_types:t_list(spec(Mod, Form));
spec(map, A, [F1, F2|OForm], Mod) when A == 2; A == 3 ->
T1 = spec(Mod, F1),
T2 = spec(Mod, F2),
case options_return_type(OForm) of
map ->
erl_types:t_map([], T1, T2);
dict ->
t_remote(dict, dict);
_ ->
erl_types:t_list(erl_types:t_tuple([T1, T2]))
end;
spec(either, 2, [F1, F2], Mod) ->
Spec1 = case erl_syntax:type(F1) of
atom -> erl_types:t_atom(erl_syntax:atom_value(F1));
_ -> spec(Mod, F1)
end,
Spec2 = spec(Mod, F2),
erl_types:t_sup([Spec1, Spec2]);
spec(and_then, 2, [_, F], Mod) ->
spec(Mod, F);
spec(host, 0, _, _) ->
erl_types:t_binary();
spec(hosts, 0, _, _) ->
erl_types:t_list(erl_types:t_binary());
spec(vcard_temp, 0, _, _) ->
erl_types:t_sup([erl_types:t_atom(undefined),
erl_types:t_tuple()]);
spec(options, A, [Form|OForm], Mod) when A == 1; A == 2 ->
case erl_syntax:type(Form) of
map_expr ->
Fs = erl_syntax:map_expr_fields(Form),
Required = options_required(OForm),
{Els, {DefK, DefV}} =
lists:mapfoldl(
fun(F, Acc) ->
Name = erl_syntax:map_field_assoc_name(F),
Val = erl_syntax:map_field_assoc_value(F),
OptType = spec(Mod, Val),
case erl_syntax:atom_value(Name) of
'_' ->
{[], {erl_types:t_atom(), OptType}};
Atom ->
Mand = case lists:member(Atom, Required) of
true -> mandatory;
false -> optional
end,
{[{erl_types:t_atom(Atom), Mand, OptType}], Acc}
end
end, {erl_types:t_none(), erl_types:t_none()}, Fs),
case options_return_type(OForm) of
map ->
erl_types:t_map(lists:keysort(1, lists:flatten(Els)), DefK, DefV);
dict ->
t_remote(dict, dict);
_ ->
erl_types:t_list(
erl_types:t_sup(
[erl_types:t_tuple([DefK, DefV])|
lists:map(
fun({K, _, V}) ->
erl_types:t_tuple([K, V])
end, lists:flatten(Els))]))
end;
_ ->
t_unknown(Mod)
end;
spec(_, _, _, Mod) ->
t_unknown(Mod).
t_from_form(Spec) ->
{T, _} = erl_types:t_from_form(
Spec, sets:new(), {type, {mod, foo, 1}}, dict:new(),
erl_types:var_table__new(), erl_types:cache__new()),
T.
t_remote(Mod, Type) ->
D = maps:from_list([{{opaque, Type, []},
{{Mod, 1, 2, []}, type}}]),
[T] = erl_types:t_opaque_from_records(D),
T.
t_unknown(_Mod) ->
throw(unknown).
t_is_any(T) ->
T == erl_types:t_any().
t_to_string(T) ->
case erl_types:is_erl_type(T) of
true -> erl_types:t_to_string(T);
false -> erl_types:t_form_to_string(T)
end.
options_return_type([]) ->
list;
options_return_type([Form]) ->
Opts = erl_syntax:concrete(Form),
proplists:get_value(return, Opts, list).
options_required([]) ->
[];
options_required([Form]) ->
Opts = erl_syntax:concrete(Form),
proplists:get_value(required, Opts, []).
format_file(Path, Form) ->
Line = case erl_syntax:get_pos(Form) of
{L, _} -> L;
L -> L
end,
filename:rootname(filename:basename(Path)) ++ ".erl:" ++
integer_to_list(Line).
module(Path) ->
list_to_atom(filename:rootname(filename:basename(Path))).
fold_beams(Fun, State, Paths) ->
Paths1 = fold_paths(Paths),
Total = length(Paths1),
{_, State1} =
lists:foldl(
fun(File, {I, Acc}) ->
io:format("Progress: ~B% (~B/~B)\r",
[round(I*100/Total), I, Total]),
case is_elixir_beam(File) of
true -> {I+1, Acc};
false ->
AbsCode = get_code_from_beam(File),
Acc2 = case is_behaviour(AbsCode, ejabberd_config) of
true ->
fold_opt(File, Fun, Acc, AbsCode);
false ->
fold_mod_opt(File, Fun, Acc, AbsCode)
end,
{I+1, Acc2}
end
end, {0, State}, Paths1),
State1.
fold_opt(File, Fun, Acc, AbsCode) ->
lists:foldl(
fun(Form, Acc1) ->
case erl_syntax_lib:analyze_form(Form) of
{function, {opt_type, 1}} ->
Fun(File, {opt_type, Form}, Acc1);
{function, {globals, 0}} ->
Fun(File, {globals, Form}, Acc1);
{function, {options, 0}} ->
Fun(File, {#state.defaults, Form}, Acc1);
{attribute, {spec, {spec, {{options, 0}, Spec}}}} ->
Fun(File, {#state.specs, hd(Spec)}, Acc1);
{attribute, {spec, {{options, 0}, Spec}}} ->
Fun(File, {#state.specs, hd(Spec)}, Acc1);
_ ->
Acc1
end
end, Acc, AbsCode).
fold_mod_opt(File, Fun, Acc, AbsCode) ->
lists:foldl(
fun(Form, Acc1) ->
case erl_syntax_lib:analyze_form(Form) of
{function, {mod_opt_type, 1}} ->
Fun(File, {mod_opt_type, Form}, Acc1);
{function, {mod_options, 1}} ->
Fun(File, {#state.mod_defaults, Form}, Acc1);
{attribute, {spec, {spec, {{mod_options, 1}, Spec}}}} ->
Fun(File, {#state.mod_specs, hd(Spec)}, Acc1);
{attribute, {spec, {{mod_options, 1}, Spec}}} ->
Fun(File, {#state.mod_specs, hd(Spec)}, Acc1);
_ ->
Acc1
end
end, Acc, AbsCode).
fold_paths(Paths) ->
lists:flatmap(
fun(Path) ->
case filelib:is_dir(Path) of
true ->
Beams = lists:reverse(
filelib:fold_files(
Path, ".+\.beam\$", false,
fun(File, Acc) ->
[File|Acc]
end, [])),
case Beams of
[] -> ok;
_ -> code:add_path(Path)
end,
Beams;
false ->
[Path]
end
end, Paths).
is_behaviour(AbsCode, Mod) ->
lists:any(
fun(Form) ->
case erl_syntax_lib:analyze_form(Form) of
{attribute, {Attr, {_, Mod}}}
when Attr == behaviour orelse Attr == behavior ->
true;
{attribute, {behaviour, Mod}} ->
true;
_ ->
false
end
end, AbsCode).
is_elixir_beam(File) ->
case filename:basename(File) of
"Elixir" ++ _ -> true;
_ -> false
end.
get_code_from_beam(File) ->
try
{ok, {_, List}} = beam_lib:chunks(File, [abstract_code]),
{_, {raw_abstract_v1, Forms}} = lists:keyfind(abstract_code, 1, List),
Forms
catch _:{badmatch, _} ->
err("no abstract code found in ~s", [File])
end.
comment() ->
"%% Generated automatically~n"
"%% DO NOT EDIT: run `make options` instead~n".
log(Format, Args) ->
log(standard_io, Format, Args).
log(Fd, Format, Args) ->
case io:format(Fd, Format ++ "~n", Args) of
ok -> ok;
{error, Reason} ->
err("Failed to write to file: ~s", [file:format_error(Reason)])
end.
warn(Format, Args) ->
io:format(standard_error, "Warning: " ++ Format ++ "~n", Args).
err(Format, Args) ->
io:format(standard_error, "Error: " ++ Format ++ "~n", Args),
halt(1).
ejabberd-24.12/tools/make-packages 0000775 0001750 0001750 00000014455 14730775155 017446 0 ustar debalance debalance #!/bin/sh
# Build DEB and RPM packages for Linux/x64 and Linux/arm64.
#
# Author: Holger Weiss .
#
# Copyright (c) 2022 ProcessOne, SARL.
# All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
set -e
set -u
myself=${0##*/}
architectures='x64 arm64'
iteration=1
usage()
{
echo >&2 "Usage: $myself [-i ]"
exit 2
}
while getopts i: opt
do
case $opt in
i)
iteration="$OPTARG"
;;
\?)
usage
;;
esac
done
shift $((OPTIND - 1))
if ! [ -e 'mix.exs' ] || ! [ -e "tools/$myself" ]
then
echo >&2 "Please call this script from the repository's root directory."
exit 2
elif [ $# -ne 0 ]
then
usage
fi
if ! type fpm >'/dev/null'
then
echo >&2 'This script requires fpm: https://fpm.readthedocs.io'
exit 1
fi
rel_name='ejabberd'
rel_vsn=$(git describe --tags | sed -e 's/-g.*//' -e 's/-/./' | tr -d '[:space:]')
conf_dir="/opt/$rel_name/conf"
pem_file="$conf_dir/server.pem"
tmp_dir=$(mktemp -d "/tmp/.$myself.XXXXXX")
trap 'rm -rf "$tmp_dir"' INT TERM EXIT
umask 022
create_scripts()
{
local dir="$1"
cat >"$dir/before-install" <<-EOF
if ! getent group '$rel_name' >'/dev/null'
then groupadd -r '$rel_name'
fi
if ! getent passwd '$rel_name' >'/dev/null'
then useradd -r -m -d '/opt/$rel_name' -g '$rel_name' '$rel_name'
fi
if ! [ -e '$pem_file' ]
then
if ! [ -e '/opt/$rel_name' ] # Huh?
then install -o '$rel_name' -g '$rel_name' -m 750 -d '/opt/$rel_name'
fi
if ! [ -e '$conf_dir' ]
then install -o '$rel_name' -g '$rel_name' -m 750 -d '$conf_dir'
fi
host=\$(hostname --fqdn 2>'/dev/null' || :)
if [ -z "\$host" ]
then host='localhost'
fi
openssl req -x509 \
-batch \
-nodes \
-newkey rsa:4096 \
-keyout '$pem_file' \
-out '$pem_file' \
-days 3650 \
-subj "/CN=\$host" >'/dev/null' 2>&1 || :
if [ -e '$pem_file' ]
then chown '$rel_name:$rel_name' '$pem_file'
else echo 'Failed to create a TLS certificate for ejabberd.' >&2
fi
fi
if ! [ -e '/opt/$rel_name/database' ]
then install -o '$rel_name' -g '$rel_name' -m 750 -d '/opt/$rel_name/database'
fi
if ! [ -e '/opt/$rel_name/logs' ]
then install -o '$rel_name' -g '$rel_name' -m 750 -d '/opt/$rel_name/logs'
fi
EOF
cat >"$dir/after-install" <<-EOF
host=\$(hostname --fqdn 2>'/dev/null' || :)
if [ -n "\$host" ]
then sed -i "s/ - localhost$/ - \$host/" '$conf_dir/$rel_name.yml'
fi
chown 'root:$rel_name' '/opt/$rel_name-$rel_vsn/lib/epam-'*'/priv/bin/epam'
chmod '4750' '/opt/$rel_name-$rel_vsn/lib/epam-'*'/priv/bin/epam'
chown -R -h '$rel_name:$rel_name' '/opt/$rel_name'
chmod 'o-rwx' '/opt/$rel_name/'*
EOF
cat >"$dir/after-upgrade" <<-EOF
chown 'root:$rel_name' '/opt/$rel_name-$rel_vsn/lib/epam-'*'/priv/bin/epam'
chmod '4750' '/opt/$rel_name-$rel_vsn/lib/epam-'*'/priv/bin/epam'
EOF
cat >"$dir/after-remove" <<-EOF
rm -f '/opt/$rel_name/.erlang.cookie'
if getent passwd '$rel_name' >'/dev/null'
then userdel '$rel_name'
fi
if getent group '$rel_name' >'/dev/null'
then groupdel '$rel_name'
fi
EOF
}
package_architecture()
{
local target="$1"
local host_target="$(uname -m)-$target"
case $host_target in
x86_64-x64)
printf 'native'
;;
x86_64-arm64)
printf 'arm64'
;;
*)
echo >&2 "Unsupported host/target combination: $host_target"
exit 1
;;
esac
}
make_package()
{
local output_type="$1"
local architecture="$(package_architecture "$2")"
local work_dir="$3"
local include_dirs="$4"
cd "$work_dir" # FPM's "--chdir" option doesn't work (as I'd expect).
fpm --output-type "$output_type" \
--input-type 'dir' \
--name "$rel_name" \
--version "$rel_vsn" \
--iteration "$iteration" \
--license 'GPL-2+' \
--category 'net' \
--provides 'stun-server' \
--provides 'turn-server' \
--provides 'xmpp-server' \
--no-depends \
--no-auto-depends \
--deb-maintainerscripts-force-errorchecks \
--deb-systemd-enable \
--deb-systemd-auto-start \
--deb-systemd "./$rel_name.service" \
--deb-init "./$rel_name" \
--rpm-init "./$rel_name" \
--config-files "$conf_dir" \
--directories "/opt/$rel_name" \
--directories "/opt/$rel_name-$rel_vsn" \
--architecture "$architecture" \
--maintainer 'ejabberd Maintainers ' \
--vendor 'ProcessOne, SARL' \
--description 'Robust and scalable XMPP/MQTT/SIP server.' \
--url 'https://ejabberd.im' \
--before-install './before-install' \
--after-install './after-install' \
--before-upgrade './before-install' \
--after-upgrade './after-upgrade' \
--after-remove './after-remove' \
$include_dirs
cd "$OLDPWD"
}
for arch in $architectures
do
tar_name="$rel_name-$rel_vsn-linux-gnu-$arch.tar.gz"
arch_dir="$tmp_dir/$arch"
opt_dir="$arch_dir/opt"
etc_dir="$arch_dir/etc"
bin_dir="$arch_dir/usr/sbin"
dst_dir="$opt_dir/$rel_name-$rel_vsn"
test -e "$tar_name" || tools/make-binaries
echo "$myself: Putting together DEB and RPM packages for $arch ..."
mkdir -p "$opt_dir" "$bin_dir"
tar -C "$opt_dir" -xzf "$tar_name"
cat >"$bin_dir/${rel_name}ctl" <<-EOF
#!/bin/sh
exec '/opt/$rel_name-$rel_vsn/bin/${rel_name}ctl' "\$@"
EOF
chmod +x "$bin_dir/${rel_name}ctl"
mkdir -p "$etc_dir/systemd/system"
mv "$dst_dir/bin/$rel_name.service" "$etc_dir/systemd/system"
mv "$dst_dir/bin/$rel_name.init" "$arch_dir/$rel_name"
sed -i \
"s|opt/$rel_name-$rel_vsn/bin/${rel_name}ctl|usr/sbin/${rel_name}ctl|g" \
"$etc_dir/systemd/system/$rel_name.service" "$arch_dir/$rel_name"
create_scripts "$arch_dir"
make_package 'rpm' "$arch" "$arch_dir" './opt ./usr ./etc'
mv "$etc_dir/systemd/system/$rel_name.service" "$arch_dir"
rm -r "$etc_dir"
make_package 'deb' "$arch" "$arch_dir" './opt ./usr'
mv "$arch_dir/$rel_name"?$rel_vsn*.??? .
done
echo "$myself: Created DEB and RPM packages successfully."
ejabberd-24.12/tools/rebar3-format.sh 0000775 0001750 0001750 00000001461 14730775155 020023 0 ustar debalance debalance #!/bin/bash
# To start formatting a file, add a line that contains:
# @format-begin
# Formatting in that file can later be disabled adding another line with:
# @format-end
#
# It can be reenabled again later in the file.
#
# Finally, call: make format
REBAR=$1
FORMAT()
{
FPATH=$1
ERLS=$(git grep --name-only @format-begin "$FPATH"/)
for ERL in $ERLS; do
csplit --quiet --prefix=$ERL-format- $ERL /@format-/ "{*}"
done
EFMTS=$(find "$FPATH"/*-format-* -type f -exec grep --files-with-matches "@format-begin" '{}' ';')
EFMTS2=""
for EFMT in $EFMTS; do
EFMTS2="$EFMTS2 --files $EFMT"
done
$REBAR format $EFMTS2
for ERL in $ERLS; do
SPLITS=$(find $ERL-format-* -type f)
rm $ERL
for SPLIT in $SPLITS; do
cat $SPLIT >> $ERL
rm $SPLIT
done
done
}
FORMAT src
FORMAT test
ejabberd-24.12/tools/generate-doap.sh 0000775 0001750 0001750 00000012062 14730775155 020071 0 ustar debalance debalance #!/bin/bash
# Erlang modules in ejabberd use a custom module attribute [1]
# named -protocol to define what XEPs and RFCs that module implements.
# General protocols are defined in ejabberd.erl
#
# The supported syntax is:
# -protocol({rfc, RFC-NUMBER}).
# -protocol({xep, XEP-NUMBER, XEP-VERSION}).
# -protocol({xep, XEP-NUMBER, XEP-VERSION, EJABBERD-VERSION, STATUS, COMMENTS}).
# Where
# RFC-NUMBER, XEP-NUMBER :: integer()
# XEP-VERSION, EJABBERD-VERSION :: atom()
# STATUS, COMMENTS :: string()
# For example:
# -protocol({rfc, 5766}).
# -protocol({xep, 111, '0.2'}).
# -protocol({xep, 222, '1.2.0', '17.09', "", ""}).
# -protocol({xep, 333, '1.11.2', '21.09', "complete", ""}).
# -protocol({xep, 333, '0.2.0', '21.09', "partial", "Only client X is supported"}).
#
# [1] https://www.erlang.org/doc/reference_manual/modules.html#module-attributes
write_doap_head()
{
cat >"$1" <<-'EOF'
ejabberd
XMPP Server with MQTT Broker and SIP Service
Robust, Ubiquitous and Massively Scalable Messaging Platform (XMPP Server, MQTT Broker, SIP Service)
2002-11-16
BSD
Linux
macOS
Windows
Erlang
C
EOF
}
write_doap_tail()
{
cat >>"$1" <<-'EOF'
EOF
}
write_rfcs()
{
rfc=rfc$1
out=$2
int=$(echo $1 | sed 's/^0*//')
imp=$(grep "\-protocol({rfc, $int," $BASE/src/* | sed "s/.*src\/\(.*\).erl.*'\([0-9.-x]*\)'.*/\1 \2/")
[ "$imp" = "" ] && imp="NA 0.0"
echo " " >>$out
}
write_xeps()
{
xep=xep-$1
out=$2
comments2=""
int=$(echo $1 | sed 's/^0*//')
imp=$(grep "\-protocol({xep, $int," $BASE/src/* | sed "s/.*src\/\(.*\).erl.*'\([0-9.-x]*\)'.*/\1 \2/")
[ "$imp" = "" ] && imp="NA 0.0"
sourcefiles=$(grep "\-protocol({xep, $int," $BASE/src/* | sed "s/.*src\/\(.*\).erl.*'\([0-9.-x]*\)'.*/\1/" | tr '\012' ',' | sed 's|,$||' | sed 's|,|, |g' | sed 's|^ejabberd$||')
versionsold=$(grep "\-protocol({xep, $int, .*'})\." $BASE/src/* | sed "s/.*'\([0-9.-x]*\)'.*/\1/" | head -1)
versionsnew=$(grep "\-protocol({xep, $int, .*\"})\." $BASE/src/* | sed "s/.*'\([0-9.-x]*\)', '.*/\1/" | head -1)
versions="$versionsold$versionsnew"
since=$(grep "\-protocol({xep, $int, .*\"})\." $BASE/src/* | sed "s/.*', '\([0-9.-x]*\)',.*/\1/" | head -1)
status=$(grep "\-protocol({xep, $int, .*\"})\." $BASE/src/* | sed "s/.*', \"\([a-z]*\)\", \".*/\1/" | head -1)
comments=$(grep "\-protocol({xep, $int, .*\"})\." $BASE/src/* | sed "s/.*\", \"\(.*\)\"}.*/\1/" | head -1)
[ -n "$comments" ] && comments2=", $comments"
note="$sourcefiles$comments2"
{
echo " "
echo " "
echo " "
echo " $versions "
echo " $since "
echo " $status "
echo " $note "
echo " "
echo " "
} >>$out
}
[ $# -eq 1 ] && BASE="$1" || BASE="$PWD"
[ -d $BASE/doc ] || mkdir $BASE/doc
temp=tools/ejabberd.temp
final=ejabberd.doap
write_doap_head $final
grep "\-protocol({rfc" $BASE/src/* | sed "s/,//" | awk '{printf("%04d\n", $2)}' | sort -u | while IFS= read -r x_num
do
write_rfcs $x_num $temp
done
echo "" >>$temp
grep "\-protocol({xep" $BASE/src/* | sed "s/,//" | awk '{printf("%04d\n", $2)}' | sort -u | while IFS= read -r x_num
do
write_xeps $x_num $temp
done
cat $temp >>$final
rm $temp
write_doap_tail $final
ejabberd-24.12/tools/extract-tr.sh 0000775 0001750 0001750 00000007772 14730775155 017467 0 ustar debalance debalance #!/usr/bin/env escript
%% -*- erlang -*-
main(Paths) ->
Dict = fold_erls(
fun(File, Tokens, Acc) ->
extract_tr(File, Tokens, Acc)
end, dict:new(), Paths),
generate_pot(Dict).
extract_tr(File, [{'?', _}, {var, _, 'T'}, {'(', Line}|Tokens], Acc) ->
case extract_string(Tokens, "") of
{"", Tokens1} ->
err("~s:~B: Warning: invalid string", [File, Line]),
extract_tr(File, Tokens1, Acc);
{String, Tokens1} ->
extract_tr(File, Tokens1, dict:append(String, {File, Line}, Acc))
end;
extract_tr(_File, [{atom,_,module}, {'(',_}, {atom,_,ejabberd_doc} | _Tokens], Acc) ->
Acc;
extract_tr(File, [{atom, _, F}, {'(',_} | Tokens], Acc)
when (F == mod_doc); (F == doc) ->
Tokens2 = consume_tokens_until_dot(Tokens),
extract_tr(File, Tokens2, Acc);
extract_tr(File, [_|Tokens], Acc) ->
%%err("~p~n", [A]),
extract_tr(File, Tokens, Acc);
extract_tr(_, [], Acc) ->
Acc.
consume_tokens_until_dot([{dot, _} | Tokens]) ->
Tokens;
consume_tokens_until_dot([_ | Tokens]) ->
consume_tokens_until_dot(Tokens).
extract_string([{string, _, S}|Tokens], Acc) ->
extract_string(Tokens, [S|Acc]);
extract_string([{')', _}|Tokens], Acc) ->
{lists:flatten(lists:reverse(Acc)), Tokens};
extract_string(Tokens, _) ->
{"", Tokens}.
fold_erls(Fun, State, Paths) ->
Paths1 = fold_paths(Paths),
Total = length(Paths1),
{_, State1} =
lists:foldl(
fun(File, {I, Acc}) ->
io:format(standard_error,
"Progress: ~B% (~B/~B)\r",
[round(I*100/Total), I, Total]),
case tokens(File) of
{ok, Tokens} ->
{I+1, Fun(File, Tokens, Acc)};
error ->
{I+1, Acc}
end
end, {0, State}, Paths1),
State1.
fold_paths(Paths) ->
lists:flatmap(
fun(Path) ->
case filelib:is_dir(Path) of
true ->
lists:reverse(
filelib:fold_files(
Path, ".+\.erl\$", false,
fun(File, Acc) ->
[File|Acc]
end, []));
false ->
[Path]
end
end, Paths).
tokens(File) ->
case file:read_file(File) of
{ok, Data} ->
case erl_scan:string(binary_to_list(Data)) of
{ok, Tokens, _} ->
{ok, Tokens};
{error, {_, Module, Desc}, Line} ->
err("~s:~n: Warning: scan error: ~s",
[filename:basename(File), Line, Module:format_error(Desc)]),
error
end;
{error, Why} ->
err("Warning: failed to read file ~s: ~s",
[File, file:format_error(Why)]),
error
end.
generate_pot(Dict) ->
io:format("~s~n~n", [pot_header()]),
lists:foreach(
fun({Msg, Location}) ->
S1 = format_location(Location),
S2 = format_msg(Msg),
io:format("~smsgstr \"\"~n~n", [S1 ++ S2])
end, lists:keysort(1, dict:to_list(Dict))).
format_location([A, B, C|T]) ->
format_location_list([A,B,C]) ++ format_location(T);
format_location([A, B|T]) ->
format_location_list([A,B]) ++ format_location(T);
format_location([A|T]) ->
format_location_list([A]) ++ format_location(T);
format_location([]) ->
"".
format_location_list(L) ->
"#: " ++ string:join(
lists:map(
fun({File, Pos}) ->
io_lib:format("~s:~B", [File, Pos])
end, L),
" ") ++ io_lib:nl().
format_msg(Bin) ->
io_lib:format("msgid \"~s\"~n", [escape(Bin)]).
escape(Bin) ->
lists:map(
fun($") -> "\\\"";
(C) -> C
end, binary_to_list(iolist_to_binary(Bin))).
pot_header() ->
string:join(
["msgid \"\"",
"msgstr \"\"",
"\"Project-Id-Version: 15.11.127\\n\"",
"\"X-Language: Language Name\\n\"",
"\"Last-Translator: Translator name and contact method\\n\"",
"\"MIME-Version: 1.0\\n\"",
"\"Content-Type: text/plain; charset=UTF-8\\n\"",
"\"Content-Transfer-Encoding: 8bit\\n\"",
"\"X-Poedit-Basepath: ../..\\n\"",
"\"X-Poedit-SearchPath-0: .\\n\""],
io_lib:nl()).
err(Format, Args) ->
io:format(standard_error, Format ++ io_lib:nl(), Args).
ejabberd-24.12/tools/update-deps-releases.pl 0000775 0001750 0001750 00000043223 14730775155 021376 0 ustar debalance debalance #!/usr/bin/perl
use v5.10;
use strict;
use warnings;
use File::Slurp qw(slurp write_file);
use File::stat;
use File::Touch;
use File::chdir;
use File::Spec;
use Data::Dumper qw(Dumper);
use Carp;
use Term::ANSIColor;
use Term::ReadKey;
use List::Util qw(first);
use Clone qw(clone);
use LWP::UserAgent;
sub get_deps {
my ($config, %fdeps) = @_;
my %deps;
return { } unless $config =~ /\{\s*deps\s*,\s*\[(.*?)\]/s;
my $sdeps = $1;
while ($sdeps =~ /\{\s* (\w+) \s*,\s* ".*?" \s*,\s* \{\s*git \s*,\s* "(.*?)" \s*,\s*
(?:
(?:{\s*tag \s*,\s* "(.*?)") |
"(.*?)" |
( \{ (?: (?-1) | [^{}]+ )+ \} ) )/sgx) {
next unless not %fdeps or exists $fdeps{$1};
$deps{$1} = { repo => $2, commit => $3 || $4 };
}
return \%deps;
}
my (%info_updates, %top_deps_updates, %sub_deps_updates, @operations);
my $epoch = 1;
sub top_deps {
state %deps;
state $my_epoch = $epoch;
if (not %deps or $my_epoch != $epoch) {
$my_epoch = $epoch;
my $config = slurp "rebar.config";
croak "Unable to extract floating_deps" unless $config =~ /\{floating_deps, \[(.*?)\]/s;
my $fdeps = $1;
$fdeps =~ s/\s*//g;
my %fdeps = map { $_ => 1 } split /,/, $fdeps;
%deps = %{get_deps($config, %fdeps)};
}
return {%deps, %top_deps_updates};
}
sub update_deps_repos {
my ($force) = @_;
my $deps = top_deps();
$epoch++;
mkdir(".deps-update") unless -d ".deps-update";
for my $dep (keys %{$deps}) {
my $dd = ".deps-update/$dep";
if (not -d $dd) {
say "Downloading $dep...";
my $repo = $deps->{$dep}->{repo};
$repo =~ s!^https?://github.com/!git\@github.com:!;
system("git", "-C", ".deps-update", "clone", $repo, $dep);
} elsif (time() - stat($dd)->mtime > 24 * 60 * 60 or $force) {
say "Updating $dep...";
system("git", "-C", $dd, "pull");
touch($dd)
}
}
}
sub sub_deps {
state %sub_deps;
state $my_epoch = $epoch;
if (not %sub_deps or $my_epoch != $epoch) {
$my_epoch = $epoch;
my $deps = top_deps();
for my $dep (keys %{$deps}) {
my $rc = ".deps-update/$dep/rebar.config";
$sub_deps{$dep} = { };
next unless -f $rc;
$sub_deps{$dep} = get_deps(scalar(slurp($rc)));
}
}
return {%sub_deps, %sub_deps_updates};
}
sub rev_deps_helper {
my ($rev_deps, $dep) = @_;
if (not exists $rev_deps->{$dep}->{indirect}) {
my %deps = %{$rev_deps->{$dep}->{direct} || {}};
for (keys %{$rev_deps->{$dep}->{direct}}) {
%deps = (%deps, %{rev_deps_helper($rev_deps, $_)});
}
$rev_deps->{$dep}->{indirect} = \%deps;
}
return $rev_deps->{$dep}->{indirect};
}
sub rev_deps {
state %rev_deps;
state $deps_epoch = $epoch;
if (not %rev_deps or $deps_epoch != $epoch) {
$deps_epoch = $epoch;
my $sub_deps = sub_deps();
for my $dep (keys %$sub_deps) {
$rev_deps{$_}->{direct}->{$dep} = 1 for keys %{$sub_deps->{$dep}};
}
for my $dep (keys %$sub_deps) {
$rev_deps{$dep}->{indirect} = rev_deps_helper(\%rev_deps, $dep);
}
}
return \%rev_deps;
}
sub update_changelog {
my ($dep, $version, @reasons) = @_;
my $cl = ".deps-update/$dep/CHANGELOG.md";
return if not -f $cl;
my $reason = join "\n", map {"* $_"} @reasons;
my $content = slurp($cl);
if (not $content =~ /^# Version $version/) {
$content = "# Version $version\n\n$reason\n\n$content";
} else {
$content =~ s/(# Version $version\n\n)/$1$reason\n/;
}
write_file($cl, $content);
}
sub edit_changelog {
my ($dep, $version) = @_;
my $cl = ".deps-update/$dep/CHANGELOG.md";
return if not -f $cl;
my $top_deps = top_deps();
my $git_info = deps_git_info();
say color("red"), "$dep", color("reset"), " ($top_deps->{$dep}->{commit}):";
say " $_" for @{$git_info->{$dep}->{new_commits}};
say "";
my $content = slurp($cl);
my $old_content = $content;
if (not $content =~ /^# Version $version/) {
$content = "# Version $version\n\n* \n\n$content";
} else {
$content =~ s/(# Version $version\n\n)/$1* \n/;
}
write_file($cl, $content);
system("$ENV{EDITOR} $cl");
my $new_content = slurp($cl);
if ($new_content eq $content) {
write_file($cl, $old_content);
} else {
system("git", "-C", ".deps-update/$dep", "commit", "-a", "-m", "Update changelog");
}
}
sub update_app_src {
my ($dep, $version) = @_;
my $app = ".deps-update/$dep/src/$dep.app.src";
return if not -f $app;
my $content = slurp($app);
$content =~ s/(\{\s*vsn\s*,\s*)".*"/$1"$version"/;
write_file($app, $content);
}
sub update_deps_versions {
my ($config_path, %deps) = @_;
my $config = slurp $config_path;
for (keys %deps) {
$config =~ s/(\{\s*$_\s*,\s*".*?"\s*,\s*\{\s*git\s*,\s*".*?"\s*,\s*)(?:{\s*tag\s*,\s*"(.*?)"\s*}|"(.*?)")/$1\{tag, "$deps{$_}"}/s;
}
write_file($config_path, $config);
}
sub cmp_ver {
my @a = split /(\d+)/, $a;
my @b = split /(\d+)/, $b;
my $is_num = 1;
return - 1 if $#a == 0;
return 1 if $#b == 0;
while (1) {
my $ap = shift @a;
my $bp = shift @b;
$is_num = 1 - $is_num;
if (defined $ap) {
if (defined $bp) {
if ($is_num) {
next if $ap == $bp;
return 1 if $ap > $bp;
return - 1;
} else {
next if $ap eq $bp or $ap eq "" or $bp eq "";
return 1 if $ap gt $bp;
return - 1;
}
} else {
return 1;
}
} elsif (defined $bp) {
return - 1;
} else {
return 0;
}
}
}
sub deps_git_info {
state %info;
state $my_epoch = $epoch;
if (not %info or $my_epoch != $epoch) {
$my_epoch = $epoch;
my $deps = top_deps();
for my $dep (keys %{$deps}) {
my $dir = ".deps-update/$dep";
my @tags = `git -C "$dir" tag`;
chomp(@tags);
@tags = sort cmp_ver @tags;
my $last_tag = $tags[$#tags];
my @new = `git -C $dir log --oneline $last_tag..origin/master`;
my $new_tag = $last_tag;
$new_tag =~ s/(\d+)$/$1+1/e;
chomp(@new);
my $cl = ".deps-update/$dep/CHANGELOG.md";
my $content = slurp($cl, err_mode => "quiet") // "";
if ($content =~ /^# Version (\S+)/) {
if (!grep({$_ eq $1} @tags) && $1 ne $new_tag) {
$new_tag = $1;
}
}
$info{$dep} = { last_tag => $last_tag, new_commits => \@new, new_tag => $new_tag };
}
}
return { %info, %info_updates };
}
sub show_commands {
my %commands = @_;
my @keys;
while (@_) {
push @keys, shift;
shift;
}
for (@keys) {
say color("red"), $_, color("reset"), ") $commands{$_}";
}
ReadMode(4);
my $wkey = "";
while (1) {
my $key = ReadKey(0);
$wkey = substr($wkey.$key, -2);
if (defined $commands{uc($key)}) {
ReadMode(0);
say "";
return uc($key);
} elsif (defined $commands{uc($wkey)}) {
ReadMode(0);
say "";
return uc($wkey);
}
}
}
sub schedule_operation {
my ($type, $dep, $tag, $reason, $op) = @_;
my $idx = first { $operations[$_]->{dep} eq $dep } 0..$#operations;
if (defined $idx) {
my $mop = $operations[$idx];
if (defined $op) {
my $oidx = first { $mop->{operations}->[$_]->[0] eq $op->[0] } 0..$#{$mop->{operations}};
if (defined $oidx) {
$mop->{reasons}->[$oidx] = $reason;
$mop->{operations}->[$oidx] = $op;
} else {
push @{$mop->{reasons}}, $reason;
push @{$mop->{operations}}, $op;
}
}
return if $type eq "update";
$mop->{type} = $type;
$info_updates{$dep}->{new_commits} = [];
return;
}
my $info = deps_git_info();
$top_deps_updates{$dep} = {commit => $tag};
$info_updates{$dep} = {last_tag => $tag, new_tag => $tag,
new_commits => $type eq "tupdate" ? [] : $info->{$dep}->{new_commits}};
my $rev_deps = rev_deps();
@operations = sort {
exists $rev_deps->{$a->{dep}}->{indirect}->{$b->{dep}} ? -1 :
exists $rev_deps->{$b->{dep}}->{indirect}->{$a->{dep}} ? 1 : $a->{dep} cmp $b->{dep}
} (@operations, {
type => $type,
dep => $dep,
version => $tag,
reasons => ($reason ? [$reason] : []),
operations => ($op ? [$op] : [])}
);
my $sub_deps = sub_deps();
for (keys %{$rev_deps->{$dep}->{direct}}) {
schedule_operation("update", $_, $info->{$_}->{new_tag}, "Updating $dep to version $tag.", [$dep, $tag]);
$sub_deps_updates{$_} = $sub_deps_updates{$_} || clone($sub_deps->{$_});
$sub_deps_updates{$_}->{$dep}->{commit} = $tag;
}
}
sub git_tag {
my ($dep, $ver, $msg) = @_;
system("git", "-C", ".deps-update/$dep", "commit", "-a", "-m", $msg);
system("git", "-C", ".deps-update/$dep", "tag", $ver, "-a", "-m", $msg);
}
sub git_push {
my ($dep) = @_;
system("git", "-C", ".deps-update/$dep", "push");
system("git", "-C", ".deps-update/$dep", "push", "--tags");
}
sub check_hex_files {
my ($dep) = @_;
my $app = ".deps-update/$dep/src/$dep.app.src";
return if not -f $app;
my $content = slurp($app);
my @paths;
if ($content =~ /{\s*files\s*,\s*\[([^\]]+)\]/) {
my $list = $1;
push @paths, $1 while $list =~ /"([^"]*?)"/g;
} else {
@paths = (
"src", "c_src", "include", "rebar.config.script", "priv",
"rebar.config", "rebar.lock", "README*", "readme*", "LICENSE*",
"license*", "NOTICE");
}
local $CWD = ".deps-update/$dep";
my @interesting_files = map {File::Spec->canonpath($_)} glob("rebar.config* src/*.erl src/*.app.src c_src/*.c c_src/*.cpp \
c_src/*.h c_src/*.hpp include/*.hrl");
my @matching_files;
for my $path (@paths) {
if (-d $path) {
push @matching_files, map {File::Spec->canonpath($_)} glob("$path/*");
} else {
push @matching_files, map {File::Spec->canonpath($_)} glob($path);
}
}
my %diff;
@diff{ @interesting_files } = undef;
delete @diff{ @matching_files };
my @diff = keys %diff;
if (@diff) {
print color("red"), "Dependency ", color("bold red"), $dep, color("reset"), color("red"), " files section doesn't match: ",
join(" ", @diff), color("reset"), "\n";
}
}
update_deps_repos();
MAIN:
while (1) {
my $top_deps = top_deps();
my $git_info = deps_git_info();
print color("bold blue"), "Dependences with newer tags:\n", color("reset");
my $old_deps = 0;
for my $dep (sort keys %$top_deps) {
next unless $git_info->{$dep}->{last_tag} ne $top_deps->{$dep}->{commit};
say color("red"), "$dep", color("reset"), ": $top_deps->{$dep}->{commit} -> $git_info->{$dep}->{last_tag}";
$old_deps = 1;
}
say "(none)" if not $old_deps;
say "";
print color("bold blue"), "Dependences that have commits after last tags:\n", color("reset");
my $changed_deps = 0;
for my $dep (sort keys %$top_deps) {
next unless @{$git_info->{$dep}->{new_commits}};
say color("red"), "$dep", color("reset"), " ($top_deps->{$dep}->{commit}):";
say " $_" for @{$git_info->{$dep}->{new_commits}};
$changed_deps = 1;
}
say "(none)" if not $changed_deps;
say "";
for my $dep (sort keys %$top_deps) {
check_hex_files($dep);
}
my $cmd = show_commands($old_deps ? (U => "Update dependency") : (),
$changed_deps ? (T => "Tag new release") : (),
@operations ? (A => "Apply changes") : (),
R => "Refresh repositories",
H => "What release to Hex",
E => "Exit");
last if $cmd eq "E";
if ($cmd eq "U") {
while (1) {
my @deps_to_update;
my @od;
my $idx = 1;
for my $dep (sort keys %$top_deps) {
next unless $git_info->{$dep}->{last_tag} ne $top_deps->{$dep}->{commit};
$od[$idx] = $dep;
push @deps_to_update, $idx++, "Update $dep to $git_info->{$dep}->{last_tag}";
}
last if $idx == 1;
my $cmd = show_commands(@deps_to_update, E => "Exit");
last if $cmd eq "E";
my $dep = $od[$cmd];
schedule_operation("update", $dep, $git_info->{$dep}->{last_tag});
$top_deps = top_deps();
$git_info = deps_git_info();
}
}
if ($cmd eq "R") {
update_deps_repos(1);
}
if ($cmd eq "H") {
my $ua = LWP::UserAgent->new();
for my $dep (sort keys %$top_deps) {
say "checking https://hex.pm/packages/$dep/$git_info->{$dep}->{last_tag}";
my $res = $ua->head("https://hex.pm/packages/$dep/$git_info->{$dep}->{last_tag}");
if ($res->code == 404) {
say color("red"), "$dep", color("reset"), " ($top_deps->{$dep}->{commit})";
}
}
}
if ($cmd eq "T") {
while (1) {
my @deps_to_tag;
my @od;
my $idx = 1;
my $count = 0;
for my $dep (sort keys %$top_deps) {
next unless @{$git_info->{$dep}->{new_commits}};
$count++;
}
for my $dep (sort keys %$top_deps) {
next unless @{$git_info->{$dep}->{new_commits}};
$od[$idx] = $dep;
my $id = $idx++;
$id = sprintf "%02d", $id if $count > 9;
push @deps_to_tag, $id, "Tag $dep with version $git_info->{$dep}->{new_tag}";
}
last if $idx == 1;
my $cmd = show_commands(@deps_to_tag, E => "Exit");
last if $cmd eq "E";
my $dep = $od[$cmd];
my $d = $git_info->{$dep};
schedule_operation("tupdate", $dep, $d->{new_tag});
$top_deps = top_deps();
$git_info = deps_git_info();
}
}
my $changelog_updated = 0;
if ($cmd eq "A") {
APPLY: {
$top_deps = top_deps();
$git_info = deps_git_info();
my $sub_deps = sub_deps();
for my $dep (keys %$top_deps) {
for my $sdep (keys %{$sub_deps->{$dep}}) {
next if not defined $top_deps->{$sdep} or
$sub_deps->{$dep}->{$sdep}->{commit} eq $top_deps->{$sdep}->{commit};
say "$dep $sdep ", $sub_deps->{$dep}->{$sdep}->{commit}, " <=> $sdep ",
$top_deps->{$sdep}->{commit};
schedule_operation("update", $dep, $git_info->{$dep}->{new_tag},
"Updating $sdep to version $top_deps->{$sdep}->{commit}.",
[ $sdep, $top_deps->{$sdep}->{commit} ]);
}
}
%info_updates = ();
%top_deps_updates = ();
%sub_deps_updates = ();
$top_deps = top_deps();
$git_info = deps_git_info();
$sub_deps = sub_deps();
print color("bold blue"), "List of operations:\n", color("reset");
for my $op (@operations) {
print color("red"), $op->{dep}, color("reset"),
" ($top_deps->{$op->{dep}}->{commit} -> $op->{version})";
if (@{$op->{operations}}) {
say ":";
say " $_->[0] -> $_->[1]" for @{$op->{operations}};
}
else {
say "";
}
}
say "";
my %to_tag;
if (not $changelog_updated) {
for my $op (@operations) {
if ($git_info->{$op->{dep}}->{last_tag} ne $op->{version}) {
$to_tag{$op->{dep}} = $op->{version};
}
}
}
my $cmd = show_commands(A => "Apply", (%to_tag ? (U => "Update Changelogs") : ()), E => "Exit");
if ($cmd eq "U") {
for my $dep (keys %to_tag) {
edit_changelog($dep, $to_tag{$dep});
}
redo APPLY;
}
elsif ($cmd eq "A") {
my %top_changes;
for my $op (@operations) {
update_changelog($op->{dep}, $op->{version}, @{$op->{reasons}})
if @{$op->{reasons}};
update_deps_versions(".deps-update/$op->{dep}/rebar.config", map {@{$_}[0,1] } @{$op->{operations}})
if @{$op->{operations}};
if ($git_info->{$op->{dep}}->{last_tag} ne $op->{version}) {
update_app_src($op->{dep}, $op->{version});
git_tag($op->{dep}, $op->{version}, "Release $op->{version}");
}
$top_changes{$op->{dep}} = $op->{version};
}
update_deps_versions("rebar.config", %top_changes);
for my $op (@operations) {
if ($git_info->{$op->{dep}}->{last_tag} ne $op->{version}) {
git_push($op->{dep});
}
}
last MAIN;
}
}
}
}
ejabberd-24.12/tools/xml_compress_gen.erl 0000664 0001750 0001750 00000037567 14730775155 021110 0 ustar debalance debalance %% File : xml_compress_gen.erl
%% Author : Pawel Chmielowski
%% Purpose :
%% Created : 14 Sep 2018 Pawel Chmielowski
%%
%%
%% 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(xml_compress_gen).
-author("pawel@process-one.net").
-include_lib("xmpp/include/xmpp.hrl").
%% API
-export([archive_analyze/3, process_stats/1, gen_code/3]).
-record(el_stats, {count = 0, empty_count = 0, only_text_count = 0, attrs = #{}, text_stats = #{}}).
-record(attr_stats, {count = 0, vals = #{}}).
archive_analyze(Host, Table, EHost) ->
case ejabberd_sql:sql_query(Host, [<<"select username, peer, kind, xml from ", Table/binary>>]) of
{selected, _, Res} ->
lists:foldl(
fun([U, P, K, X], Stats) ->
M = case K of
<<"groupchat">> ->
U;
_ ->
<>
end,
El = fxml_stream:parse_element(X),
analyze_element({El, <<"stream">>, <<"jabber:client">>, M, P}, Stats)
end, {0, #{}}, Res);
_ ->
none
end.
encode_id(Num) when Num < 64 ->
iolist_to_binary(io_lib:format("~p:8", [Num])).
gen_code(_File, _Rules, $<) ->
{error, <<"Invalid version">>};
gen_code(File, Rules, Ver) when Ver < 64 ->
{Data, _} = lists:foldl(
fun({Ns, El, Attrs, Text}, {Acc, Id}) ->
NsC = case lists:keyfind(Ns, 1, Acc) of
false -> [];
{_, L} -> L
end,
{AttrsE, _} = lists:mapfoldl(
fun({AName, AVals}, Id2) ->
{AD, Id3} = lists:mapfoldl(
fun(AVal, Id3) ->
{{AVal, encode_id(Id3)}, Id3 + 1}
end, Id2, AVals),
{{AName, AD ++ [encode_id(Id3)]}, Id3 + 1}
end, 3, Attrs),
{TextE, Id5} = lists:mapfoldl(
fun(TextV, Id4) ->
{{TextV, encode_id(Id4)}, Id4 + 1}
end, Id + 1, Text),
{lists:keystore(Ns, 1, Acc, {Ns, NsC ++ [{El, encode_id(Id), AttrsE, TextE}]}), Id5}
end, {[], 5}, Rules),
{ok, Dev} = file:open(File, [write]),
Mod = filename:basename(File, ".erl"),
io:format(Dev, "-module(~s).~n-export([encode/3, decode/3]).~n~n", [Mod]),
RulesS = iolist_to_binary(io_lib:format("~p", [Rules])),
RulesS2 = binary:replace(RulesS, <<"\n">>, <<"\n% ">>, [global]),
io:format(Dev, "% This file was generated by xml_compress_gen~n%~n"
"% Rules used:~n%~n% ~s~n~n", [RulesS2]),
VerId = iolist_to_binary(io_lib:format("~p:8", [Ver])),
gen_encode(Dev, Data, VerId),
gen_decode(Dev, Data, VerId),
file:close(Dev),
Data.
gen_decode(Dev, Data, VerId) ->
io:format(Dev, "decode(<<$<, _/binary>> = Data, _J1, _J2) ->~n"
" fxml_stream:parse_element(Data);~n"
"decode(<<~s, Rest/binary>>, J1, J2) ->~n"
" {El, _} = decode(Rest, <<\"jabber:client\">>, J1, J2),~n"
" El.~n~n", [VerId]),
io:format(Dev, "decode_string(Data) ->~n"
" case Data of~n"
" <<0:2, L:6, Str:L/binary, Rest/binary>> ->~n"
" {Str, Rest};~n"
" <<1:2, L1:6, 0:2, L2:6, Rest/binary>> ->~n"
" L = L2*64 + L1,~n"
" <> = Rest,~n"
" {Str, Rest2};~n"
" <<1:2, L1:6, 1:2, L2:6, L3:8, Rest/binary>> ->~n"
" L = (L3*64 + L2)*64 + L1,~n"
" <> = Rest,~n"
" {Str, Rest2}~n"
" end.~n~n", []),
io:format(Dev, "decode_child(<<1:8, Rest/binary>>, _PNs, _J1, _J2) ->~n"
" {Text, Rest2} = decode_string(Rest),~n"
" {{xmlcdata, Text}, Rest2};~n", []),
io:format(Dev, "decode_child(<<2:8, Rest/binary>>, PNs, J1, J2) ->~n"
" {Name, Rest2} = decode_string(Rest),~n"
" {Attrs, Rest3} = decode_attrs(Rest2),~n"
" {Children, Rest4} = decode_children(Rest3, PNs, J1, J2),~n"
" {{xmlel, Name, Attrs, Children}, Rest4};~n", []),
io:format(Dev, "decode_child(<<3:8, Rest/binary>>, PNs, J1, J2) ->~n"
" {Ns, Rest2} = decode_string(Rest),~n"
" {Name, Rest3} = decode_string(Rest2),~n"
" {Attrs, Rest4} = decode_attrs(Rest3),~n"
" {Children, Rest5} = decode_children(Rest4, Ns, J1, J2),~n"
" {{xmlel, Name, add_ns(PNs, Ns, Attrs), Children}, Rest5};~n", []),
io:format(Dev, "decode_child(<<4:8, Rest/binary>>, _PNs, _J1, _J2) ->~n"
" {stop, Rest};~n", []),
io:format(Dev, "decode_child(Other, PNs, J1, J2) ->~n"
" decode(Other, PNs, J1, J2).~n~n", []),
io:format(Dev, "decode_children(Data, PNs, J1, J2) ->~n"
" prefix_map(fun(Data2) -> decode(Data2, PNs, J1, J2) end, Data).~n~n", []),
io:format(Dev, "decode_attr(<<1:8, Rest/binary>>) ->~n"
" {Name, Rest2} = decode_string(Rest),~n"
" {Val, Rest3} = decode_string(Rest2),~n"
" {{Name, Val}, Rest3};~n", []),
io:format(Dev, "decode_attr(<<2:8, Rest/binary>>) ->~n"
" {stop, Rest}.~n~n", []),
io:format(Dev, "decode_attrs(Data) ->~n"
" prefix_map(fun decode_attr/1, Data).~n~n", []),
io:format(Dev, "prefix_map(F, Data) ->~n"
" prefix_map(F, Data, []).~n~n", []),
io:format(Dev, "prefix_map(F, Data, Acc) ->~n"
" case F(Data) of~n"
" {stop, Rest} ->~n"
" {lists:reverse(Acc), Rest};~n"
" {Val, Rest} ->~n"
" prefix_map(F, Rest, [Val | Acc])~n"
" end.~n~n", []),
io:format(Dev, "add_ns(Ns, Ns, Attrs) ->~n"
" Attrs;~n"
"add_ns(_, Ns, Attrs) ->~n"
" [{<<\"xmlns\">>, Ns} | Attrs].~n~n", []),
lists:foreach(
fun({Ns, Els}) ->
lists:foreach(
fun({Name, Id, Attrs, Text}) ->
io:format(Dev, "decode(<<~s, Rest/binary>>, PNs, J1, J2) ->~n"
" Ns = ~p,~n", [Id, Ns]),
case Attrs of
[] ->
io:format(Dev, " {Attrs, Rest2} = decode_attrs(Rest),~n", []);
_ ->
io:format(Dev, " {Attrs, Rest2} = prefix_map(fun~n", []),
lists:foreach(
fun({AName, AVals}) ->
lists:foreach(
fun({j1, AId}) ->
io:format(Dev, " (<<~s, Rest3/binary>>) ->~n"
" {{~p, J1}, Rest3};~n", [AId, AName]);
({j2, AId}) ->
io:format(Dev, " (<<~s, Rest3/binary>>) ->~n"
" {{~p, J2}, Rest3};~n", [AId, AName]);
({{j1}, AId}) ->
io:format(Dev, " (<<~s, Rest3/binary>>) ->~n"
" {AVal, Rest4} = decode_string(Rest3),~n"
" {{~p, <>}, Rest4};~n",
[AId, AName]);
({{j2}, AId}) ->
io:format(Dev, " (<<~s, Rest3/binary>>) ->~n"
" {AVal, Rest4} = decode_string(Rest3),~n"
" {{~p, <>}, Rest4};~n",
[AId, AName]);
({AVal, AId}) ->
io:format(Dev, " (<<~s, Rest3/binary>>) ->~n"
" {{~p, ~p}, Rest3};~n",
[AId, AName, AVal]);
(AId) ->
io:format(Dev, " (<<~s, Rest3/binary>>) ->~n"
" {AVal, Rest4} = decode_string(Rest3),~n"
" {{~p, AVal}, Rest4};~n",
[AId, AName])
end, AVals)
end, Attrs),
io:format(Dev, " (<<2:8, Rest3/binary>>) ->~n"
" {stop, Rest3};~n"
" (Data) ->~n"
" decode_attr(Data)~n"
" end, Rest),~n", [])
end,
case Text of
[] ->
io:format(Dev, " {Children, Rest6} = decode_children(Rest2, Ns, J1, J2),~n", []);
_ ->
io:format(Dev, " {Children, Rest6} = prefix_map(fun", []),
lists:foreach(
fun({TextS, TId}) ->
io:format(Dev, " (<<~s, Rest5/binary>>) ->~n"
" {{xmlcdata, ~p}, Rest5};~n",
[TId, TextS])
end, Text),
io:format(Dev, " (Other) ->~n"
" decode_child(Other, Ns, J1, J2)~n"
" end, Rest2),~n", [])
end,
io:format(Dev, " {{xmlel, ~p, add_ns(PNs, Ns, Attrs), Children}, Rest6};~n", [Name])
end, Els)
end, Data),
io:format(Dev, "decode(Other, PNs, J1, J2) ->~n"
" decode_child(Other, PNs, J1, J2).~n~n", []).
gen_encode(Dev, Data, VerId) ->
io:format(Dev, "encode(El, J1, J2) ->~n"
" encode_child(El, <<\"jabber:client\">>,~n"
" J1, J2, byte_size(J1), byte_size(J2), <<~s>>).~n~n", [VerId]),
io:format(Dev, "encode_attr({<<\"xmlns\">>, _}, Acc) ->~n"
" Acc;~n"
"encode_attr({N, V}, Acc) ->~n"
" <>.~n~n", []),
io:format(Dev, "encode_attrs(Attrs, Acc) ->~n"
" lists:foldl(fun encode_attr/2, Acc, Attrs).~n~n", []),
io:format(Dev, "encode_el(PNs, Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx) ->~n"
" E1 = if~n"
" PNs == Ns -> encode_attrs(Attrs, <>);~n"
" true -> encode_attrs(Attrs, <>)~n"
" end,~n"
" E2 = encode_children(Children, Ns, J1, J2, J1L, J2L, <>),~n"
" <>.~n~n", []),
io:format(Dev, "encode_child({xmlel, Name, Attrs, Children}, PNs, J1, J2, J1L, J2L, Pfx) ->~n"
" case lists:keyfind(<<\"xmlns\">>, 1, Attrs) of~n"
" false ->~n"
" encode(PNs, PNs, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx);~n"
" {_, Ns} ->~n"
" encode(PNs, Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx)~n"
" end;~n"
"encode_child({xmlcdata, Data}, _PNs, _J1, _J2, _J1L, _J2L, Pfx) ->~n"
" <>.~n~n", []),
io:format(Dev, "encode_children(Children, PNs, J1, J2, J1L, J2L, Pfx) ->~n"
" lists:foldl(~n"
" fun(Child, Acc) ->~n"
" encode_child(Child, PNs, J1, J2, J1L, J2L, Acc)~n"
" end, Pfx, Children).~n~n", []),
io:format(Dev, "encode_string(Data) ->~n"
" <> = <<(byte_size(Data)):16/unsigned-big-integer>>,~n"
" case {V1, V2, V3} of~n"
" {0, 0, V3} ->~n"
" <>;~n"
" {0, V2, V3} ->~n"
" <<(V3 bor 64):8, V2:8, Data/binary>>;~n"
" _ ->~n"
" <<(V3 bor 64):8, (V2 bor 64):8, V1:8, Data/binary>>~n"
" end.~n~n", []),
lists:foreach(
fun({Ns, Els}) ->
io:format(Dev, "encode(PNs, ~p = Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx) ->~n"
" case Name of~n", [Ns]),
lists:foreach(
fun({ElN, Id, Attrs, Text}) ->
io:format(Dev, " ~p ->~n", [ElN]),
case Attrs of
[] ->
io:format(Dev, " E = encode_attrs(Attrs, <>),~n", [Id]);
_ ->
io:format(Dev, " E = lists:foldl(fun~n", []),
lists:foreach(
fun({AName, AVals}) ->
case AVals of
[AIdS] when is_binary(AIdS) ->
io:format(Dev, " ({~p, AVal}, Acc) ->~n"
" <>;~n",
[AName, AIdS]);
_ ->
io:format(Dev, " ({~p, AVal}, Acc) ->~n"
" case AVal of~n", [AName]),
lists:foreach(
fun({j1, AId}) ->
io:format(Dev, " J1 -> <>;~n",
[AId]);
({j2, AId}) ->
io:format(Dev, " J2 -> <>;~n",
[AId]);
({{j1}, AId}) ->
io:format(Dev, " <> -> "
"<>;~n",
[AId]);
({{j2}, AId}) ->
io:format(Dev, " <> -> "
"<>;~n",
[AId]);
({AVal, AId}) ->
io:format(Dev, " ~p -> <>;~n",
[AVal, AId]);
(AId) ->
io:format(Dev, " _ -> <>~n",
[AId])
end, AVals),
io:format(Dev, " end;~n", [])
end
end, Attrs),
io:format(Dev, " (Attr, Acc) -> encode_attr(Attr, Acc)~n", []),
io:format(Dev, " end, <>, Attrs),~n", [Id])
end,
case Text of
[] ->
io:format(Dev, " E2 = encode_children(Children, Ns, "
"J1, J2, J1L, J2L, <>),~n", []);
_ ->
io:format(Dev, " E2 = lists:foldl(fun~n", []),
lists:foreach(
fun({TextV, TId}) ->
io:format(Dev, " ({xmlcdata, ~p}, Acc) -> <>;~n", [TextV, TId])
end, Text),
io:format(Dev, " (El, Acc) -> encode_child(El, Ns, J1, J2, J1L, J2L, Acc)~n", []),
io:format(Dev, " end, <>, Children),~n", [])
end,
io:format(Dev, " <>;~n", [])
end, Els),
io:format(Dev, " _ -> encode_el(PNs, Ns, Name, Attrs, Children, "
"J1, J2, J1L, J2L, Pfx)~nend;~n", [])
end, Data),
io:format(Dev, "encode(PNs, Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx) ->~n"
" encode_el(PNs, Ns, Name, Attrs, Children, "
"J1, J2, J1L, J2L, Pfx).~n~n", []).
process_stats({_Counts, Stats}) ->
SStats = lists:sort(
fun({_, #el_stats{count = C1}}, {_, #el_stats{count = C2}}) ->
C1 >= C2
end, maps:to_list(Stats)),
lists:map(
fun({Name, #el_stats{count = C, attrs = A, text_stats = T}}) ->
[Ns, El] = binary:split(Name, <<"<">>),
Attrs = lists:filtermap(
fun({AN, #attr_stats{count = AC, vals = AV}}) ->
if
AC*5 < C ->
false;
true ->
AVC = AC div min(maps:size(AV)*2, 10),
AVA = [N || {N, C2} <- maps:to_list(AV), C2 > AVC],
{true, {AN, AVA}}
end
end, maps:to_list(A)),
Text = [TE || {TE, TC} <- maps:to_list(T), TC > C/2],
{Ns, El, Attrs, Text}
end, SStats).
analyze_elements(Elements, Stats, PName, PNS, J1, J2) ->
lists:foldl(fun analyze_element/2, Stats, lists:map(fun(V) -> {V, PName, PNS, J1, J2} end, Elements)).
maps_update(Key, F, InitVal, Map) ->
case maps:is_key(Key, Map) of
true ->
maps:update_with(Key, F, Map);
_ ->
maps:put(Key, F(InitVal), Map)
end.
analyze_element({{xmlcdata, Data}, PName, PNS, _J1, _J2}, {ElCount, Stats}) ->
Stats2 = maps_update(<>,
fun(#el_stats{text_stats = TS} = E) ->
TS2 = maps_update(Data, fun(C) -> C + 1 end, 0, TS),
E#el_stats{text_stats = TS2}
end, #el_stats{}, Stats),
{ElCount, Stats2};
analyze_element({#xmlel{name = Name, attrs = Attrs, children = Children}, _PName, PNS, J1, J2}, {ElCount, Stats}) ->
XMLNS = case lists:keyfind(<<"xmlns">>, 1, Attrs) of
{_, NS} ->
NS;
false ->
PNS
end,
NStats = maps_update(<>,
fun(#el_stats{count = C, empty_count = EC, only_text_count = TC, attrs = A} = ES) ->
A2 = lists:foldl(
fun({<<"xmlns">>, _}, AMap) ->
AMap;
({AName, AVal}, AMap) ->
J1S = size(J1),
J2S = size(J2),
AVal2 = case AVal of
J1 ->
j1;
J2 ->
j2;
<> ->
{j1};
<> ->
{j2};
Other ->
Other
end,
maps_update(AName, fun(#attr_stats{count = AC, vals = AV}) ->
AV2 = maps_update(AVal2, fun(C2) -> C2 + 1 end, 0, AV),
#attr_stats{count = AC + 1, vals = AV2}
end, #attr_stats{}, AMap)
end, A, Attrs),
ES#el_stats{count = C + 1,
empty_count = if Children == [] -> EC + 1; true ->
EC end,
only_text_count = case Children of [{xmlcdata, _}] -> TC + 1; _ -> TC end,
attrs = A2}
end, #el_stats{}, Stats),
analyze_elements(Children, {ElCount + 1, NStats}, Name, XMLNS, J1, J2).
ejabberd-24.12/tools/ejabberdctl.bc 0000664 0001750 0001750 00000006354 14730775155 017575 0 ustar debalance debalance #
# bash completion for ejabberdctl
#
# For installation and details see:
# https://docs.ejabberd.im/admin/guide/managing/#bash-completion
#
get_help()
{
local CACHE_BASE=/tmp/ejabberd_bash_completion
local DATESTRING=`date +%F-%H`
local CACHE=$CACHE_BASE.$DATESTRING
local CACHE_COMS=$CACHE.coms
local CACHE_TAGS=$CACHE.tags
if [[ ! -f $CACHE_COMS ]] ; then
rm -f $CACHE_BASE.*
[ -f $CACHE_COMS ] || ejabberdctl $CTLARGS | sed "s|\x1B\[[0-9]*m||g" | grep "^ [a-z]" | awk '{print $1}' | xargs >$CACHE_COMS
[ -f $CACHE_TAGS ] || ejabberdctl $CTLARGS help tags | sed "s|\x1B\[[0-9]*m||g" | grep "^ [a-z]" | awk '{print $1}' | xargs >$CACHE_TAGS
fi
if [[ $? == 2 ]] || [[ -f $CACHE_COMS ]] ; then
ISRUNNING=1
runningcommands=`cat $CACHE_COMS`
runningtags=`cat $CACHE_TAGS`
fi
}
_ejabberdctl()
{
local cur prev
local ISRUNNING=0
local runningcommands
COMPREPLY=()
cur="${COMP_WORDS[COMP_CWORD]}"
prev="${COMP_WORDS[COMP_CWORD-1]}"
local startcoms="start foreground foreground-quiet live debug etop iexdebug iexlive ping started stopped"
local startpars="--config-dir --config --ctl-config --logs --node --spool"
local i=1
local CTLARGS=""
while [ $i -lt $COMP_CWORD ] ; do
local PARAM="${COMP_WORDS[i]}"
i=$((i+1))
case $PARAM in
--*)
CTLARGS="--node ${COMP_WORDS[i]}"
i=$((i+1)) ;;
*) break ;;
esac
done
case "${prev##*/}" in
ejabberdctl)
# This clause matches even when calling `/sbin/ejabberdctl` thanks to the ##*/ in the case
get_help
COMPREPLY=($(compgen -W "--node ${startpars} ${startcoms} ${runningcommands}" -- $cur))
return 0
;;
start|live)
COMPREPLY=($(compgen -W "--node ${startpars}" -- $cur))
return 0
;;
debug)
COMPREPLY=($(compgen -W "--node" -- $cur))
return 0
;;
help)
get_help
COMPREPLY=($(compgen -W "${runningcommands} ${runningtags}" -- $cur))
return 0
;;
--node)
RUNNINGNODES=`epmd -names | grep name | awk '{print $2"@localhost"}' | xargs`
COMPREPLY=($(compgen -W "$RUNNINGNODES" -- $cur))
return 0
;;
--config|--ctl-config)
_filedir '?(u)cfg'
return 0
;;
--config-dir|--logs|--spool)
_filedir
return 0
;;
*)
prev2="${COMP_WORDS[COMP_CWORD-2]}"
get_help
if [[ "$prev2" == --* ]]; then
COMPREPLY=($(compgen -W "--node ${startpars} ${startcoms} ${runningcommands}" -- $cur))
else
if [[ $ISRUNNING == 1 ]]; then
echo ""
ejabberdctl $CTLARGS help ${PARAM}
echo -n "${COMP_LINE}"
fi
fi
return 0
;;
esac
}
complete -F _ejabberdctl ejabberdctl
# Local variables:
# mode: shell-script
# sh-basic-offset: 4
# sh-indent-comment: t
# indent-tabs-mode: nil
# End:
# ex: ts=4 sw=4 et filetype=sh
ejabberd-24.12/tools/captcha-ng.sh 0000775 0001750 0001750 00000006140 14730775155 017363 0 ustar debalance debalance #!/bin/bash
# Copyright © 2021 Adrien Bourmault (neox@os-k.eu)
# 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 script is an example captcha script.
# It takes the text to recognize in the captcha image as a parameter.
# It return the image binary as a result. ejabberd support PNG, JPEG and GIF.
# The whole idea of the captcha script is to let server admins adapt it to
# their own needs. The goal is to be able to make the captcha generation as
# unique as possible, to make the captcha challenge difficult to bypass by
# a bot.
# Server admins are thus supposed to write and use their own captcha generators.
# This script relies on ImageMagick.
# It is NOT compliant with ImageMagick forks like GraphicsMagick.
INPUT=$1
TRANSFORMATIONS=(INTRUDER SUM)
DIGIT=(zero one two three four five six seven eight nine ten)
if test -n "${BASH_VERSION:-''}" ; then
get_random ()
{
R=$RANDOM
}
else
for n in $(od -A n -t u2 -N 64 /dev/urandom); do RL="$RL$n "; done
get_random ()
{
R=${RL%% *}
RL=${RL#* }
}
fi
INTRUDER()
{
NUMBERS=$(echo "$INPUT" | grep -o . | tr '\n' ' ')
SORTED_UNIQ_NUM=$(echo "${NUMBERS[@]}" | sort -u | tr '\n' ' ')
SORT_RANDOM_CMD="$( ( echo x|sort -R >&/dev/null && echo "sort -R" ) || ( echo x|shuf >&/dev/null && echo shuf ) || echo cat)"
RANDOM_DIGITS=$(echo 123456789 | grep -o . | eval "$SORT_RANDOM_CMD" | tr '\n' ' ')
INTRUDER=-1
for i in $RANDOM_DIGITS
do
if [[ ! " ${SORTED_UNIQ_NUM[@]} " =~ ${i} ]]; then
INTRUDER=$i
break
fi
done
# Worst case
if [[ $INTRUDER -eq "-1" ]]
then
printf "Type %s \n without changes" "$INPUT"
return
fi
for num in ${NUMBERS}
do
get_random
R=$((R % 100))
if [[ $R -lt 60 ]]; then
NEWINPUT=${NEWINPUT}${num}${INTRUDER}
else
NEWINPUT=${NEWINPUT}${num}
fi
done
get_random
R=$((R % 100))
if [[ $R -lt 50 ]]; then
printf "Type %s by\n deleting the %s" "$NEWINPUT" "${DIGIT[$INTRUDER]}"
else
printf "Enter %s by\n removing the %s" "$NEWINPUT" "${DIGIT[$INTRUDER]}"
fi
}
SUM()
{
get_random
RA=$((R % 100))
if [[ $((INPUT % 2)) -eq 0 ]]; then
A=$((INPUT - RA))
B=$RA
else
B=$((INPUT - RA))
A=$RA
fi
get_random
R=$((R % 100))
if [[ $R -lt 25 ]]; then
printf "Type the result\n of %s + %s" "$A" "$B"
elif [[ $R -lt 50 ]]; then
printf "SUMx\n %s and %s" "$A" "$B"
elif [[ $R -lt 75 ]]; then
printf "Add\n %s and %s" "$A" "$B"
else
printf "Enter the result\n of %s + %s" "$A" "$B"
fi
}
get_random
RAND_ITALIC=$((R % 25))
get_random
RAND_ANGLE=$((R % 3))
get_random
RAND_INDEX=$((R % ${#TRANSFORMATIONS[@]}))
convert -size 300x60 xc:none -pointsize 20 \
\( -clone 0 -fill black \
-stroke black -strokewidth 1 \
-annotate "${RAND_ANGLE}x${RAND_ITALIC}+0+0" "\n $(${TRANSFORMATIONS[$RAND_INDEX]})" \
-roll +"$ROLL_X"+0 \
-wave "$WAVE1_AMPLITUDE"x"$WAVE1_LENGTH" \
-roll -"$ROLL_X"+0 \) \
-flatten -crop 300x60 +repage -quality 500 -depth 11 png:-
ejabberd-24.12/tools/make-binaries 0000775 0001750 0001750 00000057267 14730775155 017474 0 ustar debalance debalance #!/bin/sh
# Build portable binary release tarballs for Linux/x64 and Linux/arm64.
#
# Author: Holger Weiss .
#
# Copyright (c) 2022 ProcessOne, SARL.
# All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
set -e
set -u
export PATH='/usr/local/bin:/usr/bin:/bin'
myself=${0##*/}
info()
{
echo "$myself: $*"
}
error()
{
echo >&2 "$myself: $*"
}
usage()
{
echo >&2 "Usage: $myself"
exit 2
}
mix_version()
{
# Mix insists on SemVer format.
local vsn="$(printf '%s' "$1" | sed 's/\.0/./')"
case $vsn in
*.*.*) printf '%s' "$vsn" ;;
*.*) printf '%s.0' "$vsn" ;;
esac
}
if ! [ -e 'mix.exs' ] || ! [ -e "tools/$myself" ]
then
error "Please call this script from the repository's root directory."
exit 2
elif [ $# -ne 0 ]
then
usage
fi
rel_name='ejabberd'
rel_vsn=$(git describe --tags | sed -e 's/-g.*//' -e 's/-/./' | tr -d '[:space:]')
mix_vsn=$(mix_version "$rel_vsn")
crosstool_vsn='1.26.0'
termcap_vsn='1.3.1'
expat_vsn='2.6.4'
zlib_vsn='1.3.1'
yaml_vsn='0.2.5'
ssl_vsn='3.3.2'
otp_vsn='26.2.5.4'
elixir_vsn='1.17.2'
pam_vsn='1.6.1'
png_vsn='1.6.43'
jpeg_vsn='9f'
webp_vsn='1.4.0'
gd_vsn='2.3.3'
odbc_vsn='2.3.12'
sqlite_vsn='3460100'
root_dir="${BUILD_DIR:-$HOME/build}"
bootstrap_dir="$root_dir/bootstrap"
ct_prefix_dir="$root_dir/x-tools"
build_dir="$root_dir/$rel_name"
crosstool_dir="crosstool-ng-$crosstool_vsn"
termcap_dir="termcap-$termcap_vsn"
expat_dir="expat-$expat_vsn"
zlib_dir="zlib-$zlib_vsn"
yaml_dir="yaml-$yaml_vsn"
ssl_dir="openssl-$ssl_vsn"
otp_dir="otp_src_$otp_vsn"
elixir_dir="elixir-$elixir_vsn"
pam_dir="Linux-PAM-$pam_vsn"
png_dir="libpng-$png_vsn"
jpeg_dir="jpeg-$jpeg_vsn"
webp_dir="libwebp-$webp_vsn"
gd_dir="libgd-$gd_vsn"
odbc_dir="unixODBC-$odbc_vsn"
sqlite_dir="sqlite-autoconf-$sqlite_vsn"
crosstool_tar="$crosstool_dir.tar.xz"
termcap_tar="$termcap_dir.tar.gz"
expat_tar="$expat_dir.tar.gz"
zlib_tar="$zlib_dir.tar.gz"
yaml_tar="$yaml_dir.tar.gz"
ssl_tar="$ssl_dir.tar.gz"
otp_tar="$otp_dir.tar.gz"
elixir_tar="v$elixir_vsn.tar.gz"
pam_tar="$pam_dir.tar.xz"
png_tar="$png_dir.tar.gz"
jpeg_tar="jpegsrc.v$jpeg_vsn.tar.gz"
webp_tar="$webp_dir.tar.gz"
gd_tar="$gd_dir.tar.gz"
sqlite_tar="$sqlite_dir.tar.gz"
odbc_tar="$odbc_dir.tar.gz"
rel_tar="$rel_name-$mix_vsn.tar.gz"
ct_jobs=$(nproc)
src_dir="$root_dir/src"
platform=$(gcc -dumpmachine)
targets='x86_64-linux-gnu aarch64-linux-gnu'
build_start=$(date '+%F %T')
have_current_deps='false'
dep_vsns_file="$build_dir/.dep_vsns"
dep_vsns=''
deps='crosstool
termcap
expat
zlib
yaml
ssl
otp
elixir
pam
png
jpeg
webp
gd
odbc
sqlite'
umask 022
#' Try to find a browser for checking dependency versions.
have_browser()
{
for browser in 'lynx' 'links' 'elinks'
do
$browser -dump 'https://ejabberd.im/' >'/dev/null' && return 0
done
return 1
}
#.
#' Check whether the given dependency version is up-to-date.
check_vsn()
{
local name="$1"
local our_vsn="$2"
local src_url="$3"
local reg_exp="$4"
local cur_vsn=$($browser -dump "$src_url" |
sed -n "s/.*$reg_exp.*/\\1/p" |
head -1)
if [ "$our_vsn" = "$cur_vsn" ]
then
return 0
else
error "Current $name version is: $cur_vsn"
error "But our $name version is: $our_vsn"
error "Update $0 or set CHECK_DEPS=false"
exit 1
fi
}
#.
#' Check whether our dependency versions are up-to-date.
check_configured_dep_vsns()
{
check_vsn 'OpenSSL' "$ssl_vsn" \
'https://openssl-library.org/source/' \
'openssl-\(3\.[1-9]\.[0-9.]*\)\.tar\.gz'
check_vsn 'LibYAML' "$yaml_vsn" \
'https://pyyaml.org/wiki/LibYAML' \
'yaml-\([0-9][0-9.]*\)\.tar\.gz'
check_vsn 'zlib' "$zlib_vsn" \
'https://zlib.net/' \
'zlib-\([1-9][0-9.]*\)\.tar\.gz'
check_vsn 'Expat' "$expat_vsn" \
'https://github.com/libexpat/libexpat/releases' \
'\([1-9]\.[0-9]*\.[0-9]*\)'
check_vsn 'Termcap' "$termcap_vsn" \
'https://ftp.gnu.org/gnu/termcap/' \
'termcap-\([1-9][0-9.]*\)\.tar\.gz'
check_vsn 'SQLite' "$sqlite_vsn" \
'https://www.sqlite.org/download.html' \
'sqlite-autoconf-\([1-9][0-9]*\)\.tar\.gz'
check_vsn 'ODBC' "$odbc_vsn" \
'http://www.unixodbc.org/download.html' \
'unixODBC-\([1-9][0-9.]*\)\.tar\.gz'
check_vsn 'Linux-PAM' "$pam_vsn" \
'https://github.com/linux-pam/linux-pam/releases' \
'[0-9]\]Linux-PAM \([1-9][0-9.]*\)'
check_vsn 'libpng' "$png_vsn" \
'http://www.libpng.org/pub/png/libpng.html' \
'libpng-\([1-9][0-9.]*\)\.tar\.gz'
check_vsn 'JPEG' "$jpeg_vsn" \
'https://www.ijg.org' \
'jpegsrc.v\([1-9][0-9a-z]*\)\.tar\.gz'
check_vsn 'WebP' "$webp_vsn" \
'https://developers.google.com/speed/webp/download' \
'libwebp-\([1-9][0-9.]*\)\.tar\.gz'
check_vsn 'LibGD' "$gd_vsn" \
'https://github.com/libgd/libgd/releases' \
'gd-\([1-9][0-9.]*\)'
check_vsn 'Elixir' "$elixir_vsn" \
'https://elixir-lang.org/install.html' \
'v\([1-9][0-9.]*\)\.tar\.gz'
}
#.
#' Check whether existing dependencies are up-to-date.
check_built_dep_vsns()
{
for dep in $deps
do
eval dep_vsns=\"\$dep_vsns\$${dep}_vsn\"
done
if [ -e "$dep_vsns_file" ]
then
if [ "$dep_vsns" = "$(cat "$dep_vsns_file")" ]
then have_current_deps='true'
fi
rm "$dep_vsns_file"
fi
}
#.
#' Save built dependency versions.
save_built_dep_vsns()
{
echo "$dep_vsns" >"$dep_vsns_file"
}
#.
#' Create common part of Crosstool-NG configuration file.
create_common_config()
{
local file="$1"
cat >"$file" <<-'EOF'
CT_CONFIG_VERSION="4"
CT_DOWNLOAD_AGENT_CURL=y
CT_OMIT_TARGET_VENDOR=y
CT_CC_LANG_CXX=y
CT_ARCH_64=y
CT_KERNEL_LINUX=y
CT_LINUX_V_3_16=y
CT_LOG_PROGRESS_BAR=n
EOF
}
#.
#' Create Crosstool-NG configuration file for glibc.
create_gnu_config()
{
local file="$1"
create_common_config "$file"
cat >>"$file" <<-'EOF'
CT_GLIBC_V_2_19=y
EOF
}
#.
#' Create Crosstool-NG configuration file for musl.
create_musl_config()
{
local file="$1"
create_common_config "$file"
cat >>"$file" <<-'EOF'
CT_EXPERIMENTAL=y
CT_LIBC_MUSL=y
EOF
}
#.
#' Create Crosstool-NG configuration file for x64.
create_x64_config()
{
local file="$1"
local libc="$2"
create_${libc}_config "$file"
cat >>"$file" <<-'EOF'
CT_ARCH_X86=y
EOF
}
#.
#' Create Crosstool-NG configuration file for arm64.
create_arm64_config()
{
local file="$1"
local libc="$2"
create_${libc}_config "$file"
cat >>"$file" <<-'EOF'
CT_ARCH_ARM=y
EOF
}
#.
#' Return our name for the given platform.
arch_name()
{
local target="$1"
case $target in
x86_64*)
printf 'x64'
;;
aarch64*)
printf 'arm64'
;;
*)
error "Unsupported target platform: $target"
exit 1
;;
esac
}
#.
#' Add native Erlang/OTP "bin" directory to PATH (for bootstrapping and Mix).
add_otp_path()
{
local mode="$1"
local prefix="$2"
if [ "$mode" = 'native' ]
then native_otp_bin="$prefix/bin"
elif [ -n "${INSTALL_DIR_FOR_OTP+x}" ] && [ -n "${INSTALL_DIR_FOR_ELIXIR+x}" ]
then
# For github runners to build for non-native systems:
# https://github.com/erlef/setup-beam#environment-variables
native_otp_bin="$INSTALL_DIR_FOR_OTP/bin"
native_elixir_bin="$INSTALL_DIR_FOR_ELIXIR/bin"
export PATH="$native_elixir_bin:$PATH"
fi
export PATH="$native_otp_bin:$PATH"
}
#.
#' Create and populate /opt/ejabberd directory.
create_data_dir()
{
local code_dir="$1"
local data_dir="$2"
mkdir "$data_dir" "$data_dir/database" "$data_dir/logs"
mv "$code_dir/conf" "$data_dir"
chmod 'o-rwx' "$data_dir/"*
curl -fsS -o "$data_dir/conf/cacert.pem" 'https://curl.se/ca/cacert.pem'
sed -i '/^loglevel:/a\
\
ca_file: /opt/ejabberd/conf/cacert.pem\
\
certfiles:\
- /opt/ejabberd/conf/server.pem' "$data_dir/conf/$rel_name.yml"
}
#.
#' Add systemd unit and init script.
add_systemd_unit()
{
local code_dir="$1"
sed -e "s|@ctlscriptpath@|/opt/$rel_name-$rel_vsn/bin|g" \
-e "s|@installuser@|$rel_name|g" 'ejabberd.service.template' \
>"$code_dir/bin/ejabberd.service"
sed -e "s|@ctlscriptpath@|/opt/$rel_name-$rel_vsn/bin|g" \
-e "s|@installuser@|$rel_name|g" 'ejabberd.init.template' \
>"$code_dir/bin/ejabberd.init"
chmod '+x' "$code_dir/bin/ejabberd.init"
}
#.
#' Add CAPTCHA script(s).
add_captcha_script()
{
local code_dir="$1"
cp -p 'tools/captcha'*'.sh' "$code_dir/lib"
}
#.
#' Use our VT100 to avoid depending on Terminfo, adjust options/paths.
edit_ejabberdctl()
{
local code_dir="$1"
sed -i \
-e "2iexport TERM='internal'" \
-e '/ERL_OPTIONS=/d' \
-e 's|_DIR:=".*}/|_DIR:="/opt/ejabberd/|' \
-e 's|/database|/database/$ERLANG_NODE|' \
-e 's|#vt100 ||' \
"$code_dir/bin/${rel_name}ctl"
}
#.
#' Delete unused files and directories, just to save some space.
remove_unused_files()
{
local code_dir="$1"
# Remove shared object file used only in test suite:
find "$code_dir/lib" -name 'otp_test_engine.so' -delete
# Remove shared object files of statically linked NIFs:
find "$code_dir/lib/crypto-"* "$code_dir/lib/asn1-"* \
'(' -name 'asn1rt_nif.so' -o \
-name 'crypto.so' -o \
-name 'lib' -o \
-name 'priv' ')' \
-delete
# Remove unused ERTS binaries (see systools_make:erts_binary_filter/0):
find "$code_dir/erts-"*'/bin' \
'(' -name 'ct_run' -o \
-name 'dialyzer' -o \
-name 'erlc' -o \
-name 'typer' -o \
-name 'yielding_c_fun' ')' \
-delete
# Remove unused Mix stuff:
find "$code_dir/bin" -name 'ejabberd' -delete
find "$code_dir/releases" -name 'COOKIE' -delete
}
#.
#' Strip ERTS binaries, shared objects, and BEAM files.
strip_files()
{
local code_dir="$1"
local strip_cmd="$2"
find "$code_dir/lib" \
-type f \
-name '*.so' \
-exec "$strip_cmd" -s '{}' '+'
find "$code_dir/erts-"*'/bin' "$code_dir/lib/"*'/priv/bin' \
-type f \
-perm '-u+x' \
-exec "$strip_cmd" -s '{}' '+' 2>'/dev/null' || :
erl -noinput -eval \
"{ok, _} = beam_lib:strip_release('$code_dir'), halt()"
}
#.
#' Build toochain for a given target.
build_toolchain()
{
local target="$1"
local prefix="$2"
local arch=$(arch_name "$target")
local libc="${target##*-}"
if [ -d "$prefix" ]
then
info "Using existing toolchain in $prefix ..."
else
if ! [ -x "$bootstrap_dir/bin/ct-ng" ]
then
info "Extracting Crosstool-NG $crosstool_vsn ..."
cd "$src_dir"
tar -xJf "$crosstool_tar"
cd "$OLDPWD"
info "Building Crosstool-NG $crosstool_vsn ..."
cd "$src_dir/$crosstool_dir"
./configure --prefix="$bootstrap_dir"
make V=0
make install
cd "$OLDPWD"
fi
info "Building toolchain for $arch-$libc ..."
cd "$root_dir"
create_${arch}_config 'defconfig' "$libc"
ct-ng defconfig
ct-ng build CT_PREFIX="$ct_prefix_dir" CT_JOBS="$ct_jobs"
rm -rf '.config' '.build' 'build.log'
cd "$OLDPWD"
fi
}
#.
#' Build target dependencies.
build_deps()
{
local mode="$1"
local target="$2"
local prefix="$3"
local arch="$(arch_name "$target")"
local libc="${target##*-}"
local target_src_dir="$prefix/src"
local saved_path="$PATH"
if [ "$mode" = 'cross' ]
then configure="./configure --host=$target --build=$platform"
else configure='./configure'
fi
mkdir "$prefix"
info 'Extracting dependencies ...'
mkdir "$target_src_dir"
cd "$target_src_dir"
tar -xzf "$src_dir/$termcap_tar"
tar -xzf "$src_dir/$sqlite_tar"
tar -xzf "$src_dir/$odbc_tar"
tar -xzf "$src_dir/$expat_tar"
tar -xzf "$src_dir/$zlib_tar"
tar -xzf "$src_dir/$yaml_tar"
tar -xzf "$src_dir/$ssl_tar"
tar -xzf "$src_dir/$otp_tar"
tar -xzf "$src_dir/$elixir_tar"
tar -xzf "$src_dir/$png_tar"
tar -xzf "$src_dir/$jpeg_tar"
tar -xzf "$src_dir/$webp_tar"
tar -xzf "$src_dir/$gd_tar"
tar -xJf "$src_dir/$pam_tar"
cd "$OLDPWD"
info "Building Termcap $termcap_vsn for $arch-$libc ..."
cd "$target_src_dir/$termcap_dir"
$configure --prefix="$prefix"
cat >'config.h' <<-'EOF'
#ifndef CONFIG_H
#define CONFIG_H
#define INTERNAL_TERMINAL "internal:\\\n" \
"\t:am:bs:ms:xn:xo:\\\n" \
"\t:co#80:it#8:li#24:vt#3:\\\n" \
"\t:@8=\\EOM:DO=\\E[%dB:K1=\\EOq:K2=\\EOr:K3=\\EOs:K4=\\EOp:K5=\\EOn:\\\n" \
"\t:LE=\\E[%dD:RA=\\E[?7l:RI=\\E[%dC:SA=\\E[?7h:UP=\\E[%dA:\\\n" \
"\t:ac=``aaffggjjkkllmmnnooppqqrrssttuuvvwwxxyyzz{{||}}~~:\\\n" \
"\t:ae=^O:as=^N:bl=^G:cb=\\E[1K:cd=\\E[J:ce=\\E[K:cl=\\E[H\\E[J:\\\n" \
"\t:cm=\\E[%i%d;%dH:cr=^M:cs=\\E[%i%d;%dr:ct=\\E[3g:do=^J:\\\n" \
"\t:eA=\\E(B\\E)0:ho=\\E[H:k0=\\EOy:k1=\\EOP:k2=\\EOQ:k3=\\EOR:\\\n" \
"\t:k4=\\EOS:k5=\\EOt:k6=\\EOu:k7=\\EOv:k8=\\EOl:k9=\\EOw:k;=\\EOx:\\\n" \
"\t:kb=^H:kd=\\EOB:ke=\\E[?1l\\E>:kl=\\EOD:kr=\\EOC:ks=\\E[?1h\\E=:\\\n" \
"\t:ku=\\EOA:le=^H:mb=\\E[5m:md=\\E[1m:me=\\E[m\\017:mr=\\E[7m:\\\n" \
"\t:nd=\\E[C:rc=\\E8:rs=\\E>\\E[?3l\\E[?4l\\E[?5l\\E[?7h\\E[?8h:\\\n" \
"\t:..sa=\\E[0%?%p1%p6%|%t;1%;%?%p2%t;4%;%?%p1%p3%|%t;7%;%?%p4%t;5%;m%?%p9%t\\016%e\\017%;:\\\n" \
"\t:sc=\\E7:se=\\E[m:sf=^J:so=\\E[7m:sr=\\EM:st=\\EH:ta=^I:ue=\\E[m:\\\n" \
"\t:up=\\E[A:us=\\E[4m:"
#endif
EOF
make CPPFLAGS="$CPPFLAGS -DHAVE_CONFIG_H=1"
make install
cd "$OLDPWD"
info "Building zlib $zlib_vsn for $arch-$libc ..."
cd "$target_src_dir/$zlib_dir"
CFLAGS="$CFLAGS -O3 -fPIC" ./configure --prefix="$prefix" --static
make
make install
cd "$OLDPWD"
info "Building OpenSSL $ssl_vsn for $arch-$libc ..."
cd "$target_src_dir/$ssl_dir"
CFLAGS="$CFLAGS -O3 -fPIC" \
./Configure no-shared no-module no-ui-console \
--prefix="$prefix" \
--openssldir="$prefix" \
--libdir='lib' \
"linux-${target%-linux-*}"
make build_libs
make install_dev
cd "$OLDPWD"
info "Building Expat $expat_vsn for $arch-$libc ..."
cd "$target_src_dir/$expat_dir"
$configure --prefix="$prefix" --enable-static --disable-shared \
--without-docbook \
CFLAGS="$CFLAGS -O3 -fPIC"
make
make install
cd "$OLDPWD"
info "Building LibYAML $yaml_vsn for $arch-$libc ..."
cd "$target_src_dir/$yaml_dir"
$configure --prefix="$prefix" --enable-static --disable-shared \
CFLAGS="$CFLAGS -O3 -fPIC"
make
make install
cd "$OLDPWD"
info "Building SQLite $sqlite_vsn for $arch-$libc ..."
cd "$target_src_dir/$sqlite_dir"
$configure --prefix="$prefix" --enable-static --disable-shared \
CFLAGS="$CFLAGS -O3 -fPIC"
make
make install
cd "$OLDPWD"
info "Building ODBC $odbc_vsn for $arch-$libc ..."
cd "$target_src_dir/$odbc_dir"
$configure --prefix="$prefix" --enable-static --disable-shared \
CFLAGS="$CFLAGS -O3 -fPIC"
make
make install
cd "$OLDPWD"
info "Building Linux-PAM $pam_vsn for $arch ..."
cd "$target_src_dir/$pam_dir"
$configure --prefix="$prefix" --includedir="$prefix/include/security" \
--enable-static --disable-shared --disable-doc --disable-examples \
--enable-db=no \
CFLAGS="$CFLAGS -O3 -fPIC"
make
make install
cd "$OLDPWD"
info "Building libpng $png_vsn for $arch-$libc ..."
cd "$target_src_dir/$png_dir"
$configure --prefix="$prefix" --enable-static --disable-shared \
CFLAGS="$CFLAGS -O3 -fPIC"
make
make install
cd "$OLDPWD"
info "Building JPEG $jpeg_vsn for $arch-$libc ..."
cd "$target_src_dir/$jpeg_dir"
$configure --prefix="$prefix" --enable-static --disable-shared \
CFLAGS="$CFLAGS -O3 -fPIC"
make
make install
cd "$OLDPWD"
info "Building WebP $webp_vsn for $arch-$libc ..."
cd "$target_src_dir/$webp_dir"
$configure --prefix="$prefix" --enable-static --disable-shared \
CFLAGS="$CFLAGS -O3 -fPIC"
make
make install
cd "$OLDPWD"
info "Building LibGD $gd_vsn for $arch-$libc ..."
cd "$target_src_dir/$gd_dir"
$configure --prefix="$prefix" --enable-static --disable-shared \
--with-zlib="$prefix" \
--with-webp="$prefix" \
--with-jpeg="$prefix" \
--with-png="$prefix" \
--without-avif \
--without-fontconfig \
--without-freetype \
--without-heif \
--without-libiconv-prefix \
--without-liq \
--without-raqm \
--without-tiff \
--without-x \
--without-xpm \
CFLAGS="$CFLAGS -O3 -fPIC"
make
make install
cd "$OLDPWD"
info "Building Erlang/OTP $otp_vsn for $arch-$libc ..."
if [ "$mode" = 'cross' ]
then
add_otp_path "$mode" "$prefix"
export erl_xcomp_sysroot="$prefix"
fi
cd "$target_src_dir/$otp_dir"
# Revert https://github.com/erlang/otp/commit/53ef5df40c733ce3d8215c5c98805f99f378f656
# because it breaks MSSQL, see https://github.com/processone/ejabberd/issues/4178
sed -i 's|if(size == 0 && (sql_type == SQL_LONGVARCHAR|if((sql_type == SQL_LONGVARCHAR|g' lib/odbc/c_src/odbcserver.c
# The additional CFLAGS/LIBS below are required by --enable-static-nifs.
# The "-ldl" flag specifically is only needed for ODBC, though.
$configure \
--prefix="$prefix" \
--with-ssl="$prefix" \
--with-odbc="$prefix" \
--without-javac \
--disable-dynamic-ssl-lib \
--enable-static-nifs \
CFLAGS="$CFLAGS -Wl,-L$prefix/lib" \
LIBS='-lcrypto -ldl'
make
make install
if [ "$mode" = 'native' ]
then add_otp_path "$mode" "$prefix"
else unset erl_xcomp_sysroot
fi
cd "$OLDPWD"
info "Building Elixir $elixir_vsn for $arch-$libc ..."
cd "$target_src_dir/$elixir_dir"
make install PREFIX="$prefix"
cd "$OLDPWD"
export PATH="$saved_path"
}
#.
#' Build the actual release.
build_rel()
{
local mode="$1"
local target="$2"
local prefix="$3"
local arch="$(arch_name "$target")"
local libc="${target##*-}"
local rel_dir="$PWD/_build/prod"
local target_data_dir="$prefix/$rel_name"
local target_dst_dir="$prefix/$rel_name-$rel_vsn"
local target_dst_tar="$rel_name-$rel_vsn-linux-$libc-$arch.tar.gz"
local saved_path="$PATH"
export PATH="$ct_prefix_dir/$target/bin:$PATH"
export CC="$target-gcc"
export CXX="$target-g++"
export CPP="$target-cpp"
export LD="$target-ld"
export AS="$target-as"
export AR="$target-ar"
export NM="$target-nm"
export RANLIB="$target-ranlib"
export OBJCOPY="$target-objcopy"
export STRIP="$target-strip"
export CPPFLAGS="-I$prefix/include"
export CFLAGS="-g0 -O2 -pipe -fomit-frame-pointer -static-libgcc $CPPFLAGS"
export CXXFLAGS="$CFLAGS -static-libstdc++"
export LDFLAGS="-L$prefix/lib -static-libgcc -static-libstdc++"
export ERL_COMPILER_OPTIONS='[no_debug_info]' # Building 25.x fails with 'deterministic'.
if [ "$mode" = 'cross' ]
then configure="./configure --host=$target --build=$platform"
else configure='./configure'
fi
if [ $have_current_deps = false ]
then build_deps "$mode" "$target" "$prefix"
fi
add_otp_path "$mode" "$prefix"
if [ "$mode" = 'native' ] # In order to only do this once.
then
info "Fetching Mix dependencies"
mix local.hex --force
mix local.rebar --force
fi
info "Removing old $rel_name builds"
rm -rf '_build' 'deps'
info "Building $rel_name $rel_vsn for $arch-$libc ..."
./autogen.sh
eimp_cflags='-fcommon'
eimp_libs='-lwebp -ljpeg -lpng -lz -lm'
export CC="$CC -Wl,-ldl" # Required by (statically linking) epam.
export LIBS="$eimp_libs -lcrypto -lpthread -ldl"
export CFLAGS="$CFLAGS $eimp_cflags"
export LDFLAGS="$LDFLAGS $eimp_libs"
if [ "$mode" = 'cross' ]
then
# Hand over --host/--build to configure scripts of dependencies.
export host_alias="$target"
export build_alias="$platform"
fi
# The cache variable makes cross compilation work.
ac_cv_erlang_root_dir="$prefix/lib/erlang" $configure \
--with-rebar='mix' \
--with-sqlite3="$prefix" \
--enable-user="$rel_name" \
--enable-all \
--disable-erlang-version-check
make deps
sed -i 's/ *-lstdc++//g' 'deps/'*'/rebar.config'* # Link statically.
if [ "$mode" = 'cross' ]
then
ln -s "$prefix/lib/erlang" 'lib/erlang'
erts_dir=$(ls -1d 'lib/erlang/erts-'*)
ei_inc="$prefix/lib/erlang/lib/erl_interface-"*'/include'
ei_lib="$prefix/lib/erlang/lib/erl_interface-"*'/lib'
export LDLIBS='-lpthread'
export ERL_EI_INCLUDE_DIR=$(ls -1d $ei_inc)
export ERL_EI_LIBDIR=$(ls -1d $ei_lib)
sed -i "/include_executables/a\\
include_erts: \"$erts_dir\"," 'mix.exs'
fi
make rel
if [ "$mode" = 'cross' ]
then
sed -i '/include_erts/d' 'mix.exs'
rm 'lib/erlang'
unset LDLIBS ERL_EI_INCLUDE_DIR ERL_EI_LIBDIR
unset host_alias build_alias
fi
info "Putting together $rel_name $rel_vsn archive for $arch-$libc ..."
mkdir "$target_dst_dir"
tar -C "$target_dst_dir" -xzf "$rel_dir/$rel_tar"
create_data_dir "$target_dst_dir" "$target_data_dir"
add_systemd_unit "$target_dst_dir"
add_captcha_script "$target_dst_dir"
edit_ejabberdctl "$target_dst_dir"
remove_unused_files "$target_dst_dir"
strip_files "$target_dst_dir" "$STRIP"
tar -C "$prefix" --owner="$rel_name" --group="$rel_name" -cf - \
"$rel_name" "$rel_name-$rel_vsn" | gzip -9 >"$target_dst_tar"
rm -rf "$target_dst_dir" "$target_data_dir"
info "Created $target_dst_tar successfully."
unset CC CXX CPP LD AS AR NM RANLIB OBJCOPY STRIP
unset CFLAGS CXXFLAGS LDFLAGS LIBS ERL_COMPILER_OPTIONS
export PATH="$saved_path"
}
#.
if [ "${CHECK_DEPS:-true}" = 'true' ]
then
if have_browser
then
check_configured_dep_vsns
else
error 'Cannot check dependency versions.'
error 'Install a browser or set CHECK_DEPS=false'
exit 1
fi
else
info "Won't check dependency versions."
fi
if ! mkdir -p "$root_dir"
then
error 'Set BUILD_DIR to a usable build directory path.'
exit 1
fi
check_built_dep_vsns
info 'Removing old bootstrap tools ...'
rm -rf "$bootstrap_dir"
mkdir "$bootstrap_dir"
if [ $have_current_deps = true ]
then
info 'Dependencies are up-to-date ...'
else
# Keep existing toolchains but rebuild everything else.
info 'Removing old builds ...'
rm -rf "$build_dir"
mkdir "$build_dir"
info 'Removing old source ...'
rm -rf "$src_dir"
mkdir "$src_dir"
info 'Downloading dependencies ...'
cd "$src_dir"
curl -fsSLO "https://github.com/crosstool-ng/crosstool-ng/releases/download/$crosstool_dir/$crosstool_tar"
curl -fsSLO "https://ftp.gnu.org/gnu/termcap/$termcap_tar"
curl -fsSLO "https://github.com/libexpat/libexpat/releases/download/R_$(printf '%s' "$expat_vsn" | sed 's/\./_/g')/$expat_tar"
curl -fsSLO "https://zlib.net/fossils/$zlib_tar"
curl -fsSLO "https://pyyaml.org/download/libyaml/$yaml_tar"
curl -fsSLO "https://github.com/openssl/openssl/releases/download/openssl-$ssl_vsn/$ssl_tar"
curl -fsSLO "https://github.com/erlang/otp/releases/download/OTP-$otp_vsn/$otp_tar"
curl -fsSLO "https://github.com/elixir-lang/elixir/archive/v$elixir_vsn.tar.gz"
curl -fsSLO "https://github.com/linux-pam/linux-pam/releases/download/v$pam_vsn/$pam_tar"
curl -fsSLO "https://download.sourceforge.net/libpng/$png_tar"
curl -fsSLO "https://www.ijg.org/files/$jpeg_tar"
curl -fsSLO "https://storage.googleapis.com/downloads.webmproject.org/releases/webp/$webp_tar"
curl -fsSLO "https://github.com/libgd/libgd/releases/download/gd-$gd_vsn/$gd_tar"
curl -fsSLO "http://www.unixodbc.org/$odbc_tar"
curl -fsSLO "https://www.sqlite.org/$(date '+%Y')/$sqlite_tar" \
|| curl -fsSLO "https://www.sqlite.org/$(date -d '1 year ago' '+%Y')/$sqlite_tar" \
|| curl -fsSLO "https://www.sqlite.org/$(date -d '2 years ago' '+%Y')/$sqlite_tar"
cd "$OLDPWD"
fi
mkdir "$bootstrap_dir/bin"
export PATH="$bootstrap_dir/bin:$PATH" # For ct-ng.
export LC_ALL='C.UTF-8' # Elixir insists on a UTF-8 environment.
for target in $targets
do
prefix="$build_dir/$target"
toolchain_dir="$ct_prefix_dir/$target"
if [ "$platform" = "$target" ]
then mode='native'
else mode='cross'
fi
build_toolchain "$target" "$toolchain_dir"
build_rel "$mode" "$target" "$prefix"
done
save_built_dep_vsns
info "Build started: $build_start"
info "Build ended: $(date '+%F %T')"
# vim:set foldmarker=#',#. foldmethod=marker:
ejabberd-24.12/tools/emacs-indent.sh 0000775 0001750 0001750 00000001466 14730775155 017733 0 ustar debalance debalance #!/bin/bash
# To indent and remove tabs, surround the piece of code with:
# %% @indent-begin
# %% @indent-end
#
# Then run:
# make indent
#
# Please note this script only indents the first occurrence.
FILES=$(git grep --name-only @indent-begin src/)
for FILENAME in $FILES; do
echo "==> Indenting $FILENAME..."
emacs -batch $FILENAME \
-f "erlang-mode" \
--eval "(goto-char (point-min))" \
--eval "(re-search-forward \"@indent-begin\" nil t)" \
--eval "(setq begin (line-beginning-position))" \
--eval "(re-search-forward \"@indent-end\" nil t)" \
--eval "(setq end (line-beginning-position))" \
--eval "(erlang-indent-region begin end)" \
--eval "(untabify begin end)" \
-f "delete-trailing-whitespace" \
-f "save-buffer"
done
ejabberd-24.12/.shellcheckrc 0000664 0001750 0001750 00000000142 14730775155 016306 0 ustar debalance debalance disable=SC2016,SC2086,SC2089,SC2090
external-sources=true
source=ejabberdctl.cfg.example
shell=sh
ejabberd-24.12/src/ 0000775 0001750 0001750 00000000000 14730775155 014445 5 ustar debalance debalance ejabberd-24.12/src/mod_admin_update_sql.erl 0000664 0001750 0001750 00000060503 14730775155 021325 0 ustar debalance debalance %%%-------------------------------------------------------------------
%%% File : mod_admin_update_sql.erl
%%% Author : Alexey Shchepin
%%% Purpose : Convert SQL DB to the new format
%%% Created : 9 Aug 2017 by Alexey Shchepin
%%%
%%%
%%% 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_admin_update_sql).
-author('alexey@process-one.net').
-behaviour(gen_mod).
-export([start/2, stop/1, reload/3, mod_options/1,
get_commands_spec/0, depends/2, mod_doc/0]).
% Commands API
-export([update_sql/0]).
% For testing
-export([update_sql/1]).
-include("logger.hrl").
-include("ejabberd_commands.hrl").
-include_lib("xmpp/include/xmpp.hrl").
-include("ejabberd_sql_pt.hrl").
-include("translate.hrl").
%%%
%%% gen_mod
%%%
start(_Host, _Opts) ->
ejabberd_commands:register_commands(?MODULE, get_commands_spec()).
stop(_Host) ->
ejabberd_commands:unregister_commands(get_commands_spec()).
reload(_Host, _NewOpts, _OldOpts) ->
ok.
depends(_Host, _Opts) ->
[].
%%%
%%% Register commands
%%%
get_commands_spec() ->
[#ejabberd_commands{name = update_sql, tags = [sql],
desc = "Convert MS SQL, MySQL or PostgreSQL DB to the new format",
note = "improved in 23.04",
module = ?MODULE, function = update_sql,
args = [],
args_example = [],
args_desc = [],
result = {res, rescode},
result_example = ok}
].
update_sql() ->
lists:foreach(
fun(Host) ->
case ejabberd_sql_sup:is_started(Host) of
false ->
ok;
true ->
update_sql(Host)
end
end, ejabberd_option:hosts()).
-record(state, {host :: binary(),
dbtype :: mysql | pgsql | sqlite | mssql | odbc,
escape}).
update_sql(Host) ->
LHost = jid:nameprep(Host),
DBType = ejabberd_option:sql_type(LHost),
IsSupported =
case DBType of
mssql -> true;
mysql -> true;
pgsql -> true;
_ -> false
end,
if
not IsSupported ->
io:format("Converting ~p DB is not supported~n", [DBType]),
error;
true ->
Escape =
case DBType of
mssql -> fun ejabberd_sql:standard_escape/1;
sqlite -> fun ejabberd_sql:standard_escape/1;
_ -> fun ejabberd_sql:escape/1
end,
State = #state{host = LHost,
dbtype = DBType,
escape = Escape},
update_tables(State),
check_config()
end.
check_config() ->
case ejabberd_sql:use_new_schema() of
true -> ok;
false ->
ejabberd_config:set_option(new_sql_schema, true),
io:format('~nNOTE: you must add "new_sql_schema: true" to ejabberd.yml before next restart~n~n', [])
end.
update_tables(State) ->
case add_sh_column(State, "users") of
true ->
drop_pkey(State, "users"),
add_pkey(State, "users", ["server_host", "username"]),
drop_sh_default(State, "users");
false ->
ok
end,
case add_sh_column(State, "last") of
true ->
drop_pkey(State, "last"),
add_pkey(State, "last", ["server_host", "username"]),
drop_sh_default(State, "last");
false ->
ok
end,
case add_sh_column(State, "rosterusers") of
true ->
drop_index(State, "rosterusers", "i_rosteru_user_jid"),
drop_index(State, "rosterusers", "i_rosteru_username"),
drop_index(State, "rosterusers", "i_rosteru_jid"),
create_unique_index(State, "rosterusers", "i_rosteru_sh_user_jid", ["server_host", "username", "jid"]),
create_index(State, "rosterusers", "i_rosteru_sh_jid", ["server_host", "jid"]),
drop_sh_default(State, "rosterusers");
false ->
ok
end,
case add_sh_column(State, "rostergroups") of
true ->
drop_index(State, "rostergroups", "pk_rosterg_user_jid"),
create_index(State, "rostergroups", "i_rosterg_sh_user_jid", ["server_host", "username", "jid"]),
drop_sh_default(State, "rostergroups");
false ->
ok
end,
case add_sh_column(State, "sr_group") of
true ->
drop_index(State, "sr_group", "i_sr_group_name"),
create_unique_index(State, "sr_group", "i_sr_group_sh_name", ["server_host", "name"]),
drop_sh_default(State, "sr_group");
false ->
ok
end,
case add_sh_column(State, "sr_user") of
true ->
drop_index(State, "sr_user", "i_sr_user_jid_grp"),
drop_index(State, "sr_user", "i_sr_user_jid"),
drop_index(State, "sr_user", "i_sr_user_grp"),
create_unique_index(State, "sr_user", "i_sr_user_sh_jid_grp", ["server_host", "jid", "grp"]),
create_index(State, "sr_user", "i_sr_user_sh_grp", ["server_host", "grp"]),
drop_sh_default(State, "sr_user");
false ->
ok
end,
case add_sh_column(State, "spool") of
true ->
drop_index(State, "spool", "i_despool"),
create_index(State, "spool", "i_spool_sh_username", ["server_host", "username"]),
drop_sh_default(State, "spool");
false ->
ok
end,
case add_sh_column(State, "archive") of
true ->
drop_index(State, "archive", "i_username"),
drop_index(State, "archive", "i_username_timestamp"),
drop_index(State, "archive", "i_timestamp"),
drop_index(State, "archive", "i_peer"),
drop_index(State, "archive", "i_bare_peer"),
drop_index(State, "archive", "i_username_peer"),
drop_index(State, "archive", "i_username_bare_peer"),
create_index(State, "archive", "i_archive_sh_username_timestamp", ["server_host", "username", "timestamp"]),
create_index(State, "archive", "i_archive_sh_timestamp", ["server_host", "timestamp"]),
create_index(State, "archive", "i_archive_sh_username_peer", ["server_host", "username", "peer"]),
create_index(State, "archive", "i_archive_sh_username_bare_peer", ["server_host", "username", "bare_peer"]),
drop_sh_default(State, "archive");
false ->
ok
end,
case add_sh_column(State, "archive_prefs") of
true ->
drop_pkey(State, "archive_prefs"),
add_pkey(State, "archive_prefs", ["server_host", "username"]),
drop_sh_default(State, "archive_prefs");
false ->
ok
end,
case add_sh_column(State, "vcard") of
true ->
drop_pkey(State, "vcard"),
add_pkey(State, "vcard", ["server_host", "username"]),
drop_sh_default(State, "vcard");
false ->
ok
end,
case add_sh_column(State, "vcard_search") of
true ->
drop_pkey(State, "vcard_search"),
drop_index(State, "vcard_search", "i_vcard_search_lfn"),
drop_index(State, "vcard_search", "i_vcard_search_lfamily"),
drop_index(State, "vcard_search", "i_vcard_search_lgiven"),
drop_index(State, "vcard_search", "i_vcard_search_lmiddle"),
drop_index(State, "vcard_search", "i_vcard_search_lnickname"),
drop_index(State, "vcard_search", "i_vcard_search_lbday"),
drop_index(State, "vcard_search", "i_vcard_search_lctry"),
drop_index(State, "vcard_search", "i_vcard_search_llocality"),
drop_index(State, "vcard_search", "i_vcard_search_lemail"),
drop_index(State, "vcard_search", "i_vcard_search_lorgname"),
drop_index(State, "vcard_search", "i_vcard_search_lorgunit"),
add_pkey(State, "vcard_search", ["server_host", "lusername"]),
create_index(State, "vcard_search", "i_vcard_search_sh_lfn", ["server_host", "lfn"]),
create_index(State, "vcard_search", "i_vcard_search_sh_lfamily", ["server_host", "lfamily"]),
create_index(State, "vcard_search", "i_vcard_search_sh_lgiven", ["server_host", "lgiven"]),
create_index(State, "vcard_search", "i_vcard_search_sh_lmiddle", ["server_host", "lmiddle"]),
create_index(State, "vcard_search", "i_vcard_search_sh_lnickname", ["server_host", "lnickname"]),
create_index(State, "vcard_search", "i_vcard_search_sh_lbday", ["server_host", "lbday"]),
create_index(State, "vcard_search", "i_vcard_search_sh_lctry", ["server_host", "lctry"]),
create_index(State, "vcard_search", "i_vcard_search_sh_llocality", ["server_host", "llocality"]),
create_index(State, "vcard_search", "i_vcard_search_sh_lemail", ["server_host", "lemail"]),
create_index(State, "vcard_search", "i_vcard_search_sh_lorgname", ["server_host", "lorgname"]),
create_index(State, "vcard_search", "i_vcard_search_sh_lorgunit", ["server_host", "lorgunit"]),
drop_sh_default(State, "vcard_search");
false ->
ok
end,
case add_sh_column(State, "privacy_default_list") of
true ->
drop_pkey(State, "privacy_default_list"),
add_pkey(State, "privacy_default_list", ["server_host", "username"]),
drop_sh_default(State, "privacy_default_list");
false ->
ok
end,
case add_sh_column(State, "privacy_list") of
true ->
drop_index(State, "privacy_list", "i_privacy_list_username"),
drop_index(State, "privacy_list", "i_privacy_list_username_name"),
create_unique_index(State, "privacy_list", "i_privacy_list_sh_username_name", ["server_host", "username", "name"]),
drop_sh_default(State, "privacy_list");
false ->
ok
end,
case add_sh_column(State, "private_storage") of
true ->
drop_index(State, "private_storage", "i_private_storage_username"),
drop_index(State, "private_storage", "i_private_storage_username_namespace"),
add_pkey(State, "private_storage", ["server_host", "username", "namespace"]),
drop_sh_default(State, "private_storage");
false ->
ok
end,
case add_sh_column(State, "roster_version") of
true ->
drop_pkey(State, "roster_version"),
add_pkey(State, "roster_version", ["server_host", "username"]),
drop_sh_default(State, "roster_version");
false ->
ok
end,
case add_sh_column(State, "muc_room") of
true ->
drop_sh_default(State, "muc_room");
false ->
ok
end,
case add_sh_column(State, "muc_registered") of
true ->
drop_sh_default(State, "muc_registered");
false ->
ok
end,
case add_sh_column(State, "muc_online_room") of
true ->
drop_sh_default(State, "muc_online_room");
false ->
ok
end,
case add_sh_column(State, "muc_online_users") of
true ->
drop_sh_default(State, "muc_online_users");
false ->
ok
end,
case add_sh_column(State, "motd") of
true ->
drop_pkey(State, "motd"),
add_pkey(State, "motd", ["server_host", "username"]),
drop_sh_default(State, "motd");
false ->
ok
end,
case add_sh_column(State, "sm") of
true ->
drop_index(State, "sm", "i_sm_sid"),
drop_index(State, "sm", "i_sm_username"),
add_pkey(State, "sm", ["usec", "pid"]),
create_index(State, "sm", "i_sm_sh_username", ["server_host", "username"]),
drop_sh_default(State, "sm");
false ->
ok
end,
case add_sh_column(State, "push_session") of
true ->
drop_index(State, "push_session", "i_push_usn"),
drop_index(State, "push_session", "i_push_ut"),
create_unique_index(State, "push_session", "i_push_session_susn", ["server_host", "username", "service", "node"]),
create_index(State, "push_session", "i_push_session_sh_username_timestamp", ["server_host", "username", "timestamp"]),
drop_sh_default(State, "push_session");
false ->
ok
end,
case add_sh_column(State, "mix_pam") of
true ->
drop_index(State, "mix_pam", "i_mix_pam"),
drop_index(State, "mix_pam", "i_mix_pam_u"),
drop_index(State, "mix_pam", "i_mix_pam_us"),
create_unique_index(State, "mix_pam", "i_mix_pam", ["username", "server_host", "channel", "service"]),
drop_sh_default(State, "mix_pam");
false ->
ok
end,
case add_sh_column(State, "mqtt_pub") of
true ->
drop_index(State, "mqtt_pub", "i_mqtt_topic"),
create_unique_index(State, "mqtt_pub", "i_mqtt_topic_server", ["topic", "server_host"]),
drop_sh_default(State, "mqtt_pub");
false ->
ok
end,
ok.
check_sh_column(#state{dbtype = mysql} = State, Table) ->
DB = ejabberd_option:sql_database(State#state.host),
sql_query(
State#state.host,
["SELECT 1 FROM information_schema.columns ",
"WHERE table_name = '", Table, "' AND column_name = 'server_host' ",
"AND table_schema = '", (State#state.escape)(DB), "' ",
"GROUP BY table_name, column_name;"], false);
check_sh_column(State, Table) ->
DB = ejabberd_option:sql_database(State#state.host),
sql_query(
State#state.host,
["SELECT 1 FROM information_schema.columns ",
"WHERE table_name = '", Table, "' AND column_name = 'server_host' ",
"AND table_catalog = '", (State#state.escape)(DB), "' ",
"GROUP BY table_name, column_name;"], false).
add_sh_column(State, Table) ->
case check_sh_column(State, Table) of
true -> false;
false ->
do_add_sh_column(State, Table),
true
end.
do_add_sh_column(#state{dbtype = pgsql} = State, Table) ->
sql_query(
State#state.host,
["ALTER TABLE ", Table, " ADD COLUMN server_host text NOT NULL DEFAULT '",
(State#state.escape)(State#state.host),
"';"]);
do_add_sh_column(#state{dbtype = mssql} = State, Table) ->
sql_query(
State#state.host,
["ALTER TABLE [", Table, "] ADD [server_host] varchar (250) NOT NULL ",
"CONSTRAINT [server_host_default] DEFAULT '",
(State#state.escape)(State#state.host),
"';"]);
do_add_sh_column(#state{dbtype = mysql} = State, Table) ->
sql_query(
State#state.host,
["ALTER TABLE ", Table, " ADD COLUMN server_host varchar(191) NOT NULL DEFAULT '",
(State#state.escape)(State#state.host),
"';"]).
drop_pkey(#state{dbtype = pgsql} = State, Table) ->
sql_query(
State#state.host,
["ALTER TABLE ", Table, " DROP CONSTRAINT ", Table, "_pkey;"]);
drop_pkey(#state{dbtype = mssql} = State, Table) ->
sql_query(
State#state.host,
["ALTER TABLE [", Table, "] DROP CONSTRAINT [", Table, "_PRIMARY];"]);
drop_pkey(#state{dbtype = mysql} = State, Table) ->
sql_query(
State#state.host,
["ALTER TABLE ", Table, " DROP PRIMARY KEY;"]).
add_pkey(#state{dbtype = pgsql} = State, Table, Cols) ->
SCols = string:join(Cols, ", "),
sql_query(
State#state.host,
["ALTER TABLE ", Table, " ADD PRIMARY KEY (", SCols, ");"]);
add_pkey(#state{dbtype = mssql} = State, Table, Cols) ->
SCols = string:join(Cols, "], ["),
sql_query(
State#state.host,
["ALTER TABLE [", Table, "] ADD CONSTRAINT [", Table, "_PRIMARY] PRIMARY KEY CLUSTERED ([", SCols, "]) ",
"WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ",
"ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY];"]);
add_pkey(#state{dbtype = mysql} = State, Table, Cols) ->
Cols2 = [C ++ mysql_keylen(Table, C) || C <- Cols],
SCols = string:join(Cols2, ", "),
sql_query(
State#state.host,
["ALTER TABLE ", Table, " ADD PRIMARY KEY (", SCols, ");"]).
drop_sh_default(#state{dbtype = pgsql} = State, Table) ->
sql_query(
State#state.host,
["ALTER TABLE ", Table, " ALTER COLUMN server_host DROP DEFAULT;"]);
drop_sh_default(#state{dbtype = mssql} = State, Table) ->
sql_query(
State#state.host,
["ALTER TABLE [", Table, "] DROP CONSTRAINT [server_host_default];"]);
drop_sh_default(#state{dbtype = mysql} = State, Table) ->
sql_query(
State#state.host,
["ALTER TABLE ", Table, " ALTER COLUMN server_host DROP DEFAULT;"]).
check_index(#state{dbtype = pgsql} = State, Table, Index) ->
sql_query(
State#state.host,
["SELECT 1 FROM pg_indexes WHERE tablename = '", Table,
"' AND indexname = '", Index, "';"], false);
check_index(#state{dbtype = mssql} = State, Table, Index) ->
sql_query(
State#state.host,
["SELECT 1 FROM sys.tables t ",
"INNER JOIN sys.indexes i ON i.object_id = t.object_id ",
"WHERE i.index_id > 0 ",
"AND i.name = '", Index, "' ",
"AND t.name = '", Table, "';"], false);
check_index(#state{dbtype = mysql} = State, Table, Index) ->
DB = ejabberd_option:sql_database(State#state.host),
sql_query(
State#state.host,
["SELECT 1 FROM information_schema.statistics ",
"WHERE table_name = '", Table, "' AND index_name = '", Index, "' ",
"AND table_schema = '", (State#state.escape)(DB), "' ",
"GROUP BY table_name, index_name;"], false).
drop_index(State, Table, Index) ->
OldIndex = old_index_name(State#state.dbtype, Index),
case check_index(State, Table, OldIndex) of
true -> do_drop_index(State, Table, OldIndex);
false -> ok
end.
do_drop_index(#state{dbtype = pgsql} = State, _Table, Index) ->
sql_query(
State#state.host,
["DROP INDEX ", Index, ";"]);
do_drop_index(#state{dbtype = mssql} = State, Table, Index) ->
sql_query(
State#state.host,
["DROP INDEX [", Index, "] ON [", Table, "];"]);
do_drop_index(#state{dbtype = mysql} = State, Table, Index) ->
sql_query(
State#state.host,
["ALTER TABLE ", Table, " DROP INDEX ", Index, ";"]).
create_unique_index(#state{dbtype = pgsql} = State, Table, Index, Cols) ->
SCols = string:join(Cols, ", "),
sql_query(
State#state.host,
["CREATE UNIQUE INDEX ", Index, " ON ", Table, " USING btree (",
SCols, ");"]);
create_unique_index(#state{dbtype = mssql} = State, Table, "i_privacy_list_sh_username_name" = Index, Cols) ->
create_index(State, Table, Index, Cols);
create_unique_index(#state{dbtype = mssql} = State, Table, Index, Cols) ->
SCols = string:join(Cols, ", "),
sql_query(
State#state.host,
["CREATE UNIQUE ", mssql_clustered(Index), "INDEX [", new_index_name(State#state.dbtype, Index), "] ",
"ON [", Table, "] (", SCols, ") ",
"WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON);"]);
create_unique_index(#state{dbtype = mysql} = State, Table, Index, Cols) ->
Cols2 = [C ++ mysql_keylen(Index, C) || C <- Cols],
SCols = string:join(Cols2, ", "),
sql_query(
State#state.host,
["CREATE UNIQUE INDEX ", Index, " ON ", Table, "(",
SCols, ");"]).
create_index(#state{dbtype = pgsql} = State, Table, Index, Cols) ->
NewIndex = new_index_name(State#state.dbtype, Index),
SCols = string:join(Cols, ", "),
sql_query(
State#state.host,
["CREATE INDEX ", NewIndex, " ON ", Table, " USING btree (",
SCols, ");"]);
create_index(#state{dbtype = mssql} = State, Table, Index, Cols) ->
NewIndex = new_index_name(State#state.dbtype, Index),
SCols = string:join(Cols, ", "),
sql_query(
State#state.host,
["CREATE INDEX [", NewIndex, "] ON [", Table, "] (", SCols, ") ",
"WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON);"]);
create_index(#state{dbtype = mysql} = State, Table, Index, Cols) ->
NewIndex = new_index_name(State#state.dbtype, Index),
Cols2 = [C ++ mysql_keylen(NewIndex, C) || C <- Cols],
SCols = string:join(Cols2, ", "),
sql_query(
State#state.host,
["CREATE INDEX ", NewIndex, " ON ", Table, "(",
SCols, ");"]).
old_index_name(mssql, "i_bare_peer") -> "archive_bare_peer";
old_index_name(mssql, "i_peer") -> "archive_peer";
old_index_name(mssql, "i_timestamp") -> "archive_timestamp";
old_index_name(mssql, "i_username") -> "archive_username";
old_index_name(mssql, "i_username_bare_peer") -> "archive_username_bare_peer";
old_index_name(mssql, "i_username_peer") -> "archive_username_peer";
old_index_name(mssql, "i_username_timestamp") -> "archive_username_timestamp";
old_index_name(mssql, "i_push_usn") -> "i_push_usn";
old_index_name(mssql, "i_push_ut") -> "i_push_ut";
old_index_name(mssql, "pk_rosterg_user_jid") -> "rostergroups_username_jid";
old_index_name(mssql, "i_rosteru_jid") -> "rosterusers_jid";
old_index_name(mssql, "i_rosteru_username") -> "rosterusers_username";
old_index_name(mssql, "i_rosteru_user_jid") -> "rosterusers_username_jid";
old_index_name(mssql, "i_despool") -> "spool_username";
old_index_name(mssql, "i_sr_user_jid_grp") -> "sr_user_jid_group";
old_index_name(mssql, Index) -> string:substr(Index, 3);
old_index_name(_Type, Index) -> Index.
new_index_name(mssql, "i_rosterg_sh_user_jid") -> "rostergroups_sh_username_jid";
new_index_name(mssql, "i_rosteru_sh_jid") -> "rosterusers_sh_jid";
new_index_name(mssql, "i_rosteru_sh_user_jid") -> "rosterusers_sh_username_jid";
new_index_name(mssql, "i_sr_user_sh_jid_grp") -> "sr_user_sh_jid_group";
new_index_name(mssql, Index) -> string:substr(Index, 3);
new_index_name(_Type, Index) -> Index.
mssql_clustered("i_mix_pam") -> "";
mssql_clustered("i_push_session_susn") -> "";
mssql_clustered(_) -> "CLUSTERED ".
mysql_keylen(_, "bare_peer") -> "(191)";
mysql_keylen(_, "channel") -> "(191)";
mysql_keylen(_, "domain") -> "(75)";
mysql_keylen(_, "jid") -> "(75)";
mysql_keylen(_, "name") -> "(75)";
mysql_keylen(_, "node") -> "(75)";
mysql_keylen(_, "peer") -> "(191)";
mysql_keylen(_, "pid") -> "(75)";
mysql_keylen(_, "server_host") -> "(191)";
mysql_keylen(_, "service") -> "(191)";
mysql_keylen(_, "topic") -> "(191)";
mysql_keylen("i_privacy_list_sh_username_name", "username") -> "(75)";
mysql_keylen("i_rosterg_sh_user_jid", "username") -> "(75)";
mysql_keylen("i_rosteru_sh_user_jid", "username") -> "(75)";
mysql_keylen(_, "username") -> "(191)";
mysql_keylen(_, _) -> "".
sql_query(Host, Query) ->
sql_query(Host, Query, true).
sql_query(Host, Query, Log) ->
case Log of
true -> io:format("executing \"~ts\" on ~ts~n", [Query, Host]);
false -> ok
end,
case ejabberd_sql:sql_query(Host, Query) of
{selected, _Cols, []} ->
false;
{selected, _Cols, [_Rows]} ->
true;
{error, Error} ->
io:format("error: ~p~n", [Error]),
false;
_ ->
ok
end.
mod_options(_) -> [].
mod_doc() ->
#{desc =>
?T("This module can be used to update existing SQL database "
"from the default to the new schema. Check the section "
"_`database.md#default-and-new-schemas|Default and New Schemas`_ for details. "
"Please note that only MS SQL, MySQL, and PostgreSQL are supported. "
"When the module is loaded use _`update_sql`_ API.")}.
ejabberd-24.12/src/mod_shared_roster_mnesia.erl 0000664 0001750 0001750 00000014327 14730775155 022217 0 ustar debalance debalance %%%-------------------------------------------------------------------
%%% File : mod_shared_roster_mnesia.erl
%%% Author : Evgeny Khramtsov
%%% Created : 14 Apr 2016 by Evgeny Khramtsov
%%%
%%%
%%% 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_shared_roster_mnesia).
-behaviour(mod_shared_roster).
%% API
-export([init/2, list_groups/1, groups_with_opts/1, create_group/3,
delete_group/2, get_group_opts/2, set_group_opts/3,
get_user_groups/2, get_group_explicit_users/2,
get_user_displayed_groups/3, is_user_in_group/3,
add_user_to_group/3, remove_user_from_group/3, import/3]).
-export([need_transform/1, transform/1, use_cache/1]).
-include("mod_roster.hrl").
-include("mod_shared_roster.hrl").
-include("logger.hrl").
-include_lib("xmpp/include/xmpp.hrl").
%%%===================================================================
%%% API
%%%===================================================================
init(_Host, _Opts) ->
ejabberd_mnesia:create(?MODULE, sr_group,
[{disc_copies, [node()]},
{attributes, record_info(fields, sr_group)}]),
ejabberd_mnesia:create(?MODULE, sr_user,
[{disc_copies, [node()]}, {type, bag},
{attributes, record_info(fields, sr_user)},
{index, [group_host]}]).
list_groups(Host) ->
mnesia:dirty_select(sr_group,
[{#sr_group{group_host = {'$1', '$2'}, _ = '_'},
[{'==', '$2', Host}], ['$1']}]).
-spec use_cache(binary()) -> boolean().
use_cache(_Host) ->
false.
groups_with_opts(Host) ->
Gs = mnesia:dirty_select(sr_group,
[{#sr_group{group_host = {'$1', Host}, opts = '$2',
_ = '_'},
[], [['$1', '$2']]}]),
lists:map(fun ([G, O]) -> {G, O} end, Gs).
create_group(Host, Group, Opts) ->
R = #sr_group{group_host = {Group, Host}, opts = Opts},
F = fun () -> mnesia:write(R) end,
mnesia:transaction(F).
delete_group(Host, Group) ->
GroupHost = {Group, Host},
F = fun () ->
mnesia:delete({sr_group, GroupHost}),
Users = mnesia:index_read(sr_user, GroupHost,
#sr_user.group_host),
lists:foreach(fun (UserEntry) ->
mnesia:delete_object(UserEntry)
end,
Users)
end,
mnesia:transaction(F).
get_group_opts(Host, Group) ->
case catch mnesia:dirty_read(sr_group, {Group, Host}) of
[#sr_group{opts = Opts}] -> {ok, Opts};
_ -> error
end.
set_group_opts(Host, Group, Opts) ->
R = #sr_group{group_host = {Group, Host}, opts = Opts},
F = fun () -> mnesia:write(R) end,
mnesia:transaction(F).
get_user_groups(US, Host) ->
case catch mnesia:dirty_read(sr_user, US) of
Rs when is_list(Rs) ->
[Group || #sr_user{group_host = {Group, H}} <- Rs, H == Host];
_ ->
[]
end.
get_group_explicit_users(Host, Group) ->
Read = (catch mnesia:dirty_index_read(sr_user,
{Group, Host}, #sr_user.group_host)),
case Read of
Rs when is_list(Rs) -> [R#sr_user.us || R <- Rs];
_ -> []
end.
get_user_displayed_groups(LUser, LServer, GroupsOpts) ->
case catch mnesia:dirty_read(sr_user, {LUser, LServer}) of
Rs when is_list(Rs) ->
[{Group, proplists:get_value(Group, GroupsOpts, [])}
|| #sr_user{group_host = {Group, H}} <- Rs,
H == LServer];
_ ->
[]
end.
is_user_in_group(US, Group, Host) ->
case mnesia:dirty_match_object(
#sr_user{us = US, group_host = {Group, Host}}) of
[] -> false;
_ -> true
end.
add_user_to_group(Host, US, Group) ->
R = #sr_user{us = US, group_host = {Group, Host}},
F = fun () -> mnesia:write(R) end,
mnesia:transaction(F).
remove_user_from_group(Host, US, Group) ->
R = #sr_user{us = US, group_host = {Group, Host}},
F = fun () -> mnesia:delete_object(R) end,
mnesia:transaction(F).
import(LServer, <<"sr_group">>, [Group, SOpts, _TimeStamp]) ->
G = #sr_group{group_host = {Group, LServer},
opts = ejabberd_sql:decode_term(SOpts)},
mnesia:dirty_write(G);
import(LServer, <<"sr_user">>, [SJID, Group, _TimeStamp]) ->
#jid{luser = U, lserver = S} = jid:decode(SJID),
User = #sr_user{us = {U, S}, group_host = {Group, LServer}},
mnesia:dirty_write(User).
need_transform({sr_group, {G, H}, _})
when is_list(G) orelse is_list(H) ->
?INFO_MSG("Mnesia table 'sr_group' will be converted to binary", []),
true;
need_transform({sr_user, {U, S}, {G, H}})
when is_list(U) orelse is_list(S) orelse is_list(G) orelse is_list(H) ->
?INFO_MSG("Mnesia table 'sr_user' will be converted to binary", []),
true;
need_transform({sr_group, {_, _}, [{name, _} | _]}) ->
?INFO_MSG("Mnesia table 'sr_group' will be converted from option Name to Label", []),
true;
need_transform(_) ->
false.
transform(#sr_group{group_host = {G, _H}, opts = Opts} = R)
when is_binary(G) ->
Opts2 = case proplists:get_value(name, Opts, false) of
false -> Opts;
Name -> [{label, Name} | proplists:delete(name, Opts)]
end,
R#sr_group{opts = Opts2};
transform(#sr_group{group_host = {G, H}, opts = Opts} = R) ->
R#sr_group{group_host = {iolist_to_binary(G), iolist_to_binary(H)},
opts = mod_shared_roster:opts_to_binary(Opts)};
transform(#sr_user{us = {U, S}, group_host = {G, H}} = R) ->
R#sr_user{us = {iolist_to_binary(U), iolist_to_binary(S)},
group_host = {iolist_to_binary(G), iolist_to_binary(H)}}.
%%%===================================================================
%%% Internal functions
%%%===================================================================
ejabberd-24.12/src/mod_mix_pam_sql.erl 0000664 0001750 0001750 00000011142 14730775155 020320 0 ustar debalance debalance %%%-------------------------------------------------------------------
%%% Author : Evgeny Khramtsov
%%% Created : 4 Dec 2018 by Evgeny Khramtsov
%%%
%%%
%%% ejabberd, Copyright (C) 2002-2018 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_mix_pam_sql).
-behaviour(mod_mix_pam).
%% API
-export([init/2, add_channel/3, get_channel/2,
get_channels/1, del_channel/2, del_channels/1]).
-export([sql_schemas/0]).
-include("logger.hrl").
-include("ejabberd_sql_pt.hrl").
%%%===================================================================
%%% API
%%%===================================================================
init(Host, _Opts) ->
ejabberd_sql_schema:update_schema(Host, ?MODULE, sql_schemas()),
ok.
sql_schemas() ->
[#sql_schema{
version = 1,
tables =
[#sql_table{
name = <<"mix_pam">>,
columns =
[#sql_column{name = <<"username">>, type = text},
#sql_column{name = <<"server_host">>, type = text},
#sql_column{name = <<"channel">>, type = text},
#sql_column{name = <<"service">>, type = text},
#sql_column{name = <<"id">>, type = text},
#sql_column{name = <<"created_at">>, type = timestamp,
default = true}],
indices = [#sql_index{
columns = [<<"username">>, <<"server_host">>,
<<"channel">>, <<"service">>],
unique = true}]}]}].
add_channel(User, Channel, ID) ->
{LUser, LServer, _} = jid:tolower(User),
{Chan, Service, _} = jid:tolower(Channel),
case ?SQL_UPSERT(LServer, "mix_pam",
["!channel=%(Chan)s",
"!service=%(Service)s",
"!username=%(LUser)s",
"!server_host=%(LServer)s",
"id=%(ID)s"]) of
ok -> ok;
_Err -> {error, db_failure}
end.
get_channel(User, Channel) ->
{LUser, LServer, _} = jid:tolower(User),
{Chan, Service, _} = jid:tolower(Channel),
case ejabberd_sql:sql_query(
LServer,
?SQL("select @(id)s from mix_pam where "
"channel=%(Chan)s and service=%(Service)s "
"and username=%(LUser)s and %(LServer)H")) of
{selected, [{ID}]} -> {ok, ID};
{selected, []} -> {error, notfound};
_Err -> {error, db_failure}
end.
get_channels(User) ->
{LUser, LServer, _} = jid:tolower(User),
SQL = ?SQL("select @(channel)s, @(service)s, @(id)s from mix_pam "
"where username=%(LUser)s and %(LServer)H"),
case ejabberd_sql:sql_query(LServer, SQL) of
{selected, Ret} ->
{ok, lists:filtermap(
fun({Chan, Service, ID}) ->
case jid:make(Chan, Service) of
error ->
report_corrupted(SQL),
false;
JID ->
{true, {JID, ID}}
end
end, Ret)};
_Err ->
{error, db_failure}
end.
del_channel(User, Channel) ->
{LUser, LServer, _} = jid:tolower(User),
{Chan, Service, _} = jid:tolower(Channel),
case ejabberd_sql:sql_query(
LServer,
?SQL("delete from mix_pam where "
"channel=%(Chan)s and service=%(Service)s "
"and username=%(LUser)s and %(LServer)H")) of
{updated, _} -> ok;
_Err -> {error, db_failure}
end.
del_channels(User) ->
{LUser, LServer, _} = jid:tolower(User),
case ejabberd_sql:sql_query(
LServer,
?SQL("delete from mix_pam where "
"username=%(LUser)s and %(LServer)H")) of
{updated, _} -> ok;
_Err -> {error, db_failure}
end.
%%%===================================================================
%%% Internal functions
%%%===================================================================
-spec report_corrupted(#sql_query{}) -> ok.
report_corrupted(SQL) ->
?ERROR_MSG("Corrupted values returned by SQL request: ~ts",
[SQL#sql_query.hash]).
ejabberd-24.12/src/ejabberd_logger.erl 0000664 0001750 0001750 00000033663 14730775155 020261 0 ustar debalance debalance %%%-------------------------------------------------------------------
%%% File : ejabberd_logger.erl
%%% Author : Evgeniy Khramtsov
%%% Purpose : ejabberd logger wrapper
%%% Created : 12 May 2013 by Evgeniy Khramtsov
%%%
%%%
%%% ejabberd, Copyright (C) 2013-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(ejabberd_logger).
-compile({no_auto_import, [get/0]}).
%% API
-export([start/0, get/0, set/1, get_log_path/0, flush/0]).
-export([convert_loglevel/1, loglevels/0, set_modules_fully_logged/1, config_reloaded/0]).
-ifndef(LAGER).
-export([progress_filter/2]).
-endif.
%% Deprecated functions
-export([restart/0, reopen_log/0, rotate_log/0]).
-deprecated([{restart, 0},
{reopen_log, 0},
{rotate_log, 0}]).
-type loglevel() :: none | emergency | alert | critical |
error | warning | notice | info | debug.
-define(is_loglevel(L),
((L == none) or (L == emergency) or (L == alert)
or (L == critical) or (L == error) or (L == warning)
or (L == notice) or (L == info) or (L == debug))).
-export_type([loglevel/0]).
%%%===================================================================
%%% API
%%%===================================================================
-spec get_log_path() -> string().
get_log_path() ->
case ejabberd_config:env_binary_to_list(ejabberd, log_path) of
{ok, Path} ->
Path;
undefined ->
case os:getenv("EJABBERD_LOG_PATH") of
false ->
"ejabberd.log";
Path ->
Path
end
end.
-spec loglevels() -> [loglevel(), ...].
loglevels() ->
[none, emergency, alert, critical, error, warning, notice, info, debug].
-spec convert_loglevel(0..5) -> loglevel().
convert_loglevel(0) -> none;
convert_loglevel(1) -> critical;
convert_loglevel(2) -> error;
convert_loglevel(3) -> warning;
convert_loglevel(4) -> info;
convert_loglevel(5) -> debug.
quiet_mode() ->
case application:get_env(ejabberd, quiet) of
{ok, true} -> true;
_ -> false
end.
-spec get_integer_env(atom(), T) -> T.
get_integer_env(Name, Default) ->
case application:get_env(ejabberd, Name) of
{ok, I} when is_integer(I), I>=0 ->
I;
{ok, infinity} ->
infinity;
undefined ->
Default;
{ok, Junk} ->
error_logger:error_msg("wrong value for ~ts: ~p; "
"using ~p as a fallback~n",
[Name, Junk, Default]),
Default
end.
-ifdef(LAGER).
-spec get_string_env(atom(), T) -> T.
get_string_env(Name, Default) ->
case application:get_env(ejabberd, Name) of
{ok, L} when is_list(L) ->
L;
undefined ->
Default;
{ok, Junk} ->
error_logger:error_msg("wrong value for ~ts: ~p; "
"using ~p as a fallback~n",
[Name, Junk, Default]),
Default
end.
start() ->
start(info).
start(Level) ->
StartedApps = application:which_applications(5000),
case lists:keyfind(logger, 1, StartedApps) of
%% Elixir logger is started. We assume everything is in place
%% to use lager to Elixir logger bridge.
{logger, _, _} ->
error_logger:info_msg("Ignoring ejabberd logger options, using Elixir Logger.", []),
%% Do not start lager, we rely on Elixir Logger
do_start_for_logger(Level);
_ ->
do_start(Level)
end.
do_start_for_logger(Level) ->
application:load(sasl),
application:set_env(sasl, sasl_error_logger, false),
application:load(lager),
application:set_env(lager, error_logger_redirect, false),
application:set_env(lager, error_logger_whitelist, ['Elixir.Logger.ErrorHandler']),
application:set_env(lager, crash_log, false),
application:set_env(lager, handlers, [{elixir_logger_backend, [{level, Level}]}]),
ejabberd:start_app(lager),
ok.
do_start(Level) ->
application:load(sasl),
application:set_env(sasl, sasl_error_logger, false),
application:load(lager),
ConsoleLog = get_log_path(),
Dir = filename:dirname(ConsoleLog),
ErrorLog = filename:join([Dir, "error.log"]),
CrashLog = filename:join([Dir, "crash.log"]),
LogRotateDate = get_string_env(log_rotate_date, ""),
LogRotateSize = case get_integer_env(log_rotate_size, 10*1024*1024) of
infinity -> 0;
V -> V
end,
LogRotateCount = get_integer_env(log_rotate_count, 1),
LogRateLimit = get_integer_env(log_rate_limit, 100),
ConsoleLevel0 = case quiet_mode() of
true -> critical;
_ -> Level
end,
ConsoleLevel = case get_lager_version() >= "3.6.0" of
true -> [{level, ConsoleLevel0}];
false -> ConsoleLevel0
end,
application:set_env(lager, error_logger_hwm, LogRateLimit),
application:set_env(
lager, handlers,
[{lager_console_backend, ConsoleLevel},
{lager_file_backend, [{file, ConsoleLog}, {level, Level}, {date, LogRotateDate},
{count, LogRotateCount}, {size, LogRotateSize}]},
{lager_file_backend, [{file, ErrorLog}, {level, error}, {date, LogRotateDate},
{count, LogRotateCount}, {size, LogRotateSize}]}]),
application:set_env(lager, crash_log, CrashLog),
application:set_env(lager, crash_log_date, LogRotateDate),
application:set_env(lager, crash_log_size, LogRotateSize),
application:set_env(lager, crash_log_count, LogRotateCount),
ejabberd:start_app(lager),
lists:foreach(fun(Handler) ->
lager:set_loghwm(Handler, LogRateLimit)
end, gen_event:which_handlers(lager_event)).
restart() ->
Level = ejabberd_option:loglevel(),
application:stop(lager),
start(Level).
config_reloaded() ->
ok.
reopen_log() ->
ok.
rotate_log() ->
catch lager_crash_log ! rotate,
lists:foreach(
fun({lager_file_backend, File}) ->
whereis(lager_event) ! {rotate, File};
(_) ->
ok
end, gen_event:which_handlers(lager_event)).
get() ->
Handlers = get_lager_handlers(),
lists:foldl(fun(lager_console_backend, _Acc) ->
lager:get_loglevel(lager_console_backend);
(elixir_logger_backend, _Acc) ->
lager:get_loglevel(elixir_logger_backend);
(_, Acc) ->
Acc
end,
none, Handlers).
set(N) when is_integer(N), N>=0, N=<5 ->
set(convert_loglevel(N));
set(Level) when ?is_loglevel(Level) ->
case get() of
Level ->
ok;
_ ->
ConsoleLog = get_log_path(),
QuietMode = quiet_mode(),
lists:foreach(
fun({lager_file_backend, File} = H) when File == ConsoleLog ->
lager:set_loglevel(H, Level);
(lager_console_backend = H) when not QuietMode ->
lager:set_loglevel(H, Level);
(elixir_logger_backend = H) ->
lager:set_loglevel(H, Level);
(_) ->
ok
end, get_lager_handlers())
end,
case Level of
debug -> xmpp:set_config([{debug, true}]);
_ -> xmpp:set_config([{debug, false}])
end.
get_lager_handlers() ->
case catch gen_event:which_handlers(lager_event) of
{'EXIT',noproc} ->
[];
Result ->
Result
end.
-spec get_lager_version() -> string().
get_lager_version() ->
Apps = application:loaded_applications(),
case lists:keyfind(lager, 1, Apps) of
{_, _, Vsn} -> Vsn;
false -> "0.0.0"
end.
set_modules_fully_logged(_) -> ok.
flush() ->
application:stop(lager),
application:stop(sasl).
-else.
-include_lib("kernel/include/logger.hrl").
-spec start() -> ok | {error, term()}.
start() ->
start(info).
start(Level) ->
EjabberdLog = get_log_path(),
Dir = filename:dirname(EjabberdLog),
ErrorLog = filename:join([Dir, "error.log"]),
LogRotateSize = get_integer_env(log_rotate_size, 10*1024*1024),
LogRotateCount = get_integer_env(log_rotate_count, 1),
LogBurstLimitWindowTime = get_integer_env(log_burst_limit_window_time, 1000),
LogBurstLimitCount = get_integer_env(log_burst_limit_count, 500),
Config = #{max_no_bytes => LogRotateSize,
max_no_files => LogRotateCount,
filesync_repeat_interval => no_repeat,
file_check => 1000,
sync_mode_qlen => 1000,
drop_mode_qlen => 1000,
flush_qlen => 5000,
burst_limit_window_time => LogBurstLimitWindowTime,
burst_limit_max_count => LogBurstLimitCount},
FmtConfig = #{legacy_header => false,
time_designator => $\s,
max_size => 100*1024,
single_line => false},
FileFmtConfig = FmtConfig#{template => file_template()},
ConsoleFmtConfig = FmtConfig#{template => console_template()},
try
ok = logger:set_primary_config(level, Level),
DefaultHandlerId = get_default_handlerid(),
ok = logger:update_formatter_config(DefaultHandlerId, ConsoleFmtConfig),
case quiet_mode() of
true ->
ok = logger:set_handler_config(DefaultHandlerId, level, critical);
_ ->
ok
end,
case logger:add_primary_filter(progress_report,
{fun ?MODULE:progress_filter/2, stop}) of
ok -> ok;
{error, {already_exist, _}} -> ok
end,
case logger:add_handler(ejabberd_log, logger_std_h,
#{level => all,
config => Config#{file => EjabberdLog},
formatter => {logger_formatter, FileFmtConfig}}) of
ok -> ok;
{error, {already_exist, _}} -> ok
end,
case logger:add_handler(error_log, logger_std_h,
#{level => error,
config => Config#{file => ErrorLog},
formatter => {logger_formatter, FileFmtConfig}}) of
ok -> ok;
{error, {already_exist, _}} -> ok
end
catch _:{Tag, Err} when Tag == badmatch; Tag == case_clause ->
?LOG_CRITICAL("Failed to set logging: ~p", [Err]),
Err
end.
get_default_handlerid() ->
Ids = logger:get_handler_ids(),
case lists:member(default, Ids) of
true -> default;
false -> hd(Ids)
end.
-spec restart() -> ok.
restart() ->
ok.
-spec config_reloaded() -> ok.
config_reloaded() ->
LogRotateSize = ejabberd_option:log_rotate_size(),
LogRotateCount = ejabberd_option:log_rotate_count(),
LogBurstLimitWindowTime = ejabberd_option:log_burst_limit_window_time(),
LogBurstLimitCount = ejabberd_option:log_burst_limit_count(),
lists:foreach(
fun(Handler) ->
case logger:get_handler_config(Handler) of
{ok, #{config := Config}} ->
Config2 = Config#{
max_no_bytes => LogRotateSize,
max_no_files => LogRotateCount,
burst_limit_window_time => LogBurstLimitWindowTime,
burst_limit_max_count => LogBurstLimitCount},
logger:update_handler_config(Handler, config, Config2);
_ ->
ok
end
end, [ejabberd_log, error_log]).
progress_filter(#{level:=info,msg:={report,#{label:={_,progress}}}} = Event, _) ->
case get() of
debug ->
logger_filters:progress(Event#{level => debug}, log);
_ ->
stop
end;
progress_filter(Event, _) ->
Event.
-ifdef(ELIXIR_ENABLED).
console_template() ->
case (false /= code:is_loaded('Elixir.Logger'))
andalso
'Elixir.System':version() >= <<"1.15">> of
true ->
{ok, DC} = logger:get_handler_config(default),
MessageFormat = case maps:get(formatter, DC) of
%% https://hexdocs.pm/logger/1.17.2/Logger.Formatter.html#module-formatting
{'Elixir.Logger.Formatter', _} ->
message;
%% https://www.erlang.org/doc/apps/kernel/logger_formatter#t:template/0
{logger_formatter, _} ->
msg
end,
[date, " ", time, " [", level, "] ", MessageFormat, "\n"];
false ->
[time, " [", level, "] " | msg()]
end.
-else.
console_template() ->
[time, " [", level, "] " | msg()].
-endif.
file_template() ->
[time, " [", level, "] ", pid,
{mfa, ["@", mfa, {line, [":", line], []}], []}, " " | msg()].
msg() ->
[{logger_formatter, [[logger_formatter, title], ":", io_lib:nl()], []},
msg, io_lib:nl()].
-spec reopen_log() -> ok.
reopen_log() ->
ok.
-spec rotate_log() -> ok.
rotate_log() ->
ok.
-spec get() -> loglevel().
get() ->
#{level := Level} = logger:get_primary_config(),
Level.
-spec set(0..5 | loglevel()) -> ok.
set(N) when is_integer(N), N>=0, N=<5 ->
set(convert_loglevel(N));
set(Level) when ?is_loglevel(Level) ->
case get() of
Level -> ok;
PrevLevel ->
?LOG_NOTICE("Changing loglevel from '~s' to '~s'",
[PrevLevel, Level]),
logger:set_primary_config(level, Level),
case Level of
debug -> xmpp:set_config([{debug, true}]);
_ -> xmpp:set_config([{debug, false}])
end
end.
set_modules_fully_logged(Modules) ->
logger:unset_module_level(),
logger:set_module_level(Modules, all).
-spec flush() -> ok.
flush() ->
lists:foreach(
fun(#{id := HandlerId, module := logger_std_h}) ->
logger_std_h:filesync(HandlerId);
(#{id := HandlerId, module := logger_disk_log_h}) ->
logger_disk_log_h:filesync(HandlerId);
(_) ->
ok
end, logger:get_handler_config()).
-endif.
ejabberd-24.12/src/mod_proxy65.erl 0000664 0001750 0001750 00000023604 14730775155 017351 0 ustar debalance debalance %%%----------------------------------------------------------------------
%%% File : mod_proxy65.erl
%%% Author : Evgeniy Khramtsov
%%% Purpose : Main supervisor.
%%% Created : 12 Oct 2006 by Evgeniy Khramtsov
%%%
%%%
%%% 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_proxy65).
-author('xram@jabber.ru').
-protocol({xep, 65, '1.8', '2.0.0', "complete", ""}).
-behaviour(gen_mod).
-behaviour(supervisor).
%% gen_mod callbacks.
-export([start/2, stop/1, reload/3]).
%% supervisor callbacks.
-export([init/1]).
-export([start_link/1, mod_opt_type/1, mod_options/1, depends/2, mod_doc/0]).
-define(PROCNAME, ejabberd_mod_proxy65).
-include("translate.hrl").
-callback init() -> any().
-callback register_stream(binary(), pid()) -> ok | {error, any()}.
-callback unregister_stream(binary()) -> ok | {error, any()}.
-callback activate_stream(binary(), binary(), pos_integer() | infinity, node()) ->
ok | {error, limit | conflict | notfound | term()}.
start(Host, Opts) ->
case mod_proxy65_service:add_listener(Host, Opts) of
{error, _} = Err ->
Err;
_ ->
Mod = gen_mod:ram_db_mod(global, ?MODULE),
Mod:init(),
Proc = gen_mod:get_module_proc(Host, ?PROCNAME),
ChildSpec = {Proc, {?MODULE, start_link, [Host]},
transient, infinity, supervisor, [?MODULE]},
supervisor:start_child(ejabberd_gen_mod_sup, ChildSpec)
end.
stop(Host) ->
case gen_mod:is_loaded_elsewhere(Host, ?MODULE) of
false ->
mod_proxy65_service:delete_listener(Host);
true ->
ok
end,
Proc = gen_mod:get_module_proc(Host, ?PROCNAME),
supervisor:terminate_child(ejabberd_gen_mod_sup, Proc),
supervisor:delete_child(ejabberd_gen_mod_sup, Proc).
reload(Host, NewOpts, OldOpts) ->
Mod = gen_mod:ram_db_mod(global, ?MODULE),
Mod:init(),
mod_proxy65_service:reload(Host, NewOpts, OldOpts).
start_link(Host) ->
Proc = gen_mod:get_module_proc(Host, ?PROCNAME),
supervisor:start_link({local, Proc}, ?MODULE, [Host]).
init([Host]) ->
Service = {mod_proxy65_service,
{mod_proxy65_service, start_link, [Host]},
transient, 5000, worker, [mod_proxy65_service]},
{ok, {{one_for_one, 10, 1}, [Service]}}.
depends(_Host, _Opts) ->
[].
mod_opt_type(access) ->
econf:acl();
mod_opt_type(hostname) ->
econf:host();
mod_opt_type(ip) ->
econf:ip();
mod_opt_type(name) ->
econf:binary();
mod_opt_type(port) ->
econf:port();
mod_opt_type(max_connections) ->
econf:pos_int(infinity);
mod_opt_type(host) ->
econf:host();
mod_opt_type(hosts) ->
econf:hosts();
mod_opt_type(ram_db_type) ->
econf:db_type(?MODULE);
mod_opt_type(server_host) ->
econf:binary();
mod_opt_type(auth_type) ->
econf:enum([plain, anonymous]);
mod_opt_type(recbuf) ->
econf:pos_int();
mod_opt_type(shaper) ->
econf:shaper();
mod_opt_type(sndbuf) ->
econf:pos_int();
mod_opt_type(vcard) ->
econf:vcard_temp().
mod_options(Host) ->
[{ram_db_type, ejabberd_config:default_ram_db(Host, ?MODULE)},
{access, all},
{host, <<"proxy.", Host/binary>>},
{hosts, []},
{hostname, undefined},
{ip, undefined},
{port, 7777},
{name, ?T("SOCKS5 Bytestreams")},
{vcard, undefined},
{max_connections, infinity},
{auth_type, anonymous},
{recbuf, 65536},
{sndbuf, 65536},
{shaper, none}].
mod_doc() ->
#{desc =>
?T("This module implements "
"https://xmpp.org/extensions/xep-0065.html"
"[XEP-0065: SOCKS5 Bytestreams]. It allows ejabberd "
"to act as a file transfer proxy between two XMPP clients."),
opts =>
[{host,
#{desc => ?T("Deprecated. Use 'hosts' instead.")}},
{hosts,
#{value => ?T("[Host, ...]"),
desc =>
?T("This option defines the Jabber IDs of the service. "
"If the 'hosts' option is not specified, the only Jabber ID will "
"be the hostname of the virtual host with the prefix \"proxy.\". "
"The keyword '@HOST@' is replaced with the real virtual host name.")}},
{name,
#{value => ?T("Name"),
desc =>
?T("The value of the service name. This name is only visible in some "
"clients that support https://xmpp.org/extensions/xep-0030.html"
"[XEP-0030: Service Discovery]. The default is \"SOCKS5 Bytestreams\".")}},
{access,
#{value => ?T("AccessName"),
desc =>
?T("Defines an access rule for file transfer initiators. "
"The default value is 'all'. You may want to restrict "
"access to the users of your server only, in order to "
"avoid abusing your proxy by the users of remote "
"servers.")}},
{ram_db_type,
#{value => "mnesia | redis | sql",
desc =>
?T("Same as top-level _`default_ram_db`_ option, "
"but applied to this module only.")}},
{ip,
#{value => ?T("IPAddress"),
desc =>
?T("This option specifies which network interface to listen "
"for. The default value is an IP address of the service's "
"DNS name, or, if fails, '127.0.0.1'.")}},
{hostname,
#{value => ?T("Host"),
desc =>
?T("Defines a hostname offered by the proxy when "
"establishing a session with clients. This is useful "
"when you run the proxy behind a NAT. The keyword "
"'@HOST@' is replaced with the virtual host name. "
"The default is to use the value of 'ip' option. "
"Examples: 'proxy.mydomain.org', '200.150.100.50'.")}},
{port,
#{value => "1..65535",
desc =>
?T("A port number to listen for incoming connections. "
"The default value is '7777'.")}},
{auth_type,
#{value => "anonymous | plain",
desc =>
?T("SOCKS5 authentication type. "
"The default value is 'anonymous'. "
"If set to 'plain', ejabberd will use "
"authentication backend as it would "
"for SASL PLAIN.")}},
{max_connections,
#{value => "pos_integer() | infinity",
desc =>
?T("Maximum number of active connections per file transfer "
"initiator. The default value is 'infinity'.")}},
{shaper,
#{value => ?T("Shaper"),
desc =>
?T("This option defines a shaper for the file transfer peers. "
"A shaper with the maximum bandwidth will be selected. "
"The default is 'none', i.e. no shaper.")}},
{recbuf,
#{value => ?T("Size"),
desc =>
?T("A size of the buffer for incoming packets. "
"If you define a shaper, set the value of this "
"option to the size of the shaper in order "
"to avoid traffic spikes in file transfers. "
"The default value is '65536' bytes.")}},
{sndbuf,
#{value => ?T("Size"),
desc =>
?T("A size of the buffer for outgoing packets. "
"If you define a shaper, set the value of this "
"option to the size of the shaper in order "
"to avoid traffic spikes in file transfers. "
"The default value is '65536' bytes.")}},
{vcard,
#{value => ?T("vCard"),
desc =>
?T("A custom vCard of the service that will be displayed "
"by some XMPP clients in Service Discovery. The value of "
"'vCard' is a YAML map constructed from an XML representation "
"of vCard. Since the representation has no attributes, "
"the mapping is straightforward.")}}],
example =>
["acl:",
" admin:",
" user: admin@example.org",
" proxy_users:",
" server: example.org",
"",
"access_rules:",
" proxy65_access:",
" allow: proxy_users",
"",
"shaper_rules:",
" proxy65_shaper:",
" none: admin",
" proxyrate: proxy_users",
"",
"shaper:",
" proxyrate: 10240",
"",
"modules:",
" mod_proxy65:",
" host: proxy1.example.org",
" name: \"File Transfer Proxy\"",
" ip: 200.150.100.1",
" port: 7778",
" max_connections: 5",
" access: proxy65_access",
" shaper: proxy65_shaper",
" recbuf: 10240",
" sndbuf: 10240"]}.
ejabberd-24.12/src/mod_version_opt.erl 0000664 0001750 0001750 00000000512 14730775155 020355 0 ustar debalance debalance %% Generated automatically
%% DO NOT EDIT: run `make options` instead
-module(mod_version_opt).
-export([show_os/1]).
-spec show_os(gen_mod:opts() | global | binary()) -> boolean().
show_os(Opts) when is_map(Opts) ->
gen_mod:get_opt(show_os, Opts);
show_os(Host) ->
gen_mod:get_module_opt(Host, mod_version, show_os).
ejabberd-24.12/src/eldap_filter.erl 0000664 0001750 0001750 00000016126 14730775155 017611 0 ustar debalance debalance %%%----------------------------------------------------------------------
%%% File: eldap_filter.erl
%%% Purpose: Converts String Representation of
%%% LDAP Search Filter (RFC 2254)
%%% to eldap's representation of filter
%%% Author: Evgeniy Khramtsov
%%%
%%%
%%% 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(eldap_filter).
%% TODO: remove this when new regexp module will be used
-export([parse/1, parse/2, do_sub/2]).
%%====================================================================
%% API
%%====================================================================
%%%-------------------------------------------------------------------
%%% Arity: parse/1
%%% Function: parse(RFC2254_Filter) -> {ok, EldapFilter} |
%%% {error, bad_filter}
%%%
%%% RFC2254_Filter = string().
%%%
%%% Description: Converts String Representation of LDAP Search Filter (RFC 2254)
%%% to eldap's representation of filter.
%%%
%%% Example:
%%% > eldap_filter:parse("(&(!(uid<=100))(mail=*))").
%%%
%%% {ok,{'and',[{'not',{lessOrEqual,{'AttributeValueAssertion',"uid","100"}}},
%%% {present,"mail"}]}}
%%%-------------------------------------------------------------------
-spec parse(binary()) -> {error, any()} | {ok, eldap:filter()}.
parse(L) ->
parse(L, []).
%%%-------------------------------------------------------------------
%%% Arity: parse/2
%%% Function: parse(RFC2254_Filter, [SubstValue |...]) ->
%%% {ok, EldapFilter} |
%%% {error, bad_filter} |
%%% {error, bad_regexp} |
%%% {error, max_substitute_recursion}
%%%
%%% SubstValue = {RegExp, Value} | {RegExp, Value, N},
%%% RFC2254_Filter = RegExp = Value = string(),
%%% N = integer().
%%%
%%% Description: The same as parse/1, but substitutes N or all occurrences
%%% of RegExp with Value *after* parsing.
%%%
%%% Example:
%%% > eldap_filter:parse(
%%% "(|(mail=%u@%d)(jid=%u@%d))",
%%% [{"%u", "xramtsov"},{"%d","gmail.com"}]).
%%%
%%% {ok,{'or',[{equalityMatch,{'AttributeValueAssertion',
%%% "mail",
%%% "xramtsov@gmail.com"}},
%%% {equalityMatch,{'AttributeValueAssertion',
%%% "jid",
%%% "xramtsov@gmail.com"}}]}}
%%%-------------------------------------------------------------------
-spec parse(binary(), [{binary(), binary()} |
{binary(), binary(), pos_integer()}]) ->
{error, any()} | {ok, eldap:filter()}.
parse(L, SList) ->
case catch eldap_filter_yecc:parse(scan(binary_to_list(L), SList)) of
{'EXIT', _} = Err ->
{error, Err};
{error, {_, _, Msg}} ->
{error, Msg};
{ok, Result} ->
{ok, Result};
{regexp, Err} ->
{error, Err}
end.
%%====================================================================
%% Internal functions
%%====================================================================
-define(do_scan(L), scan(Rest, <<>>, [{L, 1} | check(Buf, S) ++ Result], L, S)).
scan(L, SList) ->
scan(L, <<"">>, [], undefined, SList).
scan("=*)" ++ Rest, Buf, Result, '(', S) ->
scan(Rest, <<>>, [{')', 1}, {'=*', 1} | check(Buf, S) ++ Result], ')', S);
scan(":dn" ++ Rest, Buf, Result, '(', S) -> ?do_scan(':dn');
scan(":=" ++ Rest, Buf, Result, '(', S) -> ?do_scan(':=');
scan(":=" ++ Rest, Buf, Result, ':dn', S) -> ?do_scan(':=');
scan(":=" ++ Rest, Buf, Result, ':', S) -> ?do_scan(':=');
scan("~=" ++ Rest, Buf, Result, '(', S) -> ?do_scan('~=');
scan(">=" ++ Rest, Buf, Result, '(', S) -> ?do_scan('>=');
scan("<=" ++ Rest, Buf, Result, '(', S) -> ?do_scan('<=');
scan("=" ++ Rest, Buf, Result, '(', S) -> ?do_scan('=');
scan(":" ++ Rest, Buf, Result, '(', S) -> ?do_scan(':');
scan(":" ++ Rest, Buf, Result, ':dn', S) -> ?do_scan(':');
scan("&" ++ Rest, Buf, Result, '(', S) when Buf==<<"">> -> ?do_scan('&');
scan("|" ++ Rest, Buf, Result, '(', S) when Buf==<<"">> -> ?do_scan('|');
scan("!" ++ Rest, Buf, Result, '(', S) when Buf==<<"">> -> ?do_scan('!');
scan("*" ++ Rest, Buf, Result, '*', S) -> ?do_scan('*');
scan("*" ++ Rest, Buf, Result, '=', S) -> ?do_scan('*');
scan("(" ++ Rest, Buf, Result, _, S) -> ?do_scan('(');
scan(")" ++ Rest, Buf, Result, _, S) -> ?do_scan(')');
scan([Letter | Rest], Buf, Result, PreviosAtom, S) ->
scan(Rest, <>, Result, PreviosAtom, S);
scan([], Buf, Result, _, S) ->
lists:reverse(check(Buf, S) ++ Result).
check(<<>>, _) ->
[];
check(Buf, S) ->
[{str, 1, binary_to_list(do_sub(Buf, S))}].
-define(MAX_RECURSION, 100).
-spec do_sub(binary(), [{binary(), binary()} |
{binary(), binary(), pos_integer()}]) -> binary().
do_sub(S, []) ->
S;
do_sub(<<>>, _) ->
<<>>;
do_sub(S, [{RegExp, New} | T]) ->
Result = do_sub(S, {RegExp, replace_amps(New)}, 1),
do_sub(Result, T);
do_sub(S, [{RegExp, New, Times} | T]) ->
Result = do_sub(S, {RegExp, replace_amps(New), Times}, 1),
do_sub(Result, T).
do_sub(S, {RegExp, New}, Iter) ->
case ejabberd_regexp:run(S, RegExp) of
match ->
case ejabberd_regexp:replace(S, RegExp, New) of
NewS when Iter =< ?MAX_RECURSION ->
do_sub(NewS, {RegExp, New}, Iter+1);
_NewS when Iter > ?MAX_RECURSION ->
erlang:error(max_substitute_recursion)
end;
nomatch ->
S;
_ ->
erlang:error(bad_regexp)
end;
do_sub(S, {_, _, N}, _) when N<1 ->
S;
do_sub(S, {RegExp, New, Times}, Iter) ->
case ejabberd_regexp:run(S, RegExp) of
match ->
case ejabberd_regexp:replace(S, RegExp, New) of
NewS when Iter < Times ->
do_sub(NewS, {RegExp, New, Times}, Iter+1);
NewS ->
NewS
end;
nomatch ->
S;
_ ->
erlang:error(bad_regexp)
end.
replace_amps(Bin) ->
list_to_binary(
lists:flatmap(
fun($&) -> "\\&";
($\\) -> "\\\\";
(Chr) -> [Chr]
end, binary_to_list(Bin))).
ejabberd-24.12/src/ejabberd_hooks.erl 0000664 0001750 0001750 00000107356 14730775155 020126 0 ustar debalance debalance %%%----------------------------------------------------------------------
%%% File : ejabberd_hooks.erl
%%% Author : Alexey Shchepin
%%% Purpose : Manage hooks
%%% Created : 8 Aug 2004 by Alexey Shchepin
%%%
%%%
%%% 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(ejabberd_hooks).
-author('alexey@process-one.net').
-behaviour(gen_server).
%% External exports
-export([start_link/0,
add/3,
add/4,
add/5,
delete/3,
delete/4,
delete/5,
subscribe/4,
subscribe/5,
unsubscribe/4,
unsubscribe/5,
run/2,
run/3,
run_fold/3,
run_fold/4]).
%% gen_server callbacks
-export([init/1,
handle_call/3,
handle_cast/2,
code_change/3,
handle_info/2,
terminate/2]).
-export(
[
get_tracing_options/3,
trace_off/3,
trace_on/5,human_readable_time_string/1
]
).
-include("logger.hrl").
-include("ejabberd_stacktrace.hrl").
-record(state, {}).
-type subscriber() :: {Module :: atom(), Function :: atom(), InitArg :: any()}.
-type subscriber_event() :: before | 'after' | before_callback | after_callback.
-type hook() :: {Seq :: integer(), Module :: atom(), Function :: atom() | fun()}.
-define(TRACE_HOOK_KEY, '$trace_hook').
-define(TIMING_KEY, '$trace_hook_timer').
%%%----------------------------------------------------------------------
%%% API
%%%----------------------------------------------------------------------
start_link() ->
gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
-spec add(atom(), fun(), integer()) -> ok.
%% @doc See add/4.
add(Hook, Function, Seq) when is_function(Function) ->
add(Hook, global, undefined, Function, Seq).
-spec add(atom(), HostOrModule :: binary() | atom(), fun() | atom(), integer()) -> ok.
add(Hook, Host, Function, Seq) when is_function(Function) ->
add(Hook, Host, undefined, Function, Seq);
%% @doc Add a module and function to this hook.
%% The integer sequence is used to sort the calls: low number is called before high number.
add(Hook, Module, Function, Seq) ->
add(Hook, global, Module, Function, Seq).
-spec add(atom(), binary() | global, atom(), atom() | fun(), integer()) -> ok.
add(Hook, Host, Module, Function, Seq) ->
gen_server:call(?MODULE, {add, Hook, Host, Module, Function, Seq}).
-spec subscribe(atom(), atom(), atom(), any()) -> ok.
%% @doc Add a subscriber to this hook.
%%
%% Before running any hook callback, the subscriber will be called in form of
%% Module:Function(InitArg, 'before', Host :: binary() | global, Hook, HookArgs)
%% Above function should return new state.
%%
%% Before running each callback, the subscriber will be called in form of
%% Module:Function(State, 'before_callback', Host :: binary() | global, Hook, {CallbackMod, CallbackArg, Seq, HookArgs})
%% Above function should return new state.
%%
%% After running each callback, the subscriber will be called in form of
%% Module:Function(State, 'after_callback', Host :: binary() | global, Hook, {CallbackMod, CallbackArg, Seq, HookArgs})
%% Above function should return new state.
%%
%% After running any hook callback, the subscriber will be called in form of
%% Module:Function(State, 'after', Host :: binary() | global, Hook, HookArgs)
%% Return value of this function call will be dropped.
%%
%% For every ejabberd_hooks:[run|run_fold] for every subscriber above functions will be called and the hook runner
%% maintains State in above four calls.
subscribe(Hook, Module, Function, InitArg) ->
subscribe(Hook, global, Module, Function, InitArg).
-spec subscribe(atom(), binary() | global, atom(), atom(), any()) -> ok.
subscribe(Hook, Host, Module, Function, InitArg) ->
gen_server:call(?MODULE, {subscribe, Hook, Host, Module, Function, InitArg}).
-spec delete(atom(), fun(), integer()) -> ok.
%% @doc See del/4.
delete(Hook, Function, Seq) when is_function(Function) ->
delete(Hook, global, undefined, Function, Seq).
-spec delete(atom(), binary() | atom(), atom() | fun(), integer()) -> ok.
delete(Hook, Host, Function, Seq) when is_function(Function) ->
delete(Hook, Host, undefined, Function, Seq);
%% @doc Delete a module and function from this hook.
%% It is important to indicate exactly the same information than when the call was added.
delete(Hook, Module, Function, Seq) ->
delete(Hook, global, Module, Function, Seq).
-spec delete(atom(), binary() | global, atom(), atom() | fun(), integer()) -> ok.
delete(Hook, Host, Module, Function, Seq) ->
gen_server:call(?MODULE, {delete, Hook, Host, Module, Function, Seq}).
-spec unsubscribe(atom(), atom(), atom(), any()) -> ok.
%% @doc Removes a subscriber from this hook.
unsubscribe(Hook, Module, Function, InitArg) ->
unsubscribe(Hook, global, Module, Function, InitArg).
-spec unsubscribe(atom(), binary() | global, atom(), atom(), any()) -> ok.
unsubscribe(Hook, Host, Module, Function, InitArg) ->
gen_server:call(?MODULE, {unsubscribe, Hook, Host, Module, Function, InitArg}).
-spec run(atom(), list()) -> ok.
%% @doc Run the calls (and subscibers) of this hook in order, don't care about function results.
%% If a call returns stop, no more calls are performed.
run(Hook, Args) ->
run(Hook, global, Args).
-spec run(atom(), binary() | global, list()) -> ok.
run(Hook, Host, Args) ->
try ets:lookup(hooks, {Hook, Host}) of
[{_, Ls, Subs}] ->
case erlang:get(?TRACE_HOOK_KEY) of
undefined when Subs == [] ->
run1(Ls, Hook, Args);
undefined ->
Subs2 = call_subscriber_list(Subs, Host, Hook, Args, before, []),
Subs3 = run1(Ls, Hook, Args, Host, Subs2),
_Subs4 = call_subscriber_list(Subs3, Host, Hook, Args, 'after', []),
ok;
TracingHooksOpts ->
case do_get_tracing_options(Hook, Host, TracingHooksOpts) of
undefined ->
Subs2 = call_subscriber_list(Subs, Host, Hook, Args, before, []),
Subs3 = run1(Ls, Hook, Args, Host, Subs2),
_Subs4 = call_subscriber_list(Subs3, Host, Hook, Args, 'after', []),
ok;
TracingOpts ->
foreach_start_hook_tracing(TracingOpts, Hook, Host, Args),
Subs2 = call_subscriber_list(Subs, Host, Hook, Args, before, []),
Subs3 = run2(Ls, Hook, Args, Host, TracingOpts, Subs2),
_Subs4 = call_subscriber_list(Subs3, Host, Hook, Args, 'after', []),
ok
end
end;
[] ->
ok
catch _:badarg ->
ok
end.
-spec run_fold(atom(), T, list()) -> T.
%% @doc Run the calls (and subscribers) of this hook in order.
%% The arguments passed to the function are: [Val | Args].
%% The result of a call is used as Val for the next call.
%% If a call returns 'stop', no more calls are performed.
%% If a call returns {stop, NewVal}, no more calls are performed and NewVal is returned.
run_fold(Hook, Val, Args) ->
run_fold(Hook, global, Val, Args).
-spec run_fold(atom(), binary() | global, T, list()) -> T.
run_fold(Hook, Host, Val, Args) ->
try ets:lookup(hooks, {Hook, Host}) of
[{_, Ls, Subs}] ->
case erlang:get(?TRACE_HOOK_KEY) of
undefined when Subs == [] ->
run_fold1(Ls, Hook, Val, Args);
undefined ->
Subs2 = call_subscriber_list(Subs, Host, Hook, [Val | Args], before, []),
{Val2, Subs3} = run_fold1(Ls, Hook, Val, Args, Host, Subs2),
_Subs4 = call_subscriber_list(Subs3, Host, Hook, [Val2 | Args], 'after', []),
Val2;
TracingHooksOpts ->
case do_get_tracing_options(Hook, Host, TracingHooksOpts) of
undefined ->
Subs2 = call_subscriber_list(Subs, Host, Hook, [Val | Args], before, []),
{Val2, Subs3} = run_fold1(Ls, Hook, Val, Args, Host, Subs2),
_Subs4 = call_subscriber_list(Subs3, Host, Hook, [Val2 | Args], 'after', []),
Val2;
TracingOpts ->
fold_start_hook_tracing(TracingOpts, Hook, Host, [Val | Args]),
Subs2 = call_subscriber_list(Subs, Host, Hook, [Val | Args], before, []),
{Val2, Subs3} = run_fold2(Ls, Hook, Val, Args, Host, TracingOpts, Subs2),
_Subs4 = call_subscriber_list(Subs3, Host, Hook, [Val2 | Args], 'after', []),
Val2
end
end;
[] ->
Val
catch _:badarg ->
Val
end.
get_tracing_options(Hook, Host, Pid) when Pid == erlang:self() ->
do_get_tracing_options(Hook, Host, erlang:get(?TRACE_HOOK_KEY));
get_tracing_options(Hook, Host, Pid) when erlang:is_pid(Pid) ->
case erlang:process_info(Pid, dictionary) of
{_, DictPropList} ->
case lists:keyfind(?TRACE_HOOK_KEY, 1, DictPropList) of
{_, TracingHooksOpts} ->
do_get_tracing_options(Hook, Host, TracingHooksOpts);
_ ->
undefined
end;
_ ->
undefined
end.
trace_on(Hook, Host, Pid, #{}=Opts, Timeout) when Pid == erlang:self() ->
do_trace_on(Hook, Host, Opts, Timeout);
trace_on(Hook, Host, Proc, #{}=Opts, Timeout) ->
try sys:replace_state(
Proc,
fun(State) ->
do_trace_on(Hook, Host, Opts, Timeout),
State
end,
15000
) of
_ -> % process state
ok
catch
_:Reason ->
{error, Reason}
end.
trace_off(Hook, Host, Pid) when Pid == erlang:self() ->
do_trace_off(Hook, Host);
trace_off(Hook, Host, Proc) ->
try sys:replace_state(
Proc,
fun(State) ->
do_trace_off(Hook, Host),
State
end,
15000
) of
_ -> % process state
ok
catch
_:Reason ->
{error, Reason}
end.
%%%----------------------------------------------------------------------
%%% Callback functions from gen_server
%%%----------------------------------------------------------------------
init([]) ->
_ = ets:new(hooks, [named_table, {read_concurrency, true}]),
{ok, #state{}}.
handle_call({add, Hook, Host, Module, Function, Seq}, _From, State) ->
HookFormat = {Seq, Module, Function},
Reply = handle_add(Hook, Host, HookFormat),
{reply, Reply, State};
handle_call({delete, Hook, Host, Module, Function, Seq}, _From, State) ->
HookFormat = {Seq, Module, Function},
Reply = handle_delete(Hook, Host, HookFormat),
{reply, Reply, State};
handle_call({subscribe, Hook, Host, Module, Function, InitArg}, _From, State) ->
SubscriberFormat = {Module, Function, InitArg},
Reply = handle_subscribe(Hook, Host, SubscriberFormat),
{reply, Reply, State};
handle_call({unsubscribe, Hook, Host, Module, Function, InitArg}, _From, State) ->
SubscriberFormat = {Module, Function, InitArg},
Reply = handle_unsubscribe(Hook, Host, SubscriberFormat),
{reply, Reply, State};
handle_call(Request, From, State) ->
?WARNING_MSG("Unexpected call from ~p: ~p", [From, Request]),
{noreply, State}.
-spec handle_add(atom(), atom(), hook()) -> ok.
handle_add(Hook, Host, El) ->
case ets:lookup(hooks, {Hook, Host}) of
[{_, Ls, Subs}] ->
case lists:member(El, Ls) of
true ->
ok;
false ->
NewLs = lists:merge(Ls, [El]),
ets:insert(hooks, {{Hook, Host}, NewLs, Subs}),
ok
end;
[] ->
NewLs = [El],
ets:insert(hooks, {{Hook, Host}, NewLs, []}),
ok
end.
-spec handle_delete(atom(), atom(), hook()) -> ok.
handle_delete(Hook, Host, El) ->
case ets:lookup(hooks, {Hook, Host}) of
[{_, Ls, Subs}] ->
NewLs = lists:delete(El, Ls),
ets:insert(hooks, {{Hook, Host}, NewLs, Subs}),
ok;
[] ->
ok
end.
-spec handle_subscribe(atom(), atom(), subscriber()) -> ok.
handle_subscribe(Hook, Host, El) ->
case ets:lookup(hooks, {Hook, Host}) of
[{_, Ls, Subs}] ->
case lists:member(El, Subs) of
true ->
ok;
false ->
ets:insert(hooks, {{Hook, Host}, Ls, Subs ++ [El]}),
ok
end;
[] ->
ets:insert(hooks, {{Hook, Host}, [], [El]}),
ok
end.
-spec handle_unsubscribe(atom(), atom(), subscriber()) -> ok.
handle_unsubscribe(Hook, Host, El) ->
case ets:lookup(hooks, {Hook, Host}) of
[{_, Ls, Subs}] ->
ets:insert(hooks, {{Hook, Host}, Ls, lists:delete(El, Subs)}),
ok;
[] ->
ok
end.
handle_cast(Msg, State) ->
?WARNING_MSG("Unexpected cast: ~p", [Msg]),
{noreply, State}.
handle_info(Info, State) ->
?WARNING_MSG("Unexpected info: ~p", [Info]),
{noreply, State}.
terminate(_Reason, _State) ->
ok.
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
%%%----------------------------------------------------------------------
%%% Internal functions
%%%----------------------------------------------------------------------
-spec run1([hook()], atom(), list()) -> ok.
run1([], _Hook, _Args) ->
ok;
run1([{_Seq, Module, Function} | Ls], Hook, Args) ->
Res = safe_apply(Hook, Module, Function, Args),
case Res of
'EXIT' ->
run1(Ls, Hook, Args);
stop ->
ok;
_ ->
run1(Ls, Hook, Args)
end.
-spec run_fold1([hook()], atom(), T, list()) -> T.
run_fold1([], _Hook, Val, _Args) ->
Val;
run_fold1([{_Seq, Module, Function} | Ls], Hook, Val, Args) ->
Res = safe_apply(Hook, Module, Function, [Val | Args]),
case Res of
'EXIT' ->
run_fold1(Ls, Hook, Val, Args);
stop ->
Val;
{stop, NewVal} ->
NewVal;
NewVal ->
run_fold1(Ls, Hook, NewVal, Args)
end.
-spec run1([hook()], atom(), list(), binary() | global, [subscriber()]) -> [subscriber()].
run1([], _Hook, _Args, _Host, SubscriberList) ->
SubscriberList;
run1([{Seq, Module, Function} | Ls], Hook, Args, Host, SubscriberList) ->
SubscriberList2 = call_subscriber_list(SubscriberList, Host, Hook, {Module, Function, Seq, Args}, before_callback, []),
Res = safe_apply(Hook, Module, Function, Args),
SubscriberList3 = call_subscriber_list(SubscriberList2, Host, Hook, {Module, Function, Seq, Args}, after_callback, []),
case Res of
'EXIT' ->
run1(Ls, Hook, Args, Host, SubscriberList3);
stop ->
SubscriberList3;
_ ->
run1(Ls, Hook, Args, Host, SubscriberList3)
end.
-spec run_fold1([hook()], atom(), T, list(), binary() | global, [subscriber()]) -> {T, [subscriber()]}.
run_fold1([], _Hook, Val, _Args, _Host, SubscriberList) ->
{Val, SubscriberList};
run_fold1([{Seq, Module, Function} | Ls], Hook, Val, Args, Host, SubscriberList) ->
SubscriberList2 = call_subscriber_list(SubscriberList, Host, Hook, {Module, Function, Seq, [Val | Args]}, before_callback, []),
Res = safe_apply(Hook, Module, Function, [Val | Args]),
SubscriberList3 = call_subscriber_list(SubscriberList2, Host, Hook, {Module, Function, Seq, [Val | Args]}, after_callback, []),
case Res of
'EXIT' ->
run_fold1(Ls, Hook, Val, Args, Host, SubscriberList3);
stop ->
{Val, SubscriberList3};
{stop, NewVal} ->
{NewVal, SubscriberList3};
NewVal ->
run_fold1(Ls, Hook, NewVal, Args, Host, SubscriberList3)
end.
-spec safe_apply(atom(), atom(), atom() | fun(), list()) -> any().
safe_apply(Hook, Module, Function, Args) ->
?DEBUG("Running hook ~p: ~p:~p/~B",
[Hook, Module, Function, length(Args)]),
try if is_function(Function) ->
apply(Function, Args);
true ->
apply(Module, Function, Args)
end
catch ?EX_RULE(E, R, St) when E /= exit; R /= normal ->
Stack = ?EX_STACK(St),
?ERROR_MSG("Hook ~p crashed when running ~p:~p/~p:~n" ++
string:join(
["** ~ts"|
["** Arg " ++ integer_to_list(I) ++ " = ~p"
|| I <- lists:seq(1, length(Args))]],
"~n"),
[Hook, Module, Function, length(Args),
misc:format_exception(2, E, R, Stack)|Args]),
'EXIT'
end.
-spec call_subscriber_list([subscriber()], binary() | global, atom(), {atom(), atom(), integer(), list()} | list(), subscriber_event(), [subscriber()]) -> any().
call_subscriber_list([], _Host, _Hook, _CallbackOrArgs, _Event, []) ->
[];
call_subscriber_list([], _Host, _Hook, _CallbackOrArgs, _Event, Result) ->
lists:reverse(Result);
call_subscriber_list([{Mod, Func, InitArg} | SubscriberList], Host, Hook, CallbackOrArgs, Event, Result) ->
SubscriberArgs = [InitArg, Event, Host, Hook, CallbackOrArgs],
?DEBUG("Running hook subsciber ~p: ~p:~p/~B with event ~p",
[Hook, Mod, Func, length(SubscriberArgs), Event]),
try apply(Mod, Func, SubscriberArgs) of
State ->
call_subscriber_list(SubscriberList, Host, Hook, CallbackOrArgs, Event, [{Mod, Func, State} | Result])
catch ?EX_RULE(E, R, St) when E /= exit; R /= normal ->
Stack = ?EX_STACK(St),
?ERROR_MSG("Hook subscriber ~p crashed when running ~p:~p/~p:~n" ++
string:join(
["** ~ts"|
["** Arg " ++ integer_to_list(I) ++ " = ~p"
|| I <- lists:seq(1, length(SubscriberArgs))]],
"~n"),
[Hook, Mod, Func, length(SubscriberArgs),
misc:format_exception(2, E, R, Stack)|SubscriberArgs]),
%% Do not append subscriber for next calls:
call_subscriber_list(SubscriberList, Host, Hook, CallbackOrArgs, Event, Result)
end.
%%%----------------------------------------------------------------------
%%% Internal tracing functions
%%%----------------------------------------------------------------------
do_trace_on(Hook, Host, Opts, Timeout) when erlang:is_list(Host) ->
do_trace_on(Hook, erlang:list_to_binary(Host), Opts, Timeout);
do_trace_on(Hook, Host, Opts, undefined) ->
case erlang:get(?TRACE_HOOK_KEY) of
_ when Hook == all andalso Host == <<"*">> ->
% Trace everything:
erlang:put(?TRACE_HOOK_KEY, #{all => #{<<"*">> => Opts}});
#{all := #{<<"*">> := _}} -> % Already tracing everything
% Update Opts:
erlang:put(?TRACE_HOOK_KEY, #{all => #{<<"*">> => Opts}});
#{all := HostOpts} when Hook == all -> % Already Tracing everything for some hosts
% Add/Update Host and Opts:
erlang:put(?TRACE_HOOK_KEY, #{all => HostOpts#{Host => Opts}});
#{all := _} -> % Already tracing everything and Hook is not all
ok;
#{} when Hook == all ->
% Remove other hooks by just adding all:
erlang:put(?TRACE_HOOK_KEY, #{all => #{Host => Opts}});
#{}=TraceHooksOpts when Host == <<"*">> -> % Want to trace a hook for all hosts
erlang:put(?TRACE_HOOK_KEY, TraceHooksOpts#{Hook => #{Host => Opts}});
#{}=TraceHooksOpts ->
case maps:get(Hook, TraceHooksOpts, #{}) of
#{<<"*">> := _} -> % Already tracing this hook for all hosts
ok;
HostOpts ->
erlang:put(?TRACE_HOOK_KEY, TraceHooksOpts#{Hook => HostOpts#{Host => Opts}})
end;
undefined ->
erlang:put(?TRACE_HOOK_KEY, #{Hook => #{Host => Opts}})
end,
ok;
do_trace_on(Hook, Host, Opts, TimeoutSeconds) -> % Trace myself `Timeout` time
Timeout = timer:seconds(TimeoutSeconds),
ParentPid = erlang:self(),
try erlang:spawn(
fun() ->
MonitorRef = erlang:monitor(process, ParentPid),
receive
{_, MonitorRef, _, _, _} ->
ok
after Timeout ->
trace_off(Hook, Host, ParentPid)
end,
erlang:exit(normal)
end
) of
_ ->
do_trace_on(Hook, Host, Opts, undefined) % ok
catch
_:Reason -> % system_limit
{error, Reason}
end.
do_trace_off(Hook, Host) when erlang:is_list(Host) ->
do_trace_off(Hook, erlang:list_to_binary(Host));
do_trace_off(Hook, Host) ->
case erlang:get(?TRACE_HOOK_KEY) of
_ when Hook == all andalso Host == <<"*">> ->
% Remove all tracing:
erlang:erase(?TRACE_HOOK_KEY);
#{all := HostOpts} when Hook == all -> % Already tracing all hooks
% Remove Host:
HostOpts2 = maps:remove(Host, HostOpts),
if
HostOpts2 == #{} ->
% Remove all tracing:
erlang:erase(?TRACE_HOOK_KEY);
true ->
erlang:put(?TRACE_HOOK_KEY, #{all => HostOpts2})
end;
#{}=TraceHooksOpts when Host == <<"*">> ->
% Remove tracing of this hook for all hosts:
TraceHooksOpts2 = maps:remove(Hook, TraceHooksOpts),
if
TraceHooksOpts2 == #{} ->
% Remove all tracing:
erlang:erase(?TRACE_HOOK_KEY);
true ->
erlang:put(?TRACE_HOOK_KEY, TraceHooksOpts2)
end;
#{}=TraceHooksOpts ->
case maps:get(Hook, TraceHooksOpts, undefined) of
#{}=HostOpts ->
NewHostOpts = maps:remove(Host, HostOpts),
if
NewHostOpts == #{} ->
% Remove hook:
erlang:put(?TRACE_HOOK_KEY, maps:remove(Hook, TraceHooksOpts));
true ->
erlang:put(?TRACE_HOOK_KEY, TraceHooksOpts#{Hook => NewHostOpts})
end;
_ ->
ok
end;
undefined ->
ok
end,
ok.
do_get_tracing_options(Hook, Host, MaybeMap) ->
case MaybeMap of
undefined ->
undefined;
#{all := #{<<"*">> := Opts}} -> % Tracing everything
Opts;
#{all := HostOpts} -> % Tracing all hooks for some hosts
maps:get(Host, HostOpts, undefined);
#{}=TraceHooksOpts ->
HostOpts = maps:get(Hook, TraceHooksOpts, #{}),
case maps:get(Host, HostOpts, undefined) of
undefined ->
maps:get(<<"*">>, HostOpts, undefined);
Opts ->
Opts
end
end.
run2([], Hook, Args, Host, Opts, SubscriberList) ->
foreach_stop_hook_tracing(Opts, Hook, Host, Args, undefined),
SubscriberList;
run2([{Seq, Module, Function} | Ls], Hook, Args, Host, TracingOpts, SubscriberList) ->
foreach_start_callback_tracing(TracingOpts, Hook, Host, Module, Function, Args, Seq),
SubscriberList2 = call_subscriber_list(SubscriberList, Host, Hook, {Module, Function, Seq, Args}, before_callback, []),
Res = safe_apply(Hook, Module, Function, Args),
SubscriberList3 = call_subscriber_list(SubscriberList2, Host, Hook, {Module, Function, Seq, Args}, after_callback, []),
foreach_stop_callback_tracing(TracingOpts, Hook, Host, Module, Function, Args, Seq, Res),
case Res of
'EXIT' ->
run2(Ls, Hook, Args, Host, TracingOpts, SubscriberList3);
stop ->
foreach_stop_hook_tracing(TracingOpts, Hook, Host, Args, {Module, Function, Seq, Ls}),
SubscriberList3;
_ ->
run2(Ls, Hook, Args, Host, TracingOpts, SubscriberList3)
end.
run_fold2([], Hook, Val, Args, Host, Opts, SubscriberList) ->
fold_stop_hook_tracing(Opts, Hook, Host, [Val | Args], undefined),
{Val, SubscriberList};
run_fold2([{Seq, Module, Function} | Ls], Hook, Val, Args, Host, TracingOpts, SubscriberList) ->
fold_start_callback_tracing(TracingOpts, Hook, Host, Module, Function, [Val | Args], Seq),
SubscriberList2 = call_subscriber_list(SubscriberList, Host, Hook, {Module, Function, Seq, [Val | Args]}, before_callback, []),
Res = safe_apply(Hook, Module, Function, [Val | Args]),
SubscriberList3 = call_subscriber_list(SubscriberList2, Host, Hook, {Module, Function, Seq, [Val | Args]}, after_callback, []),
fold_stop_callback_tracing(TracingOpts, Hook, Host, Module, Function, [Val | Args], Seq, Res),
case Res of
'EXIT' ->
run_fold2(Ls, Hook, Val, Args, Host, TracingOpts, SubscriberList3);
stop ->
fold_stop_hook_tracing(TracingOpts, Hook, Host, [Val | Args], {Module, Function, Seq, {old, Val}, Ls}),
{Val, SubscriberList3};
{stop, NewVal} ->
fold_stop_hook_tracing(TracingOpts, Hook, Host, [Val | Args], {Module, Function, Seq, {new, NewVal}, Ls}),
{NewVal, SubscriberList3};
NewVal ->
run_fold2(Ls, Hook, NewVal, Args, Host, TracingOpts, SubscriberList3)
end.
foreach_start_hook_tracing(TracingOpts, Hook, Host, Args) ->
run_event_handlers(TracingOpts, Hook, Host, start_hook, [Args], foreach).
foreach_stop_hook_tracing(TracingOpts, Hook, Host, Args, BreakCallback) ->
run_event_handlers(TracingOpts, Hook, Host, stop_hook, [Args, BreakCallback], foreach).
foreach_start_callback_tracing(TracingOpts, Hook, Host, Mod, Func, Args, Seq) ->
run_event_handlers(TracingOpts, Hook, Host, start_callback, [Mod, Func, Args, Seq], foreach).
foreach_stop_callback_tracing(TracingOpts, Hook, Host, Mod, Func, Args, Seq, Res) ->
run_event_handlers(TracingOpts, Hook, Host, stop_callback, [Mod, Func, Args, Seq, Res], foreach).
fold_start_hook_tracing(TracingOpts, Hook, Host, Args) ->
run_event_handlers(TracingOpts, Hook, Host, start_hook, [Args], fold).
fold_stop_hook_tracing(TracingOpts, Hook, Host, Args, BreakCallback) ->
run_event_handlers(TracingOpts, Hook, Host, stop_hook, [Args, BreakCallback], fold).
fold_start_callback_tracing(TracingOpts, Hook, Host, Mod, Func, Args, Seq) ->
run_event_handlers(TracingOpts, Hook, Host, start_callback, [Mod, Func, Args, Seq], fold).
fold_stop_callback_tracing(TracingOpts, Hook, Host, Mod, Func, Args, Seq, Res) ->
run_event_handlers(TracingOpts, Hook, Host, stop_callback, [Mod, Func, Args, Seq, Res], fold).
run_event_handlers(TracingOpts, Hook, Host, Event, EventArgs, RunType) ->
EventHandlerList = maps:get(event_handler_list, TracingOpts, default_tracing_event_handler_list()),
EventHandlerOpts = maps:get(event_handler_options, TracingOpts, #{}),
if
erlang:is_list(EventHandlerList) ->
lists:foreach(
fun(EventHandler) ->
try
if
erlang:is_function(EventHandler) ->
erlang:apply(
EventHandler,
[Event, EventArgs, RunType, Hook, Host, EventHandlerOpts, TracingOpts]
);
true ->
EventHandler:handle_hook_tracing_event(
Event,
EventArgs,
RunType,
Hook,
Host,
EventHandlerOpts,
TracingOpts
)
end
of
_ ->
ok
catch
?EX_RULE(E, R, St) ->
Stack = ?EX_STACK(St),
?ERROR_MSG(
"(~0p|~ts|~0p) Tracing event '~0p' handler exception(~0p): ~0p: ~0p",
[Hook, Host, erlang:self(), EventHandler, E, R, Stack]
),
ok
end
end,
EventHandlerList
); % ok
true ->
?ERROR_MSG("(~0p|~ts|~0p) Bad event handler list: ~0p", [Hook, Host, erlang:self(), EventHandlerList]),
ok
end.
default_tracing_event_handler_list() ->
[fun tracing_timing_event_handler/7].
tracing_timing_event_handler(start_hook, EventArgs, RunType, Hook, Host, _, TracingOpts) ->
HookStart = erlang:system_time(nanosecond),
% Generate new event:
run_event_handlers(TracingOpts, Hook, Host, start_hook_timing, EventArgs ++ [HookStart], RunType);
tracing_timing_event_handler(stop_hook, EventArgs, RunType, Hook, Host, _, TracingOpts) ->
HookStop = erlang:system_time(nanosecond),
TimingMap = #{} = erlang:get(?TIMING_KEY),
{HookStart, CallbackList} = maps:get({Hook, Host}, TimingMap),
{CallbackListTiming, CallbackListTotal} = lists:foldl(
fun({_, _, _, CallbackStart, CallbackStop}=CallbackTimingInfo, {CallbackListTimingX, Total}) ->
{CallbackListTimingX ++ [CallbackTimingInfo], Total + (CallbackStop - CallbackStart)}
end,
{[], 0},
CallbackList
),
% Generate new event:
run_event_handlers(
TracingOpts,
Hook,
Host,
stop_hook_timing,
EventArgs ++ [HookStart, HookStop, CallbackListTiming, CallbackListTotal],
RunType
);
tracing_timing_event_handler(start_callback, EventArgs, RunType, Hook, Host, _, TracingOpts) ->
CallbackStart = erlang:system_time(nanosecond),
% Generate new event:
run_event_handlers(TracingOpts, Hook, Host, start_callback_timing, EventArgs ++ [CallbackStart], RunType);
tracing_timing_event_handler(stop_callback, EventArgs, RunType, Hook, Host, _, TracingOpts) ->
CallbackStop = erlang:system_time(nanosecond),
TimingMap = #{} = erlang:get(?TIMING_KEY),
{_, [{_, _, _, CallbackStart} | _]} = maps:get({Hook, Host}, TimingMap),
run_event_handlers(
TracingOpts,
Hook,
Host,
stop_callback_timing,
EventArgs ++ [CallbackStart, CallbackStop],
RunType
),
ok;
tracing_timing_event_handler(start_hook_timing, [_, HookStart], RunType, Hook, Host, EventHandlerOpts, _) ->
tracing_output(EventHandlerOpts, "(~0p|~ts|~0p|~0p) Timing started\n", [Hook, Host, erlang:self(), RunType]),
case erlang:get(?TIMING_KEY) of
#{}=TimingMap ->
erlang:put(?TIMING_KEY, TimingMap#{{Hook, Host} => {HookStart, []}});
_ ->
erlang:put(?TIMING_KEY, #{{Hook, Host} => {HookStart, []}})
end,
ok;
tracing_timing_event_handler(
stop_hook_timing,
[_, _, HookStart, HookStop, CallbackListTiming, CallbackListTotal],
RunType,
Hook,
Host,
EventHandlerOpts,
_
) ->
if
erlang:length(CallbackListTiming) < 2 -> % We don't need sorted timing result
ok;
true ->
CallbackListTimingText =
lists:foldl(
fun({Mod, Func, Arity, Diff}, CallbackListTimingText) ->
CallbackListTimingText
++ "\n\t"
++ mfa_string({Mod, Func, Arity})
++ " -> "
++ human_readable_time_string(Diff)
end,
"",
lists:keysort(
4,
[
{Mod, Func, Arity, CallbackStop - CallbackStart} ||
{Mod, Func, Arity, CallbackStart, CallbackStop} <- CallbackListTiming
]
)
),
tracing_output(
EventHandlerOpts,
"(~0p|~ts|~0p|~0p) All callbacks took ~ts to run. Sorted running time:"
++ CallbackListTimingText
++ "\n",
[Hook, Host, erlang:self(), RunType, human_readable_time_string(CallbackListTotal)]
),
tracing_output(
EventHandlerOpts,
"(~0p|~ts|~0p|~0p) Time calculations for all callbacks took ~ts\n",
[
Hook,
Host,
erlang:self(),
RunType,
human_readable_time_string((HookStop - HookStart) - CallbackListTotal)
]
)
end,
tracing_output(EventHandlerOpts, "(~0p|~ts|~0p|~0p) Timing stopped\n", [Hook, Host, erlang:self(), RunType]),
TimingMap = #{} = erlang:get(?TIMING_KEY),
NewTimingMap = maps:remove({Hook, Host}, TimingMap),
if
NewTimingMap == #{} ->
erlang:erase(?TIMING_KEY);
true ->
erlang:put(?TIMING_KEY, NewTimingMap)
end,
ok;
tracing_timing_event_handler(start_callback_timing, [Mod, Func, Args, _, CallbackStart], _, Hook, Host, _, _) ->
TimingMap = #{} = erlang:get(?TIMING_KEY),
{HookStart, Callbacks} = maps:get({Hook, Host}, TimingMap),
erlang:put(
?TIMING_KEY,
TimingMap#{
{Hook, Host} => {HookStart, [{Mod, Func, erlang:length(Args), CallbackStart} | Callbacks]}
}
),
ok;
tracing_timing_event_handler(
stop_callback_timing,
[Mod, Func, _, _, _, CallbackStart, CallbackStop],
RunType,
Hook,
Host,
EventHandlerOpts,
_
) ->
TimingMap = #{} = erlang:get(?TIMING_KEY),
{HookStart, [{Mod, Func, Arity, CallbackStart} | Callbacks]} = maps:get({Hook, Host}, TimingMap),
maps:get(output_for_each_callback, maps:get(timing, EventHandlerOpts, #{}), false) andalso tracing_output(
EventHandlerOpts,
"(~0p|~ts|~0p|~0p) "
++ mfa_string({Mod, Func, Arity})
++ " took "
++ human_readable_time_string(CallbackStop - CallbackStart)
++ "\n",
[Hook, Host, erlang:self(), RunType]
),
erlang:put(
?TIMING_KEY,
TimingMap#{
{Hook, Host} => {HookStart, [{Mod, Func, Arity, CallbackStart, CallbackStop} | Callbacks]}
}
),
ok;
tracing_timing_event_handler(_, _, _, _, _, _, _) ->
ok.
tracing_output(#{output_function := OutputF}, Text, Args) ->
try
OutputF(Text, Args)
of
_ ->
ok
catch
?EX_RULE(E, R, St) ->
Stack = ?EX_STACK(St),
?ERROR_MSG("Tracing output function exception(~0p): ~0p: ~0p", [E, R, Stack]),
ok
end;
tracing_output(#{output_log_level := Output}, Text, Args) ->
if
Output == debug ->
?DEBUG(Text, Args);
true -> % info
?INFO_MSG(Text, Args)
end,
ok;
tracing_output(Opts, Text, Args) ->
tracing_output(Opts#{output_log_level => info}, Text, Args).
mfa_string({_, Fun, _}) when erlang:is_function(Fun) ->
io_lib:format("~0p", [Fun]);
mfa_string({Mod, Func, Arity}) ->
erlang:atom_to_list(Mod) ++ ":" ++ erlang:atom_to_list(Func) ++ "/" ++ erlang:integer_to_list(Arity).
human_readable_time_string(TimeNS) ->
{Time, Unit, Decimals} =
if
TimeNS >= 1000000000 ->
{TimeNS / 1000000000, "", 10};
TimeNS >= 1000000 ->
{TimeNS / 1000000, "m", 7};
TimeNS >= 1000 ->
{TimeNS / 1000, "μ", 4};
true ->
{TimeNS / 1, "n", 0}
end,
erlang:float_to_list(Time, [{decimals, Decimals}, compact]) ++ Unit ++ "s".
ejabberd-24.12/src/mod_sip_proxy.erl 0000664 0001750 0001750 00000030665 14730775155 020056 0 ustar debalance debalance %%%-------------------------------------------------------------------
%%% File : mod_sip_proxy.erl
%%% Author : Evgeny Khramtsov
%%% Purpose :
%%% Created : 21 Apr 2014 by Evgeny Khramtsov
%%%
%%%
%%% ejabberd, Copyright (C) 2014-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_sip_proxy).
-ifndef(SIP).
-export([]).
-else.
-behaviour(p1_fsm).
%% API
-export([start/2, start_link/2, route/3, route/4]).
-export([init/1, wait_for_request/2,
wait_for_response/2, handle_event/3,
handle_sync_event/4, handle_info/3, terminate/3,
code_change/4]).
-include("logger.hrl").
-include_lib("esip/include/esip.hrl").
-define(SIGN_LIFETIME, 300). %% in seconds.
-record(state, {host = <<"">> :: binary(),
opts = [] :: [{certfile, binary()}],
orig_trid,
responses = [] :: [#sip{}],
tr_ids = [] :: list(),
orig_req = #sip{} :: #sip{}}).
%%%===================================================================
%%% API
%%%===================================================================
start(LServer, Opts) ->
supervisor:start_child(mod_sip_proxy_sup, [LServer, Opts]).
start_link(LServer, Opts) ->
p1_fsm:start_link(?MODULE, [LServer, Opts], []).
route(SIPMsg, _SIPSock, TrID, Pid) ->
p1_fsm:send_event(Pid, {SIPMsg, TrID}).
route(#sip{hdrs = Hdrs} = Req, LServer, Opts) ->
case proplists:get_bool(authenticated, Opts) of
true ->
route_statelessly(Req, LServer, Opts);
false ->
ConfiguredRRoute = get_configured_record_route(LServer),
case esip:get_hdrs('route', Hdrs) of
[{_, URI, _}|_] ->
case cmp_uri(URI, ConfiguredRRoute) of
true ->
case is_signed_by_me(URI#uri.user, Hdrs) of
true ->
route_statelessly(Req, LServer, Opts);
false ->
error
end;
false ->
error
end;
[] ->
error
end
end.
route_statelessly(Req, LServer, Opts) ->
Req1 = prepare_request(LServer, Req),
case connect(Req1, add_certfile(LServer, Opts)) of
{ok, SIPSocketsWithURIs} ->
lists:foreach(
fun({SIPSocket, _URI}) ->
Req2 = add_via(SIPSocket, LServer, Req1),
esip:send(SIPSocket, Req2)
end, SIPSocketsWithURIs);
_ ->
error
end.
%%%===================================================================
%%% gen_fsm callbacks
%%%===================================================================
init([Host, Opts]) ->
Opts1 = add_certfile(Host, Opts),
{ok, wait_for_request, #state{opts = Opts1, host = Host}}.
wait_for_request({#sip{type = request} = Req, TrID}, State) ->
Opts = State#state.opts,
Req1 = prepare_request(State#state.host, Req),
case connect(Req1, Opts) of
{ok, SIPSocketsWithURIs} ->
NewState =
lists:foldl(
fun(_SIPSocketWithURI, {error, _} = Err) ->
Err;
({SIPSocket, URI}, #state{tr_ids = TrIDs} = AccState) ->
Req2 = add_record_route_and_set_uri(
URI, State#state.host, Req1),
Req3 = add_via(SIPSocket, State#state.host, Req2),
case esip:request(SIPSocket, Req3,
{?MODULE, route, [self()]}) of
{ok, ClientTrID} ->
NewTrIDs = [ClientTrID|TrIDs],
AccState#state{tr_ids = NewTrIDs};
Err ->
cancel_pending_transactions(AccState),
Err
end
end, State, SIPSocketsWithURIs),
case NewState of
{error, _} = Err ->
{Status, Reason} = esip:error_status(Err),
esip:reply(TrID, mod_sip:make_response(
Req, #sip{type = response,
status = Status,
reason = Reason})),
{stop, normal, State};
_ ->
{next_state, wait_for_response,
NewState#state{orig_req = Req, orig_trid = TrID}}
end;
{error, notfound} ->
esip:reply(TrID, mod_sip:make_response(
Req, #sip{type = response,
status = 480,
reason = esip:reason(480)})),
{stop, normal, State};
Err ->
{Status, Reason} = esip:error_status(Err),
esip:reply(TrID, mod_sip:make_response(
Req, #sip{type = response,
status = Status,
reason = Reason})),
{stop, normal, State}
end;
wait_for_request(_Event, State) ->
{next_state, wait_for_request, State}.
wait_for_response({#sip{method = <<"CANCEL">>, type = request}, _TrID}, State) ->
cancel_pending_transactions(State),
{next_state, wait_for_response, State};
wait_for_response({Resp, TrID},
#state{orig_req = #sip{method = Method} = Req} = State) ->
case Resp of
{error, timeout} when Method /= <<"INVITE">> ->
%% Absorb useless 408. See RFC4320
choose_best_response(State),
esip:stop_transaction(State#state.orig_trid),
{stop, normal, State};
{error, _} ->
{Status, Reason} = esip:error_status(Resp),
State1 = mark_transaction_as_complete(TrID, State),
SIPResp = mod_sip:make_response(Req,
#sip{type = response,
status = Status,
reason = Reason}),
State2 = collect_response(SIPResp, State1),
case State2#state.tr_ids of
[] ->
choose_best_response(State2),
{stop, normal, State2};
_ ->
{next_state, wait_for_response, State2}
end;
#sip{status = 100} ->
{next_state, wait_for_response, State};
#sip{status = Status} ->
{[_|Vias], NewHdrs} = esip:split_hdrs('via', Resp#sip.hdrs),
NewResp = case Vias of
[] ->
Resp#sip{hdrs = NewHdrs};
_ ->
Resp#sip{hdrs = [{'via', Vias}|NewHdrs]}
end,
if Status < 300 ->
esip:reply(State#state.orig_trid, NewResp);
true ->
ok
end,
State1 = if Status >= 200 ->
mark_transaction_as_complete(TrID, State);
true ->
State
end,
State2 = if Status >= 300 ->
collect_response(NewResp, State1);
true ->
State1
end,
if Status >= 600 ->
cancel_pending_transactions(State2);
true ->
ok
end,
case State2#state.tr_ids of
[] ->
choose_best_response(State2),
{stop, normal, State2};
_ ->
{next_state, wait_for_response, State2}
end
end;
wait_for_response(_Event, State) ->
{next_state, wait_for_response, State}.
handle_event(_Event, StateName, State) ->
{next_state, StateName, State}.
handle_sync_event(_Event, _From, StateName, State) ->
Reply = ok,
{reply, Reply, StateName, State}.
handle_info(_Info, StateName, State) ->
{next_state, StateName, State}.
terminate(_Reason, _StateName, _State) ->
ok.
code_change(_OldVsn, StateName, State, _Extra) ->
{ok, StateName, State}.
%%%===================================================================
%%% Internal functions
%%%===================================================================
connect(#sip{hdrs = Hdrs} = Req, Opts) ->
{_, ToURI, _} = esip:get_hdr('to', Hdrs),
case mod_sip:at_my_host(ToURI) of
true ->
LUser = jid:nodeprep(ToURI#uri.user),
LServer = jid:nameprep(ToURI#uri.host),
case mod_sip_registrar:find_sockets(LUser, LServer) of
[_|_] = SIPSocks ->
{ok, SIPSocks};
[] ->
{error, notfound}
end;
false ->
case esip:connect(Req, Opts) of
{ok, SIPSock} ->
{ok, [{SIPSock, Req#sip.uri}]};
{error, _} = Err ->
Err
end
end.
cancel_pending_transactions(State) ->
lists:foreach(fun esip:cancel/1, State#state.tr_ids).
add_certfile(LServer, Opts) ->
case ejabberd_pkix:get_certfile(LServer) of
{ok, CertFile} ->
[{certfile, CertFile}|Opts];
error ->
Opts
end.
add_via(#sip_socket{type = Transport}, LServer, #sip{hdrs = Hdrs} = Req) ->
ConfiguredVias = get_configured_vias(LServer),
{ViaHost, ViaPort} = proplists:get_value(
Transport, ConfiguredVias, {LServer, undefined}),
ViaTransport = case Transport of
tls -> <<"TLS">>;
tcp -> <<"TCP">>;
udp -> <<"UDP">>
end,
Via = #via{transport = ViaTransport,
host = ViaHost,
port = ViaPort,
params = [{<<"branch">>, esip:make_branch()}]},
Req#sip{hdrs = [{'via', [Via]}|Hdrs]}.
add_record_route_and_set_uri(URI, LServer, #sip{hdrs = Hdrs} = Req) ->
case is_request_within_dialog(Req) of
false ->
case need_record_route(LServer) of
true ->
RR_URI = get_configured_record_route(LServer),
TS = (integer_to_binary(erlang:system_time(second))),
Sign = make_sign(TS, Hdrs),
User = <>,
NewRR_URI = RR_URI#uri{user = User},
Hdrs1 = [{'record-route', [{<<>>, NewRR_URI, []}]}|Hdrs],
Req#sip{uri = URI, hdrs = Hdrs1};
false ->
Req
end;
true ->
Req
end.
is_request_within_dialog(#sip{hdrs = Hdrs}) ->
{_, _, Params} = esip:get_hdr('to', Hdrs),
esip:has_param(<<"tag">>, Params).
need_record_route(LServer) ->
mod_sip_opt:always_record_route(LServer).
make_sign(TS, Hdrs) ->
{_, #uri{user = FUser, host = FServer}, FParams} = esip:get_hdr('from', Hdrs),
{_, #uri{user = TUser, host = TServer}, _} = esip:get_hdr('to', Hdrs),
LFUser = safe_nodeprep(FUser),
LTUser = safe_nodeprep(TUser),
LFServer = safe_nameprep(FServer),
LTServer = safe_nameprep(TServer),
FromTag = esip:get_param(<<"tag">>, FParams),
CallID = esip:get_hdr('call-id', Hdrs),
SharedKey = ejabberd_config:get_shared_key(),
str:sha([SharedKey, LFUser, LFServer, LTUser, LTServer,
FromTag, CallID, TS]).
is_signed_by_me(TS_Sign, Hdrs) ->
try
[TSBin, Sign] = str:tokens(TS_Sign, <<"-">>),
TS = (binary_to_integer(TSBin)),
NowTS = erlang:system_time(second),
true = (NowTS - TS) =< ?SIGN_LIFETIME,
Sign == make_sign(TSBin, Hdrs)
catch _:_ ->
false
end.
get_configured_vias(LServer) ->
mod_sip_opt:via(LServer).
get_configured_record_route(LServer) ->
mod_sip_opt:record_route(LServer).
get_configured_routes(LServer) ->
mod_sip_opt:routes(LServer).
mark_transaction_as_complete(TrID, State) ->
NewTrIDs = lists:delete(TrID, State#state.tr_ids),
State#state{tr_ids = NewTrIDs}.
collect_response(Resp, #state{responses = Resps} = State) ->
State#state{responses = [Resp|Resps]}.
choose_best_response(#state{responses = Responses} = State) ->
SortedResponses = lists:keysort(#sip.status, Responses),
case lists:filter(
fun(#sip{status = Status}) ->
Status >= 600
end, SortedResponses) of
[Resp|_] ->
esip:reply(State#state.orig_trid, Resp);
[] ->
case SortedResponses of
[Resp|_] ->
esip:reply(State#state.orig_trid, Resp);
[] ->
ok
end
end.
%% Just compare host part only.
cmp_uri(#uri{host = H1}, #uri{host = H2}) ->
jid:nameprep(H1) == jid:nameprep(H2).
is_my_route(URI, URIs) ->
lists:any(fun(U) -> cmp_uri(URI, U) end, URIs).
prepare_request(LServer, #sip{hdrs = Hdrs} = Req) ->
ConfiguredRRoute = get_configured_record_route(LServer),
ConfiguredRoutes = get_configured_routes(LServer),
Hdrs1 = lists:flatmap(
fun({Hdr, HdrList}) when Hdr == 'route';
Hdr == 'record-route' ->
case lists:filter(
fun({_, URI, _}) ->
not cmp_uri(URI, ConfiguredRRoute)
and not is_my_route(URI, ConfiguredRoutes)
end, HdrList) of
[] ->
[];
HdrList1 ->
[{Hdr, HdrList1}]
end;
(Hdr) ->
[Hdr]
end, Hdrs),
MF = esip:get_hdr('max-forwards', Hdrs1),
Hdrs2 = esip:set_hdr('max-forwards', MF-1, Hdrs1),
Hdrs3 = lists:filter(
fun({'proxy-authorization', {_, Params}}) ->
Realm = esip:unquote(esip:get_param(<<"realm">>, Params)),
not mod_sip:is_my_host(jid:nameprep(Realm));
(_) ->
true
end, Hdrs2),
Req#sip{hdrs = Hdrs3}.
safe_nodeprep(S) ->
case jid:nodeprep(S) of
error -> S;
S1 -> S1
end.
safe_nameprep(S) ->
case jid:nameprep(S) of
error -> S;
S1 -> S1
end.
-endif.
ejabberd-24.12/src/mod_muc_sql.erl 0000664 0001750 0001750 00000056560 14730775155 017467 0 ustar debalance debalance %%%-------------------------------------------------------------------
%%% File : mod_muc_sql.erl
%%% Author : Evgeny Khramtsov
%%% Created : 13 Apr 2016 by Evgeny Khramtsov
%%%
%%%
%%% 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_muc_sql).
-behaviour(mod_muc).
-behaviour(mod_muc_room).
%% API
-export([init/2, store_room/5, store_changes/4,
restore_room/3, forget_room/3,
can_use_nick/4, get_rooms/2, get_nick/3, set_nick/4,
import/3, export/1]).
-export([register_online_room/4, unregister_online_room/4, find_online_room/3,
get_online_rooms/3, count_online_rooms/2, rsm_supported/0,
register_online_user/4, unregister_online_user/4,
count_online_rooms_by_user/3, get_online_rooms_by_user/3,
get_subscribed_rooms/3, get_rooms_without_subscribers/2,
get_hibernated_rooms_older_than/3,
find_online_room_by_pid/2, remove_user/2]).
-export([set_affiliation/6, set_affiliations/4, get_affiliation/5,
get_affiliations/3, search_affiliation/4]).
-export([sql_schemas/0]).
-include_lib("xmpp/include/jid.hrl").
-include("mod_muc.hrl").
-include("logger.hrl").
-include("ejabberd_sql_pt.hrl").
%%%===================================================================
%%% API
%%%===================================================================
init(Host, Opts) ->
ejabberd_sql_schema:update_schema(Host, ?MODULE, sql_schemas()),
case gen_mod:ram_db_mod(Opts, mod_muc) of
?MODULE ->
clean_tables(Host);
_ ->
ok
end.
sql_schemas() ->
[#sql_schema{
version = 1,
tables =
[#sql_table{
name = <<"muc_room">>,
columns =
[#sql_column{name = <<"name">>, type = text},
#sql_column{name = <<"host">>, type = text},
#sql_column{name = <<"server_host">>, type = text},
#sql_column{name = <<"opts">>, type = {text, big}},
#sql_column{name = <<"created_at">>, type = timestamp,
default = true}],
indices = [#sql_index{
columns = [<<"name">>, <<"host">>],
unique = true},
#sql_index{
columns = [<<"host">>, <<"created_at">>]}]},
#sql_table{
name = <<"muc_registered">>,
columns =
[#sql_column{name = <<"jid">>, type = text},
#sql_column{name = <<"host">>, type = text},
#sql_column{name = <<"server_host">>, type = text},
#sql_column{name = <<"nick">>, type = text},
#sql_column{name = <<"created_at">>, type = timestamp,
default = true}],
indices = [#sql_index{
columns = [<<"jid">>, <<"host">>],
unique = true},
#sql_index{
columns = [<<"nick">>]}]},
#sql_table{
name = <<"muc_online_room">>,
columns =
[#sql_column{name = <<"name">>, type = text},
#sql_column{name = <<"host">>, type = text},
#sql_column{name = <<"server_host">>, type = text},
#sql_column{name = <<"node">>, type = text},
#sql_column{name = <<"pid">>, type = text}],
indices = [#sql_index{
columns = [<<"name">>, <<"host">>],
unique = true}]},
#sql_table{
name = <<"muc_online_users">>,
columns =
[#sql_column{name = <<"username">>, type = text},
#sql_column{name = <<"server">>, type = {text, 75}},
#sql_column{name = <<"resource">>, type = text},
#sql_column{name = <<"name">>, type = text},
#sql_column{name = <<"host">>, type = {text, 75}},
#sql_column{name = <<"server_host">>, type = text},
#sql_column{name = <<"node">>, type = text}],
indices = [#sql_index{
columns = [<<"username">>, <<"server">>,
<<"resource">>, <<"name">>,
<<"host">>],
unique = true}]},
#sql_table{
name = <<"muc_room_subscribers">>,
columns =
[#sql_column{name = <<"room">>, type = text},
#sql_column{name = <<"host">>, type = text},
#sql_column{name = <<"jid">>, type = text},
#sql_column{name = <<"nick">>, type = text},
#sql_column{name = <<"nodes">>, type = text},
#sql_column{name = <<"created_at">>, type = timestamp,
default = true}],
indices = [#sql_index{
columns = [<<"host">>, <<"room">>, <<"jid">>],
unique = true},
#sql_index{
columns = [<<"host">>, <<"jid">>]},
#sql_index{
columns = [<<"jid">>]}]}]}].
store_room(LServer, Host, Name, Opts, ChangesHints) ->
{Subs, Opts2} = case lists:keytake(subscribers, 1, Opts) of
{value, {subscribers, S}, OptN} -> {S, OptN};
_ -> {[], Opts}
end,
SOpts = misc:term_to_expr(Opts2),
Timestamp = case lists:keyfind(hibernation_time, 1, Opts) of
false -> <<"1970-01-02 00:00:00">>;
{_, undefined} -> <<"1970-01-02 00:00:00">>;
{_, Time} -> usec_to_sql_timestamp(Time)
end,
F = fun () ->
?SQL_UPSERT_T(
"muc_room",
["!name=%(Name)s",
"!host=%(Host)s",
"server_host=%(LServer)s",
"opts=%(SOpts)s",
"created_at=%(Timestamp)t"]),
case ChangesHints of
Changes when is_list(Changes) ->
[change_room(Host, Name, Change) || Change <- Changes];
_ ->
ejabberd_sql:sql_query_t(
?SQL("delete from muc_room_subscribers where "
"room=%(Name)s and host=%(Host)s")),
[change_room(Host, Name, {add_subscription, JID, Nick, Nodes})
|| {JID, Nick, Nodes} <- Subs]
end
end,
ejabberd_sql:sql_transaction(LServer, F).
store_changes(LServer, Host, Name, Changes) ->
F = fun () ->
[change_room(Host, Name, Change) || Change <- Changes]
end,
ejabberd_sql:sql_transaction(LServer, F).
change_room(Host, Room, {add_subscription, JID, Nick, Nodes}) ->
SJID = jid:encode(JID),
SNodes = misc:term_to_expr(Nodes),
?SQL_UPSERT_T(
"muc_room_subscribers",
["!jid=%(SJID)s",
"!host=%(Host)s",
"!room=%(Room)s",
"nick=%(Nick)s",
"nodes=%(SNodes)s"]);
change_room(Host, Room, {del_subscription, JID}) ->
SJID = jid:encode(JID),
ejabberd_sql:sql_query_t(?SQL("delete from muc_room_subscribers where "
"room=%(Room)s and host=%(Host)s and jid=%(SJID)s"));
change_room(Host, Room, Change) ->
?ERROR_MSG("Unsupported change on room ~ts@~ts: ~p", [Room, Host, Change]).
restore_room(LServer, Host, Name) ->
case catch ejabberd_sql:sql_query(
LServer,
?SQL("select @(opts)s from muc_room where name=%(Name)s"
" and host=%(Host)s")) of
{selected, [{Opts}]} ->
OptsD = ejabberd_sql:decode_term(Opts),
case catch ejabberd_sql:sql_query(
LServer,
?SQL("select @(jid)s, @(nick)s, @(nodes)s from muc_room_subscribers where room=%(Name)s"
" and host=%(Host)s")) of
{selected, []} ->
OptsR = mod_muc:opts_to_binary(OptsD),
case lists:keymember(subscribers, 1, OptsD) of
true ->
store_room(LServer, Host, Name, OptsR, undefined);
_ ->
ok
end,
OptsR;
{selected, Subs} ->
SubData = lists:map(
fun({Jid, Nick, Nodes}) ->
{jid:decode(Jid), Nick, ejabberd_sql:decode_term(Nodes)}
end, Subs),
Opts2 = lists:keystore(subscribers, 1, OptsD, {subscribers, SubData}),
mod_muc:opts_to_binary(Opts2);
_ ->
error
end;
_ ->
error
end.
forget_room(LServer, Host, Name) ->
F = fun () ->
ejabberd_sql:sql_query_t(
?SQL("delete from muc_room where name=%(Name)s"
" and host=%(Host)s")),
ejabberd_sql:sql_query_t(
?SQL("delete from muc_room_subscribers where room=%(Name)s"
" and host=%(Host)s"))
end,
ejabberd_sql:sql_transaction(LServer, F).
can_use_nick(LServer, ServiceOrRoom, JID, Nick) ->
SJID = jid:encode(jid:tolower(jid:remove_resource(JID))),
SqlQuery = case (jid:decode(ServiceOrRoom))#jid.lserver of
ServiceOrRoom ->
?SQL("select @(jid)s from muc_registered "
"where nick=%(Nick)s"
" and host=%(ServiceOrRoom)s");
Service ->
?SQL("select @(jid)s from muc_registered "
"where nick=%(Nick)s"
" and (host=%(ServiceOrRoom)s or host=%(Service)s)")
end,
case catch ejabberd_sql:sql_query(LServer, SqlQuery) of
{selected, [{SJID1}]} -> SJID == SJID1;
_ -> true
end.
get_rooms_without_subscribers(LServer, Host) ->
case catch ejabberd_sql:sql_query(
LServer,
?SQL("select @(name)s, @(opts)s from muc_room"
" where host=%(Host)s")) of
{selected, RoomOpts} ->
lists:map(
fun({Room, Opts}) ->
OptsD = ejabberd_sql:decode_term(Opts),
#muc_room{name_host = {Room, Host},
opts = mod_muc:opts_to_binary(OptsD)}
end, RoomOpts);
_Err ->
[]
end.
get_hibernated_rooms_older_than(LServer, Host, Timestamp) ->
TimestampS = usec_to_sql_timestamp(Timestamp),
case catch ejabberd_sql:sql_query(
LServer,
?SQL("select @(name)s, @(opts)s from muc_room"
" where host=%(Host)s and created_at < %(TimestampS)t and created_at > '1970-01-02 00:00:00'")) of
{selected, RoomOpts} ->
lists:map(
fun({Room, Opts}) ->
OptsD = ejabberd_sql:decode_term(Opts),
#muc_room{name_host = {Room, Host},
opts = mod_muc:opts_to_binary(OptsD)}
end, RoomOpts);
_Err ->
[]
end.
get_rooms(LServer, Host) ->
case catch ejabberd_sql:sql_query(
LServer,
?SQL("select @(name)s, @(opts)s from muc_room"
" where host=%(Host)s")) of
{selected, RoomOpts} ->
case catch ejabberd_sql:sql_query(
LServer,
?SQL("select @(room)s, @(jid)s, @(nick)s, @(nodes)s from muc_room_subscribers"
" where host=%(Host)s")) of
{selected, Subs} ->
SubsD = lists:foldl(
fun({Room, Jid, Nick, Nodes}, Dict) ->
Sub = {jid:decode(Jid),
Nick, ejabberd_sql:decode_term(Nodes)},
maps:update_with(
Room,
fun(SubAcc) ->
[Sub | SubAcc]
end,
[Sub],
Dict)
end, maps:new(), Subs),
lists:map(
fun({Room, Opts}) ->
OptsD = ejabberd_sql:decode_term(Opts),
OptsD2 = case {maps:find(Room, SubsD), lists:keymember(subscribers, 1, OptsD)} of
{_, true} ->
store_room(LServer, Host, Room, mod_muc:opts_to_binary(OptsD), undefined),
OptsD;
{{ok, SubsI}, false} ->
lists:keystore(subscribers, 1, OptsD, {subscribers, SubsI});
_ ->
OptsD
end,
#muc_room{name_host = {Room, Host},
opts = mod_muc:opts_to_binary(OptsD2)}
end, RoomOpts);
_Err ->
[]
end;
_Err ->
[]
end.
get_nick(LServer, Host, From) ->
SJID = jid:encode(jid:tolower(jid:remove_resource(From))),
case catch ejabberd_sql:sql_query(
LServer,
?SQL("select @(nick)s from muc_registered where"
" jid=%(SJID)s and host=%(Host)s")) of
{selected, [{Nick}]} -> Nick;
_ -> error
end.
set_nick(LServer, ServiceOrRoom, From, Nick) ->
JID = jid:encode(jid:tolower(jid:remove_resource(From))),
F = fun () ->
case Nick of
<<"">> ->
ejabberd_sql:sql_query_t(
?SQL("delete from muc_registered where"
" jid=%(JID)s and host=%(ServiceOrRoom)s")),
ok;
_ ->
Service = (jid:decode(ServiceOrRoom))#jid.lserver,
SqlQuery = case (ServiceOrRoom == Service) of
true ->
?SQL("select @(jid)s, @(host)s from muc_registered "
"where nick=%(Nick)s"
" and host=%(ServiceOrRoom)s");
false ->
?SQL("select @(jid)s, @(host)s from muc_registered "
"where nick=%(Nick)s"
" and (host=%(ServiceOrRoom)s or host=%(Service)s)")
end,
Allow = case ejabberd_sql:sql_query_t(SqlQuery) of
{selected, []}
when (ServiceOrRoom == Service) ->
%% Registering in the service...
%% check if nick is registered for some room in this service
{selected, NickRegistrations} =
ejabberd_sql:sql_query_t(
?SQL("select @(jid)s, @(host)s from muc_registered "
"where nick=%(Nick)s")),
not lists:any(fun({_NRJid, NRServiceOrRoom}) ->
Service == (jid:decode(NRServiceOrRoom))#jid.lserver end,
NickRegistrations);
{selected, []} ->
%% Nick not registered in any service or room
true;
{selected, [{_J, Host}]}
when (Host == Service) and (ServiceOrRoom /= Service) ->
%% Registering in a room, but the nick is already registered in the service
false;
{selected, [{J, _Host}]} ->
%% Registering in room (or service) a nick that is
%% already registered in this room (or service)
%% Only the owner of this registration can use the nick
J == JID
end,
if Allow ->
?SQL_UPSERT_T(
"muc_registered",
["!jid=%(JID)s",
"!host=%(ServiceOrRoom)s",
"server_host=%(LServer)s",
"nick=%(Nick)s"]),
ok;
true ->
false
end
end
end,
ejabberd_sql:sql_transaction(LServer, F).
set_affiliation(_ServerHost, _Room, _Host, _JID, _Affiliation, _Reason) ->
{error, not_implemented}.
set_affiliations(_ServerHost, _Room, _Host, _Affiliations) ->
{error, not_implemented}.
get_affiliation(_ServerHost, _Room, _Host, _LUser, _LServer) ->
{error, not_implemented}.
get_affiliations(_ServerHost, _Room, _Host) ->
{error, not_implemented}.
search_affiliation(_ServerHost, _Room, _Host, _Affiliation) ->
{error, not_implemented}.
register_online_room(ServerHost, Room, Host, Pid) ->
PidS = misc:encode_pid(Pid),
NodeS = erlang:atom_to_binary(node(Pid), latin1),
case ?SQL_UPSERT(ServerHost,
"muc_online_room",
["!name=%(Room)s",
"!host=%(Host)s",
"server_host=%(ServerHost)s",
"node=%(NodeS)s",
"pid=%(PidS)s"]) of
ok ->
ok;
Err ->
Err
end.
unregister_online_room(ServerHost, Room, Host, Pid) ->
%% TODO: report errors
PidS = misc:encode_pid(Pid),
NodeS = erlang:atom_to_binary(node(Pid), latin1),
ejabberd_sql:sql_query(
ServerHost,
?SQL("delete from muc_online_room where name=%(Room)s and "
"host=%(Host)s and node=%(NodeS)s and pid=%(PidS)s")).
find_online_room(ServerHost, Room, Host) ->
case ejabberd_sql:sql_query(
ServerHost,
?SQL("select @(pid)s, @(node)s from muc_online_room where "
"name=%(Room)s and host=%(Host)s")) of
{selected, [{PidS, NodeS}]} ->
try {ok, misc:decode_pid(PidS, NodeS)}
catch _:{bad_node, _} -> error
end;
{selected, []} ->
error;
_Err ->
error
end.
find_online_room_by_pid(ServerHost, Pid) ->
PidS = misc:encode_pid(Pid),
NodeS = erlang:atom_to_binary(node(Pid), latin1),
case ejabberd_sql:sql_query(
ServerHost,
?SQL("select @(name)s, @(host)s from muc_online_room where "
"node=%(NodeS)s and pid=%(PidS)s")) of
{selected, [{Room, Host}]} ->
{ok, Room, Host};
{selected, []} ->
error;
_Err ->
error
end.
count_online_rooms(ServerHost, Host) ->
case ejabberd_sql:sql_query(
ServerHost,
?SQL("select @(count(*))d from muc_online_room "
"where host=%(Host)s")) of
{selected, [{Num}]} ->
Num;
_Err ->
0
end.
get_online_rooms(ServerHost, Host, _RSM) ->
case ejabberd_sql:sql_query(
ServerHost,
?SQL("select @(name)s, @(pid)s, @(node)s from muc_online_room "
"where host=%(Host)s")) of
{selected, Rows} ->
lists:flatmap(
fun({Room, PidS, NodeS}) ->
try [{Room, Host, misc:decode_pid(PidS, NodeS)}]
catch _:{bad_node, _} -> []
end
end, Rows);
_Err ->
[]
end.
rsm_supported() ->
false.
register_online_user(ServerHost, {U, S, R}, Room, Host) ->
NodeS = erlang:atom_to_binary(node(), latin1),
case ?SQL_UPSERT(ServerHost, "muc_online_users",
["!username=%(U)s",
"!server=%(S)s",
"!resource=%(R)s",
"!name=%(Room)s",
"!host=%(Host)s",
"server_host=%(ServerHost)s",
"node=%(NodeS)s"]) of
ok ->
ok;
Err ->
Err
end.
unregister_online_user(ServerHost, {U, S, R}, Room, Host) ->
%% TODO: report errors
ejabberd_sql:sql_query(
ServerHost,
?SQL("delete from muc_online_users where username=%(U)s and "
"server=%(S)s and resource=%(R)s and name=%(Room)s and "
"host=%(Host)s")).
count_online_rooms_by_user(ServerHost, U, S) ->
case ejabberd_sql:sql_query(
ServerHost,
?SQL("select @(count(*))d from muc_online_users where "
"username=%(U)s and server=%(S)s")) of
{selected, [{Num}]} ->
Num;
_Err ->
0
end.
get_online_rooms_by_user(ServerHost, U, S) ->
case ejabberd_sql:sql_query(
ServerHost,
?SQL("select @(name)s, @(host)s from muc_online_users where "
"username=%(U)s and server=%(S)s")) of
{selected, Rows} ->
Rows;
_Err ->
[]
end.
export(_Server) ->
[{muc_room,
fun(Host, #muc_room{name_host = {Name, RoomHost}, opts = Opts}) ->
case str:suffix(Host, RoomHost) of
true ->
SOpts = misc:term_to_expr(Opts),
[?SQL("delete from muc_room where name=%(Name)s"
" and host=%(RoomHost)s;"),
?SQL_INSERT(
"muc_room",
["name=%(Name)s",
"host=%(RoomHost)s",
"server_host=%(Host)s",
"opts=%(SOpts)s"])];
false ->
[]
end
end},
{muc_registered,
fun(Host, #muc_registered{us_host = {{U, S}, RoomHost},
nick = Nick}) ->
case str:suffix(Host, RoomHost) of
true ->
SJID = jid:encode(jid:make(U, S)),
[?SQL("delete from muc_registered where"
" jid=%(SJID)s and host=%(RoomHost)s;"),
?SQL_INSERT(
"muc_registered",
["jid=%(SJID)s",
"host=%(RoomHost)s",
"server_host=%(Host)s",
"nick=%(Nick)s"])];
false ->
[]
end
end}].
import(_, _, _) ->
ok.
get_subscribed_rooms(LServer, Host, Jid) ->
JidS = jid:encode(Jid),
case ejabberd_sql:sql_query(
LServer,
?SQL("select @(room)s, @(nick)s, @(nodes)s from muc_room_subscribers "
"where jid=%(JidS)s and host=%(Host)s")) of
{selected, Subs} ->
{ok, [{jid:make(Room, Host), Nick, ejabberd_sql:decode_term(Nodes)}
|| {Room, Nick, Nodes} <- Subs]};
_Error ->
{error, db_failure}
end.
remove_user(LUser, LServer) ->
SJID = jid:encode(jid:make(LUser, LServer)),
ejabberd_sql:sql_query(
LServer,
?SQL("delete from muc_room_subscribers where jid=%(SJID)s")),
ok.
%%%===================================================================
%%% Internal functions
%%%===================================================================
clean_tables(ServerHost) ->
NodeS = erlang:atom_to_binary(node(), latin1),
?DEBUG("Cleaning SQL muc_online_room table...", []),
case ejabberd_sql:sql_query(
ServerHost,
?SQL("delete from muc_online_room where node=%(NodeS)s")) of
{updated, _} ->
ok;
Err1 ->
?ERROR_MSG("Failed to clean 'muc_online_room' table: ~p", [Err1]),
Err1
end,
?DEBUG("Cleaning SQL muc_online_users table...", []),
case ejabberd_sql:sql_query(
ServerHost,
?SQL("delete from muc_online_users where node=%(NodeS)s")) of
{updated, _} ->
ok;
Err2 ->
?ERROR_MSG("Failed to clean 'muc_online_users' table: ~p", [Err2]),
Err2
end.
usec_to_sql_timestamp(Timestamp) ->
TS = misc:usec_to_now(Timestamp),
case calendar:now_to_universal_time(TS) of
{{Year, Month, Day}, {Hour, Minute, Second}} ->
list_to_binary(io_lib:format("~4..0B-~2..0B-~2..0B "
"~2..0B:~2..0B:~2..0B",
[Year, Month, Day, Hour, Minute, Second]))
end.
ejabberd-24.12/src/mod_admin_extra.erl 0000664 0001750 0001750 00000271531 14730775155 020314 0 ustar debalance debalance %%%-------------------------------------------------------------------
%%% File : mod_admin_extra.erl
%%% Author : Badlop
%%% Purpose : Contributed administrative functions and commands
%%% Created : 10 Aug 2008 by Badlop
%%%
%%%
%%% 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_admin_extra).
-author('badlop@process-one.net').
-behaviour(gen_mod).
-include("logger.hrl").
-include("translate.hrl").
-export([start/2, stop/1, reload/3, mod_options/1,
get_commands_spec/0, depends/2, mod_doc/0]).
% Commands API
-export([
% Adminsys
compile/1, get_cookie/0,
restart_module/2,
% Sessions
num_resources/2, resource_num/3,
kick_session/4, status_num/2, status_num/1,
status_list/2, status_list_v3/2,
status_list/1, status_list_v3/1, connected_users_info/0,
connected_users_vhost/1, set_presence/7,
get_presence/2, user_sessions_info/2, get_last/2, set_last/4,
% Accounts
set_password/3, check_password_hash/4, delete_old_users/1,
delete_old_users_vhost/2, check_password/3,
ban_account/3, ban_account_v2/3, get_ban_details/2, unban_account/2,
% vCard
set_nickname/3, get_vcard/3,
get_vcard/4, get_vcard_multi/4, set_vcard/4,
set_vcard/5,
% Roster
add_rosteritem/7, delete_rosteritem/4,
get_roster/2, get_roster_count/2, push_roster/3,
push_roster_all/1, push_alltoall/2,
push_roster_item/5, build_roster_item/3,
% Private storage
private_get/4, private_set/3,
% Shared roster
srg_create/5, srg_add/2,
srg_delete/2, srg_list/1, srg_get_info/2,
srg_set_info/4,
srg_get_displayed/2, srg_add_displayed/3, srg_del_displayed/3,
srg_get_members/2, srg_user_add/4, srg_user_del/4,
% Send message
send_message/5, send_stanza/3, send_stanza_c2s/4,
% Privacy list
privacy_set/3,
% Stats
stats/1, stats/2
]).
-export([web_menu_main/2, web_page_main/2,
web_menu_host/3, web_page_host/3,
web_menu_hostuser/4, web_page_hostuser/4,
web_menu_hostnode/4, web_page_hostnode/4,
web_menu_node/3, web_page_node/3]).
-import(ejabberd_web_admin, [make_command/4, make_table/2]).
-include("ejabberd_commands.hrl").
-include("ejabberd_http.hrl").
-include("ejabberd_web_admin.hrl").
-include("mod_roster.hrl").
-include("mod_privacy.hrl").
-include("ejabberd_sm.hrl").
-include_lib("xmpp/include/scram.hrl").
-include_lib("xmpp/include/xmpp.hrl").
%%%
%%% gen_mod
%%%
start(_Host, _Opts) ->
ejabberd_commands:register_commands(?MODULE, get_commands_spec()),
{ok, [{hook, webadmin_menu_main, web_menu_main, 50, global},
{hook, webadmin_page_main, web_page_main, 50, global},
{hook, webadmin_menu_host, web_menu_host, 50},
{hook, webadmin_page_host, web_page_host, 50},
{hook, webadmin_menu_hostuser, web_menu_hostuser, 50},
{hook, webadmin_page_hostuser, web_page_hostuser, 50},
{hook, webadmin_menu_hostnode, web_menu_hostnode, 50},
{hook, webadmin_page_hostnode, web_page_hostnode, 50},
{hook, webadmin_menu_node, web_menu_node, 50, global},
{hook, webadmin_page_node, web_page_node, 50, global}]}.
stop(Host) ->
case gen_mod:is_loaded_elsewhere(Host, ?MODULE) of
false ->
ejabberd_commands:unregister_commands(get_commands_spec());
true ->
ok
end.
reload(_Host, _NewOpts, _OldOpts) ->
ok.
depends(_Host, _Opts) ->
[].
%%%
%%% Register commands
%%%
get_commands_spec() ->
Vcard1FieldsString = "Some vcard field names in `get`/`set_vcard` are:\n\n"
"* FN - Full Name\n"
"* NICKNAME - Nickname\n"
"* BDAY - Birthday\n"
"* TITLE - Work: Position\n"
"* ROLE - Work: Role\n",
Vcard2FieldsString = "Some vcard field names and subnames in `get`/`set_vcard2` are:\n\n"
"* N FAMILY - Family name\n"
"* N GIVEN - Given name\n"
"* N MIDDLE - Middle name\n"
"* ADR CTRY - Address: Country\n"
"* ADR LOCALITY - Address: City\n"
"* TEL HOME - Telephone: Home\n"
"* TEL CELL - Telephone: Cellphone\n"
"* TEL WORK - Telephone: Work\n"
"* TEL VOICE - Telephone: Voice\n"
"* EMAIL USERID - E-Mail Address\n"
"* ORG ORGNAME - Work: Company\n"
"* ORG ORGUNIT - Work: Department\n",
VcardXEP = "For a full list of vCard fields check [XEP-0054: vcard-temp]"
"(https://xmpp.org/extensions/xep-0054.html)",
[
#ejabberd_commands{name = compile, tags = [erlang],
desc = "Recompile and reload Erlang source code file",
module = ?MODULE, function = compile,
args = [{file, string}],
args_example = ["/home/me/srcs/ejabberd/mod_example.erl"],
args_desc = ["Filename of erlang source file to compile"],
result = {res, rescode},
result_example = ok},
#ejabberd_commands{name = get_cookie, tags = [erlang],
desc = "Get the Erlang cookie of this node",
module = ?MODULE, function = get_cookie,
args = [],
result = {cookie, string},
result_example = "MWTAVMODFELNLSMYXPPD",
result_desc = "Erlang cookie used for authentication by ejabberd"},
#ejabberd_commands{name = restart_module, tags = [erlang],
desc = "Stop an ejabberd module, reload code and start",
module = ?MODULE, function = restart_module,
args = [{host, binary}, {module, binary}],
args_example = ["myserver.com","mod_admin_extra"],
args_desc = ["Server name", "Module to restart"],
result = {res, integer},
result_example = 0,
result_desc = "Returns integer code:\n"
" - `0`: code reloaded, module restarted\n"
" - `1`: error: module not loaded\n"
" - `2`: code not reloaded, but module restarted"},
#ejabberd_commands{name = delete_old_users, tags = [accounts, purge],
desc = "Delete users that didn't log in last days, or that never logged",
longdesc = "To protect admin accounts, configure this for example:\n"
"``` yaml\n"
"access_rules:\n"
" protect_old_users:\n"
" - allow: admin\n"
" - deny: all\n"
"```\n",
module = ?MODULE, function = delete_old_users,
args = [{days, integer}],
args_example = [30],
args_desc = ["Last login age in days of accounts that should be removed"],
result = {res, restuple},
result_example = {ok, <<"Deleted 2 users: [\"oldman@myserver.com\", \"test@myserver.com\"]">>},
result_desc = "Result tuple"},
#ejabberd_commands{name = delete_old_users_vhost, tags = [accounts, purge],
desc = "Delete users that didn't log in last days in vhost, or that never logged",
longdesc = "To protect admin accounts, configure this for example:\n"
"``` yaml\n"
"access_rules:\n"
" delete_old_users:\n"
" - deny: admin\n"
" - allow: all\n"
"```\n",
module = ?MODULE, function = delete_old_users_vhost,
args = [{host, binary}, {days, integer}],
args_example = [<<"myserver.com">>, 30],
args_desc = ["Server name",
"Last login age in days of accounts that should be removed"],
result = {res, restuple},
result_example = {ok, <<"Deleted 2 users: [\"oldman@myserver.com\", \"test@myserver.com\"]">>},
result_desc = "Result tuple"},
#ejabberd_commands{name = check_account, tags = [accounts],
desc = "Check if an account exists or not",
module = ejabberd_auth, function = user_exists,
args = [{user, binary}, {host, binary}],
args_example = [<<"peter">>, <<"myserver.com">>],
args_desc = ["User name to check", "Server to check"],
result = {res, rescode},
result_example = ok},
#ejabberd_commands{name = check_password, tags = [accounts],
desc = "Check if a password is correct",
module = ?MODULE, function = check_password,
args = [{user, binary}, {host, binary}, {password, binary}],
args_example = [<<"peter">>, <<"myserver.com">>, <<"secret">>],
args_desc = ["User name to check", "Server to check", "Password to check"],
result = {res, rescode},
result_example = ok},
#ejabberd_commands{name = check_password_hash, tags = [accounts],
desc = "Check if the password hash is correct",
longdesc = "Allows hash methods from the Erlang/OTP "
"[crypto](https://www.erlang.org/doc/man/crypto) application.",
module = ?MODULE, function = check_password_hash,
args = [{user, binary}, {host, binary}, {passwordhash, binary},
{hashmethod, binary}],
args_example = [<<"peter">>, <<"myserver.com">>,
<<"5ebe2294ecd0e0f08eab7690d2a6ee69">>, <<"md5">>],
args_desc = ["User name to check", "Server to check",
"Password's hash value", "Name of hash method"],
result = {res, rescode},
result_example = ok},
#ejabberd_commands{name = change_password, tags = [accounts],
desc = "Change the password of an account",
module = ?MODULE, function = set_password,
args = [{user, binary}, {host, binary}, {newpass, binary}],
args_example = [<<"peter">>, <<"myserver.com">>, <<"blank">>],
args_desc = ["User name", "Server name",
"New password for user"],
result = {res, rescode},
result_example = ok},
#ejabberd_commands{name = ban_account, tags = [accounts],
desc = "Ban an account: kick sessions and set random password",
longdesc = "This simply sets a random password.",
module = ?MODULE, function = ban_account,
args = [{user, binary}, {host, binary}, {reason, binary}],
args_example = [<<"attacker">>, <<"myserver.com">>, <<"Spaming other users">>],
args_desc = ["User name to ban", "Server name",
"Reason for banning user"],
result = {res, rescode},
result_example = ok},
#ejabberd_commands{name = ban_account, tags = [accounts],
desc = "Ban an account",
longdesc = "This command kicks the account sessions, "
"sets a random password, and stores ban details in the "
"account private storage. "
"This command requires _`mod_private`_ to be enabled. "
"Check also _`get_ban_details`_ API "
"and _`unban_account`_ API.",
module = ?MODULE, function = ban_account_v2,
version = 2,
note = "improved in 24.06",
args = [{user, binary}, {host, binary}, {reason, binary}],
args_example = [<<"attacker">>, <<"myserver.com">>, <<"Spaming other users">>],
args_desc = ["User name to ban", "Server name",
"Reason for banning user"],
result = {res, rescode},
result_example = ok},
#ejabberd_commands{name = get_ban_details, tags = [accounts],
desc = "Get ban details about an account",
longdesc = "Check _`ban_account`_ API.",
module = ?MODULE, function = get_ban_details,
version = 2,
note = "added in 24.06",
args = [{user, binary}, {host, binary}],
args_example = [<<"attacker">>, <<"myserver.com">>],
args_desc = ["User name to unban", "Server name"],
result = {ban_details, {list,
{detail, {tuple, [{name, string},
{value, string}
]}}
}},
result_example = [{"reason", "Spamming other users"},
{"bandate", "2024-04-22T09:16:47.975312Z"},
{"lastdate", "2024-04-22T08:39:12Z"},
{"lastreason", "Connection reset by peer"}]},
#ejabberd_commands{name = unban_account, tags = [accounts],
desc = "Revert the ban from an account: set back the old password",
longdesc = "Check _`ban_account`_ API.",
module = ?MODULE, function = unban_account,
version = 2,
note = "added in 24.06",
args = [{user, binary}, {host, binary}],
args_example = [<<"gooduser">>, <<"myserver.com">>],
args_desc = ["User name to unban", "Server name"],
result = {res, rescode},
result_example = ok},
#ejabberd_commands{name = num_resources, tags = [session],
desc = "Get the number of resources of a user",
module = ?MODULE, function = num_resources,
args = [{user, binary}, {host, binary}],
args_example = [<<"peter">>, <<"myserver.com">>],
args_desc = ["User name", "Server name"],
result = {resources, integer},
result_example = 5,
result_desc = "Number of active resources for a user"},
#ejabberd_commands{name = resource_num, tags = [session],
desc = "Resource string of a session number",
module = ?MODULE, function = resource_num,
args = [{user, binary}, {host, binary}, {num, integer}],
args_example = [<<"peter">>, <<"myserver.com">>, 2],
args_desc = ["User name", "Server name", "ID of resource to return"],
result = {resource, string},
result_example = <<"Psi">>,
result_desc = "Name of user resource"},
#ejabberd_commands{name = kick_session, tags = [session],
desc = "Kick a user session",
module = ?MODULE, function = kick_session,
args = [{user, binary}, {host, binary}, {resource, binary}, {reason, binary}],
args_example = [<<"peter">>, <<"myserver.com">>, <<"Psi">>,
<<"Stuck connection">>],
args_desc = ["User name", "Server name", "User's resource",
"Reason for closing session"],
result = {res, rescode},
result_example = ok},
#ejabberd_commands{name = status_num_host, tags = [session, statistics],
desc = "Number of logged users with this status in host",
policy = admin,
module = ?MODULE, function = status_num,
args = [{host, binary}, {status, binary}],
args_example = [<<"myserver.com">>, <<"dnd">>],
args_desc = ["Server name", "Status type to check"],
result = {users, integer},
result_example = 23,
result_desc = "Number of connected sessions with given status type"},
#ejabberd_commands{name = status_num, tags = [session, statistics],
desc = "Number of logged users with this status",
policy = admin,
module = ?MODULE, function = status_num,
args = [{status, binary}],
args_example = [<<"dnd">>],
args_desc = ["Status type to check"],
result = {users, integer},
result_example = 23,
result_desc = "Number of connected sessions with given status type"},
#ejabberd_commands{name = status_list_host, tags = [session],
desc = "List of users logged in host with their statuses",
module = ?MODULE, function = status_list,
args = [{host, binary}, {status, binary}],
args_example = [<<"myserver.com">>, <<"dnd">>],
args_desc = ["Server name", "Status type to check"],
result_example = [{<<"peter">>,<<"myserver.com">>,<<"tka">>,6,<<"Busy">>}],
result = {users, {list,
{userstatus, {tuple, [
{user, string},
{host, string},
{resource, string},
{priority, integer},
{status, string}
]}}
}}},
#ejabberd_commands{name = status_list_host, tags = [session],
desc = "List of users logged in host with their statuses",
module = ?MODULE, function = status_list_v3,
version = 3,
note = "updated in 24.12",
args = [{host, binary}, {status, binary}],
args_example = [<<"myserver.com">>, <<"dnd">>],
args_desc = ["Server name", "Status type to check"],
result_example = [{<<"peter@myserver.com/tka">>,6,<<"Busy">>}],
result = {users, {list,
{userstatus, {tuple, [{jid, string},
{priority, integer},
{status, string}
]}}
}}},
#ejabberd_commands{name = status_list, tags = [session],
desc = "List of logged users with this status",
module = ?MODULE, function = status_list,
args = [{status, binary}],
args_example = [<<"dnd">>],
args_desc = ["Status type to check"],
result_example = [{<<"peter">>,<<"myserver.com">>,<<"tka">>,6,<<"Busy">>}],
result = {users, {list,
{userstatus, {tuple, [
{user, string},
{host, string},
{resource, string},
{priority, integer},
{status, string}
]}}
}}},
#ejabberd_commands{name = status_list, tags = [session],
desc = "List of logged users with this status",
module = ?MODULE, function = status_list_v3,
version = 3,
note = "updated in 24.12",
args = [{status, binary}],
args_example = [<<"dnd">>],
args_desc = ["Status type to check"],
result_example = [{<<"peter@myserver.com/tka">>,6,<<"Busy">>}],
result = {users, {list,
{userstatus, {tuple, [{jid, string},
{priority, integer},
{status, string}
]}}
}}},
#ejabberd_commands{name = connected_users_info,
tags = [session],
desc = "List all established sessions and their information",
module = ?MODULE, function = connected_users_info,
args = [],
result_example = [{"user1@myserver.com/tka",
"c2s", "127.0.0.1", 42656,8, "ejabberd@localhost",
231, <<"dnd">>, <<"tka">>, <<>>}],
result = {connected_users_info,
{list,
{session, {tuple,
[{jid, string},
{connection, string},
{ip, string},
{port, integer},
{priority, integer},
{node, string},
{uptime, integer},
{status, string},
{resource, string},
{statustext, string}
]}}
}}},
#ejabberd_commands{name = connected_users_vhost,
tags = [session],
desc = "Get the list of established sessions in a vhost",
module = ?MODULE, function = connected_users_vhost,
args_example = [<<"myexample.com">>],
args_desc = ["Server name"],
args = [{host, binary}],
result_example = [<<"user1@myserver.com/tka">>, <<"user2@localhost/tka">>],
result_desc = "List of sessions full JIDs",
result = {connected_users_vhost, {list, {sessions, string}}}},
#ejabberd_commands{name = user_sessions_info,
tags = [session],
desc = "Get information about all sessions of a user",
module = ?MODULE, function = user_sessions_info,
args = [{user, binary}, {host, binary}],
args_example = [<<"peter">>, <<"myserver.com">>],
args_desc = ["User name", "Server name"],
result_example = [{"c2s", "127.0.0.1", 42656,8, "ejabberd@localhost",
231, <<"dnd">>, <<"tka">>, <<>>}],
result = {sessions_info,
{list,
{session, {tuple,
[{connection, string},
{ip, string},
{port, integer},
{priority, integer},
{node, string},
{uptime, integer},
{status, string},
{resource, string},
{statustext, string}
]}}
}}},
#ejabberd_commands{name = get_presence, tags = [session],
desc =
"Retrieve the resource with highest priority, "
"and its presence (show and status message) "
"for a given user.",
longdesc =
"The `jid` value contains the user JID "
"with resource.\n\nThe `show` value contains "
"the user presence flag. It can take "
"limited values:\n\n - `available`\n - `chat` "
"(Free for chat)\n - `away`\n - `dnd` (Do "
"not disturb)\n - `xa` (Not available, "
"extended away)\n - `unavailable` (Not "
"connected)\n\n`status` is a free text "
"defined by the user client.",
module = ?MODULE, function = get_presence,
args = [{user, binary}, {host, binary}],
args_rename = [{server, host}],
args_example = [<<"peter">>, <<"myexample.com">>],
args_desc = ["User name", "Server name"],
result_example = {<<"user1@myserver.com/tka">>, <<"dnd">>, <<"Busy">>},
result =
{presence,
{tuple,
[{jid, string}, {show, string},
{status, string}]}}},
#ejabberd_commands{name = set_presence,
tags = [session],
desc = "Set presence of a session",
module = ?MODULE, function = set_presence,
args = [{user, binary}, {host, binary},
{resource, binary}, {type, binary},
{show, binary}, {status, binary},
{priority, binary}],
args_example = [<<"user1">>,<<"myserver.com">>,<<"tka1">>,
<<"available">>,<<"away">>,<<"BB">>, <<"7">>],
args_desc = ["User name", "Server name", "Resource",
"Type: `available`, `error`, `probe`...",
"Show: `away`, `chat`, `dnd`, `xa`.", "Status text",
"Priority, provide this value as an integer"],
result = {res, rescode}},
#ejabberd_commands{name = set_presence,
tags = [session],
desc = "Set presence of a session",
module = ?MODULE, function = set_presence,
version = 1,
note = "updated in 24.02",
args = [{user, binary}, {host, binary},
{resource, binary}, {type, binary},
{show, binary}, {status, binary},
{priority, integer}],
args_example = [<<"user1">>,<<"myserver.com">>,<<"tka1">>,
<<"available">>,<<"away">>,<<"BB">>, 7],
args_desc = ["User name", "Server name", "Resource",
"Type: `available`, `error`, `probe`...",
"Show: `away`, `chat`, `dnd`, `xa`.", "Status text",
"Priority, provide this value as an integer"],
result = {res, rescode}},
#ejabberd_commands{name = set_nickname, tags = [vcard],
desc = "Set nickname in a user's vCard",
module = ?MODULE, function = set_nickname,
args = [{user, binary}, {host, binary}, {nickname, binary}],
args_example = [<<"user1">>,<<"myserver.com">>,<<"User 1">>],
args_desc = ["User name", "Server name", "Nickname"],
result = {res, rescode}},
#ejabberd_commands{name = get_vcard, tags = [vcard],
desc = "Get content from a vCard field",
longdesc = Vcard1FieldsString ++ "\n" ++ VcardXEP,
module = ?MODULE, function = get_vcard,
args = [{user, binary}, {host, binary}, {name, binary}],
args_example = [<<"user1">>,<<"myserver.com">>,<<"NICKNAME">>],
args_desc = ["User name", "Server name", "Field name"],
result_example = "User 1",
result_desc = "Field content",
result = {content, string}},
#ejabberd_commands{name = get_vcard2, tags = [vcard],
desc = "Get content from a vCard subfield",
longdesc = Vcard2FieldsString ++ "\n" ++ VcardXEP,
module = ?MODULE, function = get_vcard,
args = [{user, binary}, {host, binary}, {name, binary}, {subname, binary}],
args_example = [<<"user1">>,<<"myserver.com">>,<<"N">>, <<"FAMILY">>],
args_desc = ["User name", "Server name", "Field name", "Subfield name"],
result_example = "Schubert",
result_desc = "Field content",
result = {content, string}},
#ejabberd_commands{name = get_vcard2_multi, tags = [vcard],
desc = "Get multiple contents from a vCard field",
longdesc = Vcard2FieldsString ++ "\n" ++ VcardXEP,
module = ?MODULE, function = get_vcard_multi,
args = [{user, binary}, {host, binary}, {name, binary}, {subname, binary}],
result = {contents, {list, {value, string}}}},
#ejabberd_commands{name = set_vcard, tags = [vcard],
desc = "Set content in a vCard field",
longdesc = Vcard1FieldsString ++ "\n" ++ VcardXEP,
module = ?MODULE, function = set_vcard,
args = [{user, binary}, {host, binary}, {name, binary}, {content, binary}],
args_example = [<<"user1">>,<<"myserver.com">>, <<"URL">>, <<"www.example.com">>],
args_desc = ["User name", "Server name", "Field name", "Value"],
result = {res, rescode}},
#ejabberd_commands{name = set_vcard2, tags = [vcard],
desc = "Set content in a vCard subfield",
longdesc = Vcard2FieldsString ++ "\n" ++ VcardXEP,
module = ?MODULE, function = set_vcard,
args = [{user, binary}, {host, binary}, {name, binary}, {subname, binary}, {content, binary}],
args_example = [<<"user1">>,<<"myserver.com">>,<<"TEL">>, <<"NUMBER">>, <<"123456">>],
args_desc = ["User name", "Server name", "Field name", "Subfield name", "Value"],
result = {res, rescode}},
#ejabberd_commands{name = set_vcard2_multi, tags = [vcard],
desc = "Set multiple contents in a vCard subfield",
longdesc = Vcard2FieldsString ++ "\n" ++ VcardXEP,
module = ?MODULE, function = set_vcard,
args = [{user, binary}, {host, binary}, {name, binary}, {subname, binary}, {contents, {list, {value, binary}}}],
result = {res, rescode}},
#ejabberd_commands{name = add_rosteritem, tags = [roster],
desc = "Add an item to a user's roster (supports ODBC)",
longdesc = "Group can be several groups separated by `;` for example: `g1;g2;g3`",
module = ?MODULE, function = add_rosteritem,
args = [{localuser, binary}, {localhost, binary},
{user, binary}, {host, binary},
{nick, binary}, {group, binary},
{subs, binary}],
args_rename = [{localserver, localhost}, {server, host}],
args_example = [<<"user1">>,<<"myserver.com">>,<<"user2">>, <<"myserver.com">>,
<<"User 2">>, <<"Friends">>, <<"both">>],
args_desc = ["User name", "Server name", "Contact user name", "Contact server name",
"Nickname", "Group", "Subscription"],
result = {res, rescode}},
#ejabberd_commands{name = add_rosteritem, tags = [roster],
desc = "Add an item to a user's roster (supports ODBC)",
module = ?MODULE, function = add_rosteritem,
version = 1,
note = "updated in 24.02",
args = [{localuser, binary}, {localhost, binary},
{user, binary}, {host, binary},
{nick, binary}, {groups, {list, {group, binary}}},
{subs, binary}],
args_rename = [{localserver, localhost}, {server, host}],
args_example = [<<"user1">>,<<"myserver.com">>,<<"user2">>, <<"myserver.com">>,
<<"User 2">>, [<<"Friends">>, <<"Team 1">>], <<"both">>],
args_desc = ["User name", "Server name", "Contact user name", "Contact server name",
"Nickname", "Groups", "Subscription"],
result = {res, rescode}},
%%{"", "subs= none, from, to or both"},
%%{"", "example: add-roster peter localhost mike server.com MiKe Employees both"},
%%{"", "will add mike@server.com to peter@localhost roster"},
#ejabberd_commands{name = delete_rosteritem, tags = [roster],
desc = "Delete an item from a user's roster (supports ODBC)",
module = ?MODULE, function = delete_rosteritem,
args = [{localuser, binary}, {localhost, binary},
{user, binary}, {host, binary}],
args_rename = [{localserver, localhost}, {server, host}],
args_example = [<<"user1">>,<<"myserver.com">>,<<"user2">>, <<"myserver.com">>],
args_desc = ["User name", "Server name", "Contact user name", "Contact server name"],
result = {res, rescode}},
#ejabberd_commands{name = process_rosteritems, tags = [roster],
desc = "List/delete rosteritems that match filter",
longdesc = "Explanation of each argument:\n\n"
"* `action`: what to do with each rosteritem that "
"matches all the filtering options\n"
"* `subs`: subscription type\n"
"* `asks`: pending subscription\n"
"* `users`: the JIDs of the local user\n"
"* `contacts`: the JIDs of the contact in the roster\n"
"\n"
"**Mnesia backend:**\n"
"\n"
"Allowed values in the arguments:\n\n"
"* `action` = `list` | `delete`\n"
"* `subs` = `any` | SUB[:SUB]*\n"
"* `asks` = `any` | ASK[:ASK]*\n"
"* `users` = `any` | JID[:JID]*\n"
"* `contacts` = `any` | JID[:JID]*\n"
"\nwhere\n\n"
"* SUB = `none` | `from `| `to` | `both`\n"
"* ASK = `none` | `out` | `in`\n"
"* JID = characters valid in a JID, and can use the "
"globs: `*`, `?`, `!` and `[...]`\n"
"\n"
"This example will list roster items with subscription "
"`none`, `from` or `to` that have any ask property, of "
"local users which JID is in the virtual host "
"`example.org` and that the contact JID is either a "
"bare server name (without user part) or that has a "
"user part and the server part contains the word `icq`"
":\n `list none:from:to any *@example.org *:*@*icq*`"
"\n\n"
"**SQL backend:**\n"
"\n"
"Allowed values in the arguments:\n\n"
"* `action` = `list` | `delete`\n"
"* `subs` = `any` | SUB\n"
"* `asks` = `any` | ASK\n"
"* `users` = JID\n"
"* `contacts` = JID\n"
"\nwhere\n\n"
"* SUB = `none` | `from` | `to` | `both`\n"
"* ASK = `none` | `out` | `in`\n"
"* JID = characters valid in a JID, and can use the "
"globs: `_` and `%`\n"
"\n"
"This example will list roster items with subscription "
"`to` that have any ask property, of "
"local users which JID is in the virtual host "
"`example.org` and that the contact JID's "
"server part contains the word `icq`"
":\n `list to any %@example.org %@%icq%`",
module = mod_roster, function = process_rosteritems,
args = [{action, string}, {subs, string},
{asks, string}, {users, string},
{contacts, string}],
result = {response,
{list,
{pairs, {tuple,
[{user, string},
{contact, string}
]}}
}}},
#ejabberd_commands{name = get_roster, tags = [roster],
desc = "Get list of contacts in a local user roster",
longdesc =
"`subscription` can be: `none`, `from`, `to`, `both`.\n\n"
"`pending` can be: `in`, `out`, `none`.",
note = "improved in 23.10",
policy = user,
module = ?MODULE, function = get_roster,
args = [],
args_rename = [{server, host}],
result_example = [{<<"user2@localhost">>, <<"User 2">>, <<"none">>, <<"subscribe">>, [<<"Group1">>]}],
result = {contacts, {list, {contact, {tuple, [
{jid, string},
{nick, string},
{subscription, string},
{pending, string},
{groups, {list, {group, string}}}
]}}}}},
#ejabberd_commands{name = get_roster_count, tags = [roster],
desc = "Get number of contacts in a local user roster",
note = "added in 24.06",
policy = user,
module = ?MODULE, function = get_roster_count,
args = [],
args_example = [<<"sun">>, <<"localhost">>],
args_rename = [{server, host}],
result_example = 5,
result_desc = "Number",
result = {value, integer}},
#ejabberd_commands{name = push_roster, tags = [roster],
desc = "Push template roster from file to a user",
longdesc = "The text file must contain an erlang term: a list "
"of tuples with username, servername, group and nick. For example:\n"
"`[{\"user1\", \"localhost\", \"Workers\", \"User 1\"},\n"
" {\"user2\", \"localhost\", \"Workers\", \"User 2\"}].`\n\n"
"If there are problems parsing UTF8 character encoding, "
"provide the corresponding string with the `<<\"STRING\"/utf8>>` syntax, for example:\n"
"`[{\"user2\", \"localhost\", \"Workers\", <<\"User 2\"/utf8>>}]`.",
module = ?MODULE, function = push_roster,
args = [{file, binary}, {user, binary}, {host, binary}],
args_example = [<<"/home/ejabberd/roster.txt">>, <<"user1">>, <<"localhost">>],
args_desc = ["File path", "User name", "Server name"],
result = {res, rescode}},
#ejabberd_commands{name = push_roster_all, tags = [roster],
desc = "Push template roster from file to all those users",
longdesc = "The text file must contain an erlang term: a list "
"of tuples with username, servername, group and nick. Example:\n"
"`[{\"user1\", \"localhost\", \"Workers\", \"User 1\"},\n"
" {\"user2\", \"localhost\", \"Workers\", \"User 2\"}].`",
module = ?MODULE, function = push_roster_all,
args = [{file, binary}],
args_example = [<<"/home/ejabberd/roster.txt">>],
args_desc = ["File path"],
result = {res, rescode}},
#ejabberd_commands{name = push_alltoall, tags = [roster],
desc = "Add all the users to all the users of Host in Group",
module = ?MODULE, function = push_alltoall,
args = [{host, binary}, {group, binary}],
args_example = [<<"myserver.com">>,<<"Everybody">>],
args_desc = ["Server name", "Group name"],
result = {res, rescode}},
#ejabberd_commands{name = get_last, tags = [last],
desc = "Get last activity information",
longdesc = "Timestamp is UTC and "
"[XEP-0082](https://xmpp.org/extensions/xep-0082.html)"
" format, for example: "
"`2017-02-23T22:25:28.063062Z ONLINE`",
module = ?MODULE, function = get_last,
args = [{user, binary}, {host, binary}],
args_example = [<<"user1">>,<<"myserver.com">>],
args_desc = ["User name", "Server name"],
result_example = {<<"2017-06-30T14:32:16.060684Z">>, "ONLINE"},
result_desc = "Last activity timestamp and status",
result = {last_activity,
{tuple, [{timestamp, string},
{status, string}
]}}},
#ejabberd_commands{name = set_last, tags = [last],
desc = "Set last activity information",
longdesc = "Timestamp is the seconds since "
"`1970-01-01 00:00:00 UTC`. For example value see `date +%s`",
module = ?MODULE, function = set_last,
args = [{user, binary}, {host, binary}, {timestamp, integer}, {status, binary}],
args_example = [<<"user1">>,<<"myserver.com">>, 1500045311, <<"GoSleeping">>],
args_desc = ["User name", "Server name", "Number of seconds since epoch", "Status message"],
result = {res, rescode}},
#ejabberd_commands{name = private_get, tags = [private],
desc = "Get some information from a user private storage",
module = ?MODULE, function = private_get,
args = [{user, binary}, {host, binary}, {element, binary}, {ns, binary}],
args_example = [<<"user1">>,<<"myserver.com">>,<<"storage">>, <<"storage:rosternotes">>],
args_desc = ["User name", "Server name", "Element name", "Namespace"],
result = {res, string}},
#ejabberd_commands{name = private_set, tags = [private],
desc = "Set to the user private storage",
module = ?MODULE, function = private_set,
args = [{user, binary}, {host, binary}, {element, binary}],
args_example = [<<"user1">>,<<"myserver.com">>,
<<" ">>],
args_desc = ["User name", "Server name", "XML storage element"],
result = {res, rescode}},
#ejabberd_commands{name = srg_create, tags = [shared_roster_group],
desc = "Create a Shared Roster Group",
longdesc = "If you want to specify several group "
"identifiers in the Display argument,\n"
"put `\\ \"` around the argument and\nseparate the "
"identifiers with `\\ \\ n`\n"
"For example:\n"
" `ejabberdctl srg_create group3 myserver.com "
"name desc \\\"group1\\\\ngroup2\\\"`",
note = "changed in 21.07",
module = ?MODULE, function = srg_create,
args = [{group, binary}, {host, binary},
{label, binary}, {description, binary}, {display, binary}],
args_rename = [{name, label}],
args_example = [<<"group3">>, <<"myserver.com">>, <<"Group3">>,
<<"Third group">>, <<"group1\\\\ngroup2">>],
args_desc = ["Group identifier", "Group server name", "Group name",
"Group description", "Groups to display"],
result = {res, rescode}},
#ejabberd_commands{name = srg_create, tags = [shared_roster_group],
desc = "Create a Shared Roster Group",
module = ?MODULE, function = srg_create,
version = 1,
note = "updated in 24.02",
args = [{group, binary}, {host, binary},
{label, binary}, {description, binary}, {display, {list, {group, binary}}}],
args_rename = [{name, label}],
args_example = [<<"group3">>, <<"myserver.com">>, <<"Group3">>,
<<"Third group">>, [<<"group1">>, <<"group2">>]],
args_desc = ["Group identifier", "Group server name", "Group name",
"Group description", "List of groups to display"],
result = {res, rescode}},
#ejabberd_commands{name = srg_add, tags = [shared_roster_group],
desc = "Add/Create a Shared Roster Group (without details)",
module = ?MODULE, function = srg_add,
note = "added in 24.06",
args = [{group, binary}, {host, binary}],
args_example = [<<"group3">>, <<"myserver.com">>],
args_desc = ["Group identifier", "Group server name"],
result = {res, rescode}},
#ejabberd_commands{name = srg_delete, tags = [shared_roster_group],
desc = "Delete a Shared Roster Group",
module = ?MODULE, function = srg_delete,
args = [{group, binary}, {host, binary}],
args_example = [<<"group3">>, <<"myserver.com">>],
args_desc = ["Group identifier", "Group server name"],
result = {res, rescode}},
#ejabberd_commands{name = srg_list, tags = [shared_roster_group],
desc = "List the Shared Roster Groups in Host",
module = ?MODULE, function = srg_list,
args = [{host, binary}],
args_example = [<<"myserver.com">>],
args_desc = ["Server name"],
result_example = [<<"group1">>, <<"group2">>],
result_desc = "List of group identifiers",
result = {groups, {list, {id, string}}}},
#ejabberd_commands{name = srg_get_info, tags = [shared_roster_group],
desc = "Get info of a Shared Roster Group",
module = ?MODULE, function = srg_get_info,
args = [{group, binary}, {host, binary}],
args_example = [<<"group3">>, <<"myserver.com">>],
args_desc = ["Group identifier", "Group server name"],
result_example = [{<<"name">>, "Group 3"}, {<<"displayed_groups">>, "group1"}],
result_desc = "List of group information, as key and value",
result = {informations, {list, {information, {tuple, [{key, string}, {value, string}]}}}}},
#ejabberd_commands{name = srg_set_info, tags = [shared_roster_group],
desc = "Set info of a Shared Roster Group",
module = ?MODULE, function = srg_set_info,
note = "added in 24.06",
args = [{group, binary}, {host, binary}, {key, binary}, {value, binary}],
args_example = [<<"group3">>, <<"myserver.com">>, <<"label">>, <<"Family">>],
args_desc = ["Group identifier", "Group server name",
"Information key: label, description",
"Information value"],
result = {res, rescode}},
#ejabberd_commands{name = srg_get_displayed, tags = [shared_roster_group],
desc = "Get displayed groups of a Shared Roster Group",
module = ?MODULE, function = srg_get_displayed,
note = "added in 24.06",
args = [{group, binary}, {host, binary}],
args_example = [<<"group3">>, <<"myserver.com">>],
args_desc = ["Group identifier", "Group server name"],
result_example = [<<"group1">>, <<"group2">>],
result_desc = "List of groups to display",
result = {display, {list, {group, binary}}}},
#ejabberd_commands{name = srg_add_displayed, tags = [shared_roster_group],
desc = "Add a group to displayed_groups of a Shared Roster Group",
module = ?MODULE, function = srg_add_displayed,
note = "added in 24.06",
args = [{group, binary}, {host, binary},
{add, binary}],
args_example = [<<"group3">>, <<"myserver.com">>, <<"group1">>],
args_desc = ["Group identifier", "Group server name",
"Group to add to displayed_groups"],
result = {res, rescode}},
#ejabberd_commands{name = srg_del_displayed, tags = [shared_roster_group],
desc = "Delete a group from displayed_groups of a Shared Roster Group",
module = ?MODULE, function = srg_del_displayed,
note = "added in 24.06",
args = [{group, binary}, {host, binary},
{del, binary}],
args_example = [<<"group3">>, <<"myserver.com">>, <<"group1">>],
args_desc = ["Group identifier", "Group server name",
"Group to delete from displayed_groups"],
result = {res, rescode}},
#ejabberd_commands{name = srg_get_members, tags = [shared_roster_group],
desc = "Get members of a Shared Roster Group",
module = ?MODULE, function = srg_get_members,
args = [{group, binary}, {host, binary}],
args_example = [<<"group3">>, <<"myserver.com">>],
args_desc = ["Group identifier", "Group server name"],
result_example = [<<"user1@localhost">>, <<"user2@localhost">>],
result_desc = "List of group identifiers",
result = {members, {list, {member, string}}}},
#ejabberd_commands{name = srg_user_add, tags = [shared_roster_group],
desc = "Add the JID user@host to the Shared Roster Group",
module = ?MODULE, function = srg_user_add,
args = [{user, binary}, {host, binary}, {group, binary}, {grouphost, binary}],
args_example = [<<"user1">>, <<"myserver.com">>, <<"group3">>, <<"myserver.com">>],
args_desc = ["Username", "User server name", "Group identifier", "Group server name"],
result = {res, rescode}},
#ejabberd_commands{name = srg_user_del, tags = [shared_roster_group],
desc = "Delete this JID user@host from the Shared Roster Group",
module = ?MODULE, function = srg_user_del,
args = [{user, binary}, {host, binary}, {group, binary}, {grouphost, binary}],
args_example = [<<"user1">>, <<"myserver.com">>, <<"group3">>, <<"myserver.com">>],
args_desc = ["Username", "User server name", "Group identifier", "Group server name"],
result = {res, rescode}},
#ejabberd_commands{name = get_offline_count,
tags = [offline],
desc = "Get the number of unread offline messages",
policy = user,
module = mod_offline, function = count_offline_messages,
args = [],
args_rename = [{server, host}],
result_example = 5,
result_desc = "Number",
result = {value, integer}},
#ejabberd_commands{name = get_offline_messages,
tags = [internal, offline],
desc = "Get the offline messages",
policy = user,
module = mod_offline, function = get_offline_messages,
args = [],
result = {queue, {list, {messages, {tuple, [{time, string},
{from, string},
{to, string},
{packet, string}
]}}}}},
#ejabberd_commands{name = send_message, tags = [stanza],
desc = "Send a message to a local or remote bare of full JID",
longdesc = "When sending a groupchat message to a MUC room, "
"`from` must be the full JID of a room occupant, "
"or the bare JID of a MUC service admin, "
"or the bare JID of a MUC/Sub subscribed user.",
module = ?MODULE, function = send_message,
args = [{type, binary}, {from, binary}, {to, binary},
{subject, binary}, {body, binary}],
args_example = [<<"headline">>, <<"admin@localhost">>, <<"user1@localhost">>,
<<"Restart">>, <<"In 5 minutes">>],
args_desc = ["Message type: `normal`, `chat`, `headline`, `groupchat`", "Sender JID",
"Receiver JID", "Subject, or empty string", "Body"],
result = {res, rescode}},
#ejabberd_commands{name = send_stanza_c2s, tags = [stanza],
desc = "Send a stanza from an existing C2S session",
longdesc = "`user`@`host`/`resource` must be an existing C2S session."
" As an alternative, use _`send_stanza`_ API instead.",
module = ?MODULE, function = send_stanza_c2s,
args = [{user, binary}, {host, binary}, {resource, binary}, {stanza, binary}],
args_example = [<<"admin">>, <<"myserver.com">>, <<"bot">>,
<<" ">>],
args_desc = ["Username", "Server name", "Resource", "Stanza"],
result = {res, rescode}},
#ejabberd_commands{name = send_stanza, tags = [stanza],
desc = "Send a stanza; provide From JID and valid To JID",
module = ?MODULE, function = send_stanza,
args = [{from, binary}, {to, binary}, {stanza, binary}],
args_example = [<<"admin@localhost">>, <<"user1@localhost">>,
<<" ">>],
args_desc = ["Sender JID", "Destination JID", "Stanza"],
result = {res, rescode}},
#ejabberd_commands{name = privacy_set, tags = [stanza],
desc = "Send a IQ set privacy stanza for a local account",
module = ?MODULE, function = privacy_set,
args = [{user, binary}, {host, binary}, {xmlquery, binary}],
args_example = [<<"user1">>, <<"myserver.com">>,
<<"...">>],
args_desc = ["Username", "Server name", "Query XML element"],
result = {res, rescode}},
#ejabberd_commands{name = stats, tags = [statistics],
desc = "Get some statistical value for the whole ejabberd server",
longdesc = "Allowed statistics `name` are: `registeredusers`, "
"`onlineusers`, `onlineusersnode`, `uptimeseconds`, `processes`.",
policy = admin,
module = ?MODULE, function = stats,
args = [{name, binary}],
args_example = [<<"registeredusers">>],
args_desc = ["Statistic name"],
result_example = 6,
result_desc = "Integer statistic value",
result = {stat, integer}},
#ejabberd_commands{name = stats_host, tags = [statistics],
desc = "Get some statistical value for this host",
longdesc = "Allowed statistics `name` are: `registeredusers`, `onlineusers`.",
policy = admin,
module = ?MODULE, function = stats,
args = [{name, binary}, {host, binary}],
args_example = [<<"registeredusers">>, <<"example.com">>],
args_desc = ["Statistic name", "Server JID"],
result_example = 6,
result_desc = "Integer statistic value",
result = {stat, integer}}
].
%%%
%%% Adminsys
%%%
compile(File) ->
Ebin = filename:join(code:lib_dir(ejabberd), "ebin"),
case ext_mod:compile_erlang_file(Ebin, File) of
{ok, Module} ->
code:purge(Module),
code:load_file(Module),
ok;
_ ->
error
end.
get_cookie() ->
atom_to_list(erlang:get_cookie()).
restart_module(Host, Module) when is_binary(Module) ->
restart_module(Host, misc:binary_to_atom(Module));
restart_module(Host, Module) when is_atom(Module) ->
case gen_mod:is_loaded(Host, Module) of
false ->
% not a running module, force code reload anyway
code:purge(Module),
code:delete(Module),
code:load_file(Module),
1;
true ->
gen_mod:stop_module(Host, Module),
case code:soft_purge(Module) of
true ->
code:delete(Module),
code:load_file(Module),
gen_mod:start_module(Host, Module),
0;
false ->
gen_mod:start_module(Host, Module),
2
end
end.
%%%
%%% Accounts
%%%
set_password(User, Host, Password) ->
Fun = fun () -> ejabberd_auth:set_password(User, Host, Password) end,
user_action(User, Host, Fun, ok).
check_password(User, Host, Password) ->
ejabberd_auth:check_password(User, <<>>, Host, Password).
%% Copied some code from ejabberd_commands.erln
check_password_hash(User, Host, PasswordHash, HashMethod) ->
AccountPass = ejabberd_auth:get_password_s(User, Host),
Methods = lists:map(fun(A) -> atom_to_binary(A, latin1) end,
proplists:get_value(hashs, crypto:supports())),
MethodAllowed = lists:member(HashMethod, Methods),
AccountPassHash = case {AccountPass, MethodAllowed} of
{A, _} when is_tuple(A) -> scrammed;
{_, true} -> get_hash(AccountPass, HashMethod);
{_, false} ->
?ERROR_MSG("Check_password_hash called "
"with hash method: ~p", [HashMethod]),
undefined
end,
case AccountPassHash of
scrammed ->
?ERROR_MSG("Passwords are scrammed, and check_password_hash cannot work.", []),
throw(passwords_scrammed_command_cannot_work);
undefined -> throw(unkown_hash_method);
PasswordHash -> ok;
_ -> false
end.
get_hash(AccountPass, Method) ->
iolist_to_binary([io_lib:format("~2.16.0B", [X])
|| X <- binary_to_list(
crypto:hash(binary_to_atom(Method, latin1), AccountPass))]).
delete_old_users(Days) ->
%% Get the list of registered users
Users = ejabberd_auth:get_users(),
{removed, N, UR} = delete_old_users(Days, Users),
{ok, io_lib:format("Deleted ~p users: ~p", [N, UR])}.
delete_old_users_vhost(Host, Days) ->
%% Get the list of registered users
Users = ejabberd_auth:get_users(Host),
{removed, N, UR} = delete_old_users(Days, Users),
{ok, io_lib:format("Deleted ~p users: ~p", [N, UR])}.
delete_old_users(Days, Users) ->
SecOlder = Days*24*60*60,
TimeStamp_now = erlang:system_time(second),
TimeStamp_oldest = TimeStamp_now - SecOlder,
F = fun({LUser, LServer}) ->
case catch delete_or_not(LUser, LServer, TimeStamp_oldest) of
true ->
ejabberd_auth:remove_user(LUser, LServer),
true;
_ ->
false
end
end,
Users_removed = lists:filter(F, Users),
{removed, length(Users_removed), Users_removed}.
delete_or_not(LUser, LServer, TimeStamp_oldest) ->
deny = acl:match_rule(LServer, protect_old_users, jid:make(LUser, LServer)),
[] = ejabberd_sm:get_user_resources(LUser, LServer),
case mod_last:get_last_info(LUser, LServer) of
{ok, TimeStamp, _Status} ->
if TimeStamp_oldest < TimeStamp ->
false;
true ->
true
end;
not_found ->
true
end.
%%
%% Ban account v0
ban_account(User, Host, ReasonText) ->
Reason = prepare_reason(ReasonText),
kick_sessions(User, Host, Reason),
set_random_password(User, Host, Reason),
ok.
kick_sessions(User, Server, Reason) ->
lists:map(
fun(Resource) ->
kick_this_session(User, Server, Resource, Reason)
end,
ejabberd_sm:get_user_resources(User, Server)).
set_random_password(User, Server, Reason) ->
NewPass = build_random_password(Reason),
set_password_auth(User, Server, NewPass).
build_random_password(Reason) ->
{{Year, Month, Day}, {Hour, Minute, Second}} = calendar:universal_time(),
Date = str:format("~4..0B~2..0B~2..0BT~2..0B:~2..0B:~2..0B",
[Year, Month, Day, Hour, Minute, Second]),
RandomString = p1_rand:get_string(),
<<"BANNED_ACCOUNT--", Date/binary, "--", RandomString/binary, "--", Reason/binary>>.
set_password_auth(User, Server, Password) ->
ok = ejabberd_auth:set_password(User, Server, Password).
prepare_reason([]) ->
<<"Kicked by administrator">>;
prepare_reason([Reason]) ->
Reason;
prepare_reason(Reason) when is_binary(Reason) ->
Reason.
%%
%% Ban account v2
ban_account_v2(User, Host, ReasonText) ->
case gen_mod:is_loaded(Host, mod_private) of
false ->
mod_private_is_required_but_disabled;
true ->
case is_banned(User, Host) of
true ->
account_was_already_banned;
false ->
ban_account_v2_b(User, Host, ReasonText)
end
end.
ban_account_v2_b(User, Host, ReasonText) ->
Reason = prepare_reason(ReasonText),
Pass = ejabberd_auth:get_password_s(User, Host),
Last = get_last(User, Host),
BanDate = xmpp_util:encode_timestamp(erlang:timestamp()),
Hash = get_hash_value(User, Host),
BanPrivateXml = build_ban_xmlel(Reason, Pass, Last, BanDate, Hash),
ok = private_set2(User, Host, BanPrivateXml),
ok = set_random_password_v2(User, Host),
kick_sessions(User, Host, Reason),
ok.
get_hash_value(User, Host) ->
Cookie = misc:atom_to_binary(erlang:get_cookie()),
misc:term_to_base64(crypto:hash(sha256, <>)).
set_random_password_v2(User, Server) ->
NewPass = p1_rand:get_string(),
ok = ejabberd_auth:set_password(User, Server, NewPass).
build_ban_xmlel(Reason, Pass, {LastDate, LastReason}, BanDate, Hash) ->
PassEls = build_pass_els(Pass),
#xmlel{name = <<"banned">>,
attrs = [{<<"xmlns">>, <<"jabber:ejabberd:banned">>}],
children = [#xmlel{name = <<"reason">>, attrs = [], children = [{xmlcdata, Reason}]},
#xmlel{name = <<"password">>, attrs = [], children = PassEls},
#xmlel{name = <<"lastdate">>, attrs = [], children = [{xmlcdata, LastDate}]},
#xmlel{name = <<"lastreason">>, attrs = [], children = [{xmlcdata, LastReason}]},
#xmlel{name = <<"bandate">>, attrs = [], children = [{xmlcdata, BanDate}]},
#xmlel{name = <<"hash">>, attrs = [], children = [{xmlcdata, Hash}]}
]}.
build_pass_els(Pass) when is_binary(Pass) ->
[{xmlcdata, Pass}];
build_pass_els(#scram{storedkey = StoredKey,
serverkey = ServerKey,
salt = Salt,
hash = Hash,
iterationcount = IterationCount}) ->
[#xmlel{name = <<"storedkey">>, attrs = [], children = [{xmlcdata, StoredKey}]},
#xmlel{name = <<"serverkey">>, attrs = [], children = [{xmlcdata, ServerKey}]},
#xmlel{name = <<"salt">>, attrs = [], children = [{xmlcdata, Salt}]},
#xmlel{name = <<"hash">>, attrs = [], children = [{xmlcdata, misc:atom_to_binary(Hash)}]},
#xmlel{name = <<"iterationcount">>, attrs = [], children = [{xmlcdata, integer_to_binary(IterationCount)}]}
].
%%
%% Get ban details
get_ban_details(User, Host) ->
case private_get2(User, Host, <<"banned">>, <<"jabber:ejabberd:banned">>) of
[El] ->
get_ban_details(User, Host, El);
[] ->
[]
end.
get_ban_details(User, Host, El) ->
Reason = fxml:get_subtag_cdata(El, <<"reason">>),
LastDate = fxml:get_subtag_cdata(El, <<"lastdate">>),
LastReason = fxml:get_subtag_cdata(El, <<"lastreason">>),
BanDate = fxml:get_subtag_cdata(El, <<"bandate">>),
Hash = fxml:get_subtag_cdata(El, <<"hash">>),
case Hash == get_hash_value(User, Host) of
true ->
[{"reason", Reason},
{"bandate", BanDate},
{"lastdate", LastDate},
{"lastreason", LastReason}];
false ->
[]
end.
is_banned(User, Host) ->
case lists:keyfind("bandate", 1, get_ban_details(User, Host)) of
{_, BanDate} when BanDate /= <<>> ->
true;
_ ->
false
end.
%%
%% Unban account
unban_account(User, Host) ->
case gen_mod:is_loaded(Host, mod_private) of
false ->
mod_private_is_required_but_disabled;
true ->
case is_banned(User, Host) of
false ->
account_was_not_banned;
true ->
unban_account2(User, Host)
end
end.
unban_account2(User, Host) ->
OldPass = get_oldpass(User, Host),
ok = ejabberd_auth:set_password(User, Host, OldPass),
UnBanPrivateXml = build_unban_xmlel(),
private_set2(User, Host, UnBanPrivateXml).
get_oldpass(User, Host) ->
[El] = private_get2(User, Host, <<"banned">>, <<"jabber:ejabberd:banned">>),
Pass = fxml:get_subtag(El, <<"password">>),
get_pass(Pass).
get_pass(#xmlel{children = [{xmlcdata, Pass}]}) ->
Pass;
get_pass(#xmlel{children = ScramEls} = Pass) when is_list(ScramEls) ->
StoredKey = fxml:get_subtag_cdata(Pass, <<"storedkey">>),
ServerKey = fxml:get_subtag_cdata(Pass, <<"serverkey">>),
Salt = fxml:get_subtag_cdata(Pass, <<"salt">>),
Hash = fxml:get_subtag_cdata(Pass, <<"hash">>),
IterationCount = fxml:get_subtag_cdata(Pass, <<"iterationcount">>),
#scram{storedkey = StoredKey,
serverkey = ServerKey,
salt = Salt,
hash = binary_to_existing_atom(Hash, latin1),
iterationcount = binary_to_integer(IterationCount)}.
build_unban_xmlel() ->
#xmlel{name = <<"banned">>, attrs = [{<<"xmlns">>, <<"jabber:ejabberd:banned">>}]}.
%%%
%%% Sessions
%%%
num_resources(User, Host) ->
length(ejabberd_sm:get_user_resources(User, Host)).
resource_num(User, Host, Num) ->
Resources = ejabberd_sm:get_user_resources(User, Host),
case (0
lists:nth(Num, Resources);
false ->
throw({bad_argument,
lists:flatten(io_lib:format("Wrong resource number: ~p", [Num]))})
end.
kick_session(User, Server, Resource, ReasonText) ->
kick_this_session(User, Server, Resource, prepare_reason(ReasonText)),
ok.
kick_this_session(User, Server, Resource, Reason) ->
ejabberd_sm:route(jid:make(User, Server, Resource),
{exit, Reason}).
status_num(Host, Status) ->
length(get_status_list(Host, Status)).
status_num(Status) ->
status_num(<<"all">>, Status).
status_list(Host, Status) ->
Res = get_status_list(Host, Status),
[{U, S, R, num_prio(P), St} || {U, S, R, P, St} <- Res].
status_list(Status) ->
status_list(<<"all">>, Status).
status_list_v3(ArgHost, Status) ->
List = status_list(ArgHost, Status),
[{jid:encode(jid:make(User, Host, Resource)), Priority, StatusText}
|| {User, Host, Resource, Priority, StatusText} <- List].
status_list_v3(Status) ->
status_list_v3(<<"all">>, Status).
get_status_list(Host, Status_required) ->
%% Get list of all logged users
Sessions = ejabberd_sm:dirty_get_my_sessions_list(),
%% Reformat the list
Sessions2 = [ {Session#session.usr, Session#session.sid, Session#session.priority} || Session <- Sessions],
Fhost = case Host of
<<"all">> ->
%% All hosts are requested, so don't filter at all
fun(_, _) -> true end;
_ ->
%% Filter the list, only Host is interesting
fun(A, B) -> A == B end
end,
Sessions3 = [ {Pid, Server, Priority} || {{_User, Server, _Resource}, {_, Pid}, Priority} <- Sessions2, apply(Fhost, [Server, Host])],
%% For each Pid, get its presence
Sessions4 = [ {catch get_presence(Pid), Server, Priority} || {Pid, Server, Priority} <- Sessions3],
%% Filter by status
Fstatus = case Status_required of
<<"all">> ->
fun(_, _) -> true end;
_ ->
fun(A, B) -> A == B end
end,
[{User, Server, Resource, num_prio(Priority), stringize(Status_text)}
|| {{User, Resource, Status, Status_text}, Server, Priority} <- Sessions4,
apply(Fstatus, [Status, Status_required])].
connected_users_info() ->
lists:filtermap(
fun({U, S, R}) ->
case user_session_info(U, S, R) of
offline ->
false;
Info ->
Jid = jid:encode(jid:make(U, S, R)),
{true, erlang:insert_element(1, Info, Jid)}
end
end,
ejabberd_sm:dirty_get_sessions_list()).
connected_users_vhost(Host) ->
USRs = ejabberd_sm:get_vh_session_list(Host),
[ jid:encode(jid:make(USR)) || USR <- USRs].
%% Make string more print-friendly
stringize(String) ->
%% Replace newline characters with other code
ejabberd_regexp:greplace(String, <<"\n">>, <<"\\n">>).
get_presence(Pid) ->
try get_presence2(Pid) of
{_, _, _, _} = Res ->
Res
catch
_:_ -> {<<"">>, <<"">>, <<"offline">>, <<"">>}
end.
get_presence2(Pid) ->
Pres = #presence{from = From} = ejabberd_c2s:get_presence(Pid),
Show = case Pres of
#presence{type = unavailable} -> <<"unavailable">>;
#presence{show = undefined} -> <<"available">>;
#presence{show = S} -> atom_to_binary(S, utf8)
end,
Status = xmpp:get_text(Pres#presence.status),
{From#jid.user, From#jid.resource, Show, Status}.
get_presence(U, S) ->
Pids = [ejabberd_sm:get_session_pid(U, S, R)
|| R <- ejabberd_sm:get_user_resources(U, S)],
OnlinePids = [Pid || Pid <- Pids, Pid=/=none],
case OnlinePids of
[] ->
{jid:encode({U, S, <<>>}), <<"unavailable">>, <<"">>};
[SessionPid|_] ->
{_User, Resource, Show, Status} = get_presence(SessionPid),
FullJID = jid:encode({U, S, Resource}),
{FullJID, Show, Status}
end.
set_presence(User, Host, Resource, Type, Show, Status, Priority) when is_binary(Priority) ->
set_presence(User, Host, Resource, Type, Show, Status, binary_to_integer(Priority));
set_presence(User, Host, Resource, Type, Show, Status, Priority) ->
Pres = #presence{
from = jid:make(User, Host, Resource),
to = jid:make(User, Host),
type = misc:binary_to_atom(Type),
status = xmpp:mk_text(Status),
show = misc:binary_to_atom(Show),
priority = Priority,
sub_els = []},
case ejabberd_sm:get_session_pid(User, Host, Resource) of
none -> throw({error, "User session not found"});
Ref -> ejabberd_c2s:set_presence(Ref, Pres)
end.
user_sessions_info(User, Host) ->
lists:filtermap(fun(Resource) ->
case user_session_info(User, Host, Resource) of
offline -> false;
Info -> {true, Info}
end
end, ejabberd_sm:get_user_resources(User, Host)).
user_session_info(User, Host, Resource) ->
CurrentSec = calendar:datetime_to_gregorian_seconds({date(), time()}),
case ejabberd_sm:get_user_info(User, Host, Resource) of
offline ->
offline;
Info ->
Now = proplists:get_value(ts, Info),
Pid = proplists:get_value(pid, Info),
{_U, _Resource, Status, StatusText} = get_presence(Pid),
Priority = proplists:get_value(priority, Info),
Conn = proplists:get_value(conn, Info),
{Ip, Port} = proplists:get_value(ip, Info),
IPS = inet_parse:ntoa(Ip),
NodeS = atom_to_list(node(Pid)),
Uptime = CurrentSec - calendar:datetime_to_gregorian_seconds(
calendar:now_to_local_time(Now)),
{atom_to_list(Conn), IPS, Port, num_prio(Priority), NodeS, Uptime, Status, Resource, StatusText}
end.
%%%
%%% Vcard
%%%
set_nickname(User, Host, Nickname) ->
VCard = xmpp:encode(#vcard_temp{nickname = Nickname}),
case mod_vcard:set_vcard(User, jid:nameprep(Host), VCard) of
{error, badarg} ->
error;
ok ->
ok
end.
get_vcard(User, Host, Name) ->
[Res | _] = get_vcard_content(User, Host, [Name]),
Res.
get_vcard(User, Host, Name, Subname) ->
[Res | _] = get_vcard_content(User, Host, [Name, Subname]),
Res.
get_vcard_multi(User, Host, Name, Subname) ->
get_vcard_content(User, Host, [Name, Subname]).
set_vcard(User, Host, Name, SomeContent) ->
set_vcard_content(User, Host, [Name], SomeContent).
set_vcard(User, Host, Name, Subname, SomeContent) ->
set_vcard_content(User, Host, [Name, Subname], SomeContent).
%%
%% Room vcard
is_muc_service(Domain) ->
try mod_muc_admin:get_room_serverhost(Domain) of
Domain -> false;
Service when is_binary(Service) -> true
catch _:{unregistered_route, _} ->
throw(error_wrong_hostname)
end.
get_room_vcard(Name, Service) ->
case mod_muc_admin:get_room_options(Name, Service) of
[] ->
throw(error_no_vcard_found);
Opts ->
case lists:keyfind(<<"vcard">>, 1, Opts) of
false ->
throw(error_no_vcard_found);
{_, VCardRaw} ->
[fxml_stream:parse_element(VCardRaw)]
end
end.
%%
%% Internal vcard
get_vcard_content(User, Server, Data) ->
case get_vcard_element(User, Server) of
[El|_] ->
case get_vcard(Data, El) of
[false] -> throw(error_no_value_found_in_vcard);
ElemList -> ?DEBUG("ELS ~p", [ElemList]), [fxml:get_tag_cdata(Elem) || Elem <- ElemList]
end;
[] ->
throw(error_no_vcard_found);
error ->
throw(database_failure)
end.
get_vcard_element(User, Server) ->
case is_muc_service(Server) of
true ->
get_room_vcard(User, Server);
false ->
mod_vcard:get_vcard(jid:nodeprep(User), jid:nameprep(Server))
end.
get_vcard([<<"TEL">>, TelType], {_, _, _, OldEls}) ->
{TakenEl, _NewEls} = take_vcard_tel(TelType, OldEls, [], not_found),
[TakenEl];
get_vcard([Data1, Data2], A1) ->
case get_subtag(A1, Data1) of
[false] -> [false];
A2List ->
lists:flatten([get_vcard([Data2], A2) || A2 <- A2List])
end;
get_vcard([Data], A1) ->
get_subtag(A1, Data).
get_subtag(Xmlelement, Name) ->
[fxml:get_subtag(Xmlelement, Name)].
set_vcard_content(User, Server, Data, SomeContent) ->
ContentList = case SomeContent of
[Bin | _] when is_binary(Bin) -> SomeContent;
Bin when is_binary(Bin) -> [SomeContent]
end,
%% Get old vcard
A4 = case mod_vcard:get_vcard(jid:nodeprep(User), jid:nameprep(Server)) of
[A1] ->
{_, _, _, A2} = A1,
update_vcard_els(Data, ContentList, A2);
[] ->
update_vcard_els(Data, ContentList, []);
error ->
throw(database_failure)
end,
%% Build new vcard
SubEl = {xmlel, <<"vCard">>, [{<<"xmlns">>,<<"vcard-temp">>}], A4},
mod_vcard:set_vcard(User, jid:nameprep(Server), SubEl).
take_vcard_tel(TelType, [{xmlel, <<"TEL">>, _, SubEls}=OldEl | OldEls], NewEls, Taken) ->
{Taken2, NewEls2} = case lists:keymember(TelType, 2, SubEls) of
true -> {fxml:get_subtag(OldEl, <<"NUMBER">>), NewEls};
false -> {Taken, [OldEl | NewEls]}
end,
take_vcard_tel(TelType, OldEls, NewEls2, Taken2);
take_vcard_tel(TelType, [OldEl | OldEls], NewEls, Taken) ->
take_vcard_tel(TelType, OldEls, [OldEl | NewEls], Taken);
take_vcard_tel(_TelType, [], NewEls, Taken) ->
{Taken, NewEls}.
update_vcard_els([<<"TEL">>, TelType], [TelValue], OldEls) ->
{_, NewEls} = take_vcard_tel(TelType, OldEls, [], not_found),
NewEl = {xmlel,<<"TEL">>,[],
[{xmlel,TelType,[],[]},
{xmlel,<<"NUMBER">>,[],[{xmlcdata,TelValue}]}]},
[NewEl | NewEls];
update_vcard_els(Data, ContentList, Els1) ->
Els2 = lists:keysort(2, Els1),
[Data1 | Data2] = Data,
NewEls = case Data2 of
[] ->
[{xmlel, Data1, [], [{xmlcdata,Content}]} || Content <- ContentList];
[D2] ->
OldEl = case lists:keysearch(Data1, 2, Els2) of
{value, A} -> A;
false -> {xmlel, Data1, [], []}
end,
{xmlel, _, _, ContentOld1} = OldEl,
Content2 = [{xmlel, D2, [], [{xmlcdata,Content}]} || Content <- ContentList],
ContentOld2 = [A || {_, X, _, _} = A <- ContentOld1, X/=D2],
ContentOld3 = lists:keysort(2, ContentOld2),
ContentNew = lists:keymerge(2, Content2, ContentOld3),
[{xmlel, Data1, [], ContentNew}]
end,
Els3 = lists:keydelete(Data1, 2, Els2),
lists:keymerge(2, NewEls, Els3).
%%%
%%% Roster
%%%
add_rosteritem(LocalUser, LocalServer, User, Server, Nick, Group, Subs) when is_binary(Group) ->
add_rosteritem(LocalUser, LocalServer, User, Server, Nick, [Group], Subs);
add_rosteritem(LocalUser, LocalServer, User, Server, Nick, Groups, Subs) ->
case {jid:make(LocalUser, LocalServer), jid:make(User, Server)} of
{error, _} ->
throw({error, "Invalid 'localuser'/'localserver'"});
{_, error} ->
throw({error, "Invalid 'user'/'server'"});
{Jid, _Jid2} ->
RosterItem = build_roster_item(User, Server, {add, Nick, Subs, Groups}),
case mod_roster:set_item_and_notify_clients(Jid, RosterItem, true) of
ok -> ok;
_ -> error
end
end.
subscribe(LU, LS, User, Server, Nick, Group, Subscription, _Xattrs) ->
case {jid:make(LU, LS), jid:make(User, Server)} of
{error, _} ->
throw({error, "Invalid 'localuser'/'localserver'"});
{_, error} ->
throw({error, "Invalid 'user'/'server'"});
{_Jid, _Jid2} ->
ItemEl = build_roster_item(User, Server, {add, Nick, Subscription, Group}),
mod_roster:set_items(LU, LS, #roster_query{items = [ItemEl]})
end.
delete_rosteritem(LocalUser, LocalServer, User, Server) ->
case {jid:make(LocalUser, LocalServer), jid:make(User, Server)} of
{error, _} ->
throw({error, "Invalid 'localuser'/'localserver'"});
{_, error} ->
throw({error, "Invalid 'user'/'server'"});
{Jid, _Jid2} ->
RosterItem = build_roster_item(User, Server, remove),
case mod_roster:set_item_and_notify_clients(Jid, RosterItem, true) of
ok -> ok;
_ -> error
end
end.
%% -----------------------------
%% Get Roster
%% -----------------------------
get_roster(User, Server) ->
case jid:make(User, Server) of
error ->
throw({error, "Invalid 'user'/'server'"});
#jid{luser = U, lserver = S} ->
Items = ejabberd_hooks:run_fold(roster_get, S, [], [{U, S}]),
make_roster_xmlrpc(Items)
end.
make_roster_xmlrpc(Roster) ->
lists:map(
fun(#roster_item{jid = JID, name = Nick, subscription = Sub, ask = Ask, groups = Groups}) ->
JIDS = jid:encode(JID),
Subs = atom_to_list(Sub),
Asks = atom_to_list(Ask),
{JIDS, Nick, Subs, Asks, Groups}
end,
Roster).
get_roster_count(User, Server) ->
case jid:make(User, Server) of
error ->
throw({error, "Invalid 'user'/'server'"});
#jid{luser = U, lserver = S} ->
Items = ejabberd_hooks:run_fold(roster_get, S, [], [{U, S}]),
length(Items)
end.
%%-----------------------------
%% Push Roster from file
%%-----------------------------
push_roster(File, User, Server) ->
{ok, [Roster]} = file:consult(File),
subscribe_roster({User, Server, <<>>, User}, Roster).
push_roster_all(File) ->
{ok, [Roster]} = file:consult(File),
subscribe_all(Roster).
subscribe_all(Roster) ->
subscribe_all(Roster, Roster).
subscribe_all([], _) ->
ok;
subscribe_all([User1 | Users], Roster) ->
subscribe_roster(User1, Roster),
subscribe_all(Users, Roster).
subscribe_roster(_, []) ->
ok;
%% Do not subscribe a user to itself
subscribe_roster({Name, Server, Group, Nick}, [{Name, Server, _, _} | Roster]) ->
subscribe_roster({Name, Server, Group, Nick}, Roster);
%% Subscribe Name2 to Name1
subscribe_roster({Name1, Server1, Group1, Nick1}, [{Name2, Server2, Group2, Nick2} | Roster]) ->
subscribe(iolist_to_binary(Name1), iolist_to_binary(Server1), iolist_to_binary(Name2), iolist_to_binary(Server2),
iolist_to_binary(Nick2), iolist_to_binary(Group2), <<"both">>, []),
subscribe_roster({Name1, Server1, Group1, Nick1}, Roster).
push_alltoall(S, G) ->
Users = ejabberd_auth:get_users(S),
Users2 = build_list_users(G, Users, []),
subscribe_all(Users2),
ok.
build_list_users(_Group, [], Res) ->
Res;
build_list_users(Group, [{User, Server}|Users], Res) ->
build_list_users(Group, Users, [{User, Server, Group, User}|Res]).
%% @spec(LU, LS, U, S, Action) -> ok
%% Action = {add, Nick, Subs, Group} | remove
%% @doc Push to the roster of account LU@LS the contact U@S.
%% The specific action to perform is defined in Action.
push_roster_item(LU, LS, U, S, Action) ->
lists:foreach(fun(R) ->
push_roster_item(LU, LS, R, U, S, Action)
end, ejabberd_sm:get_user_resources(LU, LS)).
push_roster_item(LU, LS, R, U, S, Action) ->
LJID = jid:make(LU, LS, R),
BroadcastEl = build_broadcast(U, S, Action),
ejabberd_sm:route(LJID, BroadcastEl),
Item = build_roster_item(U, S, Action),
ResIQ = build_iq_roster_push(Item),
ejabberd_router:route(
xmpp:set_from_to(ResIQ, jid:remove_resource(LJID), LJID)).
build_roster_item(U, S, {add, Nick, Subs, Groups}) when is_list(Groups) ->
#roster_item{jid = jid:make(U, S),
name = Nick,
subscription = misc:binary_to_atom(Subs),
groups = Groups};
build_roster_item(U, S, {add, Nick, Subs, Group}) ->
Groups = binary:split(Group,<<";">>, [global, trim]),
#roster_item{jid = jid:make(U, S),
name = Nick,
subscription = misc:binary_to_atom(Subs),
groups = Groups};
build_roster_item(U, S, remove) ->
#roster_item{jid = jid:make(U, S), subscription = remove}.
build_iq_roster_push(Item) ->
#iq{type = set, id = <<"push">>,
sub_els = [#roster_query{items = [Item]}]}.
build_broadcast(U, S, {add, _Nick, Subs, _Group}) ->
build_broadcast(U, S, list_to_atom(binary_to_list(Subs)));
build_broadcast(U, S, remove) ->
build_broadcast(U, S, none);
%% @spec (U::binary(), S::binary(), Subs::atom()) -> any()
%% Subs = both | from | to | none
build_broadcast(U, S, SubsAtom) when is_atom(SubsAtom) ->
{item, {U, S, <<>>}, SubsAtom}.
%%%
%%% Last Activity
%%%
get_last(User, Server) ->
{Now, Status} = case ejabberd_sm:get_user_resources(User, Server) of
[] ->
case mod_last:get_last_info(User, Server) of
not_found ->
{erlang:timestamp(), "NOT FOUND"};
{ok, Shift, Status1} ->
{{Shift div 1000000, Shift rem 1000000, 0}, Status1}
end;
_ ->
{erlang:timestamp(), "ONLINE"}
end,
{xmpp_util:encode_timestamp(Now), Status}.
set_last(User, Server, Timestamp, Status) ->
case mod_last:store_last_info(User, Server, Timestamp, Status) of
{ok, _} -> ok;
Error -> Error
end.
%%%
%%% Private Storage
%%%
%% Example usage:
%% $ ejabberdctl private_set badlop localhost "\Cluth\ "
%% $ ejabberdctl private_get badlop localhost aa bb
%% Cluth
private_get(Username, Host, Element, Ns) ->
Els = private_get2(Username, Host, Element, Ns),
binary_to_list(fxml:element_to_binary(xmpp:encode(#private{sub_els = Els}))).
private_get2(Username, Host, Element, Ns) ->
case gen_mod:is_loaded(Host, mod_private) of
true -> private_get3(Username, Host, Element, Ns);
false -> []
end.
private_get3(Username, Host, Element, Ns) ->
ElementXml = #xmlel{name = Element, attrs = [{<<"xmlns">>, Ns}]},
mod_private:get_data(jid:nodeprep(Username), jid:nameprep(Host),
[{Ns, ElementXml}]).
private_set(Username, Host, ElementString) ->
case fxml_stream:parse_element(ElementString) of
{error, Error} ->
io:format("Error found parsing the element:~n ~p~nError: ~p~n",
[ElementString, Error]),
error;
Xml ->
private_set2(Username, Host, Xml)
end.
private_set2(Username, Host, Xml) ->
NS = fxml:get_tag_attr_s(<<"xmlns">>, Xml),
JID = jid:make(Username, Host),
mod_private:set_data(JID, [{NS, Xml}]).
%%%
%%% Shared Roster Groups
%%%
srg_create(Group, Host, Label, Description, Display) when is_binary(Display) ->
DisplayList = case Display of
<<>> -> [];
_ -> ejabberd_regexp:split(Display, <<"\\\\n">>)
end,
srg_create(Group, Host, Label, Description, DisplayList);
srg_create(Group, Host, Label, Description, DisplayList) ->
{_DispGroups, WrongDispGroups} = filter_groups_existence(Host, DisplayList),
case (WrongDispGroups -- [Group]) /= [] of
true ->
{wrong_displayed_groups, WrongDispGroups};
false ->
srg_create2(Group, Host, Label, Description, DisplayList)
end.
srg_create2(Group, Host, Label, Description, DisplayList) ->
Opts = [{label, Label},
{displayed_groups, DisplayList},
{description, Description}],
{atomic, _} = mod_shared_roster:create_group(Host, Group, Opts),
ok.
srg_add(Group, Host) ->
Opts = [{label, <<"">>},
{description, <<"">>},
{displayed_groups, []}
],
{atomic, _} = mod_shared_roster:create_group(Host, Group, Opts),
ok.
srg_delete(Group, Host) ->
{atomic, _} = mod_shared_roster:delete_group(Host, Group),
ok.
srg_list(Host) ->
lists:sort(mod_shared_roster:list_groups(Host)).
srg_get_info(Group, Host) ->
Opts = case mod_shared_roster:get_group_opts(Host,Group) of
Os when is_list(Os) -> Os;
error -> []
end,
[{misc:atom_to_binary(Title), to_list(Value)} || {Title, Value} <- Opts].
to_list([]) -> [];
to_list([H|_]=List) when is_binary(H) -> lists:join(", ", [to_list(E) || E <- List]);
to_list(E) when is_atom(E) -> atom_to_list(E);
to_list(E) when is_binary(E) -> binary_to_list(E).
%% @format-begin
srg_set_info(Group, Host, Key, Value) ->
Opts =
case mod_shared_roster:get_group_opts(Host, Group) of
Os when is_list(Os) ->
Os;
error ->
[]
end,
Opts2 = srg_set_info(Key, Value, Opts),
case mod_shared_roster:set_group_opts(Host, Group, Opts2) of
{atomic, ok} ->
ok;
Problem ->
?INFO_MSG("Problem: ~n ~p", [Problem]), %+++
error
end.
srg_set_info(<<"description">>, Value, Opts) ->
[{description, Value} | proplists:delete(description, Opts)];
srg_set_info(<<"label">>, Value, Opts) ->
[{label, Value} | proplists:delete(label, Opts)];
srg_set_info(<<"all_users">>, <<"true">>, Opts) ->
[{all_users, true} | proplists:delete(all_users, Opts)];
srg_set_info(<<"online_users">>, <<"true">>, Opts) ->
[{online_users, true} | proplists:delete(online_users, Opts)];
srg_set_info(<<"all_users">>, _, Opts) ->
proplists:delete(all_users, Opts);
srg_set_info(<<"online_users">>, _, Opts) ->
proplists:delete(online_users, Opts);
srg_set_info(Key, _Value, Opts) ->
?ERROR_MSG("Unknown Key in srg_set_info: ~p", [Key]),
Opts.
srg_get_displayed(Group, Host) ->
Opts =
case mod_shared_roster:get_group_opts(Host, Group) of
Os when is_list(Os) ->
Os;
error ->
[]
end,
proplists:get_value(displayed_groups, Opts, []).
srg_add_displayed(Group, Host, NewGroup) ->
Opts =
case mod_shared_roster:get_group_opts(Host, Group) of
Os when is_list(Os) ->
Os;
error ->
[]
end,
{DispGroups, WrongDispGroups} = filter_groups_existence(Host, [NewGroup]),
case WrongDispGroups /= [] of
true ->
{wrong_displayed_groups, WrongDispGroups};
false ->
DisplayedOld = proplists:get_value(displayed_groups, Opts, []),
Opts2 =
[{displayed_groups, lists:flatten(DisplayedOld, DispGroups)}
| proplists:delete(displayed_groups, Opts)],
case mod_shared_roster:set_group_opts(Host, Group, Opts2) of
{atomic, ok} ->
ok;
Problem ->
?INFO_MSG("Problem: ~n ~p", [Problem]), %+++
error
end
end.
srg_del_displayed(Group, Host, OldGroup) ->
Opts =
case mod_shared_roster:get_group_opts(Host, Group) of
Os when is_list(Os) ->
Os;
error ->
[]
end,
DisplayedOld = proplists:get_value(displayed_groups, Opts, []),
{DispGroups, OldDispGroups} = lists:partition(fun(G) -> G /= OldGroup end, DisplayedOld),
case OldDispGroups == [] of
true ->
{inexistent_displayed_groups, OldGroup};
false ->
Opts2 = [{displayed_groups, DispGroups} | proplists:delete(displayed_groups, Opts)],
case mod_shared_roster:set_group_opts(Host, Group, Opts2) of
{atomic, ok} ->
ok;
Problem ->
?INFO_MSG("Problem: ~n ~p", [Problem]), %+++
error
end
end.
filter_groups_existence(Host, Groups) ->
lists:partition(fun(Group) -> error /= mod_shared_roster:get_group_opts(Host, Group) end,
Groups).
%% @format-end
srg_get_members(Group, Host) ->
Members = mod_shared_roster:get_group_explicit_users(Host,Group),
[jid:encode(jid:make(MUser, MServer))
|| {MUser, MServer} <- Members].
srg_user_add(User, Host, Group, GroupHost) ->
mod_shared_roster:add_user_to_group(GroupHost, {User, Host}, Group),
ok.
srg_user_del(User, Host, Group, GroupHost) ->
mod_shared_roster:remove_user_from_group(GroupHost, {User, Host}, Group),
ok.
%%%
%%% Stanza
%%%
%% @doc Send a message to an XMPP account.
-spec send_message(Type::binary(), From::binary(), To::binary(),
Subject::binary(), Body::binary()) -> ok.
send_message(Type, From, To, Subject, Body) ->
CodecOpts = ejabberd_config:codec_options(),
try xmpp:decode(
#xmlel{name = <<"message">>,
attrs = [{<<"to">>, To},
{<<"from">>, From},
{<<"type">>, Type},
{<<"id">>, p1_rand:get_string()}],
children =
[#xmlel{name = <<"subject">>,
children = [{xmlcdata, Subject}]},
#xmlel{name = <<"body">>,
children = [{xmlcdata, Body}]}]},
?NS_CLIENT, CodecOpts) of
#message{from = JID, subject = SubjectEl, body = BodyEl} = Msg ->
Msg2 = case {xmpp:get_text(SubjectEl), xmpp:get_text(BodyEl)} of
{Subject, <<>>} -> Msg;
{<<>>, Body} -> Msg#message{subject = []};
_ -> Msg
end,
State = #{jid => JID},
ejabberd_hooks:run_fold(user_send_packet, JID#jid.lserver, {Msg2, State}, []),
ejabberd_router:route(Msg2)
catch _:{xmpp_codec, Why} ->
{error, xmpp:format_error(Why)}
end.
send_stanza(FromString, ToString, Stanza) ->
try
#xmlel{} = El = fxml_stream:parse_element(Stanza),
From = jid:decode(FromString),
To = jid:decode(ToString),
CodecOpts = ejabberd_config:codec_options(),
Pkt = xmpp:decode(El, ?NS_CLIENT, CodecOpts),
Pkt2 = xmpp:set_from_to(Pkt, From, To),
State = #{jid => From},
ejabberd_hooks:run_fold(user_send_packet, From#jid.lserver,
{Pkt2, State}, []),
ejabberd_router:route(Pkt2)
catch _:{xmpp_codec, Why} ->
io:format("incorrect stanza: ~ts~n", [xmpp:format_error(Why)]),
{error, Why};
_:{badmatch, {error, {Code, Why}}} when is_integer(Code) ->
io:format("invalid xml: ~p~n", [Why]),
{error, Why};
_:{badmatch, {error, Why}} ->
io:format("invalid xml: ~p~n", [Why]),
{error, Why};
_:{bad_jid, S} ->
io:format("malformed JID: ~ts~n", [S]),
{error, "JID malformed"}
end.
-spec send_stanza_c2s(binary(), binary(), binary(), binary()) -> ok | {error, any()}.
send_stanza_c2s(Username, Host, Resource, Stanza) ->
try
#xmlel{} = El = fxml_stream:parse_element(Stanza),
CodecOpts = ejabberd_config:codec_options(),
Pkt = xmpp:decode(El, ?NS_CLIENT, CodecOpts),
case ejabberd_sm:get_session_pid(Username, Host, Resource) of
Pid when is_pid(Pid) ->
ejabberd_c2s:send(Pid, Pkt);
_ ->
{error, no_session}
end
catch _:{badmatch, {error, Why} = Err} ->
io:format("invalid xml: ~p~n", [Why]),
Err;
_:{xmpp_codec, Why} ->
io:format("incorrect stanza: ~ts~n", [xmpp:format_error(Why)]),
{error, Why}
end.
privacy_set(Username, Host, QueryS) ->
Jid = jid:make(Username, Host),
QueryEl = fxml_stream:parse_element(QueryS),
SubEl = xmpp:decode(QueryEl),
IQ = #iq{type = set, id = <<"push">>, sub_els = [SubEl],
from = Jid, to = Jid},
Result = mod_privacy:process_iq(IQ),
Result#iq.type == result.
%%%
%%% Stats
%%%
stats(Name) ->
case Name of
<<"uptimeseconds">> -> trunc(element(1, erlang:statistics(wall_clock))/1000);
<<"processes">> -> length(erlang:processes());
<<"registeredusers">> -> lists:foldl(fun(Host, Sum) -> ejabberd_auth:count_users(Host) + Sum end, 0, ejabberd_option:hosts());
<<"onlineusersnode">> -> length(ejabberd_sm:dirty_get_my_sessions_list());
<<"onlineusers">> -> length(ejabberd_sm:dirty_get_sessions_list())
end.
stats(Name, Host) ->
case Name of
<<"registeredusers">> -> ejabberd_auth:count_users(Host);
<<"onlineusers">> -> length(ejabberd_sm:get_vh_session_list(Host))
end.
user_action(User, Server, Fun, OK) ->
case ejabberd_auth:user_exists(User, Server) of
true ->
case catch Fun() of
OK -> ok;
{error, Error} -> throw(Error);
Error ->
?ERROR_MSG("Command returned: ~p", [Error]),
1
end;
false ->
throw({not_found, "unknown_user"})
end.
num_prio(Priority) when is_integer(Priority) ->
Priority;
num_prio(_) ->
-1.
%%%
%%% Web Admin
%%%
%% @format-begin
%%% Main
web_menu_main(Acc, _Lang) ->
Acc ++ [{<<"stats">>, <<"Statistics">>}].
web_page_main(_, #request{path = [<<"stats">>]} = R) ->
Res = ?H1GL(<<"Statistics">>, <<"modules/#mod_stats">>, <<"mod_stats">>)
++ [make_command(stats_host, R, [], [{only, presentation}]),
make_command(incoming_s2s_number, R, [], [{only, presentation}]),
make_command(outgoing_s2s_number, R, [], [{only, presentation}]),
make_table([<<"stat name">>, {<<"stat value">>, right}],
[{?C(<<"Registered Users:">>),
make_command(stats,
R,
[{<<"name">>, <<"registeredusers">>}],
[{only, value}])},
{?C(<<"Online Users:">>),
make_command(stats,
R,
[{<<"name">>, <<"onlineusers">>}],
[{only, value}])},
{?C(<<"S2S Connections Incoming:">>),
make_command(incoming_s2s_number, R, [], [{only, value}])},
{?C(<<"S2S Connections Outgoing:">>),
make_command(outgoing_s2s_number, R, [], [{only, value}])}])],
{stop, Res};
web_page_main(Acc, _) ->
Acc.
%%% Host
web_menu_host(Acc, _Host, _Lang) ->
Acc ++ [{<<"purge">>, <<"Purge">>}, {<<"stats">>, <<"Statistics">>}].
web_page_host(_, Host, #request{path = [<<"purge">>]} = R) ->
Head = [?XC(<<"h1">>, <<"Purge">>)],
Set = [ejabberd_web_admin:make_command(delete_old_users_vhost,
R,
[{<<"host">>, Host}],
[])],
{stop, Head ++ Set};
web_page_host(_, Host, #request{path = [<<"stats">>]} = R) ->
Res = ?H1GL(<<"Statistics">>, <<"modules/#mod_stats">>, <<"mod_stats">>)
++ [make_command(stats_host, R, [], [{only, presentation}]),
make_table([<<"stat name">>, {<<"stat value">>, right}],
[{?C(<<"Registered Users:">>),
make_command(stats_host,
R,
[{<<"host">>, Host}, {<<"name">>, <<"registeredusers">>}],
[{only, value},
{result_links, [{stat, arg_host, 3, <<"users">>}]}])},
{?C(<<"Online Users:">>),
make_command(stats_host,
R,
[{<<"host">>, Host}, {<<"name">>, <<"onlineusers">>}],
[{only, value},
{result_links,
[{stat, arg_host, 3, <<"online-users">>}]}])}])],
{stop, Res};
web_page_host(Acc, _, _) ->
Acc.
%%% HostUser
web_menu_hostuser(Acc, _Host, _Username, _Lang) ->
Acc ++ [{<<"auth">>, <<"Authentication">>}, {<<"session">>, <<"Sessions">>}].
web_page_hostuser(_, Host, User, #request{path = [<<"auth">>]} = R) ->
Ban = make_command(ban_account,
R,
[{<<"user">>, User}, {<<"host">>, Host}],
[{style, danger}]),
Unban = make_command(unban_account, R, [{<<"user">>, User}, {<<"host">>, Host}], []),
Res = ?H1GLraw(<<"Authentication">>,
<<"admin/configuration/authentication/">>,
<<"Authentication">>)
++ [make_command(register, R, [{<<"user">>, User}, {<<"host">>, Host}], []),
make_command(check_account, R, [{<<"user">>, User}, {<<"host">>, Host}], []),
?X(<<"hr">>),
make_command(check_password, R, [{<<"user">>, User}, {<<"host">>, Host}], []),
make_command(check_password_hash, R, [{<<"user">>, User}, {<<"host">>, Host}], []),
make_command(change_password,
R,
[{<<"user">>, User}, {<<"host">>, Host}],
[{style, danger}]),
?X(<<"hr">>),
make_command(get_ban_details, R, [{<<"user">>, User}, {<<"host">>, Host}], []),
Ban,
Unban,
?X(<<"hr">>),
make_command(unregister,
R,
[{<<"user">>, User}, {<<"host">>, Host}],
[{style, danger}])],
{stop, Res};
web_page_hostuser(_, Host, User, #request{path = [<<"session">>]} = R) ->
Head = [?XC(<<"h1">>, <<"Sessions">>), ?BR],
Set = [make_command(resource_num, R, [{<<"user">>, User}, {<<"host">>, Host}], []),
make_command(set_presence, R, [{<<"user">>, User}, {<<"host">>, Host}], []),
make_command(kick_user, R, [{<<"user">>, User}, {<<"host">>, Host}], [{style, danger}]),
make_command(kick_session,
R,
[{<<"user">>, User}, {<<"host">>, Host}],
[{style, danger}])],
timer:sleep(100), % kicking sessions takes a while, let's delay the get commands
Get = [make_command(user_sessions_info,
R,
[{<<"user">>, User}, {<<"host">>, Host}],
[{result_links, [{node, node, 5, <<>>}]}]),
make_command(user_resources, R, [{<<"user">>, User}, {<<"host">>, Host}], []),
make_command(get_presence, R, [{<<"user">>, User}, {<<"host">>, Host}], []),
make_command(num_resources, R, [{<<"user">>, User}, {<<"host">>, Host}], [])],
{stop, Head ++ Get ++ Set};
web_page_hostuser(Acc, _, _, _) ->
Acc.
%%% HostNode
web_menu_hostnode(Acc, _Host, _Username, _Lang) ->
Acc ++ [{<<"modules">>, <<"Modules">>}].
web_page_hostnode(_, Host, Node, #request{path = [<<"modules">>]} = R) ->
Res = ?H1GLraw(<<"Modules">>, <<"admin/configuration/modules/">>, <<"Modules Options">>)
++ [ejabberd_cluster:call(Node,
ejabberd_web_admin,
make_command,
[restart_module, R, [{<<"host">>, Host}], []])],
{stop, Res};
web_page_hostnode(Acc, _Host, _Node, _Request) ->
Acc.
%%% Node
web_menu_node(Acc, _Node, _Lang) ->
Acc ++ [{<<"stats">>, <<"Statistics">>}].
web_page_node(_, Node, #request{path = [<<"stats">>]} = R) ->
UpSecs =
ejabberd_cluster:call(Node,
ejabberd_web_admin,
make_command,
[stats, R, [{<<"name">>, <<"uptimeseconds">>}], [{only, value}]]),
UpDaysBin =
integer_to_binary(binary_to_integer(fxml:get_tag_cdata(UpSecs))
div 86400), % 24*60*60
UpDays =
#xmlel{name = <<"code">>,
attrs = [],
children = [{xmlcdata, UpDaysBin}]},
Res = ?H1GL(<<"Statistics">>, <<"modules/#mod_stats">>, <<"mod_stats">>)
++ [make_command(stats, R, [], [{only, presentation}]),
make_table([<<"stat name">>, {<<"stat value">>, right}],
[{?C(<<"Online Users in this node:">>),
ejabberd_cluster:call(Node,
ejabberd_web_admin,
make_command,
[stats,
R,
[{<<"name">>, <<"onlineusersnode">>}],
[{only, value}]])},
{?C(<<"Uptime Seconds:">>), UpSecs},
{?C(<<"Uptime Seconds (rounded to days):">>), UpDays},
{?C(<<"Processes:">>),
ejabberd_cluster:call(Node,
ejabberd_web_admin,
make_command,
[stats,
R,
[{<<"name">>, <<"processes">>}],
[{only, value}]])}])],
{stop, Res};
web_page_node(Acc, _, _) ->
Acc.
%% @format-end
%%%
%%% Document
%%%
mod_options(_) -> [].
mod_doc() ->
#{desc =>
[?T("This module provides additional administrative commands."), "",
?T("Details for some commands:"), "",
?T("_`ban_account`_ API:"),
?T("This command kicks all the connected sessions of the account "
"from the server. It also changes their password to a randomly "
"generated one, so they can't login anymore unless a server "
"administrator changes their password again. It is possible to "
"define the reason of the ban. The new password also includes "
"the reason and the date and time of the ban. See an example below."), "",
?T("_`push_roster`_ API (and _`push_roster_all`_ API):"),
?T("The roster file must be placed, if using Windows, on the "
"directory where you installed ejabberd: "
"`C:/Program Files/ejabberd` or similar. If you use other "
"Operating System, place the file on the same directory where "
"the .beam files are installed. See below an example roster file."), "",
?T("_`srg_create`_ API:"),
?T("If you want to put a group Name with blank spaces, use the "
"characters '\"\'' and '\'\"' to define when the Name starts and "
"ends. See an example below.")],
example =>
[{?T("With this configuration, vCards can only be modified with "
"mod_admin_extra commands:"),
["acl:",
" adminextraresource:",
" - resource: \"modadminextraf8x,31ad\"",
"access_rules:",
" vcard_set:",
" - allow: adminextraresource",
"modules:",
" mod_admin_extra: {}",
" mod_vcard:",
" access_set: vcard_set"]},
{?T("Content of roster file for _`push_roster`_ API:"),
["[{<<\"bob\">>, <<\"example.org\">>, <<\"workers\">>, <<\"Bob\">>},",
"{<<\"mart\">>, <<\"example.org\">>, <<\"workers\">>, <<\"Mart\">>},",
"{<<\"Rich\">>, <<\"example.org\">>, <<\"bosses\">>, <<\"Rich\">>}]."]},
{?T("With this call, the sessions of the local account which JID is "
"'boby@example.org' will be kicked, and its password will be set "
"to something like "
"'BANNED_ACCOUNT--20080425T21:45:07--2176635--Spammed_rooms'"),
["ejabberdctl vhost example.org ban_account boby \"Spammed rooms\""]},
{?T("Call to _`srg_create`_ API using double-quotes and single-quotes:"),
["ejabberdctl srg_create g1 example.org \"\'Group number 1\'\" this_is_g1 g1"]}]}.
ejabberd-24.12/src/mod_sip.erl 0000664 0001750 0001750 00000036744 14730775155 016621 0 ustar debalance debalance %%%-------------------------------------------------------------------
%%% File : mod_sip.erl
%%% Author : Evgeny Khramtsov
%%% Purpose : SIP RFC-3261
%%% Created : 21 Apr 2014 by Evgeny Khramtsov
%%%
%%%
%%% ejabberd, Copyright (C) 2014-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_sip).
-protocol({rfc, 3261}).
-include("logger.hrl").
-include("translate.hrl").
-ifndef(SIP).
-export([start/2, stop/1, depends/2, mod_options/1, mod_doc/0]).
start(_, _) ->
?CRITICAL_MSG("ejabberd is not compiled with SIP support", []),
{error, sip_not_compiled}.
stop(_) ->
ok.
depends(_, _) ->
[].
mod_options(_) ->
[].
mod_doc() ->
#{desc => [?T("SIP support has not been enabled.")]}.
-else.
-behaviour(gen_mod).
-behaviour(esip).
%% API
-export([start/2, stop/1, reload/3,
make_response/2, is_my_host/1, at_my_host/1]).
-export([data_in/2, data_out/2, message_in/2,
message_out/2, request/2, request/3, response/2,
locate/1, mod_opt_type/1, mod_options/1, depends/2,
mod_doc/0]).
-include_lib("esip/include/esip.hrl").
%%%===================================================================
%%% API
%%%===================================================================
start(_Host, _Opts) ->
ejabberd:start_app(esip),
esip:set_config_value(max_server_transactions, 10000),
esip:set_config_value(max_client_transactions, 10000),
esip:set_config_value(
software, <<"ejabberd ", (ejabberd_option:version())/binary>>),
esip:set_config_value(module, ?MODULE),
Spec = {mod_sip_registrar, {mod_sip_registrar, start_link, []},
transient, 2000, worker, [mod_sip_registrar]},
TmpSupSpec = {mod_sip_proxy_sup,
{ejabberd_tmp_sup, start_link,
[mod_sip_proxy_sup, mod_sip_proxy]},
permanent, infinity, supervisor, [ejabberd_tmp_sup]},
supervisor:start_child(ejabberd_gen_mod_sup, Spec),
supervisor:start_child(ejabberd_gen_mod_sup, TmpSupSpec),
ok.
stop(_Host) ->
ok.
reload(_Host, _NewOpts, _OldOpts) ->
ok.
depends(_Host, _Opts) ->
[].
data_in(Data, #sip_socket{type = Transport,
addr = {MyIP, MyPort},
peer = {PeerIP, PeerPort}}) ->
?DEBUG(
"SIP [~p/in] ~ts:~p -> ~ts:~p:~n~ts",
[Transport, inet_parse:ntoa(PeerIP), PeerPort,
inet_parse:ntoa(MyIP), MyPort, Data]).
data_out(Data, #sip_socket{type = Transport,
addr = {MyIP, MyPort},
peer = {PeerIP, PeerPort}}) ->
?DEBUG(
"SIP [~p/out] ~ts:~p -> ~ts:~p:~n~ts",
[Transport, inet_parse:ntoa(MyIP), MyPort,
inet_parse:ntoa(PeerIP), PeerPort, Data]).
message_in(#sip{type = request, method = M} = Req, SIPSock)
when M /= <<"ACK">>, M /= <<"CANCEL">> ->
case action(Req, SIPSock) of
{relay, _LServer} ->
ok;
Action ->
request(Req, SIPSock, undefined, Action)
end;
message_in(ping, SIPSock) ->
mod_sip_registrar:ping(SIPSock);
message_in(_, _) ->
ok.
message_out(_, _) ->
ok.
response(_Resp, _SIPSock) ->
ok.
request(#sip{method = <<"ACK">>} = Req, SIPSock) ->
case action(Req, SIPSock) of
{relay, LServer} ->
mod_sip_proxy:route(Req, LServer, [{authenticated, true}]);
{proxy_auth, LServer} ->
mod_sip_proxy:route(Req, LServer, [{authenticated, false}]);
_ ->
ok
end;
request(_Req, _SIPSock) ->
ok.
request(Req, SIPSock, TrID) ->
request(Req, SIPSock, TrID, action(Req, SIPSock)).
request(Req, SIPSock, TrID, Action) ->
case Action of
to_me ->
process(Req, SIPSock);
register ->
mod_sip_registrar:request(Req, SIPSock);
loop ->
make_response(Req, #sip{status = 483, type = response});
{unsupported, Require} ->
make_response(Req, #sip{status = 420,
type = response,
hdrs = [{'unsupported',
Require}]});
{relay, LServer} ->
case mod_sip_proxy:start(LServer, []) of
{ok, Pid} ->
mod_sip_proxy:route(Req, SIPSock, TrID, Pid),
{mod_sip_proxy, route, [Pid]};
Err ->
?WARNING_MSG("Failed to proxy request ~p: ~p", [Req, Err]),
Err
end;
{proxy_auth, LServer} ->
make_response(
Req,
#sip{status = 407,
type = response,
hdrs = [{'proxy-authenticate',
make_auth_hdr(LServer)}]});
{auth, LServer} ->
make_response(
Req,
#sip{status = 401,
type = response,
hdrs = [{'www-authenticate',
make_auth_hdr(LServer)}]});
deny ->
make_response(Req, #sip{status = 403,
type = response});
not_found ->
make_response(Req, #sip{status = 480,
type = response})
end.
locate(_SIPMsg) ->
ok.
find(#uri{user = User, host = Host}) ->
LUser = jid:nodeprep(User),
LServer = jid:nameprep(Host),
if LUser == <<"">> ->
to_me;
true ->
case mod_sip_registrar:find_sockets(LUser, LServer) of
[] ->
not_found;
[_|_] ->
{relay, LServer}
end
end.
%%%===================================================================
%%% Internal functions
%%%===================================================================
action(#sip{method = <<"REGISTER">>, type = request, hdrs = Hdrs,
uri = #uri{user = <<"">>} = URI} = Req, SIPSock) ->
case at_my_host(URI) of
true ->
Require = esip:get_hdrs('require', Hdrs) -- supported(),
case Require of
[_|_] ->
{unsupported, Require};
_ ->
{_, ToURI, _} = esip:get_hdr('to', Hdrs),
case at_my_host(ToURI) of
true ->
case check_auth(Req, 'authorization', SIPSock) of
true ->
register;
false ->
{auth, jid:nameprep(ToURI#uri.host)}
end;
false ->
deny
end
end;
false ->
deny
end;
action(#sip{method = Method, hdrs = Hdrs, type = request} = Req, SIPSock) ->
case esip:get_hdr('max-forwards', Hdrs) of
0 when Method == <<"OPTIONS">> ->
to_me;
0 ->
loop;
_ ->
Require = esip:get_hdrs('proxy-require', Hdrs) -- supported(),
case Require of
[_|_] ->
{unsupported, Require};
_ ->
{_, ToURI, _} = esip:get_hdr('to', Hdrs),
{_, FromURI, _} = esip:get_hdr('from', Hdrs),
case at_my_host(FromURI) of
true ->
case check_auth(Req, 'proxy-authorization', SIPSock) of
true ->
case at_my_host(ToURI) of
true ->
find(ToURI);
false ->
LServer = jid:nameprep(FromURI#uri.host),
{relay, LServer}
end;
false ->
{proxy_auth, FromURI#uri.host}
end;
false ->
case at_my_host(ToURI) of
true ->
find(ToURI);
false ->
deny
end
end
end
end.
check_auth(#sip{method = <<"CANCEL">>}, _, _SIPSock) ->
true;
check_auth(#sip{method = Method, hdrs = Hdrs, body = Body}, AuthHdr, _SIPSock) ->
Issuer = case AuthHdr of
'authorization' ->
to;
'proxy-authorization' ->
from
end,
{_, #uri{user = User, host = Host}, _} = esip:get_hdr(Issuer, Hdrs),
LUser = jid:nodeprep(User),
LServer = jid:nameprep(Host),
case lists:filter(
fun({_, Params}) ->
Username = esip:get_param(<<"username">>, Params),
Realm = esip:get_param(<<"realm">>, Params),
(LUser == esip:unquote(Username))
and (LServer == esip:unquote(Realm))
end, esip:get_hdrs(AuthHdr, Hdrs)) of
[Auth|_] ->
case ejabberd_auth:get_password_s(LUser, LServer) of
<<"">> ->
false;
Password when is_binary(Password) ->
esip:check_auth(Auth, Method, Body, Password);
_ScramedPassword ->
?ERROR_MSG("Unable to authenticate ~ts@~ts against SCRAM'ed "
"password", [LUser, LServer]),
false
end;
[] ->
false
end.
allow() ->
[<<"OPTIONS">>, <<"REGISTER">>].
supported() ->
[<<"path">>, <<"outbound">>].
process(#sip{method = <<"OPTIONS">>} = Req, _) ->
make_response(Req, #sip{type = response, status = 200,
hdrs = [{'allow', allow()},
{'supported', supported()}]});
process(#sip{method = <<"REGISTER">>} = Req, _) ->
make_response(Req, #sip{type = response, status = 400});
process(Req, _) ->
make_response(Req, #sip{type = response, status = 405,
hdrs = [{'allow', allow()}]}).
make_auth_hdr(LServer) ->
{<<"Digest">>, [{<<"realm">>, esip:quote(LServer)},
{<<"qop">>, esip:quote(<<"auth">>)},
{<<"nonce">>, esip:quote(esip:make_hexstr(20))}]}.
make_response(Req, Resp) ->
esip:make_response(Req, Resp, esip:make_tag()).
at_my_host(#uri{host = Host}) ->
is_my_host(jid:nameprep(Host)).
is_my_host(LServer) ->
gen_mod:is_loaded(LServer, ?MODULE).
mod_opt_type(always_record_route) ->
econf:bool();
mod_opt_type(flow_timeout_tcp) ->
econf:timeout(second);
mod_opt_type(flow_timeout_udp) ->
econf:timeout(second);
mod_opt_type(record_route) ->
econf:sip_uri();
mod_opt_type(routes) ->
econf:list(econf:sip_uri());
mod_opt_type(via) ->
econf:list(
fun(L) when is_list(L) ->
(econf:and_then(
econf:options(
#{type => econf:enum([tcp, tls, udp]),
host => econf:domain(),
port => econf:port()},
[{required, [type, host]}]),
fun(Opts) ->
Type = proplists:get_value(type, Opts),
Host = proplists:get_value(host, Opts),
Port = proplists:get_value(port, Opts),
{Type, {Host, Port}}
end))(L);
(U) ->
(econf:and_then(
econf:url([tls, tcp, udp]),
fun(URI) ->
{ok, Type, _UserInfo, Host, Port, _, _} =
misc:uri_parse(URI),
{list_to_atom(Type), {unicode:characters_to_binary(Host), Port}}
end))(U)
end, [unique]).
-spec mod_options(binary()) -> [{via, [{tcp | tls | udp, {binary(), 1..65535}}]} |
{atom(), term()}].
mod_options(Host) ->
Route = #uri{scheme = <<"sip">>,
host = Host,
params = [{<<"lr">>, <<>>}]},
[{always_record_route, true},
{flow_timeout_tcp, timer:seconds(120)},
{flow_timeout_udp, timer:seconds(29)},
{record_route, Route},
{routes, [Route]},
{via, []}].
mod_doc() ->
#{desc =>
[?T("This module adds SIP proxy/registrar support "
"for the corresponding virtual host."), "",
?T("NOTE: It is not enough to just load this module. "
"You should also configure listeners and DNS records "
"properly. For details see the section about the "
"_`listen.md#ejabberd_sip|ejabberd_sip`_ listen module "
"in the ejabberd Documentation.")],
opts =>
[{always_record_route,
#{value => "true | false",
desc =>
?T("Always insert \"Record-Route\" header into "
"SIP messages. With this approach it is possible to bypass "
"NATs/firewalls a bit more easily. "
"The default value is 'true'.")}},
{flow_timeout_tcp,
#{value => "timeout()",
desc =>
?T("The option sets a keep-alive timer for "
"https://tools.ietf.org/html/rfc5626[SIP outbound] "
"TCP connections. The default value is '2' minutes.")}},
{flow_timeout_udp,
#{value => "timeout()",
desc =>
?T("The options sets a keep-alive timer for "
"https://tools.ietf.org/html/rfc5626[SIP outbound] "
"UDP connections. The default value is '29' seconds.")}},
{record_route,
#{value => ?T("URI"),
desc =>
?T("When the option 'always_record_route' is set to "
"'true' or when https://tools.ietf.org/html/rfc5626"
"[SIP outbound] is utilized, ejabberd inserts "
"\"Record-Route\" header field with this 'URI' into "
"a SIP message. The default is a SIP URI constructed "
"from the virtual host on which the module is loaded.")}},
{routes,
#{value => "[URI, ...]",
desc =>
?T("You can set a list of SIP URIs of routes pointing "
"to this SIP proxy server. The default is a list containing "
"a single SIP URI constructed from the virtual host "
"on which the module is loaded.")}},
{via,
#{value => "[URI, ...]",
desc =>
?T("A list to construct \"Via\" headers for "
"inserting them into outgoing SIP messages. "
"This is useful if you're running your SIP proxy "
"in a non-standard network topology. Every 'URI' "
"element in the list must be in the form of "
"\"scheme://host:port\", where \"transport\" "
"must be 'tls', 'tcp', or 'udp', \"host\" must "
"be a domain name or an IP address and \"port\" "
"must be an internet port number. Note that all "
"parts of the 'URI' are mandatory (e.g. you "
"cannot omit \"port\" or \"scheme\").")}}],
example =>
["modules:",
" mod_sip:",
" always_record_route: false",
" record_route: \"sip:example.com;lr\"",
" routes:",
" - \"sip:example.com;lr\"",
" - \"sip:sip.example.com;lr\"",
" flow_timeout_udp: 30 sec",
" flow_timeout_tcp: 1 min",
" via:",
" - tls://sip-tls.example.com:5061",
" - tcp://sip-tcp.example.com:5060",
" - udp://sip-udp.example.com:5060"]}.
-endif.
ejabberd-24.12/src/ejabberd_captcha.erl 0000664 0001750 0001750 00000054615 14730775155 020405 0 ustar debalance debalance %%%-------------------------------------------------------------------
%%% File : ejabberd_captcha.erl
%%% Author : Evgeniy Khramtsov
%%% Purpose : CAPTCHA processing.
%%% Created : 26 Apr 2008 by Evgeniy Khramtsov
%%%
%%%
%%% 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(ejabberd_captcha).
-protocol({xep, 158, '1.0', '2.1.0', "complete", ""}).
-protocol({xep, 231, '1.0', '2.1.0', "complete", ""}).
-behaviour(gen_server).
%% API
-export([start_link/0]).
%% gen_server callbacks
-export([init/1, handle_call/3, handle_cast/2,
handle_info/2, terminate/2, code_change/3]).
-export([create_captcha/6, build_captcha_html/2,
check_captcha/2, process_reply/1, process/2,
is_feature_available/0, create_captcha_x/5,
host_up/1, host_down/1,
config_reloaded/0, process_iq/1]).
-include_lib("xmpp/include/xmpp.hrl").
-include("logger.hrl").
-include("ejabberd_http.hrl").
-include("translate.hrl").
-define(CAPTCHA_LIFETIME, 120000).
-define(LIMIT_PERIOD, 60*1000*1000).
-type image_error() :: efbig | enodata | limit | malformed_image | timeout.
-type priority() :: neg_integer().
-type callback() :: fun((captcha_succeed | captcha_failed) -> any()).
-record(state, {limits = treap:empty() :: treap:treap(),
enabled = false :: boolean()}).
-record(captcha, {id :: binary(),
pid :: pid() | undefined,
key :: binary(),
tref :: reference(),
args :: any()}).
start_link() ->
gen_server:start_link({local, ?MODULE}, ?MODULE, [],
[]).
-spec captcha_text(binary()) -> binary().
captcha_text(Lang) ->
translate:translate(Lang, ?T("Enter the text you see")).
-spec mk_ocr_field(binary(), binary(), binary()) -> xdata_field().
mk_ocr_field(Lang, CID, Type) ->
URI = #media_uri{type = Type, uri = <<"cid:", CID/binary>>},
[_, F] = captcha_form:encode([{ocr, <<>>}], Lang, [ocr]),
xmpp:set_els(F, [#media{uri = [URI]}]).
update_captcha_key(_Id, Key, Key) ->
ok;
update_captcha_key(Id, _Key, Key2) ->
true = ets:update_element(captcha, Id, [{4, Key2}]).
-spec create_captcha(binary(), jid(), jid(),
binary(), any(),
callback() | term()) -> {error, image_error()} |
{ok, binary(), [text()], [xmpp_element()]}.
create_captcha(SID, From, To, Lang, Limiter, Args) ->
case create_image(Limiter) of
{ok, Type, Key, Image} ->
Id = <<(p1_rand:get_string())/binary>>,
JID = jid:encode(From),
CID = <<"sha1+", (str:sha(Image))/binary, "@bob.xmpp.org">>,
Data = #bob_data{cid = CID, 'max-age' = 0, type = Type, data = Image},
Fs = captcha_form:encode(
[{from, To}, {challenge, Id}, {sid, SID},
mk_ocr_field(Lang, CID, Type)],
Lang, [challenge]),
X = #xdata{type = form, fields = Fs},
Captcha = #xcaptcha{xdata = X},
BodyString = {?T("Your subscription request and/or messages to ~s have been blocked. "
"To unblock your subscription request, visit ~s"), [JID, get_url(Id)]},
Body = xmpp:mk_text(BodyString, Lang),
OOB = #oob_x{url = get_url(Id)},
Hint = #hint{type = 'no-store'},
Tref = erlang:send_after(?CAPTCHA_LIFETIME, ?MODULE, {remove_id, Id}),
ets:insert(captcha,
#captcha{id = Id, pid = self(), key = Key, tref = Tref,
args = Args}),
{ok, Id, Body, [Hint, OOB, Captcha, Data]};
Err -> Err
end.
-spec create_captcha_x(binary(), jid(), binary(), any(), xdata()) ->
{ok, [xmpp_element()]} | {error, image_error()}.
create_captcha_x(SID, To, Lang, Limiter, #xdata{fields = Fs} = X) ->
case create_image(Limiter) of
{ok, Type, Key, Image} ->
Id = <<(p1_rand:get_string())/binary>>,
CID = <<"sha1+", (str:sha(Image))/binary, "@bob.xmpp.org">>,
Data = #bob_data{cid = CID, 'max-age' = 0, type = Type, data = Image},
HelpTxt = translate:translate(
Lang, ?T("If you don't see the CAPTCHA image here, visit the web page.")),
Imageurl = get_url(<>),
[H|T] = captcha_form:encode(
[{'captcha-fallback-text', HelpTxt},
{'captcha-fallback-url', Imageurl},
{from, To}, {challenge, Id}, {sid, SID},
mk_ocr_field(Lang, CID, Type)],
Lang, [challenge]),
Captcha = X#xdata{type = form, fields = [H|Fs ++ T]},
Tref = erlang:send_after(?CAPTCHA_LIFETIME, ?MODULE, {remove_id, Id}),
ets:insert(captcha, #captcha{id = Id, key = Key, tref = Tref}),
{ok, [Captcha, Data]};
Err -> Err
end.
-spec build_captcha_html(binary(), binary()) -> captcha_not_found |
{xmlel(),
{xmlel(), cdata(),
xmlel(), xmlel()}}.
build_captcha_html(Id, Lang) ->
case lookup_captcha(Id) of
{ok, _} ->
ImgEl = #xmlel{name = <<"img">>,
attrs =
[{<<"src">>, get_url(<>)}],
children = []},
Text = {xmlcdata, captcha_text(Lang)},
IdEl = #xmlel{name = <<"input">>,
attrs =
[{<<"type">>, <<"hidden">>}, {<<"name">>, <<"id">>},
{<<"value">>, Id}],
children = []},
KeyEl = #xmlel{name = <<"input">>,
attrs =
[{<<"type">>, <<"text">>}, {<<"name">>, <<"key">>},
{<<"size">>, <<"10">>}],
children = []},
FormEl = #xmlel{name = <<"form">>,
attrs =
[{<<"action">>, get_url(Id)},
{<<"name">>, <<"captcha">>},
{<<"method">>, <<"POST">>}],
children =
[ImgEl,
#xmlel{name = <<"br">>, attrs = [],
children = []},
Text,
#xmlel{name = <<"br">>, attrs = [],
children = []},
IdEl, KeyEl,
#xmlel{name = <<"br">>, attrs = [],
children = []},
#xmlel{name = <<"input">>,
attrs =
[{<<"type">>, <<"submit">>},
{<<"name">>, <<"enter">>},
{<<"value">>, ?T("OK")}],
children = []}]},
{FormEl, {ImgEl, Text, IdEl, KeyEl}};
_ -> captcha_not_found
end.
-spec process_reply(xmpp_element()) -> ok | {error, bad_match | not_found | malformed}.
process_reply(#xdata{} = X) ->
Required = [<<"challenge">>, <<"ocr">>],
Fs = lists:filter(
fun(#xdata_field{var = Var}) ->
lists:member(Var, [<<"FORM_TYPE">>|Required])
end, X#xdata.fields),
try captcha_form:decode(Fs, [?NS_CAPTCHA], Required) of
Props ->
Id = proplists:get_value(challenge, Props),
OCR = proplists:get_value(ocr, Props),
case check_captcha(Id, OCR) of
captcha_valid -> ok;
captcha_non_valid -> {error, bad_match};
captcha_not_found -> {error, not_found}
end
catch _:{captcha_form, Why} ->
?WARNING_MSG("Malformed CAPTCHA form: ~ts",
[captcha_form:format_error(Why)]),
{error, malformed}
end;
process_reply(#xcaptcha{xdata = #xdata{} = X}) ->
process_reply(X);
process_reply(_) ->
{error, malformed}.
-spec process_iq(iq()) -> iq().
process_iq(#iq{type = set, lang = Lang, sub_els = [#xcaptcha{} = El]} = IQ) ->
case process_reply(El) of
ok ->
xmpp:make_iq_result(IQ);
{error, malformed} ->
Txt = ?T("Incorrect CAPTCHA submit"),
xmpp:make_error(IQ, xmpp:err_bad_request(Txt, Lang));
{error, _} ->
Txt = ?T("The CAPTCHA verification has failed"),
xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang))
end;
process_iq(#iq{type = get, lang = Lang} = IQ) ->
Txt = ?T("Value 'get' of 'type' attribute is not allowed"),
xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang));
process_iq(#iq{lang = Lang} = IQ) ->
Txt = ?T("No module is handling this query"),
xmpp:make_error(IQ, xmpp:err_service_unavailable(Txt, Lang)).
process(_Handlers,
#request{method = 'GET', lang = Lang,
path = [_, Id]}) ->
case build_captcha_html(Id, Lang) of
{FormEl, _} ->
Form = #xmlel{name = <<"div">>,
attrs = [{<<"align">>, <<"center">>}],
children = [FormEl]},
ejabberd_web:make_xhtml([Form]);
captcha_not_found -> ejabberd_web:error(not_found)
end;
process(_Handlers,
#request{method = 'GET', path = [_, Id, <<"image">>],
ip = IP}) ->
{Addr, _Port} = IP,
case lookup_captcha(Id) of
{ok, #captcha{key = Key}} ->
case create_image(Addr, Key) of
{ok, Type, Key2, Img} ->
update_captcha_key(Id, Key, Key2),
{200,
[{<<"Content-Type">>, Type},
{<<"Cache-Control">>, <<"no-cache">>},
{<<"Last-Modified">>, list_to_binary(httpd_util:rfc1123_date())}],
Img};
{error, limit} -> ejabberd_web:error(not_allowed);
_ -> ejabberd_web:error(not_found)
end;
_ -> ejabberd_web:error(not_found)
end;
process(_Handlers,
#request{method = 'POST', q = Q, lang = Lang,
path = [_, Id]}) ->
ProvidedKey = proplists:get_value(<<"key">>, Q, none),
case check_captcha(Id, ProvidedKey) of
captcha_valid ->
Form = #xmlel{name = <<"p">>, attrs = [],
children =
[{xmlcdata,
translate:translate(Lang,
?T("The CAPTCHA is valid."))}]},
ejabberd_web:make_xhtml([Form]);
captcha_non_valid -> ejabberd_web:error(not_allowed);
captcha_not_found -> ejabberd_web:error(not_found)
end;
process(_Handlers, _Request) ->
ejabberd_web:error(not_found).
host_up(Host) ->
gen_iq_handler:add_iq_handler(ejabberd_sm, Host, ?NS_CAPTCHA,
?MODULE, process_iq).
host_down(Host) ->
gen_iq_handler:remove_iq_handler(ejabberd_sm, Host, ?NS_CAPTCHA).
config_reloaded() ->
gen_server:call(?MODULE, config_reloaded, timer:minutes(1)).
init([]) ->
_ = mnesia:delete_table(captcha),
_ = ets:new(captcha, [named_table, public, {keypos, #captcha.id}]),
case check_captcha_setup() of
true ->
register_handlers(),
ejabberd_hooks:add(config_reloaded, ?MODULE, config_reloaded, 70),
{ok, #state{enabled = true}};
false ->
{ok, #state{enabled = false}};
{error, Reason} ->
{stop, Reason}
end.
handle_call({is_limited, Limiter, RateLimit}, _From,
State) ->
NowPriority = now_priority(),
CleanPriority = NowPriority + (?LIMIT_PERIOD),
Limits = clean_treap(State#state.limits, CleanPriority),
case treap:lookup(Limiter, Limits) of
{ok, _, Rate} when Rate >= RateLimit ->
{reply, true, State#state{limits = Limits}};
{ok, Priority, Rate} ->
NewLimits = treap:insert(Limiter, Priority, Rate + 1,
Limits),
{reply, false, State#state{limits = NewLimits}};
_ ->
NewLimits = treap:insert(Limiter, NowPriority, 1,
Limits),
{reply, false, State#state{limits = NewLimits}}
end;
handle_call(config_reloaded, _From, #state{enabled = Enabled} = State) ->
State1 = case is_feature_available() of
true when not Enabled ->
case check_captcha_setup() of
true ->
register_handlers(),
State#state{enabled = true};
_ ->
State
end;
false when Enabled ->
unregister_handlers(),
State#state{enabled = false};
_ ->
State
end,
{reply, ok, State1};
handle_call(Request, From, State) ->
?WARNING_MSG("Unexpected call from ~p: ~p", [From, Request]),
{noreply, State}.
handle_cast(Msg, State) ->
?WARNING_MSG("Unexpected cast: ~p", [Msg]),
{noreply, State}.
handle_info({remove_id, Id}, State) ->
?DEBUG("CAPTCHA ~p timed out", [Id]),
case ets:lookup(captcha, Id) of
[#captcha{args = Args, pid = Pid}] ->
callback(captcha_failed, Pid, Args),
ets:delete(captcha, Id);
_ -> ok
end,
{noreply, State};
handle_info(Info, State) ->
?WARNING_MSG("Unexpected info: ~p", [Info]),
{noreply, State}.
terminate(_Reason, #state{enabled = Enabled}) ->
if Enabled -> unregister_handlers();
true -> ok
end,
ejabberd_hooks:delete(config_reloaded, ?MODULE, config_reloaded, 70).
register_handlers() ->
ejabberd_hooks:add(host_up, ?MODULE, host_up, 50),
ejabberd_hooks:add(host_down, ?MODULE, host_down, 50),
lists:foreach(fun host_up/1, ejabberd_option:hosts()).
unregister_handlers() ->
ejabberd_hooks:delete(host_up, ?MODULE, host_up, 50),
ejabberd_hooks:delete(host_down, ?MODULE, host_down, 50),
lists:foreach(fun host_down/1, ejabberd_option:hosts()).
code_change(_OldVsn, State, _Extra) -> {ok, State}.
-spec create_image() -> {ok, binary(), binary(), binary()} |
{error, image_error()}.
create_image() ->
create_image(undefined).
-spec create_image(term()) -> {ok, binary(), binary(), binary()} |
{error, image_error()}.
create_image(Limiter) ->
Key = str:substr(p1_rand:get_string(), 1, 6),
create_image(Limiter, Key).
-spec create_image(term(), binary()) -> {ok, binary(), binary(), binary()} |
{error, image_error()}.
create_image(Limiter, Key) ->
case is_limited(Limiter) of
true -> {error, limit};
false -> do_create_image(Key)
end.
-spec do_create_image(binary()) -> {ok, binary(), binary(), binary()} |
{error, image_error()}.
do_create_image(Key) ->
FileName = get_prog_name(),
case length(binary:split(FileName, <<"/">>)) == 1 of
true ->
do_create_image(Key, misc:binary_to_atom(FileName));
false ->
do_create_image(Key, FileName)
end.
do_create_image(Key, Module) when is_atom(Module) ->
Function = create_image,
erlang:apply(Module, Function, [Key]);
do_create_image(Key, FileName) when is_binary(FileName) ->
Cmd = lists:flatten(io_lib:format("~ts ~ts", [FileName, Key])),
case cmd(Cmd) of
{ok,
<<137, $P, $N, $G, $\r, $\n, 26, $\n, _/binary>> =
Img} ->
{ok, <<"image/png">>, Key, Img};
{ok, <<255, 216, _/binary>> = Img} ->
{ok, <<"image/jpeg">>, Key, Img};
{ok, <<$G, $I, $F, $8, X, $a, _/binary>> = Img}
when X == $7; X == $9 ->
{ok, <<"image/gif">>, Key, Img};
{error, enodata = Reason} ->
?ERROR_MSG("Failed to process output from \"~ts\". "
"Maybe ImageMagick's Convert program "
"is not installed.",
[Cmd]),
{error, Reason};
{error, Reason} ->
?ERROR_MSG("Failed to process an output from \"~ts\": ~p",
[Cmd, Reason]),
{error, Reason};
_ ->
Reason = malformed_image,
?ERROR_MSG("Failed to process an output from \"~ts\": ~p",
[Cmd, Reason]),
{error, Reason}
end.
get_prog_name() ->
case ejabberd_option:captcha_cmd() of
undefined ->
?WARNING_MSG("The option captcha_cmd is not configured, "
"but some module wants to use the CAPTCHA "
"feature.",
[]),
false;
FileName ->
maybe_warning_norequesthandler(),
FileName
end.
maybe_warning_norequesthandler() ->
Host = hd(ejabberd_option:hosts()),
URL = get_auto_url(any, ?MODULE, Host),
case URL of
undefined ->
?WARNING_MSG("The option captcha_cmd is configured, "
"but there is NO request_handler in listen option "
"configured with ejabberd_captcha. Please check "
"https://docs.ejabberd.im/admin/configuration/basic/#captcha",
[]);
_ ->
ok
end.
-spec get_url(binary()) -> binary().
get_url(Str) ->
case ejabberd_option:captcha_url() of
auto ->
Host = ejabberd_config:get_myname(),
URL = get_auto_url(any, ?MODULE, Host),
<>;
undefined ->
URL = parse_captcha_host(),
<>;
URL ->
<>
end.
-spec parse_captcha_host() -> binary().
parse_captcha_host() ->
CaptchaHost = ejabberd_option:captcha_host(),
case str:tokens(CaptchaHost, <<":">>) of
[Host] ->
<<"http://", Host/binary>>;
[<<"http", _/binary>> = TransferProt, Host] ->
<>;
[Host, PortString] ->
TransferProt = atom_to_binary(get_transfer_protocol(PortString), latin1),
<>;
[TransferProt, Host, PortString] ->
<>;
_ ->
<<"http://", (ejabberd_config:get_myname())/binary>>
end.
get_auto_url(Tls, Module, Host) ->
case find_handler_port_path(Tls, Module) of
[] -> undefined;
TPPs ->
{ThisTls, Port, Path} = case lists:keyfind(true, 1, TPPs) of
false ->
lists:keyfind(false, 1, TPPs);
TPP ->
TPP
end,
Protocol = case ThisTls of
false -> <<"http">>;
true -> <<"https">>
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)).
get_transfer_protocol(PortString) ->
PortNumber = binary_to_integer(PortString),
PortListeners = get_port_listeners(PortNumber),
get_captcha_transfer_protocol(PortListeners).
get_port_listeners(PortNumber) ->
AllListeners = ejabberd_option:listen(),
lists:filter(
fun({{Port, _IP, _Transport}, _Module, _Opts}) ->
Port == PortNumber
end, AllListeners).
get_captcha_transfer_protocol([]) ->
throw(<<"The port number mentioned in captcha_host "
"is not a ejabberd_http listener with "
"'captcha' option. Change the port number "
"or specify http:// in that option.">>);
get_captcha_transfer_protocol([{_, ejabberd_http, Opts} | Listeners]) ->
Handlers = maps:get(request_handlers, Opts, []),
case lists:any(
fun({_, ?MODULE}) -> true;
({_, _}) -> false
end, Handlers) of
true ->
case maps:get(tls, Opts) of
true -> https;
false -> http
end;
false ->
get_captcha_transfer_protocol(Listeners)
end;
get_captcha_transfer_protocol([_ | Listeners]) ->
get_captcha_transfer_protocol(Listeners).
is_limited(undefined) -> false;
is_limited(Limiter) ->
case ejabberd_option:captcha_limit() of
infinity -> false;
Int ->
case catch gen_server:call(?MODULE,
{is_limited, Limiter, Int}, 5000)
of
true -> true;
false -> false;
Err -> ?ERROR_MSG("Call failed: ~p", [Err]), false
end
end.
-define(CMD_TIMEOUT, 5000).
-define(MAX_FILE_SIZE, 64 * 1024).
-spec cmd(string()) -> {ok, binary()} | {error, image_error()}.
cmd(Cmd) ->
Port = open_port({spawn, Cmd}, [stream, eof, binary]),
TRef = erlang:start_timer(?CMD_TIMEOUT, self(),
timeout),
recv_data(Port, TRef, <<>>).
-spec recv_data(port(), reference(), binary()) -> {ok, binary()} | {error, image_error()}.
recv_data(Port, TRef, Buf) ->
receive
{Port, {data, Bytes}} ->
NewBuf = <>,
if byte_size(NewBuf) > (?MAX_FILE_SIZE) ->
return(Port, TRef, {error, efbig});
true -> recv_data(Port, TRef, NewBuf)
end;
{Port, {data, _}} -> return(Port, TRef, {error, efbig});
{Port, eof} when Buf /= <<>> ->
return(Port, TRef, {ok, Buf});
{Port, eof} -> return(Port, TRef, {error, enodata});
{timeout, TRef, _} ->
return(Port, TRef, {error, timeout})
end.
-spec return(port(), reference(), {ok, binary()} | {error, image_error()}) ->
{ok, binary()} | {error, image_error()}.
return(Port, TRef, Result) ->
misc:cancel_timer(TRef),
catch port_close(Port),
Result.
is_feature_available() ->
case get_prog_name() of
PathOrModule when is_binary(PathOrModule) -> true;
false -> false
end.
check_captcha_setup() ->
case is_feature_available() of
true ->
case create_image() of
{ok, _, _, _} ->
true;
Err ->
?CRITICAL_MSG("Captcha is enabled in the option captcha_cmd, "
"but it can't generate images.",
[]),
Err
end;
false ->
false
end.
-spec lookup_captcha(binary()) -> {ok, #captcha{}} | {error, enoent}.
lookup_captcha(Id) ->
case ets:lookup(captcha, Id) of
[C] -> {ok, C};
[] -> {error, enoent}
end.
-spec check_captcha(binary(), binary()) -> captcha_not_found |
captcha_valid |
captcha_non_valid.
check_captcha(Id, ProvidedKey) ->
case lookup_captcha(Id) of
{ok, #captcha{pid = Pid, args = Args, key = ValidKey, tref = Tref}} ->
ets:delete(captcha, Id),
misc:cancel_timer(Tref),
if ValidKey == ProvidedKey ->
callback(captcha_succeed, Pid, Args),
captcha_valid;
true ->
callback(captcha_failed, Pid, Args),
captcha_non_valid
end;
{error, _} ->
captcha_not_found
end.
-spec clean_treap(treap:treap(), priority()) -> treap:treap().
clean_treap(Treap, CleanPriority) ->
case treap:is_empty(Treap) of
true -> Treap;
false ->
{_Key, Priority, _Value} = treap:get_root(Treap),
if Priority > CleanPriority ->
clean_treap(treap:delete_root(Treap), CleanPriority);
true -> Treap
end
end.
-spec callback(captcha_succeed | captcha_failed,
pid() | undefined,
callback() | term()) -> any().
callback(Result, _Pid, F) when is_function(F) ->
F(Result);
callback(Result, Pid, Args) when is_pid(Pid) ->
Pid ! {Result, Args};
callback(_, _, _) ->
ok.
-spec now_priority() -> priority().
now_priority() ->
-erlang:system_time(microsecond).
ejabberd-24.12/src/ejabberd_router_mnesia.erl 0000664 0001750 0001750 00000014773 14730775155 021657 0 ustar debalance debalance %%%-------------------------------------------------------------------
%%% Created : 11 Jan 2017 by Evgeny Khramtsov
%%%
%%%
%%% 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(ejabberd_router_mnesia).
-behaviour(ejabberd_router).
-behaviour(gen_server).
%% API
-export([init/0, register_route/5, unregister_route/3, find_routes/1,
get_all_routes/0, use_cache/0]).
%% gen_server callbacks
-export([init/1, handle_cast/2, handle_call/3, handle_info/2,
terminate/2, code_change/3, start_link/0]).
-include("ejabberd_router.hrl").
-include("logger.hrl").
-include_lib("stdlib/include/ms_transform.hrl").
-record(state, {}).
%%%===================================================================
%%% API
%%%===================================================================
-spec init() -> ok | {error, any()}.
init() ->
Spec = {?MODULE, {?MODULE, start_link, []},
transient, 5000, worker, [?MODULE]},
case supervisor:start_child(ejabberd_backend_sup, Spec) of
{ok, _Pid} -> ok;
Err -> Err
end.
-spec start_link() -> {ok, pid()} | {error, any()}.
start_link() ->
gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
use_cache() ->
false.
register_route(Domain, ServerHost, LocalHint, undefined, Pid) ->
F = fun () ->
mnesia:write(#route{domain = Domain,
pid = Pid,
server_host = ServerHost,
local_hint = LocalHint})
end,
transaction(F);
register_route(Domain, ServerHost, _LocalHint, N, Pid) ->
F = fun () ->
case mnesia:wread({route, Domain}) of
[] ->
mnesia:write(#route{domain = Domain,
server_host = ServerHost,
pid = Pid,
local_hint = 1}),
lists:foreach(
fun (I) ->
mnesia:write(
#route{domain = Domain,
pid = undefined,
server_host = ServerHost,
local_hint = I})
end,
lists:seq(2, N));
Rs ->
lists:any(
fun (#route{pid = undefined,
local_hint = I} = R) ->
mnesia:write(
#route{domain = Domain,
pid = Pid,
server_host = ServerHost,
local_hint = I}),
mnesia:delete_object(R),
true;
(_) -> false
end,
Rs)
end
end,
transaction(F).
unregister_route(Domain, undefined, Pid) ->
F = fun () ->
case mnesia:select(
route,
ets:fun2ms(
fun(#route{domain = D, pid = P} = R)
when D == Domain, P == Pid -> R
end)) of
[R] -> mnesia:delete_object(R);
_ -> ok
end
end,
transaction(F);
unregister_route(Domain, _, Pid) ->
F = fun () ->
case mnesia:select(
route,
ets:fun2ms(
fun(#route{domain = D, pid = P} = R)
when D == Domain, P == Pid -> R
end)) of
[R] ->
I = R#route.local_hint,
ServerHost = R#route.server_host,
mnesia:write(#route{domain = Domain,
server_host = ServerHost,
pid = undefined,
local_hint = I}),
mnesia:delete_object(R);
_ -> ok
end
end,
transaction(F).
find_routes(Domain) ->
{ok, mnesia:dirty_read(route, Domain)}.
get_all_routes() ->
{ok, mnesia:dirty_select(
route,
ets:fun2ms(
fun(#route{domain = Domain, server_host = ServerHost})
when Domain /= ServerHost -> Domain
end))}.
%%%===================================================================
%%% gen_server callbacks
%%%===================================================================
init([]) ->
update_tables(),
ejabberd_mnesia:create(?MODULE, route,
[{ram_copies, [node()]},
{type, bag},
{attributes, record_info(fields, route)}]),
mnesia:subscribe({table, route, simple}),
lists:foreach(
fun (Pid) -> erlang:monitor(process, Pid) end,
mnesia:dirty_select(
route,
ets:fun2ms(
fun(#route{pid = Pid}) -> Pid end))),
{ok, #state{}}.
handle_call(Request, From, State) ->
?WARNING_MSG("Unexpected call from ~p: ~p", [From, Request]),
{noreply, State}.
handle_cast(Msg, State) ->
?WARNING_MSG("Unexpected cast: ~p", [Msg]),
{noreply, State}.
handle_info({mnesia_table_event,
{write, #route{pid = Pid}, _ActivityId}}, State) ->
erlang:monitor(process, Pid),
{noreply, State};
handle_info({mnesia_table_event, _}, State) ->
{noreply, State};
handle_info({'DOWN', _Ref, _Type, Pid, _Info}, State) ->
F = fun () ->
Es = mnesia:select(
route,
ets:fun2ms(
fun(#route{pid = P} = E)
when P == Pid -> E
end)),
lists:foreach(
fun(E) ->
if is_integer(E#route.local_hint) ->
LDomain = E#route.domain,
I = E#route.local_hint,
ServerHost = E#route.server_host,
mnesia:write(#route{domain = LDomain,
server_host = ServerHost,
pid = undefined,
local_hint = I}),
mnesia:delete_object(E);
true ->
mnesia:delete_object(E)
end
end, Es)
end,
transaction(F),
{noreply, State};
handle_info(Info, State) ->
?ERROR_MSG("Unexpected info: ~p", [Info]),
{noreply, State}.
terminate(_Reason, _State) ->
ok.
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
%%%===================================================================
%%% Internal functions
%%%===================================================================
transaction(F) ->
case mnesia:transaction(F) of
{atomic, _} ->
ok;
{aborted, Reason} ->
?ERROR_MSG("Mnesia transaction failed: ~p", [Reason]),
{error, db_failure}
end.
-spec update_tables() -> ok.
update_tables() ->
try
mnesia:transform_table(route, ignore, record_info(fields, route))
catch exit:{aborted, {no_exists, _}} ->
ok
end,
case lists:member(local_route, mnesia:system_info(tables)) of
true -> mnesia:delete_table(local_route);
false -> ok
end.
ejabberd-24.12/src/mod_time.erl 0000664 0001750 0001750 00000005034 14730775155 016750 0 ustar debalance debalance %%%----------------------------------------------------------------------
%%% File : mod_time.erl
%%% Author : Alexey Shchepin
%%% Purpose :
%%% Purpose :
%%% Created : 18 Jan 2003 by Alexey Shchepin
%%%
%%%
%%% 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_time).
-author('alexey@process-one.net').
-protocol({xep, 202, '2.0', '2.1.0', "complete", ""}).
-behaviour(gen_mod).
-export([start/2, stop/1, reload/3, process_local_iq/1,
mod_options/1, depends/2, mod_doc/0]).
-include("logger.hrl").
-include_lib("xmpp/include/xmpp.hrl").
-include("translate.hrl").
start(_Host, _Opts) ->
{ok, [{iq_handler, ejabberd_local, ?NS_TIME, process_local_iq}]}.
stop(_Host) ->
ok.
reload(_Host, _NewOpts, _OldOpts) ->
ok.
-spec process_local_iq(iq()) -> iq().
process_local_iq(#iq{type = set, lang = Lang} = IQ) ->
Txt = ?T("Value 'set' of 'type' attribute is not allowed"),
xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang));
process_local_iq(#iq{type = get} = IQ) ->
Now = os:timestamp(),
Now_universal = calendar:now_to_universal_time(Now),
Now_local = calendar:universal_time_to_local_time(Now_universal),
Seconds_diff =
calendar:datetime_to_gregorian_seconds(Now_local) -
calendar:datetime_to_gregorian_seconds(Now_universal),
{Hd, Md, _} = calendar:seconds_to_time(abs(Seconds_diff)),
xmpp:make_iq_result(IQ, #time{tzo = {Hd, Md}, utc = Now}).
depends(_Host, _Opts) ->
[].
mod_options(_Host) ->
[].
mod_doc() ->
#{desc =>
?T("This module adds support for "
"https://xmpp.org/extensions/xep-0202.html"
"[XEP-0202: Entity Time]. In other words, "
"the module reports server's system time.")}.
ejabberd-24.12/src/ejabberd_options_doc.erl 0000664 0001750 0001750 00000224457 14730775155 021325 0 ustar debalance debalance %%%----------------------------------------------------------------------
%%% 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(ejabberd_options_doc).
%% API
-export([doc/0]).
-include("translate.hrl").
%%%===================================================================
%%% API
%%%===================================================================
doc() ->
[{hosts,
#{value => ?T("[Domain1, Domain2, ...]"),
desc =>
?T("List of one or more "
"_`../configuration/basic.md#host-names|host names`_ "
"(or domains) that 'ejabberd' will serve. This is a "
"**mandatory** option.")}},
{listen,
#{value => "[Options, ...]",
desc =>
?T("The option for listeners configuration. See the "
"_`listen.md|Listen Modules`_ section "
"for details.")}},
{modules,
#{value => "{Module: Options}",
desc =>
?T("Set all the "
"_`modules.md|modules`_ configuration options.")}},
{loglevel,
#{value =>
"none | emergency | alert | critical | "
"error | warning | notice | info | debug",
desc =>
?T("Verbosity of ejabberd "
"_`../configuration/basic.md#logging|logging`_. "
"The default value is 'info'. "
"NOTE: previous versions of ejabberd had log levels "
"defined in numeric format ('0..5'). The numeric values "
"are still accepted for backward compatibility, but "
"are not recommended.")}},
{cache_life_time,
#{value => "timeout()",
desc =>
?T("The time of a cached item to keep in cache. "
"Once it's expired, the corresponding item is "
"erased from cache. The default value is '1 hour'. "
"Several modules have a similar option; and some core "
"ejabberd parts support similar options too, see "
"_`auth_cache_life_time`_, _`oauth_cache_life_time`_, "
"_`router_cache_life_time`_, and _`sm_cache_life_time`_.")}},
{cache_missed,
#{value => "true | false",
desc =>
?T("Whether or not to cache missed lookups. When there is "
"an attempt to lookup for a value in a database and "
"this value is not found and the option is set to 'true', "
"this attempt will be cached and no attempts will be "
"performed until the cache expires (see _`cache_life_time`_). "
"Usually you don't want to change it. Default is 'true'. "
"Several modules have a similar option; and some core "
"ejabberd parts support similar options too, see "
"_`auth_cache_missed`_, _`oauth_cache_missed`_, "
"_`router_cache_missed`_, and _`sm_cache_missed`_.")}},
{cache_size,
#{value => "pos_integer() | infinity",
desc =>
?T("A maximum number of items (not memory!) in cache. "
"The rule of thumb, for all tables except rosters, "
"you should set it to the number of maximum online "
"users you expect. For roster multiply this number "
"by 20 or so. If the cache size reaches this threshold, "
"it's fully cleared, i.e. all items are deleted, and "
"the corresponding warning is logged. You should avoid "
"frequent cache clearance, because this degrades "
"performance. The default value is '1000'. "
"Several modules have a similar option; and some core "
"ejabberd parts support similar options too, see "
"_`auth_cache_size`_, _`oauth_cache_size`_, "
"_`router_cache_size`_, and _`sm_cache_size`_.")}},
{use_cache,
#{value => "true | false",
desc =>
?T("Enable or disable cache. The default is 'true'. "
"Several modules have a similar option; and some core "
"ejabberd parts support similar options too, see "
"_`auth_use_cache`_, _`oauth_use_cache`_, _`router_use_cache`_, "
"and _`sm_use_cache`_.")}},
{default_db,
#{value => "mnesia | sql",
desc =>
?T("_`database.md#default-database|Default database`_ "
"to store persistent data in ejabberd. "
"Modules and other components (e.g. authentication) "
"may have its own value. The default value is 'mnesia'.")}},
{default_ram_db,
#{value => "mnesia | redis | sql",
desc =>
?T("Default volatile (in-memory) storage for ejabberd. "
"Modules and other components (e.g. session management) "
"may have its own value. The default value is 'mnesia'.")}},
{queue_type,
#{value => "ram | file",
desc =>
?T("Default type of queues in ejabberd. "
"Modules may have its own value of the option. "
"The value of 'ram' means that queues will be kept in memory. "
"If value 'file' is set, you may also specify directory "
"in _`queue_dir`_ option where file queues will be placed. "
"The default value is 'ram'.")}},
{version,
#{value => "string()",
desc =>
?T("The option can be used to set custom ejabberd version, "
"that will be used by different parts of ejabberd, for "
"example by _`mod_version`_ module. The default value is "
"obtained at compile time from the underlying version "
"control system.")}},
{acl,
#{value => "{ACLName: {ACLType: ACLValue}}",
desc =>
?T("This option defines "
"_`../configuration/basic.md#acl|access control lists`_: "
"named sets "
"of rules which are used to match against different targets "
"(such as a JID or an IP address). Every set of rules "
"has name 'ACLName': it can be any string except 'all' or 'none' "
"(those are predefined names for the rules that match all or nothing "
"respectively). The name 'ACLName' can be referenced from other "
"parts of the configuration file, for example in _`access_rules`_ "
"option. The rules of 'ACLName' are represented by mapping "
"'pass:[{ACLType: ACLValue}]'. These can be one of the following:")},
[{user,
#{value => ?T("Username"),
desc =>
?T("If 'Username' is in the form of \"user@server\", "
"the rule matches a JID against this value. "
"Otherwise, if 'Username' is in the form of \"user\", "
"the rule matches any JID that has 'Username' in the node part "
"as long as the server part of this JID is any virtual "
"host served by ejabberd.")}},
{server,
#{value => ?T("Server"),
desc =>
?T("The rule matches any JID from server 'Server'. "
"The value of 'Server' must be a valid "
"hostname or an IP address.")}},
{resource,
#{value => ?T("Resource"),
desc =>
?T("The rule matches any JID with a resource 'Resource'.")}},
{ip,
#{value => ?T("Network"),
desc =>
?T("The rule matches any IP address from the 'Network'.")}},
{user_regexp,
#{value => ?T("Regexp"),
desc =>
?T("If 'Regexp' is in the form of \"regexp@server\", the rule "
"matches any JID with node part matching regular expression "
"\"regexp\" as long as the server part of this JID is equal "
"to \"server\". If 'Regexp' is in the form of \"regexp\", the rule "
"matches any JID with node part matching regular expression "
"\"regexp\" as long as the server part of this JID is any virtual "
"host served by ejabberd.")}},
{server_regexp,
#{value => ?T("Regexp"),
desc =>
?T("The rule matches any JID from the server that "
"matches regular expression 'Regexp'.")}},
{resource_regexp,
#{value => ?T("Regexp"),
desc =>
?T("The rule matches any JID with a resource that "
"matches regular expression 'Regexp'.")}},
{node_regexp,
#{value => ?T("user_regexp@server_regexp"),
desc =>
?T("The rule matches any JID with node part matching regular "
"expression 'user_regexp' and server part matching regular "
"expression 'server_regexp'.")}},
{user_glob,
#{value => ?T("Pattern"),
desc =>
?T("Same as 'user_regexp', but matching is performed on a "
"specified 'Pattern' according to the rules used by the "
"Unix shell.")}},
{server_glob,
#{value => ?T("Pattern"),
desc =>
?T("Same as 'server_regexp', but matching is performed on a "
"specified 'Pattern' according to the rules used by the "
"Unix shell.")}},
{resource_glob,
#{value => ?T("Pattern"),
desc =>
?T("Same as 'resource_regexp', but matching is performed on a "
"specified 'Pattern' according to the rules used by the "
"Unix shell.")}},
{node_glob,
#{value => ?T("Pattern"),
desc =>
?T("Same as 'node_regexp', but matching is performed on a "
"specified 'Pattern' according to the rules used by the "
"Unix shell.")}}]},
{access_rules,
#{value => "{AccessName: {allow|deny: ACLRules|ACLName}}",
desc =>
?T("This option defines "
"_`basic.md#access-rules|Access Rules`_. "
"Each access rule is "
"assigned a name that can be referenced from other parts "
"of the configuration file (mostly from 'access' options of "
"ejabberd modules). Each rule definition may contain "
"arbitrary number of 'allow' or 'deny' sections, and each "
"section may contain any number of ACL rules (see _`acl`_ option). "
"There are no access rules defined by default."),
example =>
["access_rules:",
" configure:",
" allow: admin",
" something:",
" deny: someone",
" allow: all",
" s2s_banned:",
" deny: problematic_hosts",
" deny: banned_forever",
" deny:",
" ip: 222.111.222.111/32",
" deny:",
" ip: 111.222.111.222/32",
" allow: all",
" xmlrpc_access:",
" allow:",
" user: peter@example.com",
" allow:",
" user: ivone@example.com",
" allow:",
" user: bot@example.com",
" ip: 10.0.0.0/24"]}},
{acme,
#{value => ?T("Options"),
desc =>
?T("_`basic.md#acme|ACME`_ configuration, to automatically "
"obtain SSL certificates for the domains served by ejabberd, "
"which means that certificate requests and renewals are "
"performed to some CA server (aka \"ACME server\") in a fully "
"automated mode. The 'Options' are:"),
example =>
["acme:",
" ca_url: https://acme-v02.api.letsencrypt.org/directory",
" contact:",
" - mailto:admin@domain.tld",
" - mailto:bot@domain.tld",
" auto: true",
" cert_type: rsa"]},
[{ca_url,
#{value => ?T("URL"),
desc =>
?T("The ACME directory URL used as an entry point "
"for the ACME server. The default value is "
" - "
"the directory URL of Let's Encrypt authority.")}},
{contact,
#{value => ?T("[Contact, ...]"),
desc =>
?T("A list of contact addresses (typically emails) "
"where an ACME server will send notifications "
"when problems occur. The value of 'Contact' must "
"be in the form of \"scheme:address\" (e.g. "
"\"mailto:user@domain.tld\"). The default "
"is an empty list which means an ACME server "
"will send no notices.")}},
{auto,
#{value => "true | false",
desc =>
?T("Whether to automatically request certificates for "
"all configured domains (that yet have no a certificate) "
"on server start or configuration reload. The default is 'true'.")}},
{cert_type,
#{value => "rsa | ec",
desc =>
?T("A type of a certificate key. Available values are "
"'ec' and 'rsa' for EC and RSA certificates respectively. "
"It's better to have RSA certificates for the purpose "
"of backward compatibility with legacy clients and servers, "
"thus the default is 'rsa'.")}}]},
{allow_contrib_modules,
#{value => "true | false",
desc =>
?T("Whether to allow installation of third-party modules or not. "
"See _`../../developer/extending-ejabberd/modules.md#ejabberd-contrib|ejabberd-contrib`_ "
"documentation section. "
"The default value is 'true'.")}},
{allow_multiple_connections,
#{value => "true | false",
desc =>
?T("This option is only used when the anonymous mode is enabled. "
"Setting it to 'true' means that the same username can be "
"taken multiple times in anonymous login mode if different "
"resource are used to connect. This option is only useful "
"in very special occasions. The default value is 'false'.")}},
{anonymous_protocol,
#{value => "login_anon | sasl_anon | both",
desc =>
[?T("Define what "
"_`authentication.md#anonymous-login-and-sasl-anonymous|anonymous`_ "
"protocol will be used: "), "",
?T("* 'login_anon' means that the anonymous login method will be used. "), "",
?T("* 'sasl_anon' means that the SASL Anonymous method will be used. "), "",
?T("* 'both' means that SASL Anonymous and login anonymous are both "
"enabled."), "",
?T("The default value is 'sasl_anon'."), ""]}},
{api_permissions,
#{value => "[Permission, ...]",
desc =>
?T("Define the permissions for API access. Please consult the "
"ejabberd Docs web -> For Developers -> ejabberd ReST API -> "
"_`../../developer/ejabberd-api/permissions.md|API Permissions`_.")}},
{append_host_config,
#{value => "{Host: Options}",
desc =>
?T("Add a few specific options to a certain "
"_`../configuration/basic.md#virtual-hosting|virtual host`_.")}},
{auth_cache_life_time,
#{value => "timeout()",
desc =>
?T("Same as _`cache_life_time`_, but applied to authentication cache "
"only. If not set, the value from _`cache_life_time`_ will be used.")}},
{auth_cache_missed,
#{value => "true | false",
desc =>
?T("Same as _`cache_missed`_, but applied to authentication cache "
"only. If not set, the value from _`cache_missed`_ will be used.")}},
{auth_cache_size,
#{value => "pos_integer() | infinity",
desc =>
?T("Same as _`cache_size`_, but applied to authentication cache "
"only. If not set, the value from _`cache_size`_ will be used.")}},
{auth_method,
#{value => "[mnesia | sql | anonymous | external | jwt | ldap | pam, ...]",
desc =>
?T("A list of _`authentication.md|authentication`_ methods to use. "
"If several methods are defined, authentication is "
"considered successful as long as authentication of "
"at least one of the methods succeeds. "
"The default value is '[mnesia]'.")}},
{auth_opts,
#{value => "[Option, ...]",
desc =>
?T("This is used by the contributed module "
"'ejabberd_auth_http' that can be installed from the "
"https://github.com/processone/ejabberd-contrib[ejabberd-contrib] "
"Git repository. Please refer to that "
"module's README file for details.")}},
{auth_password_format,
#{value => "plain | scram",
note => "improved in 20.01",
desc =>
[?T("The option defines in what format the users passwords "
"are stored, plain text or in _`authentication.md#scram|SCRAM`_ format:"), "",
?T("* 'plain': The password is stored as plain text "
"in the database. This is risky because the passwords "
"can be read if your database gets compromised. "
"This is the default value. This format allows clients to "
"authenticate using: the old Jabber Non-SASL (XEP-0078), "
"SASL PLAIN, SASL DIGEST-MD5, and SASL SCRAM-SHA-1/256/512(-PLUS). "), "",
?T("* 'scram': The password is not stored, only some information "
"required to verify the hash provided by the client. "
"It is impossible to obtain the original plain password "
"from the stored information; for this reason, when this "
"value is configured it cannot be changed to plain anymore. "
"This format allows clients to authenticate using: "
"SASL PLAIN and SASL SCRAM-SHA-1/256/512(-PLUS). The SCRAM variant "
"depends on the _`auth_scram_hash`_ option."), "",
?T("The default value is 'plain'."), ""]}},
{auth_scram_hash,
#{value => "sha | sha256 | sha512",
desc =>
?T("Hash algorithm that should be used to store password in _`authentication.md#scram|SCRAM`_ format. "
"You shouldn't change this if you already have passwords generated with "
"a different algorithm - users that have such passwords will not be able "
"to authenticate. The default value is 'sha'.")}},
{auth_external_user_exists_check,
#{value => "true | false",
note => "added in 23.10",
desc =>
?T("Supplement check for user existence based on _`mod_last`_ data, for authentication "
"methods that don't have a way to reliably tell if a user exists (like is the case for "
"'jwt' and certificate based authentication). This helps with processing offline message "
"for those users. The default value is 'true'.")}},
{auth_use_cache,
#{value => "true | false",
desc =>
?T("Same as _`use_cache`_, but applied to authentication cache "
"only. If not set, the value from _`use_cache`_ will be used.")}},
{c2s_cafile,
#{value => ?T("Path"),
desc =>
[?T("Full path to a file containing one or more CA certificates "
"in PEM format. All client certificates should be signed by "
"one of these root CA certificates and should contain the "
"corresponding JID(s) in 'subjectAltName' field. "
"There is no default value."), "",
?T("You can use _`host_config`_ to specify this option per-vhost."), "",
?T("To set a specific file per listener, use the listener's _`listen-options.md#cafile|cafile`_ option. Please notice that 'c2s_cafile' overrides the listener's 'cafile' option."), ""
]}},
{c2s_ciphers,
#{value => "[Cipher, ...]",
desc =>
?T("A list of OpenSSL ciphers to use for c2s connections. "
"The default value is shown in the example below:"),
example =>
["c2s_ciphers:",
" - HIGH",
" - \"!aNULL\"",
" - \"!eNULL\"",
" - \"!3DES\"",
" - \"@STRENGTH\""]}},
{c2s_dhfile,
#{value => ?T("Path"),
desc =>
?T("Full path to a file containing custom DH parameters "
"to use for c2s connections. "
"Such a file could be created with the command '\"openssl "
"dhparam -out dh.pem 2048\"'. If this option is not specified, "
"2048-bit MODP Group with 256-bit Prime Order Subgroup will be "
"used as defined in RFC5114 Section 2.3.")}},
{c2s_protocol_options,
#{value => "[Option, ...]",
desc =>
?T("List of general SSL options to use for c2s connections. "
"These map to OpenSSL's 'set_options()'. The default value is "
"shown in the example below:"),
example =>
["c2s_protocol_options:",
" - no_sslv3",
" - cipher_server_preference",
" - no_compression"]}},
{c2s_tls_compression,
#{value => "true | false",
desc =>
?T("Whether to enable or disable TLS compression for c2s connections. "
"The default value is 'false'.")}},
{ca_file,
#{value => ?T("Path"),
desc =>
[?T("Path to a file of CA root certificates. "
"The default is to use system defined file if possible."), "",
?T("For server connections, this 'ca_file' option is overridden by the _`s2s_cafile`_ option."), ""
]}},
{captcha_cmd,
#{value => ?T("Path | ModuleName"),
note => "improved in 23.01",
desc =>
?T("Full path to a script that generates _`basic.md#captcha|CAPTCHA`_ images. "
"'@VERSION@' is replaced with ejabberd version number in 'XX.YY' format. "
"'@SEMVER@' is replaced with ejabberd version number in semver format "
"when compiled with Elixir's mix, or XX.YY format otherwise. "
"Alternatively, it can be the name of a module that implements ejabberd CAPTCHA support. "
"There is no default value: when this option is not "
"set, CAPTCHA functionality is completely disabled."),
example =>
[{?T("When using the ejabberd installers or container image, the example captcha scripts can be used like this:"),
["captcha_cmd: /opt/ejabberd-@VERSION@/lib/ejabberd-@SEMVER@/priv/bin/captcha.sh"]}]}},
{captcha_limit,
#{value => "pos_integer() | infinity",
desc =>
?T("Maximum number of _`basic.md#captcha|CAPTCHA`_ generated images per minute for "
"any given JID. The option is intended to protect the server "
"from CAPTCHA DoS. The default value is 'infinity'.")}},
{captcha_host,
#{value => "String",
desc => ?T("Deprecated. Use _`captcha_url`_ instead.")}},
{captcha_url,
#{value => ?T("URL | auto | undefined"),
note => "improved in 23.04",
desc =>
?T("An URL where _`basic.md#captcha|CAPTCHA`_ requests should be sent. NOTE: you need "
"to configure 'request_handlers' for 'ejabberd_http' listener "
"as well. "
"If set to 'auto', it builds the URL using a 'request_handler' "
"already enabled, with encryption if available. "
"If set to 'undefined', it builds the URL using "
"the deprecated _`captcha_host`_ '+ /captcha'. "
"The default value is 'auto'.")}},
{certfiles,
#{value => "[Path, ...]",
desc =>
?T("The option accepts a list of file paths (optionally with "
"wildcards) containing either PEM certificates or PEM private "
"keys. At startup or configuration reload, ejabberd reads all "
"certificates from these files, sorts them, removes duplicates, "
"finds matching private keys and then rebuilds full certificate "
"chains for the use in TLS connections. "
"Use this option when TLS is enabled in either of "
"ejabberd listeners: 'ejabberd_c2s', 'ejabberd_http' and so on. "
"NOTE: if you modify the certificate files or change the value "
"of the option, run 'ejabberdctl reload-config' in order to "
"rebuild and reload the certificate chains."),
example =>
[{?T("If you use https://letsencrypt.org[Let's Encrypt] certificates "
"for your domain \"domain.tld\", the configuration will look "
"like this:"),
["certfiles:",
" - /etc/letsencrypt/live/domain.tld/fullchain.pem",
" - /etc/letsencrypt/live/domain.tld/privkey.pem"]}]}},
{cluster_backend,
#{value => ?T("Backend"),
desc =>
?T("A database backend to use for storing information about "
"cluster. The only available value so far is 'mnesia'.")}},
{cluster_nodes,
#{value => "[Node, ...]",
desc =>
?T("A list of Erlang nodes to connect on ejabberd startup. "
"This option is mostly intended for ejabberd customization "
"and sophisticated setups. The default value is an empty list.")}},
{define_macro,
#{value => "{MacroName: MacroValue}",
desc =>
?T("Defines a "
"_`../configuration/file-format.md#macros-in-configuration-file|macro`_. "
"The value can be any valid arbitrary "
"YAML value. For convenience, it's recommended to define "
"a 'MacroName' in capital letters. Duplicated macros are not allowed. "
"Macros are processed after additional configuration files have "
"been included, so it is possible to use macros that are defined "
"in configuration files included before the usage. "
"It is possible to use a 'MacroValue' in the definition of another macro."),
example =>
["define_macro:",
" DEBUG: debug",
" LOG_LEVEL: DEBUG",
" USERBOB:",
" user: bob@localhost",
"",
"loglevel: LOG_LEVEL",
"",
"acl:",
" admin: USERBOB"]}},
{disable_sasl_scram_downgrade_protection,
#{value => "true | false",
desc =>
?T("Allows to disable sending data required by "
"'XEP-0474: SASL SCRAM Downgrade Protection'. "
"There are known buggy clients (like those that use strophejs 1.6.2) "
"which will not be able to authenticatate when servers sends data from "
"that specification. This options allows server to disable it to allow "
"even buggy clients connects, but in exchange decrease MITM protection. "
"The default value of this option is 'false' which enables this extension.")}},
{disable_sasl_mechanisms,
#{value => "[Mechanism, ...]",
desc =>
?T("Specify a list of SASL mechanisms (such as 'DIGEST-MD5' or "
"'SCRAM-SHA1') that should not be offered to the client. "
"For convenience, the value of 'Mechanism' is case-insensitive. "
"The default value is an empty list, i.e. no mechanisms "
"are disabled by default.")}},
{domain_balancing,
#{value => "{Domain: Options}",
desc =>
?T("An algorithm to "
"_`../guide/clustering.md#service-load-balancing|load-balance`_ "
"the components that are plugged "
"on an ejabberd cluster. It means that you can plug one or several "
"instances of the same component on each ejabberd node and that "
"the traffic will be automatically distributed. The algorithm "
"to deliver messages to the component(s) can be specified by "
"this option. For any component connected as 'Domain', available "
"'Options' are:"),
example =>
["domain_balancing:",
" component.domain.tld:",
" type: destination",
" component_number: 5",
" transport.example.org:",
" type: bare_source"]},
[{type,
#{value => ?T("Value"),
desc =>
?T("How to deliver stanzas to connected components. "
"The default value is 'random'. Possible values: ")},
[{'- random',
#{desc =>
?T("an instance is chosen at random")}},
{'- source',
#{desc =>
?T("by the full JID of the packet's 'from' attribute")}},
{'- bare_destination',
#{desc =>
?T("by the bare JID (without resource) of the packet's 'to' attribute")}},
{'- bare_source',
#{desc =>
?T("by the bare JID (without resource) of the packet's 'from' attribute is used")}},
{'- destination',
#{desc =>
?T("an instance is chosen by the full JID of the packet's 'to' attribute")}}]},
{component_number,
#{value => "2..1000",
desc =>
?T("The number of components to balance.")}}]},
{extauth_pool_name,
#{value => ?T("Name"),
desc =>
?T("Define the pool name appendix in "
"_`authentication.md#external-script|external auth`_, "
"so the full pool name will be "
"'extauth_pool_Name'. The default value is the hostname.")}},
{extauth_pool_size,
#{value => ?T("Size"),
desc =>
?T("The option defines the number of instances of the same "
"_`authentication.md#external-script|external auth`_ "
"program to start for better load balancing. "
"The default is the number of available CPU cores.")}},
{extauth_program,
#{value => ?T("Path"),
desc =>
?T("Indicate in this option the full path to the "
"_`authentication.md#external-script|external authentication script`_. "
"The script must be executable by ejabberd.")}},
{ext_api_headers,
#{value => "Headers",
desc =>
?T("String of headers (separated with commas ',') that will be "
"provided by ejabberd when sending ReST requests. "
"The default value is an empty string of headers: '\"\"'.")}},
{ext_api_http_pool_size,
#{value => "pos_integer()",
desc =>
?T("Define the size of the HTTP pool, that is, the maximum number "
"of sessions that the ejabberd ReST service will handle "
"simultaneously. The default value is: '100'.")}},
{ext_api_path_oauth,
#{value => "Path",
desc =>
?T("Define the base URI path when performing OAUTH ReST requests. "
"The default value is: '\"/oauth\"'.")}},
{ext_api_url,
#{value => "URL",
desc =>
?T("Define the base URI when performing ReST requests. "
"The default value is: '\"http://localhost/api\"'.")}},
{fqdn,
#{value => ?T("Domain"),
desc =>
?T("A fully qualified domain name that will be used in "
"SASL DIGEST-MD5 authentication. The default is detected "
"automatically.")}},
{hide_sensitive_log_data,
#{value => "true | false",
desc =>
?T("A privacy option to not log sensitive data "
"(mostly IP addresses). The default value "
"is 'false' for backward compatibility.")}},
{host_config,
#{value => "{Host: Options}",
desc =>
?T("The option is used to redefine 'Options' for "
"_`../configuration/basic.md#virtual-hosting|virtual host`_ "
"'Host'. "
"In the example below LDAP authentication method "
"will be used on virtual host 'domain.tld' and SQL method "
"will be used on virtual host 'example.org'."),
example =>
["hosts:",
" - domain.tld",
" - example.org",
"",
"auth_method:",
" - sql",
"",
"host_config:",
" domain.tld:",
" auth_method:",
" - ldap"]}},
{include_config_file,
#{value => "[Filename, ...\\] | {Filename: Options}",
desc =>
?T("Read and "
"_`../configuration/file-format.md#include-additional-files|include additional file`_ "
"from 'Filename'. If the "
"value is provided in 'pass:[{Filename: Options}]' format, the "
"'Options' must be one of the following:")},
[{disallow,
#{value => "[OptionName, ...]",
desc =>
?T("Disallows the usage of those options in the included "
"file 'Filename'. The options that match this criteria "
"are not accepted. The default value is an empty list.")}},
{allow_only,
#{value => "[OptionName, ...]",
desc =>
?T("Allows only the usage of those options in the included "
"file 'Filename'. The options that do not match this "
"criteria are not accepted. The default value is to include "
"all options.")}}]},
{install_contrib_modules,
#{value => "[Module, ...]",
note => "added in 23.10",
desc =>
?T("Modules to install from "
"_`../../developer/extending-ejabberd/modules.md#ejabberd-contrib|ejabberd-contrib`_ "
"at start time. "
"The default value is an empty list of modules: '[]'.")}},
{jwt_auth_only_rule,
#{value => ?T("AccessName"),
desc =>
?T("This ACL rule defines accounts that can use only the "
"_`authentication.md#jwt-authentication|JWT`_ auth "
"method, even if others are also defined in the ejabberd "
"configuration file. In other words: if there are several auth "
"methods enabled for this host (JWT, SQL, ...), users that "
"match this rule can only use JWT. "
"The default value is 'none'.")}},
{jwt_jid_field,
#{value => ?T("FieldName"),
desc =>
?T("By default, the JID is defined in the '\"jid\"' JWT field. "
"In this option you can specify other "
"_`authentication.md#jwt-authentication|JWT`_ "
"field name "
"where the JID is defined.")}},
{jwt_key,
#{value => ?T("FilePath"),
desc =>
?T("Path to the file that contains the "
"_`authentication.md#jwt-authentication|JWT`_ key. "
"The default value is 'undefined'.")}},
{language,
#{value => ?T("Language"),
desc =>
?T("Define the "
"_`../configuration/basic.md#default-language|default language`_ "
"of server strings "
"that can be seen by XMPP clients. If an XMPP client does not "
"possess 'xml:lang' attribute, the specified language is used. "
"The default value is '\"en\"'. ")}},
{ldap_servers,
#{value => "[Host, ...]",
desc =>
?T("A list of IP addresses or DNS names of your LDAP servers (see "
"_`../configuration/ldap.md#ldap-connection|LDAP connection`_). "
"ejabberd connects immediately to all of them, "
"and reconnects infinitely if connection is lost. "
"The default value is '[localhost]'.")}},
{ldap_backups,
#{value => "[Host, ...]",
desc =>
?T("A list of IP addresses or DNS names of LDAP backup servers (see "
"_`../configuration/ldap.md#ldap-connection|LDAP connection`_). "
"When no servers listed in _`ldap_servers`_ option are reachable, "
"ejabberd connects to these backup servers. "
"The default is an empty list, i.e. no backup servers specified. "
"Please notice that ejabberd only connects to the next server "
"when the existing connection is lost; it doesn't detect when a "
"previously-attempted server becomes available again.")}},
{ldap_encrypt,
#{value => "tls | none",
desc =>
?T("Whether to encrypt LDAP connection using TLS or not. "
"The default value is 'none'. NOTE: STARTTLS encryption "
"is not supported.")}},
{ldap_tls_certfile,
#{value => ?T("Path"),
desc =>
?T("A path to a file containing PEM encoded certificate "
"along with PEM encoded private key. This certificate "
"will be provided by ejabberd when TLS enabled for "
"LDAP connections. There is no default value, which means "
"no client certificate will be sent.")}},
{ldap_tls_verify,
#{value => "false | soft | hard",
desc =>
?T("This option specifies whether to verify LDAP server "
"certificate or not when TLS is enabled. When 'hard' is set, "
"ejabberd doesn't proceed if the certificate is invalid. "
"When 'soft' is set, ejabberd proceeds even if the check has failed. "
"The default is 'false', which means no checks are performed.")}},
{ldap_tls_cacertfile,
#{value => ?T("Path"),
desc =>
?T("A path to a file containing PEM encoded CA certificates. "
"This option is required when TLS verification is enabled.")}},
{ldap_tls_depth,
#{value => ?T("Number"),
desc =>
?T("Specifies the maximum verification depth when TLS verification "
"is enabled, i.e. how far in a chain of certificates the "
"verification process can proceed before the verification "
"is considered to be failed. Peer certificate = 0, "
"CA certificate = 1, higher level CA certificate = 2, etc. "
"The value '2' thus means that a chain can at most contain "
"peer cert, CA cert, next CA cert, and an additional CA cert. "
"The default value is '1'.")}},
{ldap_port,
#{value => "1..65535",
desc =>
?T("Port to connect to your LDAP server. The default port is "
"'389' if encryption is disabled and '636' if encryption is "
"enabled.")}},
{ldap_rootdn,
#{value => "RootDN",
desc =>
?T("Bind Distinguished Name. The default value is an empty "
"string, which means \"anonymous connection\".")}},
{ldap_password,
#{value => ?T("Password"),
desc =>
?T("Bind password. The default value is an empty string.")}},
{ldap_deref_aliases,
#{value => "never | always | finding | searching",
desc =>
?T("Whether to dereference aliases or not. "
"The default value is 'never'.")}},
{ldap_base,
#{value => "Base",
desc =>
?T("LDAP base directory which stores users accounts. "
"There is no default value: you must set the option "
"in order for LDAP connections to work properly.")}},
{ldap_uids,
#{value => "[Attr\\] | {Attr: AttrFormat}",
desc =>
?T("LDAP attributes which hold a list of attributes to use "
"as alternatives for getting the JID, where 'Attr' is "
"an LDAP attribute which holds the user's part of the JID and "
"'AttrFormat' must contain one and only one pattern variable "
"\"%u\" which will be replaced by the user's part of the JID. "
"For example, \"%u@example.org\". If the value is in the form "
"of '[Attr]' then 'AttrFormat' is assumed to be \"%u\".")}},
{ldap_filter,
#{value => ?T("Filter"),
desc =>
?T("An LDAP filter as defined in "
"https://tools.ietf.org/html/rfc4515[RFC4515]. "
"There is no default value. Example: "
"\"(&(objectClass=shadowAccount)(memberOf=XMPP Users))\". "
"NOTE: don't forget to close brackets and don't use superfluous "
"whitespaces. Also you must not use \"uid\" attribute in the "
"filter because this attribute will be appended to the filter "
"automatically.")}},
{ldap_dn_filter,
#{value => "{Filter: FilterAttrs}",
desc =>
?T("This filter is applied on the results returned by the main "
"filter. The filter performs an additional LDAP lookup to make "
"the complete result. This is useful when you are unable to "
"define all filter rules in 'ldap_filter'. You can define "
"\"%u\", \"%d\", \"%s\" and \"%D\" pattern variables in 'Filter': "
"\"%u\" is replaced by a user's part of the JID, \"%d\" is "
"replaced by the corresponding domain (virtual host), all \"%s\" "
"variables are consecutively replaced by values from the attributes "
"in 'FilterAttrs' and \"%D\" is replaced by Distinguished Name from "
"the result set. There is no default value, which means the "
"result is not filtered. WARNING: Since this filter makes "
"additional LDAP lookups, use it only as the last resort: "
"try to define all filter rules in _`ldap_filter`_ option if possible."),
example =>
["ldap_dn_filter:",
" \"(&(name=%s)(owner=%D)(user=%u@%d))\": [sn]"]}},
{log_rotate_count,
#{value => ?T("Number"),
desc =>
?T("The number of rotated log files to keep. "
"The default value is '1', which means that only keeps "
"`ejabberd.log.0`, `error.log.0` and `crash.log.0`.")}},
{log_rotate_size,
#{value => "pos_integer() | infinity",
desc =>
?T("The size (in bytes) of a log file to trigger rotation. "
"If set to 'infinity', log rotation is disabled. "
"The default value is 10 Mb expressed in bytes: '10485760'.")}},
{log_burst_limit_count,
#{value => ?T("Number"),
note => "added in 22.10",
desc =>
?T("The number of messages to accept in "
"`log_burst_limit_window_time` period before starting to "
"drop them. Default `500`")}},
{log_burst_limit_window_time,
#{value => ?T("Number"),
note => "added in 22.10",
desc =>
?T("The time period to rate-limit log messages "
"by. Defaults to `1` second.")}},
{log_modules_fully,
#{value => "[Module, ...]",
note => "added in 23.01",
desc =>
?T("List of modules that will log everything "
"independently from the general loglevel option.")}},
{max_fsm_queue,
#{value => ?T("Size"),
desc =>
?T("This option specifies the maximum number of elements "
"in the queue of the FSM (Finite State Machine). Roughly "
"speaking, each message in such queues represents one "
"XML stanza queued to be sent into its relevant outgoing "
"stream. If queue size reaches the limit (because, for "
"example, the receiver of stanzas is too slow), the FSM "
"and the corresponding connection (if any) will be terminated "
"and error message will be logged. The reasonable value for "
"this option depends on your hardware configuration. "
"The allowed values are positive integers. "
"The default value is '10000'.")}},
{negotiation_timeout,
#{value => "timeout()",
desc =>
?T("Time to wait for an XMPP stream negotiation to complete. "
"When timeout occurs, the corresponding XMPP stream is closed. "
"The default value is '120' seconds.")}},
{net_ticktime,
#{value => "timeout()",
desc =>
?T("This option can be used to tune tick time parameter of "
"'net_kernel'. It tells Erlang VM how often nodes should check "
"if intra-node communication was not interrupted. This option "
"must have identical value on all nodes, or it will lead to subtle "
"bugs. Usually leaving default value of this is option is best, "
"tweak it only if you know what you are doing. "
"The default value is '1 minute'.")}},
{new_sql_schema,
#{value => "true | false",
desc =>
{?T("Whether to use the "
"_`database.md#default-and-new-schemas|new SQL schema`_. "
"All schemas are located "
"at . "
"There are two schemas available. The default legacy schema "
"stores one XMPP domain into one ejabberd database. "
"The 'new' schema can handle several XMPP domains in a "
"single ejabberd database. Using this 'new' schema is best when "
"serving several XMPP domains and/or changing domains from "
"time to time. This avoid need to manage several databases and "
"handle complex configuration changes. The default depends on "
"configuration flag '--enable-new-sql-schema' which is set "
"at compile time."),
[binary:part(ejabberd_config:version(), {0,5})]}}},
{update_sql_schema,
#{value => "true | false",
note => "updated in 24.06",
desc =>
?T("Allow ejabberd to update SQL schema in "
"MySQL, PostgreSQL and SQLite databases. "
"This option was added in ejabberd 23.10, "
"and enabled by default since 24.06. "
"The default value is 'true'.")}},
{update_sql_schema_timeout,
#{value => "timeout()",
note => "added in 24.07",
desc =>
?T("Time allocated to SQL schema update queries. "
"The default value is set to 5 minutes.")}},
{oauth_access,
#{value => ?T("AccessName"),
desc => ?T("By default creating OAuth tokens is not allowed. "
"To define which users can create OAuth tokens, "
"you can refer to an ejabberd access rule in the "
"'oauth_access' option. Use 'all' to allow everyone "
"to create tokens.")}},
{oauth_cache_life_time,
#{value => "timeout()",
desc =>
?T("Same as _`cache_life_time`_, but applied to OAuth cache "
"only. If not set, the value from _`cache_life_time`_ will be used.")}},
{oauth_cache_missed,
#{value => "true | false",
desc =>
?T("Same as _`cache_missed`_, but applied to OAuth cache "
"only. If not set, the value from _`cache_missed`_ will be used.")}},
{oauth_cache_rest_failure_life_time,
#{value => "timeout()",
note => "added in 21.01",
desc =>
?T("The time that a failure in OAuth ReST is cached. "
"The default value is 'infinity'.")}},
{oauth_cache_size,
#{value => "pos_integer() | infinity",
desc =>
?T("Same as _`cache_size`_, but applied to OAuth cache "
"only. If not set, the value from _`cache_size`_ will be used.")}},
{oauth_client_id_check,
#{value => "allow | db | deny",
desc =>
?T("Define whether the client authentication is always allowed, "
"denied, or it will depend if the client ID is present in the "
"database. The default value is 'allow'.")}},
{oauth_use_cache,
#{value => "true | false",
desc =>
?T("Same as _`use_cache`_, but applied to OAuth cache "
"only. If not set, the value from _`use_cache`_ will be used.")}},
{oauth_db_type,
#{value => "mnesia | sql",
desc =>
?T("Database backend to use for OAuth authentication. "
"The default value is picked from _`default_db`_ option, or "
"if it's not set, 'mnesia' will be used.")}},
{oauth_expire,
#{value => "timeout()",
desc =>
?T("Time during which the OAuth token is valid, in seconds. "
"After that amount of time, the token expires and the delegated "
"credential cannot be used and is removed from the database. "
"The default is '4294967' seconds.")}},
{oom_killer,
#{value => "true | false",
desc =>
?T("Enable or disable OOM (out-of-memory) killer. "
"When system memory raises above the limit defined in "
"_`oom_watermark`_ option, ejabberd triggers OOM killer "
"to terminate most memory consuming Erlang processes. "
"Note that in order to maintain functionality, ejabberd only "
"attempts to kill transient processes, such as those managing "
"client sessions, s2s or database connections. "
"The default value is 'true'.")}},
{oom_queue,
#{value => ?T("Size"),
desc =>
?T("Trigger OOM killer when some of the running Erlang processes "
"have messages queue above this 'Size'. Note that "
"such processes won't be killed if _`oom_killer`_ option is set "
"to 'false' or if _`oom_watermark`_ is not reached yet.")}},
{oom_watermark,
#{value => ?T("Percent"),
desc =>
?T("A percent of total system memory consumed at which "
"OOM killer should be activated with some of the processes "
"possibly be killed (see _`oom_killer`_ option). Later, when "
"memory drops below this 'Percent', OOM killer is deactivated. "
"The default value is '80' percents.")}},
{outgoing_s2s_families,
#{value => "[ipv6 | ipv4, ...]",
note => "changed in 23.01",
desc =>
?T("Specify which address families to try, in what order. "
"The default is '[ipv6, ipv4]' which means it first tries "
"connecting with IPv6, if that fails it tries using IPv4. "
"This option is obsolete and irrelevant when using ejabberd 23.01 "
"and Erlang/OTP 22, or newer versions of them.")}},
{outgoing_s2s_ipv4_address,
#{value => "Address",
note => "added in 20.12",
desc =>
?T("Specify the IPv4 address that will be used when establishing "
"an outgoing S2S IPv4 connection, for example '\"127.0.0.1\"'. "
"The default value is 'undefined'.")}},
{outgoing_s2s_ipv6_address,
#{value => "Address",
note => "added in 20.12",
desc =>
?T("Specify the IPv6 address that will be used when establishing "
"an outgoing S2S IPv6 connection, for example "
"'\"::FFFF:127.0.0.1\"'. The default value is 'undefined'.")}},
{outgoing_s2s_port,
#{value => "1..65535",
desc =>
?T("A port number to use for outgoing s2s connections when the target "
"server doesn't have an SRV record. The default value is '5269'.")}},
{outgoing_s2s_timeout,
#{value => "timeout()",
desc =>
?T("The timeout in seconds for outgoing S2S connection attempts. "
"The default value is '10' seconds.")}},
{pam_service,
#{value => ?T("Name"),
desc =>
?T("This option defines the "
"_`authentication.md#pam-authentication|PAM`_ "
"service name. Refer to the PAM "
"documentation of your operation system for more information. "
"The default value is 'ejabberd'.")}},
{pam_userinfotype,
#{value => "username | jid",
desc =>
?T("This option defines what type of information about the "
"user ejabberd provides to the "
"_`authentication.md#pam-authentication|PAM`_ "
"service: only the username, "
"or the user's JID. Default is 'username'.")}},
{pgsql_users_number_estimate,
#{value => "true | false",
desc =>
?T("Whether to use PostgreSQL estimation when counting registered "
"users. The default value is 'false'.")}},
{queue_dir,
#{value => ?T("Directory"),
desc =>
?T("If _`queue_type`_ option is set to 'file', use this 'Directory' "
"to store file queues. The default is to keep queues inside "
"Mnesia directory.")}},
{redis_connect_timeout,
#{value => "timeout()",
desc =>
?T("A timeout to wait for the connection to be re-established "
"to the _`database.md#redis|Redis`_ "
"server. The default is '1 second'.")}},
{redis_db,
#{value => ?T("Number"),
desc => ?T("_`database.md#redis|Redis`_ "
"database number. The default is '0'.")}},
{redis_password,
#{value => ?T("Password"),
desc =>
?T("The password to the _`database.md#redis|Redis`_ server. "
"The default is an empty string, i.e. no password.")}},
{redis_pool_size,
#{value => ?T("Number"),
desc =>
?T("The number of simultaneous connections to the "
"_`database.md#redis|Redis`_ server. "
"The default value is '10'.")}},
{redis_port,
#{value => "1..65535",
desc =>
?T("The port where the _`database.md#redis|Redis`_ "
" server is accepting connections. "
"The default is '6379'.")}},
{redis_queue_type,
#{value => "ram | file",
desc =>
?T("The type of request queue for the "
"_`database.md#redis|Redis`_ server. "
"See description of _`queue_type`_ option for the explanation. "
"The default value is the value defined in _`queue_type`_ "
"or 'ram' if the latter is not set.")}},
{redis_server,
#{value => "Host | IP Address | Unix Socket Path",
note => "improved in 24.12",
desc =>
?T("A hostname, IP address or unix domain socket file of the "
"_`database.md#redis|Redis`_ server. "
"Setup the path to unix domain socket like: '\"unix:/path/to/socket\"'. "
"The default value is 'localhost'.")}},
{registration_timeout,
#{value => "timeout()",
desc =>
?T("This is a global option for module _`mod_register`_. "
"It limits the frequency of registrations from a given "
"IP or username. So, a user that tries to register a "
"new account from the same IP address or JID during "
"this time after their previous registration "
"will receive an error with the corresponding explanation. "
"To disable this limitation, set the value to 'infinity'. "
"The default value is '600 seconds'.")}},
{resource_conflict,
#{value => "setresource | closeold | closenew",
desc =>
?T("NOTE: this option is deprecated and may be removed "
"anytime in the future versions. The possible values "
"match exactly the three possibilities described in "
"https://tools.ietf.org/html/rfc6120#section-7.7.2.2"
"[XMPP Core: section 7.7.2.2]. "
"The default value is 'closeold'. If the client "
"uses old Jabber Non-SASL authentication (XEP-0078), "
"then this option is not respected, and the action performed "
"is 'closeold'.")}},
{router_cache_life_time,
#{value => "timeout()",
desc =>
?T("Same as _`cache_life_time`_, but applied to routing table cache "
"only. If not set, the value from _`cache_life_time`_ will be used.")}},
{router_cache_missed,
#{value => "true | false",
desc =>
?T("Same as _`cache_missed`_, but applied to routing table cache "
"only. If not set, the value from _`cache_missed`_ will be used.")}},
{router_cache_size,
#{value => "pos_integer() | infinity",
desc =>
?T("Same as _`cache_size`_, but applied to routing table cache "
"only. If not set, the value from _`cache_size`_ will be used.")}},
{router_db_type,
#{value => "mnesia | redis | sql",
desc =>
?T("Database backend to use for routing information. "
"The default value is picked from _`default_ram_db`_ option, or "
"if it's not set, 'mnesia' will be used.")}},
{router_use_cache,
#{value => "true | false",
desc =>
?T("Same as _`use_cache`_, but applied to routing table cache "
"only. If not set, the value from _`use_cache`_ will be used.")}},
{rpc_timeout,
#{value => "timeout()",
desc =>
?T("A timeout for remote function calls between nodes "
"in an ejabberd cluster. You should probably never change "
"this value since those calls are used for internal needs "
"only. The default value is '5' seconds.")}},
{s2s_access,
#{value => ?T("Access"),
desc =>
?T("This _`basic.md#access-rules|Access Rule`_ defines to "
"what remote servers can s2s connections be established. "
"The default value is 'all'; no restrictions are applied, it is"
" allowed to connect s2s to/from all remote servers.")}},
{s2s_cafile,
#{value => ?T("Path"),
desc =>
[?T("A path to a file with CA root certificates that will "
"be used to authenticate s2s connections. If not set, "
"the value of _`ca_file`_ will be used."), "",
?T("You can use _`host_config`_ to specify this option per-vhost."), ""
]}},
{s2s_ciphers,
#{value => "[Cipher, ...]",
desc =>
?T("A list of OpenSSL ciphers to use for s2s connections. "
"The default value is shown in the example below:"),
example =>
["s2s_ciphers:",
" - HIGH",
" - \"!aNULL\"",
" - \"!eNULL\"",
" - \"!3DES\"",
" - \"@STRENGTH\""]}},
{s2s_dhfile,
#{value => ?T("Path"),
desc =>
?T("Full path to a file containing custom DH parameters "
"to use for s2s connections. "
"Such a file could be created with the command '\"openssl "
"dhparam -out dh.pem 2048\"'. If this option is not specified, "
"2048-bit MODP Group with 256-bit Prime Order Subgroup will be "
"used as defined in RFC5114 Section 2.3.")}},
{s2s_protocol_options,
#{value => "[Option, ...]",
desc =>
?T("List of general SSL options to use for s2s connections. "
"These map to OpenSSL's 'set_options()'. The default value is "
"shown in the example below:"),
example =>
["s2s_protocol_options:",
" - no_sslv3",
" - cipher_server_preference",
" - no_compression"]}},
{s2s_tls_compression,
#{value => "true | false",
desc =>
?T("Whether to enable or disable TLS compression for s2s connections. "
"The default value is 'false'.")}},
{s2s_dns_retries,
#{value => ?T("Number"),
desc =>
?T("DNS resolving retries. The default value is '2'.")}},
{s2s_dns_timeout,
#{value => "timeout()",
desc =>
?T("The timeout for DNS resolving. The default value is '10' seconds.")}},
{s2s_max_retry_delay,
#{value => "timeout()",
desc =>
?T("The maximum allowed delay for s2s connection retry to connect after a "
"failed connection attempt. The default value is '300' seconds "
"(5 minutes).")}},
{s2s_queue_type,
#{value => "ram | file",
desc =>
?T("The type of a queue for s2s packets. "
"See description of _`queue_type`_ option for the explanation. "
"The default value is the value defined in _`queue_type`_ "
"or 'ram' if the latter is not set.")}},
{s2s_timeout,
#{value => "timeout()",
desc =>
?T("A time to wait before closing an idle s2s connection. "
"The default value is '1' hour.")}},
{s2s_use_starttls,
#{value => "true | false | optional | required",
desc =>
?T("Whether to use STARTTLS for s2s connections. "
"The value of 'false' means STARTTLS is prohibited. "
"The value of 'true' or 'optional' means STARTTLS is enabled "
"but plain connections are still allowed. And the value of "
"'required' means that only STARTTLS connections are allowed. "
"The default value is 'false' (for historical reasons).")}},
{s2s_zlib,
#{value => "true | false",
desc =>
?T("Whether to use 'zlib' compression (as defined in "
"https://xmpp.org/extensions/xep-0138.html[XEP-0138]) or not. "
"The default value is 'false'. WARNING: this type "
"of compression is nowadays considered insecure.")}},
{shaper,
#{value => "{ShaperName: Rate}",
desc =>
?T("The option defines a set of "
"_`../configuration/basic.md#shapers|shapers`_. "
"Every shaper is assigned "
"a name 'ShaperName' that can be used in other parts of the "
"configuration file, such as _`shaper_rules`_ option. The shaper "
"itself is defined by its 'Rate', where 'Rate' stands for the "
"maximum allowed incoming rate in **bytes** per second. "
"When a connection exceeds this limit, ejabberd stops reading "
"from the socket until the average rate is again below the "
"allowed maximum. In the example below shaper 'normal' limits "
"the traffic speed to 1,000 bytes/sec and shaper 'fast' limits "
"the traffic speed to 50,000 bytes/sec:"),
example =>
["shaper:",
" normal: 1000",
" fast: 50000"]}},
{shaper_rules,
#{value => "{ShaperRuleName: {Number|ShaperName: ACLRule|ACLName}}",
desc =>
?T("This option defines "
"_`../configuration/basic.md#shaper-rules|shaper rules`_ "
"to use for matching user/hosts. "
"Semantics is similar to _`access_rules`_ option, the only difference is "
"that instead using 'allow' or 'deny', a name of a shaper (defined in "
"_`shaper`_ option) or a positive number should be used."),
example =>
["shaper_rules:",
" connections_limit:",
" 10:",
" user: peter@example.com",
" 100: admin",
" 5: all",
" download_speed:",
" fast: admin",
" slow: anonymous_users",
" normal: all",
" log_days: 30"]}},
{sm_cache_life_time,
#{value => "timeout()",
desc =>
?T("Same as _`cache_life_time`_, but applied to client sessions table cache "
"only. If not set, the value from _`cache_life_time`_ will be used.")}},
{sm_cache_missed,
#{value => "true | false",
desc =>
?T("Same as _`cache_missed`_, but applied to client sessions table cache "
"only. If not set, the value from _`cache_missed`_ will be used.")}},
{sm_cache_size,
#{value => "pos_integer() | infinity",
desc =>
?T("Same as _`cache_size`_, but applied to client sessions table cache "
"only. If not set, the value from _`cache_size`_ will be used.")}},
{sm_db_type,
#{value => "mnesia | redis | sql",
desc =>
?T("Database backend to use for client sessions information. "
"The default value is picked from _`default_ram_db`_ option, or "
"if it's not set, 'mnesia' will be used.")}},
{sm_use_cache,
#{value => "true | false",
desc =>
?T("Same as _`use_cache`_, but applied to client sessions table cache "
"only. If not set, the value from _`use_cache`_ will be used.")}},
{sql_type,
#{value => "mssql | mysql | odbc | pgsql | sqlite",
desc =>
?T("The type of an SQL connection. The default is 'odbc'.")}},
{sql_connect_timeout,
#{value => "timeout()",
desc =>
?T("A time to wait for connection to an SQL server to be "
"established. The default value is '5' seconds.")}},
{sql_database,
#{value => ?T("Database"),
desc =>
?T("An SQL database name. For SQLite this must be a full "
"path to a database file. The default value is 'ejabberd'.")}},
{sql_keepalive_interval,
#{value => "timeout()",
desc =>
?T("An interval to make a dummy SQL request to keep alive the "
"connections to the database. There is no default value, so no "
"keepalive requests are made.")}},
{sql_odbc_driver,
#{value => "Path",
note => "added in 20.12",
desc =>
?T("Path to the ODBC driver to use to connect to a Microsoft SQL "
"Server database. This option only applies if the _`sql_type`_ "
"option is set to 'mssql' and _`sql_server`_ is not an ODBC "
"connection string. The default value is: 'libtdsodbc.so'")}},
{sql_password,
#{value => ?T("Password"),
desc =>
?T("The password for SQL authentication. The default is empty string.")}},
{sql_pool_size,
#{value => ?T("Size"),
desc =>
?T("Number of connections to the SQL server that ejabberd will "
"open for each virtual host. The default value is '10'. WARNING: "
"for SQLite this value is '1' by default and it's not recommended "
"to change it due to potential race conditions.")}},
{sql_port,
#{value => "1..65535",
desc =>
?T("The port where the SQL server is accepting connections. "
"The default is '3306' for MySQL, '5432' for PostgreSQL and "
"'1433' for MS SQL. The option has no effect for SQLite.")}},
{sql_prepared_statements,
#{value => "true | false",
note => "added in 20.01",
desc =>
?T("This option is 'true' by default, and is useful to disable "
"prepared statements. The option is valid for PostgreSQL and MySQL.")}},
{sql_flags,
#{value => "[mysql_alternative_upsert]",
note => "added in 24.02",
desc =>
?T("This option accepts a list of SQL flags, and is empty by default. "
"'mysql_alternative_upsert' forces the alternative upsert implementation in MySQL.")}},
{sql_query_timeout,
#{value => "timeout()",
desc =>
?T("A time to wait for an SQL query response. "
"The default value is '60' seconds.")}},
{sql_queue_type,
#{value => "ram | file",
desc =>
?T("The type of a request queue for the SQL server. "
"See description of _`queue_type`_ option for the explanation. "
"The default value is the value defined in _`queue_type`_ "
"or 'ram' if the latter is not set.")}},
{sql_server,
#{value => "Host | IP Address | ODBC Connection String | Unix Socket Path",
note => "improved in 24.06",
desc =>
?T("The hostname or IP address of the SQL server. For _`sql_type`_ "
"'mssql' or 'odbc' this can also be an ODBC connection string. "
"When _`sql_type`_ is 'mysql' or 'pgsql', this can be the path to "
"a unix domain socket expressed like: '\"unix:/path/to/socket\"'."
"The default value is 'localhost'.")}},
{sql_ssl,
#{value => "true | false",
note => "improved in 20.03",
desc =>
?T("Whether to use SSL encrypted connections to the "
"SQL server. The option is only available for MySQL, MS SQL and "
"PostgreSQL. The default value is 'false'.")}},
{sql_ssl_cafile,
#{value => ?T("Path"),
desc =>
?T("A path to a file with CA root certificates that will "
"be used to verify SQL connections. Implies _`sql_ssl`_ "
"and _`sql_ssl_verify`_ options are set to 'true'. "
"There is no default which means "
"certificate verification is disabled. "
"This option has no effect for MS SQL.")}},
{sql_ssl_certfile,
#{value => ?T("Path"),
desc =>
?T("A path to a certificate file that will be used "
"for SSL connections to the SQL server. Implies _`sql_ssl`_ "
"option is set to 'true'. There is no default which means "
"ejabberd won't provide a client certificate to the SQL "
"server. "
"This option has no effect for MS SQL.")}},
{sql_ssl_verify,
#{value => "true | false",
desc =>
?T("Whether to verify SSL connection to the SQL server against "
"CA root certificates defined in _`sql_ssl_cafile`_ option. "
"Implies _`sql_ssl`_ option is set to 'true'. "
"This option has no effect for MS SQL. "
"The default value is 'false'.")}},
{sql_start_interval,
#{value => "timeout()",
desc =>
?T("A time to wait before retrying to restore failed SQL connection. "
"The default value is '30' seconds.")}},
{sql_username,
#{value => ?T("Username"),
desc =>
?T("A user name for SQL authentication. "
"The default value is 'ejabberd'.")}},
{trusted_proxies,
#{value => "all | [Network1, Network2, ...]",
desc =>
?T("Specify what proxies are trusted when an HTTP request "
"contains the header 'X-Forwarded-For'. You can specify "
"'all' to allow all proxies, or specify a list of IPs, "
"possibly with masks. The default value is an empty list. "
"Using this option you can know the real IP "
"of the request, for admin purpose, or security configuration "
"(for example using _`mod_fail2ban`_). IMPORTANT: The proxy MUST "
"be configured to set the 'X-Forwarded-For' header if you "
"enable this option as, otherwise, the client can set it "
"itself and as a result the IP value cannot be trusted for "
"security rules in ejabberd.")}},
{validate_stream,
#{value => "true | false",
desc =>
?T("Whether to validate any incoming XML packet according "
"to the schemas of "
"https://github.com/processone/xmpp#supported-xmpp-elements"
"[supported XMPP extensions]. WARNING: the validation is only "
"intended for the use by client developers - don't enable "
"it in production environment. The default value is 'false'.")}},
{websocket_origin,
#{value => "ignore | URL",
desc =>
?T("This option enables validation for 'Origin' header to "
"protect against connections from other domains than given "
"in the configuration file. In this way, the lower layer load "
"balancer can be chosen for a specific ejabberd implementation "
"while still providing a secure WebSocket connection. "
"The default value is 'ignore'. An example value of the 'URL' is "
"'\"https://test.example.org:8081\"'.")}},
{websocket_ping_interval,
#{value => "timeout()",
desc =>
?T("Defines time between pings sent by the server to a client "
"(WebSocket level protocol pings are used for this) to keep "
"a connection active. If the client doesn't respond to two "
"consecutive pings, the connection will be assumed as closed. "
"The value of '0' can be used to disable the feature. This option "
"makes the server sending pings only for connections using the RFC "
"compliant protocol. For older style connections the server "
"expects that whitespace pings would be used for this purpose. "
"The default value is '60' seconds.")}},
{websocket_timeout,
#{value => "timeout()",
desc =>
?T("Amount of time without any communication after which the "
"connection would be closed. The default value is '300' seconds.")}}].
%%%===================================================================
%%% Internal functions
%%%===================================================================
ejabberd-24.12/src/mod_host_meta_opt.erl 0000664 0001750 0001750 00000001253 14730775155 020656 0 ustar debalance debalance %% Generated automatically
%% DO NOT EDIT: run `make options` instead
-module(mod_host_meta_opt).
-export([bosh_service_url/1]).
-export([websocket_url/1]).
-spec bosh_service_url(gen_mod:opts() | global | binary()) -> 'undefined' | binary().
bosh_service_url(Opts) when is_map(Opts) ->
gen_mod:get_opt(bosh_service_url, Opts);
bosh_service_url(Host) ->
gen_mod:get_module_opt(Host, mod_host_meta, bosh_service_url).
-spec websocket_url(gen_mod:opts() | global | binary()) -> 'undefined' | binary().
websocket_url(Opts) when is_map(Opts) ->
gen_mod:get_opt(websocket_url, Opts);
websocket_url(Host) ->
gen_mod:get_module_opt(Host, mod_host_meta, websocket_url).
ejabberd-24.12/src/mod_service_log_opt.erl 0000664 0001750 0001750 00000000523 14730775155 021173 0 ustar debalance debalance %% Generated automatically
%% DO NOT EDIT: run `make options` instead
-module(mod_service_log_opt).
-export([loggers/1]).
-spec loggers(gen_mod:opts() | global | binary()) -> [binary()].
loggers(Opts) when is_map(Opts) ->
gen_mod:get_opt(loggers, Opts);
loggers(Host) ->
gen_mod:get_module_opt(Host, mod_service_log, loggers).
ejabberd-24.12/src/mod_pres_counter_opt.erl 0000664 0001750 0001750 00000001102 14730775155 021374 0 ustar debalance debalance %% Generated automatically
%% DO NOT EDIT: run `make options` instead
-module(mod_pres_counter_opt).
-export([count/1]).
-export([interval/1]).
-spec count(gen_mod:opts() | global | binary()) -> pos_integer().
count(Opts) when is_map(Opts) ->
gen_mod:get_opt(count, Opts);
count(Host) ->
gen_mod:get_module_opt(Host, mod_pres_counter, count).
-spec interval(gen_mod:opts() | global | binary()) -> pos_integer().
interval(Opts) when is_map(Opts) ->
gen_mod:get_opt(interval, Opts);
interval(Host) ->
gen_mod:get_module_opt(Host, mod_pres_counter, interval).
ejabberd-24.12/src/mod_conversejs.erl 0000664 0001750 0001750 00000033273 14730775155 020201 0 ustar debalance debalance %%%----------------------------------------------------------------------
%%% File : mod_conversejs.erl
%%% Author : Alexey Shchepin
%%% Purpose : Serve simple page for Converse.js XMPP web browser client
%%% Created : 8 Nov 2021 by Alexey Shchepin
%%%
%%%
%%% 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_conversejs).
-author('alexey@process-one.net').
-behaviour(gen_mod).
-export([start/2, stop/1, reload/3, process/2, depends/2,
mod_opt_type/1, mod_options/1, mod_doc/0]).
-include_lib("xmpp/include/xmpp.hrl").
-include("logger.hrl").
-include("ejabberd_http.hrl").
-include("translate.hrl").
-include("ejabberd_web_admin.hrl").
start(_Host, _Opts) ->
ok.
stop(_Host) ->
ok.
reload(_Host, _NewOpts, _OldOpts) ->
ok.
depends(_Host, _Opts) ->
[].
process([], #request{method = 'GET', host = Host, raw_path = RawPath}) ->
ExtraOptions = get_auth_options(Host)
++ get_register_options(Host)
++ get_extra_options(Host),
Domain = mod_conversejs_opt:default_domain(Host),
Script = get_file_url(Host, conversejs_script,
<>,
<<"https://cdn.conversejs.org/dist/converse.min.js">>),
CSS = get_file_url(Host, conversejs_css,
<>,
<<"https://cdn.conversejs.org/dist/converse.min.css">>),
Init = [{<<"discover_connection_methods">>, false},
{<<"default_domain">>, Domain},
{<<"domain_placeholder">>, Domain},
{<<"registration_domain">>, Domain},
{<<"assets_path">>, RawPath},
{<<"view_mode">>, <<"fullscreen">>}
| ExtraOptions],
Init2 =
case mod_host_meta:get_url(?MODULE, websocket, any, Host) of
undefined -> Init;
WSURL -> [{<<"websocket_url">>, WSURL} | Init]
end,
Init3 =
case mod_host_meta:get_url(?MODULE, bosh, any, Host) of
undefined -> Init2;
BoshURL -> [{<<"bosh_service_url">>, BoshURL} | Init2]
end,
Init4 = maps:from_list(Init3),
{200, [html],
[<<"">>,
<<"">>,
<<"">>,
<<" ">>,
<<" ">>,
<<"">>,
<<"">>,
<<"">>,
<<"">>,
<<"">>,
<<"">>]};
process(LocalPath, #request{host = Host}) ->
case is_served_file(LocalPath) of
true -> serve(Host, LocalPath);
false -> ejabberd_web:error(not_found)
end.
%%----------------------------------------------------------------------
%% File server
%%----------------------------------------------------------------------
is_served_file([<<"converse.min.js">>]) -> true;
is_served_file([<<"converse.min.css">>]) -> true;
is_served_file([<<"converse.min.js.map">>]) -> true;
is_served_file([<<"converse.min.css.map">>]) -> true;
is_served_file([<<"emojis.js">>]) -> true;
is_served_file([<<"locales">>, _]) -> true;
is_served_file([<<"locales">>, <<"dayjs">>, _]) -> true;
is_served_file([<<"webfonts">>, _]) -> true;
is_served_file(_) -> false.
serve(Host, LocalPath) ->
case get_conversejs_resources(Host) of
undefined ->
Path = str:join(LocalPath, <<"/">>),
{303, [{<<"Location">>, <<"https://cdn.conversejs.org/dist/", Path/binary>>}], <<>>};
MainPath -> serve2(LocalPath, MainPath)
end.
get_conversejs_resources(Host) ->
Opts = gen_mod:get_module_opts(Host, ?MODULE),
mod_conversejs_opt:conversejs_resources(Opts).
%% Copied from mod_muc_log_http.erl
serve2(LocalPathBin, MainPathBin) ->
LocalPath = [binary_to_list(LPB) || LPB <- LocalPathBin],
MainPath = binary_to_list(MainPathBin),
FileName = filename:join(filename:split(MainPath) ++ LocalPath),
case file:read_file(FileName) of
{ok, FileContents} ->
?DEBUG("Delivering content.", []),
{200,
[{<<"Content-Type">>, content_type(FileName)}],
FileContents};
{error, eisdir} ->
{403, [], "Forbidden"};
{error, Error} ->
?DEBUG("Delivering error: ~p", [Error]),
case Error of
eacces -> {403, [], "Forbidden"};
enoent -> {404, [], "Not found"};
_Else -> {404, [], atom_to_list(Error)}
end
end.
content_type(Filename) ->
case string:to_lower(filename:extension(Filename)) of
".css" -> "text/css";
".js" -> "text/javascript";
".map" -> "application/json";
".ttf" -> "font/ttf";
".woff" -> "font/woff";
".woff2" -> "font/woff2"
end.
%%----------------------------------------------------------------------
%% Options parsing
%%----------------------------------------------------------------------
get_auth_options(Domain) ->
case {ejabberd_auth_anonymous:is_login_anonymous_enabled(Domain),
ejabberd_auth_anonymous:is_sasl_anonymous_enabled(Domain)} of
{false, false} ->
[{<<"authentication">>, <<"login">>}];
{true, false} ->
[{<<"authentication">>, <<"external">>}];
{_, true} ->
[{<<"authentication">>, <<"anonymous">>},
{<<"jid">>, Domain}]
end.
get_register_options(Server) ->
AuthSupportsRegister =
lists:any(
fun(ejabberd_auth_mnesia) -> true;
(ejabberd_auth_external) -> true;
(ejabberd_auth_sql) -> true;
(_) -> false
end,
ejabberd_auth:auth_modules(Server)),
Modules = get_register_modules(Server),
ModRegisterAllowsMe = (Modules == all) orelse lists:member(?MODULE, Modules),
[{<<"allow_registration">>, AuthSupportsRegister and ModRegisterAllowsMe}].
get_register_modules(Server) ->
try mod_register_opt:allow_modules(Server)
catch
error:{module_not_loaded, mod_register, _} ->
?DEBUG("mod_conversejs couldn't get mod_register configuration for "
"vhost ~p: module not loaded in that vhost.", [Server]),
[]
end.
get_extra_options(Host) ->
RawOpts = gen_mod:get_module_opt(Host, ?MODULE, conversejs_options),
lists:map(fun({Name, <<"true">>}) -> {Name, true};
({Name, <<"false">>}) -> {Name, false};
({<<"locked_domain">> = Name, Value}) ->
{Name, misc:expand_keyword(<<"@HOST@">>, Value, Host)};
({Name, Value}) ->
{Name, Value}
end,
RawOpts).
get_file_url(Host, Option, Filename, Default) ->
FileRaw = case gen_mod:get_module_opt(Host, ?MODULE, Option) of
auto -> get_auto_file_url(Host, Filename, Default);
F -> F
end,
misc:expand_keyword(<<"@HOST@">>, FileRaw, Host).
get_auto_file_url(Host, Filename, Default) ->
case get_conversejs_resources(Host) of
undefined -> Default;
_ -> Filename
end.
%%----------------------------------------------------------------------
%%
%%----------------------------------------------------------------------
mod_opt_type(bosh_service_url) ->
econf:either(auto, econf:binary());
mod_opt_type(websocket_url) ->
econf:either(auto, econf:binary());
mod_opt_type(conversejs_resources) ->
econf:either(undefined, econf:directory());
mod_opt_type(conversejs_options) ->
econf:map(econf:binary(), econf:either(econf:binary(), econf:int()));
mod_opt_type(conversejs_script) ->
econf:binary();
mod_opt_type(conversejs_css) ->
econf:binary();
mod_opt_type(default_domain) ->
econf:host().
mod_options(Host) ->
[{bosh_service_url, auto},
{websocket_url, auto},
{default_domain, Host},
{conversejs_resources, undefined},
{conversejs_options, []},
{conversejs_script, auto},
{conversejs_css, auto}].
mod_doc() ->
#{desc =>
[?T("This module serves a simple page for the "
"https://conversejs.org/[Converse] XMPP web browser client."), "",
?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("Make sure either _`mod_bosh`_ or _`listen.md#ejabberd_http_ws|ejabberd_http_ws`_ "
"are enabled in at least one 'request_handlers'."), "",
?T("When 'conversejs_css' and 'conversejs_script' are 'auto', "
"by default they point to the public Converse client.")
],
note => "added in 21.12 and improved in 22.05",
example =>
[{?T("Manually setup WebSocket url, and use the public Converse client:"),
["listen:",
" -",
" port: 5280",
" module: ejabberd_http",
" request_handlers:",
" /bosh: mod_bosh",
" /websocket: ejabberd_http_ws",
" /conversejs: mod_conversejs",
"",
"modules:",
" mod_bosh: {}",
" mod_conversejs:",
" websocket_url: \"ws://@HOST@:5280/websocket\""]},
{?T("Host Converse locally and let auto detection of WebSocket and Converse URLs:"),
["listen:",
" -",
" port: 443",
" module: ejabberd_http",
" tls: true",
" request_handlers:",
" /websocket: ejabberd_http_ws",
" /conversejs: mod_conversejs",
"",
"modules:",
" mod_conversejs:",
" conversejs_resources: \"/home/ejabberd/conversejs-9.0.0/package/dist\""]},
{?T("Configure some additional options for Converse"),
["modules:",
" mod_conversejs:",
" websocket_url: auto",
" conversejs_options:",
" auto_away: 30",
" clear_cache_on_logout: true",
" i18n: \"pt\"",
" locked_domain: \"@HOST@\"",
" message_archiving: always",
" theme: dracula"]}
],
opts =>
[{websocket_url,
#{value => ?T("auto | WebSocketURL"),
desc =>
?T("A WebSocket URL to which Converse can connect to. "
"The '@HOST@' keyword 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 => ?T("auto | BoshURL"),
desc =>
?T("BOSH service URL to which Converse can connect to. "
"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'.")}},
{default_domain,
#{value => ?T("Domain"),
desc =>
?T("Specify a domain to act as the default for user JIDs. "
"The keyword '@HOST@' is replaced with the hostname. "
"The default value is '@HOST@'.")}},
{conversejs_resources,
#{value => ?T("Path"),
note => "added in 22.05",
desc =>
?T("Local path to the Converse files. "
"If not set, the public Converse client will be used instead.")}},
{conversejs_options,
#{value => "{Name: Value}",
note => "added in 22.05",
desc =>
?T("Specify additional options to be passed to Converse. "
"See https://conversejs.org/docs/html/configuration.html[Converse configuration]. "
"Only boolean, integer and string values are supported; "
"lists are not supported.")}},
{conversejs_script,
#{value => ?T("auto | URL"),
desc =>
?T("Converse main script URL. "
"The keyword '@HOST@' is replaced with the hostname. "
"The default value is 'auto'.")}},
{conversejs_css,
#{value => ?T("auto | URL"),
desc =>
?T("Converse CSS URL. "
"The keyword '@HOST@' is replaced with the hostname. "
"The default value is 'auto'.")}}]
}.
ejabberd-24.12/src/misc.erl 0000664 0001750 0001750 00000056735 14730775155 016124 0 ustar debalance debalance %%%-------------------------------------------------------------------
%%% @author Evgeny Khramtsov
%%% @doc
%%% This is the place for some unsorted auxiliary functions
%%% Some functions from jlib.erl are moved here
%%% Mild rubbish heap is accepted ;)
%%% @end
%%% Created : 30 Mar 2017 by Evgeny Khramtsov
%%%
%%%
%%% 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(misc).
%% API
-export([add_delay_info/3, add_delay_info/4,
unwrap_carbon/1, unwrap_mucsub_message/1, is_standalone_chat_state/1,
tolower/1, term_to_base64/1, base64_to_term/1, ip_to_list/1,
hex_to_bin/1, hex_to_base64/1, url_encode/1, expand_keyword/3,
atom_to_binary/1, binary_to_atom/1, tuple_to_binary/1,
l2i/1, i2l/1, i2l/2, expr_to_term/1, term_to_expr/1,
now_to_usec/1, usec_to_now/1, encode_pid/1, decode_pid/2,
compile_exprs/2, join_atoms/2, try_read_file/1, get_descr/2,
css_dir/0, img_dir/0, js_dir/0, msgs_dir/0, sql_dir/0, lua_dir/0,
read_css/1, read_img/1, read_js/1, read_lua/1,
intersection/2, format_val/1, cancel_timer/1, unique_timestamp/0,
is_mucsub_message/1, best_match/2, pmap/2, peach/2, format_exception/4,
get_my_ipv4_address/0, get_my_ipv6_address/0, parse_ip_mask/1,
crypto_hmac/3, crypto_hmac/4, uri_parse/1, uri_parse/2, uri_quote/1,
json_encode/1, json_decode/1, json_encode_with_kv_lists/1,
set_proc_label/1,
match_ip_mask/3, format_hosts_list/1, format_cycle/1, delete_dir/1,
semver_to_xxyy/1, logical_processors/0, get_mucsub_event_type/1]).
%% Deprecated functions
-export([decode_base64/1, encode_base64/1]).
-deprecated([{decode_base64, 1},
{encode_base64, 1}]).
-include("logger.hrl").
-include_lib("xmpp/include/xmpp.hrl").
-include_lib("kernel/include/file.hrl").
-ifdef(OTP_BELOW_27).
%% Copied from erlang/otp/lib/stdlib/src/re.erl
-type re_mp() :: {re_pattern, _, _, _, _}.
-type json_value() :: jiffy:json_value().
-else.
-type re_mp() :: re:mp().
-type json_value() :: json:encode_value().
-endif.
-export_type([re_mp/0]).
-export_type([json_value/0]).
-type distance_cache() :: #{{string(), string()} => non_neg_integer()}.
-spec uri_parse(binary()|string()) -> {ok, string(), string(), string(), number(), string(), string()} | {error, term()}.
-ifdef(USE_OLD_HTTP_URI).
uri_parse(URL) when is_binary(URL) ->
uri_parse(binary_to_list(URL));
uri_parse(URL) ->
uri_parse(URL, []).
uri_parse(URL, Protocols) when is_binary(URL) ->
uri_parse(binary_to_list(URL), Protocols);
uri_parse(URL, Protocols) ->
case http_uri:parse(URL, [{scheme_defaults, Protocols}]) of
{ok, {Scheme, UserInfo, Host, Port, Path, Query}} ->
{ok, atom_to_list(Scheme), UserInfo, Host, Port, Path, Query};
{error, _} = E ->
E
end.
-else.
uri_parse(URL) when is_binary(URL) ->
uri_parse(binary_to_list(URL));
uri_parse(URL) ->
uri_parse(URL, [{http, 80}, {https, 443}]).
uri_parse(URL, Protocols) when is_binary(URL) ->
uri_parse(binary_to_list(URL), Protocols);
uri_parse(URL, Protocols) ->
case uri_string:parse(URL) of
#{scheme := Scheme, host := Host, port := Port, path := Path} = M1 ->
{ok, Scheme, maps:get(userinfo, M1, ""), Host, Port, Path, maps:get(query, M1, "")};
#{scheme := Scheme, host := Host, path := Path} = M2 ->
case lists:keyfind(list_to_atom(Scheme), 1, Protocols) of
{_, Port} ->
{ok, Scheme, maps:get(userinfo, M2, ""), Host, Port, Path, maps:get(query, M2, "")};
_ ->
{error, unknown_protocol}
end;
{error, Atom, _} ->
{error, Atom}
end.
-endif.
-ifdef(OTP_BELOW_25).
-ifdef(OTP_BELOW_24).
uri_quote(Data) ->
Data.
-else.
uri_quote(Data) ->
http_uri:encode(Data).
-endif.
-else.
uri_quote(Data) ->
uri_string:quote(Data).
-endif.
-ifdef(USE_OLD_CRYPTO_HMAC).
crypto_hmac(Type, Key, Data) -> crypto:hmac(Type, Key, Data).
crypto_hmac(Type, Key, Data, MacL) -> crypto:hmac(Type, Key, Data, MacL).
-else.
crypto_hmac(Type, Key, Data) -> crypto:mac(hmac, Type, Key, Data).
crypto_hmac(Type, Key, Data, MacL) -> crypto:macN(hmac, Type, Key, Data, MacL).
-endif.
-ifdef(OTP_BELOW_27).
json_encode_with_kv_lists(Term) ->
jiffy:encode(Term).
json_encode(Term) ->
jiffy:encode(Term).
json_decode(Bin) ->
jiffy:decode(Bin, [return_maps]).
-else.
json_encode_with_kv_lists(Term) ->
iolist_to_binary(json:encode(Term,
fun([{_, _} | _] = Val, Encoder) ->
json:encode_key_value_list(Val, Encoder);
(Val, Encoder) ->
json:encode_value(Val, Encoder)
end)).
json_encode(Term) ->
iolist_to_binary(json:encode(Term)).
json_decode(Bin) ->
json:decode(Bin).
-endif.
%%%===================================================================
%%% API
%%%===================================================================
-spec add_delay_info(stanza(), jid(), erlang:timestamp()) -> stanza().
add_delay_info(Stz, From, Time) ->
add_delay_info(Stz, From, Time, <<"">>).
-spec add_delay_info(stanza(), jid(), erlang:timestamp(), binary()) -> stanza().
add_delay_info(Stz, From, Time, Desc) ->
Delays = xmpp:get_subtags(Stz, #delay{stamp = {0,0,0}}),
Matching = lists:any(
fun(#delay{from = OldFrom}) when is_record(OldFrom, jid) ->
jid:tolower(From) == jid:tolower(OldFrom);
(_) ->
false
end, Delays),
case Matching of
true ->
Stz;
_ ->
NewDelay = #delay{stamp = Time, from = From, desc = Desc},
xmpp:append_subtags(Stz, [NewDelay])
end.
-spec unwrap_carbon(stanza()) -> xmpp_element().
unwrap_carbon(#message{} = Msg) ->
try
case xmpp:get_subtag(Msg, #carbons_sent{forwarded = #forwarded{}}) of
#carbons_sent{forwarded = #forwarded{sub_els = [El]}} ->
xmpp:decode(El, ?NS_CLIENT, [ignore_els]);
_ ->
case xmpp:get_subtag(Msg, #carbons_received{
forwarded = #forwarded{}}) of
#carbons_received{forwarded = #forwarded{sub_els = [El]}} ->
xmpp:decode(El, ?NS_CLIENT, [ignore_els]);
_ ->
Msg
end
end
catch _:{xmpp_codec, _} ->
Msg
end;
unwrap_carbon(Stanza) -> Stanza.
-spec unwrap_mucsub_message(xmpp_element()) -> message() | false.
unwrap_mucsub_message(#message{} = OuterMsg) ->
case xmpp:get_subtag(OuterMsg, #ps_event{}) of
#ps_event{
items = #ps_items{
node = Node,
items = [
#ps_item{
sub_els = [#message{} = InnerMsg]} | _]}}
when Node == ?NS_MUCSUB_NODES_MESSAGES;
Node == ?NS_MUCSUB_NODES_SUBJECT ->
InnerMsg;
_ ->
false
end;
unwrap_mucsub_message(_Packet) ->
false.
-spec is_mucsub_message(xmpp_element()) -> boolean().
is_mucsub_message(Packet) ->
get_mucsub_event_type(Packet) /= false.
-spec get_mucsub_event_type(xmpp_element()) -> binary() | false.
get_mucsub_event_type(#message{} = OuterMsg) ->
case xmpp:get_subtag(OuterMsg, #ps_event{}) of
#ps_event{
items = #ps_items{
node = Node}}
when Node == ?NS_MUCSUB_NODES_MESSAGES;
Node == ?NS_MUCSUB_NODES_SUBJECT;
Node == ?NS_MUCSUB_NODES_AFFILIATIONS;
Node == ?NS_MUCSUB_NODES_CONFIG;
Node == ?NS_MUCSUB_NODES_PARTICIPANTS;
Node == ?NS_MUCSUB_NODES_PRESENCE;
Node == ?NS_MUCSUB_NODES_SUBSCRIBERS ->
Node;
_ ->
false
end;
get_mucsub_event_type(_Packet) ->
false.
-spec is_standalone_chat_state(stanza()) -> boolean().
is_standalone_chat_state(Stanza) ->
case unwrap_carbon(Stanza) of
#message{body = [], subject = [], sub_els = Els} ->
IgnoreNS = [?NS_CHATSTATES, ?NS_DELAY, ?NS_EVENT, ?NS_HINTS],
Stripped = [El || El <- Els,
not lists:member(xmpp:get_ns(El), IgnoreNS)],
Stripped == [];
_ ->
false
end.
-spec tolower(binary()) -> binary().
tolower(B) ->
iolist_to_binary(tolower_s(binary_to_list(B))).
tolower_s([C | Cs]) ->
if C >= $A, C =< $Z -> [C + 32 | tolower_s(Cs)];
true -> [C | tolower_s(Cs)]
end;
tolower_s([]) -> [].
-spec term_to_base64(term()) -> binary().
term_to_base64(Term) ->
encode_base64(term_to_binary(Term)).
-spec base64_to_term(binary()) -> {term, term()} | error.
base64_to_term(Base64) ->
try binary_to_term(base64:decode(Base64), [safe]) of
Term -> {term, Term}
catch _:_ ->
error
end.
-spec decode_base64(binary()) -> binary().
decode_base64(S) ->
try base64:mime_decode(S)
catch _:badarg -> <<>>
end.
-spec encode_base64(binary()) -> binary().
encode_base64(Data) ->
base64:encode(Data).
-spec ip_to_list(inet:ip_address() | undefined |
{inet:ip_address(), inet:port_number()}) -> binary().
ip_to_list({IP, _Port}) ->
ip_to_list(IP);
%% This function clause could use inet_parse too:
ip_to_list(undefined) ->
<<"unknown">>;
ip_to_list(local) ->
<<"unix">>;
ip_to_list(IP) ->
list_to_binary(inet_parse:ntoa(IP)).
-spec hex_to_bin(binary()) -> binary().
hex_to_bin(Hex) ->
hex_to_bin(binary_to_list(Hex), []).
-spec hex_to_bin(list(), list()) -> binary().
hex_to_bin([], Acc) ->
list_to_binary(lists:reverse(Acc));
hex_to_bin([H1, H2 | T], Acc) ->
{ok, [V], []} = io_lib:fread("~16u", [H1, H2]),
hex_to_bin(T, [V | Acc]).
-spec hex_to_base64(binary()) -> binary().
hex_to_base64(Hex) ->
base64:encode(hex_to_bin(Hex)).
-spec url_encode(binary()) -> binary().
url_encode(A) ->
url_encode(A, <<>>).
-spec expand_keyword(iodata(), iodata(), iodata()) -> binary().
expand_keyword(Keyword, Input, Replacement) ->
re:replace(Input, Keyword, Replacement,
[{return, binary}, global]).
binary_to_atom(Bin) ->
erlang:binary_to_atom(Bin, utf8).
tuple_to_binary(T) ->
iolist_to_binary(tuple_to_list(T)).
%% erlang:atom_to_binary/1 is available since OTP 23
%% https://www.erlang.org/doc/apps/erts/erlang#atom_to_binary/1
%% Let's use /2 for backwards compatibility.
atom_to_binary(A) ->
erlang:atom_to_binary(A, utf8).
expr_to_term(Expr) ->
Str = binary_to_list(<>),
{ok, Tokens, _} = erl_scan:string(Str),
{ok, Term} = erl_parse:parse_term(Tokens),
Term.
term_to_expr(Term) ->
list_to_binary(io_lib:print(Term, 1, 999999, -1)).
-spec now_to_usec(erlang:timestamp()) -> non_neg_integer().
now_to_usec({MSec, Sec, USec}) ->
(MSec*1000000 + Sec)*1000000 + USec.
-spec usec_to_now(non_neg_integer()) -> erlang:timestamp().
usec_to_now(Int) ->
Secs = Int div 1000000,
USec = Int rem 1000000,
MSec = Secs div 1000000,
Sec = Secs rem 1000000,
{MSec, Sec, USec}.
l2i(I) when is_integer(I) -> I;
l2i(L) when is_binary(L) -> binary_to_integer(L).
i2l(I) when is_integer(I) -> integer_to_binary(I);
i2l(L) when is_binary(L) -> L.
i2l(I, N) when is_integer(I) -> i2l(i2l(I), N);
i2l(L, N) when is_binary(L) ->
case str:len(L) of
N -> L;
C when C > N -> L;
_ -> i2l(<<$0, L/binary>>, N)
end.
-spec encode_pid(pid()) -> binary().
encode_pid(Pid) ->
list_to_binary(erlang:pid_to_list(Pid)).
-spec decode_pid(binary(), binary()) -> pid().
decode_pid(PidBin, NodeBin) ->
PidStr = binary_to_list(PidBin),
Pid = erlang:list_to_pid(PidStr),
case erlang:binary_to_atom(NodeBin, latin1) of
Node when Node == node() ->
Pid;
Node ->
try set_node_id(PidStr, NodeBin)
catch _:badarg ->
erlang:error({bad_node, Node})
end
end.
-spec compile_exprs(module(), [string()]) -> ok | {error, any()}.
compile_exprs(Mod, Exprs) ->
try
Forms = lists:map(
fun(Expr) ->
{ok, Tokens, _} = erl_scan:string(lists:flatten(Expr)),
{ok, Form} = erl_parse:parse_form(Tokens),
Form
end, Exprs),
{ok, Code} = case compile:forms(Forms, []) of
{ok, Mod, Bin} -> {ok, Bin};
{ok, Mod, Bin, _Warnings} -> {ok, Bin};
Error -> Error
end,
{module, Mod} = code:load_binary(Mod, "nofile", Code),
ok
catch _:{badmatch, {error, ErrInfo, _ErrLocation}} ->
{error, ErrInfo};
_:{badmatch, {error, _} = Err} ->
Err;
_:{badmatch, error} ->
{error, compile_failed}
end.
-spec join_atoms([atom()], binary()) -> binary().
join_atoms(Atoms, Sep) ->
str:join([io_lib:format("~p", [A]) || A <- lists:sort(Atoms)], Sep).
%% @doc Checks if the file is readable and converts its name to binary.
%% Fails with `badarg' otherwise. The function is intended for usage
%% in configuration validators only.
-spec try_read_file(file:filename_all()) -> binary().
try_read_file(Path) ->
case file:open(Path, [read]) of
{ok, Fd} ->
file:close(Fd),
iolist_to_binary(Path);
{error, Why} ->
?ERROR_MSG("Failed to read ~ts: ~ts", [Path, file:format_error(Why)]),
erlang:error(badarg)
end.
-spec css_dir() -> file:filename().
css_dir() ->
get_dir("css").
-spec img_dir() -> file:filename().
img_dir() ->
get_dir("img").
-spec js_dir() -> file:filename().
js_dir() ->
get_dir("js").
-spec msgs_dir() -> file:filename().
msgs_dir() ->
get_dir("msgs").
-spec sql_dir() -> file:filename().
sql_dir() ->
get_dir("sql").
-spec lua_dir() -> file:filename().
lua_dir() ->
get_dir("lua").
-spec read_css(file:filename()) -> {ok, binary()} | {error, file:posix()}.
read_css(File) ->
read_file(filename:join(css_dir(), File)).
-spec read_img(file:filename()) -> {ok, binary()} | {error, file:posix()}.
read_img(File) ->
read_file(filename:join(img_dir(), File)).
-spec read_js(file:filename()) -> {ok, binary()} | {error, file:posix()}.
read_js(File) ->
read_file(filename:join(js_dir(), File)).
-spec read_lua(file:filename()) -> {ok, binary()} | {error, file:posix()}.
read_lua(File) ->
read_file(filename:join(lua_dir(), File)).
-spec get_descr(binary(), binary()) -> binary().
get_descr(Lang, Text) ->
Desc = translate:translate(Lang, Text),
Copyright = ejabberd_config:get_copyright(),
<>.
-spec intersection(list(), list()) -> list().
intersection(L1, L2) ->
lists:filter(
fun(E) ->
lists:member(E, L2)
end, L1).
-spec format_val(any()) -> iodata().
format_val({yaml, S}) when is_integer(S); is_binary(S); is_atom(S) ->
format_val(S);
format_val({yaml, YAML}) ->
S = try fast_yaml:encode(YAML)
catch _:_ -> YAML
end,
format_val(S);
format_val(I) when is_integer(I) ->
integer_to_list(I);
format_val(B) when is_atom(B) ->
erlang:atom_to_binary(B, utf8);
format_val(Term) ->
S = try iolist_to_binary(Term)
catch _:_ -> list_to_binary(io_lib:format("~p", [Term]))
end,
case binary:match(S, <<"\n">>) of
nomatch -> S;
_ -> [io_lib:nl(), S]
end.
-spec cancel_timer(reference() | undefined) -> ok.
cancel_timer(TRef) when is_reference(TRef) ->
case erlang:cancel_timer(TRef) of
false ->
receive {timeout, TRef, _} -> ok
after 0 -> ok
end;
_ ->
ok
end;
cancel_timer(_) ->
ok.
-spec best_match(atom() | binary() | string(),
[atom() | binary() | string()]) -> string().
best_match(Pattern, []) ->
Pattern;
best_match(Pattern, Opts) ->
String = to_string(Pattern),
{Ds, _} = lists:mapfoldl(
fun(Opt, Cache) ->
SOpt = to_string(Opt),
{Distance, Cache1} = ld(String, SOpt, Cache),
{{Distance, SOpt}, Cache1}
end, #{}, Opts),
element(2, lists:min(Ds)).
-spec logical_processors() -> non_neg_integer().
logical_processors() ->
case erlang:system_info(logical_processors) of
V when is_integer(V), V >= 2 -> V;
_ -> 1
end.
-spec pmap(fun((T1) -> T2), [T1]) -> [T2].
pmap(Fun, [_,_|_] = List) ->
case logical_processors() of
1 -> lists:map(Fun, List);
_ ->
Self = self(),
lists:map(
fun({Pid, Ref}) ->
receive
{Pid, Ret} ->
receive
{'DOWN', Ref, _, _, _} ->
Ret
end;
{'DOWN', Ref, _, _, Reason} ->
exit(Reason)
end
end, [spawn_monitor(
fun() -> Self ! {self(), Fun(X)} end)
|| X <- List])
end;
pmap(Fun, List) ->
lists:map(Fun, List).
-spec peach(fun((T) -> any()), [T]) -> ok.
peach(Fun, [_,_|_] = List) ->
case logical_processors() of
1 -> lists:foreach(Fun, List);
_ ->
Self = self(),
lists:foreach(
fun({Pid, Ref}) ->
receive
Pid ->
receive
{'DOWN', Ref, _, _, _} ->
ok
end;
{'DOWN', Ref, _, _, Reason} ->
exit(Reason)
end
end, [spawn_monitor(
fun() -> Fun(X), Self ! self() end)
|| X <- List])
end;
peach(Fun, List) ->
lists:foreach(Fun, List).
-ifdef(HAVE_ERL_ERROR).
format_exception(Level, Class, Reason, Stacktrace) ->
erl_error:format_exception(
Level, Class, Reason, Stacktrace,
fun(_M, _F, _A) -> false end,
fun(Term, I) ->
io_lib:print(Term, I, 80, -1)
end).
-else.
format_exception(Level, Class, Reason, Stacktrace) ->
lib:format_exception(
Level, Class, Reason, Stacktrace,
fun(_M, _F, _A) -> false end,
fun(Term, I) ->
io_lib:print(Term, I, 80, -1)
end).
-endif.
-spec get_my_ipv4_address() -> inet:ip4_address().
get_my_ipv4_address() ->
{ok, MyHostName} = inet:gethostname(),
case inet:getaddr(MyHostName, inet) of
{ok, Addr} -> Addr;
{error, _} -> {127, 0, 0, 1}
end.
-spec get_my_ipv6_address() -> inet:ip6_address().
get_my_ipv6_address() ->
{ok, MyHostName} = inet:gethostname(),
case inet:getaddr(MyHostName, inet6) of
{ok, Addr} -> Addr;
{error, _} -> {0, 0, 0, 0, 0, 0, 0, 1}
end.
-spec parse_ip_mask(binary()) -> {ok, {inet:ip4_address(), 0..32}} |
{ok, {inet:ip6_address(), 0..128}} |
error.
parse_ip_mask(S) ->
case econf:validate(econf:ip_mask(), S) of
{ok, _} = Ret -> Ret;
_ -> error
end.
-spec match_ip_mask(inet:ip_address(), inet:ip_address(), 0..128) -> boolean().
match_ip_mask({_, _, _, _} = IP, {_, _, _, _} = Net, Mask) ->
IPInt = ip_to_integer(IP),
NetInt = ip_to_integer(Net),
M = bnot (1 bsl (32 - Mask) - 1),
IPInt band M =:= NetInt band M;
match_ip_mask({_, _, _, _, _, _, _, _} = IP,
{_, _, _, _, _, _, _, _} = Net, Mask) ->
IPInt = ip_to_integer(IP),
NetInt = ip_to_integer(Net),
M = bnot (1 bsl (128 - Mask) - 1),
IPInt band M =:= NetInt band M;
match_ip_mask({_, _, _, _} = IP,
{0, 0, 0, 0, 0, 16#FFFF, _, _} = Net, Mask) ->
IPInt = ip_to_integer({0, 0, 0, 0, 0, 16#FFFF, 0, 0}) + ip_to_integer(IP),
NetInt = ip_to_integer(Net),
M = bnot (1 bsl (128 - Mask) - 1),
IPInt band M =:= NetInt band M;
match_ip_mask({0, 0, 0, 0, 0, 16#FFFF, _, _} = IP,
{_, _, _, _} = Net, Mask) ->
IPInt = ip_to_integer(IP) - ip_to_integer({0, 0, 0, 0, 0, 16#FFFF, 0, 0}),
NetInt = ip_to_integer(Net),
M = bnot (1 bsl (32 - Mask) - 1),
IPInt band M =:= NetInt band M;
match_ip_mask(_, _, _) ->
false.
-spec format_hosts_list([binary(), ...]) -> iolist().
format_hosts_list([Host]) ->
Host;
format_hosts_list([H1, H2]) ->
[H1, " and ", H2];
format_hosts_list([H1, H2, H3]) ->
[H1, ", ", H2, " and ", H3];
format_hosts_list([H1, H2|Hs]) ->
io_lib:format("~ts, ~ts and ~B more hosts",
[H1, H2, length(Hs)]).
-spec format_cycle([atom(), ...]) -> iolist().
format_cycle([M1]) ->
atom_to_list(M1);
format_cycle([M1, M2]) ->
[atom_to_list(M1), " and ", atom_to_list(M2)];
format_cycle([M|Ms]) ->
atom_to_list(M) ++ ", " ++ format_cycle(Ms).
-spec delete_dir(file:filename_all()) -> ok | {error, file:posix()}.
delete_dir(Dir) ->
try
{ok, Entries} = file:list_dir(Dir),
lists:foreach(fun(Path) ->
case filelib:is_dir(Path) of
true ->
ok = delete_dir(Path);
false ->
ok = file:delete(Path)
end
end, [filename:join(Dir, Entry) || Entry <- Entries]),
ok = file:del_dir(Dir)
catch
_:{badmatch, {error, Error}} ->
{error, Error}
end.
-spec semver_to_xxyy(binary()) -> binary().
semver_to_xxyy(<>) ->
<>;
semver_to_xxyy(<>) ->
<>;
semver_to_xxyy(<>) ->
<>;
semver_to_xxyy(Version) when is_binary(Version) ->
Version.
%%%===================================================================
%%% Internal functions
%%%===================================================================
-spec url_encode(binary(), binary()) -> binary().
url_encode(<>, Acc) when
(H >= $a andalso H =< $z) orelse
(H >= $A andalso H =< $Z) orelse
(H >= $0 andalso H =< $9) orelse
H == $_ orelse
H == $. orelse
H == $- orelse
H == $/ orelse
H == $: ->
url_encode(T, <>);
url_encode(<>, Acc) ->
case integer_to_list(H, 16) of
[X, Y] -> url_encode(T, <>);
[X] -> url_encode(T, <>)
end;
url_encode(<<>>, Acc) ->
Acc.
-spec set_node_id(string(), binary()) -> pid().
set_node_id(PidStr, NodeBin) ->
ExtPidStr = erlang:pid_to_list(
binary_to_term(
<<131,103,100,(size(NodeBin)):16,NodeBin/binary,0:72>>)),
[H|_] = string:tokens(ExtPidStr, "."),
[_|T] = string:tokens(PidStr, "."),
erlang:list_to_pid(string:join([H|T], ".")).
-spec read_file(file:filename()) -> {ok, binary()} | {error, file:posix()}.
read_file(Path) ->
case file:read_file(Path) of
{ok, Data} ->
{ok, Data};
{error, Why} = Err ->
?ERROR_MSG("Failed to read file ~ts: ~ts",
[Path, file:format_error(Why)]),
Err
end.
-spec get_dir(string()) -> file:filename().
get_dir(Type) ->
Env = "EJABBERD_" ++ string:to_upper(Type) ++ "_PATH",
case os:getenv(Env) of
false ->
case code:priv_dir(ejabberd) of
{error, _} -> filename:join(["priv", Type]);
Path -> filename:join([Path, Type])
end;
Path ->
Path
end.
%% Generates erlang:timestamp() that is guaranteed to unique
-spec unique_timestamp() -> erlang:timestamp().
unique_timestamp() ->
{MS, S, _} = erlang:timestamp(),
{MS, S, erlang:unique_integer([positive, monotonic]) rem 1000000}.
%% Levenshtein distance
-spec ld(string(), string(), distance_cache()) -> {non_neg_integer(), distance_cache()}.
ld([] = S, T, Cache) ->
{length(T), maps:put({S, T}, length(T), Cache)};
ld(S, [] = T, Cache) ->
{length(S), maps:put({S, T}, length(S), Cache)};
ld([X|S], [X|T], Cache) ->
ld(S, T, Cache);
ld([_|ST] = S, [_|TT] = T, Cache) ->
try {maps:get({S, T}, Cache), Cache}
catch _:{badkey, _} ->
{L1, C1} = ld(S, TT, Cache),
{L2, C2} = ld(ST, T, C1),
{L3, C3} = ld(ST, TT, C2),
L = 1 + lists:min([L1, L2, L3]),
{L, maps:put({S, T}, L, C3)}
end.
-spec ip_to_integer(inet:ip_address()) -> non_neg_integer().
ip_to_integer({IP1, IP2, IP3, IP4}) ->
IP1 bsl 8 bor IP2 bsl 8 bor IP3 bsl 8 bor IP4;
ip_to_integer({IP1, IP2, IP3, IP4, IP5, IP6, IP7,
IP8}) ->
IP1 bsl 16 bor IP2 bsl 16 bor IP3 bsl 16 bor IP4 bsl 16
bor IP5 bsl 16 bor IP6 bsl 16 bor IP7 bsl 16 bor IP8.
-spec to_string(atom() | binary() | string()) -> string().
to_string(A) when is_atom(A) ->
atom_to_list(A);
to_string(B) when is_binary(B) ->
binary_to_list(B);
to_string(S) ->
S.
-ifdef(OTP_BELOW_27).
set_proc_label(_Label) ->
ok.
-else.
set_proc_label(Label) ->
proc_lib:set_label(Label).
-endif.
ejabberd-24.12/src/ejabberd_c2s.erl 0000664 0001750 0001750 00000115144 14730775155 017464 0 ustar debalance debalance %%%-------------------------------------------------------------------
%%% Created : 8 Dec 2016 by Evgeny Khramtsov
%%%
%%%
%%% 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(ejabberd_c2s).
-behaviour(xmpp_stream_in).
-behaviour(ejabberd_listener).
-protocol({rfc, 3920}).
-protocol({rfc, 3921}).
-protocol({rfc, 6120}).
-protocol({rfc, 6121}).
-protocol({xep, 138, '2.1', '1.1.0', "complete", ""}).
%% ejabberd_listener callbacks
-export([start/3, start_link/3, accept/1, listen_opt_type/1, listen_options/0]).
%% xmpp_stream_in callbacks
-export([init/1, handle_call/3, handle_cast/2,
handle_info/2, terminate/2, code_change/3]).
-export([tls_options/1, tls_required/1, tls_enabled/1,
allow_unencrypted_sasl2/1, compress_methods/1, bind/2,
sasl_mechanisms/2, get_password_fun/2, check_password_fun/2,
check_password_digest_fun/2, unauthenticated_stream_features/1,
authenticated_stream_features/1, handle_stream_start/2,
handle_stream_end/2, handle_unauthenticated_packet/2,
handle_authenticated_packet/2, handle_auth_success/4,
handle_auth_failure/4, handle_send/3, handle_recv/3, handle_cdata/2,
handle_unbinded_packet/2, inline_stream_features/1,
handle_sasl2_inline/2, handle_sasl2_inline_post/3,
handle_bind2_inline/2, handle_bind2_inline_post/3, sasl_options/1,
handle_sasl2_task_next/4, handle_sasl2_task_data/3,
get_fast_tokens_fun/2, fast_mechanisms/1]).
%% Hooks
-export([handle_unexpected_cast/2, handle_unexpected_call/3,
process_auth_result/3, reject_unauthenticated_packet/2,
process_closed/2, process_terminated/2, process_info/2]).
%% API
-export([get_presence/1, set_presence/2, resend_presence/1, resend_presence/2,
open_session/1, call/3, cast/2, send/2, close/1, close/2, stop_async/1,
reply/2, copy_state/2, set_timeout/2, route/2, format_reason/2,
host_up/1, host_down/1, send_ws_ping/1, bounce_message_queue/2,
reset_vcard_xupdate_resend_presence/1]).
-include_lib("xmpp/include/xmpp.hrl").
-include("logger.hrl").
-include("mod_roster.hrl").
-include("translate.hrl").
-define(SETS, gb_sets).
-type state() :: xmpp_stream_in:state().
-export_type([state/0]).
%%%===================================================================
%%% ejabberd_listener API
%%%===================================================================
start(SockMod, Socket, Opts) ->
xmpp_stream_in:start(?MODULE, [{SockMod, Socket}, Opts],
ejabberd_config:fsm_limit_opts(Opts)).
start_link(SockMod, Socket, Opts) ->
xmpp_stream_in:start_link(?MODULE, [{SockMod, Socket}, Opts],
ejabberd_config:fsm_limit_opts(Opts)).
accept(Ref) ->
xmpp_stream_in:accept(Ref).
%%%===================================================================
%%% Common API
%%%===================================================================
-spec call(pid(), term(), non_neg_integer() | infinity) -> term().
call(Ref, Msg, Timeout) ->
xmpp_stream_in:call(Ref, Msg, Timeout).
-spec cast(pid(), term()) -> ok.
cast(Ref, Msg) ->
xmpp_stream_in:cast(Ref, Msg).
reply(Ref, Reply) ->
xmpp_stream_in:reply(Ref, Reply).
-spec get_presence(pid()) -> presence().
get_presence(Ref) ->
call(Ref, get_presence, 1000).
-spec set_presence(pid(), presence()) -> ok.
set_presence(Ref, Pres) ->
call(Ref, {set_presence, Pres}, 1000).
-spec resend_presence(pid()) -> boolean().
resend_presence(Pid) ->
resend_presence(Pid, undefined).
-spec resend_presence(pid(), jid() | undefined) -> boolean().
resend_presence(Pid, To) ->
route(Pid, {resend_presence, To}).
-spec reset_vcard_xupdate_resend_presence(pid()) -> boolean().
reset_vcard_xupdate_resend_presence(Pid) ->
route(Pid, reset_vcard_xupdate_resend_presence).
-spec close(pid()) -> ok;
(state()) -> state().
close(Ref) ->
xmpp_stream_in:close(Ref).
-spec close(pid(), atom()) -> ok.
close(Ref, Reason) ->
xmpp_stream_in:close(Ref, Reason).
-spec stop_async(pid()) -> ok.
stop_async(Pid) ->
xmpp_stream_in:stop_async(Pid).
-spec send(pid(), xmpp_element()) -> ok;
(state(), xmpp_element()) -> state().
send(Pid, Pkt) when is_pid(Pid) ->
xmpp_stream_in:send(Pid, Pkt);
send(#{lserver := LServer} = State, Pkt) ->
Pkt1 = fix_from_to(Pkt, State),
case ejabberd_hooks:run_fold(c2s_filter_send, LServer, {Pkt1, State}, []) of
{drop, State1} -> State1;
{Pkt2, State1} -> xmpp_stream_in:send(State1, Pkt2)
end.
-spec send_error(state(), xmpp_element(), stanza_error()) -> state().
send_error(#{lserver := LServer} = State, Pkt, Err) ->
case ejabberd_hooks:run_fold(c2s_filter_send, LServer, {Pkt, State}, []) of
{drop, State1} -> State1;
{Pkt1, State1} -> xmpp_stream_in:send_error(State1, Pkt1, Err)
end.
-spec send_ws_ping(pid()) -> ok;
(state()) -> state().
send_ws_ping(Ref) ->
xmpp_stream_in:send_ws_ping(Ref).
-spec route(pid(), term()) -> boolean().
route(Pid, Term) ->
ejabberd_cluster:send(Pid, Term).
-spec set_timeout(state(), timeout()) -> state().
set_timeout(State, Timeout) ->
xmpp_stream_in:set_timeout(State, Timeout).
-spec host_up(binary()) -> ok.
host_up(Host) ->
ejabberd_hooks:add(c2s_closed, Host, ?MODULE, process_closed, 100),
ejabberd_hooks:add(c2s_terminated, Host, ?MODULE,
process_terminated, 100),
ejabberd_hooks:add(c2s_unauthenticated_packet, Host, ?MODULE,
reject_unauthenticated_packet, 100),
ejabberd_hooks:add(c2s_handle_info, Host, ?MODULE,
process_info, 100),
ejabberd_hooks:add(c2s_auth_result, Host, ?MODULE,
process_auth_result, 100),
ejabberd_hooks:add(c2s_handle_cast, Host, ?MODULE,
handle_unexpected_cast, 100),
ejabberd_hooks:add(c2s_handle_call, Host, ?MODULE,
handle_unexpected_call, 100).
-spec host_down(binary()) -> ok.
host_down(Host) ->
ejabberd_hooks:delete(c2s_closed, Host, ?MODULE, process_closed, 100),
ejabberd_hooks:delete(c2s_terminated, Host, ?MODULE,
process_terminated, 100),
ejabberd_hooks:delete(c2s_unauthenticated_packet, Host, ?MODULE,
reject_unauthenticated_packet, 100),
ejabberd_hooks:delete(c2s_handle_info, Host, ?MODULE,
process_info, 100),
ejabberd_hooks:delete(c2s_auth_result, Host, ?MODULE,
process_auth_result, 100),
ejabberd_hooks:delete(c2s_handle_cast, Host, ?MODULE,
handle_unexpected_cast, 100),
ejabberd_hooks:delete(c2s_handle_call, Host, ?MODULE,
handle_unexpected_call, 100).
%% Copies content of one c2s state to another.
%% This is needed for session migration from one pid to another.
-spec copy_state(state(), state()) -> state().
copy_state(NewState,
#{jid := JID, resource := Resource, auth_module := AuthModule,
lserver := LServer, pres_a := PresA} = OldState) ->
State1 = case OldState of
#{pres_last := Pres, pres_timestamp := PresTS} ->
NewState#{pres_last => Pres, pres_timestamp => PresTS};
_ ->
NewState
end,
Conn = get_conn_type(State1),
State2 = State1#{jid => JID, resource => Resource,
conn => Conn,
auth_module => AuthModule,
pres_a => PresA},
ejabberd_hooks:run_fold(c2s_copy_session, LServer, State2, [OldState]).
-spec open_session(state()) -> {ok, state()} | state().
open_session(#{user := U, server := S, resource := R,
sid := SID, ip := IP, auth_module := AuthModule} = State) ->
JID = jid:make(U, S, R),
State1 = change_shaper(State),
Conn = get_conn_type(State1),
State2 = State1#{conn => Conn, resource => R, jid => JID},
Prio = case maps:get(pres_last, State, undefined) of
undefined -> undefined;
Pres -> get_priority_from_presence(Pres)
end,
Info = [{ip, IP}, {conn, Conn}, {auth_module, AuthModule}],
case State of
#{bind2_session_id := Tag} ->
ejabberd_sm:open_session(SID, U, S, R, Prio, Info, Tag);
_ ->
ejabberd_sm:open_session(SID, U, S, R, Prio, Info)
end,
xmpp_stream_in:establish(State2).
%%%===================================================================
%%% Hooks
%%%===================================================================
process_info(#{lserver := LServer} = State, {route, Packet}) ->
{Pass, State1} = case Packet of
#presence{} ->
process_presence_in(State, Packet);
#message{} ->
process_message_in(State, Packet);
#iq{} ->
process_iq_in(State, Packet)
end,
if Pass ->
{Packet1, State2} = ejabberd_hooks:run_fold(
user_receive_packet, LServer,
{Packet, State1}, []),
case Packet1 of
drop -> State2;
_ -> send(State2, Packet1)
end;
true ->
State1
end;
process_info(State, reset_vcard_xupdate_resend_presence) ->
case maps:get(pres_last, State, error) of
error -> State;
Pres ->
Pres2 = xmpp:remove_subtag(Pres, #vcard_xupdate{}),
process_self_presence(State#{pres_last => Pres2}, Pres2)
end;
process_info(#{jid := JID} = State, {resend_presence, To}) ->
case maps:get(pres_last, State, error) of
error -> State;
Pres when To == undefined ->
process_self_presence(State, Pres);
Pres when To#jid.luser == JID#jid.luser andalso
To#jid.lserver == JID#jid.lserver andalso
To#jid.lresource == <<"">> ->
process_self_presence(State, Pres);
Pres ->
process_presence_out(State, xmpp:set_to(Pres, To))
end;
process_info(State, Info) ->
?WARNING_MSG("Unexpected info: ~p", [Info]),
State.
handle_unexpected_call(State, From, Msg) ->
?WARNING_MSG("Unexpected call from ~p: ~p", [From, Msg]),
State.
handle_unexpected_cast(State, Msg) ->
?WARNING_MSG("Unexpected cast: ~p", [Msg]),
State.
reject_unauthenticated_packet(State, _Pkt) ->
Err = xmpp:serr_not_authorized(),
send(State, Err).
process_auth_result(#{sasl_mech := Mech, auth_module := AuthModule,
socket := Socket, ip := IP, lserver := LServer} = State,
true, User) ->
misc:set_proc_label({?MODULE, User, LServer}),
?INFO_MSG("(~ts) Accepted c2s ~ts authentication for ~ts@~ts by ~ts backend from ~ts",
[xmpp_socket:pp(Socket), Mech, User, LServer,
ejabberd_auth:backend_type(AuthModule),
ejabberd_config:may_hide_data(misc:ip_to_list(IP))]),
State;
process_auth_result(#{sasl_mech := Mech,
socket := Socket, ip := IP, lserver := LServer} = State,
{false, Reason}, User) ->
?WARNING_MSG("(~ts) Failed c2s ~ts authentication ~tsfrom ~ts: ~ts",
[xmpp_socket:pp(Socket), Mech,
if User /= <<"">> -> ["for ", User, "@", LServer, " "];
true -> ""
end,
ejabberd_config:may_hide_data(misc:ip_to_list(IP)), Reason]),
State.
process_closed(State, Reason) ->
stop_async(self()),
State#{stop_reason => Reason}.
process_terminated(#{sid := SID, jid := JID, user := U, server := S, resource := R} = State,
Reason) ->
Status = format_reason(State, Reason),
?INFO_MSG("(~ts) Closing c2s session for ~ts: ~ts",
[case maps:find(socket, State) of
{ok, Socket} -> xmpp_socket:pp(Socket);
_ -> <<"unknown">>
end, jid:encode(JID), Status]),
Pres = #presence{type = unavailable,
from = JID,
to = jid:remove_resource(JID)},
State1 = case maps:is_key(pres_last, State) of
true ->
ejabberd_sm:close_session_unset_presence(SID, U, S, R,
Status),
broadcast_presence_unavailable(State, Pres, true);
false ->
ejabberd_sm:close_session(SID, U, S, R),
broadcast_presence_unavailable(State, Pres, false)
end,
bounce_message_queue(SID, JID),
State1;
process_terminated(#{stop_reason := {tls, _}} = State, Reason) ->
?WARNING_MSG("(~ts) Failed to secure c2s connection: ~ts",
[case maps:find(socket, State) of
{ok, Socket} -> xmpp_socket:pp(Socket);
_ -> <<"unknown">>
end, format_reason(State, Reason)]),
State;
process_terminated(State, _Reason) ->
State.
%%%===================================================================
%%% xmpp_stream_in callbacks
%%%===================================================================
tls_options(#{lserver := LServer, tls_options := DefaultOpts,
stream_encrypted := Encrypted}) ->
TLSOpts1 = case {Encrypted, proplists:get_value(certfile, DefaultOpts)} of
{true, CertFile} when CertFile /= undefined -> DefaultOpts;
{_, _} ->
case ejabberd_pkix:get_certfile(LServer) of
error -> DefaultOpts;
{ok, CertFile} ->
lists:keystore(certfile, 1, DefaultOpts,
{certfile, CertFile})
end
end,
TLSOpts2 = case ejabberd_option:c2s_ciphers(LServer) of
undefined -> TLSOpts1;
Ciphers -> lists:keystore(ciphers, 1, TLSOpts1,
{ciphers, Ciphers})
end,
TLSOpts3 = case ejabberd_option:c2s_protocol_options(LServer) of
undefined -> TLSOpts2;
ProtoOpts -> lists:keystore(protocol_options, 1, TLSOpts2,
{protocol_options, ProtoOpts})
end,
TLSOpts4 = case ejabberd_option:c2s_dhfile(LServer) of
undefined -> TLSOpts3;
DHFile -> lists:keystore(dhfile, 1, TLSOpts3,
{dhfile, DHFile})
end,
TLSOpts5 = case ejabberd_option:c2s_cafile(LServer) of
undefined -> TLSOpts4;
CAFile -> lists:keystore(cafile, 1, TLSOpts4,
{cafile, CAFile})
end,
case ejabberd_option:c2s_tls_compression(LServer) of
undefined -> TLSOpts5;
false -> [compression_none | TLSOpts5];
true -> lists:delete(compression_none, TLSOpts5)
end.
tls_required(#{tls_required := TLSRequired}) ->
TLSRequired.
tls_enabled(#{tls_enabled := TLSEnabled,
tls_required := TLSRequired,
tls_verify := TLSVerify}) ->
TLSEnabled or TLSRequired or TLSVerify.
allow_unencrypted_sasl2(#{allow_unencrypted_sasl2 := AllowUnencryptedSasl2}) ->
AllowUnencryptedSasl2.
compress_methods(#{zlib := true}) ->
[<<"zlib">>];
compress_methods(_) ->
[].
unauthenticated_stream_features(#{lserver := LServer}) ->
ejabberd_hooks:run_fold(c2s_pre_auth_features, LServer, [], [LServer]).
authenticated_stream_features(#{lserver := LServer}) ->
ejabberd_hooks:run_fold(c2s_post_auth_features, LServer, [], [LServer]).
inline_stream_features(#{lserver := LServer}) ->
ejabberd_hooks:run_fold(c2s_inline_features, LServer, {[], [], []}, [LServer]).
sasl_mechanisms(Mechs, #{lserver := LServer, stream_encrypted := Encrypted} = State) ->
Type = ejabberd_auth:store_type(LServer),
Mechs1 = ejabberd_option:disable_sasl_mechanisms(LServer),
ScramHash = ejabberd_option:auth_scram_hash(LServer),
ShaAv = Type == plain orelse (Type == scram andalso ScramHash == sha),
Sha256Av = Type == plain orelse (Type == scram andalso ScramHash == sha256),
Sha512Av = Type == plain orelse (Type == scram andalso ScramHash == sha512),
%% I re-created it from cyrsasl ets magic, but I think it's wrong
%% TODO: need to check before 18.09 release
lists:filter(
fun(<<"ANONYMOUS">>) ->
ejabberd_auth_anonymous:is_sasl_anonymous_enabled(LServer);
(<<"DIGEST-MD5">>) -> Type == plain;
(<<"SCRAM-SHA-1">>) -> ShaAv;
(<<"SCRAM-SHA-1-PLUS">>) -> ShaAv andalso Encrypted;
(<<"SCRAM-SHA-256">>) -> Sha256Av;
(<<"SCRAM-SHA-256-PLUS">>) -> Sha256Av andalso Encrypted;
(<<"SCRAM-SHA-512">>) -> Sha512Av;
(<<"SCRAM-SHA-512-PLUS">>) -> Sha512Av andalso Encrypted;
(<<"PLAIN">>) -> true;
(<<"X-OAUTH2">>) -> [ejabberd_auth_anonymous] /= ejabberd_auth:auth_modules(LServer);
(<<"EXTERNAL">>) -> maps:get(tls_verify, State, false);
(_) -> false
end, Mechs -- Mechs1).
sasl_options(#{lserver := LServer}) ->
case ejabberd_option:disable_sasl_scram_downgrade_protection(LServer) of
true -> [{scram_downgrade_protection, false}];
_ -> []
end.
get_password_fun(_Mech, #{lserver := LServer}) ->
fun(U) ->
ejabberd_auth:get_password_with_authmodule(U, LServer)
end.
check_password_fun(<<"X-OAUTH2">>, #{lserver := LServer}) ->
fun(User, _AuthzId, Token) ->
case ejabberd_oauth:check_token(
User, LServer, [<<"sasl_auth">>], Token) of
true -> {true, ejabberd_oauth};
_ -> {false, ejabberd_oauth}
end
end;
check_password_fun(_Mech, #{lserver := LServer}) ->
fun(U, AuthzId, P) ->
ejabberd_auth:check_password_with_authmodule(U, AuthzId, LServer, P)
end.
check_password_digest_fun(_Mech, #{lserver := LServer}) ->
fun(U, AuthzId, P, D, DG) ->
ejabberd_auth:check_password_with_authmodule(U, AuthzId, LServer, P, D, DG)
end.
get_fast_tokens_fun(_Mech, #{lserver := LServer}) ->
fun(User, UA) ->
case gen_mod:is_loaded(LServer, mod_auth_fast) of
false -> false;
_ -> mod_auth_fast:get_tokens(LServer, User, UA)
end
end.
fast_mechanisms(#{lserver := LServer}) ->
case gen_mod:is_loaded(LServer, mod_auth_fast) of
false -> [];
_ -> mod_auth_fast:get_mechanisms(LServer)
end.
bind(<<"">>, State) ->
bind(new_uniq_id(), State);
bind(R, #{user := U, server := S, access := Access, lang := Lang,
lserver := LServer, socket := Socket,
ip := IP} = State) ->
case resource_conflict_action(U, S, R) of
closenew ->
{error, xmpp:err_conflict(), State};
{accept_resource, Resource} ->
JID = jid:make(U, S, Resource),
case acl:match_rule(LServer, Access,
#{usr => jid:split(JID), ip => IP}) of
allow ->
State1 = open_session(State#{resource => Resource,
sid => ejabberd_sm:make_sid()}),
State2 = ejabberd_hooks:run_fold(
c2s_session_opened, LServer, State1, []),
?INFO_MSG("(~ts) Opened c2s session for ~ts",
[xmpp_socket:pp(Socket), jid:encode(JID)]),
{ok, State2};
deny ->
ejabberd_hooks:run(forbidden_session_hook, LServer, [JID]),
?WARNING_MSG("(~ts) Forbidden c2s session for ~ts",
[xmpp_socket:pp(Socket), jid:encode(JID)]),
Txt = ?T("Access denied by service policy"),
{error, xmpp:err_not_allowed(Txt, Lang), State}
end
end.
handle_stream_start(StreamStart, #{lserver := LServer} = State) ->
case ejabberd_router:is_my_host(LServer) of
false ->
send(State#{lserver => ejabberd_config:get_myname()}, xmpp:serr_host_unknown());
true ->
State1 = change_shaper(State),
Opts = ejabberd_config:codec_options(),
State2 = State1#{codec_options => Opts},
ejabberd_hooks:run_fold(
c2s_stream_started, LServer, State2, [StreamStart])
end.
handle_stream_end(Reason, #{lserver := LServer} = State) ->
State1 = State#{stop_reason => Reason},
ejabberd_hooks:run_fold(c2s_closed, LServer, State1, [Reason]).
handle_auth_success(User, _Mech, AuthModule,
#{lserver := LServer} = State) ->
State1 = State#{auth_module => AuthModule},
ejabberd_hooks:run_fold(c2s_auth_result, LServer, State1, [true, User]).
handle_auth_failure(User, _Mech, Reason,
#{lserver := LServer} = State) ->
ejabberd_hooks:run_fold(c2s_auth_result, LServer, State, [{false, Reason}, User]).
handle_unbinded_packet(Pkt, #{lserver := LServer} = State) ->
ejabberd_hooks:run_fold(c2s_unbinded_packet, LServer, State, [Pkt]).
handle_unauthenticated_packet(Pkt, #{lserver := LServer} = State) ->
ejabberd_hooks:run_fold(c2s_unauthenticated_packet, LServer, State, [Pkt]).
handle_authenticated_packet(Pkt, #{lserver := LServer} = State) when not ?is_stanza(Pkt) ->
ejabberd_hooks:run_fold(c2s_authenticated_packet,
LServer, State, [Pkt]);
handle_authenticated_packet(Pkt, #{lserver := LServer, jid := JID,
ip := {IP, _}} = State) ->
Pkt1 = xmpp:put_meta(Pkt, ip, IP),
State1 = ejabberd_hooks:run_fold(c2s_authenticated_packet,
LServer, State, [Pkt1]),
#jid{luser = LUser} = JID,
{Pkt2, State2} = ejabberd_hooks:run_fold(
user_send_packet, LServer, {Pkt1, State1}, []),
case Pkt2 of
drop ->
State2;
#iq{type = set, sub_els = [_]} ->
try xmpp:try_subtag(Pkt2, #xmpp_session{}) of
#xmpp_session{} ->
% It seems that some client are expecting to have response
% to session request be sent from server jid, let's make
% sure it is that.
Pkt3 = xmpp:set_to(Pkt2, jid:make(<<>>, LServer, <<>>)),
send(State2, xmpp:make_iq_result(Pkt3));
_ ->
check_privacy_then_route(State2, Pkt2)
catch _:{xmpp_codec, Why} ->
Txt = xmpp:io_format_error(Why),
Lang = maps:get(lang, State),
Err = xmpp:err_bad_request(Txt, Lang),
send_error(State2, Pkt2, Err)
end;
#presence{to = #jid{luser = LUser, lserver = LServer,
lresource = <<"">>}} ->
process_self_presence(State2, Pkt2);
#presence{} ->
process_presence_out(State2, Pkt2);
_ ->
check_privacy_then_route(State2, Pkt2)
end.
handle_cdata(Data, #{lserver := LServer} = State) ->
ejabberd_hooks:run_fold(c2s_handle_cdata, LServer,
State, [Data]).
handle_sasl2_inline(Els, #{lserver := LServer} = State) ->
ejabberd_hooks:run_fold(c2s_handle_sasl2_inline, LServer,
{State, Els, []}, []).
handle_sasl2_inline_post(Els, Results, #{lserver := LServer} = State) ->
ejabberd_hooks:run_fold(c2s_handle_sasl2_inline_post, LServer,
State, [Els, Results]).
handle_bind2_inline(Els, #{lserver := LServer} = State) ->
ejabberd_hooks:run_fold(c2s_handle_bind2_inline, LServer,
{State, Els, []}, []).
handle_bind2_inline_post(Els, Results, #{lserver := LServer} = State) ->
ejabberd_hooks:run_fold(c2s_handle_bind2_inline_post, LServer,
State, [Els, Results]).
handle_sasl2_task_next(Task, Els, InlineEls, #{lserver := LServer} = State) ->
ejabberd_hooks:run_fold(c2s_handle_sasl2_task_next, LServer,
{abort, State}, [Task, Els, InlineEls]).
handle_sasl2_task_data(Els, InlineEls, #{lserver := LServer} = State) ->
ejabberd_hooks:run_fold(c2s_handle_sasl2_task_data, LServer,
{abort, State}, [Els, InlineEls]).
handle_recv(El, Pkt, #{lserver := LServer} = State) ->
ejabberd_hooks:run_fold(c2s_handle_recv, LServer, State, [El, Pkt]).
handle_send(Pkt, Result, #{lserver := LServer} = State) ->
ejabberd_hooks:run_fold(c2s_handle_send, LServer, State, [Pkt, Result]).
init([State, Opts]) ->
Access = proplists:get_value(access, Opts, all),
Shaper = proplists:get_value(shaper, Opts, none),
TLSOpts1 = lists:filter(
fun({certfile, _}) -> true;
({ciphers, _}) -> true;
({dhfile, _}) -> true;
({cafile, _}) -> true;
({protocol_options, _}) -> true;
(_) -> false
end, Opts),
TLSOpts2 = case proplists:get_bool(tls_compression, Opts) of
false -> [compression_none | TLSOpts1];
true -> TLSOpts1
end,
TLSEnabled = proplists:get_bool(starttls, Opts),
TLSRequired = proplists:get_bool(starttls_required, Opts),
TLSVerify = proplists:get_bool(tls_verify, Opts),
AllowUnencryptedSasl2 = proplists:get_bool(allow_unencrypted_sasl2, Opts),
Zlib = proplists:get_bool(zlib, Opts),
Timeout = ejabberd_option:negotiation_timeout(),
State1 = State#{tls_options => TLSOpts2,
tls_required => TLSRequired,
tls_enabled => TLSEnabled,
tls_verify => TLSVerify,
allow_unencrypted_sasl2 => AllowUnencryptedSasl2,
pres_a => ?SETS:new(),
zlib => Zlib,
lang => ejabberd_option:language(),
server => ejabberd_config:get_myname(),
lserver => ejabberd_config:get_myname(),
access => Access,
shaper => Shaper},
State2 = xmpp_stream_in:set_timeout(State1, Timeout),
misc:set_proc_label({?MODULE, init_state}),
ejabberd_hooks:run_fold(c2s_init, {ok, State2}, [Opts]).
handle_call(get_presence, From, #{jid := JID} = State) ->
Pres = case maps:get(pres_last, State, error) of
error ->
BareJID = jid:remove_resource(JID),
#presence{from = JID, to = BareJID, type = unavailable};
P -> P
end,
reply(From, Pres),
State;
handle_call({set_presence, Pres}, From, State) ->
reply(From, ok),
process_self_presence(State, Pres);
handle_call(Request, From, #{lserver := LServer} = State) ->
ejabberd_hooks:run_fold(
c2s_handle_call, LServer, State, [Request, From]).
handle_cast(Msg, #{lserver := LServer} = State) ->
ejabberd_hooks:run_fold(c2s_handle_cast, LServer, State, [Msg]).
handle_info(Info, #{lserver := LServer} = State) ->
ejabberd_hooks:run_fold(c2s_handle_info, LServer, State, [Info]).
terminate(Reason, #{lserver := LServer} = State) ->
ejabberd_hooks:run_fold(c2s_terminated, LServer, State, [Reason]).
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
%%%===================================================================
%%% Internal functions
%%%===================================================================
-spec process_iq_in(state(), iq()) -> {boolean(), state()}.
process_iq_in(State, #iq{} = IQ) ->
case privacy_check_packet(State, IQ, in) of
allow ->
{true, State};
deny ->
ejabberd_router:route_error(IQ, xmpp:err_service_unavailable()),
{false, State}
end.
-spec process_message_in(state(), message()) -> {boolean(), state()}.
process_message_in(State, #message{type = T} = Msg) ->
%% This function should be as simple as process_iq_in/2,
%% however, we don't route errors to MUC rooms in order
%% to avoid kicking us, because having a MUC room's JID blocked
%% most likely means having only some particular participant
%% blocked, i.e. room@conference.server.org/participant.
case privacy_check_packet(State, Msg, in) of
allow ->
{true, State};
deny when T == groupchat; T == headline ->
{false, State};
deny ->
case xmpp:has_subtag(Msg, #muc_user{}) of
true ->
ok;
false ->
ejabberd_router:route_error(
Msg, xmpp:err_service_unavailable())
end,
{false, State}
end.
-spec process_presence_in(state(), presence()) -> {boolean(), state()}.
process_presence_in(#{lserver := LServer, pres_a := PresA} = State0,
#presence{from = From, type = T} = Pres) ->
State = ejabberd_hooks:run_fold(c2s_presence_in, LServer, State0, [Pres]),
case T of
probe ->
route_probe_reply(From, State),
{false, State};
error ->
A = ?SETS:del_element(jid:tolower(From), PresA),
{true, State#{pres_a => A}};
_ ->
case privacy_check_packet(State, Pres, in) of
allow ->
{true, State};
deny ->
{false, State}
end
end.
-spec route_probe_reply(jid(), state()) -> ok.
route_probe_reply(From, #{jid := To,
pres_last := LastPres,
pres_timestamp := TS} = State) ->
{LUser, LServer, LResource} = jid:tolower(To),
IsAnotherResource = case jid:tolower(From) of
{LUser, LServer, R} when R /= LResource -> true;
_ -> false
end,
Subscription = get_subscription(To, From),
if IsAnotherResource orelse
Subscription == both orelse Subscription == from ->
Packet = xmpp:set_from_to(LastPres, To, From),
Packet2 = misc:add_delay_info(Packet, To, TS),
case privacy_check_packet(State, Packet2, out) of
deny ->
ok;
allow ->
ejabberd_hooks:run(presence_probe_hook,
LServer,
[From, To, self()]),
ejabberd_router:route(Packet2)
end;
true ->
ok
end;
route_probe_reply(_, _) ->
ok.
-spec process_presence_out(state(), presence()) -> state().
process_presence_out(#{lserver := LServer, jid := JID,
lang := Lang, pres_a := PresA} = State0,
#presence{from = From, to = To, type = Type} = Pres) ->
State1 =
if Type == subscribe; Type == subscribed;
Type == unsubscribe; Type == unsubscribed ->
Access = mod_roster_opt:access(LServer),
MyBareJID = jid:remove_resource(JID),
case acl:match_rule(LServer, Access, MyBareJID) of
deny ->
AccessErrTxt = ?T("Access denied by service policy"),
AccessErr = xmpp:err_forbidden(AccessErrTxt, Lang),
send_error(State0, Pres, AccessErr);
allow ->
ejabberd_hooks:run(roster_out_subscription, LServer, [Pres]),
State0
end;
true ->
State0
end,
case privacy_check_packet(State1, Pres, out) of
deny ->
PrivErrTxt = ?T("Your active privacy list has denied "
"the routing of this stanza."),
PrivErr = xmpp:err_not_acceptable(PrivErrTxt, Lang),
send_error(State1, Pres, PrivErr);
allow when Type == subscribe; Type == subscribed;
Type == unsubscribe; Type == unsubscribed ->
BareFrom = jid:remove_resource(From),
ejabberd_router:route(xmpp:set_from_to(Pres, BareFrom, To)),
State1;
allow when Type == error; Type == probe ->
ejabberd_router:route(Pres),
State1;
allow ->
ejabberd_router:route(Pres),
LTo = jid:tolower(To),
LBareTo = jid:remove_resource(LTo),
LBareFrom = jid:remove_resource(jid:tolower(From)),
if LBareTo /= LBareFrom ->
Subscription = get_subscription(From, To),
if Subscription /= both andalso Subscription /= from ->
A = case Type of
available -> ?SETS:add_element(LTo, PresA);
unavailable -> ?SETS:del_element(LTo, PresA)
end,
State1#{pres_a => A};
true ->
State1
end;
true ->
State1
end
end.
-spec process_self_presence(state(), presence()) -> state().
process_self_presence(#{lserver := LServer, sid := SID,
user := U, server := S, resource := R} = State,
#presence{type = unavailable} = Pres) ->
Status = xmpp:get_text(Pres#presence.status),
_ = ejabberd_sm:unset_presence(SID, U, S, R, Status),
{Pres1, State1} = ejabberd_hooks:run_fold(
c2s_self_presence, LServer, {Pres, State}, []),
State2 = broadcast_presence_unavailable(State1, Pres1, true),
maps:remove(pres_last, maps:remove(pres_timestamp, State2));
process_self_presence(#{lserver := LServer} = State,
#presence{type = available} = Pres) ->
PreviousPres = maps:get(pres_last, State, undefined),
_ = update_priority(State, Pres),
{Pres1, State1} = ejabberd_hooks:run_fold(
c2s_self_presence, LServer, {Pres, State}, []),
State2 = State1#{pres_last => Pres1,
pres_timestamp => erlang:timestamp()},
FromUnavailable = PreviousPres == undefined,
broadcast_presence_available(State2, Pres1, FromUnavailable);
process_self_presence(State, _Pres) ->
State.
-spec update_priority(state(), presence()) -> ok | {error, notfound}.
update_priority(#{sid := SID, user := U, server := S, resource := R},
Pres) ->
Priority = get_priority_from_presence(Pres),
ejabberd_sm:set_presence(SID, U, S, R, Priority, Pres).
-spec broadcast_presence_unavailable(state(), presence(), boolean()) -> state().
broadcast_presence_unavailable(#{jid := JID, pres_a := PresA} = State, Pres,
BroadcastToRoster) ->
#jid{luser = LUser, lserver = LServer} = JID,
BareJID = jid:tolower(jid:remove_resource(JID)),
Items1 = case BroadcastToRoster of
true ->
Roster = ejabberd_hooks:run_fold(roster_get, LServer,
[], [{LUser, LServer}]),
lists:foldl(
fun(#roster_item{jid = ItemJID, subscription = Sub}, Acc)
when Sub == both; Sub == from ->
maps:put(jid:tolower(ItemJID), 1, Acc);
(_, Acc) ->
Acc
end, #{BareJID => 1}, Roster);
_ ->
#{BareJID => 1}
end,
Items2 = ?SETS:fold(
fun(LJID, Acc) ->
maps:put(LJID, 1, Acc)
end, Items1, PresA),
JIDs = lists:filtermap(
fun(LJid) ->
To = jid:make(LJid),
P = xmpp:set_to(Pres, To),
case privacy_check_packet(State, P, out) of
allow -> {true, To};
deny -> false
end
end, maps:keys(Items2)),
route_multiple(State, JIDs, Pres),
State#{pres_a => ?SETS:new()}.
-spec broadcast_presence_available(state(), presence(), boolean()) -> state().
broadcast_presence_available(#{jid := JID} = State,
Pres, _FromUnavailable = true) ->
Probe = #presence{from = JID, type = probe},
#jid{luser = LUser, lserver = LServer} = JID,
BareJID = jid:remove_resource(JID),
Items = ejabberd_hooks:run_fold(roster_get, LServer,
[], [{LUser, LServer}]),
{FJIDs, TJIDs} =
lists:foldl(
fun(#roster_item{jid = To, subscription = Sub}, {F, T}) ->
F1 = if Sub == both orelse Sub == from ->
Pres1 = xmpp:set_to(Pres, To),
case privacy_check_packet(State, Pres1, out) of
allow -> [To|F];
deny -> F
end;
true -> F
end,
T1 = if Sub == both orelse Sub == to ->
Probe1 = xmpp:set_to(Probe, To),
case privacy_check_packet(State, Probe1, out) of
allow -> [To|T];
deny -> T
end;
true -> T
end,
{F1, T1}
end, {[BareJID], [BareJID]}, Items),
route_multiple(State, TJIDs, Probe),
route_multiple(State, FJIDs, Pres),
State;
broadcast_presence_available(#{jid := JID} = State,
Pres, _FromUnavailable = false) ->
#jid{luser = LUser, lserver = LServer} = JID,
BareJID = jid:remove_resource(JID),
Items = ejabberd_hooks:run_fold(
roster_get, LServer, [], [{LUser, LServer}]),
JIDs = lists:foldl(
fun(#roster_item{jid = To, subscription = Sub}, Tos)
when Sub == both orelse Sub == from ->
P = xmpp:set_to(Pres, To),
case privacy_check_packet(State, P, out) of
allow -> [To|Tos];
deny -> Tos
end;
(_, Tos) ->
Tos
end, [BareJID], Items),
route_multiple(State, JIDs, Pres),
State.
-spec check_privacy_then_route(state(), stanza()) -> state().
check_privacy_then_route(#{lang := Lang} = State, Pkt) ->
case privacy_check_packet(State, Pkt, out) of
deny ->
ErrText = ?T("Your active privacy list has denied "
"the routing of this stanza."),
Err = xmpp:err_not_acceptable(ErrText, Lang),
send_error(State, Pkt, Err);
allow ->
ejabberd_router:route(Pkt),
State
end.
-spec privacy_check_packet(state(), stanza(), in | out) -> allow | deny.
privacy_check_packet(#{lserver := LServer} = State, Pkt, Dir) ->
ejabberd_hooks:run_fold(privacy_check_packet, LServer, allow, [State, Pkt, Dir]).
-spec get_priority_from_presence(presence()) -> integer().
get_priority_from_presence(#presence{priority = Prio}) ->
case Prio of
undefined -> 0;
_ -> Prio
end.
-spec route_multiple(state(), [jid()], stanza()) -> ok.
route_multiple(#{lserver := LServer}, JIDs, Pkt) ->
From = xmpp:get_from(Pkt),
ejabberd_router_multicast:route_multicast(From, LServer, JIDs, Pkt, false).
get_subscription(#jid{luser = LUser, lserver = LServer}, JID) ->
{Subscription, _, _} = ejabberd_hooks:run_fold(
roster_get_jid_info, LServer, {none, none, []},
[LUser, LServer, JID]),
Subscription.
-spec resource_conflict_action(binary(), binary(), binary()) ->
{accept_resource, binary()} | closenew.
resource_conflict_action(U, S, R) ->
OptionRaw = case ejabberd_sm:is_existing_resource(U, S, R) of
true ->
ejabberd_option:resource_conflict(S);
false ->
acceptnew
end,
Option = case OptionRaw of
setresource -> setresource;
closeold -> acceptnew; %% ejabberd_sm will close old session
closenew -> closenew;
acceptnew -> acceptnew
end,
case Option of
acceptnew -> {accept_resource, R};
closenew -> closenew;
setresource ->
Rnew = new_uniq_id(),
{accept_resource, Rnew}
end.
-spec bounce_message_queue(ejabberd_sm:sid(), jid:jid()) -> ok.
bounce_message_queue({_, Pid} = SID, JID) ->
{U, S, R} = jid:tolower(JID),
SIDs = ejabberd_sm:get_session_sids(U, S, R),
case lists:member(SID, SIDs) of
true ->
?WARNING_MSG("The session for ~ts@~ts/~ts is supposed to "
"be unregistered, but session identifier ~p "
"still presents in the 'session' table",
[U, S, R, Pid]);
false ->
receive {route, Pkt} ->
ejabberd_router:route(Pkt),
bounce_message_queue(SID, JID)
after 100 ->
ok
end
end.
-spec new_uniq_id() -> binary().
new_uniq_id() ->
iolist_to_binary(
[p1_rand:get_string(),
integer_to_binary(erlang:unique_integer([positive]))]).
-spec get_conn_type(state()) -> c2s | c2s_tls | c2s_compressed | websocket |
c2s_compressed_tls | http_bind.
get_conn_type(State) ->
case xmpp_stream_in:get_transport(State) of
tcp -> c2s;
tls -> c2s_tls;
tcp_zlib -> c2s_compressed;
tls_zlib -> c2s_compressed_tls;
http_bind -> http_bind;
websocket -> websocket
end.
-spec fix_from_to(xmpp_element(), state()) -> stanza() | xmpp_element().
fix_from_to(Pkt, #{jid := JID}) when ?is_stanza(Pkt) ->
#jid{luser = U, lserver = S, lresource = R} = JID,
case xmpp:get_from(Pkt) of
undefined ->
Pkt;
From ->
From1 = case jid:tolower(From) of
{U, S, R} -> JID;
{U, S, _} -> jid:replace_resource(JID, From#jid.resource);
_ -> From
end,
To1 = case xmpp:get_to(Pkt) of
#jid{lresource = <<>>} = To2 -> To2;
_ -> JID
end,
xmpp:set_from_to(Pkt, From1, To1)
end;
fix_from_to(Pkt, _State) ->
Pkt.
-spec change_shaper(state()) -> state().
change_shaper(#{shaper := ShaperName, ip := {IP, _}, lserver := LServer,
user := U, server := S, resource := R} = State) ->
JID = jid:make(U, S, R),
Shaper = ejabberd_shaper:match(LServer, ShaperName,
#{usr => jid:split(JID), ip => IP}),
xmpp_stream_in:change_shaper(State, ejabberd_shaper:new(Shaper)).
-spec format_reason(state(), term()) -> binary().
format_reason(#{stop_reason := Reason}, _) ->
xmpp_stream_in:format_error(Reason);
format_reason(_, normal) ->
<<"unknown reason">>;
format_reason(_, shutdown) ->
<<"stopped by supervisor">>;
format_reason(_, {shutdown, _}) ->
<<"stopped by supervisor">>;
format_reason(_, _) ->
<<"internal server error">>.
listen_opt_type(starttls) ->
econf:bool();
listen_opt_type(starttls_required) ->
econf:bool();
listen_opt_type(allow_unencrypted_sasl2) ->
econf:bool();
listen_opt_type(tls_verify) ->
econf:bool();
listen_opt_type(zlib) ->
econf:and_then(
econf:bool(),
fun(false) -> false;
(true) ->
ejabberd:start_app(ezlib),
true
end).
listen_options() ->
[{access, all},
{shaper, none},
{ciphers, undefined},
{dhfile, undefined},
{cafile, undefined},
{protocol_options, undefined},
{tls, false},
{tls_compression, false},
{starttls, false},
{starttls_required, false},
{allow_unencrypted_sasl2, false},
{tls_verify, false},
{zlib, false},
{max_stanza_size, infinity},
{max_fsm_queue, 10000}].
ejabberd-24.12/src/mod_mqtt_bridge_session.erl 0000664 0001750 0001750 00000046161 14730775155 022064 0 ustar debalance debalance %%%-------------------------------------------------------------------
%%% @author Pawel Chmielowski
%%% @copyright (C) 2002-2024 ProcessOne, SARL. All Rights Reserved.
%%%
%%% Licensed under the Apache License, Version 2.0 (the "License");
%%% you may not use this file except in compliance with the License.
%%% You may obtain a copy of the License at
%%%
%%% http://www.apache.org/licenses/LICENSE-2.0
%%%
%%% Unless required by applicable law or agreed to in writing, software
%%% distributed under the License is distributed on an "AS IS" BASIS,
%%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%%% See the License for the specific language governing permissions and
%%% limitations under the License.
%%%
%%%-------------------------------------------------------------------
-module(mod_mqtt_bridge_session).
-behaviour(p1_server).
-define(VSN, 1).
-vsn(?VSN).
%% API
-export([start/9, start_link/9]).
%% gen_server callbacks
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
terminate/2, code_change/3]).
-include("logger.hrl").
-include("mqtt.hrl").
-include_lib("xmpp/include/xmpp.hrl").
-include_lib("public_key/include/public_key.hrl").
-type error_reason() ::
{auth, reason_code()} |
{code, reason_code()} |
{peer_disconnected, reason_code(), binary()} |
{socket, socket_error_reason()} |
{codec, mqtt_codec:error_reason()} |
{unexpected_packet, atom()} |
{tls, inet:posix() | atom() | binary()} |
{replaced, pid()} |
{resumed, pid()} |
subscribe_forbidden | publish_forbidden |
will_topic_forbidden | internal_server_error |
session_expired | idle_connection |
queue_full | shutdown | db_failure |
{payload_format_invalid, will | publish} |
session_expiry_non_zero | unknown_topic_alias.
-type socket() ::
{gen_tcp, inet:socket()} |
{fast_tls, fast_tls:tls_socket()} |
{mod_mqtt_ws, mod_mqtt_ws:socket()}.
-type seconds() :: non_neg_integer().
-type socket_error_reason() :: closed | timeout | inet:posix().
-define(PING_TIMEOUT, timer:seconds(50)).
-define(MAX_UINT32, 4294967295).
-record(state, {vsn = ?VSN :: integer(),
version :: undefined | mqtt_version(),
socket :: undefined | socket(),
usr :: undefined | {binary(), binary(), binary()},
ping_timer = undefined :: undefined | reference(),
stop_reason :: undefined | error_reason(),
subscriptions = #{},
publish = #{},
ws_codec = none,
id = 0 :: non_neg_integer(),
codec :: mqtt_codec:state(),
authentication :: #{username => binary(), password => binary(), certfile => binary()}}).
-type state() :: #state{}.
%%%===================================================================
%%% API
%%%===================================================================
start(Proc, Transport, Host, Port, Path, Publish, Subscribe, Authentication, ReplicationUser) ->
p1_server:start({local, Proc}, ?MODULE, [Proc, Transport, Host, Port, Path, Publish, Subscribe, Authentication,
ReplicationUser], []).
start_link(Proc, Transport, Host, Port, Path, Publish, Subscribe, Authentication, ReplicationUser) ->
p1_server:start_link({local, Proc}, ?MODULE, [Proc, Transport, Host, Port, Path, Publish, Subscribe,
Authentication, ReplicationUser], []).
%%%===================================================================
%%% gen_server callbacks
%%%===================================================================
init([_Proc, Proto, Host, Port, Path, Publish, Subscribe, Authentication, ReplicationUser]) ->
{Version, Transport, IsWs} =
case Proto of
mqtt -> {4, gen_tcp, false};
mqtts -> {4, ssl, false};
mqtt5 -> {5, gen_tcp, false};
mqtt5s -> {5, ssl, false};
ws -> {4, gen_tcp, true};
wss -> {4, ssl, true};
ws5 -> {5, gen_tcp, true};
wss5 -> {5, ssl, true}
end,
State = #state{version = Version,
id = p1_rand:uniform(65535),
codec = mqtt_codec:new(4096),
subscriptions = Subscribe,
authentication = Authentication,
usr = jid:tolower(ReplicationUser),
publish = Publish},
case Authentication of
#{certfile := Cert} when Transport == ssl ->
Sock = ssl:connect(Host, Port, [binary, {active, true}, {certfile, Cert}]),
if IsWs ->
connect_ws(Host, Port, Path, Sock, State, ssl, none);
true -> connect(Sock, State, ssl, none)
end;
#{username := User, password := Pass} ->
Sock = Transport:connect(Host, Port, [binary, {active, true}]),
if IsWs ->
connect_ws(Host, Port, Path, Sock, State, Transport, {User, Pass});
true -> connect(Sock, State, Transport, {User, Pass})
end;
_ ->
{stop, {error, <<"Certificate can be only used for encrypted connections">> }}
end.
handle_call(stop, _From, State) ->
{stop, normal, ok, State};
handle_call(Request, From, State) ->
?WARNING_MSG("Unexpected call from ~p: ~p", [From, Request]),
{noreply, State}.
handle_cast(Msg, State) ->
?WARNING_MSG("Unexpected cast: ~p", [Msg]),
{noreply, State}.
-spec handle_info(term(), state()) ->
{noreply, state()} | {noreply, state(), timeout()} | {stop, term(), state()}.
handle_info({Tag, TCPSock, TCPData},
#state{ws_codec = {init, Hash, Auth, Last}} = State)
when (Tag == tcp orelse Tag == ssl) ->
Data = <>,
case erlang:decode_packet(http_bin, Data, []) of
{ok, {http_response, _, 101, _}, Rest} ->
handle_info({tcp, TCPSock, Rest}, State#state{ws_codec = {inith, Hash, none, Auth, <<>>}});
{ok, {http_response, _, _, _}, _Rest} ->
stop(State, {socket, closed});
{ok, {http_error, _}, _} ->
stop(State, {socket, closed});
{error, _} ->
stop(State, {socket, closed});
{more, _} ->
{noreply, State#state{ws_codec = {init, Hash, Auth, Data}}}
end;
handle_info({Tag, TCPSock, TCPData},
#state{ws_codec = {inith, Hash, Upgrade, Auth, Last},
socket = {Transport, _}} = State)
when (Tag == tcp orelse Tag == ssl) ->
Data = <>,
case erlang:decode_packet(httph_bin, Data, []) of
{ok, {http_header, _, <<"Sec-Websocket-Accept">>, _, Val}, Rest} ->
case str:to_lower(Val) of
Hash ->
handle_info({tcp, TCPSock, Rest},
State#state{ws_codec = {inith, ok, Upgrade, Auth, <<>>}});
_ ->
stop(State, {socket, closed})
end;
{ok, {http_header, _, 'Connection', _, Val}, Rest} ->
case str:to_lower(Val) of
<<"upgrade">> ->
handle_info({tcp, TCPSock, Rest},
State#state{ws_codec = {inith, Hash, ok, Auth, <<>>}});
_ ->
stop(State, {socket, closed})
end;
{ok, {http_header, _, _, _, _}, Rest} ->
handle_info({tcp, TCPSock, Rest}, State);
{ok, {http_error, _}, _} ->
stop(State, {socket, closed});
{ok, http_eoh, Rest} ->
case {Hash, Upgrade} of
{ok, ok} ->
{ok, State2} = connect({ok, TCPSock},
State#state{ws_codec = ejabberd_websocket_codec:new_client()},
Transport, Auth),
handle_info({tcp, TCPSock, Rest}, State2);
_ ->
stop(State, {socket, closed})
end;
{error, _} ->
stop(State, {socket, closed});
{more, _} ->
{noreply, State#state{ws_codec = {inith, Hash, Upgrade, Data}}}
end;
handle_info({Tag, TCPSock, TCPData},
#state{ws_codec = WSCodec} = State)
when (Tag == tcp orelse Tag == ssl) andalso WSCodec /= none ->
{Packets, Acc0} =
case ejabberd_websocket_codec:decode(WSCodec, TCPData) of
{ok, NewWSCodec, Packets0} ->
{Packets0, {noreply, ok, State#state{ws_codec = NewWSCodec}}};
{error, _Error, Packets0} ->
{Packets0, {stop_after, {socket, closed}, State}}
end,
Res2 =
lists:foldl(
fun(_, {stop, _, _} = Res) -> Res;
({_Op, Data}, {Tag2, Res, S}) ->
case handle_info({tcp_decoded, TCPSock, Data}, S) of
{stop, _, _} = Stop ->
Stop;
{_, NewState} ->
{Tag2, Res, NewState}
end
end, Acc0, Packets),
case Res2 of
{noreply, _, State2} ->
{noreply, State2};
{Tag3, Res3, State2} when Tag3 == stop; Tag3 == stop_after ->
{stop, Res3, State2}
end;
handle_info({Tag, TCPSock, TCPData},
#state{codec = Codec} = State)
when Tag == tcp; Tag == ssl; Tag == tcp_decoded ->
case mqtt_codec:decode(Codec, TCPData) of
{ok, Pkt, Codec1} ->
?DEBUG("Got MQTT packet:~n~ts", [pp(Pkt)]),
State1 = State#state{codec = Codec1},
case handle_packet(Pkt, State1) of
{ok, State2} ->
handle_info({tcp_decoded, TCPSock, <<>>}, State2);
{error, State2, Reason} ->
stop(State2, Reason)
end;
{more, Codec1} ->
State1 = State#state{codec = Codec1},
{noreply, State1};
{error, Why} ->
stop(State, {codec, Why})
end;
handle_info({tcp_closed, _Sock}, State) ->
?DEBUG("MQTT connection reset by peer", []),
stop(State, {socket, closed});
handle_info({ssl_closed, _Sock}, State) ->
?DEBUG("MQTT connection reset by peer", []),
stop(State, {socket, closed});
handle_info({tcp_error, _Sock, Reason}, State) ->
?DEBUG("MQTT connection error: ~ts", [format_inet_error(Reason)]),
stop(State, {socket, Reason});
handle_info({ssl_error, _Sock, Reason}, State) ->
?DEBUG("MQTT connection error: ~ts", [format_inet_error(Reason)]),
stop(State, {socket, Reason});
handle_info({publish, #publish{topic = Topic} = Pkt}, #state{publish = Publish} = State) ->
case maps:find(Topic, Publish) of
{ok, RemoteTopic} ->
case send(State, Pkt#publish{qos = 0, topic = RemoteTopic}) of
{ok, State2} ->
{noreply, State2}
end;
_ ->
{noreply, State}
end;
handle_info({timeout, _TRef, ping_timeout}, State) ->
case send(State, #pingreq{}) of
{ok, State2} ->
{noreply, State2}
end;
handle_info(Info, State) ->
?WARNING_MSG("Unexpected info: ~p", [Info]),
{noreply, State}.
-spec handle_packet(mqtt_packet(), state()) ->
{ok, state()} |
{error, state(), error_reason()}.
handle_packet(#connack{} = Pkt, State) ->
handle_connack(Pkt, State);
handle_packet(#suback{}, State) ->
{ok, State};
handle_packet(#publish{} = Pkt, State) ->
handle_publish(Pkt, State);
handle_packet(#pingresp{}, State) ->
{ok, State};
handle_packet(#disconnect{properties = #{session_expiry_interval := SE}},
State) when SE > 0 ->
%% Protocol violation
{error, State, session_expiry_non_zero};
handle_packet(#disconnect{code = Code, properties = Props},
State) ->
Reason = maps:get(reason_string, Props, <<>>),
{error, State, {peer_disconnected, Code, Reason}};
handle_packet(Pkt, State) ->
?WARNING_MSG("Unexpected packet:~n~ts~n** when state:~n~ts",
[pp(Pkt), pp(State)]),
{error, State, {unexpected_packet, element(1, Pkt)}}.
terminate(Reason, State) ->
Reason1 = case Reason of
shutdown -> shutdown;
{shutdown, _} -> shutdown;
normal -> State#state.stop_reason;
{process_limit, _} -> queue_full;
_ -> internal_server_error
end,
disconnect(State, Reason1).
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
%%%===================================================================
%%% State transitions
%%%===================================================================
connect({error, Reason}, _State, _Transport, _Auth) ->
{stop, {error, Reason}};
connect({ok, Sock}, State0, Transport, Auth) ->
State = State0#state{socket = {Transport, Sock}},
Connect = case Auth of
{User, Pass} ->
#connect{client_id = integer_to_binary(State#state.id),
clean_start = true,
username = User,
password = Pass,
keep_alive = 60,
proto_level = State#state.version};
_ ->
#connect{client_id = integer_to_binary(State#state.id),
clean_start = true,
keep_alive = 60,
proto_level = State#state.version}
end,
Pkt = mqtt_codec:encode(State#state.version, Connect),
send(State, Connect),
{ok, _, Codec2} = mqtt_codec:decode(State#state.codec, Pkt),
{ok, State#state{codec = Codec2}}.
connect_ws(_Host, _Port, _Path, {error, Reason}, _State, _Transport, _Auth) ->
{stop, {error, Reason}};
connect_ws(Host, Port, Path, {ok, Sock}, State0, Transport, Auth) ->
Key = base64:encode(p1_rand:get_string()),
Hash = str:to_lower(base64:encode(crypto:hash(sha, <>))),
Data = <<"GET ", (list_to_binary(Path))/binary, " HTTP/1.1\r\n",
"Host: ", (list_to_binary(Host))/binary, ":", (integer_to_binary(Port))/binary,"\r\n",
"Upgrade: websocket\r\n",
"Connection: Upgrade\r\n",
"Sec-WebSocket-Protocol: mqtt\r\n",
"Sec-WebSocket-Key: ", Key/binary, "\r\n",
"Sec-WebSocket-Version: 13\r\n\r\n">>,
Res = Transport:send(Sock, Data),
check_sock_result({Transport, Sock}, Res),
{ok, State0#state{ws_codec = {init, Hash, Auth, <<>>}, socket = {Transport, Sock}}}.
-spec stop(state(), error_reason()) ->
{noreply, state(), infinity} |
{stop, normal, state()}.
stop(State, Reason) ->
{stop, normal, State#state{stop_reason = Reason}}.
%%%===================================================================
%%% CONNECT/PUBLISH/SUBSCRIBE/UNSUBSCRIBE handlers
%%%===================================================================
-spec handle_connack(connack(), state()) ->
{ok, state()} |
{error, state(), error_reason()}.
handle_connack(#connack{code = success}, #state{subscriptions = Subs} = State) ->
Filters = maps:fold(
fun(RemoteTopic, _LocalTopic, Acc) ->
[{RemoteTopic, #sub_opts{}} | Acc]
end, [], Subs),
Pkt = #subscribe{id = 1, filters = Filters},
send(State, Pkt);
handle_connack(#connack{}, State) ->
{error, State, {auth, 'not-authorized'}}.
-spec handle_publish(publish(), state()) ->
{ok, state()} |
{error, state(), error_reason()}.
handle_publish(#publish{topic = Topic, payload = Payload, properties = Props},
#state{usr = USR, subscriptions = Subs} = State) ->
case maps:get(Topic, Subs, none) of
none ->
{ok, State};
LocalTopic ->
MessageExpiry = maps:get(message_expiry_interval, Props, ?MAX_UINT32),
ExpiryTime = min(unix_time() + MessageExpiry, ?MAX_UINT32),
mod_mqtt:publish(USR, #publish{retain = true, topic = LocalTopic, payload = Payload, properties = Props},
ExpiryTime),
{ok, State}
end.
%%%===================================================================
%%% Socket management
%%%===================================================================
-spec send(state(), mqtt_packet()) ->
{ok, state()} |
{error, state(), error_reason()}.
send(State, #publish{} = Pkt) ->
case is_expired(Pkt) of
{false, Pkt1} ->
{ok, do_send(State, Pkt1)};
true ->
{ok, State}
end;
send(State, Pkt) ->
{ok, do_send(State, Pkt)}.
-spec do_send(state(), mqtt_packet()) -> state().
do_send(#state{ws_codec = WSCodec, socket = {SockMod, Sock} = Socket} = State, Pkt)
when WSCodec /= none ->
?DEBUG("Send MQTT packet:~n~ts", [pp(Pkt)]),
Data = mqtt_codec:encode(State#state.version, Pkt),
WSData = ejabberd_websocket_codec:encode(WSCodec, 2, Data),
Res = SockMod:send(Sock, WSData),
check_sock_result(Socket, Res),
reset_ping_timer(State);
do_send(#state{socket = {SockMod, Sock} = Socket} = State, Pkt) ->
?DEBUG("Send MQTT packet:~n~ts", [pp(Pkt)]),
Data = mqtt_codec:encode(State#state.version, Pkt),
Res = SockMod:send(Sock, Data),
check_sock_result(Socket, Res),
reset_ping_timer(State);
do_send(State, _Pkt) ->
State.
-spec disconnect(state(), error_reason()) -> state().
disconnect(#state{socket = {SockMod, Sock}} = State, Err) ->
State1 = case Err of
{auth, Code} ->
do_send(State, #connack{code = Code});
{codec, {Tag, _, _} = CErr} when Tag == unsupported_protocol_version;
Tag == unsupported_protocol_name ->
do_send(State#state{version = ?MQTT_VERSION_4},
#connack{code = mqtt_codec:error_reason_code(CErr)});
_ when State#state.version == undefined ->
State;
{Tag, _} when Tag == socket; Tag == tls ->
State;
{peer_disconnected, _, _} ->
State;
_ ->
case State of
_ when State#state.version == ?MQTT_VERSION_5 ->
Code = disconnect_reason_code(Err),
Pkt = #disconnect{code = Code},
do_send(State, Pkt);
_ ->
State
end
end,
SockMod:close(Sock),
State1#state{socket = undefined,
version = undefined,
codec = mqtt_codec:renew(State#state.codec)};
disconnect(State, _) ->
State.
-spec reset_ping_timer(state()) -> state().
reset_ping_timer(State) ->
misc:cancel_timer(State#state.ping_timer),
State#state{ping_timer = erlang:start_timer(?PING_TIMEOUT, self(), ping_timeout)}.
-spec check_sock_result(socket(), ok | {error, inet:posix()}) -> ok.
check_sock_result(_, ok) ->
ok;
check_sock_result({_, Sock}, {error, Why}) ->
self() ! {tcp_closed, Sock},
?DEBUG("MQTT socket error: ~p", [format_inet_error(Why)]).
%%%===================================================================
%%% Formatters
%%%===================================================================
-spec pp(any()) -> iolist().
pp(Term) ->
io_lib_pretty:print(Term, fun pp/2).
-spec format_inet_error(socket_error_reason()) -> string().
format_inet_error(closed) ->
"connection closed";
format_inet_error(timeout) ->
format_inet_error(etimedout);
format_inet_error(Reason) ->
case inet:format_error(Reason) of
"unknown POSIX error" -> atom_to_list(Reason);
Txt -> Txt
end.
-spec pp(atom(), non_neg_integer()) -> [atom()] | no.
pp(state, 17) -> record_info(fields, state);
pp(Rec, Size) -> mqtt_codec:pp(Rec, Size).
-spec disconnect_reason_code(error_reason()) -> reason_code().
disconnect_reason_code({code, Code}) -> Code;
disconnect_reason_code({codec, Err}) -> mqtt_codec:error_reason_code(Err);
disconnect_reason_code({unexpected_packet, _}) -> 'protocol-error';
disconnect_reason_code({replaced, _}) -> 'session-taken-over';
disconnect_reason_code({resumed, _}) -> 'session-taken-over';
disconnect_reason_code(internal_server_error) -> 'implementation-specific-error';
disconnect_reason_code(db_failure) -> 'implementation-specific-error';
disconnect_reason_code(idle_connection) -> 'keep-alive-timeout';
disconnect_reason_code(queue_full) -> 'quota-exceeded';
disconnect_reason_code(shutdown) -> 'server-shutting-down';
disconnect_reason_code(subscribe_forbidden) -> 'topic-filter-invalid';
disconnect_reason_code(publish_forbidden) -> 'topic-name-invalid';
disconnect_reason_code(will_topic_forbidden) -> 'topic-name-invalid';
disconnect_reason_code({payload_format_invalid, _}) -> 'payload-format-invalid';
disconnect_reason_code(session_expiry_non_zero) -> 'protocol-error';
disconnect_reason_code(unknown_topic_alias) -> 'protocol-error';
disconnect_reason_code(_) -> 'unspecified-error'.
%%%===================================================================
%%% Timings
%%%===================================================================
-spec unix_time() -> seconds().
unix_time() ->
erlang:system_time(second).
-spec is_expired(publish()) -> true | {false, publish()}.
is_expired(#publish{meta = Meta, properties = Props} = Pkt) ->
case maps:get(expiry_time, Meta, ?MAX_UINT32) of
?MAX_UINT32 ->
{false, Pkt};
ExpiryTime ->
Left = ExpiryTime - unix_time(),
if Left > 0 ->
Props1 = Props#{message_expiry_interval => Left},
{false, Pkt#publish{properties = Props1}};
true ->
?DEBUG("Dropping expired packet:~n~ts", [pp(Pkt)]),
true
end
end.
ejabberd-24.12/src/mod_last.erl 0000664 0001750 0001750 00000027405 14730775155 016763 0 ustar debalance debalance %%%----------------------------------------------------------------------
%%% File : mod_last.erl
%%% Author : Alexey Shchepin
%%% Purpose : jabber:iq:last support (XEP-0012)
%%% Created : 24 Oct 2003 by Alexey Shchepin
%%%
%%%
%%% 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_last).
-author('alexey@process-one.net').
-protocol({xep, 12, '2.0', '0.5.0', "complete", ""}).
-behaviour(gen_mod).
-export([start/2, stop/1, reload/3, process_local_iq/1, export/1,
process_sm_iq/1, on_presence_update/4, import_info/0,
import/5, import_start/2, store_last_info/4, get_last_info/2,
remove_user/2, mod_opt_type/1, mod_options/1, mod_doc/0,
register_user/2, depends/2, privacy_check_packet/4]).
-include("logger.hrl").
-include_lib("xmpp/include/xmpp.hrl").
-include("mod_privacy.hrl").
-include("mod_last.hrl").
-include("translate.hrl").
-define(LAST_CACHE, last_activity_cache).
-type c2s_state() :: ejabberd_c2s:state().
-callback init(binary(), gen_mod:opts()) -> any().
-callback import(binary(), #last_activity{}) -> ok | pass.
-callback get_last(binary(), binary()) ->
{ok, {non_neg_integer(), binary()}} | error | {error, any()}.
-callback store_last_info(binary(), binary(), non_neg_integer(), binary()) -> ok | {error, any()}.
-callback remove_user(binary(), binary()) -> any().
-callback use_cache(binary()) -> boolean().
-callback cache_nodes(binary()) -> [node()].
-optional_callbacks([use_cache/1, cache_nodes/1]).
start(Host, Opts) ->
Mod = gen_mod:db_mod(Opts, ?MODULE),
Mod:init(Host, Opts),
init_cache(Mod, Host, Opts),
{ok, [{iq_handler, ejabberd_local, ?NS_LAST, process_local_iq},
{iq_handler, ejabberd_sm, ?NS_LAST, process_sm_iq},
{hook, privacy_check_packet, privacy_check_packet, 30},
{hook, register_user, register_user, 50},
{hook, remove_user, remove_user, 50},
{hook, unset_presence_hook, on_presence_update, 50}]}.
stop(_Host) ->
ok.
reload(Host, NewOpts, OldOpts) ->
NewMod = gen_mod:db_mod(NewOpts, ?MODULE),
OldMod = gen_mod:db_mod(OldOpts, ?MODULE),
if NewMod /= OldMod ->
NewMod:init(Host, NewOpts);
true ->
ok
end,
init_cache(NewMod, Host, NewOpts).
%%%
%%% Uptime of ejabberd node
%%%
-spec process_local_iq(iq()) -> iq().
process_local_iq(#iq{type = set, lang = Lang} = IQ) ->
Txt = ?T("Value 'set' of 'type' attribute is not allowed"),
xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang));
process_local_iq(#iq{type = get} = IQ) ->
xmpp:make_iq_result(IQ, #last{seconds = get_node_uptime()}).
-spec get_node_uptime() -> non_neg_integer().
%% @doc Get the uptime of the ejabberd node, expressed in seconds.
%% When ejabberd is starting, ejabberd_config:start/0 stores the datetime.
get_node_uptime() ->
NodeStart = ejabberd_config:get_node_start(),
erlang:monotonic_time(second) - NodeStart.
%%%
%%% Serve queries about user last online
%%%
-spec process_sm_iq(iq()) -> iq().
process_sm_iq(#iq{type = set, lang = Lang} = IQ) ->
Txt = ?T("Value 'set' of 'type' attribute is not allowed"),
xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang));
process_sm_iq(#iq{from = From, to = To, lang = Lang} = IQ) ->
User = To#jid.luser,
Server = To#jid.lserver,
{Subscription, _Ask, _Groups} =
ejabberd_hooks:run_fold(roster_get_jid_info, Server,
{none, none, []}, [User, Server, From]),
if (Subscription == both) or (Subscription == from) or
(From#jid.luser == To#jid.luser) and
(From#jid.lserver == To#jid.lserver) ->
Pres = xmpp:set_from_to(#presence{}, To, From),
case ejabberd_hooks:run_fold(privacy_check_packet,
Server, allow,
[To, Pres, out]) of
allow -> get_last_iq(IQ, User, Server);
deny -> xmpp:make_error(IQ, xmpp:err_forbidden())
end;
true ->
Txt = ?T("Not subscribed"),
xmpp:make_error(IQ, xmpp:err_subscription_required(Txt, Lang))
end.
-spec privacy_check_packet(allow | deny, c2s_state(), stanza(), in | out) -> allow | deny | {stop, deny}.
privacy_check_packet(allow, C2SState,
#iq{from = From, to = To, type = T} = IQ, in)
when T == get; T == set ->
case xmpp:has_subtag(IQ, #last{}) of
true ->
#jid{luser = LUser, lserver = LServer} = To,
{Sub, _, _} = ejabberd_hooks:run_fold(
roster_get_jid_info, LServer,
{none, none, []}, [LUser, LServer, From]),
if Sub == from; Sub == both ->
Pres = #presence{from = To, to = From},
case ejabberd_hooks:run_fold(
privacy_check_packet, allow,
[C2SState, Pres, out]) of
allow ->
allow;
deny ->
{stop, deny}
end;
true ->
{stop, deny}
end;
false ->
allow
end;
privacy_check_packet(Acc, _, _, _) ->
Acc.
-spec get_last(binary(), binary()) -> {ok, non_neg_integer(), binary()} |
not_found | {error, any()}.
get_last(LUser, LServer) ->
Mod = gen_mod:db_mod(LServer, ?MODULE),
Res = case use_cache(Mod, LServer) of
true ->
ets_cache:lookup(
?LAST_CACHE, {LUser, LServer},
fun() -> Mod:get_last(LUser, LServer) end);
false ->
Mod:get_last(LUser, LServer)
end,
case Res of
{ok, {TimeStamp, Status}} -> {ok, TimeStamp, Status};
error -> not_found;
Err -> Err
end.
-spec get_last_iq(iq(), binary(), binary()) -> iq().
get_last_iq(#iq{lang = Lang} = IQ, LUser, LServer) ->
case ejabberd_sm:get_user_resources(LUser, LServer) of
[] ->
case get_last(LUser, LServer) of
{error, _Reason} ->
Txt = ?T("Database failure"),
xmpp:make_error(IQ, xmpp:err_internal_server_error(Txt, Lang));
not_found ->
Txt = ?T("No info about last activity found"),
xmpp:make_error(IQ, xmpp:err_service_unavailable(Txt, Lang));
{ok, TimeStamp, Status} ->
TimeStamp2 = erlang:system_time(second),
Sec = TimeStamp2 - TimeStamp,
xmpp:make_iq_result(IQ, #last{seconds = Sec, status = Status})
end;
_ ->
xmpp:make_iq_result(IQ, #last{seconds = 0})
end.
-spec register_user(binary(), binary()) -> any().
register_user(User, Server) ->
on_presence_update(
User,
Server,
<<"RegisterResource">>,
<<"Registered but didn't login">>).
-spec on_presence_update(binary(), binary(), binary(), binary()) -> any().
on_presence_update(User, Server, _Resource, Status) ->
TimeStamp = erlang:system_time(second),
store_last_info(User, Server, TimeStamp, Status).
-spec store_last_info(binary(), binary(), non_neg_integer(), binary()) -> any().
store_last_info(User, Server, TimeStamp, Status) ->
LUser = jid:nodeprep(User),
LServer = jid:nameprep(Server),
Mod = gen_mod:db_mod(LServer, ?MODULE),
case use_cache(Mod, LServer) of
true ->
ets_cache:update(
?LAST_CACHE, {LUser, LServer}, {ok, {TimeStamp, Status}},
fun() ->
Mod:store_last_info(LUser, LServer, TimeStamp, Status)
end, cache_nodes(Mod, LServer));
false ->
Mod:store_last_info(LUser, LServer, TimeStamp, Status)
end.
-spec get_last_info(binary(), binary()) -> {ok, non_neg_integer(), binary()} |
not_found.
get_last_info(LUser, LServer) ->
case get_last(LUser, LServer) of
{error, _Reason} -> not_found;
Res -> Res
end.
-spec remove_user(binary(), binary()) -> any().
remove_user(User, Server) ->
LUser = jid:nodeprep(User),
LServer = jid:nameprep(Server),
Mod = gen_mod:db_mod(LServer, ?MODULE),
Mod:remove_user(LUser, LServer),
ets_cache:delete(?LAST_CACHE, {LUser, LServer}, cache_nodes(Mod, LServer)).
-spec init_cache(module(), binary(), gen_mod:opts()) -> ok.
init_cache(Mod, Host, Opts) ->
case use_cache(Mod, Host) of
true ->
CacheOpts = cache_opts(Opts),
ets_cache:new(?LAST_CACHE, CacheOpts);
false ->
ets_cache:delete(?LAST_CACHE)
end.
-spec cache_opts(gen_mod:opts()) -> [proplists:property()].
cache_opts(Opts) ->
MaxSize = mod_last_opt:cache_size(Opts),
CacheMissed = mod_last_opt:cache_missed(Opts),
LifeTime = mod_last_opt:cache_life_time(Opts),
[{max_size, MaxSize}, {cache_missed, CacheMissed}, {life_time, LifeTime}].
-spec use_cache(module(), binary()) -> boolean().
use_cache(Mod, Host) ->
case erlang:function_exported(Mod, use_cache, 1) of
true -> Mod:use_cache(Host);
false -> mod_last_opt:use_cache(Host)
end.
-spec cache_nodes(module(), binary()) -> [node()].
cache_nodes(Mod, Host) ->
case erlang:function_exported(Mod, cache_nodes, 1) of
true -> Mod:cache_nodes(Host);
false -> ejabberd_cluster:get_nodes()
end.
import_info() ->
[{<<"last">>, 3}].
import_start(LServer, DBType) ->
Mod = gen_mod:db_mod(DBType, ?MODULE),
Mod:init(LServer, []).
import(LServer, {sql, _}, DBType, <<"last">>, [LUser, TimeStamp, State]) ->
TS = case TimeStamp of
<<"">> -> 0;
_ -> binary_to_integer(TimeStamp)
end,
LA = #last_activity{us = {LUser, LServer},
timestamp = TS,
status = State},
Mod = gen_mod:db_mod(DBType, ?MODULE),
Mod:import(LServer, LA).
export(LServer) ->
Mod = gen_mod:db_mod(LServer, ?MODULE),
Mod:export(LServer).
depends(_Host, _Opts) ->
[].
mod_opt_type(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).
mod_options(Host) ->
[{db_type, ejabberd_config:default_db(Host, ?MODULE)},
{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 adds support for "
"https://xmpp.org/extensions/xep-0012.html"
"[XEP-0012: Last Activity]. It can be used "
"to discover when a disconnected user last accessed "
"the server, to know when a connected user was last "
"active on the server, or to query the uptime of the ejabberd server."),
opts =>
[{db_type,
#{value => "mnesia | sql",
desc =>
?T("Same as top-level _`default_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.")}}]}.
ejabberd-24.12/src/ejabberd_system_monitor.erl 0000664 0001750 0001750 00000023716 14730775155 022073 0 ustar debalance debalance %%%-------------------------------------------------------------------
%%% File : ejabberd_system_monitor.erl
%%% Author : Alexey Shchepin
%%% Description : ejabberd watchdog
%%% Created : 21 Mar 2007 by Alexey Shchepin
%%%
%%%
%%% 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(ejabberd_system_monitor).
-behaviour(gen_event).
-author('alexey@process-one.net').
-author('ekhramtsov@process-one.net').
%% API
-export([start/0, config_reloaded/0, stop/0]).
%% gen_event callbacks
-export([init/1, handle_event/2, handle_call/2,
handle_info/2, terminate/2, code_change/3]).
-include("logger.hrl").
-define(CHECK_INTERVAL, timer:seconds(30)).
-record(state, {tref :: undefined | reference(),
mref :: undefined | reference()}).
-record(proc_stat, {qlen :: non_neg_integer(),
memory :: non_neg_integer(),
initial_call :: mfa(),
current_function :: mfa(),
ancestors :: [pid() | atom()],
application :: pid() | atom(),
name :: pid() | atom()}).
-type state() :: #state{}.
-type proc_stat() :: #proc_stat{}.
-type app_pids() :: #{pid() => atom()}.
%%%===================================================================
%%% API
%%%===================================================================
-spec start() -> ok.
start() ->
gen_event:add_handler(alarm_handler, ?MODULE, []),
gen_event:swap_handler(alarm_handler, {alarm_handler, swap}, {?MODULE, []}),
application:load(os_mon),
application:set_env(os_mon, start_cpu_sup, false),
application:set_env(os_mon, start_os_sup, false),
application:set_env(os_mon, start_memsup, true),
application:set_env(os_mon, start_disksup, false),
ejabberd:start_app(os_mon),
set_oom_watermark().
-spec stop() -> term().
stop() ->
gen_event:delete_handler(alarm_handler, ?MODULE, []).
excluded_apps() ->
[os_mon, mnesia, sasl, stdlib, kernel].
-spec config_reloaded() -> ok.
config_reloaded() ->
set_oom_watermark().
%%%===================================================================
%%% gen_event callbacks
%%%===================================================================
init({[], _}) -> % Called by gen_event:swap_handler
{ok, #state{}};
init([]) -> % Called by gen_event:add_handler
ejabberd_hooks:add(config_reloaded, ?MODULE, config_reloaded, 50),
{ok, #state{}}.
handle_event({set_alarm, {system_memory_high_watermark, _}}, State) ->
handle_overload(State),
{ok, restart_timer(State)};
handle_event({clear_alarm, system_memory_high_watermark}, State) ->
misc:cancel_timer(State#state.tref),
{ok, State#state{tref = undefined}};
handle_event({set_alarm, {process_memory_high_watermark, Pid}}, State) ->
case proc_stat(Pid, get_app_pids()) of
#proc_stat{name = Name} = ProcStat ->
?WARNING_MSG(
"Process ~p consumes more than 5% of OS memory (~ts)~n",
[Name, format_proc(ProcStat)]),
handle_overload(State),
{ok, State};
_ ->
{ok, State}
end;
handle_event({clear_alarm, process_memory_high_watermark}, State) ->
{ok, State};
handle_event(Event, State) ->
?WARNING_MSG("unexpected event: ~p~n", [Event]),
{ok, State}.
handle_call(_Request, State) ->
{ok, {error, badarg}, State}.
handle_info({timeout, _TRef, handle_overload}, State) ->
handle_overload(State),
{ok, restart_timer(State)};
handle_info(Info, State) ->
?WARNING_MSG("unexpected info: ~p~n", [Info]),
{ok, State}.
terminate(_Reason, State) ->
misc:cancel_timer(State#state.tref),
ejabberd_hooks:delete(config_reloaded, ?MODULE, config_reloaded, 50).
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
%%%===================================================================
%%% Internal functions
%%%===================================================================
-spec handle_overload(state()) -> ok.
handle_overload(State) ->
handle_overload(State, processes()).
-spec handle_overload(state(), [pid()]) -> ok.
handle_overload(_State, Procs) ->
AppPids = get_app_pids(),
{TotalMsgs, ProcsNum, Apps, Stats} = overloaded_procs(AppPids, Procs),
MaxMsgs = ejabberd_option:oom_queue(),
if TotalMsgs >= MaxMsgs ->
SortedStats = lists:reverse(lists:keysort(#proc_stat.qlen, Stats)),
?WARNING_MSG(
"The system is overloaded with ~b messages "
"queued by ~b process(es) (~b%) "
"from the following applications: ~ts; "
"the top processes are:~n~ts~n",
[TotalMsgs, ProcsNum,
round(ProcsNum*100/length(Procs)),
format_apps(Apps),
format_top_procs(SortedStats)]),
kill(SortedStats, round(TotalMsgs/ProcsNum));
true ->
ok
end,
lists:foreach(fun erlang:garbage_collect/1, Procs).
-spec get_app_pids() -> app_pids().
get_app_pids() ->
try application:info() of
Info ->
case lists:keyfind(running, 1, Info) of
{_, Apps} ->
lists:foldl(
fun({Name, Pid}, M) when is_pid(Pid) ->
maps:put(Pid, Name, M);
(_, M) ->
M
end, #{}, Apps);
false ->
#{}
end
catch _:_ ->
#{}
end.
-spec overloaded_procs(app_pids(), [pid()])
-> {non_neg_integer(), non_neg_integer(), dict:dict(), [proc_stat()]}.
overloaded_procs(AppPids, AllProcs) ->
lists:foldl(
fun(Pid, {TotalMsgs, ProcsNum, Apps, Stats}) ->
case proc_stat(Pid, AppPids) of
#proc_stat{qlen = QLen, application = App} = Stat
when QLen > 0 ->
{TotalMsgs + QLen, ProcsNum + 1,
dict:update_counter(App, QLen, Apps),
[Stat|Stats]};
_ ->
{TotalMsgs, ProcsNum, Apps, Stats}
end
end, {0, 0, dict:new(), []}, AllProcs).
-spec proc_stat(pid(), app_pids()) -> proc_stat() | undefined.
proc_stat(Pid, AppPids) ->
case process_info(Pid, [message_queue_len,
memory,
initial_call,
current_function,
dictionary,
group_leader,
registered_name]) of
[{_, MsgLen}, {_, Mem}, {_, InitCall},
{_, CurrFun}, {_, Dict}, {_, GL}, {_, Name}] ->
IntLen = proplists:get_value('$internal_queue_len', Dict, 0),
TrueInitCall = proplists:get_value('$initial_call', Dict, InitCall),
Ancestors = proplists:get_value('$ancestors', Dict, []),
Len = IntLen + MsgLen,
App = maps:get(GL, AppPids, kernel),
RegName = case Name of
[] -> Pid;
_ -> Name
end,
#proc_stat{qlen = Len,
memory = Mem,
initial_call = TrueInitCall,
current_function = CurrFun,
ancestors = Ancestors,
application = App,
name = RegName};
_ ->
undefined
end.
-spec restart_timer(#state{}) -> #state{}.
restart_timer(State) ->
misc:cancel_timer(State#state.tref),
TRef = erlang:start_timer(?CHECK_INTERVAL, self(), handle_overload),
State#state{tref = TRef}.
-spec format_apps(dict:dict()) -> iodata().
format_apps(Apps) ->
AppList = lists:reverse(lists:keysort(2, dict:to_list(Apps))),
string:join(
[io_lib:format("~p (~b msgs)", [App, Msgs]) || {App, Msgs} <- AppList],
", ").
-spec format_top_procs([proc_stat()]) -> iodata().
format_top_procs(Stats) ->
Stats1 = lists:sublist(Stats, 5),
string:join(
lists:map(
fun(#proc_stat{name = Name} = Stat) ->
[io_lib:format("** ~w: ", [Name]), format_proc(Stat)]
end,Stats1),
io_lib:nl()).
-spec format_proc(proc_stat()) -> iodata().
format_proc(#proc_stat{qlen = Len, memory = Mem, initial_call = InitCall,
current_function = CurrFun, ancestors = Ancs,
application = App}) ->
io_lib:format(
"msgs = ~b, memory = ~b, initial_call = ~ts, "
"current_function = ~ts, ancestors = ~w, application = ~w",
[Len, Mem, format_mfa(InitCall), format_mfa(CurrFun), Ancs, App]).
-spec format_mfa(mfa()) -> iodata().
format_mfa({M, F, A}) when is_atom(M), is_atom(F), is_integer(A) ->
io_lib:format("~ts:~ts/~b", [M, F, A]);
format_mfa(WTF) ->
io_lib:format("~w", [WTF]).
-spec kill([proc_stat()], non_neg_integer()) -> ok.
kill(Stats, Threshold) ->
case ejabberd_option:oom_killer() of
true ->
do_kill(Stats, Threshold);
false ->
ok
end.
-spec do_kill([proc_stat()], non_neg_integer()) -> ok.
do_kill(Stats, Threshold) ->
Killed = lists:filtermap(
fun(#proc_stat{qlen = Len, name = Name, application = App})
when Len >= Threshold ->
case lists:member(App, excluded_apps()) of
true ->
?WARNING_MSG(
"Unable to kill process ~p from whitelisted "
"application ~p~n", [Name, App]),
false;
false ->
case kill_proc(Name) of
false ->
false;
Pid ->
{true, Pid}
end
end;
(_) ->
false
end, Stats),
TotalKilled = length(Killed),
if TotalKilled > 0 ->
?ERROR_MSG(
"Killed ~b process(es) consuming more than ~b message(s) each~n",
[TotalKilled, Threshold]);
true ->
ok
end.
-spec kill_proc(pid() | atom()) -> false | pid().
kill_proc(undefined) ->
false;
kill_proc(Name) when is_atom(Name) ->
kill_proc(whereis(Name));
kill_proc(Pid) ->
exit(Pid, kill),
Pid.
-spec set_oom_watermark() -> ok.
set_oom_watermark() ->
WaterMark = ejabberd_option:oom_watermark(),
memsup:set_sysmem_high_watermark(WaterMark/100).
ejabberd-24.12/src/mod_vcard.erl 0000664 0001750 0001750 00000071750 14730775155 017121 0 ustar debalance debalance %%%----------------------------------------------------------------------
%%% File : mod_vcard.erl
%%% Author : Alexey Shchepin
%%% Purpose : Vcard management
%%% Created : 2 Jan 2003 by Alexey Shchepin
%%%
%%%
%%% 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_vcard).
-author('alexey@process-one.net').
-protocol({xep, 54, '1.2', '0.1.0', "complete", ""}).
-protocol({xep, 55, '1.3', '0.1.0', "complete", ""}).
-protocol({xep, 153, '1.1', '17.09', "complete", ""}).
-behaviour(gen_server).
-behaviour(gen_mod).
-export([start/2, stop/1, get_sm_features/5, mod_options/1, mod_doc/0,
process_local_iq/1, process_sm_iq/1, string2lower/1,
remove_user/2, export/1, import_info/0, import/5, import_start/2,
depends/2, process_search/1, process_vcard/1, get_vcard/2,
disco_items/5, disco_features/5, disco_identity/5,
vcard_iq_set/1, mod_opt_type/1, set_vcard/3, make_vcard_search/4]).
-export([init/1, handle_call/3, handle_cast/2,
handle_info/2, terminate/2, code_change/3]).
-export([route/1]).
-export([webadmin_menu_hostuser/4, webadmin_page_hostuser/4]).
-import(ejabberd_web_admin, [make_command/4, make_command/2, make_table/2]).
-include("logger.hrl").
-include_lib("xmpp/include/xmpp.hrl").
-include("mod_vcard.hrl").
-include("translate.hrl").
-include("ejabberd_stacktrace.hrl").
-include("ejabberd_http.hrl").
-include("ejabberd_web_admin.hrl").
-define(VCARD_CACHE, vcard_cache).
-callback init(binary(), gen_mod:opts()) -> any().
-callback stop(binary()) -> any().
-callback import(binary(), binary(), [binary()]) -> ok.
-callback get_vcard(binary(), binary()) -> {ok, [xmlel()]} | error.
-callback set_vcard(binary(), binary(),
xmlel(), #vcard_search{}) -> {atomic, any()}.
-callback search_fields(binary()) -> [{binary(), binary()}].
-callback search_reported(binary()) -> [{binary(), binary()}].
-callback search(binary(), [{binary(), [binary()]}], boolean(),
infinity | pos_integer()) -> [{binary(), binary()}].
-callback remove_user(binary(), binary()) -> {atomic, any()}.
-callback is_search_supported(binary()) -> boolean().
-callback use_cache(binary()) -> boolean().
-callback cache_nodes(binary()) -> [node()].
-optional_callbacks([use_cache/1, cache_nodes/1]).
-record(state, {hosts :: [binary()], server_host :: binary()}).
%%====================================================================
%% gen_mod callbacks
%%====================================================================
start(Host, Opts) ->
gen_mod:start_child(?MODULE, Host, Opts).
stop(Host) ->
gen_mod:stop_child(?MODULE, Host).
%%====================================================================
%% gen_server callbacks
%%====================================================================
init([Host|_]) ->
process_flag(trap_exit, true),
Opts = gen_mod:get_module_opts(Host, ?MODULE),
Mod = gen_mod:db_mod(Opts, ?MODULE),
Mod:init(Host, Opts),
init_cache(Mod, Host, Opts),
ejabberd_hooks:add(remove_user, Host, ?MODULE,
remove_user, 50),
gen_iq_handler:add_iq_handler(ejabberd_local, Host,
?NS_VCARD, ?MODULE, process_local_iq),
gen_iq_handler:add_iq_handler(ejabberd_sm, Host,
?NS_VCARD, ?MODULE, process_sm_iq),
ejabberd_hooks:add(disco_sm_features, Host, ?MODULE,
get_sm_features, 50),
ejabberd_hooks:add(vcard_iq_set, Host, ?MODULE, vcard_iq_set, 50),
ejabberd_hooks:add(webadmin_menu_hostuser, Host, ?MODULE, webadmin_menu_hostuser, 50),
ejabberd_hooks:add(webadmin_page_hostuser, Host, ?MODULE, webadmin_page_hostuser, 50),
MyHosts = gen_mod:get_opt_hosts(Opts),
Search = mod_vcard_opt:search(Opts),
if Search ->
lists:foreach(
fun(MyHost) ->
ejabberd_hooks:add(
disco_local_items, MyHost, ?MODULE, disco_items, 100),
ejabberd_hooks:add(
disco_local_features, MyHost, ?MODULE, disco_features, 100),
ejabberd_hooks:add(
disco_local_identity, MyHost, ?MODULE, disco_identity, 100),
gen_iq_handler:add_iq_handler(
ejabberd_local, MyHost, ?NS_SEARCH, ?MODULE, process_search),
gen_iq_handler:add_iq_handler(
ejabberd_local, MyHost, ?NS_VCARD, ?MODULE, process_vcard),
gen_iq_handler:add_iq_handler(
ejabberd_local, MyHost, ?NS_DISCO_ITEMS, mod_disco,
process_local_iq_items),
gen_iq_handler:add_iq_handler(
ejabberd_local, MyHost, ?NS_DISCO_INFO, mod_disco,
process_local_iq_info),
case Mod:is_search_supported(Host) of
false ->
?WARNING_MSG("vCard search functionality is "
"not implemented for ~ts backend",
[mod_vcard_opt:db_type(Opts)]);
true ->
ejabberd_router:register_route(
MyHost, Host, {apply, ?MODULE, route})
end
end, MyHosts);
true ->
ok
end,
{ok, #state{hosts = MyHosts, server_host = Host}}.
handle_call(Call, From, State) ->
?WARNING_MSG("Unexpected call from ~p: ~p", [From, Call]),
{noreply, State}.
handle_cast(Cast, State) ->
?WARNING_MSG("Unexpected cast: ~p", [Cast]),
{noreply, State}.
handle_info({route, Packet}, State) ->
try route(Packet)
catch ?EX_RULE(Class, Reason, St) ->
StackTrace = ?EX_STACK(St),
?ERROR_MSG("Failed to route packet:~n~ts~n** ~ts",
[xmpp:pp(Packet),
misc:format_exception(2, Class, Reason, StackTrace)])
end,
{noreply, State};
handle_info(Info, State) ->
?WARNING_MSG("Unexpected info: ~p", [Info]),
{noreply, State}.
terminate(_Reason, #state{hosts = MyHosts, server_host = Host}) ->
ejabberd_hooks:delete(remove_user, Host, ?MODULE, remove_user, 50),
gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_VCARD),
gen_iq_handler:remove_iq_handler(ejabberd_sm, Host, ?NS_VCARD),
ejabberd_hooks:delete(disco_sm_features, Host, ?MODULE, get_sm_features, 50),
ejabberd_hooks:delete(vcard_iq_set, Host, ?MODULE, vcard_iq_set, 50),
ejabberd_hooks:delete(webadmin_menu_hostuser, Host, ?MODULE, webadmin_menu_hostuser, 50),
ejabberd_hooks:delete(webadmin_page_hostuser, Host, ?MODULE, webadmin_page_hostuser, 50),
Mod = gen_mod:db_mod(Host, ?MODULE),
Mod:stop(Host),
lists:foreach(
fun(MyHost) ->
ejabberd_router:unregister_route(MyHost),
ejabberd_hooks:delete(disco_local_items, MyHost, ?MODULE, disco_items, 100),
ejabberd_hooks:delete(disco_local_features, MyHost, ?MODULE, disco_features, 100),
ejabberd_hooks:delete(disco_local_identity, MyHost, ?MODULE, disco_identity, 100),
gen_iq_handler:remove_iq_handler(ejabberd_local, MyHost, ?NS_SEARCH),
gen_iq_handler:remove_iq_handler(ejabberd_local, MyHost, ?NS_VCARD),
gen_iq_handler:remove_iq_handler(ejabberd_local, MyHost, ?NS_DISCO_ITEMS),
gen_iq_handler:remove_iq_handler(ejabberd_local, MyHost, ?NS_DISCO_INFO)
end, MyHosts).
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
-spec route(stanza()) -> ok.
route(#iq{} = IQ) ->
ejabberd_router:process_iq(IQ);
route(_) ->
ok.
-spec get_sm_features({error, stanza_error()} | empty | {result, [binary()]},
jid(), jid(), binary(), binary()) ->
{error, stanza_error()} | empty | {result, [binary()]}.
get_sm_features({error, _Error} = Acc, _From, _To,
_Node, _Lang) ->
Acc;
get_sm_features(Acc, _From, _To, Node, _Lang) ->
case Node of
<<"">> ->
case Acc of
{result, Features} ->
{result, [?NS_VCARD | Features]};
empty -> {result, [?NS_VCARD]}
end;
_ -> Acc
end.
-spec process_local_iq(iq()) -> iq().
process_local_iq(#iq{type = set, lang = Lang} = IQ) ->
Txt = ?T("Value 'set' of 'type' attribute is not allowed"),
xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang));
process_local_iq(#iq{type = get, to = To, lang = Lang} = IQ) ->
ServerHost = ejabberd_router:host_of_route(To#jid.lserver),
VCard = case mod_vcard_opt:vcard(ServerHost) of
undefined ->
#vcard_temp{fn = <<"ejabberd">>,
url = ejabberd_config:get_uri(),
desc = misc:get_descr(Lang, ?T("Erlang XMPP Server")),
bday = <<"2002-11-16">>};
V ->
V
end,
xmpp:make_iq_result(IQ, VCard).
-spec process_sm_iq(iq()) -> iq().
process_sm_iq(#iq{type = set, lang = Lang, from = From} = IQ) ->
#jid{lserver = LServer} = From,
case lists:member(LServer, ejabberd_option:hosts()) of
true ->
case ejabberd_hooks:run_fold(vcard_iq_set, LServer, IQ, []) of
drop -> ignore;
#stanza_error{} = Err -> xmpp:make_error(IQ, Err);
_ -> xmpp:make_iq_result(IQ)
end;
false ->
Txt = ?T("The query is only allowed from local users"),
xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang))
end;
process_sm_iq(#iq{type = get, from = From, to = To, lang = Lang} = IQ) ->
#jid{luser = LUser, lserver = LServer} = To,
case get_vcard(LUser, LServer) of
error ->
Txt = ?T("Database failure"),
xmpp:make_error(IQ, xmpp:err_internal_server_error(Txt, Lang));
[] ->
xmpp:make_iq_result(IQ, #vcard_temp{});
Els ->
IQ#iq{type = result, to = From, from = To, sub_els = Els}
end.
-spec process_vcard(iq()) -> iq().
process_vcard(#iq{type = set, lang = Lang} = IQ) ->
Txt = ?T("Value 'set' of 'type' attribute is not allowed"),
xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang));
process_vcard(#iq{type = get, lang = Lang} = IQ) ->
xmpp:make_iq_result(
IQ, #vcard_temp{fn = <<"ejabberd/mod_vcard">>,
url = ejabberd_config:get_uri(),
desc = misc:get_descr(Lang, ?T("ejabberd vCard module"))}).
-spec process_search(iq()) -> iq().
process_search(#iq{type = get, to = To, lang = Lang} = IQ) ->
ServerHost = ejabberd_router:host_of_route(To#jid.lserver),
xmpp:make_iq_result(IQ, mk_search_form(To, ServerHost, Lang));
process_search(#iq{type = set, to = To, lang = Lang,
sub_els = [#search{xdata = #xdata{type = submit,
fields = Fs}}]} = IQ) ->
ServerHost = ejabberd_router:host_of_route(To#jid.lserver),
ResultXData = search_result(Lang, To, ServerHost, Fs),
xmpp:make_iq_result(IQ, #search{xdata = ResultXData});
process_search(#iq{type = set, lang = Lang} = IQ) ->
Txt = ?T("Incorrect data form"),
xmpp:make_error(IQ, xmpp:err_bad_request(Txt, Lang)).
-spec disco_items({error, stanza_error()} | {result, [disco_item()]} | empty,
jid(), jid(), binary(), binary()) ->
{error, stanza_error()} | {result, [disco_item()]}.
disco_items(empty, _From, _To, <<"">>, _Lang) ->
{result, []};
disco_items(empty, _From, _To, _Node, Lang) ->
{error, xmpp:err_item_not_found(?T("No services available"), Lang)};
disco_items(Acc, _From, _To, _Node, _Lang) ->
Acc.
-spec disco_features({error, stanza_error()} | {result, [binary()]} | empty,
jid(), jid(), binary(), binary()) ->
{error, stanza_error()} | {result, [binary()]}.
disco_features({error, _Error} = Acc, _From, _To, _Node, _Lang) ->
Acc;
disco_features(Acc, _From, _To, <<"">>, _Lang) ->
Features = case Acc of
{result, Fs} -> Fs;
empty -> []
end,
{result, [?NS_DISCO_INFO, ?NS_DISCO_ITEMS,
?NS_VCARD, ?NS_SEARCH | Features]};
disco_features(empty, _From, _To, _Node, Lang) ->
Txt = ?T("No features available"),
{error, xmpp:err_item_not_found(Txt, Lang)};
disco_features(Acc, _From, _To, _Node, _Lang) ->
Acc.
-spec disco_identity([identity()], jid(), jid(),
binary(), binary()) -> [identity()].
disco_identity(Acc, _From, To, <<"">>, Lang) ->
Host = ejabberd_router:host_of_route(To#jid.lserver),
Name = mod_vcard_opt:name(Host),
[#identity{category = <<"directory">>,
type = <<"user">>,
name = translate:translate(Lang, Name)}|Acc];
disco_identity(Acc, _From, _To, _Node, _Lang) ->
Acc.
-spec get_vcard(binary(), binary()) -> [xmlel()] | error.
get_vcard(LUser, LServer) ->
Mod = gen_mod:db_mod(LServer, ?MODULE),
Result = case use_cache(Mod, LServer) of
true ->
ets_cache:lookup(
?VCARD_CACHE, {LUser, LServer},
fun() -> Mod:get_vcard(LUser, LServer) end);
false ->
Mod:get_vcard(LUser, LServer)
end,
case Result of
{ok, Els} -> Els;
error -> error
end.
-spec make_vcard_search(binary(), binary(), binary(), xmlel()) -> #vcard_search{}.
make_vcard_search(User, LUser, LServer, VCARD) ->
FN = fxml:get_path_s(VCARD, [{elem, <<"FN">>}, cdata]),
Family = fxml:get_path_s(VCARD,
[{elem, <<"N">>}, {elem, <<"FAMILY">>}, cdata]),
Given = fxml:get_path_s(VCARD,
[{elem, <<"N">>}, {elem, <<"GIVEN">>}, cdata]),
Middle = fxml:get_path_s(VCARD,
[{elem, <<"N">>}, {elem, <<"MIDDLE">>}, cdata]),
Nickname = fxml:get_path_s(VCARD,
[{elem, <<"NICKNAME">>}, cdata]),
BDay = fxml:get_path_s(VCARD,
[{elem, <<"BDAY">>}, cdata]),
CTRY = fxml:get_path_s(VCARD,
[{elem, <<"ADR">>}, {elem, <<"CTRY">>}, cdata]),
Locality = fxml:get_path_s(VCARD,
[{elem, <<"ADR">>}, {elem, <<"LOCALITY">>},
cdata]),
EMail1 = fxml:get_path_s(VCARD,
[{elem, <<"EMAIL">>}, {elem, <<"USERID">>}, cdata]),
EMail2 = fxml:get_path_s(VCARD,
[{elem, <<"EMAIL">>}, cdata]),
OrgName = fxml:get_path_s(VCARD,
[{elem, <<"ORG">>}, {elem, <<"ORGNAME">>}, cdata]),
OrgUnit = fxml:get_path_s(VCARD,
[{elem, <<"ORG">>}, {elem, <<"ORGUNIT">>}, cdata]),
EMail = case EMail1 of
<<"">> -> EMail2;
_ -> EMail1
end,
LFN = string2lower(FN),
LFamily = string2lower(Family),
LGiven = string2lower(Given),
LMiddle = string2lower(Middle),
LNickname = string2lower(Nickname),
LBDay = string2lower(BDay),
LCTRY = string2lower(CTRY),
LLocality = string2lower(Locality),
LEMail = string2lower(EMail),
LOrgName = string2lower(OrgName),
LOrgUnit = string2lower(OrgUnit),
US = {LUser, LServer},
#vcard_search{us = US,
user = {User, LServer},
luser = LUser, fn = FN,
lfn = LFN,
family = Family,
lfamily = LFamily,
given = Given,
lgiven = LGiven,
middle = Middle,
lmiddle = LMiddle,
nickname = Nickname,
lnickname = LNickname,
bday = BDay,
lbday = LBDay,
ctry = CTRY,
lctry = LCTRY,
locality = Locality,
llocality = LLocality,
email = EMail,
lemail = LEMail,
orgname = OrgName,
lorgname = LOrgName,
orgunit = OrgUnit,
lorgunit = LOrgUnit}.
-spec vcard_iq_set(iq()) -> iq() | {stop, stanza_error()}.
vcard_iq_set(#iq{from = #jid{user = FromUser, lserver = FromLServer},
to = #jid{user = ToUser, lserver = ToLServer},
lang = Lang})
when (FromUser /= ToUser) or (FromLServer /= ToLServer) ->
Txt = ?T("User not allowed to perform an IQ set on another user's vCard."),
{stop, xmpp:err_forbidden(Txt, Lang)};
vcard_iq_set(#iq{from = From, lang = Lang, sub_els = [VCard]} = IQ) ->
#jid{user = User, lserver = LServer} = From,
case set_vcard(User, LServer, VCard) of
{error, badarg} ->
%% Should not be here?
Txt = ?T("Nodeprep has failed"),
{stop, xmpp:err_internal_server_error(Txt, Lang)};
{error, not_implemented} ->
Txt = ?T("Updating the vCard is not supported by the vCard storage backend"),
{stop, xmpp:err_feature_not_implemented(Txt, Lang)};
ok ->
IQ
end;
vcard_iq_set(Acc) ->
Acc.
-spec set_vcard(binary(), binary(), xmlel() | vcard_temp()) ->
{error, badarg | not_implemented | binary()} | ok.
set_vcard(User, LServer, VCARD) ->
case jid:nodeprep(User) of
error ->
{error, badarg};
LUser ->
VCardEl = xmpp:encode(VCARD),
VCardSearch = make_vcard_search(User, LUser, LServer, VCardEl),
Mod = gen_mod:db_mod(LServer, ?MODULE),
case Mod:set_vcard(LUser, LServer, VCardEl, VCardSearch) of
{atomic, ok} ->
ets_cache:delete(?VCARD_CACHE, {LUser, LServer},
cache_nodes(Mod, LServer)),
ok;
{atomic, Error} ->
{error, Error}
end
end.
-spec string2lower(binary()) -> binary().
string2lower(String) ->
case stringprep:tolower_nofilter(String) of
Lower when is_binary(Lower) -> Lower;
error -> String
end.
-spec mk_tfield(binary(), binary(), binary()) -> xdata_field().
mk_tfield(Label, Var, Lang) ->
#xdata_field{type = 'text-single',
label = translate:translate(Lang, Label),
var = Var}.
-spec mk_field(binary(), binary()) -> xdata_field().
mk_field(Var, Val) ->
#xdata_field{var = Var, values = [Val]}.
-spec mk_search_form(jid(), binary(), binary()) -> search().
mk_search_form(JID, ServerHost, Lang) ->
Title = <<(translate:translate(Lang, ?T("Search users in ")))/binary,
(jid:encode(JID))/binary>>,
Mod = gen_mod:db_mod(ServerHost, ?MODULE),
SearchFields = Mod:search_fields(ServerHost),
Fs = [mk_tfield(Label, Var, Lang) || {Label, Var} <- SearchFields],
X = #xdata{type = form,
title = Title,
instructions = [make_instructions(Mod, Lang)],
fields = Fs},
#search{instructions =
translate:translate(
Lang, ?T("You need an x:data capable client to search")),
xdata = X}.
-spec make_instructions(module(), binary()) -> binary().
make_instructions(Mod, Lang) ->
Fill = translate:translate(
Lang,
?T("Fill in the form to search for any matching "
"XMPP User")),
Add = translate:translate(
Lang,
?T(" (Add * to the end of field to match substring)")),
case Mod of
mod_vcard_mnesia -> Fill;
_ -> str:concat(Fill, Add)
end.
-spec search_result(binary(), jid(), binary(), [xdata_field()]) -> xdata().
search_result(Lang, JID, ServerHost, XFields) ->
Mod = gen_mod:db_mod(ServerHost, ?MODULE),
Reported = [mk_tfield(Label, Var, Lang) ||
{Label, Var} <- Mod:search_reported(ServerHost)],
#xdata{type = result,
title = <<(translate:translate(Lang,
?T("Search Results for ")))/binary,
(jid:encode(JID))/binary>>,
reported = Reported,
items = lists:map(fun (Item) -> item_to_field(Item) end,
search(ServerHost, XFields))}.
-spec item_to_field([{binary(), binary()}]) -> [xdata_field()].
item_to_field(Items) ->
[mk_field(Var, Value) || {Var, Value} <- Items].
-spec search(binary(), [xdata_field()]) -> [binary()].
search(LServer, XFields) ->
Data = [{Var, Vals} || #xdata_field{var = Var, values = Vals} <- XFields],
Mod = gen_mod:db_mod(LServer, ?MODULE),
AllowReturnAll = mod_vcard_opt:allow_return_all(LServer),
MaxMatch = mod_vcard_opt:matches(LServer),
Mod:search(LServer, Data, AllowReturnAll, MaxMatch).
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
-spec remove_user(binary(), binary()) -> ok.
remove_user(User, Server) ->
LUser = jid:nodeprep(User),
LServer = jid:nameprep(Server),
Mod = gen_mod:db_mod(LServer, ?MODULE),
Mod:remove_user(LUser, LServer),
ets_cache:delete(?VCARD_CACHE, {LUser, LServer}, cache_nodes(Mod, LServer)).
-spec init_cache(module(), binary(), gen_mod:opts()) -> ok.
init_cache(Mod, Host, Opts) ->
case use_cache(Mod, Host) of
true ->
CacheOpts = cache_opts(Host, Opts),
ets_cache:new(?VCARD_CACHE, CacheOpts);
false ->
ets_cache:delete(?VCARD_CACHE)
end.
-spec cache_opts(binary(), gen_mod:opts()) -> [proplists:property()].
cache_opts(_Host, Opts) ->
MaxSize = mod_vcard_opt:cache_size(Opts),
CacheMissed = mod_vcard_opt:cache_missed(Opts),
LifeTime = mod_vcard_opt:cache_life_time(Opts),
[{max_size, MaxSize}, {cache_missed, CacheMissed}, {life_time, LifeTime}].
-spec use_cache(module(), binary()) -> boolean().
use_cache(Mod, Host) ->
case erlang:function_exported(Mod, use_cache, 1) of
true -> Mod:use_cache(Host);
false -> mod_vcard_opt:use_cache(Host)
end.
-spec cache_nodes(module(), binary()) -> [node()].
cache_nodes(Mod, Host) ->
case erlang:function_exported(Mod, cache_nodes, 1) of
true -> Mod:cache_nodes(Host);
false -> ejabberd_cluster:get_nodes()
end.
import_info() ->
[{<<"vcard">>, 3}, {<<"vcard_search">>, 24}].
import_start(LServer, DBType) ->
Mod = gen_mod:db_mod(DBType, ?MODULE),
Mod:init(LServer, []).
import(LServer, {sql, _}, DBType, Tab, L) ->
Mod = gen_mod:db_mod(DBType, ?MODULE),
Mod:import(LServer, Tab, L).
export(LServer) ->
Mod = gen_mod:db_mod(LServer, ?MODULE),
Mod:export(LServer).
%%%
%%% WebAdmin
%%%
webadmin_menu_hostuser(Acc, _Host, _Username, _Lang) ->
Acc ++ [{<<"vcard">>, <<"vCard">>}].
webadmin_page_hostuser(_, Host, User,
#request{path = [<<"vcard">>]} = R) ->
Head = ?H1GL(<<"vCard">>, <<"modules/#mod_vcard">>, <<"mod_vcard">>),
Set = [make_command(set_nickname, R, [{<<"user">>, User}, {<<"host">>, Host}], []),
make_command(set_vcard, R, [{<<"user">>, User}, {<<"host">>, Host}], []),
make_command(set_vcard2, R, [{<<"user">>, User}, {<<"host">>, Host}], []),
make_command(set_vcard2_multi, R, [{<<"user">>, User}, {<<"host">>, Host}], [])],
timer:sleep(100), % setting vcard takes a while, let's delay the get commands
FieldNames = [<<"VERSION">>, <<"FN">>, <<"NICKNAME">>, <<"BDAY">>],
FieldNames2 =
[{<<"N">>, <<"FAMILY">>},
{<<"N">>, <<"GIVEN">>},
{<<"N">>, <<"MIDDLE">>},
{<<"ADR">>, <<"CTRY">>},
{<<"ADR">>, <<"LOCALITY">>},
{<<"EMAIL">>, <<"USERID">>}],
Get = [make_command(get_vcard, R, [{<<"user">>, User}, {<<"host">>, Host}], []),
?XE(<<"blockquote">>,
[make_table([<<"name">>, <<"value">>],
[{?C(FieldName),
make_command(get_vcard,
R,
[{<<"user">>, User},
{<<"host">>, Host},
{<<"name">>, FieldName}],
[{only, value}])}
|| FieldName <- FieldNames])]),
make_command(get_vcard2, R, [{<<"user">>, User}, {<<"host">>, Host}], []),
?XE(<<"blockquote">>,
[make_table([<<"name">>, <<"subname">>, <<"value">>],
[{?C(FieldName),
?C(FieldSubName),
make_command(get_vcard2,
R,
[{<<"user">>, User},
{<<"host">>, Host},
{<<"name">>, FieldName},
{<<"subname">>, FieldSubName}],
[{only, value}])}
|| {FieldName, FieldSubName} <- FieldNames2])]),
make_command(get_vcard2_multi, R, [{<<"user">>, User}, {<<"host">>, Host}], [])],
{stop, Head ++ Get ++ Set};
webadmin_page_hostuser(Acc, _, _, _) -> Acc.
%%%
%%% Documentation
%%%
depends(_Host, _Opts) ->
[].
mod_opt_type(allow_return_all) ->
econf:bool();
mod_opt_type(name) ->
econf:binary();
mod_opt_type(matches) ->
econf:pos_int(infinity);
mod_opt_type(search) ->
econf:bool();
mod_opt_type(host) ->
econf:host();
mod_opt_type(hosts) ->
econf:hosts();
mod_opt_type(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);
mod_opt_type(vcard) ->
econf:vcard_temp().
mod_options(Host) ->
[{allow_return_all, false},
{host, <<"vjud.", Host/binary>>},
{hosts, []},
{matches, 30},
{search, false},
{name, ?T("vCard User Search")},
{vcard, undefined},
{db_type, ejabberd_config:default_db(Host, ?MODULE)},
{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 allows end users to store and retrieve "
"their vCard, and to retrieve other users vCards, "
"as defined in https://xmpp.org/extensions/xep-0054.html"
"[XEP-0054: vcard-temp]. The module also implements an "
"uncomplicated Jabber User Directory based on the vCards "
"of these users. Moreover, it enables the server to send "
"its vCard when queried."),
opts =>
[{allow_return_all,
#{value => "true | false",
desc =>
?T("This option enables you to specify if search "
"operations with empty input fields should return "
"all users who added some information to their vCard. "
"The default value is 'false'.")}},
{host,
#{desc => ?T("Deprecated. Use 'hosts' instead.")}},
{hosts,
#{value => ?T("[Host, ...]"),
desc =>
?T("This option defines the Jabber IDs of the service. "
"If the 'hosts' option is not specified, the only Jabber ID will "
"be the hostname of the virtual host with the prefix \"vjud.\". "
"The keyword '@HOST@' is replaced with the real virtual host name.")}},
{name,
#{value => ?T("Name"),
desc =>
?T("The value of the service name. This name is only visible in some "
"clients that support https://xmpp.org/extensions/xep-0030.html"
"[XEP-0030: Service Discovery]. The default is 'vCard User Search'.")}},
{matches,
#{value => "pos_integer() | infinity",
desc =>
?T("With this option, the number of reported search results "
"can be limited. If the option's value is set to 'infinity', "
"all search results are reported. The default value is '30'.")}},
{search,
#{value => "true | false",
desc =>
?T("This option specifies whether the search functionality "
"is enabled or not. If disabled, the options 'hosts', 'name' "
"and 'vcard' will be ignored and the Jabber User Directory "
"service will not appear in the Service Discovery item list. "
"The default value is 'false'.")}},
{db_type,
#{value => "mnesia | sql | ldap",
desc =>
?T("Same as top-level _`default_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.")}},
{vcard,
#{value => ?T("vCard"),
desc =>
?T("A custom vCard of the server that will be displayed "
"by some XMPP clients in Service Discovery. The value of "
"'vCard' is a YAML map constructed from an XML representation "
"of vCard. Since the representation has no attributes, "
"the mapping is straightforward."),
example =>
["# This XML representation of vCard:",
"# ",
"# ",
"# Conferences ",
"# ",
"# ",
"# Elm Street ",
"# ",
"# ",
"# ",
"# is translated to:",
"# ",
"vcard:",
" fn: Conferences",
" adr:",
" -",
" work: true",
" street: Elm Street"]}}]}.
ejabberd-24.12/src/mod_host_meta.erl 0000664 0001750 0001750 00000022621 14730775155 017776 0 ustar debalance debalance %%%-------------------------------------------------------------------
%%% 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 _`listen.md#ejabberd_http|ejabberd_http`_ "
"has _`listen-options.md#tls|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'.")}}]
}.
ejabberd-24.12/src/mod_stats.erl 0000664 0001750 0001750 00000020531 14730775155 017147 0 ustar debalance debalance %%%----------------------------------------------------------------------
%%% File : mod_stats.erl
%%% Author : Alexey Shchepin
%%% Purpose : Basic statistics.
%%% Created : 11 Jan 2003 by Alexey Shchepin
%%%
%%%
%%% 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_stats).
-author('alexey@process-one.net').
-protocol({xep, 39, '0.6.0', '0.1.0', "complete", ""}).
-behaviour(gen_mod).
-export([start/2, stop/1, reload/3, process_iq/1,
mod_options/1, depends/2, mod_doc/0]).
-include("logger.hrl").
-include_lib("xmpp/include/xmpp.hrl").
-include("translate.hrl").
start(_Host, _Opts) ->
{ok, [{iq_handler, ejabberd_local, ?NS_STATS, process_iq}]}.
stop(_Host) ->
ok.
reload(_Host, _NewOpts, _OldOpts) ->
ok.
depends(_Host, _Opts) ->
[].
process_iq(#iq{type = set, lang = Lang} = IQ) ->
Txt = ?T("Value 'set' of 'type' attribute is not allowed"),
xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang));
process_iq(#iq{type = get, to = To, lang = Lang,
sub_els = [#stats{} = Stats]} = IQ) ->
Node = str:tokens(Stats#stats.node, <<"/">>),
Names = [Name || #stat{name = Name} <- Stats#stats.list],
case get_local_stats(To#jid.server, Node, Names, Lang) of
{result, List} ->
xmpp:make_iq_result(IQ, Stats#stats{list = List});
{error, Error} ->
xmpp:make_error(IQ, Error)
end.
-define(STAT(Name), #stat{name = Name}).
get_local_stats(_Server, [], [], _Lang) ->
{result,
[?STAT(<<"users/online">>), ?STAT(<<"users/total">>),
?STAT(<<"users/all-hosts/online">>),
?STAT(<<"users/all-hosts/total">>)]};
get_local_stats(Server, [], Names, _Lang) ->
{result,
lists:map(fun (Name) -> get_local_stat(Server, [], Name)
end,
Names)};
get_local_stats(_Server, [<<"running nodes">>, _],
[], _Lang) ->
{result,
[?STAT(<<"time/uptime">>), ?STAT(<<"time/cputime">>),
?STAT(<<"users/online">>),
?STAT(<<"transactions/committed">>),
?STAT(<<"transactions/aborted">>),
?STAT(<<"transactions/restarted">>),
?STAT(<<"transactions/logged">>)]};
get_local_stats(_Server, [<<"running nodes">>, ENode],
Names, Lang) ->
case search_running_node(ENode) of
false ->
Txt = ?T("No running node found"),
{error, xmpp:err_item_not_found(Txt, Lang)};
Node ->
{result,
lists:map(fun (Name) -> get_node_stat(Node, Name) end,
Names)}
end;
get_local_stats(_Server, _, _, Lang) ->
Txt = ?T("No statistics found for this item"),
{error, xmpp:err_feature_not_implemented(Txt, Lang)}.
-define(STATVAL(Val, Unit), #stat{name = Name, units = Unit, value = Val}).
-define(STATERR(Code, Desc),
#stat{name = Name,
error = #stat_error{code = Code, reason = Desc}}).
get_local_stat(Server, [], Name)
when Name == <<"users/online">> ->
case catch ejabberd_sm:get_vh_session_list(Server) of
{'EXIT', _Reason} ->
?STATERR(500, <<"Internal Server Error">>);
Users ->
?STATVAL((integer_to_binary(length(Users))),
<<"users">>)
end;
get_local_stat(Server, [], Name)
when Name == <<"users/total">> ->
case catch
ejabberd_auth:count_users(Server)
of
{'EXIT', _Reason} ->
?STATERR(500, <<"Internal Server Error">>);
NUsers ->
?STATVAL((integer_to_binary(NUsers)),
<<"users">>)
end;
get_local_stat(_Server, [], Name)
when Name == <<"users/all-hosts/online">> ->
Users = ejabberd_sm:connected_users_number(),
?STATVAL((integer_to_binary(Users)), <<"users">>);
get_local_stat(_Server, [], Name)
when Name == <<"users/all-hosts/total">> ->
NumUsers = lists:foldl(fun (Host, Total) ->
ejabberd_auth:count_users(Host)
+ Total
end,
0, ejabberd_option:hosts()),
?STATVAL((integer_to_binary(NumUsers)),
<<"users">>);
get_local_stat(_Server, _, Name) ->
?STATERR(404, <<"Not Found">>).
get_node_stat(Node, Name)
when Name == <<"time/uptime">> ->
case catch ejabberd_cluster:call(Node, erlang, statistics,
[wall_clock])
of
{badrpc, _Reason} ->
?STATERR(500, <<"Internal Server Error">>);
CPUTime ->
?STATVAL(str:format("~.3f", [element(1, CPUTime) / 1000]),
<<"seconds">>)
end;
get_node_stat(Node, Name)
when Name == <<"time/cputime">> ->
case catch ejabberd_cluster:call(Node, erlang, statistics, [runtime])
of
{badrpc, _Reason} ->
?STATERR(500, <<"Internal Server Error">>);
RunTime ->
?STATVAL(str:format("~.3f", [element(1, RunTime) / 1000]),
<<"seconds">>)
end;
get_node_stat(Node, Name)
when Name == <<"users/online">> ->
case catch ejabberd_cluster:call(Node, ejabberd_sm,
dirty_get_my_sessions_list, [])
of
{badrpc, _Reason} ->
?STATERR(500, <<"Internal Server Error">>);
Users ->
?STATVAL((integer_to_binary(length(Users))),
<<"users">>)
end;
get_node_stat(Node, Name)
when Name == <<"transactions/committed">> ->
case catch ejabberd_cluster:call(Node, mnesia, system_info,
[transaction_commits])
of
{badrpc, _Reason} ->
?STATERR(500, <<"Internal Server Error">>);
Transactions ->
?STATVAL((integer_to_binary(Transactions)),
<<"transactions">>)
end;
get_node_stat(Node, Name)
when Name == <<"transactions/aborted">> ->
case catch ejabberd_cluster:call(Node, mnesia, system_info,
[transaction_failures])
of
{badrpc, _Reason} ->
?STATERR(500, <<"Internal Server Error">>);
Transactions ->
?STATVAL((integer_to_binary(Transactions)),
<<"transactions">>)
end;
get_node_stat(Node, Name)
when Name == <<"transactions/restarted">> ->
case catch ejabberd_cluster:call(Node, mnesia, system_info,
[transaction_restarts])
of
{badrpc, _Reason} ->
?STATERR(500, <<"Internal Server Error">>);
Transactions ->
?STATVAL((integer_to_binary(Transactions)),
<<"transactions">>)
end;
get_node_stat(Node, Name)
when Name == <<"transactions/logged">> ->
case catch ejabberd_cluster:call(Node, mnesia, system_info,
[transaction_log_writes])
of
{badrpc, _Reason} ->
?STATERR(500, <<"Internal Server Error">>);
Transactions ->
?STATVAL((integer_to_binary(Transactions)),
<<"transactions">>)
end;
get_node_stat(_, Name) ->
?STATERR(404, <<"Not Found">>).
search_running_node(SNode) ->
search_running_node(SNode,
mnesia:system_info(running_db_nodes)).
search_running_node(_, []) -> false;
search_running_node(SNode, [Node | Nodes]) ->
case iolist_to_binary(atom_to_list(Node)) of
SNode -> Node;
_ -> search_running_node(SNode, Nodes)
end.
mod_options(_Host) ->
[].
mod_doc() ->
#{desc =>
[?T("This module adds support for "
"https://xmpp.org/extensions/xep-0039.html"
"[XEP-0039: Statistics Gathering]. This protocol "
"allows you to retrieve the following statistics "
"from your ejabberd server:"), "",
?T("- Total number of registered users on the current "
"virtual host (users/total)."), "",
?T("- Total number of registered users on all virtual "
"hosts (users/all-hosts/total)."), "",
?T("- Total number of online users on the current "
"virtual host (users/online)."), "",
?T("- Total number of online users on all virtual "
"hosts (users/all-hosts/online)."), "",
?T("NOTE: The protocol extension is deferred and seems "
"like even a few clients that were supporting it "
"are now abandoned. So using this module makes "
"very little sense.")]}.
ejabberd-24.12/src/mod_privacy_sql.erl 0000664 0001750 0001750 00000044776 14730775155 020366 0 ustar debalance debalance %%%-------------------------------------------------------------------
%%% File : mod_privacy_sql.erl
%%% Author : Evgeny Khramtsov
%%% Created : 14 Apr 2016 by Evgeny Khramtsov
%%%
%%%
%%% 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_privacy_sql).
-behaviour(mod_privacy).
%% API
-export([init/2, set_default/3, unset_default/2, set_lists/1,
set_list/4, get_lists/2, get_list/3, remove_lists/2,
remove_list/3, import/1, export/1]).
-export([item_to_raw/1, raw_to_item/1]).
-export([sql_schemas/0]).
-include_lib("xmpp/include/xmpp.hrl").
-include("mod_privacy.hrl").
-include("logger.hrl").
-include("ejabberd_sql_pt.hrl").
%%%===================================================================
%%% API
%%%===================================================================
init(Host, _Opts) ->
ejabberd_sql_schema:update_schema(Host, ?MODULE, sql_schemas()),
ok.
sql_schemas() ->
[#sql_schema{
version = 1,
tables =
[#sql_table{
name = <<"privacy_default_list">>,
columns =
[#sql_column{name = <<"username">>, type = text},
#sql_column{name = <<"server_host">>, type = text},
#sql_column{name = <<"name">>, type = text}],
indices = [#sql_index{
columns = [<<"server_host">>, <<"username">>],
unique = true}]},
#sql_table{
name = <<"privacy_list">>,
columns =
[#sql_column{name = <<"username">>, type = text},
#sql_column{name = <<"server_host">>, type = text},
#sql_column{name = <<"name">>, type = text},
#sql_column{name = <<"id">>, type = bigserial},
#sql_column{name = <<"created_at">>, type = timestamp,
default = true}],
indices = [#sql_index{
columns = [<<"id">>],
unique = true},
#sql_index{
columns = [<<"server_host">>, <<"username">>,
<<"name">>],
unique = true}]},
#sql_table{
name = <<"privacy_list_data">>,
columns =
[#sql_column{name = <<"id">>, type = bigint,
opts = [#sql_references{
table = <<"privacy_list">>,
column = <<"id">>}]},
#sql_column{name = <<"t">>, type = {char, 1}},
#sql_column{name = <<"value">>, type = text},
#sql_column{name = <<"action">>, type = {char, 1}},
#sql_column{name = <<"ord">>, type = numeric},
#sql_column{name = <<"match_all">>, type = boolean},
#sql_column{name = <<"match_iq">>, type = boolean},
#sql_column{name = <<"match_message">>, type = boolean},
#sql_column{name = <<"match_presence_in">>, type = boolean},
#sql_column{name = <<"match_presence_out">>, type = boolean}],
indices = [#sql_index{columns = [<<"id">>]}]}]}].
unset_default(LUser, LServer) ->
case unset_default_privacy_list(LUser, LServer) of
ok ->
ok;
_Err ->
{error, db_failure}
end.
set_default(LUser, LServer, Name) ->
F = fun () ->
case get_privacy_list_names_t(LUser, LServer) of
{selected, []} ->
{error, notfound};
{selected, Names} ->
case lists:member({Name}, Names) of
true ->
set_default_privacy_list(LUser, LServer, Name);
false ->
{error, notfound}
end
end
end,
transaction(LServer, F).
remove_list(LUser, LServer, Name) ->
F = fun () ->
case get_default_privacy_list_t(LUser, LServer) of
{selected, []} ->
remove_privacy_list_t(LUser, LServer, Name);
{selected, [{Default}]} ->
if Name == Default ->
{error, conflict};
true ->
remove_privacy_list_t(LUser, LServer, Name)
end
end
end,
transaction(LServer, F).
set_lists(#privacy{us = {LUser, LServer},
default = Default,
lists = Lists}) ->
F = fun() ->
lists:foreach(
fun({Name, List}) ->
add_privacy_list(LUser, LServer, Name),
{selected, [{I}]} =
get_privacy_list_id_t(LUser, LServer, Name),
RItems = lists:map(fun item_to_raw/1, List),
set_privacy_list(I, RItems),
if is_binary(Default) ->
set_default_privacy_list(
LUser, LServer, Default);
true ->
ok
end
end, Lists)
end,
transaction(LServer, F).
set_list(LUser, LServer, Name, List) ->
RItems = lists:map(fun item_to_raw/1, List),
F = fun() ->
{ID, New} = case get_privacy_list_id_t(LUser, LServer, Name) of
{selected, []} ->
add_privacy_list(LUser, LServer, Name),
{selected, [{I}]} =
get_privacy_list_id_t(LUser, LServer, Name),
{I, true};
{selected, [{I}]} -> {I, false}
end,
case New of
false ->
set_privacy_list(ID, RItems);
_ ->
set_privacy_list_new(ID, RItems)
end
end,
transaction(LServer, F).
get_list(LUser, LServer, default) ->
case get_default_privacy_list(LUser, LServer) of
{selected, []} ->
error;
{selected, [{Default}]} ->
get_list(LUser, LServer, Default);
_Err ->
{error, db_failure}
end;
get_list(LUser, LServer, Name) ->
case get_privacy_list_data(LUser, LServer, Name) of
{selected, []} ->
error;
{selected, RItems} ->
{ok, {Name, lists:flatmap(fun raw_to_item/1, RItems)}};
_Err ->
{error, db_failure}
end.
get_lists(LUser, LServer) ->
case get_default_privacy_list(LUser, LServer) of
{selected, Selected} ->
Default = case Selected of
[] -> none;
[{DefName}] -> DefName
end,
case get_privacy_list_names(LUser, LServer) of
{selected, Names} ->
case lists:foldl(
fun(_, {error, _} = Err) ->
Err;
({Name}, Acc) ->
case get_privacy_list_data(LUser, LServer, Name) of
{selected, RItems} ->
Items = lists:flatmap(
fun raw_to_item/1,
RItems),
[{Name, Items}|Acc];
_Err ->
{error, db_failure}
end
end, [], Names) of
{error, Reason} ->
{error, Reason};
Lists ->
{ok, #privacy{default = Default,
us = {LUser, LServer},
lists = Lists}}
end;
_Err ->
{error, db_failure}
end;
_Err ->
{error, db_failure}
end.
remove_lists(LUser, LServer) ->
case del_privacy_lists(LUser, LServer) of
ok ->
ok;
_Err ->
{error, db_failure}
end.
export(Server) ->
SqlType = ejabberd_option:sql_type(Server),
case catch ejabberd_sql:sql_query(jid:nameprep(Server),
[<<"select id from privacy_list order by "
"id desc limit 1;">>]) of
{selected, [<<"id">>], [[I]]} ->
put(id, binary_to_integer(I));
_ ->
put(id, 0)
end,
[{privacy,
fun(Host, #privacy{us = {LUser, LServer}, lists = Lists,
default = Default})
when LServer == Host ->
if Default /= none ->
[?SQL("delete from privacy_default_list where"
" username=%(LUser)s and %(LServer)H;"),
?SQL_INSERT(
"privacy_default_list",
["username=%(LUser)s",
"server_host=%(LServer)s",
"name=%(Default)s"])];
true ->
[]
end ++
lists:flatmap(
fun({Name, List}) ->
RItems = lists:map(fun item_to_raw/1, List),
ID = get_id(),
[?SQL("delete from privacy_list where"
" username=%(LUser)s and %(LServer)H and"
" name=%(Name)s;"),
?SQL_INSERT(
"privacy_list",
["username=%(LUser)s",
"server_host=%(LServer)s",
"name=%(Name)s",
"id=%(ID)d"]),
?SQL("delete from privacy_list_data where"
" id=%(ID)d;")] ++
case SqlType of
pgsql ->
[?SQL("insert into privacy_list_data(id, t, "
"value, action, ord, match_all, match_iq, "
"match_message, match_presence_in, "
"match_presence_out) "
"values (%(ID)d, %(SType)s, %(SValue)s, %(SAction)s,"
" %(Order)d, CAST(%(MatchAll)b as boolean), CAST(%(MatchIQ)b as boolean),"
" CAST(%(MatchMessage)b as boolean), CAST(%(MatchPresenceIn)b as boolean),"
" CAST(%(MatchPresenceOut)b as boolean));")
|| {SType, SValue, SAction, Order,
MatchAll, MatchIQ,
MatchMessage, MatchPresenceIn,
MatchPresenceOut} <- RItems];
_ ->
[?SQL("insert into privacy_list_data(id, t, "
"value, action, ord, match_all, match_iq, "
"match_message, match_presence_in, "
"match_presence_out) "
"values (%(ID)d, %(SType)s, %(SValue)s, %(SAction)s,"
" %(Order)d, %(MatchAll)b, %(MatchIQ)b,"
" %(MatchMessage)b, %(MatchPresenceIn)b,"
" %(MatchPresenceOut)b);")
|| {SType, SValue, SAction, Order,
MatchAll, MatchIQ,
MatchMessage, MatchPresenceIn,
MatchPresenceOut} <- RItems]
end
end,
Lists);
(_Host, _R) ->
[]
end}].
get_id() ->
ID = get(id),
put(id, ID + 1),
ID + 1.
import(_) ->
ok.
%%%===================================================================
%%% Internal functions
%%%===================================================================
transaction(LServer, F) ->
case ejabberd_sql:sql_transaction(LServer, F) of
{atomic, Res} -> Res;
{aborted, _Reason} -> {error, db_failure}
end.
raw_to_item({SType, SValue, SAction, Order, MatchAll,
MatchIQ, MatchMessage, MatchPresenceIn,
MatchPresenceOut} = Row) ->
try
{Type, Value} = case SType of
<<"n">> -> {none, none};
<<"j">> ->
JID = jid:decode(SValue),
{jid, jid:tolower(JID)};
<<"g">> -> {group, SValue};
<<"s">> ->
case SValue of
<<"none">> -> {subscription, none};
<<"both">> -> {subscription, both};
<<"from">> -> {subscription, from};
<<"to">> -> {subscription, to}
end
end,
Action = case SAction of
<<"a">> -> allow;
<<"d">> -> deny
end,
[#listitem{type = Type, value = Value, action = Action,
order = Order, match_all = MatchAll, match_iq = MatchIQ,
match_message = MatchMessage,
match_presence_in = MatchPresenceIn,
match_presence_out = MatchPresenceOut}]
catch _:_ ->
?WARNING_MSG("Failed to parse row: ~p", [Row]),
[]
end.
item_to_raw(#listitem{type = Type, value = Value,
action = Action, order = Order, match_all = MatchAll,
match_iq = MatchIQ, match_message = MatchMessage,
match_presence_in = MatchPresenceIn,
match_presence_out = MatchPresenceOut}) ->
{SType, SValue} = case Type of
none -> {<<"n">>, <<"">>};
jid -> {<<"j">>, jid:encode(Value)};
group -> {<<"g">>, Value};
subscription ->
case Value of
none -> {<<"s">>, <<"none">>};
both -> {<<"s">>, <<"both">>};
from -> {<<"s">>, <<"from">>};
to -> {<<"s">>, <<"to">>}
end
end,
SAction = case Action of
allow -> <<"a">>;
deny -> <<"d">>
end,
{SType, SValue, SAction, Order, MatchAll, MatchIQ,
MatchMessage, MatchPresenceIn, MatchPresenceOut}.
get_default_privacy_list(LUser, LServer) ->
ejabberd_sql:sql_query(
LServer,
?SQL("select @(name)s from privacy_default_list "
"where username=%(LUser)s and %(LServer)H")).
get_default_privacy_list_t(LUser, LServer) ->
ejabberd_sql:sql_query_t(
?SQL("select @(name)s from privacy_default_list "
"where username=%(LUser)s and %(LServer)H")).
get_privacy_list_names(LUser, LServer) ->
ejabberd_sql:sql_query(
LServer,
?SQL("select @(name)s from privacy_list"
" where username=%(LUser)s and %(LServer)H")).
get_privacy_list_names_t(LUser, LServer) ->
ejabberd_sql:sql_query_t(
?SQL("select @(name)s from privacy_list"
" where username=%(LUser)s and %(LServer)H")).
get_privacy_list_id_t(LUser, LServer, Name) ->
ejabberd_sql:sql_query_t(
?SQL("select @(id)d from privacy_list"
" where username=%(LUser)s and %(LServer)H and name=%(Name)s")).
get_privacy_list_data(LUser, LServer, Name) ->
ejabberd_sql:sql_query(
LServer,
?SQL("select @(t)s, @(value)s, @(action)s, @(ord)d, @(match_all)b, "
"@(match_iq)b, @(match_message)b, @(match_presence_in)b, "
"@(match_presence_out)b from privacy_list_data "
"where id ="
" (select id from privacy_list"
" where username=%(LUser)s and %(LServer)H and name=%(Name)s) "
"order by ord")).
set_default_privacy_list(LUser, LServer, Name) ->
?SQL_UPSERT_T(
"privacy_default_list",
["!username=%(LUser)s",
"!server_host=%(LServer)s",
"name=%(Name)s"]).
unset_default_privacy_list(LUser, LServer) ->
case ejabberd_sql:sql_query(
LServer,
?SQL("delete from privacy_default_list"
" where username=%(LUser)s and %(LServer)H")) of
{updated, _} -> ok;
Err -> Err
end.
remove_privacy_list_t(LUser, LServer, Name) ->
case ejabberd_sql:sql_query_t(
?SQL("delete from privacy_list where"
" username=%(LUser)s and %(LServer)H and name=%(Name)s")) of
{updated, 0} -> {error, notfound};
{updated, _} -> ok;
Err -> Err
end.
add_privacy_list(LUser, LServer, Name) ->
ejabberd_sql:sql_query_t(
?SQL_INSERT(
"privacy_list",
["username=%(LUser)s",
"server_host=%(LServer)s",
"name=%(Name)s"])).
set_privacy_list_new(ID, RItems) ->
lists:foreach(
fun({SType, SValue, SAction, Order, MatchAll, MatchIQ,
MatchMessage, MatchPresenceIn, MatchPresenceOut}) ->
ejabberd_sql:sql_query_t(
?SQL("insert into privacy_list_data(id, t, "
"value, action, ord, match_all, match_iq, "
"match_message, match_presence_in, match_presence_out) "
"values (%(ID)d, %(SType)s, %(SValue)s, %(SAction)s,"
" %(Order)d, %(MatchAll)b, %(MatchIQ)b,"
" %(MatchMessage)b, %(MatchPresenceIn)b,"
" %(MatchPresenceOut)b)"))
end,
RItems).
calculate_difference(List1, List2) ->
Set1 = gb_sets:from_list(List1),
Set2 = gb_sets:from_list(List2),
{gb_sets:to_list(gb_sets:subtract(Set1, Set2)),
gb_sets:to_list(gb_sets:subtract(Set2, Set1))}.
set_privacy_list(ID, RItems) ->
case ejabberd_sql:sql_query_t(
?SQL("select @(t)s, @(value)s, @(action)s, @(ord)d, @(match_all)b, "
"@(match_iq)b, @(match_message)b, @(match_presence_in)b, "
"@(match_presence_out)b from privacy_list_data "
"where id=%(ID)d")) of
{selected, ExistingItems} ->
{ToAdd2, ToDelete2} = calculate_difference(RItems, ExistingItems),
ToAdd3 = if
ToDelete2 /= [] ->
ejabberd_sql:sql_query_t(
?SQL("delete from privacy_list_data where id=%(ID)d")),
RItems;
true ->
ToAdd2
end,
lists:foreach(
fun({SType, SValue, SAction, Order, MatchAll, MatchIQ,
MatchMessage, MatchPresenceIn, MatchPresenceOut}) ->
ejabberd_sql:sql_query_t(
?SQL("insert into privacy_list_data(id, t, "
"value, action, ord, match_all, match_iq, "
"match_message, match_presence_in, match_presence_out) "
"values (%(ID)d, %(SType)s, %(SValue)s, %(SAction)s,"
" %(Order)d, %(MatchAll)b, %(MatchIQ)b,"
" %(MatchMessage)b, %(MatchPresenceIn)b,"
" %(MatchPresenceOut)b)"))
end,
ToAdd3);
Err ->
Err
end.
del_privacy_lists(LUser, LServer) ->
case ejabberd_sql:sql_query(
LServer,
?SQL("delete from privacy_list where username=%(LUser)s and %(LServer)H")) of
{updated, _} ->
case ejabberd_sql:sql_query(
LServer,
?SQL("delete from privacy_default_list "
"where username=%(LUser)s and %(LServer)H")) of
{updated, _} -> ok;
Err -> Err
end;
Err ->
Err
end.
ejabberd-24.12/src/mod_mqtt_bridge_opt.erl 0000664 0001750 0001750 00000001377 14730775155 021203 0 ustar debalance debalance %% Generated automatically
%% DO NOT EDIT: run `make options` instead
-module(mod_mqtt_bridge_opt).
-export([replication_user/1]).
-export([servers/1]).
-spec replication_user(gen_mod:opts() | global | binary()) -> jid:jid().
replication_user(Opts) when is_map(Opts) ->
gen_mod:get_opt(replication_user, Opts);
replication_user(Host) ->
gen_mod:get_module_opt(Host, mod_mqtt_bridge, replication_user).
-spec servers(gen_mod:opts() | global | binary()) -> {[{atom(),'mqtt' | 'mqtts' | 'mqtt5' | 'mqtt5s',binary(),non_neg_integer(),#{binary()=>binary()},#{binary()=>binary()},map()}],#{binary()=>[atom()]}}.
servers(Opts) when is_map(Opts) ->
gen_mod:get_opt(servers, Opts);
servers(Host) ->
gen_mod:get_module_opt(Host, mod_mqtt_bridge, servers).
ejabberd-24.12/src/mod_ping_opt.erl 0000664 0001750 0001750 00000002242 14730775155 017627 0 ustar debalance debalance %% Generated automatically
%% DO NOT EDIT: run `make options` instead
-module(mod_ping_opt).
-export([ping_ack_timeout/1]).
-export([ping_interval/1]).
-export([send_pings/1]).
-export([timeout_action/1]).
-spec ping_ack_timeout(gen_mod:opts() | global | binary()) -> 'undefined' | pos_integer().
ping_ack_timeout(Opts) when is_map(Opts) ->
gen_mod:get_opt(ping_ack_timeout, Opts);
ping_ack_timeout(Host) ->
gen_mod:get_module_opt(Host, mod_ping, ping_ack_timeout).
-spec ping_interval(gen_mod:opts() | global | binary()) -> pos_integer().
ping_interval(Opts) when is_map(Opts) ->
gen_mod:get_opt(ping_interval, Opts);
ping_interval(Host) ->
gen_mod:get_module_opt(Host, mod_ping, ping_interval).
-spec send_pings(gen_mod:opts() | global | binary()) -> boolean().
send_pings(Opts) when is_map(Opts) ->
gen_mod:get_opt(send_pings, Opts);
send_pings(Host) ->
gen_mod:get_module_opt(Host, mod_ping, send_pings).
-spec timeout_action(gen_mod:opts() | global | binary()) -> 'kill' | 'none'.
timeout_action(Opts) when is_map(Opts) ->
gen_mod:get_opt(timeout_action, Opts);
timeout_action(Host) ->
gen_mod:get_module_opt(Host, mod_ping, timeout_action).
ejabberd-24.12/src/mod_disco_opt.erl 0000664 0001750 0001750 00000001533 14730775155 017775 0 ustar debalance debalance %% Generated automatically
%% DO NOT EDIT: run `make options` instead
-module(mod_disco_opt).
-export([extra_domains/1]).
-export([name/1]).
-export([server_info/1]).
-spec extra_domains(gen_mod:opts() | global | binary()) -> [binary()].
extra_domains(Opts) when is_map(Opts) ->
gen_mod:get_opt(extra_domains, Opts);
extra_domains(Host) ->
gen_mod:get_module_opt(Host, mod_disco, extra_domains).
-spec name(gen_mod:opts() | global | binary()) -> binary().
name(Opts) when is_map(Opts) ->
gen_mod:get_opt(name, Opts);
name(Host) ->
gen_mod:get_module_opt(Host, mod_disco, name).
-spec server_info(gen_mod:opts() | global | binary()) -> [{'all' | [module()],binary(),[binary()]}].
server_info(Opts) when is_map(Opts) ->
gen_mod:get_opt(server_info, Opts);
server_info(Host) ->
gen_mod:get_module_opt(Host, mod_disco, server_info).
ejabberd-24.12/src/mod_muc_rtbl_opt.erl 0000664 0001750 0001750 00000001126 14730775155 020501 0 ustar debalance debalance %% Generated automatically
%% DO NOT EDIT: run `make options` instead
-module(mod_muc_rtbl_opt).
-export([rtbl_node/1]).
-export([rtbl_server/1]).
-spec rtbl_node(gen_mod:opts() | global | binary()) -> binary().
rtbl_node(Opts) when is_map(Opts) ->
gen_mod:get_opt(rtbl_node, Opts);
rtbl_node(Host) ->
gen_mod:get_module_opt(Host, mod_muc_rtbl, rtbl_node).
-spec rtbl_server(gen_mod:opts() | global | binary()) -> binary().
rtbl_server(Opts) when is_map(Opts) ->
gen_mod:get_opt(rtbl_server, Opts);
rtbl_server(Host) ->
gen_mod:get_module_opt(Host, mod_muc_rtbl, rtbl_server).
ejabberd-24.12/src/ejabberd_cluster_mnesia.erl 0000664 0001750 0001750 00000011211 14730775155 022000 0 ustar debalance debalance %%%----------------------------------------------------------------------
%%% File : ejabberd_cluster_mnesia.erl
%%% Author : Christophe Romain
%%% Purpose : ejabberd clustering management via Mnesia
%%% Created : 7 Oct 2015 by Christophe Romain
%%%
%%%
%%% 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(ejabberd_cluster_mnesia).
-behaviour(ejabberd_cluster).
%% API
-export([init/0, get_nodes/0, join/1, leave/1,
get_known_nodes/0, node_id/0, get_node_by_id/1,
send/2, wait_for_sync/1, subscribe/1]).
-include("logger.hrl").
-spec init() -> ok.
init() ->
ok.
-spec get_nodes() -> [node()].
get_nodes() ->
mnesia:system_info(running_db_nodes).
-spec get_known_nodes() -> [node()].
get_known_nodes() ->
lists:usort(mnesia:system_info(db_nodes)
++ mnesia:system_info(extra_db_nodes)).
-spec join(node()) -> ok | {error, any()}.
join(Node) ->
case {node(), net_adm:ping(Node)} of
{Node, _} ->
{error, {not_master, Node}};
{_, pong} ->
application:stop(ejabberd),
application:stop(mnesia),
mnesia:delete_schema([node()]),
application:start(mnesia),
case mnesia:change_config(extra_db_nodes, [Node]) of
{ok, _} ->
replicate_database(Node),
wait_for_sync(infinity),
application:stop(mnesia),
application:start(ejabberd);
{error, Reason} ->
{error, Reason}
end;
_ ->
{error, {no_ping, Node}}
end.
-spec leave(node()) -> ok | {error, any()}.
leave(Node) ->
case {node(), net_adm:ping(Node)} of
{Node, _} ->
Cluster = get_nodes()--[Node],
leave(Cluster, Node);
{_, pong} ->
rpc:call(Node, ?MODULE, leave, [Node], 10000);
{_, pang} ->
case mnesia:del_table_copy(schema, Node) of
{atomic, ok} -> ok;
{aborted, Reason} -> {error, Reason}
end
end.
leave([], Node) ->
{error, {no_cluster, Node}};
leave([Master|_], Node) ->
application:stop(ejabberd),
application:stop(mnesia),
spawn(fun() ->
rpc:call(Master, mnesia, del_table_copy, [schema, Node]),
mnesia:delete_schema([node()]),
erlang:halt(0)
end),
ok.
-spec node_id() -> binary().
node_id() ->
integer_to_binary(erlang:phash2(node())).
-spec get_node_by_id(binary()) -> node().
get_node_by_id(Hash) ->
try binary_to_integer(Hash) of
I -> match_node_id(I)
catch _:_ ->
node()
end.
-spec send({atom(), node()}, term()) -> boolean().
send(Dst, Msg) ->
case erlang:send(Dst, Msg, [nosuspend, noconnect]) of
ok -> true;
_ -> false
end.
-spec wait_for_sync(timeout()) -> ok.
wait_for_sync(Timeout) ->
?INFO_MSG("Waiting for Mnesia synchronization to complete", []),
mnesia:wait_for_tables(mnesia:system_info(local_tables), Timeout),
ok.
-spec subscribe(_) -> ok.
subscribe(_) ->
ok.
%%%===================================================================
%%% Internal functions
%%%===================================================================
replicate_database(Node) ->
mnesia:change_table_copy_type(schema, node(), disc_copies),
lists:foreach(
fun(Table) ->
Type = rpc:call(Node, mnesia, table_info, [Table, storage_type]),
mnesia:add_table_copy(Table, node(), Type)
end, mnesia:system_info(tables)--[schema]).
-spec match_node_id(integer()) -> node().
match_node_id(I) ->
match_node_id(I, get_nodes()).
-spec match_node_id(integer(), [node()]) -> node().
match_node_id(I, [Node|Nodes]) ->
case erlang:phash2(Node) of
I -> Node;
_ -> match_node_id(I, Nodes)
end;
match_node_id(_I, []) ->
node().
ejabberd-24.12/src/mod_muc_admin_opt.erl 0000664 0001750 0001750 00000000722 14730775155 020627 0 ustar debalance debalance %% Generated automatically
%% DO NOT EDIT: run `make options` instead
-module(mod_muc_admin_opt).
-export([subscribe_room_many_max_users/1]).
-spec subscribe_room_many_max_users(gen_mod:opts() | global | binary()) -> integer().
subscribe_room_many_max_users(Opts) when is_map(Opts) ->
gen_mod:get_opt(subscribe_room_many_max_users, Opts);
subscribe_room_many_max_users(Host) ->
gen_mod:get_module_opt(Host, mod_muc_admin, subscribe_room_many_max_users).
ejabberd-24.12/src/mod_auth_fast_opt.erl 0000664 0001750 0001750 00000001613 14730775155 020651 0 ustar debalance debalance %% Generated automatically
%% DO NOT EDIT: run `make options` instead
-module(mod_auth_fast_opt).
-export([db_type/1]).
-export([token_lifetime/1]).
-export([token_refresh_age/1]).
-spec db_type(gen_mod:opts() | global | binary()) -> atom().
db_type(Opts) when is_map(Opts) ->
gen_mod:get_opt(db_type, Opts);
db_type(Host) ->
gen_mod:get_module_opt(Host, mod_auth_fast, db_type).
-spec token_lifetime(gen_mod:opts() | global | binary()) -> pos_integer().
token_lifetime(Opts) when is_map(Opts) ->
gen_mod:get_opt(token_lifetime, Opts);
token_lifetime(Host) ->
gen_mod:get_module_opt(Host, mod_auth_fast, token_lifetime).
-spec token_refresh_age(gen_mod:opts() | global | binary()) -> pos_integer().
token_refresh_age(Opts) when is_map(Opts) ->
gen_mod:get_opt(token_refresh_age, Opts);
token_refresh_age(Host) ->
gen_mod:get_module_opt(Host, mod_auth_fast, token_refresh_age).
ejabberd-24.12/src/mod_muc_opt.erl 0000664 0001750 0001750 00000022627 14730775155 017467 0 ustar debalance debalance %% Generated automatically
%% DO NOT EDIT: run `make options` instead
-module(mod_muc_opt).
-export([access/1]).
-export([access_admin/1]).
-export([access_create/1]).
-export([access_mam/1]).
-export([access_persistent/1]).
-export([access_register/1]).
-export([cleanup_affiliations_on_start/1]).
-export([db_type/1]).
-export([default_room_options/1]).
-export([hibernation_timeout/1]).
-export([history_size/1]).
-export([host/1]).
-export([hosts/1]).
-export([max_captcha_whitelist/1]).
-export([max_password/1]).
-export([max_room_desc/1]).
-export([max_room_id/1]).
-export([max_room_name/1]).
-export([max_rooms_discoitems/1]).
-export([max_user_conferences/1]).
-export([max_users/1]).
-export([max_users_admin_threshold/1]).
-export([max_users_presence/1]).
-export([min_message_interval/1]).
-export([min_presence_interval/1]).
-export([name/1]).
-export([preload_rooms/1]).
-export([queue_type/1]).
-export([ram_db_type/1]).
-export([regexp_room_id/1]).
-export([room_shaper/1]).
-export([user_message_shaper/1]).
-export([user_presence_shaper/1]).
-export([vcard/1]).
-spec access(gen_mod:opts() | global | binary()) -> 'all' | acl:acl().
access(Opts) when is_map(Opts) ->
gen_mod:get_opt(access, Opts);
access(Host) ->
gen_mod:get_module_opt(Host, mod_muc, access).
-spec access_admin(gen_mod:opts() | global | binary()) -> 'none' | acl:acl().
access_admin(Opts) when is_map(Opts) ->
gen_mod:get_opt(access_admin, Opts);
access_admin(Host) ->
gen_mod:get_module_opt(Host, mod_muc, access_admin).
-spec access_create(gen_mod:opts() | global | binary()) -> 'all' | acl:acl().
access_create(Opts) when is_map(Opts) ->
gen_mod:get_opt(access_create, Opts);
access_create(Host) ->
gen_mod:get_module_opt(Host, mod_muc, access_create).
-spec access_mam(gen_mod:opts() | global | binary()) -> 'all' | acl:acl().
access_mam(Opts) when is_map(Opts) ->
gen_mod:get_opt(access_mam, Opts);
access_mam(Host) ->
gen_mod:get_module_opt(Host, mod_muc, access_mam).
-spec access_persistent(gen_mod:opts() | global | binary()) -> 'all' | acl:acl().
access_persistent(Opts) when is_map(Opts) ->
gen_mod:get_opt(access_persistent, Opts);
access_persistent(Host) ->
gen_mod:get_module_opt(Host, mod_muc, access_persistent).
-spec access_register(gen_mod:opts() | global | binary()) -> 'all' | acl:acl().
access_register(Opts) when is_map(Opts) ->
gen_mod:get_opt(access_register, Opts);
access_register(Host) ->
gen_mod:get_module_opt(Host, mod_muc, access_register).
-spec cleanup_affiliations_on_start(gen_mod:opts() | global | binary()) -> boolean().
cleanup_affiliations_on_start(Opts) when is_map(Opts) ->
gen_mod:get_opt(cleanup_affiliations_on_start, Opts);
cleanup_affiliations_on_start(Host) ->
gen_mod:get_module_opt(Host, mod_muc, cleanup_affiliations_on_start).
-spec db_type(gen_mod:opts() | global | binary()) -> atom().
db_type(Opts) when is_map(Opts) ->
gen_mod:get_opt(db_type, Opts);
db_type(Host) ->
gen_mod:get_module_opt(Host, mod_muc, db_type).
-spec default_room_options(gen_mod:opts() | global | binary()) -> [{atom(),'anyone' | 'false' | 'moderators' | 'nobody' | 'none' | 'participants' | 'true' | 'undefined' | binary() | ['moderator' | 'participant' | 'visitor'] | pos_integer() | tuple()}].
default_room_options(Opts) when is_map(Opts) ->
gen_mod:get_opt(default_room_options, Opts);
default_room_options(Host) ->
gen_mod:get_module_opt(Host, mod_muc, default_room_options).
-spec hibernation_timeout(gen_mod:opts() | global | binary()) -> 'infinity' | pos_integer().
hibernation_timeout(Opts) when is_map(Opts) ->
gen_mod:get_opt(hibernation_timeout, Opts);
hibernation_timeout(Host) ->
gen_mod:get_module_opt(Host, mod_muc, hibernation_timeout).
-spec history_size(gen_mod:opts() | global | binary()) -> non_neg_integer().
history_size(Opts) when is_map(Opts) ->
gen_mod:get_opt(history_size, Opts);
history_size(Host) ->
gen_mod:get_module_opt(Host, mod_muc, history_size).
-spec host(gen_mod:opts() | global | binary()) -> binary().
host(Opts) when is_map(Opts) ->
gen_mod:get_opt(host, Opts);
host(Host) ->
gen_mod:get_module_opt(Host, mod_muc, host).
-spec hosts(gen_mod:opts() | global | binary()) -> [binary()].
hosts(Opts) when is_map(Opts) ->
gen_mod:get_opt(hosts, Opts);
hosts(Host) ->
gen_mod:get_module_opt(Host, mod_muc, hosts).
-spec max_captcha_whitelist(gen_mod:opts() | global | binary()) -> 'infinity' | pos_integer().
max_captcha_whitelist(Opts) when is_map(Opts) ->
gen_mod:get_opt(max_captcha_whitelist, Opts);
max_captcha_whitelist(Host) ->
gen_mod:get_module_opt(Host, mod_muc, max_captcha_whitelist).
-spec max_password(gen_mod:opts() | global | binary()) -> 'infinity' | pos_integer().
max_password(Opts) when is_map(Opts) ->
gen_mod:get_opt(max_password, Opts);
max_password(Host) ->
gen_mod:get_module_opt(Host, mod_muc, max_password).
-spec max_room_desc(gen_mod:opts() | global | binary()) -> 'infinity' | pos_integer().
max_room_desc(Opts) when is_map(Opts) ->
gen_mod:get_opt(max_room_desc, Opts);
max_room_desc(Host) ->
gen_mod:get_module_opt(Host, mod_muc, max_room_desc).
-spec max_room_id(gen_mod:opts() | global | binary()) -> 'infinity' | pos_integer().
max_room_id(Opts) when is_map(Opts) ->
gen_mod:get_opt(max_room_id, Opts);
max_room_id(Host) ->
gen_mod:get_module_opt(Host, mod_muc, max_room_id).
-spec max_room_name(gen_mod:opts() | global | binary()) -> 'infinity' | pos_integer().
max_room_name(Opts) when is_map(Opts) ->
gen_mod:get_opt(max_room_name, Opts);
max_room_name(Host) ->
gen_mod:get_module_opt(Host, mod_muc, max_room_name).
-spec max_rooms_discoitems(gen_mod:opts() | global | binary()) -> non_neg_integer().
max_rooms_discoitems(Opts) when is_map(Opts) ->
gen_mod:get_opt(max_rooms_discoitems, Opts);
max_rooms_discoitems(Host) ->
gen_mod:get_module_opt(Host, mod_muc, max_rooms_discoitems).
-spec max_user_conferences(gen_mod:opts() | global | binary()) -> pos_integer().
max_user_conferences(Opts) when is_map(Opts) ->
gen_mod:get_opt(max_user_conferences, Opts);
max_user_conferences(Host) ->
gen_mod:get_module_opt(Host, mod_muc, max_user_conferences).
-spec max_users(gen_mod:opts() | global | binary()) -> pos_integer().
max_users(Opts) when is_map(Opts) ->
gen_mod:get_opt(max_users, Opts);
max_users(Host) ->
gen_mod:get_module_opt(Host, mod_muc, max_users).
-spec max_users_admin_threshold(gen_mod:opts() | global | binary()) -> pos_integer().
max_users_admin_threshold(Opts) when is_map(Opts) ->
gen_mod:get_opt(max_users_admin_threshold, Opts);
max_users_admin_threshold(Host) ->
gen_mod:get_module_opt(Host, mod_muc, max_users_admin_threshold).
-spec max_users_presence(gen_mod:opts() | global | binary()) -> integer().
max_users_presence(Opts) when is_map(Opts) ->
gen_mod:get_opt(max_users_presence, Opts);
max_users_presence(Host) ->
gen_mod:get_module_opt(Host, mod_muc, max_users_presence).
-spec min_message_interval(gen_mod:opts() | global | binary()) -> number().
min_message_interval(Opts) when is_map(Opts) ->
gen_mod:get_opt(min_message_interval, Opts);
min_message_interval(Host) ->
gen_mod:get_module_opt(Host, mod_muc, min_message_interval).
-spec min_presence_interval(gen_mod:opts() | global | binary()) -> number().
min_presence_interval(Opts) when is_map(Opts) ->
gen_mod:get_opt(min_presence_interval, Opts);
min_presence_interval(Host) ->
gen_mod:get_module_opt(Host, mod_muc, min_presence_interval).
-spec name(gen_mod:opts() | global | binary()) -> binary().
name(Opts) when is_map(Opts) ->
gen_mod:get_opt(name, Opts);
name(Host) ->
gen_mod:get_module_opt(Host, mod_muc, name).
-spec preload_rooms(gen_mod:opts() | global | binary()) -> boolean().
preload_rooms(Opts) when is_map(Opts) ->
gen_mod:get_opt(preload_rooms, Opts);
preload_rooms(Host) ->
gen_mod:get_module_opt(Host, mod_muc, preload_rooms).
-spec queue_type(gen_mod:opts() | global | binary()) -> 'file' | 'ram'.
queue_type(Opts) when is_map(Opts) ->
gen_mod:get_opt(queue_type, Opts);
queue_type(Host) ->
gen_mod:get_module_opt(Host, mod_muc, queue_type).
-spec ram_db_type(gen_mod:opts() | global | binary()) -> atom().
ram_db_type(Opts) when is_map(Opts) ->
gen_mod:get_opt(ram_db_type, Opts);
ram_db_type(Host) ->
gen_mod:get_module_opt(Host, mod_muc, ram_db_type).
-spec regexp_room_id(gen_mod:opts() | global | binary()) -> <<>> | misc:re_mp().
regexp_room_id(Opts) when is_map(Opts) ->
gen_mod:get_opt(regexp_room_id, Opts);
regexp_room_id(Host) ->
gen_mod:get_module_opt(Host, mod_muc, regexp_room_id).
-spec room_shaper(gen_mod:opts() | global | binary()) -> atom().
room_shaper(Opts) when is_map(Opts) ->
gen_mod:get_opt(room_shaper, Opts);
room_shaper(Host) ->
gen_mod:get_module_opt(Host, mod_muc, room_shaper).
-spec user_message_shaper(gen_mod:opts() | global | binary()) -> atom().
user_message_shaper(Opts) when is_map(Opts) ->
gen_mod:get_opt(user_message_shaper, Opts);
user_message_shaper(Host) ->
gen_mod:get_module_opt(Host, mod_muc, user_message_shaper).
-spec user_presence_shaper(gen_mod:opts() | global | binary()) -> atom().
user_presence_shaper(Opts) when is_map(Opts) ->
gen_mod:get_opt(user_presence_shaper, Opts);
user_presence_shaper(Host) ->
gen_mod:get_module_opt(Host, mod_muc, user_presence_shaper).
-spec vcard(gen_mod:opts() | global | binary()) -> 'undefined' | tuple().
vcard(Opts) when is_map(Opts) ->
gen_mod:get_opt(vcard, Opts);
vcard(Host) ->
gen_mod:get_module_opt(Host, mod_muc, vcard).
ejabberd-24.12/src/ejabberd_batch.erl 0000664 0001750 0001750 00000017557 14730775155 020067 0 ustar debalance debalance %%%----------------------------------------------------------------------
%%% File : ejabberd_batch.erl
%%% Author : Paweł Chmielowski
%%% Purpose : Batch tasks manager
%%% Created : 8 mar 2022 by Paweł Chmielowski
%%%
%%%
%%% 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(ejabberd_batch).
-author("pawel@process-one.net").
-behaviour(gen_server).
-include("logger.hrl").
%% API
-export([start_link/0]).
%% gen_server callbacks
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2,
code_change/3]).
-export([register_task/5, task_status/1, abort_task/1]).
-define(SERVER, ?MODULE).
-record(state, {tasks = #{}}).
-record(task, {state = not_started, pid, steps, done_steps}).
%%%===================================================================
%%% API
%%%===================================================================
%% @doc Spawns the server and registers the local name (unique)
-spec(start_link() ->
{ok, Pid :: pid()} | ignore | {error, Reason :: term()}).
start_link() ->
gen_server:start_link({local, ?SERVER}, ?MODULE, [], []).
register_task(Type, Steps, Rate, JobState, JobFun) ->
gen_server:call(?MODULE, {register_task, Type, Steps, Rate, JobState, JobFun}).
task_status(Type) ->
gen_server:call(?MODULE, {task_status, Type}).
abort_task(Type) ->
gen_server:call(?MODULE, {abort_task, Type}).
%%%===================================================================
%%% gen_server callbacks
%%%===================================================================
%% @private
%% @doc Initializes the server
-spec(init(Args :: term()) ->
{ok, State :: #state{}} | {ok, State :: #state{}, timeout() | hibernate} |
{stop, Reason :: term()} | ignore).
init([]) ->
{ok, #state{}}.
%% @private
%% @doc Handling call messages
-spec(handle_call(Request :: term(), From :: {pid(), Tag :: term()},
State :: #state{}) ->
{reply, Reply :: term(), NewState :: #state{}} |
{reply, Reply :: term(), NewState :: #state{}, timeout() | hibernate} |
{noreply, NewState :: #state{}} |
{noreply, NewState :: #state{}, timeout() | hibernate} |
{stop, Reason :: term(), Reply :: term(), NewState :: #state{}} |
{stop, Reason :: term(), NewState :: #state{}}).
handle_call({register_task, Type, Steps, Rate, JobState, JobFun}, _From, #state{tasks = Tasks} = State) ->
case maps:get(Type, Tasks, #task{}) of
#task{state = S} when S == completed; S == not_started; S == aborted; S == failed ->
Pid = spawn(fun() -> work_loop(Type, JobState, JobFun, Rate, erlang:monotonic_time(second), 0) end),
Tasks2 = maps:put(Type, #task{state = working, pid = Pid, steps = Steps, done_steps = 0}, Tasks),
{reply, ok, #state{tasks = Tasks2}};
#task{state = working} ->
{reply, {error, in_progress}, State}
end;
handle_call({task_status, Type}, _From, #state{tasks = Tasks} = State) ->
case maps:get(Type, Tasks, none) of
none ->
{reply, not_started, State};
#task{state = not_started} ->
{reply, not_started, State};
#task{state = failed, done_steps = Steps, pid = Error} ->
{reply, {failed, Steps, Error}, State};
#task{state = aborted, done_steps = Steps} ->
{reply, {aborted, Steps}, State};
#task{state = working, done_steps = Steps} ->
{reply, {working, Steps}, State};
#task{state = completed, done_steps = Steps} ->
{reply, {completed, Steps}, State}
end;
handle_call({abort_task, Type}, _From, #state{tasks = Tasks} = State) ->
case maps:get(Type, Tasks, none) of
#task{state = working, pid = Pid} = T ->
Pid ! abort,
Tasks2 = maps:put(Type, T#task{state = aborted, pid = none}, Tasks),
{reply, aborted, State#state{tasks = Tasks2}};
_ ->
{reply, not_started, State}
end;
handle_call(_Request, _From, State = #state{}) ->
{reply, ok, State}.
%% @private
%% @doc Handling cast messages
-spec(handle_cast(Request :: term(), State :: #state{}) ->
{noreply, NewState :: #state{}} |
{noreply, NewState :: #state{}, timeout() | hibernate} |
{stop, Reason :: term(), NewState :: #state{}}).
handle_cast({task_finished, Type, Pid}, #state{tasks = Tasks} = State) ->
case maps:get(Type, Tasks, none) of
#task{state = working, pid = Pid2} = T when Pid == Pid2 ->
Tasks2 = maps:put(Type, T#task{state = completed, pid = none}, Tasks),
{noreply, State#state{tasks = Tasks2}};
_ ->
{noreply, State}
end;
handle_cast({task_progress, Type, Pid, Count}, #state{tasks = Tasks} = State) ->
case maps:get(Type, Tasks, none) of
#task{state = working, pid = Pid2, done_steps = Steps} = T when Pid == Pid2 ->
Tasks2 = maps:put(Type, T#task{done_steps = Steps + Count}, Tasks),
{noreply, State#state{tasks = Tasks2}};
_ ->
{noreply, State}
end;
handle_cast({task_error, Type, Pid, Error}, #state{tasks = Tasks} = State) ->
case maps:get(Type, Tasks, none) of
#task{state = working, pid = Pid2} = T when Pid == Pid2 ->
Tasks2 = maps:put(Type, T#task{state = failed, pid = Error}, Tasks),
{noreply, State#state{tasks = Tasks2}};
_ ->
{noreply, State}
end;
handle_cast(_Request, State = #state{}) ->
{noreply, State}.
%% @private
%% @doc Handling all non call/cast messages
-spec(handle_info(Info :: timeout() | term(), State :: #state{}) ->
{noreply, NewState :: #state{}} |
{noreply, NewState :: #state{}, timeout() | hibernate} |
{stop, Reason :: term(), NewState :: #state{}}).
handle_info(_Info, State = #state{}) ->
{noreply, State}.
%% @private
%% @doc This function is called by a gen_server when it is about to
%% terminate. It should be the opposite of Module:init/1 and do any
%% necessary cleaning up. When it returns, the gen_server terminates
%% with Reason. The return value is ignored.
-spec(terminate(Reason :: (normal | shutdown | {shutdown, term()} | term()),
State :: #state{}) -> term()).
terminate(_Reason, _State = #state{}) ->
ok.
%% @private
%% @doc Convert process state when code is changed
-spec(code_change(OldVsn :: term() | {down, term()}, State :: #state{},
Extra :: term()) ->
{ok, NewState :: #state{}} | {error, Reason :: term()}).
code_change(_OldVsn, State = #state{}, _Extra) ->
{ok, State}.
%%%===================================================================
%%% Internal functions
%%%===================================================================
work_loop(Task, JobState, JobFun, Rate, StartDate, CurrentProgress) ->
try JobFun(JobState) of
{ok, _NewState, 0} ->
gen_server:cast(?MODULE, {task_finished, Task, self()});
{ok, NewState, Count} ->
gen_server:cast(?MODULE, {task_progress, Task, self(), Count}),
NewProgress = CurrentProgress + Count,
TimeSpent = erlang:monotonic_time(second) - StartDate,
SleepTime = max(0, NewProgress/Rate*60 - TimeSpent),
receive
abort -> ok
after round(SleepTime*1000) ->
work_loop(Task, NewState, JobFun, Rate, StartDate, NewProgress)
end;
{error, Error} ->
gen_server:cast(?MODULE, {task_error, Task, self(), Error})
catch _:_ ->
gen_server:cast(?MODULE, {task_error, Task, self(), internal_error})
end.
ejabberd-24.12/src/mod_bosh_redis.erl 0000664 0001750 0001750 00000010263 14730775155 020133 0 ustar debalance debalance %%%----------------------------------------------------------------------
%%% File : mod_bosh_redis.erl
%%% Author : Evgeny Khramtsov
%%% Purpose :
%%% Created : 28 Mar 2017 by Evgeny Khramtsov
%%%
%%%
%%% ejabberd, Copyright (C) 2017-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_redis).
-behaviour(mod_bosh).
-behaviour(gen_server).
%% API
-export([init/0, open_session/2, close_session/1, find_session/1,
cache_nodes/0]).
%% gen_server callbacks
-export([init/1, handle_cast/2, handle_call/3, handle_info/2,
terminate/2, code_change/3, start_link/0]).
-include("logger.hrl").
-include("bosh.hrl").
-record(state, {}).
-define(BOSH_KEY, <<"ejabberd:bosh">>).
%%%===================================================================
%%% API
%%%===================================================================
init() ->
Spec = {?MODULE, {?MODULE, start_link, []},
transient, 5000, worker, [?MODULE]},
case supervisor:start_child(ejabberd_backend_sup, Spec) of
{ok, _Pid} -> ok;
Err -> Err
end.
-spec start_link() -> {ok, pid()} | {error, any()}.
start_link() ->
gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
open_session(SID, Pid) ->
PidBin = term_to_binary(Pid),
case ejabberd_redis:multi(
fun() ->
ejabberd_redis:hset(?BOSH_KEY, SID, PidBin),
ejabberd_redis:publish(?BOSH_KEY, SID)
end) of
{ok, _} ->
ok;
{error, _} ->
{error, db_failure}
end.
close_session(SID) ->
case ejabberd_redis:multi(
fun() ->
ejabberd_redis:hdel(?BOSH_KEY, [SID]),
ejabberd_redis:publish(?BOSH_KEY, SID)
end) of
{ok, _} ->
ok;
{error, _} ->
{error, db_failure}
end.
find_session(SID) ->
case ejabberd_redis:hget(?BOSH_KEY, SID) of
{ok, undefined} ->
{error, notfound};
{ok, Pid} ->
try
{ok, binary_to_term(Pid)}
catch _:badarg ->
?ERROR_MSG("Malformed data in redis (key = '~ts'): ~p",
[SID, Pid]),
{error, db_failure}
end;
{error, _} ->
{error, db_failure}
end.
cache_nodes() ->
[node()].
%%%===================================================================
%%% gen_server callbacks
%%%===================================================================
init([]) ->
clean_table(),
{ok, #state{}}.
handle_call(Request, From, State) ->
?WARNING_MSG("Unexpected call from ~p: ~p", [From, Request]),
{noreply, State}.
handle_cast(Msg, State) ->
?WARNING_MSG("Unexpected cast: ~p", [Msg]),
{noreply, State}.
handle_info({redis_message, ?BOSH_KEY, SID}, State) ->
ets_cache:delete(?BOSH_CACHE, SID),
{noreply, State};
handle_info(Info, State) ->
?WARNING_MSG("Unexpected info: ~p", [Info]),
{noreply, State}.
terminate(_Reason, _State) ->
ok.
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
%%%===================================================================
%%% Internal functions
%%%===================================================================
clean_table() ->
?DEBUG("Cleaning Redis BOSH sessions...", []),
case ejabberd_redis:hgetall(?BOSH_KEY) of
{ok, Vals} ->
ejabberd_redis:multi(
fun() ->
lists:foreach(
fun({SID, Pid}) when node(Pid) == node() ->
ejabberd_redis:hdel(?BOSH_KEY, [SID]);
(_) ->
ok
end, Vals)
end),
ok;
{error, _} ->
?ERROR_MSG("Failed to clean bosh sessions in redis", [])
end.
ejabberd-24.12/src/mod_last_sql.erl 0000664 0001750 0001750 00000007176 14730775155 017645 0 ustar debalance debalance %%%-------------------------------------------------------------------
%%% File : mod_last_sql.erl
%%% Author : Evgeny Khramtsov
%%% Created : 13 Apr 2016 by Evgeny Khramtsov
%%%
%%%
%%% 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_last_sql).
-behaviour(mod_last).
%% API
-export([init/2, get_last/2, store_last_info/4, remove_user/2,
import/2, export/1]).
-export([sql_schemas/0]).
-include("mod_last.hrl").
-include("logger.hrl").
-include("ejabberd_sql_pt.hrl").
%%%===================================================================
%%% API
%%%===================================================================
init(Host, _Opts) ->
ejabberd_sql_schema:update_schema(Host, ?MODULE, sql_schemas()),
ok.
sql_schemas() ->
[#sql_schema{
version = 1,
tables =
[#sql_table{
name = <<"last">>,
columns =
[#sql_column{name = <<"username">>, type = text},
#sql_column{name = <<"server_host">>, type = text},
#sql_column{name = <<"seconds">>, type = text},
#sql_column{name = <<"state">>, type = text}],
indices = [#sql_index{
columns = [<<"server_host">>, <<"username">>],
unique = true}]}]}].
get_last(LUser, LServer) ->
case ejabberd_sql:sql_query(
LServer,
?SQL("select @(seconds)d, @(state)s from last"
" where username=%(LUser)s and %(LServer)H")) of
{selected, []} ->
error;
{selected, [{TimeStamp, Status}]} ->
{ok, {TimeStamp, Status}};
_Reason ->
{error, db_failure}
end.
store_last_info(LUser, LServer, TimeStamp, Status) ->
TS = integer_to_binary(TimeStamp),
case ?SQL_UPSERT(LServer, "last",
["!username=%(LUser)s",
"!server_host=%(LServer)s",
"seconds=%(TS)s",
"state=%(Status)s"]) of
ok ->
ok;
_Err ->
{error, db_failure}
end.
remove_user(LUser, LServer) ->
ejabberd_sql:sql_query(
LServer,
?SQL("delete from last where username=%(LUser)s and %(LServer)H")).
export(_Server) ->
[{last_activity,
fun(Host, #last_activity{us = {LUser, LServer},
timestamp = TimeStamp, status = Status})
when LServer == Host ->
TS = integer_to_binary(TimeStamp),
[?SQL("delete from last where username=%(LUser)s and %(LServer)H;"),
?SQL_INSERT("last",
["username=%(LUser)s",
"server_host=%(LServer)s",
"seconds=%(TS)s",
"state=%(Status)s"])];
(_Host, _R) ->
[]
end}].
import(_LServer, _LA) ->
pass.
ejabberd-24.12/src/ejabberd_router.erl 0000664 0001750 0001750 00000040117 14730775155 020312 0 ustar debalance debalance %%%----------------------------------------------------------------------
%%% File : ejabberd_router.erl
%%% Author : Alexey Shchepin
%%% Purpose : Main router
%%% Created : 27 Nov 2002 by Alexey Shchepin
%%%
%%%
%%% 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(ejabberd_router).
-author('alexey@process-one.net').
-ifndef(GEN_SERVER).
-define(GEN_SERVER, gen_server).
-endif.
-behaviour(?GEN_SERVER).
%% API
-export([route/1,
route_error/2,
route_iq/2,
route_iq/3,
route_iq/4,
register_route/2,
register_route/3,
register_route/4,
register_routes/1,
host_of_route/1,
process_iq/1,
unregister_route/1,
unregister_route/2,
unregister_routes/1,
get_all_routes/0,
is_my_route/1,
is_my_host/1,
clean_cache/1,
config_reloaded/0,
get_backend/0]).
-export([start_link/0]).
-export([init/1, handle_call/3, handle_cast/2,
handle_info/2, terminate/2, code_change/3]).
%% Deprecated functions
-export([route/3, route_error/4]).
-deprecated([{route, 3}, {route_error, 4}]).
%% This value is used in SIP and Megaco for a transaction lifetime.
-define(IQ_TIMEOUT, 32000).
-define(CALL_TIMEOUT, timer:minutes(10)).
-include("logger.hrl").
-include("ejabberd_router.hrl").
-include_lib("xmpp/include/xmpp.hrl").
-include("ejabberd_stacktrace.hrl").
-callback init() -> any().
-callback register_route(binary(), binary(), local_hint(),
undefined | pos_integer(), pid()) -> ok | {error, term()}.
-callback unregister_route(binary(), undefined | pos_integer(), pid()) -> ok | {error, term()}.
-callback find_routes(binary()) -> {ok, [#route{}]} | {error, any()}.
-callback get_all_routes() -> {ok, [binary()]} | {error, any()}.
-record(state, {route_monitors = #{} :: #{{binary(), pid()} => reference()}}).
%%====================================================================
%% API
%%====================================================================
start_link() ->
?GEN_SERVER:start_link({local, ?MODULE}, ?MODULE, [], []).
-spec route(stanza()) -> ok.
route(Packet) ->
try do_route(Packet)
catch ?EX_RULE(Class, Reason, St) ->
StackTrace = ?EX_STACK(St),
?ERROR_MSG("Failed to route packet:~n~ts~n** ~ts",
[xmpp:pp(Packet),
misc:format_exception(2, Class, Reason, StackTrace)])
end.
-spec route(jid(), jid(), xmlel() | stanza()) -> ok.
route(#jid{} = From, #jid{} = To, #xmlel{} = El) ->
try xmpp:decode(El, ?NS_CLIENT, [ignore_els]) of
Pkt -> route(From, To, Pkt)
catch _:{xmpp_codec, Why} ->
?ERROR_MSG("Failed to decode xml element ~p when "
"routing from ~ts to ~ts: ~ts",
[El, jid:encode(From), jid:encode(To),
xmpp:format_error(Why)])
end;
route(#jid{} = From, #jid{} = To, Packet) ->
route(xmpp:set_from_to(Packet, From, To)).
-spec route_error(stanza(), stanza_error()) -> ok.
route_error(Packet, Err) ->
Type = xmpp:get_type(Packet),
if Type == error; Type == result ->
ok;
true ->
route(xmpp:make_error(Packet, Err))
end.
%% Route the error packet only if the originating packet is not an error itself.
%% RFC3920 9.3.1
-spec route_error(jid(), jid(), xmlel(), xmlel()) -> ok;
(jid(), jid(), stanza(), stanza_error()) -> ok.
route_error(From, To, #xmlel{} = ErrPacket, #xmlel{} = OrigPacket) ->
#xmlel{attrs = Attrs} = OrigPacket,
case <<"error">> == fxml:get_attr_s(<<"type">>, Attrs) of
false -> route(From, To, ErrPacket);
true -> ok
end;
route_error(From, To, Packet, #stanza_error{} = Err) ->
Type = xmpp:get_type(Packet),
if Type == error; Type == result ->
ok;
true ->
route(From, To, xmpp:make_error(Packet, Err))
end.
-spec route_iq(iq(), fun((iq() | timeout) -> any())) -> ok.
route_iq(IQ, Fun) ->
route_iq(IQ, Fun, undefined, ?IQ_TIMEOUT).
-spec route_iq(iq(), term(), pid() | atom()) -> ok.
route_iq(IQ, State, Proc) ->
route_iq(IQ, State, Proc, ?IQ_TIMEOUT).
-spec route_iq(iq(), term(), pid() | atom(), undefined | non_neg_integer()) -> ok.
route_iq(IQ, State, Proc, undefined) ->
route_iq(IQ, State, Proc, ?IQ_TIMEOUT);
route_iq(IQ, State, Proc, Timeout) ->
ejabberd_iq:route(IQ, Proc, State, Timeout).
-spec register_route(binary(), binary()) -> ok.
register_route(Domain, ServerHost) ->
register_route(Domain, ServerHost, undefined).
-spec register_route(binary(), binary(), local_hint() | undefined) -> ok.
register_route(Domain, ServerHost, LocalHint) ->
register_route(Domain, ServerHost, LocalHint, self()).
-spec register_route(binary(), binary(), local_hint() | undefined, pid()) -> ok.
register_route(Domain, ServerHost, LocalHint, Pid) ->
case {jid:nameprep(Domain), jid:nameprep(ServerHost)} of
{error, _} ->
erlang:error({invalid_domain, Domain});
{_, error} ->
erlang:error({invalid_domain, ServerHost});
{LDomain, LServerHost} ->
Mod = get_backend(),
case Mod:register_route(LDomain, LServerHost, LocalHint,
get_component_number(LDomain), Pid) of
ok ->
?DEBUG("Route registered: ~ts", [LDomain]),
monitor_route(LDomain, Pid),
ejabberd_hooks:run(route_registered, [LDomain]),
delete_cache(Mod, LDomain);
{error, Err} ->
?ERROR_MSG("Failed to register route ~ts: ~p",
[LDomain, Err])
end
end.
-spec register_routes([{binary(), binary()}]) -> ok.
register_routes(Domains) ->
lists:foreach(fun ({Domain, ServerHost}) -> register_route(Domain, ServerHost)
end,
Domains).
-spec unregister_route(binary()) -> ok.
unregister_route(Domain) ->
unregister_route(Domain, self()).
-spec unregister_route(binary(), pid()) -> ok.
unregister_route(Domain, Pid) ->
case jid:nameprep(Domain) of
error ->
erlang:error({invalid_domain, Domain});
LDomain ->
Mod = get_backend(),
case Mod:unregister_route(
LDomain, get_component_number(LDomain), Pid) of
ok ->
?DEBUG("Route unregistered: ~ts", [LDomain]),
demonitor_route(LDomain, Pid),
ejabberd_hooks:run(route_unregistered, [LDomain]),
delete_cache(Mod, LDomain);
{error, Err} ->
?ERROR_MSG("Failed to unregister route ~ts: ~p",
[LDomain, Err])
end
end.
-spec unregister_routes([binary()]) -> ok.
unregister_routes(Domains) ->
lists:foreach(fun (Domain) -> unregister_route(Domain)
end,
Domains).
-spec find_routes(binary()) -> [#route{}].
find_routes(Domain) ->
Mod = get_backend(),
case use_cache(Mod) of
true ->
case ets_cache:lookup(
?ROUTES_CACHE, {route, Domain},
fun() ->
case Mod:find_routes(Domain) of
{ok, Rs} when Rs /= [] ->
{ok, Rs};
_ ->
error
end
end) of
{ok, Rs} -> Rs;
error -> []
end;
false ->
case Mod:find_routes(Domain) of
{ok, Rs} -> Rs;
_ -> []
end
end.
-spec get_all_routes() -> [binary()].
get_all_routes() ->
Mod = get_backend(),
case use_cache(Mod) of
true ->
case ets_cache:lookup(
?ROUTES_CACHE, routes,
fun() ->
case Mod:get_all_routes() of
{ok, Rs} when Rs /= [] ->
{ok, Rs};
_ ->
error
end
end) of
{ok, Rs} -> Rs;
error -> []
end;
false ->
case Mod:get_all_routes() of
{ok, Rs} -> Rs;
_ -> []
end
end.
-spec host_of_route(binary()) -> binary().
host_of_route(Domain) ->
case jid:nameprep(Domain) of
error ->
erlang:error({invalid_domain, Domain});
LDomain ->
case find_routes(LDomain) of
[#route{server_host = ServerHost}|_] ->
ServerHost;
_ ->
erlang:error({unregistered_route, Domain})
end
end.
-spec is_my_route(binary()) -> boolean().
is_my_route(Domain) ->
case jid:nameprep(Domain) of
error ->
erlang:error({invalid_domain, Domain});
LDomain ->
find_routes(LDomain) /= []
end.
-spec is_my_host(binary()) -> boolean().
is_my_host(Domain) ->
case jid:nameprep(Domain) of
error ->
erlang:error({invalid_domain, Domain});
LDomain ->
case find_routes(LDomain) of
[#route{server_host = LDomain}|_] -> true;
_ -> false
end
end.
-spec process_iq(iq()) -> any().
process_iq(IQ) ->
gen_iq_handler:handle(IQ).
-spec config_reloaded() -> ok.
config_reloaded() ->
Mod = get_backend(),
init_cache(Mod).
%%====================================================================
%% gen_server callbacks
%%====================================================================
init([]) ->
ejabberd_hooks:add(config_reloaded, ?MODULE, config_reloaded, 50),
Mod = get_backend(),
init_cache(Mod),
Mod:init(),
clean_cache(),
{ok, #state{}}.
handle_call({monitor, Domain, Pid}, _From, State) ->
MRefs = State#state.route_monitors,
MRefs1 = case maps:is_key({Domain, Pid}, MRefs) of
true -> MRefs;
false ->
MRef = erlang:monitor(process, Pid),
MRefs#{{Domain, Pid} => MRef}
end,
{reply, ok, State#state{route_monitors = MRefs1}};
handle_call({demonitor, Domain, Pid}, _From, State) ->
MRefs = State#state.route_monitors,
MRefs1 = case maps:find({Domain, Pid}, MRefs) of
{ok, MRef} ->
erlang:demonitor(MRef, [flush]),
maps:remove({Domain, Pid}, MRefs);
error ->
MRefs
end,
{reply, ok, State#state{route_monitors = MRefs1}};
handle_call(Request, From, State) ->
?WARNING_MSG("Unexpected call from ~p: ~p", [From, Request]),
{noreply, State}.
handle_cast(Msg, State) ->
?WARNING_MSG("Unexpected cast: ~p", [Msg]),
{noreply, State}.
handle_info({route, Packet}, State) ->
route(Packet),
{noreply, State};
handle_info({'DOWN', MRef, _, Pid, Info}, State) ->
MRefs = maps:filter(
fun({Domain, P}, M) when P == Pid, M == MRef ->
?DEBUG("Process ~p with route registered to ~ts "
"has terminated unexpectedly with reason: ~p",
[P, Domain, Info]),
try unregister_route(Domain, Pid)
catch _:_ -> ok
end,
false;
(_, _) ->
true
end, State#state.route_monitors),
{noreply, State#state{route_monitors = MRefs}};
handle_info(Info, State) ->
?ERROR_MSG("Unexpected info: ~p", [Info]),
{noreply, State}.
terminate(_Reason, _State) ->
ejabberd_hooks:delete(config_reloaded, ?MODULE, config_reloaded, 50).
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
%%--------------------------------------------------------------------
%%% Internal functions
%%--------------------------------------------------------------------
-spec do_route(stanza()) -> ok.
do_route(OrigPacket1) ->
?DEBUG("Route:~n~ts", [xmpp:pp(OrigPacket1)]),
OrigPacket = process_privilege_iq(OrigPacket1),
case ejabberd_hooks:run_fold(filter_packet, OrigPacket, []) of
drop ->
ok;
Packet ->
case ejabberd_iq:dispatch(Packet) of
true ->
ok;
false ->
To = xmpp:get_to(Packet),
LDstDomain = To#jid.lserver,
case find_routes(LDstDomain) of
[] ->
ejabberd_s2s:route(Packet);
[Route] ->
do_route(Packet, Route);
Routes ->
From = xmpp:get_from(Packet),
balancing_route(From, To, Packet, Routes)
end,
ok
end
end.
%% @format-begin
process_privilege_iq(Packet) ->
To = xmpp:get_to(Packet),
case xmpp:get_meta(Packet, privilege_iq, none) of
{OriginalId, OriginalHost, ReplacedJid} when ReplacedJid == To ->
Privilege = #privilege{forwarded = #forwarded{sub_els = [Packet]}},
#iq{type = xmpp:get_type(Packet),
id = OriginalId,
to = jid:make(OriginalHost),
from = ReplacedJid,
sub_els = [Privilege]};
_ ->
Packet
end.
%% @format-end
-spec do_route(stanza(), #route{}) -> any().
do_route(Pkt, #route{local_hint = LocalHint,
pid = Pid}) when is_pid(Pid) ->
case LocalHint of
{apply, Module, Function} when node(Pid) == node() ->
Module:Function(Pkt);
_ ->
ejabberd_cluster:send(Pid, {route, Pkt})
end;
do_route(_Pkt, _Route) ->
ok.
-spec balancing_route(jid(), jid(), stanza(), [#route{}]) -> any().
balancing_route(From, To, Packet, Rs) ->
case get_domain_balancing(From, To, To#jid.lserver) of
undefined ->
Value = erlang:system_time(),
case [R || R <- Rs, node(R#route.pid) == node()] of
[] ->
R = lists:nth(erlang:phash2(Value, length(Rs))+1, Rs),
do_route(Packet, R);
LRs ->
R = lists:nth(erlang:phash2(Value, length(LRs))+1, LRs),
do_route(Packet, R)
end;
Value ->
SRs = lists:ukeysort(#route.local_hint, Rs),
R = lists:nth(erlang:phash2(Value, length(SRs))+1, SRs),
do_route(Packet, R)
end.
-spec get_component_number(binary()) -> pos_integer() | undefined.
get_component_number(LDomain) ->
M = ejabberd_option:domain_balancing(),
case maps:get(LDomain, M, undefined) of
undefined -> undefined;
Opts -> maps:get(component_number, Opts)
end.
-spec get_domain_balancing(jid(), jid(), binary()) -> integer() | ljid() | undefined.
get_domain_balancing(From, To, LDomain) ->
M = ejabberd_option:domain_balancing(),
case maps:get(LDomain, M, undefined) of
undefined -> undefined;
Opts ->
case maps:get(type, Opts, random) of
random -> erlang:system_time();
source -> jid:tolower(From);
destination -> jid:tolower(To);
bare_source -> jid:remove_resource(jid:tolower(From));
bare_destination -> jid:remove_resource(jid:tolower(To))
end
end.
-spec monitor_route(binary(), pid()) -> ok.
monitor_route(Domain, Pid) ->
?GEN_SERVER:call(?MODULE, {monitor, Domain, Pid}, ?CALL_TIMEOUT).
-spec demonitor_route(binary(), pid()) -> ok.
demonitor_route(Domain, Pid) ->
case whereis(?MODULE) == self() of
true ->
ok;
false ->
?GEN_SERVER:call(?MODULE, {demonitor, Domain, Pid}, ?CALL_TIMEOUT)
end.
-spec get_backend() -> module().
get_backend() ->
DBType = ejabberd_option:router_db_type(),
list_to_existing_atom("ejabberd_router_" ++ atom_to_list(DBType)).
-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 use_cache(module()) -> boolean().
use_cache(Mod) ->
case erlang:function_exported(Mod, use_cache, 0) of
true -> Mod:use_cache();
false -> ejabberd_option:router_use_cache()
end.
-spec delete_cache(module(), binary()) -> ok.
delete_cache(Mod, Domain) ->
case use_cache(Mod) of
true ->
ets_cache:delete(?ROUTES_CACHE, {route, Domain}, cache_nodes(Mod)),
ets_cache:delete(?ROUTES_CACHE, routes, cache_nodes(Mod));
false ->
ok
end.
-spec init_cache(module()) -> ok.
init_cache(Mod) ->
case use_cache(Mod) of
true ->
ets_cache:new(?ROUTES_CACHE, cache_opts());
false ->
ets_cache:delete(?ROUTES_CACHE)
end.
-spec cache_opts() -> [proplists:property()].
cache_opts() ->
MaxSize = ejabberd_option:router_cache_size(),
CacheMissed = ejabberd_option:router_cache_missed(),
LifeTime = ejabberd_option:router_cache_life_time(),
[{max_size, MaxSize}, {cache_missed, CacheMissed}, {life_time, LifeTime}].
-spec clean_cache(node()) -> non_neg_integer().
clean_cache(Node) ->
ets_cache:filter(
?ROUTES_CACHE,
fun(_, error) ->
false;
(routes, _) ->
false;
({route, _}, {ok, Rs}) ->
not lists:any(
fun(#route{pid = Pid}) ->
node(Pid) == Node
end, Rs)
end).
-spec clean_cache() -> ok.
clean_cache() ->
ejabberd_cluster:eval_everywhere(?MODULE, clean_cache, [node()]).
ejabberd-24.12/src/mod_pubsub_opt.erl 0000664 0001750 0001750 00000011241 14730775155 020171 0 ustar debalance debalance %% Generated automatically
%% DO NOT EDIT: run `make options` instead
-module(mod_pubsub_opt).
-export([access_createnode/1]).
-export([db_type/1]).
-export([default_node_config/1]).
-export([force_node_config/1]).
-export([host/1]).
-export([hosts/1]).
-export([ignore_pep_from_offline/1]).
-export([last_item_cache/1]).
-export([max_item_expire_node/1]).
-export([max_items_node/1]).
-export([max_nodes_discoitems/1]).
-export([max_subscriptions_node/1]).
-export([name/1]).
-export([nodetree/1]).
-export([pep_mapping/1]).
-export([plugins/1]).
-export([vcard/1]).
-spec access_createnode(gen_mod:opts() | global | binary()) -> 'all' | acl:acl().
access_createnode(Opts) when is_map(Opts) ->
gen_mod:get_opt(access_createnode, Opts);
access_createnode(Host) ->
gen_mod:get_module_opt(Host, mod_pubsub, access_createnode).
-spec db_type(gen_mod:opts() | global | binary()) -> atom().
db_type(Opts) when is_map(Opts) ->
gen_mod:get_opt(db_type, Opts);
db_type(Host) ->
gen_mod:get_module_opt(Host, mod_pubsub, db_type).
-spec default_node_config(gen_mod:opts() | global | binary()) -> [{atom(),atom() | integer()}].
default_node_config(Opts) when is_map(Opts) ->
gen_mod:get_opt(default_node_config, Opts);
default_node_config(Host) ->
gen_mod:get_module_opt(Host, mod_pubsub, default_node_config).
-spec force_node_config(gen_mod:opts() | global | binary()) -> [{misc:re_mp(),[{atom(),atom() | integer()}]}].
force_node_config(Opts) when is_map(Opts) ->
gen_mod:get_opt(force_node_config, Opts);
force_node_config(Host) ->
gen_mod:get_module_opt(Host, mod_pubsub, force_node_config).
-spec host(gen_mod:opts() | global | binary()) -> binary().
host(Opts) when is_map(Opts) ->
gen_mod:get_opt(host, Opts);
host(Host) ->
gen_mod:get_module_opt(Host, mod_pubsub, host).
-spec hosts(gen_mod:opts() | global | binary()) -> [binary()].
hosts(Opts) when is_map(Opts) ->
gen_mod:get_opt(hosts, Opts);
hosts(Host) ->
gen_mod:get_module_opt(Host, mod_pubsub, hosts).
-spec ignore_pep_from_offline(gen_mod:opts() | global | binary()) -> boolean().
ignore_pep_from_offline(Opts) when is_map(Opts) ->
gen_mod:get_opt(ignore_pep_from_offline, Opts);
ignore_pep_from_offline(Host) ->
gen_mod:get_module_opt(Host, mod_pubsub, ignore_pep_from_offline).
-spec last_item_cache(gen_mod:opts() | global | binary()) -> boolean().
last_item_cache(Opts) when is_map(Opts) ->
gen_mod:get_opt(last_item_cache, Opts);
last_item_cache(Host) ->
gen_mod:get_module_opt(Host, mod_pubsub, last_item_cache).
-spec max_item_expire_node(gen_mod:opts() | global | binary()) -> 'infinity' | pos_integer().
max_item_expire_node(Opts) when is_map(Opts) ->
gen_mod:get_opt(max_item_expire_node, Opts);
max_item_expire_node(Host) ->
gen_mod:get_module_opt(Host, mod_pubsub, max_item_expire_node).
-spec max_items_node(gen_mod:opts() | global | binary()) -> 'unlimited' | non_neg_integer().
max_items_node(Opts) when is_map(Opts) ->
gen_mod:get_opt(max_items_node, Opts);
max_items_node(Host) ->
gen_mod:get_module_opt(Host, mod_pubsub, max_items_node).
-spec max_nodes_discoitems(gen_mod:opts() | global | binary()) -> 'infinity' | non_neg_integer().
max_nodes_discoitems(Opts) when is_map(Opts) ->
gen_mod:get_opt(max_nodes_discoitems, Opts);
max_nodes_discoitems(Host) ->
gen_mod:get_module_opt(Host, mod_pubsub, max_nodes_discoitems).
-spec max_subscriptions_node(gen_mod:opts() | global | binary()) -> 'undefined' | non_neg_integer().
max_subscriptions_node(Opts) when is_map(Opts) ->
gen_mod:get_opt(max_subscriptions_node, Opts);
max_subscriptions_node(Host) ->
gen_mod:get_module_opt(Host, mod_pubsub, max_subscriptions_node).
-spec name(gen_mod:opts() | global | binary()) -> binary().
name(Opts) when is_map(Opts) ->
gen_mod:get_opt(name, Opts);
name(Host) ->
gen_mod:get_module_opt(Host, mod_pubsub, name).
-spec nodetree(gen_mod:opts() | global | binary()) -> binary().
nodetree(Opts) when is_map(Opts) ->
gen_mod:get_opt(nodetree, Opts);
nodetree(Host) ->
gen_mod:get_module_opt(Host, mod_pubsub, nodetree).
-spec pep_mapping(gen_mod:opts() | global | binary()) -> [{binary(),binary()}].
pep_mapping(Opts) when is_map(Opts) ->
gen_mod:get_opt(pep_mapping, Opts);
pep_mapping(Host) ->
gen_mod:get_module_opt(Host, mod_pubsub, pep_mapping).
-spec plugins(gen_mod:opts() | global | binary()) -> [binary()].
plugins(Opts) when is_map(Opts) ->
gen_mod:get_opt(plugins, Opts);
plugins(Host) ->
gen_mod:get_module_opt(Host, mod_pubsub, plugins).
-spec vcard(gen_mod:opts() | global | binary()) -> 'undefined' | tuple().
vcard(Opts) when is_map(Opts) ->
gen_mod:get_opt(vcard, Opts);
vcard(Host) ->
gen_mod:get_module_opt(Host, mod_pubsub, vcard).
ejabberd-24.12/src/nodetree_tree_sql.erl 0000664 0001750 0001750 00000027175 14730775155 020670 0 ustar debalance debalance %%%----------------------------------------------------------------------
%%% File : nodetree_tree_sql.erl
%%% Author : Christophe Romain
%%% Purpose : Standard node tree plugin with ODBC backend
%%% Created : 1 Dec 2007 by Christophe Romain
%%%
%%%
%%% 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.
%%%
%%%----------------------------------------------------------------------
%%% @doc The module {@module} is the default PubSub node tree plugin.
%%% It is used as a default for all unknown PubSub node type. It can serve
%%% as a developer basis and reference to build its own custom pubsub node tree
%%% types.
%%% PubSub node tree plugins are using the {@link gen_nodetree} behaviour.
%%% The API isn't stabilized yet . The pubsub plugin
%%% development is still a work in progress. However, the system is already
%%% usable and useful as is. Please, send us comments, feedback and
%%% improvements.
-module(nodetree_tree_sql).
-behaviour(gen_pubsub_nodetree).
-author('christophe.romain@process-one.net').
-include("pubsub.hrl").
-include_lib("xmpp/include/xmpp.hrl").
-include("ejabberd_sql_pt.hrl").
-include("translate.hrl").
-export([init/3, terminate/2, options/0, set_node/1,
get_node/3, get_node/2, get_node/1, get_nodes/2,
get_nodes/1, get_all_nodes/1,
get_parentnodes/3, get_parentnodes_tree/3,
get_subnodes/3, get_subnodes_tree/3, create_node/6,
delete_node/2]).
-export([raw_to_node/2]).
init(_Host, _ServerHost, _Opts) ->
ok.
terminate(_Host, _ServerHost) ->
ok.
options() ->
[{sql, true} | nodetree_tree:options()].
set_node(Record) when is_record(Record, pubsub_node) ->
{Host, Node} = Record#pubsub_node.nodeid,
Parent = case Record#pubsub_node.parents of
[] -> <<>>;
[First | _] -> First
end,
Type = Record#pubsub_node.type,
H = node_flat_sql:encode_host(Host),
Nidx = case nodeidx(Host, Node) of
{result, OldNidx} ->
catch
ejabberd_sql:sql_query_t(
?SQL("delete from pubsub_node_option "
"where nodeid=%(OldNidx)d")),
catch
ejabberd_sql:sql_query_t(
?SQL("update pubsub_node set"
" host=%(H)s, node=%(Node)s,"
" parent=%(Parent)s, plugin=%(Type)s "
"where nodeid=%(OldNidx)d")),
OldNidx;
_ ->
catch
ejabberd_sql:sql_query_t(
?SQL("insert into pubsub_node(host, node, parent, plugin) "
"values(%(H)s, %(Node)s, %(Parent)s, %(Type)s)")),
case nodeidx(Host, Node) of
{result, NewNidx} -> NewNidx;
_ -> none % this should not happen
end
end,
case Nidx of
none ->
Txt = ?T("Node index not found"),
{error, xmpp:err_internal_server_error(Txt, ejabberd_option:language())};
_ ->
lists:foreach(fun ({Key, Value}) ->
SKey = iolist_to_binary(atom_to_list(Key)),
SValue = misc:term_to_expr(Value),
catch
ejabberd_sql:sql_query_t(
?SQL("insert into pubsub_node_option(nodeid, name, val) "
"values (%(Nidx)d, %(SKey)s, %(SValue)s)"))
end,
Record#pubsub_node.options),
{result, Nidx}
end.
get_node(Host, Node, _From) ->
get_node(Host, Node).
get_node(Host, Node) ->
H = node_flat_sql:encode_host(Host),
case catch
ejabberd_sql:sql_query_t(
?SQL("select @(node)s, @(parent)s, @(plugin)s, @(nodeid)d from pubsub_node "
"where host=%(H)s and node=%(Node)s"))
of
{selected, [RItem]} ->
raw_to_node(Host, RItem);
{'EXIT', _Reason} ->
{error, xmpp:err_internal_server_error(?T("Database failure"), ejabberd_option:language())};
_ ->
{error, xmpp:err_item_not_found(?T("Node not found"), ejabberd_option:language())}
end.
get_node(Nidx) ->
case catch
ejabberd_sql:sql_query_t(
?SQL("select @(host)s, @(node)s, @(parent)s, @(plugin)s from pubsub_node "
"where nodeid=%(Nidx)d"))
of
{selected, [{Host, Node, Parent, Type}]} ->
raw_to_node(Host, {Node, Parent, Type, Nidx});
{'EXIT', _Reason} ->
{error, xmpp:err_internal_server_error(?T("Database failure"), ejabberd_option:language())};
_ ->
{error, xmpp:err_item_not_found(?T("Node not found"), ejabberd_option:language())}
end.
get_nodes(Host) ->
get_nodes(Host, infinity).
get_nodes(Host, Limit) ->
H = node_flat_sql:encode_host(Host),
Query = fun(mssql, _) when is_integer(Limit), Limit>=0 ->
ejabberd_sql:sql_query_t(
?SQL("select top %(Limit)d @(node)s, @(parent)s, @(plugin)s, @(nodeid)d "
"from pubsub_node where host=%(H)s"));
(_, _) when is_integer(Limit), Limit>=0 ->
ejabberd_sql:sql_query_t(
?SQL("select @(node)s, @(parent)s, @(plugin)s, @(nodeid)d "
"from pubsub_node where host=%(H)s limit %(Limit)d"));
(_, _) ->
ejabberd_sql:sql_query_t(
?SQL("select @(node)s, @(parent)s, @(plugin)s, @(nodeid)d "
"from pubsub_node where host=%(H)s"))
end,
case ejabberd_sql:sql_query_t(Query) of
{selected, RItems} ->
[raw_to_node(Host, Item) || Item <- RItems];
_ ->
[]
end.
get_all_nodes({_U, _S, _R} = JID) ->
SubKey = jid:tolower(JID),
GenKey = jid:remove_resource(SubKey),
EncKey = node_flat_sql:encode_jid(GenKey),
Pattern = <<(node_flat_sql:encode_jid_like(GenKey))/binary, "/%">>,
case ejabberd_sql:sql_query_t(
?SQL("select @(node)s, @(parent)s, @(plugin)s, @(nodeid)d "
"from pubsub_node where host=%(EncKey)s "
"or host like %(Pattern)s %ESCAPE")) of
{selected, RItems} ->
[raw_to_node(GenKey, Item) || Item <- RItems];
_ ->
[]
end;
get_all_nodes(Host) ->
Pattern1 = <<"%@", Host/binary>>,
Pattern2 = <<"%@", Host/binary, "/%">>,
case ejabberd_sql:sql_query_t(
?SQL("select @(node)s, @(parent)s, @(plugin)s, @(nodeid)d "
"from pubsub_node where host=%(Host)s "
"or host like %(Pattern1)s "
"or host like %(Pattern2)s %ESCAPE")) of
{selected, RItems} ->
[raw_to_node(Host, Item) || Item <- RItems];
_ ->
[]
end.
get_parentnodes(Host, Node, _From) ->
case get_node(Host, Node) of
Record when is_record(Record, pubsub_node) ->
Record#pubsub_node.parents;
_ ->
[]
end.
get_parentnodes_tree(Host, Node, _From) ->
get_parentnodes_tree(Host, Node, 0, []).
get_parentnodes_tree(Host, Node, Level, Acc) ->
case get_node(Host, Node) of
Record when is_record(Record, pubsub_node) ->
Tree = [{Level, [Record]}|Acc],
case Record#pubsub_node.parents of
[Parent] -> get_parentnodes_tree(Host, Parent, Level+1, Tree);
_ -> Tree
end;
_ ->
Acc
end.
get_subnodes(Host, Node, Limit) ->
H = node_flat_sql:encode_host(Host),
Query = fun(mssql, _) when is_integer(Limit), Limit>=0 ->
ejabberd_sql:sql_query_t(
?SQL("select top %(Limit)d @(node)s, @(parent)s, @(plugin)s, @(nodeid)d "
"from pubsub_node where host=%(H)s and parent=%(Node)s"));
(_, _) when is_integer(Limit), Limit>=0 ->
ejabberd_sql:sql_query_t(
?SQL("select @(node)s, @(parent)s, @(plugin)s, @(nodeid)d "
"from pubsub_node where host=%(H)s and parent=%(Node)s "
"limit %(Limit)d"));
(_, _) ->
ejabberd_sql:sql_query_t(
?SQL("select @(node)s, @(parent)s, @(plugin)s, @(nodeid)d "
"from pubsub_node where host=%(H)s and parent=%(Node)s"))
end,
case ejabberd_sql:sql_query_t(Query) of
{selected, RItems} ->
[raw_to_node(Host, Item) || Item <- RItems];
_ ->
[]
end.
get_subnodes_tree(Host, Node, _From) ->
get_subnodes_tree(Host, Node).
get_subnodes_tree(Host, Node) ->
case get_node(Host, Node) of
{error, _} ->
[];
Rec ->
Type = Rec#pubsub_node.type,
H = node_flat_sql:encode_host(Host),
N = <<(ejabberd_sql:escape_like_arg(Node))/binary, "/%">>,
Sub = case catch
ejabberd_sql:sql_query_t(
?SQL("select @(node)s, @(parent)s, @(plugin)s, @(nodeid)d from pubsub_node "
"where host=%(H)s and plugin=%(Type)s and"
" (parent=%(Node)s or parent like %(N)s %ESCAPE)"))
of
{selected, RItems} ->
[raw_to_node(Host, Item) || Item <- RItems];
_ ->
[]
end,
[Rec|Sub]
end.
create_node(Host, Node, Type, Owner, Options, Parents) ->
BJID = jid:tolower(jid:remove_resource(Owner)),
case nodeidx(Host, Node) of
{error, not_found} ->
ParentExists = case Host of
{_U, _S, _R} ->
%% This is special case for PEP handling
%% PEP does not uses hierarchy
true;
_ ->
case Parents of
[] ->
true;
[Parent | _] ->
case nodeidx(Host, Parent) of
{result, PNode} ->
case nodeowners(PNode) of
[{<<>>, Host, <<>>}] -> true;
Owners -> lists:member(BJID, Owners)
end;
_ ->
false
end;
_ ->
false
end
end,
case ParentExists of
true ->
case set_node(#pubsub_node{nodeid = {Host, Node},
parents = Parents, type = Type,
options = Options})
of
{result, Nidx} -> {ok, Nidx};
Other -> Other
end;
false ->
{error, xmpp:err_forbidden()}
end;
{result, _} ->
{error, xmpp:err_conflict(?T("Node already exists"), ejabberd_option:language())};
{error, db_fail} ->
{error, xmpp:err_internal_server_error(?T("Database failure"), ejabberd_option:language())}
end.
delete_node(Host, Node) ->
lists:map(
fun(Rec) ->
Nidx = Rec#pubsub_node.id,
catch ejabberd_sql:sql_query_t(
?SQL("delete from pubsub_node where nodeid=%(Nidx)d")),
Rec
end, get_subnodes_tree(Host, Node)).
%% helpers
raw_to_node(Host, [Node, Parent, Type, Nidx]) ->
raw_to_node(Host, {Node, Parent, Type, binary_to_integer(Nidx)});
raw_to_node(Host, {Node, Parent, Type, Nidx}) ->
Options = case catch
ejabberd_sql:sql_query_t(
?SQL("select @(name)s, @(val)s from pubsub_node_option "
"where nodeid=%(Nidx)d"))
of
{selected, ROptions} ->
DbOpts = lists:map(
fun({<<"max_items">>, <<"infinity">>}) ->
{max_items, max};
({Key, Value}) ->
RKey = misc:binary_to_atom(Key),
Tokens = element(2, erl_scan:string(binary_to_list(<>))),
RValue = element(2, erl_parse:parse_term(Tokens)),
{RKey, RValue}
end,
ROptions),
Module = misc:binary_to_atom(<<"node_", Type/binary, "_sql">>),
StdOpts = Module:options(),
lists:foldl(fun ({Key, Value}, Acc) ->
lists:keystore(Key, 1, Acc, {Key, Value})
end,
StdOpts, DbOpts);
_ ->
[]
end,
Parents = case Parent of
<<>> -> [];
_ -> [Parent]
end,
#pubsub_node{nodeid = {Host, Node}, id = Nidx,
parents = Parents, type = Type, options = Options}.
nodeidx(Host, Node) ->
H = node_flat_sql:encode_host(Host),
case catch
ejabberd_sql:sql_query_t(
?SQL("select @(nodeid)d from pubsub_node "
"where host=%(H)s and node=%(Node)s"))
of
{selected, [{Nidx}]} ->
{result, Nidx};
{'EXIT', _Reason} ->
{error, db_fail};
_ ->
{error, not_found}
end.
nodeowners(Nidx) ->
{result, Res} = node_flat_sql:get_node_affiliations(Nidx),
[LJID || {LJID, Aff} <- Res, Aff =:= owner].
ejabberd-24.12/src/xml_compress.erl 0000664 0001750 0001750 00000114037 14730775155 017672 0 ustar debalance debalance -module(xml_compress).
-export([encode/3, decode/3]).
% This file was generated by xml_compress_gen
%
% Rules used:
%
% [{<<"eu.siacs.conversations.axolotl">>,<<"key">>,
% [{<<"prekey">>,[<<"true">>]},{<<"rid">>,[]}],
% []},
% {<<"jabber:client">>,<<"message">>,
% [{<<"from">>,[j2,{j1}]},
% {<<"id">>,[]},
% {<<"to">>,[j1,j2,{j1}]},
% {<<"type">>,[<<"chat">>,<<"groupchat">>,<<"normal">>]},
% {<<"xml:lang">>,[<<"en">>]}],
% []},
% {<<"urn:xmpp:hints">>,<<"store">>,[],[]},
% {<<"jabber:client">>,<<"body">>,[],
% [<<73,32,115,101,110,116,32,121,111,117,32,97,110,32,79,77,69,77,79,32,101,
% 110,99,114,121,112,116,101,100,32,109,101,115,115,97,103,101,32,98,117,
% 116,32,121,111,117,114,32,99,108,105,101,110,116,32,100,111,101,115,110,
% 226,128,153,116,32,115,101,101,109,32,116,111,32,115,117,112,112,111,
% 114,116,32,116,104,97,116,46,32,70,105,110,100,32,109,111,114,101,32,
% 105,110,102,111,114,109,97,116,105,111,110,32,111,110,32,104,116,116,
% 112,115,58,47,47,99,111,110,118,101,114,115,97,116,105,111,110,115,46,
% 105,109,47,111,109,101,109,111>>]},
% {<<"urn:xmpp:sid:0">>,<<"origin-id">>,[{<<"id">>,[]}],[]},
% {<<"urn:xmpp:chat-markers:0">>,<<"markable">>,[],[]},
% {<<"eu.siacs.conversations.axolotl">>,<<"encrypted">>,[],[]},
% {<<"eu.siacs.conversations.axolotl">>,<<"header">>,[{<<"sid">>,[]}],[]},
% {<<"eu.siacs.conversations.axolotl">>,<<"iv">>,[],[]},
% {<<"eu.siacs.conversations.axolotl">>,<<"payload">>,[],[]},
% {<<"urn:xmpp:eme:0">>,<<"encryption">>,
% [{<<"name">>,[<<"OMEMO">>]},
% {<<"namespace">>,[<<"eu.siacs.conversations.axolotl">>]}],
% []},
% {<<"urn:xmpp:delay">>,<<"delay">>,[{<<"from">>,[j1]},{<<"stamp">>,[]}],[]},
% {<<"http://jabber.org/protocol/address">>,<<"address">>,
% [{<<"jid">>,[{j1}]},{<<"type">>,[<<"ofrom">>]}],
% []},
% {<<"http://jabber.org/protocol/address">>,<<"addresses">>,[],[]},
% {<<"urn:xmpp:chat-markers:0">>,<<"displayed">>,
% [{<<"id">>,[]},{<<"sender">>,[{j1},{j2}]}],
% []},
% {<<"urn:xmpp:mam:tmp">>,<<"archived">>,[{<<"by">>,[]},{<<"id">>,[]}],[]},
% {<<"urn:xmpp:sid:0">>,<<"stanza-id">>,[{<<"by">>,[]},{<<"id">>,[]}],[]},
% {<<"urn:xmpp:receipts">>,<<"request">>,[],[]},
% {<<"urn:xmpp:chat-markers:0">>,<<"received">>,[{<<"id">>,[]}],[]},
% {<<"urn:xmpp:receipts">>,<<"received">>,[{<<"id">>,[]}],[]},
% {<<"http://jabber.org/protocol/chatstates">>,<<"active">>,[],[]},
% {<<"http://jabber.org/protocol/muc#user">>,<<"invite">>,
% [{<<"from">>,[{j1}]}],
% []},
% {<<"http://jabber.org/protocol/muc#user">>,<<"reason">>,[],[]},
% {<<"http://jabber.org/protocol/muc#user">>,<<"x">>,[],[]},
% {<<"jabber:x:conference">>,<<"x">>,[{<<"jid">>,[j2]}],[]},
% {<<"jabber:client">>,<<"subject">>,[],[]},
% {<<"jabber:client">>,<<"thread">>,[],[]},
% {<<"http://jabber.org/protocol/pubsub#event">>,<<"event">>,[],[]},
% {<<"http://jabber.org/protocol/pubsub#event">>,<<"item">>,[{<<"id">>,[]}],[]},
% {<<"http://jabber.org/protocol/pubsub#event">>,<<"items">>,
% [{<<"node">>,[<<"urn:xmpp:mucsub:nodes:messages">>]}],
% []},
% {<<"p1:push:custom">>,<<"x">>,[{<<"key">>,[]},{<<"value">>,[]}],[]},
% {<<"p1:pushed">>,<<"x">>,[],[]},
% {<<"urn:xmpp:message-correct:0">>,<<"replace">>,[{<<"id">>,[]}],[]},
% {<<"http://jabber.org/protocol/chatstates">>,<<"composing">>,[],[]}]
encode(El, J1, J2) ->
encode_child(El, <<"jabber:client">>,
J1, J2, byte_size(J1), byte_size(J2), <<1:8>>).
encode_attr({<<"xmlns">>, _}, Acc) ->
Acc;
encode_attr({N, V}, Acc) ->
<>.
encode_attrs(Attrs, Acc) ->
lists:foldl(fun encode_attr/2, Acc, Attrs).
encode_el(PNs, Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx) ->
E1 = if
PNs == Ns -> encode_attrs(Attrs, <>);
true -> encode_attrs(Attrs, <>)
end,
E2 = encode_children(Children, Ns, J1, J2, J1L, J2L, <>),
<>.
encode_child({xmlel, Name, Attrs, Children}, PNs, J1, J2, J1L, J2L, Pfx) ->
case lists:keyfind(<<"xmlns">>, 1, Attrs) of
false ->
encode(PNs, PNs, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx);
{_, Ns} ->
encode(PNs, Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx)
end;
encode_child({xmlcdata, Data}, _PNs, _J1, _J2, _J1L, _J2L, Pfx) ->
<>.
encode_children(Children, PNs, J1, J2, J1L, J2L, Pfx) ->
lists:foldl(
fun(Child, Acc) ->
encode_child(Child, PNs, J1, J2, J1L, J2L, Acc)
end, Pfx, Children).
encode_string(Data) ->
<> = <<(byte_size(Data)):16/unsigned-big-integer>>,
case {V1, V2, V3} of
{0, 0, V3} ->
<>;
{0, V2, V3} ->
<<(V3 bor 64):8, V2:8, Data/binary>>;
_ ->
<<(V3 bor 64):8, (V2 bor 64):8, V1:8, Data/binary>>
end.
encode(PNs, <<"eu.siacs.conversations.axolotl">> = Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx) ->
case Name of
<<"key">> ->
E = lists:foldl(fun
({<<"prekey">>, AVal}, Acc) ->
case AVal of
<<"true">> -> <>;
_ -> <>
end;
({<<"rid">>, AVal}, Acc) ->
<>;
(Attr, Acc) -> encode_attr(Attr, Acc)
end, <>, Attrs),
E2 = encode_children(Children, Ns, J1, J2, J1L, J2L, <>),
<>;
<<"encrypted">> ->
E = encode_attrs(Attrs, <>),
E2 = encode_children(Children, Ns, J1, J2, J1L, J2L, <>),
<>;
<<"header">> ->
E = lists:foldl(fun
({<<"sid">>, AVal}, Acc) ->
<>;
(Attr, Acc) -> encode_attr(Attr, Acc)
end, <>, Attrs),
E2 = encode_children(Children, Ns, J1, J2, J1L, J2L, <>),
<>;
<<"iv">> ->
E = encode_attrs(Attrs, <>),
E2 = encode_children(Children, Ns, J1, J2, J1L, J2L, <>),
<>;
<<"payload">> ->
E = encode_attrs(Attrs, <>),
E2 = encode_children(Children, Ns, J1, J2, J1L, J2L, <>),
<>;
_ -> encode_el(PNs, Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx)
end;
encode(PNs, <<"jabber:client">> = Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx) ->
case Name of
<<"message">> ->
E = lists:foldl(fun
({<<"from">>, AVal}, Acc) ->
case AVal of
J2 -> <>;
<> -> <>;
_ -> <>
end;
({<<"id">>, AVal}, Acc) ->
<>;
({<<"to">>, AVal}, Acc) ->
case AVal of
J1 -> <>;
J2 -> <>;
<> -> <>;
_ -> <>
end;
({<<"type">>, AVal}, Acc) ->
case AVal of
<<"chat">> -> <>;
<<"groupchat">> -> <>;
<<"normal">> -> <>;
_ -> <>
end;
({<<"xml:lang">>, AVal}, Acc) ->
case AVal of
<<"en">> -> <>;
_ -> <>
end;
(Attr, Acc) -> encode_attr(Attr, Acc)
end, <>, Attrs),
E2 = encode_children(Children, Ns, J1, J2, J1L, J2L, <>),
<>;
<<"body">> ->
E = encode_attrs(Attrs, <>),
E2 = lists:foldl(fun
({xmlcdata, <<73,32,115,101,110,116,32,121,111,117,32,97,110,32,79,77,69,
77,79,32,101,110,99,114,121,112,116,101,100,32,109,101,115,
115,97,103,101,32,98,117,116,32,121,111,117,114,32,99,108,
105,101,110,116,32,100,111,101,115,110,226,128,153,116,32,
115,101,101,109,32,116,111,32,115,117,112,112,111,114,116,32,
116,104,97,116,46,32,70,105,110,100,32,109,111,114,101,32,
105,110,102,111,114,109,97,116,105,111,110,32,111,110,32,104,
116,116,112,115,58,47,47,99,111,110,118,101,114,115,97,116,
105,111,110,115,46,105,109,47,111,109,101,109,111>>}, Acc) -> <>;
(El, Acc) -> encode_child(El, Ns, J1, J2, J1L, J2L, Acc)
end, <>, Children),
<>;
<<"subject">> ->
E = encode_attrs(Attrs, <>),
E2 = encode_children(Children, Ns, J1, J2, J1L, J2L, <>),
<>;
<<"thread">> ->
E = encode_attrs(Attrs, <>),
E2 = encode_children(Children, Ns, J1, J2, J1L, J2L, <>),
<>;
_ -> encode_el(PNs, Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx)
end;
encode(PNs, <<"urn:xmpp:hints">> = Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx) ->
case Name of
<<"store">> ->
E = encode_attrs(Attrs, <>),
E2 = encode_children(Children, Ns, J1, J2, J1L, J2L, <>),
<>;
_ -> encode_el(PNs, Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx)
end;
encode(PNs, <<"urn:xmpp:sid:0">> = Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx) ->
case Name of
<<"origin-id">> ->
E = lists:foldl(fun
({<<"id">>, AVal}, Acc) ->
<>;
(Attr, Acc) -> encode_attr(Attr, Acc)
end, <>, Attrs),
E2 = encode_children(Children, Ns, J1, J2, J1L, J2L, <>),
<>;
<<"stanza-id">> ->
E = lists:foldl(fun
({<<"by">>, AVal}, Acc) ->
<>;
({<<"id">>, AVal}, Acc) ->
<>;
(Attr, Acc) -> encode_attr(Attr, Acc)
end, <>, Attrs),
E2 = encode_children(Children, Ns, J1, J2, J1L, J2L, <>),
<>;
_ -> encode_el(PNs, Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx)
end;
encode(PNs, <<"urn:xmpp:chat-markers:0">> = Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx) ->
case Name of
<<"markable">> ->
E = encode_attrs(Attrs, <>),
E2 = encode_children(Children, Ns, J1, J2, J1L, J2L, <>),
<>;
<<"displayed">> ->
E = lists:foldl(fun
({<<"id">>, AVal}, Acc) ->
<>;
({<<"sender">>, AVal}, Acc) ->
case AVal of
<> -> <>;
<> -> <>;
_ -> <>
end;
(Attr, Acc) -> encode_attr(Attr, Acc)
end, <>, Attrs),
E2 = encode_children(Children, Ns, J1, J2, J1L, J2L, <>),
<>;
<<"received">> ->
E = lists:foldl(fun
({<<"id">>, AVal}, Acc) ->
<>;
(Attr, Acc) -> encode_attr(Attr, Acc)
end, <>, Attrs),
E2 = encode_children(Children, Ns, J1, J2, J1L, J2L, <>),
<>;
_ -> encode_el(PNs, Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx)
end;
encode(PNs, <<"urn:xmpp:eme:0">> = Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx) ->
case Name of
<<"encryption">> ->
E = lists:foldl(fun
({<<"name">>, AVal}, Acc) ->
case AVal of
<<"OMEMO">> -> <>;
_ -> <>
end;
({<<"namespace">>, AVal}, Acc) ->
case AVal of
<<"eu.siacs.conversations.axolotl">> -> <>;
_ -> <>
end;
(Attr, Acc) -> encode_attr(Attr, Acc)
end, <>, Attrs),
E2 = encode_children(Children, Ns, J1, J2, J1L, J2L, <>),
<>;
_ -> encode_el(PNs, Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx)
end;
encode(PNs, <<"urn:xmpp:delay">> = Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx) ->
case Name of
<<"delay">> ->
E = lists:foldl(fun
({<<"from">>, AVal}, Acc) ->
case AVal of
J1 -> <>;
_ -> <>
end;
({<<"stamp">>, AVal}, Acc) ->
<>;
(Attr, Acc) -> encode_attr(Attr, Acc)
end, <>, Attrs),
E2 = encode_children(Children, Ns, J1, J2, J1L, J2L, <>),
<>;
_ -> encode_el(PNs, Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx)
end;
encode(PNs, <<"http://jabber.org/protocol/address">> = Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx) ->
case Name of
<<"address">> ->
E = lists:foldl(fun
({<<"jid">>, AVal}, Acc) ->
case AVal of
<> -> <>;
_ -> <>
end;
({<<"type">>, AVal}, Acc) ->
case AVal of
<<"ofrom">> -> <>;
_ -> <>
end;
(Attr, Acc) -> encode_attr(Attr, Acc)
end, <>, Attrs),
E2 = encode_children(Children, Ns, J1, J2, J1L, J2L, <>),
<>;
<<"addresses">> ->
E = encode_attrs(Attrs, <>),
E2 = encode_children(Children, Ns, J1, J2, J1L, J2L, <>),
<>;
_ -> encode_el(PNs, Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx)
end;
encode(PNs, <<"urn:xmpp:mam:tmp">> = Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx) ->
case Name of
<<"archived">> ->
E = lists:foldl(fun
({<<"by">>, AVal}, Acc) ->
<>;
({<<"id">>, AVal}, Acc) ->
<>;
(Attr, Acc) -> encode_attr(Attr, Acc)
end, <>, Attrs),
E2 = encode_children(Children, Ns, J1, J2, J1L, J2L, <>),
<>;
_ -> encode_el(PNs, Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx)
end;
encode(PNs, <<"urn:xmpp:receipts">> = Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx) ->
case Name of
<<"request">> ->
E = encode_attrs(Attrs, <>),
E2 = encode_children(Children, Ns, J1, J2, J1L, J2L, <>),
<>;
<<"received">> ->
E = lists:foldl(fun
({<<"id">>, AVal}, Acc) ->
<>;
(Attr, Acc) -> encode_attr(Attr, Acc)
end, <>, Attrs),
E2 = encode_children(Children, Ns, J1, J2, J1L, J2L, <