diff --git a/.github/container/Dockerfile b/.github/container/Dockerfile index 2a44b9035..ae2cc7311 100644 --- a/.github/container/Dockerfile +++ b/.github/container/Dockerfile @@ -1,20 +1,22 @@ #' Define default build variables -## specifc ARGs for METHOD='direct' ARG OTP_VSN='27.2' ARG ELIXIR_VSN='1.18.1' -## specifc ARGs for METHOD='package' -ARG ALPINE_VSN='3.19' -## general ARGs ARG UID='9000' ARG USER='ejabberd' ARG HOME="opt/$USER" -ARG METHOD='direct' ARG BUILD_DIR="/$USER" ARG VERSION='master' ################################################################################ -#' METHOD='direct' - build and install ejabberd directly from source -FROM docker.io/erlang:${OTP_VSN}-alpine AS direct +#' Compile ejabberdapi +FROM docker.io/golang:1.23-alpine AS api +RUN go install -v \ + github.com/processone/ejabberd-api/cmd/ejabberd@master \ + && mv bin/ejabberd bin/ejabberdapi + +################################################################################ +#' build and install ejabberd directly from source +FROM docker.io/erlang:${OTP_VSN}-alpine AS ejabberd RUN apk -U add --no-cache \ autoconf \ @@ -39,7 +41,8 @@ ARG ELIXIR_VSN RUN wget -O - https://github.com/elixir-lang/elixir/archive/v$ELIXIR_VSN.tar.gz \ | tar -xzf - -WORKDIR elixir-$ELIXIR_VSN +WORKDIR /elixir-$ELIXIR_VSN +ENV ERL_FLAGS="+JPperf true" RUN make install clean RUN mix local.hex --force \ @@ -50,6 +53,7 @@ COPY / $BUILD_DIR/ WORKDIR $BUILD_DIR RUN mv .github/container/ejabberdctl.template . \ + && mv .github/container/ejabberd.yml.example . \ && ./autogen.sh \ && ./configure --with-rebar=mix --enable-all \ && make deps \ @@ -67,36 +71,18 @@ RUN cp -p $BUILD_DIR/tools/captcha*.sh $HOME-$VERSION/lib RUN find "$HOME-$VERSION/bin" -name 'ejabberd' -delete \ && find "$HOME-$VERSION/releases" -name 'COOKIE' -delete -RUN wget -O "$HOME/conf/cacert.pem" 'https://curl.se/ca/cacert.pem' \ - && sed -i '/^loglevel:/a \ \ - \nca_file: /opt/ejabberd/conf/cacert.pem \ - \ncertfiles: \ - \n - /opt/ejabberd/conf/server.pem' "$HOME/conf/ejabberd.yml" +RUN wget -O "$HOME/conf/cacert.pem" 'https://curl.se/ca/cacert.pem' -################################################################################ -#' METHOD='package' - install ejabberd from binary tarball package -FROM docker.io/alpine:${ALPINE_VSN} AS package -COPY tarballs/ejabberd-*-linux-musl-*.tar.gz /tmp/ -WORKDIR /rootfs -ARG HOME -RUN home_root_dir=$(echo $HOME | sed 's|\(.*\)/.*|\1 |') \ - && mkdir -p $home_root_dir \ - && ARCH=$(uname -m | sed -e 's/x86_64/x64/;s/aarch64/arm64/') \ - && tar -xzf /tmp/ejabberd-*-linux-musl-$ARCH.tar.gz -C $home_root_dir - -################################################################################ #' Prepare ejabberd for runtime -FROM ${METHOD} AS ejabberd RUN apk -U add --no-cache \ git \ libcap \ openssl -WORKDIR /rootfs -ARG HOME RUN mkdir -p usr/local/bin $HOME/conf $HOME/database $HOME/logs $HOME/upload -ARG BUILD_DIR +COPY --from=api /go/bin/ejabberdapi usr/local/bin/ + RUN if [ ! -d $HOME/.ejabberd-modules ]; \ then \ if [ -d $BUILD_DIR/.ejabberd-modules ]; \ @@ -116,11 +102,35 @@ RUN export PEM=$HOME/conf/server.pem \ -days 3650 \ -subj "/CN=localhost" +RUN sed -i 's|^#CTL_OVER_HTTP=|CTL_OVER_HTTP=../|' "$HOME/conf/ejabberdctl.cfg" + RUN home_root_dir=$(echo $HOME | sed 's|\(.*\)/.*|\1 |') \ && setcap 'cap_net_bind_service=+ep' $(find $home_root_dir -name beam.smp) \ && echo -e \ "#!/bin/sh \ \n[ -z \$ERLANG_NODE_ARG ] && export ERLANG_NODE_ARG=ejabberd@localhost \ + \nexport EMA=\"\$EJABBERD_MACRO_ADMIN\" \ + \nexport HOST=\"\${EJABBERD_MACRO_HOST:-localhost}\" \ + \nif [ -n \"\$EMA\" ] \ + \nthen \ + \n if [ \"\$EMA\" != \"\${EMA%%@*}\" ] \ + \n then \ + \n export USERNAME=\"\${EMA%%@*}\" \ + \n export HOST=\"\${EMA##*@}\" \ + \n else \ + \n export USERNAME=\"\$EMA\" \ + \n export SHOW_WARNING=\"true\" \ + \n fi \ + \nelif [ -n \"\$REGISTER_ADMIN_PASSWORD\" ] \ + \nthen \ + \n export USERNAME=\"admin\" \ + \nelse \ + \n export USERNAME=\"\$(od -A n -N 8 -t x8 /dev/urandom)\" \ + \nfi \ + \nexport EJABBERD_MACRO_ADMIN=\"\$USERNAME@\$HOST\" \ + \n[ -n \"\$SHOW_WARNING\" ] && echo \"WARNING: The EJABBERD_MACRO_ADMIN environment variable was set to '\$EMA', but it should include the host... I'll overwrite it to become '\$EJABBERD_MACRO_ADMIN'.\" \ + \n[ -n \"\$CTL_ON_CREATE\" ] && export SEPARATOR=\";\" \ + \n[ -n \"\$REGISTER_ADMIN_PASSWORD\" ] && export CTL_ON_CREATE=\"register \${EJABBERD_MACRO_ADMIN%%@*} \${EJABBERD_MACRO_ADMIN##*@} \$REGISTER_ADMIN_PASSWORD \$SEPARATOR \$CTL_ON_CREATE\" \ \nexport CONFIG_DIR=/$HOME/conf \ \nexport LOGS_DIR=/$HOME/logs \ \nexport SPOOL_DIR=/$HOME/database \ @@ -137,28 +147,28 @@ RUN home_root_dir=$(echo $HOME | sed 's|\(.*\)/.*|\1 |') \ ARG UID RUN chown -R $UID:$UID $HOME +RUN cp /rootfs/$HOME-$VERSION/lib/captcha*.sh usr/local/bin/ +RUN mkdir $HOME/sql \ + && find /rootfs/$HOME-$VERSION/lib/ -name *.sql -exec cp {} $HOME/sql \; -exec cp {} $HOME/database \; + ################################################################################ -#' METHOD='direct' - Remove erlang/OTP & rebar3 -FROM docker.io/erlang:${OTP_VSN}-alpine AS runtime-direct +#' Remove erlang/OTP & rebar3 +FROM docker.io/erlang:${OTP_VSN}-alpine AS runtime RUN apk del .erlang-rundeps \ && rm -f $(which rebar3) \ && find /usr -type d -name 'erlang' -exec rm -rf {} + \ && find /usr -type l -exec test ! -e {} \; -delete -################################################################################ -#' METHOD='package' - define runtime base image -FROM docker.io/alpine:${ALPINE_VSN} AS runtime-package - -################################################################################ #' Update alpine, finalize runtime environment -FROM runtime-${METHOD} AS runtime COPY --from=ejabberd /tmp/runDeps /tmp/runDeps RUN apk -U upgrade --available --no-cache \ && apk add --no-cache \ $(cat /tmp/runDeps) \ so:libcap.so.2 \ so:libtdsodbc.so.0 \ + curl \ tini \ + && rm /tmp/runDeps \ && ln -fs /usr/lib/libtdsodbc.so.0 /usr/lib/libtdsodbc.so ARG USER @@ -167,9 +177,13 @@ ARG HOME RUN addgroup $USER -g $UID \ && adduser -s /sbin/nologin -D -u $UID -h /$HOME -G $USER $USER +RUN ln -fs /usr/local/bin/ /opt/ejabberd/bin +RUN rm -rf /home \ + && ln -fs /opt /home + ################################################################################ #' Build together production image -FROM scratch AS prod +FROM scratch ARG USER ARG HOME @@ -186,7 +200,7 @@ HEALTHCHECK \ WORKDIR /$HOME USER $USER VOLUME ["/$HOME"] -EXPOSE 1883 4369-4399 5210 5222 5269 5280 5443 +EXPOSE 1880 1883 4369-4399 5210 5222 5269 5280 5443 ENTRYPOINT ["/sbin/tini","--","ejabberdctl"] CMD ["foreground"] diff --git a/.github/container/ejabberd.yml.example b/.github/container/ejabberd.yml.example new file mode 100644 index 000000000..62dff50c9 --- /dev/null +++ b/.github/container/ejabberd.yml.example @@ -0,0 +1,278 @@ +### +### ejabberd configuration file +### +### The parameters used in this configuration file are explained at +### +### https://docs.ejabberd.im/admin/configuration +### +### The configuration file is written in YAML. +### ******************************************************* +### ******* !!! WARNING !!! ******* +### ******* YAML IS INDENTATION SENSITIVE ******* +### ******* MAKE SURE YOU INDENT SECTIONS CORRECTLY ******* +### ******************************************************* +### Refer to http://en.wikipedia.org/wiki/YAML for the brief description. +### + +define_macro: + HOST: localhost + ## ADMIN: ... # set by /usr/local/bin/ejabberdctl + PORT_C2S: 5222 + PORT_C2S_TLS: 5223 + PORT_S2S: 5269 + PORT_HTTP_TLS: 5443 + PORT_HTTP: 5280 + PORT_BROWSER: 1880 + PORT_STUN: 5478 + PORT_MQTT: 1883 + PORT_PROXY65: 7777 + +hosts: + - HOST + +loglevel: info + +## If you already have certificates, list them here +# certfiles: +# - /etc/letsencrypt/live/domain.tld/fullchain.pem +# - /etc/letsencrypt/live/domain.tld/privkey.pem + +ca_file: /opt/ejabberd/conf/cacert.pem +certfiles: + - /opt/ejabberd/conf/server.pem + +listen: + - + port: PORT_C2S + ip: "::" + module: ejabberd_c2s + max_stanza_size: 262144 + shaper: c2s_shaper + access: c2s + starttls_required: true + - + port: PORT_C2S_TLS + ip: "::" + module: ejabberd_c2s + max_stanza_size: 262144 + shaper: c2s_shaper + access: c2s + tls: true + - + port: PORT_S2S + ip: "::" + module: ejabberd_s2s_in + max_stanza_size: 524288 + shaper: s2s_shaper + - + port: PORT_HTTP_TLS + ip: "::" + module: ejabberd_http + tls: true + request_handlers: + /admin: ejabberd_web_admin + /api: mod_http_api + /bosh: mod_bosh + /captcha: ejabberd_captcha + /upload: mod_http_upload + /ws: ejabberd_http_ws + - + port: PORT_HTTP + ip: "::" + module: ejabberd_http + request_handlers: + /admin: ejabberd_web_admin + /.well-known/acme-challenge: ejabberd_acme + - + port: PORT_BROWSER + ip: "::" + module: ejabberd_http + request_handlers: + /: ejabberd_web_admin + - + port: "unix:../sockets/ctl_over_http.sock" + module: ejabberd_http + unix_socket: + mode: '0600' + request_handlers: + /ctl: ejabberd_ctl + - + port: PORT_STUN + ip: "::" + transport: udp + module: ejabberd_stun + use_turn: true + ## The server's public IPv4 address: + # turn_ipv4_address: "203.0.113.3" + ## The server's public IPv6 address: + # turn_ipv6_address: "2001:db8::3" + - + port: PORT_MQTT + ip: "::" + module: mod_mqtt + backlog: 1000 + +s2s_use_starttls: optional + +acl: + local: + user_regexp: "" + loopback: + ip: + - 127.0.0.0/8 + - ::1/128 + admin: + user: + - ADMIN + +access_rules: + local: + allow: local + c2s: + deny: blocked + allow: all + announce: + allow: admin + configure: + allow: admin + muc_create: + allow: local + pubsub_createnode: + allow: local + trusted_network: + allow: loopback + +api_permissions: + "console commands": + from: ejabberd_ctl + who: all + what: "*" + "webadmin commands": + from: ejabberd_web_admin + who: admin + what: "*" + "admin access": + who: + access: + allow: + - acl: loopback + - acl: admin + oauth: + scope: "ejabberd:admin" + access: + allow: + - acl: loopback + - acl: admin + what: + - "*" + - "!stop" + - "!start" + "public commands": + who: + ip: 127.0.0.1/8 + what: + - status + - connected_users_number + +shaper: + normal: + rate: 3000 + burst_size: 20000 + fast: 100000 + +shaper_rules: + max_user_sessions: 10 + max_user_offline_messages: + 5000: admin + 100: all + c2s_shaper: + none: admin + normal: all + s2s_shaper: fast + +modules: + mod_adhoc: {} + mod_admin_extra: {} + mod_announce: + access: announce + mod_avatar: {} + mod_blocking: {} + mod_bosh: {} + mod_caps: {} + mod_carboncopy: {} + mod_client_state: {} + mod_configure: {} + mod_disco: {} + mod_fail2ban: {} + mod_http_api: {} + mod_http_upload: + put_url: https://@HOST@:5443/upload + custom_headers: + "Access-Control-Allow-Origin": "https://@HOST@" + "Access-Control-Allow-Methods": "GET,HEAD,PUT,OPTIONS" + "Access-Control-Allow-Headers": "Content-Type" + mod_last: {} + mod_mam: + ## Mnesia is limited to 2GB, better to use an SQL backend + ## For small servers SQLite is a good fit and is very easy + ## to configure. Uncomment this when you have SQL configured: + ## db_type: sql + assume_mam_usage: true + default: always + mod_mqtt: {} + mod_muc: + access: + - allow + access_admin: + - allow: admin + access_create: muc_create + access_persistent: muc_create + access_mam: + - allow + default_room_options: + mam: true + mod_muc_admin: {} + mod_offline: + access_max_user_messages: max_user_offline_messages + mod_ping: {} + mod_privacy: {} + mod_private: {} + mod_proxy65: + access: local + max_connections: 5 + port: PORT_PROXY65 + mod_pubsub: + access_createnode: pubsub_createnode + plugins: + - flat + - pep + force_node_config: + ## Avoid buggy clients to make their bookmarks public + storage:bookmarks: + access_model: whitelist + mod_push: {} + mod_push_keepalive: {} + mod_register: + ## Only accept registration requests from the "trusted" + ## network (see access_rules section above). + ## Think twice before enabling registration from any + ## address. See the Jabber SPAM Manifesto for details: + ## https://github.com/ge0rg/jabber-spam-fighting-manifesto + ip_access: trusted_network + mod_roster: + versioning: true + mod_s2s_bidi: {} + mod_s2s_dialback: {} + mod_shared_roster: {} + mod_stream_mgmt: + resend_on_timeout: if_offline + mod_stun_disco: {} + mod_vcard: {} + mod_vcard_xupdate: {} + mod_version: + show_os: false + +### Local Variables: +### mode: yaml +### End: +### vim: set filetype=yaml tabstop=8 diff --git a/.github/container/ejabberdctl.template b/.github/container/ejabberdctl.template index a27a14587..2353040d5 100755 --- a/.github/container/ejabberdctl.template +++ b/.github/container/ejabberdctl.template @@ -305,6 +305,8 @@ stop_epmd() # if all ok, ensure runtime directory exists and make it current directory check_start() { + ECSIMAGE_DBPATH=$HOME/database/$ERLANG_NODE + [ ! -d "$ECSIMAGE_DBPATH" ] && ln -s $HOME/database $HOME/database/$ERLANG_NODE [ -n "$ERL_DIST_PORT" ] && return "$EPMD" -names 2>/dev/null | grep -q " ${ERLANG_NODE%@*} " && { pgrep -f "$ERLANG_NODE" >/dev/null && { @@ -377,6 +379,54 @@ wait_status() [ $timeout -gt 0 ] } +exec_other_command() +{ + 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'"')' \ + -s ejabberd_ctl \ + -extra "$ERLANG_NODE" $NO_TIMEOUT "$@" + result=$? + case $result in + 3) help;; + *) :;; + esac + exit $result + else + exec_ctl_over_http_socket "$@" + fi +} + +exec_ctl_over_http_socket() +{ + COMMAND=${1} + CARGS="" + while [ $# -gt 0 ]; do + [ -z "$CARGS" ] && CARGS="[" || CARGS="${CARGS}, " + CARGS="${CARGS}\"$1\"" + shift + done + CARGS="${CARGS}]" + TEMPHEADERS=temp-headers.log + curl \ + --unix-socket ${CTL_OVER_HTTP} \ + --header "Content-Type: application/json" \ + --header "Accept: application/json" \ + --data "${CARGS}" \ + --dump-header ${TEMPHEADERS} \ + --no-progress-meter \ + "http://localhost/ctl/${COMMAND}" + result=$(sed -n 's/.*status-code: \([0-9]*\).*/\1/p' < $TEMPHEADERS) + rm ${TEMPHEADERS} + case $result in + 2|3) exec_other_command help ${COMMAND};; + *) :;; + esac + exit $result +} + # ensure we can change current directory to SPOOL_DIR [ -f "$SPOOL_DIR/schema.DAT" ] || FIRST_RUN=true [ -d "$SPOOL_DIR" ] || run_cmd mkdir -p "$SPOOL_DIR" @@ -452,15 +502,6 @@ case $1 in ;; *) set_dist_client - run_erl "$(uid ctl)" -hidden -noinput \ - -eval 'net_kernel:connect_node('"'$ERLANG_NODE'"')' \ - -s ejabberd_ctl \ - -extra "$ERLANG_NODE" $NO_TIMEOUT "$@" - result=$? - case $result in - 2|3) help;; - *) :;; - esac - exit $result + exec_other_command "$@" ;; esac diff --git a/.github/workflows/container.yml b/.github/workflows/container.yml index 7940c9b68..567a2c845 100644 --- a/.github/workflows/container.yml +++ b/.github/workflows/container.yml @@ -1,8 +1,6 @@ name: Container on: - schedule: - - cron: '22 2 */6 * *' # every 6 days to avoid gha cache being evicted push: paths-ignore: - '.devcontainer/**' @@ -28,52 +26,6 @@ jobs: with: fetch-depth: 0 - - name: Cache build directory - uses: actions/cache@v4 - with: - path: ~/build/ - key: ${{runner.os}}-ctr-ct-ng-1.27.0 - - - name: Get erlang/OTP version for bootstrapping - run: | - echo "OTP_VSN=$(awk '/^otp_vsn=/ {{gsub(/[^0-9.rc-]/, ""); print}}' tools/make-binaries)" >> $GITHUB_ENV - echo "ELIXIR_VSN=$(awk '/^elixir_vsn=/ {{gsub(/[^0-9.]/, ""); print}}' tools/make-binaries)" >> $GITHUB_ENV - - - name: Install prerequisites - run: | - sudo apt-get -qq update - sudo apt-get -qq install makeself - # https://github.com/crosstool-ng/crosstool-ng/blob/master/testing/docker/ubuntu21.10/Dockerfile - sudo apt-get -qq install build-essential autoconf bison flex gawk - sudo apt-get -qq install help2man libncurses5-dev libtool libtool-bin - sudo apt-get -qq install python3-dev texinfo unzip - - - name: Install erlang/OTP - uses: erlef/setup-beam@v1 - with: - otp-version: ${{ env.OTP_VSN }} - elixir-version: ${{ env.ELIXIR_VSN }} - version-type: strict - - - name: Remove Elixir Matchers - run: | - echo "::remove-matcher owner=elixir-mixCompileWarning::" - echo "::remove-matcher owner=elixir-credoOutputDefault::" - echo "::remove-matcher owner=elixir-mixCompileError::" - echo "::remove-matcher owner=elixir-mixTestFailure::" - echo "::remove-matcher owner=elixir-dialyzerOutputDefault::" - - - name: Build musl-libc based binary archives - run: | - sed -i "s|targets='.*'|targets='x86_64-linux-musl aarch64-linux-musl'|" tools/make-binaries - mv .github/container/ejabberdctl.template . - CHECK_DEPS=false tools/make-binaries - - - name: Collect packages - run: | - mkdir tarballs - mv ejabberd-*.tar.gz tarballs - - name: Checkout ejabberd-contrib uses: actions/checkout@v4 with: @@ -111,7 +63,6 @@ jobs: uses: docker/build-push-action@v6 with: build-args: | - METHOD=package VERSION=${{ steps.gitdescribe.outputs.ver }} cache-from: type=gha cache-to: type=gha,mode=max diff --git a/CONTAINER.md b/CONTAINER.md index 95115a6c2..b8a483e07 100644 --- a/CONTAINER.md +++ b/CONTAINER.md @@ -1,10 +1,10 @@ [![GitHub tag (latest SemVer)](https://img.shields.io/github/v/tag/processone/ejabberd?sort=semver&logo=embarcadero&label=&color=49c0c4)](https://github.com/processone/ejabberd/tags) -[![ejabberd Container on GitHub](https://img.shields.io/github/v/tag/processone/ejabberd?label=ejabberd&sort=semver&logo=docker)](https://github.com/processone/ejabberd/pkgs/container/ejabberd) +[![ejabberd Container on GitHub](https://img.shields.io/github/v/tag/processone/ejabberd?label=ejabberd&sort=semver&logo=opencontainersinitiative&logoColor=2094f3)](https://github.com/processone/ejabberd/pkgs/container/ejabberd) [![ecs Container on Docker](https://img.shields.io/docker/v/ejabberd/ecs?label=ecs&sort=semver&logo=docker)](https://hub.docker.com/r/ejabberd/ecs/) -`ejabberd` Container Image -========================== +ejabberd Container Images +========================= [ejabberd][home] is an open-source, robust, scalable and extensible realtime platform built using [Erlang/OTP][erlang], @@ -16,26 +16,29 @@ that includes [XMPP][xmpp] Server, [MQTT][mqtt] Broker and [SIP][sip] Service. [mqtt]: https://mqtt.org/ [sip]: https://en.wikipedia.org/wiki/Session_Initiation_Protocol -This document explains how to use the `ejabberd` container image available in -[ghcr.io/processone/ejabberd](https://github.com/processone/ejabberd/pkgs/container/ejabberd), -built using the files in `.github/container/`. -This image is based in Alpine 3.19, includes Erlang/OTP 27.2 and Elixir 1.18.1. +This page documents those container images ([images comparison](#images-comparison)): -Alternatively, there is also the `ecs` container image available in -[docker.io/ejabberd/ecs](https://hub.docker.com/r/ejabberd/ecs/), -built using the -[docker-ejabberd/ecs](https://github.com/processone/docker-ejabberd/tree/master/ecs) -repository. -Check the [differences between `ejabberd` and `ecs` images](https://github.com/processone/docker-ejabberd/blob/master/ecs/HUB-README.md#alternative-image-in-github). +- [![ejabberd Container](https://img.shields.io/badge/ejabberd-grey?logo=opencontainersinitiative&logoColor=2094f3)](https://github.com/processone/ejabberd/pkgs/container/ejabberd) + published in [ghcr.io/processone/ejabberd](https://github.com/processone/ejabberd/pkgs/container/ejabberd), + built using [ejabberd](https://github.com/processone/ejabberd/tree/master/.github/container) repository, + both for stable ejabberd releases and the `master` branch, in x64 and arm64 architectures. -If you are using a Windows operating system, check the tutorials mentioned in -[ejabberd Docs > Docker Image](https://docs.ejabberd.im/admin/install/container/#ejabberd-container-image). +- [![ecs Container](https://img.shields.io/badge/ecs-grey?logo=docker&logoColor=2094f3)](https://hub.docker.com/r/ejabberd/ecs/) + published in [docker.io/ejabberd/ecs](https://hub.docker.com/r/ejabberd/ecs/), + built using [docker-ejabberd/ecs](https://github.com/processone/docker-ejabberd/tree/master/ecs) repository + for ejabberd stable releases in x64 architectures. + +For Microsoft Windows, see +[Docker Desktop for Windows 10](https://www.process-one.net/blog/install-ejabberd-on-windows-10-using-docker-desktop/), +and [Docker Toolbox for Windows 7](https://www.process-one.net/blog/install-ejabberd-on-windows-7-using-docker-toolbox/). + +For Kubernetes Helm, see [help-ejabberd](https://github.com/sando38/helm-ejabberd). Start ejabberd -------------- -### With default configuration +### daemon Start ejabberd in a new container: @@ -46,22 +49,28 @@ docker run --name ejabberd -d -p 5222:5222 ghcr.io/processone/ejabberd That runs the container as a daemon, using ejabberd default configuration file and XMPP domain `localhost`. -Stop the running container: - -```bash -docker stop ejabberd -``` - Restart the stopped ejabberd container: ```bash docker restart ejabberd ``` +Stop the running container: -### Start with Erlang console attached +```bash +docker stop ejabberd +``` -Start ejabberd with an Erlang console attached using the `live` command: +Remove the ejabberd container: + +```bash +docker rm ejabberd +``` + + +### with Erlang console + +Start ejabberd with an interactive Erlang console attached using the `live` command: ```bash docker run --name ejabberd -it -p 5222:5222 ghcr.io/processone/ejabberd live @@ -70,48 +79,67 @@ docker run --name ejabberd -it -p 5222:5222 ghcr.io/processone/ejabberd live That uses the default configuration file and XMPP domain `localhost`. -### Start with your configuration and database +### with your data Pass a configuration file as a volume and share the local directory to store database: ```bash -mkdir database -chown ejabberd database +mkdir conf && cp ejabberd.yml.example conf/ejabberd.yml -cp ejabberd.yml.example ejabberd.yml +mkdir database && chown ejabberd database docker run --name ejabberd -it \ - -v $(pwd)/ejabberd.yml:/opt/ejabberd/conf/ejabberd.yml \ + -v $(pwd)/conf/ejabberd.yml:/opt/ejabberd/conf/ejabberd.yml \ -v $(pwd)/database:/opt/ejabberd/database \ -p 5222:5222 ghcr.io/processone/ejabberd live ``` -Notice that ejabberd runs in the container with an account named `ejabberd`, +Notice that ejabberd runs in the container with an account named `ejabberd` +with UID 9000 and group `ejabberd` with GID 9000, and the volumes you mount must grant proper rights to that account. Next steps ---------- -### Register the administrator account +### Register admin account -The default ejabberd configuration does not grant admin privileges -to any account, -you may want to register a new account in ejabberd -and grant it admin rights. +#### [![ejabberd Container](https://img.shields.io/badge/ejabberd-grey?logo=opencontainersinitiative&logoColor=2094f3)](https://github.com/processone/ejabberd/pkgs/container/ejabberd) [:orange_circle:](#images-comparison) -Register an account using the `ejabberdctl` script: +If you set the `REGISTER_ADMIN_PASSWORD` environment variable, +an account is automatically registered with that password, +and admin privileges are granted to it. +The account created depends on what variables you have set: + +- `EJABBERD_MACRO_ADMIN=juliet@example.org` -> `juliet@example.org` +- `EJABBERD_MACRO_HOST=example.org` -> `admin@example.org` +- None of those variables are set -> `admin@localhost` + +The account registration is shown in the container log: + +``` +:> ejabberdctl register admin example.org somePassw0rd +User admin@example.org successfully registered +``` + +Alternatively, you can register the account manually yourself +and edit `conf/ejabberd.yml` and add the ACL as explained in +[ejabberd Docs: Administration Account](https://docs.ejabberd.im/admin/install/next-steps/#administration-account). + +--- + +#### [![ecs Container](https://img.shields.io/badge/ecs-grey?logo=docker&logoColor=2094f3)](https://hub.docker.com/r/ejabberd/ecs/) + +The default ejabberd configuration has already granted admin privilege +to an account that would be called `admin@localhost`, +so you just need to register it, for example: ```bash docker exec -it ejabberd ejabberdctl register admin localhost passw0rd ``` -Then edit conf/ejabberd.yml and add the ACL as explained in -[ejabberd Docs: Administration Account](https://docs.ejabberd.im/admin/install/next-steps/#administration-account) - - -### Check ejabberd log files +### Check ejabberd log Check the content of the log files inside the container, even if you do not put it on a shared persistent drive: @@ -121,7 +149,7 @@ docker exec -it ejabberd tail -f logs/ejabberd.log ``` -### Inspect the container files +### Inspect container files The container uses Alpine Linux. Start a shell inside the container: @@ -130,7 +158,7 @@ docker exec -it ejabberd sh ``` -### Open ejabberd debug console +### Open debug console Open an interactive debug Erlang console attached to a running ejabberd in a running container: @@ -155,7 +183,7 @@ docker exec -it ejabberd vi conf/ejabberd.yml and add this option: ```yaml -captcha_cmd: /opt/ejabberd-22.04/lib/captcha.sh +captcha_cmd: "$HOME/bin/captcha.sh" ``` Finally, reload the configuration file or restart the container: @@ -176,20 +204,22 @@ For more details about CAPTCHA options, please check the documentation section. -Advanced Container Configuration --------------------------------- +Advanced +-------- ### Ports -This container image exposes the ports: +The container image exposes several ports +(check also [Docs: Firewall Settings](https://docs.ejabberd.im/admin/guide/security/#firewall-settings)): - `5222`: The default port for XMPP clients. - `5269`: For XMPP federation. Only needed if you want to communicate with users on other servers. -- `5280`: For admin interface. +- `5280`: For admin interface (URL is `admin/`). +- `1880`: For admin interface (URL is `/`, useful for [podman-desktop](https://podman-desktop.io/) and [docker-desktop](https://www.docker.com/products/docker-desktop/)) [:orange_circle:](#images-comparison) - `5443`: With encryption, used for admin interface, API, CAPTCHA, OAuth, Websockets and XMPP BOSH. - `1883`: Used for MQTT - `4369-4399`: EPMD and Erlang connectivity, used for `ejabberdctl` and clustering -- `5210`: Erlang connectivity when `ERL_DIST_PORT` is set, alternative to EPMD +- `5210`: Erlang connectivity when `ERL_DIST_PORT` is set, alternative to EPMD [:orange_circle:](#images-comparison) ### Volumes @@ -206,11 +236,26 @@ You should back up or export the content of the directory to persistent storage - `/opt/ejabberd/logs/`: Directory containing log files - `/opt/ejabberd/upload/`: Directory containing uploaded files. This should also be backed up. -All these files are owned by `ejabberd` user inside the container. +All these files are owned by an account named `ejabberd` with group `ejabberd` in the container. +Its corresponding `UID:GID` is `9000:9000`. +If you prefer bind mounts instead of volumes, then +you need to map this to valid `UID:GID` on your host to get read/write access on +mounted directories. -It's possible to install additional ejabberd modules using volumes, -[this comment](https://github.com/processone/docker-ejabberd/issues/81#issuecomment-1036115146) -explains how to install an additional module using docker-compose. +If using Docker, try: +```bash +mkdir database +sudo chown 9000:9000 database +``` + +If using Podman, try: +```bash +mkdir database +podman unshare chown 9000:9000 database +``` + +It's possible to install additional ejabberd modules using volumes, check +[this Docs tutorial](http://docs.ejabberd.im/developer/extending-ejabberd/modules/#your-module-in-ejabberd-modules-with-ejabberd-container). ### Commands on start @@ -234,10 +279,10 @@ Example usage (or check the [full example](#customized-example)): ``` -### Macros in environment +### Macros in environment [:high_brightness:](#images-comparison) ejabberd reads `EJABBERD_MACRO_*` environment variables -and uses them to define the `*` +and uses them to define the corresponding [macros](https://docs.ejabberd.im/admin/configuration/file-format/#macros-in-configuration-file), overwriting the corresponding macro definition if it was set in the configuration file. This is supported since ejabberd 24.12. @@ -247,18 +292,61 @@ For example, if you configure this in `ejabberd.yml`: ```yaml acl: admin: - user: ADMINJID + user: ADMIN ``` now you can define the admin account JID using an environment variable: ```yaml environment: - - EJABBERD_MACRO_ADMINJID=admin@localhost + - EJABBERD_MACRO_ADMIN=admin@localhost ``` Check the [full example](#customized-example) for other example. +### ejabberdapi + +When the container is running (and thus ejabberd), you can exec commands inside the container +using `ejabberdctl` or any other of the available interfaces, see +[Understanding ejabberd "commands"](https://docs.ejabberd.im/developer/ejabberd-api/#understanding-ejabberd-commands) + +Additionally, the container image includes the `ejabberdapi` executable. +Please check the [ejabberd-api homepage](https://github.com/processone/ejabberd-api) +for configuration and usage details. + +For example, if you configure ejabberd like this: +```yaml +listen: + - + port: 5282 + module: ejabberd_http + request_handlers: + "/api": mod_http_api + +acl: + loopback: + ip: + - 127.0.0.0/8 + - ::1/128 + - ::FFFF:127.0.0.1/128 + +api_permissions: + "admin access": + who: + access: + allow: + acl: loopback + what: + - "register" +``` + +Then you could register new accounts with this query: + +```bash +docker exec -it ejabberd ejabberdapi register --endpoint=http://127.0.0.1:5282/ --jid=admin@localhost --password=passw0rd +``` + + ### Clustering When setting several containers to form a @@ -273,6 +361,8 @@ For this you can either: - edit `conf/ejabberdctl.cfg` and set variables `ERLANG_NODE` and `ERLANG_COOKIE` - set the environment variables `ERLANG_NODE_ARG` and `ERLANG_COOKIE` +--- + Example to connect a local `ejabberdctl` to a containerized ejabberd: 1. When creating the container, export port 5210, and set `ERLANG_COOKIE`: @@ -282,7 +372,7 @@ Example to connect a local `ejabberdctl` to a containerized ejabberd: -p 5210:5210 -p 5222:5222 \ ghcr.io/processone/ejabberd ``` -2. Set `ERL_DIST_PORT=5210` in ejabberdctl.cfg of container and local ejabberd +2. Set `ERL_DIST_PORT=5210` in `ejabberdctl.cfg` of container and local ejabberd 3. Restart the container 4. Now use `ejabberdctl` in your local ejabberd deployment @@ -298,19 +388,175 @@ Example using environment variables (see full example [docker-compose.yml](https - ERLANG_COOKIE=dummycookie123 ``` +--- -Build a Container Image ------------------------ +Once you have the ejabberd nodes properly set and running, +you can tell the secondary nodes to join the master node using the +[`join_cluster`](https://docs.ejabberd.im/developer/ejabberd-api/admin-api/#join-cluster) +API call. -This container image includes ejabberd as a standalone OTP release built using Elixir. -That OTP release is configured with: +Example using environment variables (see the full +[`docker-compose.yml` clustering example](#clustering-example)): +```yaml +environment: + - ERLANG_NODE_ARG=ejabberd@replica + - ERLANG_COOKIE=dummycookie123 + - CTL_ON_CREATE=join_cluster ejabberd@main +``` + +### Change Mnesia Node Name + +To use the same Mnesia database in a container with a different hostname, +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. + +#### Setup Old Container + +Let's assume a container running ejabberd 23.01 (or older) from +this ecs container image, with the database directory binded +and one registered account. +This can be produced with: +```bash +OLDCONTAINER=ejaold +NEWCONTAINER=ejanew + +mkdir database +sudo chown 9000:9000 database +docker run -d --name $OLDCONTAINER -p 5222:5222 \ + -v $(pwd)/database:/opt/ejabberd/database \ + ghcr.io/processone/ejabberd:23.01 +docker exec -it $OLDCONTAINER ejabberdctl started +docker exec -it $OLDCONTAINER ejabberdctl register user1 localhost somepass +docker exec -it $OLDCONTAINER ejabberdctl registered_users localhost +``` + +Methods to know the Erlang node name: +```bash +ls database/ | grep ejabberd@ +docker exec -it $OLDCONTAINER ejabberdctl status +docker exec -it $OLDCONTAINER grep "started in the node" logs/ejabberd.log +``` + +#### Change Mnesia Node + +First of all let's store the Erlang node names and paths in variables. +In this example they would be: +```bash +OLDCONTAINER=ejaold +NEWCONTAINER=ejanew +OLDNODE=ejabberd@95145ddee27c +NEWNODE=ejabberd@localhost +OLDFILE=/opt/ejabberd/database/old.backup +NEWFILE=/opt/ejabberd/database/new.backup +``` + +1. Start your old container that can still read the Mnesia database correctly. +If you have the Mnesia spool files, +but don't have access to the old container anymore, go to +[Create Temporary Container](#create-temporary-container) +and later come back here. + +2. Generate a backup file and check it was created: +```bash +docker exec -it $OLDCONTAINER ejabberdctl backup $OLDFILE +ls -l database/*.backup +``` + +3. Stop ejabberd: +```bash +docker stop $OLDCONTAINER +``` + +4. Create the new container. For example: +```bash +docker run \ + --name $NEWCONTAINER \ + -d \ + -p 5222:5222 \ + -v $(pwd)/database:/opt/ejabberd/database \ + ghcr.io/processone/ejabberd:latest +``` + +5. Convert the backup file to new node name: +```bash +docker exec -it $NEWCONTAINER ejabberdctl mnesia_change_nodename $OLDNODE $NEWNODE $OLDFILE $NEWFILE +``` + +6. Install the backup file as a fallback: +```bash +docker exec -it $NEWCONTAINER ejabberdctl install_fallback $NEWFILE +``` + +7. Restart the container: +```bash +docker restart $NEWCONTAINER +``` + +8. Check that the information of the old database is available. +In this example, it should show that the account `user1` is registered: +```bash +docker exec -it $NEWCONTAINER ejabberdctl registered_users localhost +``` + +9. When the new container is working perfectly with the converted Mnesia database, +you may want to remove the unneeded files: +the old container, the old Mnesia spool files, and the backup files. + +#### Create Temporary Container + +In case the old container that used the Mnesia database is not available anymore, +a temporary container can be created just to read the Mnesia database +and make a backup of it, as explained in the previous section. + +This method uses `--hostname` command line argument for docker, +and `ERLANG_NODE_ARG` environment variable for ejabberd. +Their values must be the hostname of your old container +and the Erlang node name of your old ejabberd node. +To know the Erlang node name please check +[Setup Old Container](#setup-old-container). + +Command line example: +```bash +OLDHOST=${OLDNODE#*@} +docker run \ + -d \ + --name $OLDCONTAINER \ + --hostname $OLDHOST \ + -p 5222:5222 \ + -v $(pwd)/database:/opt/ejabberd/database \ + -e ERLANG_NODE_ARG=$OLDNODE \ + ghcr.io/processone/ejabberd:latest +``` + +Check the old database content is available: +```bash +docker exec -it $OLDCONTAINER ejabberdctl registered_users localhost +``` + +Now that you have ejabberd running with access to the Mnesia database, +you can continue with step 2 of previous section +[Change Mnesia Node](#change-mnesia-node). + + +Build Container Image +---------------- + +The container image includes ejabberd as a standalone OTP release built using Elixir. + +### Build `ejabberd` [![ejabberd Container](https://img.shields.io/badge/ejabberd-grey?logo=opencontainersinitiative&logoColor=2094f3)](https://github.com/processone/ejabberd/pkgs/container/ejabberd) + +The ejabberd Erlang/OTP release is configured with: - `mix.exs`: Customize ejabberd release - `vars.config`: ejabberd compilation configuration options - `config/runtime.exs`: Customize ejabberd paths - `ejabberd.yml.template`: ejabberd default config file -### Direct build +#### Direct build Build ejabberd Community Server container image from ejabberd master git repository: @@ -321,7 +567,7 @@ docker buildx build \ . ``` -### Podman build +#### Podman build To build the image using Podman, please notice: @@ -346,30 +592,27 @@ podman stop eja1 podman run --name eja1 -it -e EJABBERD_BYPASS_WARNINGS=true -p 5222:5222 localhost/ejabberd live ``` -### Package build for `arm64` +### Build `ecs` [![ecs Container](https://img.shields.io/badge/ecs-grey?logo=docker&logoColor=2094f3)](https://hub.docker.com/r/ejabberd/ecs/) -By default, `.github/container/Dockerfile` builds this container by directly compiling ejabberd, -it is a fast and direct method. -However, a problem with QEMU prevents building the container in QEMU using Erlang/OTP 25 -for the `arm64` architecture. +The ejabberd Erlang/OTP release is configured with: -Providing `--build-arg METHOD=package` is an alternate method to build the container -used by the Github Actions workflow that provides `amd64` and `arm64` container images. -It first builds an ejabberd binary package, and later installs it in the image. -That method avoids using QEMU, so it can build `arm64` container images, but is extremely -slow the first time it's used, and consequently not recommended for general use. +- `rel/config.exs`: Customize ejabberd release +- `rel/dev.exs`: ejabberd environment configuration for development release +- `rel/prod.exs`: ejabberd environment configuration for production release +- `vars.config`: ejabberd compilation configuration options +- `conf/ejabberd.yml`: ejabberd default config file -In this case, to build the ejabberd container image for arm64 architecture: +Build ejabberd Community Server base image from ejabberd master on Github: ```bash -docker buildx build \ - --build-arg METHOD=package \ - --platform linux/arm64 \ - -t personal/ejabberd:$VERSION \ - -f .github/container/Dockerfile \ - . +docker build -t personal/ejabberd . ``` +Build ejabberd Community Server base image for a given ejabberd version: + +```bash +./build.sh 18.03 +``` Composer Examples ----------------- @@ -430,26 +673,21 @@ stores the mnesia database in a local path, registers an account when it's created, and checks the number of registered accounts every time it's started. -Download or copy the ejabberd configuration file: +Prepare an ejabberd configuration file: ```bash -wget https://raw.githubusercontent.com/processone/ejabberd/master/ejabberd.yml.example -mv ejabberd.yml.example ejabberd.yml -``` - -Use a macro in `ejabberd.yml` to set the served vhost, with `localhost` as default value: -```yaml -define_macro: - XMPPHOST: localhost - -hosts: - - XMPPHOST +mkdir conf && cp ejabberd.yml.example conf/ejabberd.yml ``` Create the database directory and allow the container access to it: -```bash -mkdir database -sudo chown 9000:9000 database -``` + +- Docker: + ```bash + mkdir database && sudo chown 9000:9000 database + ``` +- Podman: + ```bash + mkdir database && podman unshare chown 9000:9000 database + ``` If using Docker, write this `docker-compose.yml` file and start it with `docker-compose up`: @@ -463,8 +701,9 @@ services: image: ghcr.io/processone/ejabberd container_name: ejabberd environment: - - EJABBERD_MACRO_XMPPHOST=example.com - - CTL_ON_CREATE=register admin example.com asd + - EJABBERD_MACRO_HOST=example.com + - EJABBERD_MACRO_ADMIN=admin@example.com + - REGISTER_ADMIN_PASSWORD=somePassw0rd - CTL_ON_START=registered_users example.com ; status ports: @@ -473,7 +712,7 @@ services: - "5280:5280" - "5443:5443" volumes: - - ./ejabberd.yml:/opt/ejabberd/conf/ejabberd.yml:ro + - ./conf/ejabberd.yml:/opt/ejabberd/conf/ejabberd.yml:ro - ./database:/opt/ejabberd/database ``` @@ -494,8 +733,12 @@ spec: - name: ejabberd image: ghcr.io/processone/ejabberd env: - - name: CTL_ON_CREATE - value: register admin example.com asd + - name: EJABBERD_MACRO_HOST + value: example.com + - name: EJABBERD_MACRO_ADMIN + value: admin@example.com + - name: REGISTER_ADMIN_PASSWORD + value: somePassw0rd - name: CTL_ON_START value: registered_users example.com ; status @@ -518,7 +761,7 @@ spec: volumes: - name: config hostPath: - path: ./ejabberd.yml + path: ./conf/ejabberd.yml type: File - name: db hostPath: @@ -551,11 +794,17 @@ services: main: image: ghcr.io/processone/ejabberd - container_name: ejabberd + container_name: main environment: - ERLANG_NODE_ARG=ejabberd@main - ERLANG_COOKIE=dummycookie123 - CTL_ON_CREATE=! register admin localhost asd + healthcheck: + test: netstat -nl | grep -q 5222 + start_period: 5s + interval: 5s + timeout: 5s + retries: 120 replica: image: ghcr.io/processone/ejabberd @@ -654,92 +903,41 @@ spec: ``` -Your configuration file should use those macros to allow each ejabberd node -use different listening port numbers: -```diff -diff --git a/ejabberd.yml.example b/ejabberd.yml.example -index 39e423a64..6e875b48f 100644 ---- a/ejabberd.yml.example -+++ b/ejabberd.yml.example -@@ -24,9 +24,19 @@ loglevel: info - # - /etc/letsencrypt/live/domain.tld/fullchain.pem - # - /etc/letsencrypt/live/domain.tld/privkey.pem - -+define_macro: -+ PORT_C2S: 5222 -+ PORT_C2S_TLS: 5223 -+ PORT_S2S: 5269 -+ PORT_HTTP_TLS: 5443 -+ PORT_HTTP: 5280 -+ PORT_STUN: 5478 -+ PORT_MQTT: 1883 -+ PORT_PROXY65: 7777 -+ - listen: - - -- port: 5222 -+ port: PORT_C2S - ip: "::" - module: ejabberd_c2s - max_stanza_size: 262144 -@@ -34,7 +44,7 @@ listen: - access: c2s - starttls_required: true - - -- port: 5223 -+ port: PORT_C2S_TLS - ip: "::" - module: ejabberd_c2s - max_stanza_size: 262144 -@@ -42,13 +52,13 @@ listen: - access: c2s - tls: true - - -- port: 5269 -+ port: PORT_S2S - ip: "::" - module: ejabberd_s2s_in - max_stanza_size: 524288 - shaper: s2s_shaper - - -- port: 5443 -+ port: PORT_HTTP_TLS - ip: "::" - module: ejabberd_http - tls: true -@@ -60,14 +70,14 @@ listen: - /upload: mod_http_upload - /ws: ejabberd_http_ws - - -- port: 5280 -+ port: PORT_HTTP - ip: "::" - module: ejabberd_http - request_handlers: - /admin: ejabberd_web_admin - /.well-known/acme-challenge: ejabberd_acme - - -- port: 5478 -+ port: PORT_STUN - ip: "::" - transport: udp - module: ejabberd_stun -@@ -77,7 +87,7 @@ listen: - ## The server's public IPv6 address: - # turn_ipv6_address: "2001:db8::3" - - -- port: 1883 -+ port: PORT_MQTT - ip: "::" - module: mod_mqtt - backlog: 1000 -@@ -207,6 +217,7 @@ modules: - mod_proxy65: - access: local - max_connections: 5 -+ port: PORT_PROXY65 - mod_pubsub: - access_createnode: pubsub_createnode - plugins: -``` +Images Comparison +----------------- + +Let's summarize the differences between both container images. Legend: + +- :sparkle: is the recommended alternative +- :orange_circle: added in the latest release (ejabberd 25.xx) +- :high_brightness: added in the previous release (ejabberd 24.12) +- :low_brightness: added in the pre-previous release (ejabberd 24.10) + +| | [![ejabberd Container](https://img.shields.io/badge/ejabberd-grey?logo=opencontainersinitiative&logoColor=2094f3)](https://github.com/processone/ejabberd/pkgs/container/ejabberd) | [![ecs Container](https://img.shields.io/badge/ecs-grey?logo=docker&logoColor=2094f3)](https://hub.docker.com/r/ejabberd/ecs/) | +|:----------------------|:------------------|:-----------------------| +| Source code | [ejabberd/.github/container](https://github.com/processone/ejabberd/tree/master/.github/container) | [docker-ejabberd/ecs](https://github.com/processone/docker-ejabberd/tree/master/ecs) | +| 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
`master` branch | stable releases
[`master` branch zip](https://github.com/processone/docker-ejabberd/actions/workflows/tests.yml) | +| Architectures | `linux/amd64`
`linux/arm64` | `linux/amd64` | +| Software | Erlang/OTP 27.2-alpine
Elixir 1.18.1 | Alpine 3.19
Erlang/OTP 26.2
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/)
[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 | +| [ejabberdapi](#ejabberdapi) | included :orange_circle: | included | +| :black_square_button: **Ports** | +| [1880](#ports) for WebAdmin | yes :orange_circle: | yes :orange_circle: | +| [5210](#ports) for `ERL_DIST_PORT` | supported | supported :orange_circle: | +| :black_square_button: **Paths** | +| `$HOME` | `/opt/ejabberd/` | `/home/ejabberd/` | +| User data | `$HOME` :sparkle:
`/home/ejabberd/` :orange_circle: | `$HOME`
`/opt/ejabberd/` :sparkle: :low_brightness: | +| `ejabberdctl` | `ejabberdctl` :sparkle:
`bin/ejabberdctl` :orange_circle: | `bin/ejabberdctl`
`ejabberdctl` :sparkle: :low_brightness: | +| [`captcha.sh`](#captcha) | `$HOME/bin/captcha.sh` :orange_circle: | `$HOME/bin/captcha.sh` :orange_circle: | +| `*.sql` files | `$HOME/sql/*.sql` :sparkle: :orange_circle:
`$HOME/database/*.sql` :orange_circle: | `$HOME/database/*.sql`
`$HOME/sql/*.sql` :sparkle: :orange_circle: | +| Mnesia spool files | `$HOME/database/` :sparkle:
`$HOME/database/NODENAME/` :orange_circle: | `$HOME/database/NODENAME/`
`$HOME/database/` :sparkle: :orange_circle: | +| :black_square_button: **Variables** | +| [`EJABBERD_MACRO_*`](#macros-in-environment) | supported :high_brightness: | supported :high_brightness: | +| Macros used in `ejabberd.yml` | yes :orange_circle: | yes :orange_circle: | +| [`EJABBERD_MACRO_ADMIN`](#register-admin-account) | Grant admin rights :orange_circle:
(default `admin@localhost`)
| Hardcoded `admin@localhost` | +| [`REGISTER_ADMIN_PASSWORD`](#register-admin-account) | Register admin account :orange_circle: | unsupported | +| `CTL_OVER_HTTP` | enabled :orange_circle: | unsupported | diff --git a/ejabberdctl.cfg.example b/ejabberdctl.cfg.example index 6887fb4cc..88f99cd78 100644 --- a/ejabberdctl.cfg.example +++ b/ejabberdctl.cfg.example @@ -198,6 +198,17 @@ # #CONTRIB_MODULES_CONF_DIR=/etc/ejabberd/modules +#. +#' CTL_OVER_HTTP: Path to ejabberdctl HTTP listener socket +# +# To speedup ejabberdctl execution time for ejabberd commands, +# you can setup an ejabberd_http listener with ejabberd_ctl handling requests, +# listening in a unix domain socket. +# +# Default: disabled +# +#CTL_OVER_HTTP=sockets/ctl_over_http.sock + #. #' # vim: foldmarker=#',#. foldmethod=marker: diff --git a/ejabberdctl.template b/ejabberdctl.template index b9f01535b..7858067d1 100755 --- a/ejabberdctl.template +++ b/ejabberdctl.template @@ -179,7 +179,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 "--------------------------------------------------------------------" @@ -332,6 +334,54 @@ wait_status() [ $timeout -gt 0 ] } +exec_other_command() +{ + 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'"')' \ + -s ejabberd_ctl \ + -extra "$ERLANG_NODE" $NO_TIMEOUT "$@" + result=$? + case $result in + 3) help;; + *) :;; + esac + exit $result + else + exec_ctl_over_http_socket "$@" + fi +} + +exec_ctl_over_http_socket() +{ + COMMAND=${1} + CARGS="" + while [ $# -gt 0 ]; do + [ -z "$CARGS" ] && CARGS="[" || CARGS="${CARGS}, " + CARGS="${CARGS}\"$1\"" + shift + done + CARGS="${CARGS}]" + TEMPHEADERS=temp-headers.log + curl \ + --unix-socket ${CTL_OVER_HTTP} \ + --header "Content-Type: application/json" \ + --header "Accept: application/json" \ + --data "${CARGS}" \ + --dump-header ${TEMPHEADERS} \ + --no-progress-meter \ + "http://localhost/ctl/${COMMAND}" + result=$(sed -n 's/.*status-code: \([0-9]*\).*/\1/p' < $TEMPHEADERS) + rm ${TEMPHEADERS} + case $result in + 2|3) exec_other_command help ${COMMAND};; + *) :;; + esac + exit $result +} + # ensure we can change current directory to SPOOL_DIR [ -d "$SPOOL_DIR" ] || exec_cmd mkdir -p "$SPOOL_DIR" cd "$SPOOL_DIR" || { @@ -400,15 +450,6 @@ case $1 in ;; *) set_dist_client - exec_erl "$(uid ctl)" -hidden -noinput \ - -eval 'net_kernel:connect_node('"'$ERLANG_NODE'"')' \ - -s ejabberd_ctl \ - -extra "$ERLANG_NODE" $NO_TIMEOUT "$@" - result=$? - case $result in - 2|3) help;; - *) :;; - esac - exit $result + exec_other_command "$@" ;; esac diff --git a/src/ejabberd_admin.erl b/src/ejabberd_admin.erl index c6e955ab2..259831d7f 100644 --- a/src/ejabberd_admin.erl +++ b/src/ejabberd_admin.erl @@ -663,7 +663,7 @@ status() -> {value, {_, _, Version}} -> {ok, io_lib:format("ejabberd ~s is running in that node", [Version])} end, - {Is_running, String1 ++ " " ++String2}. + {Is_running, String1 ++ "\n" ++String2}. stop() -> _ = supervisor:terminate_child(ejabberd_sup, ejabberd_sm), diff --git a/src/ejabberd_ctl.erl b/src/ejabberd_ctl.erl index 6fcd46e1c..72aac3780 100644 --- a/src/ejabberd_ctl.erl +++ b/src/ejabberd_ctl.erl @@ -28,7 +28,7 @@ -behaviour(gen_server). -author('alexey@process-one.net'). --export([start/0, start_link/0, process/1, process2/2]). +-export([start/0, start_link/0, process/1, process/2, process2/2]). %% gen_server callbacks -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). @@ -37,6 +37,7 @@ -include("ejabberd_ctl.hrl"). -include("ejabberd_commands.hrl"). +-include("ejabberd_http.hrl"). -include("logger.hrl"). -include("ejabberd_stacktrace.hrl"). @@ -115,15 +116,44 @@ code_change(_OldVsn, State, _Extra) -> {ok, State}. %%----------------------------- -%% Process +%% Process http +%%----------------------------- + +-spec process_http([binary()], tuple()) -> {non_neg_integer(), [{binary(), binary()}], string()}. + +process_http([_Call], #request{data = Data, path = [<<"ctl">> | _]}) -> + Args = [binary_to_list(E) || E <- misc:json_decode(Data)], + process_http2(Args, ?DEFAULT_VERSION). + +process_http2(["--version", Arg | Args], _) -> + Version = + try + list_to_integer(Arg) + catch _:_ -> + throw({invalid_version, Arg}) + end, + process_http2(Args, Version); + +process_http2(Args, Version) -> + {String, Code} = process2(Args, [], Version), + String2 = case String of + [] -> String; + _ -> [String, "\n"] + end, + {200, [{<<"status-code">>, integer_to_binary(Code)}], String2}. + +%%----------------------------- +%% Process command line %%----------------------------- -spec process([string()]) -> non_neg_integer(). process(Args) -> process(Args, ?DEFAULT_VERSION). +-spec process([string() | binary()], non_neg_integer() | tuple()) -> non_neg_integer(). --spec process([string()], non_neg_integer()) -> non_neg_integer(). +process([Call], Request) when is_binary(Call) and is_record(Request, request) -> + process_http([Call], Request); %% The commands status, stop and restart are defined here to ensure %% they are usable even if ejabberd is completely stopped. @@ -232,7 +262,7 @@ process2(Args, AccessCommands, Auth, Version) -> io:format(lists:flatten(["\n" | String]++["\n"])), [CommandString | _] = Args, process(["help" | [CommandString]], Version), - {lists:flatten(String), ?STATUS_ERROR}; + {lists:flatten(String), ?STATUS_USAGE}; {String, Code} when is_list(String) and is_integer(Code) -> {lists:flatten(String), Code}; @@ -271,7 +301,7 @@ try_run_ctp(Args, Auth, AccessCommands, Version) -> try_call_command(Args, Auth, AccessCommands, Version); false -> print_usage(Version), - {"", ?STATUS_USAGE}; + {"", ?STATUS_BADRPC}; Status -> {"", Status} catch @@ -288,7 +318,7 @@ try_run_ctp(Args, Auth, AccessCommands, Version) -> try_call_command(Args, Auth, AccessCommands, Version) -> try call_command(Args, Auth, AccessCommands, Version) of {Reason, wrong_command_arguments} -> - {Reason, ?STATUS_ERROR}; + {Reason, ?STATUS_USAGE}; Res -> Res catch @@ -550,7 +580,8 @@ print_usage(HelpMode, MaxC, ShCode, Version) -> AllCommands = get_list_commands(Version), print( - ["Usage: ", "ejabberdctl", " [--no-timeout] [--node ", ?A("nodename"), "] [--version ", ?A("api_version"), "] ", + ["Usage: ", "ejabberdctl", " [--no-timeout] [--node ", ?A("name"), "] [--version ", ?A("apiv"), "] ", + "[--auth ", ?A("user host pass"), "] ", ?C("command"), " [", ?A("arguments"), "]\n" "\n" "Available commands in this ejabberd node:\n"], []), diff --git a/src/ejabberd_listener.erl b/src/ejabberd_listener.erl index 2412eb545..67f3db2de 100644 --- a/src/ejabberd_listener.erl +++ b/src/ejabberd_listener.erl @@ -216,18 +216,13 @@ listen_tcp(Port, SockOpts) -> setup_provisional_udsocket_dir(DefinitivePath) -> ProvisionalPath = get_provisional_udsocket_path(DefinitivePath), - SocketDir = filename:dirname(ProvisionalPath), - file:make_dir(SocketDir), - file:change_mode(SocketDir, 8#00700), ?DEBUG("Creating a Unix Domain Socket provisional file at ~ts for the definitive path ~s", [ProvisionalPath, DefinitivePath]), ProvisionalPath. get_provisional_udsocket_path(Path) -> - MnesiaDir = mnesia:system_info(directory), - SocketDir = filename:join(MnesiaDir, "socket"), PathBase64 = misc:term_to_base64(Path), - PathBuild = filename:join(SocketDir, PathBase64), + 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))}). @@ -236,11 +231,10 @@ get_definitive_udsocket_path(<<"unix", _>> = Unix) -> get_definitive_udsocket_path(ProvisionalPath) -> PathBase64 = filename:basename(ProvisionalPath), {term, Path} = misc:base64_to_term(PathBase64), - Path. + relative_socket_to_mnesia(Path). set_definitive_udsocket(<<"unix:", Path/binary>>, Opts) -> Prov = get_provisional_udsocket_path(Path), - timer:sleep(5000), Usd = maps:get(unix_socket, Opts), case maps:get(mode, Usd, undefined) of undefined -> ok; @@ -268,10 +262,33 @@ set_definitive_udsocket(<<"unix:", Path/binary>>, Opts) -> throw({error_setting_socket_group, Group, Prov}) end end, - file:rename(Prov, Path); + FinalPath = relative_socket_to_mnesia(Path), + FinalPathDir = filename:dirname(FinalPath), + case file:make_dir(FinalPathDir) of + ok -> + file:change_mode(FinalPathDir, 8#00700); + _ -> + ok + end, + file:rename(Prov, FinalPath); set_definitive_udsocket(_Port, _Opts) -> ok. +relative_socket_to_mnesia(Path1) -> + case filename:pathtype(Path1) of + absolute -> + Path1; + relative -> + MnesiaDir = mnesia:system_info(directory), + filename:join(MnesiaDir, Path1) + end. + +maybe_delete_udsocket_file(<<"unix:", Path/binary>>) -> + PathAbsolute = relative_socket_to_mnesia(Path), + file:delete(PathAbsolute); +maybe_delete_udsocket_file(_Port) -> + ok. + %%% %%% %%% @@ -341,11 +358,20 @@ accept(ListenSocket, Module, State, Sup, Interval, Proxy, Arity) -> gen_tcp:close(Socket), none end, - ?INFO_MSG("(~p) Accepted connection ~ts -> ~ts", - [Receiver, - ejabberd_config:may_hide_data( - format_endpoint({PPort, PAddr, tcp})), - format_endpoint({Port, Addr, tcp})]); + case is_ctl_over_http(State) of + false -> + ?INFO_MSG("(~p) Accepted connection ~ts -> ~ts", + [Receiver, + ejabberd_config:may_hide_data( + format_endpoint({PPort, PAddr, tcp})), + format_endpoint({Port, Addr, tcp})]); + true -> + ?DEBUG("(~p) Accepted connection ~ts -> ~ts", + [Receiver, + ejabberd_config:may_hide_data( + format_endpoint({PPort, PAddr, tcp})), + format_endpoint({Port, Addr, tcp})]) + end; _ -> gen_tcp:close(Socket) end, @@ -356,6 +382,16 @@ accept(ListenSocket, Module, State, Sup, Interval, Proxy, Arity) -> accept(ListenSocket, Module, State, Sup, NewInterval, Proxy, Arity) end. +is_ctl_over_http(State) -> + case lists:keyfind(request_handlers, 1, State) of + {request_handlers, Handlers} -> + case lists:keyfind(ejabberd_ctl, 2, Handlers) of + {_, ejabberd_ctl} -> true; + _ -> false + end; + _ -> false + end. + -spec udp_recv(inet:socket(), module(), state()) -> no_return(). udp_recv(Socket, Module, State) -> case gen_udp:recv(Socket, 0) of @@ -470,12 +506,13 @@ stop_listeners() -> Ports). -spec stop_listener(endpoint(), module(), opts()) -> ok | {error, any()}. -stop_listener({_, _, Transport} = EndPoint, Module, Opts) -> +stop_listener({Port, _, Transport} = EndPoint, Module, Opts) -> case supervisor:terminate_child(?MODULE, EndPoint) of ok -> ?INFO_MSG("Stop accepting ~ts connections at ~ts for ~p", [format_transport(Transport, Opts), format_endpoint(EndPoint), Module]), + maybe_delete_udsocket_file(Port), ets:delete(?MODULE, EndPoint), supervisor:delete_child(?MODULE, EndPoint); Err -> @@ -562,10 +599,12 @@ format_error(Reason) -> end. -spec format_endpoint(endpoint()) -> string(). -format_endpoint({Port, IP, _Transport}) -> +format_endpoint({Port, IP, Transport}) -> case Port of <<"unix:", _/binary>> -> Port; + <<>> when (IP == local) and (Transport == tcp) -> + "local-unix-socket-domain"; Unix when is_binary(Unix) -> Def = get_definitive_udsocket_path(Unix), <<"unix:", Def/binary>>;