mirror of
https://github.com/casjaysdevdocker/prosody
synced 2026-11-12 03:22:28 -05:00
✨ Implement ENABLE_REGISTRATION via user_api module ✨
- docker/rootfs/usr/local/share/prosody-plugins/mod_user_api.lua: new POST /user-api/register endpoint (JSON, validation, reserved names, per-IP rate limit, refuses when AUTH_TYPE is not internal) - docker/rootfs/usr/local/bin/entrypoint.sh: read ENABLE_REGISTRATION, load user_api on the main host only when ENABLE_AUTH is on, warn otherwise - docker/compose.yml: add ENABLE_REGISTRATION - README.md: document registration
This commit is contained in:
4 files changed
+98
No files matched your search
@@ -90,3 +90,12 @@ buildx
|
|||||||
|
|
||||||
Service accounts (`focus`, `jvb`, `jibri`, `jigasi`) always use local `internal_hashed`.
|
Service accounts (`focus`, `jvb`, `jibri`, `jigasi`) always use local `internal_hashed`.
|
||||||
Only SASL PLAIN is offered with `openfire`, since an external check cannot do SCRAM.
|
Only SASL PLAIN is offered with `openfire`, since an external check cannot do SCRAM.
|
||||||
|
|
||||||
|
## Registration
|
||||||
|
|
||||||
|
| Variable | Default | Meaning |
|
||||||
|
|---|---|---|
|
||||||
|
| `ENABLE_REGISTRATION` | `0` | Serve `POST /user-api/register` (used by the jitsi-web `/register` page) |
|
||||||
|
|
||||||
|
Requires `ENABLE_AUTH=1` and `AUTH_TYPE=internal`; otherwise it is ignored (with a warning) or refused (403).
|
||||||
|
Rate limited to 5 attempts per IP per hour; reserved names (`admin`, `focus`, `jvb`, ...) are rejected.
|
||||||
@@ -27,6 +27,7 @@ services:
|
|||||||
ENABLE_XMPP_WEBSOCKET: "1"
|
ENABLE_XMPP_WEBSOCKET: "1"
|
||||||
LOG_LEVEL: info
|
LOG_LEVEL: info
|
||||||
# internal = local accounts only; openfire = also accept Openfire accounts
|
# internal = local accounts only; openfire = also accept Openfire accounts
|
||||||
|
ENABLE_REGISTRATION: "0"
|
||||||
AUTH_TYPE: internal
|
AUTH_TYPE: internal
|
||||||
OPENFIRE_AUTH_ENABLED: "0"
|
OPENFIRE_AUTH_ENABLED: "0"
|
||||||
OPENFIRE_AUTH_HOST: 172.17.0.1
|
OPENFIRE_AUTH_HOST: 172.17.0.1
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ set -euo pipefail
|
|||||||
[[ "${AUTH_TYPE}" == "openfire" ]] && OPENFIRE_AUTH_ENABLED=1
|
[[ "${AUTH_TYPE}" == "openfire" ]] && OPENFIRE_AUTH_ENABLED=1
|
||||||
export OPENFIRE_AUTH_ENABLED
|
export OPENFIRE_AUTH_ENABLED
|
||||||
: "${DISABLE_POLLS:=0}"
|
: "${DISABLE_POLLS:=0}"
|
||||||
|
: "${ENABLE_REGISTRATION:=0}"
|
||||||
|
|
||||||
# Derived: AUTH_TYPE selects the authentication module for user accounts
|
# Derived: AUTH_TYPE selects the authentication module for user accounts
|
||||||
# internal -> internal_hashed (local accounts only)
|
# internal -> internal_hashed (local accounts only)
|
||||||
@@ -239,6 +240,16 @@ VirtualHost "${XMPP_DOMAIN}"
|
|||||||
"features_identity";
|
"features_identity";
|
||||||
SITE_START
|
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
|
# Optional XMPP_MODULES
|
||||||
if [[ -n "${XMPP_MODULES:-}" ]]; then
|
if [[ -n "${XMPP_MODULES:-}" ]]; then
|
||||||
while IFS=',' read -ra mods; do
|
while IFS=',' read -ra mods; do
|
||||||
|
|||||||
@@ -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