nstarter-http-request 0.1.0

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.
Files changed (263) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/README.md +160 -0
  3. package/dist/cjs/adapter/axios.adapter.js +224 -0
  4. package/dist/cjs/adapter/axios.adapter.js.map +1 -0
  5. package/dist/cjs/adapter/base.js +134 -0
  6. package/dist/cjs/adapter/base.js.map +1 -0
  7. package/dist/cjs/adapter/index.js +20 -0
  8. package/dist/cjs/adapter/index.js.map +1 -0
  9. package/dist/cjs/adapter/undici.adapter.js +272 -0
  10. package/dist/cjs/adapter/undici.adapter.js.map +1 -0
  11. package/dist/cjs/client.js +409 -0
  12. package/dist/cjs/client.js.map +1 -0
  13. package/dist/cjs/config.js +69 -0
  14. package/dist/cjs/config.js.map +1 -0
  15. package/dist/cjs/const/config.const.js +53 -0
  16. package/dist/cjs/const/config.const.js.map +1 -0
  17. package/dist/cjs/const/dns.const.js +16 -0
  18. package/dist/cjs/const/dns.const.js.map +1 -0
  19. package/dist/cjs/const/enum.const.js +22 -0
  20. package/dist/cjs/const/enum.const.js.map +1 -0
  21. package/dist/cjs/const/index.js +21 -0
  22. package/dist/cjs/const/index.js.map +1 -0
  23. package/dist/cjs/const/ip.const.js +110 -0
  24. package/dist/cjs/const/ip.const.js.map +1 -0
  25. package/dist/cjs/index.js +25 -0
  26. package/dist/cjs/index.js.map +1 -0
  27. package/dist/cjs/logger/base.js +60 -0
  28. package/dist/cjs/logger/base.js.map +1 -0
  29. package/dist/cjs/logger/http_client.logger.js +209 -0
  30. package/dist/cjs/logger/http_client.logger.js.map +1 -0
  31. package/dist/cjs/logger/index.js +20 -0
  32. package/dist/cjs/logger/index.js.map +1 -0
  33. package/dist/cjs/logger/log.filter.js +126 -0
  34. package/dist/cjs/logger/log.filter.js.map +1 -0
  35. package/dist/cjs/security/dns.validator.js +137 -0
  36. package/dist/cjs/security/dns.validator.js.map +1 -0
  37. package/dist/cjs/security/index.js +21 -0
  38. package/dist/cjs/security/index.js.map +1 -0
  39. package/dist/cjs/security/ip.validator.js +107 -0
  40. package/dist/cjs/security/ip.validator.js.map +1 -0
  41. package/dist/cjs/security/ssrf.guard.js +180 -0
  42. package/dist/cjs/security/ssrf.guard.js.map +1 -0
  43. package/dist/cjs/security/url.validator.js +170 -0
  44. package/dist/cjs/security/url.validator.js.map +1 -0
  45. package/dist/cjs/tsconfig.tsbuildinfo +1 -0
  46. package/dist/cjs/types/adapter.js +6 -0
  47. package/dist/cjs/types/adapter.js.map +1 -0
  48. package/dist/cjs/types/client.js +6 -0
  49. package/dist/cjs/types/client.js.map +1 -0
  50. package/dist/cjs/types/config.js +6 -0
  51. package/dist/cjs/types/config.js.map +1 -0
  52. package/dist/cjs/types/errors.js +35 -0
  53. package/dist/cjs/types/errors.js.map +1 -0
  54. package/dist/cjs/types/index.js +35 -0
  55. package/dist/cjs/types/index.js.map +1 -0
  56. package/dist/cjs/types/ip.js +6 -0
  57. package/dist/cjs/types/ip.js.map +1 -0
  58. package/dist/cjs/types/logger.js +6 -0
  59. package/dist/cjs/types/logger.js.map +1 -0
  60. package/dist/cjs/types/request_response.js +6 -0
  61. package/dist/cjs/types/request_response.js.map +1 -0
  62. package/dist/cjs/types/security.js +6 -0
  63. package/dist/cjs/types/security.js.map +1 -0
  64. package/dist/cjs/types/trace.js +14 -0
  65. package/dist/cjs/types/trace.js.map +1 -0
  66. package/dist/cjs/utils/common.js +31 -0
  67. package/dist/cjs/utils/common.js.map +1 -0
  68. package/dist/cjs/utils/domain.js +79 -0
  69. package/dist/cjs/utils/domain.js.map +1 -0
  70. package/dist/cjs/utils/index.js +44 -0
  71. package/dist/cjs/utils/index.js.map +1 -0
  72. package/dist/cjs/utils/ip.range.js +200 -0
  73. package/dist/cjs/utils/ip.range.js.map +1 -0
  74. package/dist/cjs/utils/trace.context.js +213 -0
  75. package/dist/cjs/utils/trace.context.js.map +1 -0
  76. package/dist/esm/adapter/axios.adapter.js +184 -0
  77. package/dist/esm/adapter/axios.adapter.js.map +1 -0
  78. package/dist/esm/adapter/base.js +130 -0
  79. package/dist/esm/adapter/base.js.map +1 -0
  80. package/dist/esm/adapter/index.js +4 -0
  81. package/dist/esm/adapter/index.js.map +1 -0
  82. package/dist/esm/adapter/undici.adapter.js +235 -0
  83. package/dist/esm/adapter/undici.adapter.js.map +1 -0
  84. package/dist/esm/client.js +405 -0
  85. package/dist/esm/client.js.map +1 -0
  86. package/dist/esm/config.js +65 -0
  87. package/dist/esm/config.js.map +1 -0
  88. package/dist/esm/const/config.const.js +50 -0
  89. package/dist/esm/const/config.const.js.map +1 -0
  90. package/dist/esm/const/dns.const.js +13 -0
  91. package/dist/esm/const/dns.const.js.map +1 -0
  92. package/dist/esm/const/enum.const.js +19 -0
  93. package/dist/esm/const/enum.const.js.map +1 -0
  94. package/dist/esm/const/index.js +5 -0
  95. package/dist/esm/const/index.js.map +1 -0
  96. package/dist/esm/const/ip.const.js +107 -0
  97. package/dist/esm/const/ip.const.js.map +1 -0
  98. package/dist/esm/index.js +9 -0
  99. package/dist/esm/index.js.map +1 -0
  100. package/dist/esm/logger/base.js +55 -0
  101. package/dist/esm/logger/base.js.map +1 -0
  102. package/dist/esm/logger/http_client.logger.js +205 -0
  103. package/dist/esm/logger/http_client.logger.js.map +1 -0
  104. package/dist/esm/logger/index.js +4 -0
  105. package/dist/esm/logger/index.js.map +1 -0
  106. package/dist/esm/logger/log.filter.js +122 -0
  107. package/dist/esm/logger/log.filter.js.map +1 -0
  108. package/dist/esm/security/dns.validator.js +133 -0
  109. package/dist/esm/security/dns.validator.js.map +1 -0
  110. package/dist/esm/security/index.js +5 -0
  111. package/dist/esm/security/index.js.map +1 -0
  112. package/dist/esm/security/ip.validator.js +103 -0
  113. package/dist/esm/security/ip.validator.js.map +1 -0
  114. package/dist/esm/security/ssrf.guard.js +176 -0
  115. package/dist/esm/security/ssrf.guard.js.map +1 -0
  116. package/dist/esm/security/url.validator.js +166 -0
  117. package/dist/esm/security/url.validator.js.map +1 -0
  118. package/dist/esm/tsconfig.esm.tsbuildinfo +1 -0
  119. package/dist/esm/types/adapter.js +5 -0
  120. package/dist/esm/types/adapter.js.map +1 -0
  121. package/dist/esm/types/client.js +5 -0
  122. package/dist/esm/types/client.js.map +1 -0
  123. package/dist/esm/types/config.js +5 -0
  124. package/dist/esm/types/config.js.map +1 -0
  125. package/dist/esm/types/errors.js +30 -0
  126. package/dist/esm/types/errors.js.map +1 -0
  127. package/dist/esm/types/index.js +19 -0
  128. package/dist/esm/types/index.js.map +1 -0
  129. package/dist/esm/types/ip.js +5 -0
  130. package/dist/esm/types/ip.js.map +1 -0
  131. package/dist/esm/types/logger.js +5 -0
  132. package/dist/esm/types/logger.js.map +1 -0
  133. package/dist/esm/types/request_response.js +5 -0
  134. package/dist/esm/types/request_response.js.map +1 -0
  135. package/dist/esm/types/security.js +5 -0
  136. package/dist/esm/types/security.js.map +1 -0
  137. package/dist/esm/types/trace.js +11 -0
  138. package/dist/esm/types/trace.js.map +1 -0
  139. package/dist/esm/utils/common.js +27 -0
  140. package/dist/esm/utils/common.js.map +1 -0
  141. package/dist/esm/utils/domain.js +71 -0
  142. package/dist/esm/utils/domain.js.map +1 -0
  143. package/dist/esm/utils/index.js +7 -0
  144. package/dist/esm/utils/index.js.map +1 -0
  145. package/dist/esm/utils/ip.range.js +187 -0
  146. package/dist/esm/utils/ip.range.js.map +1 -0
  147. package/dist/esm/utils/trace.context.js +199 -0
  148. package/dist/esm/utils/trace.context.js.map +1 -0
  149. package/dist/types/adapter/axios.adapter.d.ts +51 -0
  150. package/dist/types/adapter/axios.adapter.d.ts.map +1 -0
  151. package/dist/types/adapter/base.d.ts +56 -0
  152. package/dist/types/adapter/base.d.ts.map +1 -0
  153. package/dist/types/adapter/index.d.ts +4 -0
  154. package/dist/types/adapter/index.d.ts.map +1 -0
  155. package/dist/types/adapter/undici.adapter.d.ts +68 -0
  156. package/dist/types/adapter/undici.adapter.d.ts.map +1 -0
  157. package/dist/types/client.d.ts +105 -0
  158. package/dist/types/client.d.ts.map +1 -0
  159. package/dist/types/config.d.ts +14 -0
  160. package/dist/types/config.d.ts.map +1 -0
  161. package/dist/types/const/config.const.d.ts +23 -0
  162. package/dist/types/const/config.const.d.ts.map +1 -0
  163. package/dist/types/const/dns.const.d.ts +13 -0
  164. package/dist/types/const/dns.const.d.ts.map +1 -0
  165. package/dist/types/const/enum.const.d.ts +17 -0
  166. package/dist/types/const/enum.const.d.ts.map +1 -0
  167. package/dist/types/const/index.d.ts +5 -0
  168. package/dist/types/const/index.d.ts.map +1 -0
  169. package/dist/types/const/ip.const.d.ts +42 -0
  170. package/dist/types/const/ip.const.d.ts.map +1 -0
  171. package/dist/types/index.d.ts +9 -0
  172. package/dist/types/index.d.ts.map +1 -0
  173. package/dist/types/logger/base.d.ts +42 -0
  174. package/dist/types/logger/base.d.ts.map +1 -0
  175. package/dist/types/logger/http_client.logger.d.ts +49 -0
  176. package/dist/types/logger/http_client.logger.d.ts.map +1 -0
  177. package/dist/types/logger/index.d.ts +4 -0
  178. package/dist/types/logger/index.d.ts.map +1 -0
  179. package/dist/types/logger/log.filter.d.ts +56 -0
  180. package/dist/types/logger/log.filter.d.ts.map +1 -0
  181. package/dist/types/security/dns.validator.d.ts +61 -0
  182. package/dist/types/security/dns.validator.d.ts.map +1 -0
  183. package/dist/types/security/index.d.ts +5 -0
  184. package/dist/types/security/index.d.ts.map +1 -0
  185. package/dist/types/security/ip.validator.d.ts +31 -0
  186. package/dist/types/security/ip.validator.d.ts.map +1 -0
  187. package/dist/types/security/ssrf.guard.d.ts +54 -0
  188. package/dist/types/security/ssrf.guard.d.ts.map +1 -0
  189. package/dist/types/security/url.validator.d.ts +76 -0
  190. package/dist/types/security/url.validator.d.ts.map +1 -0
  191. package/dist/types/types/adapter.d.ts +30 -0
  192. package/dist/types/types/adapter.d.ts.map +1 -0
  193. package/dist/types/types/client.d.ts +85 -0
  194. package/dist/types/types/client.d.ts.map +1 -0
  195. package/dist/types/types/config.d.ts +99 -0
  196. package/dist/types/types/config.d.ts.map +1 -0
  197. package/dist/types/types/errors.d.ts +23 -0
  198. package/dist/types/types/errors.d.ts.map +1 -0
  199. package/dist/types/types/index.d.ts +10 -0
  200. package/dist/types/types/index.d.ts.map +1 -0
  201. package/dist/types/types/ip.d.ts +32 -0
  202. package/dist/types/types/ip.d.ts.map +1 -0
  203. package/dist/types/types/logger.d.ts +136 -0
  204. package/dist/types/types/logger.d.ts.map +1 -0
  205. package/dist/types/types/request_response.d.ts +54 -0
  206. package/dist/types/types/request_response.d.ts.map +1 -0
  207. package/dist/types/types/security.d.ts +115 -0
  208. package/dist/types/types/security.d.ts.map +1 -0
  209. package/dist/types/types/trace.d.ts +34 -0
  210. package/dist/types/types/trace.d.ts.map +1 -0
  211. package/dist/types/utils/common.d.ts +14 -0
  212. package/dist/types/utils/common.d.ts.map +1 -0
  213. package/dist/types/utils/domain.d.ts +39 -0
  214. package/dist/types/utils/domain.d.ts.map +1 -0
  215. package/dist/types/utils/index.d.ts +6 -0
  216. package/dist/types/utils/index.d.ts.map +1 -0
  217. package/dist/types/utils/ip.range.d.ts +61 -0
  218. package/dist/types/utils/ip.range.d.ts.map +1 -0
  219. package/dist/types/utils/trace.context.d.ts +106 -0
  220. package/dist/types/utils/trace.context.d.ts.map +1 -0
  221. package/docs/adapters.md +53 -0
  222. package/docs/configuration.md +149 -0
  223. package/docs/logging.md +70 -0
  224. package/docs/proxy.md +44 -0
  225. package/docs/security.md +56 -0
  226. package/docs/trace-context.md +436 -0
  227. package/package.json +50 -0
  228. package/src/adapter/axios.adapter.ts +228 -0
  229. package/src/adapter/base.ts +180 -0
  230. package/src/adapter/index.ts +3 -0
  231. package/src/adapter/undici.adapter.ts +282 -0
  232. package/src/client.ts +552 -0
  233. package/src/config.ts +86 -0
  234. package/src/const/config.const.ts +60 -0
  235. package/src/const/dns.const.ts +15 -0
  236. package/src/const/enum.const.ts +17 -0
  237. package/src/const/index.ts +4 -0
  238. package/src/const/ip.const.ts +139 -0
  239. package/src/index.ts +8 -0
  240. package/src/logger/base.ts +75 -0
  241. package/src/logger/http_client.logger.ts +272 -0
  242. package/src/logger/index.ts +3 -0
  243. package/src/logger/log.filter.ts +149 -0
  244. package/src/security/dns.validator.ts +170 -0
  245. package/src/security/index.ts +4 -0
  246. package/src/security/ip.validator.ts +124 -0
  247. package/src/security/ssrf.guard.ts +224 -0
  248. package/src/security/url.validator.ts +192 -0
  249. package/src/types/adapter.ts +38 -0
  250. package/src/types/client.ts +119 -0
  251. package/src/types/config.ts +110 -0
  252. package/src/types/errors.ts +38 -0
  253. package/src/types/index.ts +27 -0
  254. package/src/types/ip.ts +34 -0
  255. package/src/types/logger.ts +150 -0
  256. package/src/types/request_response.ts +65 -0
  257. package/src/types/security.ts +126 -0
  258. package/src/types/trace.ts +35 -0
  259. package/src/utils/common.ts +28 -0
  260. package/src/utils/domain.ts +78 -0
  261. package/src/utils/index.ts +7 -0
  262. package/src/utils/ip.range.ts +218 -0
  263. package/src/utils/trace.context.ts +240 -0
@@ -0,0 +1,170 @@
1
+ /**
2
+ * DNS 验证器
3
+ */
4
+
5
+ import { promises as dns } from 'node:dns';
6
+ import { LRUCache } from 'lru-cache';
7
+ import { CommonUtils } from '../utils';
8
+ import {
9
+ DEFAULT_DNS_CACHE_TTL,
10
+ DEFAULT_DNS_RESOLVE_TIMEOUT,
11
+ DEFAULT_DNS_CACHE_MAX_SIZE
12
+ } from '../const';
13
+ import type {
14
+ DnsResult
15
+ } from '../types';
16
+ import { IpValidator } from './ip.validator';
17
+
18
+
19
+ /**
20
+ * DNS 验证器类
21
+ */
22
+ export class DnsValidator {
23
+ private cache: LRUCache<string, DnsResult>;
24
+ private cacheTtl: number;
25
+ private resolveTimeout: number;
26
+
27
+ constructor(
28
+ cacheTtl = DEFAULT_DNS_CACHE_TTL,
29
+ resolveTimeout = DEFAULT_DNS_RESOLVE_TIMEOUT,
30
+ maxCacheSize = DEFAULT_DNS_CACHE_MAX_SIZE
31
+ ) {
32
+ this.cacheTtl = cacheTtl;
33
+ this.resolveTimeout = resolveTimeout;
34
+ this.cache = new LRUCache<string, DnsResult>({
35
+ max: maxCacheSize,
36
+ ttl: cacheTtl,
37
+ // 启用 TTL 自动清理
38
+ ttlAutopurge: true,
39
+ // 更新访问时间,实现真正的 LRU
40
+ updateAgeOnGet: true,
41
+ // 更新访问时间时也更新 has() 调用
42
+ updateAgeOnHas: true,
43
+ });
44
+ }
45
+
46
+ /**
47
+ * 解析域名
48
+ * @param hostname 域名
49
+ * @returns DNS 解析结果
50
+ */
51
+ public async resolve(hostname: string): Promise<DnsResult> {
52
+ // 检查缓存
53
+ const cached = this.cache.get(hostname);
54
+ if (cached) {
55
+ return cached;
56
+ }
57
+
58
+ try {
59
+ // 尝试解析 IPv4 和 IPv6
60
+ const addresses: string[] = [];
61
+
62
+ try {
63
+ const ipv4Addresses = await CommonUtils.withTimeout(dns.resolve4(hostname), this.resolveTimeout, 'DNS resolution timed out');
64
+ addresses.push(...ipv4Addresses);
65
+ } catch {
66
+ // IPv4 解析失败,继续尝试 IPv6
67
+ }
68
+
69
+ try {
70
+ const ipv6Addresses = await CommonUtils.withTimeout(dns.resolve6(hostname), this.resolveTimeout, 'DNS resolution timed out');
71
+ addresses.push(...ipv6Addresses);
72
+ } catch {
73
+ // IPv6 解析失败
74
+ }
75
+
76
+ if (addresses.length === 0) {
77
+ throw new Error(`Unable to resolve hostname: ${ hostname }`);
78
+ }
79
+
80
+ const result: DnsResult = {
81
+ hostname,
82
+ addresses,
83
+ timestamp: Date.now(),
84
+ ttl: this.cacheTtl
85
+ };
86
+
87
+ // 存入缓存,LRUCache 会自动处理 TTL 和容量限制
88
+ this.cache.set(hostname, result);
89
+
90
+ return result;
91
+ } catch (error) {
92
+ throw new Error(
93
+ `DNS resolution failed for ${ hostname }: ${ error instanceof Error ? error.message : String(error) }`
94
+ );
95
+ }
96
+ }
97
+
98
+
99
+ /**
100
+ * 解析并验证域名(包括 IP 安全检查)
101
+ * @param hostname 域名
102
+ * @param allowPrivateIp 是否允许私有 IP
103
+ * @returns DNS 解析结果和验证结果
104
+ */
105
+ public async resolveAndValidate(
106
+ hostname: string,
107
+ allowPrivateIp = false
108
+ ): Promise<{ dnsResult: DnsResult, allValid: boolean, invalidIps: string[] }> {
109
+ const dnsResult = await this.resolve(hostname);
110
+ const validationResults = IpValidator.validateMany(dnsResult.addresses, allowPrivateIp);
111
+
112
+ const invalidIps = validationResults
113
+ .filter(result => !result.valid)
114
+ .map(result => result.ip);
115
+
116
+ return {
117
+ dnsResult,
118
+ allValid: invalidIps.length === 0,
119
+ invalidIps
120
+ };
121
+ }
122
+
123
+ /**
124
+ * 清空缓存
125
+ */
126
+ public clearCache(): void {
127
+ this.cache.clear();
128
+ }
129
+
130
+ /**
131
+ * 清除指定域名的缓存
132
+ * @param hostname 域名
133
+ */
134
+ public clearCacheFor(hostname: string): void {
135
+ this.cache.delete(hostname);
136
+ }
137
+
138
+ /**
139
+ * 获取缓存大小
140
+ * @returns 缓存条目数量
141
+ */
142
+ public getCacheSize(): number {
143
+ return this.cache.size;
144
+ }
145
+
146
+ /**
147
+ * 清理过期的缓存条目
148
+ * LRUCache 会自动清理过期条目(ttlAutopurge: true),此方法主动触发清理
149
+ */
150
+ public cleanExpiredCache(): void {
151
+ this.cache.purgeStale();
152
+ }
153
+
154
+ /**
155
+ * 获取缓存的最大容量
156
+ * @returns 最大缓存条目数量
157
+ */
158
+ public getMaxCacheSize(): number {
159
+ return this.cache.max;
160
+ }
161
+
162
+ /**
163
+ * 检查缓存中是否存在指定域名
164
+ * @param hostname 域名
165
+ * @returns 是否存在
166
+ */
167
+ public hasCache(hostname: string): boolean {
168
+ return this.cache.has(hostname);
169
+ }
170
+ }
@@ -0,0 +1,4 @@
1
+ export * from './dns.validator';
2
+ export * from './ip.validator';
3
+ export * from './url.validator';
4
+ export * from './ssrf.guard';
@@ -0,0 +1,124 @@
1
+ /**
2
+ * IP 地址验证器
3
+ */
4
+
5
+ import {
6
+ isIPv4,
7
+ isIPv6
8
+ } from 'node:net';
9
+ import type { IpValidationResult } from '../types';
10
+ import { IpRangeUtils } from '../utils';
11
+ import {
12
+ COMPILED_IPV4_RANGES,
13
+ COMPILED_IPV6_RANGES
14
+ } from '../const';
15
+
16
+
17
+ /**
18
+ * IP 地址验证器类
19
+ */
20
+ export class IpValidator {
21
+ /**
22
+ * 验证 IP 地址是否安全
23
+ * @param ip IP 地址
24
+ * @param allowPrivateIp 是否允许私有 IP
25
+ * @returns 验证结果
26
+ */
27
+ public static validate(ip: string, allowPrivateIp = false): IpValidationResult {
28
+ // 检查是否为有效的 IP 地址
29
+ const isV4 = isIPv4(ip);
30
+ const isV6 = isIPv6(ip);
31
+
32
+ if (!isV4 && !isV6) {
33
+ return {
34
+ valid: false,
35
+ ip,
36
+ isPrivate: false,
37
+ isLoopback: false,
38
+ isLinkLocal: false,
39
+ isMulticast: false,
40
+ isReserved: false,
41
+ reason: 'Invalid IP address format'
42
+ };
43
+ }
44
+
45
+ // 使用预编译的范围进行高性能检查
46
+ let isPrivate: boolean;
47
+ let isLoopback: boolean;
48
+ let isLinkLocal: boolean;
49
+ let isMulticast: boolean;
50
+ let isReserved: boolean;
51
+
52
+ if (isV4) {
53
+ isPrivate = IpRangeUtils.isIpv4InAnyCompiledRange(ip, COMPILED_IPV4_RANGES.private);
54
+ isLoopback = IpRangeUtils.isIpv4InAnyCompiledRange(ip, COMPILED_IPV4_RANGES.loopback);
55
+ isLinkLocal = IpRangeUtils.isIpv4InAnyCompiledRange(ip, COMPILED_IPV4_RANGES.linkLocal);
56
+ isMulticast = IpRangeUtils.isIpv4InAnyCompiledRange(ip, COMPILED_IPV4_RANGES.multicast);
57
+ isReserved = IpRangeUtils.isIpv4InAnyCompiledRange(ip, COMPILED_IPV4_RANGES.reserved);
58
+ } else {
59
+ isPrivate = IpRangeUtils.isIpv6InAnyCompiledRange(ip, COMPILED_IPV6_RANGES.private);
60
+ isLoopback = IpRangeUtils.isIpv6InAnyCompiledRange(ip, COMPILED_IPV6_RANGES.loopback);
61
+ isLinkLocal = IpRangeUtils.isIpv6InAnyCompiledRange(ip, COMPILED_IPV6_RANGES.linkLocal);
62
+ isMulticast = IpRangeUtils.isIpv6InAnyCompiledRange(ip, COMPILED_IPV6_RANGES.multicast);
63
+ isReserved = IpRangeUtils.isIpv6InAnyCompiledRange(ip, COMPILED_IPV6_RANGES.reserved);
64
+ }
65
+
66
+ // 基础验证信息
67
+ const baseResult = {
68
+ ip,
69
+ isPrivate,
70
+ isLoopback,
71
+ isLinkLocal,
72
+ isMulticast,
73
+ isReserved
74
+ };
75
+
76
+ // 检查是否为受限 IP - 按优先级顺序检查
77
+ const restrictions = [
78
+ { condition: isLoopback, reason: 'Loopback address is not allowed' },
79
+ { condition: isLinkLocal, reason: 'Link-local address is not allowed' },
80
+ { condition: isMulticast, reason: 'Multicast address is not allowed' },
81
+ { condition: isReserved, reason: 'Reserved address is not allowed' },
82
+ { condition: isPrivate && !allowPrivateIp, reason: 'Private IP address is not allowed' }
83
+ ];
84
+
85
+ for (const { condition, reason } of restrictions) {
86
+ if (condition) {
87
+ return {
88
+ valid: false,
89
+ ...baseResult,
90
+ reason
91
+ };
92
+ }
93
+ }
94
+
95
+ return {
96
+ valid: true,
97
+ ...baseResult
98
+ };
99
+ }
100
+
101
+ /**
102
+ * 批量验证 IP 地址
103
+ * @param ips IP 地址列表
104
+ * @param allowPrivateIp 是否允许私有 IP
105
+ * @returns 验证结果列表
106
+ */
107
+ public static validateMany(
108
+ ips: string[],
109
+ allowPrivateIp = false
110
+ ): IpValidationResult[] {
111
+ return ips.map(ip => this.validate(ip, allowPrivateIp));
112
+ }
113
+
114
+ /**
115
+ * 检查是否所有 IP 地址都有效
116
+ * @param ips IP 地址列表
117
+ * @param allowPrivateIp 是否允许私有 IP
118
+ * @returns 是否所有 IP 都有效
119
+ */
120
+ public static validateAll(ips: string[], allowPrivateIp = false): boolean {
121
+ return this.validateMany(ips, allowPrivateIp).every(result => result.valid);
122
+ }
123
+ }
124
+
@@ -0,0 +1,224 @@
1
+ /**
2
+ * SSRF 防护
3
+ * 整合 IP、DNS 和 URL 验证,防止服务器端请求伪造攻击
4
+ */
5
+
6
+ import { URL } from 'node:url';
7
+ import { isIP } from 'node:net';
8
+ import type {
9
+ ISsrfGuard,
10
+ SecurityConfig,
11
+ SsrfGuardResult,
12
+ } from '../types';
13
+ import { SecurityError } from '../types';
14
+ import { DnsValidator } from './dns.validator';
15
+ import { IpValidator } from './ip.validator';
16
+ import { UrlValidator } from './url.validator';
17
+
18
+ /**
19
+ * SSRF 防护类
20
+ */
21
+ export class SsrfGuard implements ISsrfGuard {
22
+ private dnsValidator: DnsValidator;
23
+ private urlValidator: UrlValidator;
24
+ private config: SecurityConfig;
25
+
26
+ constructor(config: SecurityConfig) {
27
+ this.config = config;
28
+ this.dnsValidator = new DnsValidator(
29
+ config.dnsCacheTtl,
30
+ undefined, // 使用默认的 DNS 解析超时
31
+ config.dnsCacheMaxSize
32
+ );
33
+ this.urlValidator = new UrlValidator(config.whitelist, config.blacklist);
34
+ }
35
+
36
+ /**
37
+ * 验证 URL 是否安全
38
+ * @param url 要验证的 URL
39
+ * @returns 验证结果
40
+ */
41
+ public async validate(url: string): Promise<SsrfGuardResult> {
42
+ let parsedUrl: URL;
43
+
44
+ try {
45
+ parsedUrl = new URL(url);
46
+ } catch (error) {
47
+ return {
48
+ passed: false,
49
+ originalUrl: url,
50
+ hostname: '',
51
+ reason: 'Invalid URL format'
52
+ };
53
+ }
54
+
55
+ const hostname = parsedUrl.hostname;
56
+
57
+ // 第一步:URL 验证(黑白名单)
58
+ const urlValidation = this.urlValidator.validate(url);
59
+ if (!urlValidation.valid) {
60
+ return {
61
+ passed: false,
62
+ originalUrl: url,
63
+ hostname,
64
+ reason: urlValidation.reason
65
+ };
66
+ }
67
+
68
+ // 第二步:检查是否为 IP 地址
69
+ const ipVersion = isIP(hostname);
70
+ if (ipVersion !== 0) {
71
+ // 直接使用 IP 地址,需要验证 IP 安全性
72
+ if (!this.config.enableIpValidation) {
73
+ return {
74
+ passed: true,
75
+ originalUrl: url,
76
+ hostname,
77
+ resolvedIps: [hostname],
78
+ safeIp: hostname
79
+ };
80
+ }
81
+
82
+ const ipValidation = IpValidator.validate(hostname, this.config.allowPrivateIp);
83
+ if (!ipValidation.valid) {
84
+ return {
85
+ passed: false,
86
+ originalUrl: url,
87
+ hostname,
88
+ resolvedIps: [hostname],
89
+ reason: ipValidation.reason
90
+ };
91
+ }
92
+
93
+ return {
94
+ passed: true,
95
+ originalUrl: url,
96
+ hostname,
97
+ resolvedIps: [hostname],
98
+ safeIp: hostname
99
+ };
100
+ }
101
+
102
+ // 第三步:DNS 解析和验证
103
+ if (!this.config.enableDnsValidation) {
104
+ return {
105
+ passed: true,
106
+ originalUrl: url,
107
+ hostname
108
+ };
109
+ }
110
+
111
+ try {
112
+ const { dnsResult, allValid, invalidIps } = await this.dnsValidator.resolveAndValidate(
113
+ hostname,
114
+ this.config.allowPrivateIp
115
+ );
116
+
117
+ if (!allValid) {
118
+ return {
119
+ passed: false,
120
+ originalUrl: url,
121
+ hostname,
122
+ resolvedIps: dnsResult.addresses,
123
+ reason: `DNS resolved to unsafe IP addresses: ${ invalidIps.join(', ') }`
124
+ };
125
+ }
126
+
127
+ // 选择第一个有效的 IP 地址
128
+ const safeIp = dnsResult.addresses[0];
129
+
130
+ return {
131
+ passed: true,
132
+ originalUrl: url,
133
+ hostname,
134
+ resolvedIps: dnsResult.addresses,
135
+ safeIp
136
+ };
137
+ } catch (error) {
138
+ return {
139
+ passed: false,
140
+ originalUrl: url,
141
+ hostname,
142
+ reason: error instanceof Error ? error.message : String(error)
143
+ };
144
+ }
145
+ }
146
+
147
+ /**
148
+ * 验证并返回安全的 URL
149
+ * @param url 要验证的 URL
150
+ * @returns 安全的 URL 和验证结果
151
+ * @throws {SecurityError} 验证失败时抛出错误
152
+ */
153
+ public async validateAndGetSafeUrl(url: string): Promise<{ safeUrl: string, result: SsrfGuardResult }> {
154
+ const result = await this.validate(url);
155
+
156
+ if (!result.passed) {
157
+ throw new SecurityError(
158
+ result.reason || 'URL validation failed',
159
+ 'SSRF_VALIDATION_FAILED',
160
+ url
161
+ );
162
+ }
163
+
164
+ return {
165
+ safeUrl: url,
166
+ result
167
+ };
168
+ }
169
+
170
+ /**
171
+ * 更新配置
172
+ * @param config 新的安全配置
173
+ */
174
+ public updateConfig(config: Partial<SecurityConfig>): void {
175
+ this.config = { ...this.config, ...config };
176
+
177
+ // 如果 DNS 缓存配置变更,需要重新创建 DNS 验证器
178
+ if (config.dnsCacheTtl !== undefined || config.dnsCacheMaxSize !== undefined) {
179
+ this.dnsValidator = new DnsValidator(
180
+ this.config.dnsCacheTtl,
181
+ undefined,
182
+ this.config.dnsCacheMaxSize
183
+ );
184
+ }
185
+
186
+ if (config.whitelist !== undefined || config.blacklist !== undefined) {
187
+ this.urlValidator = new UrlValidator(
188
+ config.whitelist ?? this.config.whitelist,
189
+ config.blacklist ?? this.config.blacklist
190
+ );
191
+ }
192
+ }
193
+
194
+ /**
195
+ * 清空 DNS 缓存
196
+ */
197
+ public clearDnsCache(): void {
198
+ this.dnsValidator.clearCache();
199
+ }
200
+
201
+ /**
202
+ * 获取 DNS 缓存大小
203
+ * @returns 缓存条目数量
204
+ */
205
+ public getDnsCacheSize(): number {
206
+ return this.dnsValidator.getCacheSize();
207
+ }
208
+
209
+ /**
210
+ * 清理过期的 DNS 缓存
211
+ */
212
+ public cleanExpiredDnsCache(): void {
213
+ this.dnsValidator.cleanExpiredCache();
214
+ }
215
+
216
+ /**
217
+ * 获取当前配置
218
+ * @returns 安全配置副本
219
+ */
220
+ public getConfig(): SecurityConfig {
221
+ return { ...this.config };
222
+ }
223
+ }
224
+
@@ -0,0 +1,192 @@
1
+ /**
2
+ * URL 验证器
3
+ */
4
+
5
+ import { URL } from 'node:url';
6
+ import type { UrlValidationResult } from '../types';
7
+ import { DomainUtils } from '../utils';
8
+
9
+ /**
10
+ * URL 验证器类
11
+ */
12
+ export class UrlValidator {
13
+ private whitelist: string[];
14
+ private blacklist: string[];
15
+ private whitelistRegexps: RegExp[];
16
+ private blacklistRegexps: RegExp[];
17
+
18
+ constructor(whitelist: string[] = [], blacklist: string[] = []) {
19
+ this.whitelist = whitelist;
20
+ this.blacklist = blacklist;
21
+ // 预生成正则表达式,提高匹配性能和安全性
22
+ this.whitelistRegexps = DomainUtils.createDomainMatchers(whitelist);
23
+ this.blacklistRegexps = DomainUtils.createDomainMatchers(blacklist);
24
+ }
25
+
26
+ /**
27
+ * 验证 URL
28
+ * @param url URL 字符串
29
+ * @returns 验证结果
30
+ */
31
+ public validate(url: string): UrlValidationResult {
32
+ let parsedUrl: URL;
33
+
34
+ try {
35
+ parsedUrl = new URL(url);
36
+ } catch {
37
+ return {
38
+ valid: false,
39
+ url,
40
+ inWhitelist: false,
41
+ inBlacklist: false,
42
+ reason: 'Invalid URL format'
43
+ };
44
+ }
45
+
46
+ // 只允许 http 和 https 协议
47
+ if (parsedUrl.protocol !== 'http:' && parsedUrl.protocol !== 'https:') {
48
+ return {
49
+ valid: false,
50
+ url,
51
+ inWhitelist: false,
52
+ inBlacklist: false,
53
+ reason: `Protocol ${ parsedUrl.protocol } is not allowed. Only http and https are supported.`
54
+ };
55
+ }
56
+
57
+ const hostname = parsedUrl.hostname;
58
+
59
+ // 检查白名单(使用预生成的正则表达式)
60
+ const inWhitelist = this.matchesAny(hostname, this.whitelistRegexps);
61
+ // 检查黑名单(使用预生成的正则表达式)
62
+ const inBlacklist = this.matchesAny(hostname, this.blacklistRegexps);
63
+
64
+ // 配了白名单, 优先使用白名单模式, 在名单内的域名通过, 其余拒绝.
65
+ // 同时配有黑名单, 黑名单辅助拒绝其余通过的域名.
66
+ if (this.whitelist.length > 0 && !inWhitelist) {
67
+ return {
68
+ valid: false,
69
+ url,
70
+ inWhitelist,
71
+ inBlacklist,
72
+ reason: 'Domain is not in whitelist'
73
+ };
74
+ }
75
+
76
+ // 配了黑名单, 使用黑名单模式, 在黑名单内的域名拒绝, 其余通过.
77
+ if (this.blacklist.length > 0 && inBlacklist) {
78
+ return {
79
+ valid: false,
80
+ url,
81
+ inWhitelist,
82
+ inBlacklist,
83
+ reason: 'Domain is in blacklist'
84
+ };
85
+ }
86
+
87
+ // 黑白名单没配的, 默认通过.
88
+ return {
89
+ valid: true,
90
+ url,
91
+ inWhitelist,
92
+ inBlacklist
93
+ };
94
+ }
95
+
96
+ /**
97
+ * 检查域名是否匹配任一模式(使用预生成的正则表达式)
98
+ * @param hostname 域名
99
+ * @param regexps 正则表达式列表
100
+ * @returns 是否匹配
101
+ */
102
+ private matchesAny(hostname: string, regexps: RegExp[]): boolean {
103
+ return regexps.some(regex => regex.test(hostname));
104
+ }
105
+
106
+ /**
107
+ * 更新白名单
108
+ * @param whitelist 新的白名单
109
+ */
110
+ public setWhitelist(whitelist: string[]): void {
111
+ this.whitelist = whitelist;
112
+ this.whitelistRegexps = DomainUtils.createDomainMatchers(whitelist);
113
+ }
114
+
115
+ /**
116
+ * 更新黑名单
117
+ * @param blacklist 新的黑名单
118
+ */
119
+ public setBlacklist(blacklist: string[]): void {
120
+ this.blacklist = blacklist;
121
+ this.blacklistRegexps = DomainUtils.createDomainMatchers(blacklist);
122
+ }
123
+
124
+ /**
125
+ * 添加到白名单
126
+ * @param patterns 要添加的模式
127
+ */
128
+ public addToWhitelist(...patterns: string[]): void {
129
+ this.whitelist.push(...patterns);
130
+ this.whitelistRegexps = DomainUtils.createDomainMatchers(this.whitelist);
131
+ }
132
+
133
+ /**
134
+ * 添加到黑名单
135
+ * @param patterns 要添加的模式
136
+ */
137
+ public addToBlacklist(...patterns: string[]): void {
138
+ this.blacklist.push(...patterns);
139
+ this.blacklistRegexps = DomainUtils.createDomainMatchers(this.blacklist);
140
+ }
141
+
142
+ /**
143
+ * 从白名单移除
144
+ * @param patterns 要移除的模式
145
+ */
146
+ public removeFromWhitelist(...patterns: string[]): void {
147
+ this.whitelist = this.whitelist.filter(p => !patterns.includes(p));
148
+ this.whitelistRegexps = DomainUtils.createDomainMatchers(this.whitelist);
149
+ }
150
+
151
+ /**
152
+ * 从黑名单移除
153
+ * @param patterns 要移除的模式
154
+ */
155
+ public removeFromBlacklist(...patterns: string[]): void {
156
+ this.blacklist = this.blacklist.filter(p => !patterns.includes(p));
157
+ this.blacklistRegexps = DomainUtils.createDomainMatchers(this.blacklist);
158
+ }
159
+
160
+ /**
161
+ * 获取白名单
162
+ * @returns 白名单副本
163
+ */
164
+ public getWhitelist(): string[] {
165
+ return [...this.whitelist];
166
+ }
167
+
168
+ /**
169
+ * 获取黑名单
170
+ * @returns 黑名单副本
171
+ */
172
+ public getBlacklist(): string[] {
173
+ return [...this.blacklist];
174
+ }
175
+
176
+ /**
177
+ * 清空白名单
178
+ */
179
+ public clearWhitelist(): void {
180
+ this.whitelist = [];
181
+ this.whitelistRegexps = [];
182
+ }
183
+
184
+ /**
185
+ * 清空黑名单
186
+ */
187
+ public clearBlacklist(): void {
188
+ this.blacklist = [];
189
+ this.blacklistRegexps = [];
190
+ }
191
+ }
192
+