util.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719
  1. 'use strict'
  2. const assert = require('node:assert')
  3. const { kDestroyed, kBodyUsed, kListeners, kBody } = require('./symbols')
  4. const { IncomingMessage } = require('node:http')
  5. const stream = require('node:stream')
  6. const net = require('node:net')
  7. const { Blob } = require('node:buffer')
  8. const nodeUtil = require('node:util')
  9. const { stringify } = require('node:querystring')
  10. const { EventEmitter: EE } = require('node:events')
  11. const { InvalidArgumentError } = require('./errors')
  12. const { headerNameLowerCasedRecord } = require('./constants')
  13. const { tree } = require('./tree')
  14. const [nodeMajor, nodeMinor] = process.versions.node.split('.').map(v => Number(v))
  15. class BodyAsyncIterable {
  16. constructor (body) {
  17. this[kBody] = body
  18. this[kBodyUsed] = false
  19. }
  20. async * [Symbol.asyncIterator] () {
  21. assert(!this[kBodyUsed], 'disturbed')
  22. this[kBodyUsed] = true
  23. yield * this[kBody]
  24. }
  25. }
  26. function wrapRequestBody (body) {
  27. if (isStream(body)) {
  28. // TODO (fix): Provide some way for the user to cache the file to e.g. /tmp
  29. // so that it can be dispatched again?
  30. // TODO (fix): Do we need 100-expect support to provide a way to do this properly?
  31. if (bodyLength(body) === 0) {
  32. body
  33. .on('data', function () {
  34. assert(false)
  35. })
  36. }
  37. if (typeof body.readableDidRead !== 'boolean') {
  38. body[kBodyUsed] = false
  39. EE.prototype.on.call(body, 'data', function () {
  40. this[kBodyUsed] = true
  41. })
  42. }
  43. return body
  44. } else if (body && typeof body.pipeTo === 'function') {
  45. // TODO (fix): We can't access ReadableStream internal state
  46. // to determine whether or not it has been disturbed. This is just
  47. // a workaround.
  48. return new BodyAsyncIterable(body)
  49. } else if (
  50. body &&
  51. typeof body !== 'string' &&
  52. !ArrayBuffer.isView(body) &&
  53. isIterable(body)
  54. ) {
  55. // TODO: Should we allow re-using iterable if !this.opts.idempotent
  56. // or through some other flag?
  57. return new BodyAsyncIterable(body)
  58. } else {
  59. return body
  60. }
  61. }
  62. function nop () {}
  63. function isStream (obj) {
  64. return obj && typeof obj === 'object' && typeof obj.pipe === 'function' && typeof obj.on === 'function'
  65. }
  66. // based on https://github.com/node-fetch/fetch-blob/blob/8ab587d34080de94140b54f07168451e7d0b655e/index.js#L229-L241 (MIT License)
  67. function isBlobLike (object) {
  68. if (object === null) {
  69. return false
  70. } else if (object instanceof Blob) {
  71. return true
  72. } else if (typeof object !== 'object') {
  73. return false
  74. } else {
  75. const sTag = object[Symbol.toStringTag]
  76. return (sTag === 'Blob' || sTag === 'File') && (
  77. ('stream' in object && typeof object.stream === 'function') ||
  78. ('arrayBuffer' in object && typeof object.arrayBuffer === 'function')
  79. )
  80. }
  81. }
  82. function buildURL (url, queryParams) {
  83. if (url.includes('?') || url.includes('#')) {
  84. throw new Error('Query params cannot be passed when url already contains "?" or "#".')
  85. }
  86. const stringified = stringify(queryParams)
  87. if (stringified) {
  88. url += '?' + stringified
  89. }
  90. return url
  91. }
  92. function isValidPort (port) {
  93. const value = parseInt(port, 10)
  94. return (
  95. value === Number(port) &&
  96. value >= 0 &&
  97. value <= 65535
  98. )
  99. }
  100. function isHttpOrHttpsPrefixed (value) {
  101. return (
  102. value != null &&
  103. value[0] === 'h' &&
  104. value[1] === 't' &&
  105. value[2] === 't' &&
  106. value[3] === 'p' &&
  107. (
  108. value[4] === ':' ||
  109. (
  110. value[4] === 's' &&
  111. value[5] === ':'
  112. )
  113. )
  114. )
  115. }
  116. function parseURL (url) {
  117. if (typeof url === 'string') {
  118. url = new URL(url)
  119. if (!isHttpOrHttpsPrefixed(url.origin || url.protocol)) {
  120. throw new InvalidArgumentError('Invalid URL protocol: the URL must start with `http:` or `https:`.')
  121. }
  122. return url
  123. }
  124. if (!url || typeof url !== 'object') {
  125. throw new InvalidArgumentError('Invalid URL: The URL argument must be a non-null object.')
  126. }
  127. if (!(url instanceof URL)) {
  128. if (url.port != null && url.port !== '' && isValidPort(url.port) === false) {
  129. throw new InvalidArgumentError('Invalid URL: port must be a valid integer or a string representation of an integer.')
  130. }
  131. if (url.path != null && typeof url.path !== 'string') {
  132. throw new InvalidArgumentError('Invalid URL path: the path must be a string or null/undefined.')
  133. }
  134. if (url.pathname != null && typeof url.pathname !== 'string') {
  135. throw new InvalidArgumentError('Invalid URL pathname: the pathname must be a string or null/undefined.')
  136. }
  137. if (url.hostname != null && typeof url.hostname !== 'string') {
  138. throw new InvalidArgumentError('Invalid URL hostname: the hostname must be a string or null/undefined.')
  139. }
  140. if (url.origin != null && typeof url.origin !== 'string') {
  141. throw new InvalidArgumentError('Invalid URL origin: the origin must be a string or null/undefined.')
  142. }
  143. if (!isHttpOrHttpsPrefixed(url.origin || url.protocol)) {
  144. throw new InvalidArgumentError('Invalid URL protocol: the URL must start with `http:` or `https:`.')
  145. }
  146. const port = url.port != null
  147. ? url.port
  148. : (url.protocol === 'https:' ? 443 : 80)
  149. let origin = url.origin != null
  150. ? url.origin
  151. : `${url.protocol || ''}//${url.hostname || ''}:${port}`
  152. let path = url.path != null
  153. ? url.path
  154. : `${url.pathname || ''}${url.search || ''}`
  155. if (origin[origin.length - 1] === '/') {
  156. origin = origin.slice(0, origin.length - 1)
  157. }
  158. if (path && path[0] !== '/') {
  159. path = `/${path}`
  160. }
  161. // new URL(path, origin) is unsafe when `path` contains an absolute URL
  162. // From https://developer.mozilla.org/en-US/docs/Web/API/URL/URL:
  163. // If first parameter is a relative URL, second param is required, and will be used as the base URL.
  164. // If first parameter is an absolute URL, a given second param will be ignored.
  165. return new URL(`${origin}${path}`)
  166. }
  167. if (!isHttpOrHttpsPrefixed(url.origin || url.protocol)) {
  168. throw new InvalidArgumentError('Invalid URL protocol: the URL must start with `http:` or `https:`.')
  169. }
  170. return url
  171. }
  172. function parseOrigin (url) {
  173. url = parseURL(url)
  174. if (url.pathname !== '/' || url.search || url.hash) {
  175. throw new InvalidArgumentError('invalid url')
  176. }
  177. return url
  178. }
  179. function getHostname (host) {
  180. if (host[0] === '[') {
  181. const idx = host.indexOf(']')
  182. assert(idx !== -1)
  183. return host.substring(1, idx)
  184. }
  185. const idx = host.indexOf(':')
  186. if (idx === -1) return host
  187. return host.substring(0, idx)
  188. }
  189. // IP addresses are not valid server names per RFC6066
  190. // > Currently, the only server names supported are DNS hostnames
  191. function getServerName (host) {
  192. if (!host) {
  193. return null
  194. }
  195. assert(typeof host === 'string')
  196. const servername = getHostname(host)
  197. if (net.isIP(servername)) {
  198. return ''
  199. }
  200. return servername
  201. }
  202. function deepClone (obj) {
  203. return JSON.parse(JSON.stringify(obj))
  204. }
  205. function isAsyncIterable (obj) {
  206. return !!(obj != null && typeof obj[Symbol.asyncIterator] === 'function')
  207. }
  208. function isIterable (obj) {
  209. return !!(obj != null && (typeof obj[Symbol.iterator] === 'function' || typeof obj[Symbol.asyncIterator] === 'function'))
  210. }
  211. function bodyLength (body) {
  212. if (body == null) {
  213. return 0
  214. } else if (isStream(body)) {
  215. const state = body._readableState
  216. return state && state.objectMode === false && state.ended === true && Number.isFinite(state.length)
  217. ? state.length
  218. : null
  219. } else if (isBlobLike(body)) {
  220. return body.size != null ? body.size : null
  221. } else if (isBuffer(body)) {
  222. return body.byteLength
  223. }
  224. return null
  225. }
  226. function isDestroyed (body) {
  227. return body && !!(body.destroyed || body[kDestroyed] || (stream.isDestroyed?.(body)))
  228. }
  229. function destroy (stream, err) {
  230. if (stream == null || !isStream(stream) || isDestroyed(stream)) {
  231. return
  232. }
  233. if (typeof stream.destroy === 'function') {
  234. if (Object.getPrototypeOf(stream).constructor === IncomingMessage) {
  235. // See: https://github.com/nodejs/node/pull/38505/files
  236. stream.socket = null
  237. }
  238. stream.destroy(err)
  239. } else if (err) {
  240. queueMicrotask(() => {
  241. stream.emit('error', err)
  242. })
  243. }
  244. if (stream.destroyed !== true) {
  245. stream[kDestroyed] = true
  246. }
  247. }
  248. const KEEPALIVE_TIMEOUT_EXPR = /timeout=(\d+)/
  249. function parseKeepAliveTimeout (val) {
  250. const m = val.toString().match(KEEPALIVE_TIMEOUT_EXPR)
  251. return m ? parseInt(m[1], 10) * 1000 : null
  252. }
  253. /**
  254. * Retrieves a header name and returns its lowercase value.
  255. * @param {string | Buffer} value Header name
  256. * @returns {string}
  257. */
  258. function headerNameToString (value) {
  259. return typeof value === 'string'
  260. ? headerNameLowerCasedRecord[value] ?? value.toLowerCase()
  261. : tree.lookup(value) ?? value.toString('latin1').toLowerCase()
  262. }
  263. /**
  264. * Receive the buffer as a string and return its lowercase value.
  265. * @param {Buffer} value Header name
  266. * @returns {string}
  267. */
  268. function bufferToLowerCasedHeaderName (value) {
  269. return tree.lookup(value) ?? value.toString('latin1').toLowerCase()
  270. }
  271. /**
  272. * @param {Record<string, string | string[]> | (Buffer | string | (Buffer | string)[])[]} headers
  273. * @param {Record<string, string | string[]>} [obj]
  274. * @returns {Record<string, string | string[]>}
  275. */
  276. function parseHeaders (headers, obj) {
  277. if (obj === undefined) obj = {}
  278. for (let i = 0; i < headers.length; i += 2) {
  279. const key = headerNameToString(headers[i])
  280. let val = obj[key]
  281. if (val) {
  282. if (typeof val === 'string') {
  283. val = [val]
  284. obj[key] = val
  285. }
  286. val.push(headers[i + 1].toString('utf8'))
  287. } else {
  288. const headersValue = headers[i + 1]
  289. if (typeof headersValue === 'string') {
  290. obj[key] = headersValue
  291. } else {
  292. obj[key] = Array.isArray(headersValue) ? headersValue.map(x => x.toString('utf8')) : headersValue.toString('utf8')
  293. }
  294. }
  295. }
  296. // See https://github.com/nodejs/node/pull/46528
  297. if ('content-length' in obj && 'content-disposition' in obj) {
  298. obj['content-disposition'] = Buffer.from(obj['content-disposition']).toString('latin1')
  299. }
  300. return obj
  301. }
  302. function parseRawHeaders (headers) {
  303. const len = headers.length
  304. const ret = new Array(len)
  305. let hasContentLength = false
  306. let contentDispositionIdx = -1
  307. let key
  308. let val
  309. let kLen = 0
  310. for (let n = 0; n < headers.length; n += 2) {
  311. key = headers[n]
  312. val = headers[n + 1]
  313. typeof key !== 'string' && (key = key.toString())
  314. typeof val !== 'string' && (val = val.toString('utf8'))
  315. kLen = key.length
  316. if (kLen === 14 && key[7] === '-' && (key === 'content-length' || key.toLowerCase() === 'content-length')) {
  317. hasContentLength = true
  318. } else if (kLen === 19 && key[7] === '-' && (key === 'content-disposition' || key.toLowerCase() === 'content-disposition')) {
  319. contentDispositionIdx = n + 1
  320. }
  321. ret[n] = key
  322. ret[n + 1] = val
  323. }
  324. // See https://github.com/nodejs/node/pull/46528
  325. if (hasContentLength && contentDispositionIdx !== -1) {
  326. ret[contentDispositionIdx] = Buffer.from(ret[contentDispositionIdx]).toString('latin1')
  327. }
  328. return ret
  329. }
  330. function isBuffer (buffer) {
  331. // See, https://github.com/mcollina/undici/pull/319
  332. return buffer instanceof Uint8Array || Buffer.isBuffer(buffer)
  333. }
  334. function validateHandler (handler, method, upgrade) {
  335. if (!handler || typeof handler !== 'object') {
  336. throw new InvalidArgumentError('handler must be an object')
  337. }
  338. if (typeof handler.onConnect !== 'function') {
  339. throw new InvalidArgumentError('invalid onConnect method')
  340. }
  341. if (typeof handler.onError !== 'function') {
  342. throw new InvalidArgumentError('invalid onError method')
  343. }
  344. if (typeof handler.onBodySent !== 'function' && handler.onBodySent !== undefined) {
  345. throw new InvalidArgumentError('invalid onBodySent method')
  346. }
  347. if (upgrade || method === 'CONNECT') {
  348. if (typeof handler.onUpgrade !== 'function') {
  349. throw new InvalidArgumentError('invalid onUpgrade method')
  350. }
  351. } else {
  352. if (typeof handler.onHeaders !== 'function') {
  353. throw new InvalidArgumentError('invalid onHeaders method')
  354. }
  355. if (typeof handler.onData !== 'function') {
  356. throw new InvalidArgumentError('invalid onData method')
  357. }
  358. if (typeof handler.onComplete !== 'function') {
  359. throw new InvalidArgumentError('invalid onComplete method')
  360. }
  361. }
  362. }
  363. // A body is disturbed if it has been read from and it cannot
  364. // be re-used without losing state or data.
  365. function isDisturbed (body) {
  366. // TODO (fix): Why is body[kBodyUsed] needed?
  367. return !!(body && (stream.isDisturbed(body) || body[kBodyUsed]))
  368. }
  369. function isErrored (body) {
  370. return !!(body && stream.isErrored(body))
  371. }
  372. function isReadable (body) {
  373. return !!(body && stream.isReadable(body))
  374. }
  375. function getSocketInfo (socket) {
  376. return {
  377. localAddress: socket.localAddress,
  378. localPort: socket.localPort,
  379. remoteAddress: socket.remoteAddress,
  380. remotePort: socket.remotePort,
  381. remoteFamily: socket.remoteFamily,
  382. timeout: socket.timeout,
  383. bytesWritten: socket.bytesWritten,
  384. bytesRead: socket.bytesRead
  385. }
  386. }
  387. /** @type {globalThis['ReadableStream']} */
  388. function ReadableStreamFrom (iterable) {
  389. // We cannot use ReadableStream.from here because it does not return a byte stream.
  390. let iterator
  391. return new ReadableStream(
  392. {
  393. async start () {
  394. iterator = iterable[Symbol.asyncIterator]()
  395. },
  396. async pull (controller) {
  397. const { done, value } = await iterator.next()
  398. if (done) {
  399. queueMicrotask(() => {
  400. controller.close()
  401. controller.byobRequest?.respond(0)
  402. })
  403. } else {
  404. const buf = Buffer.isBuffer(value) ? value : Buffer.from(value)
  405. if (buf.byteLength) {
  406. controller.enqueue(new Uint8Array(buf))
  407. }
  408. }
  409. return controller.desiredSize > 0
  410. },
  411. async cancel (reason) {
  412. await iterator.return()
  413. },
  414. type: 'bytes'
  415. }
  416. )
  417. }
  418. // The chunk should be a FormData instance and contains
  419. // all the required methods.
  420. function isFormDataLike (object) {
  421. return (
  422. object &&
  423. typeof object === 'object' &&
  424. typeof object.append === 'function' &&
  425. typeof object.delete === 'function' &&
  426. typeof object.get === 'function' &&
  427. typeof object.getAll === 'function' &&
  428. typeof object.has === 'function' &&
  429. typeof object.set === 'function' &&
  430. object[Symbol.toStringTag] === 'FormData'
  431. )
  432. }
  433. function addAbortListener (signal, listener) {
  434. if ('addEventListener' in signal) {
  435. signal.addEventListener('abort', listener, { once: true })
  436. return () => signal.removeEventListener('abort', listener)
  437. }
  438. signal.addListener('abort', listener)
  439. return () => signal.removeListener('abort', listener)
  440. }
  441. const hasToWellFormed = typeof String.prototype.toWellFormed === 'function'
  442. const hasIsWellFormed = typeof String.prototype.isWellFormed === 'function'
  443. /**
  444. * @param {string} val
  445. */
  446. function toUSVString (val) {
  447. return hasToWellFormed ? `${val}`.toWellFormed() : nodeUtil.toUSVString(val)
  448. }
  449. /**
  450. * @param {string} val
  451. */
  452. // TODO: move this to webidl
  453. function isUSVString (val) {
  454. return hasIsWellFormed ? `${val}`.isWellFormed() : toUSVString(val) === `${val}`
  455. }
  456. /**
  457. * @see https://tools.ietf.org/html/rfc7230#section-3.2.6
  458. * @param {number} c
  459. */
  460. function isTokenCharCode (c) {
  461. switch (c) {
  462. case 0x22:
  463. case 0x28:
  464. case 0x29:
  465. case 0x2c:
  466. case 0x2f:
  467. case 0x3a:
  468. case 0x3b:
  469. case 0x3c:
  470. case 0x3d:
  471. case 0x3e:
  472. case 0x3f:
  473. case 0x40:
  474. case 0x5b:
  475. case 0x5c:
  476. case 0x5d:
  477. case 0x7b:
  478. case 0x7d:
  479. // DQUOTE and "(),/:;<=>?@[\]{}"
  480. return false
  481. default:
  482. // VCHAR %x21-7E
  483. return c >= 0x21 && c <= 0x7e
  484. }
  485. }
  486. /**
  487. * @param {string} characters
  488. */
  489. function isValidHTTPToken (characters) {
  490. if (characters.length === 0) {
  491. return false
  492. }
  493. for (let i = 0; i < characters.length; ++i) {
  494. if (!isTokenCharCode(characters.charCodeAt(i))) {
  495. return false
  496. }
  497. }
  498. return true
  499. }
  500. // headerCharRegex have been lifted from
  501. // https://github.com/nodejs/node/blob/main/lib/_http_common.js
  502. /**
  503. * Matches if val contains an invalid field-vchar
  504. * field-value = *( field-content / obs-fold )
  505. * field-content = field-vchar [ 1*( SP / HTAB ) field-vchar ]
  506. * field-vchar = VCHAR / obs-text
  507. */
  508. const headerCharRegex = /[^\t\x20-\x7e\x80-\xff]/
  509. /**
  510. * @param {string} characters
  511. */
  512. function isValidHeaderValue (characters) {
  513. return !headerCharRegex.test(characters)
  514. }
  515. // Parsed accordingly to RFC 9110
  516. // https://www.rfc-editor.org/rfc/rfc9110#field.content-range
  517. function parseRangeHeader (range) {
  518. if (range == null || range === '') return { start: 0, end: null, size: null }
  519. const m = range ? range.match(/^bytes (\d+)-(\d+)\/(\d+)?$/) : null
  520. return m
  521. ? {
  522. start: parseInt(m[1]),
  523. end: m[2] ? parseInt(m[2]) : null,
  524. size: m[3] ? parseInt(m[3]) : null
  525. }
  526. : null
  527. }
  528. function addListener (obj, name, listener) {
  529. const listeners = (obj[kListeners] ??= [])
  530. listeners.push([name, listener])
  531. obj.on(name, listener)
  532. return obj
  533. }
  534. function removeAllListeners (obj) {
  535. for (const [name, listener] of obj[kListeners] ?? []) {
  536. obj.removeListener(name, listener)
  537. }
  538. obj[kListeners] = null
  539. }
  540. function errorRequest (client, request, err) {
  541. try {
  542. request.onError(err)
  543. assert(request.aborted)
  544. } catch (err) {
  545. client.emit('error', err)
  546. }
  547. }
  548. const kEnumerableProperty = Object.create(null)
  549. kEnumerableProperty.enumerable = true
  550. const normalizedMethodRecordsBase = {
  551. delete: 'DELETE',
  552. DELETE: 'DELETE',
  553. get: 'GET',
  554. GET: 'GET',
  555. head: 'HEAD',
  556. HEAD: 'HEAD',
  557. options: 'OPTIONS',
  558. OPTIONS: 'OPTIONS',
  559. post: 'POST',
  560. POST: 'POST',
  561. put: 'PUT',
  562. PUT: 'PUT'
  563. }
  564. const normalizedMethodRecords = {
  565. ...normalizedMethodRecordsBase,
  566. patch: 'patch',
  567. PATCH: 'PATCH'
  568. }
  569. // Note: object prototypes should not be able to be referenced. e.g. `Object#hasOwnProperty`.
  570. Object.setPrototypeOf(normalizedMethodRecordsBase, null)
  571. Object.setPrototypeOf(normalizedMethodRecords, null)
  572. module.exports = {
  573. kEnumerableProperty,
  574. nop,
  575. isDisturbed,
  576. isErrored,
  577. isReadable,
  578. toUSVString,
  579. isUSVString,
  580. isBlobLike,
  581. parseOrigin,
  582. parseURL,
  583. getServerName,
  584. isStream,
  585. isIterable,
  586. isAsyncIterable,
  587. isDestroyed,
  588. headerNameToString,
  589. bufferToLowerCasedHeaderName,
  590. addListener,
  591. removeAllListeners,
  592. errorRequest,
  593. parseRawHeaders,
  594. parseHeaders,
  595. parseKeepAliveTimeout,
  596. destroy,
  597. bodyLength,
  598. deepClone,
  599. ReadableStreamFrom,
  600. isBuffer,
  601. validateHandler,
  602. getSocketInfo,
  603. isFormDataLike,
  604. buildURL,
  605. addAbortListener,
  606. isValidHTTPToken,
  607. isValidHeaderValue,
  608. isTokenCharCode,
  609. parseRangeHeader,
  610. normalizedMethodRecordsBase,
  611. normalizedMethodRecords,
  612. isValidPort,
  613. isHttpOrHttpsPrefixed,
  614. nodeMajor,
  615. nodeMinor,
  616. safeHTTPMethods: ['GET', 'HEAD', 'OPTIONS', 'TRACE'],
  617. wrapRequestBody
  618. }