File size: 6,541 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 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 | import { mkdtemp, rm } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import Fastify, { type FastifyInstance } from 'fastify';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import {
createHostCheck,
formatHostErrorMessage,
isAllowedHost,
isHostCheckDisabled,
parseAllowedHosts,
stripPort,
} from '../src/middleware/hostnames';
import { type RunningServer, startServer } from '../src/start';
import { TEST_HOST_IDENTITY } from './helpers/hostIdentity';
describe('stripPort', () => {
it('strips the port from a hostname', () => {
expect(stripPort('localhost:80')).toBe('localhost');
});
it('strips the port from bracketed IPv6', () => {
expect(stripPort('[::1]:80')).toBe('[::1]');
});
it('strips the port from an IPv4 literal', () => {
expect(stripPort('1.2.3.4:5678')).toBe('1.2.3.4');
});
it('lowercases bare hosts', () => {
expect(stripPort('LOCALHOST')).toBe('localhost');
});
});
describe('formatHostErrorMessage', () => {
it('includes the rejected host and allow guidance', () => {
expect(formatHostErrorMessage('APP.Example.com:443')).toBe(
"Invalid Host header: app.example.com; allow this host with KIMI_CODE_ALLOWED_HOSTS=app.example.com or 'kimi web --allowed-host app.example.com'.",
);
});
});
describe('isAllowedHost (default allow set)', () => {
const allow = ['localhost', 'localhost:80', 'foo.localhost', '127.0.0.1', '127.0.0.1:58627', '[::1]', '::1', '8.8.8.8'];
for (const host of allow) {
it(`allows ${host}`, () => {
expect(isAllowedHost(host, {})).toBe(true);
});
}
const deny = ['evil.example.test', 'evil.example.test:80', '127.0.0.1.evil.example.test'];
for (const host of deny) {
it(`denies ${host}`, () => {
expect(isAllowedHost(host, {})).toBe(false);
});
}
it('denies a missing Host header', () => {
expect(isAllowedHost(undefined, {})).toBe(false);
});
});
describe('isAllowedHost (boundHost)', () => {
it('allows the bound host', () => {
expect(isAllowedHost('myhost', { boundHost: 'myhost' })).toBe(true);
});
it('strips the port on both sides', () => {
expect(isAllowedHost('myhost:1234', { boundHost: 'myhost:8080' })).toBe(true);
});
it('still denies unrelated hosts', () => {
expect(isAllowedHost('otherhost', { boundHost: 'myhost' })).toBe(false);
});
});
describe('isAllowedHost (extra)', () => {
it('matches a subdomain wildcard', () => {
expect(isAllowedHost('a.example.com', { extra: ['.example.com'] })).toBe(true);
});
it('matches the bare domain of a wildcard', () => {
expect(isAllowedHost('example.com', { extra: ['.example.com'] })).toBe(true);
});
it('does not match a partial suffix', () => {
expect(isAllowedHost('baddexample.com', { extra: ['.example.com'] })).toBe(false);
});
it('matches an exact entry', () => {
expect(isAllowedHost('foo', { extra: ['foo'] })).toBe(true);
});
});
describe('isAllowedHost (disable)', () => {
it('allows everything when disabled', () => {
expect(isAllowedHost('evil.example.test', { disable: true })).toBe(true);
});
});
describe('parseAllowedHosts', () => {
it('splits, trims, and drops empties', () => {
expect(parseAllowedHosts({ KIMI_CODE_ALLOWED_HOSTS: ' a, .b.example.com, ' })).toEqual([
'a',
'.b.example.com',
]);
});
it('returns [] when unset', () => {
expect(parseAllowedHosts({})).toEqual([]);
});
});
describe('isHostCheckDisabled', () => {
it('is true when set to "1"', () => {
expect(isHostCheckDisabled({ KIMI_CODE_DISABLE_HOST_CHECK: '1' })).toBe(true);
});
it('is false when unset', () => {
expect(isHostCheckDisabled({})).toBe(false);
});
});
describe('createHostCheck (onRequest hook)', () => {
let app: FastifyInstance;
beforeEach(async () => {
app = Fastify();
app.addHook('onRequest', createHostCheck({}).onRequest);
app.get('/api/v1/probe', async () => ({ ok: true }));
await app.ready();
});
afterEach(async () => {
await app.close();
});
it('rejects a disallowed Host with the 40301 envelope', async () => {
const res = await app.inject({
method: 'GET',
url: '/api/v1/probe',
headers: { host: 'evil.example.test' },
});
expect(res.statusCode).toBe(403);
const body = res.json() as Record<string, unknown>;
expect(body['code']).toBe(40301);
expect(body['msg']).toBe(
"Invalid Host header: evil.example.test; allow this host with KIMI_CODE_ALLOWED_HOSTS=evil.example.test or 'kimi web --allowed-host evil.example.test'.",
);
expect(body['data']).toBeNull();
expect(typeof body['request_id']).toBe('string');
});
it('allows the default app.inject Host (localhost:80)', async () => {
const res = await app.inject({ method: 'GET', url: '/api/v1/probe' });
expect(res.statusCode).toBe(200);
});
});
describe('startServer allowedHosts — env + option merge', () => {
const ENV_KEY = 'KIMI_CODE_ALLOWED_HOSTS';
let server: RunningServer | undefined;
let home: string | undefined;
let prevEnv: string | undefined;
beforeEach(() => {
prevEnv = process.env[ENV_KEY];
});
afterEach(async () => {
if (prevEnv === undefined) {
delete process.env[ENV_KEY];
} else {
process.env[ENV_KEY] = prevEnv;
}
if (server !== undefined) {
await server.close();
server = undefined;
}
if (home !== undefined) {
await rm(home, { recursive: true, force: true });
home = undefined;
}
});
it('appends opts.allowedHosts to KIMI_CODE_ALLOWED_HOSTS instead of replacing it', async () => {
process.env[ENV_KEY] = 'env-only.example.com';
home = await mkdtemp(join(tmpdir(), 'kimi-server-v2-host-merge-'));
server = await startServer({
hostIdentity: TEST_HOST_IDENTITY,
host: '127.0.0.1',
port: 0,
homeDir: home,
logLevel: 'silent',
allowedHosts: ['opt-only.example.com'],
});
const token = server.authTokenService.getToken();
const probe = async (host: string): Promise<number> => {
const res = await (server as RunningServer).app.inject({
method: 'GET',
url: '/api/v1/meta',
headers: { host, authorization: `Bearer ${token}` },
});
return res.statusCode;
};
expect(await probe('env-only.example.com')).toBe(200);
expect(await probe('opt-only.example.com')).toBe(200);
expect(await probe('evil.example.com')).toBe(403);
});
});
|