Update app.py

This commit is contained in:
2026-05-05 17:35:55 +02:00
committed by GitHub
parent 111f3cf37e
commit 353e57e605
+102 -136
View File
@@ -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"):
def periodic_seed():
while True:
seed_known_users()
time.sleep(300) # 5 Minuten
# ============================================================
# FALLBACK CHECK
# ============================================================
def is_user_in_local_rooms(user_id: str) -> bool:
if not config.admin_token:
return False
try:
headers = {"Authorization": f"Bearer {config.admin_token}"}
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
invite = payload.get("invite", [])
if isinstance(invite, list) and len(invite) == 1:
return True
if payload.get("visibility") == "private" and invite:
return True
if len(invite) == 1 and not payload.get("name"):
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/<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
@@ -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/<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
# ============================================================
if __name__ == '__main__':
seed_known_users()
threading.Thread(
target=periodic_seed,
daemon=True
).start()
app.run(host='0.0.0.0', port=5000)