From 73d1c10392e906f51bf733a41bfdd704545f1b20 Mon Sep 17 00:00:00 2001 From: Half-Shot Date: Tue, 28 Feb 2023 15:47:17 +0000 Subject: [PATCH 1/5] Port the ban sync component to matrix-appservice-bridge. --- src/components/ban-sync.ts | 143 +++++++++++++++++++++++++++++++++++++ src/index.ts | 1 + 2 files changed, 144 insertions(+) create mode 100644 src/components/ban-sync.ts diff --git a/src/components/ban-sync.ts b/src/components/ban-sync.ts new file mode 100644 index 00000000..0f9bd10b --- /dev/null +++ b/src/components/ban-sync.ts @@ -0,0 +1,143 @@ + + +import { Intent, MatrixUser, WeakStateEvent, Logger } from ".."; +import { MatrixGlob } from "matrix-bot-sdk"; + +const log = new Logger("MatrixBanSync"); + +export interface MatrixBanSyncConfig { + rooms: string[]; +} + +enum BanEntityType { + Server = "m.policy.rule.server", + User = "m.policy.rule.user" +} + +interface BanEntity { + matcher: MatrixGlob; + entityType: BanEntityType; + reason: string; +} + +interface MPolicyContent { + entity: string; + reason: string; + recommendation: "m.ban"; +} + +function eventTypeToBanEntityType(eventType: string): BanEntityType|null { + switch (eventType) { + case "m.policy.rule.user": + case "org.matrix.mjolnir.rule.user": + return BanEntityType.User; + case "m.policy.rule.server": + case "org.matrix.mjolnir.rule.server": + return BanEntityType.Server + default: + return null; + } +} + +const supportedRecommendations = [ + "org.matrix.mjolnir.ban", // Used historically. + "m.ban" +]; +/** + * Synchronises Matrix `m.policy.rule` events with the bridge to filter specific + * users from using the service. + */ +export class MatrixBanSync { + private bannedEntites = new Map(); + private subscribedRooms = new Set(); + constructor(private config: MatrixBanSyncConfig) { } + + public async syncRules(intent: Intent) { + this.bannedEntites.clear(); + this.subscribedRooms.clear(); + for (const roomIdOrAlias of this.config.rooms) { + try { + const roomId = await intent.join(roomIdOrAlias); + this.subscribedRooms.add(roomId); + const roomState = await intent.roomState(roomId, false) as WeakStateEvent[]; + for (const evt of roomState) { + this.handleIncomingState(evt, roomId); + } + } + catch (ex) { + log.error(`Failed to read ban list from ${roomIdOrAlias}`, ex); + } + } + } + + /** + * Is the given room considered part of the bridge's ban list set. + * @param roomId A Matrix room ID. + * @returns true if state should be handled from the room, false otherwise. + */ + public isTrackingRoomState(roomId: string): boolean { + return this.subscribedRooms.has(roomId); + } + + /** + * Checks to see if the incoming state is a recommendation entry. + * @param evt A Matrix state event. Unknown state events will be filtered out. + * @param roomId The Matrix roomID where the event came from. + * @returns `true` if the event was a new ban, and existing clients should be checked. `false` otherwise. + */ + public handleIncomingState(evt: WeakStateEvent, roomId: string) { + const content = evt.content as unknown as MPolicyContent; + const entityType = eventTypeToBanEntityType(evt.type); + if (!entityType) { + return false; + } + const key = `${roomId}:${evt.state_key}`; + if (evt.content.entity === undefined) { + // Empty, delete instead. + log.info(`Deleted ban rule ${evt.type}/$ matching ${key}`); + this.bannedEntites.delete(key); + return false; + } + if (!supportedRecommendations.includes(content.recommendation)) { + return false; + } + if (typeof content.entity !== "string" || content.entity === "") { + throw Error('`entity` key is not valid, must be a non-empty string'); + } + this.bannedEntites.set(key, { + matcher: new MatrixGlob(content.entity), + entityType, + reason: content.reason || "No reason given", + }); + log.info(`New ban rule ${evt.type} matching ${content.entity}`); + return true; + } + + /** + * Check if a user is banned by via a ban list. + * @param user A userId string or a MatrixUser object. + * @returns Either a string reason for the ban, or false if the user was not banned. + */ + public isUserBanned(user: MatrixUser|string): string|false { + const matrixUser = typeof user === "string" ? new MatrixUser(user) : user; + for (const entry of this.bannedEntites.values()) { + if (entry.entityType === BanEntityType.Server && entry.matcher.test(matrixUser.host)) { + return entry.reason; + } + if (entry.entityType === BanEntityType.User && entry.matcher.test(matrixUser.userId)) { + return entry.reason; + } + } + return false; + } + + /** + * Should be called when the bridge config has been updated. + * @param config The new config. + * @param intent The bot user intent. + */ + public async updateConfig(config: MatrixBanSyncConfig, intent: Intent) { + this.config = config; + await this.syncRules(intent); + } +} diff --git a/src/index.ts b/src/index.ts index 21f063e5..4154ca40 100644 --- a/src/index.ts +++ b/src/index.ts @@ -28,6 +28,7 @@ export * from "./components/room-upgrade-handler"; export * from "./components/app-service-bot"; export * from "./components/state-lookup"; export * from "./components/activity-tracker"; +export * from "./components/ban-sync"; // Config and CLI export * from "./components/cli"; From bb24c0a7bbade21c572465e40913e55d105832c1 Mon Sep 17 00:00:00 2001 From: Half-Shot Date: Tue, 28 Feb 2023 16:21:20 +0000 Subject: [PATCH 2/5] Support blocking open registration --- src/components/ban-sync.ts | 120 ++++++++++++++++++++++++++++++++++--- 1 file changed, 113 insertions(+), 7 deletions(-) diff --git a/src/components/ban-sync.ts b/src/components/ban-sync.ts index 0f9bd10b..84fae161 100644 --- a/src/components/ban-sync.ts +++ b/src/components/ban-sync.ts @@ -1,12 +1,18 @@ -import { Intent, MatrixUser, WeakStateEvent, Logger } from ".."; +import { Intent, MatrixUser, WeakStateEvent, Logger, MatrixHostResolver } from ".."; import { MatrixGlob } from "matrix-bot-sdk"; +import axios from "axios"; const log = new Logger("MatrixBanSync"); +const CACHE_HOMESERVER_PROPERTIES_FOR_MS = 1000 * 60 * 30; // 30 minutes + export interface MatrixBanSyncConfig { - rooms: string[]; + rooms?: string[]; + blockOpenRegistration?: { + allowUnknown?: boolean; + }; } enum BanEntityType { @@ -43,19 +49,106 @@ const supportedRecommendations = [ "org.matrix.mjolnir.ban", // Used historically. "m.ban" ]; + +interface MatrixRegistrationResponse { + flows: { + stages: string[], + }[], +} + +enum RegistrationStatus { + Unknown = 0, + Open = 1, + ProtectedEmail = 2, + ProtectedCaptcha = 3, + Closed = 4, +} + +const AuthTypeRecaptcha = 'm.login.recaptcha'; +const AuthTypeEmail = 'm.login.email.identity'; + /** * Synchronises Matrix `m.policy.rule` events with the bridge to filter specific * users from using the service. */ export class MatrixBanSync { - private bannedEntites = new Map(); - private subscribedRooms = new Set(); - constructor(private config: MatrixBanSyncConfig) { } + private readonly homeserverPropertiesCache = new Map(); + private readonly bannedEntites = new Map(); + private readonly subscribedRooms = new Set(); + private readonly hostResolver = new MatrixHostResolver(); + constructor(private config: MatrixBanSyncConfig) { + + } + + public async getHomeserverProperties(serverName: string) { + const hsData = this.homeserverPropertiesCache.get(serverName); + // Slightly fuzz the ttl. + const ttl = CACHE_HOMESERVER_PROPERTIES_FOR_MS + (Math.random()*60000); + if (hsData && hsData.ts < ttl) { + return hsData; + } + + const { url } = await this.hostResolver.resolveMatrixServer(serverName); + const registrationResponse = await axios.post(new URL('/_matrix/client/v3/register', url).toString(), { }, { }); + + let openReg = RegistrationStatus.Unknown; + + if (registrationResponse.status === 403 && registrationResponse.data.errcode === 'M_FORBIDDEN') { + // Explicitly forbidden private server -> great! + openReg = RegistrationStatus.Closed; + } + + if (registrationResponse.status === 404) { + // Endpoint is not connected, probably also great! + openReg = RegistrationStatus.Closed; + } + + if (registrationResponse.status === 401) { + // Look at the flows. + const { flows } = registrationResponse.data as MatrixRegistrationResponse; + if (!flows) { + // Invalid response + openReg = RegistrationStatus.Unknown; + } + else if (flows.length === 0) { + // No available flows, so closed. + openReg = RegistrationStatus.Closed; + } + else { + // Check the flows + for (const flow of flows) { + // A flow with recaptcha + if (openReg > RegistrationStatus.ProtectedCaptcha && flow.stages.includes(AuthTypeRecaptcha)) { + openReg = RegistrationStatus.ProtectedCaptcha; + } + // A flow without any recaptcha stages + if (openReg > RegistrationStatus.ProtectedEmail && + flow.stages.includes(AuthTypeEmail) && !flow.stages.includes(AuthTypeRecaptcha)) { + openReg = RegistrationStatus.ProtectedEmail; + } + // A flow without any email or recaptcha stages + if (openReg > RegistrationStatus.Open && + !flow.stages.includes(AuthTypeEmail) && !flow.stages.includes(AuthTypeRecaptcha)) { + openReg = RegistrationStatus.Open; + // Already as bad as it gets + break; + } + } + } + } + + const hsProps = { + openRegistration: openReg, + ts: Date.now(), + }; + this.homeserverPropertiesCache.set(serverName, hsProps); + return hsProps; + } public async syncRules(intent: Intent) { this.bannedEntites.clear(); this.subscribedRooms.clear(); - for (const roomIdOrAlias of this.config.rooms) { + for (const roomIdOrAlias of this.config.rooms || []) { try { const roomId = await intent.join(roomIdOrAlias); this.subscribedRooms.add(roomId); @@ -118,7 +211,7 @@ export class MatrixBanSync { * @param user A userId string or a MatrixUser object. * @returns Either a string reason for the ban, or false if the user was not banned. */ - public isUserBanned(user: MatrixUser|string): string|false { + public async isUserBanned(user: MatrixUser|string): Promise { const matrixUser = typeof user === "string" ? new MatrixUser(user) : user; for (const entry of this.bannedEntites.values()) { if (entry.entityType === BanEntityType.Server && entry.matcher.test(matrixUser.host)) { @@ -128,6 +221,19 @@ export class MatrixBanSync { return entry.reason; } } + + if (this.config.blockOpenRegistration) { + // Check the user's homeserver. + const hsProps = await this.getHomeserverProperties(matrixUser.host); + if (hsProps.openRegistration === RegistrationStatus.Open) { + return `${matrixUser.host} has open registration, and this bridge is configured to block open hosts.` + } + if (this.config.blockOpenRegistration.allowUnknown + && hsProps.openRegistration === RegistrationStatus.Unknown) { + return `${matrixUser.host} may have open registration, ` + + "and this bridge is configured to block unknown hosts"; + } + } return false; } From 261b226fbe0acd639d82404c69bf4bd687ce9c8c Mon Sep 17 00:00:00 2001 From: Half-Shot Date: Thu, 20 Apr 2023 12:46:49 +0100 Subject: [PATCH 3/5] Add support for identifying the client API --- src/utils/matrix-host-resolver.ts | 167 +++++++++++++++++++++--------- 1 file changed, 116 insertions(+), 51 deletions(-) diff --git a/src/utils/matrix-host-resolver.ts b/src/utils/matrix-host-resolver.ts index d304883c..f2c74107 100644 --- a/src/utils/matrix-host-resolver.ts +++ b/src/utils/matrix-host-resolver.ts @@ -8,6 +8,16 @@ interface MatrixServerWellKnown { "m.server": string; } + +interface MatrixClientWellKnown { + "m.homeserver": { + base_url: string; + }; + "m.identity_server": { + base_url: string; + }; +} + const OneMinute = 1000 * 60; const OneHour = OneMinute * 60; @@ -21,19 +31,23 @@ const WellKnownTimeout = 10000; const log = new Logger('MatrixHostResolver'); -type CachedResult = {timestamp: number, result: HostResolveResult}|{timestamp: number, error: Error}; +type CachedResult = + {timestamp: number, result: T, cacheFor: number}|{timestamp: number, error: Error}; + +type ServerCacheResult = CachedResult; +type ClientCacheResult = CachedResult; export interface HostResolveResult { host: string; hostname: string; port: number; - cacheFor: number; } interface DnsInterface { resolveSrv(hostname: string): Promise; } + /** * Class to lookup the hostname, port and host headers of a given Matrix servername * according to the @@ -42,7 +56,8 @@ interface DnsInterface { export class MatrixHostResolver { private axios: Axios; private dns: DnsInterface; - private resultCache = new Map(); + private serverResultCache = new Map(); + private clientResultCache = new Map(); constructor(private readonly opts: {axios?: Axios, dns?: DnsInterface, currentTimeMs?: number} = {}) { // To allow for easier mocking. @@ -96,35 +111,17 @@ export class MatrixHostResolver { } } - private async getWellKnown(serverName: string): Promise<{mServer: string, cacheFor: number}> { - const url = `https://${serverName}/.well-known/matrix/server`; - const wellKnown = await this.axios.get( + private async getWellKnown( + serverName: string, kind: "server"|"client", + ): Promise<{ data: T, cacheFor: number }> { + const url = `https://${serverName}/.well-known/matrix/${kind}`; + const wellKnown = await this.axios.get( url, { validateStatus: null, }); if (wellKnown.status !== 200) { throw Error('Well known request returned non-200'); } - let data: MatrixServerWellKnown; - if (typeof wellKnown.data === "object") { - data = wellKnown.data; - } - else if (typeof wellKnown.data === "string") { - data = JSON.parse(wellKnown.data); - } - else { - throw Error('Invalid datatype for well-known response'); - } - const mServer = data["m.server"]; - if (typeof mServer !== "string") { - throw Error("Missing 'm.server' in well-known response"); - } - - const [host, portStr] = mServer.split(':'); - const port = portStr ? parseInt(portStr, 10) : DefaultMatrixServerPort; - if (!host || (port && port < 1 || port > MaxPortNumber)) { - throw Error("'m.server' was not in the format of [:]") - } let cacheFor = DefaultCacheForMs; if (wellKnown.headers['Expires']) { @@ -142,7 +139,7 @@ export class MatrixHostResolver { .map(s => s.trim()) || []; const maxAge = parseInt( - cacheControlHeader.find(s => s.startsWith('max-age'))?.substr("max-age=".length) || "NaN", + cacheControlHeader.find(s => s.startsWith('max-age'))?.substring("max-age=".length) || "NaN", 10 ); @@ -154,6 +151,31 @@ export class MatrixHostResolver { cacheFor = 0; } + if (typeof wellKnown.data === "object") { + return { data: wellKnown.data, cacheFor }; + } + else if (typeof wellKnown.data === "string") { + return { + data: JSON.parse(wellKnown.data) as T, + cacheFor, + }; + } + throw Error('Invalid datatype for well-known response'); + } + + private async getServerWellKnown(serverName: string): Promise<{mServer: string, cacheFor: number}> { + const { data, cacheFor } = await this.getWellKnown(serverName, "server"); + const mServer = data["m.server"]; + if (typeof mServer !== "string") { + throw Error("Missing 'm.server' in well-known response"); + } + + const [host, portStr] = mServer.split(':'); + const port = portStr ? parseInt(portStr, 10) : DefaultMatrixServerPort; + if (!host || (port && port < 1 || port > MaxPortNumber)) { + throw Error("'m.server' was not in the format of [:]") + } + return { cacheFor, mServer }; } @@ -163,7 +185,7 @@ export class MatrixHostResolver { * @param hostname The Matrix `hostname` to resolve. e.g. `matrix.org` * @returns An object describing the delegated details for the host. */ - async resolveMatrixServerName(hostname: string): Promise { + async resolveMatrixServerName(hostname: string): Promise { // https://spec.matrix.org/v1.1/server-server-api/#resolving-server-names const { type, host, port } = MatrixHostResolver.determineHostType(hostname); // Step 1 - IP literal / Step 2 @@ -180,7 +202,7 @@ export class MatrixHostResolver { // Step 3 - Well-known let wellKnownResponse: {mServer: string, cacheFor: number}|undefined = undefined; try { - wellKnownResponse = await this.getWellKnown(hostname); + wellKnownResponse = await this.getServerWellKnown(hostname); log.debug(`Resolved ${hostname} to be well-known`); } catch (ex) { @@ -250,6 +272,29 @@ export class MatrixHostResolver { } } + private readCache( + cache: Map>, key: string, skipCache = false) { + const cachedResult = skipCache ? false : cache.get(key); + if (cachedResult) { + const cacheAge = this.currentTime - cachedResult.timestamp; + if ("result" in cachedResult && cacheAge <= cachedResult.cacheFor) { + const { cacheFor } = cachedResult; + log.debug( + `Cached result for ${key}, returning (alive for ${cacheFor - cacheAge}ms)` + ); + return cachedResult.result; + } + else if ("error" in cachedResult && cacheAge <= CacheFailureForMS) { + log.debug( + `Cached error for ${key}, throwing (alive for ${CacheFailureForMS - cacheAge}ms)` + ); + throw cachedResult.error; + } + // Otherwise expired entry. + } + return null; + } + /** * Resolves a Matrix serverName into the baseURL for federated requests, and the * `Host` header to use when serving requests. @@ -261,32 +306,22 @@ export class MatrixHostResolver { * be overwritten. * @returns The baseurl of the Matrix server (excluding /_matrix/federation suffix), and the hostHeader to be used. */ - async resolveMatrixServer(hostname: string, skipCache = false): Promise<{url: URL, hostHeader: string}> { - const cachedResult = skipCache ? false : this.resultCache.get(hostname); + public async resolveMatrixServer(hostname: string, skipCache = false): Promise<{url: URL, hostHeader: string}> { + const cachedResult = this.readCache(this.serverResultCache, hostname, skipCache); if (cachedResult) { - const cacheAge = this.currentTime - cachedResult.timestamp; - if ("result" in cachedResult && cacheAge <= cachedResult.result.cacheFor) { - const result = cachedResult.result; - log.debug( - `Cached result for ${hostname}, returning (alive for ${result.cacheFor - cacheAge}ms)` - ); - return { - url: new URL(`https://${result.host}:${result.port}/`), - hostHeader: result.hostname, - }; - } - else if ("error" in cachedResult && cacheAge <= CacheFailureForMS) { - log.debug( - `Cached error for ${hostname}, throwing (alive for ${CacheFailureForMS - cacheAge}ms)` - ); - throw cachedResult.error; - } - // Otherwise expired entry. + return { + url: new URL(`https://${cachedResult.host}:${cachedResult.port}/`), + hostHeader: cachedResult.hostname, + }; } try { const result = await this.resolveMatrixServerName(hostname); if (result.cacheFor) { - this.resultCache.set(hostname, { result, timestamp: this.currentTime}); + this.serverResultCache.set(hostname, { + result, + timestamp: this.currentTime, + cacheFor: result.cacheFor + }); } log.debug(`No result cached for ${hostname}, caching result for ${result.cacheFor}ms`); return { @@ -295,7 +330,7 @@ export class MatrixHostResolver { }; } catch (error) { - this.resultCache.set(hostname, { + this.serverResultCache.set(hostname, { timestamp: this.currentTime, error: error instanceof Error ? error : Error(String(error)), }); @@ -303,5 +338,35 @@ export class MatrixHostResolver { throw error; } } + + public async resolveMatrixClient(hostname: string, skipCache = false): Promise { + // https://spec.matrix.org/latest/client-server-api/#well-known-uri + const cachedResult = this.readCache(this.clientResultCache, hostname, skipCache); + if (cachedResult) { + return new URL(cachedResult["m.homeserver"].base_url); + } + + try { + const { data, cacheFor } = await this.getWellKnown(hostname, "client"); + const url = data["m.homeserver"]?.base_url; + if (typeof url !== "string") { + throw Error('Invalid client well-known, no m.homeserver base_url entry.'); + } + if (cacheFor) { + this.clientResultCache.set(hostname, { result: data, timestamp: this.currentTime, cacheFor }); + } + log.debug(`No result cached for ${hostname}, caching result for ${cacheFor}ms`); + return new URL(url); + } + catch (error) { + this.clientResultCache.set(hostname, { + timestamp: this.currentTime, + error: error instanceof Error ? error : Error(String(error)), + }); + log.debug(`No result cached for ${hostname}, caching error for ${CacheFailureForMS}ms`); + throw error; + } + + } } From 6402de933ebbe98b68ee1879ba37bdb056b50e69 Mon Sep 17 00:00:00 2001 From: Half-Shot Date: Thu, 20 Apr 2023 12:47:00 +0100 Subject: [PATCH 4/5] More configurable client reg blocking --- src/components/ban-sync.ts | 120 ++++++++++++++++++++++--------------- 1 file changed, 72 insertions(+), 48 deletions(-) diff --git a/src/components/ban-sync.ts b/src/components/ban-sync.ts index 84fae161..bab292cc 100644 --- a/src/components/ban-sync.ts +++ b/src/components/ban-sync.ts @@ -10,9 +10,7 @@ const CACHE_HOMESERVER_PROPERTIES_FOR_MS = 1000 * 60 * 30; // 30 minutes export interface MatrixBanSyncConfig { rooms?: string[]; - blockOpenRegistration?: { - allowUnknown?: boolean; - }; + blockByRegistrationStatus: RegistrationStatus[]; } enum BanEntityType { @@ -67,12 +65,20 @@ enum RegistrationStatus { const AuthTypeRecaptcha = 'm.login.recaptcha'; const AuthTypeEmail = 'm.login.email.identity'; +export class MatrixBanSyncError extends Error { + constructor(message: string, private readonly cause?: Error) { + super(message); + } +} + /** * Synchronises Matrix `m.policy.rule` events with the bridge to filter specific * users from using the service. */ export class MatrixBanSync { - private readonly homeserverPropertiesCache = new Map(); + private readonly homeserverPropertiesCache = new Map(); private readonly bannedEntites = new Map(); private readonly subscribedRooms = new Set(); private readonly hostResolver = new MatrixHostResolver(); @@ -80,27 +86,28 @@ export class MatrixBanSync { } - public async getHomeserverProperties(serverName: string) { - const hsData = this.homeserverPropertiesCache.get(serverName); - // Slightly fuzz the ttl. - const ttl = CACHE_HOMESERVER_PROPERTIES_FOR_MS + (Math.random()*60000); - if (hsData && hsData.ts < ttl) { - return hsData; + /** + * Determine the state of the homeservers user registration system. + * + * @param url The base URL for the C-S API of the homeserver. + * @returns A status enum. + */ + private static async getRegistrationStatus(url: URL): Promise { + let registrationResponse = await axios.post(new URL('/_matrix/client/v3/register', url).toString(), { }); + if (registrationResponse.status === 404) { + // Fallback to old APIs + registrationResponse = await axios.post(new URL('/_matrix/client/r0/register', url).toString(), { }); } - const { url } = await this.hostResolver.resolveMatrixServer(serverName); - const registrationResponse = await axios.post(new URL('/_matrix/client/v3/register', url).toString(), { }, { }); - - let openReg = RegistrationStatus.Unknown; if (registrationResponse.status === 403 && registrationResponse.data.errcode === 'M_FORBIDDEN') { // Explicitly forbidden private server -> great! - openReg = RegistrationStatus.Closed; + return RegistrationStatus.Closed; } if (registrationResponse.status === 404) { // Endpoint is not connected, probably also great! - openReg = RegistrationStatus.Closed; + return RegistrationStatus.Closed; } if (registrationResponse.status === 401) { @@ -108,37 +115,58 @@ export class MatrixBanSync { const { flows } = registrationResponse.data as MatrixRegistrationResponse; if (!flows) { // Invalid response - openReg = RegistrationStatus.Unknown; + return RegistrationStatus.Unknown; } - else if (flows.length === 0) { + + if (flows.length === 0) { // No available flows, so closed. - openReg = RegistrationStatus.Closed; + return RegistrationStatus.Closed; } - else { - // Check the flows - for (const flow of flows) { - // A flow with recaptcha - if (openReg > RegistrationStatus.ProtectedCaptcha && flow.stages.includes(AuthTypeRecaptcha)) { - openReg = RegistrationStatus.ProtectedCaptcha; - } - // A flow without any recaptcha stages - if (openReg > RegistrationStatus.ProtectedEmail && - flow.stages.includes(AuthTypeEmail) && !flow.stages.includes(AuthTypeRecaptcha)) { - openReg = RegistrationStatus.ProtectedEmail; - } - // A flow without any email or recaptcha stages - if (openReg > RegistrationStatus.Open && - !flow.stages.includes(AuthTypeEmail) && !flow.stages.includes(AuthTypeRecaptcha)) { - openReg = RegistrationStatus.Open; - // Already as bad as it gets - break; - } + + let openReg = RegistrationStatus.Unknown; + // Check the flows + for (const flow of flows) { + // A flow with recaptcha + if (openReg > RegistrationStatus.ProtectedCaptcha && flow.stages.includes(AuthTypeRecaptcha)) { + openReg = RegistrationStatus.ProtectedCaptcha; + } + // A flow without any recaptcha stages + if (openReg > RegistrationStatus.ProtectedEmail && + flow.stages.includes(AuthTypeEmail) && !flow.stages.includes(AuthTypeRecaptcha)) { + openReg = RegistrationStatus.ProtectedEmail; + } + // A flow without any email or recaptcha stages + if (openReg > RegistrationStatus.Open && + !flow.stages.includes(AuthTypeEmail) && !flow.stages.includes(AuthTypeRecaptcha)) { + openReg = RegistrationStatus.Open; + // Already as bad as it gets + break; } } + return openReg; + } + + return RegistrationStatus.Unknown; + } + + /** + * Get properties about a given homeserver that may influence the rules + * applied to it. + * @param serverName The homeserver name. + * @returns A set of properties. + * @throws + */ + public async getHomeserverProperties(serverName: string) { + const hsData = this.homeserverPropertiesCache.get(serverName); + // Slightly fuzz the ttl. + const ttl = Date.now() + CACHE_HOMESERVER_PROPERTIES_FOR_MS + (Math.random()*60000); + if (hsData && hsData.ts < ttl) { + return hsData; } + const url = await this.hostResolver.resolveMatrixClient(serverName, true); const hsProps = { - openRegistration: openReg, + registrationStatus: await MatrixBanSync.getRegistrationStatus(url), ts: Date.now(), }; this.homeserverPropertiesCache.set(serverName, hsProps); @@ -222,16 +250,12 @@ export class MatrixBanSync { } } - if (this.config.blockOpenRegistration) { + if (this.config.blockByRegistrationStatus) { // Check the user's homeserver. - const hsProps = await this.getHomeserverProperties(matrixUser.host); - if (hsProps.openRegistration === RegistrationStatus.Open) { - return `${matrixUser.host} has open registration, and this bridge is configured to block open hosts.` - } - if (this.config.blockOpenRegistration.allowUnknown - && hsProps.openRegistration === RegistrationStatus.Unknown) { - return `${matrixUser.host} may have open registration, ` + - "and this bridge is configured to block unknown hosts"; + const { registrationStatus } = await this.getHomeserverProperties(matrixUser.host); + if (this.config.blockByRegistrationStatus.includes(registrationStatus)) { + const statusName = RegistrationStatus[registrationStatus]; + return `${matrixUser.host} has ${statusName} registration, which is blocked by this bridge.` } } return false; From 2ce1db411261249d8b1c428294cb389e6c297c9e Mon Sep 17 00:00:00 2001 From: Half-Shot Date: Thu, 20 Apr 2023 12:51:23 +0100 Subject: [PATCH 5/5] More jsdoc --- src/components/ban-sync.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/components/ban-sync.ts b/src/components/ban-sync.ts index bab292cc..8c1b9c3f 100644 --- a/src/components/ban-sync.ts +++ b/src/components/ban-sync.ts @@ -65,12 +65,6 @@ enum RegistrationStatus { const AuthTypeRecaptcha = 'm.login.recaptcha'; const AuthTypeEmail = 'm.login.email.identity'; -export class MatrixBanSyncError extends Error { - constructor(message: string, private readonly cause?: Error) { - super(message); - } -} - /** * Synchronises Matrix `m.policy.rule` events with the bridge to filter specific * users from using the service. @@ -154,7 +148,8 @@ export class MatrixBanSync { * applied to it. * @param serverName The homeserver name. * @returns A set of properties. - * @throws + * @throws This will fail if the client does not provide a well-known. Callers should + * make their own assumptions about the state of the host in this case. */ public async getHomeserverProperties(serverName: string) { const hsData = this.homeserverPropertiesCache.get(serverName); @@ -173,6 +168,11 @@ export class MatrixBanSync { return hsProps; } + /** + * Perform joins against all the configured ban list rooms, and pull all ban state. + * This can be quite expensive to run if there is lots of state to pull. + * @param intent The client intent to pull the state via. + */ public async syncRules(intent: Intent) { this.bannedEntites.clear(); this.subscribedRooms.clear();