Skip to content

Commit

Permalink
Merge pull request #365 from tadzik/tadzik/media-proxy
Browse files Browse the repository at this point in the history
Integrate MediaProxy to bridge authenticated Matrix media (MSC3916)
  • Loading branch information
tadzik authored Sep 2, 2024
2 parents eb621a6 + e1d35aa commit 830084c
Show file tree
Hide file tree
Showing 12 changed files with 114 additions and 43 deletions.
1 change: 1 addition & 0 deletions changelog.d/365.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Use MediaProxy to serve authenticated Matrix media.
14 changes: 12 additions & 2 deletions config.sample.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,20 @@ bridge:
homeserverUrl: "http://localhost:8008"
# Prefix of all users of the bridge.
userPrefix: "_bifrost_"
# If homeserverUrl is not reachable publically, the public address that media can be reached on.
# mediaserverUrl: "http://example.com:8008"
# Set this to the port you want the bridge to listen on.
appservicePort: 9555
# Config for the media proxy
# required to serve publically accessible URLs to authenticated Matrix media
mediaProxy:
# To generate a .jwk file:
# $ node src/generate-signing-key.js > signingkey.jwk
signingKeyPath: "signingkey.jwk"
# How long should the generated URLs be valid for
ttlSeconds: 3600
# The port for the media proxy to listen on
bindPort: 11111
# The publically accessible URL to the media proxy
publicUrl: "https://bifrost.bridge/media"

roomRules: []
# - room: "#badroom:example.com"
Expand Down
16 changes: 13 additions & 3 deletions config/config.schema.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,28 @@ required: ["bridge", "datastore", "purple", "portals"]
properties:
bridge:
type: object
required: ["domain", "homeserverUrl", "userPrefix"]
required: ["domain", "homeserverUrl", "userPrefix", "mediaProxy"]
properties:
domain:
type: string
homeserverUrl:
type: string
mediaserverUrl:
type: string
userPrefix:
type: string
appservicePort:
type: number
mediaProxy:
type: "object"
properties:
signingKeyPath:
type: "string"
ttlSeconds:
type: "integer"
bindPort:
type: "integer"
publicUrl:
type: "string"
required: ["signingKeyPath", "ttlSeconds", "bindPort", "publicUrl"]
datastore:
required: ["engine"]
type: "object"
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
"leven": "^3.0.0",
"marked": "^11.1.1",
"nedb": "^1.8.0",
"matrix-appservice-bridge": "^10.1.0",
"matrix-appservice-bridge": "^10.2.0",
"pg": "8.11.3",
"prom-client": "^15.1.0",
"quick-lru": "^5.0.0"
Expand Down
16 changes: 13 additions & 3 deletions src/Config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,14 @@ export class Config {
public readonly bridge: IConfigBridge = {
domain: "",
homeserverUrl: "",
mediaserverUrl: undefined,
userPrefix: "_bifrost_",
appservicePort: 9555,
mediaProxy: {
signingKeyPath: "",
ttlSeconds: 0,
bindPort: 0,
publicUrl: ""
},
};

public readonly roomRules: IConfigRoomRule[] = [];
Expand Down Expand Up @@ -102,9 +107,14 @@ export class Config {
export interface IConfigBridge {
domain: string;
homeserverUrl: string;
mediaserverUrl?: string;
userPrefix: string;
appservicePort?: number;
mediaProxy: {
signingKeyPath: string;
ttlSeconds: number;
bindPort: number;
publicUrl: string;
},
}

export interface IConfigPurple {
Expand Down Expand Up @@ -183,4 +193,4 @@ export interface IConfigRoomRule {
* Should the room be allowed, or denied.
*/
action: "allow"|"deny";
}
}
9 changes: 5 additions & 4 deletions src/MatrixEventHandler.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Bridge, MatrixUser, Request, WeakEvent, RoomBridgeStoreEntry, TypingEvent, PresenceEvent } from "matrix-appservice-bridge";
import { Bridge, MatrixUser, Request, WeakEvent, RoomBridgeStoreEntry, TypingEvent, PresenceEvent, MediaProxy } from "matrix-appservice-bridge";
import { MatrixMembershipEvent, MatrixMessageEvent } from "./MatrixTypes";
import { MROOM_TYPE_UADMIN, MROOM_TYPE_IM, MROOM_TYPE_GROUP,
IRemoteUserAdminData,
Expand Down Expand Up @@ -37,6 +37,7 @@ export class MatrixEventHandler {
private readonly config: Config,
private readonly gatewayHandler: GatewayHandler,
private readonly bridge: Bridge,
private readonly mediaProxy: MediaProxy,
private readonly autoReg: AutoRegistration|null = null,
) {
this.roomAliases = new RoomAliasSet(this.config.portals, this.purple);
Expand Down Expand Up @@ -609,7 +610,7 @@ Say \`help\` for more commands.
}
const recipient: string = context.remote.get("recipient");
log.info(`Sending IM to ${recipient}`);
const msg = MessageFormatter.matrixEventToBody(event as MatrixMessageEvent, this.config.bridge);
const msg = await MessageFormatter.matrixEventToBody(event as MatrixMessageEvent, this.config.bridge, this.mediaProxy);
acct.sendIM(recipient, msg);
}

Expand All @@ -625,7 +626,7 @@ Say \`help\` for more commands.
const isGateway: boolean = context.remote.get("gateway");
const name: string = context.remote.get("room_name");
if (isGateway) {
const msg = MessageFormatter.matrixEventToBody(event as MatrixMessageEvent, this.config.bridge);
const msg = await MessageFormatter.matrixEventToBody(event as MatrixMessageEvent, this.config.bridge, this.mediaProxy);
try {
await this.gatewayHandler.sendMatrixMessage(name, event.sender, msg, context);
} catch (ex) {
Expand All @@ -645,7 +646,7 @@ Say \`help\` for more commands.
await this.joinOrDefer(acct, name, props);
}
const roomName: string = context.remote.get("room_name");
const msg = MessageFormatter.matrixEventToBody(event as MatrixMessageEvent, this.config.bridge);
const msg = await MessageFormatter.matrixEventToBody(event as MatrixMessageEvent, this.config.bridge, this.mediaProxy);
let nick = "";
// XXX: Gnarly way of trying to determine who we are.
try {
Expand Down
8 changes: 3 additions & 5 deletions src/MessageFormatter.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { BifrostProtocol } from "./bifrost/Protocol";
import { PRPL_S4B, PRPL_XMPP } from "./ProtoHacks";
import { Parser } from "htmlparser2";
import { Intent, Logger } from "matrix-appservice-bridge";
import { Intent, Logger, MediaProxy } from "matrix-appservice-bridge";
import { IConfigBridge } from "./Config";
import { IMatrixMsgContents, MatrixMessageEvent } from "./MatrixTypes";

Expand Down Expand Up @@ -33,7 +33,7 @@ const log = new Logger("MessageFormatter");

export class MessageFormatter {

public static matrixEventToBody(event: MatrixMessageEvent, config: IConfigBridge): IBasicProtocolMessage {
public static async matrixEventToBody(event: MatrixMessageEvent, config: IConfigBridge, mediaProxy: MediaProxy): Promise<IBasicProtocolMessage> {
let content = event.content;
const originalMessage = event.content["m.relates_to"]?.event_id;
const formatted: {type: string, body: string}[] = [];
Expand All @@ -51,15 +51,13 @@ export class MessageFormatter {
return {body: `/me ${content.body}`, formatted, id: event.event_id};
}
if (["m.file", "m.image", "m.video"].includes(event.content.msgtype) && event.content.url) {
const [domain, mediaId] = event.content.url.substr("mxc://".length).split("/");
const url = (config.mediaserverUrl ? config.mediaserverUrl : config.homeserverUrl).replace(/\/$/, "");
return {
body: content.body,
id: event.event_id,
opts: {
attachments: [
{
uri: `${url}/_matrix/media/v1/download/${domain}/${mediaId}`,
uri: (await mediaProxy.generateMediaUrl(event.content.url)).toString(),
mimetype: event.content.info?.mimetype,
size: event.content.info?.size,
},
Expand Down
26 changes: 23 additions & 3 deletions src/Program.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Cli, Bridge, AppServiceRegistration, Logger, TypingEvent, Request, PresenceEvent } from "matrix-appservice-bridge";
import { Cli, Bridge, AppServiceRegistration, Logger, TypingEvent, Request, PresenceEvent, MediaProxy } from "matrix-appservice-bridge";
import { EventEmitter } from "events";
import { MatrixEventHandler } from "./MatrixEventHandler";
import { MatrixRoomHandler } from "./MatrixRoomHandler";
Expand All @@ -16,6 +16,9 @@ import { AutoRegistration } from "./AutoRegistration";
import { GatewayHandler } from "./GatewayHandler";
import { IRemoteUserAdminData, MROOM_TYPE_UADMIN } from "./store/Types";

import * as fs from "fs";
import { webcrypto } from "node:crypto";

Logger.configure({console: "debug"});
const log = new Logger("Program");
const bridgeLog = new Logger("bridge");
Expand Down Expand Up @@ -88,6 +91,21 @@ class Program {
callback(reg);
}

private async initialiseMediaProxy(): Promise<MediaProxy> {
const config = this.config.bridge.mediaProxy;
const jwk = JSON.parse(fs.readFileSync(config.signingKeyPath, "utf8").toString());
const signingKey = await webcrypto.subtle.importKey('jwk', jwk, {
name: 'HMAC',
hash: 'SHA-512',
}, true, ['sign', 'verify']);
const publicUrl = new URL(config.publicUrl);

const mediaProxy = new MediaProxy({ publicUrl, signingKey, ttl: config.ttlSeconds * 1000 }, this.bridge.getIntent().matrixClient);
mediaProxy.start(config.bindPort);

return mediaProxy;
}

private async waitForHomeserver() {
log.info("Checking if homeserver is up");
// Wait for the homeserver to start before progressing with the bridge.
Expand Down Expand Up @@ -314,8 +332,10 @@ class Program {
this.roomSync = new RoomSync(
purple, this.store, this.deduplicator, this.gatewayHandler, this.bridge.getIntent(),
);
const mediaProxy = await this.initialiseMediaProxy();

this.eventHandler = new MatrixEventHandler(
purple, this.store, this.deduplicator, this.config, this.gatewayHandler, this.bridge, autoReg,
purple, this.store, this.deduplicator, this.config, this.gatewayHandler, this.bridge, mediaProxy, autoReg
);

await this.bridge.listen(port);
Expand Down Expand Up @@ -351,4 +371,4 @@ new Program().start();

process.on('unhandledRejection', (reason, promise) => {
log.warn(`Unhandled rejection`, reason, promise);
});
});
11 changes: 11 additions & 0 deletions src/generate-signing-key.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
const webcrypto = require('node:crypto');

async function main() {
const key = await webcrypto.subtle.generateKey({
name: 'HMAC',
hash: 'SHA-512',
}, true, ['sign', 'verify']);
console.log(JSON.stringify(await webcrypto.subtle.exportKey('jwk', key), undefined, 4));
}

main().then(() => process.exit(0)).catch(err => { throw err });
3 changes: 2 additions & 1 deletion test/test_matrixeventhandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,8 @@ function createMEH() {
new Deduplicator(),
config,
gatewayHandler as any,
bridge as any
bridge as any,
{} as any,
);
return {meh, store};
}
Expand Down
43 changes: 26 additions & 17 deletions test/test_messageformatter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,14 @@ const intent = {
}),
} as any;

const mediaProxy = {
generateMediaUrl: () => Promise.resolve('http://mediaproxy/token'),
} as any;

describe("MessageFormatter", () => {
describe("matrixEventToBody", () => {
it("should transform a plain text message to a basic body", () => {
const msg = MessageFormatter.matrixEventToBody({
it("should transform a plain text message to a basic body", async () => {
const msg = await MessageFormatter.matrixEventToBody({
sender: "@foo:bar",
event_id: "$event:bar",
content: {
Expand All @@ -35,15 +39,16 @@ describe("MessageFormatter", () => {
domain: "bar",
homeserverUrl: "http://bar",
userPrefix: "_xmpp",
});
mediaProxy: {} as any,
}, mediaProxy);
expect(msg).to.deep.eq({
body: "This is some plaintext!",
formatted: [],
id: "$event:bar",
});
});
it("should transform a formatted message", () => {
const msg = MessageFormatter.matrixEventToBody({
it("should transform a formatted message", async () => {
const msg = await MessageFormatter.matrixEventToBody({
sender: "@foo:bar",
event_id: "$event:bar",
content: {
Expand All @@ -59,7 +64,8 @@ describe("MessageFormatter", () => {
domain: "bar",
homeserverUrl: "http://bar",
userPrefix: "_xmpp",
});
mediaProxy: {} as any,
}, mediaProxy);
expect(msg).to.deep.eq({
body: "This is some plaintext!",
formatted: [{
Expand All @@ -69,8 +75,8 @@ describe("MessageFormatter", () => {
id: "$event:bar",
});
});
it("should transform an info-less media event", () => {
const msg = MessageFormatter.matrixEventToBody({
it("should transform an info-less media event", async () => {
const msg = await MessageFormatter.matrixEventToBody({
sender: "@foo:bar",
event_id: "$event:bar",
content: {
Expand All @@ -85,23 +91,24 @@ describe("MessageFormatter", () => {
domain: "bar",
homeserverUrl: "http://bar",
userPrefix: "_xmpp",
});
mediaProxy: {} as any,
}, mediaProxy);
expect(msg).to.deep.eq({
body: "image.jpg",
opts: {
attachments: [
{
mimetype: undefined,
size: undefined,
uri: "http://bar/_matrix/media/v1/download/bar/foosdsd",
uri: "http://mediaproxy/token",
},
],
},
id: "$event:bar",
});
});
it("should transform a media event", () => {
const msg = MessageFormatter.matrixEventToBody({
it("should transform a media event", async () => {
const msg = await MessageFormatter.matrixEventToBody({
sender: "@foo:bar",
event_id: "$event:bar",
content: {
Expand All @@ -120,23 +127,24 @@ describe("MessageFormatter", () => {
domain: "bar",
homeserverUrl: "http://bar",
userPrefix: "_xmpp",
});
mediaProxy: {} as any,
}, mediaProxy);
expect(msg).to.deep.eq({
body: "image.jpg",
opts: {
attachments: [
{
mimetype: "image/jpeg",
size: 1000,
uri: "http://bar/_matrix/media/v1/download/bar/foosdsd",
uri: "http://mediaproxy/token",
},
],
},
id: "$event:bar",
});
});
it("should transform a emote message to a basic body", () => {
const msg = MessageFormatter.matrixEventToBody({
it("should transform a emote message to a basic body", async () => {
const msg = await MessageFormatter.matrixEventToBody({
sender: "@foo:bar",
event_id: "$event:bar",
content: {
Expand All @@ -150,7 +158,8 @@ describe("MessageFormatter", () => {
domain: "bar",
homeserverUrl: "http://bar",
userPrefix: "_xmpp",
});
mediaProxy: {} as any,
}, mediaProxy);
expect(msg).to.deep.eq({
body: "/me pets the dog",
formatted: [],
Expand Down
Loading

0 comments on commit 830084c

Please sign in to comment.