Team Ai
Datasetpublic

codekingpro/portable-devtools

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