Skip to content

Commit

Permalink
Integrate MediaProxy to bridge authenticated Matrix media (MSC3910)
Browse files Browse the repository at this point in the history
  • Loading branch information
tadzik committed May 29, 2024
1 parent 1835e04 commit 25990cf
Show file tree
Hide file tree
Showing 8 changed files with 91 additions and 30 deletions.
18 changes: 12 additions & 6 deletions config.sample.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,6 @@ homeserver:
# The URL to the home server for client-server API calls, also used to form the
# media URLs as displayed in bridged IRC channels:
url: "http://localhost:8008"
#
# The URL of the homeserver hosting media files. This is only used to transform
# mxc URIs to http URIs when bridging m.room.[file|image] events. Optional. By
# default, this is the homeserver URL, specified above.
# This key CAN be hot-reloaded.
# media_url: "http://media.repo:8008"

# Drop Matrix messages which are older than this number of seconds, according to
# the event's origin_server_ts.
Expand Down Expand Up @@ -607,6 +601,18 @@ ircService:
# Ignore users mentioned in a io.element.functional_members state event when checking admin room membership
ignoreFunctionalMembersInAdminRooms: false

# Config for the media proxy, required to serve publically accessible URLs to authenticated Matrix media
mediaProxy:
# To generate a .jwk file:
# $ yarn -s ts-node src/generate-signing-key.ts > 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://irc.bridge/media"

# Maximum number of montly active users, beyond which the bridge gets blocked (both ways)
# RMAUlimit: 100

Expand Down
14 changes: 12 additions & 2 deletions config.schema.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,6 @@ properties:
properties:
url:
type: "string"
media_url:
type: "string"
domain:
type: "string"
dropMatrixMessagesAfterSecs:
Expand Down Expand Up @@ -173,6 +171,18 @@ properties:
type: "integer"
ignoreFunctionalMembersInAdminRooms:
type: "boolean"
mediaProxy:
type: "object"
properties:
signingKeyPath:
type: "string"
ttlSeconds:
type: "integer"
bindPort:
type: "integer"
publicUrl:
type: "string"
required: ["signingKeyPath", "ttlSeconds", "bindPort", "publicUrl"]
ircHandler:
type: "object"
properties:
Expand Down
36 changes: 33 additions & 3 deletions src/bridge/IrcBridge.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import Bluebird from "bluebird";
import extend from "extend";
import * as fs from "fs";
import * as promiseutil from "../promiseutil";
import { IrcHandler, MatrixMembership } from "./IrcHandler";
import { MatrixHandler, MatrixEventInvite, OnMemberEventData, MatrixEventKick } from "./MatrixHandler";
Expand Down Expand Up @@ -41,6 +42,7 @@ import {
UserActivityTracker,
UserActivityTrackerConfig,
WeakStateEvent,
MediaProxy,

Check failure on line 45 in src/bridge/IrcBridge.ts

View workflow job for this annotation

GitHub Actions / test (20)

Module '"matrix-appservice-bridge"' has no exported member 'MediaProxy'.
} from "matrix-appservice-bridge";
import { IrcAction } from "../models/IrcAction";
import { DataStore } from "../datastore/DataStore";
Expand All @@ -56,6 +58,7 @@ import { TestingOptions } from "../config/TestOpts";
import { MatrixBanSync } from "./MatrixBanSync";
import { configure } from "../logging";
import { IrcPoolClient } from "../pool-service/IrcPoolClient";
import { webcrypto } from "node:crypto";

const log = getLogger("IrcBridge");
const DEFAULT_PORT = 8090;
Expand Down Expand Up @@ -88,6 +91,7 @@ export class IrcBridge {
public activityTracker: ActivityTracker|null = null;
public readonly roomConfigs: RoomConfig;
public readonly matrixBanSyncer?: MatrixBanSync;
private _mediaProxy?: MediaProxy;
private clientPool!: ClientPool; // This gets defined in the `run` function
private ircServers: IrcServer[] = [];
private memberListSyncers: {[domain: string]: MemberListSyncer} = {};
Expand Down Expand Up @@ -209,6 +213,7 @@ export class IrcBridge {
defaultTtlMs: 10 * 60 * 1000, // 10 mins
});
this.matrixBanSyncer = this.config.ircService.banLists && new MatrixBanSync(this.config.ircService.banLists);

this.matrixHandler = new MatrixHandler(this, this.config.ircService.matrixHandler, this.membershipQueue);
this.privacyProtection = new PrivacyProtection(this);
this.ircHandler = new IrcHandler(
Expand Down Expand Up @@ -255,9 +260,10 @@ export class IrcBridge {
log.info(`Adjusted dropMatrixMessagesAfterSecs to ${newConfig.homeserver.dropMatrixMessagesAfterSecs}`);
}

if (oldConfig.homeserver.media_url !== newConfig.homeserver.media_url) {
oldConfig.homeserver.media_url = newConfig.homeserver.media_url;
log.info(`Adjusted media_url to ${newConfig.homeserver.media_url}`);
if (JSON.stringify(oldConfig.ircService.mediaProxy) !== JSON.stringify(newConfig.ircService.mediaProxy)) {
await this._mediaProxy?.close();
this._mediaProxy = await this.initialiseMediaProxy();
log.info(`Media proxy reinitialized`);
}

await this.setupStateSyncer(newConfig);
Expand Down Expand Up @@ -308,6 +314,21 @@ export class IrcBridge {
await this.clientPool.checkForBannedConnectedUsers();
}

private async initialiseMediaProxy(): Promise<MediaProxy> {
const config = this.config.ircService.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 initialiseMetrics(bindPort: number) {
const zeroAge = new AgeCounters();
const registry = new Registry();
Expand Down Expand Up @@ -523,6 +544,13 @@ export class IrcBridge {
return `@${this.registration.getSenderLocalpart()}:${this.domain}`;
}

public get mediaProxy(): MediaProxy {
if (!this._mediaProxy) {
throw new Error(`Bridge not yet initialized`);
}
return this._mediaProxy;
}

public getStore() {
return this.dataStore;
}
Expand Down Expand Up @@ -654,6 +682,8 @@ export class IrcBridge {

await this.dataStore.removeConfigMappings();

this._mediaProxy = await this.initialiseMediaProxy();

if (this.activityTracker) {
log.info("Restoring last active times from DB");
const users = await this.dataStore.getLastSeenTimeForUsers();
Expand Down
25 changes: 12 additions & 13 deletions src/bridge/MatrixHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
StateLookup,
StateLookupEvent,
Intent,
MediaProxy,

Check failure on line 11 in src/bridge/MatrixHandler.ts

View workflow job for this annotation

GitHub Actions / test (20)

Module '"matrix-appservice-bridge"' has no exported member 'MediaProxy'.
} from "matrix-appservice-bridge";
import { IrcUser } from "../models/IrcUser";
import { ActionType, MatrixAction, MatrixMessageEvent } from "../models/MatrixAction";
Expand Down Expand Up @@ -142,7 +143,6 @@ export class MatrixHandler {
private readonly metrics: {[domain: string]: {
[metricName: string]: number;
};} = {};
private readonly mediaUrl: string;
private memberTracker?: StateLookup;
private adminHandler: AdminRoomHandler;
private config: MatrixHandlerConfig = DEFAULTS;
Expand All @@ -155,15 +155,16 @@ export class MatrixHandler {
constructor(
private readonly ircBridge: IrcBridge,
config: MatrixHandlerConfig|undefined,
private readonly membershipQueue: MembershipQueue
private readonly membershipQueue: MembershipQueue,
) {
this.onConfigChanged(config);

// The media URL to use to transform mxc:// URLs when handling m.room.[file|image]s
this.mediaUrl = ircBridge.config.homeserver.media_url || ircBridge.config.homeserver.url;
this.adminHandler = new AdminRoomHandler(ircBridge, this);
}

private get mediaProxy(): MediaProxy {
return this.ircBridge.mediaProxy;
}

public initialise() {
this.memberTracker = new StateLookup({
intent: this.ircBridge.getAppServiceBridge().getIntent(),
Expand Down Expand Up @@ -909,9 +910,7 @@ export class MatrixHandler {
req.log.debug("Message body: %s", event.content.body);
}

const mxAction = MatrixAction.fromEvent(
event, this.mediaUrl
);
const mxAction = await MatrixAction.fromEvent(event, this.mediaProxy);

// check if this message is from one of our virtual users
const servers = this.ircBridge.getServers();
Expand Down Expand Up @@ -1171,7 +1170,7 @@ export class MatrixHandler {

// This is true if the upload was a success
if (contentUri) {
const httpUrl = ContentRepo.getHttpUriForMxc(this.mediaUrl, contentUri);
const httpUrl = await this.mediaProxy.generateMediaUrl(contentUri);
// we check event.content.body since ircAction already has the markers stripped
const codeBlockMatch = event.content.body.match(/^```(\w+)?/);
if (codeBlockMatch) {
Expand All @@ -1182,7 +1181,7 @@ export class MatrixHandler {
};
}
else {
const explanation = renderTemplate(this.config.truncatedMessageTemplate, { url: httpUrl });
const explanation = renderTemplate(this.config.truncatedMessageTemplate, { url: httpUrl.toString() });
let messagePreview = trimString(
potentialMessages[0],
ircClient.getMaxLineLength() - 4 /* "... " */ - explanation.length - ircRoom.channel.length
Expand All @@ -1198,7 +1197,7 @@ export class MatrixHandler {
}

const truncatedIrcAction = IrcAction.fromMatrixAction(
MatrixAction.fromEvent(event, this.mediaUrl)
await MatrixAction.fromEvent(event, this.mediaProxy)
);
if (truncatedIrcAction) {
await this.ircBridge.sendIrcAction(ircRoom, ircClient, truncatedIrcAction);
Expand All @@ -1220,9 +1219,9 @@ export class MatrixHandler {

// Recreate action from modified event
const truncatedIrcAction = IrcAction.fromMatrixAction(
MatrixAction.fromEvent(
await MatrixAction.fromEvent(
sendingEvent,
this.mediaUrl,
this.mediaProxy,
)
);
if (truncatedIrcAction) {
Expand Down
7 changes: 6 additions & 1 deletion src/config/BridgeConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ export interface BridgeConfig {
};
homeserver: {
url: string;
media_url?: string;
domain: string;
enablePresence?: boolean;
dropMatrixMessagesAfterSecs?: number;
Expand All @@ -23,6 +22,12 @@ export interface BridgeConfig {
ircService: {
servers: {[domain: string]: IrcServerConfig};
matrixHandler?: MatrixHandlerConfig;
mediaProxy: {
signingKeyPath: string;
ttlSeconds: number;
bindPort: number;
publicUrl: string;
};
ircHandler?: IrcHandlerConfig;
provisioning: ProvisionerConfig;
logging: LoggerConfig;
Expand Down
11 changes: 11 additions & 0 deletions src/generate-signing-key.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { webcrypto } from 'node:crypto';

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

main().then(() => process.exit(0)).catch(err => { throw err });
2 changes: 1 addition & 1 deletion src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import Datastore from "nedb";
import extend from "extend";
import http from "http";
import https from "https";
import { RoomBridgeStore, UserBridgeStore, AppServiceRegistration } from "matrix-appservice-bridge";
import { RoomBridgeStore, UserBridgeStore, AppServiceRegistration, MediaProxy } from "matrix-appservice-bridge";

Check failure on line 5 in src/main.ts

View workflow job for this annotation

GitHub Actions / test (20)

Module '"matrix-appservice-bridge"' has no exported member 'MediaProxy'.
import { IrcBridge } from "./bridge/IrcBridge";
import { IrcServer, IrcServerConfig } from "./irc/IrcServer";
import ident from "./irc/Ident";
Expand Down
8 changes: 4 additions & 4 deletions src/models/MatrixAction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ limitations under the License.
import { IrcAction } from "./IrcAction";

import ircFormatting = require("../irc/formatting");
import { ContentRepo, Intent } from "matrix-appservice-bridge";
import { ContentRepo, Intent, MediaProxy } from "matrix-appservice-bridge";

Check failure on line 20 in src/models/MatrixAction.ts

View workflow job for this annotation

GitHub Actions / test (20)

Module '"matrix-appservice-bridge"' has no exported member 'MediaProxy'.
import escapeStringRegexp from "escape-string-regexp";
import logging from "../logging";
const log = logging("MatrixAction");
Expand Down Expand Up @@ -110,6 +110,7 @@ export class MatrixAction {
public htmlText: string|null = null,
public readonly ts: number = 0,
public replyEvent?: string,
private mediaProxy?: MediaProxy,
) { }

public get msgType() {
Expand Down Expand Up @@ -183,7 +184,7 @@ export class MatrixAction {
}
}

public static fromEvent(event: MatrixMessageEvent, mediaUrl: string, filename?: string) {
public static async fromEvent(event: MatrixMessageEvent, mediaProxy: MediaProxy, filename?: string) {
event.content = event.content || {};
let type = EVENT_TO_TYPE[event.type] || "message"; // mx event type to action type
let text = event.content.body;
Expand Down Expand Up @@ -212,14 +213,13 @@ export class MatrixAction {
fileSize = "(" + Math.round(event.content.info.size / 1024) + "KiB)";
}

let url = ContentRepo.getHttpUriForMxc(mediaUrl, event.content.url);
const url = await mediaProxy.generateMediaUrl(event.content.url);
if (!filename && event.content.body && /\S*\.[\w\d]{2,4}$/.test(event.content.body)) {
// Add filename to url if body is a filename.
filename = event.content.body;
}

if (filename) {
url += `/${encodeURIComponent(filename)}`;
text = `${fileSize} < ${url} >`;
}
else {
Expand Down

0 comments on commit 25990cf

Please sign in to comment.