Update app.py
This commit is contained in:
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user