CI / Generate TypeScript types (push) Successful in 11s
CI / Server unit tests (push) Successful in 31s
CI / Build server (push) Successful in 27s
CI / Build client (push) Successful in 47s
CI / Playwright E2E tests (push) Successful in 1m39s
CI / Build and push Docker image (push) Successful in 45s
CI / Notify (push) Successful in 2s
65 lines
2.8 KiB
TypeScript
65 lines
2.8 KiB
TypeScript
import net from "net";
|
|
import type { Request } from "express";
|
|
|
|
/**
|
|
* Seznam adres, ze kterých smí přijít hlavička s identitou uživatele (přihlášení přes proxy).
|
|
* Podporuje jednotlivé adresy i rozsahy CIDR, IPv4 i IPv6 (např. "10.42.0.0/16,127.0.0.1,::1").
|
|
* Neplatná položka je chyba konfigurace — server kvůli ní nenaběhne (lepší než tiše důvěřovat).
|
|
*/
|
|
export function parseTrustedIps(list: string): net.BlockList {
|
|
const blockList = new net.BlockList();
|
|
for (const raw of list.split(',').map(s => s.trim()).filter(Boolean)) {
|
|
const [address, prefix] = raw.split('/');
|
|
const family = net.isIP(address);
|
|
if (!family) {
|
|
throw new Error(`HTTP_REMOTE_TRUSTED_IPS: neplatná adresa "${raw}"`);
|
|
}
|
|
const type = family === 4 ? 'ipv4' : 'ipv6';
|
|
if (prefix === undefined) {
|
|
blockList.addAddress(address, type);
|
|
continue;
|
|
}
|
|
const bits = Number(prefix);
|
|
if (!Number.isInteger(bits) || bits < 0 || bits > (family === 4 ? 32 : 128)) {
|
|
throw new Error(`HTTP_REMOTE_TRUSTED_IPS: neplatný rozsah "${raw}"`);
|
|
}
|
|
blockList.addSubnet(address, bits, type);
|
|
}
|
|
return blockList;
|
|
}
|
|
|
|
/**
|
|
* Je adresa přímého protějšku spojení mezi důvěryhodnými? IPv4 mapovaná do IPv6
|
|
* („::ffff:10.42.0.84", jak ji hlásí Node na dual-stack socketu) se porovnává jako IPv4.
|
|
*/
|
|
export function isTrustedPeer(address: string | undefined, trusted: net.BlockList): boolean {
|
|
if (!address) return false;
|
|
const mapped = address.match(/^::ffff:(\d+\.\d+\.\d+\.\d+)$/i);
|
|
const normalized = mapped ? mapped[1] : address;
|
|
const family = net.isIP(normalized);
|
|
if (!family) return false;
|
|
return trusted.check(normalized, family === 4 ? 'ipv4' : 'ipv6');
|
|
}
|
|
|
|
const warnedPeers = new Set<string>();
|
|
|
|
/**
|
|
* Vrátí identitu uživatele z hlavičky proxy, ale jen pokud spojení přišlo z důvěryhodné adresy.
|
|
* Rozhoduje skutečná adresa TCP spojení (ne req.ip, které lze ovlivnit hlavičkou X-Forwarded-For).
|
|
* Hlavičku od nedůvěryhodné adresy ignoruje a jednou za běh ji zaloguje (odhalí chybnou konfiguraci).
|
|
*/
|
|
export function getTrustedRemoteUser(req: Request, headerName: string, trusted: net.BlockList): string | undefined {
|
|
const value = req.header(headerName);
|
|
if (!value) return undefined;
|
|
const peer = req.socket?.remoteAddress;
|
|
if (!isTrustedPeer(peer, trusted)) {
|
|
const key = peer ?? 'neznámá adresa';
|
|
if (!warnedPeers.has(key)) {
|
|
warnedPeers.add(key);
|
|
console.warn(`Přihlášení přes hlavičky: ignoruji hlavičku ${headerName} z nedůvěryhodné adresy ${key} (viz HTTP_REMOTE_TRUSTED_IPS)`);
|
|
}
|
|
return undefined;
|
|
}
|
|
return Buffer.from(value, 'latin1').toString();
|
|
}
|