-
Notifications
You must be signed in to change notification settings - Fork 7.1k
perf: ip check #6850
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
perf: ip check #6850
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,230 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import type { IncomingHttpHeaders, IncomingMessage } from 'http'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import ipaddr from 'ipaddr.js'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import proxyaddr from 'proxy-addr'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { env } from '../../env'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| type IPAddress = ipaddr.IPv4 | ipaddr.IPv6; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| type RequestWithClientIp = { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| headers?: IncomingHttpHeaders; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| socket?: { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| remoteAddress?: string | null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| connection?: { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| remoteAddress?: string | null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| type TrustProxyFn = (addr: string, i: number) => boolean; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const MAX_FORWARDED_FOR_LENGTH = 2048; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const MAX_FORWARDED_FOR_HOPS = 32; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let cachedTrustedProxyIpEnv: string | undefined; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let cachedNodeEnv: string | undefined; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let cachedTrustProxyFn: TrustProxyFn = proxyaddr.compile([]); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let warnedInvalidTrustedProxyIpEnv: string | undefined; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // 不区分大小写读取 header 值;数组类型(如 set-cookie 风格)合并为逗号分隔字符串。 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const getHeaderValue = (headers: IncomingHttpHeaders | undefined, key: string) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const value = | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| headers?.[key] ?? | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Object.entries(headers ?? {}).find(([headerKey]) => headerKey.toLowerCase() === key)?.[1]; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (Array.isArray(value)) return value.join(','); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return value; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // 剥离 IP 字符串外层的引号、IPv6 方括号以及 IPv4/IPv6 末尾的端口,返回纯地址。 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const stripIpWrapper = (rawIp: string) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const ip = rawIp.trim().replace(/^"(.+)"$/, '$1'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const bracketedIpv6 = ip.match(/^\[([^\]]+)](?::\d+)?$/); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (bracketedIpv6?.[1]) return bracketedIpv6[1]; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const ipv4WithPort = ip.match(/^(\d{1,3}(?:\.\d{1,3}){3})(?::\d+)?$/); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (ipv4WithPort?.[1]) return ipv4WithPort[1]; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return ip; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // 将原始字符串解析为 ipaddr.js 的地址对象;非法或为空时返回 null,内部走 ipaddr.process 以归一 IPv4-mapped IPv6。 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const parseIpAddress = (rawIp?: string | null): IPAddress | null => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!rawIp) return null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const ip = stripIpWrapper(rawIp); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!ipaddr.isValid(ip)) return null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return ipaddr.process(ip); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // 校验单条 TRUSTED_PROXY_IPS 配置项是否为合法的 IP 或 CIDR(校验掩码长度与地址族匹配)。 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const isValidTrustedProxyAddress = (rawValue: string) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const addressParts = rawValue.trim().split('/'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (addressParts.length > 2) return false; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const [rawAddress, rawPrefixLength] = addressParts; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const address = parseIpAddress(rawAddress); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!address) return false; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (rawPrefixLength === undefined) return true; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const prefixLength = Number(rawPrefixLength); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const maxLength = address.kind() === 'ipv4' ? 32 : 128; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return Number.isInteger(prefixLength) && prefixLength > 0 && prefixLength <= maxLength; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // 按逗号/空白拆分 TRUSTED_PROXY_IPS,过滤非法项并去重打印一次警告;非 test 环境下提示运维。 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const parseTrustedProxyIpEnv = (trustedProxyIpEnv?: string) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const validAddresses: string[] = []; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const invalidAddresses: string[] = []; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| (trustedProxyIpEnv ?? '') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .split(/[,\s]+/) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .filter(Boolean) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .forEach((item) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (isValidTrustedProxyAddress(item)) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| validAddresses.push(item); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| invalidAddresses.push(item); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const validAddresses: string[] = []; | |
| const invalidAddresses: string[] = []; | |
| (trustedProxyIpEnv ?? '') | |
| .split(/[,\s]+/) | |
| .filter(Boolean) | |
| .forEach((item) => { | |
| if (isValidTrustedProxyAddress(item)) { | |
| validAddresses.push(item); | |
| } else { | |
| invalidAddresses.push(item); | |
| } | |
| }); | |
| const validAddressSet = new Set<string>(); | |
| const invalidAddressSet = new Set<string>(); | |
| (trustedProxyIpEnv ?? '') | |
| .split(/[,\s]+/) | |
| .filter(Boolean) | |
| .forEach((item) => { | |
| if (isValidTrustedProxyAddress(item)) { | |
| validAddressSet.add(item); | |
| } else { | |
| invalidAddressSet.add(item); | |
| } | |
| }); | |
| const validAddresses = Array.from(validAddressSet); | |
| const invalidAddresses = Array.from(invalidAddressSet); |
Copilot
AI
Apr 29, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
getTrustProxyFn can return the default cachedTrustProxyFn compiled with [] without ever compiling the intended default trust list when both process.env.NODE_ENV and env.TRUSTED_PROXY_IPS are undefined on the first call (because the cache keys also start as undefined).
This breaks the documented behavior (“non-production defaults to trusting loopback”) when NODE_ENV isn’t set. Consider initializing the cache keys to a sentinel value (or null) so the first call always compiles, or initializing cachedTrustProxyFn by calling getTrustProxyFn() lazily instead of compiling [] up front.
Copilot
AI
Apr 29, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
createProxyAddrRequest’s comment says it “仅保留…XFF…避免外部 header 干扰判定”, but the implementation spreads all original headers into the new request.
To match the intent, consider only setting the single validated x-forwarded-for header (and omitting the spread), or adjust the comment if keeping other headers is intentional.
| ...(req.headers ?? {}), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The early-return condition disables IP frequency limiting in almost all cases:
if (!env.CHECK_INTERNAL_IP || !force) return;will return wheneverforceis false (the default), and it also ties rate limiting toCHECK_INTERNAL_IPrather than the IP-limit flag.This looks like a regression from the previous
USE_IP_LIMIT/forcesemantics. Consider switching back to a guard like “skip only when IP limiting is disabled AND not forced” (usingenv.USE_IP_LIMIT), so forced call sites (e.g. password login) still work while respecting the runtime toggle.