From decc32f7e6cf9068cd1f6b0fed70940d21847d77 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Thu, 12 Sep 2024 16:59:48 +1200 Subject: [PATCH] Change getPublicUrlForMxc function for authmedia --- discord/discord-command-handler.js | 5 ++-- m2d/converters/event-to-message.js | 2 ++ m2d/converters/utils.js | 43 ++++++++++++++++++++++-------- stdin.js | 1 - test/test.js | 1 + types.d.ts | 1 + 6 files changed, 39 insertions(+), 14 deletions(-) diff --git a/discord/discord-command-handler.js b/discord/discord-command-handler.js index a434bf97..d9ad80fa 100644 --- a/discord/discord-command-handler.js +++ b/discord/discord-command-handler.js @@ -11,6 +11,8 @@ const {discord, sync, db, select} = require("../passthrough") const api = sync.require("../matrix/api") /** @type {import("../matrix/file")} */ const file = sync.require("../matrix/file") +/** @type {import("../m2d/converters/utils")} */ +const mxUtils = sync.require("../matrix/utils") /** @type {import("../d2m/actions/create-space")} */ const createSpace = sync.require("../d2m/actions/create-space") /** @type {import("./utils")} */ @@ -91,9 +93,8 @@ const commands = [{ // Current avatar const avatarEvent = await api.getStateEvent(roomID, "m.room.avatar", "") - const avatarURLParts = avatarEvent?.url.match(/^mxc:\/\/([^/]+)\/(\w+)$/) let currentAvatarMessage = - ( avatarURLParts ? `Current room-specific avatar: ${reg.ooye.server_origin}/_matrix/media/r0/download/${avatarURLParts[1]}/${avatarURLParts[2]}` + ( avatarEvent.url ? `Current room-specific avatar: ${mxUtils.getPublicUrlForMxc(avatarEvent.url)}` : "No avatar. Now's your time to strike. Use `//icon` again with a link or upload to set the room-specific avatar.") // Next potential avatar diff --git a/m2d/converters/event-to-message.js b/m2d/converters/event-to-message.js index 52347554..76a32bc7 100644 --- a/m2d/converters/event-to-message.js +++ b/m2d/converters/event-to-message.js @@ -15,6 +15,8 @@ const {sync, db, discord, select, from} = passthrough const mxUtils = sync.require("../converters/utils") /** @type {import("../../discord/utils")} */ const dUtils = sync.require("../../discord/utils") +/** @type {import("../../matrix/file")} */ +const file = sync.require("../../matrix/file") /** @type {import("./emoji-sheet")} */ const emojiSheet = sync.require("./emoji-sheet") diff --git a/m2d/converters/utils.js b/m2d/converters/utils.js index 437b5c07..c1f5f010 100644 --- a/m2d/converters/utils.js +++ b/m2d/converters/utils.js @@ -1,8 +1,13 @@ // @ts-check +const assert = require("assert").strict + +const passthrough = require("../../passthrough") +const {db} = passthrough + const {reg} = require("../../matrix/read-registration") const userRegex = reg.namespaces.users.map(u => new RegExp(u.regex)) -const assert = require("assert").strict + /** @type {import("xxhash-wasm").XXHashAPI} */ // @ts-ignore let hasher = null // @ts-ignore @@ -35,16 +40,6 @@ function eventSenderIsFromDiscord(sender) { return false } -/** - * @param {string} mxc - * @returns {string?} - */ -function getPublicUrlForMxc(mxc) { - const avatarURLParts = mxc?.match(/^mxc:\/\/([^/]+)\/(\w+)$/) - if (avatarURLParts) return `${reg.ooye.server_origin}/_matrix/media/r0/download/${avatarURLParts[1]}/${avatarURLParts[2]}` - else return null -} - /** * Event IDs are really big and have more entropy than we need. * If we want to store the event ID in the database, we can store a more compact version by hashing it with this. @@ -213,6 +208,32 @@ async function getViaServersQuery(roomID, api) { return qs } +/** + * Since the introduction of authenticated media, this can no longer just be the /_matrix/media/r0/download URL + * because Discord and Discord users cannot use those URLs. Media now has to be proxied through the bridge. + * To avoid the bridge acting as a proxy for *any* media, there is a list of permitted media stored in the database. + * (The other approach would be signing the URLs with a MAC (or similar) and adding the signature, but I'm not a + * cryptographer, so I don't want to.) To reduce database disk space usage, instead of storing each permitted URL, + * we just store its xxhash as a signed (as in +/-, not signature) 64-bit integer, which fits in an SQLite integer field. + * @see https://matrix.org/blog/2024/06/26/sunsetting-unauthenticated-media/ background + * @see https://matrix.org/blog/2024/06/20/matrix-v1.11-release/ implementation details + * @see https://www.sqlite.org/fileformat2.html#record_format SQLite integer field size + * @param {string} mxc + * @returns {string?} + */ +function getPublicUrlForMxc(mxc) { + assert(hasher, "xxhash is not ready yet") + const avatarURLParts = mxc?.match(/^mxc:\/\/([^/]+)\/(\w+)$/) + if (!avatarURLParts) return null + + const serverAndMediaID = `${avatarURLParts[1]}/${avatarURLParts[2]}` + const unsignedHash = hasher.h64(serverAndMediaID) + const signedHash = unsignedHash - 0x8000000000000000n // shifting down to signed 64-bit range + db.prepare("INSERT OR IGNORE INTO media_proxy (permitted_hash) VALUES (?)").run(signedHash) + + return `${reg.ooye.bridge_origin}/download/matrix/${serverAndMediaID}` +} + module.exports.BLOCK_ELEMENTS = BLOCK_ELEMENTS module.exports.eventSenderIsFromDiscord = eventSenderIsFromDiscord module.exports.getPublicUrlForMxc = getPublicUrlForMxc diff --git a/stdin.js b/stdin.js index 5e23f720..780b9dc0 100644 --- a/stdin.js +++ b/stdin.js @@ -32,7 +32,6 @@ if (process.stdin.isTTY) { } else { Object.assign(passthrough.repl.context, extraContext) } - // @ts-expect-error Says exit isn't assignable to a string sync.addTemporaryListener(passthrough.repl, "exit", () => process.exit()) }) } diff --git a/test/test.js b/test/test.js index 1941ea2a..c2b703dc 100644 --- a/test/test.js +++ b/test/test.js @@ -23,6 +23,7 @@ reg.ooye.server_name = "cadence.moe" reg.id = "baby" // don't actually take authenticated actions on the server reg.as_token = "baby" reg.hs_token = "baby" +reg.ooye.bridge_origin = "https://bridge.example.org" reg.ooye.invite = [] const sync = new HeatSync({watchFS: false}) diff --git a/types.d.ts b/types.d.ts index 8eeda6c4..93bfc75d 100644 --- a/types.d.ts +++ b/types.d.ts @@ -21,6 +21,7 @@ export type AppServiceRegistrationConfig = { max_file_size: number server_name: string server_origin: string + bridge_origin: string content_length_workaround: boolean include_user_id_in_mxid: boolean invite: string[]