From fa6a6547eef5c43acc3696243029df03d3a13fda Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Sat, 25 Jan 2020 20:34:15 -0700 Subject: [PATCH] Add very basic subscription server for Synapse module --- config/default.yaml | 11 ++++++ src/config.ts | 7 +++- src/index.ts | 7 ++++ src/models/BanList.ts | 4 ++ src/server/BanListServer.ts | 46 ++++++++++++++++++++++ src/server/Connection.ts | 78 +++++++++++++++++++++++++++++++++++++ 6 files changed, 152 insertions(+), 1 deletion(-) create mode 100644 src/server/BanListServer.ts create mode 100644 src/server/Connection.ts diff --git a/config/default.yaml b/config/default.yaml index 3caeb00..8bbd9ea 100644 --- a/config/default.yaml +++ b/config/default.yaml @@ -82,3 +82,14 @@ protectedRooms: # Note: the management room is *excluded* from this condition. Add it to the # protected rooms to protect it. protectAllJoinedRooms: false + +# An optional server for the Synapse Mjolnir plugin to connect to. This is +# recommended if you're running the Synapse module in a worker environment, +# particularly if you're running a federation reader. +# +# It is not recommended to expose this to the wider internet. Connections +# are over TCP only. +banListServer: + enabled: false + bind: "0.0.0.0" + port: 24465 diff --git a/src/config.ts b/src/config.ts index 4697f97..a52dbaa 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,5 +1,5 @@ /* -Copyright 2019 The Matrix.org Foundation C.I.C. +Copyright 2019, 2020 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -38,6 +38,11 @@ interface IConfig { fasterMembershipChecks: boolean; automaticallyRedactForReasons: string[]; // case-insensitive globs protectAllJoinedRooms: boolean; + banListServer: { + enabled: boolean; + bind: string; + port: number; + }; /** * Config options only set at runtime. Try to avoid using the objects diff --git a/src/index.ts b/src/index.ts index 38bd4a5..087c1ea 100644 --- a/src/index.ts +++ b/src/index.ts @@ -30,6 +30,7 @@ import BanList from "./models/BanList"; import { Mjolnir } from "./Mjolnir"; import { logMessage } from "./LogProxy"; import { MembershipEvent } from "matrix-bot-sdk/lib/models/events/MembershipEvent"; +import {BanListServer} from "./server/BanListServer"; config.RUNTIME = {client: null}; @@ -94,5 +95,11 @@ LogService.info("index", "Starting bot..."); await logMessage(LogLevel.INFO, "index", "Mjolnir is starting up. Use !mjolnir to query status."); const bot = new Mjolnir(client, protectedRooms, banLists); + + if (config.banListServer && config.banListServer.enabled) { + const server = new BanListServer(bot); + await server.start(); + } + await bot.start(); })(); diff --git a/src/models/BanList.ts b/src/models/BanList.ts index 56fa39b..71c6f46 100644 --- a/src/models/BanList.ts +++ b/src/models/BanList.ts @@ -67,6 +67,10 @@ export default class BanList { return this.rules.filter(r => r.kind === RULE_ROOM); } + public get allRules(): ListRule[] { + return [...this.serverRules, ...this.userRules, ...this.roomRules]; + } + public async updateList() { this.rules = []; diff --git a/src/server/BanListServer.ts b/src/server/BanListServer.ts new file mode 100644 index 0000000..d48a31b --- /dev/null +++ b/src/server/BanListServer.ts @@ -0,0 +1,46 @@ +/* +Copyright 2020 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import {Mjolnir} from "../Mjolnir"; +import * as net from "net"; +import {Socket} from "net"; +import config from "../config"; +import {LogService} from "matrix-bot-sdk"; +import {Connection} from "./Connection"; + +export class BanListServer { + + private connections: Connection[] = []; + + constructor(private mjolnir: Mjolnir) { + } + + public async start() { + LogService.info("BanListServer", `Starting server on ${config.banListServer.bind}:${config.banListServer.port}`); + const server = net.createServer(this.onConnect.bind(this)); + server.listen(config.banListServer.port, config.banListServer.bind); + } + + private onDisconnect(connection: Connection): void { + const index = this.connections.indexOf(connection); + if (index >= 0) this.connections.splice(index, 1); + } + + private onConnect(socket: Socket) { + LogService.info("BanListServer", `New client connection from ${socket.address().toString()}`); + this.connections.push(new Connection(socket, this.mjolnir, this.onDisconnect.bind(this))); + } +} \ No newline at end of file diff --git a/src/server/Connection.ts b/src/server/Connection.ts new file mode 100644 index 0000000..a210118 --- /dev/null +++ b/src/server/Connection.ts @@ -0,0 +1,78 @@ +/* +Copyright 2020 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import {Socket} from "net"; +import {Mjolnir} from "../Mjolnir"; +import {LogService} from "matrix-bot-sdk"; +import BanList from "../models/BanList"; + +const COMMAND_PREFIX = "|C,"; +const COMMAND_SUFFIX = ",C|"; +const SUBSCRIBE_PREFIX = `${COMMAND_PREFIX}SUBSCRIBE,`; +const RESET_PREFIX = `${COMMAND_PREFIX}RESET,`; +const RULE_PREFIX = `${COMMAND_PREFIX}RULE,`; + +export class Connection { + + private currentCommand = ""; + private subscribedRooms: string[] = []; + + constructor(private socket: Socket, private mjolnir: Mjolnir, private disconnectFn: (Socket) => void) { + socket.on("data", this.onData.bind(this)); + socket.on("close", this.onClose.bind(this)); + } + + private onListUpdate(list: BanList) { + if (!this.subscribedRooms.includes(list.roomId)) { + return; // Ignore list update + } + + this.socket.write(`${RESET_PREFIX}${list.roomId}${COMMAND_SUFFIX}`); + for (const rule of list.allRules) { + this.socket.write(`${RULE_PREFIX}${rule.kind},${rule.entity}${COMMAND_SUFFIX}`); + } + } + + private onClose() { + this.disconnectFn(this); + } + + private onData(b: Buffer) { + this.currentCommand += b.toString("ascii"); + + // Try and parse the command + const commandStart = this.currentCommand.indexOf(COMMAND_PREFIX); + if (commandStart < 0) return; // No command yet + + let command = this.currentCommand.slice(commandStart); + let idx = command.indexOf(COMMAND_SUFFIX); + if (idx < 0) return; // incomplete command + command = command.substring(0, idx); + this.currentCommand = this.currentCommand.substring(commandStart + command.length); + + LogService.info("Running " + command); + if (command.startsWith(SUBSCRIBE_PREFIX)) { + const roomId = command.substring(SUBSCRIBE_PREFIX.length, (command.length - COMMAND_SUFFIX.length - 1) + SUBSCRIBE_PREFIX.length); + const banList = this.mjolnir.lists.find(i => i.roomId === roomId); + if (!banList) { + LogService.warn("Connection", `Connection tried to subscribe to unknown ban list ${roomId}`); + } else { + this.subscribedRooms.push(roomId); + this.onListUpdate(banList); + } + } // else unknown command + } +} \ No newline at end of file