dns.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571
  1. 'use strict'
  2. const { isIP } = require('node:net')
  3. const { lookup } = require('node:dns')
  4. const DecoratorHandler = require('../handler/decorator-handler')
  5. const { InvalidArgumentError, InformationalError } = require('../core/errors')
  6. const maxInt = Math.pow(2, 31) - 1
  7. function hasSafeIterator (headers) {
  8. const prototype = Object.getPrototypeOf(headers)
  9. const ownIterator = Object.prototype.hasOwnProperty.call(headers, Symbol.iterator)
  10. return ownIterator || (prototype != null && prototype !== Object.prototype && typeof headers[Symbol.iterator] === 'function')
  11. }
  12. function isHostHeader (key) {
  13. return typeof key === 'string' && key.toLowerCase() === 'host'
  14. }
  15. function normalizeHeaders (headers) {
  16. if (headers == null) {
  17. return null
  18. }
  19. if (Array.isArray(headers)) {
  20. if (headers.length === 0 || !Array.isArray(headers[0])) {
  21. return headers
  22. }
  23. const normalized = []
  24. for (const header of headers) {
  25. if (Array.isArray(header) && header.length === 2) {
  26. normalized.push(header[0], header[1])
  27. } else {
  28. normalized.push(header)
  29. }
  30. }
  31. return normalized
  32. }
  33. if (typeof headers === 'object' && hasSafeIterator(headers)) {
  34. const normalized = []
  35. for (const header of headers) {
  36. if (Array.isArray(header) && header.length === 2) {
  37. normalized.push(header[0], header[1])
  38. } else {
  39. normalized.push(header)
  40. }
  41. }
  42. return normalized
  43. }
  44. return headers
  45. }
  46. function hasHostHeader (headers) {
  47. if (headers == null) {
  48. return false
  49. }
  50. if (Array.isArray(headers)) {
  51. if (headers.length === 0) {
  52. return false
  53. }
  54. for (let i = 0; i < headers.length; i += 2) {
  55. if (isHostHeader(headers[i])) {
  56. return true
  57. }
  58. }
  59. return false
  60. }
  61. if (typeof headers === 'object') {
  62. for (const key in headers) {
  63. if (isHostHeader(key)) {
  64. return true
  65. }
  66. }
  67. }
  68. return false
  69. }
  70. function withHostHeader (host, headers) {
  71. const normalizedHeaders = normalizeHeaders(headers)
  72. if (hasHostHeader(normalizedHeaders)) {
  73. return normalizedHeaders
  74. }
  75. if (Array.isArray(normalizedHeaders)) {
  76. return ['host', host, ...normalizedHeaders]
  77. }
  78. if (normalizedHeaders && typeof normalizedHeaders === 'object') {
  79. return {
  80. host,
  81. ...normalizedHeaders
  82. }
  83. }
  84. return { host }
  85. }
  86. class DNSStorage {
  87. #maxItems = 0
  88. #records = new Map()
  89. constructor (opts) {
  90. this.#maxItems = opts.maxItems
  91. }
  92. get size () {
  93. return this.#records.size
  94. }
  95. get (hostname) {
  96. return this.#records.get(hostname) ?? null
  97. }
  98. set (hostname, records) {
  99. this.#records.set(hostname, records)
  100. }
  101. delete (hostname) {
  102. this.#records.delete(hostname)
  103. }
  104. // Delegate to storage decide can we do more lookups or not
  105. full () {
  106. return this.size >= this.#maxItems
  107. }
  108. }
  109. class DNSInstance {
  110. #maxTTL = 0
  111. #maxItems = 0
  112. dualStack = true
  113. affinity = null
  114. lookup = null
  115. pick = null
  116. storage = null
  117. constructor (opts) {
  118. this.#maxTTL = opts.maxTTL
  119. this.#maxItems = opts.maxItems
  120. this.dualStack = opts.dualStack
  121. this.affinity = opts.affinity
  122. this.lookup = opts.lookup ?? this.#defaultLookup
  123. this.pick = opts.pick ?? this.#defaultPick
  124. this.storage = opts.storage ?? new DNSStorage(opts)
  125. }
  126. runLookup (origin, opts, cb) {
  127. const ips = this.storage.get(origin.hostname)
  128. // If full, we just return the origin
  129. if (ips == null && this.storage.full()) {
  130. cb(null, origin)
  131. return
  132. }
  133. const newOpts = {
  134. affinity: this.affinity,
  135. dualStack: this.dualStack,
  136. lookup: this.lookup,
  137. pick: this.pick,
  138. ...opts.dns,
  139. maxTTL: this.#maxTTL,
  140. maxItems: this.#maxItems
  141. }
  142. // If no IPs we lookup
  143. if (ips == null) {
  144. this.lookup(origin, newOpts, (err, addresses) => {
  145. if (err || addresses == null || addresses.length === 0) {
  146. cb(err ?? new InformationalError('No DNS entries found'))
  147. return
  148. }
  149. this.setRecords(origin, addresses)
  150. const records = this.storage.get(origin.hostname)
  151. const ip = this.pick(
  152. origin,
  153. records,
  154. newOpts.affinity
  155. )
  156. let port
  157. if (typeof ip.port === 'number') {
  158. port = `:${ip.port}`
  159. } else if (origin.port !== '') {
  160. port = `:${origin.port}`
  161. } else {
  162. port = ''
  163. }
  164. cb(
  165. null,
  166. new URL(`${origin.protocol}//${
  167. ip.family === 6 ? `[${ip.address}]` : ip.address
  168. }${port}`)
  169. )
  170. })
  171. } else {
  172. // If there's IPs we pick
  173. const ip = this.pick(
  174. origin,
  175. ips,
  176. newOpts.affinity
  177. )
  178. // If no IPs we lookup - deleting old records
  179. if (ip == null) {
  180. this.storage.delete(origin.hostname)
  181. this.runLookup(origin, opts, cb)
  182. return
  183. }
  184. let port
  185. if (typeof ip.port === 'number') {
  186. port = `:${ip.port}`
  187. } else if (origin.port !== '') {
  188. port = `:${origin.port}`
  189. } else {
  190. port = ''
  191. }
  192. cb(
  193. null,
  194. new URL(`${origin.protocol}//${
  195. ip.family === 6 ? `[${ip.address}]` : ip.address
  196. }${port}`)
  197. )
  198. }
  199. }
  200. #defaultLookup (origin, opts, cb) {
  201. lookup(
  202. origin.hostname,
  203. {
  204. all: true,
  205. family: this.dualStack === false ? this.affinity : 0,
  206. order: 'ipv4first'
  207. },
  208. (err, addresses) => {
  209. if (err) {
  210. return cb(err)
  211. }
  212. const results = new Map()
  213. for (const addr of addresses) {
  214. // On linux we found duplicates, we attempt to remove them with
  215. // the latest record
  216. results.set(`${addr.address}:${addr.family}`, addr)
  217. }
  218. cb(null, results.values())
  219. }
  220. )
  221. }
  222. #defaultPick (origin, hostnameRecords, affinity) {
  223. let ip = null
  224. const { records, offset } = hostnameRecords
  225. let family
  226. if (this.dualStack) {
  227. if (affinity == null) {
  228. // Balance between ip families
  229. if (offset == null || offset === maxInt) {
  230. hostnameRecords.offset = 0
  231. affinity = 4
  232. } else {
  233. hostnameRecords.offset++
  234. affinity = (hostnameRecords.offset & 1) === 1 ? 6 : 4
  235. }
  236. }
  237. if (records[affinity] != null && records[affinity].ips.length > 0) {
  238. family = records[affinity]
  239. } else {
  240. family = records[affinity === 4 ? 6 : 4]
  241. }
  242. } else {
  243. family = records[affinity]
  244. }
  245. // If no IPs we return null
  246. if (family == null || family.ips.length === 0) {
  247. return ip
  248. }
  249. if (family.offset == null || family.offset === maxInt) {
  250. family.offset = 0
  251. } else {
  252. family.offset++
  253. }
  254. const position = family.offset % family.ips.length
  255. ip = family.ips[position] ?? null
  256. if (ip == null) {
  257. return ip
  258. }
  259. if (Date.now() - ip.timestamp > ip.ttl) { // record TTL is already in ms
  260. // We delete expired records
  261. // It is possible that they have different TTL, so we manage them individually
  262. family.ips.splice(position, 1)
  263. return this.pick(origin, hostnameRecords, affinity)
  264. }
  265. return ip
  266. }
  267. pickFamily (origin, ipFamily) {
  268. const records = this.storage.get(origin.hostname)?.records
  269. if (!records) {
  270. return null
  271. }
  272. const family = records[ipFamily]
  273. if (!family) {
  274. return null
  275. }
  276. if (family.offset == null || family.offset === maxInt) {
  277. family.offset = 0
  278. } else {
  279. family.offset++
  280. }
  281. const position = family.offset % family.ips.length
  282. const ip = family.ips[position] ?? null
  283. if (ip == null) {
  284. return ip
  285. }
  286. if (Date.now() - ip.timestamp > ip.ttl) { // record TTL is already in ms
  287. // We delete expired records
  288. // It is possible that they have different TTL, so we manage them individually
  289. family.ips.splice(position, 1)
  290. }
  291. return ip
  292. }
  293. setRecords (origin, addresses) {
  294. const timestamp = Date.now()
  295. const records = { records: { 4: null, 6: null } }
  296. let minTTL = this.#maxTTL
  297. for (const record of addresses) {
  298. record.timestamp = timestamp
  299. if (typeof record.ttl === 'number') {
  300. // The record TTL is expected to be in ms
  301. record.ttl = Math.min(record.ttl, this.#maxTTL)
  302. minTTL = Math.min(minTTL, record.ttl)
  303. } else {
  304. record.ttl = this.#maxTTL
  305. }
  306. const familyRecords = records.records[record.family] ?? { ips: [] }
  307. familyRecords.ips.push(record)
  308. records.records[record.family] = familyRecords
  309. }
  310. // We provide a default TTL if external storage will be used without TTL per record-level support
  311. this.storage.set(origin.hostname, records, { ttl: minTTL })
  312. }
  313. deleteRecords (origin) {
  314. this.storage.delete(origin.hostname)
  315. }
  316. getHandler (meta, opts) {
  317. return new DNSDispatchHandler(this, meta, opts)
  318. }
  319. }
  320. class DNSDispatchHandler extends DecoratorHandler {
  321. #state = null
  322. #opts = null
  323. #dispatch = null
  324. #origin = null
  325. #controller = null
  326. #newOrigin = null
  327. #firstTry = true
  328. constructor (state, { origin, handler, dispatch, newOrigin }, opts) {
  329. super(handler)
  330. this.#origin = origin
  331. this.#newOrigin = newOrigin
  332. this.#opts = { ...opts }
  333. this.#state = state
  334. this.#dispatch = dispatch
  335. }
  336. onResponseError (controller, err) {
  337. switch (err.code) {
  338. case 'ETIMEDOUT':
  339. case 'ECONNREFUSED': {
  340. if (this.#state.dualStack) {
  341. if (!this.#firstTry) {
  342. super.onResponseError(controller, err)
  343. return
  344. }
  345. this.#firstTry = false
  346. // Pick an ip address from the other family
  347. const otherFamily = this.#newOrigin.hostname[0] === '[' ? 4 : 6
  348. const ip = this.#state.pickFamily(this.#origin, otherFamily)
  349. if (ip == null) {
  350. super.onResponseError(controller, err)
  351. return
  352. }
  353. let port
  354. if (typeof ip.port === 'number') {
  355. port = `:${ip.port}`
  356. } else if (this.#origin.port !== '') {
  357. port = `:${this.#origin.port}`
  358. } else {
  359. port = ''
  360. }
  361. const dispatchOpts = {
  362. ...this.#opts,
  363. origin: `${this.#origin.protocol}//${
  364. ip.family === 6 ? `[${ip.address}]` : ip.address
  365. }${port}`,
  366. headers: withHostHeader(this.#origin.host, this.#opts.headers)
  367. }
  368. this.#dispatch(dispatchOpts, this)
  369. return
  370. }
  371. // if dual-stack disabled, we error out
  372. super.onResponseError(controller, err)
  373. break
  374. }
  375. case 'ENOTFOUND':
  376. this.#state.deleteRecords(this.#origin)
  377. super.onResponseError(controller, err)
  378. break
  379. default:
  380. super.onResponseError(controller, err)
  381. break
  382. }
  383. }
  384. }
  385. module.exports = interceptorOpts => {
  386. if (
  387. interceptorOpts?.maxTTL != null &&
  388. (typeof interceptorOpts?.maxTTL !== 'number' || interceptorOpts?.maxTTL < 0)
  389. ) {
  390. throw new InvalidArgumentError('Invalid maxTTL. Must be a positive number')
  391. }
  392. if (
  393. interceptorOpts?.maxItems != null &&
  394. (typeof interceptorOpts?.maxItems !== 'number' ||
  395. interceptorOpts?.maxItems < 1)
  396. ) {
  397. throw new InvalidArgumentError(
  398. 'Invalid maxItems. Must be a positive number and greater than zero'
  399. )
  400. }
  401. if (
  402. interceptorOpts?.affinity != null &&
  403. interceptorOpts?.affinity !== 4 &&
  404. interceptorOpts?.affinity !== 6
  405. ) {
  406. throw new InvalidArgumentError('Invalid affinity. Must be either 4 or 6')
  407. }
  408. if (
  409. interceptorOpts?.dualStack != null &&
  410. typeof interceptorOpts?.dualStack !== 'boolean'
  411. ) {
  412. throw new InvalidArgumentError('Invalid dualStack. Must be a boolean')
  413. }
  414. if (
  415. interceptorOpts?.lookup != null &&
  416. typeof interceptorOpts?.lookup !== 'function'
  417. ) {
  418. throw new InvalidArgumentError('Invalid lookup. Must be a function')
  419. }
  420. if (
  421. interceptorOpts?.pick != null &&
  422. typeof interceptorOpts?.pick !== 'function'
  423. ) {
  424. throw new InvalidArgumentError('Invalid pick. Must be a function')
  425. }
  426. if (
  427. interceptorOpts?.storage != null &&
  428. (typeof interceptorOpts?.storage?.get !== 'function' ||
  429. typeof interceptorOpts?.storage?.set !== 'function' ||
  430. typeof interceptorOpts?.storage?.full !== 'function' ||
  431. typeof interceptorOpts?.storage?.delete !== 'function'
  432. )
  433. ) {
  434. throw new InvalidArgumentError('Invalid storage. Must be a object with methods: { get, set, full, delete }')
  435. }
  436. const dualStack = interceptorOpts?.dualStack ?? true
  437. let affinity
  438. if (dualStack) {
  439. affinity = interceptorOpts?.affinity ?? null
  440. } else {
  441. affinity = interceptorOpts?.affinity ?? 4
  442. }
  443. const opts = {
  444. maxTTL: interceptorOpts?.maxTTL ?? 10e3, // Expressed in ms
  445. lookup: interceptorOpts?.lookup ?? null,
  446. pick: interceptorOpts?.pick ?? null,
  447. dualStack,
  448. affinity,
  449. maxItems: interceptorOpts?.maxItems ?? Infinity,
  450. storage: interceptorOpts?.storage
  451. }
  452. const instance = new DNSInstance(opts)
  453. return dispatch => {
  454. return function dnsInterceptor (origDispatchOpts, handler) {
  455. const origin =
  456. origDispatchOpts.origin.constructor === URL
  457. ? origDispatchOpts.origin
  458. : new URL(origDispatchOpts.origin)
  459. if (isIP(origin.hostname) !== 0) {
  460. return dispatch(origDispatchOpts, handler)
  461. }
  462. instance.runLookup(origin, origDispatchOpts, (err, newOrigin) => {
  463. if (err) {
  464. return handler.onResponseError(null, err)
  465. }
  466. const dispatchOpts = {
  467. ...origDispatchOpts,
  468. servername: origin.hostname, // For SNI on TLS
  469. origin: newOrigin.origin,
  470. headers: withHostHeader(origin.host, origDispatchOpts.headers)
  471. }
  472. dispatch(
  473. dispatchOpts,
  474. instance.getHandler(
  475. { origin, dispatch, handler, newOrigin },
  476. origDispatchOpts
  477. )
  478. )
  479. })
  480. return true
  481. }
  482. }
  483. }