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);
  });
});