CI / Build & Quality Checks (pull_request) Successful in 1m54s
CI / Trigger Desktop Build (pull_request) Skipped
CI / Docker image build & smoke test (pull_request) Skipped
CI / Secret scan (gitleaks) (pull_request) Successful in 10s
CI / Playwright smoke (e2e) (pull_request) Successful in 11m45s
The Server ACL editor treated an absent allow_ip_literals as false, so the first save in a room with no ACL (or one omitting the field) silently banned IP-literal servers. The spec default is true. normalizeServerAcl() fills the spec defaults once and feeds the initial state, the dirty check and the save. It also stops showing an existing ACL with no allow list as allow-all: per spec that allows no servers, and the /acl command already keeps it as-is. The checkbox is driven by onClick (folds has no onChange), now readOnly so React stops warning about a checked input without onChange. Fixes #261 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017PCb59uY3UKqDqt7B6hzd5
79 lines
3.2 KiB
TypeScript
79 lines
3.2 KiB
TypeScript
// 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<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,
|
|
};
|
|
}
|