Skip to content

Commit de7186c

Browse files
committed
x-forwarded-for to handle IP behind procies
1 parent e392d31 commit de7186c

File tree

1 file changed

+79
-18
lines changed

1 file changed

+79
-18
lines changed

src/middlewares/metrics/index.js

Lines changed: 79 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,69 @@ function normalizePath(urlPath, matchers) {
107107
return { route, params: {} };
108108
}
109109

110+
// Prefer proxy-provided client IPs when available.
111+
function sanitizeIp(rawIp) {
112+
if (!rawIp) return '';
113+
114+
let ip = String(rawIp).trim();
115+
if (!ip) return '';
116+
117+
// Handle IPv4-mapped IPv6 values (e.g. ::ffff:192.168.0.1).
118+
if (ip.startsWith('::ffff:')) {
119+
ip = ip.slice('::ffff:'.length);
120+
}
121+
122+
// Handle bracketed IPv6 with port (e.g. [2001:db8::1]:12345).
123+
const bracketedIpv6 = ip.match(/^\[([^\]]+)\](?::\d+)?$/);
124+
if (bracketedIpv6) {
125+
ip = bracketedIpv6[1];
126+
}
127+
128+
// Handle IPv4 with port (e.g. 192.168.0.1:12345).
129+
const ipv4WithPort = ip.match(/^(\d{1,3}(?:\.\d{1,3}){3}):\d+$/);
130+
if (ipv4WithPort) {
131+
ip = ipv4WithPort[1];
132+
}
133+
134+
return ip;
135+
}
136+
137+
function getClientIp(req) {
138+
const xForwardedFor = req.headers && req.headers['x-forwarded-for'];
139+
140+
if (typeof xForwardedFor === 'string' && xForwardedFor.trim()) {
141+
// X-Forwarded-For can be a list: client, proxy1, proxy2
142+
return sanitizeIp(xForwardedFor.split(',')[0]);
143+
}
144+
145+
if (Array.isArray(xForwardedFor) && xForwardedFor.length > 0) {
146+
return sanitizeIp(String(xForwardedFor[0]).split(',')[0]);
147+
}
148+
149+
return sanitizeIp(
150+
req.ip
151+
|| (req.socket && req.socket.remoteAddress)
152+
|| (req.connection && req.connection.remoteAddress)
153+
|| ''
154+
);
155+
}
156+
157+
function anonymizeIp(ip) {
158+
if (!ip) return 'Unknown';
159+
160+
if (ip.includes('.')) {
161+
const octets = ip.split('.');
162+
if (octets.length >= 2) return `${octets[0]}.${octets[1]}.0.0`;
163+
}
164+
165+
if (ip.includes(':')) {
166+
const parts = ip.split(':').filter(Boolean);
167+
if (parts.length >= 2) return `${parts[0]}:${parts[1]}::`;
168+
}
169+
170+
return 'Unknown';
171+
}
172+
110173
// ---------------------------------------------------------------------------
111174
// Public API
112175
// ---------------------------------------------------------------------------
@@ -121,29 +184,26 @@ function metricsMiddleware(basePaths = ['/rest/current', '/rest/v1'], debug = fa
121184

122185
return function trackMetrics(req, res, next) {
123186
const startMs = Date.now();
124-
125-
// IP and Geolocation logic
126-
const ip = req.ip || (req.connection && req.connection.remoteAddress) || '';
127-
if (debug) console.log('ip', ip);
128-
const geo = geoip.lookup(ip);
129-
if (debug) console.log('geo', geo);
130-
131-
req.geoStats = {
132-
country: geo ? geo.country : 'Unknown',
133-
region: geo ? geo.region : 'Unknown',
134-
city: geo ? geo.city : 'Unknown',
135-
// Anonymize IP by keeping only the first two octets (e.g. 192.168.x.x)
136-
// This way we can still get some geographic info without needing consent
137-
anonIp: ip.length > 0 ? ip.split('.').slice(0, 2).join('.') + '.0.0' : 'Unknown'
138-
};
139-
if (debug) console.log('ip', req.geoStats);
140-
141187
// Capture the full path NOW — req.path is mutated by Express after sub-router
142188
// dispatch, but req.originalUrl is always the original unmodified path.
143189
const fullPath = req.originalUrl.split('?')[0];
144190
const isFaviconRequest = fullPath.includes('favicon');
145191
const print = debug && !isFaviconRequest;
146192
if (print) console.log(`Received request: ${req.method} ${fullPath}, path ${req.path}, url ${req.url}`)
193+
194+
// IP and Geolocation logic
195+
const ip = getClientIp(req);
196+
if (debug) console.log('Client IP', ip);
197+
const geo = geoip.lookup(ip);
198+
if (debug) console.log('Geo Data', geo);
199+
200+
req.geoStats = {
201+
country: geo ? geo.country : '',
202+
region: geo ? geo.region : '',
203+
city: geo ? geo.city : '',
204+
// Keep a coarse-grained anonymized IP in labels.
205+
anonIp: anonymizeIp(ip)
206+
};
147207

148208
res.on('finish', () => {
149209
const { route, params } = normalizePath(fullPath, matchers);
@@ -158,7 +218,8 @@ function metricsMiddleware(basePaths = ['/rest/current', '/rest/v1'], debug = fa
158218
status_code: String(res.statusCode),
159219
...params
160220
};
161-
if (print) console.log('labels', labels);
221+
if (print) console.log('Labels:', labels);
222+
if (print) console.log('geoStats', req.geoStats);
162223
httpRequestsTotal.inc(labels);
163224
httpRequestDuration.observe(labels, (Date.now() - startMs) / 1000);
164225
httpGeoRequestsTotal.inc({

0 commit comments

Comments
 (0)