21 Commits

Author SHA1 Message Date
a5b34fb4a8 Only permit the v1 send_join endpoint for v1/v2 rooms 2025-11-05 12:30:51 -05:00
2212453a96 time -> get_time 2025-11-05 11:53:26 -05:00
d9583f3472 Add pre-commit configuration 2025-10-29 23:38:26 -04:00
3d9ff622fd Make ty happy 2025-10-29 21:29:10 -04:00
945f92e25f MSC4375: Admin Room Management 2025-10-29 03:58:08 -04:00
29ecfc0387 Move standardised admin APIs to a new file 2025-10-29 02:39:30 -04:00
4bdeca6139 Implement Hammerhead debug API 2025-10-27 14:54:20 -04:00
b8f8a1a7b1 Remove database 2025-10-27 10:19:03 -04:00
35896ed1ee Implement room V11 2025-10-27 10:05:33 -04:00
da82f492e8 Implement redaction for v1 to v11 events 2025-10-26 22:38:22 -04:00
17d5a6458e Allow / in event IDs on send_join 2025-10-26 18:32:17 -04:00
ad4c5cc5d2 Include event IDs on stripped state 2025-10-26 15:56:46 -04:00
8999a5e414 Add known room IDs to room bruteforcer 2025-10-26 15:35:45 -04:00
2fa647f7e7 Remove join_authorised_via_users_server from make_join 2025-10-26 07:36:09 -04:00
1c87e8b5a5 Implement Room V10 2025-10-26 06:36:15 -04:00
9d68082764 Add CouchDB stuffs 2025-10-26 05:53:44 -04:00
32cdb239ff Synapse start_job returns a blank dict 2025-10-26 00:52:22 -04:00
626c865461 make Ruff happy 2025-10-25 23:53:17 -04:00
fe5c0e23b5 Use ref hash for incoming v3+ events 2025-10-25 23:53:07 -04:00
09cb708594 Improve method for brute-force of rooms 2025-10-25 17:32:40 -04:00
ad72886fb3 Accept more room versions on invitations 2025-10-25 16:54:46 -04:00
19 changed files with 546 additions and 176 deletions

18
.pre-commit-config.yaml Normal file
View File

@@ -0,0 +1,18 @@
default_install_hook_types:
- pre-commit
- commit-msg
default_stages:
- pre-commit
- manual
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.14.2
hooks:
- id: ruff-check
args: [ "--fix" ]
- repo: https://foundry.fsky.io/vel/ty-pre-commit
rev: 0.0.1-alpha.25
hooks:
- id: ty-check

View File

@@ -22,13 +22,16 @@ Merged MSCs:
Non-merged MSCs: Non-merged MSCs:
* [MSC2666: Get rooms in common with another user](https://github.com/matrix-org/matrix-spec-proposals/pull/2666)
* [MSC4358: Out-of-room server discovery](https://github.com/matrix-org/matrix-spec-proposals/pull/4358) * [MSC4358: Out-of-room server discovery](https://github.com/matrix-org/matrix-spec-proposals/pull/4358)
* [MSC4367: via routes in the published room directory](https://github.com/matrix-org/matrix-spec-proposals/pull/4367) * [MSC4367: via routes in the published room directory](https://github.com/matrix-org/matrix-spec-proposals/pull/4367)
* [MSC4370: Federation endpoint for retrieving current extremities](https://github.com/matrix-org/matrix-spec-proposals/pull/4370) * [MSC4370: Federation endpoint for retrieving current extremities](https://github.com/matrix-org/matrix-spec-proposals/pull/4370)
* [MSC4373: Server opt-out of specific EDU types](https://github.com/matrix-org/matrix-spec-proposals/pull/4373) * [MSC4373: Server opt-out of specific EDU types](https://github.com/matrix-org/matrix-spec-proposals/pull/4373)
* [MSC4375: Admin Room Management](https://github.com/matrix-org/matrix-spec-proposals/pull/4375)
Room version MSCs: Room version MSCs:
* [MSC1759: Room V2](https://github.com/matrix-org/matrix-spec-proposals/blob/main/proposals/1759-rooms-v2.md) * [MSC1759: Room V2](https://github.com/matrix-org/matrix-spec-proposals/blob/main/proposals/1759-rooms-v2.md)
* [MSC1659: Room V3](https://github.com/matrix-org/matrix-spec-proposals/blob/main/proposals/1659-event-id-as-hashes.md) * [MSC1659: Room V3](https://github.com/matrix-org/matrix-spec-proposals/blob/main/proposals/1659-event-id-as-hashes.md)
* [MSC2002: Room V4](https://github.com/matrix-org/matrix-spec-proposals/blob/main/proposals/2002-rooms-v4.md) * [MSC2002: Room V4](https://github.com/matrix-org/matrix-spec-proposals/blob/main/proposals/2002-rooms-v4.md)
@@ -37,3 +40,5 @@ Room version MSCs:
* [MSC2998: Room V7](https://github.com/matrix-org/matrix-spec-proposals/blob/main/proposals/2998-rooms-v7.md) * [MSC2998: Room V7](https://github.com/matrix-org/matrix-spec-proposals/blob/main/proposals/2998-rooms-v7.md)
* [MSC3289: Room V8](https://github.com/matrix-org/matrix-spec-proposals/blob/main/proposals/3289-rooms-v8.md) * [MSC3289: Room V8](https://github.com/matrix-org/matrix-spec-proposals/blob/main/proposals/3289-rooms-v8.md)
* [MSC3375: Room V9](https://github.com/matrix-org/matrix-spec-proposals/blob/main/proposals/3375-room-v9.md) * [MSC3375: Room V9](https://github.com/matrix-org/matrix-spec-proposals/blob/main/proposals/3375-room-v9.md)
* [MSC3604: Room V10](https://github.com/matrix-org/matrix-spec-proposals/blob/main/proposals/3604-rooms-v10.md)
* [MSC3820: Room V11](https://github.com/matrix-org/matrix-spec-proposals/blob/main/proposals/3820-rooms-v11.md)

View File

@@ -2,7 +2,7 @@
name = "vona" name = "vona"
description = "Flazing bast Matrix homeserver" description = "Flazing bast Matrix homeserver"
license = {text = "Velicense"} license = {text = "Velicense"}
requires-python = ">=3.12" requires-python = "<4,>=3.12"
dependencies = [ dependencies = [
"httpx (>=0.28.1,<0.29.0)", "httpx (>=0.28.1,<0.29.0)",

View File

@@ -78,7 +78,7 @@ async def handle_logging(response):
else: else:
origin = "client" origin = "client"
except: except Exception:
pass pass
if request.path.startswith("/.well-known/matrix/"): if request.path.startswith("/.well-known/matrix/"):

View File

@@ -6,6 +6,9 @@ import asyncio
import random import random
import os import os
from .groups import groups
from .admin import admin
from flask import ( from flask import (
Blueprint, Blueprint,
jsonify, jsonify,
@@ -15,9 +18,8 @@ from flask import (
client = Blueprint("client", __name__) client = Blueprint("client", __name__)
from .groups import groups
client.register_blueprint(groups) client.register_blueprint(groups)
client.register_blueprint(admin)
@client.route("/_matrix/client/v3/account/password", methods=["POST"]) @client.route("/_matrix/client/v3/account/password", methods=["POST"])
@@ -60,58 +62,15 @@ async def spec_versions():
["r0.6.1"] + [f"v1.{i}" for i in range(1, 17)] ["r0.6.1"] + [f"v1.{i}" for i in range(1, 17)]
), ),
"unstable_features": { "unstable_features": {
"uk.half-shot.msc2666.query_mutual_rooms": True,
"uk.half-shot.msc2666.mutual_rooms": True,
"uk.half-shot.msc2666": True, "uk.half-shot.msc2666": True,
"uk.timedout.msc4323": True "uk.timedout.msc4323": True,
"uk.timedout.msc4375": True,
} }
}) })
@client.route("/_matrix/client/v3/admin/whois/<user>")
@client.route("/_matrix/client/r0/admin/whois/<user>")
async def whois(user):
if user.startswith("@"):
return jsonify({
"devices": {
"": {
"sessions": [{
"connections": [{
"ip": "127.0.0.1",
"last_seen": config.the_funny_number,
"user_agent": f"Vona/{globals.version}"
}]
}]
}
},
"user_id": user
})
return jsonify({
"errcode": "M_INVALID_PARAM",
"error": "Expected UserID string to start with '@'"
})
@client.route("/_matrix/client/unstable/uk.timedout.msc4323/admin/suspend/<user>", methods=["GET", "PUT"])
@client.route("/_matrix/client/v1/admin/suspend/<user>", methods=["GET", "PUT"])
async def suspend(user):
if request.method == "PUT":
req = request.get_json()
if req and "suspended" in req:
return jsonify({"suspended": req["suspended"]})
return jsonify({"suspended": True})
@client.route("/_matrix/client/unstable/uk.timedout.msc4323/admin/lock/<user>", methods=["GET", "PUT"])
@client.route("/_matrix/client/v1/admin/lock/<user>", methods=["GET", "PUT"])
async def lock(user):
if request.method == "PUT":
req = request.get_json()
if req and "locked" in req:
return jsonify({"locked": req["locked"]})
return jsonify({"locked": True})
@client.route("/_matrix/client/api/v1/rooms/<room>/members") @client.route("/_matrix/client/api/v1/rooms/<room>/members")
@client.route("/_matrix/client/v3/rooms/<room>/members") @client.route("/_matrix/client/v3/rooms/<room>/members")
@client.route("/_matrix/client/r0/rooms/<room>/members") @client.route("/_matrix/client/r0/rooms/<room>/members")
@@ -159,7 +118,7 @@ async def register():
"device_id": "VVOONNAA" "device_id": "VVOONNAA"
}) })
except: except Exception:
pass pass
return jsonify({ return jsonify({
@@ -342,7 +301,7 @@ async def sync():
if "timeout" in request.args: if "timeout" in request.args:
try: try:
wait_time = int(request.args.get("timeout")) / 1000 wait_time = int(request.args.get("timeout")) / 1000
except: except Exception:
pass pass
await asyncio.sleep(wait_time) await asyncio.sleep(wait_time)
@@ -420,7 +379,7 @@ async def events():
if "timeout" in request.args: if "timeout" in request.args:
try: try:
await asyncio.sleep(int(request.args["timeout"]) / 1000) await asyncio.sleep(int(request.args["timeout"]) / 1000)
except: except Exception:
pass pass
return jsonify({ return jsonify({

153
vona/client/admin.py Normal file
View File

@@ -0,0 +1,153 @@
import vona.globals as globals
import vona.config as config
from vona.federation import (
send_join,
bullshit,
)
from flask import (
Blueprint,
jsonify,
request,
)
admin = Blueprint("admin", __name__)
# This implements misc standard admin APIs.
@admin.route("/_matrix/client/v3/admin/whois/<user>")
@admin.route("/_matrix/client/r0/admin/whois/<user>")
async def whois(user):
if user.startswith("@"):
return jsonify({
"devices": {
"": {
"sessions": [{
"connections": [{
"ip": "127.0.0.1",
"last_seen": config.the_funny_number,
"user_agent": f"Vona/{globals.version}"
}]
}]
}
},
"user_id": user
})
return jsonify({
"errcode": "M_INVALID_PARAM",
"error": "Expected UserID string to start with '@'"
})
@admin.route("/_matrix/client/unstable/uk.timedout.msc4323/admin/suspend/<user>", methods=["GET", "PUT"])
@admin.route("/_matrix/client/v1/admin/suspend/<user>", methods=["GET", "PUT"])
async def suspend(user):
if request.method == "PUT":
req = request.get_json()
if req and "suspended" in req:
return jsonify({"suspended": req["suspended"]})
return jsonify({"suspended": True})
@admin.route("/_matrix/client/unstable/uk.timedout.msc4323/admin/lock/<user>", methods=["GET", "PUT"])
@admin.route("/_matrix/client/v1/admin/lock/<user>", methods=["GET", "PUT"])
async def lock(user):
if request.method == "PUT":
req = request.get_json()
if req and "locked" in req:
return jsonify({"locked": req["locked"]})
return jsonify({"locked": True})
@admin.route("/_matrix/client/unstable/uk.timedout.msc4375/admin/rooms")
@admin.route("/_matrix/client/v1/admin/rooms")
async def rooms():
return jsonify({
"chunk": [
f"!qa:{config.server_name}",
f"!br:{config.server_name}",
f"!3:{config.server_name}",
f"!D:{config.server_name}",
f"!U:{config.server_name}",
f"!f:{config.server_name}",
f"!gx:{config.server_name}",
f"!hx:{config.server_name}",
f"!iy:{config.server_name}",
f"!p0:{config.server_name}",
f"!jZ:{config.server_name}",
]
})
@admin.route("/_matrix/client/unstable/uk.timedout.msc4375/admin/rooms/<room>")
@admin.route("/_matrix/client/v1/admin/rooms/<room>")
async def room_state(room):
if ":" not in room:
return jsonify({
"errcode": "M_NOT_FOUND",
"error": "Unknown room"
}), 404
else:
if room.split(":")[1] != config.server_name:
return jsonify({
"errcode": "M_NOT_FOUND",
"error": "Unknown room"
}), 404
state = globals.strip_state(
send_join(bullshit, room)["state"]
)
return jsonify({
"state": [state]
})
@admin.route("/_matrix/client/unstable/uk.timedout.msc4375/admin/rooms/<room>/evacuate", methods=["POST"])
@admin.route("/_matrix/client/unstable/uk.timedout.msc4375/admin/rooms/<room>", methods=["DELETE"])
@admin.route("/_matrix/client/v1/admin/rooms/<room>/evacuate", methods=["POST"])
@admin.route("/_matrix/client/v1/admin/rooms/<room>", methods=["DELETE"])
async def evacuate_room(room):
req = request.json
if (
isinstance(req, dict)
and "background" in req
and isinstance(req["background"], bool)
):
background = req["background"]
else:
background = True
resp = {
"background": background,
}
if not background:
resp["removed"] = config.the_funny_number
return resp
@admin.route("/_matrix/client/unstable/uk.timedout.msc4375/admin/rooms/<room>/evacuate/status")
@admin.route("/_matrix/client/unstable/uk.timedout.msc4375/admin/rooms/<room>/delete/status")
@admin.route("/_matrix/client/v1/admin/rooms/<room>/evacuate/status")
@admin.route("/_matrix/client/v1/admin/rooms/<room>/delete/status")
async def evacuate_status(room):
return jsonify({
"started_at": globals.get_time(), # everything all at once always now!!!
"total": config.the_funny_number,
"evacuated": config.the_funny_number,
"failed": config.the_funny_number,
})
@admin.route("/_matrix/client/unstable/uk.timedout.msc4375/admin/rooms/<room>/blocked", methods=["PUT"])
@admin.route("/_matrix/client/v1/admin/rooms/<room>/blocked", methods=["PUT"])
async def block(room):
return jsonify({})

View File

@@ -1,6 +1,5 @@
import vona.globals as globals import vona.globals as globals
import vona.config as config import vona.config as config
import time
from flask import ( from flask import (
Blueprint, Blueprint,
@@ -89,7 +88,7 @@ async def users(group):
"attestation": globals.sign_json({ "attestation": globals.sign_json({
"group_id": group, "group_id": group,
"user_id": f"@vona:{config.server_name}", "user_id": f"@vona:{config.server_name}",
"valid_until_ms": int(str(time.time() * 1000).split(".")[0]) "valid_until_ms": globals.get_time()
}) })
} }
], ],

View File

@@ -36,6 +36,10 @@ def _load_toml(path: Path) -> dict:
except tomllib.TOMLDecodeError as e: except tomllib.TOMLDecodeError as e:
_fatal(f"Invalid TOML configuration: {e}") _fatal(f"Invalid TOML configuration: {e}")
# This will never be reached. It is here
# specifically just to please ty
return {}
def _read_signing_key_from_path(path_value) -> str | None: def _read_signing_key_from_path(path_value) -> str | None:
p = Path(path_value) p = Path(path_value)
@@ -111,11 +115,11 @@ def _apply_config(cfg: dict) -> None:
else: else:
_warn("No support contacts are defined") _warn("No support contacts are defined")
if "enable_registration" in cfg:
users_can_register = cfg["enable_registration"]
support = support_obj support = support_obj
if "enable_registration" in cfg and isinstance(cfg["enable_registration"], bool):
users_can_register = cfg["enable_registration"]
print("[INFO] Configuration file was valid") print("[INFO] Configuration file was valid")

View File

@@ -1 +1,2 @@
import vona.config import vona.config
str(vona.config.server_name) # Satisfy Ruff

View File

@@ -2,13 +2,6 @@ from flask import (
Blueprint, Blueprint,
) )
custom = Blueprint("custom", __name__)
# This implements non-standard
# endpoints created by other
# homeserver implementations.
from .hammerhead import hammerhead from .hammerhead import hammerhead
from .conduwuit import conduwuit from .conduwuit import conduwuit
from .dendrite import dendrite from .dendrite import dendrite
@@ -16,6 +9,13 @@ from .telodendria import telo
from .synapse import synapse from .synapse import synapse
from .citadel import citadel from .citadel import citadel
custom = Blueprint("custom", __name__)
# This implements non-standard
# endpoints created by other
# homeserver implementations.
custom.register_blueprint(hammerhead) custom.register_blueprint(hammerhead)
custom.register_blueprint(conduwuit) custom.register_blueprint(conduwuit)
custom.register_blueprint(dendrite) custom.register_blueprint(dendrite)

View File

@@ -30,7 +30,16 @@ async def news_stats(event):
@citadel.route("/_matrix/client/r0/citadel/rooms/<room>/closeRoom", methods=["POST"]) @citadel.route("/_matrix/client/r0/citadel/rooms/<room>/closeRoom", methods=["POST"])
async def close_room(room): async def close_room(room):
store_response = request.json.get("store_response", True) req = request.json
if (
isinstance(req, dict)
and "store_response" in req
and isinstance(req["store_response"], bool)
):
store_response = req["store_response"]
else:
store_response = True
operation_id = base64.b64encode( operation_id = base64.b64encode(
bytes( bytes(

View File

@@ -1,5 +1,4 @@
from datetime import datetime, timezone from datetime import datetime, timezone
from vona.federation import send_join
import vona.globals as globals import vona.globals as globals
import vona.config as config import vona.config as config
import time import time
@@ -9,6 +8,11 @@ from flask import (
jsonify, jsonify,
) )
from vona.federation import (
send_join,
bullshit,
)
hammerhead = Blueprint("hammerhead", __name__) hammerhead = Blueprint("hammerhead", __name__)
# Hammerhead endpoints. Not documented, but code is at: # Hammerhead endpoints. Not documented, but code is at:
@@ -24,6 +28,7 @@ async def uptime():
async def version(): async def version():
return jsonify({"version": globals.version}) return jsonify({"version": globals.version})
@hammerhead.route("/_hammerhead/admin/reload-config") @hammerhead.route("/_hammerhead/admin/reload-config")
async def reload_config(): async def reload_config():
return jsonify({}) return jsonify({})
@@ -61,10 +66,6 @@ async def server_info(server):
@hammerhead.route("/_hammerhead/admin/rooms/<room>/state-resolver") @hammerhead.route("/_hammerhead/admin/rooms/<room>/state-resolver")
async def room_state(room): async def room_state(room):
class bullshit:
def get_json():
return {}
state = send_join(bullshit, room)["state"] state = send_join(bullshit, room)["state"]
formatted_state = {} formatted_state = {}
event_cache = {} event_cache = {}
@@ -90,3 +91,44 @@ async def room_state(room):
"room_id": room, "room_id": room,
"room_version": globals.room_version_from_id(room) "room_version": globals.room_version_from_id(room)
}) })
@hammerhead.route("/_hammerhead/admin/rooms/<room>/debug")
async def debug_room(room):
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
if "event_id" in event:
event_id = event["event_id"]
else:
event_id = globals.make_ref_hash(
event,
int(globals.room_version_from_id(room))
)
event_cache[event_id] = event
if globals.room_version_from_id(room) in ["1", "2"]:
extremity = globals.make_event_id(seed=f"6_{room}")
else:
extremity = globals.make_ref_hash(
state[5],
int(globals.room_version_from_id(room)),
)
return jsonify({
"aliases": [f"#vona:{config.server_name}"],
"current_state": formatted_state,
"event_cache": event_cache,
"room_id": room,
"room_version": globals.room_version_from_id(room),
"forward_extremities": [extremity],
"servers": None
})

View File

@@ -32,6 +32,7 @@ synapse = Blueprint("synapse", __name__)
@synapse.route("/_synapse/admin/v1/rooms/<room>/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>/make_room_admin", methods=["POST"]) @synapse.route("/_synapse/admin/v1/rooms/<room>/make_room_admin", methods=["POST"])
@synapse.route("/_synapse/admin/v1/background_updates/start_job", methods=["POST"])
async def response(**kwargs): async def response(**kwargs):
return jsonify({}) return jsonify({})
@@ -275,13 +276,7 @@ async def room_stats():
@synapse.route("/_synapse/admin/v1/background_updates/enabled", methods=["POST", "GET"]) @synapse.route("/_synapse/admin/v1/background_updates/enabled", methods=["POST", "GET"])
@synapse.route("/_synapse/admin/v1/background_updates/status") @synapse.route("/_synapse/admin/v1/background_updates/status")
async def change_bg_update(): async def change_bg_update():
return jsonify({"enabled":False}) return jsonify({"enabled": False})
# No documentation on what Synapse actually returns for this API, so a blank dict for now
@synapse.route("/_synapse/admin/v1/background_updates/start_job", methods=["POST"])
async def bg_update_start_job():
return jsonify({})
@synapse.route("/_synapse/admin/v1/event_reports") @synapse.route("/_synapse/admin/v1/event_reports")
async def event_reports(): async def event_reports():

View File

@@ -10,6 +10,7 @@ from flask import (
Response, Response,
request, request,
Blueprint, Blueprint,
abort,
) )
server = Blueprint("federation", __name__) server = Blueprint("federation", __name__)
@@ -37,6 +38,7 @@ async def version():
} }
}) })
@server.route("/_matrix/key/v2/server") @server.route("/_matrix/key/v2/server")
async def keys(): async def keys():
return jsonify(globals.sign_json({ return jsonify(globals.sign_json({
@@ -50,6 +52,7 @@ async def keys():
} }
})) }))
@server.route("/_matrix/federation/v1/query/directory") @server.route("/_matrix/federation/v1/query/directory")
async def room_query(): async def room_query():
return jsonify({ return jsonify({
@@ -57,6 +60,7 @@ async def room_query():
"servers": [config.server_name] "servers": [config.server_name]
}) })
@server.route("/_matrix/federation/v1/media/download/<media_id>") @server.route("/_matrix/federation/v1/media/download/<media_id>")
async def download_media(media_id): async def download_media(media_id):
# Auth media requires this to be # Auth media requires this to be
@@ -80,6 +84,7 @@ async def download_media(media_id):
return response return response
@server.route("/_matrix/federation/v1/media/thumbnail/<media_id>") @server.route("/_matrix/federation/v1/media/thumbnail/<media_id>")
async def thumbnail_media(media_id): async def thumbnail_media(media_id):
return jsonify({ return jsonify({
@@ -88,7 +93,7 @@ async def thumbnail_media(media_id):
}), 418 }), 418
@server.route("/_matrix/federation/v1/send_join/<room>/<eventId>", methods=["PUT"]) @server.route("/_matrix/federation/v1/send_join/<room>/<path:eventId>", methods=["PUT"])
async def send_join_v1(room, eventId): async def send_join_v1(room, eventId):
if globals.room_version_from_id(room) not in ["1", "2"]: if globals.room_version_from_id(room) not in ["1", "2"]:
return jsonify({ return jsonify({
@@ -99,8 +104,12 @@ async def send_join_v1(room, eventId):
return jsonify([200, send_join(request, room)]) return jsonify([200, send_join(request, room)])
@server.route("/_matrix/federation/v2/send_join/<room>/<eventId>", methods=["PUT"]) @server.route("/_matrix/federation/v2/send_join/<room>/<path:eventId>", methods=["PUT"])
async def send_join_v2(room, eventId): async def send_join_v2(room, eventId):
if globals.room_version_from_id(room) in ["1", "2"]:
# Spec says to fallback to the v1 send_join endpoint if this fails
abort(404)
return jsonify(send_join(request, room)) return jsonify(send_join(request, room))
@@ -128,7 +137,6 @@ async def make_join(room, user):
join = { join = {
"content": { "content": {
"join_authorised_via_users_server": f"@vona:{config.server_name}",
"membership": "join" "membership": "join"
}, },
"origin": config.server_name, "origin": config.server_name,
@@ -175,7 +183,7 @@ async def make_join(room, user):
return jsonify({ return jsonify({
"event": globals.hash_and_sign_event(join), "event": globals.hash_and_sign_event(join, int(room_ver)),
"room_version": room_ver "room_version": room_ver
}) })
@@ -185,7 +193,6 @@ async def room_directory():
return jsonify(globals.room_dir) return jsonify(globals.room_dir)
# https://spec.matrix.org/latest/server-server-api/#transactions
@server.route("/_matrix/federation/v1/send/<txnId>", methods=["PUT"]) @server.route("/_matrix/federation/v1/send/<txnId>", methods=["PUT"])
async def receive_txn(txnId): async def receive_txn(txnId):
# We will need to implement a way to store every # We will need to implement a way to store every
@@ -209,7 +216,13 @@ async def receive_txn(txnId):
if "pdus" in parsed_data: if "pdus" in parsed_data:
for pdu in parsed_data["pdus"]: for pdu in parsed_data["pdus"]:
if "event_id" in pdu: if "event_id" in pdu:
response["pdus"][pdu["event_id"]] = {} # For v1 and v2 rooms
event_id = pdu["event_id"]
else:
# Assume room v4 or over as most rooms will be anyway
event_id = globals.make_ref_hash(pdu, 4)
response["pdus"][event_id] = {}
return jsonify(response) return jsonify(response)
@@ -243,8 +256,19 @@ async def user_devices(user):
@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():
req = request.json
if (
isinstance(req, dict)
and "device_keys" in req
and isinstance(req["device_keys"], dict)
):
device_keys = req["device_keys"]
else:
device_keys = {}
return jsonify({ return jsonify({
"device_keys": request.json.get("device_keys", {}) "device_keys": device_keys
}) })
@@ -252,9 +276,12 @@ async def user_keys():
async def invite_user_v2(room, txnId): async def invite_user_v2(room, txnId):
invite_data = request.json invite_data = request.json
if "event" in invite_data: if (
isinstance(invite_data, dict)
and "event" in invite_data
):
if "room_version" in invite_data: if "room_version" in invite_data:
if invite_data["room_version"] not in ["1", "2"]: if invite_data["room_version"] not in [str(i) for i in range(1, 10)]:
return jsonify({ return jsonify({
"errcode": "M_INCOMPATIBLE_ROOM_VERSION", "errcode": "M_INCOMPATIBLE_ROOM_VERSION",
"error": "Unsupported room version", "error": "Unsupported room version",
@@ -287,10 +314,19 @@ 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):
event = request.json event = request.json
content = event.get("content", {}) if (
isinstance(event, dict)
and "content" in event
and isinstance(event["content"], dict)
):
content = event["content"]
else:
content = {}
if ( if (
"content" in event isinstance(event, dict)
and isinstance(content, dict)
and "content" in event
and "membership" in content and "membership" in content
and "state_key" in event and "state_key" in event
and "room_id" in event and "room_id" in event
@@ -320,7 +356,12 @@ async def space_hierachy(room):
@server.route("/_matrix/federation/v1/org.matrix.msc4358/discover_common_rooms", methods=["POST"]) @server.route("/_matrix/federation/v1/org.matrix.msc4358/discover_common_rooms", methods=["POST"])
@server.route("/_matrix/federation/v1/discover_common_rooms", methods=["POST"]) @server.route("/_matrix/federation/v1/discover_common_rooms", methods=["POST"])
async def discover_common_rooms(): async def discover_common_rooms():
tags = request.json.get("room_participation_tags", []) req = request.json
if isinstance(req, dict):
tags = req.get("room_participation_tags", [])
else:
tags = []
return jsonify({"recognised_tags": tags}) return jsonify({"recognised_tags": tags})
@@ -330,7 +371,7 @@ async def backfill(room):
return jsonify({ return jsonify({
"origin": config.server_name, "origin": config.server_name,
"origin_server_ts": int(str(time.time() * 1000).split(".")[0]), "origin_server_ts": globals.get_time(),
"pdus": send_join(bullshit, room)["state"] "pdus": send_join(bullshit, room)["state"]
}) })
@@ -342,7 +383,7 @@ async def extremities(room):
if room.split(":")[1] != config.server_name: if room.split(":")[1] != config.server_name:
return jsonify({ return jsonify({
"errcode": "M_NOT_FOUND", "errcode": "M_NOT_FOUND",
"error": f"Room is unknown to this server" "error": "Room is unknown to this server"
}), 404 }), 404
room_ver = globals.room_version_from_id(room) room_ver = globals.room_version_from_id(room)
@@ -414,7 +455,7 @@ async def state_ids(room):
raise raise
return jsonify(resp.json()) return jsonify(resp.json())
except: except Exception:
pass pass
return explode() return explode()

View File

@@ -199,7 +199,7 @@ def v1_v2(request, room) -> dict:
remote_join = request.get_json() remote_join = request.get_json()
response = { return {
"auth_chain": event_chain, "auth_chain": event_chain,
"event": remote_join, "event": remote_join,
"members_omitted": False, "members_omitted": False,
@@ -207,10 +207,8 @@ def v1_v2(request, room) -> dict:
"state": event_chain "state": event_chain
} }
return response
# Room V3 to V10
# Room V3 to V9
def v3(request, room) -> dict: def v3(request, room) -> dict:
initial_response = v1_v2(request, room) initial_response = v1_v2(request, room)
state = list(initial_response["state"]) state = list(initial_response["state"])
@@ -225,12 +223,14 @@ def v3(request, room) -> dict:
del event["signatures"] del event["signatures"]
# m.room.create doesn't have prev_events or auth_events # m.room.create doesn't have prev_events or auth_events
events["m.room.create"] = globals.hash_and_sign_event(events["m.room.create"]) if ver >= 11:
del events["m.room.create"]["content"]["creator"]
events["m.room.create"] = globals.hash_and_sign_event(events["m.room.create"], ver)
hash_map["m.room.create"] = globals.make_ref_hash(events["m.room.create"], ver) hash_map["m.room.create"] = globals.make_ref_hash(events["m.room.create"], ver)
events["m.room.member"]["auth_events"] = [hash_map["m.room.create"]] events["m.room.member"]["auth_events"] = [hash_map["m.room.create"]]
events["m.room.member"]["prev_events"] = [hash_map["m.room.create"]] events["m.room.member"]["prev_events"] = [hash_map["m.room.create"]]
events["m.room.member"] = globals.hash_and_sign_event(events["m.room.member"]) events["m.room.member"] = globals.hash_and_sign_event(events["m.room.member"], ver)
hash_map["m.room.member"] = globals.make_ref_hash(events["m.room.member"], ver) hash_map["m.room.member"] = globals.make_ref_hash(events["m.room.member"], ver)
events["m.room.power_levels"]["auth_events"] = [ events["m.room.power_levels"]["auth_events"] = [
@@ -238,7 +238,9 @@ def v3(request, room) -> dict:
hash_map["m.room.member"], hash_map["m.room.member"],
] ]
events["m.room.power_levels"]["prev_events"] = [hash_map["m.room.member"]] events["m.room.power_levels"]["prev_events"] = [hash_map["m.room.member"]]
events["m.room.power_levels"] = globals.hash_and_sign_event(events["m.room.power_levels"]) if ver >= 10:
events["m.room.power_levels"]["content"]["users"][f"@vona:{config.server_name}"] = 100
events["m.room.power_levels"] = globals.hash_and_sign_event(events["m.room.power_levels"], ver)
hash_map["m.room.power_levels"] = globals.make_ref_hash(events["m.room.power_levels"], ver) hash_map["m.room.power_levels"] = globals.make_ref_hash(events["m.room.power_levels"], ver)
events["m.room.join_rules"]["auth_events"] = [ events["m.room.join_rules"]["auth_events"] = [
@@ -247,7 +249,7 @@ def v3(request, room) -> dict:
hash_map["m.room.power_levels"], hash_map["m.room.power_levels"],
] ]
events["m.room.join_rules"]["prev_events"] = [hash_map["m.room.power_levels"]] events["m.room.join_rules"]["prev_events"] = [hash_map["m.room.power_levels"]]
events["m.room.join_rules"] = globals.hash_and_sign_event(events["m.room.join_rules"]) events["m.room.join_rules"] = globals.hash_and_sign_event(events["m.room.join_rules"], ver)
hash_map["m.room.join_rules"] = globals.make_ref_hash(events["m.room.join_rules"], ver) hash_map["m.room.join_rules"] = globals.make_ref_hash(events["m.room.join_rules"], ver)
events["m.room.guest_access"]["auth_events"] = [ events["m.room.guest_access"]["auth_events"] = [
@@ -256,7 +258,7 @@ def v3(request, room) -> dict:
hash_map["m.room.power_levels"], hash_map["m.room.power_levels"],
] ]
events["m.room.guest_access"]["prev_events"] = [hash_map["m.room.join_rules"]] events["m.room.guest_access"]["prev_events"] = [hash_map["m.room.join_rules"]]
events["m.room.guest_access"] = globals.hash_and_sign_event(events["m.room.guest_access"]) events["m.room.guest_access"] = globals.hash_and_sign_event(events["m.room.guest_access"], ver)
hash_map["m.room.guest_access"] = globals.make_ref_hash(events["m.room.guest_access"], ver) hash_map["m.room.guest_access"] = globals.make_ref_hash(events["m.room.guest_access"], ver)
events["m.room.history_visibility"]["auth_events"] = [ events["m.room.history_visibility"]["auth_events"] = [
@@ -265,7 +267,7 @@ def v3(request, room) -> dict:
hash_map["m.room.power_levels"], hash_map["m.room.power_levels"],
] ]
events["m.room.history_visibility"]["prev_events"] = [hash_map["m.room.guest_access"]] events["m.room.history_visibility"]["prev_events"] = [hash_map["m.room.guest_access"]]
events["m.room.history_visibility"] = globals.hash_and_sign_event(events["m.room.history_visibility"]) events["m.room.history_visibility"] = globals.hash_and_sign_event(events["m.room.history_visibility"], ver)
new_state = [] new_state = []
@@ -273,12 +275,10 @@ def v3(request, room) -> dict:
new_state.append(events[event]) new_state.append(events[event])
resp = { return {
"auth_chain": new_state, "auth_chain": new_state,
"event": initial_response["event"], "event": initial_response["event"],
"members_omitted": False, "members_omitted": False,
"servers_in_room": [config.server_name], "servers_in_room": [config.server_name],
"state": new_state "state": new_state
} }
return resp

View File

@@ -1,19 +1,23 @@
from resolvematrix import ServerResolver from resolvematrix import ServerResolver
from types import SimpleNamespace from types import SimpleNamespace
from collections import Counter from collections import Counter
import vona.config as config import vona.config as config
import nacl.encoding
import nacl.signing import nacl.signing
import hashlib import hashlib
import base64 import base64
import random import random
import httpx import httpx
import json import json
import copy
import time
import re import re
version = "1.5.0" version = "1.5.0"
def canonical_json(value): def canonical_json(value: dict | list) -> bytes:
return json.dumps( return json.dumps(
value, value,
ensure_ascii=False, ensure_ascii=False,
@@ -22,7 +26,7 @@ def canonical_json(value):
).encode("UTF-8") ).encode("UTF-8")
def sign_json(data): def sign_json(data: dict) -> dict:
parts = config.signing_key.split() parts = config.signing_key.split()
base64_key = parts[2] base64_key = parts[2]
@@ -51,7 +55,7 @@ def sign_json(data):
return signed_json return signed_json
def sign_json_without_discard(data): def sign_json_without_discard(data: dict) -> dict:
parts = config.signing_key.split() parts = config.signing_key.split()
base64_key = parts[2] base64_key = parts[2]
@@ -84,7 +88,7 @@ def sign_json_without_discard(data):
return data return data
def make_event_id(seed=None): def make_event_id(seed: str | int | None = None) -> str:
if seed is not None: if seed is not None:
random.seed(seed) random.seed(seed)
@@ -102,7 +106,7 @@ def make_event_id(seed=None):
return event_id return event_id
def event_hash(event_object): def event_hash(event_object: dict) -> str:
event_object = dict(event_object) event_object = dict(event_object)
event_object.pop("unsigned", None) event_object.pop("unsigned", None)
@@ -171,9 +175,10 @@ def make_auth_header(
def redact_event( def redact_event(
event: dict, event: dict,
for_event_id: bool = False, for_event_id: bool = False,
): room_ver: int = 1,
) -> dict:
# Returns a redacted event as per # Returns a redacted event as per
# the algorithm for v1/v2 rooms. # the algorithm for v1 to v11 rooms.
allowed_keys = [ allowed_keys = [
"event_id", "event_id",
@@ -185,16 +190,18 @@ def redact_event(
"hashes", "hashes",
"depth", "depth",
"prev_events", "prev_events",
"prev_state",
"auth_events", "auth_events",
"origin",
"origin_server_ts", "origin_server_ts",
"membership",
] ]
if not for_event_id: if not for_event_id:
allowed_keys.append("signatures") allowed_keys.append("signatures")
if room_ver < 11:
allowed_keys.append("origin")
allowed_keys.append("membership")
allowed_keys.append("prev_state")
redacted_event = {k: v for k, v in event.items() if k in allowed_keys} redacted_event = {k: v for k, v in event.items() if k in allowed_keys}
if "type" in redacted_event and "content" in redacted_event: if "type" in redacted_event and "content" in redacted_event:
@@ -218,23 +225,63 @@ def redact_event(
"m.room.history_visibility": ["history_visibility"], "m.room.history_visibility": ["history_visibility"],
} }
if room_ver >= 6:
del content_key_rules["m.room.aliases"]
if room_ver >= 8:
content_key_rules["m.room.join_rules"].append("allow")
if room_ver >= 9:
content_key_rules["m.room.member"].append("join_authorised_via_users_server")
if room_ver >= 11:
content_key_rules["m.room.redaction"] = ["redacts"]
del content_key_rules["m.room.create"] # All keys will be permitted
if event_type in content_key_rules: if event_type in content_key_rules:
allowed_content_keys = content_key_rules[event_type] allowed_content_keys = content_key_rules[event_type]
if (
room_ver >= 11
and "third_party_invite" in redacted_event
and "signed" in redacted_event["third_party_invite"]
):
third_party_invite_signature = copy.deepcopy(redacted_event["third_party_invite"]["signed"])
else:
third_party_invite_signature = None
redacted_event["content"] = { redacted_event["content"] = {
k: v k: v
for k, v in redacted_event["content"].items() for k, v in redacted_event["content"].items()
if k in allowed_content_keys if k in allowed_content_keys
} }
if third_party_invite_signature:
redacted_event["content"]["third_party_invite"] = {
"signed": third_party_invite_signature
}
else:
if room_ver >= 11 and event_type == "m.room.create":
pass
else: else:
redacted_event["content"] = {} redacted_event["content"] = {}
return redacted_event return redacted_event
def hash_and_sign_event(event_object): def hash_and_sign_event(
event_object: dict,
room_ver: int = 1,
) -> dict:
content_hash = event_hash(event_object) content_hash = event_hash(event_object)
event_object["hashes"] = {"sha256": content_hash} event_object["hashes"] = {"sha256": content_hash}
stripped_object = redact_event(event_object) stripped_object = redact_event(
event=event_object,
for_event_id=False,
room_ver=room_ver,
)
signed_object = sign_json(stripped_object) signed_object = sign_json(stripped_object)
event_object["signatures"] = signed_object["signatures"] event_object["signatures"] = signed_object["signatures"]
return event_object return event_object
@@ -243,8 +290,12 @@ def hash_and_sign_event(event_object):
def make_ref_hash( def make_ref_hash(
event: dict, event: dict,
room_ver: int = 3, room_ver: int = 3,
): ) -> str:
stripped = redact_event(event, True) stripped = redact_event(
event=event,
for_event_id=True,
room_ver=room_ver,
)
evt_bytes = canonical_json(stripped) evt_bytes = canonical_json(stripped)
evt_hash = base64.b64encode( evt_hash = base64.b64encode(
@@ -260,7 +311,7 @@ def make_ref_hash(
return "$" + evt_hash return "$" + evt_hash
def room_version_from_id(room): def room_version_from_id(room) -> str:
room_id_no_sigil = ( room_id_no_sigil = (
room room
.replace("!", "") .replace("!", "")
@@ -268,7 +319,8 @@ def room_version_from_id(room):
) )
hexadecimal_room_id = bytes(room_id_no_sigil, "utf-8").hex() hexadecimal_room_id = bytes(room_id_no_sigil, "utf-8").hex()
versions = [str(i) for i in range(1, 10)] versions = [str(i) for i in range(0, 10)]
versions.append("a")
if not any(ver in hexadecimal_room_id for ver in versions): if not any(ver in hexadecimal_room_id for ver in versions):
hexadecimal_room_id = "2" + hexadecimal_room_id[1:] hexadecimal_room_id = "2" + hexadecimal_room_id[1:]
@@ -282,7 +334,13 @@ def room_version_from_id(room):
s = s.replace(" ", "").lower() s = s.replace(" ", "").lower()
counts = Counter(s) counts = Counter(s)
most_common = counts.most_common(1) most_common = counts.most_common(1)
return most_common[0] if most_common else ("2",)
actual_most_common = most_common[0] if most_common else ("4",)
if actual_most_common[0] == "0":
return ("10",)
elif actual_most_common[0] == "a":
return ("11",)
return actual_most_common
return str(most_common_character(nums)[0]) return str(most_common_character(nums)[0])
@@ -374,9 +432,9 @@ class http_client:
) )
def strip_state(l) -> list: def strip_state(state_events) -> list:
if not isinstance(l, list): if not isinstance(state_events, list):
return l return state_events
keys_to_remove = [ keys_to_remove = [
"auth_events", "auth_events",
@@ -387,15 +445,26 @@ def strip_state(l) -> list:
] ]
new_list = [] new_list = []
for d in l: for d in state_events:
if not isinstance(d, dict): if not isinstance(d, dict):
new_list.append(d) new_list.append(d)
continue continue
if "room_id" in d:
ver = int(room_version_from_id(d["room_id"]))
else:
ver = 4
event_id = make_ref_hash(d, ver)
new_dict = {} new_dict = {}
for k, v in d.items(): for k, v in d.items():
if k in keys_to_remove: if k in keys_to_remove:
continue continue
new_dict[k] = strip_state(v) new_dict[k] = strip_state(v)
new_dict["event_id"] = event_id
new_list.append(new_dict) new_list.append(new_dict)
return new_list return new_list
def get_time() -> int:
return int(str(time.time() * 1000).split(".")[0])

View File

@@ -1,5 +1,7 @@
from vona.config import server_name, the_funny_number import vona.config as config
import vona.globals as globals
import time import time
import os
from flask import ( from flask import (
Blueprint, Blueprint,
@@ -13,10 +15,8 @@ identity = Blueprint("identity", __name__)
# I'm pretty sure only Element uses this, # I'm pretty sure only Element uses this,
# but oh well. # but oh well.
# https://spec.matrix.org/latest/identity-service-api/#api-version-check
@identity.route("/_matrix/identity/versions") @identity.route("/_matrix/identity/versions")
async def versions(): async def versions():
# Stolen from the vector.im identity server
return jsonify({ return jsonify({
"versions": [ "versions": [
"r0.1.0", "r0.1.0",
@@ -32,10 +32,11 @@ async def versions():
}) })
# https://spec.matrix.org/latest/identity-service-api/#authentication
@identity.route("/_matrix/identity/v2/account") @identity.route("/_matrix/identity/v2/account")
async def account_info(): async def account_info():
return jsonify({"user_id": f"@vona:{server_name}"}) return jsonify({
"user_id": f"@vona:{config.server_name}"
})
@identity.route("/_matrix/identity/v2/account/logout", methods=["POST"]) @identity.route("/_matrix/identity/v2/account/logout", methods=["POST"])
@@ -45,98 +46,129 @@ async def logout():
@identity.route("/_matrix/identity/v2/account/register", methods=["POST"]) @identity.route("/_matrix/identity/v2/account/register", methods=["POST"])
async def register(): async def register():
return jsonify({"token":"vona"}) return jsonify({"token": "vona"})
# https://spec.matrix.org/latest/identity-service-api/#terms-of-service
@identity.route("/_matrix/identity/v2/terms", methods=["GET", "POST"]) @identity.route("/_matrix/identity/v2/terms", methods=["GET", "POST"])
async def policies(): async def policies():
if request.method == "GET": if request.method == "GET":
return jsonify({"policies":{}}) return jsonify({
"policies": {}
})
return jsonify({}) return jsonify({})
@identity.route("/_matrix/identity/v2") @identity.route("/_matrix/identity/v2")
async def status(): async def status():
return jsonify({}) return jsonify({})
@identity.route("/_matrix/identity/v2/pubkey/ephemeral/isvalid") @identity.route("/_matrix/identity/v2/pubkey/ephemeral/isvalid")
@identity.route("/_matrix/identity/v2/pubkey/isvalid") @identity.route("/_matrix/identity/v2/pubkey/isvalid")
async def pubkey_validity(): async def pubkey_validity():
return jsonify({"valid": True}) return jsonify({"valid": True})
@identity.route("/_matrix/identity/v2/pubkey/<key>") @identity.route("/_matrix/identity/v2/pubkey/<key>")
async def get_key(key): async def get_key(key):
return jsonify({ return jsonify({
"errcode": "M_NOT_FOUND", "public_key": globals.pubkey()
"error": "The public key was not found" })
}), 404
@identity.route("/_matrix/identity/v2/hash_details") @identity.route("/_matrix/identity/v2/hash_details")
async def hash_details(): async def hash_details():
return jsonify({"algorithms":["none","sha256"],"lookup_pepper": "vona"}) return jsonify({
"algorithms": [
"none",
"sha256",
],
"lookup_pepper": "vona"
})
@identity.route("/_matrix/identity/v2/lookup", methods=["POST"]) @identity.route("/_matrix/identity/v2/lookup", methods=["POST"])
async def lookup(): async def lookup():
req = request.json req = request.json
if "addresses" in req: if (
return jsonify({"mappings": {req["addresses"][0]: f"@vona:{server_name}"}}) isinstance(req, dict)
else: and "addresses" in req
return jsonify({"errcode": "M_INVALID_PEPPER","error": "Invalid pepper"}) and isinstance(req["addresses"], list)
and len(req["addresses"]) > 0
):
return jsonify({
"mappings": {
req["addresses"][0]: f"@vona:{config.server_name}"
}
})
return jsonify({
"errcode": "M_INVALID_PEPPER",
"error": "Invalid pepper"
})
@identity.route("/_matrix/identity/v2/validate/email/requestToken", methods=["POST"]) @identity.route("/_matrix/identity/v2/validate/email/requestToken", methods=["POST"])
@identity.route("/_matrix/identity/v2/validate/msisdn/requestToken", methods=["POST"]) @identity.route("/_matrix/identity/v2/validate/msisdn/requestToken", methods=["POST"])
async def request_validation_token(): async def request_validation_token():
return jsonify({"sid": str(the_funny_number)}) return jsonify({
"sid": os.urandom(16).hex()
})
@identity.route("/_matrix/identity/v2/validate/email/submitToken", methods=["GET", "POST"]) @identity.route("/_matrix/identity/v2/validate/email/submitToken", methods=["GET", "POST"])
@identity.route("/_matrix/identity/v2/validate/msisdn/submitToken", methods=["GET", "POST"]) @identity.route("/_matrix/identity/v2/validate/msisdn/submitToken", methods=["GET", "POST"])
async def submit_validation_token(): async def submit_validation_token():
return jsonify({"success": True}) return jsonify({"success": True})
@identity.route("/_matrix/identity/v2/3pid/bind", methods=["POST"]) @identity.route("/_matrix/identity/v2/3pid/bind", methods=["POST"])
async def threepid_bind(): async def threepid_bind():
if "mxid" in request.get_json(): if "mxid" in request.get_json():
mxid = request.get_json()["mxid"] mxid = request.get_json()["mxid"]
else: else:
mxid = f"@vona:{server_name}" mxid = f"@vona:{config.server_name}"
return jsonify(globals.sign_json({ return jsonify(
globals.sign_json({
"address": "abuse@matrix.org", "address": "abuse@matrix.org",
"medium": "email", "medium": "email",
"mxid": mxid, "mxid": mxid,
"not_after": int(time.time() * 1000 + 604800000), "not_after": int(time.time() * 1000 + 604800000),
"not_before": int(time.time() * 1000 - 604800000), "not_before": int(time.time() * 1000 - 604800000),
"ts": int(time.time() * 1000) "ts": int(time.time() * 1000)
})) })
)
@identity.route("/_matrix/identity/v2/3pid/unbind", methods=["POST"]) @identity.route("/_matrix/identity/v2/3pid/unbind", methods=["POST"])
async def threepid_unbind(): async def threepid_unbind():
return jsonify({}) return jsonify({})
@identity.route("/_matrix/identity/v2/3pid/getValidated3pid") @identity.route("/_matrix/identity/v2/3pid/getValidated3pid")
async def threepid_validated(): async def threepid_validated():
# Please email abuse@matrix.org # Please email abuse@matrix.org
return jsonify({ return jsonify({
"address": "abuse@matrix.org", "address": "abuse@matrix.org",
"medium": "email", "medium": "email",
"validated_at": the_funny_number "validated_at": config.the_funny_number
}) })
# https://spec.matrix.org/latest/identity-service-api/#invitation-storage
@identity.route("/_matrix/identity/v2/store-invite", methods=["POST"]) @identity.route("/_matrix/identity/v2/store-invite", methods=["POST"])
async def invite(): async def invite():
return jsonify({ return jsonify({
"display_name": "Vona", "display_name": "Vona",
"public_keys": [ "public_keys": [
{ {
"key_validity_url": f"https://{server_name}/_matrix/identity/v2/pubkey/isvalid", "key_validity_url": f"https://{config.server_name}/_matrix/identity/v2/pubkey/isvalid",
"public_key": "ohyeah" "public_key": "ohyeah"
}, },
{ {
"key_validity_url": f"https://{server_name}/_matrix/identity/v2/pubkey/ephemeral/isvalid", "key_validity_url": f"https://{config.server_name}/_matrix/identity/v2/pubkey/ephemeral/isvalid",
"public_key": "burgerkingfootlettuce" "public_key": "burgerkingfootlettuce"
} }
], ],
@@ -144,7 +176,6 @@ async def invite():
}) })
# https://spec.matrix.org/latest/identity-service-api/#ephemeral-invitation-signing
@identity.route("/_matrix/identity/v2/sign-ed25519", methods=["POST"]) @identity.route("/_matrix/identity/v2/sign-ed25519", methods=["POST"])
async def invite_signing(): async def invite_signing():
required_keys = {"mxid", "private_key", "token"} required_keys = {"mxid", "private_key", "token"}

View File

@@ -98,7 +98,10 @@ try:
except ValueError: except ValueError:
join_event["origin_server_ts"] = int(f"{time.time() * 1000}".split(".")[0]) join_event["origin_server_ts"] = int(f"{time.time() * 1000}".split(".")[0])
signed_join = globals.hash_and_sign_event(join_event) signed_join = globals.hash_and_sign_event(
join_event,
int(make_join.get("room_version", "1"))
)
try: try:
send_join_response = http_client.put( send_join_response = http_client.put(

View File

@@ -1,21 +1,62 @@
import vona.globals as globals import vona.globals as globals
import vona.config as config import vona.config as config
import os import multiprocessing
versions = [str(i) for i in range(1, 10)] versions = [str(i) for i in range(1, 10)]
desired_ver = input("Desired room version:\n\t")
if desired_ver not in versions:
os._exit(1)
try: try:
while True: desired_ver = input("Desired room version:\n\t")
room_id = os.urandom(8).hex() except (EOFError, KeyboardInterrupt):
print("")
exit(0)
known = {
"1": "qa",
"2": "br",
"3": "3",
"4": "D",
"5": "U",
"6": "f",
"7": "gx",
"8": "hx",
"9": "iy",
"10": "p0",
"11": "jZ",
}
if desired_ver in known:
print(f"!{known[desired_ver]}:{config.server_name} (from known room ID list)")
try:
input("Press enter to bruteforce anyway:\n\t")
except (EOFError, KeyboardInterrupt):
print("")
exit(0)
elif desired_ver not in versions:
print("Unsupported room version")
exit(1)
def worker(room_found):
try:
while not room_found.value:
room_id = globals.make_event_id().replace("$", "!")
room_ver = globals.room_version_from_id(room_id) room_ver = globals.room_version_from_id(room_id)
if room_ver == desired_ver: if room_ver == desired_ver:
print(f"!{room_id}:{config.server_name}") print(room_id)
room_found.value = True
break break
except: except Exception:
print("")
pass pass
room_found = multiprocessing.Value('b', False)
processes = []
for _ in range(multiprocessing.cpu_count()):
p = multiprocessing.Process(target=worker, args=(room_found,))
processes.append(p)
p.start()
for p in processes:
p.join()