134 lines · 4.4 KB · TypeScript
RawDownload
import { Schema } from "effect";import { isIP } from "node:net"; const hostnamePattern = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;const numericIpv4Pattern = /^(?:0x[0-9a-f]+|[0-9]+)(?:\.(?:0x[0-9a-f]+|[0-9]+))*$/;const loopbackHosts = ["localhost", "127.0.0.1", "::1", "host.docker.internal"]; const normalizeIp = (value: string): string | null => {  const version = isIP(value);  if (version === 4) return value;  if (version !== 6) return null;  try {    return new URL(`http://[${value}]`).hostname.slice(1, -1).toLowerCase();  } catch {    return null;  }}; export const normalizeControllerHost = (value: string): string | null => {  const candidate = value.trim().toLowerCase();  const bracketed = candidate.startsWith("[") || candidate.endsWith("]");  if (bracketed && !(candidate.startsWith("[") && candidate.endsWith("]"))) return null;  const unwrapped = bracketed ? candidate.slice(1, -1) : candidate;  const ip = normalizeIp(unwrapped);  if (ip) return bracketed && isIP(unwrapped) !== 6 ? null : ip;  if (numericIpv4Pattern.test(unwrapped)) {    try {      const numericIp = new URL(`http://${unwrapped}`).hostname;      return isIP(numericIp) === 4 ? numericIp : null;    } catch {      return null;    }  }  if (    bracketed ||    candidate.length > 253 ||    candidate.endsWith(".") ||    candidate.includes(":") ||    candidate.includes("@") ||    candidate.includes("/") ||    candidate.includes("*")  ) {    return null;  }  return candidate.split(".").every((label) => hostnamePattern.test(label)) ? candidate : null;}; export const isLoopbackHost = (value: string): boolean => {  const normalized = normalizeControllerHost(value);  return normalized === "localhost" || normalized === "127.0.0.1" || normalized === "::1";}; export const isWildcardHost = (value: string): boolean => {  const normalized = normalizeControllerHost(value);  return normalized === "0.0.0.0" || normalized === "::";}; export const normalizeHttpOrigin = (value: string): string | null => {  try {    const parsed = new URL(value.trim());    if (      (parsed.protocol !== "http:" && parsed.protocol !== "https:") ||      parsed.username ||      parsed.password ||      parsed.pathname !== "/" ||      parsed.search ||      parsed.hash ||      !normalizeControllerHost(parsed.hostname)    ) {      return null;    }    return parsed.origin === "null" ? null : parsed.origin;  } catch {    return null;  }}; const allowedHostSchema = Schema.String.check(  Schema.makeFilter(    (value) => {      const normalized = normalizeControllerHost(value);      return normalized !== null && !isWildcardHost(normalized);    },    { expected: "an exact hostname or IP address" },  ),);const allowedHostsSchema = Schema.Array(allowedHostSchema).check(Schema.isNonEmpty()); export const decodeAllowedHosts = (value: string): string[] => {  try {    const entries = Schema.decodeUnknownSync(allowedHostsSchema)(value.split(","));    return [      ...new Set(        entries.flatMap((entry) => {          const normalized = normalizeControllerHost(entry);          return normalized ? [normalized] : [];        }),      ),    ];  } catch {    throw new Error(      "LOCAL_STUDIO_ALLOWED_HOSTS must contain a nonempty comma-separated list of exact hostnames or IP addresses",    );  }}; export const defaultAllowedHosts = (host: string): string[] => {  const normalized = normalizeControllerHost(host);  if (!normalized) throw new Error("LOCAL_STUDIO_HOST must be a hostname or IP address");  if (isLoopbackHost(normalized)) return [...new Set([normalized, ...loopbackHosts])];  if (isWildcardHost(normalized)) return [];  return [normalized];}; export const normalizeRequestAuthority = (value: string, expectedPort: number): string | null => {  const candidate = value.trim().toLowerCase();  if (!candidate || /[\s/@?#]/.test(candidate)) return null;  const bracketed = candidate.startsWith("[");  const match = bracketed    ? candidate.match(/^\[([^\]]+)](?::([0-9]+))?$/)    : candidate.match(/^([^:]+)(?::([0-9]+))?$/);  if (!match?.[1]) return null;  if (bracketed && isIP(match[1]) !== 6) return null;  const host = normalizeControllerHost(match[1]);  if (!host || isWildcardHost(host)) return null;  const suppliedPort = match[2];  if (suppliedPort !== undefined) {    const port = Number(suppliedPort);    if (!Number.isSafeInteger(port) || port < 1 || port > 65_535 || port !== expectedPort) {      return null;    }  }  return host;};