Blame Raw
Cohee · 51ad27fb · · 127 lines (4.9 KB)
1 contributor
1import ipaddr from 'ipaddr.js';
2import ipMatching from 'ip-matching';
3import { RateLimiterRes } from 'rate-limiter-flexible';
4import { getConfigValue } from './util.js';
5
6const noopMiddleware = (_req, _res, next) => next();
7/** @deprecated Do not use. A global middleware is provided at the application level. */
8export const jsonParser = noopMiddleware;
9/** @deprecated Do not use. A global middleware is provided at the application level. */
10export const urlencodedParser = noopMiddleware;
11
12/**
13 * Gets the IP address of the client from the request object.
14 * @param {import('express').Request} req Request object
15 * @returns {string} IP address of the client
16 */
17export function getIpFromRequest(req) {
18 let clientIp = req.socket.remoteAddress;
19 if (!clientIp) {
20 return 'unknown';
21 }
22 let ip = ipaddr.parse(clientIp);
23 // Check if the IP address is IPv4-mapped IPv6 address
24 if (ip.kind() === 'ipv6' && ip instanceof ipaddr.IPv6 && ip.isIPv4MappedAddress()) {
25 const ipv4 = ip.toIPv4Address().toString();
26 clientIp = ipv4;
27 } else {
28 clientIp = ip.toString();
29 }
30 return clientIp;
31}
32
33/**
34 * Get the client IP address from the request headers.
35 * @param {import('express').Request} req Express request object
36 * @returns {string|undefined} The client IP address
37 */
38export function getRealOrForwardedIp(req) {
39 const xRealIpEnabled = !!getConfigValue('forwardedHeaders.xRealIp', true, 'boolean');
40 const cfConnectingIpEnabled = !!getConfigValue('forwardedHeaders.cfConnectingIp', false, 'boolean');
41 const xForwardedForEnabled = !!getConfigValue('forwardedHeaders.xForwardedFor', true, 'boolean');
42
43 // Check if X-Real-IP is available
44 if (req.headers['x-real-ip'] && xRealIpEnabled) {
45 return req.headers['x-real-ip'].toString();
46 }
47
48 // Check for CF-Connecting-IP (Cloudflare) if available
49 if (req.headers['cf-connecting-ip'] && cfConnectingIpEnabled) {
50 return req.headers['cf-connecting-ip'].toString();
51 }
52
53 // Check for X-Forwarded-For and parse if available
54 if (req.headers['x-forwarded-for'] && xForwardedForEnabled) {
55 const ipList = req.headers['x-forwarded-for'].toString().split(',').map(ip => ip.trim());
56 return ipList[0];
57 }
58
59 // If none of the headers are available, return undefined
60 return undefined;
61}
62
63/**
64 * Gets the IP address of the client, optionally including the real/forwarded IP from headers.
65 * Most common use cases: key for rate limiter, logging, etc. where you want to have the real client IP if behind a reverse proxy.
66 * @param {import('express').Request} request Request object
67 * @param {boolean} includeHeaderIp Whether to include the real/forwarded IP from headers
68 * @returns {string} IP address of the client (will include "forwarded" info if includeHeaderIp is true and headers are present)
69 */
70export function getIpAddress(request, includeHeaderIp) {
71 const socketIp = getIpFromRequest(request);
72 const forwardedIp = includeHeaderIp && getRealOrForwardedIp(request);
73 return forwardedIp ? `${socketIp} (forwarded: ${forwardedIp})` : socketIp;
74}
75
76/**
77 * Checks if the request is coming from a Firefox browser.
78 * @param {import('express').Request} req Request object
79 * @returns {boolean} True if the request is from Firefox, false otherwise.
80 */
81export function isFirefox(req) {
82 const userAgent = req.headers['user-agent'] || '';
83 return /firefox/i.test(userAgent);
84}
85
86/**
87 * Filters and validates IP patterns.
88 * @param {string[]} entries - The list of IP patterns to validate
89 * @param {(entry: string, message: string) => string} formatLog - The function to format the warning message for invalid entries
90 * @returns {string[]} The list of valid IP patterns
91 */
92export function filterValidIpPatterns(entries, formatLog) {
93 const validEntries = [];
94
95 if (!Array.isArray(entries)) {
96 return validEntries;
97 }
98
99 for (const entry of entries) {
100 try {
101 // This will throw if the entry is not a valid IP or CIDR
102 ipMatching.getMatch(entry);
103 validEntries.push(entry);
104 } catch (e) {
105 if (typeof formatLog === 'function') {
106 console.warn(formatLog(entry, e?.message || 'Unknown error'));
107 }
108 }
109 }
110
111 return validEntries;
112}
113
114/**
115 * Sets the Retry-After header on the response based on the rate limit information.
116 * @param {import('express').Response} response Express response object
117 * @param {RateLimiterRes} rateLimit The rate limit information from rate-limiter-flexible
118 * @returns {import('express').Response} The response object with the Retry-After header set if applicable
119 */
120export function retryAfter(response, rateLimit) {
121 if (response.headersSent || !(rateLimit instanceof RateLimiterRes)) {
122 return response;
123 }
124 const retryAfter = Math.ceil(rateLimit.msBeforeNext / 1000);
125 response.set('Retry-After', retryAfter.toString());
126 return response;
127}