diff --git a/app.py b/app.py index aa73d00..b404ef7 100644 --- a/app.py +++ b/app.py @@ -2,6 +2,7 @@ import json import time import os import logging +import threading from collections import defaultdict from datetime import datetime, timezone @@ -60,9 +61,7 @@ class Config: log_level = logging.DEBUG if os.getenv("DEBUG", "false").lower() == "true" else logging.INFO logging.basicConfig(level=log_level) -# 🔥 remove HTTP noise logging.getLogger("werkzeug").setLevel(logging.ERROR) - logger = logging.getLogger("matrix-interceptor") def now_iso(): @@ -73,10 +72,6 @@ def log_event(event: str, **kwargs): details = " ".join(f"{k}={v}" for k, v in kwargs.items()) logger.info(f"{base} {details}") -def debug_log(title, data): - if config.debug: - logger.debug(f"{title}: {json.dumps(data, indent=2, default=str)}") - # ============================================================ # INIT # ============================================================ @@ -120,9 +115,6 @@ def is_known_user(user_id): def remember_user(user_id): KNOWN_EXTERNAL_USERS[user_id] = time.time() -def get_role(user_id): - return "admin" if user_id in config.admin_users else "user" - def is_local_room(room_id): try: return room_id.split(":")[1] == config.local_domain @@ -130,16 +122,13 @@ def is_local_room(room_id): return False # ============================================================ -# SEED (Membership Cache) +# SEED # ============================================================ def seed_known_users(): if not config.admin_token: - logger.warning("No ADMIN_TOKEN → skipping seed") return - logger.info("Seeding known external users...") - headers = {"Authorization": f"Bearer {config.admin_token}"} seeded = 0 @@ -154,7 +143,6 @@ def seed_known_users(): return for room_id in rooms_res.json().get("joined_rooms", []): - if not is_local_room(room_id): continue @@ -174,41 +162,114 @@ def seed_known_users(): remember_user(user_id) seeded += 1 - logger.info(f"Seeded {seeded} external users (local membership)") + logger.info(f"Seed refreshed: {seeded} users") except Exception as e: logger.error(f"Seed failed: {e}") # ============================================================ -# DM DETECTION +# PERIODIC REFRESH # ============================================================ -def is_likely_dm_create(payload): - if payload.get("is_direct"): - return True +def periodic_seed(): + while True: + seed_known_users() + time.sleep(300) # 5 Minuten - invite = payload.get("invite", []) +# ============================================================ +# FALLBACK CHECK +# ============================================================ - if isinstance(invite, list) and len(invite) == 1: - return True +def is_user_in_local_rooms(user_id: str) -> bool: + if not config.admin_token: + return False - if payload.get("visibility") == "private" and invite: - return True + try: + headers = {"Authorization": f"Bearer {config.admin_token}"} - if len(invite) == 1 and not payload.get("name"): - return True + rooms_res = requests.get( + f"{config.tuwunel_url}/_matrix/client/v3/joined_rooms", + headers=headers, + timeout=5 + ) + + if rooms_res.status_code != 200: + return False + + for room_id in rooms_res.json().get("joined_rooms", []): + if not is_local_room(room_id): + continue + + members_res = requests.get( + f"{config.tuwunel_url}/_matrix/client/v3/rooms/{room_id}/joined_members", + headers=headers, + timeout=5 + ) + + if members_res.status_code != 200: + continue + + members = members_res.json().get("joined", {}) + if user_id in members: + return True + + except: + return False return False -def is_likely_dm_event(event): - unsigned = event.get("unsigned", {}) - state = unsigned.get("invite_room_state", []) +# ============================================================ +# ROUTES +# ============================================================ - member_events = [ - e for e in state if e.get("type") == "m.room.member" - ] +@app.route("/healthz") +def health(): + return {"status": "ok"} - return len(member_events) <= 2 +@app.route('/_matrix/federation/v2/invite//', methods=['PUT']) +def invite(room_id, event_id): + payload = request.get_json(force=True) + + event = payload.get("event", {}) + sender = event.get("sender", "") + + if not sender: + return Response(status=400) + + domain = extract_domain(sender) + + # whitelist + if domain in config.domain_whitelist: + remember_user(sender) + return forward_request("PUT", + f"{config.tuwunel_url}/_matrix/federation/v2/invite/{room_id}/{event_id}", + request.headers, payload + ) + + if config.block_external_dms and is_external(sender): + + if not is_known_user(sender): + + # 🔥 FALLBACK CHECK + if is_user_in_local_rooms(sender): + remember_user(sender) + else: + log_event( + "invite_blocked", + actor=sender, + domain=domain, + reason="unknown_external_user" + ) + return Response(status=403) + + remember_user(sender) + + return forward_request( + "PUT", + f"{config.tuwunel_url}/_matrix/federation/v2/invite/{room_id}/{event_id}", + request.headers, + payload + ) # ============================================================ # FORWARD @@ -217,7 +278,6 @@ def is_likely_dm_event(event): def forward_request(method, url, headers, body): try: proxy_headers = {"Content-Type": "application/json"} - if "Authorization" in headers: proxy_headers["Authorization"] = headers["Authorization"] @@ -233,112 +293,18 @@ def forward_request(method, url, headers, body): except Exception as e: logger.error(f"proxy error: {e}") - if config.fail_open: - return Response(status=200) return Response(status=502) -# ============================================================ -# ROUTES -# ============================================================ - -@app.route("/healthz") -def health(): - return { - "status": "ok", - "known_users": len(KNOWN_EXTERNAL_USERS) - } - -@app.route('/_matrix/client/v3/createRoom', methods=['POST']) -def create_room(): - payload = request.get_json(silent=True) or {} - user_id = request.headers.get("Authorization", "unknown") - - is_dm = is_likely_dm_create(payload) - domain = extract_domain(user_id) - role = get_role(user_id) - - allowed = ( - user_id in config.admin_users - or config.allow_room_creation - or is_dm - ) - - if not allowed: - log_event( - "create_room_blocked", - actor=user_id, - domain=domain, - role=role, - reason="room_creation_disabled" - ) - return Response(json.dumps({"errcode": "M_FORBIDDEN"}), status=403) - - if is_dm: - log_event( - "create_room_allowed", - actor=user_id, - domain=domain, - role=role, - room_type="dm" - ) - - return forward_request( - "POST", - f"{config.tuwunel_url}/_matrix/client/v3/createRoom", - request.headers, - payload - ) - -@app.route('/_matrix/federation/v2/invite//', methods=['PUT']) -def invite(room_id, event_id): - payload = request.get_json(force=True) - - event = payload.get("event", {}) - sender = event.get("sender", "") - - if not sender: - return Response(status=400) - - domain = extract_domain(sender) - is_dm = is_likely_dm_event(event) - - if len(RATE_LIMIT[domain]) > config.rate_limit_per_minute: - return Response(status=429) - - if domain in config.domain_whitelist: - remember_user(sender) - return forward_request( - "PUT", - f"{config.tuwunel_url}/_matrix/federation/v2/invite/{room_id}/{event_id}", - request.headers, - payload - ) - - if config.block_external_dms and is_dm and is_external(sender): - if not is_known_user(sender): - - log_event( - "invite_blocked", - actor=sender, - domain=domain, - reason="unknown_external_user" - ) - - return Response(status=403) - - remember_user(sender) - - return forward_request( - "PUT", - f"{config.tuwunel_url}/_matrix/federation/v2/invite/{room_id}/{event_id}", - request.headers, - payload - ) - # ============================================================ # START # ============================================================ if __name__ == '__main__': seed_known_users() + + threading.Thread( + target=periodic_seed, + daemon=True + ).start() + app.run(host='0.0.0.0', port=5000)