mirror of
https://github.com/casjaysdevdocker/prosody
synced 2026-11-12 03:22:28 -05:00
♻️ Move Dockerfile and rootfs to repo root per AI.md ♻️
CI / Build image (push) Failing after 1h40m8s
CI / Build image (push) Failing after 1h40m8s
- Dockerfile: moved from docker/Dockerfile; COPY paths now rootfs/ and Dockerfile - rootfs/usr/local/bin/entrypoint.sh, healthcheck.sh: moved from docker/rootfs/ - rootfs/usr/local/share/prosody-plugins/: moved from docker/rootfs/ (mod_auth_openfire_hybrid, mod_user_api) - docker/compose.yml: removed; the example now lives in the README compose section - .github/workflows/ci.yml, release.yml: build file is now Dockerfile - .github/CODEOWNERS: drop the docker/ entry - .gitignore, .dockerignore: drop docker/volumes/ - README.md: compose section replaced with the current example
This commit is contained in:
12 files changed
+59
-78
No files matched your search
@@ -0,0 +1,510 @@
|
||||
#!/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}"
|
||||
: "${OPENFIRE_AUTH_ENABLED:=0}"
|
||||
[[ "${AUTH_TYPE}" == "openfire" ]] && OPENFIRE_AUTH_ENABLED=1
|
||||
export OPENFIRE_AUTH_ENABLED
|
||||
: "${DISABLE_POLLS:=0}"
|
||||
: "${ENABLE_REGISTRATION:=0}"
|
||||
|
||||
# Derived: AUTH_TYPE selects the authentication module for user accounts
|
||||
# internal -> internal_hashed (local accounts only)
|
||||
# openfire -> openfire_hybrid (local accounts, then Openfire via OPENFIRE_AUTH_*)
|
||||
case "${AUTH_TYPE}" in
|
||||
internal) USER_AUTH_MODULE="internal_hashed" ;;
|
||||
openfire) USER_AUTH_MODULE="openfire_hybrid" ;;
|
||||
*) echo "FATAL: AUTH_TYPE must be internal or openfire (got: ${AUTH_TYPE})" >&2; exit 1 ;;
|
||||
esac
|
||||
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 = "${USER_AUTH_MODULE}"
|
||||
|
||||
-- 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", "/usr/local/share/prosody-plugins" };
|
||||
|
||||
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="${USER_AUTH_MODULE}"
|
||||
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
|
||||
|
||||
# Registration endpoint used by the jitsi-web /register page
|
||||
# Needs ENABLE_AUTH: anonymous auth has nowhere to store accounts
|
||||
if [[ "${ENABLE_REGISTRATION}" == "1" ]] || [[ "${ENABLE_REGISTRATION}" == "true" ]]; then
|
||||
if [[ "${main_auth}" == "jitsi-anonymous" ]]; then
|
||||
echo "WARN: ENABLE_REGISTRATION ignored: requires ENABLE_AUTH=1" >&2
|
||||
else
|
||||
printf ' "user_api";\n' >> "${PROSODY_SITE_CFG}"
|
||||
fi
|
||||
fi
|
||||
|
||||
# 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
|
||||
@@ -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
|
||||
@@ -0,0 +1,277 @@
|
||||
-- Modified copy of Prosody's mod_auth_internal_hashed (MIT/X11, see Prosody's COPYING).
|
||||
-- Selected with AUTH_TYPE=openfire (authentication = "openfire_hybrid"); it
|
||||
-- accepts local accounts and also accounts that exist in Openfire. Local credentials are tried first; on failure the password is
|
||||
-- verified against Openfire (direct-TLS c2s, SASL PLAIN, certificate and
|
||||
-- hostname verified). A local record is created on the first success.
|
||||
-- Settings come from the environment: OPENFIRE_AUTH_ENABLED, OPENFIRE_AUTH_HOST,
|
||||
-- OPENFIRE_AUTH_PORT, OPENFIRE_AUTH_DOMAIN, OPENFIRE_AUTH_EXCLUDE.
|
||||
-- Only SASL PLAIN is offered, because an external check cannot do SCRAM.
|
||||
--
|
||||
-- Prosody IM
|
||||
-- Copyright (C) 2008-2010 Matthew Wild
|
||||
-- Copyright (C) 2008-2010 Waqas Hussain
|
||||
-- Copyright (C) 2010 Jeff Mitchell
|
||||
--
|
||||
-- This project is MIT/X11 licensed. Please see the
|
||||
-- COPYING file in the source package for more information.
|
||||
--
|
||||
|
||||
local max = math.max;
|
||||
|
||||
local scram_hashers = require "prosody.util.sasl.scram".hashers;
|
||||
local generate_uuid = require "prosody.util.uuid".generate;
|
||||
local new_sasl = require "prosody.util.sasl".new;
|
||||
local hex = require"prosody.util.hex";
|
||||
local to_hex, from_hex = hex.encode, hex.decode;
|
||||
local saslprep = require "prosody.util.encodings".stringprep.saslprep;
|
||||
local secure_equals = require "prosody.util.hashes".equals;
|
||||
|
||||
local log = module._log;
|
||||
local host = module.host;
|
||||
|
||||
local accounts = module:open_store("accounts");
|
||||
|
||||
local hash_name = module:get_option_enum("password_hash", "SHA-1", "SHA-256");
|
||||
local get_auth_db = assert(scram_hashers[hash_name], "SCRAM-"..hash_name.." not supported by SASL library");
|
||||
local scram_name = "scram_"..hash_name:gsub("%-","_"):lower();
|
||||
|
||||
-- Default; can be set per-user
|
||||
local default_iteration_count = module:get_option_integer("default_iteration_count", 10000, 4096);
|
||||
|
||||
|
||||
-- define auth provider
|
||||
local provider = {};
|
||||
|
||||
local function local_test_password(username, password)
|
||||
log("debug", "test password for user '%s'", username);
|
||||
local credentials = accounts:get(username) or {};
|
||||
if credentials.disabled then
|
||||
return nil, "Account disabled.";
|
||||
end
|
||||
password = saslprep(password);
|
||||
if not password then
|
||||
return nil, "Password fails SASLprep.";
|
||||
end
|
||||
|
||||
if credentials.password ~= nil and string.len(credentials.password) ~= 0 then
|
||||
if not secure_equals(saslprep(credentials.password), password) then
|
||||
return nil, "Auth failed. Provided password is incorrect.";
|
||||
end
|
||||
|
||||
if provider.set_password(username, credentials.password) == nil then
|
||||
return nil, "Auth failed. Could not set hashed password from plaintext.";
|
||||
else
|
||||
return true;
|
||||
end
|
||||
end
|
||||
|
||||
if credentials.iteration_count == nil or credentials.salt == nil or string.len(credentials.salt) == 0 then
|
||||
return nil, "Auth failed. Stored salt and iteration count information is not complete.";
|
||||
end
|
||||
|
||||
local valid, stored_key, server_key = get_auth_db(password, credentials.salt, credentials.iteration_count);
|
||||
|
||||
local stored_key_hex = to_hex(stored_key);
|
||||
local server_key_hex = to_hex(server_key);
|
||||
|
||||
if valid and secure_equals(stored_key_hex, credentials.stored_key) and secure_equals(server_key_hex, credentials.server_key) then
|
||||
return true;
|
||||
else
|
||||
return nil, "Auth failed. Invalid username, password, or password hash information.";
|
||||
end
|
||||
end
|
||||
|
||||
local socket = require "socket";
|
||||
local ssl = require "ssl";
|
||||
local b64 = require "prosody.util.encodings".base64;
|
||||
local x509 = require "prosody.util.x509";
|
||||
|
||||
local of_enabled = (os.getenv("OPENFIRE_AUTH_ENABLED") or "0"):lower();
|
||||
of_enabled = of_enabled == "1" or of_enabled == "true" or of_enabled == "yes";
|
||||
local of_host = os.getenv("OPENFIRE_AUTH_HOST") or "172.17.0.1";
|
||||
local of_port = tonumber(os.getenv("OPENFIRE_AUTH_PORT") or "5223");
|
||||
local of_domain = os.getenv("OPENFIRE_AUTH_DOMAIN") or "";
|
||||
if of_domain == "" then of_enabled = false; end
|
||||
local of_excluded = {};
|
||||
for name in (os.getenv("OPENFIRE_AUTH_EXCLUDE") or "focus,jvb,jibri,recorder"):gmatch("[^,%s]+") do
|
||||
of_excluded[name:lower()] = true;
|
||||
end
|
||||
|
||||
local function xml_escape(text)
|
||||
return (text:gsub("[&<>'\"]", { ["&"] = "&", ["<"] = "<", [">"] = ">", ["'"] = "'", ['"'] = """ }));
|
||||
end
|
||||
|
||||
local function read_until(sock, needles, limit)
|
||||
local buf = "";
|
||||
while #buf < limit do
|
||||
for _, needle in ipairs(needles) do
|
||||
if buf:find(needle, 1, true) then return buf, needle; end
|
||||
end
|
||||
local ch = sock:receive(1);
|
||||
if not ch then return buf, nil; end
|
||||
buf = buf .. ch;
|
||||
end
|
||||
return buf, nil;
|
||||
end
|
||||
|
||||
local function openfire_check(username, password)
|
||||
local tcp = socket.tcp();
|
||||
tcp:settimeout(5);
|
||||
local connected, cerr = tcp:connect(of_host, of_port);
|
||||
if not connected then
|
||||
log("warn", "Openfire auth: cannot connect to %s:%d: %s", of_host, of_port, tostring(cerr));
|
||||
return nil;
|
||||
end
|
||||
local sock = ssl.wrap(tcp, {
|
||||
mode = "client", protocol = "any",
|
||||
options = { "all", "no_sslv2", "no_sslv3", "no_tlsv1", "no_tlsv1_1" },
|
||||
verify = { "peer" }, verifyext = { "lsec_continue", "lsec_ignore_purpose" },
|
||||
cafile = "/etc/ssl/certs/ca-certificates.crt",
|
||||
});
|
||||
sock:sni(of_domain);
|
||||
sock:settimeout(5);
|
||||
local hs_ok, hs_err = sock:dohandshake();
|
||||
if not hs_ok then
|
||||
log("warn", "Openfire auth: TLS handshake failed: %s", tostring(hs_err));
|
||||
sock:close();
|
||||
return nil;
|
||||
end
|
||||
local cert = sock:getpeercertificate();
|
||||
if not cert or not x509.verify_identity(of_domain, "xmpp-client", cert) then
|
||||
log("warn", "Openfire auth: certificate does not match %s", of_domain);
|
||||
sock:close();
|
||||
return nil;
|
||||
end
|
||||
sock:send("<?xml version='1.0'?><stream:stream to='" .. xml_escape(of_domain)
|
||||
.. "' xmlns='jabber:client' xmlns:stream='http://etherx.jabber.org/streams' version='1.0'>");
|
||||
if not read_until(sock, { "</stream:features>" }, 16384) then
|
||||
sock:close();
|
||||
return nil;
|
||||
end
|
||||
local token = b64.encode("\0" .. username .. "\0" .. password);
|
||||
sock:send("<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl' mechanism='PLAIN'>" .. token .. "</auth>");
|
||||
local _, found = read_until(sock, { "<success", "<failure" }, 4096);
|
||||
sock:send("</stream:stream>");
|
||||
sock:close();
|
||||
return found == "<success";
|
||||
end
|
||||
|
||||
function provider.test_password(username, password)
|
||||
local ok, err = local_test_password(username, password);
|
||||
if ok then return ok; end
|
||||
if not of_enabled or of_excluded[username:lower()] then return ok, err; end
|
||||
local credentials = accounts:get(username);
|
||||
if credentials and credentials.disabled then return ok, err; end
|
||||
if openfire_check(username, password) then
|
||||
if not credentials then
|
||||
provider.create_user(username, generate_uuid());
|
||||
log("info", "Created local record for Openfire user '%s'", username);
|
||||
end
|
||||
log("debug", "Openfire accepted user '%s'", username);
|
||||
return true;
|
||||
end
|
||||
return ok, err;
|
||||
end
|
||||
|
||||
function provider.set_password(username, password)
|
||||
log("debug", "set_password for username '%s'", username);
|
||||
local account = accounts:get(username);
|
||||
if account then
|
||||
account.salt = generate_uuid();
|
||||
account.iteration_count = max(account.iteration_count or 0, default_iteration_count);
|
||||
local valid, stored_key, server_key = get_auth_db(password, account.salt, account.iteration_count);
|
||||
if not valid then
|
||||
return valid, stored_key;
|
||||
end
|
||||
local stored_key_hex = to_hex(stored_key);
|
||||
local server_key_hex = to_hex(server_key);
|
||||
|
||||
account.stored_key = stored_key_hex
|
||||
account.server_key = server_key_hex
|
||||
|
||||
account.password = nil;
|
||||
account.updated = os.time();
|
||||
return accounts:set(username, account);
|
||||
end
|
||||
return nil, "Account not available.";
|
||||
end
|
||||
|
||||
function provider.get_account_info(username)
|
||||
local account = accounts:get(username);
|
||||
if not account then return nil, "Account not available"; end
|
||||
return {
|
||||
created = account.created;
|
||||
password_updated = account.updated;
|
||||
enabled = not account.disabled;
|
||||
};
|
||||
end
|
||||
|
||||
function provider.user_exists(username)
|
||||
local account = accounts:get(username);
|
||||
if not account then
|
||||
log("debug", "account not found for username '%s'", username);
|
||||
return nil, "Auth failed. Invalid username";
|
||||
end
|
||||
return true;
|
||||
end
|
||||
|
||||
function provider.is_enabled(username) -- luacheck: ignore 212
|
||||
local info, err = provider.get_account_info(username);
|
||||
if not info then return nil, err; end
|
||||
return info.enabled;
|
||||
end
|
||||
|
||||
function provider.enable(username)
|
||||
-- TODO map store?
|
||||
local account = accounts:get(username);
|
||||
account.disabled = nil;
|
||||
account.updated = os.time();
|
||||
return accounts:set(username, account);
|
||||
end
|
||||
|
||||
function provider.disable(username, meta)
|
||||
local account = accounts:get(username);
|
||||
account.disabled = true;
|
||||
account.disabled_meta = meta;
|
||||
account.updated = os.time();
|
||||
return accounts:set(username, account);
|
||||
end
|
||||
|
||||
function provider.users()
|
||||
return accounts:users();
|
||||
end
|
||||
|
||||
function provider.create_user(username, password)
|
||||
local now = os.time();
|
||||
if password == nil then
|
||||
return accounts:set(username, { created = now; updated = now; disabled = true });
|
||||
end
|
||||
local salt = generate_uuid();
|
||||
local valid, stored_key, server_key = get_auth_db(password, salt, default_iteration_count);
|
||||
if not valid then
|
||||
return valid, stored_key;
|
||||
end
|
||||
local stored_key_hex = to_hex(stored_key);
|
||||
local server_key_hex = to_hex(server_key);
|
||||
return accounts:set(username, {
|
||||
stored_key = stored_key_hex, server_key = server_key_hex,
|
||||
salt = salt, iteration_count = default_iteration_count,
|
||||
created = now, updated = now;
|
||||
});
|
||||
end
|
||||
|
||||
function provider.delete_user(username)
|
||||
return accounts:set(username, nil);
|
||||
end
|
||||
|
||||
function provider.get_sasl_handler()
|
||||
local testpass_authentication_profile = {
|
||||
plain_test = function(_, username, password)
|
||||
return provider.test_password(username, password), provider.is_enabled(username);
|
||||
end,
|
||||
};
|
||||
return new_sasl(host, testpass_authentication_profile);
|
||||
end
|
||||
|
||||
module:provides("auth", provider);
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
-- Account registration endpoint for the jitsi-web /register page.
|
||||
-- POST /user-api/register with JSON {"username": "...", "password": "..."}
|
||||
-- creates a local account on this virtual host. Loaded only when
|
||||
-- ENABLE_REGISTRATION is true; refuses when accounts live in Openfire.
|
||||
module:depends("http");
|
||||
|
||||
local json = require "prosody.util.json";
|
||||
local nodeprep = require "prosody.util.encodings".stringprep.nodeprep;
|
||||
local usermanager = require "prosody.core.usermanager";
|
||||
|
||||
local host = module.host;
|
||||
local auth_type = (os.getenv("AUTH_TYPE") or "internal"):lower();
|
||||
local reserved = {};
|
||||
for name in ("admin,administrator,root,system,guest,"
|
||||
.. (os.getenv("OPENFIRE_AUTH_EXCLUDE") or "focus,jvb,jibri,recorder")
|
||||
.. ",jigasi,transcriber"):gmatch("[^,%s]+") do
|
||||
reserved[name:lower()] = true;
|
||||
end
|
||||
|
||||
local max_per_hour = module:get_option_number("user_api_max_per_hour", 5);
|
||||
local attempts = {};
|
||||
|
||||
local function reply(response, code, body)
|
||||
response.status_code = code;
|
||||
response.headers.content_type = "application/json";
|
||||
return json.encode(body);
|
||||
end
|
||||
|
||||
local function rate_limited(ip)
|
||||
local now = os.time();
|
||||
local entry = attempts[ip];
|
||||
if not entry or now - entry.start > 3600 then
|
||||
entry = { start = now, count = 0 };
|
||||
attempts[ip] = entry;
|
||||
end
|
||||
entry.count = entry.count + 1;
|
||||
return entry.count > max_per_hour;
|
||||
end
|
||||
|
||||
local function register(event)
|
||||
local request, response = event.request, event.response;
|
||||
if auth_type ~= "internal" then
|
||||
return reply(response, 403, { error = "Registration is handled by the XMPP server" });
|
||||
end
|
||||
if rate_limited(request.ip or "unknown") then
|
||||
return reply(response, 429, { error = "Too many attempts, try again later" });
|
||||
end
|
||||
local data = request.body and json.decode(request.body);
|
||||
if type(data) ~= "table" or type(data.username) ~= "string" or type(data.password) ~= "string" then
|
||||
return reply(response, 400, { error = "username and password are required" });
|
||||
end
|
||||
local username = nodeprep(data.username:lower());
|
||||
if not username or not username:match("^[a-z0-9][a-z0-9._-]*$") or #username > 64 then
|
||||
return reply(response, 400, { error = "Invalid username" });
|
||||
end
|
||||
if reserved[username] then
|
||||
return reply(response, 403, { error = "This username is not allowed" });
|
||||
end
|
||||
if #data.password < 8 or #data.password > 256 then
|
||||
return reply(response, 400, { error = "Password must be 8 to 256 characters" });
|
||||
end
|
||||
if usermanager.user_exists(username, host) then
|
||||
return reply(response, 409, { error = "Username already taken" });
|
||||
end
|
||||
local ok, err = usermanager.create_user(username, data.password, host);
|
||||
if not ok then
|
||||
module:log("warn", "Registration of %s failed: %s", username, tostring(err));
|
||||
return reply(response, 500, { error = "Registration failed" });
|
||||
end
|
||||
module:log("info", "Registered account %s@%s", username, host);
|
||||
return reply(response, 201, { status = "created" });
|
||||
end
|
||||
|
||||
module:provides("http", {
|
||||
default_path = "/user-api";
|
||||
route = { ["POST /register"] = register };
|
||||
});
|
||||
Reference in new issue
Block a user