AppService start

This commit is contained in:
gnuxie 2022-07-12 14:47:12 +01:00
parent 81cd91c250
commit 94040b00d6
5 changed files with 1539 additions and 468 deletions

View File

@ -4,6 +4,7 @@ up:
before:
# Launch the reverse proxy, listening for connections *only* on the local host.
- docker run --rm --network host --name mjolnir-test-reverse-proxy -p 127.0.0.1:8081:80 -v $MX_TEST_CWD/test/nginx.conf:/etc/nginx/nginx.conf:ro -d nginx
- cp mjolnir-registration.yaml $MX_TEST_SYNAPSE_DIR/data/
after:
# Wait until Synapse is ready
- until curl localhost:9999 --stderr /dev/null > /dev/null; do echo "Waiting for Synapse..."; sleep 1s; done
@ -19,7 +20,7 @@ down:
modules:
- name: mjolnir
build:
- cp -r synapse_antispam $MX_TEST_MODULE_DIR
- cp -r synapse_antispam $MX_TEST_MODULE_DIR/
config:
module: mjolnir.Module
config: {}
@ -34,6 +35,9 @@ homeserver:
enable_registration: true
enable_registration_without_verification: true
app_service_config_files:
- "/data/mjolnir-registration.yaml"
# We remove rc_message so we can test rate limiting,
# but we keep the others because of https://github.com/matrix-org/synapse/issues/11785
# and we don't want to slow integration tests down.

View File

@ -24,6 +24,7 @@
"@types/js-yaml": "^4.0.5",
"@types/jsdom": "^16.2.11",
"@types/mocha": "^9.0.0",
"@types/nedb": "^1.8.12",
"@types/node": "^16.7.10",
"@types/shell-quote": "1.7.1",
"crypto-js": "^4.1.1",
@ -43,7 +44,8 @@
"humanize-duration-ts": "^2.1.1",
"js-yaml": "^4.1.0",
"jsdom": "^16.6.0",
"matrix-bot-sdk": "^0.5.19",
"matrix-appservice-bridge": "^4.0.1",
"nedb": "^1.8.0",
"parse-duration": "^1.0.2",
"shell-quote": "^1.7.3",
"yaml": "^2.1.1"

101
src/AppService.ts Normal file
View File

@ -0,0 +1,101 @@
/**
* What kind of vulnerabilities make process isolation count?
* Either way they have the token to the appservice even with process isolation
* the bounds are limitless.
*
* In both casees we have to migrate the configuration away from being static
* so that it can be built on the fly to start new processes.
* Yes we could also write to a file but that's disgusting and i refuse.
* The config is the biggest piece of static bullshit that makes things a pita,
* so this removes one of the arguments against in-process work.
*
* Ok so my idea is to just use fork and have a special Mjolnir instance
* that basically proxies the mjolnir in the forked processes.
*
*/
import { randomUUID } from "crypto";
import { AppServiceRegistration, Bridge, Cli, Request, WeakEvent, BridgeContext, MatrixUser, UserBridgeStore, RemoteUser } from "matrix-appservice-bridge";
// needed by appservice irc, though it looks completely dead.
import * as Datastore from "nedb";
import { MjolnirManager } from "./appservice/MjolnirManager";
//import config from "./config";
// node index.js -r -u "http://localhost:9000" # remember to add the registration! you probably want host.docker.internal if using mx-tester
// node index.js -p 9000
class MjolnirAppService {
public readonly bridge: Bridge;
private readonly mjolnirManager: MjolnirManager = new MjolnirManager();
public constructor() {
this.bridge = new Bridge({
homeserverUrl: "http://localhost:9999",
domain: "localhost",
registration: "mjolnir-registration.yaml",
controller: {
onUserQuery: this.onUserQuery.bind(this),
onEvent: this.onEvent.bind(this),
},
userStore: new UserBridgeStore(new Datastore()),
});
}
public async provisionNewMjolnir(requestingUserId: string) {
// FIXME: we need to restrict who can do it (special list? ban remote users?)
const issuedMjolnirs = await this.bridge.getUserStore()!.getRemoteUsersFromMatrixId(requestingUserId);
if (issuedMjolnirs.length === 0) {
// Now we need to make one of the transparent mjolnirs and add it to the monitor.
const mjIntent = await this.bridge.getIntentFromLocalpart(`mjolnir_${randomUUID()}`);
await mjIntent.ensureRegistered();
// we're only doing this because it's complaining about missing profiles.
// actually the user id wasn't even right, so this might not be necessary anymore.
await mjIntent.ensureProfile('Mjolnir');
this.mjolnirManager.createNew(requestingUserId, mjIntent);
// Technically the mjolnir is a remote user, but also not because it's matrix-matrix.
//const mjAsRemote = new RemoteUser(mjIntent.userId)
//const bridgeStore = this.bridge.getUserStore()!;
//bridgeStore.setRemoteUser()
await this.bridge.getUserStore()!.linkUsers(new MatrixUser(requestingUserId), new RemoteUser(mjIntent.userId));
} else {
throw new Error(`User: ${requestingUserId} has already provisioned ${issuedMjolnirs.length} mjolnirs.`);
}
}
public onUserQuery (queriedUser: MatrixUser) {
return {}; // auto-provision users with no additonal data
}
// is it ok for this to be async? seems a bit dodge.
// it should be BridgeRequestEvent not whatever this is
public async onEvent(request: Request<WeakEvent>, context: BridgeContext) {
// https://github.com/matrix-org/matrix-appservice-irc/blob/develop/src/bridge/MatrixHandler.ts#L921
// ^ that's how matrix-appservice-irc maps from room to channel, we basically need to do the same but map
// from room to which mjolnir it's for, unless that information is present in BridgeContext, which it might be...
const mxEvent = request.getData();
if ('m.room.member' === mxEvent.type) {
if ('invite' === mxEvent.content['membership'] && mxEvent.state_key === this.bridge.botUserId) {
await this.provisionNewMjolnir(mxEvent.sender);
}
}
this.mjolnirManager.onEvent(request, context);
}
}
new Cli({
registrationPath: "mjolnir-registration.yaml",
generateRegistration: function(reg, callback) {
reg.setId(AppServiceRegistration.generateToken());
reg.setHomeserverToken(AppServiceRegistration.generateToken());
reg.setAppServiceToken(AppServiceRegistration.generateToken());
reg.setSenderLocalpart("mjolnir");
reg.addRegexPattern("users", "@mjolnir_.*", true);
callback(reg);
},
run: function(port: number) {
const service = new MjolnirAppService();
console.log("Matrix-side listening on port %s", port);
service.bridge.run(port);
}
}).run();

View File

@ -0,0 +1,101 @@
import { Mjolnir } from "../Mjolnir";
import { Intent, Request, WeakEvent, BridgeContext } from "matrix-appservice-bridge";
import { IConfig, setDefaults } from "../config";
import { SHORTCODE_EVENT_TYPE } from "../models/BanList";
import { Permalinks } from "matrix-bot-sdk";
export class MjolnirManager {
private readonly mjolnirs: Map</*the user id of the mjolnir*/string, ManagedMjolnir> = new Map();
public getDefaultMjolnirConfig(managementRoom: string): IConfig {
return setDefaults({managementRoom});
}
public async createNew(requestingUserId: string, intent: Intent) {
// FIXME: We should be creating the intent here and generating the id surely?
// rather than externally...
// FIXME: We need to verify that we haven't stored a mjolnir already if we aren't doing the above.
// get mjolnir list wroking by just avoiding it for now and see if protections work
// and bans.
// Find out trade offs of changing mjolnir to make it work vs making new subcomponent of mjolnir.
const managementRoomId = (await intent.createRoom({
createAsClient: true,
options: {
preset: 'private_chat',
invite: [requestingUserId],
name: `${requestingUserId}'s mjolnir`
}
})).room_id;
const managedMjolnir = new ManagedMjolnir(intent, await Mjolnir.setupMjolnirFromConfig(intent.matrixClient, this.getDefaultMjolnirConfig(managementRoomId)));
await managedMjolnir.moveMeSomewhereCommonAndStopImplementingFunctionalityOnACommandFirstBasis(requestingUserId, 'list')
this.mjolnirs.set(intent.userId, managedMjolnir);
}
public onEvent(request: Request<WeakEvent>, context: BridgeContext) {
// We honestly don't know how we're going to map from bridge to user
// https://github.com/matrix-org/matrix-appservice-bridge/blob/6046d31c54d461ad53e6d6e244ce2d944b62f890/src/components/room-bridge-store.ts
// looks like it might work, but we will ask, figure it out later.
[...this.mjolnirs.values()].forEach((mj: ManagedMjolnir) => mj.onEvent(request));
}
}
// Isolating this mjolnir is going to require provisioning an access token just for this user to be useful.
// We can use fork and node's IPC to inform the process of matrix evnets.
export class ManagedMjolnir {
public constructor(private readonly intent: Intent, private readonly mjolnir: Mjolnir) {
}
public async onEvent(request: Request<WeakEvent>) {
// phony sync.
const mxEvent = request.getData();
if (mxEvent['type'] !== undefined) {
this.mjolnir.client.emit('room.event', mxEvent.room_id, mxEvent);
if (mxEvent.type === 'm.room.message') {
this.mjolnir.client.emit('room.message', mxEvent.room_id, mxEvent);
}
// room.join requires us to know the joined rooms before so lol.
}
if (mxEvent['type'] === 'm.room.member') {
if (mxEvent['content']['membership'] === 'invite' && mxEvent.state_key === this.intent.userId) {
this.mjolnir.client.emit('room.invite', mxEvent.room_id, mxEvent);
}
}
}
public async moveMeSomewhereCommonAndStopImplementingFunctionalityOnACommandFirstBasis(mjolnirOwnerId: string, shortcode: string) {
const powerLevels: { [key: string]: any } = {
"ban": 50,
"events": {
"m.room.name": 100,
"m.room.power_levels": 100,
},
"events_default": 50, // non-default
"invite": 0,
"kick": 50,
"notifications": {
"room": 20,
},
"redact": 50,
"state_default": 50,
"users": {
[this.intent.userId]: 100,
[mjolnirOwnerId]: 50
},
"users_default": 0,
};
const listRoomId = await this.mjolnir.client.createRoom({
preset: "public_chat",
invite: [mjolnirOwnerId],
initial_state: [{type: SHORTCODE_EVENT_TYPE, state_key: "", content: {shortcode: shortcode}}],
power_level_content_override: powerLevels,
});
const roomRef = Permalinks.forRoom(listRoomId);
await this.mjolnir.watchList(roomRef);
}
}

1795
yarn.lock

File diff suppressed because it is too large Load Diff