Skip to content

Commit 830084c

Browse files
authored
Merge pull request #365 from tadzik/tadzik/media-proxy
Integrate MediaProxy to bridge authenticated Matrix media (MSC3916)
2 parents eb621a6 + e1d35aa commit 830084c

12 files changed

+114
-43
lines changed

changelog.d/365.feature

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Use MediaProxy to serve authenticated Matrix media.

config.sample.yaml

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,20 @@ bridge:
55
homeserverUrl: "http://localhost:8008"
66
# Prefix of all users of the bridge.
77
userPrefix: "_bifrost_"
8-
# If homeserverUrl is not reachable publically, the public address that media can be reached on.
9-
# mediaserverUrl: "http://example.com:8008"
108
# Set this to the port you want the bridge to listen on.
119
appservicePort: 9555
10+
# Config for the media proxy
11+
# required to serve publically accessible URLs to authenticated Matrix media
12+
mediaProxy:
13+
# To generate a .jwk file:
14+
# $ node src/generate-signing-key.js > signingkey.jwk
15+
signingKeyPath: "signingkey.jwk"
16+
# How long should the generated URLs be valid for
17+
ttlSeconds: 3600
18+
# The port for the media proxy to listen on
19+
bindPort: 11111
20+
# The publically accessible URL to the media proxy
21+
publicUrl: "https://bifrost.bridge/media"
1222

1323
roomRules: []
1424
# - room: "#badroom:example.com"

config/config.schema.yaml

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,28 @@ required: ["bridge", "datastore", "purple", "portals"]
55
properties:
66
bridge:
77
type: object
8-
required: ["domain", "homeserverUrl", "userPrefix"]
8+
required: ["domain", "homeserverUrl", "userPrefix", "mediaProxy"]
99
properties:
1010
domain:
1111
type: string
1212
homeserverUrl:
1313
type: string
14-
mediaserverUrl:
15-
type: string
1614
userPrefix:
1715
type: string
1816
appservicePort:
1917
type: number
18+
mediaProxy:
19+
type: "object"
20+
properties:
21+
signingKeyPath:
22+
type: "string"
23+
ttlSeconds:
24+
type: "integer"
25+
bindPort:
26+
type: "integer"
27+
publicUrl:
28+
type: "string"
29+
required: ["signingKeyPath", "ttlSeconds", "bindPort", "publicUrl"]
2030
datastore:
2131
required: ["engine"]
2232
type: "object"

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@
3939
"leven": "^3.0.0",
4040
"marked": "^11.1.1",
4141
"nedb": "^1.8.0",
42-
"matrix-appservice-bridge": "^10.1.0",
42+
"matrix-appservice-bridge": "^10.2.0",
4343
"pg": "8.11.3",
4444
"prom-client": "^15.1.0",
4545
"quick-lru": "^5.0.0"

src/Config.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,14 @@ export class Config {
1212
public readonly bridge: IConfigBridge = {
1313
domain: "",
1414
homeserverUrl: "",
15-
mediaserverUrl: undefined,
1615
userPrefix: "_bifrost_",
1716
appservicePort: 9555,
17+
mediaProxy: {
18+
signingKeyPath: "",
19+
ttlSeconds: 0,
20+
bindPort: 0,
21+
publicUrl: ""
22+
},
1823
};
1924

2025
public readonly roomRules: IConfigRoomRule[] = [];
@@ -102,9 +107,14 @@ export class Config {
102107
export interface IConfigBridge {
103108
domain: string;
104109
homeserverUrl: string;
105-
mediaserverUrl?: string;
106110
userPrefix: string;
107111
appservicePort?: number;
112+
mediaProxy: {
113+
signingKeyPath: string;
114+
ttlSeconds: number;
115+
bindPort: number;
116+
publicUrl: string;
117+
},
108118
}
109119

110120
export interface IConfigPurple {
@@ -183,4 +193,4 @@ export interface IConfigRoomRule {
183193
* Should the room be allowed, or denied.
184194
*/
185195
action: "allow"|"deny";
186-
}
196+
}

src/MatrixEventHandler.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Bridge, MatrixUser, Request, WeakEvent, RoomBridgeStoreEntry, TypingEvent, PresenceEvent } from "matrix-appservice-bridge";
1+
import { Bridge, MatrixUser, Request, WeakEvent, RoomBridgeStoreEntry, TypingEvent, PresenceEvent, MediaProxy } from "matrix-appservice-bridge";
22
import { MatrixMembershipEvent, MatrixMessageEvent } from "./MatrixTypes";
33
import { MROOM_TYPE_UADMIN, MROOM_TYPE_IM, MROOM_TYPE_GROUP,
44
IRemoteUserAdminData,
@@ -37,6 +37,7 @@ export class MatrixEventHandler {
3737
private readonly config: Config,
3838
private readonly gatewayHandler: GatewayHandler,
3939
private readonly bridge: Bridge,
40+
private readonly mediaProxy: MediaProxy,
4041
private readonly autoReg: AutoRegistration|null = null,
4142
) {
4243
this.roomAliases = new RoomAliasSet(this.config.portals, this.purple);
@@ -609,7 +610,7 @@ Say \`help\` for more commands.
609610
}
610611
const recipient: string = context.remote.get("recipient");
611612
log.info(`Sending IM to ${recipient}`);
612-
const msg = MessageFormatter.matrixEventToBody(event as MatrixMessageEvent, this.config.bridge);
613+
const msg = await MessageFormatter.matrixEventToBody(event as MatrixMessageEvent, this.config.bridge, this.mediaProxy);
613614
acct.sendIM(recipient, msg);
614615
}
615616

@@ -625,7 +626,7 @@ Say \`help\` for more commands.
625626
const isGateway: boolean = context.remote.get("gateway");
626627
const name: string = context.remote.get("room_name");
627628
if (isGateway) {
628-
const msg = MessageFormatter.matrixEventToBody(event as MatrixMessageEvent, this.config.bridge);
629+
const msg = await MessageFormatter.matrixEventToBody(event as MatrixMessageEvent, this.config.bridge, this.mediaProxy);
629630
try {
630631
await this.gatewayHandler.sendMatrixMessage(name, event.sender, msg, context);
631632
} catch (ex) {
@@ -645,7 +646,7 @@ Say \`help\` for more commands.
645646
await this.joinOrDefer(acct, name, props);
646647
}
647648
const roomName: string = context.remote.get("room_name");
648-
const msg = MessageFormatter.matrixEventToBody(event as MatrixMessageEvent, this.config.bridge);
649+
const msg = await MessageFormatter.matrixEventToBody(event as MatrixMessageEvent, this.config.bridge, this.mediaProxy);
649650
let nick = "";
650651
// XXX: Gnarly way of trying to determine who we are.
651652
try {

src/MessageFormatter.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { BifrostProtocol } from "./bifrost/Protocol";
22
import { PRPL_S4B, PRPL_XMPP } from "./ProtoHacks";
33
import { Parser } from "htmlparser2";
4-
import { Intent, Logger } from "matrix-appservice-bridge";
4+
import { Intent, Logger, MediaProxy } from "matrix-appservice-bridge";
55
import { IConfigBridge } from "./Config";
66
import { IMatrixMsgContents, MatrixMessageEvent } from "./MatrixTypes";
77

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

3434
export class MessageFormatter {
3535

36-
public static matrixEventToBody(event: MatrixMessageEvent, config: IConfigBridge): IBasicProtocolMessage {
36+
public static async matrixEventToBody(event: MatrixMessageEvent, config: IConfigBridge, mediaProxy: MediaProxy): Promise<IBasicProtocolMessage> {
3737
let content = event.content;
3838
const originalMessage = event.content["m.relates_to"]?.event_id;
3939
const formatted: {type: string, body: string}[] = [];
@@ -51,15 +51,13 @@ export class MessageFormatter {
5151
return {body: `/me ${content.body}`, formatted, id: event.event_id};
5252
}
5353
if (["m.file", "m.image", "m.video"].includes(event.content.msgtype) && event.content.url) {
54-
const [domain, mediaId] = event.content.url.substr("mxc://".length).split("/");
55-
const url = (config.mediaserverUrl ? config.mediaserverUrl : config.homeserverUrl).replace(/\/$/, "");
5654
return {
5755
body: content.body,
5856
id: event.event_id,
5957
opts: {
6058
attachments: [
6159
{
62-
uri: `${url}/_matrix/media/v1/download/${domain}/${mediaId}`,
60+
uri: (await mediaProxy.generateMediaUrl(event.content.url)).toString(),
6361
mimetype: event.content.info?.mimetype,
6462
size: event.content.info?.size,
6563
},

src/Program.ts

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Cli, Bridge, AppServiceRegistration, Logger, TypingEvent, Request, PresenceEvent } from "matrix-appservice-bridge";
1+
import { Cli, Bridge, AppServiceRegistration, Logger, TypingEvent, Request, PresenceEvent, MediaProxy } from "matrix-appservice-bridge";
22
import { EventEmitter } from "events";
33
import { MatrixEventHandler } from "./MatrixEventHandler";
44
import { MatrixRoomHandler } from "./MatrixRoomHandler";
@@ -16,6 +16,9 @@ import { AutoRegistration } from "./AutoRegistration";
1616
import { GatewayHandler } from "./GatewayHandler";
1717
import { IRemoteUserAdminData, MROOM_TYPE_UADMIN } from "./store/Types";
1818

19+
import * as fs from "fs";
20+
import { webcrypto } from "node:crypto";
21+
1922
Logger.configure({console: "debug"});
2023
const log = new Logger("Program");
2124
const bridgeLog = new Logger("bridge");
@@ -88,6 +91,21 @@ class Program {
8891
callback(reg);
8992
}
9093

94+
private async initialiseMediaProxy(): Promise<MediaProxy> {
95+
const config = this.config.bridge.mediaProxy;
96+
const jwk = JSON.parse(fs.readFileSync(config.signingKeyPath, "utf8").toString());
97+
const signingKey = await webcrypto.subtle.importKey('jwk', jwk, {
98+
name: 'HMAC',
99+
hash: 'SHA-512',
100+
}, true, ['sign', 'verify']);
101+
const publicUrl = new URL(config.publicUrl);
102+
103+
const mediaProxy = new MediaProxy({ publicUrl, signingKey, ttl: config.ttlSeconds * 1000 }, this.bridge.getIntent().matrixClient);
104+
mediaProxy.start(config.bindPort);
105+
106+
return mediaProxy;
107+
}
108+
91109
private async waitForHomeserver() {
92110
log.info("Checking if homeserver is up");
93111
// Wait for the homeserver to start before progressing with the bridge.
@@ -314,8 +332,10 @@ class Program {
314332
this.roomSync = new RoomSync(
315333
purple, this.store, this.deduplicator, this.gatewayHandler, this.bridge.getIntent(),
316334
);
335+
const mediaProxy = await this.initialiseMediaProxy();
336+
317337
this.eventHandler = new MatrixEventHandler(
318-
purple, this.store, this.deduplicator, this.config, this.gatewayHandler, this.bridge, autoReg,
338+
purple, this.store, this.deduplicator, this.config, this.gatewayHandler, this.bridge, mediaProxy, autoReg
319339
);
320340

321341
await this.bridge.listen(port);
@@ -351,4 +371,4 @@ new Program().start();
351371

352372
process.on('unhandledRejection', (reason, promise) => {
353373
log.warn(`Unhandled rejection`, reason, promise);
354-
});
374+
});

src/generate-signing-key.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
const webcrypto = require('node:crypto');
2+
3+
async function main() {
4+
const key = await webcrypto.subtle.generateKey({
5+
name: 'HMAC',
6+
hash: 'SHA-512',
7+
}, true, ['sign', 'verify']);
8+
console.log(JSON.stringify(await webcrypto.subtle.exportKey('jwk', key), undefined, 4));
9+
}
10+
11+
main().then(() => process.exit(0)).catch(err => { throw err });

test/test_matrixeventhandler.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,8 @@ function createMEH() {
7676
new Deduplicator(),
7777
config,
7878
gatewayHandler as any,
79-
bridge as any
79+
bridge as any,
80+
{} as any,
8081
);
8182
return {meh, store};
8283
}

0 commit comments

Comments
 (0)