From 8fbe944df0337ddb9c2d9a08d3091c025053a6f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carl-Gerhard=20Lindesva=CC=88rd?= Date: Thu, 13 Nov 2025 23:58:23 +0100 Subject: [PATCH] fix: use correct client ip header --- apps/api/package.json | 2 - apps/api/src/controllers/event.controller.ts | 3 +- apps/api/src/controllers/misc.controller.ts | 34 +++++++- .../api/src/controllers/profile.controller.ts | 3 +- apps/api/src/controllers/track.controller.ts | 3 +- apps/api/src/hooks/ip.hook.ts | 13 ++- apps/api/src/index.ts | 2 +- apps/api/src/utils/get-client-ip.ts | 8 -- apps/public/app/api/[...op]/route.ts | 67 +++++++++++++++ apps/public/app/layout.tsx | 7 +- apps/public/content/docs/sdks/nextjs.mdx | 8 +- apps/public/package.json | 1 + packages/common/index.ts | 1 + packages/common/package.json | 3 +- packages/common/server/get-client-ip.ts | 86 +++++++++++++++++++ packages/sdks/express/index.ts | 17 ++-- packages/sdks/express/package.json | 2 +- packages/sdks/express/tsup.config.ts | 2 + .../sdks/nextjs/createNextRouteHandler.ts | 37 +++++++- pnpm-lock.yaml | 17 ++-- 20 files changed, 255 insertions(+), 61 deletions(-) delete mode 100644 apps/api/src/utils/get-client-ip.ts create mode 100644 apps/public/app/api/[...op]/route.ts create mode 100644 packages/common/server/get-client-ip.ts diff --git a/apps/api/package.json b/apps/api/package.json index 9a14c7d3..cb061c3a 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -41,7 +41,6 @@ "groupmq": "1.0.0-next.19", "jsonwebtoken": "^9.0.2", "ramda": "^0.29.1", - "request-ip": "^3.3.0", "sharp": "^0.33.5", "source-map-support": "^0.5.21", "sqlstring": "^2.3.3", @@ -58,7 +57,6 @@ "@types/js-yaml": "^4.0.9", "@types/jsonwebtoken": "^9.0.9", "@types/ramda": "^0.30.2", - "@types/request-ip": "^0.0.41", "@types/source-map-support": "^0.5.10", "@types/sqlstring": "^2.3.2", "@types/uuid": "^10.0.0", diff --git a/apps/api/src/controllers/event.controller.ts b/apps/api/src/controllers/event.controller.ts index b758a5b1..3eed7d40 100644 --- a/apps/api/src/controllers/event.controller.ts +++ b/apps/api/src/controllers/event.controller.ts @@ -1,4 +1,3 @@ -import { getClientIp } from '@/utils/get-client-ip'; import type { FastifyReply, FastifyRequest } from 'fastify'; import { generateDeviceId, parseUserAgent } from '@openpanel/common/server'; @@ -21,7 +20,7 @@ export async function postEvent( request.timestamp, request.body, ); - const ip = getClientIp(request)!; + const ip = request.clientIp; const ua = request.headers['user-agent']!; const projectId = request.client?.projectId; const headers = getStringHeaders(request.headers); diff --git a/apps/api/src/controllers/misc.controller.ts b/apps/api/src/controllers/misc.controller.ts index e441b484..a2088141 100644 --- a/apps/api/src/controllers/misc.controller.ts +++ b/apps/api/src/controllers/misc.controller.ts @@ -4,9 +4,12 @@ import { parseUrlMeta } from '@/utils/parseUrlMeta'; import type { FastifyReply, FastifyRequest } from 'fastify'; import sharp from 'sharp'; -import { getClientIp } from '@/utils/get-client-ip'; +import { + DEFAULT_HEADER_ORDER, + getClientIpFromHeaders, +} from '@openpanel/common/server/get-client-ip'; import { TABLE_NAMES, ch, chQuery, formatClickhouseDate } from '@openpanel/db'; -import { getGeoLocation } from '@openpanel/geo'; +import { type GeoLocation, getGeoLocation } from '@openpanel/geo'; import { getCache, getRedisCache } from '@openpanel/redis'; interface GetFaviconParams { @@ -394,12 +397,35 @@ export async function stats(request: FastifyRequest, reply: FastifyReply) { } export async function getGeo(request: FastifyRequest, reply: FastifyReply) { - const ip = getClientIp(request); + const ip = getClientIpFromHeaders(request.headers); + const others = await Promise.all( + DEFAULT_HEADER_ORDER.map(async (header) => { + const ip = getClientIpFromHeaders(request.headers, header); + return { + header, + ip, + geo: await getGeoLocation(ip), + }; + }), + ); + if (!ip) { return reply.status(400).send('Bad Request'); } const geo = await getGeoLocation(ip); - return reply.status(200).send(geo); + return reply.status(200).send({ + selected: { + geo, + ip, + }, + ...others.reduce( + (acc, other) => { + acc[other.header] = other; + return acc; + }, + {} as Record, + ), + }); } export async function getOgImage( diff --git a/apps/api/src/controllers/profile.controller.ts b/apps/api/src/controllers/profile.controller.ts index ed11d229..485d4b07 100644 --- a/apps/api/src/controllers/profile.controller.ts +++ b/apps/api/src/controllers/profile.controller.ts @@ -1,4 +1,3 @@ -import { getClientIp } from '@/utils/get-client-ip'; import type { FastifyReply, FastifyRequest } from 'fastify'; import { assocPath, pathOr } from 'ramda'; @@ -22,7 +21,7 @@ export async function updateProfile( if (!projectId) { return reply.status(400).send('No projectId'); } - const ip = getClientIp(request)!; + const ip = request.clientIp; const ua = request.headers['user-agent']!; const uaInfo = parseUserAgent(ua, properties); const geo = await getGeoLocation(ip); diff --git a/apps/api/src/controllers/track.controller.ts b/apps/api/src/controllers/track.controller.ts index b40b10f9..cd445815 100644 --- a/apps/api/src/controllers/track.controller.ts +++ b/apps/api/src/controllers/track.controller.ts @@ -1,4 +1,3 @@ -import { getClientIp } from '@/utils/get-client-ip'; import type { FastifyReply, FastifyRequest } from 'fastify'; import { path, assocPath, pathOr, pick } from 'ramda'; @@ -91,7 +90,7 @@ export async function handler( const timestamp = getTimestamp(request.timestamp, request.body.payload); const ip = path(['properties', '__ip'], request.body.payload) || - getClientIp(request)!; + request.clientIp; const ua = request.headers['user-agent']!; const projectId = request.client?.projectId; diff --git a/apps/api/src/hooks/ip.hook.ts b/apps/api/src/hooks/ip.hook.ts index a1fdcbe6..2e293203 100644 --- a/apps/api/src/hooks/ip.hook.ts +++ b/apps/api/src/hooks/ip.hook.ts @@ -1,13 +1,12 @@ -import { getClientIp } from '@/utils/get-client-ip'; -import type { - FastifyReply, - FastifyRequest, - HookHandlerDoneFunction, -} from 'fastify'; +import { getClientIpFromHeaders } from '@openpanel/common/server/get-client-ip'; +import type { FastifyRequest } from 'fastify'; export async function ipHook(request: FastifyRequest) { - const ip = getClientIp(request); + const ip = getClientIpFromHeaders(request.headers); + if (ip) { request.clientIp = ip; + } else { + request.clientIp = ''; } } diff --git a/apps/api/src/index.ts b/apps/api/src/index.ts index 4d2fd872..897a908a 100644 --- a/apps/api/src/index.ts +++ b/apps/api/src/index.ts @@ -55,7 +55,7 @@ process.env.TZ = 'UTC'; declare module 'fastify' { interface FastifyRequest { client: IServiceClientWithProject | null; - clientIp?: string; + clientIp: string; timestamp?: number; session: SessionValidationResult; } diff --git a/apps/api/src/utils/get-client-ip.ts b/apps/api/src/utils/get-client-ip.ts deleted file mode 100644 index ad4729c8..00000000 --- a/apps/api/src/utils/get-client-ip.ts +++ /dev/null @@ -1,8 +0,0 @@ -import type { FastifyRequest } from 'fastify'; -import requestIp from 'request-ip'; - -const ignore = ['127.0.0.1', '::1']; - -export function getClientIp(req: FastifyRequest) { - return requestIp.getClientIp(req); -} diff --git a/apps/public/app/api/[...op]/route.ts b/apps/public/app/api/[...op]/route.ts new file mode 100644 index 00000000..3660ba62 --- /dev/null +++ b/apps/public/app/api/[...op]/route.ts @@ -0,0 +1,67 @@ +import { createHash } from 'node:crypto'; +import { getClientIpFromHeaders } from '@openpanel/common/server/get-client-ip'; +// adding .js next/script import fixes an issues +// with esm and nextjs (when using pages dir) +import { NextResponse } from 'next/server.js'; + +type CreateNextRouteHandlerOptions = { + apiUrl?: string; +}; + +function createNextRouteHandler(options: CreateNextRouteHandlerOptions) { + return async function POST(req: Request) { + const apiUrl = options.apiUrl ?? 'https://api.openpanel.dev'; + const headers = new Headers(req.headers); + const clientIp = getClientIpFromHeaders(headers); + console.log('debug', { + clientIp, + userAgent: req.headers.get('user-agent'), + }); + try { + const res = await fetch(`${apiUrl}/track`, { + method: 'POST', + headers, + body: JSON.stringify(await req.json()), + }); + return NextResponse.json(await res.text(), { status: res.status }); + } catch (e) { + return NextResponse.json(e); + } + }; +} + +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, { + 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 }, + ); + } + }; +} + +export const POST = createNextRouteHandler({}); +export const GET = createScriptHandler(); diff --git a/apps/public/app/layout.tsx b/apps/public/app/layout.tsx index 3d697b66..94f9f651 100644 --- a/apps/public/app/layout.tsx +++ b/apps/public/app/layout.tsx @@ -61,12 +61,9 @@ export default async function Layout({ children }: { children: ReactNode }) { {children} -