mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-17 21:15:32 +09:00
perf(api): resolve the client ip once per request (#2218)
This commit is contained in:
1 parent
3eeba1da2b
commit
b07e2c397c
11 files changed
+316
-38
No files matched your search
@@ -1,18 +1,17 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {IpBannedError} from '@fluxer/errors/src/domains/moderation/IpBannedError';
|
||||
import {extractClientIp} from '@fluxer/ip_utils/src/ClientIp';
|
||||
import {getSameIpDecisionKey, type IpAddressFamily} from '@fluxer/ip_utils/src/IpAddress';
|
||||
import type {IKVProvider, IKVSubscription} from '@pkgs/kv_client/src/IKVProvider';
|
||||
import {createMiddleware} from 'hono/factory';
|
||||
import {AdminRepository} from '../admin/AdminRepository';
|
||||
import type {BannedIpEntry, BannedIpKind} from '../admin/IAdminRepository';
|
||||
import {Config} from '../Config';
|
||||
import {IP_BAN_REFRESH_CHANNEL} from '../constants/IpBan';
|
||||
import {Logger} from '../Logger';
|
||||
import {isIpBanExempt} from '../risk/IpBanExemptions';
|
||||
import type {HonoEnv} from '../types/HonoEnv';
|
||||
import {parseIpBanEntry, tryParseSingleIp} from '../utils/IpRangeUtils';
|
||||
import {getRequestClientIp} from '../utils/RequestClientIp';
|
||||
|
||||
type FamilyMap<T> = Record<IpAddressFamily, Map<string, T>>;
|
||||
|
||||
@@ -371,10 +370,7 @@ class IpBanCache {
|
||||
|
||||
export const ipBanCache = new IpBanCache();
|
||||
export const IpBanMiddleware = createMiddleware<HonoEnv>(async (ctx, next) => {
|
||||
const clientIp = extractClientIp(ctx.req.raw, {
|
||||
trustClientIpHeader: Config.proxy.trust_client_ip_header,
|
||||
clientIpHeaderName: Config.proxy.client_ip_header,
|
||||
});
|
||||
const clientIp = getRequestClientIp(ctx);
|
||||
const match = clientIp ? ipBanCache.getMatch(clientIp) : null;
|
||||
if (match) {
|
||||
throw new IpBannedError({
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
import {createHash} from 'node:crypto';
|
||||
import {UserFlags} from '@fluxer/constants/src/UserConstants';
|
||||
import {RateLimitError} from '@fluxer/errors/src/domains/core/RateLimitError';
|
||||
import {extractClientIp} from '@fluxer/ip_utils/src/ClientIp';
|
||||
import {getSameIpDecisionKey} from '@fluxer/ip_utils/src/IpAddress';
|
||||
import type {BucketConfig, RateLimitResult, RateLimitScope} from '@pkgs/rate_limit/src/IRateLimitService';
|
||||
import type {Context, MiddlewareHandler} from 'hono';
|
||||
@@ -11,6 +10,7 @@ import {createMiddleware} from 'hono/factory';
|
||||
import * as AuthSession from '../auth/AuthSession';
|
||||
import {Config} from '../Config';
|
||||
import type {HonoEnv} from '../types/HonoEnv';
|
||||
import {getRequestClientIp} from '../utils/RequestClientIp';
|
||||
|
||||
type AccountType = 'user' | 'bot' | 'webhook';
|
||||
|
||||
@@ -58,10 +58,7 @@ function getClientIdentifier(ctx: Context<HonoEnv>): string {
|
||||
}
|
||||
return `user:${user.id}:${tokenType}`;
|
||||
}
|
||||
const ip = extractClientIp(ctx.req.raw, {
|
||||
trustClientIpHeader: Config.proxy.trust_client_ip_header,
|
||||
clientIpHeaderName: Config.proxy.client_ip_header,
|
||||
});
|
||||
const ip = getRequestClientIp(ctx);
|
||||
if (!ip) return 'internal';
|
||||
return `ip:${getSameIpDecisionKey(ip) ?? ip}`;
|
||||
}
|
||||
|
||||
@@ -77,7 +77,7 @@ import {UserChannelRequestService} from '../user/services/UserChannelRequestServ
|
||||
import {UserContentRequestService} from '../user/services/UserContentRequestService';
|
||||
import {UserRelationshipRequestService} from '../user/services/UserRelationshipRequestService';
|
||||
import {UserService} from '../user/services/UserService';
|
||||
import {resolveRequestClientIp} from '../utils/IpUtils';
|
||||
import {getRequestClientIp} from '../utils/RequestClientIp';
|
||||
import {VoiceService} from '../voice/VoiceService';
|
||||
import {WebhookRequestService} from '../webhook/WebhookRequestService';
|
||||
import {WebhookService} from '../webhook/WebhookService';
|
||||
@@ -1050,7 +1050,7 @@ class RequestServices implements RequestScopedServices {
|
||||
export const ServiceMiddleware = createMiddleware<HonoEnv>(async (ctx, next) => {
|
||||
const apiContext = createApiContext({
|
||||
requestId: ctx.get('requestId') ?? crypto.randomUUID(),
|
||||
clientIp: resolveRequestClientIp(ctx.req.raw),
|
||||
clientIp: getRequestClientIp(ctx),
|
||||
userAgent: ctx.req.header('user-agent') ?? null,
|
||||
});
|
||||
ctx.set('apiContext', apiContext);
|
||||
|
||||
@@ -1,17 +1,13 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {IpBannedError} from '@fluxer/errors/src/domains/moderation/IpBannedError';
|
||||
import {extractClientIp} from '@fluxer/ip_utils/src/ClientIp';
|
||||
import {createMiddleware} from 'hono/factory';
|
||||
import {Config} from '../Config';
|
||||
import type {HonoEnv} from '../types/HonoEnv';
|
||||
import {getRequestClientIp} from '../utils/RequestClientIp';
|
||||
import {torExitListCache} from './TorExitListCache';
|
||||
|
||||
export const TorExitMiddleware = createMiddleware<HonoEnv>(async (ctx, next) => {
|
||||
const clientIp = extractClientIp(ctx.req.raw, {
|
||||
trustClientIpHeader: Config.proxy.trust_client_ip_header,
|
||||
clientIpHeaderName: Config.proxy.client_ip_header,
|
||||
});
|
||||
const clientIp = getRequestClientIp(ctx);
|
||||
if (clientIp && torExitListCache.isTorExit(clientIp)) {
|
||||
throw new IpBannedError({
|
||||
ipAddress: clientIp,
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
|
||||
import {APIErrorCodes} from '@fluxer/constants/src/ApiErrorCodes';
|
||||
import {ForbiddenError} from '@fluxer/errors/src/domains/core/ForbiddenError';
|
||||
import {parseIpAddress} from '@fluxer/ip_utils/src/IpAddress';
|
||||
import {createMiddleware} from 'hono/factory';
|
||||
import type {ILogger} from '../ILogger';
|
||||
import type {HonoEnv} from '../types/HonoEnv';
|
||||
import {resolveClientIpWithOptions} from '../utils/RequestClientIp';
|
||||
import {stripApiPrefix} from '../utils/RequestPathUtils';
|
||||
|
||||
interface TrustedClientIpHeaderOptions {
|
||||
@@ -46,8 +46,7 @@ export function TrustedClientIpHeaderMiddleware({
|
||||
await next();
|
||||
return;
|
||||
}
|
||||
const firstHop = clientIpHeaderValue.split(',')[0]?.trim() ?? clientIpHeaderValue;
|
||||
if (!parseIpAddress(firstHop)) {
|
||||
if (!resolveClientIpWithOptions(ctx, {trustClientIpHeader, clientIpHeaderName})) {
|
||||
logger.warn({path, clientIpHeaderName}, 'Rejected request with invalid client IP header');
|
||||
throw new ForbiddenError({code: APIErrorCodes.FORBIDDEN});
|
||||
}
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {extractClientIpDetails, requireClientIp} from '@fluxer/ip_utils/src/ClientIp';
|
||||
import type {Context} from 'hono';
|
||||
import {createMiddleware} from 'hono/factory';
|
||||
import * as AuthSession from '../auth/AuthSession';
|
||||
import {Config} from '../Config';
|
||||
import {Logger} from '../Logger';
|
||||
import type {User} from '../models/User';
|
||||
import type {HonoEnv} from '../types/HonoEnv';
|
||||
import {requireRequestClientIp} from '../utils/RequestClientIp';
|
||||
import {stripApiPrefix} from '../utils/RequestPathUtils';
|
||||
import {hashAuthToken, recordAbuseSignal} from './AbusiveIpAutoBanner';
|
||||
|
||||
@@ -61,10 +60,7 @@ function setUserInContext(ctx: Context<HonoEnv>, user: User, trackActivity: bool
|
||||
ctx.set('user', user);
|
||||
if (trackActivity) {
|
||||
const now = new Date();
|
||||
const ip = requireClientIp(ctx.req.raw, {
|
||||
trustClientIpHeader: Config.proxy.trust_client_ip_header,
|
||||
clientIpHeaderName: Config.proxy.client_ip_header,
|
||||
});
|
||||
const ip = requireRequestClientIp(ctx);
|
||||
const kvActivityTracker = ctx.get('kvActivityTracker');
|
||||
const userActivityBuffer = ctx.get('userActivityBuffer');
|
||||
userActivityBuffer.recordActivity(user.id, now, ip);
|
||||
@@ -81,16 +77,7 @@ export const UserMiddleware = createMiddleware<HonoEnv>(async (ctx, next) => {
|
||||
}
|
||||
const rawAuthHeader = ctx.req.header('Authorization');
|
||||
const parsed = parseAuthHeader(rawAuthHeader);
|
||||
const extractedClientIp = extractClientIpDetails(ctx.req.raw, {
|
||||
trustClientIpHeader: Config.proxy.trust_client_ip_header,
|
||||
clientIpHeaderName: Config.proxy.client_ip_header,
|
||||
});
|
||||
const resolvedClientIp =
|
||||
extractedClientIp?.ip ??
|
||||
requireClientIp(ctx.req.raw, {
|
||||
trustClientIpHeader: Config.proxy.trust_client_ip_header,
|
||||
clientIpHeaderName: Config.proxy.client_ip_header,
|
||||
});
|
||||
const resolvedClientIp = requireRequestClientIp(ctx);
|
||||
ctx.set('oauthBearerToken', undefined);
|
||||
ctx.set('oauthBearerApplicationId', undefined);
|
||||
ctx.set('oauthBearerAllowed', false);
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {ForbiddenError} from '@fluxer/errors/src/domains/core/ForbiddenError';
|
||||
import {IpBannedError} from '@fluxer/errors/src/domains/moderation/IpBannedError';
|
||||
import {Hono} from 'hono';
|
||||
import {beforeEach, describe, expect, it} from 'vitest';
|
||||
import {NoopLogger} from '../../test/mocks/NoopLogger';
|
||||
import type {HonoEnv} from '../../types/HonoEnv';
|
||||
import type {ClientIpResolution} from '../../utils/RequestClientIp';
|
||||
import {IpBanMiddleware, ipBanCache} from '../IpBanMiddleware';
|
||||
import {torExitListCache} from '../TorExitListCache';
|
||||
import {TorExitMiddleware} from '../TorExitMiddleware';
|
||||
import {TrustedClientIpHeaderMiddleware} from '../TrustedClientIpHeaderMiddleware';
|
||||
|
||||
interface Pipeline {
|
||||
request: (headers: Record<string, string>) => Promise<Response>;
|
||||
resolutions: Array<ClientIpResolution | undefined>;
|
||||
errors: Array<unknown>;
|
||||
}
|
||||
|
||||
function createPipeline(clientIpHeaderName = 'x-forwarded-for'): Pipeline {
|
||||
const resolutions: Array<ClientIpResolution | undefined> = [];
|
||||
const errors: Array<unknown> = [];
|
||||
const app = new Hono<HonoEnv>();
|
||||
app.use(IpBanMiddleware);
|
||||
app.use(async (ctx, next) => {
|
||||
resolutions.push(ctx.get('clientIpResolution'));
|
||||
await next();
|
||||
});
|
||||
app.use(
|
||||
TrustedClientIpHeaderMiddleware({
|
||||
enabled: true,
|
||||
logger: new NoopLogger(),
|
||||
trustClientIpHeader: true,
|
||||
clientIpHeaderName,
|
||||
}),
|
||||
);
|
||||
app.use(TorExitMiddleware);
|
||||
app.get('/v1/messages', (ctx) => {
|
||||
resolutions.push(ctx.get('clientIpResolution'));
|
||||
return ctx.text('ok');
|
||||
});
|
||||
app.onError((error) => {
|
||||
errors.push(error);
|
||||
return new Response('error', {status: 403});
|
||||
});
|
||||
return {
|
||||
request: async (headers) => app.request('http://localhost/v1/messages', {headers}),
|
||||
resolutions,
|
||||
errors,
|
||||
};
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
ipBanCache.resetCaches();
|
||||
torExitListCache.clearForTesting();
|
||||
});
|
||||
|
||||
describe('client ip resolution across the request pipeline', () => {
|
||||
it('resolves once and shares that resolution with every later middleware', async () => {
|
||||
const pipeline = createPipeline();
|
||||
const response = await pipeline.request({'x-forwarded-for': '203.0.113.10, 10.0.0.1'});
|
||||
expect(response.status).toBe(200);
|
||||
expect(pipeline.resolutions).toHaveLength(2);
|
||||
expect(pipeline.resolutions[0]?.ip).toBe('203.0.113.10');
|
||||
expect(pipeline.resolutions[1]).toBe(pipeline.resolutions[0]);
|
||||
});
|
||||
it('still blocks a banned client ip', async () => {
|
||||
ipBanCache.ban('203.0.113.20');
|
||||
const pipeline = createPipeline();
|
||||
const response = await pipeline.request({'x-forwarded-for': '203.0.113.20'});
|
||||
expect(response.status).toBe(403);
|
||||
expect(pipeline.errors[0]).toBeInstanceOf(IpBannedError);
|
||||
});
|
||||
it('still blocks a tor exit client ip', async () => {
|
||||
torExitListCache.seedForTesting(['203.0.113.30']);
|
||||
const pipeline = createPipeline();
|
||||
const response = await pipeline.request({'x-forwarded-for': '203.0.113.30'});
|
||||
expect(response.status).toBe(403);
|
||||
expect(pipeline.errors[0]).toBeInstanceOf(IpBannedError);
|
||||
});
|
||||
it('rejects a malformed client ip header after the ban check saw no address', async () => {
|
||||
const pipeline = createPipeline();
|
||||
const response = await pipeline.request({'x-forwarded-for': 'not-an-ip'});
|
||||
expect(response.status).toBe(403);
|
||||
expect(pipeline.resolutions[0]?.ip).toBe(null);
|
||||
expect(pipeline.errors[0]).toBeInstanceOf(ForbiddenError);
|
||||
expect(pipeline.errors[0]).not.toBeInstanceOf(IpBannedError);
|
||||
});
|
||||
it('passes requests through when no client ip header is present', async () => {
|
||||
const pipeline = createPipeline();
|
||||
const response = await pipeline.request({});
|
||||
expect(response.status).toBe(200);
|
||||
expect(pipeline.resolutions[0]?.ip).toBe(null);
|
||||
expect(pipeline.resolutions[1]).toBe(pipeline.resolutions[0]);
|
||||
});
|
||||
it('keeps ban checks on the configured header when the trusted header check uses another one', async () => {
|
||||
ipBanCache.ban('198.51.100.7');
|
||||
const pipeline = createPipeline('x-real-ip');
|
||||
const response = await pipeline.request({'x-forwarded-for': '203.0.113.10', 'x-real-ip': '198.51.100.7'});
|
||||
expect(response.status).toBe(200);
|
||||
expect(pipeline.resolutions[0]?.ip).toBe('203.0.113.10');
|
||||
expect(pipeline.resolutions[1]?.ip).toBe('203.0.113.10');
|
||||
});
|
||||
it('rejects an invalid trusted header even when the configured header carries a valid address', async () => {
|
||||
const pipeline = createPipeline('x-real-ip');
|
||||
const response = await pipeline.request({'x-forwarded-for': '203.0.113.10', 'x-real-ip': 'not-an-ip'});
|
||||
expect(response.status).toBe(403);
|
||||
expect(pipeline.errors[0]).toBeInstanceOf(ForbiddenError);
|
||||
});
|
||||
});
|
||||
@@ -59,6 +59,20 @@ describe('TrustedClientIpHeaderMiddleware', () => {
|
||||
});
|
||||
expect(response.status).toBe(403);
|
||||
});
|
||||
it('accepts requests when the IP header holds only whitespace (passthrough)', async () => {
|
||||
const app = createApp();
|
||||
const response = await app.request('http://localhost/v1/messages', {
|
||||
headers: {'x-real-ip': ' '},
|
||||
});
|
||||
expect(response.status).toBe(200);
|
||||
});
|
||||
it('rejects x-forwarded-for with an empty first hop', async () => {
|
||||
const app = createApp('x-forwarded-for');
|
||||
const response = await app.request('http://localhost/v1/messages', {
|
||||
headers: {'x-forwarded-for': ', 10.0.0.1'},
|
||||
});
|
||||
expect(response.status).toBe(403);
|
||||
});
|
||||
it('accepts x-forwarded-for with multiple hops and a valid first hop', async () => {
|
||||
const app = createApp('x-forwarded-for');
|
||||
const response = await app.request('http://localhost/v1/messages', {
|
||||
|
||||
@@ -81,6 +81,7 @@ import type {UserContactChangeLogService} from '../user/services/UserContactChan
|
||||
import type {UserContentRequestService} from '../user/services/UserContentRequestService';
|
||||
import type {UserRelationshipRequestService} from '../user/services/UserRelationshipRequestService';
|
||||
import type {UserService} from '../user/services/UserService';
|
||||
import type {ClientIpResolution} from '../utils/RequestClientIp';
|
||||
import type {SweegoWebhookService} from '../webhook/SweegoWebhookService';
|
||||
import type {WebhookRequestService} from '../webhook/WebhookRequestService';
|
||||
import type {WebhookService} from '../webhook/WebhookService';
|
||||
@@ -91,6 +92,7 @@ export interface HonoEnv {
|
||||
apiContext: ApiContext;
|
||||
user: User;
|
||||
requestId?: string;
|
||||
clientIpResolution?: ClientIpResolution;
|
||||
responseSchema: unknown;
|
||||
adminService: AdminService;
|
||||
adminArchiveService: AdminArchiveService;
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {extractClientIp, MissingClientIpError, resolveClientIpHeaderName} from '@fluxer/ip_utils/src/ClientIp';
|
||||
import type {Context} from 'hono';
|
||||
import {Config} from '../Config';
|
||||
import type {HonoEnv} from '../types/HonoEnv';
|
||||
|
||||
export interface ClientIpResolution {
|
||||
trustClientIpHeader: boolean;
|
||||
clientIpHeaderName: string;
|
||||
ip: string | null;
|
||||
}
|
||||
|
||||
interface ClientIpResolutionOptions {
|
||||
trustClientIpHeader: boolean;
|
||||
clientIpHeaderName: string;
|
||||
}
|
||||
|
||||
export function resolveClientIpWithOptions(ctx: Context<HonoEnv>, options: ClientIpResolutionOptions): string | null {
|
||||
const clientIpHeaderName = resolveClientIpHeaderName(options.clientIpHeaderName);
|
||||
const {trustClientIpHeader} = options;
|
||||
const cached = ctx.get('clientIpResolution');
|
||||
if (
|
||||
cached &&
|
||||
cached.trustClientIpHeader === trustClientIpHeader &&
|
||||
cached.clientIpHeaderName === clientIpHeaderName
|
||||
) {
|
||||
return cached.ip;
|
||||
}
|
||||
const ip = extractClientIp(ctx.req.raw, {trustClientIpHeader, clientIpHeaderName});
|
||||
ctx.set('clientIpResolution', {trustClientIpHeader, clientIpHeaderName, ip});
|
||||
return ip;
|
||||
}
|
||||
|
||||
export function getRequestClientIp(ctx: Context<HonoEnv>): string | null {
|
||||
return resolveClientIpWithOptions(ctx, {
|
||||
trustClientIpHeader: Config.proxy.trust_client_ip_header,
|
||||
clientIpHeaderName: Config.proxy.client_ip_header,
|
||||
});
|
||||
}
|
||||
|
||||
export function requireRequestClientIp(ctx: Context<HonoEnv>): string {
|
||||
const ip = getRequestClientIp(ctx);
|
||||
if (!ip) {
|
||||
throw new MissingClientIpError();
|
||||
}
|
||||
return ip;
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {MissingClientIpError} from '@fluxer/ip_utils/src/ClientIp';
|
||||
import {Hono} from 'hono';
|
||||
import {describe, expect, it} from 'vitest';
|
||||
import type {HonoEnv} from '../../types/HonoEnv';
|
||||
import {getRequestClientIp, requireRequestClientIp, resolveClientIpWithOptions} from '../RequestClientIp';
|
||||
|
||||
interface CountingRequest {
|
||||
request: Request;
|
||||
countReads: (name: string) => number;
|
||||
}
|
||||
|
||||
function createCountingRequest(headers: Record<string, string>): CountingRequest {
|
||||
const request = new Request('http://localhost/v1/messages', {headers});
|
||||
const reads = new Map<string, number>();
|
||||
const get = request.headers.get.bind(request.headers);
|
||||
Object.defineProperty(request.headers, 'get', {
|
||||
configurable: true,
|
||||
value: (name: string): string | null => {
|
||||
const key = name.toLowerCase();
|
||||
reads.set(key, (reads.get(key) ?? 0) + 1);
|
||||
return get(name);
|
||||
},
|
||||
});
|
||||
return {request, countReads: (name) => reads.get(name.toLowerCase()) ?? 0};
|
||||
}
|
||||
|
||||
describe('RequestClientIp', () => {
|
||||
it('resolves the client ip once and hands the same value to every consumer', async () => {
|
||||
const {request, countReads} = createCountingRequest({'x-forwarded-for': '203.0.113.10, 10.0.0.1'});
|
||||
const seen: Array<string | null> = [];
|
||||
const app = new Hono<HonoEnv>();
|
||||
app.use(async (ctx, next) => {
|
||||
seen.push(getRequestClientIp(ctx));
|
||||
await next();
|
||||
});
|
||||
app.use(async (ctx, next) => {
|
||||
seen.push(getRequestClientIp(ctx));
|
||||
await next();
|
||||
});
|
||||
app.get('/v1/messages', (ctx) => {
|
||||
seen.push(requireRequestClientIp(ctx));
|
||||
return ctx.text('ok');
|
||||
});
|
||||
const response = await app.request(request);
|
||||
expect(response.status).toBe(200);
|
||||
expect(seen).toEqual(['203.0.113.10', '203.0.113.10', '203.0.113.10']);
|
||||
expect(countReads('x-forwarded-for')).toBe(1);
|
||||
});
|
||||
it('normalizes the resolved address the same way for every consumer', async () => {
|
||||
const seen: Array<string | null> = [];
|
||||
const app = new Hono<HonoEnv>();
|
||||
app.use(async (ctx, next) => {
|
||||
seen.push(getRequestClientIp(ctx));
|
||||
await next();
|
||||
});
|
||||
app.get('/v1/messages', (ctx) => {
|
||||
seen.push(getRequestClientIp(ctx));
|
||||
return ctx.text('ok');
|
||||
});
|
||||
await app.request('http://localhost/v1/messages', {headers: {'x-forwarded-for': '2001:DB8::1'}});
|
||||
expect(seen).toEqual(['2001:db8::1', '2001:db8::1']);
|
||||
});
|
||||
it('reports a missing client ip header exactly as before', async () => {
|
||||
const {request, countReads} = createCountingRequest({});
|
||||
const seen: Array<string | null> = [];
|
||||
const errors: Array<unknown> = [];
|
||||
const app = new Hono<HonoEnv>();
|
||||
app.use(async (ctx, next) => {
|
||||
seen.push(getRequestClientIp(ctx));
|
||||
await next();
|
||||
});
|
||||
app.get('/v1/messages', (ctx) => {
|
||||
seen.push(getRequestClientIp(ctx));
|
||||
try {
|
||||
requireRequestClientIp(ctx);
|
||||
} catch (error) {
|
||||
errors.push(error);
|
||||
}
|
||||
return ctx.text('ok');
|
||||
});
|
||||
await app.request(request);
|
||||
expect(seen).toEqual([null, null]);
|
||||
expect(errors[0]).toBeInstanceOf(MissingClientIpError);
|
||||
expect(countReads('x-forwarded-for')).toBe(1);
|
||||
});
|
||||
it('reports a malformed client ip header exactly as before', async () => {
|
||||
const seen: Array<string | null> = [];
|
||||
const app = new Hono<HonoEnv>();
|
||||
app.use(async (ctx, next) => {
|
||||
seen.push(getRequestClientIp(ctx));
|
||||
await next();
|
||||
});
|
||||
app.get('/v1/messages', (ctx) => {
|
||||
seen.push(getRequestClientIp(ctx));
|
||||
return ctx.text('ok');
|
||||
});
|
||||
await app.request('http://localhost/v1/messages', {headers: {'x-forwarded-for': 'not-an-ip'}});
|
||||
expect(seen).toEqual([null, null]);
|
||||
});
|
||||
it('never reuses a resolution made under a different header name', async () => {
|
||||
const seen: Array<string | null> = [];
|
||||
const app = new Hono<HonoEnv>();
|
||||
app.get('/v1/messages', (ctx) => {
|
||||
seen.push(getRequestClientIp(ctx));
|
||||
seen.push(resolveClientIpWithOptions(ctx, {trustClientIpHeader: true, clientIpHeaderName: 'x-real-ip'}));
|
||||
seen.push(getRequestClientIp(ctx));
|
||||
return ctx.text('ok');
|
||||
});
|
||||
await app.request('http://localhost/v1/messages', {
|
||||
headers: {'x-forwarded-for': '203.0.113.10', 'x-real-ip': '198.51.100.7'},
|
||||
});
|
||||
expect(seen).toEqual(['203.0.113.10', '198.51.100.7', '203.0.113.10']);
|
||||
});
|
||||
it('never reuses a resolution made under a different trust setting', async () => {
|
||||
const seen: Array<string | null> = [];
|
||||
const app = new Hono<HonoEnv>();
|
||||
app.get('/v1/messages', (ctx) => {
|
||||
seen.push(getRequestClientIp(ctx));
|
||||
seen.push(resolveClientIpWithOptions(ctx, {trustClientIpHeader: false, clientIpHeaderName: 'x-forwarded-for'}));
|
||||
seen.push(getRequestClientIp(ctx));
|
||||
return ctx.text('ok');
|
||||
});
|
||||
await app.request('http://localhost/v1/messages', {headers: {'x-forwarded-for': '203.0.113.10'}});
|
||||
expect(seen).toEqual(['203.0.113.10', null, '203.0.113.10']);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user