chore: update deps

This commit is contained in:
Rim
2025-03-29 16:05:52 -04:00
parent a177b9bb4c
commit 505a26c84d
1489 changed files with 27814 additions and 146817 deletions

862
node_modules/undici/lib/web/cache/cache.js generated vendored Normal file
View File

@ -0,0 +1,862 @@
'use strict'
const { kConstruct } = require('../../core/symbols')
const { urlEquals, getFieldValues } = require('./util')
const { kEnumerableProperty, isDisturbed } = require('../../core/util')
const { webidl } = require('../fetch/webidl')
const { cloneResponse, fromInnerResponse, getResponseState } = require('../fetch/response')
const { Request, fromInnerRequest, getRequestState } = require('../fetch/request')
const { fetching } = require('../fetch/index')
const { urlIsHttpHttpsScheme, createDeferredPromise, readAllBytes } = require('../fetch/util')
const assert = require('node:assert')
/**
* @see https://w3c.github.io/ServiceWorker/#dfn-cache-batch-operation
* @typedef {Object} CacheBatchOperation
* @property {'delete' | 'put'} type
* @property {any} request
* @property {any} response
* @property {import('../../types/cache').CacheQueryOptions} options
*/
/**
* @see https://w3c.github.io/ServiceWorker/#dfn-request-response-list
* @typedef {[any, any][]} requestResponseList
*/
class Cache {
/**
* @see https://w3c.github.io/ServiceWorker/#dfn-relevant-request-response-list
* @type {requestResponseList}
*/
#relevantRequestResponseList
constructor () {
if (arguments[0] !== kConstruct) {
webidl.illegalConstructor()
}
webidl.util.markAsUncloneable(this)
this.#relevantRequestResponseList = arguments[1]
}
async match (request, options = {}) {
webidl.brandCheck(this, Cache)
const prefix = 'Cache.match'
webidl.argumentLengthCheck(arguments, 1, prefix)
request = webidl.converters.RequestInfo(request, prefix, 'request')
options = webidl.converters.CacheQueryOptions(options, prefix, 'options')
const p = this.#internalMatchAll(request, options, 1)
if (p.length === 0) {
return
}
return p[0]
}
async matchAll (request = undefined, options = {}) {
webidl.brandCheck(this, Cache)
const prefix = 'Cache.matchAll'
if (request !== undefined) request = webidl.converters.RequestInfo(request, prefix, 'request')
options = webidl.converters.CacheQueryOptions(options, prefix, 'options')
return this.#internalMatchAll(request, options)
}
async add (request) {
webidl.brandCheck(this, Cache)
const prefix = 'Cache.add'
webidl.argumentLengthCheck(arguments, 1, prefix)
request = webidl.converters.RequestInfo(request, prefix, 'request')
// 1.
const requests = [request]
// 2.
const responseArrayPromise = this.addAll(requests)
// 3.
return await responseArrayPromise
}
async addAll (requests) {
webidl.brandCheck(this, Cache)
const prefix = 'Cache.addAll'
webidl.argumentLengthCheck(arguments, 1, prefix)
// 1.
const responsePromises = []
// 2.
const requestList = []
// 3.
for (let request of requests) {
if (request === undefined) {
throw webidl.errors.conversionFailed({
prefix,
argument: 'Argument 1',
types: ['undefined is not allowed']
})
}
request = webidl.converters.RequestInfo(request)
if (typeof request === 'string') {
continue
}
// 3.1
const r = getRequestState(request)
// 3.2
if (!urlIsHttpHttpsScheme(r.url) || r.method !== 'GET') {
throw webidl.errors.exception({
header: prefix,
message: 'Expected http/s scheme when method is not GET.'
})
}
}
// 4.
/** @type {ReturnType<typeof fetching>[]} */
const fetchControllers = []
// 5.
for (const request of requests) {
// 5.1
const r = getRequestState(new Request(request))
// 5.2
if (!urlIsHttpHttpsScheme(r.url)) {
throw webidl.errors.exception({
header: prefix,
message: 'Expected http/s scheme.'
})
}
// 5.4
r.initiator = 'fetch'
r.destination = 'subresource'
// 5.5
requestList.push(r)
// 5.6
const responsePromise = createDeferredPromise()
// 5.7
fetchControllers.push(fetching({
request: r,
processResponse (response) {
// 1.
if (response.type === 'error' || response.status === 206 || response.status < 200 || response.status > 299) {
responsePromise.reject(webidl.errors.exception({
header: 'Cache.addAll',
message: 'Received an invalid status code or the request failed.'
}))
} else if (response.headersList.contains('vary')) { // 2.
// 2.1
const fieldValues = getFieldValues(response.headersList.get('vary'))
// 2.2
for (const fieldValue of fieldValues) {
// 2.2.1
if (fieldValue === '*') {
responsePromise.reject(webidl.errors.exception({
header: 'Cache.addAll',
message: 'invalid vary field value'
}))
for (const controller of fetchControllers) {
controller.abort()
}
return
}
}
}
},
processResponseEndOfBody (response) {
// 1.
if (response.aborted) {
responsePromise.reject(new DOMException('aborted', 'AbortError'))
return
}
// 2.
responsePromise.resolve(response)
}
}))
// 5.8
responsePromises.push(responsePromise.promise)
}
// 6.
const p = Promise.all(responsePromises)
// 7.
const responses = await p
// 7.1
const operations = []
// 7.2
let index = 0
// 7.3
for (const response of responses) {
// 7.3.1
/** @type {CacheBatchOperation} */
const operation = {
type: 'put', // 7.3.2
request: requestList[index], // 7.3.3
response // 7.3.4
}
operations.push(operation) // 7.3.5
index++ // 7.3.6
}
// 7.5
const cacheJobPromise = createDeferredPromise()
// 7.6.1
let errorData = null
// 7.6.2
try {
this.#batchCacheOperations(operations)
} catch (e) {
errorData = e
}
// 7.6.3
queueMicrotask(() => {
// 7.6.3.1
if (errorData === null) {
cacheJobPromise.resolve(undefined)
} else {
// 7.6.3.2
cacheJobPromise.reject(errorData)
}
})
// 7.7
return cacheJobPromise.promise
}
async put (request, response) {
webidl.brandCheck(this, Cache)
const prefix = 'Cache.put'
webidl.argumentLengthCheck(arguments, 2, prefix)
request = webidl.converters.RequestInfo(request, prefix, 'request')
response = webidl.converters.Response(response, prefix, 'response')
// 1.
let innerRequest = null
// 2.
if (webidl.is.Request(request)) {
innerRequest = getRequestState(request)
} else { // 3.
innerRequest = getRequestState(new Request(request))
}
// 4.
if (!urlIsHttpHttpsScheme(innerRequest.url) || innerRequest.method !== 'GET') {
throw webidl.errors.exception({
header: prefix,
message: 'Expected an http/s scheme when method is not GET'
})
}
// 5.
const innerResponse = getResponseState(response)
// 6.
if (innerResponse.status === 206) {
throw webidl.errors.exception({
header: prefix,
message: 'Got 206 status'
})
}
// 7.
if (innerResponse.headersList.contains('vary')) {
// 7.1.
const fieldValues = getFieldValues(innerResponse.headersList.get('vary'))
// 7.2.
for (const fieldValue of fieldValues) {
// 7.2.1
if (fieldValue === '*') {
throw webidl.errors.exception({
header: prefix,
message: 'Got * vary field value'
})
}
}
}
// 8.
if (innerResponse.body && (isDisturbed(innerResponse.body.stream) || innerResponse.body.stream.locked)) {
throw webidl.errors.exception({
header: prefix,
message: 'Response body is locked or disturbed'
})
}
// 9.
const clonedResponse = cloneResponse(innerResponse)
// 10.
const bodyReadPromise = createDeferredPromise()
// 11.
if (innerResponse.body != null) {
// 11.1
const stream = innerResponse.body.stream
// 11.2
const reader = stream.getReader()
// 11.3
readAllBytes(reader, bodyReadPromise.resolve, bodyReadPromise.reject)
} else {
bodyReadPromise.resolve(undefined)
}
// 12.
/** @type {CacheBatchOperation[]} */
const operations = []
// 13.
/** @type {CacheBatchOperation} */
const operation = {
type: 'put', // 14.
request: innerRequest, // 15.
response: clonedResponse // 16.
}
// 17.
operations.push(operation)
// 19.
const bytes = await bodyReadPromise.promise
if (clonedResponse.body != null) {
clonedResponse.body.source = bytes
}
// 19.1
const cacheJobPromise = createDeferredPromise()
// 19.2.1
let errorData = null
// 19.2.2
try {
this.#batchCacheOperations(operations)
} catch (e) {
errorData = e
}
// 19.2.3
queueMicrotask(() => {
// 19.2.3.1
if (errorData === null) {
cacheJobPromise.resolve()
} else { // 19.2.3.2
cacheJobPromise.reject(errorData)
}
})
return cacheJobPromise.promise
}
async delete (request, options = {}) {
webidl.brandCheck(this, Cache)
const prefix = 'Cache.delete'
webidl.argumentLengthCheck(arguments, 1, prefix)
request = webidl.converters.RequestInfo(request, prefix, 'request')
options = webidl.converters.CacheQueryOptions(options, prefix, 'options')
/**
* @type {Request}
*/
let r = null
if (webidl.is.Request(request)) {
r = getRequestState(request)
if (r.method !== 'GET' && !options.ignoreMethod) {
return false
}
} else {
assert(typeof request === 'string')
r = getRequestState(new Request(request))
}
/** @type {CacheBatchOperation[]} */
const operations = []
/** @type {CacheBatchOperation} */
const operation = {
type: 'delete',
request: r,
options
}
operations.push(operation)
const cacheJobPromise = createDeferredPromise()
let errorData = null
let requestResponses
try {
requestResponses = this.#batchCacheOperations(operations)
} catch (e) {
errorData = e
}
queueMicrotask(() => {
if (errorData === null) {
cacheJobPromise.resolve(!!requestResponses?.length)
} else {
cacheJobPromise.reject(errorData)
}
})
return cacheJobPromise.promise
}
/**
* @see https://w3c.github.io/ServiceWorker/#dom-cache-keys
* @param {any} request
* @param {import('../../types/cache').CacheQueryOptions} options
* @returns {Promise<readonly Request[]>}
*/
async keys (request = undefined, options = {}) {
webidl.brandCheck(this, Cache)
const prefix = 'Cache.keys'
if (request !== undefined) request = webidl.converters.RequestInfo(request, prefix, 'request')
options = webidl.converters.CacheQueryOptions(options, prefix, 'options')
// 1.
let r = null
// 2.
if (request !== undefined) {
// 2.1
if (webidl.is.Request(request)) {
// 2.1.1
r = getRequestState(request)
// 2.1.2
if (r.method !== 'GET' && !options.ignoreMethod) {
return []
}
} else if (typeof request === 'string') { // 2.2
r = getRequestState(new Request(request))
}
}
// 4.
const promise = createDeferredPromise()
// 5.
// 5.1
const requests = []
// 5.2
if (request === undefined) {
// 5.2.1
for (const requestResponse of this.#relevantRequestResponseList) {
// 5.2.1.1
requests.push(requestResponse[0])
}
} else { // 5.3
// 5.3.1
const requestResponses = this.#queryCache(r, options)
// 5.3.2
for (const requestResponse of requestResponses) {
// 5.3.2.1
requests.push(requestResponse[0])
}
}
// 5.4
queueMicrotask(() => {
// 5.4.1
const requestList = []
// 5.4.2
for (const request of requests) {
const requestObject = fromInnerRequest(
request,
undefined,
new AbortController().signal,
'immutable'
)
// 5.4.2.1
requestList.push(requestObject)
}
// 5.4.3
promise.resolve(Object.freeze(requestList))
})
return promise.promise
}
/**
* @see https://w3c.github.io/ServiceWorker/#batch-cache-operations-algorithm
* @param {CacheBatchOperation[]} operations
* @returns {requestResponseList}
*/
#batchCacheOperations (operations) {
// 1.
const cache = this.#relevantRequestResponseList
// 2.
const backupCache = [...cache]
// 3.
const addedItems = []
// 4.1
const resultList = []
try {
// 4.2
for (const operation of operations) {
// 4.2.1
if (operation.type !== 'delete' && operation.type !== 'put') {
throw webidl.errors.exception({
header: 'Cache.#batchCacheOperations',
message: 'operation type does not match "delete" or "put"'
})
}
// 4.2.2
if (operation.type === 'delete' && operation.response != null) {
throw webidl.errors.exception({
header: 'Cache.#batchCacheOperations',
message: 'delete operation should not have an associated response'
})
}
// 4.2.3
if (this.#queryCache(operation.request, operation.options, addedItems).length) {
throw new DOMException('???', 'InvalidStateError')
}
// 4.2.4
let requestResponses
// 4.2.5
if (operation.type === 'delete') {
// 4.2.5.1
requestResponses = this.#queryCache(operation.request, operation.options)
// TODO: the spec is wrong, this is needed to pass WPTs
if (requestResponses.length === 0) {
return []
}
// 4.2.5.2
for (const requestResponse of requestResponses) {
const idx = cache.indexOf(requestResponse)
assert(idx !== -1)
// 4.2.5.2.1
cache.splice(idx, 1)
}
} else if (operation.type === 'put') { // 4.2.6
// 4.2.6.1
if (operation.response == null) {
throw webidl.errors.exception({
header: 'Cache.#batchCacheOperations',
message: 'put operation should have an associated response'
})
}
// 4.2.6.2
const r = operation.request
// 4.2.6.3
if (!urlIsHttpHttpsScheme(r.url)) {
throw webidl.errors.exception({
header: 'Cache.#batchCacheOperations',
message: 'expected http or https scheme'
})
}
// 4.2.6.4
if (r.method !== 'GET') {
throw webidl.errors.exception({
header: 'Cache.#batchCacheOperations',
message: 'not get method'
})
}
// 4.2.6.5
if (operation.options != null) {
throw webidl.errors.exception({
header: 'Cache.#batchCacheOperations',
message: 'options must not be defined'
})
}
// 4.2.6.6
requestResponses = this.#queryCache(operation.request)
// 4.2.6.7
for (const requestResponse of requestResponses) {
const idx = cache.indexOf(requestResponse)
assert(idx !== -1)
// 4.2.6.7.1
cache.splice(idx, 1)
}
// 4.2.6.8
cache.push([operation.request, operation.response])
// 4.2.6.10
addedItems.push([operation.request, operation.response])
}
// 4.2.7
resultList.push([operation.request, operation.response])
}
// 4.3
return resultList
} catch (e) { // 5.
// 5.1
this.#relevantRequestResponseList.length = 0
// 5.2
this.#relevantRequestResponseList = backupCache
// 5.3
throw e
}
}
/**
* @see https://w3c.github.io/ServiceWorker/#query-cache
* @param {any} requestQuery
* @param {import('../../types/cache').CacheQueryOptions} options
* @param {requestResponseList} targetStorage
* @returns {requestResponseList}
*/
#queryCache (requestQuery, options, targetStorage) {
/** @type {requestResponseList} */
const resultList = []
const storage = targetStorage ?? this.#relevantRequestResponseList
for (const requestResponse of storage) {
const [cachedRequest, cachedResponse] = requestResponse
if (this.#requestMatchesCachedItem(requestQuery, cachedRequest, cachedResponse, options)) {
resultList.push(requestResponse)
}
}
return resultList
}
/**
* @see https://w3c.github.io/ServiceWorker/#request-matches-cached-item-algorithm
* @param {any} requestQuery
* @param {any} request
* @param {any | null} response
* @param {import('../../types/cache').CacheQueryOptions | undefined} options
* @returns {boolean}
*/
#requestMatchesCachedItem (requestQuery, request, response = null, options) {
// if (options?.ignoreMethod === false && request.method === 'GET') {
// return false
// }
const queryURL = new URL(requestQuery.url)
const cachedURL = new URL(request.url)
if (options?.ignoreSearch) {
cachedURL.search = ''
queryURL.search = ''
}
if (!urlEquals(queryURL, cachedURL, true)) {
return false
}
if (
response == null ||
options?.ignoreVary ||
!response.headersList.contains('vary')
) {
return true
}
const fieldValues = getFieldValues(response.headersList.get('vary'))
for (const fieldValue of fieldValues) {
if (fieldValue === '*') {
return false
}
const requestValue = request.headersList.get(fieldValue)
const queryValue = requestQuery.headersList.get(fieldValue)
// If one has the header and the other doesn't, or one has
// a different value than the other, return false
if (requestValue !== queryValue) {
return false
}
}
return true
}
#internalMatchAll (request, options, maxResponses = Infinity) {
// 1.
let r = null
// 2.
if (request !== undefined) {
if (webidl.is.Request(request)) {
// 2.1.1
r = getRequestState(request)
// 2.1.2
if (r.method !== 'GET' && !options.ignoreMethod) {
return []
}
} else if (typeof request === 'string') {
// 2.2.1
r = getRequestState(new Request(request))
}
}
// 5.
// 5.1
const responses = []
// 5.2
if (request === undefined) {
// 5.2.1
for (const requestResponse of this.#relevantRequestResponseList) {
responses.push(requestResponse[1])
}
} else { // 5.3
// 5.3.1
const requestResponses = this.#queryCache(r, options)
// 5.3.2
for (const requestResponse of requestResponses) {
responses.push(requestResponse[1])
}
}
// 5.4
// We don't implement CORs so we don't need to loop over the responses, yay!
// 5.5.1
const responseList = []
// 5.5.2
for (const response of responses) {
// 5.5.2.1
const responseObject = fromInnerResponse(response, 'immutable')
responseList.push(responseObject.clone())
if (responseList.length >= maxResponses) {
break
}
}
// 6.
return Object.freeze(responseList)
}
}
Object.defineProperties(Cache.prototype, {
[Symbol.toStringTag]: {
value: 'Cache',
configurable: true
},
match: kEnumerableProperty,
matchAll: kEnumerableProperty,
add: kEnumerableProperty,
addAll: kEnumerableProperty,
put: kEnumerableProperty,
delete: kEnumerableProperty,
keys: kEnumerableProperty
})
const cacheQueryOptionConverters = [
{
key: 'ignoreSearch',
converter: webidl.converters.boolean,
defaultValue: () => false
},
{
key: 'ignoreMethod',
converter: webidl.converters.boolean,
defaultValue: () => false
},
{
key: 'ignoreVary',
converter: webidl.converters.boolean,
defaultValue: () => false
}
]
webidl.converters.CacheQueryOptions = webidl.dictionaryConverter(cacheQueryOptionConverters)
webidl.converters.MultiCacheQueryOptions = webidl.dictionaryConverter([
...cacheQueryOptionConverters,
{
key: 'cacheName',
converter: webidl.converters.DOMString
}
])
webidl.converters.Response = webidl.interfaceConverter(
webidl.is.Response,
'Response'
)
webidl.converters['sequence<RequestInfo>'] = webidl.sequenceConverter(
webidl.converters.RequestInfo
)
module.exports = {
Cache
}

152
node_modules/undici/lib/web/cache/cachestorage.js generated vendored Normal file
View File

@ -0,0 +1,152 @@
'use strict'
const { Cache } = require('./cache')
const { webidl } = require('../fetch/webidl')
const { kEnumerableProperty } = require('../../core/util')
const { kConstruct } = require('../../core/symbols')
class CacheStorage {
/**
* @see https://w3c.github.io/ServiceWorker/#dfn-relevant-name-to-cache-map
* @type {Map<string, import('./cache').requestResponseList}
*/
#caches = new Map()
constructor () {
if (arguments[0] !== kConstruct) {
webidl.illegalConstructor()
}
webidl.util.markAsUncloneable(this)
}
async match (request, options = {}) {
webidl.brandCheck(this, CacheStorage)
webidl.argumentLengthCheck(arguments, 1, 'CacheStorage.match')
request = webidl.converters.RequestInfo(request)
options = webidl.converters.MultiCacheQueryOptions(options)
// 1.
if (options.cacheName != null) {
// 1.1.1.1
if (this.#caches.has(options.cacheName)) {
// 1.1.1.1.1
const cacheList = this.#caches.get(options.cacheName)
const cache = new Cache(kConstruct, cacheList)
return await cache.match(request, options)
}
} else { // 2.
// 2.2
for (const cacheList of this.#caches.values()) {
const cache = new Cache(kConstruct, cacheList)
// 2.2.1.2
const response = await cache.match(request, options)
if (response !== undefined) {
return response
}
}
}
}
/**
* @see https://w3c.github.io/ServiceWorker/#cache-storage-has
* @param {string} cacheName
* @returns {Promise<boolean>}
*/
async has (cacheName) {
webidl.brandCheck(this, CacheStorage)
const prefix = 'CacheStorage.has'
webidl.argumentLengthCheck(arguments, 1, prefix)
cacheName = webidl.converters.DOMString(cacheName, prefix, 'cacheName')
// 2.1.1
// 2.2
return this.#caches.has(cacheName)
}
/**
* @see https://w3c.github.io/ServiceWorker/#dom-cachestorage-open
* @param {string} cacheName
* @returns {Promise<Cache>}
*/
async open (cacheName) {
webidl.brandCheck(this, CacheStorage)
const prefix = 'CacheStorage.open'
webidl.argumentLengthCheck(arguments, 1, prefix)
cacheName = webidl.converters.DOMString(cacheName, prefix, 'cacheName')
// 2.1
if (this.#caches.has(cacheName)) {
// await caches.open('v1') !== await caches.open('v1')
// 2.1.1
const cache = this.#caches.get(cacheName)
// 2.1.1.1
return new Cache(kConstruct, cache)
}
// 2.2
const cache = []
// 2.3
this.#caches.set(cacheName, cache)
// 2.4
return new Cache(kConstruct, cache)
}
/**
* @see https://w3c.github.io/ServiceWorker/#cache-storage-delete
* @param {string} cacheName
* @returns {Promise<boolean>}
*/
async delete (cacheName) {
webidl.brandCheck(this, CacheStorage)
const prefix = 'CacheStorage.delete'
webidl.argumentLengthCheck(arguments, 1, prefix)
cacheName = webidl.converters.DOMString(cacheName, prefix, 'cacheName')
return this.#caches.delete(cacheName)
}
/**
* @see https://w3c.github.io/ServiceWorker/#cache-storage-keys
* @returns {Promise<string[]>}
*/
async keys () {
webidl.brandCheck(this, CacheStorage)
// 2.1
const keys = this.#caches.keys()
// 2.2
return [...keys]
}
}
Object.defineProperties(CacheStorage.prototype, {
[Symbol.toStringTag]: {
value: 'CacheStorage',
configurable: true
},
match: kEnumerableProperty,
has: kEnumerableProperty,
open: kEnumerableProperty,
delete: kEnumerableProperty,
keys: kEnumerableProperty
})
module.exports = {
CacheStorage
}

45
node_modules/undici/lib/web/cache/util.js generated vendored Normal file
View File

@ -0,0 +1,45 @@
'use strict'
const assert = require('node:assert')
const { URLSerializer } = require('../fetch/data-url')
const { isValidHeaderName } = require('../fetch/util')
/**
* @see https://url.spec.whatwg.org/#concept-url-equals
* @param {URL} A
* @param {URL} B
* @param {boolean | undefined} excludeFragment
* @returns {boolean}
*/
function urlEquals (A, B, excludeFragment = false) {
const serializedA = URLSerializer(A, excludeFragment)
const serializedB = URLSerializer(B, excludeFragment)
return serializedA === serializedB
}
/**
* @see https://github.com/chromium/chromium/blob/694d20d134cb553d8d89e5500b9148012b1ba299/content/browser/cache_storage/cache_storage_cache.cc#L260-L262
* @param {string} header
*/
function getFieldValues (header) {
assert(header !== null)
const values = []
for (let value of header.split(',')) {
value = value.trim()
if (isValidHeaderName(value)) {
values.push(value)
}
}
return values
}
module.exports = {
urlEquals,
getFieldValues
}

12
node_modules/undici/lib/web/cookies/constants.js generated vendored Normal file
View File

@ -0,0 +1,12 @@
'use strict'
// https://wicg.github.io/cookie-store/#cookie-maximum-attribute-value-size
const maxAttributeValueSize = 1024
// https://wicg.github.io/cookie-store/#cookie-maximum-name-value-pair-size
const maxNameValuePairSize = 4096
module.exports = {
maxAttributeValueSize,
maxNameValuePairSize
}

199
node_modules/undici/lib/web/cookies/index.js generated vendored Normal file
View File

@ -0,0 +1,199 @@
'use strict'
const { parseSetCookie } = require('./parse')
const { stringify } = require('./util')
const { webidl } = require('../fetch/webidl')
const { Headers } = require('../fetch/headers')
const brandChecks = webidl.brandCheckMultiple([Headers, globalThis.Headers].filter(Boolean))
/**
* @typedef {Object} Cookie
* @property {string} name
* @property {string} value
* @property {Date|number} [expires]
* @property {number} [maxAge]
* @property {string} [domain]
* @property {string} [path]
* @property {boolean} [secure]
* @property {boolean} [httpOnly]
* @property {'Strict'|'Lax'|'None'} [sameSite]
* @property {string[]} [unparsed]
*/
/**
* @param {Headers} headers
* @returns {Record<string, string>}
*/
function getCookies (headers) {
webidl.argumentLengthCheck(arguments, 1, 'getCookies')
brandChecks(headers)
const cookie = headers.get('cookie')
/** @type {Record<string, string>} */
const out = {}
if (!cookie) {
return out
}
for (const piece of cookie.split(';')) {
const [name, ...value] = piece.split('=')
out[name.trim()] = value.join('=')
}
return out
}
/**
* @param {Headers} headers
* @param {string} name
* @param {{ path?: string, domain?: string }|undefined} attributes
* @returns {void}
*/
function deleteCookie (headers, name, attributes) {
brandChecks(headers)
const prefix = 'deleteCookie'
webidl.argumentLengthCheck(arguments, 2, prefix)
name = webidl.converters.DOMString(name, prefix, 'name')
attributes = webidl.converters.DeleteCookieAttributes(attributes)
// Matches behavior of
// https://github.com/denoland/deno_std/blob/63827b16330b82489a04614027c33b7904e08be5/http/cookie.ts#L278
setCookie(headers, {
name,
value: '',
expires: new Date(0),
...attributes
})
}
/**
* @param {Headers} headers
* @returns {Cookie[]}
*/
function getSetCookies (headers) {
webidl.argumentLengthCheck(arguments, 1, 'getSetCookies')
brandChecks(headers)
const cookies = headers.getSetCookie()
if (!cookies) {
return []
}
return cookies.map((pair) => parseSetCookie(pair))
}
/**
* Parses a cookie string
* @param {string} cookie
*/
function parseCookie (cookie) {
cookie = webidl.converters.DOMString(cookie)
return parseSetCookie(cookie)
}
/**
* @param {Headers} headers
* @param {Cookie} cookie
* @returns {void}
*/
function setCookie (headers, cookie) {
webidl.argumentLengthCheck(arguments, 2, 'setCookie')
brandChecks(headers)
cookie = webidl.converters.Cookie(cookie)
const str = stringify(cookie)
if (str) {
headers.append('set-cookie', str, true)
}
}
webidl.converters.DeleteCookieAttributes = webidl.dictionaryConverter([
{
converter: webidl.nullableConverter(webidl.converters.DOMString),
key: 'path',
defaultValue: () => null
},
{
converter: webidl.nullableConverter(webidl.converters.DOMString),
key: 'domain',
defaultValue: () => null
}
])
webidl.converters.Cookie = webidl.dictionaryConverter([
{
converter: webidl.converters.DOMString,
key: 'name'
},
{
converter: webidl.converters.DOMString,
key: 'value'
},
{
converter: webidl.nullableConverter((value) => {
if (typeof value === 'number') {
return webidl.converters['unsigned long long'](value)
}
return new Date(value)
}),
key: 'expires',
defaultValue: () => null
},
{
converter: webidl.nullableConverter(webidl.converters['long long']),
key: 'maxAge',
defaultValue: () => null
},
{
converter: webidl.nullableConverter(webidl.converters.DOMString),
key: 'domain',
defaultValue: () => null
},
{
converter: webidl.nullableConverter(webidl.converters.DOMString),
key: 'path',
defaultValue: () => null
},
{
converter: webidl.nullableConverter(webidl.converters.boolean),
key: 'secure',
defaultValue: () => null
},
{
converter: webidl.nullableConverter(webidl.converters.boolean),
key: 'httpOnly',
defaultValue: () => null
},
{
converter: webidl.converters.USVString,
key: 'sameSite',
allowedValues: ['Strict', 'Lax', 'None']
},
{
converter: webidl.sequenceConverter(webidl.converters.DOMString),
key: 'unparsed',
defaultValue: () => new Array(0)
}
])
module.exports = {
getCookies,
deleteCookie,
getSetCookies,
setCookie,
parseCookie
}

322
node_modules/undici/lib/web/cookies/parse.js generated vendored Normal file
View File

@ -0,0 +1,322 @@
'use strict'
const { maxNameValuePairSize, maxAttributeValueSize } = require('./constants')
const { isCTLExcludingHtab } = require('./util')
const { collectASequenceOfCodePointsFast } = require('../fetch/data-url')
const assert = require('node:assert')
const { unescape } = require('node:querystring')
/**
* @description Parses the field-value attributes of a set-cookie header string.
* @see https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#section-5.4
* @param {string} header
* @returns {import('./index').Cookie|null} if the header is invalid, null will be returned
*/
function parseSetCookie (header) {
// 1. If the set-cookie-string contains a %x00-08 / %x0A-1F / %x7F
// character (CTL characters excluding HTAB): Abort these steps and
// ignore the set-cookie-string entirely.
if (isCTLExcludingHtab(header)) {
return null
}
let nameValuePair = ''
let unparsedAttributes = ''
let name = ''
let value = ''
// 2. If the set-cookie-string contains a %x3B (";") character:
if (header.includes(';')) {
// 1. The name-value-pair string consists of the characters up to,
// but not including, the first %x3B (";"), and the unparsed-
// attributes consist of the remainder of the set-cookie-string
// (including the %x3B (";") in question).
const position = { position: 0 }
nameValuePair = collectASequenceOfCodePointsFast(';', header, position)
unparsedAttributes = header.slice(position.position)
} else {
// Otherwise:
// 1. The name-value-pair string consists of all the characters
// contained in the set-cookie-string, and the unparsed-
// attributes is the empty string.
nameValuePair = header
}
// 3. If the name-value-pair string lacks a %x3D ("=") character, then
// the name string is empty, and the value string is the value of
// name-value-pair.
if (!nameValuePair.includes('=')) {
value = nameValuePair
} else {
// Otherwise, the name string consists of the characters up to, but
// not including, the first %x3D ("=") character, and the (possibly
// empty) value string consists of the characters after the first
// %x3D ("=") character.
const position = { position: 0 }
name = collectASequenceOfCodePointsFast(
'=',
nameValuePair,
position
)
value = nameValuePair.slice(position.position + 1)
}
// 4. Remove any leading or trailing WSP characters from the name
// string and the value string.
name = name.trim()
value = value.trim()
// 5. If the sum of the lengths of the name string and the value string
// is more than 4096 octets, abort these steps and ignore the set-
// cookie-string entirely.
if (name.length + value.length > maxNameValuePairSize) {
return null
}
// 6. The cookie-name is the name string, and the cookie-value is the
// value string.
// https://datatracker.ietf.org/doc/html/rfc6265
// To maximize compatibility with user agents, servers that wish to
// store arbitrary data in a cookie-value SHOULD encode that data, for
// example, using Base64 [RFC4648].
return {
name, value: unescape(value), ...parseUnparsedAttributes(unparsedAttributes)
}
}
/**
* Parses the remaining attributes of a set-cookie header
* @see https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#section-5.4
* @param {string} unparsedAttributes
* @param {Object.<string, unknown>} [cookieAttributeList={}]
*/
function parseUnparsedAttributes (unparsedAttributes, cookieAttributeList = {}) {
// 1. If the unparsed-attributes string is empty, skip the rest of
// these steps.
if (unparsedAttributes.length === 0) {
return cookieAttributeList
}
// 2. Discard the first character of the unparsed-attributes (which
// will be a %x3B (";") character).
assert(unparsedAttributes[0] === ';')
unparsedAttributes = unparsedAttributes.slice(1)
let cookieAv = ''
// 3. If the remaining unparsed-attributes contains a %x3B (";")
// character:
if (unparsedAttributes.includes(';')) {
// 1. Consume the characters of the unparsed-attributes up to, but
// not including, the first %x3B (";") character.
cookieAv = collectASequenceOfCodePointsFast(
';',
unparsedAttributes,
{ position: 0 }
)
unparsedAttributes = unparsedAttributes.slice(cookieAv.length)
} else {
// Otherwise:
// 1. Consume the remainder of the unparsed-attributes.
cookieAv = unparsedAttributes
unparsedAttributes = ''
}
// Let the cookie-av string be the characters consumed in this step.
let attributeName = ''
let attributeValue = ''
// 4. If the cookie-av string contains a %x3D ("=") character:
if (cookieAv.includes('=')) {
// 1. The (possibly empty) attribute-name string consists of the
// characters up to, but not including, the first %x3D ("=")
// character, and the (possibly empty) attribute-value string
// consists of the characters after the first %x3D ("=")
// character.
const position = { position: 0 }
attributeName = collectASequenceOfCodePointsFast(
'=',
cookieAv,
position
)
attributeValue = cookieAv.slice(position.position + 1)
} else {
// Otherwise:
// 1. The attribute-name string consists of the entire cookie-av
// string, and the attribute-value string is empty.
attributeName = cookieAv
}
// 5. Remove any leading or trailing WSP characters from the attribute-
// name string and the attribute-value string.
attributeName = attributeName.trim()
attributeValue = attributeValue.trim()
// 6. If the attribute-value is longer than 1024 octets, ignore the
// cookie-av string and return to Step 1 of this algorithm.
if (attributeValue.length > maxAttributeValueSize) {
return parseUnparsedAttributes(unparsedAttributes, cookieAttributeList)
}
// 7. Process the attribute-name and attribute-value according to the
// requirements in the following subsections. (Notice that
// attributes with unrecognized attribute-names are ignored.)
const attributeNameLowercase = attributeName.toLowerCase()
// https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#section-5.4.1
// If the attribute-name case-insensitively matches the string
// "Expires", the user agent MUST process the cookie-av as follows.
if (attributeNameLowercase === 'expires') {
// 1. Let the expiry-time be the result of parsing the attribute-value
// as cookie-date (see Section 5.1.1).
const expiryTime = new Date(attributeValue)
// 2. If the attribute-value failed to parse as a cookie date, ignore
// the cookie-av.
cookieAttributeList.expires = expiryTime
} else if (attributeNameLowercase === 'max-age') {
// https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#section-5.4.2
// If the attribute-name case-insensitively matches the string "Max-
// Age", the user agent MUST process the cookie-av as follows.
// 1. If the first character of the attribute-value is not a DIGIT or a
// "-" character, ignore the cookie-av.
const charCode = attributeValue.charCodeAt(0)
if ((charCode < 48 || charCode > 57) && attributeValue[0] !== '-') {
return parseUnparsedAttributes(unparsedAttributes, cookieAttributeList)
}
// 2. If the remainder of attribute-value contains a non-DIGIT
// character, ignore the cookie-av.
if (!/^\d+$/.test(attributeValue)) {
return parseUnparsedAttributes(unparsedAttributes, cookieAttributeList)
}
// 3. Let delta-seconds be the attribute-value converted to an integer.
const deltaSeconds = Number(attributeValue)
// 4. Let cookie-age-limit be the maximum age of the cookie (which
// SHOULD be 400 days or less, see Section 4.1.2.2).
// 5. Set delta-seconds to the smaller of its present value and cookie-
// age-limit.
// deltaSeconds = Math.min(deltaSeconds * 1000, maxExpiresMs)
// 6. If delta-seconds is less than or equal to zero (0), let expiry-
// time be the earliest representable date and time. Otherwise, let
// the expiry-time be the current date and time plus delta-seconds
// seconds.
// const expiryTime = deltaSeconds <= 0 ? Date.now() : Date.now() + deltaSeconds
// 7. Append an attribute to the cookie-attribute-list with an
// attribute-name of Max-Age and an attribute-value of expiry-time.
cookieAttributeList.maxAge = deltaSeconds
} else if (attributeNameLowercase === 'domain') {
// https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#section-5.4.3
// If the attribute-name case-insensitively matches the string "Domain",
// the user agent MUST process the cookie-av as follows.
// 1. Let cookie-domain be the attribute-value.
let cookieDomain = attributeValue
// 2. If cookie-domain starts with %x2E ("."), let cookie-domain be
// cookie-domain without its leading %x2E (".").
if (cookieDomain[0] === '.') {
cookieDomain = cookieDomain.slice(1)
}
// 3. Convert the cookie-domain to lower case.
cookieDomain = cookieDomain.toLowerCase()
// 4. Append an attribute to the cookie-attribute-list with an
// attribute-name of Domain and an attribute-value of cookie-domain.
cookieAttributeList.domain = cookieDomain
} else if (attributeNameLowercase === 'path') {
// https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#section-5.4.4
// If the attribute-name case-insensitively matches the string "Path",
// the user agent MUST process the cookie-av as follows.
// 1. If the attribute-value is empty or if the first character of the
// attribute-value is not %x2F ("/"):
let cookiePath = ''
if (attributeValue.length === 0 || attributeValue[0] !== '/') {
// 1. Let cookie-path be the default-path.
cookiePath = '/'
} else {
// Otherwise:
// 1. Let cookie-path be the attribute-value.
cookiePath = attributeValue
}
// 2. Append an attribute to the cookie-attribute-list with an
// attribute-name of Path and an attribute-value of cookie-path.
cookieAttributeList.path = cookiePath
} else if (attributeNameLowercase === 'secure') {
// https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#section-5.4.5
// If the attribute-name case-insensitively matches the string "Secure",
// the user agent MUST append an attribute to the cookie-attribute-list
// with an attribute-name of Secure and an empty attribute-value.
cookieAttributeList.secure = true
} else if (attributeNameLowercase === 'httponly') {
// https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#section-5.4.6
// If the attribute-name case-insensitively matches the string
// "HttpOnly", the user agent MUST append an attribute to the cookie-
// attribute-list with an attribute-name of HttpOnly and an empty
// attribute-value.
cookieAttributeList.httpOnly = true
} else if (attributeNameLowercase === 'samesite') {
// https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#section-5.4.7
// If the attribute-name case-insensitively matches the string
// "SameSite", the user agent MUST process the cookie-av as follows:
// 1. Let enforcement be "Default".
let enforcement = 'Default'
const attributeValueLowercase = attributeValue.toLowerCase()
// 2. If cookie-av's attribute-value is a case-insensitive match for
// "None", set enforcement to "None".
if (attributeValueLowercase.includes('none')) {
enforcement = 'None'
}
// 3. If cookie-av's attribute-value is a case-insensitive match for
// "Strict", set enforcement to "Strict".
if (attributeValueLowercase.includes('strict')) {
enforcement = 'Strict'
}
// 4. If cookie-av's attribute-value is a case-insensitive match for
// "Lax", set enforcement to "Lax".
if (attributeValueLowercase.includes('lax')) {
enforcement = 'Lax'
}
// 5. Append an attribute to the cookie-attribute-list with an
// attribute-name of "SameSite" and an attribute-value of
// enforcement.
cookieAttributeList.sameSite = enforcement
} else {
cookieAttributeList.unparsed ??= []
cookieAttributeList.unparsed.push(`${attributeName}=${attributeValue}`)
}
// 8. Return to Step 1 of this algorithm.
return parseUnparsedAttributes(unparsedAttributes, cookieAttributeList)
}
module.exports = {
parseSetCookie,
parseUnparsedAttributes
}

282
node_modules/undici/lib/web/cookies/util.js generated vendored Normal file
View File

@ -0,0 +1,282 @@
'use strict'
/**
* @param {string} value
* @returns {boolean}
*/
function isCTLExcludingHtab (value) {
for (let i = 0; i < value.length; ++i) {
const code = value.charCodeAt(i)
if (
(code >= 0x00 && code <= 0x08) ||
(code >= 0x0A && code <= 0x1F) ||
code === 0x7F
) {
return true
}
}
return false
}
/**
CHAR = <any US-ASCII character (octets 0 - 127)>
token = 1*<any CHAR except CTLs or separators>
separators = "(" | ")" | "<" | ">" | "@"
| "," | ";" | ":" | "\" | <">
| "/" | "[" | "]" | "?" | "="
| "{" | "}" | SP | HT
* @param {string} name
*/
function validateCookieName (name) {
for (let i = 0; i < name.length; ++i) {
const code = name.charCodeAt(i)
if (
code < 0x21 || // exclude CTLs (0-31), SP and HT
code > 0x7E || // exclude non-ascii and DEL
code === 0x22 || // "
code === 0x28 || // (
code === 0x29 || // )
code === 0x3C || // <
code === 0x3E || // >
code === 0x40 || // @
code === 0x2C || // ,
code === 0x3B || // ;
code === 0x3A || // :
code === 0x5C || // \
code === 0x2F || // /
code === 0x5B || // [
code === 0x5D || // ]
code === 0x3F || // ?
code === 0x3D || // =
code === 0x7B || // {
code === 0x7D // }
) {
throw new Error('Invalid cookie name')
}
}
}
/**
cookie-value = *cookie-octet / ( DQUOTE *cookie-octet DQUOTE )
cookie-octet = %x21 / %x23-2B / %x2D-3A / %x3C-5B / %x5D-7E
; US-ASCII characters excluding CTLs,
; whitespace DQUOTE, comma, semicolon,
; and backslash
* @param {string} value
*/
function validateCookieValue (value) {
let len = value.length
let i = 0
// if the value is wrapped in DQUOTE
if (value[0] === '"') {
if (len === 1 || value[len - 1] !== '"') {
throw new Error('Invalid cookie value')
}
--len
++i
}
while (i < len) {
const code = value.charCodeAt(i++)
if (
code < 0x21 || // exclude CTLs (0-31)
code > 0x7E || // non-ascii and DEL (127)
code === 0x22 || // "
code === 0x2C || // ,
code === 0x3B || // ;
code === 0x5C // \
) {
throw new Error('Invalid cookie value')
}
}
}
/**
* path-value = <any CHAR except CTLs or ";">
* @param {string} path
*/
function validateCookiePath (path) {
for (let i = 0; i < path.length; ++i) {
const code = path.charCodeAt(i)
if (
code < 0x20 || // exclude CTLs (0-31)
code === 0x7F || // DEL
code === 0x3B // ;
) {
throw new Error('Invalid cookie path')
}
}
}
/**
* I have no idea why these values aren't allowed to be honest,
* but Deno tests these. - Khafra
* @param {string} domain
*/
function validateCookieDomain (domain) {
if (
domain.startsWith('-') ||
domain.endsWith('.') ||
domain.endsWith('-')
) {
throw new Error('Invalid cookie domain')
}
}
const IMFDays = [
'Sun', 'Mon', 'Tue', 'Wed',
'Thu', 'Fri', 'Sat'
]
const IMFMonths = [
'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'
]
const IMFPaddedNumbers = Array(61).fill(0).map((_, i) => i.toString().padStart(2, '0'))
/**
* @see https://www.rfc-editor.org/rfc/rfc7231#section-7.1.1.1
* @param {number|Date} date
IMF-fixdate = day-name "," SP date1 SP time-of-day SP GMT
; fixed length/zone/capitalization subset of the format
; see Section 3.3 of [RFC5322]
day-name = %x4D.6F.6E ; "Mon", case-sensitive
/ %x54.75.65 ; "Tue", case-sensitive
/ %x57.65.64 ; "Wed", case-sensitive
/ %x54.68.75 ; "Thu", case-sensitive
/ %x46.72.69 ; "Fri", case-sensitive
/ %x53.61.74 ; "Sat", case-sensitive
/ %x53.75.6E ; "Sun", case-sensitive
date1 = day SP month SP year
; e.g., 02 Jun 1982
day = 2DIGIT
month = %x4A.61.6E ; "Jan", case-sensitive
/ %x46.65.62 ; "Feb", case-sensitive
/ %x4D.61.72 ; "Mar", case-sensitive
/ %x41.70.72 ; "Apr", case-sensitive
/ %x4D.61.79 ; "May", case-sensitive
/ %x4A.75.6E ; "Jun", case-sensitive
/ %x4A.75.6C ; "Jul", case-sensitive
/ %x41.75.67 ; "Aug", case-sensitive
/ %x53.65.70 ; "Sep", case-sensitive
/ %x4F.63.74 ; "Oct", case-sensitive
/ %x4E.6F.76 ; "Nov", case-sensitive
/ %x44.65.63 ; "Dec", case-sensitive
year = 4DIGIT
GMT = %x47.4D.54 ; "GMT", case-sensitive
time-of-day = hour ":" minute ":" second
; 00:00:00 - 23:59:60 (leap second)
hour = 2DIGIT
minute = 2DIGIT
second = 2DIGIT
*/
function toIMFDate (date) {
if (typeof date === 'number') {
date = new Date(date)
}
return `${IMFDays[date.getUTCDay()]}, ${IMFPaddedNumbers[date.getUTCDate()]} ${IMFMonths[date.getUTCMonth()]} ${date.getUTCFullYear()} ${IMFPaddedNumbers[date.getUTCHours()]}:${IMFPaddedNumbers[date.getUTCMinutes()]}:${IMFPaddedNumbers[date.getUTCSeconds()]} GMT`
}
/**
max-age-av = "Max-Age=" non-zero-digit *DIGIT
; In practice, both expires-av and max-age-av
; are limited to dates representable by the
; user agent.
* @param {number} maxAge
*/
function validateCookieMaxAge (maxAge) {
if (maxAge < 0) {
throw new Error('Invalid cookie max-age')
}
}
/**
* @see https://www.rfc-editor.org/rfc/rfc6265#section-4.1.1
* @param {import('./index').Cookie} cookie
*/
function stringify (cookie) {
if (cookie.name.length === 0) {
return null
}
validateCookieName(cookie.name)
validateCookieValue(cookie.value)
const out = [`${cookie.name}=${cookie.value}`]
// https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-cookie-prefixes-00#section-3.1
// https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-cookie-prefixes-00#section-3.2
if (cookie.name.startsWith('__Secure-')) {
cookie.secure = true
}
if (cookie.name.startsWith('__Host-')) {
cookie.secure = true
cookie.domain = null
cookie.path = '/'
}
if (cookie.secure) {
out.push('Secure')
}
if (cookie.httpOnly) {
out.push('HttpOnly')
}
if (typeof cookie.maxAge === 'number') {
validateCookieMaxAge(cookie.maxAge)
out.push(`Max-Age=${cookie.maxAge}`)
}
if (cookie.domain) {
validateCookieDomain(cookie.domain)
out.push(`Domain=${cookie.domain}`)
}
if (cookie.path) {
validateCookiePath(cookie.path)
out.push(`Path=${cookie.path}`)
}
if (cookie.expires && cookie.expires.toString() !== 'Invalid Date') {
out.push(`Expires=${toIMFDate(cookie.expires)}`)
}
if (cookie.sameSite) {
out.push(`SameSite=${cookie.sameSite}`)
}
for (const part of cookie.unparsed) {
if (!part.includes('=')) {
throw new Error('Invalid unparsed')
}
const [key, ...value] = part.split('=')
out.push(`${key.trim()}=${value.join('=')}`)
}
return out.join('; ')
}
module.exports = {
isCTLExcludingHtab,
validateCookieName,
validateCookiePath,
validateCookieValue,
toIMFDate,
stringify
}

View File

@ -0,0 +1,399 @@
'use strict'
const { Transform } = require('node:stream')
const { isASCIINumber, isValidLastEventId } = require('./util')
/**
* @type {number[]} BOM
*/
const BOM = [0xEF, 0xBB, 0xBF]
/**
* @type {10} LF
*/
const LF = 0x0A
/**
* @type {13} CR
*/
const CR = 0x0D
/**
* @type {58} COLON
*/
const COLON = 0x3A
/**
* @type {32} SPACE
*/
const SPACE = 0x20
/**
* @typedef {object} EventSourceStreamEvent
* @type {object}
* @property {string} [event] The event type.
* @property {string} [data] The data of the message.
* @property {string} [id] A unique ID for the event.
* @property {string} [retry] The reconnection time, in milliseconds.
*/
/**
* @typedef eventSourceSettings
* @type {object}
* @property {string} [lastEventId] The last event ID received from the server.
* @property {string} [origin] The origin of the event source.
* @property {number} [reconnectionTime] The reconnection time, in milliseconds.
*/
class EventSourceStream extends Transform {
/**
* @type {eventSourceSettings}
*/
state
/**
* Leading byte-order-mark check.
* @type {boolean}
*/
checkBOM = true
/**
* @type {boolean}
*/
crlfCheck = false
/**
* @type {boolean}
*/
eventEndCheck = false
/**
* @type {Buffer|null}
*/
buffer = null
pos = 0
event = {
data: undefined,
event: undefined,
id: undefined,
retry: undefined
}
/**
* @param {object} options
* @param {boolean} [options.readableObjectMode]
* @param {eventSourceSettings} [options.eventSourceSettings]
* @param {(chunk: any, encoding?: BufferEncoding | undefined) => boolean} [options.push]
*/
constructor (options = {}) {
// Enable object mode as EventSourceStream emits objects of shape
// EventSourceStreamEvent
options.readableObjectMode = true
super(options)
this.state = options.eventSourceSettings || {}
if (options.push) {
this.push = options.push
}
}
/**
* @param {Buffer} chunk
* @param {string} _encoding
* @param {Function} callback
* @returns {void}
*/
_transform (chunk, _encoding, callback) {
if (chunk.length === 0) {
callback()
return
}
// Cache the chunk in the buffer, as the data might not be complete while
// processing it
// TODO: Investigate if there is a more performant way to handle
// incoming chunks
// see: https://github.com/nodejs/undici/issues/2630
if (this.buffer) {
this.buffer = Buffer.concat([this.buffer, chunk])
} else {
this.buffer = chunk
}
// Strip leading byte-order-mark if we opened the stream and started
// the processing of the incoming data
if (this.checkBOM) {
switch (this.buffer.length) {
case 1:
// Check if the first byte is the same as the first byte of the BOM
if (this.buffer[0] === BOM[0]) {
// If it is, we need to wait for more data
callback()
return
}
// Set the checkBOM flag to false as we don't need to check for the
// BOM anymore
this.checkBOM = false
// The buffer only contains one byte so we need to wait for more data
callback()
return
case 2:
// Check if the first two bytes are the same as the first two bytes
// of the BOM
if (
this.buffer[0] === BOM[0] &&
this.buffer[1] === BOM[1]
) {
// If it is, we need to wait for more data, because the third byte
// is needed to determine if it is the BOM or not
callback()
return
}
// Set the checkBOM flag to false as we don't need to check for the
// BOM anymore
this.checkBOM = false
break
case 3:
// Check if the first three bytes are the same as the first three
// bytes of the BOM
if (
this.buffer[0] === BOM[0] &&
this.buffer[1] === BOM[1] &&
this.buffer[2] === BOM[2]
) {
// If it is, we can drop the buffered data, as it is only the BOM
this.buffer = Buffer.alloc(0)
// Set the checkBOM flag to false as we don't need to check for the
// BOM anymore
this.checkBOM = false
// Await more data
callback()
return
}
// If it is not the BOM, we can start processing the data
this.checkBOM = false
break
default:
// The buffer is longer than 3 bytes, so we can drop the BOM if it is
// present
if (
this.buffer[0] === BOM[0] &&
this.buffer[1] === BOM[1] &&
this.buffer[2] === BOM[2]
) {
// Remove the BOM from the buffer
this.buffer = this.buffer.subarray(3)
}
// Set the checkBOM flag to false as we don't need to check for the
this.checkBOM = false
break
}
}
while (this.pos < this.buffer.length) {
// If the previous line ended with an end-of-line, we need to check
// if the next character is also an end-of-line.
if (this.eventEndCheck) {
// If the the current character is an end-of-line, then the event
// is finished and we can process it
// If the previous line ended with a carriage return, we need to
// check if the current character is a line feed and remove it
// from the buffer.
if (this.crlfCheck) {
// If the current character is a line feed, we can remove it
// from the buffer and reset the crlfCheck flag
if (this.buffer[this.pos] === LF) {
this.buffer = this.buffer.subarray(this.pos + 1)
this.pos = 0
this.crlfCheck = false
// It is possible that the line feed is not the end of the
// event. We need to check if the next character is an
// end-of-line character to determine if the event is
// finished. We simply continue the loop to check the next
// character.
// As we removed the line feed from the buffer and set the
// crlfCheck flag to false, we basically don't make any
// distinction between a line feed and a carriage return.
continue
}
this.crlfCheck = false
}
if (this.buffer[this.pos] === LF || this.buffer[this.pos] === CR) {
// If the current character is a carriage return, we need to
// set the crlfCheck flag to true, as we need to check if the
// next character is a line feed so we can remove it from the
// buffer
if (this.buffer[this.pos] === CR) {
this.crlfCheck = true
}
this.buffer = this.buffer.subarray(this.pos + 1)
this.pos = 0
if (
this.event.data !== undefined || this.event.event || this.event.id || this.event.retry) {
this.processEvent(this.event)
}
this.clearEvent()
continue
}
// If the current character is not an end-of-line, then the event
// is not finished and we have to reset the eventEndCheck flag
this.eventEndCheck = false
continue
}
// If the current character is an end-of-line, we can process the
// line
if (this.buffer[this.pos] === LF || this.buffer[this.pos] === CR) {
// If the current character is a carriage return, we need to
// set the crlfCheck flag to true, as we need to check if the
// next character is a line feed
if (this.buffer[this.pos] === CR) {
this.crlfCheck = true
}
// In any case, we can process the line as we reached an
// end-of-line character
this.parseLine(this.buffer.subarray(0, this.pos), this.event)
// Remove the processed line from the buffer
this.buffer = this.buffer.subarray(this.pos + 1)
// Reset the position as we removed the processed line from the buffer
this.pos = 0
// A line was processed and this could be the end of the event. We need
// to check if the next line is empty to determine if the event is
// finished.
this.eventEndCheck = true
continue
}
this.pos++
}
callback()
}
/**
* @param {Buffer} line
* @param {EventSourceStreamEvent} event
*/
parseLine (line, event) {
// If the line is empty (a blank line)
// Dispatch the event, as defined below.
// This will be handled in the _transform method
if (line.length === 0) {
return
}
// If the line starts with a U+003A COLON character (:)
// Ignore the line.
const colonPosition = line.indexOf(COLON)
if (colonPosition === 0) {
return
}
let field = ''
let value = ''
// If the line contains a U+003A COLON character (:)
if (colonPosition !== -1) {
// Collect the characters on the line before the first U+003A COLON
// character (:), and let field be that string.
// TODO: Investigate if there is a more performant way to extract the
// field
// see: https://github.com/nodejs/undici/issues/2630
field = line.subarray(0, colonPosition).toString('utf8')
// Collect the characters on the line after the first U+003A COLON
// character (:), and let value be that string.
// If value starts with a U+0020 SPACE character, remove it from value.
let valueStart = colonPosition + 1
if (line[valueStart] === SPACE) {
++valueStart
}
// TODO: Investigate if there is a more performant way to extract the
// value
// see: https://github.com/nodejs/undici/issues/2630
value = line.subarray(valueStart).toString('utf8')
// Otherwise, the string is not empty but does not contain a U+003A COLON
// character (:)
} else {
// Process the field using the steps described below, using the whole
// line as the field name, and the empty string as the field value.
field = line.toString('utf8')
value = ''
}
// Modify the event with the field name and value. The value is also
// decoded as UTF-8
switch (field) {
case 'data':
if (event[field] === undefined) {
event[field] = value
} else {
event[field] += `\n${value}`
}
break
case 'retry':
if (isASCIINumber(value)) {
event[field] = value
}
break
case 'id':
if (isValidLastEventId(value)) {
event[field] = value
}
break
case 'event':
if (value.length > 0) {
event[field] = value
}
break
}
}
/**
* @param {EventSourceStreamEvent} event
*/
processEvent (event) {
if (event.retry && isASCIINumber(event.retry)) {
this.state.reconnectionTime = parseInt(event.retry, 10)
}
if (event.id && isValidLastEventId(event.id)) {
this.state.lastEventId = event.id
}
// only dispatch event, when data is provided
if (event.data !== undefined) {
this.push({
type: event.event || 'message',
options: {
data: event.data,
lastEventId: this.state.lastEventId,
origin: this.state.origin
}
})
}
}
clearEvent () {
this.event = {
data: undefined,
event: undefined,
id: undefined,
retry: undefined
}
}
}
module.exports = {
EventSourceStream
}

484
node_modules/undici/lib/web/eventsource/eventsource.js generated vendored Normal file
View File

@ -0,0 +1,484 @@
'use strict'
const { pipeline } = require('node:stream')
const { fetching } = require('../fetch')
const { makeRequest } = require('../fetch/request')
const { webidl } = require('../fetch/webidl')
const { EventSourceStream } = require('./eventsource-stream')
const { parseMIMEType } = require('../fetch/data-url')
const { createFastMessageEvent } = require('../websocket/events')
const { isNetworkError } = require('../fetch/response')
const { delay } = require('./util')
const { kEnumerableProperty } = require('../../core/util')
const { environmentSettingsObject } = require('../fetch/util')
let experimentalWarned = false
/**
* A reconnection time, in milliseconds. This must initially be an implementation-defined value,
* probably in the region of a few seconds.
*
* In Comparison:
* - Chrome uses 3000ms.
* - Deno uses 5000ms.
*
* @type {3000}
*/
const defaultReconnectionTime = 3000
/**
* The readyState attribute represents the state of the connection.
* @typedef ReadyState
* @type {0|1|2}
* @readonly
* @see https://html.spec.whatwg.org/multipage/server-sent-events.html#dom-eventsource-readystate-dev
*/
/**
* The connection has not yet been established, or it was closed and the user
* agent is reconnecting.
* @type {0}
*/
const CONNECTING = 0
/**
* The user agent has an open connection and is dispatching events as it
* receives them.
* @type {1}
*/
const OPEN = 1
/**
* The connection is not open, and the user agent is not trying to reconnect.
* @type {2}
*/
const CLOSED = 2
/**
* Requests for the element will have their mode set to "cors" and their credentials mode set to "same-origin".
* @type {'anonymous'}
*/
const ANONYMOUS = 'anonymous'
/**
* Requests for the element will have their mode set to "cors" and their credentials mode set to "include".
* @type {'use-credentials'}
*/
const USE_CREDENTIALS = 'use-credentials'
/**
* The EventSource interface is used to receive server-sent events. It
* connects to a server over HTTP and receives events in text/event-stream
* format without closing the connection.
* @extends {EventTarget}
* @see https://html.spec.whatwg.org/multipage/server-sent-events.html#server-sent-events
* @api public
*/
class EventSource extends EventTarget {
#events = {
open: null,
error: null,
message: null
}
#url
#withCredentials = false
/**
* @type {ReadyState}
*/
#readyState = CONNECTING
#request = null
#controller = null
#dispatcher
/**
* @type {import('./eventsource-stream').eventSourceSettings}
*/
#state
/**
* Creates a new EventSource object.
* @param {string} url
* @param {EventSourceInit} [eventSourceInitDict={}]
* @see https://html.spec.whatwg.org/multipage/server-sent-events.html#the-eventsource-interface
*/
constructor (url, eventSourceInitDict = {}) {
// 1. Let ev be a new EventSource object.
super()
webidl.util.markAsUncloneable(this)
const prefix = 'EventSource constructor'
webidl.argumentLengthCheck(arguments, 1, prefix)
if (!experimentalWarned) {
experimentalWarned = true
process.emitWarning('EventSource is experimental, expect them to change at any time.', {
code: 'UNDICI-ES'
})
}
url = webidl.converters.USVString(url)
eventSourceInitDict = webidl.converters.EventSourceInitDict(eventSourceInitDict, prefix, 'eventSourceInitDict')
this.#dispatcher = eventSourceInitDict.dispatcher
this.#state = {
lastEventId: '',
reconnectionTime: defaultReconnectionTime
}
// 2. Let settings be ev's relevant settings object.
// https://html.spec.whatwg.org/multipage/webappapis.html#environment-settings-object
const settings = environmentSettingsObject
let urlRecord
try {
// 3. Let urlRecord be the result of encoding-parsing a URL given url, relative to settings.
urlRecord = new URL(url, settings.settingsObject.baseUrl)
this.#state.origin = urlRecord.origin
} catch (e) {
// 4. If urlRecord is failure, then throw a "SyntaxError" DOMException.
throw new DOMException(e, 'SyntaxError')
}
// 5. Set ev's url to urlRecord.
this.#url = urlRecord.href
// 6. Let corsAttributeState be Anonymous.
let corsAttributeState = ANONYMOUS
// 7. If the value of eventSourceInitDict's withCredentials member is true,
// then set corsAttributeState to Use Credentials and set ev's
// withCredentials attribute to true.
if (eventSourceInitDict.withCredentials === true) {
corsAttributeState = USE_CREDENTIALS
this.#withCredentials = true
}
// 8. Let request be the result of creating a potential-CORS request given
// urlRecord, the empty string, and corsAttributeState.
const initRequest = {
redirect: 'follow',
keepalive: true,
// @see https://html.spec.whatwg.org/multipage/urls-and-fetching.html#cors-settings-attributes
mode: 'cors',
credentials: corsAttributeState === 'anonymous'
? 'same-origin'
: 'omit',
referrer: 'no-referrer'
}
// 9. Set request's client to settings.
initRequest.client = environmentSettingsObject.settingsObject
// 10. User agents may set (`Accept`, `text/event-stream`) in request's header list.
initRequest.headersList = [['accept', { name: 'accept', value: 'text/event-stream' }]]
// 11. Set request's cache mode to "no-store".
initRequest.cache = 'no-store'
// 12. Set request's initiator type to "other".
initRequest.initiator = 'other'
initRequest.urlList = [new URL(this.#url)]
// 13. Set ev's request to request.
this.#request = makeRequest(initRequest)
this.#connect()
}
/**
* Returns the state of this EventSource object's connection. It can have the
* values described below.
* @returns {ReadyState}
* @readonly
*/
get readyState () {
return this.#readyState
}
/**
* Returns the URL providing the event stream.
* @readonly
* @returns {string}
*/
get url () {
return this.#url
}
/**
* Returns a boolean indicating whether the EventSource object was
* instantiated with CORS credentials set (true), or not (false, the default).
*/
get withCredentials () {
return this.#withCredentials
}
#connect () {
if (this.#readyState === CLOSED) return
this.#readyState = CONNECTING
const fetchParams = {
request: this.#request,
dispatcher: this.#dispatcher
}
// 14. Let processEventSourceEndOfBody given response res be the following step: if res is not a network error, then reestablish the connection.
const processEventSourceEndOfBody = (response) => {
if (isNetworkError(response)) {
this.dispatchEvent(new Event('error'))
this.close()
}
this.#reconnect()
}
// 15. Fetch request, with processResponseEndOfBody set to processEventSourceEndOfBody...
fetchParams.processResponseEndOfBody = processEventSourceEndOfBody
// and processResponse set to the following steps given response res:
fetchParams.processResponse = (response) => {
// 1. If res is an aborted network error, then fail the connection.
if (isNetworkError(response)) {
// 1. When a user agent is to fail the connection, the user agent
// must queue a task which, if the readyState attribute is set to a
// value other than CLOSED, sets the readyState attribute to CLOSED
// and fires an event named error at the EventSource object. Once the
// user agent has failed the connection, it does not attempt to
// reconnect.
if (response.aborted) {
this.close()
this.dispatchEvent(new Event('error'))
return
// 2. Otherwise, if res is a network error, then reestablish the
// connection, unless the user agent knows that to be futile, in
// which case the user agent may fail the connection.
} else {
this.#reconnect()
return
}
}
// 3. Otherwise, if res's status is not 200, or if res's `Content-Type`
// is not `text/event-stream`, then fail the connection.
const contentType = response.headersList.get('content-type', true)
const mimeType = contentType !== null ? parseMIMEType(contentType) : 'failure'
const contentTypeValid = mimeType !== 'failure' && mimeType.essence === 'text/event-stream'
if (
response.status !== 200 ||
contentTypeValid === false
) {
this.close()
this.dispatchEvent(new Event('error'))
return
}
// 4. Otherwise, announce the connection and interpret res's body
// line by line.
// When a user agent is to announce the connection, the user agent
// must queue a task which, if the readyState attribute is set to a
// value other than CLOSED, sets the readyState attribute to OPEN
// and fires an event named open at the EventSource object.
// @see https://html.spec.whatwg.org/multipage/server-sent-events.html#sse-processing-model
this.#readyState = OPEN
this.dispatchEvent(new Event('open'))
// If redirected to a different origin, set the origin to the new origin.
this.#state.origin = response.urlList[response.urlList.length - 1].origin
const eventSourceStream = new EventSourceStream({
eventSourceSettings: this.#state,
push: (event) => {
this.dispatchEvent(createFastMessageEvent(
event.type,
event.options
))
}
})
pipeline(response.body.stream,
eventSourceStream,
(error) => {
if (
error?.aborted === false
) {
this.close()
this.dispatchEvent(new Event('error'))
}
})
}
this.#controller = fetching(fetchParams)
}
/**
* @see https://html.spec.whatwg.org/multipage/server-sent-events.html#sse-processing-model
* @returns {Promise<void>}
*/
async #reconnect () {
// When a user agent is to reestablish the connection, the user agent must
// run the following steps. These steps are run in parallel, not as part of
// a task. (The tasks that it queues, of course, are run like normal tasks
// and not themselves in parallel.)
// 1. Queue a task to run the following steps:
// 1. If the readyState attribute is set to CLOSED, abort the task.
if (this.#readyState === CLOSED) return
// 2. Set the readyState attribute to CONNECTING.
this.#readyState = CONNECTING
// 3. Fire an event named error at the EventSource object.
this.dispatchEvent(new Event('error'))
// 2. Wait a delay equal to the reconnection time of the event source.
await delay(this.#state.reconnectionTime)
// 5. Queue a task to run the following steps:
// 1. If the EventSource object's readyState attribute is not set to
// CONNECTING, then return.
if (this.#readyState !== CONNECTING) return
// 2. Let request be the EventSource object's request.
// 3. If the EventSource object's last event ID string is not the empty
// string, then:
// 1. Let lastEventIDValue be the EventSource object's last event ID
// string, encoded as UTF-8.
// 2. Set (`Last-Event-ID`, lastEventIDValue) in request's header
// list.
if (this.#state.lastEventId.length) {
this.#request.headersList.set('last-event-id', this.#state.lastEventId, true)
}
// 4. Fetch request and process the response obtained in this fashion, if any, as described earlier in this section.
this.#connect()
}
/**
* Closes the connection, if any, and sets the readyState attribute to
* CLOSED.
*/
close () {
webidl.brandCheck(this, EventSource)
if (this.#readyState === CLOSED) return
this.#readyState = CLOSED
this.#controller.abort()
this.#request = null
}
get onopen () {
return this.#events.open
}
set onopen (fn) {
if (this.#events.open) {
this.removeEventListener('open', this.#events.open)
}
if (typeof fn === 'function') {
this.#events.open = fn
this.addEventListener('open', fn)
} else {
this.#events.open = null
}
}
get onmessage () {
return this.#events.message
}
set onmessage (fn) {
if (this.#events.message) {
this.removeEventListener('message', this.#events.message)
}
if (typeof fn === 'function') {
this.#events.message = fn
this.addEventListener('message', fn)
} else {
this.#events.message = null
}
}
get onerror () {
return this.#events.error
}
set onerror (fn) {
if (this.#events.error) {
this.removeEventListener('error', this.#events.error)
}
if (typeof fn === 'function') {
this.#events.error = fn
this.addEventListener('error', fn)
} else {
this.#events.error = null
}
}
}
const constantsPropertyDescriptors = {
CONNECTING: {
__proto__: null,
configurable: false,
enumerable: true,
value: CONNECTING,
writable: false
},
OPEN: {
__proto__: null,
configurable: false,
enumerable: true,
value: OPEN,
writable: false
},
CLOSED: {
__proto__: null,
configurable: false,
enumerable: true,
value: CLOSED,
writable: false
}
}
Object.defineProperties(EventSource, constantsPropertyDescriptors)
Object.defineProperties(EventSource.prototype, constantsPropertyDescriptors)
Object.defineProperties(EventSource.prototype, {
close: kEnumerableProperty,
onerror: kEnumerableProperty,
onmessage: kEnumerableProperty,
onopen: kEnumerableProperty,
readyState: kEnumerableProperty,
url: kEnumerableProperty,
withCredentials: kEnumerableProperty
})
webidl.converters.EventSourceInitDict = webidl.dictionaryConverter([
{
key: 'withCredentials',
converter: webidl.converters.boolean,
defaultValue: () => false
},
{
key: 'dispatcher', // undici only
converter: webidl.converters.any
}
])
module.exports = {
EventSource,
defaultReconnectionTime
}

37
node_modules/undici/lib/web/eventsource/util.js generated vendored Normal file
View File

@ -0,0 +1,37 @@
'use strict'
/**
* Checks if the given value is a valid LastEventId.
* @param {string} value
* @returns {boolean}
*/
function isValidLastEventId (value) {
// LastEventId should not contain U+0000 NULL
return value.indexOf('\u0000') === -1
}
/**
* Checks if the given value is a base 10 digit.
* @param {string} value
* @returns {boolean}
*/
function isASCIINumber (value) {
if (value.length === 0) return false
for (let i = 0; i < value.length; i++) {
if (value.charCodeAt(i) < 0x30 || value.charCodeAt(i) > 0x39) return false
}
return true
}
// https://github.com/nodejs/undici/issues/2664
function delay (ms) {
return new Promise((resolve) => {
setTimeout(resolve, ms).unref()
})
}
module.exports = {
isValidLastEventId,
isASCIINumber,
delay
}

532
node_modules/undici/lib/web/fetch/body.js generated vendored Normal file
View File

@ -0,0 +1,532 @@
'use strict'
const util = require('../../core/util')
const {
ReadableStreamFrom,
readableStreamClose,
createDeferredPromise,
fullyReadBody,
extractMimeType,
utf8DecodeBytes
} = require('./util')
const { FormData, setFormDataState } = require('./formdata')
const { webidl } = require('./webidl')
const { Blob } = require('node:buffer')
const assert = require('node:assert')
const { isErrored, isDisturbed } = require('node:stream')
const { isArrayBuffer } = require('node:util/types')
const { serializeAMimeType } = require('./data-url')
const { multipartFormDataParser } = require('./formdata-parser')
let random
try {
const crypto = require('node:crypto')
random = (max) => crypto.randomInt(0, max)
} catch {
random = (max) => Math.floor(Math.random() * max)
}
const textEncoder = new TextEncoder()
function noop () {}
const hasFinalizationRegistry = globalThis.FinalizationRegistry && process.version.indexOf('v18') !== 0
let streamRegistry
if (hasFinalizationRegistry) {
streamRegistry = new FinalizationRegistry((weakRef) => {
const stream = weakRef.deref()
if (stream && !stream.locked && !isDisturbed(stream) && !isErrored(stream)) {
stream.cancel('Response object has been garbage collected').catch(noop)
}
})
}
// https://fetch.spec.whatwg.org/#concept-bodyinit-extract
function extractBody (object, keepalive = false) {
// 1. Let stream be null.
let stream = null
// 2. If object is a ReadableStream object, then set stream to object.
if (webidl.is.ReadableStream(object)) {
stream = object
} else if (webidl.is.Blob(object)) {
// 3. Otherwise, if object is a Blob object, set stream to the
// result of running objects get stream.
stream = object.stream()
} else {
// 4. Otherwise, set stream to a new ReadableStream object, and set
// up stream with byte reading support.
stream = new ReadableStream({
async pull (controller) {
const buffer = typeof source === 'string' ? textEncoder.encode(source) : source
if (buffer.byteLength) {
controller.enqueue(buffer)
}
queueMicrotask(() => readableStreamClose(controller))
},
start () {},
type: 'bytes'
})
}
// 5. Assert: stream is a ReadableStream object.
assert(webidl.is.ReadableStream(stream))
// 6. Let action be null.
let action = null
// 7. Let source be null.
let source = null
// 8. Let length be null.
let length = null
// 9. Let type be null.
let type = null
// 10. Switch on object:
if (typeof object === 'string') {
// Set source to the UTF-8 encoding of object.
// Note: setting source to a Uint8Array here breaks some mocking assumptions.
source = object
// Set type to `text/plain;charset=UTF-8`.
type = 'text/plain;charset=UTF-8'
} else if (webidl.is.URLSearchParams(object)) {
// URLSearchParams
// spec says to run application/x-www-form-urlencoded on body.list
// this is implemented in Node.js as apart of an URLSearchParams instance toString method
// See: https://github.com/nodejs/node/blob/e46c680bf2b211bbd52cf959ca17ee98c7f657f5/lib/internal/url.js#L490
// and https://github.com/nodejs/node/blob/e46c680bf2b211bbd52cf959ca17ee98c7f657f5/lib/internal/url.js#L1100
// Set source to the result of running the application/x-www-form-urlencoded serializer with objects list.
source = object.toString()
// Set type to `application/x-www-form-urlencoded;charset=UTF-8`.
type = 'application/x-www-form-urlencoded;charset=UTF-8'
} else if (isArrayBuffer(object)) {
// BufferSource/ArrayBuffer
// Set source to a copy of the bytes held by object.
source = new Uint8Array(object.slice())
} else if (ArrayBuffer.isView(object)) {
// BufferSource/ArrayBufferView
// Set source to a copy of the bytes held by object.
source = new Uint8Array(object.buffer.slice(object.byteOffset, object.byteOffset + object.byteLength))
} else if (webidl.is.FormData(object)) {
const boundary = `----formdata-undici-0${`${random(1e11)}`.padStart(11, '0')}`
const prefix = `--${boundary}\r\nContent-Disposition: form-data`
/*! formdata-polyfill. MIT License. Jimmy Wärting <https://jimmy.warting.se/opensource> */
const escape = (str) =>
str.replace(/\n/g, '%0A').replace(/\r/g, '%0D').replace(/"/g, '%22')
const normalizeLinefeeds = (value) => value.replace(/\r?\n|\r/g, '\r\n')
// Set action to this step: run the multipart/form-data
// encoding algorithm, with objects entry list and UTF-8.
// - This ensures that the body is immutable and can't be changed afterwords
// - That the content-length is calculated in advance.
// - And that all parts are pre-encoded and ready to be sent.
const blobParts = []
const rn = new Uint8Array([13, 10]) // '\r\n'
length = 0
let hasUnknownSizeValue = false
for (const [name, value] of object) {
if (typeof value === 'string') {
const chunk = textEncoder.encode(prefix +
`; name="${escape(normalizeLinefeeds(name))}"` +
`\r\n\r\n${normalizeLinefeeds(value)}\r\n`)
blobParts.push(chunk)
length += chunk.byteLength
} else {
const chunk = textEncoder.encode(`${prefix}; name="${escape(normalizeLinefeeds(name))}"` +
(value.name ? `; filename="${escape(value.name)}"` : '') + '\r\n' +
`Content-Type: ${
value.type || 'application/octet-stream'
}\r\n\r\n`)
blobParts.push(chunk, value, rn)
if (typeof value.size === 'number') {
length += chunk.byteLength + value.size + rn.byteLength
} else {
hasUnknownSizeValue = true
}
}
}
// CRLF is appended to the body to function with legacy servers and match other implementations.
// https://github.com/curl/curl/blob/3434c6b46e682452973972e8313613dfa58cd690/lib/mime.c#L1029-L1030
// https://github.com/form-data/form-data/issues/63
const chunk = textEncoder.encode(`--${boundary}--\r\n`)
blobParts.push(chunk)
length += chunk.byteLength
if (hasUnknownSizeValue) {
length = null
}
// Set source to object.
source = object
action = async function * () {
for (const part of blobParts) {
if (part.stream) {
yield * part.stream()
} else {
yield part
}
}
}
// Set type to `multipart/form-data; boundary=`,
// followed by the multipart/form-data boundary string generated
// by the multipart/form-data encoding algorithm.
type = `multipart/form-data; boundary=${boundary}`
} else if (webidl.is.Blob(object)) {
// Blob
// Set source to object.
source = object
// Set length to objects size.
length = object.size
// If objects type attribute is not the empty byte sequence, set
// type to its value.
if (object.type) {
type = object.type
}
} else if (typeof object[Symbol.asyncIterator] === 'function') {
// If keepalive is true, then throw a TypeError.
if (keepalive) {
throw new TypeError('keepalive')
}
// If object is disturbed or locked, then throw a TypeError.
if (util.isDisturbed(object) || object.locked) {
throw new TypeError(
'Response body object should not be disturbed or locked'
)
}
stream =
webidl.is.ReadableStream(object) ? object : ReadableStreamFrom(object)
}
// 11. If source is a byte sequence, then set action to a
// step that returns source and length to sources length.
if (typeof source === 'string' || util.isBuffer(source)) {
length = Buffer.byteLength(source)
}
// 12. If action is non-null, then run these steps in in parallel:
if (action != null) {
// Run action.
let iterator
stream = new ReadableStream({
async start () {
iterator = action(object)[Symbol.asyncIterator]()
},
async pull (controller) {
const { value, done } = await iterator.next()
if (done) {
// When running action is done, close stream.
queueMicrotask(() => {
controller.close()
controller.byobRequest?.respond(0)
})
} else {
// Whenever one or more bytes are available and stream is not errored,
// enqueue a Uint8Array wrapping an ArrayBuffer containing the available
// bytes into stream.
if (!isErrored(stream)) {
const buffer = new Uint8Array(value)
if (buffer.byteLength) {
controller.enqueue(buffer)
}
}
}
return controller.desiredSize > 0
},
async cancel (reason) {
await iterator.return()
},
type: 'bytes'
})
}
// 13. Let body be a body whose stream is stream, source is source,
// and length is length.
const body = { stream, source, length }
// 14. Return (body, type).
return [body, type]
}
// https://fetch.spec.whatwg.org/#bodyinit-safely-extract
function safelyExtractBody (object, keepalive = false) {
// To safely extract a body and a `Content-Type` value from
// a byte sequence or BodyInit object object, run these steps:
// 1. If object is a ReadableStream object, then:
if (webidl.is.ReadableStream(object)) {
// Assert: object is neither disturbed nor locked.
// istanbul ignore next
assert(!util.isDisturbed(object), 'The body has already been consumed.')
// istanbul ignore next
assert(!object.locked, 'The stream is locked.')
}
// 2. Return the results of extracting object.
return extractBody(object, keepalive)
}
function cloneBody (instance, body) {
// To clone a body body, run these steps:
// https://fetch.spec.whatwg.org/#concept-body-clone
// 1. Let « out1, out2 » be the result of teeing bodys stream.
const [out1, out2] = body.stream.tee()
if (hasFinalizationRegistry) {
streamRegistry.register(instance, new WeakRef(out1))
}
// 2. Set bodys stream to out1.
body.stream = out1
// 3. Return a body whose stream is out2 and other members are copied from body.
return {
stream: out2,
length: body.length,
source: body.source
}
}
function throwIfAborted (state) {
if (state.aborted) {
throw new DOMException('The operation was aborted.', 'AbortError')
}
}
function bodyMixinMethods (instance, getInternalState) {
const methods = {
blob () {
// The blob() method steps are to return the result of
// running consume body with this and the following step
// given a byte sequence bytes: return a Blob whose
// contents are bytes and whose type attribute is thiss
// MIME type.
return consumeBody(this, (bytes) => {
let mimeType = bodyMimeType(getInternalState(this))
if (mimeType === null) {
mimeType = ''
} else if (mimeType) {
mimeType = serializeAMimeType(mimeType)
}
// Return a Blob whose contents are bytes and type attribute
// is mimeType.
return new Blob([bytes], { type: mimeType })
}, instance, getInternalState)
},
arrayBuffer () {
// The arrayBuffer() method steps are to return the result
// of running consume body with this and the following step
// given a byte sequence bytes: return a new ArrayBuffer
// whose contents are bytes.
return consumeBody(this, (bytes) => {
return new Uint8Array(bytes).buffer
}, instance, getInternalState)
},
text () {
// The text() method steps are to return the result of running
// consume body with this and UTF-8 decode.
return consumeBody(this, utf8DecodeBytes, instance, getInternalState)
},
json () {
// The json() method steps are to return the result of running
// consume body with this and parse JSON from bytes.
return consumeBody(this, parseJSONFromBytes, instance, getInternalState)
},
formData () {
// The formData() method steps are to return the result of running
// consume body with this and the following step given a byte sequence bytes:
return consumeBody(this, (value) => {
// 1. Let mimeType be the result of get the MIME type with this.
const mimeType = bodyMimeType(getInternalState(this))
// 2. If mimeType is non-null, then switch on mimeTypes essence and run
// the corresponding steps:
if (mimeType !== null) {
switch (mimeType.essence) {
case 'multipart/form-data': {
// 1. ... [long step]
// 2. If that fails for some reason, then throw a TypeError.
const parsed = multipartFormDataParser(value, mimeType)
// 3. Return a new FormData object, appending each entry,
// resulting from the parsing operation, to its entry list.
const fd = new FormData()
setFormDataState(fd, parsed)
return fd
}
case 'application/x-www-form-urlencoded': {
// 1. Let entries be the result of parsing bytes.
const entries = new URLSearchParams(value.toString())
// 2. If entries is failure, then throw a TypeError.
// 3. Return a new FormData object whose entry list is entries.
const fd = new FormData()
for (const [name, value] of entries) {
fd.append(name, value)
}
return fd
}
}
}
// 3. Throw a TypeError.
throw new TypeError(
'Content-Type was not one of "multipart/form-data" or "application/x-www-form-urlencoded".'
)
}, instance, getInternalState)
},
bytes () {
// The bytes() method steps are to return the result of running consume body
// with this and the following step given a byte sequence bytes: return the
// result of creating a Uint8Array from bytes in thiss relevant realm.
return consumeBody(this, (bytes) => {
return new Uint8Array(bytes)
}, instance, getInternalState)
}
}
return methods
}
function mixinBody (prototype, getInternalState) {
Object.assign(prototype.prototype, bodyMixinMethods(prototype, getInternalState))
}
/**
* @see https://fetch.spec.whatwg.org/#concept-body-consume-body
* @param {any} object internal state
* @param {(value: unknown) => unknown} convertBytesToJSValue
* @param {any} instance
* @param {(target: any) => any} getInternalState
*/
async function consumeBody (object, convertBytesToJSValue, instance, getInternalState) {
webidl.brandCheck(object, instance)
const state = getInternalState(object)
// 1. If object is unusable, then return a promise rejected
// with a TypeError.
if (bodyUnusable(state)) {
throw new TypeError('Body is unusable: Body has already been read')
}
throwIfAborted(state)
// 2. Let promise be a new promise.
const promise = createDeferredPromise()
// 3. Let errorSteps given error be to reject promise with error.
const errorSteps = (error) => promise.reject(error)
// 4. Let successSteps given a byte sequence data be to resolve
// promise with the result of running convertBytesToJSValue
// with data. If that threw an exception, then run errorSteps
// with that exception.
const successSteps = (data) => {
try {
promise.resolve(convertBytesToJSValue(data))
} catch (e) {
errorSteps(e)
}
}
// 5. If objects body is null, then run successSteps with an
// empty byte sequence.
if (state.body == null) {
successSteps(Buffer.allocUnsafe(0))
return promise.promise
}
// 6. Otherwise, fully read objects body given successSteps,
// errorSteps, and objects relevant global object.
fullyReadBody(state.body, successSteps, errorSteps)
// 7. Return promise.
return promise.promise
}
/**
* @see https://fetch.spec.whatwg.org/#body-unusable
* @param {any} object internal state
*/
function bodyUnusable (object) {
const body = object.body
// An object including the Body interface mixin is
// said to be unusable if its body is non-null and
// its bodys stream is disturbed or locked.
return body != null && (body.stream.locked || util.isDisturbed(body.stream))
}
/**
* @see https://infra.spec.whatwg.org/#parse-json-bytes-to-a-javascript-value
* @param {Uint8Array} bytes
*/
function parseJSONFromBytes (bytes) {
return JSON.parse(utf8DecodeBytes(bytes))
}
/**
* @see https://fetch.spec.whatwg.org/#concept-body-mime-type
* @param {any} requestOrResponse internal state
*/
function bodyMimeType (requestOrResponse) {
// 1. Let headers be null.
// 2. If requestOrResponse is a Request object, then set headers to requestOrResponses requests header list.
// 3. Otherwise, set headers to requestOrResponses responses header list.
/** @type {import('./headers').HeadersList} */
const headers = requestOrResponse.headersList
// 4. Let mimeType be the result of extracting a MIME type from headers.
const mimeType = extractMimeType(headers)
// 5. If mimeType is failure, then return null.
if (mimeType === 'failure') {
return null
}
// 6. Return mimeType.
return mimeType
}
module.exports = {
extractBody,
safelyExtractBody,
cloneBody,
mixinBody,
streamRegistry,
hasFinalizationRegistry,
bodyUnusable
}

131
node_modules/undici/lib/web/fetch/constants.js generated vendored Normal file
View File

@ -0,0 +1,131 @@
'use strict'
const corsSafeListedMethods = /** @type {const} */ (['GET', 'HEAD', 'POST'])
const corsSafeListedMethodsSet = new Set(corsSafeListedMethods)
const nullBodyStatus = /** @type {const} */ ([101, 204, 205, 304])
const redirectStatus = /** @type {const} */ ([301, 302, 303, 307, 308])
const redirectStatusSet = new Set(redirectStatus)
/**
* @see https://fetch.spec.whatwg.org/#block-bad-port
*/
const badPorts = /** @type {const} */ ([
'1', '7', '9', '11', '13', '15', '17', '19', '20', '21', '22', '23', '25', '37', '42', '43', '53', '69', '77', '79',
'87', '95', '101', '102', '103', '104', '109', '110', '111', '113', '115', '117', '119', '123', '135', '137',
'139', '143', '161', '179', '389', '427', '465', '512', '513', '514', '515', '526', '530', '531', '532',
'540', '548', '554', '556', '563', '587', '601', '636', '989', '990', '993', '995', '1719', '1720', '1723',
'2049', '3659', '4045', '4190', '5060', '5061', '6000', '6566', '6665', '6666', '6667', '6668', '6669', '6679',
'6697', '10080'
])
const badPortsSet = new Set(badPorts)
/**
* @see https://w3c.github.io/webappsec-referrer-policy/#referrer-policy-header
*/
const referrerPolicyTokens = /** @type {const} */ ([
'no-referrer',
'no-referrer-when-downgrade',
'same-origin',
'origin',
'strict-origin',
'origin-when-cross-origin',
'strict-origin-when-cross-origin',
'unsafe-url'
])
/**
* @see https://w3c.github.io/webappsec-referrer-policy/#referrer-policies
*/
const referrerPolicy = /** @type {const} */ ([
'',
...referrerPolicyTokens
])
const referrerPolicyTokensSet = new Set(referrerPolicyTokens)
const requestRedirect = /** @type {const} */ (['follow', 'manual', 'error'])
const safeMethods = /** @type {const} */ (['GET', 'HEAD', 'OPTIONS', 'TRACE'])
const safeMethodsSet = new Set(safeMethods)
const requestMode = /** @type {const} */ (['navigate', 'same-origin', 'no-cors', 'cors'])
const requestCredentials = /** @type {const} */ (['omit', 'same-origin', 'include'])
const requestCache = /** @type {const} */ ([
'default',
'no-store',
'reload',
'no-cache',
'force-cache',
'only-if-cached'
])
/**
* @see https://fetch.spec.whatwg.org/#request-body-header-name
*/
const requestBodyHeader = /** @type {const} */ ([
'content-encoding',
'content-language',
'content-location',
'content-type',
// See https://github.com/nodejs/undici/issues/2021
// 'Content-Length' is a forbidden header name, which is typically
// removed in the Headers implementation. However, undici doesn't
// filter out headers, so we add it here.
'content-length'
])
/**
* @see https://fetch.spec.whatwg.org/#enumdef-requestduplex
*/
const requestDuplex = /** @type {const} */ ([
'half'
])
/**
* @see http://fetch.spec.whatwg.org/#forbidden-method
*/
const forbiddenMethods = /** @type {const} */ (['CONNECT', 'TRACE', 'TRACK'])
const forbiddenMethodsSet = new Set(forbiddenMethods)
const subresource = /** @type {const} */ ([
'audio',
'audioworklet',
'font',
'image',
'manifest',
'paintworklet',
'script',
'style',
'track',
'video',
'xslt',
''
])
const subresourceSet = new Set(subresource)
module.exports = {
subresource,
forbiddenMethods,
requestBodyHeader,
referrerPolicy,
requestRedirect,
requestMode,
requestCredentials,
requestCache,
redirectStatus,
corsSafeListedMethods,
nullBodyStatus,
safeMethods,
badPorts,
requestDuplex,
subresourceSet,
badPortsSet,
redirectStatusSet,
corsSafeListedMethodsSet,
safeMethodsSet,
forbiddenMethodsSet,
referrerPolicyTokens: referrerPolicyTokensSet
}

744
node_modules/undici/lib/web/fetch/data-url.js generated vendored Normal file
View File

@ -0,0 +1,744 @@
'use strict'
const assert = require('node:assert')
const encoder = new TextEncoder()
/**
* @see https://mimesniff.spec.whatwg.org/#http-token-code-point
*/
const HTTP_TOKEN_CODEPOINTS = /^[!#$%&'*+\-.^_|~A-Za-z0-9]+$/
const HTTP_WHITESPACE_REGEX = /[\u000A\u000D\u0009\u0020]/ // eslint-disable-line
const ASCII_WHITESPACE_REPLACE_REGEX = /[\u0009\u000A\u000C\u000D\u0020]/g // eslint-disable-line
/**
* @see https://mimesniff.spec.whatwg.org/#http-quoted-string-token-code-point
*/
const HTTP_QUOTED_STRING_TOKENS = /^[\u0009\u0020-\u007E\u0080-\u00FF]+$/ // eslint-disable-line
// https://fetch.spec.whatwg.org/#data-url-processor
/** @param {URL} dataURL */
function dataURLProcessor (dataURL) {
// 1. Assert: dataURLs scheme is "data".
assert(dataURL.protocol === 'data:')
// 2. Let input be the result of running the URL
// serializer on dataURL with exclude fragment
// set to true.
let input = URLSerializer(dataURL, true)
// 3. Remove the leading "data:" string from input.
input = input.slice(5)
// 4. Let position point at the start of input.
const position = { position: 0 }
// 5. Let mimeType be the result of collecting a
// sequence of code points that are not equal
// to U+002C (,), given position.
let mimeType = collectASequenceOfCodePointsFast(
',',
input,
position
)
// 6. Strip leading and trailing ASCII whitespace
// from mimeType.
// Undici implementation note: we need to store the
// length because if the mimetype has spaces removed,
// the wrong amount will be sliced from the input in
// step #9
const mimeTypeLength = mimeType.length
mimeType = removeASCIIWhitespace(mimeType, true, true)
// 7. If position is past the end of input, then
// return failure
if (position.position >= input.length) {
return 'failure'
}
// 8. Advance position by 1.
position.position++
// 9. Let encodedBody be the remainder of input.
const encodedBody = input.slice(mimeTypeLength + 1)
// 10. Let body be the percent-decoding of encodedBody.
let body = stringPercentDecode(encodedBody)
// 11. If mimeType ends with U+003B (;), followed by
// zero or more U+0020 SPACE, followed by an ASCII
// case-insensitive match for "base64", then:
if (/;(\u0020){0,}base64$/i.test(mimeType)) {
// 1. Let stringBody be the isomorphic decode of body.
const stringBody = isomorphicDecode(body)
// 2. Set body to the forgiving-base64 decode of
// stringBody.
body = forgivingBase64(stringBody)
// 3. If body is failure, then return failure.
if (body === 'failure') {
return 'failure'
}
// 4. Remove the last 6 code points from mimeType.
mimeType = mimeType.slice(0, -6)
// 5. Remove trailing U+0020 SPACE code points from mimeType,
// if any.
mimeType = mimeType.replace(/(\u0020)+$/, '')
// 6. Remove the last U+003B (;) code point from mimeType.
mimeType = mimeType.slice(0, -1)
}
// 12. If mimeType starts with U+003B (;), then prepend
// "text/plain" to mimeType.
if (mimeType.startsWith(';')) {
mimeType = 'text/plain' + mimeType
}
// 13. Let mimeTypeRecord be the result of parsing
// mimeType.
let mimeTypeRecord = parseMIMEType(mimeType)
// 14. If mimeTypeRecord is failure, then set
// mimeTypeRecord to text/plain;charset=US-ASCII.
if (mimeTypeRecord === 'failure') {
mimeTypeRecord = parseMIMEType('text/plain;charset=US-ASCII')
}
// 15. Return a new data: URL struct whose MIME
// type is mimeTypeRecord and body is body.
// https://fetch.spec.whatwg.org/#data-url-struct
return { mimeType: mimeTypeRecord, body }
}
// https://url.spec.whatwg.org/#concept-url-serializer
/**
* @param {URL} url
* @param {boolean} excludeFragment
*/
function URLSerializer (url, excludeFragment = false) {
if (!excludeFragment) {
return url.href
}
const href = url.href
const hashLength = url.hash.length
const serialized = hashLength === 0 ? href : href.substring(0, href.length - hashLength)
if (!hashLength && href.endsWith('#')) {
return serialized.slice(0, -1)
}
return serialized
}
// https://infra.spec.whatwg.org/#collect-a-sequence-of-code-points
/**
* @param {(char: string) => boolean} condition
* @param {string} input
* @param {{ position: number }} position
*/
function collectASequenceOfCodePoints (condition, input, position) {
// 1. Let result be the empty string.
let result = ''
// 2. While position doesnt point past the end of input and the
// code point at position within input meets the condition condition:
while (position.position < input.length && condition(input[position.position])) {
// 1. Append that code point to the end of result.
result += input[position.position]
// 2. Advance position by 1.
position.position++
}
// 3. Return result.
return result
}
/**
* A faster collectASequenceOfCodePoints that only works when comparing a single character.
* @param {string} char
* @param {string} input
* @param {{ position: number }} position
*/
function collectASequenceOfCodePointsFast (char, input, position) {
const idx = input.indexOf(char, position.position)
const start = position.position
if (idx === -1) {
position.position = input.length
return input.slice(start)
}
position.position = idx
return input.slice(start, position.position)
}
// https://url.spec.whatwg.org/#string-percent-decode
/** @param {string} input */
function stringPercentDecode (input) {
// 1. Let bytes be the UTF-8 encoding of input.
const bytes = encoder.encode(input)
// 2. Return the percent-decoding of bytes.
return percentDecode(bytes)
}
/**
* @param {number} byte
*/
function isHexCharByte (byte) {
// 0-9 A-F a-f
return (byte >= 0x30 && byte <= 0x39) || (byte >= 0x41 && byte <= 0x46) || (byte >= 0x61 && byte <= 0x66)
}
/**
* @param {number} byte
*/
function hexByteToNumber (byte) {
return (
// 0-9
byte >= 0x30 && byte <= 0x39
? (byte - 48)
// Convert to uppercase
// ((byte & 0xDF) - 65) + 10
: ((byte & 0xDF) - 55)
)
}
// https://url.spec.whatwg.org/#percent-decode
/** @param {Uint8Array} input */
function percentDecode (input) {
const length = input.length
// 1. Let output be an empty byte sequence.
/** @type {Uint8Array} */
const output = new Uint8Array(length)
let j = 0
// 2. For each byte byte in input:
for (let i = 0; i < length; ++i) {
const byte = input[i]
// 1. If byte is not 0x25 (%), then append byte to output.
if (byte !== 0x25) {
output[j++] = byte
// 2. Otherwise, if byte is 0x25 (%) and the next two bytes
// after byte in input are not in the ranges
// 0x30 (0) to 0x39 (9), 0x41 (A) to 0x46 (F),
// and 0x61 (a) to 0x66 (f), all inclusive, append byte
// to output.
} else if (
byte === 0x25 &&
!(isHexCharByte(input[i + 1]) && isHexCharByte(input[i + 2]))
) {
output[j++] = 0x25
// 3. Otherwise:
} else {
// 1. Let bytePoint be the two bytes after byte in input,
// decoded, and then interpreted as hexadecimal number.
// 2. Append a byte whose value is bytePoint to output.
output[j++] = (hexByteToNumber(input[i + 1]) << 4) | hexByteToNumber(input[i + 2])
// 3. Skip the next two bytes in input.
i += 2
}
}
// 3. Return output.
return length === j ? output : output.subarray(0, j)
}
// https://mimesniff.spec.whatwg.org/#parse-a-mime-type
/** @param {string} input */
function parseMIMEType (input) {
// 1. Remove any leading and trailing HTTP whitespace
// from input.
input = removeHTTPWhitespace(input, true, true)
// 2. Let position be a position variable for input,
// initially pointing at the start of input.
const position = { position: 0 }
// 3. Let type be the result of collecting a sequence
// of code points that are not U+002F (/) from
// input, given position.
const type = collectASequenceOfCodePointsFast(
'/',
input,
position
)
// 4. If type is the empty string or does not solely
// contain HTTP token code points, then return failure.
// https://mimesniff.spec.whatwg.org/#http-token-code-point
if (type.length === 0 || !HTTP_TOKEN_CODEPOINTS.test(type)) {
return 'failure'
}
// 5. If position is past the end of input, then return
// failure
if (position.position >= input.length) {
return 'failure'
}
// 6. Advance position by 1. (This skips past U+002F (/).)
position.position++
// 7. Let subtype be the result of collecting a sequence of
// code points that are not U+003B (;) from input, given
// position.
let subtype = collectASequenceOfCodePointsFast(
';',
input,
position
)
// 8. Remove any trailing HTTP whitespace from subtype.
subtype = removeHTTPWhitespace(subtype, false, true)
// 9. If subtype is the empty string or does not solely
// contain HTTP token code points, then return failure.
if (subtype.length === 0 || !HTTP_TOKEN_CODEPOINTS.test(subtype)) {
return 'failure'
}
const typeLowercase = type.toLowerCase()
const subtypeLowercase = subtype.toLowerCase()
// 10. Let mimeType be a new MIME type record whose type
// is type, in ASCII lowercase, and subtype is subtype,
// in ASCII lowercase.
// https://mimesniff.spec.whatwg.org/#mime-type
const mimeType = {
type: typeLowercase,
subtype: subtypeLowercase,
/** @type {Map<string, string>} */
parameters: new Map(),
// https://mimesniff.spec.whatwg.org/#mime-type-essence
essence: `${typeLowercase}/${subtypeLowercase}`
}
// 11. While position is not past the end of input:
while (position.position < input.length) {
// 1. Advance position by 1. (This skips past U+003B (;).)
position.position++
// 2. Collect a sequence of code points that are HTTP
// whitespace from input given position.
collectASequenceOfCodePoints(
// https://fetch.spec.whatwg.org/#http-whitespace
char => HTTP_WHITESPACE_REGEX.test(char),
input,
position
)
// 3. Let parameterName be the result of collecting a
// sequence of code points that are not U+003B (;)
// or U+003D (=) from input, given position.
let parameterName = collectASequenceOfCodePoints(
(char) => char !== ';' && char !== '=',
input,
position
)
// 4. Set parameterName to parameterName, in ASCII
// lowercase.
parameterName = parameterName.toLowerCase()
// 5. If position is not past the end of input, then:
if (position.position < input.length) {
// 1. If the code point at position within input is
// U+003B (;), then continue.
if (input[position.position] === ';') {
continue
}
// 2. Advance position by 1. (This skips past U+003D (=).)
position.position++
}
// 6. If position is past the end of input, then break.
if (position.position >= input.length) {
break
}
// 7. Let parameterValue be null.
let parameterValue = null
// 8. If the code point at position within input is
// U+0022 ("), then:
if (input[position.position] === '"') {
// 1. Set parameterValue to the result of collecting
// an HTTP quoted string from input, given position
// and the extract-value flag.
parameterValue = collectAnHTTPQuotedString(input, position, true)
// 2. Collect a sequence of code points that are not
// U+003B (;) from input, given position.
collectASequenceOfCodePointsFast(
';',
input,
position
)
// 9. Otherwise:
} else {
// 1. Set parameterValue to the result of collecting
// a sequence of code points that are not U+003B (;)
// from input, given position.
parameterValue = collectASequenceOfCodePointsFast(
';',
input,
position
)
// 2. Remove any trailing HTTP whitespace from parameterValue.
parameterValue = removeHTTPWhitespace(parameterValue, false, true)
// 3. If parameterValue is the empty string, then continue.
if (parameterValue.length === 0) {
continue
}
}
// 10. If all of the following are true
// - parameterName is not the empty string
// - parameterName solely contains HTTP token code points
// - parameterValue solely contains HTTP quoted-string token code points
// - mimeTypes parameters[parameterName] does not exist
// then set mimeTypes parameters[parameterName] to parameterValue.
if (
parameterName.length !== 0 &&
HTTP_TOKEN_CODEPOINTS.test(parameterName) &&
(parameterValue.length === 0 || HTTP_QUOTED_STRING_TOKENS.test(parameterValue)) &&
!mimeType.parameters.has(parameterName)
) {
mimeType.parameters.set(parameterName, parameterValue)
}
}
// 12. Return mimeType.
return mimeType
}
// https://infra.spec.whatwg.org/#forgiving-base64-decode
/** @param {string} data */
function forgivingBase64 (data) {
// 1. Remove all ASCII whitespace from data.
data = data.replace(ASCII_WHITESPACE_REPLACE_REGEX, '')
let dataLength = data.length
// 2. If datas code point length divides by 4 leaving
// no remainder, then:
if (dataLength % 4 === 0) {
// 1. If data ends with one or two U+003D (=) code points,
// then remove them from data.
if (data.charCodeAt(dataLength - 1) === 0x003D) {
--dataLength
if (data.charCodeAt(dataLength - 1) === 0x003D) {
--dataLength
}
}
}
// 3. If datas code point length divides by 4 leaving
// a remainder of 1, then return failure.
if (dataLength % 4 === 1) {
return 'failure'
}
// 4. If data contains a code point that is not one of
// U+002B (+)
// U+002F (/)
// ASCII alphanumeric
// then return failure.
if (/[^+/0-9A-Za-z]/.test(data.length === dataLength ? data : data.substring(0, dataLength))) {
return 'failure'
}
const buffer = Buffer.from(data, 'base64')
return new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength)
}
// https://fetch.spec.whatwg.org/#collect-an-http-quoted-string
// tests: https://fetch.spec.whatwg.org/#example-http-quoted-string
/**
* @param {string} input
* @param {{ position: number }} position
* @param {boolean} [extractValue=false]
*/
function collectAnHTTPQuotedString (input, position, extractValue = false) {
// 1. Let positionStart be position.
const positionStart = position.position
// 2. Let value be the empty string.
let value = ''
// 3. Assert: the code point at position within input
// is U+0022 (").
assert(input[position.position] === '"')
// 4. Advance position by 1.
position.position++
// 5. While true:
while (true) {
// 1. Append the result of collecting a sequence of code points
// that are not U+0022 (") or U+005C (\) from input, given
// position, to value.
value += collectASequenceOfCodePoints(
(char) => char !== '"' && char !== '\\',
input,
position
)
// 2. If position is past the end of input, then break.
if (position.position >= input.length) {
break
}
// 3. Let quoteOrBackslash be the code point at position within
// input.
const quoteOrBackslash = input[position.position]
// 4. Advance position by 1.
position.position++
// 5. If quoteOrBackslash is U+005C (\), then:
if (quoteOrBackslash === '\\') {
// 1. If position is past the end of input, then append
// U+005C (\) to value and break.
if (position.position >= input.length) {
value += '\\'
break
}
// 2. Append the code point at position within input to value.
value += input[position.position]
// 3. Advance position by 1.
position.position++
// 6. Otherwise:
} else {
// 1. Assert: quoteOrBackslash is U+0022 (").
assert(quoteOrBackslash === '"')
// 2. Break.
break
}
}
// 6. If the extract-value flag is set, then return value.
if (extractValue) {
return value
}
// 7. Return the code points from positionStart to position,
// inclusive, within input.
return input.slice(positionStart, position.position)
}
/**
* @see https://mimesniff.spec.whatwg.org/#serialize-a-mime-type
*/
function serializeAMimeType (mimeType) {
assert(mimeType !== 'failure')
const { parameters, essence } = mimeType
// 1. Let serialization be the concatenation of mimeTypes
// type, U+002F (/), and mimeTypes subtype.
let serialization = essence
// 2. For each name → value of mimeTypes parameters:
for (let [name, value] of parameters.entries()) {
// 1. Append U+003B (;) to serialization.
serialization += ';'
// 2. Append name to serialization.
serialization += name
// 3. Append U+003D (=) to serialization.
serialization += '='
// 4. If value does not solely contain HTTP token code
// points or value is the empty string, then:
if (!HTTP_TOKEN_CODEPOINTS.test(value)) {
// 1. Precede each occurrence of U+0022 (") or
// U+005C (\) in value with U+005C (\).
value = value.replace(/(\\|")/g, '\\$1')
// 2. Prepend U+0022 (") to value.
value = '"' + value
// 3. Append U+0022 (") to value.
value += '"'
}
// 5. Append value to serialization.
serialization += value
}
// 3. Return serialization.
return serialization
}
/**
* @see https://fetch.spec.whatwg.org/#http-whitespace
* @param {number} char
*/
function isHTTPWhiteSpace (char) {
// "\r\n\t "
return char === 0x00d || char === 0x00a || char === 0x009 || char === 0x020
}
/**
* @see https://fetch.spec.whatwg.org/#http-whitespace
* @param {string} str
* @param {boolean} [leading=true]
* @param {boolean} [trailing=true]
*/
function removeHTTPWhitespace (str, leading = true, trailing = true) {
return removeChars(str, leading, trailing, isHTTPWhiteSpace)
}
/**
* @see https://infra.spec.whatwg.org/#ascii-whitespace
* @param {number} char
*/
function isASCIIWhitespace (char) {
// "\r\n\t\f "
return char === 0x00d || char === 0x00a || char === 0x009 || char === 0x00c || char === 0x020
}
/**
* @see https://infra.spec.whatwg.org/#strip-leading-and-trailing-ascii-whitespace
* @param {string} str
* @param {boolean} [leading=true]
* @param {boolean} [trailing=true]
*/
function removeASCIIWhitespace (str, leading = true, trailing = true) {
return removeChars(str, leading, trailing, isASCIIWhitespace)
}
/**
* @param {string} str
* @param {boolean} leading
* @param {boolean} trailing
* @param {(charCode: number) => boolean} predicate
* @returns
*/
function removeChars (str, leading, trailing, predicate) {
let lead = 0
let trail = str.length - 1
if (leading) {
while (lead < str.length && predicate(str.charCodeAt(lead))) lead++
}
if (trailing) {
while (trail > 0 && predicate(str.charCodeAt(trail))) trail--
}
return lead === 0 && trail === str.length - 1 ? str : str.slice(lead, trail + 1)
}
/**
* @see https://infra.spec.whatwg.org/#isomorphic-decode
* @param {Uint8Array} input
* @returns {string}
*/
function isomorphicDecode (input) {
// 1. To isomorphic decode a byte sequence input, return a string whose code point
// length is equal to inputs length and whose code points have the same values
// as the values of inputs bytes, in the same order.
const length = input.length
if ((2 << 15) - 1 > length) {
return String.fromCharCode.apply(null, input)
}
let result = ''; let i = 0
let addition = (2 << 15) - 1
while (i < length) {
if (i + addition > length) {
addition = length - i
}
result += String.fromCharCode.apply(null, input.subarray(i, i += addition))
}
return result
}
/**
* @see https://mimesniff.spec.whatwg.org/#minimize-a-supported-mime-type
* @param {Exclude<ReturnType<typeof parseMIMEType>, 'failure'>} mimeType
*/
function minimizeSupportedMimeType (mimeType) {
switch (mimeType.essence) {
case 'application/ecmascript':
case 'application/javascript':
case 'application/x-ecmascript':
case 'application/x-javascript':
case 'text/ecmascript':
case 'text/javascript':
case 'text/javascript1.0':
case 'text/javascript1.1':
case 'text/javascript1.2':
case 'text/javascript1.3':
case 'text/javascript1.4':
case 'text/javascript1.5':
case 'text/jscript':
case 'text/livescript':
case 'text/x-ecmascript':
case 'text/x-javascript':
// 1. If mimeType is a JavaScript MIME type, then return "text/javascript".
return 'text/javascript'
case 'application/json':
case 'text/json':
// 2. If mimeType is a JSON MIME type, then return "application/json".
return 'application/json'
case 'image/svg+xml':
// 3. If mimeTypes essence is "image/svg+xml", then return "image/svg+xml".
return 'image/svg+xml'
case 'text/xml':
case 'application/xml':
// 4. If mimeType is an XML MIME type, then return "application/xml".
return 'application/xml'
}
// 2. If mimeType is a JSON MIME type, then return "application/json".
if (mimeType.subtype.endsWith('+json')) {
return 'application/json'
}
// 4. If mimeType is an XML MIME type, then return "application/xml".
if (mimeType.subtype.endsWith('+xml')) {
return 'application/xml'
}
// 5. If mimeType is supported by the user agent, then return mimeTypes essence.
// Technically, node doesn't support any mimetypes.
// 6. Return the empty string.
return ''
}
module.exports = {
dataURLProcessor,
URLSerializer,
collectASequenceOfCodePoints,
collectASequenceOfCodePointsFast,
stringPercentDecode,
parseMIMEType,
collectAnHTTPQuotedString,
serializeAMimeType,
removeChars,
removeHTTPWhitespace,
minimizeSupportedMimeType,
HTTP_TOKEN_CODEPOINTS,
isomorphicDecode
}

View File

@ -0,0 +1,46 @@
'use strict'
const { kConnected, kSize } = require('../../core/symbols')
class CompatWeakRef {
constructor (value) {
this.value = value
}
deref () {
return this.value[kConnected] === 0 && this.value[kSize] === 0
? undefined
: this.value
}
}
class CompatFinalizer {
constructor (finalizer) {
this.finalizer = finalizer
}
register (dispatcher, key) {
if (dispatcher.on) {
dispatcher.on('disconnect', () => {
if (dispatcher[kConnected] === 0 && dispatcher[kSize] === 0) {
this.finalizer(key)
}
})
}
}
unregister (key) {}
}
module.exports = function () {
// FIXME: remove workaround when the Node bug is backported to v18
// https://github.com/nodejs/node/issues/49344#issuecomment-1741776308
if (process.env.NODE_V8_COVERAGE && process.version.startsWith('v18')) {
process._rawDebug('Using compatibility WeakRef and FinalizationRegistry')
return {
WeakRef: CompatWeakRef,
FinalizationRegistry: CompatFinalizer
}
}
return { WeakRef, FinalizationRegistry }
}

501
node_modules/undici/lib/web/fetch/formdata-parser.js generated vendored Normal file
View File

@ -0,0 +1,501 @@
'use strict'
const { isUSVString, bufferToLowerCasedHeaderName } = require('../../core/util')
const { utf8DecodeBytes } = require('./util')
const { HTTP_TOKEN_CODEPOINTS, isomorphicDecode } = require('./data-url')
const { makeEntry } = require('./formdata')
const { webidl } = require('./webidl')
const assert = require('node:assert')
const { File: NodeFile } = require('node:buffer')
const File = globalThis.File ?? NodeFile
const formDataNameBuffer = Buffer.from('form-data; name="')
const filenameBuffer = Buffer.from('filename')
const dd = Buffer.from('--')
const ddcrlf = Buffer.from('--\r\n')
/**
* @param {string} chars
*/
function isAsciiString (chars) {
for (let i = 0; i < chars.length; ++i) {
if ((chars.charCodeAt(i) & ~0x7F) !== 0) {
return false
}
}
return true
}
/**
* @see https://andreubotella.github.io/multipart-form-data/#multipart-form-data-boundary
* @param {string} boundary
*/
function validateBoundary (boundary) {
const length = boundary.length
// - its length is greater or equal to 27 and lesser or equal to 70, and
if (length < 27 || length > 70) {
return false
}
// - it is composed by bytes in the ranges 0x30 to 0x39, 0x41 to 0x5A, or
// 0x61 to 0x7A, inclusive (ASCII alphanumeric), or which are 0x27 ('),
// 0x2D (-) or 0x5F (_).
for (let i = 0; i < length; ++i) {
const cp = boundary.charCodeAt(i)
if (!(
(cp >= 0x30 && cp <= 0x39) ||
(cp >= 0x41 && cp <= 0x5a) ||
(cp >= 0x61 && cp <= 0x7a) ||
cp === 0x27 ||
cp === 0x2d ||
cp === 0x5f
)) {
return false
}
}
return true
}
/**
* @see https://andreubotella.github.io/multipart-form-data/#multipart-form-data-parser
* @param {Buffer} input
* @param {ReturnType<import('./data-url')['parseMIMEType']>} mimeType
*/
function multipartFormDataParser (input, mimeType) {
// 1. Assert: mimeTypes essence is "multipart/form-data".
assert(mimeType !== 'failure' && mimeType.essence === 'multipart/form-data')
const boundaryString = mimeType.parameters.get('boundary')
// 2. If mimeTypes parameters["boundary"] does not exist, return failure.
// Otherwise, let boundary be the result of UTF-8 decoding mimeTypes
// parameters["boundary"].
if (boundaryString === undefined) {
throw parsingError('missing boundary in content-type header')
}
const boundary = Buffer.from(`--${boundaryString}`, 'utf8')
// 3. Let entry list be an empty entry list.
const entryList = []
// 4. Let position be a pointer to a byte in input, initially pointing at
// the first byte.
const position = { position: 0 }
// Note: undici addition, allows leading and trailing CRLFs.
while (input[position.position] === 0x0d && input[position.position + 1] === 0x0a) {
position.position += 2
}
let trailing = input.length
while (input[trailing - 1] === 0x0a && input[trailing - 2] === 0x0d) {
trailing -= 2
}
if (trailing !== input.length) {
input = input.subarray(0, trailing)
}
// 5. While true:
while (true) {
// 5.1. If position points to a sequence of bytes starting with 0x2D 0x2D
// (`--`) followed by boundary, advance position by 2 + the length of
// boundary. Otherwise, return failure.
// Note: boundary is padded with 2 dashes already, no need to add 2.
if (input.subarray(position.position, position.position + boundary.length).equals(boundary)) {
position.position += boundary.length
} else {
throw parsingError('expected a value starting with -- and the boundary')
}
// 5.2. If position points to the sequence of bytes 0x2D 0x2D 0x0D 0x0A
// (`--` followed by CR LF) followed by the end of input, return entry list.
// Note: a body does NOT need to end with CRLF. It can end with --.
if (
(position.position === input.length - 2 && bufferStartsWith(input, dd, position)) ||
(position.position === input.length - 4 && bufferStartsWith(input, ddcrlf, position))
) {
return entryList
}
// 5.3. If position does not point to a sequence of bytes starting with 0x0D
// 0x0A (CR LF), return failure.
if (input[position.position] !== 0x0d || input[position.position + 1] !== 0x0a) {
throw parsingError('expected CRLF')
}
// 5.4. Advance position by 2. (This skips past the newline.)
position.position += 2
// 5.5. Let name, filename and contentType be the result of parsing
// multipart/form-data headers on input and position, if the result
// is not failure. Otherwise, return failure.
const result = parseMultipartFormDataHeaders(input, position)
let { name, filename, contentType, encoding } = result
// 5.6. Advance position by 2. (This skips past the empty line that marks
// the end of the headers.)
position.position += 2
// 5.7. Let body be the empty byte sequence.
let body
// 5.8. Body loop: While position is not past the end of input:
// TODO: the steps here are completely wrong
{
const boundaryIndex = input.indexOf(boundary.subarray(2), position.position)
if (boundaryIndex === -1) {
throw parsingError('expected boundary after body')
}
body = input.subarray(position.position, boundaryIndex - 4)
position.position += body.length
// Note: position must be advanced by the body's length before being
// decoded, otherwise the parsing will fail.
if (encoding === 'base64') {
body = Buffer.from(body.toString(), 'base64')
}
}
// 5.9. If position does not point to a sequence of bytes starting with
// 0x0D 0x0A (CR LF), return failure. Otherwise, advance position by 2.
if (input[position.position] !== 0x0d || input[position.position + 1] !== 0x0a) {
throw parsingError('expected CRLF')
} else {
position.position += 2
}
// 5.10. If filename is not null:
let value
if (filename !== null) {
// 5.10.1. If contentType is null, set contentType to "text/plain".
contentType ??= 'text/plain'
// 5.10.2. If contentType is not an ASCII string, set contentType to the empty string.
// Note: `buffer.isAscii` can be used at zero-cost, but converting a string to a buffer is a high overhead.
// Content-Type is a relatively small string, so it is faster to use `String#charCodeAt`.
if (!isAsciiString(contentType)) {
contentType = ''
}
// 5.10.3. Let value be a new File object with name filename, type contentType, and body body.
value = new File([body], filename, { type: contentType })
} else {
// 5.11. Otherwise:
// 5.11.1. Let value be the UTF-8 decoding without BOM of body.
value = utf8DecodeBytes(Buffer.from(body))
}
// 5.12. Assert: name is a scalar value string and value is either a scalar value string or a File object.
assert(isUSVString(name))
assert((typeof value === 'string' && isUSVString(value)) || webidl.is.File(value))
// 5.13. Create an entry with name and value, and append it to entry list.
entryList.push(makeEntry(name, value, filename))
}
}
/**
* @see https://andreubotella.github.io/multipart-form-data/#parse-multipart-form-data-headers
* @param {Buffer} input
* @param {{ position: number }} position
*/
function parseMultipartFormDataHeaders (input, position) {
// 1. Let name, filename and contentType be null.
let name = null
let filename = null
let contentType = null
let encoding = null
// 2. While true:
while (true) {
// 2.1. If position points to a sequence of bytes starting with 0x0D 0x0A (CR LF):
if (input[position.position] === 0x0d && input[position.position + 1] === 0x0a) {
// 2.1.1. If name is null, return failure.
if (name === null) {
throw parsingError('header name is null')
}
// 2.1.2. Return name, filename and contentType.
return { name, filename, contentType, encoding }
}
// 2.2. Let header name be the result of collecting a sequence of bytes that are
// not 0x0A (LF), 0x0D (CR) or 0x3A (:), given position.
let headerName = collectASequenceOfBytes(
(char) => char !== 0x0a && char !== 0x0d && char !== 0x3a,
input,
position
)
// 2.3. Remove any HTTP tab or space bytes from the start or end of header name.
headerName = removeChars(headerName, true, true, (char) => char === 0x9 || char === 0x20)
// 2.4. If header name does not match the field-name token production, return failure.
if (!HTTP_TOKEN_CODEPOINTS.test(headerName.toString())) {
throw parsingError('header name does not match the field-name token production')
}
// 2.5. If the byte at position is not 0x3A (:), return failure.
if (input[position.position] !== 0x3a) {
throw parsingError('expected :')
}
// 2.6. Advance position by 1.
position.position++
// 2.7. Collect a sequence of bytes that are HTTP tab or space bytes given position.
// (Do nothing with those bytes.)
collectASequenceOfBytes(
(char) => char === 0x20 || char === 0x09,
input,
position
)
// 2.8. Byte-lowercase header name and switch on the result:
switch (bufferToLowerCasedHeaderName(headerName)) {
case 'content-disposition': {
// 1. Set name and filename to null.
name = filename = null
// 2. If position does not point to a sequence of bytes starting with
// `form-data; name="`, return failure.
if (!bufferStartsWith(input, formDataNameBuffer, position)) {
throw parsingError('expected form-data; name=" for content-disposition header')
}
// 3. Advance position so it points at the byte after the next 0x22 (")
// byte (the one in the sequence of bytes matched above).
position.position += 17
// 4. Set name to the result of parsing a multipart/form-data name given
// input and position, if the result is not failure. Otherwise, return
// failure.
name = parseMultipartFormDataName(input, position)
// 5. If position points to a sequence of bytes starting with `; filename="`:
if (input[position.position] === 0x3b /* ; */ && input[position.position + 1] === 0x20 /* ' ' */) {
const at = { position: position.position + 2 }
if (bufferStartsWith(input, filenameBuffer, at)) {
if (input[at.position + 8] === 0x2a /* '*' */) {
at.position += 10 // skip past filename*=
// Remove leading http tab and spaces. See RFC for examples.
// https://datatracker.ietf.org/doc/html/rfc6266#section-5
collectASequenceOfBytes(
(char) => char === 0x20 || char === 0x09,
input,
at
)
const headerValue = collectASequenceOfBytes(
(char) => char !== 0x20 && char !== 0x0d && char !== 0x0a, // ' ' or CRLF
input,
at
)
if (
(headerValue[0] !== 0x75 && headerValue[0] !== 0x55) || // u or U
(headerValue[1] !== 0x74 && headerValue[1] !== 0x54) || // t or T
(headerValue[2] !== 0x66 && headerValue[2] !== 0x46) || // f or F
headerValue[3] !== 0x2d || // -
headerValue[4] !== 0x38 // 8
) {
throw parsingError('unknown encoding, expected utf-8\'\'')
}
// skip utf-8''
filename = decodeURIComponent(new TextDecoder().decode(headerValue.subarray(7)))
position.position = at.position
} else {
// 1. Advance position so it points at the byte after the next 0x22 (") byte
// (the one in the sequence of bytes matched above).
position.position += 11
// Remove leading http tab and spaces. See RFC for examples.
// https://datatracker.ietf.org/doc/html/rfc6266#section-5
collectASequenceOfBytes(
(char) => char === 0x20 || char === 0x09,
input,
position
)
position.position++ // skip past " after removing whitespace
// 2. Set filename to the result of parsing a multipart/form-data name given
// input and position, if the result is not failure. Otherwise, return failure.
filename = parseMultipartFormDataName(input, position)
}
}
}
break
}
case 'content-type': {
// 1. Let header value be the result of collecting a sequence of bytes that are
// not 0x0A (LF) or 0x0D (CR), given position.
let headerValue = collectASequenceOfBytes(
(char) => char !== 0x0a && char !== 0x0d,
input,
position
)
// 2. Remove any HTTP tab or space bytes from the end of header value.
headerValue = removeChars(headerValue, false, true, (char) => char === 0x9 || char === 0x20)
// 3. Set contentType to the isomorphic decoding of header value.
contentType = isomorphicDecode(headerValue)
break
}
case 'content-transfer-encoding': {
let headerValue = collectASequenceOfBytes(
(char) => char !== 0x0a && char !== 0x0d,
input,
position
)
headerValue = removeChars(headerValue, false, true, (char) => char === 0x9 || char === 0x20)
encoding = isomorphicDecode(headerValue)
break
}
default: {
// Collect a sequence of bytes that are not 0x0A (LF) or 0x0D (CR), given position.
// (Do nothing with those bytes.)
collectASequenceOfBytes(
(char) => char !== 0x0a && char !== 0x0d,
input,
position
)
}
}
// 2.9. If position does not point to a sequence of bytes starting with 0x0D 0x0A
// (CR LF), return failure. Otherwise, advance position by 2 (past the newline).
if (input[position.position] !== 0x0d && input[position.position + 1] !== 0x0a) {
throw parsingError('expected CRLF')
} else {
position.position += 2
}
}
}
/**
* @see https://andreubotella.github.io/multipart-form-data/#parse-a-multipart-form-data-name
* @param {Buffer} input
* @param {{ position: number }} position
*/
function parseMultipartFormDataName (input, position) {
// 1. Assert: The byte at (position - 1) is 0x22 (").
assert(input[position.position - 1] === 0x22)
// 2. Let name be the result of collecting a sequence of bytes that are not 0x0A (LF), 0x0D (CR) or 0x22 ("), given position.
/** @type {string | Buffer} */
let name = collectASequenceOfBytes(
(char) => char !== 0x0a && char !== 0x0d && char !== 0x22,
input,
position
)
// 3. If the byte at position is not 0x22 ("), return failure. Otherwise, advance position by 1.
if (input[position.position] !== 0x22) {
throw parsingError('expected "')
} else {
position.position++
}
// 4. Replace any occurrence of the following subsequences in name with the given byte:
// - `%0A`: 0x0A (LF)
// - `%0D`: 0x0D (CR)
// - `%22`: 0x22 (")
name = new TextDecoder().decode(name)
.replace(/%0A/ig, '\n')
.replace(/%0D/ig, '\r')
.replace(/%22/g, '"')
// 5. Return the UTF-8 decoding without BOM of name.
return name
}
/**
* @param {(char: number) => boolean} condition
* @param {Buffer} input
* @param {{ position: number }} position
*/
function collectASequenceOfBytes (condition, input, position) {
let start = position.position
while (start < input.length && condition(input[start])) {
++start
}
return input.subarray(position.position, (position.position = start))
}
/**
* @param {Buffer} buf
* @param {boolean} leading
* @param {boolean} trailing
* @param {(charCode: number) => boolean} predicate
* @returns {Buffer}
*/
function removeChars (buf, leading, trailing, predicate) {
let lead = 0
let trail = buf.length - 1
if (leading) {
while (lead < buf.length && predicate(buf[lead])) lead++
}
if (trailing) {
while (trail > 0 && predicate(buf[trail])) trail--
}
return lead === 0 && trail === buf.length - 1 ? buf : buf.subarray(lead, trail + 1)
}
/**
* Checks if {@param buffer} starts with {@param start}
* @param {Buffer} buffer
* @param {Buffer} start
* @param {{ position: number }} position
*/
function bufferStartsWith (buffer, start, position) {
if (buffer.length < start.length) {
return false
}
for (let i = 0; i < start.length; i++) {
if (start[i] !== buffer[position.position + i]) {
return false
}
}
return true
}
function parsingError (cause) {
return new TypeError('Failed to parse body as FormData.', { cause: new TypeError(cause) })
}
module.exports = {
multipartFormDataParser,
validateBoundary
}

263
node_modules/undici/lib/web/fetch/formdata.js generated vendored Normal file
View File

@ -0,0 +1,263 @@
'use strict'
const { iteratorMixin } = require('./util')
const { kEnumerableProperty } = require('../../core/util')
const { webidl } = require('./webidl')
const { File: NativeFile } = require('node:buffer')
const nodeUtil = require('node:util')
/** @type {globalThis['File']} */
const File = globalThis.File ?? NativeFile
// https://xhr.spec.whatwg.org/#formdata
class FormData {
#state = []
constructor (form) {
webidl.util.markAsUncloneable(this)
if (form !== undefined) {
throw webidl.errors.conversionFailed({
prefix: 'FormData constructor',
argument: 'Argument 1',
types: ['undefined']
})
}
}
append (name, value, filename = undefined) {
webidl.brandCheck(this, FormData)
const prefix = 'FormData.append'
webidl.argumentLengthCheck(arguments, 2, prefix)
name = webidl.converters.USVString(name)
if (arguments.length === 3 || webidl.is.Blob(value)) {
value = webidl.converters.Blob(value, prefix, 'value')
if (filename !== undefined) {
filename = webidl.converters.USVString(filename)
}
} else {
value = webidl.converters.USVString(value)
}
// 1. Let value be value if given; otherwise blobValue.
// 2. Let entry be the result of creating an entry with
// name, value, and filename if given.
const entry = makeEntry(name, value, filename)
// 3. Append entry to thiss entry list.
this.#state.push(entry)
}
delete (name) {
webidl.brandCheck(this, FormData)
const prefix = 'FormData.delete'
webidl.argumentLengthCheck(arguments, 1, prefix)
name = webidl.converters.USVString(name)
// The delete(name) method steps are to remove all entries whose name
// is name from thiss entry list.
this.#state = this.#state.filter(entry => entry.name !== name)
}
get (name) {
webidl.brandCheck(this, FormData)
const prefix = 'FormData.get'
webidl.argumentLengthCheck(arguments, 1, prefix)
name = webidl.converters.USVString(name)
// 1. If there is no entry whose name is name in thiss entry list,
// then return null.
const idx = this.#state.findIndex((entry) => entry.name === name)
if (idx === -1) {
return null
}
// 2. Return the value of the first entry whose name is name from
// thiss entry list.
return this.#state[idx].value
}
getAll (name) {
webidl.brandCheck(this, FormData)
const prefix = 'FormData.getAll'
webidl.argumentLengthCheck(arguments, 1, prefix)
name = webidl.converters.USVString(name)
// 1. If there is no entry whose name is name in thiss entry list,
// then return the empty list.
// 2. Return the values of all entries whose name is name, in order,
// from thiss entry list.
return this.#state
.filter((entry) => entry.name === name)
.map((entry) => entry.value)
}
has (name) {
webidl.brandCheck(this, FormData)
const prefix = 'FormData.has'
webidl.argumentLengthCheck(arguments, 1, prefix)
name = webidl.converters.USVString(name)
// The has(name) method steps are to return true if there is an entry
// whose name is name in thiss entry list; otherwise false.
return this.#state.findIndex((entry) => entry.name === name) !== -1
}
set (name, value, filename = undefined) {
webidl.brandCheck(this, FormData)
const prefix = 'FormData.set'
webidl.argumentLengthCheck(arguments, 2, prefix)
name = webidl.converters.USVString(name)
if (arguments.length === 3 || webidl.is.Blob(value)) {
value = webidl.converters.Blob(value, prefix, 'value')
if (filename !== undefined) {
filename = webidl.converters.USVString(filename)
}
} else {
value = webidl.converters.USVString(value)
}
// The set(name, value) and set(name, blobValue, filename) method steps
// are:
// 1. Let value be value if given; otherwise blobValue.
// 2. Let entry be the result of creating an entry with name, value, and
// filename if given.
const entry = makeEntry(name, value, filename)
// 3. If there are entries in thiss entry list whose name is name, then
// replace the first such entry with entry and remove the others.
const idx = this.#state.findIndex((entry) => entry.name === name)
if (idx !== -1) {
this.#state = [
...this.#state.slice(0, idx),
entry,
...this.#state.slice(idx + 1).filter((entry) => entry.name !== name)
]
} else {
// 4. Otherwise, append entry to thiss entry list.
this.#state.push(entry)
}
}
[nodeUtil.inspect.custom] (depth, options) {
const state = this.#state.reduce((a, b) => {
if (a[b.name]) {
if (Array.isArray(a[b.name])) {
a[b.name].push(b.value)
} else {
a[b.name] = [a[b.name], b.value]
}
} else {
a[b.name] = b.value
}
return a
}, { __proto__: null })
options.depth ??= depth
options.colors ??= true
const output = nodeUtil.formatWithOptions(options, state)
// remove [Object null prototype]
return `FormData ${output.slice(output.indexOf(']') + 2)}`
}
/**
* @param {FormData} formData
*/
static getFormDataState (formData) {
return formData.#state
}
/**
* @param {FormData} formData
* @param {any[]} newState
*/
static setFormDataState (formData, newState) {
formData.#state = newState
}
}
const { getFormDataState, setFormDataState } = FormData
Reflect.deleteProperty(FormData, 'getFormDataState')
Reflect.deleteProperty(FormData, 'setFormDataState')
iteratorMixin('FormData', FormData, getFormDataState, 'name', 'value')
Object.defineProperties(FormData.prototype, {
append: kEnumerableProperty,
delete: kEnumerableProperty,
get: kEnumerableProperty,
getAll: kEnumerableProperty,
has: kEnumerableProperty,
set: kEnumerableProperty,
[Symbol.toStringTag]: {
value: 'FormData',
configurable: true
}
})
/**
* @see https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#create-an-entry
* @param {string} name
* @param {string|Blob} value
* @param {?string} filename
* @returns
*/
function makeEntry (name, value, filename) {
// 1. Set name to the result of converting name into a scalar value string.
// Note: This operation was done by the webidl converter USVString.
// 2. If value is a string, then set value to the result of converting
// value into a scalar value string.
if (typeof value === 'string') {
// Note: This operation was done by the webidl converter USVString.
} else {
// 3. Otherwise:
// 1. If value is not a File object, then set value to a new File object,
// representing the same bytes, whose name attribute value is "blob"
if (!webidl.is.File(value)) {
value = new File([value], 'blob', { type: value.type })
}
// 2. If filename is given, then set value to a new File object,
// representing the same bytes, whose name attribute is filename.
if (filename !== undefined) {
/** @type {FilePropertyBag} */
const options = {
type: value.type,
lastModified: value.lastModified
}
value = new File([value], filename, options)
}
}
// 4. Return an entry whose name is name and whose value is value.
return { name, value }
}
webidl.is.FormData = webidl.util.MakeTypeAssertion(FormData)
module.exports = { FormData, makeEntry, setFormDataState }

40
node_modules/undici/lib/web/fetch/global.js generated vendored Normal file
View File

@ -0,0 +1,40 @@
'use strict'
// In case of breaking changes, increase the version
// number to avoid conflicts.
const globalOrigin = Symbol.for('undici.globalOrigin.1')
function getGlobalOrigin () {
return globalThis[globalOrigin]
}
function setGlobalOrigin (newOrigin) {
if (newOrigin === undefined) {
Object.defineProperty(globalThis, globalOrigin, {
value: undefined,
writable: true,
enumerable: false,
configurable: false
})
return
}
const parsedURL = new URL(newOrigin)
if (parsedURL.protocol !== 'http:' && parsedURL.protocol !== 'https:') {
throw new TypeError(`Only http & https urls are allowed, received ${parsedURL.protocol}`)
}
Object.defineProperty(globalThis, globalOrigin, {
value: parsedURL,
writable: true,
enumerable: false,
configurable: false
})
}
module.exports = {
getGlobalOrigin,
setGlobalOrigin
}

719
node_modules/undici/lib/web/fetch/headers.js generated vendored Normal file
View File

@ -0,0 +1,719 @@
// https://github.com/Ethan-Arrowood/undici-fetch
'use strict'
const { kConstruct } = require('../../core/symbols')
const { kEnumerableProperty } = require('../../core/util')
const {
iteratorMixin,
isValidHeaderName,
isValidHeaderValue
} = require('./util')
const { webidl } = require('./webidl')
const assert = require('node:assert')
const util = require('node:util')
/**
* @param {number} code
* @returns {code is (0x0a | 0x0d | 0x09 | 0x20)}
*/
function isHTTPWhiteSpaceCharCode (code) {
return code === 0x0a || code === 0x0d || code === 0x09 || code === 0x20
}
/**
* @see https://fetch.spec.whatwg.org/#concept-header-value-normalize
* @param {string} potentialValue
* @returns {string}
*/
function headerValueNormalize (potentialValue) {
// To normalize a byte sequence potentialValue, remove
// any leading and trailing HTTP whitespace bytes from
// potentialValue.
let i = 0; let j = potentialValue.length
while (j > i && isHTTPWhiteSpaceCharCode(potentialValue.charCodeAt(j - 1))) --j
while (j > i && isHTTPWhiteSpaceCharCode(potentialValue.charCodeAt(i))) ++i
return i === 0 && j === potentialValue.length ? potentialValue : potentialValue.substring(i, j)
}
/**
* @param {Headers} headers
* @param {Array|Object} object
*/
function fill (headers, object) {
// To fill a Headers object headers with a given object object, run these steps:
// 1. If object is a sequence, then for each header in object:
// Note: webidl conversion to array has already been done.
if (Array.isArray(object)) {
for (let i = 0; i < object.length; ++i) {
const header = object[i]
// 1. If header does not contain exactly two items, then throw a TypeError.
if (header.length !== 2) {
throw webidl.errors.exception({
header: 'Headers constructor',
message: `expected name/value pair to be length 2, found ${header.length}.`
})
}
// 2. Append (headers first item, headers second item) to headers.
appendHeader(headers, header[0], header[1])
}
} else if (typeof object === 'object' && object !== null) {
// Note: null should throw
// 2. Otherwise, object is a record, then for each key → value in object,
// append (key, value) to headers
const keys = Object.keys(object)
for (let i = 0; i < keys.length; ++i) {
appendHeader(headers, keys[i], object[keys[i]])
}
} else {
throw webidl.errors.conversionFailed({
prefix: 'Headers constructor',
argument: 'Argument 1',
types: ['sequence<sequence<ByteString>>', 'record<ByteString, ByteString>']
})
}
}
/**
* @see https://fetch.spec.whatwg.org/#concept-headers-append
* @param {Headers} headers
* @param {string} name
* @param {string} value
*/
function appendHeader (headers, name, value) {
// 1. Normalize value.
value = headerValueNormalize(value)
// 2. If name is not a header name or value is not a
// header value, then throw a TypeError.
if (!isValidHeaderName(name)) {
throw webidl.errors.invalidArgument({
prefix: 'Headers.append',
value: name,
type: 'header name'
})
} else if (!isValidHeaderValue(value)) {
throw webidl.errors.invalidArgument({
prefix: 'Headers.append',
value,
type: 'header value'
})
}
// 3. If headerss guard is "immutable", then throw a TypeError.
// 4. Otherwise, if headerss guard is "request" and name is a
// forbidden header name, return.
// 5. Otherwise, if headerss guard is "request-no-cors":
// TODO
// Note: undici does not implement forbidden header names
if (getHeadersGuard(headers) === 'immutable') {
throw new TypeError('immutable')
}
// 6. Otherwise, if headerss guard is "response" and name is a
// forbidden response-header name, return.
// 7. Append (name, value) to headerss header list.
return getHeadersList(headers).append(name, value, false)
// 8. If headerss guard is "request-no-cors", then remove
// privileged no-CORS request headers from headers
}
// https://fetch.spec.whatwg.org/#concept-header-list-sort-and-combine
/**
* @param {Headers} target
*/
function headersListSortAndCombine (target) {
const headersList = getHeadersList(target)
if (!headersList) {
return []
}
if (headersList.sortedMap) {
return headersList.sortedMap
}
// 1. Let headers be an empty list of headers with the key being the name
// and value the value.
const headers = []
// 2. Let names be the result of convert header names to a sorted-lowercase
// set with all the names of the headers in list.
const names = headersList.toSortedArray()
const cookies = headersList.cookies
// fast-path
if (cookies === null || cookies.length === 1) {
// Note: The non-null assertion of value has already been done by `HeadersList#toSortedArray`
return (headersList.sortedMap = names)
}
// 3. For each name of names:
for (let i = 0; i < names.length; ++i) {
const { 0: name, 1: value } = names[i]
// 1. If name is `set-cookie`, then:
if (name === 'set-cookie') {
// 1. Let values be a list of all values of headers in list whose name
// is a byte-case-insensitive match for name, in order.
// 2. For each value of values:
// 1. Append (name, value) to headers.
for (let j = 0; j < cookies.length; ++j) {
headers.push([name, cookies[j]])
}
} else {
// 2. Otherwise:
// 1. Let value be the result of getting name from list.
// 2. Assert: value is non-null.
// Note: This operation was done by `HeadersList#toSortedArray`.
// 3. Append (name, value) to headers.
headers.push([name, value])
}
}
// 4. Return headers.
return (headersList.sortedMap = headers)
}
function compareHeaderName (a, b) {
return a[0] < b[0] ? -1 : 1
}
class HeadersList {
/** @type {[string, string][]|null} */
cookies = null
sortedMap
headersMap
constructor (init) {
if (init instanceof HeadersList) {
this.headersMap = new Map(init.headersMap)
this.sortedMap = init.sortedMap
this.cookies = init.cookies === null ? null : [...init.cookies]
} else {
this.headersMap = new Map(init)
this.sortedMap = null
}
}
/**
* @see https://fetch.spec.whatwg.org/#header-list-contains
* @param {string} name
* @param {boolean} isLowerCase
*/
contains (name, isLowerCase) {
// A header list list contains a header name name if list
// contains a header whose name is a byte-case-insensitive
// match for name.
return this.headersMap.has(isLowerCase ? name : name.toLowerCase())
}
clear () {
this.headersMap.clear()
this.sortedMap = null
this.cookies = null
}
/**
* @see https://fetch.spec.whatwg.org/#concept-header-list-append
* @param {string} name
* @param {string} value
* @param {boolean} isLowerCase
*/
append (name, value, isLowerCase) {
this.sortedMap = null
// 1. If list contains name, then set name to the first such
// headers name.
const lowercaseName = isLowerCase ? name : name.toLowerCase()
const exists = this.headersMap.get(lowercaseName)
// 2. Append (name, value) to list.
if (exists) {
const delimiter = lowercaseName === 'cookie' ? '; ' : ', '
this.headersMap.set(lowercaseName, {
name: exists.name,
value: `${exists.value}${delimiter}${value}`
})
} else {
this.headersMap.set(lowercaseName, { name, value })
}
if (lowercaseName === 'set-cookie') {
(this.cookies ??= []).push(value)
}
}
/**
* @see https://fetch.spec.whatwg.org/#concept-header-list-set
* @param {string} name
* @param {string} value
* @param {boolean} isLowerCase
*/
set (name, value, isLowerCase) {
this.sortedMap = null
const lowercaseName = isLowerCase ? name : name.toLowerCase()
if (lowercaseName === 'set-cookie') {
this.cookies = [value]
}
// 1. If list contains name, then set the value of
// the first such header to value and remove the
// others.
// 2. Otherwise, append header (name, value) to list.
this.headersMap.set(lowercaseName, { name, value })
}
/**
* @see https://fetch.spec.whatwg.org/#concept-header-list-delete
* @param {string} name
* @param {boolean} isLowerCase
*/
delete (name, isLowerCase) {
this.sortedMap = null
if (!isLowerCase) name = name.toLowerCase()
if (name === 'set-cookie') {
this.cookies = null
}
this.headersMap.delete(name)
}
/**
* @see https://fetch.spec.whatwg.org/#concept-header-list-get
* @param {string} name
* @param {boolean} isLowerCase
* @returns {string | null}
*/
get (name, isLowerCase) {
// 1. If list does not contain name, then return null.
// 2. Return the values of all headers in list whose name
// is a byte-case-insensitive match for name,
// separated from each other by 0x2C 0x20, in order.
return this.headersMap.get(isLowerCase ? name : name.toLowerCase())?.value ?? null
}
* [Symbol.iterator] () {
// use the lowercased name
for (const { 0: name, 1: { value } } of this.headersMap) {
yield [name, value]
}
}
get entries () {
const headers = {}
if (this.headersMap.size !== 0) {
for (const { name, value } of this.headersMap.values()) {
headers[name] = value
}
}
return headers
}
rawValues () {
return this.headersMap.values()
}
get entriesList () {
const headers = []
if (this.headersMap.size !== 0) {
for (const { 0: lowerName, 1: { name, value } } of this.headersMap) {
if (lowerName === 'set-cookie') {
for (const cookie of this.cookies) {
headers.push([name, cookie])
}
} else {
headers.push([name, value])
}
}
}
return headers
}
// https://fetch.spec.whatwg.org/#convert-header-names-to-a-sorted-lowercase-set
toSortedArray () {
const size = this.headersMap.size
const array = new Array(size)
// In most cases, you will use the fast-path.
// fast-path: Use binary insertion sort for small arrays.
if (size <= 32) {
if (size === 0) {
// If empty, it is an empty array. To avoid the first index assignment.
return array
}
// Improve performance by unrolling loop and avoiding double-loop.
// Double-loop-less version of the binary insertion sort.
const iterator = this.headersMap[Symbol.iterator]()
const firstValue = iterator.next().value
// set [name, value] to first index.
array[0] = [firstValue[0], firstValue[1].value]
// https://fetch.spec.whatwg.org/#concept-header-list-sort-and-combine
// 3.2.2. Assert: value is non-null.
assert(firstValue[1].value !== null)
for (
let i = 1, j = 0, right = 0, left = 0, pivot = 0, x, value;
i < size;
++i
) {
// get next value
value = iterator.next().value
// set [name, value] to current index.
x = array[i] = [value[0], value[1].value]
// https://fetch.spec.whatwg.org/#concept-header-list-sort-and-combine
// 3.2.2. Assert: value is non-null.
assert(x[1] !== null)
left = 0
right = i
// binary search
while (left < right) {
// middle index
pivot = left + ((right - left) >> 1)
// compare header name
if (array[pivot][0] <= x[0]) {
left = pivot + 1
} else {
right = pivot
}
}
if (i !== pivot) {
j = i
while (j > left) {
array[j] = array[--j]
}
array[left] = x
}
}
/* c8 ignore next 4 */
if (!iterator.next().done) {
// This is for debugging and will never be called.
throw new TypeError('Unreachable')
}
return array
} else {
// This case would be a rare occurrence.
// slow-path: fallback
let i = 0
for (const { 0: name, 1: { value } } of this.headersMap) {
array[i++] = [name, value]
// https://fetch.spec.whatwg.org/#concept-header-list-sort-and-combine
// 3.2.2. Assert: value is non-null.
assert(value !== null)
}
return array.sort(compareHeaderName)
}
}
}
// https://fetch.spec.whatwg.org/#headers-class
class Headers {
#guard
/**
* @type {HeadersList}
*/
#headersList
/**
* @param {HeadersInit|Symbol} [init]
* @returns
*/
constructor (init = undefined) {
webidl.util.markAsUncloneable(this)
if (init === kConstruct) {
return
}
this.#headersList = new HeadersList()
// The new Headers(init) constructor steps are:
// 1. Set thiss guard to "none".
this.#guard = 'none'
// 2. If init is given, then fill this with init.
if (init !== undefined) {
init = webidl.converters.HeadersInit(init, 'Headers constructor', 'init')
fill(this, init)
}
}
// https://fetch.spec.whatwg.org/#dom-headers-append
append (name, value) {
webidl.brandCheck(this, Headers)
webidl.argumentLengthCheck(arguments, 2, 'Headers.append')
const prefix = 'Headers.append'
name = webidl.converters.ByteString(name, prefix, 'name')
value = webidl.converters.ByteString(value, prefix, 'value')
return appendHeader(this, name, value)
}
// https://fetch.spec.whatwg.org/#dom-headers-delete
delete (name) {
webidl.brandCheck(this, Headers)
webidl.argumentLengthCheck(arguments, 1, 'Headers.delete')
const prefix = 'Headers.delete'
name = webidl.converters.ByteString(name, prefix, 'name')
// 1. If name is not a header name, then throw a TypeError.
if (!isValidHeaderName(name)) {
throw webidl.errors.invalidArgument({
prefix: 'Headers.delete',
value: name,
type: 'header name'
})
}
// 2. If thiss guard is "immutable", then throw a TypeError.
// 3. Otherwise, if thiss guard is "request" and name is a
// forbidden header name, return.
// 4. Otherwise, if thiss guard is "request-no-cors", name
// is not a no-CORS-safelisted request-header name, and
// name is not a privileged no-CORS request-header name,
// return.
// 5. Otherwise, if thiss guard is "response" and name is
// a forbidden response-header name, return.
// Note: undici does not implement forbidden header names
if (this.#guard === 'immutable') {
throw new TypeError('immutable')
}
// 6. If thiss header list does not contain name, then
// return.
if (!this.#headersList.contains(name, false)) {
return
}
// 7. Delete name from thiss header list.
// 8. If thiss guard is "request-no-cors", then remove
// privileged no-CORS request headers from this.
this.#headersList.delete(name, false)
}
// https://fetch.spec.whatwg.org/#dom-headers-get
get (name) {
webidl.brandCheck(this, Headers)
webidl.argumentLengthCheck(arguments, 1, 'Headers.get')
const prefix = 'Headers.get'
name = webidl.converters.ByteString(name, prefix, 'name')
// 1. If name is not a header name, then throw a TypeError.
if (!isValidHeaderName(name)) {
throw webidl.errors.invalidArgument({
prefix,
value: name,
type: 'header name'
})
}
// 2. Return the result of getting name from thiss header
// list.
return this.#headersList.get(name, false)
}
// https://fetch.spec.whatwg.org/#dom-headers-has
has (name) {
webidl.brandCheck(this, Headers)
webidl.argumentLengthCheck(arguments, 1, 'Headers.has')
const prefix = 'Headers.has'
name = webidl.converters.ByteString(name, prefix, 'name')
// 1. If name is not a header name, then throw a TypeError.
if (!isValidHeaderName(name)) {
throw webidl.errors.invalidArgument({
prefix,
value: name,
type: 'header name'
})
}
// 2. Return true if thiss header list contains name;
// otherwise false.
return this.#headersList.contains(name, false)
}
// https://fetch.spec.whatwg.org/#dom-headers-set
set (name, value) {
webidl.brandCheck(this, Headers)
webidl.argumentLengthCheck(arguments, 2, 'Headers.set')
const prefix = 'Headers.set'
name = webidl.converters.ByteString(name, prefix, 'name')
value = webidl.converters.ByteString(value, prefix, 'value')
// 1. Normalize value.
value = headerValueNormalize(value)
// 2. If name is not a header name or value is not a
// header value, then throw a TypeError.
if (!isValidHeaderName(name)) {
throw webidl.errors.invalidArgument({
prefix,
value: name,
type: 'header name'
})
} else if (!isValidHeaderValue(value)) {
throw webidl.errors.invalidArgument({
prefix,
value,
type: 'header value'
})
}
// 3. If thiss guard is "immutable", then throw a TypeError.
// 4. Otherwise, if thiss guard is "request" and name is a
// forbidden header name, return.
// 5. Otherwise, if thiss guard is "request-no-cors" and
// name/value is not a no-CORS-safelisted request-header,
// return.
// 6. Otherwise, if thiss guard is "response" and name is a
// forbidden response-header name, return.
// Note: undici does not implement forbidden header names
if (this.#guard === 'immutable') {
throw new TypeError('immutable')
}
// 7. Set (name, value) in thiss header list.
// 8. If thiss guard is "request-no-cors", then remove
// privileged no-CORS request headers from this
this.#headersList.set(name, value, false)
}
// https://fetch.spec.whatwg.org/#dom-headers-getsetcookie
getSetCookie () {
webidl.brandCheck(this, Headers)
// 1. If thiss header list does not contain `Set-Cookie`, then return « ».
// 2. Return the values of all headers in thiss header list whose name is
// a byte-case-insensitive match for `Set-Cookie`, in order.
const list = this.#headersList.cookies
if (list) {
return [...list]
}
return []
}
[util.inspect.custom] (depth, options) {
options.depth ??= depth
return `Headers ${util.formatWithOptions(options, this.#headersList.entries)}`
}
static getHeadersGuard (o) {
return o.#guard
}
static setHeadersGuard (o, guard) {
o.#guard = guard
}
/**
* @param {Headers} o
*/
static getHeadersList (o) {
return o.#headersList
}
/**
* @param {Headers} target
* @param {HeadersList} list
*/
static setHeadersList (target, list) {
target.#headersList = list
}
}
const { getHeadersGuard, setHeadersGuard, getHeadersList, setHeadersList } = Headers
Reflect.deleteProperty(Headers, 'getHeadersGuard')
Reflect.deleteProperty(Headers, 'setHeadersGuard')
Reflect.deleteProperty(Headers, 'getHeadersList')
Reflect.deleteProperty(Headers, 'setHeadersList')
iteratorMixin('Headers', Headers, headersListSortAndCombine, 0, 1)
Object.defineProperties(Headers.prototype, {
append: kEnumerableProperty,
delete: kEnumerableProperty,
get: kEnumerableProperty,
has: kEnumerableProperty,
set: kEnumerableProperty,
getSetCookie: kEnumerableProperty,
[Symbol.toStringTag]: {
value: 'Headers',
configurable: true
},
[util.inspect.custom]: {
enumerable: false
}
})
webidl.converters.HeadersInit = function (V, prefix, argument) {
if (webidl.util.Type(V) === webidl.util.Types.OBJECT) {
const iterator = Reflect.get(V, Symbol.iterator)
// A work-around to ensure we send the properly-cased Headers when V is a Headers object.
// Read https://github.com/nodejs/undici/pull/3159#issuecomment-2075537226 before touching, please.
if (!util.types.isProxy(V) && iterator === Headers.prototype.entries) { // Headers object
try {
return getHeadersList(V).entriesList
} catch {
// fall-through
}
}
if (typeof iterator === 'function') {
return webidl.converters['sequence<sequence<ByteString>>'](V, prefix, argument, iterator.bind(V))
}
return webidl.converters['record<ByteString, ByteString>'](V, prefix, argument)
}
throw webidl.errors.conversionFailed({
prefix: 'Headers constructor',
argument: 'Argument 1',
types: ['sequence<sequence<ByteString>>', 'record<ByteString, ByteString>']
})
}
module.exports = {
fill,
// for test.
compareHeaderName,
Headers,
HeadersList,
getHeadersGuard,
setHeadersGuard,
setHeadersList,
getHeadersList
}

2258
node_modules/undici/lib/web/fetch/index.js generated vendored Normal file

File diff suppressed because it is too large Load Diff

1099
node_modules/undici/lib/web/fetch/request.js generated vendored Normal file

File diff suppressed because it is too large Load Diff

636
node_modules/undici/lib/web/fetch/response.js generated vendored Normal file
View File

@ -0,0 +1,636 @@
'use strict'
const { Headers, HeadersList, fill, getHeadersGuard, setHeadersGuard, setHeadersList } = require('./headers')
const { extractBody, cloneBody, mixinBody, hasFinalizationRegistry, streamRegistry, bodyUnusable } = require('./body')
const util = require('../../core/util')
const nodeUtil = require('node:util')
const { kEnumerableProperty } = util
const {
isValidReasonPhrase,
isCancelled,
isAborted,
serializeJavascriptValueToJSONString,
isErrorLike,
isomorphicEncode,
environmentSettingsObject: relevantRealm
} = require('./util')
const {
redirectStatusSet,
nullBodyStatus
} = require('./constants')
const { webidl } = require('./webidl')
const { URLSerializer } = require('./data-url')
const { kConstruct } = require('../../core/symbols')
const assert = require('node:assert')
const { types } = require('node:util')
const textEncoder = new TextEncoder('utf-8')
// https://fetch.spec.whatwg.org/#response-class
class Response {
/** @type {Headers} */
#headers
#state
// Creates network error Response.
static error () {
// The static error() method steps are to return the result of creating a
// Response object, given a new network error, "immutable", and thiss
// relevant Realm.
const responseObject = fromInnerResponse(makeNetworkError(), 'immutable')
return responseObject
}
// https://fetch.spec.whatwg.org/#dom-response-json
static json (data, init = undefined) {
webidl.argumentLengthCheck(arguments, 1, 'Response.json')
if (init !== null) {
init = webidl.converters.ResponseInit(init)
}
// 1. Let bytes the result of running serialize a JavaScript value to JSON bytes on data.
const bytes = textEncoder.encode(
serializeJavascriptValueToJSONString(data)
)
// 2. Let body be the result of extracting bytes.
const body = extractBody(bytes)
// 3. Let responseObject be the result of creating a Response object, given a new response,
// "response", and thiss relevant Realm.
const responseObject = fromInnerResponse(makeResponse({}), 'response')
// 4. Perform initialize a response given responseObject, init, and (body, "application/json").
initializeResponse(responseObject, init, { body: body[0], type: 'application/json' })
// 5. Return responseObject.
return responseObject
}
// Creates a redirect Response that redirects to url with status status.
static redirect (url, status = 302) {
webidl.argumentLengthCheck(arguments, 1, 'Response.redirect')
url = webidl.converters.USVString(url)
status = webidl.converters['unsigned short'](status)
// 1. Let parsedURL be the result of parsing url with current settings
// objects API base URL.
// 2. If parsedURL is failure, then throw a TypeError.
// TODO: base-URL?
let parsedURL
try {
parsedURL = new URL(url, relevantRealm.settingsObject.baseUrl)
} catch (err) {
throw new TypeError(`Failed to parse URL from ${url}`, { cause: err })
}
// 3. If status is not a redirect status, then throw a RangeError.
if (!redirectStatusSet.has(status)) {
throw new RangeError(`Invalid status code ${status}`)
}
// 4. Let responseObject be the result of creating a Response object,
// given a new response, "immutable", and thiss relevant Realm.
const responseObject = fromInnerResponse(makeResponse({}), 'immutable')
// 5. Set responseObjects responses status to status.
responseObject.#state.status = status
// 6. Let value be parsedURL, serialized and isomorphic encoded.
const value = isomorphicEncode(URLSerializer(parsedURL))
// 7. Append `Location`/value to responseObjects responses header list.
responseObject.#state.headersList.append('location', value, true)
// 8. Return responseObject.
return responseObject
}
// https://fetch.spec.whatwg.org/#dom-response
constructor (body = null, init = undefined) {
webidl.util.markAsUncloneable(this)
if (body === kConstruct) {
return
}
if (body !== null) {
body = webidl.converters.BodyInit(body)
}
init = webidl.converters.ResponseInit(init)
// 1. Set thiss response to a new response.
this.#state = makeResponse({})
// 2. Set thiss headers to a new Headers object with thiss relevant
// Realm, whose header list is thiss responses header list and guard
// is "response".
this.#headers = new Headers(kConstruct)
setHeadersGuard(this.#headers, 'response')
setHeadersList(this.#headers, this.#state.headersList)
// 3. Let bodyWithType be null.
let bodyWithType = null
// 4. If body is non-null, then set bodyWithType to the result of extracting body.
if (body != null) {
const [extractedBody, type] = extractBody(body)
bodyWithType = { body: extractedBody, type }
}
// 5. Perform initialize a response given this, init, and bodyWithType.
initializeResponse(this, init, bodyWithType)
}
// Returns responses type, e.g., "cors".
get type () {
webidl.brandCheck(this, Response)
// The type getter steps are to return thiss responses type.
return this.#state.type
}
// Returns responses URL, if it has one; otherwise the empty string.
get url () {
webidl.brandCheck(this, Response)
const urlList = this.#state.urlList
// The url getter steps are to return the empty string if thiss
// responses URL is null; otherwise thiss responses URL,
// serialized with exclude fragment set to true.
const url = urlList[urlList.length - 1] ?? null
if (url === null) {
return ''
}
return URLSerializer(url, true)
}
// Returns whether response was obtained through a redirect.
get redirected () {
webidl.brandCheck(this, Response)
// The redirected getter steps are to return true if thiss responses URL
// list has more than one item; otherwise false.
return this.#state.urlList.length > 1
}
// Returns responses status.
get status () {
webidl.brandCheck(this, Response)
// The status getter steps are to return thiss responses status.
return this.#state.status
}
// Returns whether responses status is an ok status.
get ok () {
webidl.brandCheck(this, Response)
// The ok getter steps are to return true if thiss responses status is an
// ok status; otherwise false.
return this.#state.status >= 200 && this.#state.status <= 299
}
// Returns responses status message.
get statusText () {
webidl.brandCheck(this, Response)
// The statusText getter steps are to return thiss responses status
// message.
return this.#state.statusText
}
// Returns responses headers as Headers.
get headers () {
webidl.brandCheck(this, Response)
// The headers getter steps are to return thiss headers.
return this.#headers
}
get body () {
webidl.brandCheck(this, Response)
return this.#state.body ? this.#state.body.stream : null
}
get bodyUsed () {
webidl.brandCheck(this, Response)
return !!this.#state.body && util.isDisturbed(this.#state.body.stream)
}
// Returns a clone of response.
clone () {
webidl.brandCheck(this, Response)
// 1. If this is unusable, then throw a TypeError.
if (bodyUnusable(this.#state)) {
throw webidl.errors.exception({
header: 'Response.clone',
message: 'Body has already been consumed.'
})
}
// 2. Let clonedResponse be the result of cloning thiss response.
const clonedResponse = cloneResponse(this.#state)
// 3. Return the result of creating a Response object, given
// clonedResponse, thiss headerss guard, and thiss relevant Realm.
return fromInnerResponse(clonedResponse, getHeadersGuard(this.#headers))
}
[nodeUtil.inspect.custom] (depth, options) {
if (options.depth === null) {
options.depth = 2
}
options.colors ??= true
const properties = {
status: this.status,
statusText: this.statusText,
headers: this.headers,
body: this.body,
bodyUsed: this.bodyUsed,
ok: this.ok,
redirected: this.redirected,
type: this.type,
url: this.url
}
return `Response ${nodeUtil.formatWithOptions(options, properties)}`
}
/**
* @param {Response} response
*/
static getResponseHeaders (response) {
return response.#headers
}
/**
* @param {Response} response
* @param {Headers} newHeaders
*/
static setResponseHeaders (response, newHeaders) {
response.#headers = newHeaders
}
/**
* @param {Response} response
*/
static getResponseState (response) {
return response.#state
}
/**
* @param {Response} response
* @param {any} newState
*/
static setResponseState (response, newState) {
response.#state = newState
}
}
const { getResponseHeaders, setResponseHeaders, getResponseState, setResponseState } = Response
Reflect.deleteProperty(Response, 'getResponseHeaders')
Reflect.deleteProperty(Response, 'setResponseHeaders')
Reflect.deleteProperty(Response, 'getResponseState')
Reflect.deleteProperty(Response, 'setResponseState')
mixinBody(Response, getResponseState)
Object.defineProperties(Response.prototype, {
type: kEnumerableProperty,
url: kEnumerableProperty,
status: kEnumerableProperty,
ok: kEnumerableProperty,
redirected: kEnumerableProperty,
statusText: kEnumerableProperty,
headers: kEnumerableProperty,
clone: kEnumerableProperty,
body: kEnumerableProperty,
bodyUsed: kEnumerableProperty,
[Symbol.toStringTag]: {
value: 'Response',
configurable: true
}
})
Object.defineProperties(Response, {
json: kEnumerableProperty,
redirect: kEnumerableProperty,
error: kEnumerableProperty
})
// https://fetch.spec.whatwg.org/#concept-response-clone
function cloneResponse (response) {
// To clone a response response, run these steps:
// 1. If response is a filtered response, then return a new identical
// filtered response whose internal response is a clone of responses
// internal response.
if (response.internalResponse) {
return filterResponse(
cloneResponse(response.internalResponse),
response.type
)
}
// 2. Let newResponse be a copy of response, except for its body.
const newResponse = makeResponse({ ...response, body: null })
// 3. If responses body is non-null, then set newResponses body to the
// result of cloning responses body.
if (response.body != null) {
newResponse.body = cloneBody(newResponse, response.body)
}
// 4. Return newResponse.
return newResponse
}
function makeResponse (init) {
return {
aborted: false,
rangeRequested: false,
timingAllowPassed: false,
requestIncludesCredentials: false,
type: 'default',
status: 200,
timingInfo: null,
cacheState: '',
statusText: '',
...init,
headersList: init?.headersList
? new HeadersList(init?.headersList)
: new HeadersList(),
urlList: init?.urlList ? [...init.urlList] : []
}
}
function makeNetworkError (reason) {
const isError = isErrorLike(reason)
return makeResponse({
type: 'error',
status: 0,
error: isError
? reason
: new Error(reason ? String(reason) : reason),
aborted: reason && reason.name === 'AbortError'
})
}
// @see https://fetch.spec.whatwg.org/#concept-network-error
function isNetworkError (response) {
return (
// A network error is a response whose type is "error",
response.type === 'error' &&
// status is 0
response.status === 0
)
}
function makeFilteredResponse (response, state) {
state = {
internalResponse: response,
...state
}
return new Proxy(response, {
get (target, p) {
return p in state ? state[p] : target[p]
},
set (target, p, value) {
assert(!(p in state))
target[p] = value
return true
}
})
}
// https://fetch.spec.whatwg.org/#concept-filtered-response
function filterResponse (response, type) {
// Set response to the following filtered response with response as its
// internal response, depending on requests response tainting:
if (type === 'basic') {
// A basic filtered response is a filtered response whose type is "basic"
// and header list excludes any headers in internal responses header list
// whose name is a forbidden response-header name.
// Note: undici does not implement forbidden response-header names
return makeFilteredResponse(response, {
type: 'basic',
headersList: response.headersList
})
} else if (type === 'cors') {
// A CORS filtered response is a filtered response whose type is "cors"
// and header list excludes any headers in internal responses header
// list whose name is not a CORS-safelisted response-header name, given
// internal responses CORS-exposed header-name list.
// Note: undici does not implement CORS-safelisted response-header names
return makeFilteredResponse(response, {
type: 'cors',
headersList: response.headersList
})
} else if (type === 'opaque') {
// An opaque filtered response is a filtered response whose type is
// "opaque", URL list is the empty list, status is 0, status message
// is the empty byte sequence, header list is empty, and body is null.
return makeFilteredResponse(response, {
type: 'opaque',
urlList: Object.freeze([]),
status: 0,
statusText: '',
body: null
})
} else if (type === 'opaqueredirect') {
// An opaque-redirect filtered response is a filtered response whose type
// is "opaqueredirect", status is 0, status message is the empty byte
// sequence, header list is empty, and body is null.
return makeFilteredResponse(response, {
type: 'opaqueredirect',
status: 0,
statusText: '',
headersList: [],
body: null
})
} else {
assert(false)
}
}
// https://fetch.spec.whatwg.org/#appropriate-network-error
function makeAppropriateNetworkError (fetchParams, err = null) {
// 1. Assert: fetchParams is canceled.
assert(isCancelled(fetchParams))
// 2. Return an aborted network error if fetchParams is aborted;
// otherwise return a network error.
return isAborted(fetchParams)
? makeNetworkError(Object.assign(new DOMException('The operation was aborted.', 'AbortError'), { cause: err }))
: makeNetworkError(Object.assign(new DOMException('Request was cancelled.'), { cause: err }))
}
// https://whatpr.org/fetch/1392.html#initialize-a-response
function initializeResponse (response, init, body) {
// 1. If init["status"] is not in the range 200 to 599, inclusive, then
// throw a RangeError.
if (init.status !== null && (init.status < 200 || init.status > 599)) {
throw new RangeError('init["status"] must be in the range of 200 to 599, inclusive.')
}
// 2. If init["statusText"] does not match the reason-phrase token production,
// then throw a TypeError.
if ('statusText' in init && init.statusText != null) {
// See, https://datatracker.ietf.org/doc/html/rfc7230#section-3.1.2:
// reason-phrase = *( HTAB / SP / VCHAR / obs-text )
if (!isValidReasonPhrase(String(init.statusText))) {
throw new TypeError('Invalid statusText')
}
}
// 3. Set responses responses status to init["status"].
if ('status' in init && init.status != null) {
getResponseState(response).status = init.status
}
// 4. Set responses responses status message to init["statusText"].
if ('statusText' in init && init.statusText != null) {
getResponseState(response).statusText = init.statusText
}
// 5. If init["headers"] exists, then fill responses headers with init["headers"].
if ('headers' in init && init.headers != null) {
fill(getResponseHeaders(response), init.headers)
}
// 6. If body was given, then:
if (body) {
// 1. If response's status is a null body status, then throw a TypeError.
if (nullBodyStatus.includes(response.status)) {
throw webidl.errors.exception({
header: 'Response constructor',
message: `Invalid response status code ${response.status}`
})
}
// 2. Set response's body to body's body.
getResponseState(response).body = body.body
// 3. If body's type is non-null and response's header list does not contain
// `Content-Type`, then append (`Content-Type`, body's type) to response's header list.
if (body.type != null && !getResponseState(response).headersList.contains('content-type', true)) {
getResponseState(response).headersList.append('content-type', body.type, true)
}
}
}
/**
* @see https://fetch.spec.whatwg.org/#response-create
* @param {any} innerResponse
* @param {'request' | 'immutable' | 'request-no-cors' | 'response' | 'none'} guard
* @returns {Response}
*/
function fromInnerResponse (innerResponse, guard) {
const response = new Response(kConstruct)
setResponseState(response, innerResponse)
const headers = new Headers(kConstruct)
setResponseHeaders(response, headers)
setHeadersList(headers, innerResponse.headersList)
setHeadersGuard(headers, guard)
if (hasFinalizationRegistry && innerResponse.body?.stream) {
// If the target (response) is reclaimed, the cleanup callback may be called at some point with
// the held value provided for it (innerResponse.body.stream). The held value can be any value:
// a primitive or an object, even undefined. If the held value is an object, the registry keeps
// a strong reference to it (so it can pass it to the cleanup callback later). Reworded from
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/FinalizationRegistry
streamRegistry.register(response, new WeakRef(innerResponse.body.stream))
}
return response
}
// https://fetch.spec.whatwg.org/#typedefdef-xmlhttprequestbodyinit
webidl.converters.XMLHttpRequestBodyInit = function (V, prefix, name) {
if (typeof V === 'string') {
return webidl.converters.USVString(V, prefix, name)
}
if (webidl.is.Blob(V)) {
return V
}
if (ArrayBuffer.isView(V) || types.isArrayBuffer(V)) {
return V
}
if (webidl.is.FormData(V)) {
return V
}
if (webidl.is.URLSearchParams(V)) {
return V
}
return webidl.converters.DOMString(V, prefix, name)
}
// https://fetch.spec.whatwg.org/#bodyinit
webidl.converters.BodyInit = function (V, prefix, argument) {
if (webidl.is.ReadableStream(V)) {
return V
}
// Note: the spec doesn't include async iterables,
// this is an undici extension.
if (V?.[Symbol.asyncIterator]) {
return V
}
return webidl.converters.XMLHttpRequestBodyInit(V, prefix, argument)
}
webidl.converters.ResponseInit = webidl.dictionaryConverter([
{
key: 'status',
converter: webidl.converters['unsigned short'],
defaultValue: () => 200
},
{
key: 'statusText',
converter: webidl.converters.ByteString,
defaultValue: () => ''
},
{
key: 'headers',
converter: webidl.converters.HeadersInit
}
])
webidl.is.Response = webidl.util.MakeTypeAssertion(Response)
module.exports = {
isNetworkError,
makeNetworkError,
makeResponse,
makeAppropriateNetworkError,
filterResponse,
Response,
cloneResponse,
fromInnerResponse,
getResponseState
}

1782
node_modules/undici/lib/web/fetch/util.js generated vendored Normal file

File diff suppressed because it is too large Load Diff

740
node_modules/undici/lib/web/fetch/webidl.js generated vendored Normal file
View File

@ -0,0 +1,740 @@
'use strict'
const { types, inspect } = require('node:util')
const { markAsUncloneable } = require('node:worker_threads')
const { toUSVString } = require('../../core/util')
const UNDEFINED = 1
const BOOLEAN = 2
const STRING = 3
const SYMBOL = 4
const NUMBER = 5
const BIGINT = 6
const NULL = 7
const OBJECT = 8 // function and object
const FunctionPrototypeSymbolHasInstance = Function.call.bind(Function.prototype[Symbol.hasInstance])
/** @type {import('../../../types/webidl').Webidl} */
const webidl = {
converters: {},
util: {},
errors: {},
is: {}
}
webidl.errors.exception = function (message) {
return new TypeError(`${message.header}: ${message.message}`)
}
webidl.errors.conversionFailed = function (context) {
const plural = context.types.length === 1 ? '' : ' one of'
const message =
`${context.argument} could not be converted to` +
`${plural}: ${context.types.join(', ')}.`
return webidl.errors.exception({
header: context.prefix,
message
})
}
webidl.errors.invalidArgument = function (context) {
return webidl.errors.exception({
header: context.prefix,
message: `"${context.value}" is an invalid ${context.type}.`
})
}
// https://webidl.spec.whatwg.org/#implements
webidl.brandCheck = function (V, I) {
if (!FunctionPrototypeSymbolHasInstance(I, V)) {
const err = new TypeError('Illegal invocation')
err.code = 'ERR_INVALID_THIS' // node compat.
throw err
}
}
webidl.brandCheckMultiple = function (List) {
const prototypes = List.map((c) => webidl.util.MakeTypeAssertion(c))
return (V) => {
if (prototypes.every(typeCheck => !typeCheck(V))) {
const err = new TypeError('Illegal invocation')
err.code = 'ERR_INVALID_THIS' // node compat.
throw err
}
}
}
webidl.argumentLengthCheck = function ({ length }, min, ctx) {
if (length < min) {
throw webidl.errors.exception({
message: `${min} argument${min !== 1 ? 's' : ''} required, ` +
`but${length ? ' only' : ''} ${length} found.`,
header: ctx
})
}
}
webidl.illegalConstructor = function () {
throw webidl.errors.exception({
header: 'TypeError',
message: 'Illegal constructor'
})
}
webidl.util.MakeTypeAssertion = function (I) {
return (O) => FunctionPrototypeSymbolHasInstance(I, O)
}
// https://tc39.es/ecma262/#sec-ecmascript-data-types-and-values
webidl.util.Type = function (V) {
switch (typeof V) {
case 'undefined': return UNDEFINED
case 'boolean': return BOOLEAN
case 'string': return STRING
case 'symbol': return SYMBOL
case 'number': return NUMBER
case 'bigint': return BIGINT
case 'function':
case 'object': {
if (V === null) {
return NULL
}
return OBJECT
}
}
}
webidl.util.Types = {
UNDEFINED,
BOOLEAN,
STRING,
SYMBOL,
NUMBER,
BIGINT,
NULL,
OBJECT
}
webidl.util.TypeValueToString = function (o) {
switch (webidl.util.Type(o)) {
case UNDEFINED: return 'Undefined'
case BOOLEAN: return 'Boolean'
case STRING: return 'String'
case SYMBOL: return 'Symbol'
case NUMBER: return 'Number'
case BIGINT: return 'BigInt'
case NULL: return 'Null'
case OBJECT: return 'Object'
}
}
webidl.util.markAsUncloneable = markAsUncloneable || (() => {})
// https://webidl.spec.whatwg.org/#abstract-opdef-converttoint
webidl.util.ConvertToInt = function (V, bitLength, signedness, opts) {
let upperBound
let lowerBound
// 1. If bitLength is 64, then:
if (bitLength === 64) {
// 1. Let upperBound be 2^53 1.
upperBound = Math.pow(2, 53) - 1
// 2. If signedness is "unsigned", then let lowerBound be 0.
if (signedness === 'unsigned') {
lowerBound = 0
} else {
// 3. Otherwise let lowerBound be 2^53 + 1.
lowerBound = Math.pow(-2, 53) + 1
}
} else if (signedness === 'unsigned') {
// 2. Otherwise, if signedness is "unsigned", then:
// 1. Let lowerBound be 0.
lowerBound = 0
// 2. Let upperBound be 2^bitLength 1.
upperBound = Math.pow(2, bitLength) - 1
} else {
// 3. Otherwise:
// 1. Let lowerBound be -2^bitLength 1.
lowerBound = Math.pow(-2, bitLength) - 1
// 2. Let upperBound be 2^bitLength 1 1.
upperBound = Math.pow(2, bitLength - 1) - 1
}
// 4. Let x be ? ToNumber(V).
let x = Number(V)
// 5. If x is 0, then set x to +0.
if (x === 0) {
x = 0
}
// 6. If the conversion is to an IDL type associated
// with the [EnforceRange] extended attribute, then:
if (opts?.enforceRange === true) {
// 1. If x is NaN, +∞, or −∞, then throw a TypeError.
if (
Number.isNaN(x) ||
x === Number.POSITIVE_INFINITY ||
x === Number.NEGATIVE_INFINITY
) {
throw webidl.errors.exception({
header: 'Integer conversion',
message: `Could not convert ${webidl.util.Stringify(V)} to an integer.`
})
}
// 2. Set x to IntegerPart(x).
x = webidl.util.IntegerPart(x)
// 3. If x < lowerBound or x > upperBound, then
// throw a TypeError.
if (x < lowerBound || x > upperBound) {
throw webidl.errors.exception({
header: 'Integer conversion',
message: `Value must be between ${lowerBound}-${upperBound}, got ${x}.`
})
}
// 4. Return x.
return x
}
// 7. If x is not NaN and the conversion is to an IDL
// type associated with the [Clamp] extended
// attribute, then:
if (!Number.isNaN(x) && opts?.clamp === true) {
// 1. Set x to min(max(x, lowerBound), upperBound).
x = Math.min(Math.max(x, lowerBound), upperBound)
// 2. Round x to the nearest integer, choosing the
// even integer if it lies halfway between two,
// and choosing +0 rather than 0.
if (Math.floor(x) % 2 === 0) {
x = Math.floor(x)
} else {
x = Math.ceil(x)
}
// 3. Return x.
return x
}
// 8. If x is NaN, +0, +∞, or −∞, then return +0.
if (
Number.isNaN(x) ||
(x === 0 && Object.is(0, x)) ||
x === Number.POSITIVE_INFINITY ||
x === Number.NEGATIVE_INFINITY
) {
return 0
}
// 9. Set x to IntegerPart(x).
x = webidl.util.IntegerPart(x)
// 10. Set x to x modulo 2^bitLength.
x = x % Math.pow(2, bitLength)
// 11. If signedness is "signed" and x ≥ 2^bitLength 1,
// then return x 2^bitLength.
if (signedness === 'signed' && x >= Math.pow(2, bitLength) - 1) {
return x - Math.pow(2, bitLength)
}
// 12. Otherwise, return x.
return x
}
// https://webidl.spec.whatwg.org/#abstract-opdef-integerpart
webidl.util.IntegerPart = function (n) {
// 1. Let r be floor(abs(n)).
const r = Math.floor(Math.abs(n))
// 2. If n < 0, then return -1 × r.
if (n < 0) {
return -1 * r
}
// 3. Otherwise, return r.
return r
}
webidl.util.Stringify = function (V) {
const type = webidl.util.Type(V)
switch (type) {
case SYMBOL:
return `Symbol(${V.description})`
case OBJECT:
return inspect(V)
case STRING:
return `"${V}"`
default:
return `${V}`
}
}
// https://webidl.spec.whatwg.org/#es-sequence
webidl.sequenceConverter = function (converter) {
return (V, prefix, argument, Iterable) => {
// 1. If Type(V) is not Object, throw a TypeError.
if (webidl.util.Type(V) !== OBJECT) {
throw webidl.errors.exception({
header: prefix,
message: `${argument} (${webidl.util.Stringify(V)}) is not iterable.`
})
}
// 2. Let method be ? GetMethod(V, @@iterator).
/** @type {Generator} */
const method = typeof Iterable === 'function' ? Iterable() : V?.[Symbol.iterator]?.()
const seq = []
let index = 0
// 3. If method is undefined, throw a TypeError.
if (
method === undefined ||
typeof method.next !== 'function'
) {
throw webidl.errors.exception({
header: prefix,
message: `${argument} is not iterable.`
})
}
// https://webidl.spec.whatwg.org/#create-sequence-from-iterable
while (true) {
const { done, value } = method.next()
if (done) {
break
}
seq.push(converter(value, prefix, `${argument}[${index++}]`))
}
return seq
}
}
// https://webidl.spec.whatwg.org/#es-to-record
webidl.recordConverter = function (keyConverter, valueConverter) {
return (O, prefix, argument) => {
// 1. If Type(O) is not Object, throw a TypeError.
if (webidl.util.Type(O) !== OBJECT) {
throw webidl.errors.exception({
header: prefix,
message: `${argument} ("${webidl.util.TypeValueToString(O)}") is not an Object.`
})
}
// 2. Let result be a new empty instance of record<K, V>.
const result = {}
if (!types.isProxy(O)) {
// 1. Let desc be ? O.[[GetOwnProperty]](key).
const keys = [...Object.getOwnPropertyNames(O), ...Object.getOwnPropertySymbols(O)]
for (const key of keys) {
const keyName = webidl.util.Stringify(key)
// 1. Let typedKey be key converted to an IDL value of type K.
const typedKey = keyConverter(key, prefix, `Key ${keyName} in ${argument}`)
// 2. Let value be ? Get(O, key).
// 3. Let typedValue be value converted to an IDL value of type V.
const typedValue = valueConverter(O[key], prefix, `${argument}[${keyName}]`)
// 4. Set result[typedKey] to typedValue.
result[typedKey] = typedValue
}
// 5. Return result.
return result
}
// 3. Let keys be ? O.[[OwnPropertyKeys]]().
const keys = Reflect.ownKeys(O)
// 4. For each key of keys.
for (const key of keys) {
// 1. Let desc be ? O.[[GetOwnProperty]](key).
const desc = Reflect.getOwnPropertyDescriptor(O, key)
// 2. If desc is not undefined and desc.[[Enumerable]] is true:
if (desc?.enumerable) {
// 1. Let typedKey be key converted to an IDL value of type K.
const typedKey = keyConverter(key, prefix, argument)
// 2. Let value be ? Get(O, key).
// 3. Let typedValue be value converted to an IDL value of type V.
const typedValue = valueConverter(O[key], prefix, argument)
// 4. Set result[typedKey] to typedValue.
result[typedKey] = typedValue
}
}
// 5. Return result.
return result
}
}
webidl.interfaceConverter = function (TypeCheck, name) {
return (V, prefix, argument) => {
if (!TypeCheck(V)) {
throw webidl.errors.exception({
header: prefix,
message: `Expected ${argument} ("${webidl.util.Stringify(V)}") to be an instance of ${name}.`
})
}
return V
}
}
webidl.dictionaryConverter = function (converters) {
return (dictionary, prefix, argument) => {
const dict = {}
if (dictionary != null && webidl.util.Type(dictionary) !== OBJECT) {
throw webidl.errors.exception({
header: prefix,
message: `Expected ${dictionary} to be one of: Null, Undefined, Object.`
})
}
for (const options of converters) {
const { key, defaultValue, required, converter } = options
if (required === true) {
if (dictionary == null || !Object.hasOwn(dictionary, key)) {
throw webidl.errors.exception({
header: prefix,
message: `Missing required key "${key}".`
})
}
}
let value = dictionary?.[key]
const hasDefault = defaultValue !== undefined
// Only use defaultValue if value is undefined and
// a defaultValue options was provided.
if (hasDefault && value === undefined) {
value = defaultValue()
}
// A key can be optional and have no default value.
// When this happens, do not perform a conversion,
// and do not assign the key a value.
if (required || hasDefault || value !== undefined) {
value = converter(value, prefix, `${argument}.${key}`)
if (
options.allowedValues &&
!options.allowedValues.includes(value)
) {
throw webidl.errors.exception({
header: prefix,
message: `${value} is not an accepted type. Expected one of ${options.allowedValues.join(', ')}.`
})
}
dict[key] = value
}
}
return dict
}
}
webidl.nullableConverter = function (converter) {
return (V, prefix, argument) => {
if (V === null) {
return V
}
return converter(V, prefix, argument)
}
}
webidl.is.ReadableStream = webidl.util.MakeTypeAssertion(ReadableStream)
webidl.is.Blob = webidl.util.MakeTypeAssertion(Blob)
webidl.is.URLSearchParams = webidl.util.MakeTypeAssertion(URLSearchParams)
webidl.is.File = webidl.util.MakeTypeAssertion(globalThis.File ?? require('node:buffer').File)
webidl.is.URL = webidl.util.MakeTypeAssertion(URL)
webidl.is.AbortSignal = webidl.util.MakeTypeAssertion(AbortSignal)
webidl.is.MessagePort = webidl.util.MakeTypeAssertion(MessagePort)
// https://webidl.spec.whatwg.org/#es-DOMString
webidl.converters.DOMString = function (V, prefix, argument, opts) {
// 1. If V is null and the conversion is to an IDL type
// associated with the [LegacyNullToEmptyString]
// extended attribute, then return the DOMString value
// that represents the empty string.
if (V === null && opts?.legacyNullToEmptyString) {
return ''
}
// 2. Let x be ? ToString(V).
if (typeof V === 'symbol') {
throw webidl.errors.exception({
header: prefix,
message: `${argument} is a symbol, which cannot be converted to a DOMString.`
})
}
// 3. Return the IDL DOMString value that represents the
// same sequence of code units as the one the
// ECMAScript String value x represents.
return String(V)
}
// https://webidl.spec.whatwg.org/#es-ByteString
webidl.converters.ByteString = function (V, prefix, argument) {
// 1. Let x be ? ToString(V).
if (typeof V === 'symbol') {
throw webidl.errors.exception({
header: prefix,
message: `${argument} is a symbol, which cannot be converted to a ByteString.`
})
}
const x = String(V)
// 2. If the value of any element of x is greater than
// 255, then throw a TypeError.
for (let index = 0; index < x.length; index++) {
if (x.charCodeAt(index) > 255) {
throw new TypeError(
'Cannot convert argument to a ByteString because the character at ' +
`index ${index} has a value of ${x.charCodeAt(index)} which is greater than 255.`
)
}
}
// 3. Return an IDL ByteString value whose length is the
// length of x, and where the value of each element is
// the value of the corresponding element of x.
return x
}
// https://webidl.spec.whatwg.org/#es-USVString
// TODO: rewrite this so we can control the errors thrown
webidl.converters.USVString = toUSVString
// https://webidl.spec.whatwg.org/#es-boolean
webidl.converters.boolean = function (V) {
// 1. Let x be the result of computing ToBoolean(V).
const x = Boolean(V)
// 2. Return the IDL boolean value that is the one that represents
// the same truth value as the ECMAScript Boolean value x.
return x
}
// https://webidl.spec.whatwg.org/#es-any
webidl.converters.any = function (V) {
return V
}
// https://webidl.spec.whatwg.org/#es-long-long
webidl.converters['long long'] = function (V, prefix, argument) {
// 1. Let x be ? ConvertToInt(V, 64, "signed").
const x = webidl.util.ConvertToInt(V, 64, 'signed', undefined, prefix, argument)
// 2. Return the IDL long long value that represents
// the same numeric value as x.
return x
}
// https://webidl.spec.whatwg.org/#es-unsigned-long-long
webidl.converters['unsigned long long'] = function (V, prefix, argument) {
// 1. Let x be ? ConvertToInt(V, 64, "unsigned").
const x = webidl.util.ConvertToInt(V, 64, 'unsigned', undefined, prefix, argument)
// 2. Return the IDL unsigned long long value that
// represents the same numeric value as x.
return x
}
// https://webidl.spec.whatwg.org/#es-unsigned-long
webidl.converters['unsigned long'] = function (V, prefix, argument) {
// 1. Let x be ? ConvertToInt(V, 32, "unsigned").
const x = webidl.util.ConvertToInt(V, 32, 'unsigned', undefined, prefix, argument)
// 2. Return the IDL unsigned long value that
// represents the same numeric value as x.
return x
}
// https://webidl.spec.whatwg.org/#es-unsigned-short
webidl.converters['unsigned short'] = function (V, prefix, argument, opts) {
// 1. Let x be ? ConvertToInt(V, 16, "unsigned").
const x = webidl.util.ConvertToInt(V, 16, 'unsigned', opts, prefix, argument)
// 2. Return the IDL unsigned short value that represents
// the same numeric value as x.
return x
}
// https://webidl.spec.whatwg.org/#idl-ArrayBuffer
webidl.converters.ArrayBuffer = function (V, prefix, argument, opts) {
// 1. If Type(V) is not Object, or V does not have an
// [[ArrayBufferData]] internal slot, then throw a
// TypeError.
// see: https://tc39.es/ecma262/#sec-properties-of-the-arraybuffer-instances
// see: https://tc39.es/ecma262/#sec-properties-of-the-sharedarraybuffer-instances
if (
webidl.util.Type(V) !== OBJECT ||
!types.isAnyArrayBuffer(V)
) {
throw webidl.errors.conversionFailed({
prefix,
argument: `${argument} ("${webidl.util.Stringify(V)}")`,
types: ['ArrayBuffer']
})
}
// 2. If the conversion is not to an IDL type associated
// with the [AllowShared] extended attribute, and
// IsSharedArrayBuffer(V) is true, then throw a
// TypeError.
if (opts?.allowShared === false && types.isSharedArrayBuffer(V)) {
throw webidl.errors.exception({
header: 'ArrayBuffer',
message: 'SharedArrayBuffer is not allowed.'
})
}
// 3. If the conversion is not to an IDL type associated
// with the [AllowResizable] extended attribute, and
// IsResizableArrayBuffer(V) is true, then throw a
// TypeError.
if (V.resizable || V.growable) {
throw webidl.errors.exception({
header: 'ArrayBuffer',
message: 'Received a resizable ArrayBuffer.'
})
}
// 4. Return the IDL ArrayBuffer value that is a
// reference to the same object as V.
return V
}
webidl.converters.TypedArray = function (V, T, prefix, name, opts) {
// 1. Let T be the IDL type V is being converted to.
// 2. If Type(V) is not Object, or V does not have a
// [[TypedArrayName]] internal slot with a value
// equal to Ts name, then throw a TypeError.
if (
webidl.util.Type(V) !== OBJECT ||
!types.isTypedArray(V) ||
V.constructor.name !== T.name
) {
throw webidl.errors.conversionFailed({
prefix,
argument: `${name} ("${webidl.util.Stringify(V)}")`,
types: [T.name]
})
}
// 3. If the conversion is not to an IDL type associated
// with the [AllowShared] extended attribute, and
// IsSharedArrayBuffer(V.[[ViewedArrayBuffer]]) is
// true, then throw a TypeError.
if (opts?.allowShared === false && types.isSharedArrayBuffer(V.buffer)) {
throw webidl.errors.exception({
header: 'ArrayBuffer',
message: 'SharedArrayBuffer is not allowed.'
})
}
// 4. If the conversion is not to an IDL type associated
// with the [AllowResizable] extended attribute, and
// IsResizableArrayBuffer(V.[[ViewedArrayBuffer]]) is
// true, then throw a TypeError.
if (V.buffer.resizable || V.buffer.growable) {
throw webidl.errors.exception({
header: 'ArrayBuffer',
message: 'Received a resizable ArrayBuffer.'
})
}
// 5. Return the IDL value of type T that is a reference
// to the same object as V.
return V
}
webidl.converters.DataView = function (V, prefix, name, opts) {
// 1. If Type(V) is not Object, or V does not have a
// [[DataView]] internal slot, then throw a TypeError.
if (webidl.util.Type(V) !== OBJECT || !types.isDataView(V)) {
throw webidl.errors.exception({
header: prefix,
message: `${name} is not a DataView.`
})
}
// 2. If the conversion is not to an IDL type associated
// with the [AllowShared] extended attribute, and
// IsSharedArrayBuffer(V.[[ViewedArrayBuffer]]) is true,
// then throw a TypeError.
if (opts?.allowShared === false && types.isSharedArrayBuffer(V.buffer)) {
throw webidl.errors.exception({
header: 'ArrayBuffer',
message: 'SharedArrayBuffer is not allowed.'
})
}
// 3. If the conversion is not to an IDL type associated
// with the [AllowResizable] extended attribute, and
// IsResizableArrayBuffer(V.[[ViewedArrayBuffer]]) is
// true, then throw a TypeError.
if (V.buffer.resizable || V.buffer.growable) {
throw webidl.errors.exception({
header: 'ArrayBuffer',
message: 'Received a resizable ArrayBuffer.'
})
}
// 4. Return the IDL DataView value that is a reference
// to the same object as V.
return V
}
webidl.converters['sequence<ByteString>'] = webidl.sequenceConverter(
webidl.converters.ByteString
)
webidl.converters['sequence<sequence<ByteString>>'] = webidl.sequenceConverter(
webidl.converters['sequence<ByteString>']
)
webidl.converters['record<ByteString, ByteString>'] = webidl.recordConverter(
webidl.converters.ByteString,
webidl.converters.ByteString
)
webidl.converters.Blob = webidl.interfaceConverter(webidl.is.Blob, 'Blob')
webidl.converters.AbortSignal = webidl.interfaceConverter(
webidl.is.AbortSignal,
'AbortSignal'
)
module.exports = {
webidl
}

325
node_modules/undici/lib/web/websocket/connection.js generated vendored Normal file
View File

@ -0,0 +1,325 @@
'use strict'
const { uid, states, sentCloseFrameState, emptyBuffer, opcodes } = require('./constants')
const { parseExtensions, isClosed, isClosing, isEstablished, validateCloseCodeAndReason } = require('./util')
const { channels } = require('../../core/diagnostics')
const { makeRequest } = require('../fetch/request')
const { fetching } = require('../fetch/index')
const { Headers, getHeadersList } = require('../fetch/headers')
const { getDecodeSplit } = require('../fetch/util')
const { WebsocketFrameSend } = require('./frame')
const assert = require('node:assert')
/** @type {import('crypto')} */
let crypto
try {
crypto = require('node:crypto')
/* c8 ignore next 3 */
} catch {
}
/**
* @see https://websockets.spec.whatwg.org/#concept-websocket-establish
* @param {URL} url
* @param {string|string[]} protocols
* @param {import('./websocket').Handler} handler
* @param {Partial<import('../../../types/websocket').WebSocketInit>} options
*/
function establishWebSocketConnection (url, protocols, client, handler, options) {
// 1. Let requestURL be a copy of url, with its scheme set to "http", if urls
// scheme is "ws", and to "https" otherwise.
const requestURL = url
requestURL.protocol = url.protocol === 'ws:' ? 'http:' : 'https:'
// 2. Let request be a new request, whose URL is requestURL, client is client,
// service-workers mode is "none", referrer is "no-referrer", mode is
// "websocket", credentials mode is "include", cache mode is "no-store" ,
// and redirect mode is "error".
const request = makeRequest({
urlList: [requestURL],
client,
serviceWorkers: 'none',
referrer: 'no-referrer',
mode: 'websocket',
credentials: 'include',
cache: 'no-store',
redirect: 'error'
})
// Note: undici extension, allow setting custom headers.
if (options.headers) {
const headersList = getHeadersList(new Headers(options.headers))
request.headersList = headersList
}
// 3. Append (`Upgrade`, `websocket`) to requests header list.
// 4. Append (`Connection`, `Upgrade`) to requests header list.
// Note: both of these are handled by undici currently.
// https://github.com/nodejs/undici/blob/68c269c4144c446f3f1220951338daef4a6b5ec4/lib/client.js#L1397
// 5. Let keyValue be a nonce consisting of a randomly selected
// 16-byte value that has been forgiving-base64-encoded and
// isomorphic encoded.
const keyValue = crypto.randomBytes(16).toString('base64')
// 6. Append (`Sec-WebSocket-Key`, keyValue) to requests
// header list.
request.headersList.append('sec-websocket-key', keyValue, true)
// 7. Append (`Sec-WebSocket-Version`, `13`) to requests
// header list.
request.headersList.append('sec-websocket-version', '13', true)
// 8. For each protocol in protocols, combine
// (`Sec-WebSocket-Protocol`, protocol) in requests header
// list.
for (const protocol of protocols) {
request.headersList.append('sec-websocket-protocol', protocol, true)
}
// 9. Let permessageDeflate be a user-agent defined
// "permessage-deflate" extension header value.
// https://github.com/mozilla/gecko-dev/blob/ce78234f5e653a5d3916813ff990f053510227bc/netwerk/protocol/websocket/WebSocketChannel.cpp#L2673
const permessageDeflate = 'permessage-deflate; client_max_window_bits'
// 10. Append (`Sec-WebSocket-Extensions`, permessageDeflate) to
// requests header list.
request.headersList.append('sec-websocket-extensions', permessageDeflate, true)
// 11. Fetch request with useParallelQueue set to true, and
// processResponse given response being these steps:
const controller = fetching({
request,
useParallelQueue: true,
dispatcher: options.dispatcher,
processResponse (response) {
if (response.type === 'error') {
// If the WebSocket connection could not be established, it is also said
// that _The WebSocket Connection is Closed_, but not _cleanly_.
handler.readyState = states.CLOSED
}
// 1. If response is a network error or its status is not 101,
// fail the WebSocket connection.
if (response.type === 'error' || response.status !== 101) {
failWebsocketConnection(handler, 1002, 'Received network error or non-101 status code.')
return
}
// 2. If protocols is not the empty list and extracting header
// list values given `Sec-WebSocket-Protocol` and responses
// header list results in null, failure, or the empty byte
// sequence, then fail the WebSocket connection.
if (protocols.length !== 0 && !response.headersList.get('Sec-WebSocket-Protocol')) {
failWebsocketConnection(handler, 1002, 'Server did not respond with sent protocols.')
return
}
// 3. Follow the requirements stated step 2 to step 6, inclusive,
// of the last set of steps in section 4.1 of The WebSocket
// Protocol to validate response. This either results in fail
// the WebSocket connection or the WebSocket connection is
// established.
// 2. If the response lacks an |Upgrade| header field or the |Upgrade|
// header field contains a value that is not an ASCII case-
// insensitive match for the value "websocket", the client MUST
// _Fail the WebSocket Connection_.
if (response.headersList.get('Upgrade')?.toLowerCase() !== 'websocket') {
failWebsocketConnection(handler, 1002, 'Server did not set Upgrade header to "websocket".')
return
}
// 3. If the response lacks a |Connection| header field or the
// |Connection| header field doesn't contain a token that is an
// ASCII case-insensitive match for the value "Upgrade", the client
// MUST _Fail the WebSocket Connection_.
if (response.headersList.get('Connection')?.toLowerCase() !== 'upgrade') {
failWebsocketConnection(handler, 1002, 'Server did not set Connection header to "upgrade".')
return
}
// 4. If the response lacks a |Sec-WebSocket-Accept| header field or
// the |Sec-WebSocket-Accept| contains a value other than the
// base64-encoded SHA-1 of the concatenation of the |Sec-WebSocket-
// Key| (as a string, not base64-decoded) with the string "258EAFA5-
// E914-47DA-95CA-C5AB0DC85B11" but ignoring any leading and
// trailing whitespace, the client MUST _Fail the WebSocket
// Connection_.
const secWSAccept = response.headersList.get('Sec-WebSocket-Accept')
const digest = crypto.createHash('sha1').update(keyValue + uid).digest('base64')
if (secWSAccept !== digest) {
failWebsocketConnection(handler, 1002, 'Incorrect hash received in Sec-WebSocket-Accept header.')
return
}
// 5. If the response includes a |Sec-WebSocket-Extensions| header
// field and this header field indicates the use of an extension
// that was not present in the client's handshake (the server has
// indicated an extension not requested by the client), the client
// MUST _Fail the WebSocket Connection_. (The parsing of this
// header field to determine which extensions are requested is
// discussed in Section 9.1.)
const secExtension = response.headersList.get('Sec-WebSocket-Extensions')
let extensions
if (secExtension !== null) {
extensions = parseExtensions(secExtension)
if (!extensions.has('permessage-deflate')) {
failWebsocketConnection(handler, 1002, 'Sec-WebSocket-Extensions header does not match.')
return
}
}
// 6. If the response includes a |Sec-WebSocket-Protocol| header field
// and this header field indicates the use of a subprotocol that was
// not present in the client's handshake (the server has indicated a
// subprotocol not requested by the client), the client MUST _Fail
// the WebSocket Connection_.
const secProtocol = response.headersList.get('Sec-WebSocket-Protocol')
if (secProtocol !== null) {
const requestProtocols = getDecodeSplit('sec-websocket-protocol', request.headersList)
// The client can request that the server use a specific subprotocol by
// including the |Sec-WebSocket-Protocol| field in its handshake. If it
// is specified, the server needs to include the same field and one of
// the selected subprotocol values in its response for the connection to
// be established.
if (!requestProtocols.includes(secProtocol)) {
failWebsocketConnection(handler, 1002, 'Protocol was not set in the opening handshake.')
return
}
}
response.socket.on('data', handler.onSocketData)
response.socket.on('close', handler.onSocketClose)
response.socket.on('error', handler.onSocketError)
if (channels.open.hasSubscribers) {
channels.open.publish({
address: response.socket.address(),
protocol: secProtocol,
extensions: secExtension
})
}
handler.wasEverConnected = true
handler.onConnectionEstablished(response, extensions)
}
})
return controller
}
/**
* @see https://whatpr.org/websockets/48.html#close-the-websocket
* @param {import('./websocket').Handler} object
* @param {number} [code=null]
* @param {string} [reason='']
*/
function closeWebSocketConnection (object, code, reason, validate = false) {
// 1. If code was not supplied, let code be null.
code ??= null
// 2. If reason was not supplied, let reason be the empty string.
reason ??= ''
// 3. Validate close code and reason with code and reason.
if (validate) validateCloseCodeAndReason(code, reason)
// 4. Run the first matching steps from the following list:
// - If objects ready state is CLOSING (2) or CLOSED (3)
// - If the WebSocket connection is not yet established [WSP]
// - If the WebSocket closing handshake has not yet been started [WSP]
// - Otherwise
if (isClosed(object.readyState) || isClosing(object.readyState)) {
// Do nothing.
} else if (!isEstablished(object.readyState)) {
// Fail the WebSocket connection and set objects ready state to CLOSING (2). [WSP]
failWebsocketConnection(object)
object.readyState = states.CLOSING
} else if (!object.closeState.has(sentCloseFrameState.SENT) && !object.closeState.has(sentCloseFrameState.RECEIVED)) {
// Upon either sending or receiving a Close control frame, it is said
// that _The WebSocket Closing Handshake is Started_ and that the
// WebSocket connection is in the CLOSING state.
const frame = new WebsocketFrameSend()
// If neither code nor reason is present, the WebSocket Close
// message must not have a body.
// If code is present, then the status code to use in the
// WebSocket Close message must be the integer given by code.
// If code is null and reason is the empty string, the WebSocket Close frame must not have a body.
// If reason is non-empty but code is null, then set code to 1000 ("Normal Closure").
if (reason.length !== 0 && code === null) {
code = 1000
}
// If code is set, then the status code to use in the WebSocket Close frame must be the integer given by code.
assert(code === null || Number.isInteger(code))
if (code === null && reason.length === 0) {
frame.frameData = emptyBuffer
} else if (code !== null && reason === null) {
frame.frameData = Buffer.allocUnsafe(2)
frame.frameData.writeUInt16BE(code, 0)
} else if (code !== null && reason !== null) {
// If reason is also present, then reasonBytes must be
// provided in the Close message after the status code.
frame.frameData = Buffer.allocUnsafe(2 + Buffer.byteLength(reason))
frame.frameData.writeUInt16BE(code, 0)
// the body MAY contain UTF-8-encoded data with value /reason/
frame.frameData.write(reason, 2, 'utf-8')
} else {
frame.frameData = emptyBuffer
}
object.socket.write(frame.createFrame(opcodes.CLOSE))
object.closeState.add(sentCloseFrameState.SENT)
// Upon either sending or receiving a Close control frame, it is said
// that _The WebSocket Closing Handshake is Started_ and that the
// WebSocket connection is in the CLOSING state.
object.readyState = states.CLOSING
} else {
// Set objects ready state to CLOSING (2).
object.readyState = states.CLOSING
}
}
/**
* @param {import('./websocket').Handler} handler
* @param {number} code
* @param {string|undefined} reason
* @returns {void}
*/
function failWebsocketConnection (handler, code, reason) {
// If _The WebSocket Connection is Established_ prior to the point where
// the endpoint is required to _Fail the WebSocket Connection_, the
// endpoint SHOULD send a Close frame with an appropriate status code
// (Section 7.4) before proceeding to _Close the WebSocket Connection_.
if (isEstablished(handler.readyState)) {
closeWebSocketConnection(handler, code, reason, false)
}
handler.controller.abort()
if (handler.socket?.destroyed === false) {
handler.socket.destroy()
}
handler.onFail(code, reason)
}
module.exports = {
establishWebSocketConnection,
failWebsocketConnection,
closeWebSocketConnection
}

126
node_modules/undici/lib/web/websocket/constants.js generated vendored Normal file
View File

@ -0,0 +1,126 @@
'use strict'
/**
* This is a Globally Unique Identifier unique used to validate that the
* endpoint accepts websocket connections.
* @see https://www.rfc-editor.org/rfc/rfc6455.html#section-1.3
* @type {'258EAFA5-E914-47DA-95CA-C5AB0DC85B11'}
*/
const uid = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'
/**
* @type {PropertyDescriptor}
*/
const staticPropertyDescriptors = {
enumerable: true,
writable: false,
configurable: false
}
/**
* The states of the WebSocket connection.
*
* @readonly
* @enum
* @property {0} CONNECTING
* @property {1} OPEN
* @property {2} CLOSING
* @property {3} CLOSED
*/
const states = {
CONNECTING: 0,
OPEN: 1,
CLOSING: 2,
CLOSED: 3
}
/**
* @readonly
* @enum
* @property {0} NOT_SENT
* @property {1} PROCESSING
* @property {2} SENT
*/
const sentCloseFrameState = {
SENT: 1,
RECEIVED: 2
}
/**
* The WebSocket opcodes.
*
* @readonly
* @enum
* @property {0x0} CONTINUATION
* @property {0x1} TEXT
* @property {0x2} BINARY
* @property {0x8} CLOSE
* @property {0x9} PING
* @property {0xA} PONG
* @see https://datatracker.ietf.org/doc/html/rfc6455#section-5.2
*/
const opcodes = {
CONTINUATION: 0x0,
TEXT: 0x1,
BINARY: 0x2,
CLOSE: 0x8,
PING: 0x9,
PONG: 0xA
}
/**
* The maximum value for an unsigned 16-bit integer.
*
* @type {65535} 2 ** 16 - 1
*/
const maxUnsigned16Bit = 65535
/**
* The states of the parser.
*
* @readonly
* @enum
* @property {0} INFO
* @property {2} PAYLOADLENGTH_16
* @property {3} PAYLOADLENGTH_64
* @property {4} READ_DATA
*/
const parserStates = {
INFO: 0,
PAYLOADLENGTH_16: 2,
PAYLOADLENGTH_64: 3,
READ_DATA: 4
}
/**
* An empty buffer.
*
* @type {Buffer}
*/
const emptyBuffer = Buffer.allocUnsafe(0)
/**
* @readonly
* @property {1} text
* @property {2} typedArray
* @property {3} arrayBuffer
* @property {4} blob
*/
const sendHints = {
text: 1,
typedArray: 2,
arrayBuffer: 3,
blob: 4
}
module.exports = {
uid,
sentCloseFrameState,
staticPropertyDescriptors,
states,
opcodes,
maxUnsigned16Bit,
parserStates,
emptyBuffer,
sendHints
}

331
node_modules/undici/lib/web/websocket/events.js generated vendored Normal file
View File

@ -0,0 +1,331 @@
'use strict'
const { webidl } = require('../fetch/webidl')
const { kEnumerableProperty } = require('../../core/util')
const { kConstruct } = require('../../core/symbols')
/**
* @see https://html.spec.whatwg.org/multipage/comms.html#messageevent
*/
class MessageEvent extends Event {
#eventInit
constructor (type, eventInitDict = {}) {
if (type === kConstruct) {
super(arguments[1], arguments[2])
webidl.util.markAsUncloneable(this)
return
}
const prefix = 'MessageEvent constructor'
webidl.argumentLengthCheck(arguments, 1, prefix)
type = webidl.converters.DOMString(type, prefix, 'type')
eventInitDict = webidl.converters.MessageEventInit(eventInitDict, prefix, 'eventInitDict')
super(type, eventInitDict)
this.#eventInit = eventInitDict
webidl.util.markAsUncloneable(this)
}
get data () {
webidl.brandCheck(this, MessageEvent)
return this.#eventInit.data
}
get origin () {
webidl.brandCheck(this, MessageEvent)
return this.#eventInit.origin
}
get lastEventId () {
webidl.brandCheck(this, MessageEvent)
return this.#eventInit.lastEventId
}
get source () {
webidl.brandCheck(this, MessageEvent)
return this.#eventInit.source
}
get ports () {
webidl.brandCheck(this, MessageEvent)
if (!Object.isFrozen(this.#eventInit.ports)) {
Object.freeze(this.#eventInit.ports)
}
return this.#eventInit.ports
}
initMessageEvent (
type,
bubbles = false,
cancelable = false,
data = null,
origin = '',
lastEventId = '',
source = null,
ports = []
) {
webidl.brandCheck(this, MessageEvent)
webidl.argumentLengthCheck(arguments, 1, 'MessageEvent.initMessageEvent')
return new MessageEvent(type, {
bubbles, cancelable, data, origin, lastEventId, source, ports
})
}
static createFastMessageEvent (type, init) {
const messageEvent = new MessageEvent(kConstruct, type, init)
messageEvent.#eventInit = init
messageEvent.#eventInit.data ??= null
messageEvent.#eventInit.origin ??= ''
messageEvent.#eventInit.lastEventId ??= ''
messageEvent.#eventInit.source ??= null
messageEvent.#eventInit.ports ??= []
return messageEvent
}
}
const { createFastMessageEvent } = MessageEvent
delete MessageEvent.createFastMessageEvent
/**
* @see https://websockets.spec.whatwg.org/#the-closeevent-interface
*/
class CloseEvent extends Event {
#eventInit
constructor (type, eventInitDict = {}) {
const prefix = 'CloseEvent constructor'
webidl.argumentLengthCheck(arguments, 1, prefix)
type = webidl.converters.DOMString(type, prefix, 'type')
eventInitDict = webidl.converters.CloseEventInit(eventInitDict)
super(type, eventInitDict)
this.#eventInit = eventInitDict
webidl.util.markAsUncloneable(this)
}
get wasClean () {
webidl.brandCheck(this, CloseEvent)
return this.#eventInit.wasClean
}
get code () {
webidl.brandCheck(this, CloseEvent)
return this.#eventInit.code
}
get reason () {
webidl.brandCheck(this, CloseEvent)
return this.#eventInit.reason
}
}
// https://html.spec.whatwg.org/multipage/webappapis.html#the-errorevent-interface
class ErrorEvent extends Event {
#eventInit
constructor (type, eventInitDict) {
const prefix = 'ErrorEvent constructor'
webidl.argumentLengthCheck(arguments, 1, prefix)
super(type, eventInitDict)
webidl.util.markAsUncloneable(this)
type = webidl.converters.DOMString(type, prefix, 'type')
eventInitDict = webidl.converters.ErrorEventInit(eventInitDict ?? {})
this.#eventInit = eventInitDict
}
get message () {
webidl.brandCheck(this, ErrorEvent)
return this.#eventInit.message
}
get filename () {
webidl.brandCheck(this, ErrorEvent)
return this.#eventInit.filename
}
get lineno () {
webidl.brandCheck(this, ErrorEvent)
return this.#eventInit.lineno
}
get colno () {
webidl.brandCheck(this, ErrorEvent)
return this.#eventInit.colno
}
get error () {
webidl.brandCheck(this, ErrorEvent)
return this.#eventInit.error
}
}
Object.defineProperties(MessageEvent.prototype, {
[Symbol.toStringTag]: {
value: 'MessageEvent',
configurable: true
},
data: kEnumerableProperty,
origin: kEnumerableProperty,
lastEventId: kEnumerableProperty,
source: kEnumerableProperty,
ports: kEnumerableProperty,
initMessageEvent: kEnumerableProperty
})
Object.defineProperties(CloseEvent.prototype, {
[Symbol.toStringTag]: {
value: 'CloseEvent',
configurable: true
},
reason: kEnumerableProperty,
code: kEnumerableProperty,
wasClean: kEnumerableProperty
})
Object.defineProperties(ErrorEvent.prototype, {
[Symbol.toStringTag]: {
value: 'ErrorEvent',
configurable: true
},
message: kEnumerableProperty,
filename: kEnumerableProperty,
lineno: kEnumerableProperty,
colno: kEnumerableProperty,
error: kEnumerableProperty
})
webidl.converters.MessagePort = webidl.interfaceConverter(
webidl.is.MessagePort,
'MessagePort'
)
webidl.converters['sequence<MessagePort>'] = webidl.sequenceConverter(
webidl.converters.MessagePort
)
const eventInit = [
{
key: 'bubbles',
converter: webidl.converters.boolean,
defaultValue: () => false
},
{
key: 'cancelable',
converter: webidl.converters.boolean,
defaultValue: () => false
},
{
key: 'composed',
converter: webidl.converters.boolean,
defaultValue: () => false
}
]
webidl.converters.MessageEventInit = webidl.dictionaryConverter([
...eventInit,
{
key: 'data',
converter: webidl.converters.any,
defaultValue: () => null
},
{
key: 'origin',
converter: webidl.converters.USVString,
defaultValue: () => ''
},
{
key: 'lastEventId',
converter: webidl.converters.DOMString,
defaultValue: () => ''
},
{
key: 'source',
// Node doesn't implement WindowProxy or ServiceWorker, so the only
// valid value for source is a MessagePort.
converter: webidl.nullableConverter(webidl.converters.MessagePort),
defaultValue: () => null
},
{
key: 'ports',
converter: webidl.converters['sequence<MessagePort>'],
defaultValue: () => new Array(0)
}
])
webidl.converters.CloseEventInit = webidl.dictionaryConverter([
...eventInit,
{
key: 'wasClean',
converter: webidl.converters.boolean,
defaultValue: () => false
},
{
key: 'code',
converter: webidl.converters['unsigned short'],
defaultValue: () => 0
},
{
key: 'reason',
converter: webidl.converters.USVString,
defaultValue: () => ''
}
])
webidl.converters.ErrorEventInit = webidl.dictionaryConverter([
...eventInit,
{
key: 'message',
converter: webidl.converters.DOMString,
defaultValue: () => ''
},
{
key: 'filename',
converter: webidl.converters.USVString,
defaultValue: () => ''
},
{
key: 'lineno',
converter: webidl.converters['unsigned long'],
defaultValue: () => 0
},
{
key: 'colno',
converter: webidl.converters['unsigned long'],
defaultValue: () => 0
},
{
key: 'error',
converter: webidl.converters.any
}
])
module.exports = {
MessageEvent,
CloseEvent,
ErrorEvent,
createFastMessageEvent
}

138
node_modules/undici/lib/web/websocket/frame.js generated vendored Normal file
View File

@ -0,0 +1,138 @@
'use strict'
const { maxUnsigned16Bit, opcodes } = require('./constants')
const BUFFER_SIZE = 8 * 1024
/** @type {import('crypto')} */
let crypto
let buffer = null
let bufIdx = BUFFER_SIZE
try {
crypto = require('node:crypto')
/* c8 ignore next 3 */
} catch {
crypto = {
// not full compatibility, but minimum.
randomFillSync: function randomFillSync (buffer, _offset, _size) {
for (let i = 0; i < buffer.length; ++i) {
buffer[i] = Math.random() * 255 | 0
}
return buffer
}
}
}
function generateMask () {
if (bufIdx === BUFFER_SIZE) {
bufIdx = 0
crypto.randomFillSync((buffer ??= Buffer.allocUnsafeSlow(BUFFER_SIZE)), 0, BUFFER_SIZE)
}
return [buffer[bufIdx++], buffer[bufIdx++], buffer[bufIdx++], buffer[bufIdx++]]
}
class WebsocketFrameSend {
/**
* @param {Buffer|undefined} data
*/
constructor (data) {
this.frameData = data
}
createFrame (opcode) {
const frameData = this.frameData
const maskKey = generateMask()
const bodyLength = frameData?.byteLength ?? 0
/** @type {number} */
let payloadLength = bodyLength // 0-125
let offset = 6
if (bodyLength > maxUnsigned16Bit) {
offset += 8 // payload length is next 8 bytes
payloadLength = 127
} else if (bodyLength > 125) {
offset += 2 // payload length is next 2 bytes
payloadLength = 126
}
const buffer = Buffer.allocUnsafe(bodyLength + offset)
// Clear first 2 bytes, everything else is overwritten
buffer[0] = buffer[1] = 0
buffer[0] |= 0x80 // FIN
buffer[0] = (buffer[0] & 0xF0) + opcode // opcode
/*! ws. MIT License. Einar Otto Stangvik <einaros@gmail.com> */
buffer[offset - 4] = maskKey[0]
buffer[offset - 3] = maskKey[1]
buffer[offset - 2] = maskKey[2]
buffer[offset - 1] = maskKey[3]
buffer[1] = payloadLength
if (payloadLength === 126) {
buffer.writeUInt16BE(bodyLength, 2)
} else if (payloadLength === 127) {
// Clear extended payload length
buffer[2] = buffer[3] = 0
buffer.writeUIntBE(bodyLength, 4, 6)
}
buffer[1] |= 0x80 // MASK
// mask body
for (let i = 0; i < bodyLength; ++i) {
buffer[offset + i] = frameData[i] ^ maskKey[i & 3]
}
return buffer
}
/**
* @param {Uint8Array} buffer
*/
static createFastTextFrame (buffer) {
const maskKey = generateMask()
const bodyLength = buffer.length
// mask body
for (let i = 0; i < bodyLength; ++i) {
buffer[i] ^= maskKey[i & 3]
}
let payloadLength = bodyLength
let offset = 6
if (bodyLength > maxUnsigned16Bit) {
offset += 8 // payload length is next 8 bytes
payloadLength = 127
} else if (bodyLength > 125) {
offset += 2 // payload length is next 2 bytes
payloadLength = 126
}
const head = Buffer.allocUnsafeSlow(offset)
head[0] = 0x80 /* FIN */ | opcodes.TEXT /* opcode TEXT */
head[1] = payloadLength | 0x80 /* MASK */
head[offset - 4] = maskKey[0]
head[offset - 3] = maskKey[1]
head[offset - 2] = maskKey[2]
head[offset - 1] = maskKey[3]
if (payloadLength === 126) {
head.writeUInt16BE(bodyLength, 2)
} else if (payloadLength === 127) {
head[2] = head[3] = 0
head.writeUIntBE(bodyLength, 4, 6)
}
return [head, buffer]
}
}
module.exports = {
WebsocketFrameSend
}

View File

@ -0,0 +1,70 @@
'use strict'
const { createInflateRaw, Z_DEFAULT_WINDOWBITS } = require('node:zlib')
const { isValidClientWindowBits } = require('./util')
const tail = Buffer.from([0x00, 0x00, 0xff, 0xff])
const kBuffer = Symbol('kBuffer')
const kLength = Symbol('kLength')
class PerMessageDeflate {
/** @type {import('node:zlib').InflateRaw} */
#inflate
#options = {}
constructor (extensions) {
this.#options.serverNoContextTakeover = extensions.has('server_no_context_takeover')
this.#options.serverMaxWindowBits = extensions.get('server_max_window_bits')
}
decompress (chunk, fin, callback) {
// An endpoint uses the following algorithm to decompress a message.
// 1. Append 4 octets of 0x00 0x00 0xff 0xff to the tail end of the
// payload of the message.
// 2. Decompress the resulting data using DEFLATE.
if (!this.#inflate) {
let windowBits = Z_DEFAULT_WINDOWBITS
if (this.#options.serverMaxWindowBits) { // empty values default to Z_DEFAULT_WINDOWBITS
if (!isValidClientWindowBits(this.#options.serverMaxWindowBits)) {
callback(new Error('Invalid server_max_window_bits'))
return
}
windowBits = Number.parseInt(this.#options.serverMaxWindowBits)
}
this.#inflate = createInflateRaw({ windowBits })
this.#inflate[kBuffer] = []
this.#inflate[kLength] = 0
this.#inflate.on('data', (data) => {
this.#inflate[kBuffer].push(data)
this.#inflate[kLength] += data.length
})
this.#inflate.on('error', (err) => {
this.#inflate = null
callback(err)
})
}
this.#inflate.write(chunk)
if (fin) {
this.#inflate.write(tail)
}
this.#inflate.flush(() => {
const full = Buffer.concat(this.#inflate[kBuffer], this.#inflate[kLength])
this.#inflate[kBuffer].length = 0
this.#inflate[kLength] = 0
callback(null, full)
})
}
}
module.exports = { PerMessageDeflate }

454
node_modules/undici/lib/web/websocket/receiver.js generated vendored Normal file
View File

@ -0,0 +1,454 @@
'use strict'
const { Writable } = require('node:stream')
const assert = require('node:assert')
const { parserStates, opcodes, states, emptyBuffer, sentCloseFrameState } = require('./constants')
const { channels } = require('../../core/diagnostics')
const {
isValidStatusCode,
isValidOpcode,
websocketMessageReceived,
utf8Decode,
isControlFrame,
isTextBinaryFrame,
isContinuationFrame
} = require('./util')
const { failWebsocketConnection } = require('./connection')
const { WebsocketFrameSend } = require('./frame')
const { PerMessageDeflate } = require('./permessage-deflate')
// This code was influenced by ws released under the MIT license.
// Copyright (c) 2011 Einar Otto Stangvik <einaros@gmail.com>
// Copyright (c) 2013 Arnout Kazemier and contributors
// Copyright (c) 2016 Luigi Pinca and contributors
class ByteParser extends Writable {
#buffers = []
#fragmentsBytes = 0
#byteOffset = 0
#loop = false
#state = parserStates.INFO
#info = {}
#fragments = []
/** @type {Map<string, PerMessageDeflate>} */
#extensions
/** @type {import('./websocket').Handler} */
#handler
constructor (handler, extensions) {
super()
this.#handler = handler
this.#extensions = extensions == null ? new Map() : extensions
if (this.#extensions.has('permessage-deflate')) {
this.#extensions.set('permessage-deflate', new PerMessageDeflate(extensions))
}
}
/**
* @param {Buffer} chunk
* @param {() => void} callback
*/
_write (chunk, _, callback) {
this.#buffers.push(chunk)
this.#byteOffset += chunk.length
this.#loop = true
this.run(callback)
}
/**
* Runs whenever a new chunk is received.
* Callback is called whenever there are no more chunks buffering,
* or not enough bytes are buffered to parse.
*/
run (callback) {
while (this.#loop) {
if (this.#state === parserStates.INFO) {
// If there aren't enough bytes to parse the payload length, etc.
if (this.#byteOffset < 2) {
return callback()
}
const buffer = this.consume(2)
const fin = (buffer[0] & 0x80) !== 0
const opcode = buffer[0] & 0x0F
const masked = (buffer[1] & 0x80) === 0x80
const fragmented = !fin && opcode !== opcodes.CONTINUATION
const payloadLength = buffer[1] & 0x7F
const rsv1 = buffer[0] & 0x40
const rsv2 = buffer[0] & 0x20
const rsv3 = buffer[0] & 0x10
if (!isValidOpcode(opcode)) {
failWebsocketConnection(this.#handler, 1002, 'Invalid opcode received')
return callback()
}
if (masked) {
failWebsocketConnection(this.#handler, 1002, 'Frame cannot be masked')
return callback()
}
// MUST be 0 unless an extension is negotiated that defines meanings
// for non-zero values. If a nonzero value is received and none of
// the negotiated extensions defines the meaning of such a nonzero
// value, the receiving endpoint MUST _Fail the WebSocket
// Connection_.
// This document allocates the RSV1 bit of the WebSocket header for
// PMCEs and calls the bit the "Per-Message Compressed" bit. On a
// WebSocket connection where a PMCE is in use, this bit indicates
// whether a message is compressed or not.
if (rsv1 !== 0 && !this.#extensions.has('permessage-deflate')) {
failWebsocketConnection(this.#handler, 1002, 'Expected RSV1 to be clear.')
return
}
if (rsv2 !== 0 || rsv3 !== 0) {
failWebsocketConnection(this.#handler, 1002, 'RSV1, RSV2, RSV3 must be clear')
return
}
if (fragmented && !isTextBinaryFrame(opcode)) {
// Only text and binary frames can be fragmented
failWebsocketConnection(this.#handler, 1002, 'Invalid frame type was fragmented.')
return
}
// If we are already parsing a text/binary frame and do not receive either
// a continuation frame or close frame, fail the connection.
if (isTextBinaryFrame(opcode) && this.#fragments.length > 0) {
failWebsocketConnection(this.#handler, 1002, 'Expected continuation frame')
return
}
if (this.#info.fragmented && fragmented) {
// A fragmented frame can't be fragmented itself
failWebsocketConnection(this.#handler, 1002, 'Fragmented frame exceeded 125 bytes.')
return
}
// "All control frames MUST have a payload length of 125 bytes or less
// and MUST NOT be fragmented."
if ((payloadLength > 125 || fragmented) && isControlFrame(opcode)) {
failWebsocketConnection(this.#handler, 1002, 'Control frame either too large or fragmented')
return
}
if (isContinuationFrame(opcode) && this.#fragments.length === 0 && !this.#info.compressed) {
failWebsocketConnection(this.#handler, 1002, 'Unexpected continuation frame')
return
}
if (payloadLength <= 125) {
this.#info.payloadLength = payloadLength
this.#state = parserStates.READ_DATA
} else if (payloadLength === 126) {
this.#state = parserStates.PAYLOADLENGTH_16
} else if (payloadLength === 127) {
this.#state = parserStates.PAYLOADLENGTH_64
}
if (isTextBinaryFrame(opcode)) {
this.#info.binaryType = opcode
this.#info.compressed = rsv1 !== 0
}
this.#info.opcode = opcode
this.#info.masked = masked
this.#info.fin = fin
this.#info.fragmented = fragmented
} else if (this.#state === parserStates.PAYLOADLENGTH_16) {
if (this.#byteOffset < 2) {
return callback()
}
const buffer = this.consume(2)
this.#info.payloadLength = buffer.readUInt16BE(0)
this.#state = parserStates.READ_DATA
} else if (this.#state === parserStates.PAYLOADLENGTH_64) {
if (this.#byteOffset < 8) {
return callback()
}
const buffer = this.consume(8)
const upper = buffer.readUInt32BE(0)
// 2^31 is the maximum bytes an arraybuffer can contain
// on 32-bit systems. Although, on 64-bit systems, this is
// 2^53-1 bytes.
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Errors/Invalid_array_length
// https://source.chromium.org/chromium/chromium/src/+/main:v8/src/common/globals.h;drc=1946212ac0100668f14eb9e2843bdd846e510a1e;bpv=1;bpt=1;l=1275
// https://source.chromium.org/chromium/chromium/src/+/main:v8/src/objects/js-array-buffer.h;l=34;drc=1946212ac0100668f14eb9e2843bdd846e510a1e
if (upper > 2 ** 31 - 1) {
failWebsocketConnection(this.#handler, 1009, 'Received payload length > 2^31 bytes.')
return
}
const lower = buffer.readUInt32BE(4)
this.#info.payloadLength = (upper << 8) + lower
this.#state = parserStates.READ_DATA
} else if (this.#state === parserStates.READ_DATA) {
if (this.#byteOffset < this.#info.payloadLength) {
return callback()
}
const body = this.consume(this.#info.payloadLength)
if (isControlFrame(this.#info.opcode)) {
this.#loop = this.parseControlFrame(body)
this.#state = parserStates.INFO
} else {
if (!this.#info.compressed) {
this.writeFragments(body)
// If the frame is not fragmented, a message has been received.
// If the frame is fragmented, it will terminate with a fin bit set
// and an opcode of 0 (continuation), therefore we handle that when
// parsing continuation frames, not here.
if (!this.#info.fragmented && this.#info.fin) {
websocketMessageReceived(this.#handler, this.#info.binaryType, this.consumeFragments())
}
this.#state = parserStates.INFO
} else {
this.#extensions.get('permessage-deflate').decompress(body, this.#info.fin, (error, data) => {
if (error) {
failWebsocketConnection(this.#handler, 1007, error.message)
return
}
this.writeFragments(data)
if (!this.#info.fin) {
this.#state = parserStates.INFO
this.#loop = true
this.run(callback)
return
}
websocketMessageReceived(this.#handler, this.#info.binaryType, this.consumeFragments())
this.#loop = true
this.#state = parserStates.INFO
this.run(callback)
})
this.#loop = false
break
}
}
}
}
}
/**
* Take n bytes from the buffered Buffers
* @param {number} n
* @returns {Buffer}
*/
consume (n) {
if (n > this.#byteOffset) {
throw new Error('Called consume() before buffers satiated.')
} else if (n === 0) {
return emptyBuffer
}
this.#byteOffset -= n
const first = this.#buffers[0]
if (first.length > n) {
// replace with remaining buffer
this.#buffers[0] = first.subarray(n, first.length)
return first.subarray(0, n)
} else if (first.length === n) {
// prefect match
return this.#buffers.shift()
} else {
let offset = 0
// If Buffer.allocUnsafe is used, extra copies will be made because the offset is non-zero.
const buffer = Buffer.allocUnsafeSlow(n)
while (offset !== n) {
const next = this.#buffers[0]
const length = next.length
if (length + offset === n) {
buffer.set(this.#buffers.shift(), offset)
break
} else if (length + offset > n) {
buffer.set(next.subarray(0, n - offset), offset)
this.#buffers[0] = next.subarray(n - offset)
break
} else {
buffer.set(this.#buffers.shift(), offset)
offset += length
}
}
return buffer
}
}
writeFragments (fragment) {
this.#fragmentsBytes += fragment.length
this.#fragments.push(fragment)
}
consumeFragments () {
const fragments = this.#fragments
if (fragments.length === 1) {
// single fragment
this.#fragmentsBytes = 0
return fragments.shift()
}
let offset = 0
// If Buffer.allocUnsafe is used, extra copies will be made because the offset is non-zero.
const output = Buffer.allocUnsafeSlow(this.#fragmentsBytes)
for (let i = 0; i < fragments.length; ++i) {
const buffer = fragments[i]
output.set(buffer, offset)
offset += buffer.length
}
this.#fragments = []
this.#fragmentsBytes = 0
return output
}
parseCloseBody (data) {
assert(data.length !== 1)
// https://datatracker.ietf.org/doc/html/rfc6455#section-7.1.5
/** @type {number|undefined} */
let code
if (data.length >= 2) {
// _The WebSocket Connection Close Code_ is
// defined as the status code (Section 7.4) contained in the first Close
// control frame received by the application
code = data.readUInt16BE(0)
}
if (code !== undefined && !isValidStatusCode(code)) {
return { code: 1002, reason: 'Invalid status code', error: true }
}
// https://datatracker.ietf.org/doc/html/rfc6455#section-7.1.6
/** @type {Buffer} */
let reason = data.subarray(2)
// Remove BOM
if (reason[0] === 0xEF && reason[1] === 0xBB && reason[2] === 0xBF) {
reason = reason.subarray(3)
}
try {
reason = utf8Decode(reason)
} catch {
return { code: 1007, reason: 'Invalid UTF-8', error: true }
}
return { code, reason, error: false }
}
/**
* Parses control frames.
* @param {Buffer} body
*/
parseControlFrame (body) {
const { opcode, payloadLength } = this.#info
if (opcode === opcodes.CLOSE) {
if (payloadLength === 1) {
failWebsocketConnection(this.#handler, 1002, 'Received close frame with a 1-byte body.')
return false
}
this.#info.closeInfo = this.parseCloseBody(body)
if (this.#info.closeInfo.error) {
const { code, reason } = this.#info.closeInfo
failWebsocketConnection(this.#handler, code, reason)
return false
}
// Upon receiving such a frame, the other peer sends a
// Close frame in response, if it hasn't already sent one.
if (!this.#handler.closeState.has(sentCloseFrameState.SENT) && !this.#handler.closeState.has(sentCloseFrameState.RECEIVED)) {
// If an endpoint receives a Close frame and did not previously send a
// Close frame, the endpoint MUST send a Close frame in response. (When
// sending a Close frame in response, the endpoint typically echos the
// status code it received.)
let body = emptyBuffer
if (this.#info.closeInfo.code) {
body = Buffer.allocUnsafe(2)
body.writeUInt16BE(this.#info.closeInfo.code, 0)
}
const closeFrame = new WebsocketFrameSend(body)
this.#handler.socket.write(closeFrame.createFrame(opcodes.CLOSE))
this.#handler.closeState.add(sentCloseFrameState.SENT)
}
// Upon either sending or receiving a Close control frame, it is said
// that _The WebSocket Closing Handshake is Started_ and that the
// WebSocket connection is in the CLOSING state.
this.#handler.readyState = states.CLOSING
this.#handler.closeState.add(sentCloseFrameState.RECEIVED)
return false
} else if (opcode === opcodes.PING) {
// Upon receipt of a Ping frame, an endpoint MUST send a Pong frame in
// response, unless it already received a Close frame.
// A Pong frame sent in response to a Ping frame must have identical
// "Application data"
if (!this.#handler.closeState.has(sentCloseFrameState.RECEIVED)) {
const frame = new WebsocketFrameSend(body)
this.#handler.socket.write(frame.createFrame(opcodes.PONG))
if (channels.ping.hasSubscribers) {
channels.ping.publish({
payload: body
})
}
}
} else if (opcode === opcodes.PONG) {
// A Pong frame MAY be sent unsolicited. This serves as a
// unidirectional heartbeat. A response to an unsolicited Pong frame is
// not expected.
if (channels.pong.hasSubscribers) {
channels.pong.publish({
payload: body
})
}
}
return true
}
get closingInfo () {
return this.#info.closeInfo
}
}
module.exports = {
ByteParser
}

109
node_modules/undici/lib/web/websocket/sender.js generated vendored Normal file
View File

@ -0,0 +1,109 @@
'use strict'
const { WebsocketFrameSend } = require('./frame')
const { opcodes, sendHints } = require('./constants')
const FixedQueue = require('../../dispatcher/fixed-queue')
/**
* @typedef {object} SendQueueNode
* @property {Promise<void> | null} promise
* @property {((...args: any[]) => any)} callback
* @property {Buffer | null} frame
*/
class SendQueue {
/**
* @type {FixedQueue}
*/
#queue = new FixedQueue()
/**
* @type {boolean}
*/
#running = false
/** @type {import('node:net').Socket} */
#socket
constructor (socket) {
this.#socket = socket
}
add (item, cb, hint) {
if (hint !== sendHints.blob) {
if (!this.#running) {
// TODO(@tsctx): support fast-path for string on running
if (hint === sendHints.text) {
// special fast-path for string
const { 0: head, 1: body } = WebsocketFrameSend.createFastTextFrame(item)
this.#socket.cork()
this.#socket.write(head)
this.#socket.write(body, cb)
this.#socket.uncork()
} else {
// direct writing
this.#socket.write(createFrame(item, hint), cb)
}
} else {
/** @type {SendQueueNode} */
const node = {
promise: null,
callback: cb,
frame: createFrame(item, hint)
}
this.#queue.push(node)
}
return
}
/** @type {SendQueueNode} */
const node = {
promise: item.arrayBuffer().then((ab) => {
node.promise = null
node.frame = createFrame(ab, hint)
}),
callback: cb,
frame: null
}
this.#queue.push(node)
if (!this.#running) {
this.#run()
}
}
async #run () {
this.#running = true
const queue = this.#queue
while (!queue.isEmpty()) {
const node = queue.shift()
// wait pending promise
if (node.promise !== null) {
await node.promise
}
// write
this.#socket.write(node.frame, node.callback)
// cleanup
node.callback = node.frame = null
}
this.#running = false
}
}
function createFrame (data, hint) {
return new WebsocketFrameSend(toBuffer(data, hint)).createFrame(hint === sendHints.text ? opcodes.TEXT : opcodes.BINARY)
}
function toBuffer (data, hint) {
switch (hint) {
case sendHints.text:
case sendHints.typedArray:
return new Uint8Array(data.buffer, data.byteOffset, data.byteLength)
case sendHints.arrayBuffer:
case sendHints.blob:
return new Uint8Array(data)
}
}
module.exports = { SendQueue }

View File

@ -0,0 +1,83 @@
'use strict'
const { webidl } = require('../../fetch/webidl')
const { validateCloseCodeAndReason } = require('../util')
const { kConstruct } = require('../../../core/symbols')
const { kEnumerableProperty } = require('../../../core/util')
class WebSocketError extends DOMException {
#closeCode
#reason
constructor (message = '', init = undefined) {
message = webidl.converters.DOMString(message, 'WebSocketError', 'message')
// 1. Set this 's name to " WebSocketError ".
// 2. Set this 's message to message .
super(message, 'WebSocketError')
if (init === kConstruct) {
return
} else if (init !== null) {
init = webidl.converters.WebSocketCloseInfo(init)
}
// 3. Let code be init [" closeCode "] if it exists , or null otherwise.
let code = init.closeCode ?? null
// 4. Let reason be init [" reason "] if it exists , or the empty string otherwise.
const reason = init.reason ?? ''
// 5. Validate close code and reason with code and reason .
validateCloseCodeAndReason(code, reason)
// 6. If reason is non-empty, but code is not set, then set code to 1000 ("Normal Closure").
if (reason.length !== 0 && code === null) {
code = 1000
}
// 7. Set this 's closeCode to code .
this.#closeCode = code
// 8. Set this 's reason to reason .
this.#reason = reason
}
get closeCode () {
return this.#closeCode
}
get reason () {
return this.#reason
}
/**
* @param {string} message
* @param {number|null} code
* @param {string} reason
*/
static createUnvalidatedWebSocketError (message, code, reason) {
const error = new WebSocketError(message, kConstruct)
error.#closeCode = code
error.#reason = reason
return error
}
}
const { createUnvalidatedWebSocketError } = WebSocketError
delete WebSocketError.createUnvalidatedWebSocketError
Object.defineProperties(WebSocketError.prototype, {
closeCode: kEnumerableProperty,
reason: kEnumerableProperty,
[Symbol.toStringTag]: {
value: 'WebSocketError',
writable: false,
enumerable: false,
configurable: true
}
})
webidl.is.WebSocketError = webidl.util.MakeTypeAssertion(WebSocketError)
module.exports = { WebSocketError, createUnvalidatedWebSocketError }

View File

@ -0,0 +1,485 @@
'use strict'
const { createDeferredPromise, environmentSettingsObject } = require('../../fetch/util')
const { states, opcodes, sentCloseFrameState } = require('../constants')
const { webidl } = require('../../fetch/webidl')
const { getURLRecord, isValidSubprotocol, isEstablished, utf8Decode } = require('../util')
const { establishWebSocketConnection, failWebsocketConnection, closeWebSocketConnection } = require('../connection')
const { types } = require('node:util')
const { channels } = require('../../../core/diagnostics')
const { WebsocketFrameSend } = require('../frame')
const { ByteParser } = require('../receiver')
const { WebSocketError, createUnvalidatedWebSocketError } = require('./websocketerror')
const { utf8DecodeBytes } = require('../../fetch/util')
const { kEnumerableProperty } = require('../../../core/util')
let emittedExperimentalWarning = false
class WebSocketStream {
// Each WebSocketStream object has an associated url , which is a URL record .
/** @type {URL} */
#url
// Each WebSocketStream object has an associated opened promise , which is a promise.
/** @type {ReturnType<typeof createDeferredPromise>} */
#openedPromise
// Each WebSocketStream object has an associated closed promise , which is a promise.
/** @type {ReturnType<typeof createDeferredPromise>} */
#closedPromise
// Each WebSocketStream object has an associated readable stream , which is a ReadableStream .
/** @type {ReadableStream} */
#readableStream
/** @type {ReadableStreamDefaultController} */
#readableStreamController
// Each WebSocketStream object has an associated writable stream , which is a WritableStream .
/** @type {WritableStream} */
#writableStream
// Each WebSocketStream object has an associated boolean handshake aborted , which is initially false.
#handshakeAborted = false
/** @type {import('../websocket').Handler} */
#handler = {
// https://whatpr.org/websockets/48/7b748d3...d5570f3.html#feedback-to-websocket-stream-from-the-protocol
onConnectionEstablished: (response, extensions) => this.#onConnectionEstablished(response, extensions),
onFail: (_code, _reason) => {},
onMessage: (opcode, data) => this.#onMessage(opcode, data),
onParserError: (err) => failWebsocketConnection(this.#handler, null, err.message),
onParserDrain: () => this.#handler.socket.resume(),
onSocketData: (chunk) => {
if (!this.#parser.write(chunk)) {
this.#handler.socket.pause()
}
},
onSocketError: (err) => {
this.#handler.readyState = states.CLOSING
if (channels.socketError.hasSubscribers) {
channels.socketError.publish(err)
}
this.#handler.socket.destroy()
},
onSocketClose: () => this.#onSocketClose(),
readyState: states.CONNECTING,
socket: null,
closeState: new Set(),
controller: null,
wasEverConnected: false
}
/** @type {import('../receiver').ByteParser} */
#parser
constructor (url, options = undefined) {
if (!emittedExperimentalWarning) {
process.emitWarning('WebSocketStream is experimental! Expect it to change at any time.', {
code: 'UNDICI-WSS'
})
emittedExperimentalWarning = true
}
webidl.argumentLengthCheck(arguments, 1, 'WebSocket')
url = webidl.converters.USVString(url)
if (options !== null) {
options = webidl.converters.WebSocketStreamOptions(options)
}
// 1. Let baseURL be this 's relevant settings object 's API base URL .
const baseURL = environmentSettingsObject.settingsObject.baseUrl
// 2. Let urlRecord be the result of getting a URL record given url and baseURL .
const urlRecord = getURLRecord(url, baseURL)
// 3. Let protocols be options [" protocols "] if it exists , otherwise an empty sequence.
const protocols = options.protocols
// 4. If any of the values in protocols occur more than once or otherwise fail to match the requirements for elements that comprise the value of ` Sec-WebSocket-Protocol ` fields as defined by The WebSocket Protocol , then throw a " SyntaxError " DOMException . [WSP]
if (protocols.length !== new Set(protocols.map(p => p.toLowerCase())).size) {
throw new DOMException('Invalid Sec-WebSocket-Protocol value', 'SyntaxError')
}
if (protocols.length > 0 && !protocols.every(p => isValidSubprotocol(p))) {
throw new DOMException('Invalid Sec-WebSocket-Protocol value', 'SyntaxError')
}
// 5. Set this 's url to urlRecord .
this.#url = urlRecord.toString()
// 6. Set this 's opened promise and closed promise to new promises.
this.#openedPromise = createDeferredPromise()
this.#closedPromise = createDeferredPromise()
// 7. Apply backpressure to the WebSocket.
// TODO
// 8. If options [" signal "] exists ,
if (options.signal != null) {
// 8.1. Let signal be options [" signal "].
const signal = options.signal
// 8.2. If signal is aborted , then reject this 's opened promise and closed promise with signal s abort reason
// and return.
if (signal.aborted) {
this.#openedPromise.reject(signal.reason)
this.#closedPromise.reject(signal.reason)
return
}
// 8.3. Add the following abort steps to signal :
signal.addEventListener('abort', () => {
// 8.3.1. If the WebSocket connection is not yet established : [WSP]
if (!isEstablished(this.#handler.readyState)) {
// 8.3.1.1. Fail the WebSocket connection .
failWebsocketConnection(this.#handler)
// Set this 's ready state to CLOSING .
this.#handler.readyState = states.CLOSING
// Reject this 's opened promise and closed promise with signal s abort reason .
this.#openedPromise.reject(signal.reason)
this.#closedPromise.reject(signal.reason)
// Set this 's handshake aborted to true.
this.#handshakeAborted = true
}
}, { once: true })
}
// 9. Let client be this 's relevant settings object .
const client = environmentSettingsObject.settingsObject
// 10. Run this step in parallel :
// 10.1. Establish a WebSocket connection given urlRecord , protocols , and client . [FETCH]
this.#handler.controller = establishWebSocketConnection(
urlRecord,
protocols,
client,
this.#handler,
options
)
}
// The url getter steps are to return this 's url , serialized .
get url () {
return this.#url.toString()
}
// The opened getter steps are to return this 's opened promise .
get opened () {
return this.#openedPromise.promise
}
// The closed getter steps are to return this 's closed promise .
get closed () {
return this.#closedPromise.promise
}
// The close( closeInfo ) method steps are:
close (closeInfo = undefined) {
if (closeInfo !== null) {
closeInfo = webidl.converters.WebSocketCloseInfo(closeInfo)
}
// 1. Let code be closeInfo [" closeCode "] if present, or null otherwise.
const code = closeInfo.closeCode ?? null
// 2. Let reason be closeInfo [" reason "].
const reason = closeInfo.reason
// 3. Close the WebSocket with this , code , and reason .
closeWebSocketConnection(this.#handler, code, reason, true)
}
#write (chunk) {
// 1. Let promise be a new promise created in stream s relevant realm .
const promise = createDeferredPromise()
// 2. Let data be null.
let data = null
// 3. Let opcode be null.
let opcode = null
// 4. If chunk is a BufferSource ,
if (ArrayBuffer.isView(chunk) || types.isArrayBuffer(chunk)) {
// 4.1. Set data to a copy of the bytes given chunk .
data = new Uint8Array(ArrayBuffer.isView(chunk) ? new Uint8Array(chunk.buffer, chunk.byteOffset, chunk.byteLength) : chunk)
// 4.2. Set opcode to a binary frame opcode.
opcode = opcodes.BINARY
} else {
// 5. Otherwise,
// 5.1. Let string be the result of converting chunk to an IDL USVString .
// If this throws an exception, return a promise rejected with the exception.
let string
try {
string = webidl.converters.DOMString(chunk)
} catch (e) {
promise.reject(e)
return
}
// 5.2. Set data to the result of UTF-8 encoding string .
data = new TextEncoder().encode(string)
// 5.3. Set opcode to a text frame opcode.
opcode = opcodes.TEXT
}
// 6. In parallel,
// 6.1. Wait until there is sufficient buffer space in stream to send the message.
// 6.2. If the closing handshake has not yet started , Send a WebSocket Message to stream comprised of data using opcode .
if (!this.#handler.closeState.has(sentCloseFrameState.SENT) && !this.#handler.closeState.has(sentCloseFrameState.RECEIVED)) {
const frame = new WebsocketFrameSend(data)
this.#handler.socket.write(frame.createFrame(opcode), () => {
promise.resolve(undefined)
})
}
// 6.3. Queue a global task on the WebSocket task source given stream s relevant global object to resolve promise with undefined.
return promise
}
/** @type {import('../websocket').Handler['onConnectionEstablished']} */
#onConnectionEstablished (response, parsedExtensions) {
this.#handler.socket = response.socket
const parser = new ByteParser(this.#handler, parsedExtensions)
parser.on('drain', () => this.#handler.onParserDrain())
parser.on('error', (err) => this.#handler.onParserError(err))
this.#parser = parser
// 1. Change stream s ready state to OPEN (1).
this.#handler.readyState = states.OPEN
// 2. Set stream s was ever connected to true.
// This is done in the opening handshake.
// 3. Let extensions be the extensions in use .
const extensions = parsedExtensions ?? ''
// 4. Let protocol be the subprotocol in use .
const protocol = response.headersList.get('sec-websocket-protocol') ?? ''
// 5. Let pullAlgorithm be an action that pulls bytes from stream .
// 6. Let cancelAlgorithm be an action that cancels stream with reason , given reason .
// 7. Let readable be a new ReadableStream .
// 8. Set up readable with pullAlgorithm and cancelAlgorithm .
const readable = new ReadableStream({
start: (controller) => {
this.#readableStreamController = controller
},
pull (controller) {
let chunk
while (controller.desiredSize > 0 && (chunk = response.socket.read()) !== null) {
controller.enqueue(chunk)
}
},
cancel: (reason) => this.#cancel(reason)
})
// 9. Let writeAlgorithm be an action that writes chunk to stream , given chunk .
// 10. Let closeAlgorithm be an action that closes stream .
// 11. Let abortAlgorithm be an action that aborts stream with reason , given reason .
// 12. Let writable be a new WritableStream .
// 13. Set up writable with writeAlgorithm , closeAlgorithm , and abortAlgorithm .
const writable = new WritableStream({
write: (chunk) => this.#write(chunk),
close: () => closeWebSocketConnection(this.#handler, null, null),
abort: (reason) => this.#closeUsingReason(reason)
})
// Set stream s readable stream to readable .
this.#readableStream = readable
// Set stream s writable stream to writable .
this.#writableStream = writable
// Resolve stream s opened promise with WebSocketOpenInfo «[ " extensions " → extensions , " protocol " → protocol , " readable " → readable , " writable " → writable ]».
this.#openedPromise.resolve({
extensions,
protocol,
readable,
writable
})
}
/** @type {import('../websocket').Handler['onMessage']} */
#onMessage (type, data) {
// 1. If streams ready state is not OPEN (1), then return.
if (this.#handler.readyState !== states.OPEN) {
return
}
// 2. Let chunk be determined by switching on type:
// - type indicates that the data is Text
// a new DOMString containing data
// - type indicates that the data is Binary
// a new Uint8Array object, created in the relevant Realm of the
// WebSocketStream object, whose contents are data
let chunk
if (type === opcodes.TEXT) {
try {
chunk = utf8Decode(data)
} catch {
failWebsocketConnection(this.#handler, 'Received invalid UTF-8 in text frame.')
return
}
} else if (type === opcodes.BINARY) {
chunk = new Uint8Array(data.buffer, data.byteOffset, data.byteLength)
}
// 3. Enqueue chunk into streams readable stream.
this.#readableStreamController.enqueue(chunk)
// 4. Apply backpressure to the WebSocket.
}
/** @type {import('../websocket').Handler['onSocketClose']} */
#onSocketClose () {
const wasClean =
this.#handler.closeState.has(sentCloseFrameState.SENT) &&
this.#handler.closeState.has(sentCloseFrameState.RECEIVED)
// 1. Change the ready state to CLOSED (3).
this.#handler.readyState = states.CLOSED
// 2. If stream s handshake aborted is true, then return.
if (this.#handshakeAborted) {
return
}
// 3. If stream s was ever connected is false, then reject stream s opened promise with a new WebSocketError.
if (!this.#handler.wasEverConnected) {
this.#openedPromise.reject(new WebSocketError('Socket never opened'))
}
const result = this.#parser.closingInfo
// 4. Let code be the WebSocket connection close code .
// https://datatracker.ietf.org/doc/html/rfc6455#section-7.1.5
// If this Close control frame contains no status code, _The WebSocket
// Connection Close Code_ is considered to be 1005. If _The WebSocket
// Connection is Closed_ and no Close control frame was received by the
// endpoint (such as could occur if the underlying transport connection
// is lost), _The WebSocket Connection Close Code_ is considered to be
// 1006.
let code = result?.code ?? 1005
if (!this.#handler.closeState.has(sentCloseFrameState.SENT) && !this.#handler.closeState.has(sentCloseFrameState.RECEIVED)) {
code = 1006
}
// 5. Let reason be the result of applying UTF-8 decode without BOM to the WebSocket connection close reason .
const reason = result?.reason == null ? '' : utf8DecodeBytes(Buffer.from(result.reason))
// 6. If the connection was closed cleanly ,
if (wasClean) {
// 6.1. Close stream s readable stream .
this.#readableStream.cancel().catch(() => {})
// 6.2. Error stream s writable stream with an " InvalidStateError " DOMException indicating that a closed WebSocketStream cannot be written to.
if (!this.#writableStream.locked) {
this.#writableStream.abort(new DOMException('A closed WebSocketStream cannot be written to', 'InvalidStateError'))
}
// 6.3. Resolve stream s closed promise with WebSocketCloseInfo «[ " closeCode " → code , " reason " → reason ]».
this.#closedPromise.resolve({
closeCode: code,
reason
})
} else {
// 7. Otherwise,
// 7.1. Let error be a new WebSocketError whose closeCode is code and reason is reason .
const error = createUnvalidatedWebSocketError('unclean close', code, reason)
// 7.2. Error stream s readable stream with error .
this.#readableStreamController.error(error)
// 7.3. Error stream s writable stream with error .
this.#writableStream.abort(error)
// 7.4. Reject stream s closed promise with error .
this.#closedPromise.reject(error)
}
}
#closeUsingReason (reason) {
// 1. Let code be null.
let code = null
// 2. Let reasonString be the empty string.
let reasonString = ''
// 3. If reason implements WebSocketError ,
if (webidl.is.WebSocketError(reason)) {
// 3.1. Set code to reason s closeCode .
code = reason.closeCode
// 3.2. Set reasonString to reason s reason .
reasonString = reason.reason
}
// 4. Close the WebSocket with stream , code , and reasonString . If this throws an exception,
// discard code and reasonString and close the WebSocket with stream .
closeWebSocketConnection(this.#handler, code, reasonString)
}
// To cancel a WebSocketStream stream given reason , close using reason giving stream and reason .
#cancel (reason) {
this.#closeUsingReason(reason)
}
}
Object.defineProperties(WebSocketStream.prototype, {
url: kEnumerableProperty,
opened: kEnumerableProperty,
closed: kEnumerableProperty,
close: kEnumerableProperty,
[Symbol.toStringTag]: {
value: 'WebSocketStream',
writable: false,
enumerable: false,
configurable: true
}
})
webidl.converters.WebSocketStreamOptions = webidl.dictionaryConverter([
{
key: 'protocols',
converter: webidl.sequenceConverter(webidl.converters.USVString),
defaultValue: () => []
},
{
key: 'signal',
converter: webidl.nullableConverter(webidl.converters.AbortSignal),
defaultValue: () => null
}
])
webidl.converters.WebSocketCloseInfo = webidl.dictionaryConverter([
{
key: 'closeCode',
converter: (V) => webidl.converters['unsigned short'](V, { enforceRange: true })
},
{
key: 'reason',
converter: webidl.converters.USVString,
defaultValue: () => ''
}
])
module.exports = { WebSocketStream }

338
node_modules/undici/lib/web/websocket/util.js generated vendored Normal file
View File

@ -0,0 +1,338 @@
'use strict'
const { states, opcodes } = require('./constants')
const { isUtf8 } = require('node:buffer')
const { collectASequenceOfCodePointsFast, removeHTTPWhitespace } = require('../fetch/data-url')
/**
* @param {number} readyState
* @returns {boolean}
*/
function isConnecting (readyState) {
// If the WebSocket connection is not yet established, and the connection
// is not yet closed, then the WebSocket connection is in the CONNECTING state.
return readyState === states.CONNECTING
}
/**
* @param {number} readyState
* @returns {boolean}
*/
function isEstablished (readyState) {
// If the server's response is validated as provided for above, it is
// said that _The WebSocket Connection is Established_ and that the
// WebSocket Connection is in the OPEN state.
return readyState === states.OPEN
}
/**
* @param {number} readyState
* @returns {boolean}
*/
function isClosing (readyState) {
// Upon either sending or receiving a Close control frame, it is said
// that _The WebSocket Closing Handshake is Started_ and that the
// WebSocket connection is in the CLOSING state.
return readyState === states.CLOSING
}
/**
* @param {number} readyState
* @returns {boolean}
*/
function isClosed (readyState) {
return readyState === states.CLOSED
}
/**
* @see https://dom.spec.whatwg.org/#concept-event-fire
* @param {string} e
* @param {EventTarget} target
* @param {(...args: ConstructorParameters<typeof Event>) => Event} eventFactory
* @param {EventInit | undefined} eventInitDict
* @returns {void}
*/
function fireEvent (e, target, eventFactory = (type, init) => new Event(type, init), eventInitDict = {}) {
// 1. If eventConstructor is not given, then let eventConstructor be Event.
// 2. Let event be the result of creating an event given eventConstructor,
// in the relevant realm of target.
// 3. Initialize events type attribute to e.
const event = eventFactory(e, eventInitDict)
// 4. Initialize any other IDL attributes of event as described in the
// invocation of this algorithm.
// 5. Return the result of dispatching event at target, with legacy target
// override flag set if set.
target.dispatchEvent(event)
}
/**
* @see https://websockets.spec.whatwg.org/#feedback-from-the-protocol
* @param {import('./websocket').Handler} handler
* @param {number} type Opcode
* @param {Buffer} data application data
* @returns {void}
*/
function websocketMessageReceived (handler, type, data) {
handler.onMessage(type, data)
}
/**
* @param {Buffer} buffer
* @returns {ArrayBuffer}
*/
function toArrayBuffer (buffer) {
if (buffer.byteLength === buffer.buffer.byteLength) {
return buffer.buffer
}
return new Uint8Array(buffer).buffer
}
/**
* @see https://datatracker.ietf.org/doc/html/rfc6455
* @see https://datatracker.ietf.org/doc/html/rfc2616
* @see https://bugs.chromium.org/p/chromium/issues/detail?id=398407
* @param {string} protocol
* @returns {boolean}
*/
function isValidSubprotocol (protocol) {
// If present, this value indicates one
// or more comma-separated subprotocol the client wishes to speak,
// ordered by preference. The elements that comprise this value
// MUST be non-empty strings with characters in the range U+0021 to
// U+007E not including separator characters as defined in
// [RFC2616] and MUST all be unique strings.
if (protocol.length === 0) {
return false
}
for (let i = 0; i < protocol.length; ++i) {
const code = protocol.charCodeAt(i)
if (
code < 0x21 || // CTL, contains SP (0x20) and HT (0x09)
code > 0x7E ||
code === 0x22 || // "
code === 0x28 || // (
code === 0x29 || // )
code === 0x2C || // ,
code === 0x2F || // /
code === 0x3A || // :
code === 0x3B || // ;
code === 0x3C || // <
code === 0x3D || // =
code === 0x3E || // >
code === 0x3F || // ?
code === 0x40 || // @
code === 0x5B || // [
code === 0x5C || // \
code === 0x5D || // ]
code === 0x7B || // {
code === 0x7D // }
) {
return false
}
}
return true
}
/**
* @see https://datatracker.ietf.org/doc/html/rfc6455#section-7-4
* @param {number} code
* @returns {boolean}
*/
function isValidStatusCode (code) {
if (code >= 1000 && code < 1015) {
return (
code !== 1004 && // reserved
code !== 1005 && // "MUST NOT be set as a status code"
code !== 1006 // "MUST NOT be set as a status code"
)
}
return code >= 3000 && code <= 4999
}
/**
* @see https://datatracker.ietf.org/doc/html/rfc6455#section-5.5
* @param {number} opcode
* @returns {boolean}
*/
function isControlFrame (opcode) {
return (
opcode === opcodes.CLOSE ||
opcode === opcodes.PING ||
opcode === opcodes.PONG
)
}
/**
* @param {number} opcode
* @returns {boolean}
*/
function isContinuationFrame (opcode) {
return opcode === opcodes.CONTINUATION
}
/**
* @param {number} opcode
* @returns {boolean}
*/
function isTextBinaryFrame (opcode) {
return opcode === opcodes.TEXT || opcode === opcodes.BINARY
}
/**
*
* @param {number} opcode
* @returns {boolean}
*/
function isValidOpcode (opcode) {
return isTextBinaryFrame(opcode) || isContinuationFrame(opcode) || isControlFrame(opcode)
}
/**
* Parses a Sec-WebSocket-Extensions header value.
* @param {string} extensions
* @returns {Map<string, string>}
*/
// TODO(@Uzlopak, @KhafraDev): make compliant https://datatracker.ietf.org/doc/html/rfc6455#section-9.1
function parseExtensions (extensions) {
const position = { position: 0 }
const extensionList = new Map()
while (position.position < extensions.length) {
const pair = collectASequenceOfCodePointsFast(';', extensions, position)
const [name, value = ''] = pair.split('=', 2)
extensionList.set(
removeHTTPWhitespace(name, true, false),
removeHTTPWhitespace(value, false, true)
)
position.position++
}
return extensionList
}
/**
* @see https://www.rfc-editor.org/rfc/rfc7692#section-7.1.2.2
* @description "client-max-window-bits = 1*DIGIT"
* @param {string} value
* @returns {boolean}
*/
function isValidClientWindowBits (value) {
for (let i = 0; i < value.length; i++) {
const byte = value.charCodeAt(i)
if (byte < 0x30 || byte > 0x39) {
return false
}
}
return true
}
/**
* @see https://whatpr.org/websockets/48/7b748d3...d5570f3.html#get-a-url-record
* @param {string} url
* @param {string} [baseURL]
*/
function getURLRecord (url, baseURL) {
// 1. Let urlRecord be the result of applying the URL parser to url with baseURL .
// 2. If urlRecord is failure, then throw a " SyntaxError " DOMException .
let urlRecord
try {
urlRecord = new URL(url, baseURL)
} catch (e) {
throw new DOMException(e, 'SyntaxError')
}
// 3. If urlRecord s scheme is " http ", then set urlRecord s scheme to " ws ".
// 4. Otherwise, if urlRecord s scheme is " https ", set urlRecord s scheme to " wss ".
if (urlRecord.protocol === 'http:') {
urlRecord.protocol = 'ws:'
} else if (urlRecord.protocol === 'https:') {
urlRecord.protocol = 'wss:'
}
// 5. If urlRecord s scheme is not " ws " or " wss ", then throw a " SyntaxError " DOMException .
if (urlRecord.protocol !== 'ws:' && urlRecord.protocol !== 'wss:') {
throw new DOMException('expected a ws: or wss: url', 'SyntaxError')
}
// If urlRecord s fragment is non-null, then throw a " SyntaxError " DOMException .
if (urlRecord.hash.length || urlRecord.href.endsWith('#')) {
throw new DOMException('hash', 'SyntaxError')
}
// Return urlRecord .
return urlRecord
}
// https://whatpr.org/websockets/48.html#validate-close-code-and-reason
function validateCloseCodeAndReason (code, reason) {
// 1. If code is not null, but is neither an integer equal to
// 1000 nor an integer in the range 3000 to 4999, inclusive,
// throw an "InvalidAccessError" DOMException.
if (code !== null) {
if (code !== 1000 && (code < 3000 || code > 4999)) {
throw new DOMException('invalid code', 'InvalidAccessError')
}
}
// 2. If reason is not null, then:
if (reason !== null) {
// 2.1. Let reasonBytes be the result of UTF-8 encoding reason.
// 2.2. If reasonBytes is longer than 123 bytes, then throw a
// "SyntaxError" DOMException.
const reasonBytesLength = Buffer.byteLength(reason)
if (reasonBytesLength > 123) {
throw new DOMException(`Reason must be less than 123 bytes; received ${reasonBytesLength}`, 'SyntaxError')
}
}
}
/**
* Converts a Buffer to utf-8, even on platforms without icu.
* @type {(buffer: Buffer) => string}
*/
const utf8Decode = (() => {
if (typeof process.versions.icu === 'string') {
const fatalDecoder = new TextDecoder('utf-8', { fatal: true })
return fatalDecoder.decode.bind(fatalDecoder)
}
return function (buffer) {
if (isUtf8(buffer)) {
return buffer.toString('utf-8')
}
throw new TypeError('Invalid utf-8 received.')
}
})()
module.exports = {
isConnecting,
isEstablished,
isClosing,
isClosed,
fireEvent,
isValidSubprotocol,
isValidStatusCode,
websocketMessageReceived,
utf8Decode,
isControlFrame,
isContinuationFrame,
isTextBinaryFrame,
isValidOpcode,
parseExtensions,
isValidClientWindowBits,
toArrayBuffer,
getURLRecord,
validateCloseCodeAndReason
}

686
node_modules/undici/lib/web/websocket/websocket.js generated vendored Normal file
View File

@ -0,0 +1,686 @@
'use strict'
const { webidl } = require('../fetch/webidl')
const { URLSerializer } = require('../fetch/data-url')
const { environmentSettingsObject } = require('../fetch/util')
const { staticPropertyDescriptors, states, sentCloseFrameState, sendHints, opcodes } = require('./constants')
const {
isConnecting,
isEstablished,
isClosing,
isValidSubprotocol,
fireEvent,
utf8Decode,
toArrayBuffer,
getURLRecord
} = require('./util')
const { establishWebSocketConnection, closeWebSocketConnection, failWebsocketConnection } = require('./connection')
const { ByteParser } = require('./receiver')
const { kEnumerableProperty } = require('../../core/util')
const { getGlobalDispatcher } = require('../../global')
const { types } = require('node:util')
const { ErrorEvent, CloseEvent, createFastMessageEvent } = require('./events')
const { SendQueue } = require('./sender')
const { channels } = require('../../core/diagnostics')
/**
* @typedef {object} Handler
* @property {(response: any, extensions?: string[]) => void} onConnectionEstablished
* @property {(code: number, reason: any) => void} onFail
* @property {(opcode: number, data: Buffer) => void} onMessage
* @property {(error: Error) => void} onParserError
* @property {() => void} onParserDrain
* @property {(chunk: Buffer) => void} onSocketData
* @property {(err: Error) => void} onSocketError
* @property {() => void} onSocketClose
*
* @property {number} readyState
* @property {import('stream').Duplex} socket
* @property {Set<number>} closeState
* @property {import('../fetch/index').Fetch} controller
* @property {boolean} [wasEverConnected=false]
*/
// https://websockets.spec.whatwg.org/#interface-definition
class WebSocket extends EventTarget {
#events = {
open: null,
error: null,
close: null,
message: null
}
#bufferedAmount = 0
#protocol = ''
#extensions = ''
/** @type {SendQueue} */
#sendQueue
/** @type {Handler} */
#handler = {
onConnectionEstablished: (response, extensions) => this.#onConnectionEstablished(response, extensions),
onFail: (code, reason) => this.#onFail(code, reason),
onMessage: (opcode, data) => this.#onMessage(opcode, data),
onParserError: (err) => failWebsocketConnection(this.#handler, null, err.message),
onParserDrain: () => this.#onParserDrain(),
onSocketData: (chunk) => {
if (!this.#parser.write(chunk)) {
this.#handler.socket.pause()
}
},
onSocketError: (err) => {
this.#handler.readyState = states.CLOSING
if (channels.socketError.hasSubscribers) {
channels.socketError.publish(err)
}
this.#handler.socket.destroy()
},
onSocketClose: () => this.#onSocketClose(),
readyState: states.CONNECTING,
socket: null,
closeState: new Set(),
controller: null,
wasEverConnected: false
}
#url
#binaryType
/** @type {import('./receiver').ByteParser} */
#parser
/**
* @param {string} url
* @param {string|string[]} protocols
*/
constructor (url, protocols = []) {
super()
webidl.util.markAsUncloneable(this)
const prefix = 'WebSocket constructor'
webidl.argumentLengthCheck(arguments, 1, prefix)
const options = webidl.converters['DOMString or sequence<DOMString> or WebSocketInit'](protocols, prefix, 'options')
url = webidl.converters.USVString(url)
protocols = options.protocols
// 1. Let baseURL be this's relevant settings object's API base URL.
const baseURL = environmentSettingsObject.settingsObject.baseUrl
// 2. Let urlRecord be the result of getting a URL record given url and baseURL.
const urlRecord = getURLRecord(url, baseURL)
// 3. If protocols is a string, set protocols to a sequence consisting
// of just that string.
if (typeof protocols === 'string') {
protocols = [protocols]
}
// 4. If any of the values in protocols occur more than once or otherwise
// fail to match the requirements for elements that comprise the value
// of `Sec-WebSocket-Protocol` fields as defined by The WebSocket
// protocol, then throw a "SyntaxError" DOMException.
if (protocols.length !== new Set(protocols.map(p => p.toLowerCase())).size) {
throw new DOMException('Invalid Sec-WebSocket-Protocol value', 'SyntaxError')
}
if (protocols.length > 0 && !protocols.every(p => isValidSubprotocol(p))) {
throw new DOMException('Invalid Sec-WebSocket-Protocol value', 'SyntaxError')
}
// 5. Set this's url to urlRecord.
this.#url = new URL(urlRecord.href)
// 6. Let client be this's relevant settings object.
const client = environmentSettingsObject.settingsObject
// 7. Run this step in parallel:
// 7.1. Establish a WebSocket connection given urlRecord, protocols,
// and client.
this.#handler.controller = establishWebSocketConnection(
urlRecord,
protocols,
client,
this.#handler,
options
)
// Each WebSocket object has an associated ready state, which is a
// number representing the state of the connection. Initially it must
// be CONNECTING (0).
this.#handler.readyState = WebSocket.CONNECTING
// The extensions attribute must initially return the empty string.
// The protocol attribute must initially return the empty string.
// Each WebSocket object has an associated binary type, which is a
// BinaryType. Initially it must be "blob".
this.#binaryType = 'blob'
}
/**
* @see https://websockets.spec.whatwg.org/#dom-websocket-close
* @param {number|undefined} code
* @param {string|undefined} reason
*/
close (code = undefined, reason = undefined) {
webidl.brandCheck(this, WebSocket)
const prefix = 'WebSocket.close'
if (code !== undefined) {
code = webidl.converters['unsigned short'](code, prefix, 'code', { clamp: true })
}
if (reason !== undefined) {
reason = webidl.converters.USVString(reason)
}
// 1. If code is the special value "missing", then set code to null.
code ??= null
// 2. If reason is the special value "missing", then set reason to the empty string.
reason ??= ''
// 3. Close the WebSocket with this, code, and reason.
closeWebSocketConnection(this.#handler, code, reason, true)
}
/**
* @see https://websockets.spec.whatwg.org/#dom-websocket-send
* @param {NodeJS.TypedArray|ArrayBuffer|Blob|string} data
*/
send (data) {
webidl.brandCheck(this, WebSocket)
const prefix = 'WebSocket.send'
webidl.argumentLengthCheck(arguments, 1, prefix)
data = webidl.converters.WebSocketSendData(data, prefix, 'data')
// 1. If this's ready state is CONNECTING, then throw an
// "InvalidStateError" DOMException.
if (isConnecting(this.#handler.readyState)) {
throw new DOMException('Sent before connected.', 'InvalidStateError')
}
// 2. Run the appropriate set of steps from the following list:
// https://datatracker.ietf.org/doc/html/rfc6455#section-6.1
// https://datatracker.ietf.org/doc/html/rfc6455#section-5.2
if (!isEstablished(this.#handler.readyState) || isClosing(this.#handler.readyState)) {
return
}
// If data is a string
if (typeof data === 'string') {
// If the WebSocket connection is established and the WebSocket
// closing handshake has not yet started, then the user agent
// must send a WebSocket Message comprised of the data argument
// using a text frame opcode; if the data cannot be sent, e.g.
// because it would need to be buffered but the buffer is full,
// the user agent must flag the WebSocket as full and then close
// the WebSocket connection. Any invocation of this method with a
// string argument that does not throw an exception must increase
// the bufferedAmount attribute by the number of bytes needed to
// express the argument as UTF-8.
const buffer = Buffer.from(data)
this.#bufferedAmount += buffer.byteLength
this.#sendQueue.add(buffer, () => {
this.#bufferedAmount -= buffer.byteLength
}, sendHints.text)
} else if (types.isArrayBuffer(data)) {
// If the WebSocket connection is established, and the WebSocket
// closing handshake has not yet started, then the user agent must
// send a WebSocket Message comprised of data using a binary frame
// opcode; if the data cannot be sent, e.g. because it would need
// to be buffered but the buffer is full, the user agent must flag
// the WebSocket as full and then close the WebSocket connection.
// The data to be sent is the data stored in the buffer described
// by the ArrayBuffer object. Any invocation of this method with an
// ArrayBuffer argument that does not throw an exception must
// increase the bufferedAmount attribute by the length of the
// ArrayBuffer in bytes.
this.#bufferedAmount += data.byteLength
this.#sendQueue.add(data, () => {
this.#bufferedAmount -= data.byteLength
}, sendHints.arrayBuffer)
} else if (ArrayBuffer.isView(data)) {
// If the WebSocket connection is established, and the WebSocket
// closing handshake has not yet started, then the user agent must
// send a WebSocket Message comprised of data using a binary frame
// opcode; if the data cannot be sent, e.g. because it would need to
// be buffered but the buffer is full, the user agent must flag the
// WebSocket as full and then close the WebSocket connection. The
// data to be sent is the data stored in the section of the buffer
// described by the ArrayBuffer object that data references. Any
// invocation of this method with this kind of argument that does
// not throw an exception must increase the bufferedAmount attribute
// by the length of datas buffer in bytes.
this.#bufferedAmount += data.byteLength
this.#sendQueue.add(data, () => {
this.#bufferedAmount -= data.byteLength
}, sendHints.typedArray)
} else if (webidl.is.Blob(data)) {
// If the WebSocket connection is established, and the WebSocket
// closing handshake has not yet started, then the user agent must
// send a WebSocket Message comprised of data using a binary frame
// opcode; if the data cannot be sent, e.g. because it would need to
// be buffered but the buffer is full, the user agent must flag the
// WebSocket as full and then close the WebSocket connection. The data
// to be sent is the raw data represented by the Blob object. Any
// invocation of this method with a Blob argument that does not throw
// an exception must increase the bufferedAmount attribute by the size
// of the Blob objects raw data, in bytes.
this.#bufferedAmount += data.size
this.#sendQueue.add(data, () => {
this.#bufferedAmount -= data.size
}, sendHints.blob)
}
}
get readyState () {
webidl.brandCheck(this, WebSocket)
// The readyState getter steps are to return this's ready state.
return this.#handler.readyState
}
get bufferedAmount () {
webidl.brandCheck(this, WebSocket)
return this.#bufferedAmount
}
get url () {
webidl.brandCheck(this, WebSocket)
// The url getter steps are to return this's url, serialized.
return URLSerializer(this.#url)
}
get extensions () {
webidl.brandCheck(this, WebSocket)
return this.#extensions
}
get protocol () {
webidl.brandCheck(this, WebSocket)
return this.#protocol
}
get onopen () {
webidl.brandCheck(this, WebSocket)
return this.#events.open
}
set onopen (fn) {
webidl.brandCheck(this, WebSocket)
if (this.#events.open) {
this.removeEventListener('open', this.#events.open)
}
if (typeof fn === 'function') {
this.#events.open = fn
this.addEventListener('open', fn)
} else {
this.#events.open = null
}
}
get onerror () {
webidl.brandCheck(this, WebSocket)
return this.#events.error
}
set onerror (fn) {
webidl.brandCheck(this, WebSocket)
if (this.#events.error) {
this.removeEventListener('error', this.#events.error)
}
if (typeof fn === 'function') {
this.#events.error = fn
this.addEventListener('error', fn)
} else {
this.#events.error = null
}
}
get onclose () {
webidl.brandCheck(this, WebSocket)
return this.#events.close
}
set onclose (fn) {
webidl.brandCheck(this, WebSocket)
if (this.#events.close) {
this.removeEventListener('close', this.#events.close)
}
if (typeof fn === 'function') {
this.#events.close = fn
this.addEventListener('close', fn)
} else {
this.#events.close = null
}
}
get onmessage () {
webidl.brandCheck(this, WebSocket)
return this.#events.message
}
set onmessage (fn) {
webidl.brandCheck(this, WebSocket)
if (this.#events.message) {
this.removeEventListener('message', this.#events.message)
}
if (typeof fn === 'function') {
this.#events.message = fn
this.addEventListener('message', fn)
} else {
this.#events.message = null
}
}
get binaryType () {
webidl.brandCheck(this, WebSocket)
return this.#binaryType
}
set binaryType (type) {
webidl.brandCheck(this, WebSocket)
if (type !== 'blob' && type !== 'arraybuffer') {
this.#binaryType = 'blob'
} else {
this.#binaryType = type
}
}
/**
* @see https://websockets.spec.whatwg.org/#feedback-from-the-protocol
*/
#onConnectionEstablished (response, parsedExtensions) {
// processResponse is called when the "responses header list has been received and initialized."
// once this happens, the connection is open
this.#handler.socket = response.socket
const parser = new ByteParser(this.#handler, parsedExtensions)
parser.on('drain', () => this.#handler.onParserDrain())
parser.on('error', (err) => this.#handler.onParserError(err))
this.#parser = parser
this.#sendQueue = new SendQueue(response.socket)
// 1. Change the ready state to OPEN (1).
this.#handler.readyState = states.OPEN
// 2. Change the extensions attributes value to the extensions in use, if
// it is not the null value.
// https://datatracker.ietf.org/doc/html/rfc6455#section-9.1
const extensions = response.headersList.get('sec-websocket-extensions')
if (extensions !== null) {
this.#extensions = extensions
}
// 3. Change the protocol attributes value to the subprotocol in use, if
// it is not the null value.
// https://datatracker.ietf.org/doc/html/rfc6455#section-1.9
const protocol = response.headersList.get('sec-websocket-protocol')
if (protocol !== null) {
this.#protocol = protocol
}
// 4. Fire an event named open at the WebSocket object.
fireEvent('open', this)
}
#onFail (code, reason) {
if (reason) {
// TODO: process.nextTick
fireEvent('error', this, (type, init) => new ErrorEvent(type, init), {
error: new Error(reason),
message: reason
})
}
if (!this.#handler.wasEverConnected) {
this.#handler.readyState = states.CLOSED
// If the WebSocket connection could not be established, it is also said
// that _The WebSocket Connection is Closed_, but not _cleanly_.
fireEvent('close', this, (type, init) => new CloseEvent(type, init), {
wasClean: false, code, reason
})
}
}
#onMessage (type, data) {
// 1. If ready state is not OPEN (1), then return.
if (this.#handler.readyState !== states.OPEN) {
return
}
// 2. Let dataForEvent be determined by switching on type and binary type:
let dataForEvent
if (type === opcodes.TEXT) {
// -> type indicates that the data is Text
// a new DOMString containing data
try {
dataForEvent = utf8Decode(data)
} catch {
failWebsocketConnection(this.#handler, 1007, 'Received invalid UTF-8 in text frame.')
return
}
} else if (type === opcodes.BINARY) {
if (this.#binaryType === 'blob') {
// -> type indicates that the data is Binary and binary type is "blob"
// a new Blob object, created in the relevant Realm of the WebSocket
// object, that represents data as its raw data
dataForEvent = new Blob([data])
} else {
// -> type indicates that the data is Binary and binary type is "arraybuffer"
// a new ArrayBuffer object, created in the relevant Realm of the
// WebSocket object, whose contents are data
dataForEvent = toArrayBuffer(data)
}
}
// 3. Fire an event named message at the WebSocket object, using MessageEvent,
// with the origin attribute initialized to the serialization of the WebSocket
// objects url's origin, and the data attribute initialized to dataForEvent.
fireEvent('message', this, createFastMessageEvent, {
origin: this.#url.origin,
data: dataForEvent
})
}
#onParserDrain () {
this.#handler.socket.resume()
}
/**
* @see https://websockets.spec.whatwg.org/#feedback-from-the-protocol
* @see https://datatracker.ietf.org/doc/html/rfc6455#section-7.1.4
*/
#onSocketClose () {
// If the TCP connection was closed after the
// WebSocket closing handshake was completed, the WebSocket connection
// is said to have been closed _cleanly_.
const wasClean =
this.#handler.closeState.has(sentCloseFrameState.SENT) &&
this.#handler.closeState.has(sentCloseFrameState.RECEIVED)
let code = 1005
let reason = ''
const result = this.#parser.closingInfo
if (result && !result.error) {
code = result.code ?? 1005
reason = result.reason
} else if (!this.#handler.closeState.has(sentCloseFrameState.RECEIVED)) {
// If _The WebSocket
// Connection is Closed_ and no Close control frame was received by the
// endpoint (such as could occur if the underlying transport connection
// is lost), _The WebSocket Connection Close Code_ is considered to be
// 1006.
code = 1006
}
// 1. Change the ready state to CLOSED (3).
this.#handler.readyState = states.CLOSED
// 2. If the user agent was required to fail the WebSocket
// connection, or if the WebSocket connection was closed
// after being flagged as full, fire an event named error
// at the WebSocket object.
// TODO
// 3. Fire an event named close at the WebSocket object,
// using CloseEvent, with the wasClean attribute
// initialized to true if the connection closed cleanly
// and false otherwise, the code attribute initialized to
// the WebSocket connection close code, and the reason
// attribute initialized to the result of applying UTF-8
// decode without BOM to the WebSocket connection close
// reason.
// TODO: process.nextTick
fireEvent('close', this, (type, init) => new CloseEvent(type, init), {
wasClean, code, reason
})
if (channels.close.hasSubscribers) {
channels.close.publish({
websocket: this,
code,
reason
})
}
}
}
// https://websockets.spec.whatwg.org/#dom-websocket-connecting
WebSocket.CONNECTING = WebSocket.prototype.CONNECTING = states.CONNECTING
// https://websockets.spec.whatwg.org/#dom-websocket-open
WebSocket.OPEN = WebSocket.prototype.OPEN = states.OPEN
// https://websockets.spec.whatwg.org/#dom-websocket-closing
WebSocket.CLOSING = WebSocket.prototype.CLOSING = states.CLOSING
// https://websockets.spec.whatwg.org/#dom-websocket-closed
WebSocket.CLOSED = WebSocket.prototype.CLOSED = states.CLOSED
Object.defineProperties(WebSocket.prototype, {
CONNECTING: staticPropertyDescriptors,
OPEN: staticPropertyDescriptors,
CLOSING: staticPropertyDescriptors,
CLOSED: staticPropertyDescriptors,
url: kEnumerableProperty,
readyState: kEnumerableProperty,
bufferedAmount: kEnumerableProperty,
onopen: kEnumerableProperty,
onerror: kEnumerableProperty,
onclose: kEnumerableProperty,
close: kEnumerableProperty,
onmessage: kEnumerableProperty,
binaryType: kEnumerableProperty,
send: kEnumerableProperty,
extensions: kEnumerableProperty,
protocol: kEnumerableProperty,
[Symbol.toStringTag]: {
value: 'WebSocket',
writable: false,
enumerable: false,
configurable: true
}
})
Object.defineProperties(WebSocket, {
CONNECTING: staticPropertyDescriptors,
OPEN: staticPropertyDescriptors,
CLOSING: staticPropertyDescriptors,
CLOSED: staticPropertyDescriptors
})
webidl.converters['sequence<DOMString>'] = webidl.sequenceConverter(
webidl.converters.DOMString
)
webidl.converters['DOMString or sequence<DOMString>'] = function (V, prefix, argument) {
if (webidl.util.Type(V) === webidl.util.Types.OBJECT && Symbol.iterator in V) {
return webidl.converters['sequence<DOMString>'](V)
}
return webidl.converters.DOMString(V, prefix, argument)
}
// This implements the proposal made in https://github.com/whatwg/websockets/issues/42
webidl.converters.WebSocketInit = webidl.dictionaryConverter([
{
key: 'protocols',
converter: webidl.converters['DOMString or sequence<DOMString>'],
defaultValue: () => new Array(0)
},
{
key: 'dispatcher',
converter: webidl.converters.any,
defaultValue: () => getGlobalDispatcher()
},
{
key: 'headers',
converter: webidl.nullableConverter(webidl.converters.HeadersInit)
}
])
webidl.converters['DOMString or sequence<DOMString> or WebSocketInit'] = function (V) {
if (webidl.util.Type(V) === webidl.util.Types.OBJECT && !(Symbol.iterator in V)) {
return webidl.converters.WebSocketInit(V)
}
return { protocols: webidl.converters['DOMString or sequence<DOMString>'](V) }
}
webidl.converters.WebSocketSendData = function (V) {
if (webidl.util.Type(V) === webidl.util.Types.OBJECT) {
if (webidl.is.Blob(V)) {
return V
}
if (ArrayBuffer.isView(V) || types.isArrayBuffer(V)) {
return V
}
}
return webidl.converters.USVString(V)
}
module.exports = {
WebSocket
}