fix: use correct client ip header
This commit is contained in:
@@ -9,3 +9,4 @@ export * from './src/url';
|
||||
export * from './src/id';
|
||||
export * from './src/get-previous-metric';
|
||||
export * from './src/group-by-labels';
|
||||
export * from './server/get-client-ip';
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
"main": "index.ts",
|
||||
"exports": {
|
||||
".": "./index.ts",
|
||||
"./server": "./server/index.ts"
|
||||
"./server": "./server/index.ts",
|
||||
"./server/get-client-ip": "./server/get-client-ip.ts"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "vitest",
|
||||
|
||||
86
packages/common/server/get-client-ip.ts
Normal file
86
packages/common/server/get-client-ip.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
/**
|
||||
* Get client IP from headers
|
||||
*
|
||||
* Can be configured via IP_HEADER_ORDER env variable
|
||||
* Example: IP_HEADER_ORDER="cf-connecting-ip,x-real-ip,x-forwarded-for"
|
||||
*/
|
||||
|
||||
export const DEFAULT_HEADER_ORDER = [
|
||||
'cf-connecting-ip',
|
||||
'true-client-ip',
|
||||
'x-real-ip',
|
||||
'x-client-ip',
|
||||
'fastly-client-ip',
|
||||
'x-cluster-client-ip',
|
||||
'x-appengine-user-ip',
|
||||
'do-connecting-ip',
|
||||
'x-nf-client-connection-ip',
|
||||
'x-forwarded-for',
|
||||
'x-forwarded',
|
||||
'forwarded',
|
||||
];
|
||||
|
||||
function getHeaderOrder(): string[] {
|
||||
if (typeof process !== 'undefined' && process.env?.IP_HEADER_ORDER) {
|
||||
return process.env.IP_HEADER_ORDER.split(',').map((h) => h.trim());
|
||||
}
|
||||
return DEFAULT_HEADER_ORDER;
|
||||
}
|
||||
|
||||
function isValidIp(ip: string): boolean {
|
||||
// Basic IP validation
|
||||
const ipv4 = /^(\d{1,3}\.){3}\d{1,3}$/;
|
||||
const ipv6 = /^([0-9a-fA-F]{0,4}:){2,7}[0-9a-fA-F]{0,4}$/;
|
||||
return ipv4.test(ip) || ipv6.test(ip);
|
||||
}
|
||||
|
||||
export function getClientIpFromHeaders(
|
||||
headers: Record<string, string | string[] | undefined> | Headers,
|
||||
overrideHeaderName?: string,
|
||||
): string {
|
||||
let headerOrder = getHeaderOrder();
|
||||
|
||||
if (overrideHeaderName) {
|
||||
headerOrder = [overrideHeaderName];
|
||||
}
|
||||
|
||||
for (const headerName of headerOrder) {
|
||||
let value: string | null = null;
|
||||
|
||||
// Get header value
|
||||
if (headers instanceof Headers) {
|
||||
value = headers.get(headerName);
|
||||
} else {
|
||||
const headerValue = headers[headerName];
|
||||
if (Array.isArray(headerValue)) {
|
||||
value = headerValue[0] || null;
|
||||
} else {
|
||||
value = headerValue || null;
|
||||
}
|
||||
}
|
||||
|
||||
if (!value) continue;
|
||||
|
||||
// Handle x-forwarded-for (comma separated)
|
||||
if (headerName === 'x-forwarded-for') {
|
||||
const firstIp = value.split(',')[0]?.trim();
|
||||
if (firstIp && isValidIp(firstIp)) {
|
||||
return firstIp;
|
||||
}
|
||||
}
|
||||
// Handle forwarded header (RFC 7239)
|
||||
else if (headerName === 'forwarded') {
|
||||
const match = value.match(/for=(?:"?\[?([^\]"]+)\]?"?)/i);
|
||||
const ip = match?.[1];
|
||||
if (ip && isValidIp(ip)) {
|
||||
return ip;
|
||||
}
|
||||
}
|
||||
// Regular headers
|
||||
else if (isValidIp(value)) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { getClientIpFromHeaders } from '@openpanel/common/server/get-client-ip';
|
||||
import type { NextFunction, Request, Response } from 'express';
|
||||
import { getClientIp } from 'request-ip';
|
||||
|
||||
import type { OpenPanelOptions } from '@openpanel/sdk';
|
||||
import { OpenPanel } from '@openpanel/sdk';
|
||||
@@ -22,7 +22,7 @@ export type OpenpanelOptions = OpenPanelOptions & {
|
||||
export default function createMiddleware(options: OpenpanelOptions) {
|
||||
return function middleware(req: Request, res: Response, next: NextFunction) {
|
||||
const sdk = new OpenPanel(options);
|
||||
const ip = getClientIp(req);
|
||||
const ip = getClientIpFromHeaders(req.headers);
|
||||
if (ip) {
|
||||
sdk.api.addHeader('x-client-ip', ip);
|
||||
}
|
||||
@@ -30,20 +30,15 @@ export default function createMiddleware(options: OpenpanelOptions) {
|
||||
sdk.api.addHeader('x-user-agent', req.headers['user-agent'] as string);
|
||||
}
|
||||
|
||||
if (options.getProfileId) {
|
||||
const profileId = options.getProfileId(req);
|
||||
if (profileId) {
|
||||
sdk.identify({
|
||||
profileId,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (options.trackRequest?.(req.url)) {
|
||||
const profileId = options.getProfileId
|
||||
? options.getProfileId(req)
|
||||
: undefined;
|
||||
sdk.track('request', {
|
||||
url: req.url,
|
||||
method: req.method,
|
||||
query: req.query,
|
||||
profileId,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@openpanel/sdk": "workspace:1.0.0-local",
|
||||
"request-ip": "^3.3.0"
|
||||
"@openpanel/common": "workspace:*"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"express": "^4.17.0 || ^5.0.0"
|
||||
|
||||
@@ -8,4 +8,6 @@ export default defineConfig({
|
||||
sourcemap: false,
|
||||
clean: true,
|
||||
minify: true,
|
||||
// Bundle @openpanel/common into the output instead of treating it as external
|
||||
noExternal: ['@openpanel/common'],
|
||||
});
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { createHash } from 'node:crypto';
|
||||
// adding .js next/script import fixes an issues
|
||||
// with esm and nextjs (when using pages dir)
|
||||
import { NextResponse } from 'next/server.js';
|
||||
@@ -16,9 +17,43 @@ export function createNextRouteHandler(options: CreateNextRouteHandlerOptions) {
|
||||
headers,
|
||||
body: JSON.stringify(await req.json()),
|
||||
});
|
||||
return NextResponse.json(await res.text());
|
||||
return NextResponse.json(await res.text(), { status: res.status });
|
||||
} catch (e) {
|
||||
return NextResponse.json(e);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function createScriptHandler() {
|
||||
return async function GET(req: Request) {
|
||||
if (!req.url.endsWith('op1.js')) {
|
||||
return NextResponse.json({ error: 'Not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
const scriptUrl = 'https://openpanel.dev/op1.js';
|
||||
try {
|
||||
const res = await fetch(scriptUrl, {
|
||||
// @ts-expect-error
|
||||
next: { revalidate: 86400 },
|
||||
});
|
||||
const text = await res.text();
|
||||
const etag = `"${createHash('md5').update(text).digest('hex')}"`;
|
||||
return new NextResponse(text, {
|
||||
headers: {
|
||||
'Content-Type': 'text/javascript',
|
||||
'Cache-Control':
|
||||
'public, max-age=86400, stale-while-revalidate=86400',
|
||||
ETag: etag,
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'Failed to fetch script',
|
||||
message: e instanceof Error ? e.message : String(e),
|
||||
},
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user