diff --git a/matrix/.env b/matrix/.env new file mode 100644 index 0000000..f857837 --- /dev/null +++ b/matrix/.env @@ -0,0 +1 @@ +REGISTRATION_SHARED_SECRET=5c388ec3c5727803d2f44b6da96e91ca038e9557a5b9cff8c5dc5ace37d6cf83 diff --git a/matrix/create_invite.py b/matrix/create_invite.py deleted file mode 100644 index 112c2f1..0000000 --- a/matrix/create_invite.py +++ /dev/null @@ -1,80 +0,0 @@ -#!/usr/bin/env python3 -""" -Erstellt einen Matrix-Einladungslink für einen neuen User. -Nutzung: python create_invite.py -""" - -import json -import urllib.request -import urllib.error -from datetime import datetime, timedelta - -# Nur für den generierten Link – API-Calls gehen direkt über localhost -PUBLIC_URL = "https://matrix.theocloud.dev" -HOMESERVER = "http://192.168.12.151:8008" -ADMIN_USER = "admin" - - -def login(password: str) -> str: - data = json.dumps({ - "type": "m.login.password", - "user": ADMIN_USER, - "password": password, - }).encode() - - req = urllib.request.Request( - f"{HOMESERVER}/_matrix/client/v3/login", - data=data, - headers={"Content-Type": "application/json"}, - ) - with urllib.request.urlopen(req) as resp: - return json.loads(resp.read())["access_token"] - - -def create_token(access_token: str, uses: int, expires_in_hours: int | None) -> str: - payload: dict = {"uses_allowed": uses} - - if expires_in_hours: - expiry = datetime.now() + timedelta(hours=expires_in_hours) - payload["expiry_time"] = int(expiry.timestamp() * 1000) - - data = json.dumps(payload).encode() - req = urllib.request.Request( - f"{HOMESERVER}/_synapse/admin/v1/registration_tokens/new", - data=data, - headers={ - "Content-Type": "application/json", - "Authorization": f"Bearer {access_token}", - }, - ) - with urllib.request.urlopen(req) as resp: - return json.loads(resp.read())["token"] - - -def main(): - password = input(f"Admin-Passwort für '{ADMIN_USER}': ") - - uses_input = input("Wie oft soll der Link nutzbar sein? [1]: ").strip() - uses = int(uses_input) if uses_input else 1 - - expires_input = input("Ablauf in Stunden? (leer = kein Ablauf): ").strip() - expires_in_hours = int(expires_input) if expires_input else None - - print("\nErstelle Token...") - try: - access_token = login(password) - token = create_token(access_token, uses, expires_in_hours) - except urllib.error.HTTPError as e: - print(f"Fehler: {e.status} – {e.read().decode()}") - return - - link = f"{PUBLIC_URL}/#/register?token={token}" - print(f"\nEinladungslink:\n{link}") - if expires_in_hours: - expires_at = datetime.now() + timedelta(hours=expires_in_hours) - print(f"Gültig bis: {expires_at.strftime('%d.%m.%Y %H:%M')}") - print(f"Nutzungen: {uses}x") - - -if __name__ == "__main__": - main() diff --git a/matrix/docker-compose.yml b/matrix/docker-compose.yml index 45c628c..5964424 100644 --- a/matrix/docker-compose.yml +++ b/matrix/docker-compose.yml @@ -41,30 +41,46 @@ services: if grep -q "^enable_registration:" /data/homeserver.yaml; then sed -i 's/^enable_registration:.*/enable_registration: true/' /data/homeserver.yaml else - echo "enable_registration: true" >> /data/homeserver.yaml + printf '\nenable_registration: true\n' >> /data/homeserver.yaml fi # Nur per Token registrieren erlauben if grep -q "^registration_requires_token:" /data/homeserver.yaml; then sed -i 's/^registration_requires_token:.*/registration_requires_token: true/' /data/homeserver.yaml else - echo "registration_requires_token: true" >> /data/homeserver.yaml + printf '\nregistration_requires_token: true\n' >> /data/homeserver.yaml fi # Shared Secret für register_new_matrix_user CLI if grep -q "^registration_shared_secret:" /data/homeserver.yaml; then sed -i "s/^registration_shared_secret:.*/registration_shared_secret: ${REGISTRATION_SHARED_SECRET}/" /data/homeserver.yaml else - echo "registration_shared_secret: ${REGISTRATION_SHARED_SECRET}" >> /data/homeserver.yaml + printf '\nregistration_shared_secret: %s\n' "${REGISTRATION_SHARED_SECRET}" >> /data/homeserver.yaml fi # Email-Verifizierung deaktiviert (kein SMTP nötig) if grep -q "^enable_registration_without_verification:" /data/homeserver.yaml; then sed -i 's/^enable_registration_without_verification:.*/enable_registration_without_verification: true/' /data/homeserver.yaml else - echo "enable_registration_without_verification: true" >> /data/homeserver.yaml + printf '\nenable_registration_without_verification: true\n' >> /data/homeserver.yaml fi echo "Starting Synapse..." exec /start.py volumes: - synapse_data:/data + well-known: + image: nginx:alpine + container_name: matrix-well-known + restart: unless-stopped + ports: + - 8070:80 + volumes: + - ./nginx/well-known.conf:/etc/nginx/conf.d/default.conf:ro + + invite-app: + build: ./invite-app + container_name: matrix-invite + restart: unless-stopped + ports: + - 8050:8090 + volumes: synapse_data: diff --git a/matrix/docs/setup.md b/matrix/docs/setup.md index 886b1ee..f6b12c7 100644 --- a/matrix/docs/setup.md +++ b/matrix/docs/setup.md @@ -45,7 +45,6 @@ Einmalig nach dem ersten Start: ```bash docker exec -it synapse register_new_matrix_user \ - -c /data/homeserver.yaml \ -u admin \ -p SICHERESPASSWORT \ --admin \ diff --git a/matrix/invite-app/Dockerfile b/matrix/invite-app/Dockerfile new file mode 100644 index 0000000..fecaa49 --- /dev/null +++ b/matrix/invite-app/Dockerfile @@ -0,0 +1,4 @@ +FROM python:3.12-slim +WORKDIR /app +COPY app.py . +CMD ["python", "app.py"] diff --git a/matrix/invite-app/app.py b/matrix/invite-app/app.py new file mode 100644 index 0000000..2436b8c --- /dev/null +++ b/matrix/invite-app/app.py @@ -0,0 +1,139 @@ +#!/usr/bin/env python3 +import json +import urllib.request +import urllib.error +from datetime import datetime, timedelta +from http.server import BaseHTTPRequestHandler, HTTPServer +from urllib.parse import parse_qs + +HOMESERVER = "http://192.168.12.151:8008" +PUBLIC_URL = "https://matrix.theocloud.dev" +ADMIN_USER = "admin" + +HTML = """ + + + + +Matrix Einladung + + + +
+

Matrix Einladungslink

+
+ + + + + + + +
+ {content} +
+ +""" + + +def login(password: str) -> str: + data = json.dumps({ + "type": "m.login.password", + "user": ADMIN_USER, + "password": password, + }).encode() + req = urllib.request.Request( + f"{HOMESERVER}/_matrix/client/v3/login", + data=data, + headers={"Content-Type": "application/json"}, + ) + with urllib.request.urlopen(req) as resp: + return json.loads(resp.read())["access_token"] + + +def create_token(access_token: str, uses: int, expires_in_hours: int | None) -> str: + payload: dict = {"uses_allowed": uses} + if expires_in_hours: + expiry = datetime.now() + timedelta(hours=expires_in_hours) + payload["expiry_time"] = int(expiry.timestamp() * 1000) + data = json.dumps(payload).encode() + req = urllib.request.Request( + f"{HOMESERVER}/_synapse/admin/v1/registration_tokens/new", + data=data, + headers={ + "Content-Type": "application/json", + "Authorization": f"Bearer {access_token}", + }, + ) + with urllib.request.urlopen(req) as resp: + return json.loads(resp.read())["token"] + + +class Handler(BaseHTTPRequestHandler): + def log_message(self, format, *args): + pass + + def send_html(self, content: str, status: int = 200): + body = HTML.format(content=content).encode() + self.send_response(status) + self.send_header("Content-Type", "text/html; charset=utf-8") + self.send_header("Content-Length", len(body)) + self.end_headers() + self.wfile.write(body) + + def do_GET(self): + self.send_html("") + + def do_POST(self): + length = int(self.headers.get("Content-Length", 0)) + body = self.rfile.read(length).decode() + params = parse_qs(body) + + password = params.get("password", [""])[0] + uses = int(params.get("uses", ["1"])[0] or 1) + expires_raw = params.get("expires", [""])[0].strip() + expires_in_hours = int(expires_raw) if expires_raw else None + + try: + access_token = login(password) + token = create_token(access_token, uses, expires_in_hours) + except urllib.error.HTTPError as e: + error_body = e.read().decode() + try: + msg = json.loads(error_body).get("error", error_body) + except Exception: + msg = error_body + self.send_html(f'
{msg}
') + return + + link = f"{PUBLIC_URL}/#/register?token={token}" + meta_parts = [f"{uses}x nutzbar"] + if expires_in_hours: + expires_at = datetime.now() + timedelta(hours=expires_in_hours) + meta_parts.append(f"gültig bis {expires_at.strftime('%d.%m.%Y %H:%M')}") + + self.send_html(f""" +
+ {link} +
{' · '.join(meta_parts)}
+
""") + + +if __name__ == "__main__": + server = HTTPServer(("0.0.0.0", 8090), Handler) + print("Invite app running on http://0.0.0.0:8090") + server.serve_forever() diff --git a/matrix/nginx/well-known.conf b/matrix/nginx/well-known.conf new file mode 100644 index 0000000..2632b9e --- /dev/null +++ b/matrix/nginx/well-known.conf @@ -0,0 +1,22 @@ +server { + listen 80; + + location /.well-known/matrix/server { + default_type application/json; + return 200 '{"m.server":"matrix.theocloud.dev:443"}'; + } + + location /.well-known/matrix/client { + default_type application/json; + add_header Access-Control-Allow-Origin *; + return 200 '{"m.homeserver":{"base_url":"https://matrix.theocloud.dev"}}'; + } + + location / { + proxy_pass http://192.168.12.151:8008; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } +}