mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-26 11:12:53 +00:00
* refactor: rename allowedHosts to hostnames for DNS rebinding config The old name confused users who thought it was related to CORS. "hostnames" communicates that you're declaring the daemon's own hostnames, not granting access to external parties. Backward compatible: old config files (daemon.allowedHosts), env var (PASEO_ALLOWED_HOSTS), CLI flag (--allowed-hosts), and Nix option (allowedHosts) all still work as deprecated aliases. * fix(nix): update npmDepsHash to match current package-lock.json
98 lines
2.9 KiB
TypeScript
98 lines
2.9 KiB
TypeScript
import net from "node:net";
|
|
|
|
export type HostnamesConfig = true | string[] | undefined;
|
|
|
|
function normalizeHostname(hostname: string): string {
|
|
return hostname.trim().toLowerCase();
|
|
}
|
|
|
|
function parseHostnameFromHostHeader(hostHeader: string): string | null {
|
|
const trimmed = hostHeader.trim();
|
|
if (!trimmed) return null;
|
|
|
|
// IPv6 in brackets: [::1]:6767
|
|
if (trimmed.startsWith("[")) {
|
|
const end = trimmed.indexOf("]");
|
|
if (end === -1) return null;
|
|
return normalizeHostname(trimmed.slice(1, end));
|
|
}
|
|
|
|
// IPv4/hostname with optional port: localhost:6767
|
|
const colonIndex = trimmed.indexOf(":");
|
|
if (colonIndex === -1) {
|
|
return normalizeHostname(trimmed);
|
|
}
|
|
return normalizeHostname(trimmed.slice(0, colonIndex));
|
|
}
|
|
|
|
function matchesHostnamePattern(hostname: string, pattern: string): boolean {
|
|
const normalizedPattern = normalizeHostname(pattern);
|
|
if (!normalizedPattern) return false;
|
|
|
|
if (normalizedPattern.startsWith(".")) {
|
|
const base = normalizedPattern.slice(1);
|
|
if (!base) return false;
|
|
return hostname === base || hostname.endsWith(`.${base}`);
|
|
}
|
|
|
|
return hostname === normalizedPattern;
|
|
}
|
|
|
|
function isDefaultAllowedHostname(hostname: string): boolean {
|
|
// Vite-style defaults: localhost, *.localhost, and all IP addresses.
|
|
if (hostname === "localhost") return true;
|
|
if (hostname.endsWith(".localhost")) return true;
|
|
if (net.isIP(hostname) !== 0) return true;
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Vite-style hostname allowlist check, adapted to raw Host headers.
|
|
*
|
|
* Semantics:
|
|
* - `hostnames === true` => allow any host.
|
|
* - `hostnames === []` or `undefined` => allow localhost, *.localhost, and all IPs.
|
|
* - `hostnames === ['.example.com', 'myhost']` => allow those *in addition* to defaults.
|
|
*/
|
|
export function isHostnameAllowed(
|
|
hostHeader: string | undefined,
|
|
hostnames: HostnamesConfig,
|
|
): boolean {
|
|
const hostname = hostHeader ? parseHostnameFromHostHeader(hostHeader) : null;
|
|
if (!hostname) return false;
|
|
|
|
if (hostnames === true) return true;
|
|
|
|
// Defaults are always allowed.
|
|
if (isDefaultAllowedHostname(hostname)) return true;
|
|
|
|
const patterns = hostnames ?? [];
|
|
for (const pattern of patterns) {
|
|
if (matchesHostnamePattern(hostname, pattern)) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
export function mergeHostnames(values: Array<HostnamesConfig>): HostnamesConfig {
|
|
let merged: string[] = [];
|
|
for (const value of values) {
|
|
if (value === true) return true;
|
|
if (!value) continue;
|
|
merged = merged.concat(value);
|
|
}
|
|
|
|
const deduped = Array.from(new Set(merged.map((v) => v.trim()).filter((v) => v.length > 0)));
|
|
return deduped;
|
|
}
|
|
|
|
export function parseHostnamesEnv(raw: string | undefined): HostnamesConfig {
|
|
if (!raw) return undefined;
|
|
const trimmed = raw.trim();
|
|
if (!trimmed) return undefined;
|
|
if (trimmed.toLowerCase() === "true") return true;
|
|
return trimmed
|
|
.split(",")
|
|
.map((s) => s.trim())
|
|
.filter((s) => s.length > 0);
|
|
}
|