| 1 | import ipaddr from 'ipaddr.js'; |
| 2 | import ipMatching from 'ip-matching'; |
| 3 | import { RateLimiterRes } from 'rate-limiter-flexible'; |
| 4 | import { getConfigValue } from './util.js'; |
| 5 | |
| 6 | const noopMiddleware = (_req, _res, next) => next(); |
| 7 | /** @deprecated Do not use. A global middleware is provided at the application level. */ |
| 8 | export const jsonParser = noopMiddleware; |
| 9 | /** @deprecated Do not use. A global middleware is provided at the application level. */ |
| 10 | export 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 | */ |
| 17 | export 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 | */ |
| 38 | export 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 | */ |
| 70 | export 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 | */ |
| 81 | export 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 | */ |
| 92 | export 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 | */ |
| 120 | export 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 | } |