Преглед изворни кода

feat: implement client IP extraction utility and update AdClickInput structure

Dave пре 1 месец
родитељ
комит
62274645d8

+ 3 - 3
apps/box-stats-api/src/feature/stats-events/ads-stats.controller.ts

@@ -19,8 +19,8 @@ import {
 } from '@nestjs/swagger';
 import { Request } from 'express';
 import { StatsAdClickPublisherService } from './stats-ad-click.publisher.service';
-import { ApiPropertyOptional } from '@nestjs/swagger';
-import { IsOptional, IsNumber, IsString } from 'class-validator';
+import { extractClientIp } from '../../utils/client-ip.util';
+import { IsString } from 'class-validator';
 
 export class AdClickRequestDto {
   @ApiProperty({ description: '用户唯一设备ID', example: 'xxxxxx' })
@@ -87,7 +87,7 @@ export class AdsStatsController {
       channelId: body.channelId,
       adsId: body.adsId,
       headers: req.headers,
-      ipFallback: req.ip, // ✅ raw fallback only
+      clientIp: extractClientIp(req),
     });
 
     return { ok: true };

+ 2 - 15
apps/box-stats-api/src/feature/stats-events/stats-ad-click.publisher.service.ts

@@ -15,7 +15,7 @@ export interface AdClickInput {
   channelId: string;
   adsId: string; // ✅ required
   headers: IncomingHttpHeaders;
-  ipFallback?: string;
+  clientIp?: string;
 }
 
 @Injectable()
@@ -79,11 +79,7 @@ export class StatsAdClickPublisherService
     const sentAtSec = nowSec;
 
     const headers = input.headers;
-    const ip =
-      (headers['cf-connecting-ip'] as string | undefined) ||
-      this.extractForwardedFor(headers) ||
-      input.ipFallback ||
-      'unknown';
+    const ip = input.clientIp || 'unknown';
 
     const machine =
       (headers['x-machine'] as string | undefined) ||
@@ -109,15 +105,6 @@ export class StatsAdClickPublisherService
     });
   }
 
-  private extractForwardedFor(
-    headers?: IncomingHttpHeaders,
-  ): string | undefined {
-    const header = headers?.['x-forwarded-for'];
-    if (!header) return undefined;
-    const value = Array.isArray(header) ? header[0] : header;
-    return value.split(',')[0].trim() || undefined;
-  }
-
   async publish(payload: unknown): Promise<void> {
     if (!this.channel) {
       throw new Error('RabbitMQ channel not ready for stats.ad.click');

+ 74 - 0
apps/box-stats-api/src/utils/client-ip.util.ts

@@ -0,0 +1,74 @@
+import { Request } from 'express';
+
+const IPV4_REGEX = /^(?:\d{1,3}\.){3}\d{1,3}$/;
+
+const normalizeIpValue = (value: string): string | null => {
+  if (!value) return null;
+  const trimmed = value.trim();
+  if (!trimmed) return null;
+
+  const lower = trimmed.toLowerCase();
+  if (lower.startsWith('::ffff:')) {
+    const candidate = lower.slice(7);
+    if (IPV4_REGEX.test(candidate)) {
+      return candidate;
+    }
+  }
+
+  return trimmed;
+};
+
+const isIpv4 = (value: string): boolean => {
+  if (!value) return false;
+  if (!IPV4_REGEX.test(value)) return false;
+  return value.split('.').every((part) => Number.parseInt(part, 10) <= 255);
+};
+
+const extractHeaderIps = (value?: string | string[]): string[] => {
+  if (!value) return [];
+  const raw = Array.isArray(value) ? value.join(',') : value;
+  return raw
+    .split(',')
+    .map((entry) => entry.trim())
+    .filter(Boolean);
+};
+
+export function extractClientIp(req: Request): string {
+  const headersToCheck = [
+    'cf-connecting-ip',
+    'x-forwarded-for',
+    'x-real-ip',
+  ] as const;
+
+  let firstIpv4: string | undefined;
+  let fallbackIp: string | undefined;
+
+  for (const header of headersToCheck) {
+    const entries = extractHeaderIps(req.headers[header]);
+    for (const entry of entries) {
+      const normalized = normalizeIpValue(entry);
+      if (!normalized) continue;
+      if (!fallbackIp) {
+        fallbackIp = normalized;
+      }
+      if (!firstIpv4 && isIpv4(normalized)) {
+        firstIpv4 = normalized;
+        break;
+      }
+    }
+    if (firstIpv4) {
+      break;
+    }
+  }
+
+  const socketIp =
+    req.socket?.remoteAddress && normalizeIpValue(req.socket.remoteAddress);
+  if (!fallbackIp && socketIp) {
+    fallbackIp = socketIp;
+  }
+  if (!firstIpv4 && socketIp && isIpv4(socketIp)) {
+    firstIpv4 = socketIp;
+  }
+
+  return firstIpv4 ?? fallbackIp ?? '';
+}