Update app.py

This commit is contained in:
2026-05-05 17:35:55 +02:00
committed by GitHub
parent 111f3cf37e
commit 353e57e605
+100 -134
View File
@@ -2,6 +2,7 @@ import json
import time import time
import os import os
import logging import logging
import threading
from collections import defaultdict from collections import defaultdict
from datetime import datetime, timezone 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 log_level = logging.DEBUG if os.getenv("DEBUG", "false").lower() == "true" else logging.INFO
logging.basicConfig(level=log_level) logging.basicConfig(level=log_level)
# 🔥 remove HTTP noise
logging.getLogger("werkzeug").setLevel(logging.ERROR) logging.getLogger("werkzeug").setLevel(logging.ERROR)
logger = logging.getLogger("matrix-interceptor") logger = logging.getLogger("matrix-interceptor")
def now_iso(): def now_iso():
@@ -73,10 +72,6 @@ def log_event(event: str, **kwargs):
details = " ".join(f"{k}={v}" for k, v in kwargs.items()) details = " ".join(f"{k}={v}" for k, v in kwargs.items())
logger.info(f"{base} {details}") 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 # INIT
# ============================================================ # ============================================================
@@ -120,9 +115,6 @@ def is_known_user(user_id):
def remember_user(user_id): def remember_user(user_id):
KNOWN_EXTERNAL_USERS[user_id] = time.time() 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): def is_local_room(room_id):
try: try:
return room_id.split(":")[1] == config.local_domain return room_id.split(":")[1] == config.local_domain
@@ -130,16 +122,13 @@ def is_local_room(room_id):
return False return False
# ============================================================ # ============================================================
# SEED (Membership Cache) # SEED
# ============================================================ # ============================================================
def seed_known_users(): def seed_known_users():
if not config.admin_token: if not config.admin_token:
logger.warning("No ADMIN_TOKEN → skipping seed")
return return
logger.info("Seeding known external users...")
headers = {"Authorization": f"Bearer {config.admin_token}"} headers = {"Authorization": f"Bearer {config.admin_token}"}
seeded = 0 seeded = 0
@@ -154,7 +143,6 @@ def seed_known_users():
return return
for room_id in rooms_res.json().get("joined_rooms", []): for room_id in rooms_res.json().get("joined_rooms", []):
if not is_local_room(room_id): if not is_local_room(room_id):
continue continue
@@ -174,41 +162,114 @@ def seed_known_users():
remember_user(user_id) remember_user(user_id)
seeded += 1 seeded += 1
logger.info(f"Seeded {seeded} external users (local membership)") logger.info(f"Seed refreshed: {seeded} users")
except Exception as e: except Exception as e:
logger.error(f"Seed failed: {e}") logger.error(f"Seed failed: {e}")
# ============================================================ # ============================================================
# DM DETECTION # PERIODIC REFRESH
# ============================================================ # ============================================================
def is_likely_dm_create(payload): def periodic_seed():
if payload.get("is_direct"): while True:
return True seed_known_users()
time.sleep(300) # 5 Minuten
invite = payload.get("invite", []) # ============================================================
# FALLBACK CHECK
# ============================================================
if isinstance(invite, list) and len(invite) == 1: def is_user_in_local_rooms(user_id: str) -> bool:
return True if not config.admin_token:
return False
if payload.get("visibility") == "private" and invite: try:
return True headers = {"Authorization": f"Bearer {config.admin_token}"}
if len(invite) == 1 and not payload.get("name"): rooms_res = requests.get(
return True 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 return False
def is_likely_dm_event(event): # ============================================================
unsigned = event.get("unsigned", {}) # ROUTES
state = unsigned.get("invite_room_state", []) # ============================================================
member_events = [ @app.route("/healthz")
e for e in state if e.get("type") == "m.room.member" def health():
] return {"status": "ok"}
return len(member_events) <= 2 @app.route('/_matrix/federation/v2/invite/<room_id>/<event_id>', 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 # FORWARD
@@ -217,7 +278,6 @@ def is_likely_dm_event(event):
def forward_request(method, url, headers, body): def forward_request(method, url, headers, body):
try: try:
proxy_headers = {"Content-Type": "application/json"} proxy_headers = {"Content-Type": "application/json"}
if "Authorization" in headers: if "Authorization" in headers:
proxy_headers["Authorization"] = headers["Authorization"] proxy_headers["Authorization"] = headers["Authorization"]
@@ -233,112 +293,18 @@ def forward_request(method, url, headers, body):
except Exception as e: except Exception as e:
logger.error(f"proxy error: {e}") logger.error(f"proxy error: {e}")
if config.fail_open:
return Response(status=200)
return Response(status=502) 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/<room_id>/<event_id>', 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 # START
# ============================================================ # ============================================================
if __name__ == '__main__': if __name__ == '__main__':
seed_known_users() seed_known_users()
threading.Thread(
target=periodic_seed,
daemon=True
).start()
app.run(host='0.0.0.0', port=5000) app.run(host='0.0.0.0', port=5000)