feat(rcon): allow administrator-defined endpoints
CI / validate (push) Successful in 6m16s
Release / release (push) Successful in 8m27s

This commit is contained in:
dmg
2026-08-08 07:45:24 -04:00
parent f9ccfd821d
commit 7f6d69e0a7
9 changed files with 35 additions and 43 deletions
@@ -22,7 +22,7 @@ const savedMessages: Record<string, string> = {
};
const errorMessages: Record<string, string> = {
"invalid-connection": "Enter a valid allowlisted hostname, port, name, and password.",
"invalid-connection": "Enter a valid DNS hostname, port, name, and password.",
"duplicate-name": "Connection names must be unique.",
configuration: "RCON credential encryption is not configured.",
"save-failed": "The RCON connection could not be saved.",
+17 -15
View File
@@ -1,32 +1,36 @@
import { describe, expect, it } from "vitest";
import { sanitizeRconOutput, validateRconCommand, validateRconConnection } from "./rcon-validation";
const allowed = "season4.somc.svc.cluster.local:25575,creative.somc.svc.cluster.local:25576";
describe("RCON validation", () => {
it("normalizes an allowlisted internal endpoint", () => {
it("normalizes any valid DNS hostname and port without deployment configuration", () => {
expect(validateRconConnection({
name: " Season 4 ",
host: "SEASON4.SOMC.SVC.CLUSTER.LOCAL",
port: "25575",
password: "correct horse battery staple",
}, { allowedEndpoints: allowed, passwordRequired: true })).toEqual({
}, { passwordRequired: true })).toEqual({
name: "Season 4",
host: "season4.somc.svc.cluster.local",
port: 25575,
password: "correct horse battery staple",
});
expect(validateRconConnection({
name: "Creative",
host: "creative.example.net",
port: "43210",
password: "secret",
}, { passwordRequired: true })).toEqual({
name: "Creative",
host: "creative.example.net",
port: 43210,
password: "secret",
});
});
it("rejects unlisted hosts, ports, IP literals, and suffix confusion", () => {
for (const [host, port] of [
["postgres.somc.svc.cluster.local", "5432"],
["season4.somc.svc.cluster.local", "5432"],
["season4.somc.svc.cluster.local.attacker.example", "25575"],
["10.0.0.1", "25575"],
]) {
expect(validateRconConnection({ name: "Server", host, port, password: "secret" }, {
allowedEndpoints: allowed,
it("rejects IP literals and malformed DNS hostnames", () => {
for (const host of ["10.0.0.1", "2001:db8::1", "season4.", "-season4.example", "season4..example"]) {
expect(validateRconConnection({ name: "Server", host, port: "25575", password: "secret" }, {
passwordRequired: true,
})).toBeNull();
}
@@ -34,11 +38,9 @@ describe("RCON validation", () => {
it("allows a blank replacement password only while editing", () => {
expect(validateRconConnection({ name: "Server", host: "season4.somc.svc.cluster.local", port: "25575", password: "" }, {
allowedEndpoints: allowed,
passwordRequired: false,
})?.password).toBeNull();
expect(validateRconConnection({ name: "Server", host: "season4.somc.svc.cluster.local", port: "25575", password: "" }, {
allowedEndpoints: allowed,
passwordRequired: true,
})).toBeNull();
});
+1 -7
View File
@@ -12,25 +12,19 @@ export type ValidRconConnection = {
password: string | null;
};
function endpointSet(value: string) {
return new Set(value.split(",").map((endpoint) => endpoint.trim().toLowerCase()).filter(Boolean));
}
export function validateRconConnection(
input: { name: unknown; host: unknown; port: unknown; password: unknown },
options: { allowedEndpoints?: string; passwordRequired: boolean },
options: { passwordRequired: boolean },
): ValidRconConnection | null {
const name = typeof input.name === "string" ? input.name.trim() : "";
const host = typeof input.host === "string" ? input.host.trim().toLowerCase() : "";
const portText = typeof input.port === "string" || typeof input.port === "number" ? String(input.port).trim() : "";
const passwordText = typeof input.password === "string" ? input.password : "";
const port = Number(portText);
const allowed = endpointSet(options.allowedEndpoints ?? process.env.RCON_ALLOWED_ENDPOINTS ?? "");
if (!name || name.length > 100 || CONTROL_PATTERN.test(name)) return null;
if (!host || host.endsWith(".") || isIP(host) !== 0 || !HOST_PATTERN.test(host)) return null;
if (!Number.isInteger(port) || port < 1 || port > 65_535) return null;
if (!allowed.has(`${host}:${port}`)) return null;
if (passwordText.length > 512 || CONTROL_PATTERN.test(passwordText)) return null;
if (options.passwordRequired && !passwordText) return null;