Compare commits
189 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3a36a722c5 | |||
| 00c75c3dc9 | |||
| cae7850a70 | |||
| ce668bef14 | |||
| 3887b6d930 | |||
| b7bd0e196d | |||
| 6d63842ad3 | |||
| 2f3b9015e9 | |||
| 6ae48eb991 | |||
| c508795ad4 | |||
| 4a053807e0 | |||
| dd5bbda2dc | |||
| 98469678a0 | |||
| b8550e087e | |||
| 644d468b4f | |||
| 74c810eeaa | |||
| a46325166a | |||
| 654d4b81b1 | |||
| ff3d33dde4 | |||
| 3183e2f733 | |||
| e1dc686ae7 | |||
| 38b203feb1 | |||
| 8b61cf0742 | |||
| a02c75aa08 | |||
| 51af393baa | |||
| 41318e45a5 | |||
| a94f227103 | |||
| 517776acd4 | |||
| 212a5ded6e | |||
| fd9c929e37 | |||
| ce828163af | |||
| 97e1b419a0 | |||
| d70ac7f7c5 | |||
| 7065cb69f1 | |||
| 7815463ba0 | |||
| 48e6631751 | |||
| 903e6b70b4 | |||
| 5edba59b24 | |||
| 31cb4b06e4 | |||
| 10f6723f00 | |||
| f594620c68 | |||
| dacfad61d8 | |||
| 7c1da7e0cf | |||
| e709f99b47 | |||
| f150419891 | |||
| 053fd26994 | |||
| bba1a1e3ca | |||
| e5da1efea4 | |||
| fbfd41c16e | |||
| 4391921727 | |||
| 4cd3c657e2 | |||
| 7647b77225 | |||
| fe8710fe00 | |||
| 1a9b147baf | |||
| 6214e0385d | |||
| f7002c31f0 | |||
| 99b75396ad | |||
| b1c3baa7bd | |||
| 355eb5dfde | |||
| d269e32c3a | |||
| 214b76f763 | |||
| 73a8fbdfb5 | |||
| 9b6f0aeb3c | |||
| 45a6aed57f | |||
| 9d17a160b6 | |||
| 850d097660 | |||
| 8ce8f67c06 | |||
| a17c2c166d | |||
| a64aa9e280 | |||
| 3834a47a39 | |||
| 9f6ff515ff | |||
| 64a210842e | |||
| c0de52c967 | |||
| 67c3df05b3 | |||
| 4a66616756 | |||
| e4d424bf56 | |||
| c0fc6091b1 | |||
| ef35d19ff1 | |||
| cfa787c4b6 | |||
| 611ebce0d2 | |||
| 1e0b8cb547 | |||
| 99d323b1dd | |||
| 72bc9b6c7f | |||
| 4694a482f4 | |||
| 443f39bfdb | |||
| b118dd8fc6 | |||
| bf39da7b8b | |||
| ad3eee059e | |||
| e94ccabcf0 | |||
| 427a29c74e | |||
| 1d79edbae0 | |||
| 9e4a6d09df | |||
| 7b08289799 | |||
| bc937546ec | |||
| 3b972fe4a3 | |||
| 5e93725044 | |||
| 500af47b79 | |||
| d862e04186 | |||
| 5f293cb1e0 | |||
| c93ea2c22f | |||
| c567005241 | |||
| a6823d157c | |||
| bf54cc59e1 | |||
| 740b0c7dd7 | |||
| c3f5083f15 | |||
| b65c11daf6 | |||
| 263e1f59f7 | |||
| 3d89c9199c | |||
| 4a51bf90ab | |||
| a77c7e36b0 | |||
| 88ae3fddf3 | |||
| bddcf0624e | |||
| 6b47d3eb0d | |||
| a0c97b33e0 | |||
| 5def9cef9f | |||
| c20d745028 | |||
| e099435cd6 | |||
| d6a00f5151 | |||
| 653413e912 | |||
| 288eecc23d | |||
| b55b6f3d26 | |||
| d00561b58c | |||
| 432810db89 | |||
| f3b1b5d419 | |||
| d9a7b67f0e | |||
| 85f05192c8 | |||
| 149b715b4f | |||
| bae345b92b | |||
| 10ec128b94 | |||
| 7a6e409879 | |||
| ea19e4bc7f | |||
| 6122a525d2 | |||
| b607d95a93 | |||
| ee46333def | |||
| 34b40aec66 | |||
| 639147be41 | |||
| c48aa38c39 | |||
| 70bec7b714 | |||
| a7c15eaccf | |||
| c78e99dd54 | |||
| 3196779308 | |||
| 064b005ec5 | |||
| 2d2b98e525 | |||
| 71f623ddbf | |||
| f03b5f4c44 | |||
| aff8b47b6c | |||
| 6c1452435d | |||
| 38f365ffeb | |||
| ed846c4a88 | |||
| 8855a304cc | |||
| 95a083a6f4 | |||
| 0bb99bb371 | |||
| 38cc3ccb1e | |||
| c51b044b3f | |||
| 363351b18c | |||
| 167bbc768a | |||
| 591e15f0f6 | |||
| b4a917db09 | |||
| 7755fcc846 | |||
| 250af8f06a | |||
| 9569e407b5 | |||
| 573d5525ec | |||
| f1de7b008b | |||
| c10e6ded78 | |||
| 9bc991cb7d | |||
| ffa7c32d80 | |||
| 038491d2ec | |||
| 9d1d57cd82 | |||
| c38b2bfc21 | |||
| 18e7805ef5 | |||
| 010eab6e30 | |||
| 30c8088d73 | |||
| 354009033a | |||
| bf3f904fe9 | |||
| d65cafae64 | |||
| 128103b7b2 | |||
| cbb88638d2 | |||
| f046aeeaa2 | |||
| bd5f9537c5 | |||
| 838bbd70ef | |||
| e7997244af | |||
| 3874e71971 | |||
| 67cc0c5286 | |||
| 826123db56 | |||
| 05b0037462 | |||
| 54796f888e | |||
| 82ec0a4837 | |||
| 7167df7979 | |||
| 45e7d8426d |
@@ -1,6 +1,6 @@
|
||||
#' Define default build variables
|
||||
ARG OTP_VSN='27.3.2'
|
||||
ARG ELIXIR_VSN='1.18.3'
|
||||
ARG OTP_VSN='27.3.4.2'
|
||||
ARG ELIXIR_VSN='1.18.4'
|
||||
ARG UID='9000'
|
||||
ARG USER='ejabberd'
|
||||
ARG HOME="opt/$USER"
|
||||
@@ -9,7 +9,7 @@ ARG VERSION='master'
|
||||
|
||||
################################################################################
|
||||
#' Compile ejabberdapi
|
||||
FROM docker.io/golang:1.24-alpine AS api
|
||||
FROM docker.io/golang:1.25-alpine AS api
|
||||
RUN go install -v \
|
||||
github.com/processone/ejabberd-api/cmd/ejabberd@master \
|
||||
&& mv bin/ejabberd bin/ejabberdapi
|
||||
|
||||
@@ -206,7 +206,7 @@ modules:
|
||||
mod_fail2ban: {}
|
||||
mod_http_api: {}
|
||||
mod_http_upload:
|
||||
put_url: https://@HOST@:5443/upload
|
||||
put_url: https://@HOST_URL_ENCODE@:5443/upload
|
||||
custom_headers:
|
||||
"Access-Control-Allow-Origin": "https://@HOST@"
|
||||
"Access-Control-Allow-Methods": "GET,HEAD,PUT,OPTIONS"
|
||||
|
||||
@@ -195,7 +195,9 @@ livewarning()
|
||||
echo "Please be extremely cautious with your actions,"
|
||||
echo "and exit immediately if you are not completely sure."
|
||||
echo ""
|
||||
echo "To exit and detach this shell from ejabberd, press:"
|
||||
echo "To stop ejabberd gracefully:"
|
||||
echo " ejabberd:stop()."
|
||||
echo "To quit erlang immediately, press:"
|
||||
echo " control+g and then q"
|
||||
echo ""
|
||||
echo "--------------------------------------------------------------------"
|
||||
@@ -363,6 +365,13 @@ post_waiter_loop()
|
||||
# allow sync calls
|
||||
wait_status()
|
||||
{
|
||||
wait_status_node "$ERLANG_NODE" $1 $2 $3
|
||||
}
|
||||
|
||||
wait_status_node()
|
||||
{
|
||||
CONNECT_NODE=$1
|
||||
shift
|
||||
# args: status try delay
|
||||
# return: 0 OK, 1 KO
|
||||
timeout="$2"
|
||||
@@ -374,9 +383,9 @@ wait_status()
|
||||
status="$1"
|
||||
else
|
||||
run_erl "$(uid ctl)" -hidden -noinput \
|
||||
-eval 'net_kernel:connect_node('"'$ERLANG_NODE'"')' \
|
||||
-eval 'net_kernel:connect_node('"'$CONNECT_NODE'"')' \
|
||||
-s ejabberd_ctl \
|
||||
-extra "$ERLANG_NODE" $NO_TIMEOUT status > /dev/null
|
||||
-extra "$CONNECT_NODE" $NO_TIMEOUT status > /dev/null
|
||||
status="$?"
|
||||
fi
|
||||
done
|
||||
@@ -385,19 +394,26 @@ wait_status()
|
||||
|
||||
exec_other_command()
|
||||
{
|
||||
exec_other_command_node $ERLANG_NODE "$@"
|
||||
}
|
||||
|
||||
exec_other_command_node()
|
||||
{
|
||||
CONNECT_NODE=$1
|
||||
shift
|
||||
if [ -z "$CTL_OVER_HTTP" ] || [ ! -S "$CTL_OVER_HTTP" ] \
|
||||
|| [ ! -x "$(command -v curl)" ] || [ -z "$1" ] || [ "$1" = "help" ] \
|
||||
|| [ "$1" = "mnesia_info_ctl" ]|| [ "$1" = "print_sql_schema" ] ; then
|
||||
run_erl "$(uid ctl)" -hidden -noinput \
|
||||
-eval 'net_kernel:connect_node('"'$ERLANG_NODE'"')' \
|
||||
-eval 'net_kernel:connect_node('"'$CONNECT_NODE'"')' \
|
||||
-s ejabberd_ctl \
|
||||
-extra "$ERLANG_NODE" $NO_TIMEOUT "$@"
|
||||
-extra "$CONNECT_NODE" $NO_TIMEOUT "$@"
|
||||
result=$?
|
||||
case $result in
|
||||
3) help;;
|
||||
*) :;;
|
||||
esac
|
||||
exit $result
|
||||
return $result
|
||||
else
|
||||
exec_ctl_over_http_socket "$@"
|
||||
fi
|
||||
@@ -439,6 +455,103 @@ cd "$SPOOL_DIR" || {
|
||||
exit 6
|
||||
}
|
||||
|
||||
printe()
|
||||
{
|
||||
printf "\n"
|
||||
printf "\e[1;40;32m==> %s\e[0m\n" "$1"
|
||||
}
|
||||
|
||||
## Function copied from tools/make-installers
|
||||
user_agrees()
|
||||
{
|
||||
question="$*"
|
||||
|
||||
if [ -t 0 ]
|
||||
then
|
||||
printe "$question (y/n) [n]"
|
||||
read -r 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
|
||||
}
|
||||
|
||||
mnesia_change()
|
||||
{
|
||||
ERLANG_NODE_OLD="$1"
|
||||
[ "$ERLANG_NODE_OLD" = "" ] \
|
||||
&& echo "Error: Please provide the old erlang node name, for example:" \
|
||||
&& echo " ejabberdctl mnesia_change ejabberd@oldmachine" \
|
||||
&& exit 1
|
||||
|
||||
SPOOL_DIR_BACKUP=$SPOOL_DIR/$ERLANG_NODE_OLD-backup/
|
||||
OLDFILE=$SPOOL_DIR_BACKUP/$ERLANG_NODE_OLD.backup
|
||||
NEWFILE=$SPOOL_DIR_BACKUP/$ERLANG_NODE.backup
|
||||
|
||||
printe "This changes your mnesia database from node name '$ERLANG_NODE_OLD' to '$ERLANG_NODE'"
|
||||
|
||||
[ -d "$SPOOL_DIR_BACKUP" ] && printe "WARNING! A backup of old node already exists in $SPOOL_DIR_BACKUP"
|
||||
|
||||
if ! user_agrees "Do you want to proceed?"
|
||||
then
|
||||
echo 'Operation aborted.'
|
||||
exit 1
|
||||
fi
|
||||
|
||||
printe "Starting ejabberd with old node name $ERLANG_NODE_OLD ..."
|
||||
exec_erl "$ERLANG_NODE_OLD" $EJABBERD_OPTS -detached
|
||||
wait_status_node $ERLANG_NODE_OLD 0 30 2
|
||||
result=$?
|
||||
case $result in
|
||||
1) echo "There was a problem starting ejabberd with the old erlang node name. " \
|
||||
&& echo "Check for log errors in $EJABBERD_LOG_PATH" \
|
||||
&& exit $result;;
|
||||
*) :;;
|
||||
esac
|
||||
exec_other_command_node $ERLANG_NODE_OLD "status"
|
||||
|
||||
printe "Making backup of old database to file $OLDFILE ..."
|
||||
mkdir $SPOOL_DIR_BACKUP
|
||||
exec_other_command_node $ERLANG_NODE_OLD backup "$OLDFILE"
|
||||
|
||||
printe "Changing node name in new backup file $NEWFILE ..."
|
||||
exec_other_command_node $ERLANG_NODE_OLD mnesia_change_nodename "$ERLANG_NODE_OLD" "$ERLANG_NODE" "$OLDFILE" "$NEWFILE"
|
||||
|
||||
printe "Stopping old ejabberd..."
|
||||
exec_other_command_node $ERLANG_NODE_OLD "stop"
|
||||
wait_status_node $ERLANG_NODE_OLD 3 30 2 && stop_epmd
|
||||
|
||||
printe "Moving old mnesia spool files to backup subdirectory $SPOOL_DIR_BACKUP ..."
|
||||
mv $SPOOL_DIR/*.DAT $SPOOL_DIR_BACKUP
|
||||
mv $SPOOL_DIR/*.DCD $SPOOL_DIR_BACKUP
|
||||
mv $SPOOL_DIR/*.LOG $SPOOL_DIR_BACKUP
|
||||
|
||||
printe "Starting ejabberd with new node name $ERLANG_NODE ..."
|
||||
exec_erl "$ERLANG_NODE" $EJABBERD_OPTS -detached
|
||||
wait_status 0 30 2
|
||||
exec_other_command "status"
|
||||
|
||||
printe "Installing fallback of new mnesia..."
|
||||
exec_other_command install_fallback "$NEWFILE"
|
||||
|
||||
printe "Stopping new ejabberd..."
|
||||
exec_other_command "stop"
|
||||
wait_status 3 30 2 && stop_epmd
|
||||
|
||||
printe "Finished, now you can start ejabberd normally"
|
||||
}
|
||||
|
||||
# main
|
||||
case $1 in
|
||||
start)
|
||||
@@ -501,6 +614,9 @@ case $1 in
|
||||
set_dist_client
|
||||
wait_status 3 30 2 && stop_epmd # wait 30x2s before timeout
|
||||
;;
|
||||
mnesia_change)
|
||||
mnesia_change $2
|
||||
;;
|
||||
post_waiter)
|
||||
post_waiter_waiting
|
||||
;;
|
||||
|
||||
@@ -25,7 +25,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
otp: ['25', '26', '27']
|
||||
otp: ['25', '26', '27', '28']
|
||||
runs-on: ubuntu-24.04
|
||||
services:
|
||||
redis:
|
||||
@@ -35,7 +35,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- name: Test shell scripts
|
||||
if: matrix.otp == '27'
|
||||
@@ -120,6 +120,7 @@ jobs:
|
||||
- run: make dialyzer
|
||||
- run: make test-eunit
|
||||
- run: make elvis
|
||||
if: matrix.otp >= '26'
|
||||
|
||||
- name: Check Production Release
|
||||
run: |
|
||||
@@ -145,7 +146,7 @@ jobs:
|
||||
- name: Run XMPP Interoperability Tests against CI server.
|
||||
if: matrix.otp == '27'
|
||||
continue-on-error: true
|
||||
uses: XMPP-Interop-Testing/xmpp-interop-tests-action@v1.5.0
|
||||
uses: XMPP-Interop-Testing/xmpp-interop-tests-action@v1.6.0
|
||||
with:
|
||||
domain: 'localhost'
|
||||
adminAccountUsername: 'admin'
|
||||
|
||||
@@ -22,12 +22,12 @@ jobs:
|
||||
packages: write
|
||||
steps:
|
||||
- name: Check out repository code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Checkout ejabberd-contrib
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
repository: processone/ejabberd-contrib
|
||||
path: .ejabberd-modules/sources/ejabberd-contrib
|
||||
|
||||
@@ -41,7 +41,7 @@ jobs:
|
||||
gem install --no-document --user-install fpm
|
||||
echo $HOME/.local/share/gem/ruby/*/bin >> $GITHUB_PATH
|
||||
- name: Check out repository code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Build binary archives
|
||||
@@ -74,7 +74,7 @@ jobs:
|
||||
if: github.ref_type == 'tag'
|
||||
steps:
|
||||
- name: Download packages
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v5
|
||||
with:
|
||||
name: ejabberd-packages
|
||||
- name: Draft Release
|
||||
|
||||
@@ -31,20 +31,24 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
otp: ['20', '25', '26', '27']
|
||||
otp: ['24', '25', '26', '27', '28']
|
||||
rebar: ['rebar', 'rebar3']
|
||||
exclude:
|
||||
- otp: '24'
|
||||
rebar: 'rebar'
|
||||
- otp: '27'
|
||||
rebar: 'rebar'
|
||||
- otp: '28'
|
||||
rebar: 'rebar'
|
||||
runs-on: ubuntu-24.04
|
||||
container:
|
||||
image: public.ecr.aws/docker/library/erlang:${{ matrix.otp }}
|
||||
|
||||
steps:
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- name: Get compatible Rebar binaries
|
||||
- name: Get old compatible Rebar binaries
|
||||
if: matrix.otp < 24
|
||||
run: |
|
||||
rm rebar
|
||||
@@ -54,6 +58,16 @@ jobs:
|
||||
chmod +x rebar
|
||||
chmod +x rebar3
|
||||
|
||||
- name: Get recent compatible Rebar binaries
|
||||
if: matrix.otp > 23 && matrix.otp < 25
|
||||
run: |
|
||||
rm rebar
|
||||
rm rebar3
|
||||
wget https://github.com/processone/ejabberd/raw/24.12/rebar
|
||||
wget https://github.com/processone/ejabberd/raw/24.12/rebar3
|
||||
chmod +x rebar
|
||||
chmod +x rebar3
|
||||
|
||||
- name: Prepare libraries
|
||||
run: |
|
||||
apt-get -qq update
|
||||
@@ -68,7 +82,7 @@ jobs:
|
||||
~/.cache/rebar3/
|
||||
key: ${{matrix.otp}}-${{hashFiles('rebar.config')}}
|
||||
|
||||
- name: Get compatible Rebar binaries
|
||||
- name: Unlock eredis dependency
|
||||
if: matrix.rebar == 'rebar3' && matrix.otp < 21
|
||||
run: rebar3 unlock eredis
|
||||
|
||||
@@ -77,6 +91,7 @@ jobs:
|
||||
./autogen.sh
|
||||
./configure --with-rebar=./${{ matrix.rebar }} \
|
||||
--prefix=/tmp/ejabberd \
|
||||
--with-min-erlang=9.0.5 \
|
||||
--enable-all \
|
||||
--disable-elixir \
|
||||
--disable-tools \
|
||||
@@ -164,14 +179,14 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
elixir: ['1.13', '1.14', '1.15', '1.16', '1.17', '1.18']
|
||||
elixir: ['1.14', '1.15', '1.16', '1.17', '1.18']
|
||||
runs-on: ubuntu-24.04
|
||||
container:
|
||||
image: public.ecr.aws/docker/library/elixir:${{ matrix.elixir }}
|
||||
|
||||
steps:
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- name: Prepare libraries
|
||||
run: |
|
||||
@@ -287,14 +302,14 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
elixir: ['1.13', '1.14', '1.15', '1.16', '1.17', '1.18']
|
||||
elixir: ['1.14', '1.15', '1.16', '1.17', '1.18']
|
||||
runs-on: ubuntu-24.04
|
||||
container:
|
||||
image: public.ecr.aws/docker/library/elixir:${{ matrix.elixir }}
|
||||
|
||||
steps:
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- name: Prepare libraries
|
||||
run: |
|
||||
@@ -344,7 +359,6 @@ jobs:
|
||||
- run: make dialyzer
|
||||
|
||||
- run: make edoc
|
||||
if: matrix.elixir >= '1.14'
|
||||
|
||||
- name: Run rel
|
||||
run: |
|
||||
|
||||
+123
@@ -1,3 +1,126 @@
|
||||
## Version 25.08
|
||||
|
||||
#### API Commands
|
||||
|
||||
- `ban_account`: Run `sm_kick_user` event when kicking account ([#4415](https://github.com/processone/ejabberd/issues/4415))
|
||||
- `ban_account`: No need to change password ([#4415](https://github.com/processone/ejabberd/issues/4415))
|
||||
- `mnesia_change`: New command in `ejabberdctl` script that helps changing the mnesia node name
|
||||
|
||||
#### Configuration
|
||||
|
||||
- Rename `auth_password_types_hidden_in_scram1` option to `auth_password_types_hidden_in_sasl1`
|
||||
- `econf`: If a host in configuration is encoded IDNA, decode it ([#3519](https://github.com/processone/ejabberd/issues/3519))
|
||||
- `ejabberd_config`: New predefined keyword `HOST_URL_ENCODE`
|
||||
- `ejabberd.yml.example`: Use `HOST_URL_ENCODE` to handle case when vhost is non-latin1
|
||||
- `mod_conversejs`: Add option `conversejs_plugins` ([#4413](https://github.com/processone/ejabberd/issues/4413))
|
||||
- `mod_matrix_gw`: Add `leave_timeout` option ([#4386](https://github.com/processone/ejabberd/issues/4386))
|
||||
|
||||
#### Documentation and Tests
|
||||
|
||||
- `COMPILE.md`: Mention dependencies and add link to Docs ([#4431](https://github.com/processone/ejabberd/issues/4431))
|
||||
- `ejabberd_doc`: Document commands tags for modules
|
||||
- CI: bump XMPP-Interop-Testing/xmpp-interop-tests-action ([#4425](https://github.com/processone/ejabberd/issues/4425))
|
||||
- Runtime: Raise the minimum Erlang tested to Erlang/OTP 24
|
||||
|
||||
#### Installers and Container
|
||||
|
||||
- Bump Erlang/OTP version to 27.3.4.2
|
||||
- Bump OpenSSL version to 3.5.2
|
||||
- `make-binaries`: Disable Linux-PAM's `logind` support
|
||||
|
||||
#### Core and Modules
|
||||
|
||||
- Bump `p1_acme` to fix `'AttributePKCS-10'` and OTP 28 ([processone/p1_acme#4](https://github.com/processone/p1_acme/issues/4))
|
||||
- Prevent loops in `xml_compress:decode` with corrupted data
|
||||
- `ejabberd_auth_mnesia`: Fix issue with filtering duplicates in `get_users()`
|
||||
- `ejabberd_listener`: Add secret in temporary unix domain socket path ([#4422](https://github.com/processone/ejabberd/issues/4422))
|
||||
- `ejabberd_listener`: Log error when cannot set definitive unix socket ([#4422](https://github.com/processone/ejabberd/issues/4422))
|
||||
- `ejabberd_listener`: Try to create provisional socket in final directory ([#4422](https://github.com/processone/ejabberd/issues/4422))
|
||||
- `ejabberd_logger`: Print log lines colorized in console when using rebar3
|
||||
- `mod_conversejs`: Ensure assets_path ends in `/` as required by Converse ([#4414](https://github.com/processone/ejabberd/issues/4414))
|
||||
- `mod_conversejs`: Ensure plugins URL is separated with `/` ([#4413](https://github.com/processone/ejabberd/issues/4413))
|
||||
- `mod_http_upload`: Encode URLs into IDNA when showing to XMPP client ([#3519](https://github.com/processone/ejabberd/issues/3519))
|
||||
- `mod_matrix_gw`: Add support for null values in `is_canonical_json` ([#4421](https://github.com/processone/ejabberd/issues/4421))
|
||||
- `mod_matrix_gw`: Don't send empty direct Matrix messages ([#4420](https://github.com/processone/ejabberd/issues/4420))
|
||||
- `mod_matrix_gw`: Matrix gateway updates
|
||||
- `mod_muc`: Report db failures when restoring rooms
|
||||
- `mod_muc`: Unsubscribe users from members-only rooms when expelled ([#4412](https://github.com/processone/ejabberd/issues/4412))
|
||||
- `mod_providers`: New module to serve easily XMPP Providers files
|
||||
- `mod_register`: Don't duplicate welcome subject and message
|
||||
- `mod_scram_upgrade`: Fix format of passwords updates
|
||||
- `mod_scram_upgrade`: Only offer upgrades to methods that aren't already stored
|
||||
|
||||
## Version 25.07
|
||||
|
||||
#### Security fix
|
||||
|
||||
- `ext_mod`: Add temporary workaround for zip including absolute path
|
||||
|
||||
#### Compilation
|
||||
|
||||
- Raise the minimum Elixir tested version to 1.14.0 ([#4281](https://github.com/processone/ejabberd/issues/4281))
|
||||
- Raise Erlang/OTP minimum requirement to 25.0 ([#4281](https://github.com/processone/ejabberd/issues/4281))
|
||||
- `configure.ac`: Allow to specify minimal erlang version using `--with-min-erlang`
|
||||
- `Makefile.in`: Add target `test-<group>`
|
||||
- `rebar3-format.sh`: Replace csplit with perl
|
||||
- Container: Bump Erlang/OTP 27.3.4.1, Elixir 1.18.4
|
||||
- Installers: Bump Erlang/OTP 27.3.4.1, Elixir 1.18.4, libexpat 2.7.1, OpenSSL 3.5.1
|
||||
|
||||
#### Configuration and Tests
|
||||
|
||||
- Add `rest_proxy*` options to configure proxy used by rest module
|
||||
- `ejabberd_c2s`: Add `auth_password_types_hidden_in_scram1` option
|
||||
- `ejabberd_http`: Remove unused `default_host` option and state element
|
||||
- `ejabberd_http`: New option `hosts_alias` and function `resolve_host_alias/1` ([#4400](https://github.com/processone/ejabberd/issues/4400))
|
||||
- New predefined keywords: `CONFIG_PATH` and `LOG_PATH`
|
||||
- Fix macro used in string options when defined in env var
|
||||
- Use auxiliary function to get `$HOME`, use Mnesia directory when not set ([#4402](https://github.com/processone/ejabberd/issues/4402))
|
||||
- `ejabberd_config`: Better `lists:uniq` substitute
|
||||
- Tests: update readme and compose to work with current sw versions
|
||||
- Update Elvis to 4.1.1, fix some warnings and enable their tests
|
||||
|
||||
#### Erlang/OTP 28 support
|
||||
|
||||
- Add workaround in `p1_acme` for Jose 1.11.10 not supporting OTP 28 `ecPrivkeyVer1` ([#4393](https://github.com/processone/ejabberd/issues/4393))
|
||||
- Bump `fast_xml` and `xmpp` for improved Erlang/OTP 28 support
|
||||
- Bump `xmpp` and `p1_acme` patched with Erlang/OTP 28 support
|
||||
- Fix `make options` in Erlang/OTP 28 ([#4352](https://github.com/processone/ejabberd/issues/4352))
|
||||
- Fix crash in `rebar3 cover` with Erlang/OTP 28 ([#4353](https://github.com/processone/ejabberd/issues/4353))
|
||||
- Rebar/Rebar3: Update binaries to work with Erlang/OTP 25-28 ([#4354](https://github.com/processone/ejabberd/issues/4354))
|
||||
- CI and Runtime: Add Erlang/OTP 28 to the versions matrix
|
||||
|
||||
#### SQL
|
||||
|
||||
- Fix mnesia to sql exporter after changes to auth tables
|
||||
- Update code for switching to new schema type to users table changes
|
||||
- Add mssql specific implementation of `delete_old_mam_messages`
|
||||
- Make `delete_old_mam_messages_batch` work with sqlite
|
||||
- `ejabberd_sm_sql`: Use misc:encode_pid/1
|
||||
- `mysql.sql`: Fix typo in commit 7862c6a when creating users table
|
||||
- `pg.sql`: Fix missing comma in postgres schema ([#4409](https://github.com/processone/ejabberd/issues/4409))
|
||||
|
||||
#### Core and Modules
|
||||
|
||||
- `ejabberd_s2s_in`: Allow S2S connections to accept client certificates that have only server purpose ([#4392](https://github.com/processone/ejabberd/issues/4392))
|
||||
- `ext_mod`: Recommend to write README.md instead txt (processone/ejabberd-contrib#363)
|
||||
- `ext_mod`: Support library path installed from Debian (processone/ejabberd-contrib#363)
|
||||
- `ext_mod`: When upgrading module, clean also the compiled directories
|
||||
- `gen_mod`: Add support to prepare module stopping before actually stopping any module
|
||||
- `mod_antispam`: Imported from ejabberd-contrib and improved ([#4373](https://github.com/processone/ejabberd/issues/4373))
|
||||
- `mod_auth_fast`: Clear tokens on kick, change pass and unregister ([#4397](https://github.com/processone/ejabberd/issues/4397))([#4398](https://github.com/processone/ejabberd/issues/4398))([#4399](https://github.com/processone/ejabberd/issues/4399))
|
||||
- `mod_conversejs`: Add link in WebAdmin to local Converse if configured
|
||||
- `mod_mam`: Present mam full text search in xep-431 compatible way
|
||||
- `mod_mam_mnesia`: Handle objects that don't need conversion in `transform/0`
|
||||
- `mod_matrix_gw`: Don't send empty messages in Matrix rooms ([#4385](https://github.com/processone/ejabberd/issues/4385))
|
||||
- `mod_matrix_gw`: Support older Matrix rooms versions starting from version 4
|
||||
- `mod_matrix_gw`: When encoding JSON, handle term that is key-value list ([#4379](https://github.com/processone/ejabberd/issues/4379))
|
||||
- `mod_matrix_gw_s2s`: Fix key validation in `check_signature`
|
||||
- `mod_mix` and `mod_muc_rtbl`: Support list of IDs in `pubsub-items-retract` (processone/xmpp#100)
|
||||
- `mod_pubsub_serverinfo`: Imported module from ejabberd-contrib ([#4408](https://github.com/processone/ejabberd/issues/4408))
|
||||
- `mod_register`: Normalize username when determining if user want to change pass
|
||||
- `mod_register`: Strip query data when returning errors
|
||||
- WebAdmin: New hooks `webadmin_menu_system` to add items to system menu
|
||||
|
||||
## Version 25.04
|
||||
|
||||
#### Security fixes
|
||||
|
||||
+7
-3
@@ -19,7 +19,7 @@ To compile ejabberd you need:
|
||||
- GCC
|
||||
- Libexpat ≥ 1.95
|
||||
- Libyaml ≥ 0.1.4
|
||||
- Erlang/OTP ≥ 20.0
|
||||
- Erlang/OTP ≥ 25.0
|
||||
- OpenSSL ≥ 1.0.0
|
||||
|
||||
Other optional libraries are:
|
||||
@@ -28,8 +28,7 @@ Other optional libraries are:
|
||||
- PAM library, for Pluggable Authentication Modules (PAM)
|
||||
- ImageMagick's Convert program and Ghostscript fonts, for CAPTCHA
|
||||
challenges
|
||||
- Elixir ≥ 1.10.3, for Elixir support. It is recommended Elixir 1.13.4 or higher
|
||||
and Erlang/OTP 23.0 or higher.
|
||||
- Elixir ≥ 1.10.3, for Elixir support. It is recommended Elixir 1.14.0 or higher
|
||||
|
||||
If your system splits packages in libraries and development headers,
|
||||
install the development packages too.
|
||||
@@ -66,6 +65,11 @@ To configure the compilation, features, install paths...
|
||||
|
||||
./configure --help
|
||||
|
||||
The build tool automatically downloads and compiles the
|
||||
erlang libraries that [ejabberd depends on][docs-repo].
|
||||
|
||||
[docs-repo]: https://docs.ejabberd.im/developer/repositories/
|
||||
|
||||
|
||||
Install in the System
|
||||
---------------------
|
||||
|
||||
+150
-4
@@ -310,6 +310,152 @@ now you can define the admin account JID using an environment variable:
|
||||
Check the [full example](#customized-example) for other example.
|
||||
|
||||
|
||||
### ejabberd-contrib
|
||||
|
||||
This section addresses those topics related to
|
||||
[ejabberd-contrib](https://docs.ejabberd.im/admin/guide/modules/#ejabberd-contrib):
|
||||
|
||||
- [Download source code](#download-source-code)
|
||||
- [Install a module](#install-a-module)
|
||||
- [Install git for dependencies](#install-git-for-dependencies)
|
||||
- [Install your module](#install-your-module)
|
||||
|
||||
---
|
||||
|
||||
#### Download source code
|
||||
|
||||
The `ejabberd` container image includes the ejabberd-contrib git repository source code,
|
||||
but `ecs` does not, so first download it:
|
||||
```bash
|
||||
$ docker exec ejabberd ejabberdctl modules_update_specs
|
||||
```
|
||||
|
||||
#### Install a module
|
||||
|
||||
Compile and install any of the contributed modules, for example:
|
||||
```bash
|
||||
docker exec ejabberd ejabberdctl module_install mod_statsdx
|
||||
|
||||
Module mod_statsdx has been installed and started.
|
||||
It's configured in the file:
|
||||
/opt/ejabberd/.ejabberd-modules/mod_statsdx/conf/mod_statsdx.yml
|
||||
Configure the module in that file, or remove it
|
||||
and configure in your main ejabberd.yml
|
||||
```
|
||||
|
||||
#### Install git for dependencies
|
||||
|
||||
Some modules depend on erlang libraries,
|
||||
but the container images do not include `git` or `mix` to download them.
|
||||
Consequently, when you attempt to install such a module,
|
||||
there will be error messages like:
|
||||
|
||||
```bash
|
||||
docker exec ejabberd ejabberdctl module_install ejabberd_observer_cli
|
||||
|
||||
I'll download "recon" using git because I can't use Mix to fetch from hex.pm:
|
||||
/bin/sh: mix: not found
|
||||
Fetching dependency observer_cli:
|
||||
/bin/sh: git: not found
|
||||
...
|
||||
```
|
||||
|
||||
the solution is to install `git` in the container image:
|
||||
|
||||
```bash
|
||||
docker exec --user root ejabberd apk add git
|
||||
|
||||
fetch https://dl-cdn.alpinelinux.org/alpine/v3.21/main/x86_64/APKINDEX.tar.gz
|
||||
fetch https://dl-cdn.alpinelinux.org/alpine/v3.21/community/x86_64/APKINDEX.tar.gz
|
||||
(1/3) Installing pcre2 (10.43-r0)
|
||||
(2/3) Installing git (2.47.2-r0)
|
||||
(3/3) Installing git-init-template (2.47.2-r0)
|
||||
Executing busybox-1.37.0-r12.trigger
|
||||
OK: 27 MiB in 42 packages
|
||||
```
|
||||
|
||||
and now you can upgrade the module:
|
||||
|
||||
```bash
|
||||
docker exec ejabberd ejabberdctl module_upgrade ejabberd_observer_cli
|
||||
|
||||
I'll download "recon" using git because I can't use Mix to fetch from hex.pm:
|
||||
/bin/sh: mix: not found
|
||||
Fetching dependency observer_cli: Cloning into 'observer_cli'...
|
||||
Fetching dependency os_stats: Cloning into 'os_stats'...
|
||||
Fetching dependency recon: Cloning into 'recon'...
|
||||
Inlining: inline_size=24 inline_effort=150
|
||||
Old inliner: threshold=0 functions=[{insert,2},{merge,2}]
|
||||
Module ejabberd_observer_cli has been installed.
|
||||
Now you can configure it in your ejabberd.yml
|
||||
I'll download "recon" using git because I can't use Mix to fetch from hex.pm:
|
||||
/bin/sh: mix: not found
|
||||
```
|
||||
|
||||
#### Install your module
|
||||
|
||||
If you [developed an ejabberd module](https://docs.ejabberd.im/developer/extending-ejabberd/modules/),
|
||||
you can install it in your container image:
|
||||
|
||||
1. Create a local directory for `ejabberd-modules`:
|
||||
|
||||
``` sh
|
||||
mkdir docker-modules
|
||||
```
|
||||
|
||||
2. Then create the directory structure for your custom module:
|
||||
|
||||
``` sh
|
||||
cd docker-modules
|
||||
|
||||
mkdir -p sources/mod_hello_world/
|
||||
touch sources/mod_hello_world/mod_hello_world.spec
|
||||
|
||||
mkdir sources/mod_hello_world/src/
|
||||
mv mod_hello_world.erl sources/mod_hello_world/src/
|
||||
|
||||
mkdir sources/mod_hello_world/conf/
|
||||
echo -e "modules:\n mod_hello_world: {}" > sources/mod_hello_world/conf/mod_hello_world.yml
|
||||
|
||||
cd ..
|
||||
```
|
||||
|
||||
3. Grant ownership of that directory to the UID that ejabberd will use inside the Docker image:
|
||||
|
||||
``` sh
|
||||
sudo chown 9000 -R docker-modules/
|
||||
```
|
||||
|
||||
4. Start ejabberd in the container:
|
||||
|
||||
``` sh
|
||||
sudo docker run \
|
||||
--name hellotest \
|
||||
-d \
|
||||
--volume "$(pwd)/docker-modules:/home/ejabberd/.ejabberd-modules/" \
|
||||
-p 5222:5222 \
|
||||
-p 5280:5280 \
|
||||
ejabberd/ecs
|
||||
```
|
||||
|
||||
5. Check the module is available for installing, and then install it:
|
||||
|
||||
``` sh
|
||||
sudo docker exec -it hellotest ejabberdctl modules_available
|
||||
mod_hello_world []
|
||||
|
||||
sudo docker exec -it hellotest ejabberdctl module_install mod_hello_world
|
||||
```
|
||||
|
||||
6. If the module works correctly, you will see `Hello` in the ejabberd logs when it starts:
|
||||
|
||||
``` sh
|
||||
sudo docker exec -it hellotest grep Hello logs/ejabberd.log
|
||||
2020-10-06 13:40:13.154335+00:00 [info]
|
||||
<0.492.0>@mod_hello_world:start/2:15 Hello, ejabberd world!
|
||||
```
|
||||
|
||||
|
||||
### ejabberdapi
|
||||
|
||||
When the container is running (and thus ejabberd), you can exec commands inside the container
|
||||
@@ -418,12 +564,12 @@ it is necessary to change the old hostname stored in Mnesia.
|
||||
This section is equivalent to the ejabberd Documentation
|
||||
[Change Computer Hostname](https://docs.ejabberd.im/admin/guide/managing/#change-computer-hostname),
|
||||
but particularized to containers that use this
|
||||
ecs container image from ejabberd 23.01 or older.
|
||||
`ecs` container image from ejabberd 23.01 or older.
|
||||
|
||||
#### Setup Old Container
|
||||
|
||||
Let's assume a container running ejabberd 23.01 (or older) from
|
||||
this ecs container image, with the database directory binded
|
||||
this `ecs` container image, with the database directory binded
|
||||
and one registered account.
|
||||
This can be produced with:
|
||||
```bash
|
||||
@@ -926,10 +1072,10 @@ Let's summarize the differences between both container images. Legend:
|
||||
| Generated by | [container.yml](https://github.com/processone/ejabberd/blob/master/.github/workflows/container.yml) | [tests.yml](https://github.com/processone/docker-ejabberd/blob/master/.github/workflows/tests.yml) |
|
||||
| Built for | stable releases <br /> `master` branch | stable releases <br /> [`master` branch zip](https://github.com/processone/docker-ejabberd/actions/workflows/tests.yml) |
|
||||
| Architectures | `linux/amd64` <br /> `linux/arm64` | `linux/amd64` |
|
||||
| Software | Erlang/OTP 27.3.2-alpine <br /> Elixir 1.18.3 | Alpine 3.19 <br /> Erlang/OTP 26.2 <br /> Elixir 1.15.7 |
|
||||
| Software | Erlang/OTP 27.3.4.2-alpine <br /> Elixir 1.18.4 | Alpine 3.19 <br /> Erlang/OTP 26.2 <br /> Elixir 1.15.7 |
|
||||
| Published in | [ghcr.io/processone/ejabberd](https://github.com/processone/ejabberd/pkgs/container/ejabberd) | [docker.io/ejabberd/ecs](https://hub.docker.com/r/ejabberd/ecs/) <br /> [ghcr.io/processone/ecs](https://github.com/processone/docker-ejabberd/pkgs/container/ecs) |
|
||||
| :black_square_button: **Additional content** |
|
||||
| [ejabberd-contrib](https://docs.ejabberd.im/admin/guide/modules/#ejabberd-contrib) | included | not included |
|
||||
| [ejabberd-contrib](#ejabberd-contrib) | included | not included |
|
||||
| [ejabberdapi](#ejabberdapi) | included :orange_circle: | included |
|
||||
| :black_square_button: **Ports** |
|
||||
| [1880](#ports) for WebAdmin | yes :orange_circle: | yes :orange_circle: |
|
||||
|
||||
+12
@@ -661,6 +661,17 @@ test:
|
||||
@cd priv && ln -sf ../sql
|
||||
$(REBAR) $(SKIPDEPS) ct
|
||||
|
||||
.PHONY: test-%
|
||||
define test-group-target
|
||||
test-$1:
|
||||
$(REBAR) $(SKIPDEPS) ct --suite=test/ejabberd_SUITE --group=$1
|
||||
endef
|
||||
|
||||
ifneq ($(filter test-%,$(MAKECMDGOALS)),)
|
||||
group_to_test := $(patsubst test-%,%,$(filter test-%,$(MAKECMDGOALS)))
|
||||
$(eval $(call test-group-target,$(group_to_test)))
|
||||
endif
|
||||
|
||||
test-eunit:
|
||||
$(REBAR) $(SKIPDEPS) eunit --verbose
|
||||
|
||||
@@ -711,6 +722,7 @@ help:
|
||||
@echo " hooks Run hooks validator"
|
||||
@echo " test Run Common Tests suite [rebar3]"
|
||||
@echo " test-eunit Run EUnit suite [rebar3]"
|
||||
@echo " test-<group> Run Common Test suite for specific group only [rebar3]"
|
||||
@echo " xref Run cross reference analysis [rebar3]"
|
||||
|
||||
#.
|
||||
|
||||
+11
-2
@@ -2,8 +2,17 @@
|
||||
# Process this file with autoconf to produce a configure script.
|
||||
|
||||
AC_PREREQ(2.59)
|
||||
AC_INIT(ejabberd, m4_esyscmd([echo `git describe --tags 2>/dev/null || echo 25.04` | sed 's/-g.*//;s/-/./' | tr -d '\012']), [ejabberd@process-one.net], [ejabberd])
|
||||
REQUIRE_ERLANG_MIN="9.0.5 (Erlang/OTP 20.0)"
|
||||
AC_INIT(ejabberd, m4_esyscmd([echo `git describe --tags 2>/dev/null || echo 25.08` | sed 's/-g.*//;s/-/./' | tr -d '\012']), [ejabberd@process-one.net], [ejabberd])
|
||||
|
||||
AC_ARG_WITH(min-erlang,
|
||||
AS_HELP_STRING([--with-min-erlang=version],[set minimal required erlang version, default to OTP25]),
|
||||
[if test "X$withval" = "X"; then
|
||||
REQUIRE_ERLANG_MIN="13.0 (Erlang/OTP 25.0)"
|
||||
else
|
||||
REQUIRE_ERLANG_MIN="$withval"
|
||||
fi
|
||||
], [REQUIRE_ERLANG_MIN="13.0 (Erlang/OTP 25.0)"])
|
||||
|
||||
REQUIRE_ERLANG_MAX="100.0.0 (No Max)"
|
||||
|
||||
AC_CONFIG_MACRO_DIR([m4])
|
||||
|
||||
+21
-3
@@ -773,6 +773,15 @@
|
||||
<xmpp:note>mod_mam</xmpp:note>
|
||||
</xmpp:SupportedXep>
|
||||
</implements>
|
||||
<implements>
|
||||
<xmpp:SupportedXep>
|
||||
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0431.html"/>
|
||||
<xmpp:version>0.2.0</xmpp:version>
|
||||
<xmpp:since>24.12</xmpp:since>
|
||||
<xmpp:status>complete</xmpp:status>
|
||||
<xmpp:note>mod_mam</xmpp:note>
|
||||
</xmpp:SupportedXep>
|
||||
</implements>
|
||||
<implements>
|
||||
<xmpp:SupportedXep>
|
||||
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0440.html"/>
|
||||
@@ -821,10 +830,19 @@
|
||||
<implements>
|
||||
<xmpp:SupportedXep>
|
||||
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0485.html"/>
|
||||
<xmpp:version>0.2.0</xmpp:version>
|
||||
<xmpp:since>24.02</xmpp:since>
|
||||
<xmpp:version>0.1.1</xmpp:version>
|
||||
<xmpp:since>25.07</xmpp:since>
|
||||
<xmpp:status>complete</xmpp:status>
|
||||
<xmpp:note>, mod_pubsub_serverinfo in ejabberd-contrib.git</xmpp:note>
|
||||
<xmpp:note>mod_pubsub_serverinfo</xmpp:note>
|
||||
</xmpp:SupportedXep>
|
||||
</implements>
|
||||
<implements>
|
||||
<xmpp:SupportedXep>
|
||||
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0486.html"/>
|
||||
<xmpp:version>0.1.0</xmpp:version>
|
||||
<xmpp:since>24.07</xmpp:since>
|
||||
<xmpp:status>complete</xmpp:status>
|
||||
<xmpp:note>mod_muc</xmpp:note>
|
||||
</xmpp:SupportedXep>
|
||||
</implements>
|
||||
</Project>
|
||||
|
||||
@@ -179,7 +179,7 @@ modules:
|
||||
mod_fail2ban: {}
|
||||
mod_http_api: {}
|
||||
mod_http_upload:
|
||||
put_url: https://@HOST@:5443/upload
|
||||
put_url: https://@HOST_URL_ENCODE@:5443/upload
|
||||
custom_headers:
|
||||
"Access-Control-Allow-Origin": "https://@HOST@"
|
||||
"Access-Control-Allow-Methods": "GET,HEAD,PUT,OPTIONS"
|
||||
|
||||
+119
-5
@@ -318,6 +318,13 @@ check_start()
|
||||
# allow sync calls
|
||||
wait_status()
|
||||
{
|
||||
wait_status_node "$ERLANG_NODE" $1 $2 $3
|
||||
}
|
||||
|
||||
wait_status_node()
|
||||
{
|
||||
CONNECT_NODE=$1
|
||||
shift
|
||||
# args: status try delay
|
||||
# return: 0 OK, 1 KO
|
||||
timeout="$2"
|
||||
@@ -329,9 +336,9 @@ wait_status()
|
||||
status="$1"
|
||||
else
|
||||
exec_erl "$(uid ctl)" -hidden -noinput \
|
||||
-eval 'net_kernel:connect_node('"'$ERLANG_NODE'"')' \
|
||||
-eval 'net_kernel:connect_node('"'$CONNECT_NODE'"')' \
|
||||
-s ejabberd_ctl \
|
||||
-extra "$ERLANG_NODE" $NO_TIMEOUT status > /dev/null
|
||||
-extra "$CONNECT_NODE" $NO_TIMEOUT status > /dev/null
|
||||
status="$?"
|
||||
fi
|
||||
done
|
||||
@@ -340,19 +347,26 @@ wait_status()
|
||||
|
||||
exec_other_command()
|
||||
{
|
||||
exec_other_command_node $ERLANG_NODE "$@"
|
||||
}
|
||||
|
||||
exec_other_command_node()
|
||||
{
|
||||
CONNECT_NODE=$1
|
||||
shift
|
||||
if [ -z "$CTL_OVER_HTTP" ] || [ ! -S "$CTL_OVER_HTTP" ] \
|
||||
|| [ ! -x "$(command -v curl)" ] || [ -z "$1" ] || [ "$1" = "help" ] \
|
||||
|| [ "$1" = "mnesia_info_ctl" ]|| [ "$1" = "print_sql_schema" ] ; then
|
||||
exec_erl "$(uid ctl)" -hidden -noinput \
|
||||
-eval 'net_kernel:connect_node('"'$ERLANG_NODE'"')' \
|
||||
-eval 'net_kernel:connect_node('"'$CONNECT_NODE'"')' \
|
||||
-s ejabberd_ctl \
|
||||
-extra "$ERLANG_NODE" $NO_TIMEOUT "$@"
|
||||
-extra "$CONNECT_NODE" $NO_TIMEOUT "$@"
|
||||
result=$?
|
||||
case $result in
|
||||
3) help;;
|
||||
*) :;;
|
||||
esac
|
||||
exit $result
|
||||
return $result
|
||||
else
|
||||
exec_ctl_over_http_socket "$@"
|
||||
fi
|
||||
@@ -393,6 +407,103 @@ cd "$SPOOL_DIR" || {
|
||||
exit 6
|
||||
}
|
||||
|
||||
printe()
|
||||
{
|
||||
printf "\n"
|
||||
printf "\e[1;40;32m==> %s\e[0m\n" "$1"
|
||||
}
|
||||
|
||||
## Function copied from tools/make-installers
|
||||
user_agrees()
|
||||
{
|
||||
question="$*"
|
||||
|
||||
if [ -t 0 ]
|
||||
then
|
||||
printe "$question (y/n) [n]"
|
||||
read -r 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
|
||||
}
|
||||
|
||||
mnesia_change()
|
||||
{
|
||||
ERLANG_NODE_OLD="$1"
|
||||
[ "$ERLANG_NODE_OLD" = "" ] \
|
||||
&& echo "Error: Please provide the old erlang node name, for example:" \
|
||||
&& echo " ejabberdctl mnesia_change ejabberd@oldmachine" \
|
||||
&& exit 1
|
||||
|
||||
SPOOL_DIR_BACKUP=$SPOOL_DIR/$ERLANG_NODE_OLD-backup/
|
||||
OLDFILE=$SPOOL_DIR_BACKUP/$ERLANG_NODE_OLD.backup
|
||||
NEWFILE=$SPOOL_DIR_BACKUP/$ERLANG_NODE.backup
|
||||
|
||||
printe "This changes your mnesia database from node name '$ERLANG_NODE_OLD' to '$ERLANG_NODE'"
|
||||
|
||||
[ -d "$SPOOL_DIR_BACKUP" ] && printe "WARNING! A backup of old node already exists in $SPOOL_DIR_BACKUP"
|
||||
|
||||
if ! user_agrees "Do you want to proceed?"
|
||||
then
|
||||
echo 'Operation aborted.'
|
||||
exit 1
|
||||
fi
|
||||
|
||||
printe "Starting ejabberd with old node name $ERLANG_NODE_OLD ..."
|
||||
exec_erl "$ERLANG_NODE_OLD" $EJABBERD_OPTS -detached
|
||||
wait_status_node $ERLANG_NODE_OLD 0 30 2
|
||||
result=$?
|
||||
case $result in
|
||||
1) echo "There was a problem starting ejabberd with the old erlang node name. " \
|
||||
&& echo "Check for log errors in $EJABBERD_LOG_PATH" \
|
||||
&& exit $result;;
|
||||
*) :;;
|
||||
esac
|
||||
exec_other_command_node $ERLANG_NODE_OLD "status"
|
||||
|
||||
printe "Making backup of old database to file $OLDFILE ..."
|
||||
mkdir $SPOOL_DIR_BACKUP
|
||||
exec_other_command_node $ERLANG_NODE_OLD backup "$OLDFILE"
|
||||
|
||||
printe "Changing node name in new backup file $NEWFILE ..."
|
||||
exec_other_command_node $ERLANG_NODE_OLD mnesia_change_nodename "$ERLANG_NODE_OLD" "$ERLANG_NODE" "$OLDFILE" "$NEWFILE"
|
||||
|
||||
printe "Stopping old ejabberd..."
|
||||
exec_other_command_node $ERLANG_NODE_OLD "stop"
|
||||
wait_status_node $ERLANG_NODE_OLD 3 30 2 && stop_epmd
|
||||
|
||||
printe "Moving old mnesia spool files to backup subdirectory $SPOOL_DIR_BACKUP ..."
|
||||
mv $SPOOL_DIR/*.DAT $SPOOL_DIR_BACKUP
|
||||
mv $SPOOL_DIR/*.DCD $SPOOL_DIR_BACKUP
|
||||
mv $SPOOL_DIR/*.LOG $SPOOL_DIR_BACKUP
|
||||
|
||||
printe "Starting ejabberd with new node name $ERLANG_NODE ..."
|
||||
exec_erl "$ERLANG_NODE" $EJABBERD_OPTS -detached
|
||||
wait_status 0 30 2
|
||||
exec_other_command "status"
|
||||
|
||||
printe "Installing fallback of new mnesia..."
|
||||
exec_other_command install_fallback "$NEWFILE"
|
||||
|
||||
printe "Stopping new ejabberd..."
|
||||
exec_other_command "stop"
|
||||
wait_status 3 30 2 && stop_epmd
|
||||
|
||||
printe "Finished, now you can start ejabberd normally"
|
||||
}
|
||||
|
||||
# main
|
||||
case $1 in
|
||||
start)
|
||||
@@ -452,6 +563,9 @@ case $1 in
|
||||
set_dist_client
|
||||
wait_status 3 30 2 && stop_epmd # wait 30x2s before timeout
|
||||
;;
|
||||
mnesia_change)
|
||||
mnesia_change $2
|
||||
;;
|
||||
*)
|
||||
set_dist_client
|
||||
exec_other_command "$@"
|
||||
|
||||
+3
-3
@@ -16,17 +16,17 @@
|
||||
{elvis_style, function_naming_convention, disable},
|
||||
{elvis_style, god_modules, #{limit => 300}},
|
||||
{elvis_style, invalid_dynamic_call, disable},
|
||||
{elvis_style, macro_module_names, disable},
|
||||
{elvis_style, macro_names, disable},
|
||||
{elvis_style, max_function_arity, disable}, % #{max_arity => 15}},
|
||||
{elvis_style, nesting_level, disable},
|
||||
{elvis_style, no_author, disable},
|
||||
{elvis_style, no_boolean_in_comparison, disable},
|
||||
{elvis_style, no_catch_expressions, disable},
|
||||
{elvis_style, no_debug_call, disable},
|
||||
{elvis_style, no_if_expression, disable},
|
||||
{elvis_style, no_import, disable},
|
||||
{elvis_style, no_match_in_condition, disable},
|
||||
{elvis_style, no_nested_try_catch, disable},
|
||||
{elvis_style, no_operation_on_same_value, disable},
|
||||
{elvis_style, no_receive_without_timeout, disable},
|
||||
{elvis_style, no_single_clause_case, disable},
|
||||
{elvis_style, no_spec_with_records, disable},
|
||||
{elvis_style, no_throw, disable},
|
||||
|
||||
+31
-5
@@ -39,20 +39,46 @@
|
||||
-else.
|
||||
-include_lib("kernel/include/logger.hrl").
|
||||
|
||||
-define(CLEAD, "\e[1"). % bold
|
||||
-define(CMID, "\e[0"). % normal
|
||||
-define(CCLEAN, "\e[0m"). % clean
|
||||
|
||||
-define(CDEFAULT, ";49;95m"). % light magenta
|
||||
-define(CDEBUG, ";49;90m"). % dark gray
|
||||
-define(CINFO, ";49;92m"). % green
|
||||
-define(CWARNING, ";49;93m"). % light yellow
|
||||
-define(CERROR, ";49;91m"). % light magenta
|
||||
-define(CCRITICAL,";49;31m"). % light red
|
||||
|
||||
-define(DEBUG(Format, Args),
|
||||
begin ?LOG_DEBUG(Format, Args), ok end).
|
||||
begin ?LOG_DEBUG(Format, Args,
|
||||
#{clevel => ?CLEAD ++ ?CDEBUG,
|
||||
ctext => ?CMID ++ ?CDEBUG}),
|
||||
ok end).
|
||||
|
||||
-define(INFO_MSG(Format, Args),
|
||||
begin ?LOG_INFO(Format, Args), ok end).
|
||||
begin ?LOG_INFO(Format, Args,
|
||||
#{clevel => ?CLEAD ++ ?CINFO,
|
||||
ctext => ?CCLEAN}),
|
||||
ok end).
|
||||
|
||||
-define(WARNING_MSG(Format, Args),
|
||||
begin ?LOG_WARNING(Format, Args), ok end).
|
||||
begin ?LOG_WARNING(Format, Args,
|
||||
#{clevel => ?CLEAD ++ ?CWARNING,
|
||||
ctext => ?CMID ++ ?CWARNING}),
|
||||
ok end).
|
||||
|
||||
-define(ERROR_MSG(Format, Args),
|
||||
begin ?LOG_ERROR(Format, Args), ok end).
|
||||
begin ?LOG_ERROR(Format, Args,
|
||||
#{clevel => ?CLEAD ++ ?CERROR,
|
||||
ctext => ?CMID ++ ?CERROR}),
|
||||
ok end).
|
||||
|
||||
-define(CRITICAL_MSG(Format, Args),
|
||||
begin ?LOG_CRITICAL(Format, Args), ok end).
|
||||
begin ?LOG_CRITICAL(Format, Args,
|
||||
#{clevel => ?CLEAD++ ?CCRITICAL,
|
||||
ctext => ?CMID ++ ?CCRITICAL}),
|
||||
ok end).
|
||||
-endif.
|
||||
|
||||
%% Use only when trying to troubleshoot test problem with ExUnit
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
%%%----------------------------------------------------------------------
|
||||
%%%
|
||||
%%% ejabberd, Copyright (C) 2002-2025 ProcessOne
|
||||
%%%
|
||||
%%% This program is free software; you can redistribute it and/or
|
||||
%%% modify it under the terms of the GNU General Public License as
|
||||
%%% published by the Free Software Foundation; either version 2 of the
|
||||
%%% License, or (at your option) any later version.
|
||||
%%%
|
||||
%%% This program is distributed in the hope that it will be useful,
|
||||
%%% but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
%%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
%%% General Public License for more details.
|
||||
%%%
|
||||
%%% You should have received a copy of the GNU General Public License along
|
||||
%%% with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
%%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
%%%
|
||||
%%%----------------------------------------------------------------------
|
||||
|
||||
-define(MODULE_ANTISPAM, mod_antispam).
|
||||
|
||||
-type url() :: binary().
|
||||
-type filename() :: binary() | none | false.
|
||||
-type jid_set() :: sets:set(ljid()).
|
||||
-type url_set() :: sets:set(url()).
|
||||
|
||||
-define(DEFAULT_RTBL_DOMAINS_NODE, <<"spam_source_domains">>).
|
||||
|
||||
-record(rtbl_service,
|
||||
{host = none :: binary() | none,
|
||||
node = ?DEFAULT_RTBL_DOMAINS_NODE :: binary(),
|
||||
subscribed = false :: boolean(),
|
||||
retry_timer = undefined :: reference() | undefined}).
|
||||
|
||||
-type rtbl_service() :: #rtbl_service{}.
|
||||
@@ -21,8 +21,16 @@
|
||||
-record(room_version,
|
||||
{id :: binary(),
|
||||
%% use the same field names as in Synapse
|
||||
enforce_key_validity :: boolean(),
|
||||
special_case_aliases_auth :: boolean(),
|
||||
strict_canonicaljson :: boolean(),
|
||||
limit_notifications_power_levels :: boolean(),
|
||||
knock_join_rule :: boolean(),
|
||||
restricted_join_rule :: boolean(),
|
||||
restricted_join_rule_fix :: boolean(),
|
||||
knock_restricted_join_rule :: boolean(),
|
||||
enforce_int_power_levels :: boolean(),
|
||||
implicit_room_creator :: boolean(),
|
||||
updated_redaction_rules :: boolean()
|
||||
updated_redaction_rules :: boolean(),
|
||||
hydra :: boolean()
|
||||
}).
|
||||
|
||||
+509
-19
@@ -2,12 +2,12 @@
|
||||
.\" Title: ejabberd.yml
|
||||
.\" Author: [see the "AUTHOR" section]
|
||||
.\" Generator: DocBook XSL Stylesheets vsnapshot <http://docbook.sf.net/>
|
||||
.\" Date: 04/16/2025
|
||||
.\" Date: 08/22/2025
|
||||
.\" Manual: \ \&
|
||||
.\" Source: \ \&
|
||||
.\" Language: English
|
||||
.\"
|
||||
.TH "EJABBERD\&.YML" "5" "04/16/2025" "\ \&" "\ \&"
|
||||
.TH "EJABBERD\&.YML" "5" "08/22/2025" "\ \&" "\ \&"
|
||||
.\" -----------------------------------------------------------------
|
||||
.\" * Define some portability stuff
|
||||
.\" -----------------------------------------------------------------
|
||||
@@ -82,12 +82,12 @@ All options can be changed in runtime by running \fIejabberdctl reload\-config\f
|
||||
.sp
|
||||
Some options can be specified for particular virtual host(s) only using \fIhost_config\fR or \fIappend_host_config\fR options\&. Such options are called \fIlocal\fR\&. Examples are \fImodules\fR, \fIauth_method\fR and \fIdefault_db\fR\&. The options that cannot be defined per virtual host are called \fIglobal\fR\&. Examples are \fIloglevel\fR, \fIcertfiles\fR and \fIlisten\fR\&. It is a configuration mistake to put \fIglobal\fR options under \fIhost_config\fR or \fIappend_host_config\fR section \- ejabberd will refuse to load such configuration\&.
|
||||
.sp
|
||||
It is not recommended to write ejabberd\&.yml from scratch\&. Instead it is better to start from "default" configuration file available at https://github\&.com/processone/ejabberd/blob/25\&.04/ejabberd\&.yml\&.example\&. Once you get ejabberd running you can start changing configuration options to meet your requirements\&.
|
||||
It is not recommended to write ejabberd\&.yml from scratch\&. Instead it is better to start from "default" configuration file available at https://github\&.com/processone/ejabberd/blob/25\&.08/ejabberd\&.yml\&.example\&. Once you get ejabberd running you can start changing configuration options to meet your requirements\&.
|
||||
.sp
|
||||
Note that this document is intended to provide comprehensive description of all configuration options that can be consulted to understand the meaning of a particular option, its format and possible values\&. It will be quite hard to understand how to configure ejabberd by reading this document only \- for this purpose the reader is recommended to read online Configuration Guide available at https://docs\&.ejabberd\&.im/admin/configuration\&.
|
||||
.SH "TOP LEVEL OPTIONS"
|
||||
.sp
|
||||
This section describes top level options of ejabberd 25\&.04\&. The options that changed in this version are marked with 🟤\&.
|
||||
This section describes top level options of ejabberd 25\&.08\&. The options that changed in this version are marked with 🟤\&.
|
||||
.PP
|
||||
\fBaccess_rules\fR: \fI{AccessName: {allow|deny: ACLName|ACLDefinition}}\fR
|
||||
.RS 4
|
||||
@@ -461,6 +461,12 @@ option\&.
|
||||
.sp
|
||||
The default value is \fIplain\fR\&.
|
||||
.PP
|
||||
\fBauth_password_types_hidden_in_sasl1\fR: \fI[plain | scram_sha1 | scram_sha256 | scram_sha512]\fR
|
||||
.RS 4
|
||||
\fINote\fR
|
||||
about this option: added in 25\&.07\&. List of password types that should not be offered in SASL1 authenticatication\&. Because SASL1, unlike SASL2, can\(cqt have list of available mechanisms tailored to individual user, it\(cqs possible that offered mechanisms will not be compatible with stored password, especially if new password type was added recently\&. This option allows disabling offering some mechanisms in SASL1, to a time until new password type will be available for all users\&.
|
||||
.RE
|
||||
.PP
|
||||
\fBauth_scram_hash\fR: \fIsha | sha256 | sha512\fR
|
||||
.RS 4
|
||||
Hash algorithm that should be used to store password in
|
||||
@@ -697,13 +703,19 @@ A list of Erlang nodes to connect on ejabberd startup\&. This option is mostly i
|
||||
\fBdefault_db\fR: \fImnesia | sql\fR
|
||||
.RS 4
|
||||
\fIdatabase\&.md#default\-database|Default database\fR
|
||||
to store persistent data in ejabberd\&. Modules and other components (e\&.g\&. authentication) may have its own value\&. The default value is
|
||||
to store persistent data in ejabberd\&. Some components can be configured with specific toplevel options like
|
||||
\fIoauth_db_type\fR\&. Many modules can be configured with specific module options, usually named
|
||||
db_type\&. The default value is
|
||||
\fImnesia\fR\&.
|
||||
.RE
|
||||
.PP
|
||||
\fBdefault_ram_db\fR: \fImnesia | redis | sql\fR
|
||||
.RS 4
|
||||
Default volatile (in\-memory) storage for ejabberd\&. Modules and other components (e\&.g\&. session management) may have its own value\&. The default value is
|
||||
Default volatile (in\-memory) storage for ejabberd\&. Some components can be configured with specific toplevel options like
|
||||
\fIrouter_db_type\fR
|
||||
and
|
||||
\fIsm_db_type\fR\&. Some modules can be configured with specific module options, usually named
|
||||
ram_db_type\&. The default value is
|
||||
\fImnesia\fR\&.
|
||||
.RE
|
||||
.PP
|
||||
@@ -951,6 +963,34 @@ List of one or more
|
||||
option\&.
|
||||
.RE
|
||||
.PP
|
||||
\fBhosts_alias\fR: \fI{Alias: Host}\fR
|
||||
.RS 4
|
||||
\fINote\fR
|
||||
about this option: added in 25\&.07\&. Define aliases for existing vhosts managed by ejabberd\&. An alias may be a regexp expression\&. This option is only consulted by the
|
||||
\fIejabberd_http\fR
|
||||
listener\&.
|
||||
.sp
|
||||
\fBExample\fR:
|
||||
.sp
|
||||
.if n \{\
|
||||
.RS 4
|
||||
.\}
|
||||
.nf
|
||||
hosts:
|
||||
\- domain\&.tld
|
||||
\- example\&.org
|
||||
|
||||
hosts_alias:
|
||||
xmpp\&.domain\&.tld: domain\&.tld
|
||||
jabber\&.domain\&.tld: domain\&.tld
|
||||
mytest\&.net: example\&.org
|
||||
"exa*": example\&.org
|
||||
.fi
|
||||
.if n \{\
|
||||
.RE
|
||||
.\}
|
||||
.RE
|
||||
.PP
|
||||
\fBinclude_config_file\fR: \fI[Filename, \&.\&.\&.] | {Filename: Options}\fR
|
||||
.RS 4
|
||||
Read and
|
||||
@@ -1246,7 +1286,7 @@ This option can be used to tune tick time parameter of
|
||||
.RS 4
|
||||
Whether to use the
|
||||
\fIdatabase\&.md#default\-and\-new\-schemas|new SQL schema\fR\&. All schemas are located at
|
||||
https://github\&.com/processone/ejabberd/tree/25\&.04/sql\&. There are two schemas available\&. The default legacy schema stores one XMPP domain into one ejabberd database\&. The
|
||||
https://github\&.com/processone/ejabberd/tree/25\&.08/sql\&. There are two schemas available\&. The default legacy schema stores one XMPP domain into one ejabberd database\&. The
|
||||
\fInew\fR
|
||||
schema can handle several XMPP domains in a single ejabberd database\&. Using this
|
||||
\fInew\fR
|
||||
@@ -1513,6 +1553,30 @@ XMPP Core: section 7\&.7\&.2\&.2\&. The default value is
|
||||
\fIcloseold\fR\&.
|
||||
.RE
|
||||
.PP
|
||||
\fBrest_proxy\fR: \fIHost\fR
|
||||
.RS 4
|
||||
\fINote\fR
|
||||
about this option: added in 25\&.07\&. Address of a HTTP Connect proxy used by modules issuing rest calls (like ejabberd_oauth_rest)
|
||||
.RE
|
||||
.PP
|
||||
\fBrest_proxy_password\fR: \fIstring()\fR
|
||||
.RS 4
|
||||
\fINote\fR
|
||||
about this option: added in 25\&.07\&. Password used to authenticate to HTTP Connect proxy used by modules issuing rest calls (like ejabberd_oauth_rest)
|
||||
.RE
|
||||
.PP
|
||||
\fBrest_proxy_port\fR: \fI1\&.\&.65535\fR
|
||||
.RS 4
|
||||
\fINote\fR
|
||||
about this option: added in 25\&.07\&. Port of a HTTP Connect proxy used by modules issuing rest calls (like ejabberd_oauth_rest)
|
||||
.RE
|
||||
.PP
|
||||
\fBrest_proxy_username\fR: \fIstring()\fR
|
||||
.RS 4
|
||||
\fINote\fR
|
||||
about this option: added in 25\&.07\&. Username used to authenticate to HTTP Connect proxy used by modules issuing rest calls (like ejabberd_oauth_rest)
|
||||
.RE
|
||||
.PP
|
||||
\fBrouter_cache_life_time\fR: \fItimeout()\fR
|
||||
.RS 4
|
||||
Same as
|
||||
@@ -2043,9 +2107,13 @@ seconds\&.
|
||||
.RE
|
||||
.SH "MODULES"
|
||||
.sp
|
||||
This section describes modules options of ejabberd 25\&.04\&. The modules that changed in this version are marked with 🟤\&.
|
||||
This section describes modules options of ejabberd 25\&.08\&. The modules that changed in this version are marked with 🟤\&.
|
||||
.SS "mod_adhoc"
|
||||
.sp
|
||||
def:ad\-hoc command
|
||||
.sp
|
||||
: Command that can be executed by an XMPP client using XEP\-0050\&.
|
||||
.sp
|
||||
This module implements XEP\-0050: Ad\-Hoc Commands\&. It\(cqs an auxiliary module and is only needed by some of the other modules\&.
|
||||
.sp
|
||||
.it 1 an-trap
|
||||
@@ -2066,7 +2134,7 @@ Provide the Commands item in the Service Discovery\&. Default value:
|
||||
.sp
|
||||
\fINote\fR about this option: added in 25\&.03\&.
|
||||
.sp
|
||||
Execute API Commands in a XMPP client using XEP\-0050: Ad\-Hoc Commands\&. This module requires \fImod_adhoc\fR (to execute the commands), and recommends \fImod_disco\fR (to discover the commands)\&.
|
||||
Execute (def:API commands) in a XMPP client using XEP\-0050: Ad\-Hoc Commands\&. This module requires \fImod_adhoc\fR (to execute the commands), and recommends \fImod_disco\fR (to discover the commands)\&.
|
||||
.sp
|
||||
.it 1 an-trap
|
||||
.nr an-no-space-flag 1
|
||||
@@ -2078,7 +2146,7 @@ Execute API Commands in a XMPP client using XEP\-0050: Ad\-Hoc Commands\&. This
|
||||
.PP
|
||||
\fBdefault_version\fR: \fIinteger() | string()\fR
|
||||
.RS 4
|
||||
What API version to use\&. If setting an ejabberd version, it will use the latest API version that was available in that ejabberd version\&. For example, setting
|
||||
What API version to use\&. If setting an ejabberd version, it will use the latest API version that was available in that (def:c2s) ejabberd version\&. For example, setting
|
||||
\fI"24\&.06"\fR
|
||||
in this option implies
|
||||
\fI2\fR\&. The default value is the latest version\&.
|
||||
@@ -2199,6 +2267,8 @@ ejabberdctl srg_create g1 example\&.org "\*(AqGroup number 1\*(Aq" this_is_g1 g1
|
||||
.if n \{\
|
||||
.RE
|
||||
.\}
|
||||
.sp
|
||||
\fBAPI Tags:\fR \fI\&.\&./\&.\&./developer/ejabberd\-api/admin\-tags\&.md#accounts|accounts\fR, \fI\&.\&./\&.\&./developer/ejabberd\-api/admin\-tags\&.md#erlang|erlang\fR, \fI\&.\&./\&.\&./developer/ejabberd\-api/admin\-tags\&.md#last|last\fR, \fI\&.\&./\&.\&./developer/ejabberd\-api/admin\-tags\&.md#private|private\fR, \fI\&.\&./\&.\&./developer/ejabberd\-api/admin\-tags\&.md#purge|purge\fR, \fI\&.\&./\&.\&./developer/ejabberd\-api/admin\-tags\&.md#roster|roster\fR, \fI\&.\&./\&.\&./developer/ejabberd\-api/admin\-tags\&.md#session|session\fR, \fI\&.\&./\&.\&./developer/ejabberd\-api/admin\-tags\&.md#shared_roster_group|shared_roster_group\fR, \fI\&.\&./\&.\&./developer/ejabberd\-api/admin\-tags\&.md#stanza|stanza\fR, \fI\&.\&./\&.\&./developer/ejabberd\-api/admin\-tags\&.md#statistics|statistics\fR, \fI\&.\&./\&.\&./developer/ejabberd\-api/admin\-tags\&.md#vcard|vcard\fR
|
||||
.RE
|
||||
.SS "mod_admin_update_sql"
|
||||
.sp
|
||||
@@ -2393,6 +2463,134 @@ Same as top\-level
|
||||
option, but applied to this module only\&.
|
||||
.RE
|
||||
.RE
|
||||
.SS "mod_antispam"
|
||||
.sp
|
||||
\fINote\fR about this option: added in 25\&.07\&.
|
||||
.sp
|
||||
Filter spam messages and subscription requests received from remote servers based on Real\-Time Block Lists (RTBL), lists of known spammer JIDs and/or URLs mentioned in spam messages\&. Traffic classified as spam is rejected with an error (and an \fI[info]\fR message is logged) unless the sender is subscribed to the recipient\(cqs presence\&.
|
||||
.sp
|
||||
.it 1 an-trap
|
||||
.nr an-no-space-flag 1
|
||||
.nr an-break-flag 1
|
||||
.br
|
||||
.ps +1
|
||||
\fBAvailable options:\fR
|
||||
.RS 4
|
||||
.PP
|
||||
\fBaccess_spam\fR: \fIAccess\fR
|
||||
.RS 4
|
||||
Access rule that controls what accounts may receive spam messages\&. If the rule returns
|
||||
\fIallow\fR
|
||||
for a given recipient, spam messages aren\(cqt rejected for that recipient\&. The default value is
|
||||
\fInone\fR, which means that all recipients are subject to spam filtering verification\&.
|
||||
.RE
|
||||
.PP
|
||||
\fBcache_size\fR: \fIpos_integer()\fR
|
||||
.RS 4
|
||||
Maximum number of JIDs that will be cached due to sending spam URLs\&. If that limit is exceeded, the least recently used entries are removed from the cache\&. Setting this option to
|
||||
\fI0\fR
|
||||
disables the caching feature\&. Note that separate caches are used for each virtual host, and that the caches aren\(cqt distributed across cluster nodes\&. The default value is
|
||||
\fI10000\fR\&.
|
||||
.RE
|
||||
.PP
|
||||
\fBrtbl_services\fR: \fI[Service]\fR
|
||||
.RS 4
|
||||
Query a RTBL service to get domains to block, as provided by
|
||||
xmppbl\&.org\&. Please note right now this option only supports one service in that list\&. For blocking spam and abuse on MUC channels, please use
|
||||
\fImod_muc_rtbl\fR
|
||||
for now\&. If only the host is provided, the default node names will be assumed\&. If the node name is different than
|
||||
\fIspam_source_domains\fR, you can setup the custom node name with the option
|
||||
\fIspam_source_domains_node\fR\&. The default value is an empty list of services\&.
|
||||
.sp
|
||||
\fBExample\fR:
|
||||
.sp
|
||||
.if n \{\
|
||||
.RS 4
|
||||
.\}
|
||||
.nf
|
||||
rtbl_services:
|
||||
\- pubsub\&.server1\&.localhost:
|
||||
spam_source_domains_node: actual_custom_pubsub_node
|
||||
.fi
|
||||
.if n \{\
|
||||
.RE
|
||||
.\}
|
||||
.RE
|
||||
.PP
|
||||
\fBspam_domains_file\fR: \fInone | Path\fR
|
||||
.RS 4
|
||||
Path to a plain text file containing a list of known spam domains, one domain per line\&. Messages and subscription requests sent from one of the listed domains are classified as spam if sender is not in recipient\(cqs roster\&. This list of domains gets merged with the one retrieved by an RTBL host if any given\&. Use an absolute path, or the
|
||||
\fI@CONFIG_PATH@\fR
|
||||
predefined keyword
|
||||
if the file is available in the configuration directory\&. The default value is
|
||||
\fInone\fR\&.
|
||||
.RE
|
||||
.PP
|
||||
\fBspam_dump_file\fR: \fIfalse | true | Path\fR
|
||||
.RS 4
|
||||
Path to the file to store blocked messages\&. Use an absolute path, or the
|
||||
\fI@LOG_PATH@\fR
|
||||
predefined keyword
|
||||
to store logs in the same place that the other ejabberd log files\&. If set to
|
||||
\fIfalse\fR, it doesn\(cqt dump stanzas, which is the default\&. If set to
|
||||
\fItrue\fR, it stores in
|
||||
\fI"@LOG_PATH@/spam_dump_@HOST@\&.log"\fR\&.
|
||||
.RE
|
||||
.PP
|
||||
\fBspam_jids_file\fR: \fInone | Path\fR
|
||||
.RS 4
|
||||
Path to a plain text file containing a list of known spammer JIDs, one JID per line\&. Messages and subscription requests sent from one of the listed JIDs are classified as spam\&. Messages containing at least one of the listed JIDsare classified as spam as well\&. Furthermore, the sender\(cqs JID will be cached, so that future traffic originating from that JID will also be classified as spam\&. Use an absolute path, or the
|
||||
\fI@CONFIG_PATH@\fR
|
||||
predefined keyword
|
||||
if the file is available in the configuration directory\&. The default value is
|
||||
\fInone\fR\&.
|
||||
.RE
|
||||
.PP
|
||||
\fBspam_urls_file\fR: \fInone | Path\fR
|
||||
.RS 4
|
||||
Path to a plain text file containing a list of URLs known to be mentioned in spam message bodies\&. Messages containing at least one of the listed URLs are classified as spam\&. Furthermore, the sender\(cqs JID will be cached, so that future traffic originating from that JID will be classified as spam as well\&. Use an absolute path, or the
|
||||
\fI@CONFIG_PATH@\fR
|
||||
predefined keyword
|
||||
if the file is available in the configuration directory\&. The default value is
|
||||
\fInone\fR\&.
|
||||
.RE
|
||||
.PP
|
||||
\fBwhitelist_domains_file\fR: \fInone | Path\fR
|
||||
.RS 4
|
||||
Path to a file containing a list of domains to whitelist from being blocked, one per line\&. If either it is in
|
||||
\fIspam_domains_file\fR
|
||||
or more realistically in a domain sent by a RTBL host (see option
|
||||
\fIrtbl_services\fR) then this domain will be ignored and stanzas from there won\(cqt be blocked\&. Use an absolute path, or the
|
||||
\fI@CONFIG_PATH@\fR
|
||||
predefined keyword
|
||||
if the file is available in the configuration directory\&. The default value is
|
||||
\fInone\fR\&.
|
||||
.RE
|
||||
.RE
|
||||
.sp
|
||||
.it 1 an-trap
|
||||
.nr an-no-space-flag 1
|
||||
.nr an-break-flag 1
|
||||
.br
|
||||
.ps +1
|
||||
\fBExample:\fR
|
||||
.RS 4
|
||||
.sp
|
||||
.if n \{\
|
||||
.RS 4
|
||||
.\}
|
||||
.nf
|
||||
modules:
|
||||
mod_antispam:
|
||||
rtbl_services:
|
||||
\- xmppbl\&.org
|
||||
spam_jids_file: "@CONFIG_PATH@/spam_jids\&.txt"
|
||||
spam_dump_file: "@LOG_PATH@/spam/host\-@HOST@\&.log"
|
||||
.fi
|
||||
.if n \{\
|
||||
.RE
|
||||
.\}
|
||||
.RE
|
||||
.SS "mod_auth_fast"
|
||||
.sp
|
||||
\fINote\fR about this option: added in 24\&.12\&.
|
||||
@@ -2846,7 +3044,7 @@ modules:
|
||||
.RE
|
||||
.SS "mod_conversejs"
|
||||
.sp
|
||||
\fINote\fR about this option: added in 21\&.12 and improved in 22\&.05\&.
|
||||
\fINote\fR about this option: improved in 25\&.07\&.
|
||||
.sp
|
||||
This module serves a simple page for the Converse XMPP web browser client\&.
|
||||
.sp
|
||||
@@ -2856,6 +3054,8 @@ Make sure either \fImod_bosh\fR or \fIlisten\&.md#ejabberd_http_ws|ejabberd_http
|
||||
.sp
|
||||
When \fIconversejs_css\fR and \fIconversejs_script\fR are \fIauto\fR, by default they point to the public Converse client\&.
|
||||
.sp
|
||||
This module is available since ejabberd 21\&.12\&.
|
||||
.sp
|
||||
.it 1 an-trap
|
||||
.nr an-no-space-flag 1
|
||||
.nr an-break-flag 1
|
||||
@@ -2888,6 +3088,17 @@ about this option: added in 22\&.05\&. Specify additional options to be passed t
|
||||
Converse configuration\&. Only boolean, integer and string values are supported; lists are not supported\&.
|
||||
.RE
|
||||
.PP
|
||||
\fBconversejs_plugins\fR: \fI[Filename]\fR
|
||||
.RS 4
|
||||
List of additional local files to include as scripts in the homepage\&. Please make sure those files are available in the path specified in
|
||||
\fIconversejs_resources\fR
|
||||
option, in subdirectory
|
||||
\fIplugins/\fR\&. If using the public Converse client, then
|
||||
\fI"libsignal"\fR
|
||||
gets replaced with the URL of the public library\&. The default value is
|
||||
\fI[]\fR\&.
|
||||
.RE
|
||||
.PP
|
||||
\fBconversejs_resources\fR: \fIPath\fR
|
||||
.RS 4
|
||||
\fINote\fR
|
||||
@@ -2946,6 +3157,7 @@ listen:
|
||||
modules:
|
||||
mod_bosh: {}
|
||||
mod_conversejs:
|
||||
conversejs_plugins: ["libsignal"]
|
||||
websocket_url: "ws://@HOST@:5280/websocket"
|
||||
.fi
|
||||
.if n \{\
|
||||
@@ -2969,7 +3181,9 @@ listen:
|
||||
|
||||
modules:
|
||||
mod_conversejs:
|
||||
conversejs_resources: "/home/ejabberd/conversejs\-9\&.0\&.0/package/dist"
|
||||
conversejs_resources: "/home/ejabberd/conversejs\-x\&.y\&.z/package/dist"
|
||||
conversejs_plugins: ["libsignal\-protocol\&.min\&.js"]
|
||||
# File path is: /home/ejabberd/conversejs\-x\&.y\&.z/package/dist/plugins/libsignal\-protocol\&.min\&.js
|
||||
.fi
|
||||
.if n \{\
|
||||
.RE
|
||||
@@ -3232,6 +3446,8 @@ hour\&.
|
||||
The number of C2S authentication failures to trigger the IP ban\&. The default value is
|
||||
\fI20\fR\&.
|
||||
.RE
|
||||
.sp
|
||||
\fBAPI Tags:\fR \fI\&.\&./\&.\&./developer/ejabberd\-api/admin\-tags\&.md#accounts|accounts\fR
|
||||
.RE
|
||||
.SS "mod_host_meta"
|
||||
.sp
|
||||
@@ -3592,7 +3808,9 @@ A name of the service in the Service Discovery\&. The default value is
|
||||
.RS 4
|
||||
This option specifies the initial part of the PUT URLs used for file uploads\&. The keyword
|
||||
\fI@HOST@\fR
|
||||
is replaced with the virtual host name\&. NOTE: different virtual hosts cannot use the same PUT URL\&. The default value is
|
||||
is replaced with the virtual host name\&. And
|
||||
\fI@HOST_URL_ENCODE@\fR
|
||||
is replaced with the host name encoded for URL, useful when your virtual hosts contain non\-latin characters\&. NOTE: different virtual hosts cannot use the same PUT URL\&. The default value is
|
||||
\fI"https://@HOST@:5443/upload"\fR\&.
|
||||
.RE
|
||||
.PP
|
||||
@@ -3955,12 +4173,14 @@ option, but applied to this module only\&.
|
||||
When this option is disabled, for each individual subscriber a separate mucsub message is stored\&. With this option enabled, when a user fetches archive virtual mucsub, messages are generated from muc archives\&. The default value is
|
||||
\fIfalse\fR\&.
|
||||
.RE
|
||||
.sp
|
||||
\fBAPI Tags:\fR \fI\&.\&./\&.\&./developer/ejabberd\-api/admin\-tags\&.md#mam|mam\fR, \fI\&.\&./\&.\&./developer/ejabberd\-api/admin\-tags\&.md#purge|purge\fR
|
||||
.RE
|
||||
.SS "mod_matrix_gw"
|
||||
.SS "mod_matrix_gw 🟤"
|
||||
.sp
|
||||
\fINote\fR about this option: improved in 25\&.03\&.
|
||||
\fINote\fR about this option: improved in 25\&.08\&.
|
||||
.sp
|
||||
Matrix gateway\&. Erlang/OTP 25 or higher is required to use this module\&. This module is available since ejabberd 24\&.02\&.
|
||||
Matrix gateway\&. Supports room versions 9, 10 and 11 since ejabberd 25\&.03; room versions 4 and higher since ejabberd 25\&.07; room version 12 (hydra rooms) since ejabberd 25\&.08\&. Erlang/OTP 25 or higher is required to use this module\&. This module is available since ejabberd 24\&.02\&.
|
||||
.sp
|
||||
.it 1 an-trap
|
||||
.nr an-no-space-flag 1
|
||||
@@ -3990,6 +4210,13 @@ Value of the matrix signing key, in base64\&.
|
||||
Name of the matrix signing key\&.
|
||||
.RE
|
||||
.PP
|
||||
\fBleave_timeout\fR: \fIinteger()\fR
|
||||
.RS 4
|
||||
Delay in seconds between a user leaving a MUC room and sending
|
||||
\fIleave\fR
|
||||
Matrix event\&.
|
||||
.RE
|
||||
.PP
|
||||
\fBmatrix_domain\fR: \fIDomain\fR
|
||||
.RS 4
|
||||
Specify a domain in the Matrix federation\&. The keyword
|
||||
@@ -4014,6 +4241,11 @@ is the JID of the gateway service as set by the
|
||||
option\&. The default is
|
||||
\fIfalse\fR\&.
|
||||
.RE
|
||||
.PP
|
||||
\fBnotary_servers\fR: \fI[Server, \&.\&.\&.]\fR
|
||||
.RS 4
|
||||
A list of notary servers\&.
|
||||
.RE
|
||||
.RE
|
||||
.sp
|
||||
.it 1 an-trap
|
||||
@@ -4978,6 +5210,8 @@ about this option: added in 22\&.05\&. How many users can be subscribed to a roo
|
||||
API\&. The default value is
|
||||
\fI50\fR\&.
|
||||
.RE
|
||||
.sp
|
||||
\fBAPI Tags:\fR \fI\&.\&./\&.\&./developer/ejabberd\-api/admin\-tags\&.md#muc|muc\fR, \fI\&.\&./\&.\&./developer/ejabberd\-api/admin\-tags\&.md#muc_room|muc_room\fR, \fI\&.\&./\&.\&./developer/ejabberd\-api/admin\-tags\&.md#muc_sub|muc_sub\fR
|
||||
.RE
|
||||
.SS "mod_muc_log"
|
||||
.sp
|
||||
@@ -5555,6 +5789,8 @@ modules:
|
||||
.if n \{\
|
||||
.RE
|
||||
.\}
|
||||
.sp
|
||||
\fBAPI Tags:\fR \fI\&.\&./\&.\&./developer/ejabberd\-api/admin\-tags\&.md#offline|offline\fR
|
||||
.RE
|
||||
.SS "mod_ping"
|
||||
.sp
|
||||
@@ -5798,6 +6034,8 @@ Same as top\-level
|
||||
\fIuse_cache\fR
|
||||
option, but applied to this module only\&.
|
||||
.RE
|
||||
.sp
|
||||
\fBAPI Tags:\fR \fI\&.\&./\&.\&./developer/ejabberd\-api/admin\-tags\&.md#private|private\fR
|
||||
.RE
|
||||
.SS "mod_privilege"
|
||||
.sp
|
||||
@@ -5965,6 +6203,199 @@ modules:
|
||||
.RE
|
||||
.\}
|
||||
.RE
|
||||
.SS "mod_providers 🟤"
|
||||
.sp
|
||||
\fINote\fR about this option: added in 25\&.08\&.
|
||||
.sp
|
||||
This module serves JSON provider files API v2 as described by XMPP Providers\&.
|
||||
.sp
|
||||
It attempts to fill some properties gathering values automatically from your existing ejabberd configuration\&. Try enabling the module, check what values are displayed, and then customize using the options\&.
|
||||
.sp
|
||||
To use this module, in addition to adding it to the \fImodules\fR section, you must also enable it in \fIlisten\fR → \fIejabberd_http\fR → \fIlisten\-options\&.md#request_handlers|request_handlers\fR\&. Notice you should set in \fIlisten\&.md#ejabberd_http|ejabberd_http\fR the option \fIlisten\-options\&.md#tls|tls\fR enabled\&.
|
||||
.sp
|
||||
.it 1 an-trap
|
||||
.nr an-no-space-flag 1
|
||||
.nr an-break-flag 1
|
||||
.br
|
||||
.ps +1
|
||||
\fBAvailable options:\fR
|
||||
.RS 4
|
||||
.PP
|
||||
\fBalternativeJids\fR: \fI[string()]\fR
|
||||
.RS 4
|
||||
List of JIDs (XMPP server domains) a provider offers for registration other than its main JID\&. The default value is
|
||||
\fI[]\fR\&.
|
||||
.RE
|
||||
.PP
|
||||
\fBbusFactor\fR: \fIinteger()\fR
|
||||
.RS 4
|
||||
Bus factor of the XMPP service (i\&.e\&., the minimum number of team members that the service could not survive losing) or
|
||||
\fI\-1\fR
|
||||
for n/a\&. The default value is
|
||||
\fI\-1\fR\&.
|
||||
.RE
|
||||
.PP
|
||||
\fBfreeOfCharge\fR: \fItrue | false\fR
|
||||
.RS 4
|
||||
Whether the XMPP service can be used for free\&. The default value is
|
||||
\fIfalse\fR\&.
|
||||
.RE
|
||||
.PP
|
||||
\fBlanguages\fR: \fI[string()]\fR
|
||||
.RS 4
|
||||
List of language codes that your pages are available\&. Some options define URL where the keyword
|
||||
\fI@LANGUAGE_URL@\fR
|
||||
will be replaced with each of those language codes\&. The default value is a list with the language set in the option
|
||||
\fIlanguage\fR, for example:
|
||||
\fI[en]\fR\&.
|
||||
.RE
|
||||
.PP
|
||||
\fBlegalNotice\fR: \fIstring()\fR
|
||||
.RS 4
|
||||
Legal notice web page (per language)\&. The keyword
|
||||
\fI@LANGUAGE_URL@\fR
|
||||
is replaced with each language\&. The default value is
|
||||
\fI""\fR\&.
|
||||
.RE
|
||||
.PP
|
||||
\fBmaximumHttpFileUploadStorageTime\fR: \fIinteger()\fR
|
||||
.RS 4
|
||||
Maximum storage duration of each shared file (number in days,
|
||||
\fI0\fR
|
||||
for no limit or
|
||||
\fI\-1\fR
|
||||
for less than 1 day)\&. The default value is the same as option
|
||||
\fImax_days\fR
|
||||
from module
|
||||
\fImod_http_upload_quota\fR, or
|
||||
\fI0\fR
|
||||
otherwise\&.
|
||||
.RE
|
||||
.PP
|
||||
\fBmaximumHttpFileUploadTotalSize\fR: \fIinteger()\fR
|
||||
.RS 4
|
||||
Maximum size of all shared files in total per user (number in megabytes (MB),
|
||||
\fI0\fR
|
||||
for no limit or
|
||||
\fI\-1\fR
|
||||
for less than 1 MB)\&. Attention: MB is used instead of MiB (e\&.g\&., 104,857,600 bytes = 100 MiB H 104 MB)\&. This property is not about the maximum size of each shared file, which is already retrieved via XMPP\&. The default value is the value of the shaper value of option
|
||||
\fIaccess_hard_quota\fR
|
||||
from module
|
||||
\fImod_http_upload_quota\fR, or
|
||||
\fI0\fR
|
||||
otherwise\&.
|
||||
.RE
|
||||
.PP
|
||||
\fBmaximumMessageArchiveManagementStorageTime\fR: \fIinteger()\fR
|
||||
.RS 4
|
||||
Maximum storage duration of each exchanged message (number in days,
|
||||
\fI0\fR
|
||||
for no limit or
|
||||
\fI\-1\fR
|
||||
for less than 1 day)\&. The default value is
|
||||
\fI0\fR\&.
|
||||
.RE
|
||||
.PP
|
||||
\fBorganization\fR: \fIstring()\fR
|
||||
.RS 4
|
||||
Type of organization providing the XMPP service\&. Allowed values are:
|
||||
\fIcompany\fR,
|
||||
\fI"commercial person"\fR,
|
||||
\fI"private person"\fR,
|
||||
\fIgovernmental\fR,
|
||||
\fI"non\-governmental"\fR
|
||||
or
|
||||
\fI""\fR\&. The default value is
|
||||
\fI""\fR\&.
|
||||
.RE
|
||||
.PP
|
||||
\fBpasswordReset\fR: \fIstring()\fR
|
||||
.RS 4
|
||||
Password reset web page (per language) used for an automatic password reset (e\&.g\&., via email) or describing how to manually reset a password (e\&.g\&., by contacting the provider)\&. The keyword
|
||||
\fI@LANGUAGE_URL@\fR
|
||||
is replaced with each language\&. The default value is an URL built automatically if
|
||||
\fImod_register_web\fR
|
||||
is configured as a
|
||||
\fIrequest_handler\fR, or
|
||||
\fI""\fR
|
||||
otherwise\&.
|
||||
.RE
|
||||
.PP
|
||||
\fBprofessionalHosting\fR: \fItrue | false\fR
|
||||
.RS 4
|
||||
Whether the XMPP server is hosted with good internet connection speed, uninterruptible power supply, access protection and regular backups\&. The default value is
|
||||
\fIfalse\fR\&.
|
||||
.RE
|
||||
.PP
|
||||
\fBserverLocations\fR: \fI[string()]\fR
|
||||
.RS 4
|
||||
List of language codes of Server/Backup locations\&. The default value is an empty list:
|
||||
\fI[]\fR\&.
|
||||
.RE
|
||||
.PP
|
||||
\fBserverTesting\fR: \fItrue | false\fR
|
||||
.RS 4
|
||||
Whether tests against the provider\(cqs server are allowed (e\&.g\&., certificate checks and uptime monitoring)\&. The default value is
|
||||
\fIfalse\fR\&.
|
||||
.RE
|
||||
.PP
|
||||
\fBsince\fR: \fIstring()\fR
|
||||
.RS 4
|
||||
Date since the XMPP service is available\&. The default value is an empty string:
|
||||
\fI""\fR\&.
|
||||
.RE
|
||||
.PP
|
||||
\fBwebsite\fR: \fIstring()\fR
|
||||
.RS 4
|
||||
Provider website\&. The keyword
|
||||
\fI@LANGUAGE_URL@\fR
|
||||
is replaced with each language\&. The default value is
|
||||
\fI""\fR\&.
|
||||
.RE
|
||||
.RE
|
||||
.sp
|
||||
.it 1 an-trap
|
||||
.nr an-no-space-flag 1
|
||||
.nr an-break-flag 1
|
||||
.br
|
||||
.ps +1
|
||||
\fBExample:\fR
|
||||
.RS 4
|
||||
.sp
|
||||
.if n \{\
|
||||
.RS 4
|
||||
.\}
|
||||
.nf
|
||||
listen:
|
||||
\-
|
||||
port: 443
|
||||
module: ejabberd_http
|
||||
tls: true
|
||||
request_handlers:
|
||||
/\&.well\-known/xmpp\-provider\-v2\&.json: mod_providers
|
||||
|
||||
modules:
|
||||
mod_providers:
|
||||
alternativeJids: ["example1\&.com", "example2\&.com"]
|
||||
busFactor: 1
|
||||
freeOfCharge: true
|
||||
languages: [ag, ao, bg, en]
|
||||
legalNotice: "http://@HOST@/legal/@LANGUAGE_URL@/"
|
||||
maximumHttpFileUploadStorageTime: 0
|
||||
maximumHttpFileUploadTotalSize: 0
|
||||
maximumMessageArchiveManagementStorageTime: 0
|
||||
organization: "non\-governmental"
|
||||
passwordReset: "http://@HOST@/reset/@LANGUAGE_URL@/"
|
||||
professionalHosting: true
|
||||
serverLocations: [ao, bg]
|
||||
serverTesting: true
|
||||
since: "2025\-12\-31"
|
||||
website: "http://@HOST@/website/@LANGUAGE_URL@/"
|
||||
.fi
|
||||
.if n \{\
|
||||
.RE
|
||||
.\}
|
||||
.RE
|
||||
.SS "mod_proxy65"
|
||||
.sp
|
||||
This module implements XEP\-0065: SOCKS5 Bytestreams\&. It allows ejabberd to act as a file transfer proxy between two XMPP clients\&.
|
||||
@@ -6426,6 +6857,61 @@ modules:
|
||||
.if n \{\
|
||||
.RE
|
||||
.\}
|
||||
.sp
|
||||
\fBAPI Tags:\fR \fI\&.\&./\&.\&./developer/ejabberd\-api/admin\-tags\&.md#purge|purge\fR
|
||||
.RE
|
||||
.SS "mod_pubsub_serverinfo"
|
||||
.sp
|
||||
\fINote\fR about this option: added in 25\&.07\&.
|
||||
.sp
|
||||
This module adds support for XEP\-0485: PubSub Server Information to expose S2S information over the Pub/Sub service\&.
|
||||
.sp
|
||||
Active S2S connections are published to a local PubSub node\&. Currently the node name is hardcoded as \fI"serverinfo"\fR\&.
|
||||
.sp
|
||||
Connections that support this feature are exposed with their domain names, otherwise they are shown as anonymous nodes\&. At startup a list of well known public servers is fetched\&. Those are not shown as anonymous even if they don\(cqt support this feature\&.
|
||||
.sp
|
||||
Please note that the module only shows S2S connections established while the module is running\&. If you install the module at runtime, run \fIstop_s2s_connections\fR API or restart ejabberd to force S2S reconnections that the module will detect and publish\&.
|
||||
.sp
|
||||
This module depends on \fImod_pubsub\fR and \fImod_disco\fR\&.
|
||||
.sp
|
||||
.it 1 an-trap
|
||||
.nr an-no-space-flag 1
|
||||
.nr an-break-flag 1
|
||||
.br
|
||||
.ps +1
|
||||
\fBAvailable options:\fR
|
||||
.RS 4
|
||||
.PP
|
||||
\fBpubsub_host\fR: \fIundefined | string()\fR
|
||||
.RS 4
|
||||
Use this local PubSub host to advertise S2S connections\&. This must be a host local to this service handled by
|
||||
\fImod_pubsub\fR\&. This option is only needed if your configuration has more than one host in mod_pubsub\(cqs
|
||||
\fIhosts\fR
|
||||
option\&. The default value is the first host defined in mod_pubsub
|
||||
\fIhosts\fR
|
||||
option\&.
|
||||
.RE
|
||||
.RE
|
||||
.sp
|
||||
.it 1 an-trap
|
||||
.nr an-no-space-flag 1
|
||||
.nr an-break-flag 1
|
||||
.br
|
||||
.ps +1
|
||||
\fBExample:\fR
|
||||
.RS 4
|
||||
.sp
|
||||
.if n \{\
|
||||
.RS 4
|
||||
.\}
|
||||
.nf
|
||||
modules:
|
||||
mod_pubsub_serverinfo:
|
||||
pubsub_host: custom\&.pubsub\&.domain\&.local
|
||||
.fi
|
||||
.if n \{\
|
||||
.RE
|
||||
.\}
|
||||
.RE
|
||||
.SS "mod_push"
|
||||
.sp
|
||||
@@ -6496,6 +6982,8 @@ Same as top\-level
|
||||
\fIuse_cache\fR
|
||||
option, but applied to this module only\&.
|
||||
.RE
|
||||
.sp
|
||||
\fBAPI Tags:\fR \fI\&.\&./\&.\&./developer/ejabberd\-api/admin\-tags\&.md#purge|purge\fR
|
||||
.RE
|
||||
.SS "mod_push_keepalive"
|
||||
.sp
|
||||
@@ -6845,6 +7333,8 @@ modules:
|
||||
.if n \{\
|
||||
.RE
|
||||
.\}
|
||||
.sp
|
||||
\fBAPI Tags:\fR \fI\&.\&./\&.\&./developer/ejabberd\-api/admin\-tags\&.md#roster|roster\fR
|
||||
.RE
|
||||
.SS "mod_s2s_bidi"
|
||||
.sp
|
||||
@@ -8372,7 +8862,7 @@ Should the operating system be revealed or not\&. The default value is
|
||||
.RE
|
||||
.SH "LISTENERS"
|
||||
.sp
|
||||
This section describes listeners options of ejabberd 25\&.04\&.
|
||||
This section describes listeners options of ejabberd 25\&.08\&.
|
||||
.sp
|
||||
TODO
|
||||
.SH "AUTHOR"
|
||||
@@ -8380,13 +8870,13 @@ TODO
|
||||
ProcessOne\&.
|
||||
.SH "VERSION"
|
||||
.sp
|
||||
This document describes the configuration file of ejabberd 25\&.04\&. Configuration options of other ejabberd versions may differ significantly\&.
|
||||
This document describes the configuration file of ejabberd 25\&.08\&. Configuration options of other ejabberd versions may differ significantly\&.
|
||||
.SH "REPORTING BUGS"
|
||||
.sp
|
||||
Report bugs to https://github\&.com/processone/ejabberd/issues
|
||||
.SH "SEE ALSO"
|
||||
.sp
|
||||
Default configuration file: https://github\&.com/processone/ejabberd/blob/25\&.04/ejabberd\&.yml\&.example
|
||||
Default configuration file: https://github\&.com/processone/ejabberd/blob/25\&.08/ejabberd\&.yml\&.example
|
||||
.sp
|
||||
Main site: https://ejabberd\&.im
|
||||
.sp
|
||||
|
||||
@@ -120,18 +120,18 @@ defmodule Ejabberd.MixProject do
|
||||
{:dialyxir, "~> 1.2", only: [:test], runtime: false},
|
||||
{:eimp, "~> 1.0"},
|
||||
{:ex_doc, "~> 0.31", only: [:edoc], runtime: false},
|
||||
{:fast_tls, "~> 1.1.22"},
|
||||
{:fast_xml, "~> 1.1.53"},
|
||||
{:fast_tls, "~> 1.1.24"},
|
||||
{:fast_xml, "~> 1.1.56"},
|
||||
{:fast_yaml, "~> 1.0"},
|
||||
{:idna, "~> 6.0"},
|
||||
{:mqtree, "~> 1.0"},
|
||||
{:p1_acme, "~> 1.0"},
|
||||
{:p1_acme, ">= 1.0.28"},
|
||||
{:p1_oauth2, "~> 0.6"},
|
||||
{:p1_utils, "~> 1.0"},
|
||||
{:pkix, "~> 1.0"},
|
||||
{:stringprep, ">= 1.0.26"},
|
||||
{:xmpp, "~> 1.10.0"},
|
||||
{:yconf, ">= 1.0.18"}]
|
||||
{:xmpp, ">= 1.11.1"},
|
||||
{:yconf, ">= 1.0.21"}]
|
||||
++ cond_deps()
|
||||
end
|
||||
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
%{
|
||||
"base64url": {:hex, :base64url, "1.0.1", "f8c7f2da04ca9a5d0f5f50258f055e1d699f0e8bf4cfdb30b750865368403cf6", [:rebar3], [], "hexpm", "f9b3add4731a02a9b0410398b475b33e7566a695365237a6bdee1bb447719f5c"},
|
||||
"cache_tab": {:hex, :cache_tab, "1.0.31", "e4097b50a6f373ab1e0a5f01bab0bef6626771a4cd6c93404ed6d54810e11fbc", [:rebar3], [{:p1_utils, "1.0.26", [hex: :p1_utils, repo: "hexpm", optional: false]}], "hexpm", "8582b60a4a09b247ef86355ba9e07fce9e11edc0345a775c9171f971c72b6351"},
|
||||
"dialyxir": {:hex, :dialyxir, "1.4.5", "ca1571ac18e0f88d4ab245f0b60fa31ff1b12cbae2b11bd25d207f865e8ae78a", [:mix], [{:erlex, ">= 0.2.7", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "b0fb08bb8107c750db5c0b324fa2df5ceaa0f9307690ee3c1f6ba5b9eb5d35c3"},
|
||||
"cache_tab": {:hex, :cache_tab, "1.0.33", "e2542afb34f17ee3ca19d2b0f546a074922c2b99fb6b2acfb38160d7d0336ec3", [:rebar3], [{:p1_utils, "1.0.28", [hex: :p1_utils, repo: "hexpm", optional: false]}], "hexpm", "4258009eb050b22aabe0c848e230bba58401a6895c58c2ff74dfb635e3c35900"},
|
||||
"dialyxir": {:hex, :dialyxir, "1.4.6", "7cca478334bf8307e968664343cbdb432ee95b4b68a9cba95bdabb0ad5bdfd9a", [:mix], [{:erlex, ">= 0.2.7", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "8cf5615c5cd4c2da6c501faae642839c8405b49f8aa057ad4ae401cb808ef64d"},
|
||||
"earmark_parser": {:hex, :earmark_parser, "1.4.44", "f20830dd6b5c77afe2b063777ddbbff09f9759396500cdbe7523efd58d7a339c", [:mix], [], "hexpm", "4778ac752b4701a5599215f7030989c989ffdc4f6df457c5f36938cc2d2a2750"},
|
||||
"eimp": {:hex, :eimp, "1.0.24", "853db317ba394d479d2f1181e0b0135a3cd3c2c7eb48ff6cfb035a28dd354b14", [:rebar3], [{:p1_utils, "~> 1.0.25", [hex: :p1_utils, repo: "hexpm", optional: false]}], "hexpm", "7d61432eb8a45659c0be475f44e75eeb651743aa64a1de8adf785cdad81961ad"},
|
||||
"eimp": {:hex, :eimp, "1.0.26", "c0b05f32e35629c4d9bcfb832ff879a92b0f92b19844bc7835e0a45635f2899a", [:rebar3], [{:p1_utils, "~> 1.0.25", [hex: :p1_utils, repo: "hexpm", optional: false]}], "hexpm", "d96d4e8572b9dfc40f271e47f0cb1d8849373bc98a21223268781765ed52044c"},
|
||||
"epam": {:hex, :epam, "1.0.14", "aa0b85d27f4ef3a756ae995179df952a0721237e83c6b79d644347b75016681a", [:rebar3], [], "hexpm", "2f3449e72885a72a6c2a843f561add0fc2f70d7a21f61456930a547473d4d989"},
|
||||
"eredis": {:hex, :eredis, "1.7.1", "39e31aa02adcd651c657f39aafd4d31a9b2f63c6c700dc9cece98d4bc3c897ab", [:mix, :rebar3], [], "hexpm", "7c2b54c566fed55feef3341ca79b0100a6348fd3f162184b7ed5118d258c3cc1"},
|
||||
"erlex": {:hex, :erlex, "0.2.7", "810e8725f96ab74d17aac676e748627a07bc87eb950d2b83acd29dc047a30595", [:mix], [], "hexpm", "3ed95f79d1a844c3f6bf0cea61e0d5612a42ce56da9c03f01df538685365efb0"},
|
||||
"esip": {:hex, :esip, "1.0.57", "4b14e4832d08b9ffc10d855b5d10b3083232b1d53deb4c046679496ce85569c4", [:rebar3], [{:fast_tls, "1.1.22", [hex: :fast_tls, repo: "hexpm", optional: false]}, {:p1_utils, "1.0.26", [hex: :p1_utils, repo: "hexpm", optional: false]}, {:stun, "1.2.17", [hex: :stun, repo: "hexpm", optional: false]}], "hexpm", "19c357e1817b1e04792ef359bf900400f3e6d0e5ade929fd72f88ea9b44af2ed"},
|
||||
"ex_doc": {:hex, :ex_doc, "0.37.3", "f7816881a443cd77872b7d6118e8a55f547f49903aef8747dbcb345a75b462f9", [:mix], [{:earmark_parser, "~> 1.4.42", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "e6aebca7156e7c29b5da4daa17f6361205b2ae5f26e5c7d8ca0d3f7e18972233"},
|
||||
"esip": {:hex, :esip, "1.0.59", "eb202f8c62928193588091dfedbc545fe3274c34ecd209961f86dcb6c9ebce88", [:rebar3], [{:fast_tls, "1.1.25", [hex: :fast_tls, repo: "hexpm", optional: false]}, {:p1_utils, "1.0.28", [hex: :p1_utils, repo: "hexpm", optional: false]}, {:stun, "1.2.21", [hex: :stun, repo: "hexpm", optional: false]}], "hexpm", "0bdf2e3c349dc0b144f173150329e675c6a51ac473d7a0b2e362245faad3fbe6"},
|
||||
"ex_doc": {:hex, :ex_doc, "0.38.2", "504d25eef296b4dec3b8e33e810bc8b5344d565998cd83914ffe1b8503737c02", [:mix], [{:earmark_parser, "~> 1.4.44", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "732f2d972e42c116a70802f9898c51b54916e542cc50968ac6980512ec90f42b"},
|
||||
"exsync": {:hex, :exsync, "0.4.1", "0a14fe4bfcb80a509d8a0856be3dd070fffe619b9ba90fec13c58b316c176594", [:mix], [{:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}], "hexpm", "cefb22aa805ec97ffc5b75a4e1dc54bcaf781e8b32564bf74abbe5803d1b5178"},
|
||||
"ezlib": {:hex, :ezlib, "1.0.13", "3c7f62862850a241159c10b218ecf580bce54d0890601b65144dacc2633be2b0", [:rebar3], [{:p1_utils, "1.0.26", [hex: :p1_utils, repo: "hexpm", optional: false]}], "hexpm", "9ee62ab3f8ed55a0fd11a9569fcb8e458683f95575417272192b069f092abfbb"},
|
||||
"fast_tls": {:hex, :fast_tls, "1.1.22", "44356b256afad4399c2fc5059a3066669dafd8bd4e4e796c9c1cf8910ddd265e", [:rebar3], [{:p1_utils, "1.0.26", [hex: :p1_utils, repo: "hexpm", optional: false]}], "hexpm", "e65779aefb7ab15c4755230fef8077e687d20cc5a3984a5974f9f657e8e2485b"},
|
||||
"fast_xml": {:hex, :fast_xml, "1.1.55", "ace020f2521f2a484ac8467d2822af85534a346e2aae03ffcbc34f29318befaf", [:rebar3], [{:p1_utils, "1.0.26", [hex: :p1_utils, repo: "hexpm", optional: false]}], "hexpm", "83f3e23a780ed5f567cdec73953f06c95b838d709dbfa86b59a98a8d23c99f85"},
|
||||
"fast_yaml": {:hex, :fast_yaml, "1.0.37", "f71d472fbf787ccd161b914d1eb486116a0f4f2e835337a378fbd31b59d2e74b", [:rebar3], [{:p1_utils, "1.0.26", [hex: :p1_utils, repo: "hexpm", optional: false]}], "hexpm", "8de868721bf7e2172414f7d3148ede0f3c922b496455cd625dd5c4429515a769"},
|
||||
"ezlib": {:hex, :ezlib, "1.0.15", "d74f5df191784744726a5b1ae9062522c606334f11086363385eb3b772d91357", [:rebar3], [{:p1_utils, "1.0.28", [hex: :p1_utils, repo: "hexpm", optional: false]}], "hexpm", "dd14ba6c12521af5cfe6923e73e3d545f4a0897dc66bfab5287fbb7ae3962eab"},
|
||||
"fast_tls": {:hex, :fast_tls, "1.1.25", "da8ed6f05a2452121b087158b17234749f36704c1f2b74dc51db99a1e27ed5e8", [:rebar3], [{:p1_utils, "~> 1.0.26", [hex: :p1_utils, repo: "hexpm", optional: false]}], "hexpm", "59e183b5740e670e02b8aa6be673b5e7779e5fe5bfcc679fe2d4993d1949a821"},
|
||||
"fast_xml": {:hex, :fast_xml, "1.1.57", "31efc0f9bceda92069704f7a25830407da5dc3dad1272b810d6f2e13e73cc11a", [:rebar3], [{:p1_utils, "1.0.28", [hex: :p1_utils, repo: "hexpm", optional: false]}], "hexpm", "eec34e90adacafe467d5ddab635a014ded73b98b4061554b2d1972173d929c39"},
|
||||
"fast_yaml": {:hex, :fast_yaml, "1.0.39", "2e71168091949bab0e5f583b340a99072b4d22d93eb86624e7850a12b1517be4", [:rebar3], [{:p1_utils, "1.0.28", [hex: :p1_utils, repo: "hexpm", optional: false]}], "hexpm", "24c7b9ab9e2b9269d64e45f4a2a1280966adb17d31e63365cfd3ee277fb0a78d"},
|
||||
"file_system": {:hex, :file_system, "1.1.0", "08d232062284546c6c34426997dd7ef6ec9f8bbd090eb91780283c9016840e8f", [:mix], [], "hexpm", "bfcf81244f416871f2a2e15c1b515287faa5db9c6bcf290222206d120b3d43f6"},
|
||||
"idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"},
|
||||
"jiffy": {:hex, :jiffy, "1.1.2", "a9b6c9a7ec268e7cf493d028f0a4c9144f59ccb878b1afe42841597800840a1b", [:rebar3], [], "hexpm", "bb61bc42a720bbd33cb09a410e48bb79a61012c74cb8b3e75f26d988485cf381"},
|
||||
@@ -22,18 +22,18 @@
|
||||
"makeup": {:hex, :makeup, "1.2.1", "e90ac1c65589ef354378def3ba19d401e739ee7ee06fb47f94c687016e3713d1", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "d36484867b0bae0fea568d10131197a4c2e47056a6fbe84922bf6ba71c8d17ce"},
|
||||
"makeup_elixir": {:hex, :makeup_elixir, "1.0.1", "e928a4f984e795e41e3abd27bfc09f51db16ab8ba1aebdba2b3a575437efafc2", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "7284900d412a3e5cfd97fdaed4f5ed389b8f2b4cb49efc0eb3bd10e2febf9507"},
|
||||
"makeup_erlang": {:hex, :makeup_erlang, "1.0.2", "03e1804074b3aa64d5fad7aa64601ed0fb395337b982d9bcf04029d68d51b6a7", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "af33ff7ef368d5893e4a267933e7744e46ce3cf1f61e2dccf53a111ed3aa3727"},
|
||||
"mqtree": {:hex, :mqtree, "1.0.17", "82f54b8f2d22b4445db1d6cccb7fe9ead049d61410c29e32475f3ceb3ee62a89", [:rebar3], [{:p1_utils, "1.0.26", [hex: :p1_utils, repo: "hexpm", optional: false]}], "hexpm", "5fe8b7cf8fbc4783d0fceb94654ac2bbf3242a58cd0397d249ded8ae021be2a3"},
|
||||
"mqtree": {:hex, :mqtree, "1.0.19", "d769c25f898810725fc7db0dbffe5f72098647048b1be2e6d772f1c2f31d8476", [:rebar3], [{:p1_utils, "1.0.28", [hex: :p1_utils, repo: "hexpm", optional: false]}], "hexpm", "c81065715c49a1882812f80a5ae2d842e80dd3f2d130530df35990248bf8ce3c"},
|
||||
"nimble_parsec": {:hex, :nimble_parsec, "1.4.2", "8efba0122db06df95bfaa78f791344a89352ba04baedd3849593bfce4d0dc1c6", [:mix], [], "hexpm", "4b21398942dda052b403bbe1da991ccd03a053668d147d53fb8c4e0efe09c973"},
|
||||
"p1_acme": {:hex, :p1_acme, "1.0.25", "db91f0d6c193cd1d5c0b0fa3939a898dbf56a6075db4347cde26e802715de50c", [:rebar3], [{:base64url, "~> 1.0", [hex: :base64url, repo: "hexpm", optional: false]}, {:idna, "~> 6.0", [hex: :idna, repo: "hexpm", optional: false]}, {:jiffy, "~> 1.1.1", [hex: :jiffy, repo: "hexpm", optional: false]}, {:jose, "~> 1.11.10", [hex: :jose, repo: "hexpm", optional: false]}, {:yconf, "~> 1.0.17", [hex: :yconf, repo: "hexpm", optional: false]}], "hexpm", "a7b55b47495ddb4f98a15e65451ec3ad43f4637b955c74cd695d98e6a645d08c"},
|
||||
"p1_acme": {:hex, :p1_acme, "1.0.28", "64d9c17f5412aa92d75b29206b2b984d734a4fe1b7eacb66c3d7a7c697ac612c", [:rebar3], [{:base64url, "~> 1.0", [hex: :base64url, repo: "hexpm", optional: false]}, {:idna, "~> 6.0", [hex: :idna, repo: "hexpm", optional: false]}, {:jiffy, "~> 1.1.1", [hex: :jiffy, repo: "hexpm", optional: false]}, {:jose, "~> 1.11.10", [hex: :jose, repo: "hexpm", optional: false]}, {:yconf, "~> 1.0.17", [hex: :yconf, repo: "hexpm", optional: false]}], "hexpm", "ce686986de3f9d5fd285afe87523cb45329a349c6c6be7acc1ed916725d46423"},
|
||||
"p1_mysql": {:hex, :p1_mysql, "1.0.26", "574d07c9936c53b1ec3556db3cf064cc14a6c39039835b3d940471bfa5ac8e2b", [:rebar3], [], "hexpm", "ea138083f2c54719b9cf549dbf5802a288b0019ea3e5449b354c74cc03fafdec"},
|
||||
"p1_oauth2": {:hex, :p1_oauth2, "0.6.14", "1c5f82535574de87e2059695ac4b91f8f9aebacbc1c80287dae6f02552d47aea", [:rebar3], [], "hexpm", "1fd3ac474e43722d9d5a87c6df8d36f698ed87af7bb81cbbb66361451d99ae8f"},
|
||||
"p1_pgsql": {:hex, :p1_pgsql, "1.1.32", "3f95d7e3413fc8f0be80abb4be1a0d7f67066a36905085cd5a423145598b0cb0", [:rebar3], [{:xmpp, "~> 1.10.0", [hex: :xmpp, repo: "hexpm", optional: false]}], "hexpm", "268b01e8f4eb75c211a31495a25c2815c549aecce2f0df1a161c6e0a2cde061e"},
|
||||
"p1_utils": {:hex, :p1_utils, "1.0.26", "67b0c4ac9fa3ba3ef563b31aa111b0a004439a37fac85e027f1c3617e1c7ec6c", [:rebar3], [], "hexpm", "d0379e8c1156b98bd64f8129c1de022fcca4f2fdb7486ce73bf0ed2c3376b04c"},
|
||||
"p1_pgsql": {:hex, :p1_pgsql, "1.1.35", "e13d89f14d717553e85c88a152ce77461916b013d88fcb851e354a0b332d4218", [:rebar3], [{:xmpp, "~> 1.11.0", [hex: :xmpp, repo: "hexpm", optional: false]}], "hexpm", "e99594446c411c660696795b062336f5c4bd800451d8f620bb4d4ce304e255c2"},
|
||||
"p1_utils": {:hex, :p1_utils, "1.0.28", "9a7088a98d788b4c4880fd3c82d0c135650db13f2e4ef7e10db179791bc94d59", [:rebar3], [], "hexpm", "c49bd44bc4a40ad996691af826dd7e0aa56d4d0cd730817190a1f84d1a7f0033"},
|
||||
"pkix": {:hex, :pkix, "1.0.10", "d3bfadf7b7cfe2a3636f1b256c9cce5f646a07ce31e57ee527668502850765a0", [:rebar3], [], "hexpm", "e02164f83094cb124c41b1ab28988a615d54b9adc38575f00f19a597a3ac5d0e"},
|
||||
"sqlite3": {:hex, :sqlite3, "1.1.15", "e819defd280145c328457d7af897d2e45e8e5270e18812ee30b607c99cdd21af", [:rebar3], [], "hexpm", "3c0ba4e13322c2ad49de4e2ddd28311366adde54beae8dba9d9e3888f69d2857"},
|
||||
"stringprep": {:hex, :stringprep, "1.0.31", "fa1688c156dd271722aa18c423a4163e710d2f4f475ad0bc220910df669b53af", [:rebar3], [{:p1_utils, "1.0.26", [hex: :p1_utils, repo: "hexpm", optional: false]}], "hexpm", "e9699c88e8db16b3a41f0e45ac6874a4da81a6e4854a77d76ede6d09b08e3530"},
|
||||
"stun": {:hex, :stun, "1.2.17", "c54614a592812ea125a2e6827aac5a438571b591616426ec1419ba9b48252f54", [:rebar3], [{:fast_tls, "1.1.22", [hex: :fast_tls, repo: "hexpm", optional: false]}, {:p1_utils, "1.0.26", [hex: :p1_utils, repo: "hexpm", optional: false]}], "hexpm", "6b318244c21e8524a9aae3ac9a05cd8234ee994c1c2c815de68d306086ad768d"},
|
||||
"unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"},
|
||||
"xmpp": {:hex, :xmpp, "1.10.0", "68a6dff8db8987c4592b2d5dd71d3f947b4ebd15209c9acaca5909a642670630", [:rebar3], [{:ezlib, "~> 1.0.12", [hex: :ezlib, repo: "hexpm", optional: false]}, {:fast_tls, "~> 1.1.19", [hex: :fast_tls, repo: "hexpm", optional: false]}, {:fast_xml, "~> 1.1.51", [hex: :fast_xml, repo: "hexpm", optional: false]}, {:idna, "~> 6.0", [hex: :idna, repo: "hexpm", optional: false]}, {:p1_utils, "~> 1.0.25", [hex: :p1_utils, repo: "hexpm", optional: false]}, {:stringprep, "~> 1.0.29", [hex: :stringprep, repo: "hexpm", optional: false]}], "hexpm", "ceeae43b8fe97649d8f8546b3f7f2b38ecfc931c0cdd5c7445ffb3f80fcb7d85"},
|
||||
"yconf": {:hex, :yconf, "1.0.18", "e565edc8aabb8164c3bebc86969095d296ad315dcbb46af65dccbc6c71eae0f6", [:rebar3], [{:fast_yaml, "1.0.37", [hex: :fast_yaml, repo: "hexpm", optional: false]}], "hexpm", "fa950ec6503f92d6417fb8cc1d982403f041697e8e1bbf4d4588fb919b9562ea"},
|
||||
"stringprep": {:hex, :stringprep, "1.0.33", "22f42866b4f6f3c238ea2b9cb6241791184ddedbab55e94a025511f46325f3ca", [:rebar3], [{:p1_utils, "1.0.28", [hex: :p1_utils, repo: "hexpm", optional: false]}], "hexpm", "96f8b30bc50887f605b33b46bca1d248c19a879319b8c482790e3b4da5da98c0"},
|
||||
"stun": {:hex, :stun, "1.2.21", "735855314ad22cb7816b88597d2f5ca22e24aa5e4d6010a0ef3affb33ceed6a5", [:rebar3], [{:fast_tls, "1.1.25", [hex: :fast_tls, repo: "hexpm", optional: false]}, {:p1_utils, "1.0.28", [hex: :p1_utils, repo: "hexpm", optional: false]}], "hexpm", "3d7fe8efb9d05b240a6aa9a6bf8b8b7bff2d802895d170443c588987dc1e12d9"},
|
||||
"unicode_util_compat": {:hex, :unicode_util_compat, "0.7.1", "a48703a25c170eedadca83b11e88985af08d35f37c6f664d6dcfb106a97782fc", [:rebar3], [], "hexpm", "b3a917854ce3ae233619744ad1e0102e05673136776fb2fa76234f3e03b23642"},
|
||||
"xmpp": {:hex, :xmpp, "1.11.1", "60181e7d3e8e48aa3b23b2792075cda37e2e507ec152490b866e61e5320cb1da", [:rebar3], [{:ezlib, "~> 1.0.12", [hex: :ezlib, repo: "hexpm", optional: false]}, {:fast_tls, "~> 1.1.19", [hex: :fast_tls, repo: "hexpm", optional: false]}, {:fast_xml, "~> 1.1.51", [hex: :fast_xml, repo: "hexpm", optional: false]}, {:idna, "~> 6.0", [hex: :idna, repo: "hexpm", optional: false]}, {:p1_utils, "~> 1.0.26", [hex: :p1_utils, repo: "hexpm", optional: false]}, {:stringprep, "~> 1.0.29", [hex: :stringprep, repo: "hexpm", optional: false]}], "hexpm", "a5c933df904ab3cec15425da334e410ce84ec3ae7b81efe069e5db368a7b3716"},
|
||||
"yconf": {:hex, :yconf, "1.0.21", "dbae1589381e044529e112b7e0097c89d88df89e446ead53bd33e8d27e2bcc83", [:rebar3], [{:fast_yaml, "1.0.39", [hex: :fast_yaml, repo: "hexpm", optional: false]}], "hexpm", "c524a5f1fd86875d85b469cc2e368c204f97cca1c3918736e21f5001c01d096c"},
|
||||
}
|
||||
|
||||
+4
-1
@@ -131,9 +131,12 @@ ul li #navhead a, ul li #navheadsub a, ul li #navheadsubsub a {
|
||||
background: #424a55;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
#navitemlogin-start {
|
||||
border-top: 0.2em solid #cae7e4;
|
||||
}
|
||||
#navitemlogin {
|
||||
padding: 0.5em;
|
||||
border-top: 0.2em solid #cae7e4;
|
||||
border-bottom: 0.2em solid #cae7e4;
|
||||
padding-left: 0.5em;
|
||||
}
|
||||
|
||||
+48
-1
@@ -12,9 +12,10 @@
|
||||
{"A Web Page","Μία ιστοσελίδα"}.
|
||||
{"Accept","Αποδοχή"}.
|
||||
{"Access denied by service policy","Άρνηση πρόσβασης, λόγω τακτικής παροχής υπηρεσιών"}.
|
||||
{"Access model","Καθορίστε το μοντέλο πρόσβασης"}.
|
||||
{"Access model","Μοντέλο πρόσβασης"}.
|
||||
{"Account doesn't exist","Ο λογαριασμός δεν υπάρχει"}.
|
||||
{"Action on user","Eνέργεια για το χρήστη"}.
|
||||
{"Add a hat to a user","Προσθέστε ένα καπέλο σε έναν χρήστη"}.
|
||||
{"Add User","Προσθήκη Χρήστη"}.
|
||||
{"Administration of ","Διαχείριση του "}.
|
||||
{"Administration","Διαχείριση"}.
|
||||
@@ -45,7 +46,9 @@
|
||||
{"Anyone with a presence subscription of both or from may subscribe and retrieve items","Όποιος έχει συνδρομή παρουσίας και των δύο ή από μπορεί να εγγραφεί και να ανακτήσει στοιχεία"}.
|
||||
{"Anyone with Voice","Οποιοσδήποτε με Φωνή"}.
|
||||
{"Anyone","Οποιοσδήποτε"}.
|
||||
{"API Commands","Εντολές του API"}.
|
||||
{"April","Απρίλιος"}.
|
||||
{"Arguments","Επιχειρήματα"}.
|
||||
{"Attribute 'channel' is required for this request","Το δηλωτικό 'channel' απαιτείται για αυτό το Ερώτημα"}.
|
||||
{"Attribute 'id' is mandatory for MIX messages","Το δηλωτικό 'id' επιτακτικό για μηνύματα MIX"}.
|
||||
{"Attribute 'jid' is not allowed here","Το δηλωτικό 'jid' δεν επιτρέπεται εδώ"}.
|
||||
@@ -71,6 +74,7 @@
|
||||
{"Changing role/affiliation is not allowed","Η αλλαγή ρόλου/ομάδας δεν επιτρέπεται"}.
|
||||
{"Channel already exists","Το κανάλι υπάρχει ήδη"}.
|
||||
{"Channel does not exist","Το κανάλι δεν υπάρχει"}.
|
||||
{"Channel JID","JID καναλιού"}.
|
||||
{"Channels","Κανάλια"}.
|
||||
{"Characters not allowed:","Χαρακτήρες που δεν επιτρέπονται:"}.
|
||||
{"Chatroom configuration modified","Η ρύθμιση παραμέτρων της αίθουσας σύνεδριασης τροποποιηθηκε"}.
|
||||
@@ -84,6 +88,7 @@
|
||||
{"Choose whether to approve this entity's subscription.","Επιλέξτε αν θα εγκρίθεί η εγγραφή αυτής της οντότητας."}.
|
||||
{"City","Πόλη"}.
|
||||
{"Client acknowledged more stanzas than sent by server","Ο πελάτης γνωρίζει περισσότερα δωμάτια από αυτά που στάλθηκαν από τον εξυπηρετητή"}.
|
||||
{"Clustering","Συσταδοποίηση"}.
|
||||
{"Commands","Εντολές"}.
|
||||
{"Conference room does not exist","Η αίθουσα σύνεδριασης δεν υπάρχει"}.
|
||||
{"Configuration of room ~s","Διαμόρφωση δωματίου ~ s"}.
|
||||
@@ -119,6 +124,7 @@
|
||||
{"ejabberd","ejabberd"}.
|
||||
{"Email Address","Ηλεκτρονική Διεύθυνση"}.
|
||||
{"Email","Ηλεκτρονικό ταχυδρομείο"}.
|
||||
{"Enable hats","Ενεργοποίηση καπέλων"}.
|
||||
{"Enable logging","Ενεργοποίηση καταγραφής"}.
|
||||
{"Enable message archiving","Ενεργοποιήστε την αρχειοθέτηση μηνυμάτων"}.
|
||||
{"Enabling push without 'node' attribute is not supported","Η ενεργοποίηση της ώθησης χωρίς το χαρακτηριστικό 'κόμβος' δεν υποστηρίζεται"}.
|
||||
@@ -151,6 +157,8 @@
|
||||
{"Full List of Room Admins","Πλήρης Κατάλογος Διαχειριστών αιθουσών"}.
|
||||
{"Full List of Room Owners","Πλήρης Κατάλογος Ιδιοκτητών αιθουσών"}.
|
||||
{"Full Name","Ονοματεπώνυμο"}.
|
||||
{"Get List of Online Users","Λίστα online χρηστών"}.
|
||||
{"Get List of Registered Users","Λίστα εγγεγραμμένων χρηστών"}.
|
||||
{"Get Number of Online Users","Έκθεση αριθμού συνδεδεμένων χρηστών"}.
|
||||
{"Get Number of Registered Users","Έκθεση αριθμού εγγεγραμμένων χρηστών"}.
|
||||
{"Get Pending","Λήψη των εκκρεμοτήτων"}.
|
||||
@@ -163,6 +171,10 @@
|
||||
{"has been kicked because of an affiliation change","έχει αποβληθεί λόγω αλλαγής υπαγωγής"}.
|
||||
{"has been kicked because the room has been changed to members-only","αποβλήθηκε επειδή η αίθουσα αλλάξε γιά μέλη μόνο"}.
|
||||
{"has been kicked","αποβλήθηκε"}.
|
||||
{"Hash of the vCard-temp avatar of this room","Hash του vCard-temp avatar αυτού του δωματίου"}.
|
||||
{"Hat title","Τίτλος καπέλου"}.
|
||||
{"Hat URI","Καπέλο URI"}.
|
||||
{"Hats limit exceeded","Υπέρβαση του ορίου καπέλων"}.
|
||||
{"Host unknown","Άγνωστος εξυπηρετητής"}.
|
||||
{"HTTP File Upload","Ανέβασμα αρχείου"}.
|
||||
{"Idle connection","Αδρανής σύνδεση"}.
|
||||
@@ -183,6 +195,8 @@
|
||||
{"Incorrect value of 'action' attribute","Λανθασμένη τιμή του χαρακτηριστικού 'action'"}.
|
||||
{"Incorrect value of 'action' in data form","Λανθασμένη τιμή 'action' στη φόρμα δεδομένων"}.
|
||||
{"Incorrect value of 'path' in data form","Λανθασμένη τιμή 'path' στη φόρμα δεδομένων"}.
|
||||
{"Installed Modules:","Εγκατεστημένες ενότητες:"}.
|
||||
{"Install","Εγκατάσταση"}.
|
||||
{"Insufficient privilege","Ανεπαρκή προνόμια"}.
|
||||
{"Internal server error","Εσωτερικό σφάλμα"}.
|
||||
{"Invalid 'from' attribute in forwarded message","Μη έγκυρο χαρακτηριστικό 'από' στο προωθούμενο μήνυμα"}.
|
||||
@@ -198,6 +212,8 @@
|
||||
{"January","Ιανουάριος"}.
|
||||
{"JID normalization denied by service policy","Απετράπη η κανονικοποίηση του JID, λόγω της τακτικής Παροχής Υπηρεσιών"}.
|
||||
{"JID normalization failed","Απετράπη η κανονικοποίηση του JID"}.
|
||||
{"Joined MIX channels of ~ts","Ενσωματωμένα κανάλια MIX του ~ts"}.
|
||||
{"Joined MIX channels:","Ενσωματωμένα κανάλια MIX:"}.
|
||||
{"joins the room","συνδέεται στην αίθουσα"}.
|
||||
{"July","Ιούλιος"}.
|
||||
{"June","Ιούνιος"}.
|
||||
@@ -209,6 +225,9 @@
|
||||
{"Last year","Πέρυσι"}.
|
||||
{"Least significant bits of SHA-256 hash of text should equal hexadecimal label","Τα ψηφία μικρότερης αξίας του αθροίσματος SHA-256 του κειμένου θα έπρεπε να ισούνται με την δεκαεξαδική ετικέτα"}.
|
||||
{"leaves the room","εγκαταλείπει την αίθουσα"}.
|
||||
{"List of users with hats","Λίστα των χρηστών με καπέλα"}.
|
||||
{"List users with hats","Λίστα χρηστών με καπέλα"}.
|
||||
{"Logged Out","Αποσυνδεδεμένος"}.
|
||||
{"Logging","Καταγραφή"}.
|
||||
{"Make participants list public","Κάντε δημόσιο τον κατάλογο συμμετεχόντων"}.
|
||||
{"Make room CAPTCHA protected","Κάντε την αίθουσα προστατεύομενη με CAPTCHA"}.
|
||||
@@ -220,6 +239,7 @@
|
||||
{"Malformed username","Λανθασμένη μορφή ονόματος χρήστη"}.
|
||||
{"MAM preference modification denied by service policy","Άρνηση αλλαγής προτιμήσεων MAM, λόγω της τακτικής Παροχής Υπηρεσιών"}.
|
||||
{"March","Μάρτιος"}.
|
||||
{"Max # of items to persist, or `max` for no specific limit other than a server imposed maximum","Μέγιστος αριθμός στοιχείων που πρέπει να παραμείνουν, ή « Μέγιστο » για κανένα συγκεκριμένο όριο εκτός από το μέγιστο που επιβάλλει ο διακομιστής"}.
|
||||
{"Max payload size in bytes","Μέγιστο μέγεθος φορτίου σε bytes"}.
|
||||
{"Maximum file size","Μέγιστο μέγεθος αρχείου"}.
|
||||
{"Maximum Number of History Messages Returned by Room","Μέγιστος αριθμός μηνυμάτων Ιστορικού που επιστρέφονται από την Αίθουσα"}.
|
||||
@@ -290,6 +310,7 @@
|
||||
{"Node ~p","Κόμβος ~p"}.
|
||||
{"Nodeprep has failed","Το Nodeprep απέτυχε"}.
|
||||
{"Nodes","Κόμβοι"}.
|
||||
{"Node","Κόμβος"}.
|
||||
{"None","Κανένα"}.
|
||||
{"Not allowed","Δεν επιτρέπεται"}.
|
||||
{"Not Found","Δε βρέθηκε"}.
|
||||
@@ -303,7 +324,9 @@
|
||||
{"Number of Offline Messages","Πλήθος μηνυμάτων Χωρίς Σύνδεση"}.
|
||||
{"Number of online users","Αριθμός συνδεδεμένων χρηστών"}.
|
||||
{"Number of registered users","Αριθμός εγγεγραμμένων χρηστών"}.
|
||||
{"Number of seconds after which to automatically purge items, or `max` for no specific limit other than a server imposed maximum","Αριθμός δευτερολέπτων μετά από τα οποία θα καθαρίζονται αυτόματα τα στοιχεία, ή `max` για κανένα συγκεκριμένο όριο εκτός από το μέγιστο που επιβάλλει ο διακομιστής"}.
|
||||
{"Occupants are allowed to invite others","Οι συμμετέχοντες μπορούν να προσκαλέσουν και άλλους"}.
|
||||
{"Occupants are allowed to query others","Οι κάτοικοι επιτρέπεται να ρωτούν άλλους"}.
|
||||
{"Occupants May Change the Subject","Επιτρέψτε στους χρήστες να αλλάζουν το Θέμα"}.
|
||||
{"October","Οκτώβριος"}.
|
||||
{"OK","Εντάξει"}.
|
||||
@@ -317,6 +340,7 @@
|
||||
{"Only members may query archives of this room","Μόνο μέλη μπορούν να δούνε τα αρχεία αυτής της αίθουσας"}.
|
||||
{"Only moderators and participants are allowed to change the subject in this room","Μόνο οι συντονιστές και οι συμμετέχοντες μπορούν να αλλάξουν το θέμα αυτής της αίθουσας"}.
|
||||
{"Only moderators are allowed to change the subject in this room","Μόνο οι συντονιστές μπορούν να αλλάξουν το θέμα αυτής της αίθουσας"}.
|
||||
{"Only moderators are allowed to retract messages","Μόνο οι συντονιστές επιτρέπεται να αποσύρουν μηνύματα"}.
|
||||
{"Only moderators can approve voice requests","Μόνο οι συντονιστές μπορούν να εγκρίνουν τις αιτήσεις φωνής"}.
|
||||
{"Only occupants are allowed to send messages to the conference","Μόνο οι συμμετέχοντες επιτρέπεται να στέλνουν μηνύματα στο συνέδριο"}.
|
||||
{"Only occupants are allowed to send queries to the conference","Μόνο οι συμμετέχοντες επιτρέπεται να στείλουν ερωτήματα στη διάσκεψη"}.
|
||||
@@ -326,9 +350,11 @@
|
||||
{"Only those on a whitelist may subscribe and retrieve items","Μόνο όσοι βρίσκονται στη λίστα επιτρεπόμενων μπορούν να εγγραφούν και να ανακτήσουν αντικείμενα"}.
|
||||
{"Organization Name","Όνομα Οργανισμού"}.
|
||||
{"Organization Unit","Μονάδα Οργανισμού"}.
|
||||
{"Other Modules Available:","Διαθέσιμες άλλες ενότητες:"}.
|
||||
{"Outgoing s2s Connections","Εξερχόμενες S2S Συνδέσεις"}.
|
||||
{"Owner privileges required","Aπαιτούνται προνόμια ιδιοκτήτη"}.
|
||||
{"Packet relay is denied by service policy","Απαγορεύεται η αναμετάδοση πακέτων, λόγω της τακτικής Παροχής Υπηρεσιών"}.
|
||||
{"Participant ID","ID συμμετέχοντος"}.
|
||||
{"Participant","Συμμετέχων"}.
|
||||
{"Password Verification","Επαλήθευση κωδικού πρόσβασης"}.
|
||||
{"Password Verification:","Επαλήθευση κωδικού πρόσβασης:"}.
|
||||
@@ -336,6 +362,7 @@
|
||||
{"Password:","Κωδικός πρόσβασης:"}.
|
||||
{"Path to Dir","Τοποθεσία κατάλογου αρχείων"}.
|
||||
{"Path to File","Τοποθεσία Αρχείου"}.
|
||||
{"Payload semantic type information","Πληροφορίες σημασιολογικού τύπου ωφέλιμου φορτίου"}.
|
||||
{"Period: ","Περίοδος: "}.
|
||||
{"Persist items to storage","Μόνιμη αποθήκευση στοιχείων"}.
|
||||
{"Persistent","Μόνιμη"}.
|
||||
@@ -371,6 +398,7 @@
|
||||
{"Register an XMPP account","Καταχωρείστε έναν XMPP λογαριασμό χρήστη"}.
|
||||
{"Register","Καταχωρήστε"}.
|
||||
{"Remote copy","Εξ αποστάσεως αντίγραφο"}.
|
||||
{"Remove a hat from a user","Αφαίρεση ενός καπέλου από έναν χρήστη"}.
|
||||
{"Remove User","Αφαίρεση χρήστη"}.
|
||||
{"Replaced by new connection","Αντικαταστάθηκε από μια νέα σύνδεση"}.
|
||||
{"Request has timed out","Το αίτημα έληξε"}.
|
||||
@@ -383,6 +411,7 @@
|
||||
{"Restore binary backup immediately:","Επαναφορά δυαδικού αντιγράφου ασφαλείας αμέσως:"}.
|
||||
{"Restore plain text backup immediately:","Επαναφορά αντιγράφου ασφαλείας από αρχείο κειμένου αμέσως:"}.
|
||||
{"Restore","Επαναφορά Αντιγράφου Ασφαλείας"}.
|
||||
{"Result","Αποτέλεσμα"}.
|
||||
{"Roles and Affiliations that May Retrieve Member List","Ρόλοι και δεσμοί που μπορούν να λάβουν την λίστα μελών"}.
|
||||
{"Roles for which Presence is Broadcasted","Ρόλοι των οποίων η παρουσία δηλώνεται δημόσια"}.
|
||||
{"Roles that May Send Private Messages","Ρόλοι που επιτρέπεται να αποστέλλουν ιδιωτικά μηνύματα"}.
|
||||
@@ -414,13 +443,16 @@
|
||||
{"Set message of the day on all hosts and send to online users","Ορίστε μήνυμα ημέρας και άμεση αποστολή στους συνδεδεμένους χρήστες σε όλους τους κεντρικούς υπολογιστές"}.
|
||||
{"Shared Roster Groups","Κοινές Ομάδες Καταλόγων Επαφών"}.
|
||||
{"Show Integral Table","Δείτε Ολοκληρωτικό Πίνακα"}.
|
||||
{"Show Occupants Join/Leave","Εμφάνιση ενοίκων Join/Leave"}.
|
||||
{"Show Ordinary Table","Δείτε Κοινό Πίνακα"}.
|
||||
{"Shut Down Service","Τερματισμός Υπηρεσίας"}.
|
||||
{"SOCKS5 Bytestreams","Bytestreams του SOCKS5"}.
|
||||
{"Some XMPP clients can store your password in the computer, but you should do this only in your personal computer for safety reasons.","Ορισμένοι πελάτες XMPP μπορούν να αποθηκεύσουν τον κωδικό πρόσβασής σας στον υπολογιστή, αλλά θα πρέπει να το κάνετε μόνο στον προσωπικό σας υπολογιστή για λόγους ασφαλείας."}.
|
||||
{"Sources Specs:","Πηγές Προδιαγραφές:"}.
|
||||
{"Specify the access model","Καθορίστε το μοντέλο πρόσβασης"}.
|
||||
{"Specify the event message type","Καθορίστε τον τύπο μηνύματος συμβάντος"}.
|
||||
{"Specify the publisher model","Καθορίστε το μοντέλο εκδότη"}.
|
||||
{"Stanza id is not valid","Το Stanza id δεν είναι έγκυρο"}.
|
||||
{"Stanza ID","Ταυτότητα Δωματίου"}.
|
||||
{"Statically specify a replyto of the node owner(s)","Προσδιορίστε (στατικά) το Απάντηση Προς του ιδιοκτήτη-ων του κόμβου"}.
|
||||
{"Stopped Nodes","Σταματημένοι Κόμβοι"}.
|
||||
@@ -457,7 +489,10 @@
|
||||
{"The JIDs of those to contact with questions","Το JID αυτών με τους οποίους θα επικοινωνήσετε με ερωτήσεις"}.
|
||||
{"The JIDs of those with an affiliation of owner","Το JID αυτών που σχετίζονται με τον ιδιοκτήτη"}.
|
||||
{"The JIDs of those with an affiliation of publisher","Το JID αυτών που σχετίζονται με τον εκδότη"}.
|
||||
{"The list of all online users","Ο κατάλογος όλων των online χρηστών"}.
|
||||
{"The list of all users","Ο κατάλογος όλων των χρηστών"}.
|
||||
{"The list of JIDs that may associate leaf nodes with a collection","Λίστα των JIDs που μπορούν να σχετίζουν leaf κόμβους με μια Συλλογή"}.
|
||||
{"The maximum number of child nodes that can be associated with a collection, or `max` for no specific limit other than a server imposed maximum","Ο μέγιστος αριθμός των παιδικών κόμβων που μπορούν να συσχετιστούν με μια συλλογή, ή `max` για κανένα συγκεκριμένο όριο εκτός από το μέγιστο που επιβάλλει ο διακομιστής"}.
|
||||
{"The minimum number of milliseconds between sending any two notification digests","Το ελάχιστο πλήθος χιλιοστών του δευτερολέπτου μεταξύ της αποστολής δύο συγχωνεύσεων ειδοποιήσεων"}.
|
||||
{"The name of the node","Το όνομα του κόμβου"}.
|
||||
{"The node is a collection node","Ο κόμβος είναι κόμβος Συλλογής"}.
|
||||
@@ -476,6 +511,7 @@
|
||||
{"The query is only allowed from local users","Το ερώτημα επιτρέπεται μόνο από τοπικούς χρήστες"}.
|
||||
{"The query must not contain <item/> elements","Το ερώτημα δεν πρέπει να περιέχει στοιχείο <item/>"}.
|
||||
{"The room subject can be modified by participants","Το θέμα μπορεί να τροποποιηθεί από τους συμμετέχοντες"}.
|
||||
{"The semantic type information of data in the node, usually specified by the namespace of the payload (if any)","Οι πληροφορίες σημασιολογικού τύπου των δεδομένων στον κόμβο, που συνήθως καθορίζονται από το χώρο ονομάτων του ωφέλιμου φορτίου (εάν υπάρχει)"}.
|
||||
{"The sender of the last received message","Ο αποστολέας του τελευταίου εισερχομένου μηνύματος"}.
|
||||
{"The stanza MUST contain only one <active/> element, one <default/> element, or one <list/> element","Η stanza ΠΡΕΠΕΙ να περιέχει μόνο ένα στοιχείο <active />, ένα στοιχείο <default /> ή ένα στοιχείο <list />"}.
|
||||
{"The subscription identifier associated with the subscription request","Το αναγνωριστικό συνδρομής συσχετίστηκε με το αίτημα συνδρομής"}.
|
||||
@@ -505,6 +541,7 @@
|
||||
{"Too many unacked stanzas","Πάρα πολλές μη αναγνωρισμένες stanzas"}.
|
||||
{"Too many users in this conference","Πάρα πολλοί χρήστες σε αυτή τη διάσκεψη"}.
|
||||
{"Traffic rate limit is exceeded","Υπέρφορτωση"}.
|
||||
{"~ts's MAM Archive","Αρχείο MAM του ~ts"}.
|
||||
{"~ts's Offline Messages Queue","~ts's Χωρίς Σύνδεση Μηνύματα"}.
|
||||
{"Tuesday","Τρίτη"}.
|
||||
{"Unable to generate a CAPTCHA","Αδύνατη η δημιουργία CAPTCHA"}.
|
||||
@@ -512,17 +549,23 @@
|
||||
{"Unauthorized","Χωρίς Εξουσιοδότηση"}.
|
||||
{"Unexpected action","Απροσδόκητη ενέργεια"}.
|
||||
{"Unexpected error condition: ~p","Απροσδόκητες συνθήκες σφάλματος: ~p"}.
|
||||
{"Uninstall","Απεγκατάσταση"}.
|
||||
{"Unregister an XMPP account","Καταργήση λογαριασμού XMPP"}.
|
||||
{"Unregister","Καταργήση εγγραφής"}.
|
||||
{"Unsupported <index/> element","Μη υποστηριζόμενο στοιχείο <index />"}.
|
||||
{"Unsupported version","Μη υποστηριζόμενη έκδοση"}.
|
||||
{"Update message of the day (don't send)","Ενημέρωση μηνύματος ημέρας (χωρίς άμεση αποστολή)"}.
|
||||
{"Update message of the day on all hosts (don't send)","Ενημέρωση μηνύματος ημέρας σε όλους τους κεντρικούς υπολογιστές (χωρίς άμεση αποστολή)"}.
|
||||
{"Update specs to get modules source, then install desired ones.","Ενημερώστε τις προδιαγραφές για να λάβετε την πηγή των ενοτήτων και, στη συνέχεια, εγκαταστήστε τις επιθυμητές."}.
|
||||
{"Update Specs","Προδιαγραφές ενημέρωσης"}.
|
||||
{"Updating the vCard is not supported by the vCard storage backend","Η ενημέρωση της vCard δεν υποστηρίζεται από το backend αποθήκευσης vCard"}.
|
||||
{"Upgrade","Αναβάθμιση"}.
|
||||
{"URL for Archived Discussion Logs","URL αρχειοθετημένων καταγραφών συζητήσεων"}.
|
||||
{"User already exists","Ο χρήστης υπάρχει ήδη"}.
|
||||
{"User JID","JID Χρήστη"}.
|
||||
{"User (jid)","Χρήστης (jid)"}.
|
||||
{"User Management","Διαχείριση χρηστών"}.
|
||||
{"User not allowed to perform an IQ set on another user's vCard.","Ο χρήστης δεν επιτρέπεται να εκτελέσει ένα σετ IQ στην vCard ενός άλλου χρήστη."}.
|
||||
{"User removed","Ο Χρήστης αφαιρέθηκε"}.
|
||||
{"User session not found","Η περίοδος σύνδεσης χρήστη δεν βρέθηκε"}.
|
||||
{"User session terminated","Η περίοδος σύνδεσης χρήστη τερματίστηκε"}.
|
||||
@@ -538,12 +581,14 @@
|
||||
{"Value of '~s' should be integer","Η τιμή του '~s' θα πρέπει να είναι ακέραιος"}.
|
||||
{"Value 'set' of 'type' attribute is not allowed","Δεν επιτρέπεται η παράμετρος 'set' του 'type'"}.
|
||||
{"vCard User Search","vCard Αναζήτηση χρηστών"}.
|
||||
{"View joined MIX channels","Προβολή ενταγμένων καναλιών MIX"}.
|
||||
{"Virtual Hosts","Eικονικοί κεντρικοί υπολογιστές"}.
|
||||
{"Visitors are not allowed to change their nicknames in this room","Οι επισκέπτες δεν επιτρέπεται να αλλάξουν τα ψευδώνυμα τους σε αυτή την αίθουσα"}.
|
||||
{"Visitors are not allowed to send messages to all occupants","Οι επισκέπτες δεν επιτρέπεται να στείλουν μηνύματα σε όλους τους συμμετέχοντες"}.
|
||||
{"Visitor","Επισκέπτης"}.
|
||||
{"Voice requests are disabled in this conference","Τα αιτήματα φωνής είναι απενεργοποιημένα, σε αυτό το συνέδριο"}.
|
||||
{"Voice request","Αίτημα φωνής"}.
|
||||
{"Web client which allows to join the room anonymously","Web client που επιτρέπει την ανώνυμη είσοδο στην αίθουσα"}.
|
||||
{"Wednesday","Τετάρτη"}.
|
||||
{"When a new subscription is processed and whenever a subscriber comes online","Όταν μία νέα συνδρομή βρίσκεται εν επεξεργασία και όποτε ένας συνδρομητής συνδεθεί"}.
|
||||
{"When a new subscription is processed","Όταν μία νέα συνδρομή βρίσκεται εν επεξεργασία"}.
|
||||
@@ -556,6 +601,7 @@
|
||||
{"Whether to allow subscriptions","Εάν επιτρέπονται συνδρομές"}.
|
||||
{"Whether to make all subscriptions temporary, based on subscriber presence","Αν επιτρέπεται να γίνουν όλες οι συνδρομές προσωρινές, βασιζόμενοι στην παρουσία του συνδρομητή"}.
|
||||
{"Whether to notify owners about new subscribers and unsubscribes","Αν πρέπει να ειδοποιούνται οι ιδιοκτήτες για νέους συνδρομητές και αποχωρήσεις"}.
|
||||
{"Who can send private messages","Ποιος μπορεί να στείλει ιδιωτικά μηνύματα"}.
|
||||
{"Who may associate leaf nodes with a collection","Ποιός μπορεί να συσχετίζει leaf nodes με μία συλλογή"}.
|
||||
{"Wrong parameters in the web formulary","Εσφαλμένες παράμετροι στην διαμόρφωση τυπικότητας του δυκτίου"}.
|
||||
{"Wrong xmlns","Εσφαλμένο xmlns"}.
|
||||
@@ -567,6 +613,7 @@
|
||||
{"XMPP Show Value of XA (Extended Away)","Δείξε τιμή XMPP Αξία του Λίαν Απομακρυσμένος"}.
|
||||
{"XMPP URI of Associated Publish-Subscribe Node","XMPP URI του συσχετισμένου κόμβου Δημοσίευσης-Εγγραφής"}.
|
||||
{"You are being removed from the room because of a system shutdown","Απαιτείται η απομάκρυνσή σας από την αίθουσα, λόγω τερματισμού συστήματος"}.
|
||||
{"You are not allowed to send private messages","Δεν επιτρέπεται η αποστολή ιδιωτικών μηνυμάτων"}.
|
||||
{"You are not joined to the channel","Δεν λαμβάνετε μέρος στο κανάλι"}.
|
||||
{"You can later change your password using an XMPP client.","Μπορείτε αργότερα να αλλάξετε τον κωδικό πρόσβασής σας χρησιμοποιώντας ένα πρόγραμμα-πελάτη XMPP."}.
|
||||
{"You have been banned from this room","Σας έχει απαγορευθεί η είσοδος σε αυτή την αίθουσα"}.
|
||||
|
||||
+1
-1
@@ -358,7 +358,7 @@
|
||||
{"Too many child elements","Túl sok gyermekelem"}.
|
||||
{"Too many <item/> elements","Túl sok <item/> elem"}.
|
||||
{"Too many <list/> elements","Túl sok <list/> elem"}.
|
||||
{"Too many (~p) failed authentications from this IP address (~s). The address will be unblocked at ~s UTC","Túl sok (~p) sikertelen hitelesítés erről az IP-címről (~ts) A cím ~ts-kor lesz feloldva UTC szerint"}.
|
||||
{"Too many (~p) failed authentications from this IP address (~s). The address will be unblocked at ~s UTC","Túl sok (~p) sikertelen hitelesítés erről az IP-címről (~s) A cím ~s-kor lesz feloldva UTC szerint"}.
|
||||
{"Too many receiver fields were specified","Túl sok fogadómező lett meghatározva"}.
|
||||
{"Too many unacked stanzas","Túl sok nyugtázatlan stanza"}.
|
||||
{"Too many users in this conference","Túl sok felhasználó ebben a konferenciában"}.
|
||||
|
||||
+14
-9
@@ -46,7 +46,9 @@
|
||||
{"Anyone with a presence subscription of both or from may subscribe and retrieve items","அல்லது இரண்டின் இருப்பு சந்தா உள்ள எவரும் உருப்படிகளை குழுசேர் மற்றும் மீட்டெடுக்கலாம்"}.
|
||||
{"Anyone with Voice","குரல் உள்ள எவரும்"}.
|
||||
{"Anyone","யாரும்"}.
|
||||
{"API Commands","பநிஇ கட்டளைகள்"}.
|
||||
{"April","ப-சித்திரை"}.
|
||||
{"Arguments","வாதங்கள்"}.
|
||||
{"Attribute 'channel' is required for this request","இந்த கோரிக்கைக்கு 'சேனல்' என்ற பண்புக்கூறு தேவை"}.
|
||||
{"Attribute 'id' is mandatory for MIX messages","கலவை செய்திகளுக்கு 'ஐடி' கட்டாயமாகும்"}.
|
||||
{"Attribute 'jid' is not allowed here","'சிட்' என்ற பண்புக்கூறு இங்கே அனுமதிக்கப்படவில்லை"}.
|
||||
@@ -86,6 +88,7 @@
|
||||
{"Choose whether to approve this entity's subscription.","இந்த நிறுவனத்தின் சந்தாவை அங்கீகரிக்க வேண்டுமா என்பதைத் தேர்வுசெய்க."}.
|
||||
{"City","நகரம்"}.
|
||||
{"Client acknowledged more stanzas than sent by server","சேவையகத்தால் அனுப்பப்பட்டதை விட கிளையன்ட் அதிக சரணத்தை ஒப்புக் கொண்டார்"}.
|
||||
{"Clustering","கிளச்டரிங்"}.
|
||||
{"Commands","கட்டளைகள்"}.
|
||||
{"Conference room does not exist","மாநாட்டு அறை இல்லை"}.
|
||||
{"Configuration of room ~s","அறையின் உள்ளமைவு ~s"}.
|
||||
@@ -259,7 +262,7 @@
|
||||
{"Module failed to handle the query","தொகுதி வினவலைக் கையாளத் தவறிவிட்டது"}.
|
||||
{"Monday","திங்கள்"}.
|
||||
{"Multicast","மல்டிகாச்ட்"}.
|
||||
{"Multiple <item/> elements are not allowed by RFC6121","பல <உருப்படி/> கூறுகள் RFC6121 ஆல் அனுமதிக்கப்படாது"}.
|
||||
{"Multiple <item/> elements are not allowed by RFC6121","பல <item/> கூறுகள் RFC6121 ஆல் அனுமதிக்கப்படாது"}.
|
||||
{"Multi-User Chat","பல பயனர் அரட்டை"}.
|
||||
{"Name","பெயர்"}.
|
||||
{"Natural Language for Room Discussions","அறை விவாதங்களுக்கு இயற்கை மொழி"}.
|
||||
@@ -281,7 +284,7 @@
|
||||
{"No data form found","தரவு படிவம் எதுவும் கிடைக்கவில்லை"}.
|
||||
{"No Data","தரவு இல்லை"}.
|
||||
{"No features available","நற்பொருத்தங்கள் எதுவும் கிடைக்கவில்லை"}.
|
||||
{"No <forwarded/> element found","இல்லை <முன்னோக்கி/> உறுப்பு காணப்பட்டது"}.
|
||||
{"No <forwarded/> element found","இல்லை <forwarded/> உறுப்பு காணப்பட்டது"}.
|
||||
{"No hook has processed this command","இந்த கட்டளையை எந்த ஊக்கும் செயலாக்கவில்லை"}.
|
||||
{"No info about last activity found","கடைசி செயல்பாட்டைப் பற்றிய எந்த தகவலும் கிடைக்கவில்லை"}.
|
||||
{"No 'item' element found","'உருப்படி' உறுப்பு இல்லை"}.
|
||||
@@ -332,8 +335,8 @@
|
||||
{"Online","ஆன்லைனில்"}.
|
||||
{"Only collection node owners may associate leaf nodes with the collection","சேகரிப்பு முனை உரிமையாளர்கள் மட்டுமே இலை முனைகளை சேகரிப்புடன் தொடர்புபடுத்தலாம்"}.
|
||||
{"Only deliver notifications to available users","கிடைக்கக்கூடிய பயனர்களுக்கு மட்டுமே அறிவிப்புகளை வழங்கவும்"}.
|
||||
{"Only <enable/> or <disable/> tags are allowed","<anafice/> அல்லது <முடக்கு/> குறிச்சொற்கள் மட்டுமே அனுமதிக்கப்படுகின்றன"}.
|
||||
{"Only <list/> element is allowed in this query","இந்த வினவலில் <பட்டியல்/> உறுப்பு மட்டுமே அனுமதிக்கப்படுகிறது"}.
|
||||
{"Only <enable/> or <disable/> tags are allowed","<enable/>அல்லது <disable/> குறிச்சொற்கள் மட்டுமே அனுமதிக்கப்படுகின்றன"}.
|
||||
{"Only <list/> element is allowed in this query","இந்த வினவலில் <list/> உறுப்பு மட்டுமே அனுமதிக்கப்படுகிறது"}.
|
||||
{"Only members may query archives of this room","உறுப்பினர்கள் மட்டுமே இந்த அறையின் காப்பகங்களை வினவலாம்"}.
|
||||
{"Only moderators and participants are allowed to change the subject in this room","இந்த அறையில் உள்ள விசயத்தை மாற்ற மதிப்பீட்டாளர்கள் மற்றும் பங்கேற்பாளர்கள் மட்டுமே அனுமதிக்கப்படுகிறார்கள்"}.
|
||||
{"Only moderators are allowed to change the subject in this room","இந்த அறையில் உள்ள விசயத்தை மாற்ற மதிப்பீட்டாளர்கள் மட்டுமே அனுமதிக்கப்படுகிறார்கள்"}.
|
||||
@@ -408,6 +411,7 @@
|
||||
{"Restore binary backup immediately:","பைனரி காப்புப்பிரதியை உடனடியாக மீட்டமைக்கவும்:"}.
|
||||
{"Restore plain text backup immediately:","எளிய உரை காப்புப்பிரதியை உடனடியாக மீட்டெடுக்கவும்:"}.
|
||||
{"Restore","மீட்டமை"}.
|
||||
{"Result","விளைவு"}.
|
||||
{"Roles and Affiliations that May Retrieve Member List","உறுப்பினர் பட்டியலை மீட்டெடுக்கக்கூடிய பாத்திரங்கள் மற்றும் இணைப்புகள்"}.
|
||||
{"Roles for which Presence is Broadcasted","இருப்பு ஒளிபரப்பப்படும் பாத்திரங்கள்"}.
|
||||
{"Roles that May Send Private Messages","தனிப்பட்ட செய்திகளை அனுப்பக்கூடிய பாத்திரங்கள்"}.
|
||||
@@ -505,11 +509,11 @@
|
||||
{"The passwords are different","கடவுச்சொற்கள் வேறுபட்டவை"}.
|
||||
{"The presence states for which an entity wants to receive notifications","ஒரு நிறுவனம் அறிவிப்புகளைப் பெற விரும்பும் இருப்பு நிலைகள்"}.
|
||||
{"The query is only allowed from local users","வினவல் உள்ளக பயனர்களிடமிருந்து மட்டுமே அனுமதிக்கப்படுகிறது"}.
|
||||
{"The query must not contain <item/> elements","வினவலில் <பொருள்/> கூறுகள் இருக்கக்கூடாது"}.
|
||||
{"The query must not contain <item/> elements","வினவலில் <item/>கூறுகள் இருக்கக் கூடாது"}.
|
||||
{"The room subject can be modified by participants","அறை பொருள் பங்கேற்பாளர்களால் மாற்றப்படலாம்"}.
|
||||
{"The semantic type information of data in the node, usually specified by the namespace of the payload (if any)","முனையில் உள்ள தரவின் சொற்பொருள் வகை செய்தி, பொதுவாக பேலோடின் பெயர்வெளியால் குறிப்பிடப்படுகிறது (ஏதேனும் இருந்தால்)"}.
|
||||
{"The sender of the last received message","கடைசியாக பெறப்பட்ட செய்தியை அனுப்பு"}.
|
||||
{"The stanza MUST contain only one <active/> element, one <default/> element, or one <list/> element","ச்டான்சாவில் ஒரே <செயலில்/> உறுப்பு, ஒரு <இயல்புநிலை/> உறுப்பு அல்லது ஒரு <பட்டியல்/> உறுப்பு மட்டுமே இருக்க வேண்டும்"}.
|
||||
{"The stanza MUST contain only one <active/> element, one <default/> element, or one <list/> element","ச்டான்சாவில் ஒரே <active/> உறுப்பு, ஒரு <default/>உறுப்பு அல்லது ஒரு <list/>உறுப்பு மட்டுமே இருக்க வேண்டும்"}.
|
||||
{"The subscription identifier associated with the subscription request","சந்தா கோரிக்கையுடன் தொடர்புடைய சந்தா அடையாளங்காட்டி"}.
|
||||
{"The URL of an XSL transformation which can be applied to payloads in order to generate an appropriate message body element.","பொருத்தமான செய்தி உடல் உறுப்பை உருவாக்குவதற்காக பேலோடுகளுக்கு பயன்படுத்தக்கூடிய எக்ச்எச்எல் மாற்றத்தின் முகவரி."}.
|
||||
{"The URL of an XSL transformation which can be applied to the payload format in order to generate a valid Data Forms result that the client could display using a generic Data Forms rendering engine","செல்லுபடியாகும் தரவு படிவங்களை உருவாக்குவதற்காக பேலோட் வடிவத்தில் பயன்படுத்தக்கூடிய ஒரு எக்ச்எச்எல் உருமாற்றத்தின் முகவரி, கிளையன்ட் பொதுவான தரவு படிவங்கள் வழங்குதல் எஞ்சின் பயன்படுத்தி காண்பிக்க முடியும்"}.
|
||||
@@ -530,8 +534,8 @@
|
||||
{"Too many active bytestreams","பல செயலில் உள்ள பைட்டிரீம்கள்"}.
|
||||
{"Too many CAPTCHA requests","பல கேப்ட்சா கோரிக்கைகள்"}.
|
||||
{"Too many child elements","பல குழந்தை கூறுகள்"}.
|
||||
{"Too many <item/> elements","பல <உருப்படி/> கூறுகள்"}.
|
||||
{"Too many <list/> elements","பல <பட்டியல்/> கூறுகள்"}.
|
||||
{"Too many <item/> elements","பல <item/> கூறுகள்"}.
|
||||
{"Too many <list/> elements","பல <list/> கூறுகள்"}.
|
||||
{"Too many (~p) failed authentications from this IP address (~s). The address will be unblocked at ~s UTC","இந்த ஐபி முகவரியிலிருந்து (~p) பல (~s) தோல்வியுற்ற அங்கீகாரங்கள் ~s. முகவரி UTC இல் தடைசெய்யப்படும்"}.
|
||||
{"Too many receiver fields were specified","அதிகமான ரிசீவர் புலங்கள் குறிப்பிடப்பட்டன"}.
|
||||
{"Too many unacked stanzas","பல அறியப்படாத சரணங்கள்"}.
|
||||
@@ -548,7 +552,7 @@
|
||||
{"Uninstall","நிறுவல் நீக்க"}.
|
||||
{"Unregister an XMPP account","ஒரு எக்ச்எம்பிபி கணக்கை பதிவு செய்யவும்"}.
|
||||
{"Unregister","பதிவு செய்யப்படாதது"}.
|
||||
{"Unsupported <index/> element","ஆதரிக்கப்படாத <குறியீட்டு/> உறுப்பு"}.
|
||||
{"Unsupported <index/> element","ஆதரிக்கப்படாத <index/> உறுப்பு"}.
|
||||
{"Unsupported version","ஆதரிக்கப்படாத பதிப்பு"}.
|
||||
{"Update message of the day (don't send)","அன்றைய செய்தியைப் புதுப்பிக்கவும் (அனுப்ப வேண்டாம்)"}.
|
||||
{"Update message of the day on all hosts (don't send)","எல்லா ஓச்ட்களிலும் அன்றைய செய்தியைப் புதுப்பிக்கவும் (அனுப்ப வேண்டாம்)"}.
|
||||
@@ -584,6 +588,7 @@
|
||||
{"Visitor","பார்வையாளர்"}.
|
||||
{"Voice requests are disabled in this conference","இந்த மாநாட்டில் குரல் கோரிக்கைகள் முடக்கப்பட்டுள்ளன"}.
|
||||
{"Voice request","குரல் கோரிக்கை"}.
|
||||
{"Web client which allows to join the room anonymously","அநாமதேயமாக அறையில் சேர அனுமதிக்கும் வலை கிளையண்ட்"}.
|
||||
{"Wednesday","புதன்கிழமை"}.
|
||||
{"When a new subscription is processed and whenever a subscriber comes online","புதிய சந்தா செயலாக்கப்படும் போது, சந்தாதாரர் ஆன்லைனில் வரும்போதெல்லாம்"}.
|
||||
{"When a new subscription is processed","புதிய சந்தா செயலாக்கப்படும் போது"}.
|
||||
|
||||
+10
-10
@@ -149,7 +149,7 @@
|
||||
{"Failed to process option '~s'","Не вдалося обробити параметр \"~s\""}.
|
||||
{"Family Name","Прізвище"}.
|
||||
{"FAQ Entry","Запис в ЧаПи"}.
|
||||
{"February","лютого"}.
|
||||
{"February","Лютого"}.
|
||||
{"File larger than ~w bytes","Файл більший, ніж ~w байт"}.
|
||||
{"Fill in the form to search for any matching XMPP User","Заповніть форму для пошуку будь-якого відповідного користувача XMPP"}.
|
||||
{"Friday","П'ятниця"}.
|
||||
@@ -209,14 +209,14 @@
|
||||
{"It is not allowed to send private messages of type \"groupchat\"","Не дозволяється надсилати приватні повідомлення типу \"groupchat\""}.
|
||||
{"It is not allowed to send private messages to the conference","Не дозволяється надсилати приватні повідомлення в конференцію"}.
|
||||
{"Jabber ID","Jabber ID"}.
|
||||
{"January","січня"}.
|
||||
{"January","Січня"}.
|
||||
{"JID normalization denied by service policy","Створювати конференцію заборонено політикою служби"}.
|
||||
{"JID normalization failed","Помилка нормалізації JID"}.
|
||||
{"Joined MIX channels of ~ts","Приєднався до каналів MIX ~ts"}.
|
||||
{"Joined MIX channels:","Приєднався до каналів MIX:"}.
|
||||
{"joins the room","увійшов(ла) в кімнату"}.
|
||||
{"July","липня"}.
|
||||
{"June","червня"}.
|
||||
{"July","Липня"}.
|
||||
{"June","Червня"}.
|
||||
{"Just created","Щойно створено"}.
|
||||
{"Last Activity","Останнє підключення"}.
|
||||
{"Last login","Останнє підключення"}.
|
||||
@@ -313,12 +313,12 @@
|
||||
{"Node","Вузол"}.
|
||||
{"None","Немає"}.
|
||||
{"Not allowed","Не дозволяється"}.
|
||||
{"Not Found","не знайдено"}.
|
||||
{"Not Found","Не знайдено"}.
|
||||
{"Not subscribed","Не підписаний"}.
|
||||
{"Notify subscribers when items are removed from the node","Повідомляти абонентів про видалення публікацій із збірника"}.
|
||||
{"Notify subscribers when the node configuration changes","Повідомляти абонентів про зміни в конфігурації збірника"}.
|
||||
{"Notify subscribers when the node is deleted","Повідомляти абонентів про видалення збірника"}.
|
||||
{"November","листопада"}.
|
||||
{"November","Листопада"}.
|
||||
{"Number of answers required","Кількість необхідних відповідей"}.
|
||||
{"Number of occupants","Кількість присутніх"}.
|
||||
{"Number of Offline Messages","Кількість автономних повідомлень"}.
|
||||
@@ -397,7 +397,7 @@
|
||||
{"Recipient is not in the conference room","Адресата немає в конференції"}.
|
||||
{"Register an XMPP account","Зареєструвати XMPP-запис"}.
|
||||
{"Register","Реєстрація"}.
|
||||
{"Remote copy","не зберігаеться локально"}.
|
||||
{"Remote copy","Віддалене копіювання"}.
|
||||
{"Remove a hat from a user","Зняти шапку з користувача"}.
|
||||
{"Remove User","Видалити користувача"}.
|
||||
{"Replaced by new connection","Замінено новим з'єднанням"}.
|
||||
@@ -435,7 +435,7 @@
|
||||
{"Send announcement to all online users","Надіслати сповіщення всім підключеним користувачам"}.
|
||||
{"Send announcement to all users on all hosts","Надіслати сповіщення до усіх користувачів на усіх хостах"}.
|
||||
{"Send announcement to all users","Надіслати сповіщення всім користувачам"}.
|
||||
{"September","вересня"}.
|
||||
{"September","Вересня"}.
|
||||
{"Server:","Сервер:"}.
|
||||
{"Service list retrieval timed out","Час очікування отримання списку послуг минув"}.
|
||||
{"Session state copying timed out","Час очікування копіювання стану сеансу минув"}.
|
||||
@@ -551,7 +551,7 @@
|
||||
{"Unexpected error condition: ~p","Умова несподіваної помилки: ~p"}.
|
||||
{"Uninstall","Видалити"}.
|
||||
{"Unregister an XMPP account","Видалити обліковий запис XMPP"}.
|
||||
{"Unregister","Видалити"}.
|
||||
{"Unregister","Скасувати реєстрацію"}.
|
||||
{"Unsupported <index/> element","Непідтримуваний елемент <index/>"}.
|
||||
{"Unsupported version","Непідтримувана версія"}.
|
||||
{"Update message of the day (don't send)","Оновити повідомлення дня (не надсилати)"}.
|
||||
@@ -582,7 +582,7 @@
|
||||
{"Value 'set' of 'type' attribute is not allowed","Значення 'set' атрибута 'type' не допускається"}.
|
||||
{"vCard User Search","Пошук користувачів по vCard"}.
|
||||
{"View joined MIX channels","Перегляд приєднаних каналів MIX"}.
|
||||
{"Virtual Hosts","віртуальні хости"}.
|
||||
{"Virtual Hosts","Віртуальні хости"}.
|
||||
{"Visitors are not allowed to change their nicknames in this room","Відвідувачам не дозволяється змінювати псевдонім в цій кімнаті"}.
|
||||
{"Visitors are not allowed to send messages to all occupants","Відвідувачам не дозволяється надсилати повідомлення всім присутнім"}.
|
||||
{"Visitor","Відвідувач"}.
|
||||
|
||||
+17
-16
@@ -26,8 +26,8 @@
|
||||
{if_version_below, "24",
|
||||
{base64url, "~> 1.0", {git, "https://github.com/dvv/base64url", {tag, "1.0.1"}}}
|
||||
}},
|
||||
{cache_tab, "~> 1.0.31", {git, "https://github.com/processone/cache_tab", {tag, "1.0.31"}}},
|
||||
{eimp, "~> 1.0.24", {git, "https://github.com/processone/eimp", {tag, "1.0.24"}}},
|
||||
{cache_tab, "~> 1.0.33", {git, "https://github.com/processone/cache_tab", {tag, "1.0.33"}}},
|
||||
{eimp, "~> 1.0.26", {git, "https://github.com/processone/eimp", {tag, "1.0.26"}}},
|
||||
{if_var_true, pam,
|
||||
{epam, "~> 1.0.14", {git, "https://github.com/processone/epam", {tag, "1.0.14"}}}},
|
||||
{if_var_true, redis,
|
||||
@@ -41,12 +41,12 @@
|
||||
{eredis, "~> 1.7.1", {git, "https://github.com/Nordix/eredis/", {tag, "v1.7.1"}}}
|
||||
}}},
|
||||
{if_var_true, sip,
|
||||
{esip, "~> 1.0.57", {git, "https://github.com/processone/esip", {tag, "1.0.57"}}}},
|
||||
{esip, "~> 1.0.59", {git, "https://github.com/processone/esip", {tag, "1.0.59"}}}},
|
||||
{if_var_true, zlib,
|
||||
{ezlib, "~> 1.0.13", {git, "https://github.com/processone/ezlib", {tag, "1.0.13"}}}},
|
||||
{fast_tls, "~> 1.1.22", {git, "https://github.com/processone/fast_tls", {tag, "1.1.22"}}},
|
||||
{fast_xml, "~> 1.1.55", {git, "https://github.com/processone/fast_xml", {tag, "1.1.55"}}},
|
||||
{fast_yaml, "~> 1.0.37", {git, "https://github.com/processone/fast_yaml", {tag, "1.0.37"}}},
|
||||
{ezlib, "~> 1.0.15", {git, "https://github.com/processone/ezlib", {tag, "1.0.15"}}}},
|
||||
{fast_tls, "~> 1.1.25", {git, "https://github.com/processone/fast_tls", {tag, "1.1.25"}}},
|
||||
{fast_xml, "~> 1.1.57", {git, "https://github.com/processone/fast_xml", {tag, "1.1.57"}}},
|
||||
{fast_yaml, "~> 1.0.39", {git, "https://github.com/processone/fast_yaml", {tag, "1.0.39"}}},
|
||||
{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"}}}
|
||||
@@ -63,22 +63,22 @@
|
||||
{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.17", {git, "https://github.com/processone/mqtree", {tag, "1.0.17"}}},
|
||||
{p1_acme, "~> 1.0.25", {git, "https://github.com/processone/p1_acme", {tag, "1.0.25"}}},
|
||||
{mqtree, "~> 1.0.19", {git, "https://github.com/processone/mqtree", {tag, "1.0.19"}}},
|
||||
{p1_acme, "~> 1.0.28", {git, "https://github.com/processone/p1_acme", {tag, "1.0.28"}}},
|
||||
{if_var_true, mysql,
|
||||
{p1_mysql, "~> 1.0.26", {git, "https://github.com/processone/p1_mysql", {tag, "1.0.26"}}}},
|
||||
{p1_oauth2, "~> 0.6.14", {git, "https://github.com/processone/p1_oauth2", {tag, "0.6.14"}}},
|
||||
{if_var_true, pgsql,
|
||||
{p1_pgsql, "~> 1.1.32", {git, "https://github.com/processone/p1_pgsql", {tag, "1.1.32"}}}},
|
||||
{p1_utils, "~> 1.0.27", {git, "https://github.com/processone/p1_utils", {tag, "1.0.27"}}},
|
||||
{p1_pgsql, "~> 1.1.35", {git, "https://github.com/processone/p1_pgsql", {tag, "1.1.35"}}}},
|
||||
{p1_utils, "~> 1.0.28", {git, "https://github.com/processone/p1_utils", {tag, "1.0.28"}}},
|
||||
{pkix, "~> 1.0.10", {git, "https://github.com/processone/pkix", {tag, "1.0.10"}}},
|
||||
{if_var_true, sqlite,
|
||||
{sqlite3, "~> 1.1.15", {git, "https://github.com/processone/erlang-sqlite3", {tag, "1.1.15"}}}},
|
||||
{stringprep, "~> 1.0.31", {git, "https://github.com/processone/stringprep", {tag, "1.0.31"}}},
|
||||
{stringprep, "~> 1.0.33", {git, "https://github.com/processone/stringprep", {tag, "1.0.33"}}},
|
||||
{if_var_true, stun,
|
||||
{stun, "~> 1.2.17", {git, "https://github.com/processone/stun", {tag, "1.2.17"}}}},
|
||||
{xmpp, "~> 1.10.0", {git, "https://github.com/processone/xmpp", {tag, "1.10.0"}}},
|
||||
{yconf, "~> 1.0.18", {git, "https://github.com/processone/yconf", {tag, "1.0.18"}}}
|
||||
{stun, "~> 1.2.21", {git, "https://github.com/processone/stun", {tag, "1.2.21"}}}},
|
||||
{xmpp, "~> 1.11.1", {git, "https://github.com/processone/xmpp", {tag, "1.11.1"}}},
|
||||
{yconf, "~> 1.0.21", {git, "https://github.com/processone/yconf", {tag, "1.0.21"}}}
|
||||
]}.
|
||||
|
||||
{gitonly_deps, [ejabberd_po]}.
|
||||
@@ -165,7 +165,7 @@
|
||||
}]}}.
|
||||
{if_rebar3, {project_plugins, [configure_deps,
|
||||
{if_var_true, tools, rebar3_format},
|
||||
{if_var_true, tools, rebar3_lint}
|
||||
{if_var_true, tools, {rebar3_lint, "4.1.1"}}
|
||||
]}}.
|
||||
{if_not_rebar3, {plugins, [
|
||||
deps_erl_opts, override_deps_versions2, override_opts, configure_deps
|
||||
@@ -255,6 +255,7 @@
|
||||
|
||||
{cover_enabled, true}.
|
||||
{cover_export_enabled, true}.
|
||||
{cover_excl_mods, [eldap_filter_yecc]}.
|
||||
{coveralls_coverdata, "_build/test/cover/ct.coverdata"}.
|
||||
{coveralls_service_name, "github"}.
|
||||
|
||||
|
||||
+48
-48
@@ -1,86 +1,86 @@
|
||||
{"1.2.0",
|
||||
[{<<"base64url">>,{pkg,<<"base64url">>,<<"1.0.1">>},1},
|
||||
{<<"cache_tab">>,{pkg,<<"cache_tab">>,<<"1.0.31">>},0},
|
||||
{<<"eimp">>,{pkg,<<"eimp">>,<<"1.0.24">>},0},
|
||||
{<<"cache_tab">>,{pkg,<<"cache_tab">>,<<"1.0.33">>},0},
|
||||
{<<"eimp">>,{pkg,<<"eimp">>,<<"1.0.26">>},0},
|
||||
{<<"epam">>,{pkg,<<"epam">>,<<"1.0.14">>},0},
|
||||
{<<"eredis">>,{pkg,<<"eredis">>,<<"1.7.1">>},0},
|
||||
{<<"esip">>,{pkg,<<"esip">>,<<"1.0.57">>},0},
|
||||
{<<"ezlib">>,{pkg,<<"ezlib">>,<<"1.0.13">>},0},
|
||||
{<<"fast_tls">>,{pkg,<<"fast_tls">>,<<"1.1.22">>},0},
|
||||
{<<"fast_xml">>,{pkg,<<"fast_xml">>,<<"1.1.55">>},0},
|
||||
{<<"fast_yaml">>,{pkg,<<"fast_yaml">>,<<"1.0.37">>},0},
|
||||
{<<"esip">>,{pkg,<<"esip">>,<<"1.0.59">>},0},
|
||||
{<<"ezlib">>,{pkg,<<"ezlib">>,<<"1.0.15">>},0},
|
||||
{<<"fast_tls">>,{pkg,<<"fast_tls">>,<<"1.1.25">>},0},
|
||||
{<<"fast_xml">>,{pkg,<<"fast_xml">>,<<"1.1.57">>},0},
|
||||
{<<"fast_yaml">>,{pkg,<<"fast_yaml">>,<<"1.0.39">>},0},
|
||||
{<<"idna">>,{pkg,<<"idna">>,<<"6.1.1">>},0},
|
||||
{<<"jiffy">>,{pkg,<<"jiffy">>,<<"1.1.2">>},1},
|
||||
{<<"jose">>,{pkg,<<"jose">>,<<"1.11.10">>},0},
|
||||
{<<"luerl">>,{pkg,<<"luerl">>,<<"1.2.3">>},0},
|
||||
{<<"mqtree">>,{pkg,<<"mqtree">>,<<"1.0.17">>},0},
|
||||
{<<"p1_acme">>,{pkg,<<"p1_acme">>,<<"1.0.25">>},0},
|
||||
{<<"mqtree">>,{pkg,<<"mqtree">>,<<"1.0.19">>},0},
|
||||
{<<"p1_acme">>,{pkg,<<"p1_acme">>,<<"1.0.28">>},0},
|
||||
{<<"p1_mysql">>,{pkg,<<"p1_mysql">>,<<"1.0.26">>},0},
|
||||
{<<"p1_oauth2">>,{pkg,<<"p1_oauth2">>,<<"0.6.14">>},0},
|
||||
{<<"p1_pgsql">>,{pkg,<<"p1_pgsql">>,<<"1.1.32">>},0},
|
||||
{<<"p1_utils">>,{pkg,<<"p1_utils">>,<<"1.0.27">>},0},
|
||||
{<<"p1_pgsql">>,{pkg,<<"p1_pgsql">>,<<"1.1.35">>},0},
|
||||
{<<"p1_utils">>,{pkg,<<"p1_utils">>,<<"1.0.28">>},0},
|
||||
{<<"pkix">>,{pkg,<<"pkix">>,<<"1.0.10">>},0},
|
||||
{<<"sqlite3">>,{pkg,<<"sqlite3">>,<<"1.1.15">>},0},
|
||||
{<<"stringprep">>,{pkg,<<"stringprep">>,<<"1.0.31">>},0},
|
||||
{<<"stun">>,{pkg,<<"stun">>,<<"1.2.17">>},0},
|
||||
{<<"unicode_util_compat">>,{pkg,<<"unicode_util_compat">>,<<"0.7.0">>},1},
|
||||
{<<"xmpp">>,{pkg,<<"xmpp">>,<<"1.10.0">>},0},
|
||||
{<<"yconf">>,{pkg,<<"yconf">>,<<"1.0.18">>},0}]}.
|
||||
{<<"stringprep">>,{pkg,<<"stringprep">>,<<"1.0.33">>},0},
|
||||
{<<"stun">>,{pkg,<<"stun">>,<<"1.2.21">>},0},
|
||||
{<<"unicode_util_compat">>,{pkg,<<"unicode_util_compat">>,<<"0.7.1">>},1},
|
||||
{<<"xmpp">>,{pkg,<<"xmpp">>,<<"1.11.1">>},0},
|
||||
{<<"yconf">>,{pkg,<<"yconf">>,<<"1.0.21">>},0}]}.
|
||||
[
|
||||
{pkg_hash,[
|
||||
{<<"base64url">>, <<"F8C7F2DA04CA9A5D0F5F50258F055E1D699F0E8BF4CFDB30B750865368403CF6">>},
|
||||
{<<"cache_tab">>, <<"E4097B50A6F373AB1E0A5F01BAB0BEF6626771A4CD6C93404ED6D54810E11FBC">>},
|
||||
{<<"eimp">>, <<"853DB317BA394D479D2F1181E0B0135A3CD3C2C7EB48FF6CFB035A28DD354B14">>},
|
||||
{<<"cache_tab">>, <<"E2542AFB34F17EE3CA19D2B0F546A074922C2B99FB6B2ACFB38160D7D0336EC3">>},
|
||||
{<<"eimp">>, <<"C0B05F32E35629C4D9BCFB832FF879A92B0F92B19844BC7835E0A45635F2899A">>},
|
||||
{<<"epam">>, <<"AA0B85D27F4EF3A756AE995179DF952A0721237E83C6B79D644347B75016681A">>},
|
||||
{<<"eredis">>, <<"39E31AA02ADCD651C657F39AAFD4D31A9B2F63C6C700DC9CECE98D4BC3C897AB">>},
|
||||
{<<"esip">>, <<"4B14E4832D08B9FFC10D855B5D10B3083232B1D53DEB4C046679496CE85569C4">>},
|
||||
{<<"ezlib">>, <<"3C7F62862850A241159C10B218ECF580BCE54D0890601B65144DACC2633BE2B0">>},
|
||||
{<<"fast_tls">>, <<"44356B256AFAD4399C2FC5059A3066669DAFD8BD4E4E796C9C1CF8910DDD265E">>},
|
||||
{<<"fast_xml">>, <<"ACE020F2521F2A484AC8467D2822AF85534A346E2AAE03FFCBC34F29318BEFAF">>},
|
||||
{<<"fast_yaml">>, <<"F71D472FBF787CCD161B914D1EB486116A0F4F2E835337A378FBD31B59D2E74B">>},
|
||||
{<<"esip">>, <<"EB202F8C62928193588091DFEDBC545FE3274C34ECD209961F86DCB6C9EBCE88">>},
|
||||
{<<"ezlib">>, <<"D74F5DF191784744726A5B1AE9062522C606334F11086363385EB3B772D91357">>},
|
||||
{<<"fast_tls">>, <<"DA8ED6F05A2452121B087158B17234749F36704C1F2B74DC51DB99A1E27ED5E8">>},
|
||||
{<<"fast_xml">>, <<"31EFC0F9BCEDA92069704F7A25830407DA5DC3DAD1272B810D6F2E13E73CC11A">>},
|
||||
{<<"fast_yaml">>, <<"2E71168091949BAB0E5F583B340A99072B4D22D93EB86624E7850A12B1517BE4">>},
|
||||
{<<"idna">>, <<"8A63070E9F7D0C62EB9D9FCB360A7DE382448200FBBD1B106CC96D3D8099DF8D">>},
|
||||
{<<"jiffy">>, <<"A9B6C9A7EC268E7CF493D028F0A4C9144F59CCB878B1AFE42841597800840A1B">>},
|
||||
{<<"jose">>, <<"A903F5227417BD2A08C8A00A0CBCC458118BE84480955E8D251297A425723F83">>},
|
||||
{<<"luerl">>, <<"DF25F41944E57A7C4D9EF09D238BC3E850276C46039CFC12B8BB42ECCF36FCB1">>},
|
||||
{<<"mqtree">>, <<"82F54B8F2D22B4445DB1D6CCCB7FE9EAD049D61410C29E32475F3CEB3EE62A89">>},
|
||||
{<<"p1_acme">>, <<"DB91F0D6C193CD1D5C0B0FA3939A898DBF56A6075DB4347CDE26E802715DE50C">>},
|
||||
{<<"mqtree">>, <<"D769C25F898810725FC7DB0DBFFE5F72098647048B1BE2E6D772F1C2F31D8476">>},
|
||||
{<<"p1_acme">>, <<"64D9C17F5412AA92D75B29206B2B984D734A4FE1B7EACB66C3D7A7C697AC612C">>},
|
||||
{<<"p1_mysql">>, <<"574D07C9936C53B1EC3556DB3CF064CC14A6C39039835B3D940471BFA5AC8E2B">>},
|
||||
{<<"p1_oauth2">>, <<"1C5F82535574DE87E2059695AC4B91F8F9AEBACBC1C80287DAE6F02552D47AEA">>},
|
||||
{<<"p1_pgsql">>, <<"3F95D7E3413FC8F0BE80ABB4BE1A0D7F67066A36905085CD5A423145598B0CB0">>},
|
||||
{<<"p1_utils">>, <<"F468D84C6FFA6E4B12A6160826DCF2D015527189D57865568A78B49C5ED972A1">>},
|
||||
{<<"p1_pgsql">>, <<"E13D89F14D717553E85C88A152CE77461916B013D88FCB851E354A0B332D4218">>},
|
||||
{<<"p1_utils">>, <<"9A7088A98D788B4C4880FD3C82D0C135650DB13F2E4EF7E10DB179791BC94D59">>},
|
||||
{<<"pkix">>, <<"D3BFADF7B7CFE2A3636F1B256C9CCE5F646A07CE31E57EE527668502850765A0">>},
|
||||
{<<"sqlite3">>, <<"E819DEFD280145C328457D7AF897D2E45E8E5270E18812EE30B607C99CDD21AF">>},
|
||||
{<<"stringprep">>, <<"FA1688C156DD271722AA18C423A4163E710D2F4F475AD0BC220910DF669B53AF">>},
|
||||
{<<"stun">>, <<"C54614A592812EA125A2E6827AAC5A438571B591616426EC1419BA9B48252F54">>},
|
||||
{<<"unicode_util_compat">>, <<"BC84380C9AB48177092F43AC89E4DFA2C6D62B40B8BD132B1059ECC7232F9A78">>},
|
||||
{<<"xmpp">>, <<"68A6DFF8DB8987C4592B2D5DD71D3F947B4EBD15209C9ACACA5909A642670630">>},
|
||||
{<<"yconf">>, <<"E565EDC8AABB8164C3BEBC86969095D296AD315DCBB46AF65DCCBC6C71EAE0F6">>}]},
|
||||
{<<"stringprep">>, <<"22F42866B4F6F3C238EA2B9CB6241791184DDEDBAB55E94A025511F46325F3CA">>},
|
||||
{<<"stun">>, <<"735855314AD22CB7816B88597D2F5CA22E24AA5E4D6010A0EF3AFFB33CEED6A5">>},
|
||||
{<<"unicode_util_compat">>, <<"A48703A25C170EEDADCA83B11E88985AF08D35F37C6F664D6DCFB106A97782FC">>},
|
||||
{<<"xmpp">>, <<"60181E7D3E8E48AA3B23B2792075CDA37E2E507EC152490B866E61E5320CB1DA">>},
|
||||
{<<"yconf">>, <<"DBAE1589381E044529E112B7E0097C89D88DF89E446EAD53BD33E8D27E2BCC83">>}]},
|
||||
{pkg_hash_ext,[
|
||||
{<<"base64url">>, <<"F9B3ADD4731A02A9B0410398B475B33E7566A695365237A6BDEE1BB447719F5C">>},
|
||||
{<<"cache_tab">>, <<"8582B60A4A09B247EF86355BA9E07FCE9E11EDC0345A775C9171F971C72B6351">>},
|
||||
{<<"eimp">>, <<"7D61432EB8A45659C0BE475F44E75EEB651743AA64A1DE8ADF785CDAD81961AD">>},
|
||||
{<<"cache_tab">>, <<"4258009EB050B22AABE0C848E230BBA58401A6895C58C2FF74DFB635E3C35900">>},
|
||||
{<<"eimp">>, <<"D96D4E8572B9DFC40F271E47F0CB1D8849373BC98A21223268781765ED52044C">>},
|
||||
{<<"epam">>, <<"2F3449E72885A72A6C2A843F561ADD0FC2F70D7A21F61456930A547473D4D989">>},
|
||||
{<<"eredis">>, <<"7C2B54C566FED55FEEF3341CA79B0100A6348FD3F162184B7ED5118D258C3CC1">>},
|
||||
{<<"esip">>, <<"19C357E1817B1E04792EF359BF900400F3E6D0E5ADE929FD72F88EA9B44AF2ED">>},
|
||||
{<<"ezlib">>, <<"9EE62AB3F8ED55A0FD11A9569FCB8E458683F95575417272192B069F092ABFBB">>},
|
||||
{<<"fast_tls">>, <<"E65779AEFB7AB15C4755230FEF8077E687D20CC5A3984A5974F9F657E8E2485B">>},
|
||||
{<<"fast_xml">>, <<"83F3E23A780ED5F567CDEC73953F06C95B838D709DBFA86B59A98A8D23C99F85">>},
|
||||
{<<"fast_yaml">>, <<"8DE868721BF7E2172414F7D3148EDE0F3C922B496455CD625DD5C4429515A769">>},
|
||||
{<<"esip">>, <<"0BDF2E3C349DC0B144F173150329E675C6A51AC473D7A0B2E362245FAAD3FBE6">>},
|
||||
{<<"ezlib">>, <<"DD14BA6C12521AF5CFE6923E73E3D545F4A0897DC66BFAB5287FBB7AE3962EAB">>},
|
||||
{<<"fast_tls">>, <<"59E183B5740E670E02B8AA6BE673B5E7779E5FE5BFCC679FE2D4993D1949A821">>},
|
||||
{<<"fast_xml">>, <<"EEC34E90ADACAFE467D5DDAB635A014DED73B98B4061554B2D1972173D929C39">>},
|
||||
{<<"fast_yaml">>, <<"24C7B9AB9E2B9269D64E45F4A2A1280966ADB17D31E63365CFD3EE277FB0A78D">>},
|
||||
{<<"idna">>, <<"92376EB7894412ED19AC475E4A86F7B413C1B9FBB5BD16DCCD57934157944CEA">>},
|
||||
{<<"jiffy">>, <<"BB61BC42A720BBD33CB09A410E48BB79A61012C74CB8B3E75F26D988485CF381">>},
|
||||
{<<"jose">>, <<"0D6CD36FF8BA174DB29148FC112B5842186B68A90CE9FC2B3EC3AFE76593E614">>},
|
||||
{<<"luerl">>, <<"1B4B9D0CA5D7D280D1D2787A6A5EE9F5A212641B62BFF91556BAA53805DF3AED">>},
|
||||
{<<"mqtree">>, <<"5FE8B7CF8FBC4783D0FCEB94654AC2BBF3242A58CD0397D249DED8AE021BE2A3">>},
|
||||
{<<"p1_acme">>, <<"A7B55B47495DDB4F98A15E65451EC3AD43F4637B955C74CD695D98E6A645D08C">>},
|
||||
{<<"mqtree">>, <<"C81065715C49A1882812F80A5AE2D842E80DD3F2D130530DF35990248BF8CE3C">>},
|
||||
{<<"p1_acme">>, <<"CE686986DE3F9D5FD285AFE87523CB45329A349C6C6BE7ACC1ED916725D46423">>},
|
||||
{<<"p1_mysql">>, <<"EA138083F2C54719B9CF549DBF5802A288B0019EA3E5449B354C74CC03FAFDEC">>},
|
||||
{<<"p1_oauth2">>, <<"1FD3AC474E43722D9D5A87C6DF8D36F698ED87AF7BB81CBBB66361451D99AE8F">>},
|
||||
{<<"p1_pgsql">>, <<"268B01E8F4EB75C211A31495A25C2815C549AECCE2F0DF1A161C6E0A2CDE061E">>},
|
||||
{<<"p1_utils">>, <<"F1AF942B0A62BCFA0D59FBE30679BE4FFEB5E241A0C49ED5F094DB2F5B80F5E0">>},
|
||||
{<<"p1_pgsql">>, <<"E99594446C411C660696795B062336F5C4BD800451D8F620BB4D4CE304E255C2">>},
|
||||
{<<"p1_utils">>, <<"C49BD44BC4A40AD996691AF826DD7E0AA56D4D0CD730817190A1F84D1A7F0033">>},
|
||||
{<<"pkix">>, <<"E02164F83094CB124C41B1AB28988A615D54B9ADC38575F00F19A597A3AC5D0E">>},
|
||||
{<<"sqlite3">>, <<"3C0BA4E13322C2AD49DE4E2DDD28311366ADDE54BEAE8DBA9D9E3888F69D2857">>},
|
||||
{<<"stringprep">>, <<"E9699C88E8DB16B3A41F0E45AC6874A4DA81A6E4854A77D76EDE6D09B08E3530">>},
|
||||
{<<"stun">>, <<"6B318244C21E8524A9AAE3AC9A05CD8234EE994C1C2C815DE68D306086AD768D">>},
|
||||
{<<"unicode_util_compat">>, <<"25EEE6D67DF61960CF6A794239566599B09E17E668D3700247BC498638152521">>},
|
||||
{<<"xmpp">>, <<"CEEAE43B8FE97649D8F8546B3F7F2B38ECFC931C0CDD5C7445FFB3F80FCB7D85">>},
|
||||
{<<"yconf">>, <<"FA950EC6503F92D6417FB8CC1D982403F041697E8E1BBF4D4588FB919B9562EA">>}]}
|
||||
{<<"stringprep">>, <<"96F8B30BC50887F605B33B46BCA1D248C19A879319B8C482790E3B4DA5DA98C0">>},
|
||||
{<<"stun">>, <<"3D7FE8EFB9D05B240A6AA9A6BF8B8B7BFF2D802895D170443C588987DC1E12D9">>},
|
||||
{<<"unicode_util_compat">>, <<"B3A917854CE3AE233619744AD1E0102E05673136776FB2FA76234F3E03B23642">>},
|
||||
{<<"xmpp">>, <<"A5C933DF904AB3CEC15425DA334E410CE84EC3AE7B81EFE069E5DB368A7B3716">>},
|
||||
{<<"yconf">>, <<"C524A5F1FD86875D85B469CC2E368C204F97CCA1C3918736E21F5001C01D096C">>}]}
|
||||
].
|
||||
|
||||
+1
-1
@@ -18,7 +18,7 @@
|
||||
|
||||
CREATE TABLE users (
|
||||
username varchar(191) NOT NULL,
|
||||
type smallint NOT NULL,,
|
||||
type smallint NOT NULL,
|
||||
password text NOT NULL,
|
||||
serverkey varchar(128) NOT NULL DEFAULT '',
|
||||
salt varchar(128) NOT NULL DEFAULT '',
|
||||
|
||||
+1
-1
@@ -20,7 +20,7 @@
|
||||
|
||||
-- ALTER TABLE users ADD COLUMN server_host text NOT NULL DEFAULT '<HOST>';
|
||||
-- ALTER TABLE users DROP CONSTRAINT users_pkey;
|
||||
-- ALTER TABLE users ADD PRIMARY KEY (server_host, username);
|
||||
-- ALTER TABLE users ADD PRIMARY KEY (server_host, username, "type");
|
||||
-- ALTER TABLE users ALTER COLUMN server_host DROP DEFAULT;
|
||||
|
||||
-- ALTER TABLE last ADD COLUMN server_host text NOT NULL DEFAULT '<HOST>';
|
||||
|
||||
+1
-1
@@ -18,7 +18,7 @@
|
||||
|
||||
CREATE TABLE users (
|
||||
username text NOT NULL,
|
||||
"type" smallint NOT NULL
|
||||
"type" smallint NOT NULL,
|
||||
"password" text NOT NULL,
|
||||
serverkey text NOT NULL DEFAULT '',
|
||||
salt text NOT NULL DEFAULT '',
|
||||
|
||||
@@ -482,6 +482,8 @@ domain() ->
|
||||
non_empty(binary()),
|
||||
fun(Val) ->
|
||||
try jid:tolower(jid:decode(Val)) of
|
||||
{<<"">>, <<"xn--", _/binary>> = Domain, <<"">>} ->
|
||||
unicode:characters_to_binary(idna:decode(binary_to_list(Domain)), utf8);
|
||||
{<<"">>, Domain, <<"">>} -> Domain;
|
||||
_ -> fail({bad_domain, Val})
|
||||
catch _:{bad_jid, _} ->
|
||||
|
||||
@@ -42,7 +42,6 @@
|
||||
-protocol({xep, 388, '0.4.0', '24.02', "complete", ""}).
|
||||
-protocol({xep, 440, '0.4.0', '24.02', "complete", ""}).
|
||||
-protocol({xep, 474, '0.4.0', '24.02', "complete", "0.4.0 since 25.03"}).
|
||||
-protocol({xep, 485, '0.2.0', '24.02', "complete", "mod_pubsub_serverinfo in ejabberd-contrib.git"}).
|
||||
|
||||
-export([start/0, stop/0, halt/0, start_app/1, start_app/2,
|
||||
get_pid_file/0, check_apps/0, module_name/1, is_loaded/0]).
|
||||
|
||||
@@ -44,6 +44,7 @@ start(normal, _Args) ->
|
||||
ejabberd_logger:start(),
|
||||
write_pid_file(),
|
||||
start_included_apps(),
|
||||
misc:warn_unset_home(),
|
||||
start_elixir_application(),
|
||||
setup_if_elixir_conf_used(),
|
||||
case ejabberd_config:load() of
|
||||
@@ -108,6 +109,7 @@ prep_stop(State) ->
|
||||
ejabberd_service:stop(),
|
||||
ejabberd_s2s:stop(),
|
||||
ejabberd_system_monitor:stop(),
|
||||
gen_mod:prep_stop(),
|
||||
gen_mod:stop(),
|
||||
State.
|
||||
|
||||
@@ -176,6 +178,10 @@ file_queue_init() ->
|
||||
Err -> throw({?MODULE, Err})
|
||||
end.
|
||||
|
||||
%%%
|
||||
%%% Elixir
|
||||
%%%
|
||||
|
||||
-ifdef(ELIXIR_ENABLED).
|
||||
is_using_elixir_config() ->
|
||||
Config = ejabberd_config:path(),
|
||||
|
||||
@@ -237,6 +237,7 @@ check_password(User, AuthzId, Server, Password, Digest, DigestGen) ->
|
||||
case check_password_with_authmodule(
|
||||
User, AuthzId, Server, Password, Digest, DigestGen) of
|
||||
{true, _AuthModule} -> true;
|
||||
{false, _ErrorAtom, _Reason} -> false;
|
||||
false -> false
|
||||
end.
|
||||
|
||||
@@ -423,9 +424,8 @@ count_users(Server, Opts) ->
|
||||
|
||||
-spec get_password(binary(), binary()) -> false | [password()].
|
||||
get_password(User, Server) ->
|
||||
case get_password_with_authmodule(User, Server) of
|
||||
{Passwords, _} -> Passwords
|
||||
end.
|
||||
{Passwords, _} = get_password_with_authmodule(User, Server),
|
||||
Passwords.
|
||||
|
||||
-spec get_password_s(binary(), binary()) -> password().
|
||||
get_password_s(User, Server) ->
|
||||
@@ -756,7 +756,7 @@ db_set_password(User, Server, PlainPassword, Passwords, Mod) ->
|
||||
end
|
||||
end,
|
||||
case Ret of
|
||||
{ok, _} -> ok;
|
||||
{ok, _} -> ejabberd_hooks:run(set_password, Server, [User, Server]);
|
||||
{error, _} = Err -> Err
|
||||
end.
|
||||
|
||||
@@ -815,7 +815,7 @@ db_user_exists(User, Server, Mod) ->
|
||||
end,
|
||||
case Val of
|
||||
{ok, _} ->
|
||||
{true, Mod /= ejabberd_auth_anonymous} ;
|
||||
{true, Mod /= ejabberd_auth_anonymous};
|
||||
not_found ->
|
||||
{false, Mod /= ejabberd_auth_anonymous};
|
||||
error ->
|
||||
|
||||
@@ -151,13 +151,7 @@ get_users(Server, []) ->
|
||||
Users = mnesia:dirty_select(passwd,
|
||||
[{#passwd{us = '$1', _ = '_'},
|
||||
[{'==', {element, 2, '$1'}, Server}], ['$1']}]),
|
||||
{_, Res} = lists:foldl(
|
||||
fun({U, S, _}, {{U2, S2}, _} = Acc) when U == U2 andalso S == S2 ->
|
||||
Acc;
|
||||
({U, S, _}, {_, Res}) ->
|
||||
{{U, S}, [{U, S} | Res]}
|
||||
end, {{none, none}, []}, Users),
|
||||
Res;
|
||||
misc:lists_uniq([{U, S} || {U, S, _} <- Users]);
|
||||
get_users(Server, [{from, Start}, {to, End}])
|
||||
when is_integer(Start) and is_integer(End) ->
|
||||
get_users(Server, [{limit, End - Start + 1}, {offset, Start}]);
|
||||
|
||||
+13
-16
@@ -258,13 +258,6 @@ drop_password_type(LServer, Hash) ->
|
||||
?SQL("delete from users"
|
||||
" where type=%(Type)d and %(LServer)H")).
|
||||
|
||||
scram_hash_encode(Hash, StoreKey) ->
|
||||
case Hash of
|
||||
sha -> StoreKey;
|
||||
sha256 -> <<"sha256:", StoreKey/binary>>;
|
||||
sha512 -> <<"sha512:", StoreKey/binary>>
|
||||
end.
|
||||
|
||||
set_password_scram_t(LUser, LServer, Hash,
|
||||
StoredKey, ServerKey, Salt, IterationCount) ->
|
||||
Type = hash_to_num(Hash),
|
||||
@@ -406,40 +399,44 @@ which_users_exists(LServer, LUsers) ->
|
||||
|
||||
export(_Server) ->
|
||||
[{passwd,
|
||||
fun(Host, #passwd{us = {LUser, LServer}, password = Password})
|
||||
fun(Host, #passwd{us = {LUser, LServer, plain}, password = Password})
|
||||
when LServer == Host,
|
||||
is_binary(Password) ->
|
||||
[?SQL("delete from users where username=%(LUser)s and %(LServer)H;"),
|
||||
[?SQL("delete from users where username=%(LUser)s and type=1 and %(LServer)H;"),
|
||||
?SQL_INSERT(
|
||||
"users",
|
||||
["username=%(LUser)s",
|
||||
"server_host=%(LServer)s",
|
||||
"type=1",
|
||||
"password=%(Password)s"])];
|
||||
(Host, {passwd, {LUser, LServer},
|
||||
{scram, StoredKey1, ServerKey, Salt, IterationCount}})
|
||||
(Host, {passwd, {LUser, LServer, _},
|
||||
{scram, StoredKey, ServerKey, Salt, IterationCount}})
|
||||
when LServer == Host ->
|
||||
Hash = sha,
|
||||
StoredKey = scram_hash_encode(Hash, StoredKey1),
|
||||
[?SQL("delete from users where username=%(LUser)s and %(LServer)H;"),
|
||||
Type = hash_to_num(Hash),
|
||||
[?SQL("delete from users where username=%(LUser)s and type=%(Type)d and %(LServer)H;"),
|
||||
?SQL_INSERT(
|
||||
"users",
|
||||
["username=%(LUser)s",
|
||||
"server_host=%(LServer)s",
|
||||
"type=%(Type)d",
|
||||
"password=%(StoredKey)s",
|
||||
"serverkey=%(ServerKey)s",
|
||||
"salt=%(Salt)s",
|
||||
"iterationcount=%(IterationCount)d"])];
|
||||
(Host, #passwd{us = {LUser, LServer}, password = #scram{} = Scram})
|
||||
(Host, #passwd{us = {LUser, LServer, _}, password = #scram{} = Scram})
|
||||
when LServer == Host ->
|
||||
StoredKey = scram_hash_encode(Scram#scram.hash, Scram#scram.storedkey),
|
||||
StoredKey = Scram#scram.storedkey,
|
||||
ServerKey = Scram#scram.serverkey,
|
||||
Salt = Scram#scram.salt,
|
||||
IterationCount = Scram#scram.iterationcount,
|
||||
[?SQL("delete from users where username=%(LUser)s and %(LServer)H;"),
|
||||
Type = hash_to_num(Scram#scram.hash),
|
||||
[?SQL("delete from users where username=%(LUser)s and type=%(Type)d and %(LServer)H;"),
|
||||
?SQL_INSERT(
|
||||
"users",
|
||||
["username=%(LUser)s",
|
||||
"server_host=%(LServer)s",
|
||||
"type=%(Type)d",
|
||||
"password=%(StoredKey)s",
|
||||
"serverkey=%(ServerKey)s",
|
||||
"salt=%(Salt)s",
|
||||
|
||||
+28
-17
@@ -416,8 +416,8 @@ unauthenticated_stream_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]).
|
||||
inline_stream_features(#{lserver := LServer} = State) ->
|
||||
ejabberd_hooks:run_fold(c2s_inline_features, LServer, {[], [], []}, [LServer, State]).
|
||||
|
||||
sasl_mechanisms(Mechs, #{lserver := LServer, stream_encrypted := Encrypted} = State) ->
|
||||
Type = ejabberd_auth:store_type(LServer),
|
||||
@@ -440,21 +440,32 @@ sasl_mechanisms(Mechs, #{lserver := LServer, stream_encrypted := Encrypted} = St
|
||||
end,
|
||||
%% 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">>) -> Digest;
|
||||
(<<"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).
|
||||
Mechs2 = lists:filter(
|
||||
fun(<<"ANONYMOUS">>) ->
|
||||
ejabberd_auth_anonymous:is_sasl_anonymous_enabled(LServer);
|
||||
(<<"DIGEST-MD5">>) -> Digest;
|
||||
(<<"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),
|
||||
case ejabberd_option:auth_password_types_hidden_in_sasl1() of
|
||||
[] -> Mechs2;
|
||||
List ->
|
||||
Mechs3 = lists:foldl(
|
||||
fun(plain, Acc) -> Acc -- [<<"PLAIN">>];
|
||||
(scram_sha1, Acc) -> Acc -- [<<"SCRAM-SHA-1">>, <<"SCRAM-SHA-1-PLUS">>];
|
||||
(scram_sha256, Acc) -> Acc -- [<<"SCRAM-SHA-256">>, <<"SCRAM-SHA-256-PLUS">>];
|
||||
(scram_sha512, Acc) -> Acc -- [<<"SCRAM-SHA-512">>, <<"SCRAM-SHA-512-PLUS">>]
|
||||
end, Mechs2, List),
|
||||
{Mechs3, Mechs2}
|
||||
end.
|
||||
|
||||
sasl_options(#{lserver := LServer}) ->
|
||||
case ejabberd_option:disable_sasl_scram_downgrade_protection(LServer) of
|
||||
|
||||
@@ -398,7 +398,7 @@ gen_doc(#ejabberd_commands{name=Name, tags=Tags, desc=Desc, longdesc=LongDesc,
|
||||
TagsText = ?RAW(string:join(["_`"++atom_to_list(Tag)++"`_" || Tag <- Tags], ", ")),
|
||||
IsDefinerMod = case Definer of
|
||||
unknown -> false;
|
||||
_ -> lists:member(gen_mod, proplists:get_value(behaviour, Definer:module_info(attributes)))
|
||||
_ -> lists:member(gen_mod, lists:flatten(proplists:get_all_values(behaviour, Definer:module_info(attributes))))
|
||||
end,
|
||||
ModuleText = case IsDefinerMod of
|
||||
true ->
|
||||
|
||||
+67
-21
@@ -39,6 +39,7 @@
|
||||
-export([callback_modules/1]).
|
||||
-export([set_option/2]).
|
||||
-export([get_defined_keywords/1, get_predefined_keywords/1, replace_keywords/2, replace_keywords/3]).
|
||||
-export([resolve_host_alias/1]).
|
||||
|
||||
%% Deprecated functions
|
||||
-export([get_option/2]).
|
||||
@@ -263,30 +264,31 @@ version() ->
|
||||
|
||||
-spec default_db(binary() | global, module()) -> atom().
|
||||
default_db(Host, Module) ->
|
||||
default_db(default_db, Host, Module, mnesia).
|
||||
default_db(default_db, db_type, Host, Module, mnesia).
|
||||
|
||||
-spec default_db(binary() | global, module(), atom()) -> atom().
|
||||
default_db(Host, Module, Default) ->
|
||||
default_db(default_db, Host, Module, Default).
|
||||
default_db(default_db, db_type, Host, Module, Default).
|
||||
|
||||
-spec default_ram_db(binary() | global, module()) -> atom().
|
||||
default_ram_db(Host, Module) ->
|
||||
default_db(default_ram_db, Host, Module, mnesia).
|
||||
default_db(default_ram_db, ram_db_type, Host, Module, mnesia).
|
||||
|
||||
-spec default_ram_db(binary() | global, module(), atom()) -> atom().
|
||||
default_ram_db(Host, Module, Default) ->
|
||||
default_db(default_ram_db, Host, Module, Default).
|
||||
default_db(default_ram_db, ram_db_type, Host, Module, Default).
|
||||
|
||||
-spec default_db(default_db | default_ram_db, binary() | global, module(), atom()) -> atom().
|
||||
default_db(Opt, Host, Mod, Default) ->
|
||||
-spec default_db(default_db | default_ram_db, db_type | ram_db_type, binary() | global, module(), atom()) -> atom().
|
||||
default_db(Opt, ModOpt, Host, Mod, Default) ->
|
||||
Type = get_option({Opt, Host}),
|
||||
DBMod = list_to_atom(atom_to_list(Mod) ++ "_" ++ atom_to_list(Type)),
|
||||
case code:ensure_loaded(DBMod) of
|
||||
{module, _} -> Type;
|
||||
{error, _} ->
|
||||
?WARNING_MSG("Module ~ts doesn't support database '~ts' "
|
||||
"defined in option '~ts', using "
|
||||
"'~ts' as fallback", [Mod, Type, Opt, Default]),
|
||||
"defined in toplevel option '~ts': will use the value "
|
||||
"set in ~ts option '~ts', or '~ts' as fallback",
|
||||
[Mod, Type, Opt, Mod, ModOpt, Default]),
|
||||
Default
|
||||
end.
|
||||
|
||||
@@ -340,7 +342,12 @@ may_hide_data(Data) ->
|
||||
-spec env_binary_to_list(atom(), atom()) -> {ok, any()} | undefined.
|
||||
env_binary_to_list(Application, Parameter) ->
|
||||
%% Application need to be loaded to allow setting parameters
|
||||
application:load(Application),
|
||||
case proplists:is_defined(Application, application:loaded_applications()) of
|
||||
true ->
|
||||
ok;
|
||||
false ->
|
||||
application:load(Application)
|
||||
end,
|
||||
case application:get_env(Application, Parameter) of
|
||||
{ok, Val} when is_binary(Val) ->
|
||||
BVal = binary_to_list(Val),
|
||||
@@ -501,15 +508,62 @@ get_predefined_keywords(Host) ->
|
||||
global ->
|
||||
[];
|
||||
_ ->
|
||||
[{<<"HOST">>, Host}]
|
||||
[{<<"HOST">>, Host}, {<<"HOST_URL_ENCODE">>, misc:url_encode(Host)}]
|
||||
end,
|
||||
{ok, [[Home]]} = init:get_argument(home),
|
||||
Home = misc:get_home(),
|
||||
ConfigDirPath =
|
||||
iolist_to_binary(filename:dirname(
|
||||
ejabberd_config:path())),
|
||||
LogDirPath =
|
||||
iolist_to_binary(filename:dirname(
|
||||
ejabberd_logger:get_log_path())),
|
||||
HostList
|
||||
++ [{<<"HOME">>, list_to_binary(Home)},
|
||||
{<<"CONFIG_PATH">>, ConfigDirPath},
|
||||
{<<"LOG_PATH">>, LogDirPath},
|
||||
{<<"SEMVER">>, ejabberd_option:version()},
|
||||
{<<"VERSION">>,
|
||||
misc:semver_to_xxyy(
|
||||
ejabberd_option:version())}].
|
||||
|
||||
resolve_host_alias(Host) ->
|
||||
case lists:member(Host, ejabberd_option:hosts()) of
|
||||
true ->
|
||||
Host;
|
||||
false ->
|
||||
resolve_host_alias2(Host)
|
||||
end.
|
||||
|
||||
resolve_host_alias2(Host) ->
|
||||
Result =
|
||||
lists:filter(fun({Alias1, _Vhost}) -> is_glob_match(Host, Alias1) end,
|
||||
ejabberd_option:hosts_alias()),
|
||||
case Result of
|
||||
[{_, Vhost} | _] when is_binary(Vhost) ->
|
||||
?DEBUG("(~p) Alias host '~s' resolved into vhost '~s'", [self(), Host, Vhost]),
|
||||
Vhost;
|
||||
[] ->
|
||||
?DEBUG("(~p) Request sent to host '~s', which isn't a vhost or an alias",
|
||||
[self(), Host]),
|
||||
Host
|
||||
end.
|
||||
|
||||
%% Copied from ejabberd-2.0.0/src/acl.erl
|
||||
is_regexp_match(String, RegExp) ->
|
||||
case ejabberd_regexp:run(String, RegExp) of
|
||||
nomatch ->
|
||||
false;
|
||||
match ->
|
||||
true;
|
||||
{error, ErrDesc} ->
|
||||
io:format("Wrong regexp ~p in ACL: ~p", [RegExp, ErrDesc]),
|
||||
false
|
||||
end.
|
||||
|
||||
is_glob_match(String, <<"!", Glob/binary>>) ->
|
||||
not is_regexp_match(String, ejabberd_regexp:sh_to_awk(Glob));
|
||||
is_glob_match(String, Glob) ->
|
||||
is_regexp_match(String, ejabberd_regexp:sh_to_awk(Glob)).
|
||||
%% @format-end
|
||||
|
||||
%%%===================================================================
|
||||
@@ -578,15 +632,7 @@ callback_modules(external) ->
|
||||
end
|
||||
end, beams(external));
|
||||
callback_modules(all) ->
|
||||
lists_uniq(callback_modules(local) ++ callback_modules(external)).
|
||||
|
||||
-ifdef(OTP_BELOW_25).
|
||||
lists_uniq(List) ->
|
||||
lists:usort(List).
|
||||
-else.
|
||||
lists_uniq(List) ->
|
||||
lists:uniq(List).
|
||||
-endif.
|
||||
misc:lists_uniq(callback_modules(local) ++ callback_modules(external)).
|
||||
|
||||
-spec validators(module(), [atom()], [any()]) -> econf:validators().
|
||||
validators(Mod, Disallowed, DK) ->
|
||||
@@ -656,7 +702,7 @@ get_additional_macros() ->
|
||||
|
||||
parse_macro_string(MacroString) ->
|
||||
[NameString, ValueString] = string:split(MacroString, "="),
|
||||
{ok, [ValueDecoded]} = fast_yaml:decode(ValueString, [plain_as_atom]),
|
||||
{ok, [ValueDecoded]} = fast_yaml:decode(ValueString),
|
||||
{list_to_atom(NameString), ValueDecoded}.
|
||||
|
||||
read_yaml_files(Files, Opts) ->
|
||||
|
||||
@@ -230,6 +230,8 @@ filter(_Host, captcha_host, _, _) ->
|
||||
filter(_Host, route_subdomains, _, _) ->
|
||||
warn_removed_option(route_subdomains, s2s_access),
|
||||
false;
|
||||
filter(_Host, auth_password_types_hidden_in_scram1, Val, _) ->
|
||||
{true, {auth_password_types_hidden_in_sasl1, Val}};
|
||||
filter(Host, modules, ModOpts, State) ->
|
||||
NoDialbackHosts = maps:get(remove_s2s_dialback, State, []),
|
||||
ModOpts1 = lists:filter(
|
||||
|
||||
@@ -1148,6 +1148,17 @@ get_commands_spec() ->
|
||||
desc = "Get list of commands, or help of a command (only ejabberdctl)",
|
||||
longdesc = "This command is exclusive for the ejabberdctl command-line script, "
|
||||
"don't attempt to execute it using any other API frontend."},
|
||||
#ejabberd_commands{name = mnesia_change, tags = [ejabberdctl, mnesia],
|
||||
desc = "Change the erlang node name in the mnesia database (only ejabberdctl)",
|
||||
longdesc = "This command internally calls the _`mnesia_change_nodename`_ API. "
|
||||
"This is a special command that starts and stops ejabberd several times: "
|
||||
"do not attempt to run this command when ejabberd is running. "
|
||||
"This command is exclusive for the ejabberdctl command-line script, "
|
||||
"don't attempt to execute it using any other API frontend.",
|
||||
note = "added in 25.08",
|
||||
args = [{old_node_name, string}],
|
||||
args_desc = ["Old erlang node name"],
|
||||
args_example = ["ejabberd@oldmachine"]},
|
||||
#ejabberd_commands{name = mnesia_info_ctl, tags = [ejabberdctl, mnesia],
|
||||
desc = "Show information of Mnesia system (only ejabberdctl)",
|
||||
note = "renamed in 24.02",
|
||||
|
||||
+33
-2
@@ -24,6 +24,7 @@
|
||||
%% API
|
||||
-export([man/0, man/1, have_a2x/0]).
|
||||
|
||||
-include("ejabberd_commands.hrl").
|
||||
-include("translate.hrl").
|
||||
|
||||
%%%===================================================================
|
||||
@@ -46,7 +47,8 @@ man(Lang) ->
|
||||
DocOpts = maps:get(opts, Map, []),
|
||||
Example = maps:get(example, Map, []),
|
||||
Note = maps:get(note, Map, []),
|
||||
{[{M, Descr, DocOpts, #{example => Example, note => Note}}|Mods], SubMods};
|
||||
Apitags = get_module_apitags(M),
|
||||
{[{M, Descr, DocOpts, #{example => Example, note => Note, apitags => Apitags}}|Mods], SubMods};
|
||||
#{opts := DocOpts} ->
|
||||
{ParentMod, Backend} = strip_backend_suffix(M),
|
||||
{Mods, dict:append(ParentMod, {M, Backend, DocOpts}, SubMods)};
|
||||
@@ -113,7 +115,8 @@ man(Lang) ->
|
||||
format_versions(Lang, Example) ++ [io_lib:nl()] ++
|
||||
tr_multi(Lang, Descr) ++ [io_lib:nl()] ++
|
||||
opts_to_man(Lang, [{M, '', DocOpts}|Backends]) ++
|
||||
format_example(0, Lang, Example)
|
||||
format_example(0, Lang, Example) ++ [io_lib:nl()] ++
|
||||
format_apitags(Lang, Example)
|
||||
end, lists:keysort(1, ModDoc1)),
|
||||
ListenOptions =
|
||||
[io_lib:nl(),
|
||||
@@ -190,6 +193,34 @@ format_versions(_Lang, #{note := Note}) when Note /= [] ->
|
||||
format_versions(_, _) ->
|
||||
[].
|
||||
|
||||
%% @format-begin
|
||||
get_module_apitags(M) ->
|
||||
AllCommands = ejabberd_commands:get_commands_definition(),
|
||||
Tags = [C#ejabberd_commands.tags || C <- AllCommands, C#ejabberd_commands.module == M],
|
||||
TagsClean =
|
||||
lists:sort(
|
||||
misc:lists_uniq(
|
||||
lists:flatten(Tags))),
|
||||
TagsStrings = [atom_to_list(C) || C <- TagsClean],
|
||||
TagFiltering =
|
||||
fun ("internal") ->
|
||||
false;
|
||||
([$v | Rest]) ->
|
||||
{error, no_integer} == string:to_integer(Rest);
|
||||
(_) ->
|
||||
true
|
||||
end,
|
||||
TagsFiltered = lists:filter(TagFiltering, TagsStrings),
|
||||
TagsUrls =
|
||||
[["_`../../developer/ejabberd-api/admin-tags.md#", C, "|", C, "`_"] || C <- TagsFiltered],
|
||||
lists:join(", ", TagsUrls).
|
||||
|
||||
format_apitags(_Lang, #{apitags := TagsString}) when TagsString /= "" ->
|
||||
["**API Tags:** ", TagsString];
|
||||
format_apitags(_, _) ->
|
||||
[].
|
||||
%% @format-end
|
||||
|
||||
format_desc(Lang, #{desc := Desc}) ->
|
||||
tr_multi(Lang, Desc).
|
||||
|
||||
|
||||
+4
-25
@@ -66,7 +66,6 @@
|
||||
request_headers = [],
|
||||
end_of_request = false,
|
||||
options = [],
|
||||
default_host,
|
||||
custom_headers,
|
||||
trail = <<>>,
|
||||
allow_unencrypted_sasl2,
|
||||
@@ -170,9 +169,8 @@ send_file(State, Fd, Size, FileName) ->
|
||||
try
|
||||
case State#state.sockmod of
|
||||
gen_tcp ->
|
||||
case file:sendfile(Fd, State#state.socket, 0, Size, []) of
|
||||
{ok, _} -> ok
|
||||
end;
|
||||
{ok, _} = file:sendfile(Fd, State#state.socket, 0, Size, []),
|
||||
ok;
|
||||
_ ->
|
||||
case file:read(Fd, ?SEND_BUF) of
|
||||
{ok, Data} ->
|
||||
@@ -275,7 +273,7 @@ process_header(State, Data) ->
|
||||
request_headers = add_header(Name, Langs, State)};
|
||||
{ok, {http_header, _, 'Host' = Name, _, Value}} ->
|
||||
{Host, Port, TP} = get_transfer_protocol(State#state.addr_re, SockMod, Value),
|
||||
State#state{request_host = Host,
|
||||
State#state{request_host = ejabberd_config:resolve_host_alias(Host),
|
||||
request_port = Port,
|
||||
request_tp = TP,
|
||||
request_headers = add_header(Name, Value, State)};
|
||||
@@ -301,7 +299,6 @@ process_header(State, Data) ->
|
||||
#state{sockmod = SockMod, socket = Socket,
|
||||
trail = State3#state.trail,
|
||||
options = State#state.options,
|
||||
default_host = State#state.default_host,
|
||||
custom_headers = State#state.custom_headers,
|
||||
request_handlers = State#state.request_handlers,
|
||||
addr_re = State#state.addr_re};
|
||||
@@ -309,7 +306,6 @@ process_header(State, Data) ->
|
||||
#state{end_of_request = true,
|
||||
trail = State3#state.trail,
|
||||
options = State#state.options,
|
||||
default_host = State#state.default_host,
|
||||
custom_headers = State#state.custom_headers,
|
||||
request_handlers = State#state.request_handlers,
|
||||
addr_re = State#state.addr_re}
|
||||
@@ -317,7 +313,6 @@ process_header(State, Data) ->
|
||||
_ ->
|
||||
#state{end_of_request = true,
|
||||
options = State#state.options,
|
||||
default_host = State#state.default_host,
|
||||
custom_headers = State#state.custom_headers,
|
||||
request_handlers = State#state.request_handlers,
|
||||
addr_re = State#state.addr_re}
|
||||
@@ -723,7 +718,7 @@ file_format_error(Reason) ->
|
||||
url_decode_q_split_normalize(Path) ->
|
||||
{NPath, Query} = url_decode_q_split(Path),
|
||||
LPath = normalize_path([NPE
|
||||
|| NPE <- str:tokens(path_decode(NPath), <<"/">>)]),
|
||||
|| NPE <- str:tokens(misc:uri_decode(NPath), <<"/">>)]),
|
||||
{LPath, Query}.
|
||||
|
||||
% Code below is taken (with some modifications) from the yaws webserver, which
|
||||
@@ -751,19 +746,6 @@ url_decode_q_split(<<H, T/binary>>, Acc) when H /= 0 ->
|
||||
url_decode_q_split(<<>>, Ack) ->
|
||||
{path_norm_reverse(Ack), <<>>}.
|
||||
|
||||
%% @doc Decode a part of the URL and return string()
|
||||
path_decode(Path) -> path_decode(Path, <<>>).
|
||||
|
||||
path_decode(<<$%, Hi, Lo, Tail/binary>>, Acc) ->
|
||||
Hex = list_to_integer([Hi, Lo], 16),
|
||||
if Hex == 0 -> exit(badurl);
|
||||
true -> ok
|
||||
end,
|
||||
path_decode(Tail, <<Acc/binary, Hex>>);
|
||||
path_decode(<<H, T/binary>>, Acc) when H /= 0 ->
|
||||
path_decode(T, <<Acc/binary, H>>);
|
||||
path_decode(<<>>, Acc) -> Acc.
|
||||
|
||||
path_norm_reverse(<<"/", T/binary>>) -> start_dir(0, <<"/">>, T);
|
||||
path_norm_reverse(T) -> start_dir(0, <<"">>, T).
|
||||
|
||||
@@ -927,8 +909,6 @@ listen_opt_type(request_handlers) ->
|
||||
econf:binary(),
|
||||
fun(Path) -> str:tokens(Path, <<"/">>) end),
|
||||
econf:beam([[{socket_handoff, 3}, {process, 2}]]));
|
||||
listen_opt_type(default_host) ->
|
||||
econf:domain();
|
||||
listen_opt_type(custom_headers) ->
|
||||
econf:map(
|
||||
econf:binary(),
|
||||
@@ -944,5 +924,4 @@ listen_options() ->
|
||||
{allow_unencrypted_sasl2, false},
|
||||
{request_handlers, []},
|
||||
{tag, <<>>},
|
||||
{default_host, undefined},
|
||||
{custom_headers, []}].
|
||||
|
||||
+68
-39
@@ -113,12 +113,12 @@ init({Port, _, udp} = EndPoint, Module, Opts, SockOpts) ->
|
||||
{Port, SockOpts}
|
||||
end,
|
||||
ExtraOpts2 = lists:keydelete(send_timeout, 1, ExtraOpts),
|
||||
case gen_udp:open(Port2, [binary,
|
||||
case {gen_udp:open(Port2, [binary,
|
||||
{active, false},
|
||||
{reuseaddr, true} |
|
||||
ExtraOpts2]) of
|
||||
{ok, Socket} ->
|
||||
set_definitive_udsocket(Port, Opts),
|
||||
ExtraOpts2]),
|
||||
set_definitive_udsocket(Port, Opts)} of
|
||||
{{ok, Socket}, ok} ->
|
||||
misc:set_proc_label({?MODULE, udp, Port}),
|
||||
case inet:sockname(Socket) of
|
||||
{ok, {Addr, Port1}} ->
|
||||
@@ -139,18 +139,18 @@ init({Port, _, udp} = EndPoint, Module, Opts, SockOpts) ->
|
||||
{error, _} ->
|
||||
ok
|
||||
end;
|
||||
{error, Reason} = Err ->
|
||||
report_socket_error(Reason, EndPoint, Module),
|
||||
proc_lib:init_ack(Err)
|
||||
{error, Reason} ->
|
||||
return_socket_error(Reason, EndPoint, Module)
|
||||
end;
|
||||
{error, Reason} = Err ->
|
||||
report_socket_error(Reason, EndPoint, Module),
|
||||
proc_lib:init_ack(Err)
|
||||
{{error, Reason}, _} ->
|
||||
return_socket_error(Reason, EndPoint, Module);
|
||||
{_, {error, Reason} } ->
|
||||
return_socket_error(Reason, EndPoint, Module)
|
||||
end;
|
||||
init({Port, _, tcp} = EndPoint, Module, Opts, SockOpts) ->
|
||||
case listen_tcp(Port, SockOpts) of
|
||||
{ok, ListenSocket} ->
|
||||
set_definitive_udsocket(Port, Opts),
|
||||
case {listen_tcp(Port, SockOpts),
|
||||
set_definitive_udsocket(Port, Opts)} of
|
||||
{{ok, ListenSocket}, ok} ->
|
||||
case inet:sockname(ListenSocket) of
|
||||
{ok, {Addr, Port1}} ->
|
||||
proc_lib:init_ack({ok, self()}),
|
||||
@@ -174,13 +174,13 @@ init({Port, _, tcp} = EndPoint, Module, Opts, SockOpts) ->
|
||||
{error, _} ->
|
||||
ok
|
||||
end;
|
||||
{error, Reason} = Err ->
|
||||
report_socket_error(Reason, EndPoint, Module),
|
||||
proc_lib:init_ack(Err)
|
||||
{error, Reason} ->
|
||||
return_socket_error(Reason, EndPoint, Module)
|
||||
end;
|
||||
{error, Reason} = Err ->
|
||||
report_socket_error(Reason, EndPoint, Module),
|
||||
proc_lib:init_ack(Err)
|
||||
{{error, Reason}, _} ->
|
||||
return_socket_error(Reason, EndPoint, Module);
|
||||
{_, {error, Reason}} ->
|
||||
return_socket_error(Reason, EndPoint, Module)
|
||||
end.
|
||||
|
||||
-spec listen_tcp(inet:port_number(), [gen_tcp:option()]) ->
|
||||
@@ -216,23 +216,39 @@ listen_tcp(Port, SockOpts) ->
|
||||
|
||||
setup_provisional_udsocket_dir(DefinitivePath) ->
|
||||
ProvisionalPath = get_provisional_udsocket_path(DefinitivePath),
|
||||
?DEBUG("Creating a Unix Domain Socket provisional file at ~ts for the definitive path ~s",
|
||||
?INFO_MSG("Creating a Unix Domain Socket provisional file at ~ts for the definitive path ~s",
|
||||
[ProvisionalPath, DefinitivePath]),
|
||||
ProvisionalPath.
|
||||
ProvisionalPathAbsolute = relative_socket_to_mnesia(ProvisionalPath),
|
||||
create_base_dir(ProvisionalPathAbsolute),
|
||||
ProvisionalPathAbsolute.
|
||||
|
||||
get_provisional_udsocket_path(Path) ->
|
||||
PathBase64 = misc:term_to_base64(Path),
|
||||
PathBuild = filename:join(os:getenv("HOME"), PathBase64),
|
||||
%% Shorthen the path, a long path produces a crash when opening the socket.
|
||||
binary:part(PathBuild, {0, erlang:min(107, byte_size(PathBuild))}).
|
||||
ReproducibleSecret = binary:part(crypto:hash(sha, misc:atom_to_binary(erlang:get_cookie())), 1, 8),
|
||||
PathBase64 = misc:term_to_base64({ReproducibleSecret, Path}),
|
||||
PathBuild = filename:join(misc:get_home(), PathBase64),
|
||||
DestPath = filename:join(filename:dirname(Path), PathBase64),
|
||||
case {byte_size(DestPath) > 107, byte_size(PathBuild) > 107} of
|
||||
{false, _} ->
|
||||
DestPath;
|
||||
{true, false} ->
|
||||
?INFO_MSG("The provisional Unix Domain Socket path ~ts is longer than 107, let's use home directory instead which is ~p", [DestPath, byte_size(PathBuild)]),
|
||||
PathBuild;
|
||||
{true, true} ->
|
||||
?ERROR_MSG("The Unix Domain Socket path ~ts is too long, "
|
||||
"and I cannot create the provisional file safely. "
|
||||
"Please configure a shorter path and try again.", [Path]),
|
||||
throw({error_socket_path_too_long, Path})
|
||||
end.
|
||||
|
||||
get_definitive_udsocket_path(<<"unix", _>> = Unix) ->
|
||||
Unix;
|
||||
get_definitive_udsocket_path(ProvisionalPath) ->
|
||||
PathBase64 = filename:basename(ProvisionalPath),
|
||||
{term, Path} = misc:base64_to_term(PathBase64),
|
||||
{term, {_, Path}} = misc:base64_to_term(PathBase64),
|
||||
relative_socket_to_mnesia(Path).
|
||||
|
||||
-spec set_definitive_udsocket(integer() | binary(), opts()) -> ok | {error, file:posix() | badarg}.
|
||||
|
||||
set_definitive_udsocket(<<"unix:", Path/binary>>, Opts) ->
|
||||
Prov = get_provisional_udsocket_path(Path),
|
||||
Usd = maps:get(unix_socket, Opts),
|
||||
@@ -263,16 +279,19 @@ set_definitive_udsocket(<<"unix:", Path/binary>>, Opts) ->
|
||||
end
|
||||
end,
|
||||
FinalPath = relative_socket_to_mnesia(Path),
|
||||
FinalPathDir = filename:dirname(FinalPath),
|
||||
case file:make_dir(FinalPathDir) of
|
||||
create_base_dir(FinalPath),
|
||||
file:rename(Prov, FinalPath);
|
||||
set_definitive_udsocket(Port, _Opts) when is_integer(Port) ->
|
||||
ok.
|
||||
|
||||
create_base_dir(Path) ->
|
||||
Dirname = filename:dirname(Path),
|
||||
case file:make_dir(Dirname) of
|
||||
ok ->
|
||||
file:change_mode(FinalPathDir, 8#00700);
|
||||
file:change_mode(Dirname, 8#00700);
|
||||
_ ->
|
||||
ok
|
||||
end,
|
||||
file:rename(Prov, FinalPath);
|
||||
set_definitive_udsocket(_Port, _Opts) ->
|
||||
ok.
|
||||
end.
|
||||
|
||||
relative_socket_to_mnesia(Path1) ->
|
||||
case filename:pathtype(Path1) of
|
||||
@@ -297,11 +316,11 @@ maybe_delete_udsocket_file(_Port) ->
|
||||
split_opts(Transport, Opts) ->
|
||||
maps:fold(
|
||||
fun(Opt, Val, {ModOpts, SockOpts}) ->
|
||||
case OptVal = {Opt, Val} of
|
||||
case {Opt, Val} of
|
||||
{ip, _} ->
|
||||
{ModOpts, [OptVal|SockOpts]};
|
||||
{ModOpts, [{Opt, Val} | SockOpts]};
|
||||
{backlog, _} when Transport == tcp ->
|
||||
{ModOpts, [OptVal|SockOpts]};
|
||||
{ModOpts, [{Opt, Val} | SockOpts]};
|
||||
{backlog, _} ->
|
||||
{ModOpts, SockOpts};
|
||||
_ ->
|
||||
@@ -584,10 +603,20 @@ config_reloaded() ->
|
||||
end
|
||||
end, New).
|
||||
|
||||
-spec report_socket_error(inet:posix(), endpoint(), module()) -> ok.
|
||||
report_socket_error(Reason, EndPoint, Module) ->
|
||||
-spec return_socket_error(inet:posix(), endpoint(), module()) -> no_return().
|
||||
return_socket_error(Reason, EndPoint, Module) ->
|
||||
?ERROR_MSG("Failed to open socket at ~ts for ~ts: ~ts",
|
||||
[format_endpoint(EndPoint), Module, format_error(Reason)]).
|
||||
[format_endpoint(EndPoint), Module, format_error(Reason)]),
|
||||
return_init_error(Reason).
|
||||
|
||||
-ifdef(OTP_BELOW_26).
|
||||
return_init_error(Reason) ->
|
||||
proc_lib:init_ack({error, Reason}).
|
||||
-else.
|
||||
-spec return_init_error(inet:posix()) -> no_return().
|
||||
return_init_error(Reason) ->
|
||||
proc_lib:init_fail({error, Reason}, {exit, normal}).
|
||||
-endif.
|
||||
|
||||
-spec format_error(inet:posix() | atom()) -> string().
|
||||
format_error(Reason) ->
|
||||
|
||||
@@ -47,6 +47,8 @@
|
||||
|
||||
-export_type([loglevel/0]).
|
||||
|
||||
-include("logger.hrl").
|
||||
|
||||
%%%===================================================================
|
||||
%%% API
|
||||
%%%===================================================================
|
||||
@@ -383,19 +385,21 @@ console_template() ->
|
||||
false ->
|
||||
[time, " [", level, "] " | msg()]
|
||||
end.
|
||||
msg() ->
|
||||
[{logger_formatter, [[logger_formatter, title], ":", io_lib:nl()], []},
|
||||
msg, io_lib:nl()].
|
||||
-else.
|
||||
console_template() ->
|
||||
[time, " [", level, "] " | msg()].
|
||||
[time, " ", ?CLEAD, ?CDEFAULT, clevel, "[", level, "] ", ?CMID, ?CDEFAULT, ctext | msg()].
|
||||
msg() ->
|
||||
[{logger_formatter, [[logger_formatter, title], ":", io_lib:nl()], []},
|
||||
msg, ?CCLEAN, io_lib:nl()].
|
||||
-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.
|
||||
|
||||
@@ -80,10 +80,11 @@ init([]) ->
|
||||
Schema = read_schema_file(),
|
||||
{ok, #state{schema = Schema}};
|
||||
false ->
|
||||
?CRITICAL_MSG("Node name mismatch: I'm [~ts], "
|
||||
"the database is owned by ~p", [MyNode, DbNodes]),
|
||||
?CRITICAL_MSG("Erlang node name mismatch: I'm running in node [~ts], "
|
||||
"but the mnesia database is owned by ~p", [MyNode, DbNodes]),
|
||||
?CRITICAL_MSG("Either set ERLANG_NODE in ejabberdctl.cfg "
|
||||
"or change node name in Mnesia", []),
|
||||
"or change node name in Mnesia by running: "
|
||||
"ejabberdctl mnesia_change ~ts", [hd(DbNodes)]),
|
||||
{stop, node_name_mismatch}
|
||||
end.
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
-export([auth_method/0, auth_method/1]).
|
||||
-export([auth_opts/0, auth_opts/1]).
|
||||
-export([auth_password_format/0, auth_password_format/1]).
|
||||
-export([auth_password_types_hidden_in_sasl1/0, auth_password_types_hidden_in_sasl1/1]).
|
||||
-export([auth_scram_hash/0, auth_scram_hash/1]).
|
||||
-export([auth_stored_password_types/0, auth_stored_password_types/1]).
|
||||
-export([auth_use_cache/0, auth_use_cache/1]).
|
||||
@@ -55,6 +56,7 @@
|
||||
-export([hide_sensitive_log_data/0, hide_sensitive_log_data/1]).
|
||||
-export([host_config/0]).
|
||||
-export([hosts/0]).
|
||||
-export([hosts_alias/0]).
|
||||
-export([include_config_file/0, include_config_file/1]).
|
||||
-export([install_contrib_modules/0]).
|
||||
-export([jwt_auth_only_rule/0, jwt_auth_only_rule/1]).
|
||||
@@ -119,6 +121,10 @@
|
||||
-export([redis_server/0]).
|
||||
-export([registration_timeout/0]).
|
||||
-export([resource_conflict/0, resource_conflict/1]).
|
||||
-export([rest_proxy/0, rest_proxy/1]).
|
||||
-export([rest_proxy_password/0, rest_proxy_password/1]).
|
||||
-export([rest_proxy_port/0, rest_proxy_port/1]).
|
||||
-export([rest_proxy_username/0, rest_proxy_username/1]).
|
||||
-export([router_cache_life_time/0]).
|
||||
-export([router_cache_missed/0]).
|
||||
-export([router_cache_size/0]).
|
||||
@@ -258,6 +264,13 @@ auth_password_format() ->
|
||||
auth_password_format(Host) ->
|
||||
ejabberd_config:get_option({auth_password_format, Host}).
|
||||
|
||||
-spec auth_password_types_hidden_in_sasl1() -> ['plain' | 'scram_sha1' | 'scram_sha256' | 'scram_sha512'].
|
||||
auth_password_types_hidden_in_sasl1() ->
|
||||
auth_password_types_hidden_in_sasl1(global).
|
||||
-spec auth_password_types_hidden_in_sasl1(global | binary()) -> ['plain' | 'scram_sha1' | 'scram_sha256' | 'scram_sha512'].
|
||||
auth_password_types_hidden_in_sasl1(Host) ->
|
||||
ejabberd_config:get_option({auth_password_types_hidden_in_sasl1, Host}).
|
||||
|
||||
-spec auth_scram_hash() -> 'sha' | 'sha256' | 'sha512'.
|
||||
auth_scram_hash() ->
|
||||
auth_scram_hash(global).
|
||||
@@ -475,6 +488,10 @@ host_config() ->
|
||||
hosts() ->
|
||||
ejabberd_config:get_option({hosts, global}).
|
||||
|
||||
-spec hosts_alias() -> [{binary(),binary()}].
|
||||
hosts_alias() ->
|
||||
ejabberd_config:get_option({hosts_alias, global}).
|
||||
|
||||
-spec include_config_file() -> any().
|
||||
include_config_file() ->
|
||||
include_config_file(global).
|
||||
@@ -833,6 +850,34 @@ resource_conflict() ->
|
||||
resource_conflict(Host) ->
|
||||
ejabberd_config:get_option({resource_conflict, Host}).
|
||||
|
||||
-spec rest_proxy() -> binary().
|
||||
rest_proxy() ->
|
||||
rest_proxy(global).
|
||||
-spec rest_proxy(global | binary()) -> binary().
|
||||
rest_proxy(Host) ->
|
||||
ejabberd_config:get_option({rest_proxy, Host}).
|
||||
|
||||
-spec rest_proxy_password() -> string().
|
||||
rest_proxy_password() ->
|
||||
rest_proxy_password(global).
|
||||
-spec rest_proxy_password(global | binary()) -> string().
|
||||
rest_proxy_password(Host) ->
|
||||
ejabberd_config:get_option({rest_proxy_password, Host}).
|
||||
|
||||
-spec rest_proxy_port() -> char().
|
||||
rest_proxy_port() ->
|
||||
rest_proxy_port(global).
|
||||
-spec rest_proxy_port(global | binary()) -> char().
|
||||
rest_proxy_port(Host) ->
|
||||
ejabberd_config:get_option({rest_proxy_port, Host}).
|
||||
|
||||
-spec rest_proxy_username() -> string().
|
||||
rest_proxy_username() ->
|
||||
rest_proxy_username(global).
|
||||
-spec rest_proxy_username(global | binary()) -> string().
|
||||
rest_proxy_username(Host) ->
|
||||
ejabberd_config:get_option({rest_proxy_username, Host}).
|
||||
|
||||
-spec router_cache_life_time() -> 'infinity' | pos_integer().
|
||||
router_cache_life_time() ->
|
||||
ejabberd_config:get_option({router_cache_life_time, global}).
|
||||
|
||||
@@ -79,6 +79,8 @@ opt_type(auth_opts) ->
|
||||
end;
|
||||
opt_type(auth_stored_password_types) ->
|
||||
econf:list(econf:enum([plain, scram_sha1, scram_sha256, scram_sha512]));
|
||||
opt_type(auth_password_types_hidden_in_sasl1) ->
|
||||
econf:list(econf:enum([plain, scram_sha1, scram_sha256, scram_sha512]));
|
||||
opt_type(auth_password_format) ->
|
||||
econf:enum([plain, scram]);
|
||||
opt_type(auth_scram_hash) ->
|
||||
@@ -182,6 +184,13 @@ opt_type(host_config) ->
|
||||
[unique]));
|
||||
opt_type(hosts) ->
|
||||
econf:non_empty(econf:list(econf:domain(), [unique]));
|
||||
opt_type(hosts_alias) ->
|
||||
econf:and_then(
|
||||
econf:map(econf:domain(), econf:domain(), [unique]),
|
||||
econf:map(
|
||||
econf:domain(),
|
||||
econf:enum(ejabberd_config:get_option(hosts)),
|
||||
[unique]));
|
||||
opt_type(include_config_file) ->
|
||||
econf:any();
|
||||
opt_type(install_contrib_modules) ->
|
||||
@@ -333,6 +342,14 @@ opt_type(registration_timeout) ->
|
||||
econf:timeout(second, infinity);
|
||||
opt_type(resource_conflict) ->
|
||||
econf:enum([setresource, closeold, closenew, acceptnew]);
|
||||
opt_type(rest_proxy) ->
|
||||
econf:domain();
|
||||
opt_type(rest_proxy_port) ->
|
||||
econf:port();
|
||||
opt_type(rest_proxy_username) ->
|
||||
econf:string();
|
||||
opt_type(rest_proxy_password) ->
|
||||
econf:string();
|
||||
opt_type(router_cache_life_time) ->
|
||||
econf:timeout(second, infinity);
|
||||
opt_type(router_cache_missed) ->
|
||||
@@ -549,6 +566,7 @@ options() ->
|
||||
{auth_password_format, plain},
|
||||
{auth_scram_hash, sha},
|
||||
{auth_stored_password_types, []},
|
||||
{auth_password_types_hidden_in_sasl1, []},
|
||||
{auth_external_user_exists_check, true},
|
||||
{auth_use_cache,
|
||||
fun(Host) -> ejabberd_config:get_option({use_cache, Host}) end},
|
||||
@@ -579,6 +597,7 @@ options() ->
|
||||
{extauth_program, undefined},
|
||||
{fqdn, fun fqdn/1},
|
||||
{hide_sensitive_log_data, false},
|
||||
{hosts_alias, []},
|
||||
{host_config, []},
|
||||
{include_config_file, []},
|
||||
{language, <<"en">>},
|
||||
@@ -652,6 +671,10 @@ options() ->
|
||||
{redis_server, "localhost"},
|
||||
{registration_timeout, timer:seconds(600)},
|
||||
{resource_conflict, acceptnew},
|
||||
{rest_proxy, <<>>},
|
||||
{rest_proxy_port, 0},
|
||||
{rest_proxy_username, ""},
|
||||
{rest_proxy_password, ""},
|
||||
{router_cache_life_time,
|
||||
fun(Host) -> ejabberd_config:get_option({cache_life_time, Host}) end},
|
||||
{router_cache_missed,
|
||||
@@ -754,6 +777,7 @@ globals() ->
|
||||
ext_api_path_oauth,
|
||||
fqdn,
|
||||
hosts,
|
||||
hosts_alias,
|
||||
host_config,
|
||||
install_contrib_modules,
|
||||
listen,
|
||||
|
||||
@@ -109,14 +109,20 @@ doc() ->
|
||||
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'.")}},
|
||||
"Some components can be configured with specific toplevel options "
|
||||
"like _`oauth_db_type`_. "
|
||||
"Many modules can be configured with specific module options, "
|
||||
"usually named `db_type`. "
|
||||
"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'.")}},
|
||||
"Some components can be configured with specific toplevel options "
|
||||
"like _`router_db_type`_ and _`sm_db_type`_. "
|
||||
"Some modules can be configured with specific module options, "
|
||||
"usually named `ram_db_type`. "
|
||||
"The default value is 'mnesia'.")}},
|
||||
{queue_type,
|
||||
#{value => "ram | file",
|
||||
desc =>
|
||||
@@ -392,6 +398,17 @@ doc() ->
|
||||
"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_password_types_hidden_in_sasl1,
|
||||
#{value => "[plain | scram_sha1 | scram_sha256 | scram_sha512]",
|
||||
note => "added in 25.07",
|
||||
desc =>
|
||||
?T("List of password types that should not be offered in SASL1 authenticatication. "
|
||||
"Because SASL1, unlike SASL2, can't have list of available mechanisms tailored to "
|
||||
"individual user, it's possible that offered mechanisms will not be compatible "
|
||||
"with stored password, especially if new password type was added recently. "
|
||||
"This option allows disabling offering some mechanisms in SASL1, to a time until new "
|
||||
"password type will be available for all users.")}},
|
||||
{auth_scram_hash,
|
||||
#{value => "sha | sha256 | sha512",
|
||||
desc =>
|
||||
@@ -712,6 +729,23 @@ doc() ->
|
||||
" domain.tld:",
|
||||
" auth_method:",
|
||||
" - ldap"]}},
|
||||
{hosts_alias,
|
||||
#{value => "{Alias: Host}",
|
||||
desc =>
|
||||
?T("Define aliases for existing vhosts managed by ejabberd. "
|
||||
"An alias may be a regexp expression. "
|
||||
"This option is only consulted by the 'ejabberd_http' listener."),
|
||||
note => "added in 25.07",
|
||||
example =>
|
||||
["hosts:",
|
||||
" - domain.tld",
|
||||
" - example.org",
|
||||
"",
|
||||
"hosts_alias:",
|
||||
" xmpp.domain.tld: domain.tld",
|
||||
" jabber.domain.tld: domain.tld",
|
||||
" mytest.net: example.org",
|
||||
" \"exa*\": example.org"]}},
|
||||
{include_config_file,
|
||||
#{value => "[Filename, ...\\] | {Filename: Options}",
|
||||
desc =>
|
||||
@@ -1194,6 +1228,26 @@ doc() ->
|
||||
"uses old Jabber Non-SASL authentication (XEP-0078), "
|
||||
"then this option is not respected, and the action performed "
|
||||
"is 'closeold'.")}},
|
||||
{rest_proxy,
|
||||
#{value => "Host",
|
||||
note => "added in 25.07",
|
||||
desc => ?T("Address of a HTTP Connect proxy used by modules issuing rest calls "
|
||||
"(like ejabberd_oauth_rest)")}},
|
||||
{rest_proxy_port,
|
||||
#{value => "1..65535",
|
||||
note => "added in 25.07",
|
||||
desc => ?T("Port of a HTTP Connect proxy used by modules issuing rest calls "
|
||||
"(like ejabberd_oauth_rest)")}},
|
||||
{rest_proxy_username,
|
||||
#{value => "string()",
|
||||
note => "added in 25.07",
|
||||
desc => ?T("Username used to authenticate to HTTP Connect proxy used by modules issuing rest calls "
|
||||
"(like ejabberd_oauth_rest)")}},
|
||||
{rest_proxy_password,
|
||||
#{value => "string()",
|
||||
note => "added in 25.07",
|
||||
desc => ?T("Password used to authenticate to HTTP Connect proxy used by modules issuing rest calls "
|
||||
"(like ejabberd_oauth_rest)")}},
|
||||
{router_cache_life_time,
|
||||
#{value => "timeout()",
|
||||
desc =>
|
||||
|
||||
@@ -138,7 +138,7 @@ process_closed(#{server := LServer} = State, Reason) ->
|
||||
%%% xmpp_stream_in callbacks
|
||||
%%%===================================================================
|
||||
tls_options(#{tls_options := TLSOpts, lserver := LServer, server_host := ServerHost}) ->
|
||||
ejabberd_s2s:tls_options(LServer, ServerHost, TLSOpts).
|
||||
[override_cert_purpose | ejabberd_s2s:tls_options(LServer, ServerHost, TLSOpts)].
|
||||
|
||||
tls_required(#{server_host := ServerHost}) ->
|
||||
ejabberd_s2s:tls_required(ServerHost).
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
|
||||
-export([start_link/0, new/1, update/2, match/3, get_max_rate/1]).
|
||||
-export([reload_from_config/0]).
|
||||
-export([read_shaper_rules/2]).
|
||||
-export([validator/1, shaper_rules_validator/0]).
|
||||
%% gen_server callbacks
|
||||
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
|
||||
|
||||
+3
-1
@@ -481,8 +481,10 @@ c2s_handle_info(#{lang := Lang, bind2_session_id := {Tag, _}} = State,
|
||||
{stop, ejabberd_c2s:send(State1, Err)};
|
||||
c2s_handle_info(State, {replaced_with_bind_tag, _}) ->
|
||||
State;
|
||||
c2s_handle_info(#{lang := Lang} = State, kick) ->
|
||||
c2s_handle_info(#{lang := Lang, jid := JID} = State, kick) ->
|
||||
Err = xmpp:serr_policy_violation(?T("has been kicked"), Lang),
|
||||
ejabberd_hooks:run(sm_kick_user, JID#jid.lserver,
|
||||
[JID#jid.luser, JID#jid.lserver]),
|
||||
{stop, ejabberd_c2s:send(State, Err)};
|
||||
c2s_handle_info(#{lang := Lang} = State, {exit, Reason}) ->
|
||||
Err = xmpp:serr_conflict(Reason, Lang),
|
||||
|
||||
@@ -109,7 +109,7 @@ set_session(#session{sid = {Now, Pid}, usr = {U, LServer, R},
|
||||
|
||||
delete_session(#session{usr = {_, LServer, _}, sid = {Now, Pid}}) ->
|
||||
TS = now_to_timestamp(Now),
|
||||
PidS = list_to_binary(erlang:pid_to_list(Pid)),
|
||||
PidS = misc:encode_pid(Pid),
|
||||
case ejabberd_sql:sql_query(
|
||||
LServer,
|
||||
?SQL("delete from sm where usec=%(TS)d and pid=%(PidS)s")) of
|
||||
|
||||
@@ -1376,7 +1376,7 @@ write_file_if_new(File, Payload) ->
|
||||
|
||||
tmp_dir() ->
|
||||
case os:type() of
|
||||
{win32, _} -> filename:join([os:getenv("HOME"), "conf"]);
|
||||
{win32, _} -> filename:join([misc:get_home(), "conf"]);
|
||||
_ -> filename:join(["/tmp", "ejabberd"])
|
||||
end.
|
||||
|
||||
|
||||
@@ -1660,9 +1660,19 @@ make_login_items(#request{us = {Username, Host}} = R, Level) ->
|
||||
_ ->
|
||||
UserEl
|
||||
end,
|
||||
MenuPost =
|
||||
case ejabberd_hooks:run_fold(webadmin_menu_system_post, [], [R]) of
|
||||
[] ->
|
||||
[];
|
||||
PostElements ->
|
||||
[{xmlel,
|
||||
<<"div">>,
|
||||
[{<<"id">>, <<"navitemlogin">>}],
|
||||
[?XE(<<"ul">>, PostElements)]}]
|
||||
end,
|
||||
[{xmlel,
|
||||
<<"li">>,
|
||||
[],
|
||||
[{<<"id">>, <<"navitemlogin-start">>}],
|
||||
[{xmlel,
|
||||
<<"div">>,
|
||||
[{<<"id">>, <<"navitemlogin">>}],
|
||||
@@ -1673,10 +1683,12 @@ make_login_items(#request{us = {Username, Host}} = R, Level) ->
|
||||
R,
|
||||
[{<<"sentence">>, misc:atom_to_binary(node())}],
|
||||
[{only, value},
|
||||
{result_links, [{sentence, node, Level, <<"">>}]}])]),
|
||||
?LI([?C(unicode:characters_to_binary("📤")),
|
||||
?AC(<<(binary:copy(<<"../">>, Level))/binary, "logout/">>,
|
||||
<<"Logout">>)])])]}]}].
|
||||
{result_links, [{sentence, node, Level, <<"">>}]}])])]
|
||||
++ ejabberd_hooks:run_fold(webadmin_menu_system_inside, [], [R])
|
||||
++ [?LI([?C(unicode:characters_to_binary("📤")),
|
||||
?AC(<<(binary:copy(<<"../">>, Level))/binary, "logout/">>,
|
||||
<<"Logout">>)])])]}]
|
||||
++ MenuPost}].
|
||||
|
||||
%%%==================================
|
||||
|
||||
@@ -1925,7 +1937,9 @@ lists_zipwith3(Combine, [E1 | List1], [E2 | List2], [], DefX, DefY, DefZ, Res) -
|
||||
E123 = Combine(E1, E2, DefZ),
|
||||
lists_zipwith3(Combine, List1, List2, [], DefX, DefY, DefZ, [E123 | Res]).
|
||||
|
||||
-else.
|
||||
-endif.
|
||||
|
||||
-ifndef(OTP_BELOW_26).
|
||||
|
||||
lists_zipwith3(Combine, List1, List2, List3, How) ->
|
||||
lists:zipwith3(Combine, List1, List2, List3, How).
|
||||
|
||||
+35
-5
@@ -49,6 +49,7 @@
|
||||
-include("logger.hrl").
|
||||
-include("translate.hrl").
|
||||
-include_lib("xmpp/include/xmpp.hrl").
|
||||
-include_lib("stdlib/include/zip.hrl").
|
||||
|
||||
-define(REPOS, "git@github.com:processone/ejabberd-contrib.git").
|
||||
|
||||
@@ -148,7 +149,8 @@ get_commands_spec() ->
|
||||
#ejabberd_commands{name = module_upgrade,
|
||||
tags = [modules],
|
||||
desc = "Upgrade the running code of an installed module",
|
||||
longdesc = "In practice, this uninstalls and installs the module",
|
||||
longdesc = "In practice, this uninstalls, cleans the compiled files, and installs the module",
|
||||
note = "improved in 25.07",
|
||||
module = ?MODULE, function = upgrade,
|
||||
args_desc = ["Module name"],
|
||||
args_example = [<<"mod_rest">>],
|
||||
@@ -289,8 +291,19 @@ upgrade(Module) when is_atom(Module) ->
|
||||
upgrade(misc:atom_to_binary(Module));
|
||||
upgrade(Package) when is_binary(Package) ->
|
||||
uninstall(Package),
|
||||
clean(Package),
|
||||
install(Package).
|
||||
|
||||
clean(Package) ->
|
||||
Spec = [S || {Mod, S} <- available(), misc:atom_to_binary(Mod)==Package],
|
||||
case Spec of
|
||||
[] ->
|
||||
{error, not_available};
|
||||
[Attrs] ->
|
||||
Path = proplists:get_value(path, Attrs),
|
||||
[delete_path(SubPath) || SubPath <- filelib:wildcard(Path++"/{deps,ebin}")]
|
||||
end.
|
||||
|
||||
add_sources(Path) when is_list(Path) ->
|
||||
add_sources(iolist_to_binary(module_name(Path)), Path).
|
||||
add_sources(_, "") ->
|
||||
@@ -371,8 +384,6 @@ geturl(Url) ->
|
||||
{error, Reason}
|
||||
end.
|
||||
|
||||
getenv(Env) ->
|
||||
getenv(Env, "").
|
||||
getenv(Env, Default) ->
|
||||
case os:getenv(Env) of
|
||||
false -> Default;
|
||||
@@ -389,6 +400,23 @@ extract(tar, {ok, _, Body}, DestDir) ->
|
||||
extract(_, {error, Reason}, _) ->
|
||||
{error, Reason};
|
||||
extract(zip, Zip, DestDir) ->
|
||||
{ok, DirList} = zip:list_dir(Zip),
|
||||
Offending =
|
||||
lists:filter(fun (#zip_comment{}) ->
|
||||
false;
|
||||
(#zip_file{name = Filename}) ->
|
||||
absolute == filename:pathtype(Filename)
|
||||
end,
|
||||
DirList),
|
||||
case Offending of
|
||||
[] ->
|
||||
extract(zip_verified, Zip, DestDir);
|
||||
_ ->
|
||||
Filenames = [F#zip_file.name || F <- Offending],
|
||||
?ERROR_MSG("The zip file includes absolute file paths:~n ~p", [Filenames]),
|
||||
{error, {zip_absolute_path, Filenames}}
|
||||
end;
|
||||
extract(zip_verified, Zip, DestDir) ->
|
||||
case zip:extract(Zip, [{cwd, DestDir}]) of
|
||||
{ok, _} -> ok;
|
||||
Error -> Error
|
||||
@@ -453,7 +481,7 @@ delete_path(Path, Package) ->
|
||||
delete_path(filename:join(filename:dirname(Path), Package)).
|
||||
|
||||
modules_dir() ->
|
||||
DefaultDir = filename:join(getenv("HOME"), ".ejabberd-modules"),
|
||||
DefaultDir = filename:join(misc:get_home(), ".ejabberd-modules"),
|
||||
getenv("CONTRIB_MODULES_PATH", DefaultDir).
|
||||
|
||||
sources_dir() ->
|
||||
@@ -543,7 +571,7 @@ check_sources(Module) ->
|
||||
true -> Acc;
|
||||
false -> [{missing, Name}|Acc]
|
||||
end
|
||||
end, HaveSrc, [{is_file, "README.txt"},
|
||||
end, HaveSrc, [{is_file, "README.md"},
|
||||
{is_file, "COPYING"},
|
||||
{is_file, SpecFile}]),
|
||||
SpecCheck = case consult(SpecFile) of
|
||||
@@ -645,6 +673,8 @@ compile_options() ->
|
||||
++ maybe_define_lager_macro()
|
||||
++ [{i, filename:join(app_dir(App), "include")}
|
||||
|| App <- [fast_xml, xmpp, p1_utils, ejabberd]]
|
||||
++ [{i, filename:join(app_dir(App), "include")}
|
||||
|| App <- [p1_xml, p1_xmpp]] % paths used in Debian packages
|
||||
++ [{i, filename:join(mod_dir(Mod), "include")}
|
||||
|| Mod <- installed()].
|
||||
|
||||
|
||||
+38
-2
@@ -27,7 +27,7 @@
|
||||
-author('alexey@process-one.net').
|
||||
|
||||
-export([init/1, start_link/0, start_child/3, start_child/4,
|
||||
stop_child/1, stop_child/2, stop/0, config_reloaded/0]).
|
||||
stop_child/1, stop_child/2, prep_stop/0, stop/0, config_reloaded/0]).
|
||||
-export([start_module/2, stop_module/2, stop_module_keep_config/2,
|
||||
get_opt/2, set_opt/3, get_opt_hosts/1, is_equal_opt/3,
|
||||
get_module_opt/3, get_module_opts/2, get_module_opt_hosts/2,
|
||||
@@ -76,6 +76,7 @@
|
||||
-callback start(binary(), opts()) ->
|
||||
ok | {ok, pid()} |
|
||||
{ok, [registration()]} | {error, term()}.
|
||||
-callback prep_stop(binary()) -> any().
|
||||
-callback stop(binary()) -> any().
|
||||
-callback reload(binary(), opts(), opts()) -> ok | {ok, pid()} | {error, term()}.
|
||||
-callback mod_opt_type(atom()) -> econf:validator().
|
||||
@@ -86,7 +87,7 @@
|
||||
example => [string()] | [{binary(), [string()]}]}.
|
||||
-callback depends(binary(), opts()) -> [{module(), hard | soft}].
|
||||
|
||||
-optional_callbacks([mod_opt_type/1, reload/3]).
|
||||
-optional_callbacks([mod_opt_type/1, reload/3, prep_stop/1]).
|
||||
|
||||
-export_type([opts/0]).
|
||||
-export_type([db_type/0]).
|
||||
@@ -114,6 +115,10 @@ init([]) ->
|
||||
{read_concurrency, true}]),
|
||||
{ok, {{one_for_one, 10, 1}, []}}.
|
||||
|
||||
-spec prep_stop() -> ok.
|
||||
prep_stop() ->
|
||||
prep_stop_modules().
|
||||
|
||||
-spec stop() -> ok.
|
||||
stop() ->
|
||||
ejabberd_hooks:delete(config_reloaded, ?MODULE, config_reloaded, 60),
|
||||
@@ -301,6 +306,21 @@ is_app_running(AppName) ->
|
||||
lists:keymember(AppName, 1,
|
||||
application:which_applications(Timeout)).
|
||||
|
||||
-spec prep_stop_modules() -> ok.
|
||||
prep_stop_modules() ->
|
||||
lists:foreach(
|
||||
fun(Host) ->
|
||||
prep_stop_modules(Host)
|
||||
end, ejabberd_option:hosts()).
|
||||
|
||||
-spec prep_stop_modules(binary()) -> ok.
|
||||
prep_stop_modules(Host) ->
|
||||
Modules = lists:reverse(loaded_modules_with_opts(Host)),
|
||||
lists:foreach(
|
||||
fun({Module, _Args}) ->
|
||||
prep_stop_module_keep_config(Host, Module)
|
||||
end, Modules).
|
||||
|
||||
-spec stop_modules() -> ok.
|
||||
stop_modules() ->
|
||||
lists:foreach(
|
||||
@@ -320,6 +340,22 @@ stop_modules(Host) ->
|
||||
stop_module(Host, Module) ->
|
||||
stop_module_keep_config(Host, Module).
|
||||
|
||||
-spec prep_stop_module_keep_config(binary(), atom()) -> error | ok.
|
||||
prep_stop_module_keep_config(Host, Module) ->
|
||||
?DEBUG("Preparing to stop ~ts at ~ts", [Module, Host]),
|
||||
try Module:prep_stop(Host) of
|
||||
_ ->
|
||||
ok
|
||||
catch ?EX_RULE(error, undef, _St) ->
|
||||
ok;
|
||||
?EX_RULE(Class, Reason, St) ->
|
||||
StackTrace = ?EX_STACK(St),
|
||||
?ERROR_MSG("Failed to prepare stop module ~ts at ~ts:~n** ~ts",
|
||||
[Module, Host,
|
||||
misc:format_exception(2, Class, Reason, StackTrace)]),
|
||||
error
|
||||
end.
|
||||
|
||||
-spec stop_module_keep_config(binary(), atom()) -> error | ok.
|
||||
stop_module_keep_config(Host, Module) ->
|
||||
?DEBUG("Stopping ~ts at ~ts", [Module, Host]),
|
||||
|
||||
+70
-43
@@ -36,16 +36,19 @@
|
||||
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,
|
||||
get_home/0, warn_unset_home/0,
|
||||
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,
|
||||
uri_decode/1,
|
||||
json_encode/1, json_decode/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]).
|
||||
semver_to_xxyy/1, logical_processors/0, get_mucsub_event_type/1,
|
||||
lists_uniq/1]).
|
||||
|
||||
%% Deprecated functions
|
||||
-export([decode_base64/1, encode_base64/1]).
|
||||
@@ -70,45 +73,11 @@
|
||||
-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, []).
|
||||
yconf:parse_uri(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.
|
||||
yconf:parse_uri(URL, Protocols).
|
||||
|
||||
-ifdef(OTP_BELOW_25).
|
||||
-ifdef(OTP_BELOW_24).
|
||||
@@ -123,6 +92,26 @@ uri_quote(Data) ->
|
||||
uri_string:quote(Data).
|
||||
-endif.
|
||||
|
||||
%% @doc Decode a part of the URL and return string()
|
||||
%% -spec url_decode(binary()) -> bitstring().
|
||||
|
||||
-ifdef(OTP_BELOW_24).
|
||||
uri_decode(Path) -> uri_decode(Path, <<>>).
|
||||
|
||||
uri_decode(<<$%, Hi, Lo, Tail/binary>>, Acc) ->
|
||||
Hex = list_to_integer([Hi, Lo], 16),
|
||||
if Hex == 0 -> exit(badurl);
|
||||
true -> ok
|
||||
end,
|
||||
uri_decode(Tail, <<Acc/binary, Hex>>);
|
||||
uri_decode(<<H, T/binary>>, Acc) when H /= 0 ->
|
||||
uri_decode(T, <<Acc/binary, H>>);
|
||||
uri_decode(<<>>, Acc) -> Acc.
|
||||
-else.
|
||||
uri_decode(Path) ->
|
||||
uri_string:percent_decode(Path).
|
||||
-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).
|
||||
@@ -132,20 +121,21 @@ 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) ->
|
||||
json_encode({[{_Key, _Value} | _]} = Term) ->
|
||||
iolist_to_binary(json:encode(Term,
|
||||
fun({Val}, Encoder) when is_list(Val) ->
|
||||
json:encode_key_value_list(Val, Encoder);
|
||||
(Val, Encoder) ->
|
||||
json:encode_value(Val, Encoder)
|
||||
end)).
|
||||
json:encode_value(Val, Encoder)
|
||||
end));
|
||||
json_encode({[]}) ->
|
||||
%% Jiffy was able to handle this case, but Json library does not
|
||||
<<"{}">>;
|
||||
json_encode(Term) ->
|
||||
iolist_to_binary(json:encode(Term)).
|
||||
json_decode(Bin) ->
|
||||
@@ -472,6 +462,25 @@ get_descr(Lang, Text) ->
|
||||
Copyright = ejabberd_config:get_copyright(),
|
||||
<<Desc/binary, $\n, Copyright/binary>>.
|
||||
|
||||
-spec get_home() -> string().
|
||||
get_home() ->
|
||||
case init:get_argument(home) of
|
||||
{ok, [[Home]]} ->
|
||||
Home;
|
||||
error ->
|
||||
mnesia:system_info(directory)
|
||||
end.
|
||||
|
||||
warn_unset_home() ->
|
||||
case init:get_argument(home) of
|
||||
{ok, [[_Home]]} ->
|
||||
ok;
|
||||
error ->
|
||||
?INFO_MSG("The 'HOME' environment variable is not set, "
|
||||
"ejabberd will use as HOME the Mnesia directory: ~s.",
|
||||
[mnesia:system_info(directory)])
|
||||
end.
|
||||
|
||||
-spec intersection(list(), list()) -> list().
|
||||
intersection(L1, L2) ->
|
||||
lists:filter(
|
||||
@@ -801,3 +810,21 @@ set_proc_label(_Label) ->
|
||||
set_proc_label(Label) ->
|
||||
proc_lib:set_label(Label).
|
||||
-endif.
|
||||
|
||||
-ifdef(OTP_BELOW_25).
|
||||
-spec lists_uniq([term()]) -> [term()].
|
||||
lists_uniq(List) ->
|
||||
lists_uniq_int(List, #{}).
|
||||
|
||||
lists_uniq_int([El | Rest], Existing) ->
|
||||
case maps:is_key(El, Existing) of
|
||||
true -> lists_uniq_int(Rest, Existing);
|
||||
_ -> [El | lists_uniq_int(Rest, Existing#{El => true})]
|
||||
end;
|
||||
lists_uniq_int([], _) ->
|
||||
[].
|
||||
-else.
|
||||
-spec lists_uniq([term()]) -> [term()].
|
||||
lists_uniq(List) ->
|
||||
lists:uniq(List).
|
||||
-endif.
|
||||
|
||||
+4
-2
@@ -252,9 +252,11 @@ mod_options(_Host) ->
|
||||
|
||||
mod_doc() ->
|
||||
#{desc =>
|
||||
?T("This module implements https://xmpp.org/extensions/xep-0050.html"
|
||||
[?T("def:ad-hoc command"), "",
|
||||
?T(": Command that can be executed by an XMPP client using XEP-0050."), "",
|
||||
?T("This module implements https://xmpp.org/extensions/xep-0050.html"
|
||||
"[XEP-0050: Ad-Hoc Commands]. It's an auxiliary module and is "
|
||||
"only needed by some of the other modules."),
|
||||
"only needed by some of the other modules.")],
|
||||
opts =>
|
||||
[{report_commands_node,
|
||||
#{value => "true | false",
|
||||
|
||||
@@ -90,7 +90,7 @@ depends(_Host, _Opts) ->
|
||||
|
||||
mod_doc() ->
|
||||
#{desc =>
|
||||
?T("Execute https://docs.ejabberd.im/developer/ejabberd-api/[API Commands] "
|
||||
?T("Execute (def:API commands) "
|
||||
"in a XMPP client using "
|
||||
"https://xmpp.org/extensions/xep-0050.html[XEP-0050: Ad-Hoc Commands]. "
|
||||
"This module requires _`mod_adhoc`_ (to execute the commands), "
|
||||
@@ -102,7 +102,7 @@ mod_doc() ->
|
||||
desc =>
|
||||
?T("What API version to use. "
|
||||
"If setting an ejabberd version, it will use the latest API "
|
||||
"version that was available in that ejabberd version. "
|
||||
"version that was available in that (def:c2s) ejabberd version. "
|
||||
"For example, setting '\"24.06\"' in this option implies '2'. "
|
||||
"The default value is the latest version.")}}],
|
||||
example =>
|
||||
@@ -664,7 +664,9 @@ lists_zip3_pad([A | As], Nil, Nil, Xs) when (Nil == none) or (Nil == []) ->
|
||||
lists_zip3_pad([], Nil, Nil, Xs) when (Nil == none) or (Nil == []) ->
|
||||
lists:reverse(Xs).
|
||||
|
||||
-else.
|
||||
-endif.
|
||||
|
||||
-ifndef(OTP_BELOW_26).
|
||||
|
||||
lists_zip3_pad(As, Bs, Cs) ->
|
||||
lists:zip3(As, Bs, Cs, {pad, {error_missing_args_def, "", ""}}).
|
||||
|
||||
+33
-67
@@ -268,14 +268,14 @@ get_commands_spec() ->
|
||||
#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. "
|
||||
"stores ban details in the account private storage, "
|
||||
"which blocks login to the account. "
|
||||
"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",
|
||||
note = "improved in 25.08",
|
||||
args = [{user, binary}, {host, binary}, {reason, binary}],
|
||||
args_example = [<<"attacker">>, <<"myserver.com">>, <<"Spaming other users">>],
|
||||
args_desc = ["User name to ban", "Server name",
|
||||
@@ -301,7 +301,7 @@ get_commands_spec() ->
|
||||
{"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",
|
||||
desc = "Remove the ban from an account",
|
||||
longdesc = "Check _`ban_account`_ API.",
|
||||
module = ?MODULE, function = unban_account,
|
||||
version = 2,
|
||||
@@ -1158,6 +1158,7 @@ ban_account(User, Host, ReasonText) ->
|
||||
ok.
|
||||
|
||||
kick_sessions(User, Server, Reason) ->
|
||||
ejabberd_hooks:run(sm_kick_user, Server, [User, Server]),
|
||||
lists:map(
|
||||
fun(Resource) ->
|
||||
kick_this_session(User, Server, Resource, Reason)
|
||||
@@ -1189,27 +1190,29 @@ prepare_reason(Reason) when is_binary(Reason) ->
|
||||
%% Ban account v2
|
||||
|
||||
ban_account_v2(User, Host, ReasonText) ->
|
||||
case gen_mod:is_loaded(Host, mod_private) of
|
||||
false ->
|
||||
IsPrivateEnabled = gen_mod:is_loaded(Host, mod_private),
|
||||
Exists = ejabberd_auth:user_exists(User, Host),
|
||||
IsBanned = is_banned(User, Host),
|
||||
case {IsPrivateEnabled, Exists, IsBanned} of
|
||||
{true, true, false} ->
|
||||
ban_account_v2_b(User, Host, ReasonText);
|
||||
{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
|
||||
{_, false, _} ->
|
||||
account_does_not_exist;
|
||||
{_, _, true} ->
|
||||
account_was_already_banned;
|
||||
{_, _, _} ->
|
||||
other_error
|
||||
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),
|
||||
BanPrivateXml = build_ban_xmlel(Reason, Last, BanDate, Hash),
|
||||
ok = private_set2(User, Host, BanPrivateXml),
|
||||
ok = set_random_password_v2(User, Host),
|
||||
kick_sessions(User, Host, Reason),
|
||||
ok.
|
||||
|
||||
@@ -1217,36 +1220,16 @@ get_hash_value(User, Host) ->
|
||||
Cookie = misc:atom_to_binary(erlang:get_cookie()),
|
||||
misc:term_to_base64(crypto:hash(sha256, <<User/binary, Host/binary, Cookie/binary>>)).
|
||||
|
||||
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),
|
||||
build_ban_xmlel(Reason, {LastDate, LastReason}, BanDate, Hash) ->
|
||||
#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
|
||||
|
||||
@@ -1286,43 +1269,26 @@ is_banned(User, Host) ->
|
||||
%% Unban account
|
||||
|
||||
unban_account(User, Host) ->
|
||||
case gen_mod:is_loaded(Host, mod_private) of
|
||||
false ->
|
||||
IsPrivateEnabled = gen_mod:is_loaded(Host, mod_private),
|
||||
Exists = ejabberd_auth:user_exists(User, Host),
|
||||
IsBanned = is_banned(User, Host),
|
||||
case {IsPrivateEnabled, Exists, IsBanned} of
|
||||
{true, true, true} ->
|
||||
unban_account2(User, Host);
|
||||
{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
|
||||
{_, false, _} ->
|
||||
account_does_not_exist;
|
||||
{_, _, false} ->
|
||||
account_was_not_banned;
|
||||
{_, _, _} ->
|
||||
other_error
|
||||
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">>}]}.
|
||||
|
||||
|
||||
@@ -130,7 +130,7 @@ update_tables(State) ->
|
||||
case add_sh_column(State, "users") of
|
||||
true ->
|
||||
drop_pkey(State, "users"),
|
||||
add_pkey(State, "users", ["server_host", "username"]),
|
||||
add_pkey(State, "users", ["server_host", "username", "type"]),
|
||||
drop_sh_default(State, "users");
|
||||
false ->
|
||||
ok
|
||||
@@ -443,7 +443,8 @@ drop_pkey(#state{dbtype = mysql} = State, Table) ->
|
||||
["ALTER TABLE ", Table, " DROP PRIMARY KEY;"]).
|
||||
|
||||
add_pkey(#state{dbtype = pgsql} = State, Table, Cols) ->
|
||||
SCols = string:join(Cols, ", "),
|
||||
Cols2 = lists:map(fun("type") -> "\"type\""; (V) -> V end, Cols),
|
||||
SCols = string:join(Cols2, ", "),
|
||||
sql_query(
|
||||
State#state.host,
|
||||
["ALTER TABLE ", Table, " ADD PRIMARY KEY (", SCols, ");"]);
|
||||
|
||||
@@ -0,0 +1,913 @@
|
||||
%%%----------------------------------------------------------------------
|
||||
%%% File : mod_antispam.erl
|
||||
%%% Author : Holger Weiss <holger@zedat.fu-berlin.de>
|
||||
%%% Author : Stefan Strigler <stefan@strigler.de>
|
||||
%%% Purpose : Filter spam messages based on sender JID and content
|
||||
%%% Created : 31 Mar 2019 by Holger Weiss <holger@zedat.fu-berlin.de>
|
||||
%%%
|
||||
%%%
|
||||
%%% ejabberd, Copyright (C) 2019-2025 ProcessOne
|
||||
%%%
|
||||
%%% This program is free software; you can redistribute it and/or
|
||||
%%% modify it under the terms of the GNU General Public License as
|
||||
%%% published by the Free Software Foundation; either version 2 of the
|
||||
%%% License, or (at your option) any later version.
|
||||
%%%
|
||||
%%% This program is distributed in the hope that it will be useful,
|
||||
%%% but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
%%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
%%% General Public License for more details.
|
||||
%%%
|
||||
%%% You should have received a copy of the GNU General Public License along
|
||||
%%% with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
%%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
%%%
|
||||
%%%----------------------------------------------------------------------
|
||||
|
||||
%%| definitions
|
||||
|
||||
-module(mod_antispam).
|
||||
-author('holger@zedat.fu-berlin.de').
|
||||
-author('stefan@strigler.de').
|
||||
|
||||
-behaviour(gen_server).
|
||||
-behaviour(gen_mod).
|
||||
|
||||
%% gen_mod callbacks.
|
||||
-export([start/2,
|
||||
prep_stop/1,
|
||||
stop/1,
|
||||
reload/3,
|
||||
depends/2,
|
||||
mod_doc/0,
|
||||
mod_opt_type/1,
|
||||
mod_options/1]).
|
||||
|
||||
%% gen_server callbacks.
|
||||
-export([init/1,
|
||||
handle_call/3,
|
||||
handle_cast/2,
|
||||
handle_info/2,
|
||||
terminate/2,
|
||||
code_change/3]).
|
||||
|
||||
-export([get_rtbl_services_option/1]).
|
||||
|
||||
%% ejabberd_commands callbacks.
|
||||
-export([add_blocked_domain/2,
|
||||
add_to_spam_filter_cache/2,
|
||||
drop_from_spam_filter_cache/2,
|
||||
expire_spam_filter_cache/2,
|
||||
get_blocked_domains/1,
|
||||
get_commands_spec/0,
|
||||
get_spam_filter_cache/1,
|
||||
reload_spam_filter_files/1,
|
||||
remove_blocked_domain/2]).
|
||||
|
||||
-include("ejabberd_commands.hrl").
|
||||
-include("logger.hrl").
|
||||
-include("mod_antispam.hrl").
|
||||
-include("translate.hrl").
|
||||
|
||||
-include_lib("xmpp/include/xmpp.hrl").
|
||||
|
||||
-record(state,
|
||||
{host = <<>> :: binary(),
|
||||
dump_fd = undefined :: file:io_device() | undefined,
|
||||
url_set = sets:new() :: url_set(),
|
||||
jid_set = sets:new() :: jid_set(),
|
||||
jid_cache = #{} :: map(),
|
||||
max_cache_size = 0 :: non_neg_integer() | unlimited,
|
||||
rtbl_host = none :: binary() | none,
|
||||
rtbl_subscribed = false :: boolean(),
|
||||
rtbl_retry_timer = undefined :: reference() | undefined,
|
||||
rtbl_domains_node :: binary(),
|
||||
blocked_domains = #{} :: #{binary() => any()},
|
||||
whitelist_domains = #{} :: #{binary() => false}
|
||||
}).
|
||||
|
||||
-type state() :: #state{}.
|
||||
|
||||
-define(COMMAND_TIMEOUT, timer:seconds(30)).
|
||||
-define(DEFAULT_CACHE_SIZE, 10000).
|
||||
|
||||
%% @format-begin
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%%| gen_mod callbacks
|
||||
|
||||
-spec start(binary(), gen_mod:opts()) -> ok | {error, any()}.
|
||||
start(Host, Opts) ->
|
||||
case gen_mod:is_loaded_elsewhere(Host, ?MODULE) of
|
||||
false ->
|
||||
ejabberd_commands:register_commands(?MODULE, get_commands_spec());
|
||||
true ->
|
||||
ok
|
||||
end,
|
||||
gen_mod:start_child(?MODULE, Host, Opts).
|
||||
|
||||
-spec prep_stop(binary()) -> ok | {error, any()}.
|
||||
prep_stop(Host) ->
|
||||
case try_call_by_host(Host, prepare_stop) of
|
||||
ready_to_stop ->
|
||||
ok
|
||||
end.
|
||||
|
||||
-spec stop(binary()) -> ok | {error, any()}.
|
||||
stop(Host) ->
|
||||
case gen_mod:is_loaded_elsewhere(Host, ?MODULE) of
|
||||
false ->
|
||||
ejabberd_commands:unregister_commands(get_commands_spec());
|
||||
true ->
|
||||
ok
|
||||
end,
|
||||
gen_mod:stop_child(?MODULE, Host).
|
||||
|
||||
-spec reload(binary(), gen_mod:opts(), gen_mod:opts()) -> ok.
|
||||
reload(Host, NewOpts, OldOpts) ->
|
||||
?DEBUG("reloading", []),
|
||||
Proc = get_proc_name(Host),
|
||||
gen_server:cast(Proc, {reload, NewOpts, OldOpts}).
|
||||
|
||||
-spec depends(binary(), gen_mod:opts()) -> [{module(), hard | soft}].
|
||||
depends(_Host, _Opts) ->
|
||||
[{mod_pubsub, soft}].
|
||||
|
||||
-spec mod_opt_type(atom()) -> econf:validator().
|
||||
mod_opt_type(access_spam) ->
|
||||
econf:acl();
|
||||
mod_opt_type(cache_size) ->
|
||||
econf:pos_int(unlimited);
|
||||
mod_opt_type(rtbl_services) ->
|
||||
econf:list(
|
||||
econf:either(
|
||||
econf:binary(),
|
||||
econf:map(
|
||||
econf:binary(),
|
||||
econf:map(
|
||||
econf:enum([spam_source_domains_node]), econf:binary()))));
|
||||
mod_opt_type(spam_domains_file) ->
|
||||
econf:either(
|
||||
econf:enum([none]), econf:file());
|
||||
mod_opt_type(spam_dump_file) ->
|
||||
econf:either(
|
||||
econf:bool(), econf:file(write));
|
||||
mod_opt_type(spam_jids_file) ->
|
||||
econf:either(
|
||||
econf:enum([none]), econf:file());
|
||||
mod_opt_type(spam_urls_file) ->
|
||||
econf:either(
|
||||
econf:enum([none]), econf:file());
|
||||
mod_opt_type(whitelist_domains_file) ->
|
||||
econf:either(
|
||||
econf:enum([none]), econf:file()).
|
||||
|
||||
-spec mod_options(binary()) -> [{rtbl_services, [tuple()]} | {atom(), any()}].
|
||||
mod_options(_Host) ->
|
||||
[{access_spam, none},
|
||||
{cache_size, ?DEFAULT_CACHE_SIZE},
|
||||
{rtbl_services, []},
|
||||
{spam_domains_file, none},
|
||||
{spam_dump_file, false},
|
||||
{spam_jids_file, none},
|
||||
{spam_urls_file, none},
|
||||
{whitelist_domains_file, none}].
|
||||
|
||||
mod_doc() ->
|
||||
#{desc =>
|
||||
?T("Filter spam messages and subscription requests received from "
|
||||
"remote servers based on "
|
||||
"https://xmppbl.org/[Real-Time Block Lists (RTBL)], "
|
||||
"lists of known spammer JIDs and/or URLs mentioned in spam messages. "
|
||||
"Traffic classified as spam is rejected with an error "
|
||||
"(and an '[info]' message is logged) unless the sender "
|
||||
"is subscribed to the recipient's presence."),
|
||||
note => "added in 25.07",
|
||||
opts =>
|
||||
[{access_spam,
|
||||
#{value => ?T("Access"),
|
||||
desc =>
|
||||
?T("Access rule that controls what accounts may receive spam messages. "
|
||||
"If the rule returns 'allow' for a given recipient, "
|
||||
"spam messages aren't rejected for that recipient. "
|
||||
"The default value is 'none', which means that all recipients "
|
||||
"are subject to spam filtering verification.")}},
|
||||
{cache_size,
|
||||
#{value => "pos_integer()",
|
||||
desc =>
|
||||
?T("Maximum number of JIDs that will be cached due to sending spam URLs. "
|
||||
"If that limit is exceeded, the least recently used "
|
||||
"entries are removed from the cache. "
|
||||
"Setting this option to '0' disables the caching feature. "
|
||||
"Note that separate caches are used for each virtual host, "
|
||||
" and that the caches aren't distributed across cluster nodes. "
|
||||
"The default value is '10000'.")}},
|
||||
{rtbl_services,
|
||||
#{value => ?T("[Service]"),
|
||||
example =>
|
||||
["rtbl_services:",
|
||||
" - pubsub.server1.localhost:",
|
||||
" spam_source_domains_node: actual_custom_pubsub_node"],
|
||||
desc =>
|
||||
?T("Query a RTBL service to get domains to block, as provided by "
|
||||
"https://xmppbl.org/[xmppbl.org]. "
|
||||
"Please note right now this option only supports one service in that list. "
|
||||
"For blocking spam and abuse on MUC channels, please use _`mod_muc_rtbl`_ for now. "
|
||||
"If only the host is provided, the default node names will be assumed. "
|
||||
"If the node name is different than 'spam_source_domains', "
|
||||
"you can setup the custom node name with the option 'spam_source_domains_node'. "
|
||||
"The default value is an empty list of services.")}},
|
||||
{spam_domains_file,
|
||||
#{value => ?T("none | Path"),
|
||||
desc =>
|
||||
?T("Path to a plain text file containing a list of "
|
||||
"known spam domains, one domain per line. "
|
||||
"Messages and subscription requests sent from one of the listed domains "
|
||||
"are classified as spam if sender is not in recipient's roster. "
|
||||
"This list of domains gets merged with the one retrieved "
|
||||
"by an RTBL host if any given. "
|
||||
"Use an absolute path, or the '@CONFIG_PATH@' "
|
||||
"https://docs.ejabberd.im/admin/configuration/file-format/#predefined-keywords[predefined keyword] "
|
||||
"if the file is available in the configuration directory. "
|
||||
"The default value is 'none'.")}},
|
||||
{spam_dump_file,
|
||||
#{value => ?T("false | true | Path"),
|
||||
desc =>
|
||||
?T("Path to the file to store blocked messages. "
|
||||
"Use an absolute path, or the '@LOG_PATH@' "
|
||||
"https://docs.ejabberd.im/admin/configuration/file-format/#predefined-keywords[predefined keyword] "
|
||||
"to store logs "
|
||||
"in the same place that the other ejabberd log files. "
|
||||
"If set to 'false', it doesn't dump stanzas, which is the default. "
|
||||
"If set to 'true', it stores in '\"@LOG_PATH@/spam_dump_@HOST@.log\"'.")}},
|
||||
{spam_jids_file,
|
||||
#{value => ?T("none | Path"),
|
||||
desc =>
|
||||
?T("Path to a plain text file containing a list of "
|
||||
"known spammer JIDs, one JID per line. "
|
||||
"Messages and subscription requests sent from one of "
|
||||
"the listed JIDs are classified as spam. "
|
||||
"Messages containing at least one of the listed JIDs"
|
||||
"are classified as spam as well. "
|
||||
"Furthermore, the sender's JID will be cached, "
|
||||
"so that future traffic originating from that JID will also be classified as spam. "
|
||||
"Use an absolute path, or the '@CONFIG_PATH@' "
|
||||
"https://docs.ejabberd.im/admin/configuration/file-format/#predefined-keywords[predefined keyword] "
|
||||
"if the file is available in the configuration directory. "
|
||||
"The default value is 'none'.")}},
|
||||
{spam_urls_file,
|
||||
#{value => ?T("none | Path"),
|
||||
desc =>
|
||||
?T("Path to a plain text file containing a list of "
|
||||
"URLs known to be mentioned in spam message bodies. "
|
||||
"Messages containing at least one of the listed URLs are classified as spam. "
|
||||
"Furthermore, the sender's JID will be cached, "
|
||||
"so that future traffic originating from that JID will be classified as spam as well. "
|
||||
"Use an absolute path, or the '@CONFIG_PATH@' "
|
||||
"https://docs.ejabberd.im/admin/configuration/file-format/#predefined-keywords[predefined keyword] "
|
||||
"if the file is available in the configuration directory. "
|
||||
"The default value is 'none'.")}},
|
||||
{whitelist_domains_file,
|
||||
#{value => ?T("none | Path"),
|
||||
desc =>
|
||||
?T("Path to a file containing a list of "
|
||||
"domains to whitelist from being blocked, one per line. "
|
||||
"If either it is in 'spam_domains_file' or more realistically "
|
||||
"in a domain sent by a RTBL host (see option 'rtbl_services') "
|
||||
"then this domain will be ignored and stanzas from there won't be blocked. "
|
||||
"Use an absolute path, or the '@CONFIG_PATH@' "
|
||||
"https://docs.ejabberd.im/admin/configuration/file-format/#predefined-keywords[predefined keyword] "
|
||||
"if the file is available in the configuration directory. "
|
||||
"The default value is 'none'.")}}],
|
||||
example =>
|
||||
["modules:",
|
||||
" mod_antispam:",
|
||||
" rtbl_services:",
|
||||
" - xmppbl.org",
|
||||
" spam_jids_file: \"@CONFIG_PATH@/spam_jids.txt\"",
|
||||
" spam_dump_file: \"@LOG_PATH@/spam/host-@HOST@.log\""]}.
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%%| gen_server callbacks
|
||||
|
||||
-spec init(list()) -> {ok, state()} | {stop, term()}.
|
||||
init([Host, Opts]) ->
|
||||
process_flag(trap_exit, true),
|
||||
mod_antispam_files:init_files(Host),
|
||||
FilesResults = read_files(Host),
|
||||
#{jid := JIDsSet,
|
||||
url := URLsSet,
|
||||
domains := SpamDomainsSet,
|
||||
whitelist_domains := WhitelistDomains} =
|
||||
FilesResults,
|
||||
ejabberd_hooks:add(local_send_to_resource_hook,
|
||||
Host,
|
||||
mod_antispam_rtbl,
|
||||
pubsub_event_handler,
|
||||
50),
|
||||
[#rtbl_service{host = RTBLHost, node = RTBLDomainsNode}] = get_rtbl_services_option(Opts),
|
||||
mod_antispam_filter:init_filtering(Host),
|
||||
InitState =
|
||||
#state{host = Host,
|
||||
jid_set = JIDsSet,
|
||||
url_set = URLsSet,
|
||||
dump_fd = mod_antispam_dump:init_dumping(Host),
|
||||
max_cache_size = gen_mod:get_opt(cache_size, Opts),
|
||||
blocked_domains = set_to_map(SpamDomainsSet),
|
||||
whitelist_domains = set_to_map(WhitelistDomains, false),
|
||||
rtbl_host = RTBLHost,
|
||||
rtbl_domains_node = RTBLDomainsNode},
|
||||
mod_antispam_rtbl:request_blocked_domains(RTBLHost, RTBLDomainsNode, Host),
|
||||
{ok, InitState}.
|
||||
|
||||
-spec handle_call(term(), {pid(), term()}, state()) ->
|
||||
{reply, {spam_filter, term()}, state()} | {noreply, state()}.
|
||||
handle_call({check_jid, From}, _From, #state{jid_set = JIDsSet} = State) ->
|
||||
{Result, State1} = filter_jid(From, JIDsSet, State),
|
||||
{reply, {spam_filter, Result}, State1};
|
||||
handle_call({check_body, URLs, JIDs, From},
|
||||
_From,
|
||||
#state{url_set = URLsSet, jid_set = JIDsSet} = State) ->
|
||||
{Result1, State1} = filter_body(URLs, URLsSet, From, State),
|
||||
{Result2, State2} = filter_body(JIDs, JIDsSet, From, State1),
|
||||
Result =
|
||||
if Result1 == spam ->
|
||||
Result1;
|
||||
true ->
|
||||
Result2
|
||||
end,
|
||||
{reply, {spam_filter, Result}, State2};
|
||||
handle_call(reload_spam_files, _From, State) ->
|
||||
{Result, State1} = reload_files(State),
|
||||
{reply, {spam_filter, Result}, State1};
|
||||
handle_call({expire_cache, Age}, _From, State) ->
|
||||
{Result, State1} = expire_cache(Age, State),
|
||||
{reply, {spam_filter, Result}, State1};
|
||||
handle_call({add_to_cache, JID}, _From, State) ->
|
||||
{Result, State1} = add_to_cache(JID, State),
|
||||
{reply, {spam_filter, Result}, State1};
|
||||
handle_call({drop_from_cache, JID}, _From, State) ->
|
||||
{Result, State1} = drop_from_cache(JID, State),
|
||||
{reply, {spam_filter, Result}, State1};
|
||||
handle_call(get_cache, _From, #state{jid_cache = Cache} = State) ->
|
||||
{reply, {spam_filter, maps:to_list(Cache)}, State};
|
||||
handle_call({add_blocked_domain, Domain},
|
||||
_From,
|
||||
#state{blocked_domains = BlockedDomains} = State) ->
|
||||
BlockedDomains1 = maps:merge(BlockedDomains, #{Domain => true}),
|
||||
Txt = format("~s added to blocked domains", [Domain]),
|
||||
{reply, {spam_filter, {ok, Txt}}, State#state{blocked_domains = BlockedDomains1}};
|
||||
handle_call({remove_blocked_domain, Domain},
|
||||
_From,
|
||||
#state{blocked_domains = BlockedDomains} = State) ->
|
||||
BlockedDomains1 = maps:remove(Domain, BlockedDomains),
|
||||
Txt = format("~s removed from blocked domains", [Domain]),
|
||||
{reply, {spam_filter, {ok, Txt}}, State#state{blocked_domains = BlockedDomains1}};
|
||||
handle_call(get_blocked_domains,
|
||||
_From,
|
||||
#state{blocked_domains = BlockedDomains, whitelist_domains = WhitelistDomains} =
|
||||
State) ->
|
||||
{reply, {blocked_domains, maps:merge(BlockedDomains, WhitelistDomains)}, State};
|
||||
handle_call({is_blocked_domain, Domain},
|
||||
_From,
|
||||
#state{blocked_domains = BlockedDomains, whitelist_domains = WhitelistDomains} =
|
||||
State) ->
|
||||
{reply,
|
||||
maps:get(Domain, maps:merge(BlockedDomains, WhitelistDomains), false) =/= false,
|
||||
State};
|
||||
handle_call(prepare_stop,
|
||||
_From,
|
||||
#state{host = Host,
|
||||
rtbl_host = RTBLHost,
|
||||
rtbl_domains_node = RTBLDomainsNode} =
|
||||
State) ->
|
||||
mod_antispam_rtbl:unsubscribe(RTBLHost, RTBLDomainsNode, Host),
|
||||
{reply, ready_to_stop, State};
|
||||
handle_call(Request, From, State) ->
|
||||
?ERROR_MSG("Got unexpected request from ~p: ~p", [From, Request]),
|
||||
{noreply, State}.
|
||||
|
||||
-spec handle_cast(term(), state()) -> {noreply, state()}.
|
||||
handle_cast({dump_stanza, XML}, #state{dump_fd = Fd} = State) ->
|
||||
mod_antispam_dump:write_stanza_dump(Fd, XML),
|
||||
{noreply, State};
|
||||
handle_cast(reopen_log, #state{host = Host, dump_fd = Fd} = State) ->
|
||||
{noreply, State#state{dump_fd = mod_antispam_dump:reopen_dump_file(Host, Fd)}};
|
||||
handle_cast({reload, NewOpts, OldOpts},
|
||||
#state{host = Host,
|
||||
dump_fd = Fd,
|
||||
rtbl_host = OldRTBLHost,
|
||||
rtbl_domains_node = OldRTBLDomainsNode,
|
||||
rtbl_retry_timer = RTBLRetryTimer} =
|
||||
State) ->
|
||||
misc:cancel_timer(RTBLRetryTimer),
|
||||
State1 =
|
||||
State#state{dump_fd = mod_antispam_dump:reload_dumping(Host, Fd, OldOpts, NewOpts)},
|
||||
State2 =
|
||||
case {gen_mod:get_opt(cache_size, OldOpts), gen_mod:get_opt(cache_size, NewOpts)} of
|
||||
{OldMax, NewMax} when NewMax < OldMax ->
|
||||
shrink_cache(State1#state{max_cache_size = NewMax});
|
||||
{OldMax, NewMax} when NewMax > OldMax ->
|
||||
State1#state{max_cache_size = NewMax};
|
||||
{_OldMax, _NewMax} ->
|
||||
State1
|
||||
end,
|
||||
ok = mod_antispam_rtbl:unsubscribe(OldRTBLHost, OldRTBLDomainsNode, Host),
|
||||
{_Result, State3} = reload_files(State2#state{blocked_domains = #{}}),
|
||||
[#rtbl_service{host = RTBLHost, node = RTBLDomainsNode}] =
|
||||
get_rtbl_services_option(NewOpts),
|
||||
ok = mod_antispam_rtbl:request_blocked_domains(RTBLHost, RTBLDomainsNode, Host),
|
||||
{noreply, State3#state{rtbl_host = RTBLHost, rtbl_domains_node = RTBLDomainsNode}};
|
||||
handle_cast({update_blocked_domains, NewItems},
|
||||
#state{blocked_domains = BlockedDomains} = State) ->
|
||||
{noreply, State#state{blocked_domains = maps:merge(BlockedDomains, NewItems)}};
|
||||
handle_cast(Request, State) ->
|
||||
?ERROR_MSG("Got unexpected request from: ~p", [Request]),
|
||||
{noreply, State}.
|
||||
|
||||
-spec handle_info(term(), state()) -> {noreply, state()}.
|
||||
handle_info({iq_reply, timeout, blocked_domains}, State) ->
|
||||
?WARNING_MSG("Fetching blocked domains failed: fetch timeout. Retrying in 60 seconds",
|
||||
[]),
|
||||
{noreply,
|
||||
State#state{rtbl_retry_timer =
|
||||
erlang:send_after(60000, self(), request_blocked_domains)}};
|
||||
handle_info({iq_reply, #iq{type = error} = IQ, blocked_domains}, State) ->
|
||||
?WARNING_MSG("Fetching blocked domains failed: ~p. Retrying in 60 seconds",
|
||||
[xmpp:format_stanza_error(
|
||||
xmpp:get_error(IQ))]),
|
||||
{noreply,
|
||||
State#state{rtbl_retry_timer =
|
||||
erlang:send_after(60000, self(), request_blocked_domains)}};
|
||||
handle_info({iq_reply, IQReply, blocked_domains},
|
||||
#state{blocked_domains = OldBlockedDomains,
|
||||
rtbl_host = RTBLHost,
|
||||
rtbl_domains_node = RTBLDomainsNode,
|
||||
host = Host} =
|
||||
State) ->
|
||||
case mod_antispam_rtbl:parse_blocked_domains(IQReply) of
|
||||
undefined ->
|
||||
?WARNING_MSG("Fetching initial list failed: invalid result payload", []),
|
||||
{noreply, State#state{rtbl_retry_timer = undefined}};
|
||||
NewBlockedDomains ->
|
||||
ok = mod_antispam_rtbl:subscribe(RTBLHost, RTBLDomainsNode, Host),
|
||||
{noreply,
|
||||
State#state{rtbl_retry_timer = undefined,
|
||||
rtbl_subscribed = true,
|
||||
blocked_domains = maps:merge(OldBlockedDomains, NewBlockedDomains)}}
|
||||
end;
|
||||
handle_info({iq_reply, timeout, subscribe_result}, State) ->
|
||||
?WARNING_MSG("Subscription error: request timeout", []),
|
||||
{noreply, State#state{rtbl_subscribed = false}};
|
||||
handle_info({iq_reply, #iq{type = error} = IQ, subscribe_result}, State) ->
|
||||
?WARNING_MSG("Subscription error: ~p",
|
||||
[xmpp:format_stanza_error(
|
||||
xmpp:get_error(IQ))]),
|
||||
{noreply, State#state{rtbl_subscribed = false}};
|
||||
handle_info({iq_reply, IQReply, subscribe_result}, State) ->
|
||||
?DEBUG("Got subscribe result: ~p", [IQReply]),
|
||||
{noreply, State#state{rtbl_subscribed = true}};
|
||||
handle_info({iq_reply, _IQReply, unsubscribe_result}, State) ->
|
||||
%% FIXME: we should check it's true (of type `result`, not `error`), but at that point, what
|
||||
%% would we do?
|
||||
{noreply, State#state{rtbl_subscribed = false}};
|
||||
handle_info(request_blocked_domains,
|
||||
#state{host = Host,
|
||||
rtbl_host = RTBLHost,
|
||||
rtbl_domains_node = RTBLDomainsNode} =
|
||||
State) ->
|
||||
mod_antispam_rtbl:request_blocked_domains(RTBLHost, RTBLDomainsNode, Host),
|
||||
{noreply, State};
|
||||
handle_info(Info, State) ->
|
||||
?ERROR_MSG("Got unexpected info: ~p", [Info]),
|
||||
{noreply, State}.
|
||||
|
||||
-spec terminate(normal | shutdown | {shutdown, term()} | term(), state()) -> ok.
|
||||
terminate(Reason,
|
||||
#state{host = Host,
|
||||
dump_fd = Fd,
|
||||
rtbl_host = RTBLHost,
|
||||
rtbl_domains_node = RTBLDomainsNode,
|
||||
rtbl_retry_timer = RTBLRetryTimer} =
|
||||
_State) ->
|
||||
?DEBUG("Stopping spam filter process for ~s: ~p", [Host, Reason]),
|
||||
misc:cancel_timer(RTBLRetryTimer),
|
||||
mod_antispam_dump:terminate_dumping(Host, Fd),
|
||||
mod_antispam_files:terminate_files(Host),
|
||||
mod_antispam_filter:terminate_filtering(Host),
|
||||
ejabberd_hooks:delete(local_send_to_resource_hook,
|
||||
Host,
|
||||
mod_antispam_rtbl,
|
||||
pubsub_event_handler,
|
||||
50),
|
||||
mod_antispam_rtbl:unsubscribe(RTBLHost, RTBLDomainsNode, Host),
|
||||
ok.
|
||||
|
||||
-spec code_change({down, term()} | term(), state(), term()) -> {ok, state()}.
|
||||
code_change(_OldVsn, #state{host = Host} = State, _Extra) ->
|
||||
?DEBUG("Updating spam filter process for ~s", [Host]),
|
||||
{ok, State}.
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%%| Internal functions
|
||||
|
||||
-spec filter_jid(ljid(), jid_set(), state()) -> {ham | spam, state()}.
|
||||
filter_jid(From, Set, #state{host = Host} = State) ->
|
||||
case sets:is_element(From, Set) of
|
||||
true ->
|
||||
?DEBUG("Spam JID found: ~s", [jid:encode(From)]),
|
||||
ejabberd_hooks:run(spam_found, Host, [{jid, From}]),
|
||||
{spam, State};
|
||||
false ->
|
||||
case cache_lookup(From, State) of
|
||||
{true, State1} ->
|
||||
?DEBUG("Spam JID found: ~s", [jid:encode(From)]),
|
||||
ejabberd_hooks:run(spam_found, Host, [{jid, From}]),
|
||||
{spam, State1};
|
||||
{false, State1} ->
|
||||
?DEBUG("JID not listed: ~s", [jid:encode(From)]),
|
||||
{ham, State1}
|
||||
end
|
||||
end.
|
||||
|
||||
-spec filter_body({urls, [url()]} | {jids, [ljid()]} | none,
|
||||
url_set() | jid_set(),
|
||||
jid(),
|
||||
state()) ->
|
||||
{ham | spam, state()}.
|
||||
filter_body({_, Addrs}, Set, From, #state{host = Host} = State) ->
|
||||
case lists:any(fun(Addr) -> sets:is_element(Addr, Set) end, Addrs) of
|
||||
true ->
|
||||
?DEBUG("Spam addresses found: ~p", [Addrs]),
|
||||
ejabberd_hooks:run(spam_found, Host, [{body, Addrs}]),
|
||||
{spam, cache_insert(From, State)};
|
||||
false ->
|
||||
?DEBUG("Addresses not listed: ~p", [Addrs]),
|
||||
{ham, State}
|
||||
end;
|
||||
filter_body(none, _Set, _From, State) ->
|
||||
{ham, State}.
|
||||
|
||||
-spec reload_files(state()) -> {ok | {error, binary()}, state()}.
|
||||
reload_files(#state{host = Host, blocked_domains = BlockedDomains} = State) ->
|
||||
case read_files(Host) of
|
||||
#{jid := JIDsSet,
|
||||
url := URLsSet,
|
||||
domains := SpamDomainsSet,
|
||||
whitelist_domains := WhitelistDomains} ->
|
||||
case sets_equal(JIDsSet, State#state.jid_set) of
|
||||
true ->
|
||||
?INFO_MSG("Reloaded spam JIDs for ~s (unchanged)", [Host]);
|
||||
false ->
|
||||
?INFO_MSG("Reloaded spam JIDs for ~s (changed)", [Host])
|
||||
end,
|
||||
case sets_equal(URLsSet, State#state.url_set) of
|
||||
true ->
|
||||
?INFO_MSG("Reloaded spam URLs for ~s (unchanged)", [Host]);
|
||||
false ->
|
||||
?INFO_MSG("Reloaded spam URLs for ~s (changed)", [Host])
|
||||
end,
|
||||
{ok,
|
||||
State#state{jid_set = JIDsSet,
|
||||
url_set = URLsSet,
|
||||
blocked_domains = maps:merge(BlockedDomains, set_to_map(SpamDomainsSet)),
|
||||
whitelist_domains = set_to_map(WhitelistDomains, false)}};
|
||||
{config_error, ErrorText} ->
|
||||
{{error, ErrorText}, State}
|
||||
end.
|
||||
|
||||
set_to_map(Set) ->
|
||||
set_to_map(Set, true).
|
||||
|
||||
set_to_map(Set, V) ->
|
||||
sets:fold(fun(K, M) -> M#{K => V} end, #{}, Set).
|
||||
|
||||
read_files(Host) ->
|
||||
AccInitial =
|
||||
#{jid => sets:new(),
|
||||
url => sets:new(),
|
||||
domains => sets:new(),
|
||||
whitelist_domains => sets:new()},
|
||||
Files =
|
||||
#{jid => gen_mod:get_module_opt(Host, ?MODULE, spam_jids_file),
|
||||
url => gen_mod:get_module_opt(Host, ?MODULE, spam_urls_file),
|
||||
domains => gen_mod:get_module_opt(Host, ?MODULE, spam_domains_file),
|
||||
whitelist_domains => gen_mod:get_module_opt(Host, ?MODULE, whitelist_domains_file)},
|
||||
ejabberd_hooks:run_fold(antispam_get_lists, Host, AccInitial, [Files]).
|
||||
|
||||
get_rtbl_services_option(Host) when is_binary(Host) ->
|
||||
get_rtbl_services_option(gen_mod:get_module_opts(Host, ?MODULE));
|
||||
get_rtbl_services_option(Opts) when is_map(Opts) ->
|
||||
Services = gen_mod:get_opt(rtbl_services, Opts),
|
||||
case length(Services) =< 1 of
|
||||
true ->
|
||||
ok;
|
||||
false ->
|
||||
?WARNING_MSG("Option rtbl_services only supports one service, but several "
|
||||
"were configured. Will use only first one",
|
||||
[])
|
||||
end,
|
||||
case Services of
|
||||
[] ->
|
||||
[#rtbl_service{}];
|
||||
[Host | _] when is_binary(Host) ->
|
||||
[#rtbl_service{host = Host, node = ?DEFAULT_RTBL_DOMAINS_NODE}];
|
||||
[[{Host, [{spam_source_domains_node, Node}]}] | _] ->
|
||||
[#rtbl_service{host = Host, node = Node}]
|
||||
end.
|
||||
|
||||
-spec get_proc_name(binary()) -> atom().
|
||||
get_proc_name(Host) ->
|
||||
gen_mod:get_module_proc(Host, ?MODULE).
|
||||
|
||||
-spec get_spam_filter_hosts() -> [binary()].
|
||||
get_spam_filter_hosts() ->
|
||||
[H || H <- ejabberd_option:hosts(), gen_mod:is_loaded(H, ?MODULE)].
|
||||
|
||||
-spec sets_equal(sets:set(), sets:set()) -> boolean().
|
||||
sets_equal(A, B) ->
|
||||
sets:is_subset(A, B) andalso sets:is_subset(B, A).
|
||||
|
||||
-spec format(io:format(), [term()]) -> binary().
|
||||
format(Format, Data) ->
|
||||
iolist_to_binary(io_lib:format(Format, Data)).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%%| Caching
|
||||
|
||||
-spec cache_insert(ljid(), state()) -> state().
|
||||
cache_insert(_LJID, #state{max_cache_size = 0} = State) ->
|
||||
State;
|
||||
cache_insert(LJID, #state{jid_cache = Cache, max_cache_size = MaxSize} = State)
|
||||
when MaxSize /= unlimited, map_size(Cache) >= MaxSize ->
|
||||
cache_insert(LJID, shrink_cache(State));
|
||||
cache_insert(LJID, #state{jid_cache = Cache} = State) ->
|
||||
?INFO_MSG("Caching spam JID: ~s", [jid:encode(LJID)]),
|
||||
Cache1 = Cache#{LJID => erlang:monotonic_time(second)},
|
||||
State#state{jid_cache = Cache1}.
|
||||
|
||||
-spec cache_lookup(ljid(), state()) -> {boolean(), state()}.
|
||||
cache_lookup(LJID, #state{jid_cache = Cache} = State) ->
|
||||
case Cache of
|
||||
#{LJID := _Timestamp} ->
|
||||
Cache1 = Cache#{LJID => erlang:monotonic_time(second)},
|
||||
State1 = State#state{jid_cache = Cache1},
|
||||
{true, State1};
|
||||
#{} ->
|
||||
{false, State}
|
||||
end.
|
||||
|
||||
-spec shrink_cache(state()) -> state().
|
||||
shrink_cache(#state{jid_cache = Cache, max_cache_size = MaxSize} = State) ->
|
||||
ShrinkedSize = round(MaxSize / 2),
|
||||
N = map_size(Cache) - ShrinkedSize,
|
||||
L = lists:keysort(2, maps:to_list(Cache)),
|
||||
Cache1 =
|
||||
maps:from_list(
|
||||
lists:nthtail(N, L)),
|
||||
State#state{jid_cache = Cache1}.
|
||||
|
||||
-spec expire_cache(integer(), state()) -> {{ok, binary()}, state()}.
|
||||
expire_cache(Age, #state{jid_cache = Cache} = State) ->
|
||||
Threshold = erlang:monotonic_time(second) - Age,
|
||||
Cache1 = maps:filter(fun(_, TS) -> TS >= Threshold end, Cache),
|
||||
NumExp = map_size(Cache) - map_size(Cache1),
|
||||
Txt = format("Expired ~B cache entries", [NumExp]),
|
||||
{{ok, Txt}, State#state{jid_cache = Cache1}}.
|
||||
|
||||
-spec add_to_cache(ljid(), state()) -> {{ok, binary()}, state()}.
|
||||
add_to_cache(LJID, State) ->
|
||||
State1 = cache_insert(LJID, State),
|
||||
Txt = format("~s added to cache", [jid:encode(LJID)]),
|
||||
{{ok, Txt}, State1}.
|
||||
|
||||
-spec drop_from_cache(ljid(), state()) -> {{ok, binary()}, state()}.
|
||||
drop_from_cache(LJID, #state{jid_cache = Cache} = State) ->
|
||||
Cache1 = maps:remove(LJID, Cache),
|
||||
if map_size(Cache1) < map_size(Cache) ->
|
||||
Txt = format("~s removed from cache", [jid:encode(LJID)]),
|
||||
{{ok, Txt}, State#state{jid_cache = Cache1}};
|
||||
true ->
|
||||
Txt = format("~s wasn't cached", [jid:encode(LJID)]),
|
||||
{{ok, Txt}, State}
|
||||
end.
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%%| ejabberd command callbacks
|
||||
|
||||
-spec get_commands_spec() -> [ejabberd_commands()].
|
||||
get_commands_spec() ->
|
||||
[#ejabberd_commands{name = reload_spam_filter_files,
|
||||
tags = [spam],
|
||||
desc = "Reload spam JID/URL files",
|
||||
module = ?MODULE,
|
||||
function = reload_spam_filter_files,
|
||||
note = "added in 25.07",
|
||||
args = [{host, binary}],
|
||||
result = {res, rescode}},
|
||||
#ejabberd_commands{name = get_spam_filter_cache,
|
||||
tags = [spam],
|
||||
desc = "Show spam filter cache contents",
|
||||
module = ?MODULE,
|
||||
function = get_spam_filter_cache,
|
||||
note = "added in 25.07",
|
||||
args = [{host, binary}],
|
||||
result =
|
||||
{spammers,
|
||||
{list, {spammer, {tuple, [{jid, string}, {timestamp, integer}]}}}}},
|
||||
#ejabberd_commands{name = expire_spam_filter_cache,
|
||||
tags = [spam],
|
||||
desc = "Remove old/unused spam JIDs from cache",
|
||||
module = ?MODULE,
|
||||
function = expire_spam_filter_cache,
|
||||
note = "added in 25.07",
|
||||
args = [{host, binary}, {seconds, integer}],
|
||||
result = {res, restuple}},
|
||||
#ejabberd_commands{name = add_to_spam_filter_cache,
|
||||
tags = [spam],
|
||||
desc = "Add JID to spam filter cache",
|
||||
module = ?MODULE,
|
||||
function = add_to_spam_filter_cache,
|
||||
note = "added in 25.07",
|
||||
args = [{host, binary}, {jid, binary}],
|
||||
result = {res, restuple}},
|
||||
#ejabberd_commands{name = drop_from_spam_filter_cache,
|
||||
tags = [spam],
|
||||
desc = "Drop JID from spam filter cache",
|
||||
module = ?MODULE,
|
||||
function = drop_from_spam_filter_cache,
|
||||
note = "added in 25.07",
|
||||
args = [{host, binary}, {jid, binary}],
|
||||
result = {res, restuple}},
|
||||
#ejabberd_commands{name = get_blocked_domains,
|
||||
tags = [spam],
|
||||
desc = "Get list of domains being blocked",
|
||||
module = ?MODULE,
|
||||
function = get_blocked_domains,
|
||||
note = "added in 25.07",
|
||||
args = [{host, binary}],
|
||||
result = {blocked_domains, {list, {jid, string}}}},
|
||||
#ejabberd_commands{name = add_blocked_domain,
|
||||
tags = [spam],
|
||||
desc = "Add domain to list of blocked domains",
|
||||
module = ?MODULE,
|
||||
function = add_blocked_domain,
|
||||
note = "added in 25.07",
|
||||
args = [{host, binary}, {domain, binary}],
|
||||
result = {res, restuple}},
|
||||
#ejabberd_commands{name = remove_blocked_domain,
|
||||
tags = [spam],
|
||||
desc = "Remove domain from list of blocked domains",
|
||||
module = ?MODULE,
|
||||
function = remove_blocked_domain,
|
||||
note = "added in 25.07",
|
||||
args = [{host, binary}, {domain, binary}],
|
||||
result = {res, restuple}}].
|
||||
|
||||
for_all_hosts(F, A) ->
|
||||
try lists:map(fun(Host) -> apply(F, [Host | A]) end, get_spam_filter_hosts()) of
|
||||
List ->
|
||||
case lists:filter(fun ({error, _}) ->
|
||||
true;
|
||||
(_) ->
|
||||
false
|
||||
end,
|
||||
List)
|
||||
of
|
||||
[] ->
|
||||
hd(List);
|
||||
Errors ->
|
||||
hd(Errors)
|
||||
end
|
||||
catch
|
||||
error:{badmatch, {error, _Reason} = Error} ->
|
||||
Error
|
||||
end.
|
||||
|
||||
try_call_by_host(Host, Call) ->
|
||||
LServer = jid:nameprep(Host),
|
||||
Proc = get_proc_name(LServer),
|
||||
try gen_server:call(Proc, Call, ?COMMAND_TIMEOUT) of
|
||||
Result ->
|
||||
Result
|
||||
catch
|
||||
exit:{noproc, _} ->
|
||||
{error, "Not configured for " ++ binary_to_list(Host)};
|
||||
exit:{timeout, _} ->
|
||||
{error, "Timeout while querying ejabberd"}
|
||||
end.
|
||||
|
||||
-spec reload_spam_filter_files(binary()) -> ok | {error, string()}.
|
||||
reload_spam_filter_files(<<"global">>) ->
|
||||
for_all_hosts(fun reload_spam_filter_files/1, []);
|
||||
reload_spam_filter_files(Host) ->
|
||||
case try_call_by_host(Host, reload_spam_files) of
|
||||
{spam_filter, ok} ->
|
||||
ok;
|
||||
{spam_filter, {error, Txt}} ->
|
||||
{error, Txt};
|
||||
{error, _R} = Error ->
|
||||
Error
|
||||
end.
|
||||
|
||||
-spec get_blocked_domains(binary()) -> [binary()].
|
||||
get_blocked_domains(Host) ->
|
||||
case try_call_by_host(Host, get_blocked_domains) of
|
||||
{blocked_domains, BlockedDomains} ->
|
||||
maps:keys(
|
||||
maps:filter(fun (_, false) ->
|
||||
false;
|
||||
(_, _) ->
|
||||
true
|
||||
end,
|
||||
BlockedDomains));
|
||||
{error, _R} = Error ->
|
||||
Error
|
||||
end.
|
||||
|
||||
-spec add_blocked_domain(binary(), binary()) -> {ok, string()}.
|
||||
add_blocked_domain(<<"global">>, Domain) ->
|
||||
for_all_hosts(fun add_blocked_domain/2, [Domain]);
|
||||
add_blocked_domain(Host, Domain) ->
|
||||
case try_call_by_host(Host, {add_blocked_domain, Domain}) of
|
||||
{spam_filter, {Status, Txt}} ->
|
||||
{Status, binary_to_list(Txt)};
|
||||
{error, _R} = Error ->
|
||||
Error
|
||||
end.
|
||||
|
||||
-spec remove_blocked_domain(binary(), binary()) -> {ok, string()}.
|
||||
remove_blocked_domain(<<"global">>, Domain) ->
|
||||
for_all_hosts(fun remove_blocked_domain/2, [Domain]);
|
||||
remove_blocked_domain(Host, Domain) ->
|
||||
case try_call_by_host(Host, {remove_blocked_domain, Domain}) of
|
||||
{spam_filter, {Status, Txt}} ->
|
||||
{Status, binary_to_list(Txt)};
|
||||
{error, _R} = Error ->
|
||||
Error
|
||||
end.
|
||||
|
||||
-spec get_spam_filter_cache(binary()) -> [{binary(), integer()}] | {error, string()}.
|
||||
get_spam_filter_cache(Host) ->
|
||||
case try_call_by_host(Host, get_cache) of
|
||||
{spam_filter, Cache} ->
|
||||
[{jid:encode(JID), TS + erlang:time_offset(second)} || {JID, TS} <- Cache];
|
||||
{error, _R} = Error ->
|
||||
Error
|
||||
end.
|
||||
|
||||
-spec expire_spam_filter_cache(binary(), integer()) -> {ok | error, string()}.
|
||||
expire_spam_filter_cache(<<"global">>, Age) ->
|
||||
for_all_hosts(fun expire_spam_filter_cache/2, [Age]);
|
||||
expire_spam_filter_cache(Host, Age) ->
|
||||
case try_call_by_host(Host, {expire_cache, Age}) of
|
||||
{spam_filter, {Status, Txt}} ->
|
||||
{Status, binary_to_list(Txt)};
|
||||
{error, _R} = Error ->
|
||||
Error
|
||||
end.
|
||||
|
||||
-spec add_to_spam_filter_cache(binary(), binary()) ->
|
||||
[{binary(), integer()}] | {error, string()}.
|
||||
add_to_spam_filter_cache(<<"global">>, JID) ->
|
||||
for_all_hosts(fun add_to_spam_filter_cache/2, [JID]);
|
||||
add_to_spam_filter_cache(Host, EncJID) ->
|
||||
try jid:decode(EncJID) of
|
||||
#jid{} = JID ->
|
||||
LJID =
|
||||
jid:remove_resource(
|
||||
jid:tolower(JID)),
|
||||
case try_call_by_host(Host, {add_to_cache, LJID}) of
|
||||
{spam_filter, {Status, Txt}} ->
|
||||
{Status, binary_to_list(Txt)};
|
||||
{error, _R} = Error ->
|
||||
Error
|
||||
end
|
||||
catch
|
||||
_:{bad_jid, _} ->
|
||||
{error, "Not a valid JID: " ++ binary_to_list(EncJID)}
|
||||
end.
|
||||
|
||||
-spec drop_from_spam_filter_cache(binary(), binary()) -> {ok | error, string()}.
|
||||
drop_from_spam_filter_cache(<<"global">>, JID) ->
|
||||
for_all_hosts(fun drop_from_spam_filter_cache/2, [JID]);
|
||||
drop_from_spam_filter_cache(Host, EncJID) ->
|
||||
try jid:decode(EncJID) of
|
||||
#jid{} = JID ->
|
||||
LJID =
|
||||
jid:remove_resource(
|
||||
jid:tolower(JID)),
|
||||
case try_call_by_host(Host, {drop_from_cache, LJID}) of
|
||||
{spam_filter, {Status, Txt}} ->
|
||||
{Status, binary_to_list(Txt)};
|
||||
{error, _R} = Error ->
|
||||
Error
|
||||
end
|
||||
catch
|
||||
_:{bad_jid, _} ->
|
||||
{error, "Not a valid JID: " ++ binary_to_list(EncJID)}
|
||||
end.
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
%%| vim: set foldmethod=marker foldmarker=%%|,%%-:
|
||||
@@ -0,0 +1,186 @@
|
||||
%%%----------------------------------------------------------------------
|
||||
%%% File : mod_antispam_dump.erl
|
||||
%%% Author : Holger Weiss <holger@zedat.fu-berlin.de>
|
||||
%%% Author : Stefan Strigler <stefan@strigler.de>
|
||||
%%% Purpose : Manage dump file for filtered spam messages
|
||||
%%% Created : 31 Mar 2019 by Holger Weiss <holger@zedat.fu-berlin.de>
|
||||
%%%
|
||||
%%%
|
||||
%%% ejabberd, Copyright (C) 2019-2025 ProcessOne
|
||||
%%%
|
||||
%%% This program is free software; you can redistribute it and/or
|
||||
%%% modify it under the terms of the GNU General Public License as
|
||||
%%% published by the Free Software Foundation; either version 2 of the
|
||||
%%% License, or (at your option) any later version.
|
||||
%%%
|
||||
%%% This program is distributed in the hope that it will be useful,
|
||||
%%% but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
%%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
%%% General Public License for more details.
|
||||
%%%
|
||||
%%% You should have received a copy of the GNU General Public License along
|
||||
%%% with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
%%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
%%%
|
||||
%%%----------------------------------------------------------------------
|
||||
|
||||
%%| Definitions
|
||||
%% @format-begin
|
||||
|
||||
-module(mod_antispam_dump).
|
||||
|
||||
-author('holger@zedat.fu-berlin.de').
|
||||
-author('stefan@strigler.de').
|
||||
|
||||
-export([init_dumping/1, terminate_dumping/2, reload_dumping/4, reopen_dump_file/2,
|
||||
write_stanza_dump/2]).
|
||||
%% ejabberd_hooks callbacks
|
||||
-export([dump_spam_stanza/1, reopen_log/0]).
|
||||
|
||||
-include("logger.hrl").
|
||||
-include("mod_antispam.hrl").
|
||||
-include("translate.hrl").
|
||||
|
||||
-include_lib("xmpp/include/xmpp.hrl").
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%%| Exported
|
||||
|
||||
init_dumping(Host) ->
|
||||
case get_path_option(Host) of
|
||||
false ->
|
||||
undefined;
|
||||
DumpFile when is_binary(DumpFile) ->
|
||||
case filelib:ensure_dir(DumpFile) of
|
||||
ok ->
|
||||
ejabberd_hooks:add(spam_stanza_rejected, Host, ?MODULE, dump_spam_stanza, 50),
|
||||
ejabberd_hooks:add(reopen_log_hook, ?MODULE, reopen_log, 50),
|
||||
open_dump_file(DumpFile);
|
||||
{error, Reason} ->
|
||||
Dirname = filename:dirname(DumpFile),
|
||||
throw({open, Dirname, Reason})
|
||||
end
|
||||
end.
|
||||
|
||||
terminate_dumping(_Host, false) ->
|
||||
ok;
|
||||
terminate_dumping(Host, Fd) ->
|
||||
DumpFile1 = get_path_option(Host),
|
||||
close_dump_file(Fd, DumpFile1),
|
||||
ejabberd_hooks:delete(spam_stanza_rejected, Host, ?MODULE, dump_spam_stanza, 50),
|
||||
case gen_mod:is_loaded_elsewhere(Host, ?MODULE) of
|
||||
false ->
|
||||
ejabberd_hooks:delete(reopen_log_hook, ?MODULE, reopen_log, 50);
|
||||
true ->
|
||||
ok
|
||||
end.
|
||||
|
||||
reload_dumping(Host, Fd, OldOpts, NewOpts) ->
|
||||
case {get_path_option(Host, OldOpts), get_path_option(Host, NewOpts)} of
|
||||
{Old, Old} ->
|
||||
Fd;
|
||||
{Old, New} ->
|
||||
reopen_dump_file(Fd, Old, New)
|
||||
end.
|
||||
|
||||
-spec reopen_dump_file(binary(), file:io_device()) -> file:io_device().
|
||||
reopen_dump_file(Host, Fd) ->
|
||||
DumpFile1 = get_path_option(Host),
|
||||
reopen_dump_file(Fd, DumpFile1, DumpFile1).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%%| Hook callbacks
|
||||
|
||||
-spec dump_spam_stanza(message()) -> ok.
|
||||
dump_spam_stanza(#message{to = #jid{lserver = LServer}} = Msg) ->
|
||||
By = jid:make(<<>>, LServer),
|
||||
Proc = get_proc_name(LServer),
|
||||
Time = erlang:timestamp(),
|
||||
Msg1 = misc:add_delay_info(Msg, By, Time),
|
||||
XML = fxml:element_to_binary(
|
||||
xmpp:encode(Msg1)),
|
||||
gen_server:cast(Proc, {dump_stanza, XML}).
|
||||
|
||||
-spec reopen_log() -> ok.
|
||||
reopen_log() ->
|
||||
lists:foreach(fun(Host) ->
|
||||
Proc = get_proc_name(Host),
|
||||
gen_server:cast(Proc, reopen_log)
|
||||
end,
|
||||
get_spam_filter_hosts()).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%%| File management
|
||||
|
||||
-spec open_dump_file(filename()) -> undefined | file:io_device().
|
||||
open_dump_file(false) ->
|
||||
undefined;
|
||||
open_dump_file(Name) ->
|
||||
Modes = [append, raw, binary, delayed_write],
|
||||
case file:open(Name, Modes) of
|
||||
{ok, Fd} ->
|
||||
?DEBUG("Opened ~s", [Name]),
|
||||
Fd;
|
||||
{error, Reason} ->
|
||||
?ERROR_MSG("Cannot open dump file ~s: ~s", [Name, file:format_error(Reason)]),
|
||||
undefined
|
||||
end.
|
||||
|
||||
-spec close_dump_file(undefined | file:io_device(), filename()) -> ok.
|
||||
close_dump_file(undefined, false) ->
|
||||
ok;
|
||||
close_dump_file(Fd, Name) ->
|
||||
case file:close(Fd) of
|
||||
ok ->
|
||||
?DEBUG("Closed ~s", [Name]);
|
||||
{error, Reason} ->
|
||||
?ERROR_MSG("Cannot close ~s: ~s", [Name, file:format_error(Reason)])
|
||||
end.
|
||||
|
||||
-spec reopen_dump_file(file:io_device(), binary(), binary()) -> file:io_device().
|
||||
reopen_dump_file(Fd, OldDumpFile, NewDumpFile) ->
|
||||
close_dump_file(Fd, OldDumpFile),
|
||||
open_dump_file(NewDumpFile).
|
||||
|
||||
write_stanza_dump(Fd, XML) ->
|
||||
case file:write(Fd, [XML, <<$\n>>]) of
|
||||
ok ->
|
||||
ok;
|
||||
{error, Reason} ->
|
||||
?ERROR_MSG("Cannot write spam to dump file: ~s", [file:format_error(Reason)])
|
||||
end.
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%%| Auxiliary
|
||||
|
||||
get_path_option(Host) ->
|
||||
Opts = gen_mod:get_module_opts(Host, ?MODULE_ANTISPAM),
|
||||
get_path_option(Host, Opts).
|
||||
|
||||
get_path_option(Host, Opts) ->
|
||||
case gen_mod:get_opt(spam_dump_file, Opts) of
|
||||
false ->
|
||||
false;
|
||||
true ->
|
||||
LogDirPath =
|
||||
iolist_to_binary(filename:dirname(
|
||||
ejabberd_logger:get_log_path())),
|
||||
filename:join([LogDirPath, <<"spam_dump_", Host/binary, ".log">>]);
|
||||
B when is_binary(B) ->
|
||||
B
|
||||
end.
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%%| Copied from mod_antispam.erl
|
||||
|
||||
-spec get_proc_name(binary()) -> atom().
|
||||
get_proc_name(Host) ->
|
||||
gen_mod:get_module_proc(Host, ?MODULE_ANTISPAM).
|
||||
|
||||
-spec get_spam_filter_hosts() -> [binary()].
|
||||
get_spam_filter_hosts() ->
|
||||
[H || H <- ejabberd_option:hosts(), gen_mod:is_loaded(H, ?MODULE_ANTISPAM)].
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
%%| vim: set foldmethod=marker foldmarker=%%|,%%-:
|
||||
@@ -0,0 +1,181 @@
|
||||
%%%----------------------------------------------------------------------
|
||||
%%% File : mod_antispam_files.erl
|
||||
%%% Author : Holger Weiss <holger@zedat.fu-berlin.de>
|
||||
%%% Author : Stefan Strigler <stefan@strigler.de>
|
||||
%%% Purpose : Filter spam messages based on sender JID and content
|
||||
%%% Created : 31 Mar 2019 by Holger Weiss <holger@zedat.fu-berlin.de>
|
||||
%%%
|
||||
%%%
|
||||
%%% ejabberd, Copyright (C) 2019-2025 ProcessOne
|
||||
%%%
|
||||
%%% This program is free software; you can redistribute it and/or
|
||||
%%% modify it under the terms of the GNU General Public License as
|
||||
%%% published by the Free Software Foundation; either version 2 of the
|
||||
%%% License, or (at your option) any later version.
|
||||
%%%
|
||||
%%% This program is distributed in the hope that it will be useful,
|
||||
%%% but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
%%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
%%% General Public License for more details.
|
||||
%%%
|
||||
%%% You should have received a copy of the GNU General Public License along
|
||||
%%% with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
%%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
%%%
|
||||
%%%----------------------------------------------------------------------
|
||||
|
||||
%%| definitions
|
||||
%% @format-begin
|
||||
|
||||
-module(mod_antispam_files).
|
||||
|
||||
-author('holger@zedat.fu-berlin.de').
|
||||
-author('stefan@strigler.de').
|
||||
|
||||
%% Exported
|
||||
-export([init_files/1, terminate_files/1]).
|
||||
% Hooks
|
||||
-export([get_files_lists/2]).
|
||||
|
||||
-include("ejabberd_commands.hrl").
|
||||
-include("logger.hrl").
|
||||
-include("mod_antispam.hrl").
|
||||
-include("translate.hrl").
|
||||
|
||||
-include_lib("xmpp/include/xmpp.hrl").
|
||||
|
||||
-type files_map() :: #{atom() => filename()}.
|
||||
-type lists_map() ::
|
||||
#{jid => jid_set(),
|
||||
url => url_set(),
|
||||
atom() => sets:set(binary())}.
|
||||
|
||||
-define(COMMAND_TIMEOUT, timer:seconds(30)).
|
||||
-define(DEFAULT_CACHE_SIZE, 10000).
|
||||
-define(HTTPC_TIMEOUT, timer:seconds(3)).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%%| Exported
|
||||
|
||||
init_files(Host) ->
|
||||
ejabberd_hooks:add(antispam_get_lists, Host, ?MODULE, get_files_lists, 50).
|
||||
|
||||
terminate_files(Host) ->
|
||||
ejabberd_hooks:delete(antispam_get_lists, Host, ?MODULE, get_files_lists, 50).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%%| Hooks
|
||||
|
||||
-spec get_files_lists(lists_map(), files_map()) -> lists_map().
|
||||
get_files_lists(#{jid := AccJids,
|
||||
url := AccUrls,
|
||||
domains := AccDomains,
|
||||
whitelist_domains := AccWhitelist} =
|
||||
Acc,
|
||||
Files) ->
|
||||
try read_files(Files) of
|
||||
#{jid := JIDsSet,
|
||||
url := URLsSet,
|
||||
domains := SpamDomainsSet,
|
||||
whitelist_domains := WhitelistDomains} ->
|
||||
Acc#{jid => sets:union(AccJids, JIDsSet),
|
||||
url => sets:union(AccUrls, URLsSet),
|
||||
domains => sets:union(AccDomains, SpamDomainsSet),
|
||||
whitelist_domains => sets:union(AccWhitelist, WhitelistDomains)}
|
||||
catch
|
||||
{Op, File, Reason} when Op == open; Op == read ->
|
||||
ErrorText = format("Error trying to ~s file ~s: ~s", [Op, File, format_error(Reason)]),
|
||||
?CRITICAL_MSG(ErrorText, []),
|
||||
{stop, {config_error, ErrorText}}
|
||||
end.
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%%| read_files
|
||||
|
||||
-spec read_files(files_map()) -> lists_map().
|
||||
read_files(Files) ->
|
||||
maps:map(fun(Type, Filename) -> read_file(Filename, line_parser(Type)) end, Files).
|
||||
|
||||
-spec line_parser(Type :: atom()) -> fun((binary()) -> binary()).
|
||||
line_parser(jid) ->
|
||||
fun parse_jid/1;
|
||||
line_parser(url) ->
|
||||
fun parse_url/1;
|
||||
line_parser(_) ->
|
||||
fun trim/1.
|
||||
|
||||
-spec read_file(filename(), fun((binary()) -> ljid() | url())) -> jid_set() | url_set().
|
||||
read_file(none, _ParseLine) ->
|
||||
sets:new();
|
||||
read_file(File, ParseLine) ->
|
||||
case file:open(File, [read, binary, raw, {read_ahead, 65536}]) of
|
||||
{ok, Fd} ->
|
||||
try
|
||||
read_line(Fd, ParseLine, sets:new())
|
||||
catch
|
||||
E ->
|
||||
throw({read, File, E})
|
||||
after
|
||||
ok = file:close(Fd)
|
||||
end;
|
||||
{error, Reason} ->
|
||||
throw({open, File, Reason})
|
||||
end.
|
||||
|
||||
-spec read_line(file:io_device(),
|
||||
fun((binary()) -> ljid() | url()),
|
||||
jid_set() | url_set()) ->
|
||||
jid_set() | url_set().
|
||||
read_line(Fd, ParseLine, Set) ->
|
||||
case file:read_line(Fd) of
|
||||
{ok, Line} ->
|
||||
read_line(Fd, ParseLine, sets:add_element(ParseLine(Line), Set));
|
||||
{error, Reason} ->
|
||||
throw(Reason);
|
||||
eof ->
|
||||
Set
|
||||
end.
|
||||
|
||||
-spec parse_jid(binary()) -> ljid().
|
||||
parse_jid(S) ->
|
||||
try jid:decode(trim(S)) of
|
||||
#jid{} = JID ->
|
||||
jid:remove_resource(
|
||||
jid:tolower(JID))
|
||||
catch
|
||||
_:{bad_jid, _} ->
|
||||
throw({bad_jid, S})
|
||||
end.
|
||||
|
||||
-spec parse_url(binary()) -> url().
|
||||
parse_url(S) ->
|
||||
URL = trim(S),
|
||||
RE = <<"https?://\\S+$">>,
|
||||
Options = [anchored, caseless, {capture, none}],
|
||||
case re:run(URL, RE, Options) of
|
||||
match ->
|
||||
URL;
|
||||
nomatch ->
|
||||
throw({bad_url, S})
|
||||
end.
|
||||
|
||||
-spec trim(binary()) -> binary().
|
||||
trim(S) ->
|
||||
re:replace(S, <<"\\s+$">>, <<>>, [{return, binary}]).
|
||||
|
||||
%% Function copied from mod_antispam.erl
|
||||
-spec format(io:format(), [term()]) -> binary().
|
||||
format(Format, Data) ->
|
||||
iolist_to_binary(io_lib:format(Format, Data)).
|
||||
|
||||
-spec format_error(atom() | tuple()) -> binary().
|
||||
format_error({bad_jid, JID}) ->
|
||||
<<"Not a valid JID: ", JID/binary>>;
|
||||
format_error({bad_url, URL}) ->
|
||||
<<"Not an HTTP(S) URL: ", URL/binary>>;
|
||||
format_error(Reason) ->
|
||||
list_to_binary(file:format_error(Reason)).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
%%| vim: set foldmethod=marker foldmarker=%%|,%%-:
|
||||
@@ -0,0 +1,298 @@
|
||||
%%%----------------------------------------------------------------------
|
||||
%%% File : mod_antispam_filter.erl
|
||||
%%% Author : Holger Weiss <holger@zedat.fu-berlin.de>
|
||||
%%% Author : Stefan Strigler <stefan@strigler.de>
|
||||
%%% Purpose : Filter C2S and S2S stanzas
|
||||
%%% Created : 31 Mar 2019 by Holger Weiss <holger@zedat.fu-berlin.de>
|
||||
%%%
|
||||
%%%
|
||||
%%% ejabberd, Copyright (C) 2019-2025 ProcessOne
|
||||
%%%
|
||||
%%% This program is free software; you can redistribute it and/or
|
||||
%%% modify it under the terms of the GNU General Public License as
|
||||
%%% published by the Free Software Foundation; either version 2 of the
|
||||
%%% License, or (at your option) any later version.
|
||||
%%%
|
||||
%%% This program is distributed in the hope that it will be useful,
|
||||
%%% but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
%%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
%%% General Public License for more details.
|
||||
%%%
|
||||
%%% You should have received a copy of the GNU General Public License along
|
||||
%%% with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
%%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
%%%
|
||||
%%%----------------------------------------------------------------------
|
||||
|
||||
%%| Definitions
|
||||
%% @format-begin
|
||||
|
||||
-module(mod_antispam_filter).
|
||||
|
||||
-author('holger@zedat.fu-berlin.de').
|
||||
-author('stefan@strigler.de').
|
||||
|
||||
-export([init_filtering/1, terminate_filtering/1]).
|
||||
%% ejabberd_hooks callbacks
|
||||
-export([s2s_in_handle_info/2, s2s_receive_packet/1, sm_receive_packet/1]).
|
||||
|
||||
-include("logger.hrl").
|
||||
-include("translate.hrl").
|
||||
-include("mod_antispam.hrl").
|
||||
|
||||
-include_lib("xmpp/include/xmpp.hrl").
|
||||
|
||||
-type s2s_in_state() :: ejabberd_s2s_in:state().
|
||||
|
||||
-define(HTTPC_TIMEOUT, timer:seconds(3)).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%%| Exported
|
||||
|
||||
init_filtering(Host) ->
|
||||
ejabberd_hooks:add(s2s_in_handle_info, Host, ?MODULE, s2s_in_handle_info, 90),
|
||||
ejabberd_hooks:add(s2s_receive_packet, Host, ?MODULE, s2s_receive_packet, 50),
|
||||
ejabberd_hooks:add(sm_receive_packet, Host, ?MODULE, sm_receive_packet, 50).
|
||||
|
||||
terminate_filtering(Host) ->
|
||||
ejabberd_hooks:delete(s2s_receive_packet, Host, ?MODULE, s2s_receive_packet, 50),
|
||||
ejabberd_hooks:delete(sm_receive_packet, Host, ?MODULE, sm_receive_packet, 50),
|
||||
ejabberd_hooks:delete(s2s_in_handle_info, Host, ?MODULE, s2s_in_handle_info, 90).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%%| Hook callbacks
|
||||
|
||||
-spec s2s_receive_packet({stanza() | drop, s2s_in_state()}) ->
|
||||
{stanza() | drop, s2s_in_state()} | {stop, {drop, s2s_in_state()}}.
|
||||
s2s_receive_packet({A, State}) ->
|
||||
case sm_receive_packet(A) of
|
||||
{stop, drop} ->
|
||||
{stop, {drop, State}};
|
||||
Result ->
|
||||
{Result, State}
|
||||
end.
|
||||
|
||||
-spec sm_receive_packet(stanza() | drop) -> stanza() | drop | {stop, drop}.
|
||||
sm_receive_packet(drop = Acc) ->
|
||||
Acc;
|
||||
sm_receive_packet(#message{from = From,
|
||||
to = #jid{lserver = LServer} = To,
|
||||
type = Type} =
|
||||
Msg)
|
||||
when Type /= groupchat, Type /= error ->
|
||||
do_check(From, To, LServer, Msg);
|
||||
sm_receive_packet(#presence{from = From,
|
||||
to = #jid{lserver = LServer} = To,
|
||||
type = subscribe} =
|
||||
Presence) ->
|
||||
do_check(From, To, LServer, Presence);
|
||||
sm_receive_packet(Acc) ->
|
||||
Acc.
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%%| Filtering deciding
|
||||
|
||||
do_check(From, To, LServer, Stanza) ->
|
||||
case needs_checking(From, To) of
|
||||
true ->
|
||||
case check_from(LServer, From) of
|
||||
ham ->
|
||||
case check_stanza(LServer, From, Stanza) of
|
||||
ham ->
|
||||
Stanza;
|
||||
spam ->
|
||||
reject(Stanza),
|
||||
{stop, drop}
|
||||
end;
|
||||
spam ->
|
||||
reject(Stanza),
|
||||
{stop, drop}
|
||||
end;
|
||||
false ->
|
||||
Stanza
|
||||
end.
|
||||
|
||||
check_stanza(LServer, From, #message{body = Body}) ->
|
||||
check_body(LServer, From, xmpp:get_text(Body));
|
||||
check_stanza(_, _, _) ->
|
||||
ham.
|
||||
|
||||
-spec s2s_in_handle_info(s2s_in_state(), any()) ->
|
||||
s2s_in_state() | {stop, s2s_in_state()}.
|
||||
s2s_in_handle_info(State, {_Ref, {spam_filter, _}}) ->
|
||||
?DEBUG("Dropping expired spam filter result", []),
|
||||
{stop, State};
|
||||
s2s_in_handle_info(State, _) ->
|
||||
State.
|
||||
|
||||
-spec needs_checking(jid(), jid()) -> boolean().
|
||||
needs_checking(#jid{lserver = FromHost} = From, #jid{lserver = LServer} = To) ->
|
||||
case gen_mod:is_loaded(LServer, ?MODULE_ANTISPAM) of
|
||||
true ->
|
||||
Access = gen_mod:get_module_opt(LServer, ?MODULE_ANTISPAM, access_spam),
|
||||
case acl:match_rule(LServer, Access, To) of
|
||||
allow ->
|
||||
?DEBUG("Spam not filtered for ~s", [jid:encode(To)]),
|
||||
false;
|
||||
deny ->
|
||||
?DEBUG("Spam is filtered for ~s", [jid:encode(To)]),
|
||||
not mod_roster:is_subscribed(From, To)
|
||||
andalso not
|
||||
mod_roster:is_subscribed(
|
||||
jid:make(<<>>, FromHost),
|
||||
To) % likely a gateway
|
||||
end;
|
||||
false ->
|
||||
?DEBUG("~s not loaded for ~s", [?MODULE_ANTISPAM, LServer]),
|
||||
false
|
||||
end.
|
||||
|
||||
-spec check_from(binary(), jid()) -> ham | spam.
|
||||
check_from(Host, From) ->
|
||||
Proc = get_proc_name(Host),
|
||||
LFrom =
|
||||
{_, FromDomain, _} =
|
||||
jid:remove_resource(
|
||||
jid:tolower(From)),
|
||||
try
|
||||
case gen_server:call(Proc, {is_blocked_domain, FromDomain}) of
|
||||
true ->
|
||||
?DEBUG("Spam JID found in blocked domains: ~p", [From]),
|
||||
ejabberd_hooks:run(spam_found, Host, [{jid, From}]),
|
||||
spam;
|
||||
false ->
|
||||
case gen_server:call(Proc, {check_jid, LFrom}) of
|
||||
{spam_filter, Result} ->
|
||||
Result
|
||||
end
|
||||
end
|
||||
catch
|
||||
exit:{timeout, _} ->
|
||||
?WARNING_MSG("Timeout while checking ~s against list of blocked domains or spammers",
|
||||
[jid:encode(From)]),
|
||||
ham
|
||||
end.
|
||||
|
||||
-spec check_body(binary(), jid(), binary()) -> ham | spam.
|
||||
check_body(Host, From, Body) ->
|
||||
case {extract_urls(Host, Body), extract_jids(Body)} of
|
||||
{none, none} ->
|
||||
?DEBUG("No JIDs/URLs found in message", []),
|
||||
ham;
|
||||
{URLs, JIDs} ->
|
||||
Proc = get_proc_name(Host),
|
||||
LFrom =
|
||||
jid:remove_resource(
|
||||
jid:tolower(From)),
|
||||
try gen_server:call(Proc, {check_body, URLs, JIDs, LFrom}) of
|
||||
{spam_filter, Result} ->
|
||||
Result
|
||||
catch
|
||||
exit:{timeout, _} ->
|
||||
?WARNING_MSG("Timeout while checking body", []),
|
||||
ham
|
||||
end
|
||||
end.
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%%| Auxiliary
|
||||
|
||||
-spec extract_urls(binary(), binary()) -> {urls, [url()]} | none.
|
||||
extract_urls(Host, Body) ->
|
||||
RE = <<"https?://\\S+">>,
|
||||
Options = [global, {capture, all, binary}],
|
||||
case re:run(Body, RE, Options) of
|
||||
{match, Captured} when is_list(Captured) ->
|
||||
Urls = resolve_redirects(Host, lists:flatten(Captured)),
|
||||
{urls, Urls};
|
||||
nomatch ->
|
||||
none
|
||||
end.
|
||||
|
||||
-spec resolve_redirects(binary(), [url()]) -> [url()].
|
||||
resolve_redirects(_Host, URLs) ->
|
||||
try do_resolve_redirects(URLs, []) of
|
||||
ResolvedURLs ->
|
||||
ResolvedURLs
|
||||
catch
|
||||
exit:{timeout, _} ->
|
||||
?WARNING_MSG("Timeout while resolving redirects: ~p", [URLs]),
|
||||
URLs
|
||||
end.
|
||||
|
||||
-spec do_resolve_redirects([url()], [url()]) -> [url()].
|
||||
do_resolve_redirects([], Result) ->
|
||||
Result;
|
||||
do_resolve_redirects([URL | Rest], Acc) ->
|
||||
case httpc:request(get,
|
||||
{URL, [{"user-agent", "curl/8.7.1"}]},
|
||||
[{autoredirect, false}, {timeout, ?HTTPC_TIMEOUT}],
|
||||
[])
|
||||
of
|
||||
{ok, {{_, StatusCode, _}, Headers, _Body}} when StatusCode >= 300, StatusCode < 400 ->
|
||||
Location = proplists:get_value("location", Headers),
|
||||
case Location == undefined orelse lists:member(Location, Acc) of
|
||||
true ->
|
||||
do_resolve_redirects(Rest, [URL | Acc]);
|
||||
false ->
|
||||
do_resolve_redirects([Location | Rest], [URL | Acc])
|
||||
end;
|
||||
_Res ->
|
||||
do_resolve_redirects(Rest, [URL | Acc])
|
||||
end.
|
||||
|
||||
-spec extract_jids(binary()) -> {jids, [ljid()]} | none.
|
||||
extract_jids(Body) ->
|
||||
RE = <<"\\S+@\\S+">>,
|
||||
Options = [global, {capture, all, binary}],
|
||||
case re:run(Body, RE, Options) of
|
||||
{match, Captured} when is_list(Captured) ->
|
||||
{jids, lists:filtermap(fun try_decode_jid/1, lists:flatten(Captured))};
|
||||
nomatch ->
|
||||
none
|
||||
end.
|
||||
|
||||
-spec try_decode_jid(binary()) -> {true, ljid()} | false.
|
||||
try_decode_jid(S) ->
|
||||
try jid:decode(S) of
|
||||
#jid{} = JID ->
|
||||
{true,
|
||||
jid:remove_resource(
|
||||
jid:tolower(JID))}
|
||||
catch
|
||||
_:{bad_jid, _} ->
|
||||
false
|
||||
end.
|
||||
|
||||
-spec reject(stanza()) -> ok.
|
||||
reject(#message{from = From,
|
||||
to = To,
|
||||
type = Type,
|
||||
lang = Lang} =
|
||||
Msg)
|
||||
when Type /= groupchat, Type /= error ->
|
||||
?INFO_MSG("Rejecting unsolicited message from ~s to ~s",
|
||||
[jid:encode(From), jid:encode(To)]),
|
||||
Txt = <<"Your message is unsolicited">>,
|
||||
Err = xmpp:err_policy_violation(Txt, Lang),
|
||||
ejabberd_hooks:run(spam_stanza_rejected, To#jid.lserver, [Msg]),
|
||||
ejabberd_router:route_error(Msg, Err);
|
||||
reject(#presence{from = From,
|
||||
to = To,
|
||||
lang = Lang} =
|
||||
Presence) ->
|
||||
?INFO_MSG("Rejecting unsolicited presence from ~s to ~s",
|
||||
[jid:encode(From), jid:encode(To)]),
|
||||
Txt = <<"Your traffic is unsolicited">>,
|
||||
Err = xmpp:err_policy_violation(Txt, Lang),
|
||||
ejabberd_router:route_error(Presence, Err);
|
||||
reject(_) ->
|
||||
ok.
|
||||
|
||||
-spec get_proc_name(binary()) -> atom().
|
||||
get_proc_name(Host) ->
|
||||
gen_mod:get_module_proc(Host, ?MODULE_ANTISPAM).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
%%| vim: set foldmethod=marker foldmarker=%%|,%%-:
|
||||
@@ -0,0 +1,62 @@
|
||||
%% Generated automatically
|
||||
%% DO NOT EDIT: run `make options` instead
|
||||
|
||||
-module(mod_antispam_opt).
|
||||
|
||||
-export([access_spam/1]).
|
||||
-export([cache_size/1]).
|
||||
-export([rtbl_services/1]).
|
||||
-export([spam_domains_file/1]).
|
||||
-export([spam_dump_file/1]).
|
||||
-export([spam_jids_file/1]).
|
||||
-export([spam_urls_file/1]).
|
||||
-export([whitelist_domains_file/1]).
|
||||
|
||||
-spec access_spam(gen_mod:opts() | global | binary()) -> 'none' | acl:acl().
|
||||
access_spam(Opts) when is_map(Opts) ->
|
||||
gen_mod:get_opt(access_spam, Opts);
|
||||
access_spam(Host) ->
|
||||
gen_mod:get_module_opt(Host, mod_antispam, access_spam).
|
||||
|
||||
-spec cache_size(gen_mod:opts() | global | binary()) -> 'unlimited' | pos_integer().
|
||||
cache_size(Opts) when is_map(Opts) ->
|
||||
gen_mod:get_opt(cache_size, Opts);
|
||||
cache_size(Host) ->
|
||||
gen_mod:get_module_opt(Host, mod_antispam, cache_size).
|
||||
|
||||
-spec rtbl_services(gen_mod:opts() | global | binary()) -> [binary() | [{binary(),[{'spam_source_domains_node',binary()}]}]].
|
||||
rtbl_services(Opts) when is_map(Opts) ->
|
||||
gen_mod:get_opt(rtbl_services, Opts);
|
||||
rtbl_services(Host) ->
|
||||
gen_mod:get_module_opt(Host, mod_antispam, rtbl_services).
|
||||
|
||||
-spec spam_domains_file(gen_mod:opts() | global | binary()) -> 'none' | binary().
|
||||
spam_domains_file(Opts) when is_map(Opts) ->
|
||||
gen_mod:get_opt(spam_domains_file, Opts);
|
||||
spam_domains_file(Host) ->
|
||||
gen_mod:get_module_opt(Host, mod_antispam, spam_domains_file).
|
||||
|
||||
-spec spam_dump_file(gen_mod:opts() | global | binary()) -> boolean() | binary().
|
||||
spam_dump_file(Opts) when is_map(Opts) ->
|
||||
gen_mod:get_opt(spam_dump_file, Opts);
|
||||
spam_dump_file(Host) ->
|
||||
gen_mod:get_module_opt(Host, mod_antispam, spam_dump_file).
|
||||
|
||||
-spec spam_jids_file(gen_mod:opts() | global | binary()) -> 'none' | binary().
|
||||
spam_jids_file(Opts) when is_map(Opts) ->
|
||||
gen_mod:get_opt(spam_jids_file, Opts);
|
||||
spam_jids_file(Host) ->
|
||||
gen_mod:get_module_opt(Host, mod_antispam, spam_jids_file).
|
||||
|
||||
-spec spam_urls_file(gen_mod:opts() | global | binary()) -> 'none' | binary().
|
||||
spam_urls_file(Opts) when is_map(Opts) ->
|
||||
gen_mod:get_opt(spam_urls_file, Opts);
|
||||
spam_urls_file(Host) ->
|
||||
gen_mod:get_module_opt(Host, mod_antispam, spam_urls_file).
|
||||
|
||||
-spec whitelist_domains_file(gen_mod:opts() | global | binary()) -> 'none' | binary().
|
||||
whitelist_domains_file(Opts) when is_map(Opts) ->
|
||||
gen_mod:get_opt(whitelist_domains_file, Opts);
|
||||
whitelist_domains_file(Host) ->
|
||||
gen_mod:get_module_opt(Host, mod_antispam, whitelist_domains_file).
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
%%%----------------------------------------------------------------------
|
||||
%%% File : mod_antispam_rtbl.erl
|
||||
%%% Author : Stefan Strigler <stefan@strigler.de>
|
||||
%%% Purpose : Collection of RTBL specific functionality
|
||||
%%% Created : 20 Mar 2025 by Stefan Strigler <stefan@strigler.de>
|
||||
%%%
|
||||
%%%
|
||||
%%% ejabberd, Copyright (C) 2025 ProcessOne
|
||||
%%%
|
||||
%%% This program is free software; you can redistribute it and/or
|
||||
%%% modify it under the terms of the GNU General Public License as
|
||||
%%% published by the Free Software Foundation; either version 2 of the
|
||||
%%% License, or (at your option) any later version.
|
||||
%%%
|
||||
%%% This program is distributed in the hope that it will be useful,
|
||||
%%% but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
%%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
%%% General Public License for more details.
|
||||
%%%
|
||||
%%% You should have received a copy of the GNU General Public License along
|
||||
%%% with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
%%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
%%%
|
||||
%%%----------------------------------------------------------------------
|
||||
-module(mod_antispam_rtbl).
|
||||
-author('stefan@strigler.de').
|
||||
|
||||
-include_lib("xmpp/include/xmpp.hrl").
|
||||
-include("logger.hrl").
|
||||
-include("mod_antispam.hrl").
|
||||
|
||||
-define(SERVICE_MODULE, mod_antispam).
|
||||
-define(SERVICE_JID_PREFIX, "rtbl-").
|
||||
|
||||
-export([parse_blocked_domains/1,
|
||||
parse_pubsub_event/1,
|
||||
pubsub_event_handler/1,
|
||||
request_blocked_domains/3,
|
||||
subscribe/3,
|
||||
unsubscribe/3]).
|
||||
|
||||
%% @format-begin
|
||||
|
||||
subscribe(RTBLHost, RTBLDomainsNode, From) ->
|
||||
FromJID = service_jid(From),
|
||||
SubIQ =
|
||||
#iq{type = set,
|
||||
to = jid:make(RTBLHost),
|
||||
from = FromJID,
|
||||
sub_els = [#pubsub{subscribe = #ps_subscribe{jid = FromJID, node = RTBLDomainsNode}}]},
|
||||
?DEBUG("Sending subscription request:~n~p", [xmpp:encode(SubIQ)]),
|
||||
ejabberd_router:route_iq(SubIQ, subscribe_result, self()).
|
||||
|
||||
-spec unsubscribe(binary() | none, binary(), binary()) -> ok.
|
||||
unsubscribe(none, _PSNode, _From) ->
|
||||
ok;
|
||||
unsubscribe(RTBLHost, RTBLDomainsNode, From) ->
|
||||
FromJID = jid:make(From),
|
||||
SubIQ =
|
||||
#iq{type = set,
|
||||
to = jid:make(RTBLHost),
|
||||
from = FromJID,
|
||||
sub_els =
|
||||
[#pubsub{unsubscribe = #ps_unsubscribe{jid = FromJID, node = RTBLDomainsNode}}]},
|
||||
ejabberd_router:route_iq(SubIQ, unsubscribe_result, self()).
|
||||
|
||||
-spec request_blocked_domains(binary() | none, binary(), binary()) -> ok.
|
||||
request_blocked_domains(none, _PSNode, _From) ->
|
||||
ok;
|
||||
request_blocked_domains(RTBLHost, RTBLDomainsNode, From) ->
|
||||
IQ = #iq{type = get,
|
||||
from = jid:make(From),
|
||||
to = jid:make(RTBLHost),
|
||||
sub_els = [#pubsub{items = #ps_items{node = RTBLDomainsNode}}]},
|
||||
?DEBUG("Requesting RTBL blocked domains from ~s:~n~p", [RTBLHost, xmpp:encode(IQ)]),
|
||||
ejabberd_router:route_iq(IQ, blocked_domains, self()).
|
||||
|
||||
-spec parse_blocked_domains(stanza()) -> #{binary() => any()} | undefined.
|
||||
parse_blocked_domains(#iq{to = #jid{lserver = LServer}, type = result} = IQ) ->
|
||||
?DEBUG("parsing iq-result items: ~p", [IQ]),
|
||||
[#rtbl_service{node = RTBLDomainsNode}] = mod_antispam:get_rtbl_services_option(LServer),
|
||||
case xmpp:get_subtag(IQ, #pubsub{}) of
|
||||
#pubsub{items = #ps_items{node = RTBLDomainsNode, items = Items}} ->
|
||||
?DEBUG("Got items:~n~p", [Items]),
|
||||
parse_items(Items);
|
||||
_ ->
|
||||
undefined
|
||||
end.
|
||||
|
||||
-spec parse_pubsub_event(stanza()) -> #{binary() => any()}.
|
||||
parse_pubsub_event(#message{to = #jid{lserver = LServer}} = Msg) ->
|
||||
[#rtbl_service{node = RTBLDomainsNode}] = mod_antispam:get_rtbl_services_option(LServer),
|
||||
case xmpp:get_subtag(Msg, #ps_event{}) of
|
||||
#ps_event{items =
|
||||
#ps_items{node = RTBLDomainsNode,
|
||||
items = Items,
|
||||
retract = RetractIds}} ->
|
||||
maps:merge(retract_items(RetractIds), parse_items(Items));
|
||||
Other ->
|
||||
?WARNING_MSG("Couldn't extract items: ~p", [Other]),
|
||||
#{}
|
||||
end.
|
||||
|
||||
-spec parse_items([ps_item()]) -> #{binary() => any()}.
|
||||
parse_items(Items) ->
|
||||
lists:foldl(fun(#ps_item{id = ID}, Acc) ->
|
||||
%% TODO extract meta/extra instructions
|
||||
maps:put(ID, true, Acc)
|
||||
end,
|
||||
#{},
|
||||
Items).
|
||||
|
||||
-spec retract_items([binary()]) -> #{binary() => false}.
|
||||
retract_items(Ids) ->
|
||||
lists:foldl(fun(ID, Acc) -> Acc#{ID => false} end, #{}, Ids).
|
||||
|
||||
-spec service_jid(binary()) -> jid().
|
||||
service_jid(Host) ->
|
||||
jid:make(<<>>, Host, <<?SERVICE_JID_PREFIX, (ejabberd_cluster:node_id())/binary>>).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Hook callbacks.
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
-spec pubsub_event_handler(stanza()) -> drop | stanza().
|
||||
pubsub_event_handler(#message{from = FromJid,
|
||||
to =
|
||||
#jid{lserver = LServer,
|
||||
lresource = <<?SERVICE_JID_PREFIX, _/binary>>}} =
|
||||
Msg) ->
|
||||
?DEBUG("Got RTBL message:~n~p", [Msg]),
|
||||
From = jid:encode(FromJid),
|
||||
[#rtbl_service{host = RTBLHost}] = mod_antispam:get_rtbl_services_option(LServer),
|
||||
case RTBLHost of
|
||||
From ->
|
||||
ParsedItems = parse_pubsub_event(Msg),
|
||||
Proc = gen_mod:get_module_proc(LServer, ?SERVICE_MODULE),
|
||||
gen_server:cast(Proc, {update_blocked_domains, ParsedItems}),
|
||||
%% FIXME what's the difference between `{drop, ...}` and `{stop, {drop, ...}}`?
|
||||
drop;
|
||||
_Other ->
|
||||
?INFO_MSG("Got unexpected message from ~s to rtbl resource:~n~p", [From, Msg]),
|
||||
Msg
|
||||
end;
|
||||
pubsub_event_handler(Acc) ->
|
||||
?DEBUG("unexpected something on pubsub_event_handler: ~p", [Acc]),
|
||||
Acc.
|
||||
+14
-4
@@ -29,8 +29,8 @@
|
||||
-export([start/2, stop/1, reload/3, depends/2, mod_options/1, mod_opt_type/1]).
|
||||
-export([mod_doc/0]).
|
||||
%% Hooks
|
||||
-export([c2s_inline_features/2, c2s_handle_sasl2_inline/1,
|
||||
get_tokens/3, get_mechanisms/1]).
|
||||
-export([c2s_inline_features/3, c2s_handle_sasl2_inline/1,
|
||||
get_tokens/3, get_mechanisms/1, remove_user_tokens/2]).
|
||||
|
||||
-include_lib("xmpp/include/xmpp.hrl").
|
||||
-include_lib("xmpp/include/scram.hrl").
|
||||
@@ -54,7 +54,10 @@ start(Host, Opts) ->
|
||||
Mod = gen_mod:db_mod(Opts, ?MODULE),
|
||||
Mod:init(Host, Opts),
|
||||
{ok, [{hook, c2s_inline_features, c2s_inline_features, 50},
|
||||
{hook, c2s_handle_sasl2_inline, c2s_handle_sasl2_inline, 10}]}.
|
||||
{hook, c2s_handle_sasl2_inline, c2s_handle_sasl2_inline, 10},
|
||||
{hook, set_password, remove_user_tokens, 50},
|
||||
{hook, sm_kick_user, remove_user_tokens, 50},
|
||||
{hook, remove_user, remove_user_tokens, 50}]}.
|
||||
|
||||
-spec stop(binary()) -> ok.
|
||||
stop(_Host) ->
|
||||
@@ -128,7 +131,7 @@ get_tokens(LServer, LUser, UA) ->
|
||||
{{Type, CreatedAt < ToRefresh}, Token}
|
||||
end, Mod:get_tokens(LServer, LUser, ua_hash(UA))).
|
||||
|
||||
c2s_inline_features({Sasl, Bind, Extra}, Host) ->
|
||||
c2s_inline_features({Sasl, Bind, Extra}, Host, _State) ->
|
||||
{Sasl ++ [#fast{mechs = get_mechanisms(Host)}], Bind, Extra}.
|
||||
|
||||
gen_token(#{sasl2_ua_id := UA, server := Server, user := User}) ->
|
||||
@@ -165,3 +168,10 @@ c2s_handle_sasl2_inline({#{server := Server, user := User, sasl2_ua_id := UA,
|
||||
_ ->
|
||||
Acc
|
||||
end.
|
||||
|
||||
-spec remove_user_tokens(binary(), binary()) -> ok.
|
||||
remove_user_tokens(User, Server) ->
|
||||
LUser = jid:nodeprep(User),
|
||||
LServer = jid:nameprep(Server),
|
||||
Mod = gen_mod:db_mod(LServer, ?MODULE),
|
||||
Mod:del_tokens(LServer, LUser).
|
||||
|
||||
@@ -28,12 +28,12 @@
|
||||
|
||||
%% API
|
||||
-export([init/2]).
|
||||
-export([get_tokens/3, del_token/4, set_token/6, rotate_token/3]).
|
||||
-export([get_tokens/3, del_token/4, del_tokens/2, set_token/6, rotate_token/3]).
|
||||
|
||||
-include_lib("xmpp/include/xmpp.hrl").
|
||||
-include("logger.hrl").
|
||||
|
||||
-record(mod_auth_fast, {key = {<<"">>, <<"">>, <<"">>} :: {binary(), binary(), binary()} | '$1',
|
||||
-record(mod_auth_fast, {key = {<<"">>, <<"">>, <<"">>} :: {binary(), binary(), binary() | '_'} | '$1',
|
||||
token = <<>> :: binary() | '_',
|
||||
created_at = 0 :: non_neg_integer() | '_',
|
||||
expires_at = 0 :: non_neg_integer() | '_'}).
|
||||
@@ -94,6 +94,14 @@ del_token(LServer, LUser, UA, Type) ->
|
||||
end,
|
||||
transaction(F).
|
||||
|
||||
-spec del_tokens(binary(), binary()) -> ok | {error, atom()}.
|
||||
del_tokens(LServer, LUser) ->
|
||||
F = fun() ->
|
||||
Elements = mnesia:match_object(#mod_auth_fast{key = {LServer, LUser, '_'}, _ = '_'}),
|
||||
[mnesia:delete_object(E) || E <- Elements]
|
||||
end,
|
||||
transaction(F).
|
||||
|
||||
-spec set_token(binary(), binary(), binary(), current | next, binary(), non_neg_integer()) ->
|
||||
ok | {error, atom()}.
|
||||
set_token(LServer, LUser, UA, Type, Token, Expires) ->
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
iq_handler/1, disco_features/5,
|
||||
depends/2, mod_options/1, mod_doc/0]).
|
||||
-export([c2s_copy_session/2, c2s_session_opened/1, c2s_session_resumed/1,
|
||||
c2s_inline_features/2, c2s_handle_bind2_inline/1]).
|
||||
c2s_inline_features/3, c2s_handle_bind2_inline/1]).
|
||||
%% For debugging purposes
|
||||
-export([list/2]).
|
||||
|
||||
@@ -145,7 +145,7 @@ c2s_session_resumed(State) ->
|
||||
c2s_session_opened(State) ->
|
||||
maps:remove(carboncopy, State).
|
||||
|
||||
c2s_inline_features({Sasl, Bind, Extra} = Acc, Host) ->
|
||||
c2s_inline_features({Sasl, Bind, Extra} = Acc, Host, _State) ->
|
||||
case gen_mod:is_loaded(Host, ?MODULE) of
|
||||
true ->
|
||||
{Sasl, [#bind2_feature{var = ?NS_CARBONS_2} | Bind], Extra};
|
||||
|
||||
+113
-7
@@ -31,6 +31,7 @@
|
||||
|
||||
-export([start/2, stop/1, reload/3, process/2, depends/2,
|
||||
mod_opt_type/1, mod_options/1, mod_doc/0]).
|
||||
-export([web_menu_system/2]).
|
||||
|
||||
-include_lib("xmpp/include/xmpp.hrl").
|
||||
-include("logger.hrl").
|
||||
@@ -39,7 +40,7 @@
|
||||
-include("ejabberd_web_admin.hrl").
|
||||
|
||||
start(_Host, _Opts) ->
|
||||
ok.
|
||||
{ok, [{hook, webadmin_menu_system_post, web_menu_system, 50, global}]}.
|
||||
|
||||
stop(_Host) ->
|
||||
ok.
|
||||
@@ -50,8 +51,10 @@ reload(_Host, _NewOpts, _OldOpts) ->
|
||||
depends(_Host, _Opts) ->
|
||||
[].
|
||||
|
||||
process([], #request{method = 'GET', host = Host, raw_path = RawPath}) ->
|
||||
process([], #request{method = 'GET', host = Host, q = Query, raw_path = RawPath1}) ->
|
||||
[RawPath | _] = string:split(RawPath1, "?"),
|
||||
ExtraOptions = get_auth_options(Host)
|
||||
++ get_autologin_options(Query)
|
||||
++ get_register_options(Host)
|
||||
++ get_extra_options(Host),
|
||||
Domain = mod_conversejs_opt:default_domain(Host),
|
||||
@@ -61,11 +64,12 @@ process([], #request{method = 'GET', host = Host, raw_path = RawPath}) ->
|
||||
CSS = get_file_url(Host, conversejs_css,
|
||||
<<RawPath/binary, "/converse.min.css">>,
|
||||
<<"https://cdn.conversejs.org/dist/converse.min.css">>),
|
||||
PluginsHtml = get_plugins_html(Host, RawPath),
|
||||
Init = [{<<"discover_connection_methods">>, false},
|
||||
{<<"default_domain">>, Domain},
|
||||
{<<"domain_placeholder">>, Domain},
|
||||
{<<"registration_domain">>, Domain},
|
||||
{<<"assets_path">>, RawPath},
|
||||
{<<"assets_path">>, <<RawPath/binary, "/">>},
|
||||
{<<"view_mode">>, <<"fullscreen">>}
|
||||
| ExtraOptions],
|
||||
Init2 =
|
||||
@@ -86,7 +90,8 @@ process([], #request{method = 'GET', host = Host, raw_path = RawPath}) ->
|
||||
<<"<meta charset='utf-8'>">>,
|
||||
<<"<link rel='stylesheet' type='text/css' media='screen' href='">>,
|
||||
fxml:crypt(CSS), <<"'>">>,
|
||||
<<"<script src='">>, fxml:crypt(Script), <<"' charset='utf-8'></script>">>,
|
||||
<<"<script src='">>, fxml:crypt(Script), <<"' charset='utf-8'></script>">>
|
||||
] ++ PluginsHtml ++ [
|
||||
<<"</head>">>,
|
||||
<<"<body>">>,
|
||||
<<"<script>">>,
|
||||
@@ -112,6 +117,7 @@ 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([<<"plugins">>, _]) -> true;
|
||||
is_served_file(_) -> false.
|
||||
|
||||
serve(Host, LocalPath) ->
|
||||
@@ -221,6 +227,90 @@ get_auto_file_url(Host, Filename, Default) ->
|
||||
_ -> Filename
|
||||
end.
|
||||
|
||||
get_plugins_html(Host, RawPath) ->
|
||||
Resources = get_conversejs_resources(Host),
|
||||
lists:map(fun(F) ->
|
||||
Plugin =
|
||||
case {F, Resources} of
|
||||
{<<"libsignal">>, undefined} ->
|
||||
<<"https://cdn.conversejs.org/3rdparty/libsignal-protocol.min.js">>;
|
||||
{<<"libsignal">>, Path} ->
|
||||
?WARNING_MSG("~p is configured to use local Converse files "
|
||||
"from path ~ts but the public plugin ~ts!",
|
||||
[?MODULE, Path, F]),
|
||||
<<"https://cdn.conversejs.org/3rdparty/libsignal-protocol.min.js">>;
|
||||
_ ->
|
||||
fxml:crypt(<<RawPath/binary, "/plugins/", F/binary>>)
|
||||
end,
|
||||
<<"<script src='", Plugin/binary, "' charset='utf-8'></script>">>
|
||||
end,
|
||||
gen_mod:get_module_opt(Host, ?MODULE, conversejs_plugins)).
|
||||
|
||||
%%----------------------------------------------------------------------
|
||||
%% WebAdmin link and autologin
|
||||
%%----------------------------------------------------------------------
|
||||
|
||||
%% @format-begin
|
||||
|
||||
web_menu_system(Result,
|
||||
#request{host = Host,
|
||||
auth = Auth,
|
||||
tp = Protocol}) ->
|
||||
AutoUrl = mod_host_meta:get_auto_url(any, ?MODULE),
|
||||
ConverseUrl = misc:expand_keyword(<<"@HOST@">>, AutoUrl, Host),
|
||||
AutologinQuery =
|
||||
case {Protocol, Auth} of
|
||||
{http, {Jid, _Password}} ->
|
||||
<<"/?autologinjid=", Jid/binary>>;
|
||||
{https, {Jid, Password}} ->
|
||||
AuthToken = build_token(Jid, Password),
|
||||
<<"/?autologinjid=", Jid/binary, "&autologintoken=", AuthToken/binary>>;
|
||||
_ ->
|
||||
<<"">>
|
||||
end,
|
||||
ConverseEl =
|
||||
?LI([?C(unicode:characters_to_binary("☯️")),
|
||||
?XAE(<<"a">>,
|
||||
[{<<"href">>, <<ConverseUrl/binary, AutologinQuery/binary>>},
|
||||
{<<"target">>, <<"_blank">>}],
|
||||
[?C(unicode:characters_to_binary("Converse"))])]),
|
||||
[ConverseEl | Result].
|
||||
|
||||
get_autologin_options(Query) ->
|
||||
case {proplists:get_value(<<"autologinjid">>, Query),
|
||||
proplists:get_value(<<"autologintoken">>, Query)}
|
||||
of
|
||||
{undefined, _} ->
|
||||
[];
|
||||
{Jid, Token} ->
|
||||
[{<<"auto_login">>, <<"true">>},
|
||||
{<<"jid">>, <<"admin@localhost">>},
|
||||
{<<"password">>, check_token_get_password(Jid, Token)}]
|
||||
end.
|
||||
|
||||
build_token(Jid, Password) ->
|
||||
Minutes =
|
||||
integer_to_binary(calendar:datetime_to_gregorian_seconds(
|
||||
calendar:universal_time())
|
||||
div 60),
|
||||
Cookie =
|
||||
misc:atom_to_binary(
|
||||
erlang:get_cookie()),
|
||||
str:sha(<<Jid/binary, Password/binary, Minutes/binary, Cookie/binary>>).
|
||||
|
||||
check_token_get_password(_, undefined) ->
|
||||
<<"">>;
|
||||
check_token_get_password(JidString, TokenProvided) ->
|
||||
Jid = jid:decode(JidString),
|
||||
Password = ejabberd_auth:get_password_s(Jid#jid.luser, Jid#jid.lserver),
|
||||
case build_token(JidString, Password) of
|
||||
TokenProvided ->
|
||||
Password;
|
||||
_ ->
|
||||
<<"">>
|
||||
end.
|
||||
%% @format-end
|
||||
|
||||
%%----------------------------------------------------------------------
|
||||
%%
|
||||
%%----------------------------------------------------------------------
|
||||
@@ -237,6 +327,8 @@ mod_opt_type(conversejs_script) ->
|
||||
econf:binary();
|
||||
mod_opt_type(conversejs_css) ->
|
||||
econf:binary();
|
||||
mod_opt_type(conversejs_plugins) ->
|
||||
econf:list(econf:binary());
|
||||
mod_opt_type(default_domain) ->
|
||||
econf:host().
|
||||
|
||||
@@ -247,6 +339,7 @@ mod_options(Host) ->
|
||||
{conversejs_resources, undefined},
|
||||
{conversejs_options, []},
|
||||
{conversejs_script, auto},
|
||||
{conversejs_plugins, []},
|
||||
{conversejs_css, auto}].
|
||||
|
||||
mod_doc() ->
|
||||
@@ -259,9 +352,10 @@ mod_doc() ->
|
||||
?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.")
|
||||
"by default they point to the public Converse client."), "",
|
||||
?T("This module is available since ejabberd 21.12.")
|
||||
],
|
||||
note => "added in 21.12 and improved in 22.05",
|
||||
note => "improved in 25.07",
|
||||
example =>
|
||||
[{?T("Manually setup WebSocket url, and use the public Converse client:"),
|
||||
["listen:",
|
||||
@@ -276,6 +370,7 @@ mod_doc() ->
|
||||
"modules:",
|
||||
" mod_bosh: {}",
|
||||
" mod_conversejs:",
|
||||
" conversejs_plugins: [\"libsignal\"]",
|
||||
" websocket_url: \"ws://@HOST@:5280/websocket\""]},
|
||||
{?T("Host Converse locally and let auto detection of WebSocket and Converse URLs:"),
|
||||
["listen:",
|
||||
@@ -289,7 +384,9 @@ mod_doc() ->
|
||||
"",
|
||||
"modules:",
|
||||
" mod_conversejs:",
|
||||
" conversejs_resources: \"/home/ejabberd/conversejs-9.0.0/package/dist\""]},
|
||||
" conversejs_resources: \"/home/ejabberd/conversejs-x.y.z/package/dist\"",
|
||||
" conversejs_plugins: [\"libsignal-protocol.min.js\"]",
|
||||
" # File path is: /home/ejabberd/conversejs-x.y.z/package/dist/plugins/libsignal-protocol.min.js"]},
|
||||
{?T("Configure some additional options for Converse"),
|
||||
["modules:",
|
||||
" mod_conversejs:",
|
||||
@@ -341,6 +438,15 @@ mod_doc() ->
|
||||
"See https://conversejs.org/docs/html/configuration.html[Converse configuration]. "
|
||||
"Only boolean, integer and string values are supported; "
|
||||
"lists are not supported.")}},
|
||||
{conversejs_plugins,
|
||||
#{value => ?T("[Filename]"),
|
||||
desc =>
|
||||
?T("List of additional local files to include as scripts in the homepage. "
|
||||
"Please make sure those files are available in the path specified in "
|
||||
"'conversejs_resources' option, in subdirectory 'plugins/'. "
|
||||
"If using the public Converse client, then '\"libsignal\"' "
|
||||
"gets replaced with the URL of the public library. "
|
||||
"The default value is '[]'.")}},
|
||||
{conversejs_script,
|
||||
#{value => ?T("auto | URL"),
|
||||
desc =>
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
-export([bosh_service_url/1]).
|
||||
-export([conversejs_css/1]).
|
||||
-export([conversejs_options/1]).
|
||||
-export([conversejs_plugins/1]).
|
||||
-export([conversejs_resources/1]).
|
||||
-export([conversejs_script/1]).
|
||||
-export([default_domain/1]).
|
||||
@@ -29,6 +30,12 @@ conversejs_options(Opts) when is_map(Opts) ->
|
||||
conversejs_options(Host) ->
|
||||
gen_mod:get_module_opt(Host, mod_conversejs, conversejs_options).
|
||||
|
||||
-spec conversejs_plugins(gen_mod:opts() | global | binary()) -> [binary()].
|
||||
conversejs_plugins(Opts) when is_map(Opts) ->
|
||||
gen_mod:get_opt(conversejs_plugins, Opts);
|
||||
conversejs_plugins(Host) ->
|
||||
gen_mod:get_module_opt(Host, mod_conversejs, conversejs_plugins).
|
||||
|
||||
-spec conversejs_resources(gen_mod:opts() | global | binary()) -> 'undefined' | binary().
|
||||
conversejs_resources(Opts) when is_map(Opts) ->
|
||||
gen_mod:get_opt(conversejs_resources, Opts);
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
-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]).
|
||||
-export([get_url/4, get_auto_url/2]).
|
||||
|
||||
-include("logger.hrl").
|
||||
|
||||
@@ -151,10 +151,10 @@ get_auto_url(Tls, Module) ->
|
||||
[] -> 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">>
|
||||
{true, ejabberd_http_ws} -> <<"wss">>;
|
||||
{false, _} -> <<"http">>;
|
||||
{true, _} -> <<"https">>
|
||||
end,
|
||||
<<Protocol/binary,
|
||||
"://@HOST@:",
|
||||
|
||||
+23
-7
@@ -319,8 +319,10 @@ mod_doc() ->
|
||||
desc =>
|
||||
?T("This option specifies the initial part of the PUT URLs "
|
||||
"used for file uploads. The keyword '@HOST@' is replaced "
|
||||
"with the virtual host name. NOTE: different virtual "
|
||||
"hosts cannot use the same PUT URL. "
|
||||
"with the virtual host name. "
|
||||
"And '@HOST_URL_ENCODE@' is replaced with the host name encoded for URL, "
|
||||
"useful when your virtual hosts contain non-latin characters. "
|
||||
"NOTE: different virtual hosts cannot use the same PUT URL. "
|
||||
"The default value is '\"https://@HOST@:5443/upload\"'.")}},
|
||||
{get_url,
|
||||
#{value => ?T("URL"),
|
||||
@@ -531,7 +533,8 @@ process(LocalPath, #request{method = Method, host = Host, ip = IP})
|
||||
[Method, encode_addr(IP), Host]),
|
||||
http_response(404);
|
||||
process(_LocalPath, #request{method = 'PUT', host = Host, ip = IP,
|
||||
length = Length} = Request) ->
|
||||
length = Length} = Request0) ->
|
||||
Request = Request0#request{host = redecode_url(Host)},
|
||||
{Proc, Slot} = parse_http_request(Request),
|
||||
try gen_server:call(Proc, {use_slot, Slot, Length}, ?CALL_TIMEOUT) of
|
||||
{ok, Path, FileMode, DirMode, GetPrefix, Thumbnail, CustomHeaders} ->
|
||||
@@ -571,9 +574,10 @@ process(_LocalPath, #request{method = 'PUT', host = Host, ip = IP,
|
||||
[encode_addr(IP), Host, Error]),
|
||||
http_response(500)
|
||||
end;
|
||||
process(_LocalPath, #request{method = Method, host = Host, ip = IP} = Request)
|
||||
process(_LocalPath, #request{method = Method, host = Host, ip = IP} = Request0)
|
||||
when Method == 'GET';
|
||||
Method == 'HEAD' ->
|
||||
Request = Request0#request{host = redecode_url(Host)},
|
||||
{Proc, [_UserDir, _RandDir, FileName] = Slot} = parse_http_request(Request),
|
||||
try gen_server:call(Proc, get_conf, ?CALL_TIMEOUT) of
|
||||
{ok, DocRoot, CustomHeaders} ->
|
||||
@@ -717,7 +721,7 @@ get_proc_name(ServerHost, ModuleName, PutURL) ->
|
||||
|
||||
-spec expand_home(binary()) -> binary().
|
||||
expand_home(Input) ->
|
||||
{ok, [[Home]]} = init:get_argument(home),
|
||||
Home = misc:get_home(),
|
||||
misc:expand_keyword(<<"@HOME@">>, Input, Home).
|
||||
|
||||
-spec expand_host(binary(), binary()) -> binary().
|
||||
@@ -907,8 +911,8 @@ mk_slot(Slot, #state{put_url = PutPrefix, get_url = GetPrefix}, XMLNS, Query) ->
|
||||
GetURL = str:join([GetPrefix | Slot], <<$/>>),
|
||||
mk_slot(PutURL, GetURL, XMLNS, Query);
|
||||
mk_slot(PutURL, GetURL, XMLNS, Query) ->
|
||||
PutURL1 = <<(misc:url_encode(PutURL))/binary, Query/binary>>,
|
||||
GetURL1 = misc:url_encode(GetURL),
|
||||
PutURL1 = <<(reencode_url(PutURL))/binary, Query/binary>>,
|
||||
GetURL1 = reencode_url(GetURL),
|
||||
case XMLNS of
|
||||
?NS_HTTP_UPLOAD_0 ->
|
||||
#upload_slot_0{get = GetURL1, put = PutURL1, xmlns = XMLNS};
|
||||
@@ -916,6 +920,18 @@ mk_slot(PutURL, GetURL, XMLNS, Query) ->
|
||||
#upload_slot{get = GetURL1, put = PutURL1, xmlns = XMLNS}
|
||||
end.
|
||||
|
||||
reencode_url(UrlString) ->
|
||||
{ok, _, _, Host, _, _, _} = yconf:parse_uri(UrlString),
|
||||
HostDecoded = misc:uri_decode(Host),
|
||||
HostIdna = idna:encode(HostDecoded),
|
||||
re:replace(UrlString, Host, HostIdna, [{return, binary}]).
|
||||
|
||||
redecode_url(UrlString) ->
|
||||
{ok, _, _, HostIdna, _, _, _} = yconf:parse_uri(<<"http://", UrlString/binary>>),
|
||||
HostDecoded = idna:decode(HostIdna),
|
||||
Host = misc:uri_quote(HostDecoded),
|
||||
re:replace(UrlString, HostIdna, Host, [{return, binary}]).
|
||||
|
||||
-spec make_user_string(jid(), sha1 | node) -> binary().
|
||||
make_user_string(#jid{luser = U, lserver = S}, sha1) ->
|
||||
str:sha(<<U/binary, $@, S/binary>>);
|
||||
|
||||
+15
-4
@@ -31,6 +31,7 @@
|
||||
-protocol({xep, 424, '0.4.2', '24.02', "partial", "Tombstones not implemented"}).
|
||||
-protocol({xep, 425, '0.3.0', '24.06', "complete", ""}).
|
||||
-protocol({xep, 441, '0.2.0', '15.06', "complete", ""}).
|
||||
-protocol({xep, 431, '0.2.0', '24.12', "complete", ""}).
|
||||
|
||||
-behaviour(gen_mod).
|
||||
|
||||
@@ -78,7 +79,7 @@
|
||||
-callback delete_old_messages(binary() | global,
|
||||
erlang:timestamp(),
|
||||
all | chat | groupchat) -> any().
|
||||
-callback extended_fields() -> [mam_query:property() | #xdata_field{}].
|
||||
-callback extended_fields(binary()) -> [mam_query:property() | #xdata_field{}].
|
||||
-callback store(xmlel(), binary(), {binary(), binary()}, chat | groupchat,
|
||||
jid(), binary(), recv | send, integer(), binary(),
|
||||
{true, binary()} | false) -> ok | any().
|
||||
@@ -605,8 +606,18 @@ parse_query(#mam_query{xdata = #xdata{}} = Query, Lang) ->
|
||||
#xdata_field{var = <<"FORM_TYPE">>,
|
||||
type = hidden, values = [?NS_MAM_1]},
|
||||
Query#mam_query.xdata),
|
||||
try mam_query:decode(X#xdata.fields) of
|
||||
Form -> {ok, Form}
|
||||
{Fields, WithText} = case lists:keytake(<<"{urn:xmpp:fulltext:0}fulltext">>, #xdata_field.var, X#xdata.fields) of
|
||||
false -> {X#xdata.fields, <<>>};
|
||||
{value, #xdata_field{values = [V]}, F} -> {F, V};
|
||||
{value, _, F} -> {F, <<>>}
|
||||
end,
|
||||
try mam_query:decode(Fields) of
|
||||
Form ->
|
||||
if WithText /= <<>> ->
|
||||
{ok, lists:keystore(withtext, 1, Form, {withtext, WithText})};
|
||||
true ->
|
||||
{ok, Form}
|
||||
end
|
||||
catch _:{mam_query, Why} ->
|
||||
Txt = mam_query:format_error(Why),
|
||||
{error, xmpp:err_bad_request(Txt, Lang)}
|
||||
@@ -818,7 +829,7 @@ process_iq(LServer, #iq{sub_els = [#mam_query{xmlns = NS}]} = IQ) ->
|
||||
CommonFields = [{with, undefined},
|
||||
{start, undefined},
|
||||
{'end', undefined}],
|
||||
ExtendedFields = Mod:extended_fields(),
|
||||
ExtendedFields = Mod:extended_fields(LServer),
|
||||
Fields = mam_query:encode(CommonFields ++ ExtendedFields),
|
||||
X = xmpp_util:set_xdata_field(
|
||||
#xdata_field{var = <<"FORM_TYPE">>, type = hidden, values = [NS]},
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
|
||||
%% API
|
||||
-export([init/2, remove_user/2, remove_room/3, delete_old_messages/3,
|
||||
extended_fields/0, store/10, write_prefs/4, get_prefs/2, select/6,
|
||||
extended_fields/1, store/10, write_prefs/4, get_prefs/2, select/6,
|
||||
remove_from_archive/3,
|
||||
is_empty_for_user/2, is_empty_for_room/3, delete_old_messages_batch/5,
|
||||
transform/1]).
|
||||
@@ -185,7 +185,7 @@ delete_old_messages_batch(LServer, TimeStamp, Type, Batch, LastUS) ->
|
||||
{error, Err}
|
||||
end.
|
||||
|
||||
extended_fields() ->
|
||||
extended_fields(_) ->
|
||||
[].
|
||||
|
||||
store(Pkt, _, {LUser, LServer}, Type, Peer, Nick, _Dir, TS,
|
||||
@@ -365,4 +365,6 @@ transform({archive_msg, US, ID, Timestamp, Peer, BarePeer,
|
||||
packet = Packet,
|
||||
nick = Nick,
|
||||
type = Type,
|
||||
origin_id = <<"">>}.
|
||||
origin_id = <<"">>};
|
||||
transform(Other) ->
|
||||
Other.
|
||||
|
||||
+53
-18
@@ -29,7 +29,7 @@
|
||||
|
||||
%% API
|
||||
-export([init/2, remove_user/2, remove_room/3, delete_old_messages/3,
|
||||
extended_fields/0, store/10, write_prefs/4, get_prefs/2, select/7, export/1, remove_from_archive/3,
|
||||
extended_fields/1, store/10, write_prefs/4, get_prefs/2, select/7, export/1, remove_from_archive/3,
|
||||
is_empty_for_user/2, is_empty_for_room/3, select_with_mucsub/6,
|
||||
delete_old_messages_batch/4, count_messages_to_delete/3]).
|
||||
-export([sql_schemas/0]).
|
||||
@@ -213,21 +213,47 @@ count_messages_to_delete(ServerHost, TimeStamp, Type) ->
|
||||
delete_old_messages_batch(ServerHost, TimeStamp, Type, Batch) ->
|
||||
TS = misc:now_to_usec(TimeStamp),
|
||||
Res =
|
||||
case Type of
|
||||
all ->
|
||||
ejabberd_sql:sql_query(
|
||||
ServerHost,
|
||||
?SQL("delete from archive"
|
||||
" where timestamp < %(TS)d and %(ServerHost)H limit %(Batch)d"));
|
||||
_ ->
|
||||
SType = misc:atom_to_binary(Type),
|
||||
ejabberd_sql:sql_query(
|
||||
ServerHost,
|
||||
?SQL("delete from archive"
|
||||
" where timestamp < %(TS)d"
|
||||
" and kind=%(SType)s"
|
||||
" and %(ServerHost)H limit %(Batch)d"))
|
||||
end,
|
||||
case Type of
|
||||
all ->
|
||||
ejabberd_sql:sql_query(
|
||||
ServerHost,
|
||||
fun(sqlite, _) ->
|
||||
ejabberd_sql:sql_query_t(
|
||||
?SQL("delete from archive where rowid in "
|
||||
"(select rowid from archive where timestamp < %(TS)d and %(ServerHost)H limit %(Batch)d)"));
|
||||
(mssql, _) ->
|
||||
ejabberd_sql:sql_query_t(
|
||||
?SQL("delete top(%(Batch)d)§ from archive"
|
||||
" where timestamp < %(TS)d and %(ServerHost)H"));
|
||||
(_, _) ->
|
||||
ejabberd_sql:sql_query_t(
|
||||
?SQL("delete from archive"
|
||||
" where timestamp < %(TS)d and %(ServerHost)H limit %(Batch)d"))
|
||||
end);
|
||||
_ ->
|
||||
SType = misc:atom_to_binary(Type),
|
||||
ejabberd_sql:sql_query(
|
||||
ServerHost,
|
||||
fun(sqlite,_)->
|
||||
ejabberd_sql:sql_query_t(
|
||||
?SQL("delete from archive where rowid in ("
|
||||
" select rowid from archive where timestamp < %(TS)d"
|
||||
" and kind=%(SType)s"
|
||||
" and %(ServerHost)H limit %(Batch)d)"));
|
||||
(mssql, _)->
|
||||
ejabberd_sql:sql_query_t(
|
||||
?SQL("delete top(%(Batch)d) from archive"
|
||||
" where timestamp < %(TS)d"
|
||||
" and kind=%(SType)s"
|
||||
" and %(ServerHost)H"));
|
||||
(_,_)->
|
||||
ejabberd_sql:sql_query_t(
|
||||
?SQL("delete from archive"
|
||||
" where timestamp < %(TS)d"
|
||||
" and kind=%(SType)s"
|
||||
" and %(ServerHost)H limit %(Batch)d"))
|
||||
end)
|
||||
end,
|
||||
case Res of
|
||||
{updated, Count} ->
|
||||
{ok, Count};
|
||||
@@ -254,8 +280,17 @@ delete_old_messages(ServerHost, TimeStamp, Type) ->
|
||||
end,
|
||||
ok.
|
||||
|
||||
extended_fields() ->
|
||||
[{withtext, <<"">>}].
|
||||
extended_fields(LServer) ->
|
||||
case ejabberd_option:sql_type(LServer) of
|
||||
mysql ->
|
||||
[{withtext, <<"">>},
|
||||
#xdata_field{var = <<"{urn:xmpp:fulltext:0}fulltext">>,
|
||||
type = 'text-single',
|
||||
label = <<"Search the text">>,
|
||||
values = []}];
|
||||
_ ->
|
||||
[]
|
||||
end.
|
||||
|
||||
store(Pkt, LServer, {LUser, LHost}, Type, Peer, Nick, _Dir, TS,
|
||||
OriginID, Retract) ->
|
||||
|
||||
+83
-23
@@ -41,6 +41,7 @@
|
||||
handle_info/2, terminate/2, code_change/3,
|
||||
depends/2, mod_opt_type/1, mod_options/1, mod_doc/0]).
|
||||
-export([parse_auth/1, encode_canonical_json/1,
|
||||
is_canonical_json/1,
|
||||
get_id_domain_exn/1,
|
||||
base64_decode/1, base64_encode/1,
|
||||
prune_event/2, get_event_id/2, content_hash/1,
|
||||
@@ -155,8 +156,8 @@ process([<<"federation">>, <<"v2">>, <<"invite">>, RoomID, EventID],
|
||||
%% TODO: check type and userid
|
||||
Host = ejabberd_config:get_myname(),
|
||||
PrunedEvent = prune_event(Event, RoomVersion),
|
||||
?DEBUG("invite ~p~n", [{RoomID, EventID, Event, RoomVer, catch mod_matrix_gw_s2s:check_signature(Host, PrunedEvent), get_pruned_event_id(PrunedEvent)}]),
|
||||
case mod_matrix_gw_s2s:check_signature(Host, PrunedEvent) of
|
||||
%?DEBUG("invite ~p~n", [{RoomID, EventID, Event, RoomVer, catch mod_matrix_gw_s2s:check_signature(Host, PrunedEvent, RoomVersion), get_pruned_event_id(PrunedEvent)}]),
|
||||
case mod_matrix_gw_s2s:check_signature(Host, PrunedEvent, RoomVersion) of
|
||||
true ->
|
||||
case get_pruned_event_id(PrunedEvent) of
|
||||
EventID ->
|
||||
@@ -610,28 +611,40 @@ parse_auth4(<<>>, Key, Val, Ts) ->
|
||||
|
||||
prune_event(#{<<"type">> := Type, <<"content">> := Content} = Event,
|
||||
RoomVersion) ->
|
||||
Event2 =
|
||||
Keys =
|
||||
case RoomVersion#room_version.updated_redaction_rules of
|
||||
false ->
|
||||
maps:with(
|
||||
[<<"event_id">>, <<"type">>, <<"room_id">>, <<"sender">>,
|
||||
<<"state_key">>, <<"content">>, <<"hashes">>,
|
||||
<<"signatures">>, <<"depth">>, <<"prev_events">>,
|
||||
<<"prev_state">>, <<"auth_events">>, <<"origin">>,
|
||||
<<"origin_server_ts">>, <<"membership">>], Event);
|
||||
[<<"event_id">>, <<"type">>, <<"room_id">>, <<"sender">>,
|
||||
<<"state_key">>, <<"content">>, <<"hashes">>,
|
||||
<<"signatures">>, <<"depth">>, <<"prev_events">>,
|
||||
<<"prev_state">>, <<"auth_events">>, <<"origin">>,
|
||||
<<"origin_server_ts">>, <<"membership">>];
|
||||
true ->
|
||||
maps:with(
|
||||
[<<"event_id">>, <<"type">>, <<"room_id">>, <<"sender">>,
|
||||
<<"state_key">>, <<"content">>, <<"hashes">>,
|
||||
<<"signatures">>, <<"depth">>, <<"prev_events">>,
|
||||
<<"auth_events">>, <<"origin_server_ts">>], Event)
|
||||
[<<"event_id">>, <<"type">>, <<"room_id">>, <<"sender">>,
|
||||
<<"state_key">>, <<"content">>, <<"hashes">>,
|
||||
<<"signatures">>, <<"depth">>, <<"prev_events">>,
|
||||
<<"auth_events">>, <<"origin_server_ts">>]
|
||||
end,
|
||||
Keys2 =
|
||||
case {RoomVersion#room_version.hydra, Type} of
|
||||
{true, <<"m.room.create">>} ->
|
||||
lists:delete(<<"room_id">>, Keys);
|
||||
_ ->
|
||||
Keys
|
||||
end,
|
||||
Event2 = maps:with(Keys2, Event),
|
||||
Content2 =
|
||||
case Type of
|
||||
<<"m.room.member">> ->
|
||||
C3 = maps:with([<<"membership">>,
|
||||
<<"join_authorised_via_users_server">>],
|
||||
Content),
|
||||
C3 =
|
||||
case RoomVersion#room_version.restricted_join_rule_fix of
|
||||
true ->
|
||||
maps:with([<<"membership">>,
|
||||
<<"join_authorised_via_users_server">>],
|
||||
Content);
|
||||
false ->
|
||||
maps:with([<<"membership">>], Content)
|
||||
end,
|
||||
case RoomVersion#room_version.updated_redaction_rules of
|
||||
false ->
|
||||
C3;
|
||||
@@ -653,7 +666,12 @@ prune_event(#{<<"type">> := Type, <<"content">> := Content} = Event,
|
||||
Content
|
||||
end;
|
||||
<<"m.room.join_rules">> ->
|
||||
maps:with([<<"join_rule">>, <<"allow">>], Content);
|
||||
case RoomVersion#room_version.restricted_join_rule of
|
||||
false ->
|
||||
maps:with([<<"join_rule">>], Content);
|
||||
true ->
|
||||
maps:with([<<"join_rule">>, <<"allow">>], Content)
|
||||
end;
|
||||
<<"m.room.power_levels">> ->
|
||||
case RoomVersion#room_version.updated_redaction_rules of
|
||||
false ->
|
||||
@@ -677,6 +695,8 @@ prune_event(#{<<"type">> := Type, <<"content">> := Content} = Event,
|
||||
true ->
|
||||
maps:with([<<"redacts">>], Content)
|
||||
end;
|
||||
<<"m.room.aliases">> when RoomVersion#room_version.special_case_aliases_auth ->
|
||||
maps:with([<<"aliases">>], Content);
|
||||
_ -> #{}
|
||||
end,
|
||||
Event2#{<<"content">> := Content2}.
|
||||
@@ -704,7 +724,7 @@ get_pruned_event_id(PrunedEvent) ->
|
||||
|
||||
encode_canonical_json(JSON) ->
|
||||
JSON2 = sort_json(JSON),
|
||||
misc:json_encode_with_kv_lists(JSON2).
|
||||
misc:json_encode(JSON2).
|
||||
|
||||
sort_json(#{} = Map) ->
|
||||
Map2 = maps:map(fun(_K, V) ->
|
||||
@@ -716,6 +736,29 @@ sort_json(List) when is_list(List) ->
|
||||
sort_json(JSON) ->
|
||||
JSON.
|
||||
|
||||
is_canonical_json(N) when is_integer(N),
|
||||
-16#1FFFFFFFFFFFFF =< N,
|
||||
N =< 16#1FFFFFFFFFFFFF ->
|
||||
true;
|
||||
is_canonical_json(B) when is_binary(B) ->
|
||||
true;
|
||||
is_canonical_json(B) when is_boolean(B) ->
|
||||
true;
|
||||
is_canonical_json(null) ->
|
||||
true;
|
||||
is_canonical_json(Map) when is_map(Map) ->
|
||||
maps:fold(
|
||||
fun(_K, V, true) ->
|
||||
is_canonical_json(V);
|
||||
(_K, _V, false) ->
|
||||
false
|
||||
end, true, Map);
|
||||
is_canonical_json(List) when is_list(List) ->
|
||||
lists:all(fun is_canonical_json/1, List);
|
||||
is_canonical_json(_) ->
|
||||
false.
|
||||
|
||||
|
||||
base64_decode(B) ->
|
||||
Fixed =
|
||||
case size(B) rem 4 of
|
||||
@@ -941,7 +984,11 @@ mod_opt_type(key) ->
|
||||
crypto:generate_key(eddsa, ed25519, Key2)
|
||||
end;
|
||||
mod_opt_type(matrix_id_as_jid) ->
|
||||
econf:bool().
|
||||
econf:bool();
|
||||
mod_opt_type(notary_servers) ->
|
||||
econf:list(econf:host());
|
||||
mod_opt_type(leave_timeout) ->
|
||||
econf:non_neg_int().
|
||||
|
||||
-spec mod_options(binary()) -> [{key, {binary(), binary()}} |
|
||||
{atom(), any()}].
|
||||
@@ -951,14 +998,19 @@ mod_options(Host) ->
|
||||
{host, <<"matrix.", Host/binary>>},
|
||||
{key_name, <<"">>},
|
||||
{key, {<<"">>, <<"">>}},
|
||||
{matrix_id_as_jid, false}].
|
||||
{matrix_id_as_jid, false},
|
||||
{notary_servers, []},
|
||||
{leave_timeout, 0}].
|
||||
|
||||
mod_doc() ->
|
||||
#{desc =>
|
||||
[?T("https://matrix.org/[Matrix] gateway. "),
|
||||
?T("Supports room versions 9, 10 and 11 since ejabberd 25.03; "
|
||||
"room versions 4 and higher since ejabberd 25.07; "
|
||||
"room version 12 (hydra rooms) since ejabberd 25.08. "),
|
||||
?T("Erlang/OTP 25 or higher is required to use this module."),
|
||||
?T("This module is available since ejabberd 24.02.")],
|
||||
note => "improved in 25.03",
|
||||
note => "improved in 25.08",
|
||||
example =>
|
||||
["listen:",
|
||||
" -",
|
||||
@@ -1007,7 +1059,15 @@ mod_doc() ->
|
||||
"Matrix user '@user:matrixdomain.tld', the client must send a message "
|
||||
"to the JID 'user%matrixdomain.tld@matrix.myxmppdomain.tld', where "
|
||||
"'matrix.myxmppdomain.tld' is the JID of the gateway service as set by the "
|
||||
"'host' option. The default is 'false'.")}}
|
||||
"'host' option. The default is 'false'.")}},
|
||||
{notary_servers,
|
||||
#{value => "[Server, ...]",
|
||||
desc =>
|
||||
?T("A list of notary servers.")}},
|
||||
{leave_timeout,
|
||||
#{value => "integer()",
|
||||
desc =>
|
||||
?T("Delay in seconds between a user leaving a MUC room and sending 'leave' Matrix event.")}}
|
||||
]
|
||||
}.
|
||||
-endif.
|
||||
|
||||
@@ -6,8 +6,10 @@
|
||||
-export([host/1]).
|
||||
-export([key/1]).
|
||||
-export([key_name/1]).
|
||||
-export([leave_timeout/1]).
|
||||
-export([matrix_domain/1]).
|
||||
-export([matrix_id_as_jid/1]).
|
||||
-export([notary_servers/1]).
|
||||
|
||||
-spec host(gen_mod:opts() | global | binary()) -> binary().
|
||||
host(Opts) when is_map(Opts) ->
|
||||
@@ -27,6 +29,12 @@ key_name(Opts) when is_map(Opts) ->
|
||||
key_name(Host) ->
|
||||
gen_mod:get_module_opt(Host, mod_matrix_gw, key_name).
|
||||
|
||||
-spec leave_timeout(gen_mod:opts() | global | binary()) -> non_neg_integer().
|
||||
leave_timeout(Opts) when is_map(Opts) ->
|
||||
gen_mod:get_opt(leave_timeout, Opts);
|
||||
leave_timeout(Host) ->
|
||||
gen_mod:get_module_opt(Host, mod_matrix_gw, leave_timeout).
|
||||
|
||||
-spec matrix_domain(gen_mod:opts() | global | binary()) -> binary().
|
||||
matrix_domain(Opts) when is_map(Opts) ->
|
||||
gen_mod:get_opt(matrix_domain, Opts);
|
||||
@@ -39,3 +47,9 @@ matrix_id_as_jid(Opts) when is_map(Opts) ->
|
||||
matrix_id_as_jid(Host) ->
|
||||
gen_mod:get_module_opt(Host, mod_matrix_gw, matrix_id_as_jid).
|
||||
|
||||
-spec notary_servers(gen_mod:opts() | global | binary()) -> [binary()].
|
||||
notary_servers(Opts) when is_map(Opts) ->
|
||||
gen_mod:get_opt(notary_servers, Opts);
|
||||
notary_servers(Host) ->
|
||||
gen_mod:get_module_opt(Host, mod_matrix_gw, notary_servers).
|
||||
|
||||
|
||||
+828
-360
File diff suppressed because it is too large
Load Diff
+184
-158
@@ -28,7 +28,7 @@
|
||||
|
||||
%% API
|
||||
-export([start_link/2, supervisor/1, create_db/0,
|
||||
get_connection/2, check_auth/5, check_signature/2,
|
||||
get_connection/2, check_auth/5, check_signature/3,
|
||||
get_matrix_host_port/2]).
|
||||
|
||||
%% gen_statem callbacks
|
||||
@@ -38,17 +38,29 @@
|
||||
-include("logger.hrl").
|
||||
-include("ejabberd_http.hrl").
|
||||
-include_lib("kernel/include/inet.hrl").
|
||||
-include("mod_matrix_gw.hrl").
|
||||
|
||||
-record(matrix_s2s,
|
||||
{to :: binary(),
|
||||
pid :: pid()}).
|
||||
|
||||
-record(pending,
|
||||
{request_id :: any(),
|
||||
servers :: [binary()],
|
||||
key_queue = []}).
|
||||
|
||||
-record(wait,
|
||||
{timer_ref :: reference(),
|
||||
last :: integer()}).
|
||||
|
||||
-record(data,
|
||||
{host :: binary(),
|
||||
matrix_server :: binary(),
|
||||
matrix_host_port :: {binary(), integer()} | undefined,
|
||||
keys = #{},
|
||||
key_queue = #{}}).
|
||||
state :: #pending{} | #wait{}}).
|
||||
|
||||
-define(KEYS_REQUEST_TIMEOUT, 600000).
|
||||
|
||||
%%%===================================================================
|
||||
%%% API
|
||||
@@ -169,23 +181,29 @@ check_auth(Host, MatrixServer, AuthParams, Content, Request) ->
|
||||
false
|
||||
end.
|
||||
|
||||
check_signature(Host, JSON) ->
|
||||
check_signature(Host, JSON, RoomVersion) ->
|
||||
case JSON of
|
||||
#{<<"sender">> := Sender,
|
||||
<<"signatures">> := Sigs} ->
|
||||
<<"signatures">> := Sigs,
|
||||
<<"origin_server_ts">> := OriginServerTS} ->
|
||||
MatrixServer = mod_matrix_gw:get_id_domain_exn(Sender),
|
||||
case Sigs of
|
||||
#{MatrixServer := #{} = KeySig} ->
|
||||
case maps:next(maps:iterator(KeySig)) of
|
||||
{KeyID, _Sig, _} ->
|
||||
case catch get_key(Host, MatrixServer, KeyID) of
|
||||
{ok, VerifyKey, _ValidUntil} ->
|
||||
%% TODO: check ValidUntil
|
||||
case check_signature(JSON, MatrixServer, KeyID, VerifyKey) of
|
||||
{ok, VerifyKey, ValidUntil} ->
|
||||
if
|
||||
not RoomVersion#room_version.enforce_key_validity or
|
||||
(OriginServerTS =< ValidUntil) ->
|
||||
case check_signature(JSON, MatrixServer, KeyID, VerifyKey) of
|
||||
true ->
|
||||
true;
|
||||
false ->
|
||||
?WARNING_MSG("Failed authentication: ~p", [JSON]),
|
||||
false
|
||||
end;
|
||||
true ->
|
||||
true;
|
||||
false ->
|
||||
?WARNING_MSG("Failed authentication: ~p", [JSON]),
|
||||
false
|
||||
end;
|
||||
_ ->
|
||||
@@ -220,8 +238,11 @@ init([Host, MatrixServer]) ->
|
||||
#matrix_s2s{to = MatrixServer,
|
||||
pid = self()}),
|
||||
{ok, state_name,
|
||||
#data{host = Host,
|
||||
matrix_server = MatrixServer}}.
|
||||
request_keys(
|
||||
MatrixServer,
|
||||
#data{host = Host,
|
||||
matrix_server = MatrixServer,
|
||||
state = #wait{timer_ref = make_ref(), last = 0}})}.
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% @private
|
||||
@@ -239,7 +260,7 @@ init([Host, MatrixServer]) ->
|
||||
handle_event({call, From}, get_matrix_host_port, _State, Data) ->
|
||||
case Data#data.matrix_host_port of
|
||||
undefined ->
|
||||
Result = do_get_matrix_host_port(Data),
|
||||
Result = do_get_matrix_host_port(Data#data.matrix_server),
|
||||
Data2 = Data#data{matrix_host_port = Result},
|
||||
{keep_state, Data2, [{reply, From, Result}]};
|
||||
Result ->
|
||||
@@ -247,104 +268,59 @@ handle_event({call, From}, get_matrix_host_port, _State, Data) ->
|
||||
end;
|
||||
handle_event({call, From}, {get_key, KeyID}, State, Data) ->
|
||||
case maps:find(KeyID, Data#data.keys) of
|
||||
{ok, {ok, _, _} = Result} ->
|
||||
{keep_state, Data, [{reply, From, Result}]};
|
||||
{ok, error = Result} ->
|
||||
{keep_state, Data, [{reply, From, Result}]};
|
||||
{ok, pending} ->
|
||||
KeyQueue = maps:update_with(
|
||||
KeyID,
|
||||
fun(Xs) ->
|
||||
[From | Xs]
|
||||
end,
|
||||
[From],
|
||||
Data#data.key_queue),
|
||||
{next_state, State,
|
||||
Data#data{key_queue = KeyQueue}, []};
|
||||
{ok, {Key, ValidUntil}} ->
|
||||
{keep_state, Data, [{reply, From, {ok, Key, ValidUntil}}]};
|
||||
error ->
|
||||
{MHost, MPort} = do_get_matrix_host_port(Data),
|
||||
URL = <<"https://", MHost/binary,
|
||||
":", (integer_to_binary(MPort))/binary,
|
||||
"/_matrix/key/v2/server/", KeyID/binary>>,
|
||||
Self = self(),
|
||||
httpc:request(get, {URL, []},
|
||||
[{timeout, 5000}],
|
||||
[{sync, false},
|
||||
{receiver,
|
||||
fun({_RequestId, Result}) ->
|
||||
gen_statem:cast(
|
||||
Self, {key_reply, KeyID, Result})
|
||||
end}]),
|
||||
Keys = (Data#data.keys)#{KeyID => pending},
|
||||
KeyQueue = maps:update_with(
|
||||
KeyID,
|
||||
fun(Xs) ->
|
||||
[From | Xs]
|
||||
end,
|
||||
[From],
|
||||
Data#data.key_queue),
|
||||
{next_state, State,
|
||||
Data#data{keys = Keys,
|
||||
key_queue = KeyQueue},
|
||||
[]}
|
||||
case Data#data.state of
|
||||
#pending{key_queue = KeyQueue} = St ->
|
||||
KeyQueue2 = [{From, KeyID} | KeyQueue],
|
||||
{next_state, State,
|
||||
Data#data{state = St#pending{key_queue = KeyQueue2}}, []};
|
||||
#wait{timer_ref = TimerRef, last = Last} ->
|
||||
TS = erlang:system_time(millisecond),
|
||||
if
|
||||
Last + ?KEYS_REQUEST_TIMEOUT =< TS ->
|
||||
Data2 = request_keys(Data#data.matrix_server, Data),
|
||||
#pending{key_queue = KeyQueue} = St = Data2#data.state,
|
||||
KeyQueue2 = [{From, KeyID} | KeyQueue],
|
||||
{next_state, State,
|
||||
Data2#data{state = St#pending{key_queue = KeyQueue2}}, []};
|
||||
true ->
|
||||
Timeout =
|
||||
case erlang:read_timer(TimerRef) of
|
||||
false ->
|
||||
Last + ?KEYS_REQUEST_TIMEOUT - TS;
|
||||
Left ->
|
||||
erlang:cancel_timer(TimerRef),
|
||||
min(Left,
|
||||
Last + ?KEYS_REQUEST_TIMEOUT - TS)
|
||||
end,
|
||||
TRef = erlang:start_timer(Timeout, self(), []),
|
||||
{next_state, State,
|
||||
Data#data{state = #wait{timer_ref = TRef,
|
||||
last = Last}},
|
||||
[{reply, From, error}]}
|
||||
end
|
||||
end
|
||||
end;
|
||||
handle_event(cast, {query, AuthParams, _Query, _JSON, _Request} = Msg,
|
||||
State, Data) ->
|
||||
#{<<"key">> := KeyID} = AuthParams,
|
||||
case maps:find(KeyID, Data#data.keys) of
|
||||
{ok, {ok, VerifyKey, _ValidUntil}} ->
|
||||
Data2 = process_unverified_query(
|
||||
KeyID, VerifyKey, Msg, Data),
|
||||
{next_state, State, Data2, []};
|
||||
{ok, error} ->
|
||||
%TODO
|
||||
{next_state, State, Data, []};
|
||||
{ok, pending} ->
|
||||
KeyQueue = maps:update_with(
|
||||
KeyID,
|
||||
fun(Xs) ->
|
||||
[Msg | Xs]
|
||||
end,
|
||||
[Msg],
|
||||
Data#data.key_queue),
|
||||
{next_state, State,
|
||||
Data#data{key_queue = KeyQueue}, []};
|
||||
error ->
|
||||
{MHost, MPort} = do_get_matrix_host_port(Data),
|
||||
URL = <<"https://", MHost/binary,
|
||||
":", (integer_to_binary(MPort))/binary,
|
||||
"/_matrix/key/v2/server/", KeyID/binary>>,
|
||||
Self = self(),
|
||||
httpc:request(get, {URL, []},
|
||||
[{timeout, 5000}],
|
||||
[{sync, false},
|
||||
{receiver,
|
||||
fun({_RequestId, Result}) ->
|
||||
gen_statem:cast(
|
||||
Self, {key_reply, KeyID, Result})
|
||||
end}]),
|
||||
Keys = (Data#data.keys)#{KeyID => pending},
|
||||
KeyQueue = maps:update_with(
|
||||
KeyID,
|
||||
fun(Xs) ->
|
||||
[Msg | Xs]
|
||||
end,
|
||||
[Msg],
|
||||
Data#data.key_queue),
|
||||
{next_state, State,
|
||||
Data#data{keys = Keys,
|
||||
key_queue = KeyQueue},
|
||||
[]}
|
||||
end;
|
||||
handle_event(cast, {key_reply, KeyID, HTTPResult}, State, Data) ->
|
||||
KeyVal =
|
||||
handle_event(cast, {key_reply, RequestID, HTTPResult}, State,
|
||||
#data{state = #pending{request_id = RequestID,
|
||||
servers = Servers,
|
||||
key_queue = KeyQueue} = St} = Data) ->
|
||||
TS = erlang:system_time(millisecond),
|
||||
Res =
|
||||
case HTTPResult of
|
||||
{{_, 200, _}, _, SJSON} ->
|
||||
try
|
||||
JSON = misc:json_decode(SJSON),
|
||||
?DEBUG("key ~p~n", [JSON]),
|
||||
JSON1 = misc:json_decode(SJSON),
|
||||
JSON =
|
||||
case JSON1 of
|
||||
#{<<"server_keys">> := [J]} -> J;
|
||||
_ -> JSON1
|
||||
end,
|
||||
?DEBUG("keys ~p~n", [JSON]),
|
||||
#{<<"verify_keys">> := VerifyKeys} = JSON,
|
||||
#{KeyID := KeyData} = VerifyKeys,
|
||||
{KeyID, KeyData, _} = maps:next(maps:iterator(VerifyKeys)),
|
||||
#{<<"key">> := SKey} = KeyData,
|
||||
VerifyKey = mod_matrix_gw:base64_decode(SKey),
|
||||
?DEBUG("key ~p~n", [VerifyKey]),
|
||||
@@ -359,22 +335,77 @@ handle_event(cast, {key_reply, KeyID, HTTPResult}, State, Data) ->
|
||||
ValidUntil2 =
|
||||
min(ValidUntil,
|
||||
erlang:system_time(millisecond) + timer:hours(24 * 7)),
|
||||
{ok, VerifyKey, ValidUntil2}
|
||||
OldKeysJSON =
|
||||
case JSON of
|
||||
#{<<"old_verify_keys">> := OldKeysJ}
|
||||
when is_map(OldKeysJ) ->
|
||||
OldKeysJ;
|
||||
_ ->
|
||||
#{}
|
||||
end,
|
||||
OldKeys =
|
||||
maps:filtermap(
|
||||
fun(_KID,
|
||||
#{<<"key">> := SK,
|
||||
<<"expired_ts">> := Exp})
|
||||
when is_integer(Exp),
|
||||
is_binary(SK) ->
|
||||
{true, {mod_matrix_gw:base64_decode(SK),
|
||||
Exp}};
|
||||
(_, _) -> false
|
||||
end, OldKeysJSON),
|
||||
NewKeys =
|
||||
maps:filtermap(
|
||||
fun(_KID,
|
||||
#{<<"key">> := SK})
|
||||
when is_binary(SK) ->
|
||||
{true, {mod_matrix_gw:base64_decode(SK),
|
||||
ValidUntil2}};
|
||||
(_, _) -> false
|
||||
end, VerifyKeys),
|
||||
{ok, maps:merge(OldKeys, NewKeys), ValidUntil2}
|
||||
catch
|
||||
_:_ ->
|
||||
error
|
||||
{ok, Data#data.keys, TS + ?KEYS_REQUEST_TIMEOUT}
|
||||
end;
|
||||
_ ->
|
||||
error
|
||||
case Servers of
|
||||
[] ->
|
||||
{ok, Data#data.keys, TS + ?KEYS_REQUEST_TIMEOUT};
|
||||
[S | Servers1] ->
|
||||
{error,
|
||||
request_keys(
|
||||
S,
|
||||
Data#data{state = St#pending{servers = Servers1}})}
|
||||
end
|
||||
end,
|
||||
Keys = (Data#data.keys)#{KeyID => KeyVal},
|
||||
Froms = maps:get(KeyID, Data#data.key_queue, []),
|
||||
KeyQueue = maps:remove(KeyID, Data#data.key_queue),
|
||||
Data2 = Data#data{keys = Keys,
|
||||
key_queue = KeyQueue},
|
||||
Replies = lists:map(fun(From) -> {reply, From, KeyVal} end, Froms),
|
||||
?DEBUG("KEYS ~p~n", [{Keys, Data2}]),
|
||||
{next_state, State, Data2, Replies};
|
||||
case Res of
|
||||
{ok, Keys, ValidTS} ->
|
||||
Replies =
|
||||
lists:map(
|
||||
fun({From, KeyID}) ->
|
||||
case maps:find(KeyID, Keys) of
|
||||
{ok, {Key, KeyValidUntil}} ->
|
||||
{reply, From, {ok, Key, KeyValidUntil}};
|
||||
error ->
|
||||
{reply, From, error}
|
||||
end
|
||||
end,
|
||||
KeyQueue),
|
||||
TimerRef = erlang:start_timer(max(ValidTS - TS, ?KEYS_REQUEST_TIMEOUT),
|
||||
self(), []),
|
||||
Data2 = Data#data{keys = Keys,
|
||||
state = #wait{timer_ref = TimerRef,
|
||||
last = TS}},
|
||||
?DEBUG("KEYS ~p~n", [{Keys, Data2}]),
|
||||
{next_state, State, Data2, Replies};
|
||||
{error, Data2} ->
|
||||
{next_state, State, Data2, []}
|
||||
end;
|
||||
handle_event(info, {timeout, TimerRef, []}, State,
|
||||
#data{state = #wait{timer_ref = TimerRef}} = Data) ->
|
||||
Data2 = request_keys(Data#data.matrix_server, Data),
|
||||
{next_state, State, Data2, []};
|
||||
handle_event(cast, Msg, State, Data) ->
|
||||
?WARNING_MSG("Unexpected cast: ~p", [Msg]),
|
||||
{next_state, State, Data, []};
|
||||
@@ -420,8 +451,7 @@ callback_mode() ->
|
||||
%%% Internal functions
|
||||
%%%===================================================================
|
||||
|
||||
do_get_matrix_host_port(Data) ->
|
||||
MatrixServer = Data#data.matrix_server,
|
||||
do_get_matrix_host_port(MatrixServer) ->
|
||||
case binary:split(MatrixServer, <<":">>) of
|
||||
[Addr] ->
|
||||
case inet:parse_address(binary_to_list(Addr)) of
|
||||
@@ -523,47 +553,43 @@ check_signature(JSON, SignatureName, KeyID, VerifyKey) ->
|
||||
false
|
||||
end.
|
||||
|
||||
%process_unverified_queries(KeyID, Data) ->
|
||||
% case maps:find(KeyID, Data#data.keys) of
|
||||
% {ok, {ok, VerifyKey, _ValidUntil}} ->
|
||||
% Queue = maps:get(KeyID, Data#data.key_queue, []),
|
||||
% KeyQueue = maps:remove(KeyID, Data#data.key_queue),
|
||||
% Data2 = Data#data{key_queue = KeyQueue},
|
||||
% lists:foldl(
|
||||
% fun(Query, DataAcc) ->
|
||||
% process_unverified_query(KeyID, VerifyKey, Query, DataAcc)
|
||||
% end, Data2, Queue);
|
||||
% _ ->
|
||||
% %% TODO
|
||||
% Data
|
||||
% end.
|
||||
|
||||
process_unverified_query(
|
||||
KeyID, VerifyKey, {query, AuthParams, _Query, Content, Request} = _Msg, Data) ->
|
||||
Destination = mod_matrix_gw_opt:matrix_domain(Data#data.host),
|
||||
#{<<"sig">> := Sig} = AuthParams,
|
||||
JSON = #{<<"method">> => atom_to_binary(Request#request.method, latin1),
|
||||
<<"uri">> => Request#request.raw_path,
|
||||
<<"origin">> => Data#data.matrix_server,
|
||||
<<"destination">> => Destination,
|
||||
<<"signatures">> => #{
|
||||
Data#data.matrix_server => #{KeyID => Sig}
|
||||
}
|
||||
},
|
||||
JSON2 =
|
||||
case Content of
|
||||
none -> JSON;
|
||||
_ ->
|
||||
JSON#{<<"content">> => Content}
|
||||
request_keys(Via, Data) ->
|
||||
{MHost, MPort} = do_get_matrix_host_port(Via),
|
||||
URL =
|
||||
case Data#data.matrix_server of
|
||||
Via ->
|
||||
<<"https://", MHost/binary,
|
||||
":", (integer_to_binary(MPort))/binary,
|
||||
"/_matrix/key/v2/server">>;
|
||||
MatrixServer ->
|
||||
<<"https://", MHost/binary,
|
||||
":", (integer_to_binary(MPort))/binary,
|
||||
"/_matrix/key/v2/query/", MatrixServer/binary>>
|
||||
end,
|
||||
case check_signature(JSON2, Data#data.matrix_server, KeyID, VerifyKey) of
|
||||
true ->
|
||||
todo_remove_me;
|
||||
%process_query(Msg, Data);
|
||||
false ->
|
||||
?WARNING_MSG("Failed authentication: ~p", [JSON]),
|
||||
%% TODO
|
||||
Data
|
||||
Self = self(),
|
||||
{ok, RequestID} =
|
||||
httpc:request(get, {URL, []},
|
||||
[{timeout, 5000}],
|
||||
[{sync, false},
|
||||
{receiver,
|
||||
fun({RequestId, Result}) ->
|
||||
gen_statem:cast(
|
||||
Self, {key_reply, RequestId, Result})
|
||||
end}]),
|
||||
case Data#data.state of
|
||||
#pending{request_id = OldReqID} = St ->
|
||||
case OldReqID of
|
||||
undefined ->
|
||||
ok;
|
||||
_ ->
|
||||
httpc:cancel_request(OldReqID)
|
||||
end,
|
||||
Data#data{state = St#pending{request_id = RequestID}};
|
||||
#wait{timer_ref = TimerRef} ->
|
||||
erlang:cancel_timer(TimerRef),
|
||||
NotaryServers = mod_matrix_gw_opt:notary_servers(Data#data.host),
|
||||
Data#data{state = #pending{request_id = RequestID,
|
||||
servers = NotaryServers}}
|
||||
end.
|
||||
|
||||
-endif.
|
||||
|
||||
+1
-1
@@ -626,7 +626,7 @@ notify_participant_joined(Mod, LServer, To, From, ID, Nick) ->
|
||||
notify_participant_left(Mod, LServer, To, ID) ->
|
||||
{Chan, Host, _} = jid:tolower(To),
|
||||
Items = #ps_items{node = ?NS_MIX_NODES_PARTICIPANTS,
|
||||
retract = ID},
|
||||
retract = [ID]},
|
||||
Event = #ps_event{items = Items},
|
||||
Msg = #message{from = jid:remove_resource(To),
|
||||
id = p1_rand:get_string(),
|
||||
|
||||
@@ -128,6 +128,7 @@ set_participant(_LServer, Channel, Service, JID, ID, Nick) ->
|
||||
nick = Nick,
|
||||
created_at = erlang:timestamp()}).
|
||||
|
||||
-spec get_participant(binary(), binary(), binary(), jid:jid()) -> {ok, {binary(), binary()}} | {error, notfound}.
|
||||
get_participant(_LServer, Channel, Service, JID) ->
|
||||
{User, Domain, _} = jid:tolower(JID),
|
||||
case mnesia:dirty_read(mix_participant, {User, Domain, Channel, Service}) of
|
||||
|
||||
@@ -167,6 +167,7 @@ set_participant(LServer, Channel, Service, JID, ID, Nick) ->
|
||||
_Err -> {error, db_failure}
|
||||
end.
|
||||
|
||||
-spec get_participant(binary(), binary(), binary(), jid:jid()) -> {ok, {binary(), binary()}} | {error, notfound | db_failure}.
|
||||
get_participant(LServer, Channel, Service, JID) ->
|
||||
{User, Domain, _} = jid:tolower(JID),
|
||||
case ejabberd_sql:sql_query(
|
||||
|
||||
+7
-7
@@ -26,6 +26,7 @@
|
||||
-author('alexey@process-one.net').
|
||||
-protocol({xep, 45, '1.25', '0.5.0', "complete", ""}).
|
||||
-protocol({xep, 249, '1.2', '0.5.0', "complete", ""}).
|
||||
-protocol({xep, 486, '0.1.0', '24.07', "complete", ""}).
|
||||
-ifndef(GEN_SERVER).
|
||||
-define(GEN_SERVER, gen_server).
|
||||
-endif.
|
||||
@@ -96,7 +97,7 @@
|
||||
-callback import(binary(), binary(), [binary()]) -> ok.
|
||||
-callback store_room(binary(), binary(), binary(), list(), list()|undefined) -> {atomic, any()}.
|
||||
-callback store_changes(binary(), binary(), binary(), list()) -> {atomic, any()}.
|
||||
-callback restore_room(binary(), binary(), binary()) -> muc_room_opts() | error.
|
||||
-callback restore_room(binary(), binary(), binary()) -> muc_room_opts() | error | {error, atom()}.
|
||||
-callback forget_room(binary(), binary(), binary()) -> {atomic, any()}.
|
||||
-callback can_use_nick(binary(), binary(), jid(), binary()) -> boolean().
|
||||
-callback get_rooms(binary(), binary()) -> [#muc_room{}].
|
||||
@@ -590,20 +591,17 @@ extract_password(#iq{} = IQ) ->
|
||||
false
|
||||
end.
|
||||
|
||||
-spec unhibernate_room(binary(), binary(), binary()) -> {ok, pid()} | error.
|
||||
-spec unhibernate_room(binary(), binary(), binary()) -> {ok, pid()} | {error, notfound | db_failure | term()}.
|
||||
unhibernate_room(ServerHost, Host, Room) ->
|
||||
unhibernate_room(ServerHost, Host, Room, true).
|
||||
|
||||
-spec unhibernate_room(binary(), binary(), binary(), boolean()) -> {ok, pid()} | error.
|
||||
-spec unhibernate_room(binary(), binary(), binary(), boolean()) -> {ok, pid()} | {error, notfound | db_failure | term()}.
|
||||
unhibernate_room(ServerHost, Host, Room, ResetHibernationTime) ->
|
||||
RMod = gen_mod:ram_db_mod(ServerHost, ?MODULE),
|
||||
case RMod:find_online_room(ServerHost, Room, Host) of
|
||||
error ->
|
||||
Proc = procname(ServerHost, {Room, Host}),
|
||||
case ?GEN_SERVER:call(Proc, {unhibernate, Room, Host, ResetHibernationTime}, 20000) of
|
||||
{ok, _} = R -> R;
|
||||
_ -> error
|
||||
end;
|
||||
?GEN_SERVER:call(Proc, {unhibernate, Room, Host, ResetHibernationTime}, 20000);
|
||||
{ok, _} = R2 -> R2
|
||||
end.
|
||||
|
||||
@@ -887,6 +885,8 @@ load_room(RMod, Host, ServerHost, Room, ResetHibernationTime) ->
|
||||
case restore_room(ServerHost, Host, Room) of
|
||||
error ->
|
||||
{error, notfound};
|
||||
{error, _} = Err ->
|
||||
Err;
|
||||
Opts0 ->
|
||||
Mod = gen_mod:db_mod(ServerHost, mod_muc),
|
||||
case proplists:get_bool(persistent, Opts0) of
|
||||
|
||||
+32
-7
@@ -1289,6 +1289,8 @@ create_room_with_opts(Name1, Host1, ServerHost1, CustomRoomOpts) ->
|
||||
{error, _} ->
|
||||
throw({error, "Unable to start room"})
|
||||
end;
|
||||
{db_failure, _Name, _Host} ->
|
||||
throw({error, "Database error"});
|
||||
_ ->
|
||||
throw({error, "Room already exists"})
|
||||
end.
|
||||
@@ -1307,6 +1309,8 @@ destroy_room(Name1, Service1) ->
|
||||
case get_room_pid_validate(Name1, Service1, <<"service">>) of
|
||||
{room_not_found, _, _} ->
|
||||
throw({error, "Room doesn't exists"});
|
||||
{db_failure, _Name, _Host} ->
|
||||
throw({error, "Database error"});
|
||||
{Pid, _, _} ->
|
||||
mod_muc_room:destroy(Pid),
|
||||
ok
|
||||
@@ -1698,6 +1702,8 @@ change_room_option(Name, Service, OptionString, ValueString) ->
|
||||
case get_room_pid_validate(Name, Service, <<"service">>) of
|
||||
{room_not_found, _, _} ->
|
||||
throw({error, "Room not found"});
|
||||
{db_failure, _Name, _Host} ->
|
||||
throw({error, "Database error"});
|
||||
{Pid, _, _} ->
|
||||
{Option, Value} = format_room_option(OptionString, ValueString),
|
||||
change_room_option(Pid, Option, Value)
|
||||
@@ -1823,25 +1829,29 @@ parse_nodes(_, _) ->
|
||||
throw({error, "Invalid 'subscribers' - unknown node name used"}).
|
||||
|
||||
-spec get_room_pid_validate(binary(), binary(), binary()) ->
|
||||
{pid() | room_not_found, binary(), binary()}.
|
||||
{pid() | room_not_found | db_failure, binary(), binary()}.
|
||||
get_room_pid_validate(Name, Service, ServiceArg) ->
|
||||
Name2 = validate_room(Name),
|
||||
{ServerHost, Service2} = validate_muc2(Service, ServiceArg),
|
||||
case mod_muc:unhibernate_room(ServerHost, Service2, Name2) of
|
||||
error ->
|
||||
{error, notfound} ->
|
||||
{room_not_found, Name2, Service2};
|
||||
{error, db_failure} ->
|
||||
{db_failure, Name2, Service2};
|
||||
{ok, Pid} ->
|
||||
{Pid, Name2, Service2}
|
||||
end.
|
||||
|
||||
%% @doc Get the Pid of an existing MUC room, or 'room_not_found'.
|
||||
-spec get_room_pid(binary(), binary()) -> pid() | room_not_found | invalid_service | unknown_service.
|
||||
-spec get_room_pid(binary(), binary()) -> pid() | room_not_found | db_failure | invalid_service | unknown_service.
|
||||
get_room_pid(Name, Service) ->
|
||||
try get_room_serverhost(Service) of
|
||||
ServerHost ->
|
||||
case mod_muc:unhibernate_room(ServerHost, Service, Name) of
|
||||
error ->
|
||||
{error, notfound} ->
|
||||
room_not_found;
|
||||
{error, db_failure} ->
|
||||
db_failure;
|
||||
{ok, Pid} ->
|
||||
Pid
|
||||
end
|
||||
@@ -1954,6 +1964,8 @@ get_room_affiliations(Name, Service) ->
|
||||
({{Uname, Domain, _Res}, Aff}) when is_atom(Aff)->
|
||||
{Uname, Domain, Aff, <<>>}
|
||||
end, Affiliations);
|
||||
{db_failure, _Name, _Host} ->
|
||||
throw({error, "Database error"});
|
||||
_ ->
|
||||
throw({error, "The room does not exist."})
|
||||
end.
|
||||
@@ -1975,6 +1987,8 @@ get_room_affiliations_v3(Name, Service) ->
|
||||
Jid = makeencode(Uname, Domain),
|
||||
{Jid, Aff, <<>>}
|
||||
end, Affiliations);
|
||||
{db_failure, _Name, _Host} ->
|
||||
throw({error, "Database error"});
|
||||
_ ->
|
||||
throw({error, "The room does not exist."})
|
||||
end.
|
||||
@@ -1993,6 +2007,8 @@ get_room_history(Name, Service) ->
|
||||
_ ->
|
||||
throw({error, "Unable to fetch room state."})
|
||||
end;
|
||||
{db_failure, _Name, _Host} ->
|
||||
throw({error, "Database error"});
|
||||
_ ->
|
||||
throw({error, "The room does not exist."})
|
||||
end.
|
||||
@@ -2012,6 +2028,8 @@ get_room_affiliation(Name, Service, JID) ->
|
||||
{ok, StateData} = mod_muc_room:get_state(Pid),
|
||||
UserJID = jid:decode(JID),
|
||||
mod_muc_room:get_affiliation(UserJID, StateData);
|
||||
{db_failure, _Name, _Host} ->
|
||||
throw({error, "Database error"});
|
||||
_ ->
|
||||
throw({error, "The room does not exist."})
|
||||
end.
|
||||
@@ -2052,6 +2070,8 @@ set_room_affiliation(Name, Service, JID, AffiliationString) ->
|
||||
{error, _} ->
|
||||
throw({error, "Unable to perform change"})
|
||||
end;
|
||||
{db_failure, _Name, _Host} ->
|
||||
throw({error, "Database error"});
|
||||
_ ->
|
||||
throw({error, "Room doesn't exists"})
|
||||
end.
|
||||
@@ -2084,6 +2104,8 @@ subscribe_room(User, Nick, Room, NodeList) ->
|
||||
{error, Reason} ->
|
||||
throw({error, binary_to_list(Reason)})
|
||||
end;
|
||||
{db_failure, _Name, _Host} ->
|
||||
throw({error, "Database error"});
|
||||
_ ->
|
||||
throw({error, "The room does not exist"})
|
||||
end
|
||||
@@ -2129,6 +2151,8 @@ unsubscribe_room(User, Room) ->
|
||||
{error, Reason} ->
|
||||
throw({error, binary_to_list(Reason)})
|
||||
end;
|
||||
{db_failure, _Name, _Host} ->
|
||||
throw({error, "Database error"});
|
||||
_ ->
|
||||
throw({error, "The room does not exist"})
|
||||
end
|
||||
@@ -2146,6 +2170,8 @@ get_subscribers(Name, Host) ->
|
||||
{Pid, _, _} when is_pid(Pid) ->
|
||||
{ok, JIDList} = mod_muc_room:get_subscribers(Pid),
|
||||
[jid:encode(jid:remove_resource(J)) || J <- JIDList];
|
||||
{db_failure, _Name, _Host} ->
|
||||
throw({error, "Database error"});
|
||||
_ ->
|
||||
throw({error, "The room does not exist"})
|
||||
end.
|
||||
@@ -2230,9 +2256,8 @@ find_services_validate(Global, _Name) when Global == global;
|
||||
Global == <<"global">> ->
|
||||
find_services(Global);
|
||||
find_services_validate(Service, Name) ->
|
||||
case validate_muc(Service, Name) of
|
||||
Service2 -> find_services(Service2)
|
||||
end.
|
||||
Service2 = validate_muc(Service, Name),
|
||||
find_services(Service2).
|
||||
|
||||
find_services(Global) when Global == global;
|
||||
Global == <<"global">> ->
|
||||
|
||||
@@ -76,9 +76,11 @@ store_room(_LServer, Host, Name, Opts, _) ->
|
||||
mnesia:transaction(F).
|
||||
|
||||
restore_room(_LServer, Host, Name) ->
|
||||
case catch mnesia:dirty_read(muc_room, {Name, Host}) of
|
||||
try mnesia:dirty_read(muc_room, {Name, Host}) of
|
||||
[#muc_room{opts = Opts}] -> Opts;
|
||||
_ -> error
|
||||
catch
|
||||
_:_ -> {error, db_failure}
|
||||
end.
|
||||
|
||||
forget_room(_LServer, Host, Name) ->
|
||||
|
||||
@@ -3221,6 +3221,7 @@ process_item_change(Item, SD, UJID) ->
|
||||
true ->
|
||||
send_kickban_presence(UJID, JID, Reason, 321, none, SD),
|
||||
maybe_send_affiliation(JID, none, SD),
|
||||
unsubscribe_from_room(JID, SD),
|
||||
SD1 = set_affiliation(JID, none, SD),
|
||||
set_role(JID, none, SD1);
|
||||
_ ->
|
||||
@@ -3237,6 +3238,7 @@ process_item_change(Item, SD, UJID) ->
|
||||
{JID, affiliation, outcast, Reason} ->
|
||||
send_kickban_presence(UJID, JID, Reason, 301, outcast, SD),
|
||||
maybe_send_affiliation(JID, outcast, SD),
|
||||
unsubscribe_from_room(JID, SD),
|
||||
{result, undefined, SD2} =
|
||||
process_iq_mucsub(JID,
|
||||
#iq{type = set,
|
||||
@@ -3279,6 +3281,30 @@ process_item_change(Item, SD, UJID) ->
|
||||
{error, xmpp:err_internal_server_error()}
|
||||
end.
|
||||
|
||||
-spec unsubscribe_from_room(jid(), state()) -> ok | error.
|
||||
unsubscribe_from_room(JID, SD) ->
|
||||
case SD#state.config#config.members_only of
|
||||
false ->
|
||||
ok;
|
||||
true ->
|
||||
case mod_muc:unhibernate_room(SD#state.server_host, SD#state.host, SD#state.room) of
|
||||
{error, _Reason0} ->
|
||||
error;
|
||||
{ok, Pid} ->
|
||||
_UnsubPid =
|
||||
spawn(fun() ->
|
||||
case unsubscribe(Pid, JID) of
|
||||
ok ->
|
||||
ok;
|
||||
{error, Reason} ->
|
||||
?WARNING_MSG("Failed to automatically unsubscribe expelled member from room: ~ts",
|
||||
[Reason]),
|
||||
error
|
||||
end
|
||||
end)
|
||||
end
|
||||
end.
|
||||
|
||||
-spec find_changed_items(jid(), affiliation(), role(),
|
||||
[muc_item()], binary(), state(), [admin_action()]) ->
|
||||
{result, [admin_action()]}.
|
||||
|
||||
@@ -151,9 +151,9 @@ pubsub_event_handler(#message{from = #jid{luser = <<>>, lserver = SServer},
|
||||
SNode = mod_muc_rtbl_opt:rtbl_node(Server),
|
||||
if SServer == SServer2 ->
|
||||
case xmpp:get_subtag(Msg, #ps_event{}) of
|
||||
#ps_event{items = #ps_items{node = Node, retract = Retract}} when Node == SNode,
|
||||
#ps_event{items = #ps_items{node = Node, retract = [Retract | _] = RetractList}} when Node == SNode,
|
||||
is_binary(Retract) ->
|
||||
mnesia:dirty_delete(muc_rtbl, {Server, Retract});
|
||||
[mnesia:dirty_delete(muc_rtbl, {Server, R1}) || R1 <- RetractList];
|
||||
#ps_event{items = #ps_items{node = Node, items = Items}} when Node == SNode ->
|
||||
Added = lists:foldl(
|
||||
fun(#ps_item{id = ID}, Acc) ->
|
||||
|
||||
+4
-2
@@ -220,10 +220,12 @@ restore_room(LServer, Host, Name) ->
|
||||
Opts2 = lists:keystore(subscribers, 1, OptsD, {subscribers, SubData}),
|
||||
mod_muc:opts_to_binary(Opts2);
|
||||
_ ->
|
||||
error
|
||||
{error, db_failure}
|
||||
end;
|
||||
{selected, _} ->
|
||||
error;
|
||||
_ ->
|
||||
error
|
||||
{error, db_failure}
|
||||
end.
|
||||
|
||||
forget_room(LServer, Host, Name) ->
|
||||
|
||||
@@ -0,0 +1,462 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% File : mod_providers.erl
|
||||
%%% Author : Badlop <badlop@process-one.net>
|
||||
%%% Purpose : Serve xmpp-provider-v2.json files as described by XMPP Providers
|
||||
%%% Created : 7 August 2025 by Badlop <badlop@process-one.net>
|
||||
%%%
|
||||
%%%
|
||||
%%% ejabberd, Copyright (C) 2025 ProcessOne
|
||||
%%%
|
||||
%%% This program is free software; you can redistribute it and/or
|
||||
%%% modify it under the terms of the GNU General Public License as
|
||||
%%% published by the Free Software Foundation; either version 2 of the
|
||||
%%% License, or (at your option) any later version.
|
||||
%%%
|
||||
%%% This program is distributed in the hope that it will be useful,
|
||||
%%% but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
%%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
%%% General Public License for more details.
|
||||
%%%
|
||||
%%% You should have received a copy of the GNU General Public License along
|
||||
%%% with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
%%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
%%%
|
||||
%%%----------------------------------------------------------------------
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%%| Definitions
|
||||
|
||||
%% This module is based in mod_host_meta.erl
|
||||
|
||||
%% @format-begin
|
||||
|
||||
-module(mod_providers).
|
||||
|
||||
-author('badlop@process-one.net').
|
||||
|
||||
-behaviour(gen_mod).
|
||||
|
||||
-export([start/2, stop/1, reload/3, process/2, mod_opt_type/1, mod_options/1, depends/2]).
|
||||
-export([mod_doc/0]).
|
||||
|
||||
-include("logger.hrl").
|
||||
|
||||
-include_lib("xmpp/include/xmpp.hrl").
|
||||
|
||||
-include("ejabberd_http.hrl").
|
||||
-include("ejabberd_web_admin.hrl").
|
||||
-include("translate.hrl").
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%%| gen_mod callbacks
|
||||
|
||||
start(_Host, _Opts) ->
|
||||
report_hostmeta_listener(),
|
||||
ok.
|
||||
|
||||
stop(_Host) ->
|
||||
ok.
|
||||
|
||||
reload(_Host, _NewOpts, _OldOpts) ->
|
||||
report_hostmeta_listener(),
|
||||
ok.
|
||||
|
||||
depends(_Host, _Opts) ->
|
||||
[].
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%%| HTTP handlers
|
||||
|
||||
process([],
|
||||
#request{method = 'GET',
|
||||
host = Host,
|
||||
path = Path}) ->
|
||||
case lists:last(Path) of
|
||||
<<"xmpp-provider-v2.json">> ->
|
||||
file_json(Host)
|
||||
end;
|
||||
process(_Path, _Request) ->
|
||||
{404, [], "Not Found"}.
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%%| JSON
|
||||
|
||||
file_json(Host) ->
|
||||
Content =
|
||||
#{website => build_urls(Host, website),
|
||||
alternativeJids => gen_mod:get_module_opt(Host, ?MODULE, alternativeJids),
|
||||
busFactor => gen_mod:get_module_opt(Host, ?MODULE, busFactor),
|
||||
organization => gen_mod:get_module_opt(Host, ?MODULE, organization),
|
||||
passwordReset => get_password_url(Host),
|
||||
serverTesting => gen_mod:get_module_opt(Host, ?MODULE, serverTesting),
|
||||
maximumHttpFileUploadTotalSize => get_upload_size(Host),
|
||||
maximumHttpFileUploadStorageTime => get_upload_time(Host),
|
||||
maximumMessageArchiveManagementStorageTime =>
|
||||
gen_mod:get_module_opt(Host, ?MODULE, maximumMessageArchiveManagementStorageTime),
|
||||
professionalHosting => gen_mod:get_module_opt(Host, ?MODULE, professionalHosting),
|
||||
freeOfCharge => gen_mod:get_module_opt(Host, ?MODULE, freeOfCharge),
|
||||
legalNotice => build_urls(Host, legalNotice),
|
||||
serverLocations => gen_mod:get_module_opt(Host, ?MODULE, serverLocations),
|
||||
since => gen_mod:get_module_opt(Host, ?MODULE, since)},
|
||||
{200,
|
||||
[html,
|
||||
{<<"Content-Type">>, <<"application/json">>},
|
||||
{<<"Access-Control-Allow-Origin">>, <<"*">>}],
|
||||
[misc:json_encode(Content)]}.
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%%| Upload Size
|
||||
|
||||
get_upload_size(Host) ->
|
||||
case gen_mod:get_module_opt(Host, ?MODULE, maximumHttpFileUploadTotalSize) of
|
||||
default_value ->
|
||||
get_upload_size_mhuq(Host);
|
||||
I when is_integer(I) ->
|
||||
I
|
||||
end.
|
||||
|
||||
get_upload_size_mhuq(Host) ->
|
||||
case gen_mod:is_loaded(Host, mod_http_upload_quota) of
|
||||
true ->
|
||||
Access = gen_mod:get_module_opt(Host, mod_http_upload_quota, access_hard_quota),
|
||||
Rules = ejabberd_shaper:read_shaper_rules(Access, Host),
|
||||
get_upload_size_rules(Rules);
|
||||
false ->
|
||||
0
|
||||
end.
|
||||
|
||||
get_upload_size_rules(Rules) ->
|
||||
case lists:keysearch([{acl, all}], 2, Rules) of
|
||||
{value, {Size, _}} ->
|
||||
Size;
|
||||
false ->
|
||||
0
|
||||
end.
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%%| Upload Time
|
||||
|
||||
get_upload_time(Host) ->
|
||||
case gen_mod:get_module_opt(Host, ?MODULE, maximumHttpFileUploadStorageTime) of
|
||||
default_value ->
|
||||
get_upload_time_mhuq(Host);
|
||||
I when is_integer(I) ->
|
||||
I
|
||||
end.
|
||||
|
||||
get_upload_time_mhuq(Host) ->
|
||||
case gen_mod:is_loaded(Host, mod_http_upload_quota) of
|
||||
true ->
|
||||
case gen_mod:get_module_opt(Host, mod_http_upload_quota, max_days) of
|
||||
infinity ->
|
||||
0;
|
||||
I when is_integer(I) ->
|
||||
I
|
||||
end;
|
||||
false ->
|
||||
0
|
||||
end.
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%%| Password URL
|
||||
|
||||
get_password_url(Host) ->
|
||||
build_urls(Host, get_password_url2(Host)).
|
||||
|
||||
get_password_url2(Host) ->
|
||||
case gen_mod:get_module_opt(Host, ?MODULE, passwordReset) of
|
||||
default_value ->
|
||||
get_password_url3(Host);
|
||||
U when is_binary(U) ->
|
||||
U
|
||||
end.
|
||||
|
||||
get_password_url3(Host) ->
|
||||
case find_handler_port_path2(any, mod_register_web) of
|
||||
[] ->
|
||||
<<"">>;
|
||||
[{ThisTls, Port, Path} | _] ->
|
||||
Protocol =
|
||||
case ThisTls of
|
||||
false ->
|
||||
<<"http">>;
|
||||
true ->
|
||||
<<"https">>
|
||||
end,
|
||||
<<Protocol/binary,
|
||||
"://",
|
||||
Host/binary,
|
||||
":",
|
||||
(integer_to_binary(Port))/binary,
|
||||
"/",
|
||||
(str:join(Path, <<"/">>))/binary,
|
||||
"/">>
|
||||
end.
|
||||
|
||||
%% TODO Ya hay otra funciona como esta
|
||||
find_handler_port_path2(Tls, Module) ->
|
||||
lists:filtermap(fun ({{Port, _, _},
|
||||
ejabberd_http,
|
||||
#{tls := ThisTls, request_handlers := Handlers}})
|
||||
when (Tls == any) or (Tls == ThisTls) ->
|
||||
case lists:keyfind(Module, 2, Handlers) of
|
||||
false ->
|
||||
false;
|
||||
{Path, Module} ->
|
||||
{true, {ThisTls, Port, Path}}
|
||||
end;
|
||||
(_) ->
|
||||
false
|
||||
end,
|
||||
ets:tab2list(ejabberd_listener)).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%%| Build URLs
|
||||
|
||||
build_urls(Host, Option) when is_atom(Option) ->
|
||||
build_urls(Host, gen_mod:get_module_opt(Host, ?MODULE, Option));
|
||||
build_urls(_Host, <<"">>) ->
|
||||
#{};
|
||||
build_urls(Host, Url) when not is_atom(Url) ->
|
||||
Languages = gen_mod:get_module_opt(Host, ?MODULE, languages),
|
||||
maps:from_list([{L, misc:expand_keyword(<<"@LANGUAGE_URL@">>, Url, L)}
|
||||
|| L <- Languages]).
|
||||
|
||||
find_handler_port_path(Tls, Module) ->
|
||||
lists:filtermap(fun ({{Port, _, _},
|
||||
ejabberd_http,
|
||||
#{tls := ThisTls, request_handlers := Handlers}})
|
||||
when is_integer(Port) and ((Tls == any) or (Tls == ThisTls)) ->
|
||||
case lists:keyfind(Module, 2, Handlers) of
|
||||
false ->
|
||||
false;
|
||||
{Path, Module} ->
|
||||
{true, {ThisTls, Port, Path}}
|
||||
end;
|
||||
(_) ->
|
||||
false
|
||||
end,
|
||||
ets:tab2list(ejabberd_listener)).
|
||||
|
||||
report_hostmeta_listener() ->
|
||||
case {find_handler_port_path(false, ?MODULE), find_handler_port_path(true, ?MODULE)} of
|
||||
{[], []} ->
|
||||
?CRITICAL_MSG("It seems you enabled ~p in 'modules' but forgot to "
|
||||
"add it as a request_handler in an ejabberd_http "
|
||||
"listener.",
|
||||
[?MODULE]);
|
||||
{[_ | _], _} ->
|
||||
?WARNING_MSG("Apparently ~p is enabled in a request_handler in a "
|
||||
"non-encrypted ejabberd_http listener. If this is "
|
||||
"not desired, enable 'tls' in that "
|
||||
"listener, or setup a proxy encryption mechanism.",
|
||||
[?MODULE]);
|
||||
{[], [_ | _]} ->
|
||||
ok
|
||||
end.
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%%| Options
|
||||
|
||||
mod_opt_type(languages) ->
|
||||
econf:list(
|
||||
econf:binary());
|
||||
mod_opt_type(website) ->
|
||||
econf:binary();
|
||||
mod_opt_type(alternativeJids) ->
|
||||
econf:list(
|
||||
econf:domain(), [unique]);
|
||||
mod_opt_type(busFactor) ->
|
||||
econf:int();
|
||||
mod_opt_type(organization) ->
|
||||
econf:enum([company,
|
||||
'commercial person',
|
||||
'private person',
|
||||
governmental,
|
||||
'non-governmental']);
|
||||
mod_opt_type(passwordReset) ->
|
||||
econf:binary();
|
||||
mod_opt_type(serverTesting) ->
|
||||
econf:bool();
|
||||
mod_opt_type(maximumHttpFileUploadTotalSize) ->
|
||||
econf:int();
|
||||
mod_opt_type(maximumHttpFileUploadStorageTime) ->
|
||||
econf:int();
|
||||
mod_opt_type(maximumMessageArchiveManagementStorageTime) ->
|
||||
econf:int();
|
||||
mod_opt_type(professionalHosting) ->
|
||||
econf:bool();
|
||||
mod_opt_type(freeOfCharge) ->
|
||||
econf:bool();
|
||||
mod_opt_type(legalNotice) ->
|
||||
econf:binary();
|
||||
mod_opt_type(serverLocations) ->
|
||||
econf:list(
|
||||
econf:binary());
|
||||
mod_opt_type(since) ->
|
||||
econf:binary().
|
||||
|
||||
mod_options(Host) ->
|
||||
[{languages, [ejabberd_option:language(Host)]},
|
||||
{website, <<"">>},
|
||||
{alternativeJids, []},
|
||||
{busFactor, -1},
|
||||
{organization, ''},
|
||||
{passwordReset, default_value},
|
||||
{serverTesting, false},
|
||||
{maximumHttpFileUploadTotalSize, default_value},
|
||||
{maximumHttpFileUploadStorageTime, default_value},
|
||||
{maximumMessageArchiveManagementStorageTime, 0},
|
||||
{professionalHosting, false},
|
||||
{freeOfCharge, false},
|
||||
{legalNotice, <<"">>},
|
||||
{serverLocations, []},
|
||||
{since, <<"">>}].
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%%| Doc
|
||||
|
||||
mod_doc() ->
|
||||
#{desc =>
|
||||
[?T("This module serves JSON provider files API v2 as described by "
|
||||
"https://providers.xmpp.net/provider-file-generator/[XMPP Providers]."),
|
||||
"",
|
||||
?T("It attempts to fill some properties gathering values automatically from your existing ejabberd configuration. Try enabling the module, check what values are displayed, and then customize using the options."),
|
||||
"",
|
||||
?T("To use this module, in addition to adding it to the 'modules' "
|
||||
"section, you must also enable it in 'listen' -> 'ejabberd_http' -> "
|
||||
"_`listen-options.md#request_handlers|request_handlers`_. "
|
||||
"Notice you should set in _`listen.md#ejabberd_http|ejabberd_http`_ "
|
||||
"the option _`listen-options.md#tls|tls`_ enabled.")],
|
||||
note => "added in 25.08",
|
||||
opts =>
|
||||
[{languages,
|
||||
#{value => "[string()]",
|
||||
desc =>
|
||||
?T("List of language codes that your pages are available. "
|
||||
"Some options define URL where the keyword '@LANGUAGE_URL@' "
|
||||
"will be replaced with each of those language codes. "
|
||||
"The default value is a list with the language set in the "
|
||||
"option _`language`_, for example: '[en]'.")}},
|
||||
{website,
|
||||
#{value => "string()",
|
||||
desc =>
|
||||
?T("Provider website. "
|
||||
"The keyword '@LANGUAGE_URL@' is replaced with each language. "
|
||||
"The default value is '\"\"'.")}},
|
||||
{alternativeJids,
|
||||
#{value => "[string()]",
|
||||
desc =>
|
||||
?T("List of JIDs (XMPP server domains) a provider offers for "
|
||||
"registration other than its main JID. "
|
||||
"The default value is '[]'.")}},
|
||||
{busFactor,
|
||||
#{value => "integer()",
|
||||
desc =>
|
||||
?T("Bus factor of the XMPP service (i.e., the minimum number of "
|
||||
"team members that the service could not survive losing) or '-1' for n/a. "
|
||||
"The default value is '-1'.")}},
|
||||
{organization,
|
||||
#{value => "string()",
|
||||
desc =>
|
||||
?T("Type of organization providing the XMPP service. "
|
||||
"Allowed values are: 'company', '\"commercial person\"', '\"private person\"', "
|
||||
"'governmental', '\"non-governmental\"' or '\"\"'. "
|
||||
"The default value is '\"\"'.")}},
|
||||
{passwordReset,
|
||||
#{value => "string()",
|
||||
desc =>
|
||||
?T("Password reset web page (per language) used for an automatic password reset "
|
||||
"(e.g., via email) or describing how to manually reset a password "
|
||||
"(e.g., by contacting the provider). "
|
||||
"The keyword '@LANGUAGE_URL@' is replaced with each language. "
|
||||
"The default value is an URL built automatically "
|
||||
"if _`mod_register_web`_ is configured as a 'request_handler', "
|
||||
"or '\"\"' otherwise.")}},
|
||||
{serverTesting,
|
||||
#{value => "true | false",
|
||||
desc =>
|
||||
?T("Whether tests against the provider's server are allowed "
|
||||
"(e.g., certificate checks and uptime monitoring). "
|
||||
"The default value is 'false'.")}},
|
||||
{maximumHttpFileUploadTotalSize,
|
||||
#{value => "integer()",
|
||||
desc =>
|
||||
?T("Maximum size of all shared files in total per user (number in megabytes (MB), "
|
||||
"'0' for no limit or '-1' for less than 1 MB). "
|
||||
"Attention: MB is used instead of MiB (e.g., 104,857,600 bytes = 100 MiB ≈ 104 MB). "
|
||||
"This property is not about the maximum size of each shared file, "
|
||||
"which is already retrieved via XMPP. "
|
||||
"The default value is the value of the shaper value "
|
||||
"of option 'access_hard_quota' "
|
||||
"from module _`mod_http_upload_quota`_, or '0' otherwise.")}},
|
||||
{maximumHttpFileUploadStorageTime,
|
||||
#{value => "integer()",
|
||||
desc =>
|
||||
?T("Maximum storage duration of each shared file "
|
||||
"(number in days, '0' for no limit or '-1' for less than 1 day). "
|
||||
"The default value is the same as option 'max_days' "
|
||||
"from module _`mod_http_upload_quota`_, or '0' otherwise.")}},
|
||||
{maximumMessageArchiveManagementStorageTime,
|
||||
#{value => "integer()",
|
||||
desc =>
|
||||
?T("Maximum storage duration of each exchanged message "
|
||||
"(number in days, '0' for no limit or '-1' for less than 1 day). "
|
||||
"The default value is '0'.")}},
|
||||
{professionalHosting,
|
||||
#{value => "true | false",
|
||||
desc =>
|
||||
?T("Whether the XMPP server is hosted with good internet connection speed, "
|
||||
"uninterruptible power supply, access protection and regular backups. "
|
||||
"The default value is 'false'.")}},
|
||||
{freeOfCharge,
|
||||
#{value => "true | false",
|
||||
desc =>
|
||||
?T("Whether the XMPP service can be used for free. "
|
||||
"The default value is 'false'.")}},
|
||||
{legalNotice,
|
||||
#{value => "string()",
|
||||
desc =>
|
||||
?T("Legal notice web page (per language). "
|
||||
"The keyword '@LANGUAGE_URL@' is replaced with each language. "
|
||||
"The default value is '\"\"'.")}},
|
||||
{serverLocations,
|
||||
#{value => "[string()]",
|
||||
desc =>
|
||||
?T("List of language codes of Server/Backup locations. "
|
||||
"The default value is an empty list: '[]'.")}},
|
||||
{since,
|
||||
#{value => "string()",
|
||||
desc =>
|
||||
?T("Date since the XMPP service is available. "
|
||||
"The default value is an empty string: '\"\"'.")}}],
|
||||
example =>
|
||||
["listen:",
|
||||
" -",
|
||||
" port: 443",
|
||||
" module: ejabberd_http",
|
||||
" tls: true",
|
||||
" request_handlers:",
|
||||
" /.well-known/xmpp-provider-v2.json: mod_providers",
|
||||
"",
|
||||
"modules:",
|
||||
" mod_providers:",
|
||||
" alternativeJids: [\"example1.com\", \"example2.com\"]",
|
||||
" busFactor: 1",
|
||||
" freeOfCharge: true",
|
||||
" languages: [ag, ao, bg, en]",
|
||||
" legalNotice: \"http://@HOST@/legal/@LANGUAGE_URL@/\"",
|
||||
" maximumHttpFileUploadStorageTime: 0",
|
||||
" maximumHttpFileUploadTotalSize: 0",
|
||||
" maximumMessageArchiveManagementStorageTime: 0",
|
||||
" organization: \"non-governmental\"",
|
||||
" passwordReset: \"http://@HOST@/reset/@LANGUAGE_URL@/\"",
|
||||
" professionalHosting: true",
|
||||
" serverLocations: [ao, bg]",
|
||||
" serverTesting: true",
|
||||
" since: \"2025-12-31\"",
|
||||
" website: \"http://@HOST@/website/@LANGUAGE_URL@/\""]}.
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
%%| vim: set foldmethod=marker foldmarker=%%|,%%-:
|
||||
@@ -0,0 +1,111 @@
|
||||
%% Generated automatically
|
||||
%% DO NOT EDIT: run `make options` instead
|
||||
|
||||
-module(mod_providers_opt).
|
||||
|
||||
-export([alternativeJids/1]).
|
||||
-export([busFactor/1]).
|
||||
-export([freeOfCharge/1]).
|
||||
-export([languages/1]).
|
||||
-export([legalNotice/1]).
|
||||
-export([maximumHttpFileUploadStorageTime/1]).
|
||||
-export([maximumHttpFileUploadTotalSize/1]).
|
||||
-export([maximumMessageArchiveManagementStorageTime/1]).
|
||||
-export([organization/1]).
|
||||
-export([passwordReset/1]).
|
||||
-export([professionalHosting/1]).
|
||||
-export([serverLocations/1]).
|
||||
-export([serverTesting/1]).
|
||||
-export([since/1]).
|
||||
-export([website/1]).
|
||||
|
||||
-spec alternativeJids(gen_mod:opts() | global | binary()) -> [binary()].
|
||||
alternativeJids(Opts) when is_map(Opts) ->
|
||||
gen_mod:get_opt(alternativeJids, Opts);
|
||||
alternativeJids(Host) ->
|
||||
gen_mod:get_module_opt(Host, mod_providers, alternativeJids).
|
||||
|
||||
-spec busFactor(gen_mod:opts() | global | binary()) -> integer().
|
||||
busFactor(Opts) when is_map(Opts) ->
|
||||
gen_mod:get_opt(busFactor, Opts);
|
||||
busFactor(Host) ->
|
||||
gen_mod:get_module_opt(Host, mod_providers, busFactor).
|
||||
|
||||
-spec freeOfCharge(gen_mod:opts() | global | binary()) -> boolean().
|
||||
freeOfCharge(Opts) when is_map(Opts) ->
|
||||
gen_mod:get_opt(freeOfCharge, Opts);
|
||||
freeOfCharge(Host) ->
|
||||
gen_mod:get_module_opt(Host, mod_providers, freeOfCharge).
|
||||
|
||||
-spec languages(gen_mod:opts() | global | binary()) -> [binary()].
|
||||
languages(Opts) when is_map(Opts) ->
|
||||
gen_mod:get_opt(languages, Opts);
|
||||
languages(Host) ->
|
||||
gen_mod:get_module_opt(Host, mod_providers, languages).
|
||||
|
||||
-spec legalNotice(gen_mod:opts() | global | binary()) -> binary().
|
||||
legalNotice(Opts) when is_map(Opts) ->
|
||||
gen_mod:get_opt(legalNotice, Opts);
|
||||
legalNotice(Host) ->
|
||||
gen_mod:get_module_opt(Host, mod_providers, legalNotice).
|
||||
|
||||
-spec maximumHttpFileUploadStorageTime(gen_mod:opts() | global | binary()) -> 'default_value' | integer().
|
||||
maximumHttpFileUploadStorageTime(Opts) when is_map(Opts) ->
|
||||
gen_mod:get_opt(maximumHttpFileUploadStorageTime, Opts);
|
||||
maximumHttpFileUploadStorageTime(Host) ->
|
||||
gen_mod:get_module_opt(Host, mod_providers, maximumHttpFileUploadStorageTime).
|
||||
|
||||
-spec maximumHttpFileUploadTotalSize(gen_mod:opts() | global | binary()) -> 'default_value' | integer().
|
||||
maximumHttpFileUploadTotalSize(Opts) when is_map(Opts) ->
|
||||
gen_mod:get_opt(maximumHttpFileUploadTotalSize, Opts);
|
||||
maximumHttpFileUploadTotalSize(Host) ->
|
||||
gen_mod:get_module_opt(Host, mod_providers, maximumHttpFileUploadTotalSize).
|
||||
|
||||
-spec maximumMessageArchiveManagementStorageTime(gen_mod:opts() | global | binary()) -> integer().
|
||||
maximumMessageArchiveManagementStorageTime(Opts) when is_map(Opts) ->
|
||||
gen_mod:get_opt(maximumMessageArchiveManagementStorageTime, Opts);
|
||||
maximumMessageArchiveManagementStorageTime(Host) ->
|
||||
gen_mod:get_module_opt(Host, mod_providers, maximumMessageArchiveManagementStorageTime).
|
||||
|
||||
-spec organization(gen_mod:opts() | global | binary()) -> '' | 'commercial person' | 'company' | 'governmental' | 'non-governmental' | 'private person'.
|
||||
organization(Opts) when is_map(Opts) ->
|
||||
gen_mod:get_opt(organization, Opts);
|
||||
organization(Host) ->
|
||||
gen_mod:get_module_opt(Host, mod_providers, organization).
|
||||
|
||||
-spec passwordReset(gen_mod:opts() | global | binary()) -> 'default_value' | binary().
|
||||
passwordReset(Opts) when is_map(Opts) ->
|
||||
gen_mod:get_opt(passwordReset, Opts);
|
||||
passwordReset(Host) ->
|
||||
gen_mod:get_module_opt(Host, mod_providers, passwordReset).
|
||||
|
||||
-spec professionalHosting(gen_mod:opts() | global | binary()) -> boolean().
|
||||
professionalHosting(Opts) when is_map(Opts) ->
|
||||
gen_mod:get_opt(professionalHosting, Opts);
|
||||
professionalHosting(Host) ->
|
||||
gen_mod:get_module_opt(Host, mod_providers, professionalHosting).
|
||||
|
||||
-spec serverLocations(gen_mod:opts() | global | binary()) -> [binary()].
|
||||
serverLocations(Opts) when is_map(Opts) ->
|
||||
gen_mod:get_opt(serverLocations, Opts);
|
||||
serverLocations(Host) ->
|
||||
gen_mod:get_module_opt(Host, mod_providers, serverLocations).
|
||||
|
||||
-spec serverTesting(gen_mod:opts() | global | binary()) -> boolean().
|
||||
serverTesting(Opts) when is_map(Opts) ->
|
||||
gen_mod:get_opt(serverTesting, Opts);
|
||||
serverTesting(Host) ->
|
||||
gen_mod:get_module_opt(Host, mod_providers, serverTesting).
|
||||
|
||||
-spec since(gen_mod:opts() | global | binary()) -> binary().
|
||||
since(Opts) when is_map(Opts) ->
|
||||
gen_mod:get_opt(since, Opts);
|
||||
since(Host) ->
|
||||
gen_mod:get_module_opt(Host, mod_providers, since).
|
||||
|
||||
-spec website(gen_mod:opts() | global | binary()) -> binary().
|
||||
website(Opts) when is_map(Opts) ->
|
||||
gen_mod:get_opt(website, Opts);
|
||||
website(Host) ->
|
||||
gen_mod:get_module_opt(Host, mod_providers, website).
|
||||
|
||||
@@ -0,0 +1,391 @@
|
||||
%%%----------------------------------------------------------------------
|
||||
%%% File : mod_pubsub_serverinfo.erl
|
||||
%%% Author : Stefan Strigler <stefan@strigler.de>
|
||||
%%% Purpose : Exposes server information over Pub/Sub
|
||||
%%% Created : 26 Dec 2023 by Guus der Kinderen <guus.der.kinderen@gmail.com>
|
||||
%%%
|
||||
%%%
|
||||
%%% ejabberd, Copyright (C) 2023 - 2025 ProcessOne
|
||||
%%%
|
||||
%%% This program is free software; you can redistribute it and/or
|
||||
%%% modify it under the terms of the GNU General Public License as
|
||||
%%% published by the Free Software Foundation; either version 2 of the
|
||||
%%% License, or (at your option) any later version.
|
||||
%%%
|
||||
%%% This program is distributed in the hope that it will be useful,
|
||||
%%% but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
%%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
%%% General Public License for more details.
|
||||
%%%
|
||||
%%% You should have received a copy of the GNU General Public License along
|
||||
%%% with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
%%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
%%%
|
||||
%%%----------------------------------------------------------------------
|
||||
|
||||
-module(mod_pubsub_serverinfo).
|
||||
-author('stefan@strigler.de').
|
||||
|
||||
-protocol({xep, 485, '0.1.1', '25.07', "complete", ""}).
|
||||
|
||||
-behaviour(gen_mod).
|
||||
-behaviour(gen_server).
|
||||
|
||||
-include("logger.hrl").
|
||||
-include("translate.hrl").
|
||||
|
||||
-include_lib("xmpp/include/xmpp.hrl").
|
||||
|
||||
%% gen_mod callbacks.
|
||||
-export([start/2, stop/1, depends/2, mod_options/1, mod_opt_type/1, get_local_features/5, mod_doc/0]).
|
||||
-export([init/1, handle_cast/2, handle_call/3, handle_info/2, terminate/2]).
|
||||
-export([in_auth_result/3, out_auth_result/2, get_info/5]).
|
||||
|
||||
-define(NS_URN_SERVERINFO, <<"urn:xmpp:serverinfo:0">>).
|
||||
-define(PUBLIC_HOSTS_URL, <<"https://data.xmpp.net/providers/v2/providers-Ds.json">>).
|
||||
|
||||
-record(state, {host, pubsub_host, node, monitors = #{}, timer = undefined, public_hosts = []}).
|
||||
|
||||
%% @format-begin
|
||||
|
||||
start(Host, Opts) ->
|
||||
case pubsub_host(Host, Opts) of
|
||||
{error, _Reason} = Error ->
|
||||
Error;
|
||||
PubsubHost ->
|
||||
ejabberd_hooks:add(disco_local_features, Host, ?MODULE, get_local_features, 50),
|
||||
ejabberd_hooks:add(disco_info, Host, ?MODULE, get_info, 50),
|
||||
ejabberd_hooks:add(s2s_out_auth_result, Host, ?MODULE, out_auth_result, 50),
|
||||
ejabberd_hooks:add(s2s_in_auth_result, Host, ?MODULE, in_auth_result, 50),
|
||||
gen_mod:start_child(?MODULE, Host, PubsubHost)
|
||||
end.
|
||||
|
||||
stop(Host) ->
|
||||
ejabberd_hooks:delete(disco_local_features, Host, ?MODULE, get_local_features, 50),
|
||||
ejabberd_hooks:delete(disco_info, Host, ?MODULE, get_info, 50),
|
||||
ejabberd_hooks:delete(s2s_out_auth_result, Host, ?MODULE, out_auth_result, 50),
|
||||
ejabberd_hooks:delete(s2s_in_auth_result, Host, ?MODULE, in_auth_result, 50),
|
||||
gen_mod:stop_child(?MODULE, Host).
|
||||
|
||||
init([Host, PubsubHost]) ->
|
||||
TRef =
|
||||
timer:send_interval(
|
||||
timer:minutes(5), self(), update_pubsub),
|
||||
Monitors = init_monitors(Host),
|
||||
PublicHosts = fetch_public_hosts(),
|
||||
State =
|
||||
#state{host = Host,
|
||||
pubsub_host = PubsubHost,
|
||||
node = <<"serverinfo">>,
|
||||
timer = TRef,
|
||||
monitors = Monitors,
|
||||
public_hosts = PublicHosts},
|
||||
self() ! update_pubsub,
|
||||
{ok, State}.
|
||||
|
||||
-spec init_monitors(binary()) -> map().
|
||||
init_monitors(Host) ->
|
||||
lists:foldl(fun(Domain, Monitors) ->
|
||||
RefIn = make_ref(), % just dummies
|
||||
RefOut = make_ref(),
|
||||
maps:merge(#{RefIn => {incoming, {Host, Domain, true}},
|
||||
RefOut => {outgoing, {Host, Domain, true}}},
|
||||
Monitors)
|
||||
end,
|
||||
#{},
|
||||
ejabberd_option:hosts() -- [Host]).
|
||||
|
||||
-spec fetch_public_hosts() -> list().
|
||||
fetch_public_hosts() ->
|
||||
try
|
||||
{ok, {{_, 200, _}, _Headers, Body}} =
|
||||
httpc:request(get, {?PUBLIC_HOSTS_URL, []}, [{timeout, 1000}], [{body_format, binary}]),
|
||||
case misc:json_decode(Body) of
|
||||
PublicHosts when is_list(PublicHosts) ->
|
||||
PublicHosts;
|
||||
Other ->
|
||||
?WARNING_MSG("Parsed JSON for public hosts was not a list: ~p", [Other]),
|
||||
[]
|
||||
end
|
||||
catch
|
||||
E:R ->
|
||||
?WARNING_MSG("Failed fetching public hosts (~p): ~p", [E, R]),
|
||||
[]
|
||||
end.
|
||||
|
||||
handle_cast({Event, Domain, Pid}, #state{host = Host, monitors = Mons} = State)
|
||||
when Event == register_in; Event == register_out ->
|
||||
Ref = monitor(process, Pid),
|
||||
IsPublic = check_if_public(Domain, State),
|
||||
NewMons = maps:put(Ref, {event_to_dir(Event), {Host, Domain, IsPublic}}, Mons),
|
||||
{noreply, State#state{monitors = NewMons}};
|
||||
handle_cast(_, State) ->
|
||||
{noreply, State}.
|
||||
|
||||
event_to_dir(register_in) ->
|
||||
incoming;
|
||||
event_to_dir(register_out) ->
|
||||
outgoing.
|
||||
|
||||
handle_call(pubsub_host, _From, #state{pubsub_host = PubsubHost} = State) ->
|
||||
{reply, {ok, PubsubHost}, State};
|
||||
handle_call(_Request, _From, State) ->
|
||||
{noreply, State}.
|
||||
|
||||
handle_info({iq_reply, IQReply, {LServer, RServer}}, #state{monitors = Mons} = State) ->
|
||||
case IQReply of
|
||||
#iq{type = result, sub_els = [El]} ->
|
||||
case xmpp:decode(El) of
|
||||
#disco_info{features = Features} ->
|
||||
case lists:member(?NS_URN_SERVERINFO, Features) of
|
||||
true ->
|
||||
NewMons =
|
||||
maps:fold(fun (Ref, {Dir, {LServer0, RServer0, _}}, Acc)
|
||||
when LServer == LServer0, RServer == RServer0 ->
|
||||
maps:put(Ref,
|
||||
{Dir, {LServer, RServer, true}},
|
||||
Acc);
|
||||
(Ref, Other, Acc) ->
|
||||
maps:put(Ref, Other, Acc)
|
||||
end,
|
||||
#{},
|
||||
Mons),
|
||||
{noreply, State#state{monitors = NewMons}};
|
||||
_ ->
|
||||
{noreply, State}
|
||||
end;
|
||||
_ ->
|
||||
{noreply, State}
|
||||
end;
|
||||
_ ->
|
||||
{noreply, State}
|
||||
end;
|
||||
handle_info(update_pubsub, State) ->
|
||||
update_pubsub(State),
|
||||
{noreply, State};
|
||||
handle_info({'DOWN', Mon, process, _Pid, _Info}, #state{monitors = Mons} = State) ->
|
||||
{noreply, State#state{monitors = maps:remove(Mon, Mons)}};
|
||||
handle_info(_Request, State) ->
|
||||
{noreply, State}.
|
||||
|
||||
terminate(_Reason, #state{monitors = Mons, timer = Timer}) ->
|
||||
case is_reference(Timer) of
|
||||
true ->
|
||||
case erlang:cancel_timer(Timer) of
|
||||
false ->
|
||||
receive
|
||||
{timeout, Timer, _} ->
|
||||
ok
|
||||
after 0 ->
|
||||
ok
|
||||
end;
|
||||
_ ->
|
||||
ok
|
||||
end;
|
||||
_ ->
|
||||
ok
|
||||
end,
|
||||
maps:fold(fun(Mon, _, _) -> demonitor(Mon) end, ok, Mons).
|
||||
|
||||
depends(_Host, _Opts) ->
|
||||
[{mod_pubsub, hard}].
|
||||
|
||||
mod_options(_Host) ->
|
||||
[{pubsub_host, undefined}].
|
||||
|
||||
mod_opt_type(pubsub_host) ->
|
||||
econf:either(undefined, econf:host()).
|
||||
|
||||
mod_doc() ->
|
||||
#{desc =>
|
||||
[?T("This module adds support for "
|
||||
"https://xmpp.org/extensions/xep-0485.html[XEP-0485: PubSub Server Information] "
|
||||
"to expose S2S information over the Pub/Sub service."),
|
||||
"",
|
||||
?T("Active S2S connections are published to a local PubSub node. "
|
||||
"Currently the node name is hardcoded as '\"serverinfo\"'."),
|
||||
"",
|
||||
?T("Connections that support this feature are exposed with their domain names, "
|
||||
"otherwise they are shown as anonymous nodes. "
|
||||
"At startup a list of well known public servers is fetched. "
|
||||
"Those are not shown as anonymous even if they don't support this feature."),
|
||||
"",
|
||||
?T("Please note that the module only shows S2S connections established while the module is running. "
|
||||
"If you install the module at runtime, run _`stop_s2s_connections`_ API or restart ejabberd "
|
||||
"to force S2S reconnections that the module will detect and publish."),
|
||||
"",
|
||||
?T("This module depends on _`mod_pubsub`_ and _`mod_disco`_.")],
|
||||
note => "added in 25.07",
|
||||
opts =>
|
||||
[{pubsub_host,
|
||||
#{value => "undefined | string()",
|
||||
desc =>
|
||||
?T("Use this local PubSub host to advertise S2S connections. "
|
||||
"This must be a host local to this service handled by _`mod_pubsub`_. "
|
||||
"This option is only needed if your configuration has more than one host in mod_pubsub's 'hosts' option. "
|
||||
"The default value is the first host defined in mod_pubsub 'hosts' option.")}}],
|
||||
example =>
|
||||
["modules:", " mod_pubsub_serverinfo:", " pubsub_host: custom.pubsub.domain.local"]}.
|
||||
|
||||
in_auth_result(#{server_host := Host, remote_server := RServer} = State, true, _Server) ->
|
||||
gen_server:cast(
|
||||
gen_mod:get_module_proc(Host, ?MODULE), {register_in, RServer, self()}),
|
||||
State;
|
||||
in_auth_result(State, _, _) ->
|
||||
State.
|
||||
|
||||
out_auth_result(#{server_host := Host, remote_server := RServer} = State, true) ->
|
||||
gen_server:cast(
|
||||
gen_mod:get_module_proc(Host, ?MODULE), {register_out, RServer, self()}),
|
||||
State;
|
||||
out_auth_result(State, _) ->
|
||||
State.
|
||||
|
||||
check_if_public(Domain, State) ->
|
||||
maybe_send_disco_info(is_public(Domain, State) orelse is_monitored(Domain, State),
|
||||
Domain,
|
||||
State).
|
||||
|
||||
is_public(Domain, #state{public_hosts = PublicHosts}) ->
|
||||
lists:member(Domain, PublicHosts).
|
||||
|
||||
is_monitored(Domain, #state{host = Host, monitors = Mons}) ->
|
||||
maps:size(
|
||||
maps:filter(fun (_Ref, {_Dir, {LServer, RServer, IsPublic}})
|
||||
when LServer == Host, RServer == Domain ->
|
||||
IsPublic;
|
||||
(_Ref, _Other) ->
|
||||
false
|
||||
end,
|
||||
Mons))
|
||||
=/= 0.
|
||||
|
||||
maybe_send_disco_info(true, _Domain, _State) ->
|
||||
true;
|
||||
maybe_send_disco_info(false, Domain, #state{host = Host}) ->
|
||||
Proc = gen_mod:get_module_proc(Host, ?MODULE),
|
||||
IQ = #iq{type = get,
|
||||
from = jid:make(Host),
|
||||
to = jid:make(Domain),
|
||||
sub_els = [#disco_info{}]},
|
||||
ejabberd_router:route_iq(IQ, {Host, Domain}, Proc),
|
||||
false.
|
||||
|
||||
update_pubsub(#state{host = Host,
|
||||
pubsub_host = PubsubHost,
|
||||
node = Node,
|
||||
monitors = Mons}) ->
|
||||
Map = maps:fold(fun(_, {Dir, {MyDomain, Target, IsPublic}}, Acc) ->
|
||||
maps:update_with(MyDomain,
|
||||
fun(Acc2) ->
|
||||
maps:update_with(Target,
|
||||
fun({Types, _}) ->
|
||||
{Types#{Dir => true}, IsPublic}
|
||||
end,
|
||||
{#{Dir => true}, IsPublic},
|
||||
Acc2)
|
||||
end,
|
||||
#{Target => {#{Dir => true}, IsPublic}},
|
||||
Acc)
|
||||
end,
|
||||
#{},
|
||||
Mons),
|
||||
Domains =
|
||||
maps:fold(fun(MyDomain, Targets, Acc) ->
|
||||
Remote =
|
||||
maps:fold(fun (Remote, {Types, true}, Acc2) ->
|
||||
[#pubsub_serverinfo_remote_domain{name = Remote,
|
||||
type =
|
||||
maps:keys(Types)}
|
||||
| Acc2];
|
||||
(_HiddenRemote, {Types, false}, Acc2) ->
|
||||
[#pubsub_serverinfo_remote_domain{type =
|
||||
maps:keys(Types)}
|
||||
| Acc2]
|
||||
end,
|
||||
[],
|
||||
Targets),
|
||||
[#pubsub_serverinfo_domain{name = MyDomain, remote_domain = Remote} | Acc]
|
||||
end,
|
||||
[],
|
||||
Map),
|
||||
|
||||
PubOpts = [{persist_items, true}, {max_items, 1}, {access_model, open}],
|
||||
?DEBUG("Publishing serverinfo pubsub item on ~s: ~p", [PubsubHost, Domains]),
|
||||
mod_pubsub:publish_item(PubsubHost,
|
||||
Host,
|
||||
Node,
|
||||
jid:make(Host),
|
||||
<<"current">>,
|
||||
[xmpp:encode(#pubsub_serverinfo{domain = Domains})],
|
||||
PubOpts,
|
||||
all).
|
||||
|
||||
get_local_features({error, _} = Acc, _From, _To, _Node, _Lang) ->
|
||||
Acc;
|
||||
get_local_features(Acc, _From, _To, Node, _Lang) when Node == <<>> ->
|
||||
case Acc of
|
||||
{result, Features} ->
|
||||
{result, [?NS_URN_SERVERINFO | Features]};
|
||||
empty ->
|
||||
{result, [?NS_URN_SERVERINFO]}
|
||||
end;
|
||||
get_local_features(Acc, _From, _To, _Node, _Lang) ->
|
||||
Acc.
|
||||
|
||||
get_info(Acc, Host, Mod, Node, Lang)
|
||||
when Mod == undefined orelse Mod == mod_disco, Node == <<"">> ->
|
||||
case mod_disco:get_info(Acc, Host, Mod, Node, Lang) of
|
||||
[#xdata{fields = Fields} = XD | Rest] ->
|
||||
PubsubHost = pubsub_host(Host),
|
||||
NodeField =
|
||||
#xdata_field{var = <<"serverinfo-pubsub-node">>,
|
||||
values = [<<"xmpp:", PubsubHost/binary, "?;node=serverinfo">>]},
|
||||
{stop, [XD#xdata{fields = Fields ++ [NodeField]} | Rest]};
|
||||
_ ->
|
||||
Acc
|
||||
end;
|
||||
get_info(Acc, Host, Mod, Node, _Lang) when Node == <<"">>, is_atom(Mod) ->
|
||||
PubsubHost = pubsub_host(Host),
|
||||
[#xdata{type = result,
|
||||
fields =
|
||||
[#xdata_field{type = hidden,
|
||||
var = <<"FORM_TYPE">>,
|
||||
values = [?NS_SERVERINFO]},
|
||||
#xdata_field{var = <<"serverinfo-pubsub-node">>,
|
||||
values = [<<"xmpp:", PubsubHost/binary, "?;node=serverinfo">>]}]}
|
||||
| Acc];
|
||||
get_info(Acc, _Host, _Mod, _Node, _Lang) ->
|
||||
Acc.
|
||||
|
||||
pubsub_host(Host) ->
|
||||
{ok, PubsubHost} =
|
||||
gen_server:call(
|
||||
gen_mod:get_module_proc(Host, ?MODULE), pubsub_host),
|
||||
PubsubHost.
|
||||
|
||||
pubsub_host(Host, Opts) ->
|
||||
case gen_mod:get_opt(pubsub_host, Opts) of
|
||||
undefined ->
|
||||
PubsubHost = hd(get_mod_pubsub_hosts(Host)),
|
||||
?INFO_MSG("No pubsub_host in configuration for ~p, choosing ~s", [?MODULE, PubsubHost]),
|
||||
PubsubHost;
|
||||
PubsubHost ->
|
||||
case check_pubsub_host_exists(Host, PubsubHost) of
|
||||
true ->
|
||||
PubsubHost;
|
||||
false ->
|
||||
{error, {pubsub_host_does_not_exist, PubsubHost}}
|
||||
end
|
||||
end.
|
||||
|
||||
check_pubsub_host_exists(Host, PubsubHost) ->
|
||||
lists:member(PubsubHost, get_mod_pubsub_hosts(Host)).
|
||||
|
||||
get_mod_pubsub_hosts(Host) ->
|
||||
case gen_mod:get_module_opt(Host, mod_pubsub, hosts) of
|
||||
[] ->
|
||||
[gen_mod:get_module_opt(Host, mod_pubsub, host)];
|
||||
PubsubHosts ->
|
||||
PubsubHosts
|
||||
end.
|
||||
@@ -0,0 +1,13 @@
|
||||
%% Generated automatically
|
||||
%% DO NOT EDIT: run `make options` instead
|
||||
|
||||
-module(mod_pubsub_serverinfo_opt).
|
||||
|
||||
-export([pubsub_host/1]).
|
||||
|
||||
-spec pubsub_host(gen_mod:opts() | global | binary()) -> 'undefined' | binary().
|
||||
pubsub_host(Opts) when is_map(Opts) ->
|
||||
gen_mod:get_opt(pubsub_host, Opts);
|
||||
pubsub_host(Host) ->
|
||||
gen_mod:get_module_opt(Host, mod_pubsub_serverinfo, pubsub_host).
|
||||
|
||||
+27
-26
@@ -87,7 +87,7 @@ c2s_unauthenticated_packet(#{ip := IP, server := Server} = State,
|
||||
catch _:{xmpp_codec, Why} ->
|
||||
Txt = xmpp:io_format_error(Why),
|
||||
Lang = maps:get(lang, State),
|
||||
Err = xmpp:make_error(IQ, xmpp:err_bad_request(Txt, Lang)),
|
||||
Err = make_stripped_error(IQ, xmpp:err_bad_request(Txt, Lang)),
|
||||
{stop, ejabberd_c2s:send(State, Err)}
|
||||
end;
|
||||
c2s_unauthenticated_packet(State, _) ->
|
||||
@@ -116,7 +116,7 @@ process_iq(#iq{type = set, lang = Lang,
|
||||
sub_els = [#register{remove = true}]} = IQ,
|
||||
_Source, _IsCaptchaEnabled, _AllowRemove = false) ->
|
||||
Txt = ?T("Access denied by service policy"),
|
||||
xmpp:make_error(IQ, xmpp:err_forbidden(Txt, Lang));
|
||||
make_stripped_error(IQ, xmpp:err_forbidden(Txt, Lang));
|
||||
process_iq(#iq{type = set, lang = Lang, to = To, from = From,
|
||||
sub_els = [#register{remove = true,
|
||||
username = User,
|
||||
@@ -141,12 +141,12 @@ process_iq(#iq{type = set, lang = Lang, to = To, from = From,
|
||||
ignore;
|
||||
false ->
|
||||
Txt = ?T("Incorrect password"),
|
||||
xmpp:make_error(
|
||||
make_stripped_error(
|
||||
IQ, xmpp:err_forbidden(Txt, Lang))
|
||||
end;
|
||||
true ->
|
||||
Txt = ?T("No 'password' found in this query"),
|
||||
xmpp:make_error(IQ, xmpp:err_bad_request(Txt, Lang))
|
||||
make_stripped_error(IQ, xmpp:err_bad_request(Txt, Lang))
|
||||
end
|
||||
end;
|
||||
true ->
|
||||
@@ -158,7 +158,7 @@ process_iq(#iq{type = set, lang = Lang, to = To, from = From,
|
||||
ignore;
|
||||
_ ->
|
||||
Txt = ?T("The query is only allowed from local users"),
|
||||
xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang))
|
||||
make_stripped_error(IQ, xmpp:err_not_allowed(Txt, Lang))
|
||||
end
|
||||
end;
|
||||
process_iq(#iq{type = set, to = To,
|
||||
@@ -186,17 +186,17 @@ process_iq(#iq{type = set, to = To,
|
||||
User, Server, Password, IQ, Source, true);
|
||||
_ ->
|
||||
Txt = ?T("Incorrect data form"),
|
||||
xmpp:make_error(IQ, xmpp:err_bad_request(Txt, Lang))
|
||||
make_stripped_error(IQ, xmpp:err_bad_request(Txt, Lang))
|
||||
end;
|
||||
{error, malformed} ->
|
||||
Txt = ?T("Incorrect CAPTCHA submit"),
|
||||
xmpp:make_error(IQ, xmpp:err_bad_request(Txt, Lang));
|
||||
make_stripped_error(IQ, xmpp:err_bad_request(Txt, Lang));
|
||||
_ ->
|
||||
ErrText = ?T("The CAPTCHA verification has failed"),
|
||||
xmpp:make_error(IQ, xmpp:err_not_allowed(ErrText, Lang))
|
||||
make_stripped_error(IQ, xmpp:err_not_allowed(ErrText, Lang))
|
||||
end;
|
||||
process_iq(#iq{type = set} = IQ, _Source, _IsCaptchaEnabled, _AllowRemove) ->
|
||||
xmpp:make_error(IQ, xmpp:err_bad_request());
|
||||
make_stripped_error(IQ, xmpp:err_bad_request());
|
||||
process_iq(#iq{type = get, from = From, to = To, id = ID, lang = Lang} = IQ,
|
||||
Source, IsCaptchaEnabled, _AllowRemove) ->
|
||||
Server = To#jid.lserver,
|
||||
@@ -248,11 +248,11 @@ process_iq(#iq{type = get, from = From, to = To, id = ID, lang = Lang} = IQ,
|
||||
sub_els = [Xdata | CaptchaEls2]});
|
||||
{error, limit} ->
|
||||
ErrText = ?T("Too many CAPTCHA requests"),
|
||||
xmpp:make_error(
|
||||
make_stripped_error(
|
||||
IQ, xmpp:err_resource_constraint(ErrText, Lang));
|
||||
_Err ->
|
||||
ErrText = ?T("Unable to generate a CAPTCHA"),
|
||||
xmpp:make_error(
|
||||
make_stripped_error(
|
||||
IQ, xmpp:err_internal_server_error(ErrText, Lang))
|
||||
end;
|
||||
true ->
|
||||
@@ -267,8 +267,11 @@ process_iq(#iq{type = get, from = From, to = To, id = ID, lang = Lang} = IQ,
|
||||
try_register_or_set_password(User, Server, Password,
|
||||
#iq{from = From, lang = Lang} = IQ,
|
||||
Source, CaptchaSucceed) ->
|
||||
case From of
|
||||
#jid{user = User, lserver = Server} ->
|
||||
case {jid:nodeprep(User), From} of
|
||||
{error, _} ->
|
||||
Err = xmpp:err_jid_malformed(format_error(invalid_jid), Lang),
|
||||
make_stripped_error(IQ, Err);
|
||||
{UserP, #jid{user = User2, lserver = Server}} when UserP == User2 ->
|
||||
try_set_password(User, Server, Password, IQ);
|
||||
_ when CaptchaSucceed ->
|
||||
case check_from(From, Server) of
|
||||
@@ -277,14 +280,14 @@ try_register_or_set_password(User, Server, Password,
|
||||
ok ->
|
||||
xmpp:make_iq_result(IQ);
|
||||
{error, Error} ->
|
||||
xmpp:make_error(IQ, Error)
|
||||
make_stripped_error(IQ, Error)
|
||||
end;
|
||||
deny ->
|
||||
Txt = ?T("Access denied by service policy"),
|
||||
xmpp:make_error(IQ, xmpp:err_forbidden(Txt, Lang))
|
||||
make_stripped_error(IQ, xmpp:err_forbidden(Txt, Lang))
|
||||
end;
|
||||
_ ->
|
||||
xmpp:make_error(IQ, xmpp:err_not_allowed())
|
||||
make_stripped_error(IQ, xmpp:err_not_allowed())
|
||||
end.
|
||||
|
||||
try_set_password(User, Server, Password) ->
|
||||
@@ -307,15 +310,15 @@ try_set_password(User, Server, Password, #iq{lang = Lang, meta = M} = IQ) ->
|
||||
xmpp:make_iq_result(IQ);
|
||||
{error, not_allowed} ->
|
||||
Txt = ?T("Changing password is not allowed"),
|
||||
xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang));
|
||||
make_stripped_error(IQ, xmpp:err_not_allowed(Txt, Lang));
|
||||
{error, invalid_jid = Why} ->
|
||||
xmpp:make_error(IQ, xmpp:err_jid_malformed(format_error(Why), Lang));
|
||||
make_stripped_error(IQ, xmpp:err_jid_malformed(format_error(Why), Lang));
|
||||
{error, invalid_password = Why} ->
|
||||
xmpp:make_error(IQ, xmpp:err_not_allowed(format_error(Why), Lang));
|
||||
make_stripped_error(IQ, xmpp:err_not_allowed(format_error(Why), Lang));
|
||||
{error, weak_password = Why} ->
|
||||
xmpp:make_error(IQ, xmpp:err_not_acceptable(format_error(Why), Lang));
|
||||
make_stripped_error(IQ, xmpp:err_not_acceptable(format_error(Why), Lang));
|
||||
{error, db_failure = Why} ->
|
||||
xmpp:make_error(IQ, xmpp:err_internal_server_error(format_error(Why), Lang))
|
||||
make_stripped_error(IQ, xmpp:err_internal_server_error(format_error(Why), Lang))
|
||||
end.
|
||||
|
||||
try_register(User, Server, Password, SourceRaw, Module) ->
|
||||
@@ -417,11 +420,6 @@ send_welcome_message(JID) ->
|
||||
to = JID,
|
||||
type = chat,
|
||||
subject = xmpp:mk_text(Subj),
|
||||
body = xmpp:mk_text(<<Subj/binary, "\n\n", Body/binary>>)}),
|
||||
ejabberd_router:route(
|
||||
#message{from = jid:make(Host),
|
||||
to = JID,
|
||||
subject = xmpp:mk_text(Subj),
|
||||
body = xmpp:mk_text(Body)})
|
||||
end.
|
||||
|
||||
@@ -562,6 +560,9 @@ is_strong_password2(Server, Password) ->
|
||||
ejabberd_auth:entropy(Password) >= Entropy
|
||||
end.
|
||||
|
||||
make_stripped_error(IQ, Err) ->
|
||||
xmpp:make_error(xmpp:remove_subtag(IQ, #register{}), Err).
|
||||
|
||||
%%%
|
||||
%%% ip_access management
|
||||
%%%
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user