20 Commits
1.4.2 ... 1.4.4

Author SHA1 Message Date
492ffc62e9 1.4.4 2025-10-17 17:49:30 -04:00
9ca4c913f3 Make sending Matrix requests easier 2025-10-17 17:44:41 -04:00
6dd4fb04e3 Follow upstream Hammerhead response for state-resolver 2025-10-17 15:49:19 -04:00
f771130a16 Implement MSC4367 room directory 2025-10-15 13:46:00 -04:00
9aa2e062e5 Better logging (for the 3rd time), fix devices list over federation 2025-10-13 17:55:48 -04:00
c8d6f57e5b add security policy 2025-10-13 16:08:17 -04:00
753625ad5c some Citadel endpoints 2025-10-13 15:44:57 -04:00
74a36e0b34 Import configuration properly 2025-10-13 15:20:01 -04:00
b178011ad9 Remove padding from base64 hashes 2025-10-13 11:12:39 -04:00
76f89beb29 Add backfill 2025-10-13 10:38:24 -04:00
7e8eb6de7f msc: 4358 2025-10-13 09:53:56 -04:00
4e28507dea Add Hammerhead endpoints 2025-10-12 23:39:25 -04:00
357d3b7429 Add resolvematrix dependency 2025-10-12 20:36:57 -04:00
8a2bae706e Implement Telodendria admin API 2025-10-12 18:43:01 -04:00
4603ed86ac Support environment variable to specify configuration path 2025-10-12 17:05:32 -04:00
4a6e65226e Add validation for POST/PUT/PATCH requests
This was made on my phone
2025-10-11 20:02:24 -04:00
fc9787b8ec Add room join script 2025-10-08 19:59:14 -04:00
2dbec63ff7 Support room version 1 on invitations 2025-10-08 18:10:15 -04:00
2021fc027b Fix auth_events for guest access and history visibility 2025-10-08 17:50:56 -04:00
e0115fe8db Add federation self-testing 2025-10-08 13:52:24 -04:00
15 changed files with 589 additions and 109 deletions

3
SECURITY.md Normal file
View File

@@ -0,0 +1,3 @@
# Security
If you believe you have found a vulnerability in Vona, please email vel@riseup.net or DM `@vel:faelix.im` on Matrix.
Do not disclose the details of the vulnerability until a public fix has been made.

1
TODO.md Normal file
View File

@@ -0,0 +1 @@
Nothing yet...

View File

@@ -8,6 +8,7 @@ dependencies = [
"httpx (>=0.28.1,<0.29.0)", "httpx (>=0.28.1,<0.29.0)",
"pynacl (>=1.6.0,<2.0.0)", "pynacl (>=1.6.0,<2.0.0)",
"flask[async] (>=3.1.2,<4.0.0)", "flask[async] (>=3.1.2,<4.0.0)",
"resolvematrix @ git+https://codeberg.org/timedout/resolvematrix.git",
] ]
authors = [ authors = [

View File

@@ -2,7 +2,9 @@ from flask import Flask, jsonify, request, redirect
import vona.globals as globals import vona.globals as globals
from datetime import datetime from datetime import datetime
import vona.config as config import vona.config as config
import threading
import logging import logging
import os
from vona.federation import server from vona.federation import server
from vona.custom import custom from vona.custom import custom
@@ -24,9 +26,20 @@ app.register_blueprint(server)
app.register_blueprint(apps) app.register_blueprint(apps)
@app.before_request @app.before_request
async def preflight(): async def validate_json():
if request.method == "OPTIONS": if request.method == "OPTIONS":
return "", 204 return "", 200
elif request.method in ["PUT", "POST", "PATCH"]:
if "media" in request.path:
# Don't check media uploads
return
try:
request.get_json(force=True)
except Exception as e:
return jsonify({"error": "Content not JSON.", "errcode": "M_NOT_JSON"}), 400
@app.after_request @app.after_request
async def handle_logging(response): async def handle_logging(response):
@@ -36,7 +49,6 @@ async def handle_logging(response):
response.headers["Access-Control-Allow-Methods"] = "GET, HEAD, POST, PUT, DELETE, OPTIONS" response.headers["Access-Control-Allow-Methods"] = "GET, HEAD, POST, PUT, DELETE, OPTIONS"
if request.method == "OPTIONS": if request.method == "OPTIONS":
# Discard logs for OPTIONS
return response return response
origin = "unknown" origin = "unknown"
@@ -44,13 +56,31 @@ async def handle_logging(response):
try: try:
if "Authorization" in request.headers: if "Authorization" in request.headers:
if request.headers["Authorization"].split()[0] == "X-Matrix": if request.headers["Authorization"].split()[0] == "X-Matrix":
origin = request.headers["Authorization"].split('origin="')[1].split('"')[0] origin = (
request.headers["Authorization"]
.split("origin=")[1]
.split(",")[0]
)
while '"' in origin:
origin = origin.replace('"', "")
if origin == config.server_name:
return response
else: else:
origin = "client" origin = "client"
except: except:
pass pass
print(f'[{origin}] [{request.remote_addr}] [{datetime.now().strftime("%d/%b/%Y:%H:%M:%S")}] {request.method} {request.full_path} {response.status_code}') print(
f"[{origin}] " +
f'[{datetime.now().strftime("%d/%b/%Y:%H:%M:%S")}] ' +
request.method + " " +
request.full_path.rstrip("?") + " " +
str(response.status_code)
)
return response return response
@@ -100,4 +130,20 @@ async def client():
}) })
def federation_self_test():
try:
resp = globals.http_client().get(
path="/",
destination=config.server_name,
)
resp.raise_for_status()
print("[INFO] Federation self-test OK")
except Exception as e:
print(f"[FATL] Federation self-test failed: {e}")
os._exit(1)
threading.Thread(target=federation_self_test).start()
app.run(host=config.addr, port=config.port) app.run(host=config.addr, port=config.port)

View File

@@ -56,7 +56,7 @@ async def spec_versions():
@client.route("/_matrix/client/v3/admin/whois/<user>") @client.route("/_matrix/client/v3/admin/whois/<user>")
@client.route("/_matrix/client/r0/admin/whois/<user>") @client.route("/_matrix/client/r0/admin/whois/<user>")
async def whois(user): async def whois(user):
if userId.startswith("@"): if user.startswith("@"):
return jsonify({ return jsonify({
"devices": { "devices": {
"": { "": {
@@ -644,14 +644,10 @@ async def presence(user):
if request.method == "PUT": if request.method == "PUT":
return jsonify({}) return jsonify({})
return jsonify({ return jsonify({"presence": "online"})
"presence": "online"
})
@client.route("/_matrix/client/v3/publicRooms", methods=["GET", "POST"])
@client.route("/_matrix/client/r0/publicRooms", methods=["GET", "POST"]) @client.route("/_matrix/client/r0/publicRooms", methods=["GET", "POST"])
async def room_directory(): async def room_directory():
return jsonify({ return jsonify(globals.room_dir)
"chunk": [],
"total_room_count_estimate": 0
})

View File

@@ -12,7 +12,7 @@ server_name: str = ""
signing_key: str = "" signing_key: str = ""
support: dict = {"contacts": []} support: dict = {"contacts": []}
_CONFIG_PATH = Path("/etc/vona/config.toml") _CONFIG_PATH = Path(os.getenv("VONA_CONFIG", "/etc/vona/config.toml"))
def _fatal(msg: str) -> None: def _fatal(msg: str) -> None:

View File

@@ -2,16 +2,21 @@ from flask import Blueprint
custom = Blueprint("custom", __name__) custom = Blueprint("custom", __name__)
# This implements custom endpoints # This implements non-standard
# used by other homeserver # endpoints created by other
# implementations. They do not start # homeserver implementations.
# with /_matrix/
from .hammerhead import hammerhead
from .conduwuit import conduwuit from .conduwuit import conduwuit
from .dendrite import dendrite from .dendrite import dendrite
from .telodendria import telo
from .synapse import synapse from .synapse import synapse
from .citadel import citadel
custom.register_blueprint(hammerhead)
custom.register_blueprint(conduwuit) custom.register_blueprint(conduwuit)
custom.register_blueprint(dendrite) custom.register_blueprint(dendrite)
custom.register_blueprint(synapse) custom.register_blueprint(synapse)
custom.register_blueprint(citadel)
custom.register_blueprint(telo)

61
vona/custom/citadel.py Normal file
View File

@@ -0,0 +1,61 @@
from flask import Blueprint, jsonify, request
import vona.config as config
import base64
import os
citadel = Blueprint("citadel", __name__)
# These are endpoints made by Thales Citadel
# TODO: Add more endpoints, this likely
# isn't all of them
@citadel.route("/_matrix/client/r0/citadel/stats/m.news/<event>", methods=["GET", "PUT"])
async def news_stats(event):
if request.method == "PUT":
return jsonify({"success": True})
return jsonify({
"total_clicks": config.the_funny_number,
"user_readings": config.the_funny_number
})
@citadel.route("/_matrix/client/r0/citadel/rooms/<room>/closeRoom", methods=["POST"])
async def close_room(room):
store_response = request.json.get("store_response", True)
operation_id = base64.b64encode(
bytes(
os.urandom(8).hex(),
"utf-8"
)
).decode("utf-8")[:14]
if store_response:
resp = {"operation_id": operation_id}
else:
resp = {
"operation_id": operation_id,
"previous_state": {
"progress": {
"steps": {
"step_kick_users": {
"status": "complete"
},
"step_purge_history": {
"status": "complete"
},
"step_purge_media": {
"status": "complete"
}
}
},
"status": "complete"
}
}
return jsonify(resp)

77
vona/custom/hammerhead.py Normal file
View File

@@ -0,0 +1,77 @@
from flask import Blueprint, request, jsonify
from vona.config import the_funny_number
from datetime import datetime, timezone
from vona.federation import send_join
import vona.globals as globals
import vona.config as config
import time
hammerhead = Blueprint("hammerhead", __name__)
# Hammerhead endpoints. Not documented, but code is at:
# https://codeberg.org/timedout/hammerhead/src/branch/dev/hammerhead/router/routes/hammerhead
@hammerhead.route("/_hammerhead/uptime")
async def uptime():
return jsonify({"started_at": config.the_funny_number})
@hammerhead.route("/_hammerhead/version")
async def version():
return jsonify({"version": globals.version})
@hammerhead.route("/_hammerhead/admin/server-info/<server>")
async def server_info(server):
return jsonify({
"destination": {
"expires": datetime.now(timezone.utc).isoformat(),
"host_header": server,
"ip_port": [
f"{server}:443"
],
"server_name": server
},
"keys": globals.sign_json({
"old_verify_keys": {},
"server_name": server,
"valid_until_ts": int(time.time() * 1000 + 604800000),
"verify_keys": {
f"ed25519:{config.signing_key.split()[1]}": {
"key": globals.pubkey()
}
}
}),
"software": {
"server": {
"version": globals.version,
"name": "Vona"
}
}
})
@hammerhead.route("/_hammerhead/admin/rooms/<room>/state-resolver")
async def room_state(room):
class bullshit:
def get_json():
return {}
state = send_join(bullshit, room)["state"]
formatted_state = {}
event_cache = {}
for event in state:
key = f"({event["type"]},'{event["state_key"]}')"
formatted_state[key] = event
event_id = event["event_id"]
event_cache[event_id] = event
return jsonify({
"current_state": formatted_state,
"event_cache": event_cache,
"room_id": room,
"room_version": globals.room_version_from_id(room)
})

View File

@@ -24,9 +24,9 @@ synapse = Blueprint("synapse", __name__)
@synapse.route("/_synapse/admin/v1/media/unquarantine/<s>/<media_id>", methods=["POST"]) @synapse.route("/_synapse/admin/v1/media/unquarantine/<s>/<media_id>", methods=["POST"])
@synapse.route("/_synapse/admin/v1/federation/destinations/<destination>/reset_connection", methods=["POST"]) @synapse.route("/_synapse/admin/v1/federation/destinations/<destination>/reset_connection", methods=["POST"])
@synapse.route("/_synapse/admin/v1/rooms/<room>") @synapse.route("/_synapse/admin/v1/rooms/<room>")
@synapse.route("/_synapse/admin/v1/rooms/<room_id>/timestamp_to_event") @synapse.route("/_synapse/admin/v1/rooms/<room>/timestamp_to_event")
@synapse.route("/_synapse/admin/v2/rooms/delete_status/<delete_id>") @synapse.route("/_synapse/admin/v2/rooms/delete_status/<delete_id>")
@synapse.route("/_synapse/admin/v1/rooms/<room_id_or_alias>/make_room_admin", methods=["POST"]) @synapse.route("/_synapse/admin/v1/rooms/<room>/make_room_admin", methods=["POST"])
async def response(**kwargs): async def response(**kwargs):
return jsonify({}) return jsonify({})
@@ -118,9 +118,24 @@ async def account_data(user_id):
@synapse.route("/_synapse/admin/v1/users/<user_id>/media", methods=["GET", "DELETE"]) @synapse.route("/_synapse/admin/v1/users/<user_id>/media", methods=["GET", "DELETE"])
async def account_media(user_id): async def account_media(user_id):
if request.method == "GET": if request.method == "GET":
return jsonify({"media": [{"created_ts":config.the_funny_number,"last_access_ts":config.the_funny_number,"media_id":"cat","media_length":config.the_funny_number,"media_type":"image/jpeg","quarantined_by":"null","safe_from_quarantine":False,"upload_name":"cat.jpg"}], "total": config.the_funny_number}) return jsonify({
"media": [{
"created_ts": config.the_funny_number,
"last_access_ts": config.the_funny_number,
"media_id": "cat",
"media_length": config.the_funny_number,
"media_type": "image/jpeg",
"quarantined_by": None,
"safe_from_quarantine": False,
"upload_name": "cat.jpg"
}],
"total": config.the_funny_number
})
return jsonify({"deleted_media": ["cat"], "total": config.the_funny_number}) return jsonify({
"deleted_media": ["cat"],
"total": config.the_funny_number
})
@synapse.route("/_synapse/admin/v1/users/<user_id>/login", methods=["POST"]) @synapse.route("/_synapse/admin/v1/users/<user_id>/login", methods=["POST"])
async def account_login(user_id): async def account_login(user_id):
@@ -137,7 +152,7 @@ async def device_list(user_id):
"devices": [{ "devices": [{
"device_id": "VVOONNAA", "device_id": "VVOONNAA",
"display_name": "Vona", "display_name": "Vona",
"last_seen_ip": "127.0.0.1", "last_seen_ip": config.addr,
"last_seen_ts": config.the_funny_number, "last_seen_ts": config.the_funny_number,
"last_seen_user_agent": f"Vona/{globals.version}" "last_seen_user_agent": f"Vona/{globals.version}"
}], }],
@@ -152,7 +167,7 @@ async def device_info(user_id, device_id):
return jsonify({ return jsonify({
"device_id": "VVOONNAA", "device_id": "VVOONNAA",
"display_name": "Vona", "display_name": "Vona",
"last_seen_ip": "127.0.0.1", "last_seen_ip": config.addr,
"last_seen_ts": config.the_funny_number, "last_seen_ts": config.the_funny_number,
"last_seen_user_agent": f"Vona/{globals.version}" "last_seen_user_agent": f"Vona/{globals.version}"
}) })
@@ -169,11 +184,11 @@ async def username_available():
@synapse.route("/_synapse/admin/v1/threepid/<medium>/users/<addr>") @synapse.route("/_synapse/admin/v1/threepid/<medium>/users/<addr>")
@synapse.route("/_synapse/admin/v1/auth_providers/<provider>/users/<ext>") @synapse.route("/_synapse/admin/v1/auth_providers/<provider>/users/<ext>")
async def threepid(p, a): async def threepid(**kwargs):
return jsonify({"user_id": f"@vona:{config.server_name}"}) return jsonify({"user_id": f"@vona:{config.server_name}"})
@synapse.route("/_synapse/admin/v1/<user_id>/redact") @synapse.route("/_synapse/admin/v1/<user_id>/redact")
def redact(user_id): async def redact(user_id):
return jsonify({"redact_id": os.urandom(16).hex()}) return jsonify({"redact_id": os.urandom(16).hex()})
@synapse.route("/_synapse/admin/v1/user/redact_status/<redact_id>") @synapse.route("/_synapse/admin/v1/user/redact_status/<redact_id>")
@@ -191,8 +206,8 @@ async def register():
return jsonify({"access_token": "vona"}) return jsonify({"access_token": "vona"})
@synapse.route("/_synapse/admin/v1/join/<roomId>", methods=["POST"]) @synapse.route("/_synapse/admin/v1/join/<room>", methods=["POST"])
async def membership_manipulation(roomId): async def membership_manipulation(room):
return jsonify({"room_id": globals.make_event_id().replace("$", "!")}) return jsonify({"room_id": globals.make_event_id().replace("$", "!")})
@synapse.route("/_synapse/admin/v1/account_validity/validity", methods=["POST"]) @synapse.route("/_synapse/admin/v1/account_validity/validity", methods=["POST"])
@@ -204,8 +219,8 @@ async def account_validity():
async def server_notice(**kwargs): async def server_notice(**kwargs):
return jsonify({"event_id": globals.make_event_id()}) return jsonify({"event_id": globals.make_event_id()})
@synapse.route("/_synapse/admin/v1/purge_history/<room_id>/<event_id>", methods=["POST"]) @synapse.route("/_synapse/admin/v1/purge_history/<room>/<event_id>", methods=["POST"])
@synapse.route("/_synapse/admin/v1/purge_history/<room_id>", methods=["POST"]) @synapse.route("/_synapse/admin/v1/purge_history/<room>", methods=["POST"])
async def purge_event(**kwargs): async def purge_event(**kwargs):
return jsonify({"purge_id": os.urandom(16).hex()}) return jsonify({"purge_id": os.urandom(16).hex()})
@@ -213,12 +228,12 @@ async def purge_event(**kwargs):
async def purge_status(purge_id): async def purge_status(purge_id):
return jsonify({"status":"active"}) return jsonify({"status":"active"})
@synapse.route("/_synapse/admin/v1/room/<room_id>/media") @synapse.route("/_synapse/admin/v1/room/<room>/media")
async def room_media(room_id): async def room_media(room):
return jsonify({"local": [f"mxc://{config.server_name}/cat"], "remote": []}) return jsonify({"local": [f"mxc://{config.server_name}/cat"], "remote": []})
@synapse.route("/_synapse/admin/v1/room/<room_id>/media/quarantine", methods=["POST"]) @synapse.route("/_synapse/admin/v1/room/<room>/media/quarantine", methods=["POST"])
async def quarantine_room_media(room_id): async def quarantine_room_media(room):
return jsonify({"num_quarantined": config.the_funny_number}) return jsonify({"num_quarantined": config.the_funny_number})
@synapse.route("/_synapse/admin/v1/media/<s>/delete", methods=["POST"]) @synapse.route("/_synapse/admin/v1/media/<s>/delete", methods=["POST"])
@@ -233,7 +248,15 @@ async def delete_remote_media():
@synapse.route("/_synapse/admin/v1/statistics/users/media") @synapse.route("/_synapse/admin/v1/statistics/users/media")
async def media_stats(): async def media_stats():
return jsonify({"users":[{"displayname":"Vona","media_count":config.the_funny_number,"media_length":config.the_funny_number,"user_id":f"@vona:{config.server_name}"}],"total":config.the_funny_number}) return jsonify({
"users": [{
"displayname": "Vona",
"media_count": config.the_funny_number,
"media_length": config.the_funny_number,
"user_id": f"@vona:{config.server_name}"
}],
"total": config.the_funny_number
})
@synapse.route("/_synapse/admin/v1/statistics/database/rooms") @synapse.route("/_synapse/admin/v1/statistics/database/rooms")
async def room_stats(): async def room_stats():
@@ -302,7 +325,7 @@ async def interact_with_reported_event(report_id):
@synapse.route("/_synapse/admin/v1/federation/destinations") @synapse.route("/_synapse/admin/v1/federation/destinations")
async def federation_destinations(): async def federation_destinations():
return jsonify({ return jsonify({
"destinations": [{}], "destinations": [],
"total": 0 "total": 0
}) })
@@ -366,7 +389,7 @@ async def rooms():
"total_rooms": 0 "total_rooms": 0
}) })
@synapse.route("/_synapse/admin/v1/rooms/<room_id>/members") @synapse.route("/_synapse/admin/v1/rooms/<room>/members")
async def room_members(room): async def room_members(room):
return jsonify({ return jsonify({
"members": [ "members": [
@@ -375,11 +398,11 @@ async def room_members(room):
"total": 1 "total": 1
}) })
@synapse.route("/_synapse/admin/v1/rooms/<room_id>/state") @synapse.route("/_synapse/admin/v1/rooms/<room>/state")
async def room_state(room): async def room_state(room):
return jsonify({"state": []}) return jsonify({"state": []})
@synapse.route("/_synapse/admin/v1/rooms/<room_id>/state") @synapse.route("/_synapse/admin/v1/rooms/<room>/state")
async def room_messages(room): async def room_messages(room):
return jsonify({ return jsonify({
"chunk": [], "chunk": [],
@@ -387,11 +410,11 @@ async def room_messages(room):
"start": "vona" "start": "vona"
}) })
@synapse.route("/_synapse/admin/v1/rooms/<room_id>/block", methods=["GET", "PUT"]) @synapse.route("/_synapse/admin/v1/rooms/<room>/block", methods=["GET", "PUT"])
async def block_room(room): async def block_room(room):
return jsonify({"block": False}) return jsonify({"block": False})
@synapse.route("/_synapse/admin/v1/rooms/<room_id>", methods=["DELETE"]) @synapse.route("/_synapse/admin/v1/rooms/<room>", methods=["DELETE"])
async def room_delete(room): async def room_delete(room):
return jsonify({ return jsonify({
"kicked_users": [ "kicked_users": [
@@ -399,18 +422,18 @@ async def room_delete(room):
], ],
"failed_to_kick_users": [], "failed_to_kick_users": [],
"local_aliases": [], "local_aliases": [],
"new_room_id": f"!vona:{config.server_name}" "new_room_id": globals.make_event_id(seed=room)
}) })
@synapse.route("/_synapse/admin/v2/rooms/<room_id>", methods=["DELETE"]) @synapse.route("/_synapse/admin/v2/rooms/<room>", methods=["DELETE"])
async def room_delete_v2(room): async def room_delete_v2(room):
return jsonify({"delete_id": "vona"}) return jsonify({"delete_id": "vona"})
@synapse.route("/_synapse/admin/v2/rooms/<room_id>/delete_status") @synapse.route("/_synapse/admin/v2/rooms/<room>/delete_status")
async def room_delete_status(room): async def room_delete_status(room):
return jsonify({"results": []}) return jsonify({"results": []})
@synapse.route("/_synapse/admin/v1/rooms/<room_id_or_alias>/forward_extremities", methods=["GET"]) @synapse.route("/_synapse/admin/v1/rooms/<room>/forward_extremities", methods=["GET"])
async def forward_extremities(room): async def forward_extremities(room):
if request.method == "DELETE": if request.method == "DELETE":
return jsonify({"deleted": 0}) return jsonify({"deleted": 0})

View File

@@ -0,0 +1,85 @@
from flask import Blueprint, request, jsonify
from vona.globals import version
import vona.config as config
telo = Blueprint("telodendria", __name__)
# The telodendria admin API as specified by
# https://git.telodendria.org/Telodendria/Telodendria/src/branch/master/docs/user/admin/README.md
@telo.route("/_telodendria/admin/v1/restart", methods=["POST"])
@telo.route("/_telodendria/admin/v1/shutdown", methods=["POST"])
async def process_management(**kwargs):
return jsonify({})
@telo.route("/_telodendria/admin/v1/privileges/<lp>", methods=["GET", "PUT", "POST", "DELETE"])
@telo.route("/_telodendria/admin/v1/privileges/", methods=["GET", "POST"])
async def privileges(lp=None):
return jsonify({
"privileges": [
"GRANT_PRIVILEGES",
"PROC_CONTROL",
"ISSUE_TOKENS",
"DEACTIVATE",
"CONFIG",
"ALIAS",
"ALL"
]
})
@telo.route("/_telodendria/admin/v1/config", methods=["GET", "PUT", "POST"])
async def configuration():
if request.method == "GET":
return jsonify({
"listen": [
{
"tls": None,
"port": config.port,
"threads": 1,
"maxConnections": 32
}
],
"serverName": config.server_name,
"pid": "/dev/null",
"baseUrl": f"https://{config.server_name}/",
"identityServer": f"https://{config.server_name}/",
"runAs": None,
"federation": True,
"registration": config.users_can_register,
"log": {
"output": "stdout",
"level": "message",
"timestampFormat": "default",
"color": True
},
"maxCache": config.the_funny_number
})
return jsonify({"restart_required": True})
@telo.route("/_telodendria/admin/v1/stats")
async def stats():
return jsonify({
"memory_allocated": config.the_funny_number,
"version": version
})
@telo.route("/_telodendria/admin/v1/tokens/<name>", methods=["GET", "DELETE"])
@telo.route("/_telodendria/admin/v1/tokens", methods=["GET", "POST"])
async def tokens(name=None):
if request.method == "DELETE":
return jsonify({})
return jsonify({
"name": "vona",
"created_by": "vona",
"created_on": config.the_funny_number,
"expires_on": config.the_funny_number,
"used": config.the_funny_number,
"uses": config.the_funny_number
})

View File

@@ -1,7 +1,6 @@
from flask import jsonify, Response, request, send_file, abort, Blueprint from flask import jsonify, Response, request, send_file, abort, Blueprint
from vona.config import *
import vona.globals as globals import vona.globals as globals
import httpx import vona.config as config
import json import json
import time import time
import os import os
@@ -25,13 +24,13 @@ def send_join(request, roomId) -> dict:
create_event = { create_event = {
"content": { "content": {
"m.federate": True, "m.federate": True,
"creator": f"@vona:{server_name}", "creator": f"@vona:{config.server_name}",
"room_version": globals.room_version_from_id(roomId) "room_version": globals.room_version_from_id(roomId)
}, },
"event_id": event_ids[0], "event_id": event_ids[0],
"origin_server_ts": 1, "origin_server_ts": 1,
"room_id": roomId, "room_id": roomId,
"sender": f"@vona:{server_name}", "sender": f"@vona:{config.server_name}",
"state_key": "", "state_key": "",
"depth": 1, "depth": 1,
"type": "m.room.create", "type": "m.room.create",
@@ -45,12 +44,12 @@ def send_join(request, roomId) -> dict:
our_join = { our_join = {
"content": { "content": {
"displayname": "Vona", "displayname": "Vona",
"avatar_url": f"mxc://{server_name}/cat", "avatar_url": f"mxc://{config.server_name}/cat",
"membership": "join" "membership": "join"
}, },
"origin_server_ts": 2, "origin_server_ts": 2,
"sender": f"@vona:{server_name}", "sender": f"@vona:{config.server_name}",
"state_key": f"@vona:{server_name}", "state_key": f"@vona:{config.server_name}",
"type": "m.room.member", "type": "m.room.member",
"event_id": event_ids[1], "event_id": event_ids[1],
"room_id": roomId, "room_id": roomId,
@@ -71,17 +70,17 @@ def send_join(request, roomId) -> dict:
pls = { pls = {
"content": { "content": {
"users": { "users": {
f"@vona:{server_name}": "100" f"@vona:{config.server_name}": "100"
} }
}, },
"origin_server_ts": 3, "origin_server_ts": 3,
"room_id": roomId, "room_id": roomId,
"sender": f"@vona:{server_name}", "sender": f"@vona:{config.server_name}",
"state_key": "", "state_key": "",
"type": "m.room.power_levels", "type": "m.room.power_levels",
"event_id": event_ids[2], "event_id": event_ids[2],
"depth": 3, "depth": 3,
"user_id": f"@vona:{server_name}", "user_id": f"@vona:{config.server_name}",
"auth_events": [ "auth_events": [
[ [
screate_event["event_id"], screate_event["event_id"],
@@ -106,7 +105,7 @@ def send_join(request, roomId) -> dict:
"join_rule": "public" "join_rule": "public"
}, },
"origin_server_ts": 4, "origin_server_ts": 4,
"sender": f"@vona:{server_name}", "sender": f"@vona:{config.server_name}",
"state_key": "", "state_key": "",
"type": "m.room.join_rules", "type": "m.room.join_rules",
"event_id": event_ids[3], "event_id": event_ids[3],
@@ -141,7 +140,7 @@ def send_join(request, roomId) -> dict:
}, },
"origin_server_ts": 5, "origin_server_ts": 5,
"depth": 5, "depth": 5,
"sender": f"@vona:{server_name}", "sender": f"@vona:{config.server_name}",
"state_key": "", "state_key": "",
"type": "m.room.guest_access", "type": "m.room.guest_access",
"event_id": event_ids[4], "event_id": event_ids[4],
@@ -158,10 +157,6 @@ def send_join(request, roomId) -> dict:
[ [
spls["event_id"], spls["event_id"],
spls["hashes"] spls["hashes"]
],
[
sjoin_rule["event_id"],
sjoin_rule["hashes"]
] ]
], ],
"prev_events": [[ "prev_events": [[
@@ -178,7 +173,7 @@ def send_join(request, roomId) -> dict:
"history_visibility": "shared" "history_visibility": "shared"
}, },
"type": "m.room.history_visibility", "type": "m.room.history_visibility",
"sender": f"@vona:{server_name}", "sender": f"@vona:{config.server_name}",
"state_key": "", "state_key": "",
"origin_server_ts": 6, "origin_server_ts": 6,
"depth": 6, "depth": 6,
@@ -196,10 +191,6 @@ def send_join(request, roomId) -> dict:
[ [
spls["event_id"], spls["event_id"],
spls["hashes"] spls["hashes"]
],
[
sjoin_rule["event_id"],
sjoin_rule["hashes"]
] ]
], ],
"prev_events": [[ "prev_events": [[
@@ -217,7 +208,7 @@ def send_join(request, roomId) -> dict:
"auth_chain": event_chain, "auth_chain": event_chain,
"event": remote_join, "event": remote_join,
"members_omitted": False, "members_omitted": False,
"servers_in_room": [server_name], "servers_in_room": [config.server_name],
"state": event_chain "state": event_chain
} }
@@ -237,10 +228,10 @@ async def version():
async def keys(): async def keys():
return jsonify(globals.sign_json({ return jsonify(globals.sign_json({
"old_verify_keys": {}, "old_verify_keys": {},
"server_name": server_name, "server_name": config.server_name,
"valid_until_ts": int(time.time() * 1000 + 604800000), "valid_until_ts": int(time.time() * 1000 + 604800000),
"verify_keys": { "verify_keys": {
f"ed25519:{signing_key.split()[1]}": { f"ed25519:{config.signing_key.split()[1]}": {
"key": globals.pubkey() "key": globals.pubkey()
} }
} }
@@ -250,7 +241,7 @@ async def keys():
async def room_query(): async def room_query():
return jsonify({ return jsonify({
"room_id": globals.make_event_id().replace("$", "!"), "room_id": globals.make_event_id().replace("$", "!"),
"servers": [server_name] "servers": [config.server_name]
}) })
@server.route("/_matrix/federation/v1/media/download/<media_id>") @server.route("/_matrix/federation/v1/media/download/<media_id>")
@@ -259,7 +250,7 @@ async def download_media(media_id):
# multipart despite not even using # multipart despite not even using
# it for anything. Minor annoyance. # it for anything. Minor annoyance.
with open(cat, "rb") as img_file: with open(config.cat, "rb") as img_file:
image_data = img_file.read() image_data = img_file.read()
boundary = "----WebKitFormBoundary7MA4YWxkTrZu0gW" boundary = "----WebKitFormBoundary7MA4YWxkTrZu0gW"
response_body = ( response_body = (
@@ -300,7 +291,7 @@ async def make_join(roomId, userId):
}), 403 }), 403
try: try:
if roomId.split(":")[1] != server_name: if roomId.split(":")[1] != config.server_name:
return not_invited() return not_invited()
except: except:
return not_invited() return not_invited()
@@ -316,10 +307,10 @@ async def make_join(roomId, userId):
join = { join = {
"content": { "content": {
"join_authorised_via_users_server": f"@vona:{server_name}", "join_authorised_via_users_server": f"@vona:{config.server_name}",
"membership": "join" "membership": "join"
}, },
"origin": server_name, "origin": config.server_name,
"origin_server_ts": 7, "origin_server_ts": 7,
"room_id": roomId, "room_id": roomId,
"sender": userId, "sender": userId,
@@ -353,12 +344,10 @@ async def make_join(roomId, userId):
"room_version": globals.room_version_from_id(roomId) "room_version": globals.room_version_from_id(roomId)
}) })
@server.route("/_matrix/federation/v1/publicRooms", methods=["POST", "GET"]) @server.route("/_matrix/federation/v1/publicRooms", methods=["POST", "GET"])
async def room_directory(): async def room_directory():
return jsonify({ return jsonify(globals.room_dir)
"chunk": [],
"total_room_count_estimate": 0
})
# https://spec.matrix.org/latest/server-server-api/#transactions # https://spec.matrix.org/latest/server-server-api/#transactions
@@ -395,7 +384,7 @@ async def user_profile():
field = request.args.get("field") field = request.args.get("field")
if field: if field:
if field == "avatar_url": if field == "avatar_url":
return jsonify({"avatar_url":f"mxc://{server_name}/cat"}) return jsonify({"avatar_url":f"mxc://{config.server_name}/cat"})
elif field == "displayname": elif field == "displayname":
return jsonify({"displayname":"Vona"}) return jsonify({"displayname":"Vona"})
@@ -404,31 +393,24 @@ async def user_profile():
"error": "The requested profile key does not exist." "error": "The requested profile key does not exist."
}), 404 }), 404
return jsonify({"avatar_url": f"mxc://{server_name}/cat","displayname": "Vona"}) return jsonify({"avatar_url": f"mxc://{config.server_name}/cat","displayname": "Vona"})
# https://spec.matrix.org/latest/server-server-api/#device-management @server.route(f"/_matrix/federation/v1/user/devices/@/:{config.server_name}")
@server.route(f"/_matrix/federation/v1/user/devices/@/:{server_name}")
@server.route("/_matrix/federation/v1/user/devices/<user>") @server.route("/_matrix/federation/v1/user/devices/<user>")
async def user_devices(user): async def user_devices(user):
return jsonify({ return jsonify({
"devices": [], "devices": [],
"stream_id": the_funny_number, "stream_id": config.the_funny_number,
"user_id": f"@vona:{server_name}" "user_id": f"@vona:{config.server_name}"
}) })
@server.route("/_matrix/federation/v1/user/keys/query", methods=["POST"]) @server.route("/_matrix/federation/v1/user/keys/query", methods=["POST"])
async def user_keys(): async def user_keys():
try: return jsonify({
users = request.json["device_keys"] "device_keys": request.json.get("device_keys", {})
except: })
return jsonify({
"errcode": "M_NOT_FOUND",
"error": "User does not exist"
}), 404
return jsonify({"device_keys": users})
@server.route("/_matrix/federation/v2/invite/<room>/<txnId>", methods=["PUT"]) @server.route("/_matrix/federation/v2/invite/<room>/<txnId>", methods=["PUT"])
@@ -438,21 +420,16 @@ async def invite_user_v2(room, txnId):
@server.route("/_matrix/federation/v1/invite/<room>/<txnId>", methods=["PUT"]) @server.route("/_matrix/federation/v1/invite/<room>/<txnId>", methods=["PUT"])
async def invite_user_v1(room, txnId): async def invite_user_v1(room, txnId):
return [200, invite_user(request.data)] return [200, invite_user(request.json)]
def invite_user(data): def invite_user(invite_data):
try:
invite_data = json.loads(data)
except:
return jsonify({"errcode":"M_NOT_JSON","error":"Content not JSON."}),
if "event" in invite_data: if "event" in invite_data:
if "room_version" in invite_data: if "room_version" in invite_data:
if invite_data["room_version"] != "2": if invite_data["room_version"] not in ["1", "2"]:
return jsonify({ return jsonify({
"errcode": "M_INCOMPATIBLE_ROOM_VERSION", "errcode": "M_INCOMPATIBLE_ROOM_VERSION",
"error": "Vona only supports room version 2.", "error": "Unsupported room version",
"room_version": invite_data["room_version"] "room_version": invite_data["room_version"]
}), 400 }), 400
@@ -466,9 +443,12 @@ def invite_user(data):
and "state_key" in event and "state_key" in event
and "room_id" in event and "room_id" in event
and content["membership"] == "invite" and content["membership"] == "invite"
and event["state_key"] == f"@vona:{server_name}" and event["state_key"] == f"@vona:{config.server_name}"
): ):
return jsonify({"event": globals.sign_json_without_discard(invite_data["event"]), "room_version": "2"}) return jsonify({
"event": globals.sign_json_without_discard(invite_data["event"]),
"room_version": invite_data["room_version"]
})
return jsonify({ return jsonify({
@@ -482,4 +462,26 @@ async def space_hierachy(roomId):
return jsonify({ return jsonify({
"errcode": "M_NOT_FOUND", "errcode": "M_NOT_FOUND",
"error": "Room does not exist." "error": "Room does not exist."
}), 404
@server.route("/_matrix/federation/v1/org.matrix.msc4358/discover_common_rooms", methods=["POST"])
@server.route("/_matrix/federation/v1/discover_common_rooms", methods=["POST"])
async def discover_common_rooms():
tags = request.json.get("room_participation_tags", [])
return jsonify({"recognised_tags": tags})
@server.route("/_matrix/federation/v1/backfill/<room>")
async def backfill(room):
# TODO: burger king foot lettuce
class bullshit:
def get_json():
return {}
return jsonify({
"origin": config.server_name,
"origin_server_ts": int(str(time.time() * 1000).split(".")[0]),
"pdus": send_join(bullshit, room)["state"]
}) })

View File

@@ -1,14 +1,17 @@
from resolvematrix import ServerResolver
from types import SimpleNamespace
from collections import Counter from collections import Counter
import vona.config as config import vona.config as config
import nacl.signing import nacl.signing
import hashlib import hashlib
import base64 import base64
import random import random
import httpx
import copy import copy
import json import json
import re import re
version = "1.4.2" version = "1.4.4"
def canonical_json(value): def canonical_json(value):
@@ -109,7 +112,9 @@ def event_hash(event_object):
event_json_bytes = canonical_json(event_object) event_json_bytes = canonical_json(event_object)
return base64.b64encode(hashlib.sha256(event_json_bytes).digest()).decode("utf-8") return base64.b64encode(
hashlib.sha256(event_json_bytes).digest()
).decode("utf-8").rstrip("=")
def pubkey() -> str: def pubkey() -> str:
@@ -127,7 +132,12 @@ def pubkey() -> str:
) )
def make_auth_header(destination, method, path, content=None) -> str: def make_auth_header(
destination: str,
method: str,
path: str,
content = None
) -> str:
request_json = { request_json = {
"method": method, "method": method,
"uri": path, "uri": path,
@@ -250,3 +260,89 @@ def room_version_from_id(room_id):
return most_common[0] if most_common else None return most_common[0] if most_common else None
return most_common_character(nums)[0] return most_common_character(nums)[0]
room_dir = {
"chunk": [{
"avatar_url": f"mxc://{config.server_name}/cat",
"guest_can_join": False,
"join_rule": "public",
"name": "Vona",
"num_joined_members": 1,
"room_id": make_event_id().replace("$", "!"),
"room_type": "m.room",
"topic": "",
"world_readable": False,
"via": [config.server_name]
}],
"total_room_count_estimate": 1
}
class http_client:
http = httpx.Client(headers={"User-Agent": f"Vona/{version}"})
resolver = ServerResolver(client=http)
def _resolve(self, target) -> SimpleNamespace:
r = self.resolver.resolve(target)
if r.sni:
sni = r.sni
else:
sni = r.host_header
return SimpleNamespace(
base_url=r.base_url,
host_header=r.host_header,
sni=sni
)
def put(
self,
path: str,
destination: str,
headers: dict = {},
authorize: bool = True,
json: dict = {},
):
resolved = self._resolve(destination)
if authorize:
headers["Authorization"] = make_auth_header(
method="PUT",
destination=destination,
path=path,
)
headers["Host"] = resolved.host_header
return self.http.put(
f"{resolved.base_url}{path}",
headers=headers,
extensions={"sni_hostname": resolved.sni},
json=json
)
def get(
self,
path: str,
destination: str,
headers: dict = {},
authorize: bool = True,
):
resolved = self._resolve(destination)
if authorize:
headers["Authorization"] = make_auth_header(
method="GET",
destination=destination,
path=path,
)
headers["Host"] = resolved.host_header
return self.http.get(
f"{resolved.base_url}{path}",
headers=headers,
extensions={"sni_hostname": resolved.sni}
)

View File

@@ -1,7 +1,8 @@
print("Available utils:") print("Available utils:")
a = [ a = [
"makekey" "makekey",
"joinroom"
] ]
for t in a: for t in a:

83
vona/utils/joinroom.py Normal file
View File

@@ -0,0 +1,83 @@
from resolvematrix import ServerResolver
import urllib.parse, time, json, httpx
import vona.globals as globals
import vona.config as config
http_client = globals.http_client()
def get_user_input(prompt):
try:
answer = input(prompt)
while "\\n" in answer:
answer = answer.replace("\\n", "\n")
return urllib.parse.quote(answer)
except Exception as e:
print(f"Error reading input: {e}")
return None
username = get_user_input("Username:\n\t")
room_id = get_user_input("Room ID:\n\t")
try:
server_name = input("\nServer name to join via:\n\t")
except Exception as e:
print(f"Error reading server names: {e}")
exit(1)
resolver = ServerResolver(client=http_client)
try:
print("\nSending make_join request..")
make_join_response = http_client.get(
path=f"/_matrix/federation/v1/make_join/{room_id}/%40{username}%3A{config.server_name}?ver=1&ver=2&ver=3&ver=4&ver=5&ver=6&ver=7&ver=8&ver=9&ver=10&ver=11&ver=12",
destination=server_name,
)
make_join_response.raise_for_status()
make_join = make_join_response.json()
except httpx.HTTPStatusError as e:
print(f"HTTP error occurred: {e.response.status_code} - {e.response.text}")
exit(1)
except json.JSONDecodeError:
print("Failed to decode response.")
exit(1)
except Exception as e:
print(f"An error occurred: {e}")
exit(1)
join_event = make_join.get("event", {})
if make_join.get("room_version", "1") in ["1", "2"]:
# NOTE: if we always make it opaque than Synapse will 500 lmao
join_event["event_id"] = globals.make_event_id()
timestamp = input("\nTimestamp (leave blank for now):\n\t")
try:
join_event["origin_server_ts"] = int(timestamp)
except ValueError:
join_event["origin_server_ts"] = int(f"{time.time() * 1000}".split(".")[0])
signed_join = globals.hash_and_sign_event(join_event)
try:
send_join_response = http_client.put(
path=f"/_matrix/federation/v2/send_join/{room_id}/%24doesntmatter?omit_members=true",
json=signed_join,
destination=server_name,
)
send_join_response.raise_for_status()
print("\nSuccess :)")
except httpx.HTTPStatusError as e:
print(
f"HTTP error occurred during send_join: {e.response.status_code} - {e.response.text}"
)
except Exception as e:
print(f"An error occurred during send_join: {e}")