Files
SnowLuma/packages/core/tests/webui-client-ip.test.ts
DrSmoothl 3c97e71650 fix(onebot): strengthen endpoint defaults
Use safer defaults for newly created service endpoints and guide operators toward suitable access credentials without disrupting existing deployments.

Apply the same policy at the save boundary, retain deliberate local-only setups and external client compatibility, and add regression coverage for editing, recovery, and trusted access paths.
2026-08-01 18:12:04 +08:00

123 lines
4.7 KiB
TypeScript

import { describe, expect, it } from 'vitest';
import {
describeTrustProxy,
isLoopbackClientIp,
parseTrustProxy,
pickClientIp,
type TrustProxyMode,
} from '../src/webui/client-ip';
describe('isLoopbackClientIp', () => {
it.each(['127.0.0.1', '127.0.0.2', '::1', '0:0:0:0:0:0:0:1', '::ffff:127.0.0.9'])(
'accepts loopback address %s',
(address) => expect(isLoopbackClientIp(address)).toBe(true),
);
it.each(['', 'localhost', '0.0.0.0', '192.168.1.2', '::ffff:192.168.1.2'])(
'rejects non-loopback or unverified address %s',
(address) => expect(isLoopbackClientIp(address)).toBe(false),
);
});
function mockCtx(headers: Record<string, string> = {}) {
return {
req: {
header: (name: string) => headers[name.toLowerCase()],
},
} as unknown as Parameters<typeof pickClientIp>[0];
}
describe('parseTrustProxy', () => {
it('treats empty / unset as none (safe default)', () => {
expect(parseTrustProxy(undefined)).toEqual({ kind: 'none' });
expect(parseTrustProxy('')).toEqual({ kind: 'none' });
expect(parseTrustProxy(' ')).toEqual({ kind: 'none' });
});
it('recognises "1" / "true" / "all" as full trust', () => {
expect(parseTrustProxy('1')).toEqual({ kind: 'all' });
expect(parseTrustProxy('true')).toEqual({ kind: 'all' });
expect(parseTrustProxy('ALL')).toEqual({ kind: 'all' });
});
it('recognises "loopback" case-insensitively', () => {
expect(parseTrustProxy('Loopback')).toEqual({ kind: 'loopback' });
});
it('parses a comma-separated IP list', () => {
const mode = parseTrustProxy('10.0.0.1, 192.168.1.5, ');
expect(mode.kind).toBe('ip-list');
if (mode.kind !== 'ip-list') throw new Error('unreachable');
expect([...mode.ips].sort()).toEqual(['10.0.0.1', '192.168.1.5']);
});
});
describe('pickClientIp', () => {
const xff = { 'x-forwarded-for': '203.0.113.7, 10.0.0.1' };
const realIp = { 'x-real-ip': '203.0.113.42' };
const both = { ...xff, ...realIp };
it('returns socket peer and IGNORES headers when trust=none (default)', () => {
const mode: TrustProxyMode = { kind: 'none' };
expect(pickClientIp(mockCtx(both), mode, () => '198.51.100.9')).toBe('198.51.100.9');
});
it('honors X-Real-IP first, then XFF first hop, when trust=all', () => {
const mode: TrustProxyMode = { kind: 'all' };
expect(pickClientIp(mockCtx(both), mode, () => '10.0.0.1')).toBe('203.0.113.42');
expect(pickClientIp(mockCtx(xff), mode, () => '10.0.0.1')).toBe('203.0.113.7');
});
it('falls back to socket peer when trust=all but no headers present', () => {
const mode: TrustProxyMode = { kind: 'all' };
expect(pickClientIp(mockCtx({}), mode, () => '10.0.0.1')).toBe('10.0.0.1');
});
it('trust=loopback honors headers only when socket is loopback', () => {
const mode: TrustProxyMode = { kind: 'loopback' };
expect(pickClientIp(mockCtx(both), mode, () => '127.0.0.1')).toBe('203.0.113.42');
expect(pickClientIp(mockCtx(both), mode, () => '::1')).toBe('203.0.113.42');
expect(pickClientIp(mockCtx(both), mode, () => '::ffff:127.0.0.1')).toBe('203.0.113.42');
// Non-loopback peer → headers ignored, socket wins.
expect(pickClientIp(mockCtx(both), mode, () => '10.0.0.1')).toBe('10.0.0.1');
});
it('trust=ip-list honors headers only when socket is in the list', () => {
const mode: TrustProxyMode = { kind: 'ip-list', ips: new Set(['10.0.0.1', '10.0.0.2']) };
expect(pickClientIp(mockCtx(both), mode, () => '10.0.0.2')).toBe('203.0.113.42');
expect(pickClientIp(mockCtx(both), mode, () => '10.0.0.9')).toBe('10.0.0.9');
});
it('survives a socket resolver that throws', () => {
const mode: TrustProxyMode = { kind: 'none' };
expect(pickClientIp(mockCtx({}), mode, () => { throw new Error('boom'); })).toBe('127.0.0.1');
});
it('can fail closed when a security decision cannot resolve the socket peer', () => {
const mode: TrustProxyMode = { kind: 'none' };
expect(pickClientIp(mockCtx({}), mode, () => { throw new Error('boom'); }, '')).toBe('');
});
it('does not trust forwarded headers when the socket peer is unknown', () => {
const mode: TrustProxyMode = { kind: 'all' };
expect(
pickClientIp(
mockCtx({ 'x-real-ip': '127.0.0.1' }),
mode,
() => { throw new Error('boom'); },
'',
),
).toBe('');
});
});
describe('describeTrustProxy', () => {
it('renders each mode in a recognisable form for the boot log', () => {
expect(describeTrustProxy({ kind: 'none' })).toContain('socket peer');
expect(describeTrustProxy({ kind: 'all' })).toContain('any peer');
expect(describeTrustProxy({ kind: 'loopback' })).toContain('loopback');
expect(describeTrustProxy({ kind: 'ip-list', ips: new Set(['10.0.0.1']) }))
.toContain('10.0.0.1');
});
});