// Shared helpers for `m.room.server_acl` server-name globs, used by both the // Room Settings ACL editor and the `/acl` slash command so their validation and // self-lockout detection stay identical. /** * Validate a server-name glob for an ACL entry. * * Matrix ACL `allow`/`deny` entries are globs where `*` (any run of chars) and * `?` (single char) may appear ANYWHERE — e.g. `*`, `*.example.com`, * `1.2.3.*`, `10.0.0.?`, `*.evil.*`, `*bad*`. We therefore validate the *glob* * rather than a concrete hostname: * - reject empty / whitespace-only * - allow only hostname/IP chars plus the wildcards `*` and `?` * (letters, digits, dots, hyphens, colons for ports/IPv6 — NO underscore) * - reject consecutive/leading/trailing dots (`...`, `.foo`, `foo.`) * - reject entries with no alphanumeric or wildcard char (bare `-`, lone `:`) */ export function isValidServerPattern(value: string): boolean { const v = value.trim(); if (!v) return false; // Only hostname/IP glob chars — wildcards may appear at any position. if (!/^[A-Za-z0-9.:*?-]+$/.test(v)) return false; // Structural rules for the dotted parts. if (v.startsWith('.') || v.endsWith('.') || v.includes('..')) return false; // Must carry actual signal — reject pure punctuation like `-`, `:` or `-.-`. if (!/[A-Za-z0-9*?]/.test(v)) return false; return true; } /** * Convert an ACL glob (`*` = any run, `?` = single char) to an anchored RegExp, * escaping every other regex metacharacter. Used only for local self-ban * detection — never sent to the server. */ export function globToRegExp(glob: string): RegExp { const escaped = glob.replace(/[.+^${}()|[\]\\]/g, '\\$&'); const pattern = escaped.replace(/\*/g, '.*').replace(/\?/g, '.'); // Case-INsensitive: Synapse's glob_to_regex uses IGNORECASE and hostnames are // case-insensitive, so a deny like `MATRIX.foo.org` must still be detected as // self-banning `matrix.foo.org` (otherwise the warning is a false negative). return new RegExp(`^${pattern}$`, 'i'); } export function matchesAnyGlob(domain: string, globs: string[]): boolean { return globs.some((glob) => { try { return globToRegExp(glob).test(domain); } catch { return false; } }); } /** * `m.room.server_acl` content with the spec's defaults filled in (#261). * `allow_ip_literals` defaults to **true** when absent; `allow` and `deny` * default to empty lists, but with no ACL event at all every server is allowed, * so the editor starts from `allow: ['*']` in that case. * https://spec.matrix.org/v1.12/client-server-api/#mroomserver_acl */ export type ServerAclContent = { allow: string[]; deny: string[]; allow_ip_literals: boolean; }; const stringList = (value: unknown): string[] | undefined => Array.isArray(value) ? value.filter((v): v is string => typeof v === 'string') : undefined; export function normalizeServerAcl(content: Record | undefined): ServerAclContent { if (!content) return { allow: ['*'], deny: [], allow_ip_literals: true }; return { allow: stringList(content.allow) ?? [], deny: stringList(content.deny) ?? [], allow_ip_literals: typeof content.allow_ip_literals === 'boolean' ? content.allow_ip_literals : true, }; }