diff --git a/mode/jwt.sh b/mode/jwt.sh index c69a4dc..3124ecc 100644 --- a/mode/jwt.sh +++ b/mode/jwt.sh @@ -14,16 +14,18 @@ done #DEBUG if [ "$MODE" = "debug" ]; then -set -x + set -x fi DOMAIN="$(find /etc/prosody/conf.d/ -name \*.lua|awk -F'.cfg' '!/localhost/{print $1}'|xargs basename)" MEET_CONF="/etc/jitsi/meet/$DOMAIN-config.js" JICOFO_SIP="/etc/jitsi/jicofo/sip-communicator.properties" +JICOFO_CONF="/etc/jitsi/jicofo/jicofo.conf" PROSODY_FILE="/etc/prosody/conf.d/$DOMAIN.cfg.lua" PROSODY_SYS="/etc/prosody/prosody.cfg.lua" APP_ID="$(tr -dc "a-zA-Z0-9" < /dev/urandom | fold -w 16 | head -n1)" SECRET_APP="$(tr -dc "a-zA-Z0-9" < /dev/urandom | fold -w 64 | head -n1)" +ROOM="Two-Hour-Test-Room" SRP_STR="$(grep -n "VirtualHost \"$DOMAIN\"" "$PROSODY_FILE" | head -n1 | cut -d ":" -f1)" SRP_END="$((SRP_STR + 10))" @@ -37,6 +39,15 @@ if command -v prosodyctl >/dev/null 2>&1; then esac fi +# Custom 5.4 lua workaround for prosody 0.12 +echo "Warning: Ubuntu 22.04/24.04 don't ship the required lua inspect module 5.4," +echo " so, we work arround it, be careful on further upgrades or changes." +install -d -m 755 /usr/share/lua/5.4 +ln -sf /usr/share/lua/5.3/inspect.lua /usr/share/lua/5.4/inspect.lua +systemctl restart prosody jicofo jitsi-videobridge2 + +sleep .1 + # Install dependencies apt-get -y install python3-jwt @@ -45,41 +56,36 @@ echo "set jitsi-meet-tokens/appsecret password $SECRET_APP" | debconf-set-select apt-get install -y jitsi-meet-tokens -# Setting up +# Setting up prosody sed -i "s|c2s_require_encryption = true|c2s_require_encryption = false|" "$PROSODY_SYS" #- sed -i "$SRP_STR,$SRP_END{s|authentication = \"jitsi-anonymous\"|authentication = \"token\"|}" "$PROSODY_FILE" sed -i "s|--app_id=\"example_app_id\"|app_id=\"$APP_ID\"|" "$PROSODY_FILE" sed -i "s|--app_secret=\"example_app_secret\"|app_secret=\"$SECRET_APP\"|" "$PROSODY_FILE" sed -i "/app_secret/a \\\\" "$PROSODY_FILE" +## Only token owners can create, open the room and become moderators: allow_empty_token = false +## other participants are redirected authentication to guest. sed -i "/app_secret/a \ \ \ \ allow_empty_token = false" "$PROSODY_FILE" sed -i "/app_secret/a \\\\" "$PROSODY_FILE" sed -i "/app_secret/a \ \ \ \ asap_accepted_issuers = { \"$APP_ID\" }" "$PROSODY_FILE" -sed -i "/app_secret/a \ \ \ \ asap_accepted_audiences = { \"$APP_ID\", \"RocketChat\" }" "$PROSODY_FILE" +sed -i "/app_secret/a \ \ \ \ asap_accepted_audiences = { \"$APP_ID\" }" "$PROSODY_FILE" sed -i "/app_secret/a \\\\" "$PROSODY_FILE" sed -i "s|--allow_empty_token =.*|allow_empty_token = false|" "$PROSODY_FILE" sed -i 's|--"token_verification"|"token_verification"|' "$PROSODY_FILE" +sed -i "/muc_lobby_rooms/a \ \ \ \ \ \ \ \ \"persistent_lobby\";" "$PROSODY_FILE" +sed -i "/token_verification/a \ \ \ \ \ \ \ \ \"muc_wait_for_host\";" "$PROSODY_FILE" -# Request auth -## JWT via Prosody: don't touch Jicofo -#sed -i "s|#org.jitsi.jicofo.auth.URL=EXT_JWT:|org.jitsi.jicofo.auth.URL=EXT_JWT:|" "$JICOFO_SIP" +# Set JWT and Guest settings +## Harden JWT auth, preventing "free" moderator by racing into room, +## only participants with token with moderator:true. +sed -i '1ijicofo.conference.enable-auto-owner = false' "$JICOFO_CONF" +## config.js sed -i "s|// anonymousdomain: 'guest.example.com'|anonymousdomain: \'guest.$DOMAIN\'|" "$MEET_CONF" -# Enable jibri recording -cat << REC-JIBRI >> "$PROSODY_FILE" - -VirtualHost "recorder.$DOMAIN" - modules_enabled = { - "ping"; - } - authentication = "internal_hashed" - -REC-JIBRI - # Setup guests and lobby cat << P_SR >> "$PROSODY_FILE" VirtualHost "guest.$DOMAIN" - authentication = "jitsi-anonymous" + authentication = "anonymous" c2s_require_encryption = false speakerstats_component = "speakerstats.$DOMAIN" @@ -93,15 +99,12 @@ echo -e "\nUse the following for your App (e.g. Rocket.Chat):\n" echo -e "\nAPP_ID: $APP_ID" && \ echo -e "SECRET_APP: $SECRET_APP\n" -echo -e "You can test JWT authentication with the following token for the next hour:\n" -pyjwt3 --key="$SECRET_APP" \ - encode \ - --alg HS256 \ - group="Rocket.Chat" \ - aud="$APP_ID" \ - iss="$APP_ID" \ - sub="$DOMAIN" \ - room="*" \ - exp="$(($(date +%s)+3600))" +echo -e "You can test JWT authentication with the following token for the next 2 hours:\n" +python3 tools/jitsi_token_maker_features.py \ + --app-id "$APP_ID" --secret-file - \ + --domain "$DOMAIN" --room "$ROOM" \ + --moderator --features-all \ + --minutes 120 --nbf-offset 300 --include-iat \ + --url "https://$DOMAIN/" <<<"$APP_SECRET" read -n 1 -s -r -p $'\n'"Press any key to continue..."$'\n' diff --git a/tools/jitsi_token_maker_features.py b/tools/jitsi_token_maker_features.py new file mode 100755 index 0000000..8bb30a2 --- /dev/null +++ b/tools/jitsi_token_maker_features.py @@ -0,0 +1,158 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +JWT generator for self‑hosted Jitsi (also compatible with JAAS if desired) +- HS256 (HMAC) signing using only Python standard library (no external deps). +- Flags to omit exp/nbf (test tokens), include iat, and read secret from file/STDIN. +- Flags to populate context.features: recording, livestreaming, transcription, sip-in/out. +- Robust URL construction (escapes the room name). +""" +import argparse, base64, hashlib, hmac, json, time, sys +from urllib.parse import quote + +def b64url(data: bytes) -> str: + return base64.urlsafe_b64encode(data).rstrip(b"=").decode("ascii") + +def sign_hs256(secret: str, signing_input: str) -> str: + sig = hmac.new(secret.encode("utf-8"), signing_input.encode("ascii"), hashlib.sha256).digest() + return b64url(sig) + +def main(): + p = argparse.ArgumentParser(description="JWT generator for Jitsi (HS256)") + # Identity / target + p.add_argument("--app-id", required=True, help="app_id configured in Prosody/JAAS") + p.add_argument("--secret", required=False, help="app_secret (HMAC/HS256)") + p.add_argument("--secret-file", help="Read secret from file or '-' for STDIN") + p.add_argument("--domain", help="Jitsi domain (e.g. meet.example.com) used as 'sub' in self-hosted") + p.add_argument("--room", default="*", help="Target room (or '*' for all)") + # Time + p.add_argument("--minutes", type=int, default=60, help="Validity (minutes). Ignored if --no-exp") + p.add_argument("--no-exp", action="store_true", help="Do not include 'exp' (tests only)") + p.add_argument("--nbf-offset", type=int, default=10, help="Backdating seconds for 'nbf' (default: 10)") + p.add_argument("--no-nbf", action="store_true", help="Do not include 'nbf' (tests only)") + p.add_argument("--include-iat", action="store_true", help="Include 'iat'=now") + # User + p.add_argument("--user-name", default=None, help="User display name") + p.add_argument("--user-email", default=None, help="User email") + p.add_argument("--user-id", default=None, help="User unique ID") + p.add_argument("--avatar", default=None, help="Avatar URL") + p.add_argument("--moderator", action="store_true", help="Grant moderator role via token") + p.add_argument("--moderator-as-string", action="store_true", + help="Use 'moderator': 'true'/'false' (string) instead of boolean") + # Features (self-hosted with enableFeaturesBasedOnToken) + p.add_argument("--feature-recording", action="store_true", help="Enable 'recording' in context.features") + p.add_argument("--feature-livestreaming", action="store_true", help="Enable 'livestreaming' in context.features") + p.add_argument("--feature-transcription", action="store_true", help="Enable 'transcription' in context.features") + p.add_argument("--feature-sip-in", action="store_true", help="Enable 'sip-inbound-call' in context.features") + p.add_argument("--feature-sip-out", action="store_true", help="Enable 'sip-outbound-call' in context.features") + p.add_argument("--features-all", action="store_true", help="Enable all the features above") + # Overrides / modes + p.add_argument("--aud", default=None, help="Override 'aud' (default: app_id in self-hosted)") + p.add_argument("--iss", default=None, help="Override 'iss' (default: app_id in self-hosted)") + p.add_argument("--jaas", action="store_true", + help="JAAS mode: aud='jitsi', iss='chat', sub=app_id (ignores --domain for 'sub')") + # Output + p.add_argument("--url", default=None, + help="If provided (e.g. 'https://meet.example.com/'), prints full join URL with ?jwt=") + p.add_argument("--print-json", action="store_true", help="Print payload JSON to STDERR (debug)") + + args = p.parse_args() + + # Secret: --secret-file takes precedence + secret = args.secret + if args.secret_file: + if args.secret_file == "-": + secret = sys.stdin.read().strip() + else: + with open(args.secret_file, "r", encoding="utf-8") as fh: + secret = fh.read().strip() + if not secret: + p.error("You must provide --secret or --secret-file (or --secret-file - for STDIN).") + + now = int(time.time()) + exp = None if args.no_exp else (now + args.minutes * 60) + nbf = None if args.no_nbf else (now - max(args.nbf_offset, 0)) + + # Header + header = {"typ": "JWT", "alg": "HS256"} + + # Base claims by mode + if args.jaas: + aud = "jitsi" + iss = "chat" + sub = args.app_id + else: + if not args.domain: + p.error("--domain is required in self-hosted mode (without --jaas).") + aud = args.aud or args.app_id + iss = args.iss or args.app_id + sub = args.domain + + # User / contexto + user = {} + if args.user_id: user["id"] = args.user_id + if args.user_name: user["name"] = args.user_name + if args.user_email: user["email"] = args.user_email + if args.avatar: user["avatar"] = args.avatar + if args.moderator: + if args.moderator_as_string: + user["moderator"] = "true" + else: + user["moderator"] = True + + # Features + features = {} + if args.features_all: + features = { + "recording": True, + "livestreaming": True, + "transcription": True, + "sip-inbound-call": True, + "sip-outbound-call": True + } + else: + if args.feature_recording: features["recording"] = True + if args.feature_livestreaming: features["livestreaming"] = True + if args.feature_transcription: features["transcription"] = True + if args.feature_sip_in: features["sip-inbound-call"] = True + if args.feature_sip_out: features["sip-outbound-call"] = True + + context = {} + if user: context["user"] = user + if features: context["features"] = features + + payload = { + "aud": aud, + "iss": iss, + "sub": sub, + "room": args.room, + } + if context: + payload["context"] = context + if exp is not None: + payload["exp"] = exp + if nbf is not None: + payload["nbf"] = nbf + if args.include_iat: + payload["iat"] = now + + # Build JWT manually + signing_input = f"{b64url(json.dumps(header, separators=(',', ':'), ensure_ascii=False).encode())}." \ + f"{b64url(json.dumps(payload, separators=(',', ':'), ensure_ascii=False).encode())}" + signature = sign_hs256(secret, signing_input) + token = f"{signing_input}.{signature}" + + if args.print_json: + print(json.dumps(payload, indent=2, ensure_ascii=False), file=sys.stderr) + + if args.url: + base = args.url if args.url.endswith("/") else args.url + "/" + room_path = "" if args.room == "*" else quote(args.room, safe="") + join_url = base + room_path + sep = "&" if "?" in join_url else "?" + print(f"{join_url}{sep}jwt={token}") + else: + print(token) + +if __name__ == "__main__": + main()