2026-07-18 21:23:35 -04:00
|
|
|
// 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;
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
2026-10-19 14:07:03 -04:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* `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<string, unknown> | 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,
|
|
|
|
|
};
|
|
|
|
|
}
|