proteum 2.2.2 → 2.2.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -17,6 +17,7 @@ import type { HttpMethod, HttpHeaders } from '..';
17
17
  import ApiClient from './api';
18
18
  import ServerResponse from '../response';
19
19
  import type { TAnyRouter } from '..';
20
+ import { resolveRequestIp } from './ip';
20
21
 
21
22
  /*----------------------------------
22
23
  - TYPES
@@ -108,7 +109,7 @@ export default class ServerRequest<TRouter extends TAnyRouter = TAnyRouter> exte
108
109
  this.domain = res.req.hostname;
109
110
  this.cookies = res.req.cookies;
110
111
 
111
- this.ip = res.req.ip;
112
+ this.ip = resolveRequestIp(res.req);
112
113
 
113
114
  this.data = data || {};
114
115
  }
@@ -0,0 +1,60 @@
1
+ const assert = require('node:assert/strict');
2
+ const test = require('node:test');
3
+
4
+ const { resolveRequestIp } = require('./ip.ts');
5
+
6
+ const request = (headers, ip = '192.0.2.10') => ({
7
+ headers,
8
+ ip,
9
+ });
10
+
11
+ test('Cloudflare client IP wins over Express fallback IP', () => {
12
+ assert.equal(
13
+ resolveRequestIp(request({ 'cf-connecting-ip': '203.0.113.10' }, '192.0.2.10')),
14
+ '203.0.113.10',
15
+ );
16
+ });
17
+
18
+ test('True-Client-IP wins when Cloudflare IP is absent', () => {
19
+ assert.equal(
20
+ resolveRequestIp(request({ 'true-client-ip': '198.51.100.24' }, '192.0.2.10')),
21
+ '198.51.100.24',
22
+ );
23
+ });
24
+
25
+ test('Fastly-Client-IP wins when Cloudflare and True-Client-IP are absent', () => {
26
+ assert.equal(
27
+ resolveRequestIp(request({ 'fastly-client-ip': '51.182.144.154' }, '192.0.2.10')),
28
+ '51.182.144.154',
29
+ );
30
+ });
31
+
32
+ test('invalid and spoof-looking values are ignored before falling back to Express IP', () => {
33
+ assert.equal(
34
+ resolveRequestIp(
35
+ request(
36
+ {
37
+ 'cf-connecting-ip': 'not-an-ip',
38
+ 'true-client-ip': '203.0.113.10, 198.51.100.24',
39
+ 'fastly-client-ip': 'unknown',
40
+ 'x-forwarded-for': '203.0.113.200',
41
+ forwarded: 'for=203.0.113.201',
42
+ },
43
+ '192.0.2.10',
44
+ ),
45
+ ),
46
+ '192.0.2.10',
47
+ );
48
+ });
49
+
50
+ test('IPv4-mapped IPv6 and bracketed IPv6 candidates normalize correctly', () => {
51
+ assert.equal(
52
+ resolveRequestIp(request({ 'fastly-client-ip': ' ::ffff:51.182.144.154 ' }, '192.0.2.10')),
53
+ '51.182.144.154',
54
+ );
55
+ assert.equal(resolveRequestIp(request({ 'true-client-ip': '[2001:db8::12]' }, '192.0.2.10')), '2001:db8::12');
56
+ });
57
+
58
+ test('IPv4 port suffix candidates normalize correctly', () => {
59
+ assert.equal(resolveRequestIp(request({ 'fastly-client-ip': '51.182.144.154:443' }, '192.0.2.10')), '51.182.144.154');
60
+ });
@@ -0,0 +1,71 @@
1
+ /*----------------------------------
2
+ - DEPENDANCES
3
+ ----------------------------------*/
4
+
5
+ // Npm
6
+ import type express from 'express';
7
+ import { isIP } from 'net';
8
+
9
+ /*----------------------------------
10
+ - TYPES
11
+ ----------------------------------*/
12
+
13
+ type THeaderValue = string | string[] | undefined;
14
+
15
+ /*----------------------------------
16
+ - CONSTANTS
17
+ ----------------------------------*/
18
+
19
+ const trustedClientIpHeaders = ['cf-connecting-ip', 'true-client-ip', 'fastly-client-ip'] as const;
20
+ const bracketedIpPattern = /^\[([^\]]+)\](?::\d+)?$/;
21
+ const ipv4WithPortPattern = /^(\d{1,3}(?:\.\d{1,3}){3}):\d+$/;
22
+ const ipv4MappedIpv6Pattern = /^::ffff:(\d{1,3}(?:\.\d{1,3}){3})$/i;
23
+
24
+ /*----------------------------------
25
+ - HELPERS
26
+ ----------------------------------*/
27
+
28
+ const readHeader = (headers: Record<string, THeaderValue>, name: string): string | null => {
29
+ const normalizedName = name.toLowerCase();
30
+ const directValue = headers[name] ?? headers[normalizedName] ?? headers[name.toUpperCase()];
31
+ const value =
32
+ directValue !== undefined
33
+ ? directValue
34
+ : Object.entries(headers).find(([key]) => key.toLowerCase() === normalizedName)?.[1];
35
+
36
+ if (Array.isArray(value)) {
37
+ const firstValue = value.find((entry) => typeof entry === 'string' && entry.trim());
38
+ return typeof firstValue === 'string' ? firstValue.trim() : null;
39
+ }
40
+
41
+ return typeof value === 'string' && value.trim() ? value.trim() : null;
42
+ };
43
+
44
+ export const normalizeRequestIpCandidate = (value: string | null | undefined): string | null => {
45
+ let candidate = typeof value === 'string' ? value.trim() : '';
46
+ if (!candidate) return null;
47
+
48
+ const bracketedIp = bracketedIpPattern.exec(candidate);
49
+ if (bracketedIp) candidate = bracketedIp[1].trim();
50
+
51
+ const ipv4MappedIpv6 = ipv4MappedIpv6Pattern.exec(candidate);
52
+ if (ipv4MappedIpv6 && isIP(ipv4MappedIpv6[1]) === 4) {
53
+ candidate = ipv4MappedIpv6[1];
54
+ } else {
55
+ const ipv4WithPort = ipv4WithPortPattern.exec(candidate);
56
+ if (ipv4WithPort && isIP(ipv4WithPort[1]) === 4) candidate = ipv4WithPort[1];
57
+ }
58
+
59
+ return isIP(candidate) ? candidate : null;
60
+ };
61
+
62
+ export const resolveRequestIp = (req: Pick<express.Request, 'headers' | 'ip'>): string | undefined => {
63
+ const headers = req.headers as Record<string, THeaderValue>;
64
+
65
+ for (const headerName of trustedClientIpHeaders) {
66
+ const ip = normalizeRequestIpCandidate(readHeader(headers, headerName));
67
+ if (ip) return ip;
68
+ }
69
+
70
+ return normalizeRequestIpCandidate(req.ip) || undefined;
71
+ };