From dfd79d5774f1bbd4fd6d3dc7346b3636c9ed589a Mon Sep 17 00:00:00 2001 From: casjay Date: Tue, 19 May 2026 12:54:07 -0400 Subject: [PATCH] =?UTF-8?q?=20=F0=9F=A6=88=F0=9F=8F=A0=F0=9F=90=9C?= =?UTF-8?q?=E2=9D=97=20Initial=20Commit=20=E2=9D=97=F0=9F=90=9C?= =?UTF-8?q?=F0=9F=A6=88=F0=9F=8F=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .dockerignore | 28 ++ .gitattributes | 98 +++++ .github/CODEOWNERS | 8 + .github/workflows/ci.yml | 58 +++ .github/workflows/release.yml | 71 +++ .gitignore | 69 +++ CLAUDE.md | 7 + IDEA.md | 32 ++ LICENSE.md | 13 + README.md | 8 + docker/Dockerfile | 69 +++ docker/docker-compose.yml | 29 ++ docker/rootfs/usr/local/bin/entrypoint.sh | 489 +++++++++++++++++++++ docker/rootfs/usr/local/bin/healthcheck.sh | 7 + 14 files changed, 986 insertions(+) create mode 100644 .dockerignore create mode 100644 .gitattributes create mode 100644 .github/CODEOWNERS create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/release.yml create mode 100644 .gitignore create mode 100644 CLAUDE.md create mode 100644 IDEA.md create mode 100644 LICENSE.md create mode 100644 README.md create mode 100644 docker/Dockerfile create mode 100644 docker/docker-compose.yml create mode 100644 docker/rootfs/usr/local/bin/entrypoint.sh create mode 100644 docker/rootfs/usr/local/bin/healthcheck.sh diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..3f0c0cd --- /dev/null +++ b/.dockerignore @@ -0,0 +1,28 @@ +# .dockerignore created on 05/19/26 at 09:30 + +# version control +.git/ +.gitignore +.gitattributes + +# local and secret config +.env +app.env +default.env +.claude/ + +# build artifacts +binaries/ +releases/ + +# runtime volume data (never in image) +volumes/ +docker/volumes/ + +# OS files +.DS_Store +Thumbs.db + +# docs and meta (not needed in image) +*.md +LICENSE* diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..517388d --- /dev/null +++ b/.gitattributes @@ -0,0 +1,98 @@ +# Template generated on Sun May 17 10:58:44 PM EDT 2026 from https://github.com/alexkaratarakis/gitattributes" +# Common settings that generally should always be used with your language specific settings +# Auto detect text files and perform LF normalization +* text=auto +# The above will handle all files NOT found below +# Documents +*.bibtex text diff=bibtex +*.doc diff=astextplain +*.DOC diff=astextplain +*.docx diff=astextplain +*.DOCX diff=astextplain +*.dot diff=astextplain +*.DOT diff=astextplain +*.pdf diff=astextplain +*.PDF diff=astextplain +*.rtf diff=astextplain +*.RTF diff=astextplain +*.md text diff=markdown +*.mdx text diff=markdown +*.tex text diff=tex +*.adoc text +*.textile text +*.mustache text +*.csv text eol=crlf +*.tab text +*.tsv text +*.txt text +*.sql text +*.epub diff=astextplain +# Graphics +*.png binary +*.jpg binary +*.jpeg binary +*.gif binary +*.tif binary +*.tiff binary +*.ico binary +# SVG treated as text by default. +*.svg text +# If you want to treat it as binary, +# use the following line instead. +# *.svg binary +*.eps binary +# Scripts +*.bash text eol=lf +*.fish text eol=lf +*.ksh text eol=lf +*.sh text eol=lf +*.zsh text eol=lf +# These are explicitly windows files and should use crlf +*.bat text eol=crlf +*.cmd text eol=crlf +*.ps1 text eol=crlf +# Serialisation +*.json text +*.toml text +*.xml text +*.yaml text +*.yml text +# Archives +*.7z binary +*.bz binary +*.bz2 binary +*.bzip2 binary +*.gz binary +*.lz binary +*.lzma binary +*.rar binary +*.tar binary +*.taz binary +*.tbz binary +*.tbz2 binary +*.tgz binary +*.tlz binary +*.txz binary +*.xz binary +*.Z binary +*.zip binary +*.zst binary +# Text files where line endings should be preserved +*.patch -text +# Exclude files from exporting +.gitattributes export-ignore +.gitignore export-ignore +.gitkeep export-ignore + +# Template generated on Sun May 17 10:58:44 PM EDT 2026 +# Files for git large file system +*.7z filter=lfs diff=lfs merge=lfs -text +*.gz filter=lfs diff=lfs merge=lfs -text +*.xz filter=lfs diff=lfs merge=lfs -text +*.tar filter=lfs diff=lfs merge=lfs -text +*.bz2 filter=lfs diff=lfs merge=lfs -text +*.zip filter=lfs diff=lfs merge=lfs -text +*.rpm filter=lfs diff=lfs merge=lfs -text +*.7zip filter=lfs diff=lfs merge=lfs -text +*.bzip2 filter=lfs diff=lfs merge=lfs -text + diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..f596e3f --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,8 @@ +# Global owners — review required for everything not covered below +* @casjaysdevdocker + +# Workflow changes require maintainer sign-off +.github/ @casjaysdevdocker + +# Docker assets +docker/ @casjaysdevdocker diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..828e424 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,58 @@ +name: CI + +on: + push: + branches: ["main"] + pull_request: + branches: ["main"] + +permissions: + contents: read + packages: write + +jobs: + build: + name: Build image + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - uses: docker/setup-buildx-action@b5730e1a0a27db01e64c4ab5f4f7c8a63c1de14a # v3.10.0 + + - uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0 + if: github.event_name != 'pull_request' + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - uses: docker/metadata-action@902fa8ec7d6ecbea8a986b37db18de1c4e72470b # v5.7.0 + id: meta + with: + images: ghcr.io/${{ github.repository_owner }}/${{ github.event.repository.name }} + tags: | + type=edge,branch=main + type=sha,prefix=sha- + annotations: | + maintainer=${{ github.repository_owner }} <${{ github.repository_owner }}@casjay.pro> + org.opencontainers.image.vendor=${{ github.repository_owner }} + org.opencontainers.image.authors=${{ github.repository_owner }} + org.opencontainers.image.title=${{ github.event.repository.name }} + org.opencontainers.image.description=Containerized version of ${{ github.event.repository.name }} + org.opencontainers.image.url=${{ github.event.repository.html_url }} + org.opencontainers.image.source=${{ github.event.repository.html_url }} + org.opencontainers.image.documentation=${{ github.event.repository.html_url }} + org.opencontainers.image.vcs-type=Git + com.github.containers.toolbox=false + + - uses: docker/build-push-action@14487ce63c7a62a4a324b0bfb37086795e31c6c1 # v6.16.0 + with: + context: . + file: docker/Dockerfile + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.meta.outputs.tags }} + annotations: ${{ steps.meta.outputs.annotations }} + labels: "" + platforms: linux/amd64,linux/arm64 + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..0d592e7 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,71 @@ +name: Release + +on: + push: + tags: ["v*"] + +permissions: + contents: write + packages: write + +jobs: + release: + name: Build and publish release image + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - uses: docker/setup-buildx-action@b5730e1a0a27db01e64c4ab5f4f7c8a63c1de14a # v3.10.0 + + - uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract version from tag + id: version + run: echo "version=${GITHUB_REF_NAME#v}" >> "$GITHUB_OUTPUT" + + - uses: docker/metadata-action@902fa8ec7d6ecbea8a986b37db18de1c4e72470b # v5.7.0 + id: meta + with: + images: ghcr.io/${{ github.repository_owner }}/${{ github.event.repository.name }} + tags: | + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=raw,value=latest + type=raw,value=${{ steps.version.outputs.version }} + annotations: | + maintainer=${{ github.repository_owner }} <${{ github.repository_owner }}@casjay.pro> + org.opencontainers.image.vendor=${{ github.repository_owner }} + org.opencontainers.image.authors=${{ github.repository_owner }} + org.opencontainers.image.title=${{ github.event.repository.name }} + org.opencontainers.image.description=Containerized version of ${{ github.event.repository.name }} + org.opencontainers.image.url=${{ github.event.repository.html_url }} + org.opencontainers.image.source=${{ github.event.repository.html_url }} + org.opencontainers.image.documentation=${{ github.event.repository.html_url }} + org.opencontainers.image.vcs-type=Git + com.github.containers.toolbox=false + + - uses: docker/build-push-action@14487ce63c7a62a4a324b0bfb37086795e31c6c1 # v6.16.0 + with: + context: . + file: docker/Dockerfile + push: true + tags: ${{ steps.meta.outputs.tags }} + annotations: ${{ steps.meta.outputs.annotations }} + labels: "" + platforms: linux/amd64,linux/arm64 + build-args: | + VERSION=${{ steps.version.outputs.version }} + BUILD_DATE=${{ github.event.head_commit.timestamp }} + VCS_REF=${{ github.sha }} + cache-from: type=gha + cache-to: type=gha,mode=max + + - name: Create GitHub Release + uses: softprops/action-gh-release@da05d552573ad5aba039edc1654bcb6e02e54e87 # v2.2.2 + with: + generate_release_notes: true + tag_name: ${{ github.ref_name }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7b0c3e3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,69 @@ +# gitignore created on 05/19/26 at 09:30 +ignoredirmessage + +# ignore commit message +**/.gitcommit + +# ignore build failure markers +**/.build_failed* + +# ignore backup files +**/*.bak + +# ignore push/git control files +**/.no_push +**/.no_git + +# ignore install markers +**/.installed + +# ignore work in progress scripts +**/*.rewrite.sh +**/*.refactor.sh + +# ignore dotenv files +.env +app.env +default.env + +# ignore log and temp files +*.log +*.tmp +*.temp + +# OS generated files +### Linux ### +*~ +.fuse_hidden* +.directory +.Trash-* +.nfs* + +### macOS ### +.DS_Store? +.AppleDouble +.LSOverride +._* +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent +.AppleDB +.AppleDesktop + +# runtime volume data — never committed +volumes/ +docker/volumes/ + +# Claude Code runtime files +.claude/settings.local.json +.claude/backups/ +.claude/cache/ +.claude/file-history/ +.claude/history.jsonl +.claude/projects/ +.claude/statsFile +.claude/*.lock diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..a45c125 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,7 @@ +# Claude loader + +Project spec: [IDEA.md](IDEA.md) + +This is a Docker image project. Implementation conventions come from +`~/.claude/memory/dockerfile_conventions.md`. All Docker assets live +under `docker/`. diff --git a/IDEA.md b/IDEA.md new file mode 100644 index 0000000..23b1233 --- /dev/null +++ b/IDEA.md @@ -0,0 +1,32 @@ +## Project description + +A production-ready Docker image for [Prosody XMPP](https://prosody.im/) configured specifically for Jitsi Meet deployments. Replaces the upstream `jitsi/docker-jitsi-meet` prosody image with a standards-compliant CasjaysDev image built on Alpine Linux with full BOSH and WebSocket support (mod_websocket). + +## Project variables + +``` +project_name: prosody +project_org: casjaysdevdocker +internal_name: prosody +internal_org: casjaysdevdocker +image_registry: ghcr.io +upstream_image: prosody (Alpine package) +jitsi_repo: https://github.com/jitsi/docker-jitsi-meet +``` + +## Business logic + +- Must be a drop-in replacement for `jitsi/docker-jitsi-meet` prosody container +- Must support all standard Jitsi Docker env vars: `XMPP_DOMAIN`, `XMPP_AUTH_DOMAIN`, `XMPP_MUC_DOMAIN`, `XMPP_INTERNAL_MUC_DOMAIN`, `XMPP_GUEST_DOMAIN`, `JICOFO_AUTH_PASSWORD`, `JVB_AUTH_PASSWORD`, `ENABLE_AUTH`, `ENABLE_GUESTS`, `ENABLE_XMPP_WEBSOCKET`, `PUBLIC_URL`, `LOG_LEVEL` +- Must have BOSH working on `/http-bind` at port 5280 +- Must have WebSocket working on `/xmpp-websocket` at port 5280 (this is the primary fix over upstream) +- Must register jicofo and jvb users automatically at startup +- Must generate self-signed TLS certificates for Jitsi domains at startup if not present +- Must expose ports 5222 (c2s), 5280 (HTTP/BOSH/WS), 5347 (component) +- Must include all Jitsi prosody plugins from `jitsi/docker-jitsi-meet` prosody image +- Must run prosody as the `prosody` system user (non-root), not as root +- Must use `tini` as PID 1 +- Must work with zero config — sane defaults for all env vars +- Must NOT require a separate nginx reverse proxy in the container +- Must NOT include s6 or any other process supervisor — single process per container +- `consider_websocket_secure = true` and `consider_bosh_secure = true` must always be set so trusted-proxy HTTPS termination works diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..27b62a2 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,13 @@ + DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE + Version 2, December 2004 + + Copyright (C) 2026 casjay + + Everyone is permitted to copy and distribute verbatim or modified + copies of this license document, and changing it is allowed as long + as the name is changed. + + DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 1. You just DO WHAT THE FUCK YOU WANT TO. diff --git a/README.md b/README.md new file mode 100644 index 0000000..c5caf03 --- /dev/null +++ b/README.md @@ -0,0 +1,8 @@ +## 👋 Welcome to prosody 🚀 + +prosody README + + +## Author + +🤖 casjay: [Github](https://github.com/casjay) 🤖 diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 0000000..dafba61 --- /dev/null +++ b/docker/Dockerfile @@ -0,0 +1,69 @@ +# ============================================================================= +# Jitsi Source Stage — extract plugins and pure-Lua libs from the official +# jitsi/prosody image at build time so we never fetch at runtime +# ============================================================================= +FROM jitsi/prosody:stable AS jitsi-source + +# ============================================================================= +# Runtime Stage +# ============================================================================= +FROM alpine:latest + +ARG VERSION=dev +ARG BUILD_DATE +ARG VCS_REF +ARG LICENSE=MIT + +# Install runtime dependencies +# prosody from Alpine 13.x matches jitsi/prosody:stable (also 13.x) +# su-exec drops from root to prosody user before exec +RUN apk add --no-cache \ + bash \ + curl \ + openssl \ + prosody \ + su-exec \ + tini + +# Apply prosody 13.x compatibility patch: +# idna_to_ascii was removed in libicu 72+ but the Lua util still references it; +# the jitsi base image applies this same patch (sed -i '/idna_to_ascii/d') +RUN sed -i '/idna_to_ascii/d' /usr/lib/prosody/util/jid.lua 2>/dev/null || true + +# Create runtime directories +RUN mkdir -p \ + /config/certs \ + /config/conf.d \ + /config/data \ + /prosody-plugins \ + /prosody-plugins-contrib \ + /prosody-plugins-custom + +# Copy Jitsi-specific prosody plugins (pure Lua — architecture-independent) +COPY --from=jitsi-source /prosody-plugins /prosody-plugins +COPY --from=jitsi-source /prosody-plugins-contrib /prosody-plugins-contrib + +# Copy pure-Lua libraries that the Jitsi plugins depend on (basexx, net-url, etc.) +# We do NOT copy /usr/local/lib/lua (compiled .so for glibc/Debian — incompatible with musl/Alpine) +# lua5.4-cjson is provided by Alpine's prosody dependency and takes precedence +COPY --from=jitsi-source /usr/local/share/lua/5.4 /usr/local/share/lua/5.4 + +# Copy rootfs overlay (mirrors Linux FHS) +COPY docker/rootfs/ / + +# Copy Dockerfile into image for reference +COPY docker/Dockerfile /root/Dockerfile + +# Set ownership and permissions +RUN chown -R prosody:prosody /config /prosody-plugins-custom /prosody-plugins-contrib && \ + chmod 755 /usr/local/bin/* + +# BOSH + WebSocket (5280), c2s client connections (5222), component connections (5347) +EXPOSE 5222 5280 5347 + +STOPSIGNAL SIGRTMIN+3 + +HEALTHCHECK --start-period=2m --interval=1m --timeout=10s --retries=3 \ + CMD /usr/local/bin/healthcheck.sh || exit 1 + +ENTRYPOINT [ "tini", "-p", "SIGTERM", "--", "/usr/local/bin/entrypoint.sh" ] diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml new file mode 100644 index 0000000..2a2d507 --- /dev/null +++ b/docker/docker-compose.yml @@ -0,0 +1,29 @@ +services: + prosody: + image: ghcr.io/casjaysdevdocker/prosody:latest + restart: unless-stopped + environment: + - XMPP_DOMAIN=meet.jitsi + - XMPP_AUTH_DOMAIN=auth.meet.jitsi + - XMPP_GUEST_DOMAIN=guest.meet.jitsi + - XMPP_MUC_DOMAIN=muc.meet.jitsi + - XMPP_INTERNAL_MUC_DOMAIN=internal-muc.meet.jitsi + - XMPP_HIDDEN_DOMAIN=hidden.meet.jitsi + - JICOFO_AUTH_PASSWORD=changeme_jicofo + - JVB_AUTH_PASSWORD=changeme_jvb + - ENABLE_AUTH=0 + - ENABLE_GUESTS=0 + - ENABLE_XMPP_WEBSOCKET=1 + - LOG_LEVEL=info + volumes: + - ./volumes/config:/config + ports: + - "5222:5222" + - "5280:5280" + - "5347:5347" + networks: + - prosody-net + +networks: + prosody-net: + driver: bridge diff --git a/docker/rootfs/usr/local/bin/entrypoint.sh b/docker/rootfs/usr/local/bin/entrypoint.sh new file mode 100644 index 0000000..606772c --- /dev/null +++ b/docker/rootfs/usr/local/bin/entrypoint.sh @@ -0,0 +1,489 @@ +#!/usr/bin/env bash +# shellcheck shell=bash +# entrypoint.sh — container startup script +# Called by: tini → entrypoint.sh → prosody + +set -euo pipefail + +# - - - - - - - - - - - - - - - - - - - - - - - - - +# Defaults +# - - - - - - - - - - - - - - - - - - - - - - - - - + +: "${XMPP_DOMAIN:=meet.jitsi}" +: "${XMPP_AUTH_DOMAIN:=auth.meet.jitsi}" +: "${XMPP_GUEST_DOMAIN:=guest.meet.jitsi}" +: "${XMPP_MUC_DOMAIN:=muc.meet.jitsi}" +: "${XMPP_INTERNAL_MUC_DOMAIN:=internal-muc.meet.jitsi}" +: "${XMPP_HIDDEN_DOMAIN:=hidden.meet.jitsi}" +: "${XMPP_PORT:=5222}" +: "${PROSODY_HTTP_PORT:=5280}" +: "${ENABLE_AUTH:=0}" +: "${ENABLE_GUESTS:=0}" +: "${ENABLE_XMPP_WEBSOCKET:=1}" +: "${ENABLE_LOBBY:=1}" +: "${ENABLE_BREAKOUT_ROOMS:=1}" +: "${ENABLE_AV_MODERATION:=1}" +: "${ENABLE_END_CONFERENCE:=1}" +: "${ENABLE_RECORDING:=0}" +: "${ENABLE_TRANSCRIPTIONS:=0}" +: "${JVB_AUTH_USER:=jvb}" +: "${JIBRI_RECORDER_USER:=recorder}" +: "${JIBRI_XMPP_USER:=jibri}" +: "${JIGASI_XMPP_USER:=jigasi}" +: "${LOG_LEVEL:=info}" +: "${PROSODY_MODE:=client}" +: "${PROSODY_C2S_REQUIRE_ENCRYPTION:=true}" +: "${PROSODY_ADMINS:=}" +: "${AUTH_TYPE:=internal}" +: "${DISABLE_POLLS:=0}" + +# Derived +XMPP_MUC_DOMAIN_PREFIX="${XMPP_MUC_DOMAIN%%.*}" +PROSODY_CFG="/config/prosody.cfg.lua" +PROSODY_SITE_CFG="/config/conf.d/jitsi-meet.cfg.lua" + +# - - - - - - - - - - - - - - - - - - - - - - - - - +# Guards +# - - - - - - - - - - - - - - - - - - - - - - - - - + +[[ -n "${JICOFO_AUTH_PASSWORD:-}" ]] || { echo "FATAL: JICOFO_AUTH_PASSWORD must be set" >&2; exit 1; } +[[ -n "${JVB_AUTH_PASSWORD:-}" ]] || { echo "FATAL: JVB_AUTH_PASSWORD must be set" >&2; exit 1; } + +# - - - - - - - - - - - - - - - - - - - - - - - - - +# Directory setup +# - - - - - - - - - - - - - - - - - - - - - - - - - + +mkdir -p /config/certs /config/conf.d /config/data \ + /prosody-plugins-custom /prosody-plugins-contrib + +chown -R prosody:prosody /config /prosody-plugins-custom /prosody-plugins-contrib 2>/dev/null || true + +# - - - - - - - - - - - - - - - - - - - - - - - - - +# Generate main prosody.cfg.lua +# - - - - - - - - - - - - - - - - - - - - - - - - - + +__generate_main_cfg() { + # Build admins list + local admins_list="" + if [[ -n "${PROSODY_ADMINS}" ]]; then + while IFS=',' read -ra parts; do + for admin in "${parts[@]}"; do + admin="${admin// /}" + [[ -n "$admin" ]] && admins_list+=" \"${admin}\";\n" + done + done <<< "${PROSODY_ADMINS}" + fi + + cat > "${PROSODY_CFG}" << MAIN_CFG +-- Generated by entrypoint.sh — do not edit manually + +---------- Server-wide settings ---------- + +admins = { +$(printf '%b' "${admins_list}")} + +component_admins_as_room_owners = true + +modules_enabled = { + -- Core + "roster"; + "saslauth"; + "tls"; + "disco"; + "ping"; + "version"; + "posix"; + "limits"; + "private"; + + -- HTTP (BOSH + WebSocket — required for Jitsi) + "bosh"; + "websocket"; + "http"; + "http_health"; + + -- Status + "uptime"; +}; + +modules_disabled = { + "offline"; + "register"; + "s2s"; +}; + +-- Never allow unauthenticated registration globally +allow_registration = false; + +-- Rate limits for incoming connections +limits = { + c2s = { + rate = "10kb/s"; + }; +}; + +-- Garbage collector +gc = { + mode = "incremental"; + threshold = 400; + speed = 250; + step_size = 13; +}; + +pidfile = "/config/data/prosody.pid"; + +c2s_require_encryption = ${PROSODY_C2S_REQUIRE_ENCRYPTION}; +c2s_ports = { ${XMPP_PORT} } +c2s_interfaces = { "*" } + +s2s_secure_auth = false + +authentication = "internal_hashed" + +-- Logging +log = { + { levels = { min = "${LOG_LEVEL}" }, timestamps = "%Y-%m-%d %X", to = "console" }; +}; + +-- HTTP configuration +-- consider_bosh_secure and consider_websocket_secure allow HTTPS-terminated +-- connections from a reverse proxy to be treated as secure. +consider_bosh_secure = true; +consider_websocket_secure = true; + +-- Allow WebSocket + BOSH on the same port (5280) that the reverse proxy reaches +http_ports = { ${PROSODY_HTTP_PORT} } +http_interfaces = { "*" } + +-- Stream management (required for smacks / WebSocket reconnect) +smacks_max_unacked_stanzas = 5; +smacks_hibernation_time = 60; +smacks_max_old_sessions = 1; + +-- epoll backend for better performance +network_backend = "epoll"; +network_settings = { + tcp_backlog = 511; +}; + +-- Data storage +data_path = "/config/data"; + +-- Plugin paths +plugin_paths = { "/prosody-plugins-custom", "/prosody-plugins/", "/prosody-plugins-contrib" }; + +Include "conf.d/*.cfg.lua" +MAIN_CFG +} + +# - - - - - - - - - - - - - - - - - - - - - - - - - +# Generate conf.d/jitsi-meet.cfg.lua +# - - - - - - - - - - - - - - - - - - - - - - - - - + +__generate_site_cfg() { + # Determine main VirtualHost authentication + local main_auth="jitsi-anonymous" + if [[ "${ENABLE_AUTH}" == "1" ]] || [[ "${ENABLE_AUTH}" == "true" ]]; then + main_auth="internal_hashed" + fi + + # Determine guest VirtualHost authentication + local guest_auth="jitsi-anonymous" + + # Build global admins block for conf.d + cat > "${PROSODY_SITE_CFG}" << SITE_START +-- Generated by entrypoint.sh — do not edit manually + +admins = { + "focus@${XMPP_AUTH_DOMAIN}", + "${JVB_AUTH_USER}@${XMPP_AUTH_DOMAIN}" +} + +unlimited_jids = { + "focus@${XMPP_AUTH_DOMAIN}", + "${JVB_AUTH_USER}@${XMPP_AUTH_DOMAIN}" +} + +muc_mapper_domain_base = "${XMPP_DOMAIN}"; +muc_mapper_domain_prefix = "${XMPP_MUC_DOMAIN_PREFIX}"; + +recorder_prefixes = { "${JIBRI_RECORDER_USER}@${XMPP_HIDDEN_DOMAIN}" }; +transcriber_prefixes = { "${JIGASI_TRANSCRIBER_USER:-transcriber}@${XMPP_HIDDEN_DOMAIN}" }; + +http_default_host = "${XMPP_DOMAIN}" + +-- Main virtual host +VirtualHost "${XMPP_DOMAIN}" + authentication = "${main_auth}" + ssl = { + key = "/config/certs/${XMPP_DOMAIN}.key"; + certificate = "/config/certs/${XMPP_DOMAIN}.crt"; + } + modules_enabled = { + "bosh"; + "websocket"; + "smacks"; + "conference_duration"; + "muc_lobby_rooms"; + "muc_breakout_rooms"; + "features_identity"; +SITE_START + + # Optional XMPP_MODULES + if [[ -n "${XMPP_MODULES:-}" ]]; then + while IFS=',' read -ra mods; do + for mod in "${mods[@]}"; do + mod="${mod// /}" + [[ -n "$mod" ]] && printf ' "%s";\n' "${mod}" >> "${PROSODY_SITE_CFG}" + done + done <<< "${XMPP_MODULES}" + fi + + cat >> "${PROSODY_SITE_CFG}" << SITE_MAIN_HOST + } + main_muc = "${XMPP_MUC_DOMAIN}" + lobby_muc = "lobby.${XMPP_DOMAIN}" + breakout_rooms_muc = "breakout.${XMPP_DOMAIN}" + c2s_require_encryption = ${PROSODY_C2S_REQUIRE_ENCRYPTION} + +SITE_MAIN_HOST + + # Guest domain (only when auth is enabled and guests are enabled) + if { [[ "${ENABLE_AUTH}" == "1" ]] || [[ "${ENABLE_AUTH}" == "true" ]]; } && \ + { [[ "${ENABLE_GUESTS}" == "1" ]] || [[ "${ENABLE_GUESTS}" == "true" ]]; }; then + cat >> "${PROSODY_SITE_CFG}" << SITE_GUEST +VirtualHost "${XMPP_GUEST_DOMAIN}" + authentication = "${guest_auth}" + modules_enabled = { + "smacks"; + } + main_muc = "${XMPP_MUC_DOMAIN}" + lobby_muc = "lobby.${XMPP_DOMAIN}" + breakout_rooms_muc = "breakout.${XMPP_DOMAIN}" + c2s_require_encryption = ${PROSODY_C2S_REQUIRE_ENCRYPTION} + +SITE_GUEST + fi + + # Auth domain + cat >> "${PROSODY_SITE_CFG}" << SITE_AUTH +VirtualHost "${XMPP_AUTH_DOMAIN}" + ssl = { + key = "/config/certs/${XMPP_AUTH_DOMAIN}.key"; + certificate = "/config/certs/${XMPP_AUTH_DOMAIN}.crt"; + } + modules_enabled = { + "limits_exception"; + "smacks"; + } + authentication = "internal_hashed" + smacks_hibernation_time = 15; + +SITE_AUTH + + # Hidden domain for recording/transcription + if [[ "${ENABLE_RECORDING}" == "1" ]] || [[ "${ENABLE_RECORDING}" == "true" ]] || \ + [[ "${ENABLE_TRANSCRIPTIONS}" == "1" ]] || [[ "${ENABLE_TRANSCRIPTIONS}" == "true" ]]; then + cat >> "${PROSODY_SITE_CFG}" << SITE_HIDDEN +VirtualHost "${XMPP_HIDDEN_DOMAIN}" + modules_enabled = { + "smacks"; + } + authentication = "internal_hashed" + +SITE_HIDDEN + fi + + # Components + cat >> "${PROSODY_SITE_CFG}" << SITE_COMPONENTS +-- Internal MUC (JVB/jicofo signaling — never exposed to clients) +Component "${XMPP_INTERNAL_MUC_DOMAIN}" "muc" + storage = "memory" + modules_enabled = { + "muc_hide_all"; + "muc_filter_access"; + } + restrict_room_creation = true + muc_filter_whitelist = "${XMPP_AUTH_DOMAIN}" + muc_room_locking = false + muc_room_default_public_jids = true + muc_room_cache_size = 1000 + muc_tombstones = false + muc_room_allow_persistent = false + +-- Conference MUC (participants join rooms here) +Component "${XMPP_MUC_DOMAIN}" "muc" + restrict_room_creation = true + storage = "memory" + modules_enabled = { + "muc_hide_all"; + "muc_meeting_id"; + "muc_domain_mapper"; + "muc_password_whitelist"; + } + muc_room_cache_size = 10000 + muc_room_locking = false + muc_room_default_public_jids = true + muc_tombstones = false + muc_room_allow_persistent = false + muc_password_whitelist = { + "focus@${XMPP_AUTH_DOMAIN}"; + } + +-- Focus component proxy +Component "focus.${XMPP_DOMAIN}" "client_proxy" + target_address = "focus@${XMPP_AUTH_DOMAIN}" + +-- Speakerstats component +Component "speakerstats.${XMPP_DOMAIN}" "speakerstats_component" + muc_component = "${XMPP_MUC_DOMAIN}" + +-- End conference component +Component "endconference.${XMPP_DOMAIN}" "end_conference" + muc_component = "${XMPP_MUC_DOMAIN}" + +-- AV moderation component +Component "avmoderation.${XMPP_DOMAIN}" "av_moderation_component" + muc_component = "${XMPP_MUC_DOMAIN}" + +-- Lobby MUC +Component "lobby.${XMPP_DOMAIN}" "muc" + storage = "memory" + restrict_room_creation = true + muc_tombstones = false + muc_room_allow_persistent = false + muc_room_cache_size = 10000 + muc_room_locking = false + muc_room_default_public_jids = true + modules_enabled = { + "muc_hide_all"; + } + +-- Breakout rooms MUC +Component "breakout.${XMPP_DOMAIN}" "muc" + storage = "memory" + restrict_room_creation = true + muc_room_cache_size = 10000 + muc_room_locking = false + muc_room_default_public_jids = true + muc_tombstones = false + muc_room_allow_persistent = false + modules_enabled = { + "muc_hide_all"; + "muc_meeting_id"; + } + +-- Room metadata component +Component "metadata.${XMPP_DOMAIN}" "room_metadata_component" + muc_component = "${XMPP_MUC_DOMAIN}" + breakout_rooms_component = "breakout.${XMPP_DOMAIN}" + +SITE_COMPONENTS + + # Polls component (optional disable) + if [[ "${DISABLE_POLLS}" != "1" ]] && [[ "${DISABLE_POLLS}" != "true" ]]; then + cat >> "${PROSODY_SITE_CFG}" << SITE_POLLS +Component "polls.${XMPP_DOMAIN}" "polls_component" + +SITE_POLLS + fi +} + +# - - - - - - - - - - - - - - - - - - - - - - - - - +# Certificate generation +# - - - - - - - - - - - - - - - - - - - - - - - - - + +__generate_cert() { + local domain="${1}" + local cert_dir="/config/certs" + local key="${cert_dir}/${domain}.key" + local crt="${cert_dir}/${domain}.crt" + + [[ -f "${key}" && -f "${crt}" ]] && return 0 + + echo "INFO: Generating self-signed certificate for ${domain}" + openssl req -x509 -newkey rsa:4096 \ + -keyout "${key}" -out "${crt}" \ + -days 3650 -nodes \ + -subj "/CN=${domain}" 2>/dev/null + chmod 600 "${key}" + chown prosody:prosody "${key}" "${crt}" 2>/dev/null || true +} + +__generate_certs() { + __generate_cert "${XMPP_DOMAIN}" + __generate_cert "${XMPP_AUTH_DOMAIN}" +} + +# - - - - - - - - - - - - - - - - - - - - - - - - - +# User registration +# - - - - - - - - - - - - - - - - - - - - - - - - - + +__register_user() { + local user="${1}" domain="${2}" pass="${3}" + # prosodyctl register is idempotent — re-register just updates the password + su-exec prosody prosodyctl --config "${PROSODY_CFG}" \ + register "${user}" "${domain}" "${pass}" 2>/dev/null || true +} + +__register_jitsi_users() { + # Start prosody temporarily in background so prosodyctl can connect + su-exec prosody prosody --config "${PROSODY_CFG}" -F & + local pid=$! + + # Wait until prosody is accepting connections (up to 30s) + local i=0 + while ! su-exec prosody prosodyctl --config "${PROSODY_CFG}" status >/dev/null 2>&1; do + sleep 1 + i=$(( i + 1 )) + if (( i >= 30 )); then + echo "FATAL: prosody did not start within 30s" >&2 + kill "${pid}" 2>/dev/null || true + exit 1 + fi + done + + echo "INFO: Registering jicofo user" + __register_user "focus" "${XMPP_AUTH_DOMAIN}" "${JICOFO_AUTH_PASSWORD}" + + echo "INFO: Registering jvb user" + __register_user "${JVB_AUTH_USER}" "${XMPP_AUTH_DOMAIN}" "${JVB_AUTH_PASSWORD}" + + # Subscribe focus to the focus component so roster is correct + if [[ "${PROSODY_MODE}" == "client" ]]; then + su-exec prosody prosodyctl --config "${PROSODY_CFG}" \ + mod_roster_command subscribe "focus.${XMPP_DOMAIN}" "focus@${XMPP_AUTH_DOMAIN}" 2>/dev/null || true + fi + + # Optional: jibri user + if [[ -n "${JIBRI_XMPP_PASSWORD:-}" ]]; then + echo "INFO: Registering jibri user" + __register_user "${JIBRI_XMPP_USER}" "${XMPP_AUTH_DOMAIN}" "${JIBRI_XMPP_PASSWORD}" + fi + + # Optional: jigasi user + if [[ -n "${JIGASI_XMPP_PASSWORD:-}" ]]; then + echo "INFO: Registering jigasi user" + __register_user "${JIGASI_XMPP_USER}" "${XMPP_AUTH_DOMAIN}" "${JIGASI_XMPP_PASSWORD}" + fi + + # Shut down the temporary instance + kill "${pid}" 2>/dev/null || true + wait "${pid}" 2>/dev/null || true + # Clean up pid file so the main instance can start + rm -f /config/data/prosody.pid + sleep 1 +} + +# - - - - - - - - - - - - - - - - - - - - - - - - - +# Main +# - - - - - - - - - - - - - - - - - - - - - - - - - + +echo "INFO: Generating prosody configuration" +__generate_main_cfg +__generate_site_cfg +__generate_certs +__register_jitsi_users + +echo "INFO: Starting prosody" +exec su-exec prosody prosody --config "${PROSODY_CFG}" -F diff --git a/docker/rootfs/usr/local/bin/healthcheck.sh b/docker/rootfs/usr/local/bin/healthcheck.sh new file mode 100644 index 0000000..2cded45 --- /dev/null +++ b/docker/rootfs/usr/local/bin/healthcheck.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash +# healthcheck.sh — container health probe +# Returns 0 (healthy) if prosody's HTTP health endpoint responds 200 + +: "${PROSODY_HTTP_PORT:=5280}" + +curl -q -LSs --max-time 5 "http://127.0.0.1:${PROSODY_HTTP_PORT}/health" -o /dev/null