File size: 3,228 Bytes
4e23b01 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 | import net from 'node:net';
import type { FastifyReply, FastifyRequest } from 'fastify';
import { errEnvelope } from '../envelope';
const HOST_ERROR_CODE = 40301;
export interface HostCheckOptions {
readonly boundHost?: string;
readonly extra?: readonly string[];
readonly disable?: boolean;
}
export interface HostCheck {
readonly onRequest: (req: FastifyRequest, reply: FastifyReply) => Promise<FastifyReply | void>;
readonly isAllowed: (host: string | undefined) => boolean;
}
export function parseAllowedHosts(env: NodeJS.ProcessEnv = process.env): string[] {
const raw = env['KIMI_CODE_ALLOWED_HOSTS'];
if (raw === undefined) {
return [];
}
return raw
.split(',')
.map((entry) => entry.trim())
.filter((entry) => entry.length > 0);
}
export function isHostCheckDisabled(env: NodeJS.ProcessEnv = process.env): boolean {
return env['KIMI_CODE_DISABLE_HOST_CHECK'] === '1';
}
export function stripPort(host: string): string {
if (host.startsWith('[')) {
const end = host.indexOf(']');
return (end === -1 ? host : host.slice(0, end + 1)).toLowerCase();
}
const firstColon = host.indexOf(':');
if (firstColon === -1) {
return host.toLowerCase();
}
const lastColon = host.lastIndexOf(':');
if (firstColon === lastColon) {
const after = host.slice(lastColon + 1);
if (after.length > 0 && /^\d+$/.test(after)) {
return host.slice(0, lastColon).toLowerCase();
}
}
return host.toLowerCase();
}
export function formatHostErrorMessage(host: string | undefined): string {
const normalizedHost = host === undefined || host.length === 0 ? undefined : stripPort(host);
const hostLabel = normalizedHost ?? '<missing>';
const hostArg = normalizedHost ?? '<host>';
return `Invalid Host header: ${hostLabel}; allow this host with KIMI_CODE_ALLOWED_HOSTS=${hostArg} or 'kimi web --allowed-host ${hostArg}'.`;
}
export function isAllowedHost(host: string | undefined, opts: HostCheckOptions): boolean {
if (opts.disable === true) {
return true;
}
if (host === undefined || host.length === 0) {
return false;
}
const h = stripPort(host);
if (h === 'localhost' || h === '127.0.0.1' || h === '::1' || h === '[::1]') {
return true;
}
if (h.endsWith('.localhost')) {
return true;
}
if (net.isIP(h) !== 0) {
return true;
}
if (opts.boundHost !== undefined && h === stripPort(opts.boundHost)) {
return true;
}
if (opts.extra !== undefined) {
for (const entry of opts.extra) {
if (entry.startsWith('.')) {
const base = entry.slice(1);
if (h === base || h.endsWith(entry)) {
return true;
}
} else if (h === entry) {
return true;
}
}
}
return false;
}
export function createHostCheck(opts: HostCheckOptions): HostCheck {
const isAllowed = (host: string | undefined): boolean => isAllowedHost(host, opts);
const onRequest = async (
req: FastifyRequest,
reply: FastifyReply,
): Promise<FastifyReply | void> => {
if (!isAllowed(req.headers.host)) {
return reply.code(403).send(errEnvelope(HOST_ERROR_CODE, formatHostErrorMessage(req.headers.host), req.id));
}
};
return { onRequest, isAllowed };
}
|