format: prettify entire project

This commit is contained in:
Rim
2025-04-02 06:50:39 -04:00
parent 86f0782a98
commit 7ccc0be712
1711 changed files with 755867 additions and 235931 deletions

File diff suppressed because it is too large Load Diff

View File

@ -1,52 +1,53 @@
'use strict'
'use strict';
const { Cache } = require('./cache')
const { webidl } = require('../fetch/webidl')
const { kEnumerableProperty } = require('../../core/util')
const { kConstruct } = require('../../core/symbols')
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()
#caches = new Map();
constructor () {
constructor() {
if (arguments[0] !== kConstruct) {
webidl.illegalConstructor()
webidl.illegalConstructor();
}
webidl.util.markAsUncloneable(this)
webidl.util.markAsUncloneable(this);
}
async match (request, options = {}) {
webidl.brandCheck(this, CacheStorage)
webidl.argumentLengthCheck(arguments, 1, 'CacheStorage.match')
async match(request, options = {}) {
webidl.brandCheck(this, CacheStorage);
webidl.argumentLengthCheck(arguments, 1, 'CacheStorage.match');
request = webidl.converters.RequestInfo(request)
options = webidl.converters.MultiCacheQueryOptions(options)
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)
const cacheList = this.#caches.get(options.cacheName);
const cache = new Cache(kConstruct, cacheList);
return await cache.match(request, options)
return await cache.match(request, options);
}
} else { // 2.
} else {
// 2.
// 2.2
for (const cacheList of this.#caches.values()) {
const cache = new Cache(kConstruct, cacheList)
const cache = new Cache(kConstruct, cacheList);
// 2.2.1.2
const response = await cache.match(request, options)
const response = await cache.match(request, options);
if (response !== undefined) {
return response
return response;
}
}
}
@ -57,17 +58,17 @@ class CacheStorage {
* @param {string} cacheName
* @returns {Promise<boolean>}
*/
async has (cacheName) {
webidl.brandCheck(this, CacheStorage)
async has(cacheName) {
webidl.brandCheck(this, CacheStorage);
const prefix = 'CacheStorage.has'
webidl.argumentLengthCheck(arguments, 1, prefix)
const prefix = 'CacheStorage.has';
webidl.argumentLengthCheck(arguments, 1, prefix);
cacheName = webidl.converters.DOMString(cacheName, prefix, 'cacheName')
cacheName = webidl.converters.DOMString(cacheName, prefix, 'cacheName');
// 2.1.1
// 2.2
return this.#caches.has(cacheName)
return this.#caches.has(cacheName);
}
/**
@ -75,33 +76,33 @@ class CacheStorage {
* @param {string} cacheName
* @returns {Promise<Cache>}
*/
async open (cacheName) {
webidl.brandCheck(this, CacheStorage)
async open(cacheName) {
webidl.brandCheck(this, CacheStorage);
const prefix = 'CacheStorage.open'
webidl.argumentLengthCheck(arguments, 1, prefix)
const prefix = 'CacheStorage.open';
webidl.argumentLengthCheck(arguments, 1, prefix);
cacheName = webidl.converters.DOMString(cacheName, prefix, 'cacheName')
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)
const cache = this.#caches.get(cacheName);
// 2.1.1.1
return new Cache(kConstruct, cache)
return new Cache(kConstruct, cache);
}
// 2.2
const cache = []
const cache = [];
// 2.3
this.#caches.set(cacheName, cache)
this.#caches.set(cacheName, cache);
// 2.4
return new Cache(kConstruct, cache)
return new Cache(kConstruct, cache);
}
/**
@ -109,44 +110,44 @@ class CacheStorage {
* @param {string} cacheName
* @returns {Promise<boolean>}
*/
async delete (cacheName) {
webidl.brandCheck(this, CacheStorage)
async delete(cacheName) {
webidl.brandCheck(this, CacheStorage);
const prefix = 'CacheStorage.delete'
webidl.argumentLengthCheck(arguments, 1, prefix)
const prefix = 'CacheStorage.delete';
webidl.argumentLengthCheck(arguments, 1, prefix);
cacheName = webidl.converters.DOMString(cacheName, prefix, 'cacheName')
cacheName = webidl.converters.DOMString(cacheName, prefix, 'cacheName');
return this.#caches.delete(cacheName)
return this.#caches.delete(cacheName);
}
/**
* @see https://w3c.github.io/ServiceWorker/#cache-storage-keys
* @returns {Promise<string[]>}
*/
async keys () {
webidl.brandCheck(this, CacheStorage)
async keys() {
webidl.brandCheck(this, CacheStorage);
// 2.1
const keys = this.#caches.keys()
const keys = this.#caches.keys();
// 2.2
return [...keys]
return [...keys];
}
}
Object.defineProperties(CacheStorage.prototype, {
[Symbol.toStringTag]: {
value: 'CacheStorage',
configurable: true
configurable: true,
},
match: kEnumerableProperty,
has: kEnumerableProperty,
open: kEnumerableProperty,
delete: kEnumerableProperty,
keys: kEnumerableProperty
})
keys: kEnumerableProperty,
});
module.exports = {
CacheStorage
}
CacheStorage,
};

View File

@ -1,8 +1,8 @@
'use strict'
'use strict';
const assert = require('node:assert')
const { URLSerializer } = require('../fetch/data-url')
const { isValidHeaderName } = require('../fetch/util')
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
@ -11,35 +11,35 @@ const { isValidHeaderName } = require('../fetch/util')
* @param {boolean | undefined} excludeFragment
* @returns {boolean}
*/
function urlEquals (A, B, excludeFragment = false) {
const serializedA = URLSerializer(A, excludeFragment)
function urlEquals(A, B, excludeFragment = false) {
const serializedA = URLSerializer(A, excludeFragment);
const serializedB = URLSerializer(B, excludeFragment)
const serializedB = URLSerializer(B, excludeFragment);
return serializedA === serializedB
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)
function getFieldValues(header) {
assert(header !== null);
const values = []
const values = [];
for (let value of header.split(',')) {
value = value.trim()
value = value.trim();
if (isValidHeaderName(value)) {
values.push(value)
values.push(value);
}
}
return values
return values;
}
module.exports = {
urlEquals,
getFieldValues
}
getFieldValues,
};

View File

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

View File

@ -1,11 +1,13 @@
'use strict'
'use strict';
const { parseSetCookie } = require('./parse')
const { stringify } = require('./util')
const { webidl } = require('../fetch/webidl')
const { Headers } = require('../fetch/headers')
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))
const brandChecks = webidl.brandCheckMultiple(
[Headers, globalThis.Headers].filter(Boolean)
);
/**
* @typedef {Object} Cookie
@ -25,27 +27,27 @@ const brandChecks = webidl.brandCheckMultiple([Headers, globalThis.Headers].filt
* @param {Headers} headers
* @returns {Record<string, string>}
*/
function getCookies (headers) {
webidl.argumentLengthCheck(arguments, 1, 'getCookies')
function getCookies(headers) {
webidl.argumentLengthCheck(arguments, 1, 'getCookies');
brandChecks(headers)
brandChecks(headers);
const cookie = headers.get('cookie')
const cookie = headers.get('cookie');
/** @type {Record<string, string>} */
const out = {}
const out = {};
if (!cookie) {
return out
return out;
}
for (const piece of cookie.split(';')) {
const [name, ...value] = piece.split('=')
const [name, ...value] = piece.split('=');
out[name.trim()] = value.join('=')
out[name.trim()] = value.join('=');
}
return out
return out;
}
/**
@ -54,14 +56,14 @@ function getCookies (headers) {
* @param {{ path?: string, domain?: string }|undefined} attributes
* @returns {void}
*/
function deleteCookie (headers, name, attributes) {
brandChecks(headers)
function deleteCookie(headers, name, attributes) {
brandChecks(headers);
const prefix = 'deleteCookie'
webidl.argumentLengthCheck(arguments, 2, prefix)
const prefix = 'deleteCookie';
webidl.argumentLengthCheck(arguments, 2, prefix);
name = webidl.converters.DOMString(name, prefix, 'name')
attributes = webidl.converters.DeleteCookieAttributes(attributes)
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
@ -69,36 +71,36 @@ function deleteCookie (headers, name, attributes) {
name,
value: '',
expires: new Date(0),
...attributes
})
...attributes,
});
}
/**
* @param {Headers} headers
* @returns {Cookie[]}
*/
function getSetCookies (headers) {
webidl.argumentLengthCheck(arguments, 1, 'getSetCookies')
function getSetCookies(headers) {
webidl.argumentLengthCheck(arguments, 1, 'getSetCookies');
brandChecks(headers)
brandChecks(headers);
const cookies = headers.getSetCookie()
const cookies = headers.getSetCookie();
if (!cookies) {
return []
return [];
}
return cookies.map((pair) => parseSetCookie(pair))
return cookies.map((pair) => parseSetCookie(pair));
}
/**
* Parses a cookie string
* @param {string} cookie
*/
function parseCookie (cookie) {
cookie = webidl.converters.DOMString(cookie)
function parseCookie(cookie) {
cookie = webidl.converters.DOMString(cookie);
return parseSetCookie(cookie)
return parseSetCookie(cookie);
}
/**
@ -106,17 +108,17 @@ function parseCookie (cookie) {
* @param {Cookie} cookie
* @returns {void}
*/
function setCookie (headers, cookie) {
webidl.argumentLengthCheck(arguments, 2, 'setCookie')
function setCookie(headers, cookie) {
webidl.argumentLengthCheck(arguments, 2, 'setCookie');
brandChecks(headers)
brandChecks(headers);
cookie = webidl.converters.Cookie(cookie)
cookie = webidl.converters.Cookie(cookie);
const str = stringify(cookie)
const str = stringify(cookie);
if (str) {
headers.append('set-cookie', str, true)
headers.append('set-cookie', str, true);
}
}
@ -124,76 +126,76 @@ webidl.converters.DeleteCookieAttributes = webidl.dictionaryConverter([
{
converter: webidl.nullableConverter(webidl.converters.DOMString),
key: 'path',
defaultValue: () => null
defaultValue: () => null,
},
{
converter: webidl.nullableConverter(webidl.converters.DOMString),
key: 'domain',
defaultValue: () => null
}
])
defaultValue: () => null,
},
]);
webidl.converters.Cookie = webidl.dictionaryConverter([
{
converter: webidl.converters.DOMString,
key: 'name'
key: 'name',
},
{
converter: webidl.converters.DOMString,
key: 'value'
key: 'value',
},
{
converter: webidl.nullableConverter((value) => {
if (typeof value === 'number') {
return webidl.converters['unsigned long long'](value)
return webidl.converters['unsigned long long'](value);
}
return new Date(value)
return new Date(value);
}),
key: 'expires',
defaultValue: () => null
defaultValue: () => null,
},
{
converter: webidl.nullableConverter(webidl.converters['long long']),
key: 'maxAge',
defaultValue: () => null
defaultValue: () => null,
},
{
converter: webidl.nullableConverter(webidl.converters.DOMString),
key: 'domain',
defaultValue: () => null
defaultValue: () => null,
},
{
converter: webidl.nullableConverter(webidl.converters.DOMString),
key: 'path',
defaultValue: () => null
defaultValue: () => null,
},
{
converter: webidl.nullableConverter(webidl.converters.boolean),
key: 'secure',
defaultValue: () => null
defaultValue: () => null,
},
{
converter: webidl.nullableConverter(webidl.converters.boolean),
key: 'httpOnly',
defaultValue: () => null
defaultValue: () => null,
},
{
converter: webidl.converters.USVString,
key: 'sameSite',
allowedValues: ['Strict', 'Lax', 'None']
allowedValues: ['Strict', 'Lax', 'None'],
},
{
converter: webidl.sequenceConverter(webidl.converters.DOMString),
key: 'unparsed',
defaultValue: () => new Array(0)
}
])
defaultValue: () => new Array(0),
},
]);
module.exports = {
getCookies,
deleteCookie,
getSetCookies,
setCookie,
parseCookie
}
parseCookie,
};

View File

@ -1,10 +1,10 @@
'use strict'
'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')
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.
@ -12,18 +12,18 @@ const { unescape } = require('node:querystring')
* @param {string} header
* @returns {import('./index').Cookie|null} if the header is invalid, null will be returned
*/
function parseSetCookie (header) {
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
return null;
}
let nameValuePair = ''
let unparsedAttributes = ''
let name = ''
let value = ''
let nameValuePair = '';
let unparsedAttributes = '';
let name = '';
let value = '';
// 2. If the set-cookie-string contains a %x3B (";") character:
if (header.includes(';')) {
@ -31,48 +31,44 @@ function parseSetCookie (header) {
// 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 }
const position = { position: 0 };
nameValuePair = collectASequenceOfCodePointsFast(';', header, position)
unparsedAttributes = header.slice(position.position)
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
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
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)
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()
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
return null;
}
// 6. The cookie-name is the name string, and the cookie-value is the
@ -82,8 +78,10 @@ function parseSetCookie (header) {
// store arbitrary data in a cookie-value SHOULD encode that data, for
// example, using Base64 [RFC4648].
return {
name, value: unescape(value), ...parseUnparsedAttributes(unparsedAttributes)
}
name,
value: unescape(value),
...parseUnparsedAttributes(unparsedAttributes),
};
}
/**
@ -92,43 +90,41 @@ function parseSetCookie (header) {
* @param {string} unparsedAttributes
* @param {Object.<string, unknown>} [cookieAttributeList={}]
*/
function parseUnparsedAttributes (unparsedAttributes, 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
return cookieAttributeList;
}
// 2. Discard the first character of the unparsed-attributes (which
// will be a %x3B (";") character).
assert(unparsedAttributes[0] === ';')
unparsedAttributes = unparsedAttributes.slice(1)
assert(unparsedAttributes[0] === ';');
unparsedAttributes = unparsedAttributes.slice(1);
let cookieAv = ''
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)
cookieAv = collectASequenceOfCodePointsFast(';', unparsedAttributes, {
position: 0,
});
unparsedAttributes = unparsedAttributes.slice(cookieAv.length);
} else {
// Otherwise:
// 1. Consume the remainder of the unparsed-attributes.
cookieAv = unparsedAttributes
unparsedAttributes = ''
cookieAv = unparsedAttributes;
unparsedAttributes = '';
}
// Let the cookie-av string be the characters consumed in this step.
let attributeName = ''
let attributeValue = ''
let attributeName = '';
let attributeValue = '';
// 4. If the cookie-av string contains a %x3D ("=") character:
if (cookieAv.includes('=')) {
@ -137,37 +133,33 @@ function parseUnparsedAttributes (unparsedAttributes, cookieAttributeList = {})
// character, and the (possibly empty) attribute-value string
// consists of the characters after the first %x3D ("=")
// character.
const position = { position: 0 }
const position = { position: 0 };
attributeName = collectASequenceOfCodePointsFast(
'=',
cookieAv,
position
)
attributeValue = cookieAv.slice(position.position + 1)
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
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()
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)
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()
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
@ -175,12 +167,12 @@ function parseUnparsedAttributes (unparsedAttributes, cookieAttributeList = {})
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)
const expiryTime = new Date(attributeValue);
// 2. If the attribute-value failed to parse as a cookie date, ignore
// the cookie-av.
cookieAttributeList.expires = expiryTime
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-
@ -188,20 +180,20 @@ function parseUnparsedAttributes (unparsedAttributes, cookieAttributeList = {})
// 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)
const charCode = attributeValue.charCodeAt(0);
if ((charCode < 48 || charCode > 57) && attributeValue[0] !== '-') {
return parseUnparsedAttributes(unparsedAttributes, cookieAttributeList)
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)
return parseUnparsedAttributes(unparsedAttributes, cookieAttributeList);
}
// 3. Let delta-seconds be the attribute-value converted to an integer.
const deltaSeconds = Number(attributeValue)
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).
@ -218,27 +210,27 @@ function parseUnparsedAttributes (unparsedAttributes, cookieAttributeList = {})
// 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
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
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)
cookieDomain = cookieDomain.slice(1);
}
// 3. Convert the cookie-domain to lower case.
cookieDomain = cookieDomain.toLowerCase()
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
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",
@ -246,27 +238,27 @@ function parseUnparsedAttributes (unparsedAttributes, cookieAttributeList = {})
// 1. If the attribute-value is empty or if the first character of the
// attribute-value is not %x2F ("/"):
let cookiePath = ''
let cookiePath = '';
if (attributeValue.length === 0 || attributeValue[0] !== '/') {
// 1. Let cookie-path be the default-path.
cookiePath = '/'
cookiePath = '/';
} else {
// Otherwise:
// 1. Let cookie-path be the attribute-value.
cookiePath = attributeValue
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
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
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
@ -274,49 +266,49 @@ function parseUnparsedAttributes (unparsedAttributes, cookieAttributeList = {})
// attribute-list with an attribute-name of HttpOnly and an empty
// attribute-value.
cookieAttributeList.httpOnly = true
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'
let enforcement = 'Default';
const attributeValueLowercase = attributeValue.toLowerCase()
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'
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'
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'
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
cookieAttributeList.sameSite = enforcement;
} else {
cookieAttributeList.unparsed ??= []
cookieAttributeList.unparsed ??= [];
cookieAttributeList.unparsed.push(`${attributeName}=${attributeValue}`)
cookieAttributeList.unparsed.push(`${attributeName}=${attributeValue}`);
}
// 8. Return to Step 1 of this algorithm.
return parseUnparsedAttributes(unparsedAttributes, cookieAttributeList)
return parseUnparsedAttributes(unparsedAttributes, cookieAttributeList);
}
module.exports = {
parseSetCookie,
parseUnparsedAttributes
}
parseUnparsedAttributes,
};

View File

@ -1,22 +1,22 @@
'use strict'
'use strict';
/**
* @param {string} value
* @returns {boolean}
*/
function isCTLExcludingHtab (value) {
function isCTLExcludingHtab(value) {
for (let i = 0; i < value.length; ++i) {
const code = value.charCodeAt(i)
const code = value.charCodeAt(i);
if (
(code >= 0x00 && code <= 0x08) ||
(code >= 0x0A && code <= 0x1F) ||
code === 0x7F
(code >= 0x0a && code <= 0x1f) ||
code === 0x7f
) {
return true
return true;
}
}
return false
return false;
}
/**
@ -28,32 +28,32 @@ function isCTLExcludingHtab (value) {
| "{" | "}" | SP | HT
* @param {string} name
*/
function validateCookieName (name) {
function validateCookieName(name) {
for (let i = 0; i < name.length; ++i) {
const code = name.charCodeAt(i)
const code = name.charCodeAt(i);
if (
code < 0x21 || // exclude CTLs (0-31), SP and HT
code > 0x7E || // exclude non-ascii and DEL
code > 0x7e || // exclude non-ascii and DEL
code === 0x22 || // "
code === 0x28 || // (
code === 0x29 || // )
code === 0x3C || // <
code === 0x3E || // >
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 // }
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')
throw new Error('Invalid cookie name');
}
}
}
@ -66,31 +66,31 @@ function validateCookieName (name) {
; and backslash
* @param {string} value
*/
function validateCookieValue (value) {
let len = value.length
let i = 0
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')
throw new Error('Invalid cookie value');
}
--len
++i
--len;
++i;
}
while (i < len) {
const code = value.charCodeAt(i++)
const code = value.charCodeAt(i++);
if (
code < 0x21 || // exclude CTLs (0-31)
code > 0x7E || // non-ascii and DEL (127)
code > 0x7e || // non-ascii and DEL (127)
code === 0x22 || // "
code === 0x2C || // ,
code === 0x3B || // ;
code === 0x5C // \
code === 0x2c || // ,
code === 0x3b || // ;
code === 0x5c // \
) {
throw new Error('Invalid cookie value')
throw new Error('Invalid cookie value');
}
}
}
@ -99,16 +99,16 @@ function validateCookieValue (value) {
* path-value = <any CHAR except CTLs or ";">
* @param {string} path
*/
function validateCookiePath (path) {
function validateCookiePath(path) {
for (let i = 0; i < path.length; ++i) {
const code = path.charCodeAt(i)
const code = path.charCodeAt(i);
if (
code < 0x20 || // exclude CTLs (0-31)
code === 0x7F || // DEL
code === 0x3B // ;
code === 0x7f || // DEL
code === 0x3b // ;
) {
throw new Error('Invalid cookie path')
throw new Error('Invalid cookie path');
}
}
}
@ -118,27 +118,32 @@ function validateCookiePath (path) {
* but Deno tests these. - Khafra
* @param {string} domain
*/
function validateCookieDomain (domain) {
if (
domain.startsWith('-') ||
domain.endsWith('.') ||
domain.endsWith('-')
) {
throw new Error('Invalid cookie 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 IMFDays = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
const IMFMonths = [
'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'
]
'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'))
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
@ -181,12 +186,12 @@ const IMFPaddedNumbers = Array(61).fill(0).map((_, i) => i.toString().padStart(2
minute = 2DIGIT
second = 2DIGIT
*/
function toIMFDate (date) {
function toIMFDate(date) {
if (typeof date === 'number') {
date = new Date(date)
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`
return `${IMFDays[date.getUTCDay()]}, ${IMFPaddedNumbers[date.getUTCDate()]} ${IMFMonths[date.getUTCMonth()]} ${date.getUTCFullYear()} ${IMFPaddedNumbers[date.getUTCHours()]}:${IMFPaddedNumbers[date.getUTCMinutes()]}:${IMFPaddedNumbers[date.getUTCSeconds()]} GMT`;
}
/**
@ -196,9 +201,9 @@ function toIMFDate (date) {
; user agent.
* @param {number} maxAge
*/
function validateCookieMaxAge (maxAge) {
function validateCookieMaxAge(maxAge) {
if (maxAge < 0) {
throw new Error('Invalid cookie max-age')
throw new Error('Invalid cookie max-age');
}
}
@ -206,70 +211,70 @@ function validateCookieMaxAge (maxAge) {
* @see https://www.rfc-editor.org/rfc/rfc6265#section-4.1.1
* @param {import('./index').Cookie} cookie
*/
function stringify (cookie) {
function stringify(cookie) {
if (cookie.name.length === 0) {
return null
return null;
}
validateCookieName(cookie.name)
validateCookieValue(cookie.value)
validateCookieName(cookie.name);
validateCookieValue(cookie.value);
const out = [`${cookie.name}=${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
cookie.secure = true;
}
if (cookie.name.startsWith('__Host-')) {
cookie.secure = true
cookie.domain = null
cookie.path = '/'
cookie.secure = true;
cookie.domain = null;
cookie.path = '/';
}
if (cookie.secure) {
out.push('Secure')
out.push('Secure');
}
if (cookie.httpOnly) {
out.push('HttpOnly')
out.push('HttpOnly');
}
if (typeof cookie.maxAge === 'number') {
validateCookieMaxAge(cookie.maxAge)
out.push(`Max-Age=${cookie.maxAge}`)
validateCookieMaxAge(cookie.maxAge);
out.push(`Max-Age=${cookie.maxAge}`);
}
if (cookie.domain) {
validateCookieDomain(cookie.domain)
out.push(`Domain=${cookie.domain}`)
validateCookieDomain(cookie.domain);
out.push(`Domain=${cookie.domain}`);
}
if (cookie.path) {
validateCookiePath(cookie.path)
out.push(`Path=${cookie.path}`)
validateCookiePath(cookie.path);
out.push(`Path=${cookie.path}`);
}
if (cookie.expires && cookie.expires.toString() !== 'Invalid Date') {
out.push(`Expires=${toIMFDate(cookie.expires)}`)
out.push(`Expires=${toIMFDate(cookie.expires)}`);
}
if (cookie.sameSite) {
out.push(`SameSite=${cookie.sameSite}`)
out.push(`SameSite=${cookie.sameSite}`);
}
for (const part of cookie.unparsed) {
if (!part.includes('=')) {
throw new Error('Invalid unparsed')
throw new Error('Invalid unparsed');
}
const [key, ...value] = part.split('=')
const [key, ...value] = part.split('=');
out.push(`${key.trim()}=${value.join('=')}`)
out.push(`${key.trim()}=${value.join('=')}`);
}
return out.join('; ')
return out.join('; ');
}
module.exports = {
@ -278,5 +283,5 @@ module.exports = {
validateCookiePath,
validateCookieValue,
toIMFDate,
stringify
}
stringify,
};

View File

@ -1,27 +1,27 @@
'use strict'
const { Transform } = require('node:stream')
const { isASCIINumber, isValidLastEventId } = require('./util')
'use strict';
const { Transform } = require('node:stream');
const { isASCIINumber, isValidLastEventId } = require('./util');
/**
* @type {number[]} BOM
*/
const BOM = [0xEF, 0xBB, 0xBF]
const BOM = [0xef, 0xbb, 0xbf];
/**
* @type {10} LF
*/
const LF = 0x0A
const LF = 0x0a;
/**
* @type {13} CR
*/
const CR = 0x0D
const CR = 0x0d;
/**
* @type {58} COLON
*/
const COLON = 0x3A
const COLON = 0x3a;
/**
* @type {32} SPACE
*/
const SPACE = 0x20
const SPACE = 0x20;
/**
* @typedef {object} EventSourceStreamEvent
@ -44,37 +44,37 @@ class EventSourceStream extends Transform {
/**
* @type {eventSourceSettings}
*/
state
state;
/**
* Leading byte-order-mark check.
* @type {boolean}
*/
checkBOM = true
checkBOM = true;
/**
* @type {boolean}
*/
crlfCheck = false
crlfCheck = false;
/**
* @type {boolean}
*/
eventEndCheck = false
eventEndCheck = false;
/**
* @type {Buffer|null}
*/
buffer = null
buffer = null;
pos = 0
pos = 0;
event = {
data: undefined,
event: undefined,
id: undefined,
retry: undefined
}
retry: undefined,
};
/**
* @param {object} options
@ -82,16 +82,16 @@ class EventSourceStream extends Transform {
* @param {eventSourceSettings} [options.eventSourceSettings]
* @param {(chunk: any, encoding?: BufferEncoding | undefined) => boolean} [options.push]
*/
constructor (options = {}) {
constructor(options = {}) {
// Enable object mode as EventSourceStream emits objects of shape
// EventSourceStreamEvent
options.readableObjectMode = true
options.readableObjectMode = true;
super(options)
super(options);
this.state = options.eventSourceSettings || {}
this.state = options.eventSourceSettings || {};
if (options.push) {
this.push = options.push
this.push = options.push;
}
}
@ -101,10 +101,10 @@ class EventSourceStream extends Transform {
* @param {Function} callback
* @returns {void}
*/
_transform (chunk, _encoding, callback) {
_transform(chunk, _encoding, callback) {
if (chunk.length === 0) {
callback()
return
callback();
return;
}
// Cache the chunk in the buffer, as the data might not be complete while
@ -113,9 +113,9 @@ class EventSourceStream extends Transform {
// incoming chunks
// see: https://github.com/nodejs/undici/issues/2630
if (this.buffer) {
this.buffer = Buffer.concat([this.buffer, chunk])
this.buffer = Buffer.concat([this.buffer, chunk]);
} else {
this.buffer = chunk
this.buffer = chunk;
}
// Strip leading byte-order-mark if we opened the stream and started
@ -126,33 +126,30 @@ class EventSourceStream extends Transform {
// 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
callback();
return;
}
// Set the checkBOM flag to false as we don't need to check for the
// BOM anymore
this.checkBOM = false
this.checkBOM = false;
// The buffer only contains one byte so we need to wait for more data
callback()
return
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 (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
callback();
return;
}
// Set the checkBOM flag to false as we don't need to check for the
// BOM anymore
this.checkBOM = false
break
this.checkBOM = false;
break;
case 3:
// Check if the first three bytes are the same as the first three
// bytes of the BOM
@ -162,18 +159,18 @@ class EventSourceStream extends Transform {
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)
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
this.checkBOM = false;
// Await more data
callback()
return
callback();
return;
}
// If it is not the BOM, we can start processing the data
this.checkBOM = false
break
this.checkBOM = false;
break;
default:
// The buffer is longer than 3 bytes, so we can drop the BOM if it is
// present
@ -183,12 +180,12 @@ class EventSourceStream extends Transform {
this.buffer[2] === BOM[2]
) {
// Remove the BOM from the buffer
this.buffer = this.buffer.subarray(3)
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
this.checkBOM = false;
break;
}
}
@ -206,9 +203,9 @@ class EventSourceStream extends Transform {
// 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
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
@ -219,9 +216,9 @@ class EventSourceStream extends Transform {
// 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
continue;
}
this.crlfCheck = false
this.crlfCheck = false;
}
if (this.buffer[this.pos] === LF || this.buffer[this.pos] === CR) {
@ -230,22 +227,26 @@ class EventSourceStream extends Transform {
// next character is a line feed so we can remove it from the
// buffer
if (this.buffer[this.pos] === CR) {
this.crlfCheck = true
this.crlfCheck = true;
}
this.buffer = this.buffer.subarray(this.pos + 1)
this.pos = 0
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.event.data !== undefined ||
this.event.event ||
this.event.id ||
this.event.retry
) {
this.processEvent(this.event);
}
this.clearEvent()
continue
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
this.eventEndCheck = false;
continue;
}
// If the current character is an end-of-line, we can process the
@ -255,51 +256,51 @@ class EventSourceStream extends Transform {
// 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
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)
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)
this.buffer = this.buffer.subarray(this.pos + 1);
// Reset the position as we removed the processed line from the buffer
this.pos = 0
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.eventEndCheck = true;
continue;
}
this.pos++
this.pos++;
}
callback()
callback();
}
/**
* @param {Buffer} line
* @param {EventSourceStreamEvent} event
*/
parseLine (line, 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
return;
}
// If the line starts with a U+003A COLON character (:)
// Ignore the line.
const colonPosition = line.indexOf(COLON)
const colonPosition = line.indexOf(COLON);
if (colonPosition === 0) {
return
return;
}
let field = ''
let value = ''
let field = '';
let value = '';
// If the line contains a U+003A COLON character (:)
if (colonPosition !== -1) {
@ -308,27 +309,27 @@ class EventSourceStream extends Transform {
// 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')
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
let valueStart = colonPosition + 1;
if (line[valueStart] === SPACE) {
++valueStart
++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')
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 = ''
field = line.toString('utf8');
value = '';
}
// Modify the event with the field name and value. The value is also
@ -336,39 +337,39 @@ class EventSourceStream extends Transform {
switch (field) {
case 'data':
if (event[field] === undefined) {
event[field] = value
event[field] = value;
} else {
event[field] += `\n${value}`
event[field] += `\n${value}`;
}
break
break;
case 'retry':
if (isASCIINumber(value)) {
event[field] = value
event[field] = value;
}
break
break;
case 'id':
if (isValidLastEventId(value)) {
event[field] = value
event[field] = value;
}
break
break;
case 'event':
if (value.length > 0) {
event[field] = value
event[field] = value;
}
break
break;
}
}
/**
* @param {EventSourceStreamEvent} event
*/
processEvent (event) {
processEvent(event) {
if (event.retry && isASCIINumber(event.retry)) {
this.state.reconnectionTime = parseInt(event.retry, 10)
this.state.reconnectionTime = parseInt(event.retry, 10);
}
if (event.id && isValidLastEventId(event.id)) {
this.state.lastEventId = event.id
this.state.lastEventId = event.id;
}
// only dispatch event, when data is provided
@ -378,22 +379,22 @@ class EventSourceStream extends Transform {
options: {
data: event.data,
lastEventId: this.state.lastEventId,
origin: this.state.origin
}
})
origin: this.state.origin,
},
});
}
}
clearEvent () {
clearEvent() {
this.event = {
data: undefined,
event: undefined,
id: undefined,
retry: undefined
}
retry: undefined,
};
}
}
module.exports = {
EventSourceStream
}
EventSourceStream,
};

View File

@ -1,18 +1,18 @@
'use strict'
'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')
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
let experimentalWarned = false;
/**
* A reconnection time, in milliseconds. This must initially be an implementation-defined value,
@ -24,7 +24,7 @@ let experimentalWarned = false
*
* @type {3000}
*/
const defaultReconnectionTime = 3000
const defaultReconnectionTime = 3000;
/**
* The readyState attribute represents the state of the connection.
@ -39,32 +39,32 @@ const defaultReconnectionTime = 3000
* agent is reconnecting.
* @type {0}
*/
const CONNECTING = 0
const CONNECTING = 0;
/**
* The user agent has an open connection and is dispatching events as it
* receives them.
* @type {1}
*/
const OPEN = 1
const OPEN = 1;
/**
* The connection is not open, and the user agent is not trying to reconnect.
* @type {2}
*/
const CLOSED = 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'
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'
const USE_CREDENTIALS = 'use-credentials';
/**
* The EventSource interface is used to receive server-sent events. It
@ -78,26 +78,26 @@ class EventSource extends EventTarget {
#events = {
open: null,
error: null,
message: null
}
message: null,
};
#url
#withCredentials = false
#url;
#withCredentials = false;
/**
* @type {ReadyState}
*/
#readyState = CONNECTING
#readyState = CONNECTING;
#request = null
#controller = null
#request = null;
#controller = null;
#dispatcher
#dispatcher;
/**
* @type {import('./eventsource-stream').eventSourceSettings}
*/
#state
#state;
/**
* Creates a new EventSource object.
@ -105,58 +105,65 @@ class EventSource extends EventTarget {
* @param {EventSourceInit} [eventSourceInitDict={}]
* @see https://html.spec.whatwg.org/multipage/server-sent-events.html#the-eventsource-interface
*/
constructor (url, eventSourceInitDict = {}) {
constructor(url, eventSourceInitDict = {}) {
// 1. Let ev be a new EventSource object.
super()
super();
webidl.util.markAsUncloneable(this)
webidl.util.markAsUncloneable(this);
const prefix = 'EventSource constructor'
webidl.argumentLengthCheck(arguments, 1, prefix)
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'
})
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')
url = webidl.converters.USVString(url);
eventSourceInitDict = webidl.converters.EventSourceInitDict(
eventSourceInitDict,
prefix,
'eventSourceInitDict'
);
this.#dispatcher = eventSourceInitDict.dispatcher
this.#dispatcher = eventSourceInitDict.dispatcher;
this.#state = {
lastEventId: '',
reconnectionTime: defaultReconnectionTime
}
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
const settings = environmentSettingsObject;
let urlRecord
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
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')
throw new DOMException(e, 'SyntaxError');
}
// 5. Set ev's url to urlRecord.
this.#url = urlRecord.href
this.#url = urlRecord.href;
// 6. Let corsAttributeState be Anonymous.
let corsAttributeState = 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
corsAttributeState = USE_CREDENTIALS;
this.#withCredentials = true;
}
// 8. Let request be the result of creating a potential-CORS request given
@ -166,30 +173,30 @@ class EventSource extends EventTarget {
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'
}
credentials: corsAttributeState === 'anonymous' ? 'same-origin' : 'omit',
referrer: 'no-referrer',
};
// 9. Set request's client to settings.
initRequest.client = environmentSettingsObject.settingsObject
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' }]]
initRequest.headersList = [
['accept', { name: 'accept', value: 'text/event-stream' }],
];
// 11. Set request's cache mode to "no-store".
initRequest.cache = 'no-store'
initRequest.cache = 'no-store';
// 12. Set request's initiator type to "other".
initRequest.initiator = 'other'
initRequest.initiator = 'other';
initRequest.urlList = [new URL(this.#url)]
initRequest.urlList = [new URL(this.#url)];
// 13. Set ev's request to request.
this.#request = makeRequest(initRequest)
this.#request = makeRequest(initRequest);
this.#connect()
this.#connect();
}
/**
@ -198,8 +205,8 @@ class EventSource extends EventTarget {
* @returns {ReadyState}
* @readonly
*/
get readyState () {
return this.#readyState
get readyState() {
return this.#readyState;
}
/**
@ -207,40 +214,40 @@ class EventSource extends EventTarget {
* @readonly
* @returns {string}
*/
get url () {
return this.#url
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
get withCredentials() {
return this.#withCredentials;
}
#connect () {
if (this.#readyState === CLOSED) return
#connect() {
if (this.#readyState === CLOSED) return;
this.#readyState = CONNECTING
this.#readyState = CONNECTING;
const fetchParams = {
request: this.#request,
dispatcher: this.#dispatcher
}
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.dispatchEvent(new Event('error'));
this.close();
}
this.#reconnect()
}
this.#reconnect();
};
// 15. Fetch request, with processResponseEndOfBody set to processEventSourceEndOfBody...
fetchParams.processResponseEndOfBody = processEventSourceEndOfBody
fetchParams.processResponseEndOfBody = processEventSourceEndOfBody;
// and processResponse set to the following steps given response res:
fetchParams.processResponse = (response) => {
@ -254,30 +261,29 @@ class EventSource extends EventTarget {
// user agent has failed the connection, it does not attempt to
// reconnect.
if (response.aborted) {
this.close()
this.dispatchEvent(new Event('error'))
return
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
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
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
@ -288,42 +294,35 @@ class EventSource extends EventTarget {
// 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'))
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
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
))
this.dispatchEvent(createFastMessageEvent(event.type, event.options));
},
});
pipeline(response.body.stream, eventSourceStream, (error) => {
if (error?.aborted === false) {
this.close();
this.dispatchEvent(new Event('error'));
}
})
});
};
pipeline(response.body.stream,
eventSourceStream,
(error) => {
if (
error?.aborted === false
) {
this.close()
this.dispatchEvent(new Event('error'))
}
})
}
this.#controller = fetching(fetchParams)
this.#controller = fetching(fetchParams);
}
/**
* @see https://html.spec.whatwg.org/multipage/server-sent-events.html#sse-processing-model
* @returns {Promise<void>}
*/
async #reconnect () {
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
@ -332,22 +331,22 @@ class EventSource extends EventTarget {
// 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
if (this.#readyState === CLOSED) return;
// 2. Set the readyState attribute to CONNECTING.
this.#readyState = CONNECTING
this.#readyState = CONNECTING;
// 3. Fire an event named error at the EventSource object.
this.dispatchEvent(new Event('error'))
this.dispatchEvent(new Event('error'));
// 2. Wait a delay equal to the reconnection time of the event source.
await delay(this.#state.reconnectionTime)
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
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
@ -357,74 +356,78 @@ class EventSource extends EventTarget {
// 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)
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()
this.#connect();
}
/**
* Closes the connection, if any, and sets the readyState attribute to
* CLOSED.
*/
close () {
webidl.brandCheck(this, EventSource)
close() {
webidl.brandCheck(this, EventSource);
if (this.#readyState === CLOSED) return
this.#readyState = CLOSED
this.#controller.abort()
this.#request = null
if (this.#readyState === CLOSED) return;
this.#readyState = CLOSED;
this.#controller.abort();
this.#request = null;
}
get onopen () {
return this.#events.open
get onopen() {
return this.#events.open;
}
set onopen (fn) {
set onopen(fn) {
if (this.#events.open) {
this.removeEventListener('open', this.#events.open)
this.removeEventListener('open', this.#events.open);
}
if (typeof fn === 'function') {
this.#events.open = fn
this.addEventListener('open', fn)
this.#events.open = fn;
this.addEventListener('open', fn);
} else {
this.#events.open = null
this.#events.open = null;
}
}
get onmessage () {
return this.#events.message
get onmessage() {
return this.#events.message;
}
set onmessage (fn) {
set onmessage(fn) {
if (this.#events.message) {
this.removeEventListener('message', this.#events.message)
this.removeEventListener('message', this.#events.message);
}
if (typeof fn === 'function') {
this.#events.message = fn
this.addEventListener('message', fn)
this.#events.message = fn;
this.addEventListener('message', fn);
} else {
this.#events.message = null
this.#events.message = null;
}
}
get onerror () {
return this.#events.error
get onerror() {
return this.#events.error;
}
set onerror (fn) {
set onerror(fn) {
if (this.#events.error) {
this.removeEventListener('error', this.#events.error)
this.removeEventListener('error', this.#events.error);
}
if (typeof fn === 'function') {
this.#events.error = fn
this.addEventListener('error', fn)
this.#events.error = fn;
this.addEventListener('error', fn);
} else {
this.#events.error = null
this.#events.error = null;
}
}
}
@ -435,26 +438,26 @@ const constantsPropertyDescriptors = {
configurable: false,
enumerable: true,
value: CONNECTING,
writable: false
writable: false,
},
OPEN: {
__proto__: null,
configurable: false,
enumerable: true,
value: OPEN,
writable: false
writable: false,
},
CLOSED: {
__proto__: null,
configurable: false,
enumerable: true,
value: CLOSED,
writable: false
}
}
writable: false,
},
};
Object.defineProperties(EventSource, constantsPropertyDescriptors)
Object.defineProperties(EventSource.prototype, constantsPropertyDescriptors)
Object.defineProperties(EventSource, constantsPropertyDescriptors);
Object.defineProperties(EventSource.prototype, constantsPropertyDescriptors);
Object.defineProperties(EventSource.prototype, {
close: kEnumerableProperty,
@ -463,22 +466,22 @@ Object.defineProperties(EventSource.prototype, {
onopen: kEnumerableProperty,
readyState: kEnumerableProperty,
url: kEnumerableProperty,
withCredentials: kEnumerableProperty
})
withCredentials: kEnumerableProperty,
});
webidl.converters.EventSourceInitDict = webidl.dictionaryConverter([
{
key: 'withCredentials',
converter: webidl.converters.boolean,
defaultValue: () => false
defaultValue: () => false,
},
{
key: 'dispatcher', // undici only
converter: webidl.converters.any
}
])
converter: webidl.converters.any,
},
]);
module.exports = {
EventSource,
defaultReconnectionTime
}
defaultReconnectionTime,
};

View File

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

View File

@ -1,99 +1,106 @@
'use strict'
'use strict';
const util = require('../../core/util')
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
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)
const crypto = require('node:crypto');
random = (max) => crypto.randomInt(0, max);
} catch {
random = (max) => Math.floor(Math.random() * max)
random = (max) => Math.floor(Math.random() * max);
}
const textEncoder = new TextEncoder()
function noop () {}
const textEncoder = new TextEncoder();
function noop() {}
const hasFinalizationRegistry = globalThis.FinalizationRegistry && process.version.indexOf('v18') !== 0
let streamRegistry
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)
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) {
function extractBody(object, keepalive = false) {
// 1. Let stream be null.
let stream = null
let stream = null;
// 2. If object is a ReadableStream object, then set stream to object.
if (webidl.is.ReadableStream(object)) {
stream = 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()
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
async pull(controller) {
const buffer =
typeof source === 'string' ? textEncoder.encode(source) : source;
if (buffer.byteLength) {
controller.enqueue(buffer)
controller.enqueue(buffer);
}
queueMicrotask(() => readableStreamClose(controller))
queueMicrotask(() => readableStreamClose(controller));
},
start () {},
type: 'bytes'
})
start() {},
type: 'bytes',
});
}
// 5. Assert: stream is a ReadableStream object.
assert(webidl.is.ReadableStream(stream))
assert(webidl.is.ReadableStream(stream));
// 6. Let action be null.
let action = null
let action = null;
// 7. Let source be null.
let source = null
let source = null;
// 8. Let length be null.
let length = null
let length = null;
// 9. Let type be null.
let type = 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
source = object;
// Set type to `text/plain;charset=UTF-8`.
type = 'text/plain;charset=UTF-8'
type = 'text/plain;charset=UTF-8';
} else if (webidl.is.URLSearchParams(object)) {
// URLSearchParams
@ -103,28 +110,33 @@ function extractBody (object, keepalive = false) {
// 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()
source = object.toString();
// Set type to `application/x-www-form-urlencoded;charset=UTF-8`.
type = '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())
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))
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`
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')
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.
@ -132,29 +144,32 @@ function extractBody (object, keepalive = false) {
// - 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
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
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)
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
length += chunk.byteLength + value.size + rn.byteLength;
} else {
hasUnknownSizeValue = true
hasUnknownSizeValue = true;
}
}
}
@ -162,113 +177,113 @@ function extractBody (object, keepalive = false) {
// 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
const chunk = textEncoder.encode(`--${boundary}--\r\n`);
blobParts.push(chunk);
length += chunk.byteLength;
if (hasUnknownSizeValue) {
length = null
length = null;
}
// Set source to object.
source = object
source = object;
action = async function * () {
action = async function* () {
for (const part of blobParts) {
if (part.stream) {
yield * part.stream()
yield* part.stream();
} else {
yield part
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}`
type = `multipart/form-data; boundary=${boundary}`;
} else if (webidl.is.Blob(object)) {
// Blob
// Set source to object.
source = object
source = object;
// Set length to objects size.
length = object.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
type = object.type;
}
} else if (typeof object[Symbol.asyncIterator] === 'function') {
// If keepalive is true, then throw a TypeError.
if (keepalive) {
throw new TypeError('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)
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)
length = Buffer.byteLength(source);
}
// 12. If action is non-null, then run these steps in in parallel:
if (action != null) {
// Run action.
let iterator
let iterator;
stream = new ReadableStream({
async start () {
iterator = action(object)[Symbol.asyncIterator]()
async start() {
iterator = action(object)[Symbol.asyncIterator]();
},
async pull (controller) {
const { value, done } = await iterator.next()
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)
})
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)
const buffer = new Uint8Array(value);
if (buffer.byteLength) {
controller.enqueue(buffer)
controller.enqueue(buffer);
}
}
}
return controller.desiredSize > 0
return controller.desiredSize > 0;
},
async cancel (reason) {
await iterator.return()
async cancel(reason) {
await iterator.return();
},
type: 'bytes'
})
type: 'bytes',
});
}
// 13. Let body be a body whose stream is stream, source is source,
// and length is length.
const body = { stream, source, length }
const body = { stream, source, length };
// 14. Return (body, type).
return [body, type]
return [body, type];
}
// https://fetch.spec.whatwg.org/#bodyinit-safely-extract
function safelyExtractBody (object, keepalive = false) {
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:
@ -276,152 +291,175 @@ function safelyExtractBody (object, keepalive = false) {
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.')
assert(!util.isDisturbed(object), 'The body has already been consumed.');
// istanbul ignore next
assert(!object.locked, 'The stream is locked.')
assert(!object.locked, 'The stream is locked.');
}
// 2. Return the results of extracting object.
return extractBody(object, keepalive)
return extractBody(object, keepalive);
}
function cloneBody (instance, body) {
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()
const [out1, out2] = body.stream.tee();
if (hasFinalizationRegistry) {
streamRegistry.register(instance, new WeakRef(out1))
streamRegistry.register(instance, new WeakRef(out1));
}
// 2. Set bodys stream to out1.
body.stream = 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
}
source: body.source,
};
}
function throwIfAborted (state) {
function throwIfAborted(state) {
if (state.aborted) {
throw new DOMException('The operation was aborted.', 'AbortError')
throw new DOMException('The operation was aborted.', 'AbortError');
}
}
function bodyMixinMethods (instance, getInternalState) {
function bodyMixinMethods(instance, getInternalState) {
const methods = {
blob () {
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))
return consumeBody(
this,
(bytes) => {
let mimeType = bodyMimeType(getInternalState(this));
if (mimeType === null) {
mimeType = ''
} else if (mimeType) {
mimeType = serializeAMimeType(mimeType)
}
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)
// Return a Blob whose contents are bytes and type attribute
// is mimeType.
return new Blob([bytes], { type: mimeType });
},
instance,
getInternalState
);
},
arrayBuffer () {
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)
return consumeBody(
this,
(bytes) => {
return new Uint8Array(bytes).buffer;
},
instance,
getInternalState
);
},
text () {
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)
return consumeBody(this, utf8DecodeBytes, instance, getInternalState);
},
json () {
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)
return consumeBody(this, parseJSONFromBytes, instance, getInternalState);
},
formData () {
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))
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)
// 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)
// 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;
}
case 'application/x-www-form-urlencoded': {
// 1. Let entries be the result of parsing bytes.
const entries = new URLSearchParams(value.toString());
return fd
// 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)
// 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 () {
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 consumeBody(
this,
(bytes) => {
return new Uint8Array(bytes);
},
instance,
getInternalState
);
},
};
return methods
return methods;
}
function mixinBody (prototype, getInternalState) {
Object.assign(prototype.prototype, bodyMixinMethods(prototype, getInternalState))
function mixinBody(prototype, getInternalState) {
Object.assign(
prototype.prototype,
bodyMixinMethods(prototype, getInternalState)
);
}
/**
@ -431,24 +469,29 @@ function mixinBody (prototype, getInternalState) {
* @param {any} instance
* @param {(target: any) => any} getInternalState
*/
async function consumeBody (object, convertBytesToJSValue, instance, getInternalState) {
webidl.brandCheck(object, instance)
async function consumeBody(
object,
convertBytesToJSValue,
instance,
getInternalState
) {
webidl.brandCheck(object, instance);
const state = getInternalState(object)
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')
throw new TypeError('Body is unusable: Body has already been read');
}
throwIfAborted(state)
throwIfAborted(state);
// 2. Let promise be a new promise.
const promise = createDeferredPromise()
const promise = createDeferredPromise();
// 3. Let errorSteps given error be to reject promise with error.
const errorSteps = (error) => promise.reject(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
@ -456,69 +499,69 @@ async function consumeBody (object, convertBytesToJSValue, instance, getInternal
// with that exception.
const successSteps = (data) => {
try {
promise.resolve(convertBytesToJSValue(data))
promise.resolve(convertBytesToJSValue(data));
} catch (e) {
errorSteps(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
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)
fullyReadBody(state.body, successSteps, errorSteps);
// 7. Return promise.
return promise.promise
return promise.promise;
}
/**
* @see https://fetch.spec.whatwg.org/#body-unusable
* @param {any} object internal state
*/
function bodyUnusable (object) {
const body = object.body
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))
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))
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) {
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
const headers = requestOrResponse.headersList;
// 4. Let mimeType be the result of extracting a MIME type from headers.
const mimeType = extractMimeType(headers)
const mimeType = extractMimeType(headers);
// 5. If mimeType is failure, then return null.
if (mimeType === 'failure') {
return null
return null;
}
// 6. Return mimeType.
return mimeType
return mimeType;
}
module.exports = {
@ -528,5 +571,5 @@ module.exports = {
mixinBody,
streamRegistry,
hasFinalizationRegistry,
bodyUnusable
}
bodyUnusable,
};

View File

@ -1,25 +1,101 @@
'use strict'
'use strict';
const corsSafeListedMethods = /** @type {const} */ (['GET', 'HEAD', 'POST'])
const corsSafeListedMethodsSet = new Set(corsSafeListedMethods)
const corsSafeListedMethods = /** @type {const} */ (['GET', 'HEAD', 'POST']);
const corsSafeListedMethodsSet = new Set(corsSafeListedMethods);
const nullBodyStatus = /** @type {const} */ ([101, 204, 205, 304])
const nullBodyStatus = /** @type {const} */ ([101, 204, 205, 304]);
const redirectStatus = /** @type {const} */ ([301, 302, 303, 307, 308])
const redirectStatusSet = new Set(redirectStatus)
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)
'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
@ -32,26 +108,32 @@ const referrerPolicyTokens = /** @type {const} */ ([
'strict-origin',
'origin-when-cross-origin',
'strict-origin-when-cross-origin',
'unsafe-url'
])
'unsafe-url',
]);
/**
* @see https://w3c.github.io/webappsec-referrer-policy/#referrer-policies
*/
const referrerPolicy = /** @type {const} */ ([
'',
...referrerPolicyTokens
])
const referrerPolicyTokensSet = new Set(referrerPolicyTokens)
const referrerPolicy = /** @type {const} */ (['', ...referrerPolicyTokens]);
const referrerPolicyTokensSet = new Set(referrerPolicyTokens);
const requestRedirect = /** @type {const} */ (['follow', 'manual', 'error'])
const requestRedirect = /** @type {const} */ (['follow', 'manual', 'error']);
const safeMethods = /** @type {const} */ (['GET', 'HEAD', 'OPTIONS', 'TRACE'])
const safeMethodsSet = new Set(safeMethods)
const safeMethods = /** @type {const} */ (['GET', 'HEAD', 'OPTIONS', 'TRACE']);
const safeMethodsSet = new Set(safeMethods);
const requestMode = /** @type {const} */ (['navigate', 'same-origin', 'no-cors', 'cors'])
const requestMode = /** @type {const} */ ([
'navigate',
'same-origin',
'no-cors',
'cors',
]);
const requestCredentials = /** @type {const} */ (['omit', 'same-origin', 'include'])
const requestCredentials = /** @type {const} */ ([
'omit',
'same-origin',
'include',
]);
const requestCache = /** @type {const} */ ([
'default',
@ -59,8 +141,8 @@ const requestCache = /** @type {const} */ ([
'reload',
'no-cache',
'force-cache',
'only-if-cached'
])
'only-if-cached',
]);
/**
* @see https://fetch.spec.whatwg.org/#request-body-header-name
@ -74,21 +156,19 @@ const requestBodyHeader = /** @type {const} */ ([
// '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'
])
'content-length',
]);
/**
* @see https://fetch.spec.whatwg.org/#enumdef-requestduplex
*/
const requestDuplex = /** @type {const} */ ([
'half'
])
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 forbiddenMethods = /** @type {const} */ (['CONNECT', 'TRACE', 'TRACK']);
const forbiddenMethodsSet = new Set(forbiddenMethods);
const subresource = /** @type {const} */ ([
'audio',
@ -102,9 +182,9 @@ const subresource = /** @type {const} */ ([
'track',
'video',
'xslt',
''
])
const subresourceSet = new Set(subresource)
'',
]);
const subresourceSet = new Set(subresource);
module.exports = {
subresource,
@ -127,5 +207,5 @@ module.exports = {
corsSafeListedMethodsSet,
safeMethodsSet,
forbiddenMethodsSet,
referrerPolicyTokens: referrerPolicyTokensSet
}
referrerPolicyTokens: referrerPolicyTokensSet,
};

View File

@ -1,45 +1,41 @@
'use strict'
'use strict';
const assert = require('node:assert')
const assert = require('node:assert');
const encoder = new TextEncoder()
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
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
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) {
function dataURLProcessor(dataURL) {
// 1. Assert: dataURLs scheme is "data".
assert(dataURL.protocol === '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)
let input = URLSerializer(dataURL, true);
// 3. Remove the leading "data:" string from input.
input = input.slice(5)
input = input.slice(5);
// 4. Let position point at the start of input.
const position = { position: 0 }
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
)
let mimeType = collectASequenceOfCodePointsFast(',', input, position);
// 6. Strip leading and trailing ASCII whitespace
// from mimeType.
@ -47,71 +43,71 @@ function dataURLProcessor (dataURL) {
// 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)
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'
return 'failure';
}
// 8. Advance position by 1.
position.position++
position.position++;
// 9. Let encodedBody be the remainder of input.
const encodedBody = input.slice(mimeTypeLength + 1)
const encodedBody = input.slice(mimeTypeLength + 1);
// 10. Let body be the percent-decoding of encodedBody.
let body = stringPercentDecode(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)
const stringBody = isomorphicDecode(body);
// 2. Set body to the forgiving-base64 decode of
// stringBody.
body = forgivingBase64(stringBody)
body = forgivingBase64(stringBody);
// 3. If body is failure, then return failure.
if (body === 'failure') {
return 'failure'
return 'failure';
}
// 4. Remove the last 6 code points from mimeType.
mimeType = mimeType.slice(0, -6)
mimeType = mimeType.slice(0, -6);
// 5. Remove trailing U+0020 SPACE code points from mimeType,
// if any.
mimeType = mimeType.replace(/(\u0020)+$/, '')
mimeType = mimeType.replace(/(\u0020)+$/, '');
// 6. Remove the last U+003B (;) code point from mimeType.
mimeType = mimeType.slice(0, -1)
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
mimeType = 'text/plain' + mimeType;
}
// 13. Let mimeTypeRecord be the result of parsing
// mimeType.
let mimeTypeRecord = parseMIMEType(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')
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 }
return { mimeType: mimeTypeRecord, body };
}
// https://url.spec.whatwg.org/#concept-url-serializer
@ -119,21 +115,22 @@ function dataURLProcessor (dataURL) {
* @param {URL} url
* @param {boolean} excludeFragment
*/
function URLSerializer (url, excludeFragment = false) {
function URLSerializer(url, excludeFragment = false) {
if (!excludeFragment) {
return url.href
return url.href;
}
const href = url.href
const hashLength = url.hash.length
const href = url.href;
const hashLength = url.hash.length;
const serialized = hashLength === 0 ? href : href.substring(0, href.length - hashLength)
const serialized =
hashLength === 0 ? href : href.substring(0, href.length - hashLength);
if (!hashLength && href.endsWith('#')) {
return serialized.slice(0, -1)
return serialized.slice(0, -1);
}
return serialized
return serialized;
}
// https://infra.spec.whatwg.org/#collect-a-sequence-of-code-points
@ -142,22 +139,25 @@ function URLSerializer (url, excludeFragment = false) {
* @param {string} input
* @param {{ position: number }} position
*/
function collectASequenceOfCodePoints (condition, input, position) {
function collectASequenceOfCodePoints(condition, input, position) {
// 1. Let result be the empty string.
let result = ''
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])) {
while (
position.position < input.length &&
condition(input[position.position])
) {
// 1. Append that code point to the end of result.
result += input[position.position]
result += input[position.position];
// 2. Advance position by 1.
position.position++
position.position++;
}
// 3. Return result.
return result
return result;
}
/**
@ -166,150 +166,147 @@ function collectASequenceOfCodePoints (condition, input, position) {
* @param {string} input
* @param {{ position: number }} position
*/
function collectASequenceOfCodePointsFast (char, input, position) {
const idx = input.indexOf(char, position.position)
const start = position.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 = input.length;
return input.slice(start);
}
position.position = idx
return input.slice(start, position.position)
position.position = idx;
return input.slice(start, position.position);
}
// https://url.spec.whatwg.org/#string-percent-decode
/** @param {string} input */
function stringPercentDecode (input) {
function stringPercentDecode(input) {
// 1. Let bytes be the UTF-8 encoding of input.
const bytes = encoder.encode(input)
const bytes = encoder.encode(input);
// 2. Return the percent-decoding of bytes.
return percentDecode(bytes)
return percentDecode(bytes);
}
/**
* @param {number} byte
*/
function isHexCharByte (byte) {
function isHexCharByte(byte) {
// 0-9 A-F a-f
return (byte >= 0x30 && byte <= 0x39) || (byte >= 0x41 && byte <= 0x46) || (byte >= 0x61 && byte <= 0x66)
return (
(byte >= 0x30 && byte <= 0x39) ||
(byte >= 0x41 && byte <= 0x46) ||
(byte >= 0x61 && byte <= 0x66)
);
}
/**
* @param {number} byte
*/
function hexByteToNumber (byte) {
function hexByteToNumber(byte) {
return (
// 0-9
byte >= 0x30 && byte <= 0x39
? (byte - 48)
// Convert to uppercase
// ((byte & 0xDF) - 65) + 10
: ((byte & 0xDF) - 55)
)
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
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
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]
const byte = input[i];
// 1. If byte is not 0x25 (%), then append byte to output.
if (byte !== 0x25) {
output[j++] = byte
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.
// 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
output[j++] = 0x25;
// 3. Otherwise:
// 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])
output[j++] =
(hexByteToNumber(input[i + 1]) << 4) | hexByteToNumber(input[i + 2]);
// 3. Skip the next two bytes in input.
i += 2
i += 2;
}
}
// 3. Return output.
return length === j ? output : output.subarray(0, j)
return length === j ? output : output.subarray(0, j);
}
// https://mimesniff.spec.whatwg.org/#parse-a-mime-type
/** @param {string} input */
function parseMIMEType (input) {
function parseMIMEType(input) {
// 1. Remove any leading and trailing HTTP whitespace
// from input.
input = removeHTTPWhitespace(input, true, true)
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 }
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
)
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'
return 'failure';
}
// 5. If position is past the end of input, then return
// failure
if (position.position >= input.length) {
return 'failure'
return 'failure';
}
// 6. Advance position by 1. (This skips past U+002F (/).)
position.position++
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
)
let subtype = collectASequenceOfCodePointsFast(';', input, position);
// 8. Remove any trailing HTTP whitespace from subtype.
subtype = removeHTTPWhitespace(subtype, false, true)
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'
return 'failure';
}
const typeLowercase = type.toLowerCase()
const subtypeLowercase = subtype.toLowerCase()
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,
@ -321,22 +318,22 @@ function parseMIMEType (input) {
/** @type {Map<string, string>} */
parameters: new Map(),
// https://mimesniff.spec.whatwg.org/#mime-type-essence
essence: `${typeLowercase}/${subtypeLowercase}`
}
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++
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),
(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 (;)
@ -345,31 +342,31 @@ function parseMIMEType (input) {
(char) => char !== ';' && char !== '=',
input,
position
)
);
// 4. Set parameterName to parameterName, in ASCII
// lowercase.
parameterName = parameterName.toLowerCase()
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
continue;
}
// 2. Advance position by 1. (This skips past U+003D (=).)
position.position++
position.position++;
}
// 6. If position is past the end of input, then break.
if (position.position >= input.length) {
break
break;
}
// 7. Let parameterValue be null.
let parameterValue = null
let parameterValue = null;
// 8. If the code point at position within input is
// U+0022 ("), then:
@ -377,33 +374,25 @@ function parseMIMEType (input) {
// 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)
parameterValue = collectAnHTTPQuotedString(input, position, true);
// 2. Collect a sequence of code points that are not
// U+003B (;) from input, given position.
collectASequenceOfCodePointsFast(
';',
input,
position
)
collectASequenceOfCodePointsFast(';', input, position);
// 9. Otherwise:
// 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
)
parameterValue = collectASequenceOfCodePointsFast(';', input, position);
// 2. Remove any trailing HTTP whitespace from parameterValue.
parameterValue = removeHTTPWhitespace(parameterValue, false, true)
parameterValue = removeHTTPWhitespace(parameterValue, false, true);
// 3. If parameterValue is the empty string, then continue.
if (parameterValue.length === 0) {
continue
continue;
}
}
@ -416,33 +405,34 @@ function parseMIMEType (input) {
if (
parameterName.length !== 0 &&
HTTP_TOKEN_CODEPOINTS.test(parameterName) &&
(parameterValue.length === 0 || HTTP_QUOTED_STRING_TOKENS.test(parameterValue)) &&
(parameterValue.length === 0 ||
HTTP_QUOTED_STRING_TOKENS.test(parameterValue)) &&
!mimeType.parameters.has(parameterName)
) {
mimeType.parameters.set(parameterName, parameterValue)
mimeType.parameters.set(parameterName, parameterValue);
}
}
// 12. Return mimeType.
return mimeType
return mimeType;
}
// https://infra.spec.whatwg.org/#forgiving-base64-decode
/** @param {string} data */
function forgivingBase64 (data) {
function forgivingBase64(data) {
// 1. Remove all ASCII whitespace from data.
data = data.replace(ASCII_WHITESPACE_REPLACE_REGEX, '')
data = data.replace(ASCII_WHITESPACE_REPLACE_REGEX, '');
let dataLength = data.length
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
if (data.charCodeAt(dataLength - 1) === 0x003d) {
--dataLength;
if (data.charCodeAt(dataLength - 1) === 0x003d) {
--dataLength;
}
}
}
@ -450,7 +440,7 @@ function forgivingBase64 (data) {
// 3. If datas code point length divides by 4 leaving
// a remainder of 1, then return failure.
if (dataLength % 4 === 1) {
return 'failure'
return 'failure';
}
// 4. If data contains a code point that is not one of
@ -458,12 +448,16 @@ function forgivingBase64 (data) {
// U+002F (/)
// ASCII alphanumeric
// then return failure.
if (/[^+/0-9A-Za-z]/.test(data.length === dataLength ? data : data.substring(0, dataLength))) {
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)
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
@ -473,19 +467,19 @@ function forgivingBase64 (data) {
* @param {{ position: number }} position
* @param {boolean} [extractValue=false]
*/
function collectAnHTTPQuotedString (input, position, extractValue = false) {
function collectAnHTTPQuotedString(input, position, extractValue = false) {
// 1. Let positionStart be position.
const positionStart = position.position
const positionStart = position.position;
// 2. Let value be the empty string.
let value = ''
let value = '';
// 3. Assert: the code point at position within input
// is U+0022 (").
assert(input[position.position] === '"')
assert(input[position.position] === '"');
// 4. Advance position by 1.
position.position++
position.position++;
// 5. While true:
while (true) {
@ -496,106 +490,106 @@ function collectAnHTTPQuotedString (input, position, extractValue = false) {
(char) => char !== '"' && char !== '\\',
input,
position
)
);
// 2. If position is past the end of input, then break.
if (position.position >= input.length) {
break
break;
}
// 3. Let quoteOrBackslash be the code point at position within
// input.
const quoteOrBackslash = input[position.position]
const quoteOrBackslash = input[position.position];
// 4. Advance position by 1.
position.position++
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
value += '\\';
break;
}
// 2. Append the code point at position within input to value.
value += input[position.position]
value += input[position.position];
// 3. Advance position by 1.
position.position++
position.position++;
// 6. Otherwise:
// 6. Otherwise:
} else {
// 1. Assert: quoteOrBackslash is U+0022 (").
assert(quoteOrBackslash === '"')
assert(quoteOrBackslash === '"');
// 2. Break.
break
break;
}
}
// 6. If the extract-value flag is set, then return value.
if (extractValue) {
return value
return value;
}
// 7. Return the code points from positionStart to position,
// inclusive, within input.
return input.slice(positionStart, position.position)
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
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
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 += ';'
serialization += ';';
// 2. Append name to serialization.
serialization += name
serialization += name;
// 3. Append U+003D (=) to serialization.
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')
value = value.replace(/(\\|")/g, '\\$1');
// 2. Prepend U+0022 (") to value.
value = '"' + value
value = '"' + value;
// 3. Append U+0022 (") to value.
value += '"'
value += '"';
}
// 5. Append value to serialization.
serialization += value
serialization += value;
}
// 3. Return serialization.
return serialization
return serialization;
}
/**
* @see https://fetch.spec.whatwg.org/#http-whitespace
* @param {number} char
*/
function isHTTPWhiteSpace (char) {
function isHTTPWhiteSpace(char) {
// "\r\n\t "
return char === 0x00d || char === 0x00a || char === 0x009 || char === 0x020
return char === 0x00d || char === 0x00a || char === 0x009 || char === 0x020;
}
/**
@ -604,17 +598,23 @@ function isHTTPWhiteSpace (char) {
* @param {boolean} [leading=true]
* @param {boolean} [trailing=true]
*/
function removeHTTPWhitespace (str, leading = true, trailing = true) {
return removeChars(str, leading, trailing, isHTTPWhiteSpace)
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) {
function isASCIIWhitespace(char) {
// "\r\n\t\f "
return char === 0x00d || char === 0x00a || char === 0x009 || char === 0x00c || char === 0x020
return (
char === 0x00d ||
char === 0x00a ||
char === 0x009 ||
char === 0x00c ||
char === 0x020
);
}
/**
@ -623,8 +623,8 @@ function isASCIIWhitespace (char) {
* @param {boolean} [leading=true]
* @param {boolean} [trailing=true]
*/
function removeASCIIWhitespace (str, leading = true, trailing = true) {
return removeChars(str, leading, trailing, isASCIIWhitespace)
function removeASCIIWhitespace(str, leading = true, trailing = true) {
return removeChars(str, leading, trailing, isASCIIWhitespace);
}
/**
@ -634,19 +634,21 @@ function removeASCIIWhitespace (str, leading = true, trailing = true) {
* @param {(charCode: number) => boolean} predicate
* @returns
*/
function removeChars (str, leading, trailing, predicate) {
let lead = 0
let trail = str.length - 1
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++
while (lead < str.length && predicate(str.charCodeAt(lead))) lead++;
}
if (trailing) {
while (trail > 0 && predicate(str.charCodeAt(trail))) trail--
while (trail > 0 && predicate(str.charCodeAt(trail))) trail--;
}
return lead === 0 && trail === str.length - 1 ? str : str.slice(lead, trail + 1)
return lead === 0 && trail === str.length - 1 ?
str
: str.slice(lead, trail + 1);
}
/**
@ -654,30 +656,34 @@ function removeChars (str, leading, trailing, predicate) {
* @param {Uint8Array} input
* @returns {string}
*/
function isomorphicDecode (input) {
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
const length = input.length;
if ((2 << 15) - 1 > length) {
return String.fromCharCode.apply(null, input)
return String.fromCharCode.apply(null, input);
}
let result = ''; let i = 0
let addition = (2 << 15) - 1
let result = '';
let i = 0;
let addition = (2 << 15) - 1;
while (i < length) {
if (i + addition > length) {
addition = length - i
addition = length - i;
}
result += String.fromCharCode.apply(null, input.subarray(i, i += addition))
result += String.fromCharCode.apply(
null,
input.subarray(i, (i += addition))
);
}
return result
return result;
}
/**
* @see https://mimesniff.spec.whatwg.org/#minimize-a-supported-mime-type
* @param {Exclude<ReturnType<typeof parseMIMEType>, 'failure'>} mimeType
*/
function minimizeSupportedMimeType (mimeType) {
function minimizeSupportedMimeType(mimeType) {
switch (mimeType.essence) {
case 'application/ecmascript':
case 'application/javascript':
@ -696,35 +702,35 @@ function minimizeSupportedMimeType (mimeType) {
case 'text/x-ecmascript':
case 'text/x-javascript':
// 1. If mimeType is a JavaScript MIME type, then return "text/javascript".
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'
return 'application/json';
case 'image/svg+xml':
// 3. If mimeTypes essence is "image/svg+xml", then return "image/svg+xml".
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'
return 'application/xml';
}
// 2. If mimeType is a JSON MIME type, then return "application/json".
if (mimeType.subtype.endsWith('+json')) {
return 'application/json'
return 'application/json';
}
// 4. If mimeType is an XML MIME type, then return "application/xml".
if (mimeType.subtype.endsWith('+xml')) {
return 'application/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 ''
return '';
}
module.exports = {
@ -740,5 +746,5 @@ module.exports = {
removeHTTPWhitespace,
minimizeSupportedMimeType,
HTTP_TOKEN_CODEPOINTS,
isomorphicDecode
}
isomorphicDecode,
};

View File

@ -1,46 +1,46 @@
'use strict'
'use strict';
const { kConnected, kSize } = require('../../core/symbols')
const { kConnected, kSize } = require('../../core/symbols');
class CompatWeakRef {
constructor (value) {
this.value = value
constructor(value) {
this.value = value;
}
deref () {
return this.value[kConnected] === 0 && this.value[kSize] === 0
? undefined
: this.value
deref() {
return this.value[kConnected] === 0 && this.value[kSize] === 0 ?
undefined
: this.value;
}
}
class CompatFinalizer {
constructor (finalizer) {
this.finalizer = finalizer
constructor(finalizer) {
this.finalizer = finalizer;
}
register (dispatcher, key) {
register(dispatcher, key) {
if (dispatcher.on) {
dispatcher.on('disconnect', () => {
if (dispatcher[kConnected] === 0 && dispatcher[kSize] === 0) {
this.finalizer(key)
this.finalizer(key);
}
})
});
}
}
unregister (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')
process._rawDebug('Using compatibility WeakRef and FinalizationRegistry');
return {
WeakRef: CompatWeakRef,
FinalizationRegistry: CompatFinalizer
}
FinalizationRegistry: CompatFinalizer,
};
}
return { WeakRef, FinalizationRegistry }
}
return { WeakRef, FinalizationRegistry };
};

View File

@ -1,63 +1,68 @@
'use strict'
'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 {
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 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')
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) {
function isAsciiString(chars) {
for (let i = 0; i < chars.length; ++i) {
if ((chars.charCodeAt(i) & ~0x7F) !== 0) {
return false
if ((chars.charCodeAt(i) & ~0x7f) !== 0) {
return false;
}
}
return true
return true;
}
/**
* @see https://andreubotella.github.io/multipart-form-data/#multipart-form-data-boundary
* @param {string} boundary
*/
function validateBoundary (boundary) {
const length = boundary.length
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
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)
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
if (
!(
(cp >= 0x30 && cp <= 0x39) ||
(cp >= 0x41 && cp <= 0x5a) ||
(cp >= 0x61 && cp <= 0x7a) ||
cp === 0x27 ||
cp === 0x2d ||
cp === 0x5f
)
) {
return false;
}
}
return true
return true;
}
/**
@ -65,41 +70,44 @@ function validateBoundary (boundary) {
* @param {Buffer} input
* @param {ReturnType<import('./data-url')['parseMIMEType']>} mimeType
*/
function multipartFormDataParser (input, mimeType) {
function multipartFormDataParser(input, mimeType) {
// 1. Assert: mimeTypes essence is "multipart/form-data".
assert(mimeType !== 'failure' && mimeType.essence === 'multipart/form-data')
assert(mimeType !== 'failure' && mimeType.essence === 'multipart/form-data');
const boundaryString = mimeType.parameters.get('boundary')
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')
throw parsingError('missing boundary in content-type header');
}
const boundary = Buffer.from(`--${boundaryString}`, 'utf8')
const boundary = Buffer.from(`--${boundaryString}`, 'utf8');
// 3. Let entry list be an empty entry list.
const entryList = []
const entryList = [];
// 4. Let position be a pointer to a byte in input, initially pointing at
// the first byte.
const position = { position: 0 }
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
while (
input[position.position] === 0x0d &&
input[position.position + 1] === 0x0a
) {
position.position += 2;
}
let trailing = input.length
let trailing = input.length;
while (input[trailing - 1] === 0x0a && input[trailing - 2] === 0x0d) {
trailing -= 2
trailing -= 2;
}
if (trailing !== input.length) {
input = input.subarray(0, trailing)
input = input.subarray(0, trailing);
}
// 5. While true:
@ -108,103 +116,120 @@ function multipartFormDataParser (input, mimeType) {
// (`--`) 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
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')
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))
(position.position === input.length - 2 &&
bufferStartsWith(input, dd, position)) ||
(position.position === input.length - 4 &&
bufferStartsWith(input, ddcrlf, position))
) {
return entryList
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')
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
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)
const result = parseMultipartFormDataHeaders(input, position);
let { name, filename, contentType, encoding } = result
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
position.position += 2;
// 5.7. Let body be the empty byte sequence.
let body
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)
const boundaryIndex = input.indexOf(
boundary.subarray(2),
position.position
);
if (boundaryIndex === -1) {
throw parsingError('expected boundary after body')
throw parsingError('expected boundary after body');
}
body = input.subarray(position.position, boundaryIndex - 4)
body = input.subarray(position.position, boundaryIndex - 4);
position.position += body.length
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')
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')
if (
input[position.position] !== 0x0d ||
input[position.position + 1] !== 0x0a
) {
throw parsingError('expected CRLF');
} else {
position.position += 2
position.position += 2;
}
// 5.10. If filename is not null:
let value
let value;
if (filename !== null) {
// 5.10.1. If contentType is null, set contentType to "text/plain".
contentType ??= '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 = ''
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 })
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))
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))
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))
entryList.push(makeEntry(name, value, filename));
}
}
@ -213,24 +238,27 @@ function multipartFormDataParser (input, mimeType) {
* @param {Buffer} input
* @param {{ position: number }} position
*/
function parseMultipartFormDataHeaders (input, 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
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) {
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')
throw parsingError('header name is null');
}
// 2.1.2. Return name, filename and contentType.
return { name, filename, contentType, encoding }
return { name, filename, contentType, encoding };
}
// 2.2. Let header name be the result of collecting a sequence of bytes that are
@ -239,23 +267,30 @@ function parseMultipartFormDataHeaders (input, position) {
(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)
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')
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 :')
throw parsingError('expected :');
}
// 2.6. Advance position by 1.
position.position++
position.position++;
// 2.7. Collect a sequence of bytes that are HTTP tab or space bytes given position.
// (Do nothing with those bytes.)
@ -263,36 +298,41 @@ function parseMultipartFormDataHeaders (input, position) {
(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
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')
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
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)
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 (
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*=
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
@ -300,13 +340,13 @@ function parseMultipartFormDataHeaders (input, position) {
(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
@ -315,17 +355,19 @@ function parseMultipartFormDataHeaders (input, position) {
headerValue[3] !== 0x2d || // -
headerValue[4] !== 0x38 // 8
) {
throw parsingError('unknown encoding, expected utf-8\'\'')
throw parsingError("unknown encoding, expected utf-8''");
}
// skip utf-8''
filename = decodeURIComponent(new TextDecoder().decode(headerValue.subarray(7)))
filename = decodeURIComponent(
new TextDecoder().decode(headerValue.subarray(7))
);
position.position = at.position
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
position.position += 11;
// Remove leading http tab and spaces. See RFC for examples.
// https://datatracker.ietf.org/doc/html/rfc6266#section-5
@ -333,18 +375,18 @@ function parseMultipartFormDataHeaders (input, position) {
(char) => char === 0x20 || char === 0x09,
input,
position
)
);
position.position++ // skip past " after removing whitespace
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)
filename = parseMultipartFormDataName(input, position);
}
}
}
break
break;
}
case 'content-type': {
// 1. Let header value be the result of collecting a sequence of bytes that are
@ -353,28 +395,38 @@ function parseMultipartFormDataHeaders (input, position) {
(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)
headerValue = removeChars(
headerValue,
false,
true,
(char) => char === 0x9 || char === 0x20
);
// 3. Set contentType to the isomorphic decoding of header value.
contentType = isomorphicDecode(headerValue)
contentType = isomorphicDecode(headerValue);
break
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)
headerValue = removeChars(
headerValue,
false,
true,
(char) => char === 0x9 || char === 0x20
);
encoding = isomorphicDecode(headerValue)
encoding = isomorphicDecode(headerValue);
break
break;
}
default: {
// Collect a sequence of bytes that are not 0x0A (LF) or 0x0D (CR), given position.
@ -383,16 +435,19 @@ function parseMultipartFormDataHeaders (input, position) {
(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')
if (
input[position.position] !== 0x0d &&
input[position.position + 1] !== 0x0a
) {
throw parsingError('expected CRLF');
} else {
position.position += 2
position.position += 2;
}
}
}
@ -402,9 +457,9 @@ function parseMultipartFormDataHeaders (input, position) {
* @param {Buffer} input
* @param {{ position: number }} position
*/
function parseMultipartFormDataName (input, position) {
function parseMultipartFormDataName(input, position) {
// 1. Assert: The byte at (position - 1) is 0x22 (").
assert(input[position.position - 1] === 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} */
@ -412,26 +467,27 @@ function parseMultipartFormDataName (input, position) {
(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 "')
throw parsingError('expected "');
} else {
position.position++
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, '"')
name = new TextDecoder()
.decode(name)
.replace(/%0A/gi, '\n')
.replace(/%0D/gi, '\r')
.replace(/%22/g, '"');
// 5. Return the UTF-8 decoding without BOM of name.
return name
return name;
}
/**
@ -439,14 +495,14 @@ function parseMultipartFormDataName (input, position) {
* @param {Buffer} input
* @param {{ position: number }} position
*/
function collectASequenceOfBytes (condition, input, position) {
let start = position.position
function collectASequenceOfBytes(condition, input, position) {
let start = position.position;
while (start < input.length && condition(input[start])) {
++start
++start;
}
return input.subarray(position.position, (position.position = start))
return input.subarray(position.position, (position.position = start));
}
/**
@ -456,19 +512,21 @@ function collectASequenceOfBytes (condition, input, position) {
* @param {(charCode: number) => boolean} predicate
* @returns {Buffer}
*/
function removeChars (buf, leading, trailing, predicate) {
let lead = 0
let trail = buf.length - 1
function removeChars(buf, leading, trailing, predicate) {
let lead = 0;
let trail = buf.length - 1;
if (leading) {
while (lead < buf.length && predicate(buf[lead])) lead++
while (lead < buf.length && predicate(buf[lead])) lead++;
}
if (trailing) {
while (trail > 0 && predicate(buf[trail])) trail--
while (trail > 0 && predicate(buf[trail])) trail--;
}
return lead === 0 && trail === buf.length - 1 ? buf : buf.subarray(lead, trail + 1)
return lead === 0 && trail === buf.length - 1 ?
buf
: buf.subarray(lead, trail + 1);
}
/**
@ -477,25 +535,27 @@ function removeChars (buf, leading, trailing, predicate) {
* @param {Buffer} start
* @param {{ position: number }} position
*/
function bufferStartsWith (buffer, start, position) {
function bufferStartsWith(buffer, start, position) {
if (buffer.length < start.length) {
return false
return false;
}
for (let i = 0; i < start.length; i++) {
if (start[i] !== buffer[position.position + i]) {
return false
return false;
}
}
return true
return true;
}
function parsingError (cause) {
return new TypeError('Failed to parse body as FormData.', { cause: new TypeError(cause) })
function parsingError(cause) {
return new TypeError('Failed to parse body as FormData.', {
cause: new TypeError(cause),
});
}
module.exports = {
multipartFormDataParser,
validateBoundary
}
validateBoundary,
};

View File

@ -1,98 +1,98 @@
'use strict'
'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')
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
const File = globalThis.File ?? NativeFile;
// https://xhr.spec.whatwg.org/#formdata
class FormData {
#state = []
#state = [];
constructor (form) {
webidl.util.markAsUncloneable(this)
constructor(form) {
webidl.util.markAsUncloneable(this);
if (form !== undefined) {
throw webidl.errors.conversionFailed({
prefix: 'FormData constructor',
argument: 'Argument 1',
types: ['undefined']
})
types: ['undefined'],
});
}
}
append (name, value, filename = undefined) {
webidl.brandCheck(this, FormData)
append(name, value, filename = undefined) {
webidl.brandCheck(this, FormData);
const prefix = 'FormData.append'
webidl.argumentLengthCheck(arguments, 2, prefix)
const prefix = 'FormData.append';
webidl.argumentLengthCheck(arguments, 2, prefix);
name = webidl.converters.USVString(name)
name = webidl.converters.USVString(name);
if (arguments.length === 3 || webidl.is.Blob(value)) {
value = webidl.converters.Blob(value, prefix, 'value')
value = webidl.converters.Blob(value, prefix, 'value');
if (filename !== undefined) {
filename = webidl.converters.USVString(filename)
filename = webidl.converters.USVString(filename);
}
} else {
value = webidl.converters.USVString(value)
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)
const entry = makeEntry(name, value, filename);
// 3. Append entry to thiss entry list.
this.#state.push(entry)
this.#state.push(entry);
}
delete (name) {
webidl.brandCheck(this, FormData)
delete(name) {
webidl.brandCheck(this, FormData);
const prefix = 'FormData.delete'
webidl.argumentLengthCheck(arguments, 1, prefix)
const prefix = 'FormData.delete';
webidl.argumentLengthCheck(arguments, 1, prefix);
name = webidl.converters.USVString(name)
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)
this.#state = this.#state.filter((entry) => entry.name !== name);
}
get (name) {
webidl.brandCheck(this, FormData)
get(name) {
webidl.brandCheck(this, FormData);
const prefix = 'FormData.get'
webidl.argumentLengthCheck(arguments, 1, prefix)
const prefix = 'FormData.get';
webidl.argumentLengthCheck(arguments, 1, prefix);
name = webidl.converters.USVString(name)
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)
const idx = this.#state.findIndex((entry) => entry.name === name);
if (idx === -1) {
return null
return null;
}
// 2. Return the value of the first entry whose name is name from
// thiss entry list.
return this.#state[idx].value
return this.#state[idx].value;
}
getAll (name) {
webidl.brandCheck(this, FormData)
getAll(name) {
webidl.brandCheck(this, FormData);
const prefix = 'FormData.getAll'
webidl.argumentLengthCheck(arguments, 1, prefix)
const prefix = 'FormData.getAll';
webidl.argumentLengthCheck(arguments, 1, prefix);
name = webidl.converters.USVString(name)
name = webidl.converters.USVString(name);
// 1. If there is no entry whose name is name in thiss entry list,
// then return the empty list.
@ -100,38 +100,38 @@ class FormData {
// from thiss entry list.
return this.#state
.filter((entry) => entry.name === name)
.map((entry) => entry.value)
.map((entry) => entry.value);
}
has (name) {
webidl.brandCheck(this, FormData)
has(name) {
webidl.brandCheck(this, FormData);
const prefix = 'FormData.has'
webidl.argumentLengthCheck(arguments, 1, prefix)
const prefix = 'FormData.has';
webidl.argumentLengthCheck(arguments, 1, prefix);
name = webidl.converters.USVString(name)
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
return this.#state.findIndex((entry) => entry.name === name) !== -1;
}
set (name, value, filename = undefined) {
webidl.brandCheck(this, FormData)
set(name, value, filename = undefined) {
webidl.brandCheck(this, FormData);
const prefix = 'FormData.set'
webidl.argumentLengthCheck(arguments, 2, prefix)
const prefix = 'FormData.set';
webidl.argumentLengthCheck(arguments, 2, prefix);
name = webidl.converters.USVString(name)
name = webidl.converters.USVString(name);
if (arguments.length === 3 || webidl.is.Blob(value)) {
value = webidl.converters.Blob(value, prefix, 'value')
value = webidl.converters.Blob(value, prefix, 'value');
if (filename !== undefined) {
filename = webidl.converters.USVString(filename)
filename = webidl.converters.USVString(filename);
}
} else {
value = webidl.converters.USVString(value)
value = webidl.converters.USVString(value);
}
// The set(name, value) and set(name, blobValue, filename) method steps
@ -141,68 +141,71 @@ class FormData {
// 2. Let entry be the result of creating an entry with name, value, and
// filename if given.
const entry = makeEntry(name, value, filename)
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)
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)
]
...this.#state.slice(idx + 1).filter((entry) => entry.name !== name),
];
} else {
// 4. Otherwise, append entry to thiss entry list.
this.#state.push(entry)
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)
[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] = [a[b.name], b.value]
a[b.name] = b.value;
}
} else {
a[b.name] = b.value
}
return a
}, { __proto__: null })
return a;
},
{ __proto__: null }
);
options.depth ??= depth
options.colors ??= true
options.depth ??= depth;
options.colors ??= true;
const output = nodeUtil.formatWithOptions(options, state)
const output = nodeUtil.formatWithOptions(options, state);
// remove [Object null prototype]
return `FormData ${output.slice(output.indexOf(']') + 2)}`
return `FormData ${output.slice(output.indexOf(']') + 2)}`;
}
/**
* @param {FormData} formData
*/
static getFormDataState (formData) {
return formData.#state
static getFormDataState(formData) {
return formData.#state;
}
/**
* @param {FormData} formData
* @param {any[]} newState
*/
static setFormDataState (formData, newState) {
formData.#state = newState
static setFormDataState(formData, newState) {
formData.#state = newState;
}
}
const { getFormDataState, setFormDataState } = FormData
Reflect.deleteProperty(FormData, 'getFormDataState')
Reflect.deleteProperty(FormData, 'setFormDataState')
const { getFormDataState, setFormDataState } = FormData;
Reflect.deleteProperty(FormData, 'getFormDataState');
Reflect.deleteProperty(FormData, 'setFormDataState');
iteratorMixin('FormData', FormData, getFormDataState, 'name', 'value')
iteratorMixin('FormData', FormData, getFormDataState, 'name', 'value');
Object.defineProperties(FormData.prototype, {
append: kEnumerableProperty,
@ -213,9 +216,9 @@ Object.defineProperties(FormData.prototype, {
set: kEnumerableProperty,
[Symbol.toStringTag]: {
value: 'FormData',
configurable: true
}
})
configurable: true,
},
});
/**
* @see https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#create-an-entry
@ -224,7 +227,7 @@ Object.defineProperties(FormData.prototype, {
* @param {?string} filename
* @returns
*/
function makeEntry (name, value, filename) {
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.
@ -238,7 +241,7 @@ function makeEntry (name, value, filename) {
// 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 })
value = new File([value], 'blob', { type: value.type });
}
// 2. If filename is given, then set value to a new File object,
@ -247,17 +250,17 @@ function makeEntry (name, value, filename) {
/** @type {FilePropertyBag} */
const options = {
type: value.type,
lastModified: value.lastModified
}
lastModified: value.lastModified,
};
value = new File([value], filename, options)
value = new File([value], filename, options);
}
}
// 4. Return an entry whose name is name and whose value is value.
return { name, value }
return { name, value };
}
webidl.is.FormData = webidl.util.MakeTypeAssertion(FormData)
webidl.is.FormData = webidl.util.MakeTypeAssertion(FormData);
module.exports = { FormData, makeEntry, setFormDataState }
module.exports = { FormData, makeEntry, setFormDataState };

View File

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

View File

@ -1,24 +1,24 @@
// https://github.com/Ethan-Arrowood/undici-fetch
'use strict'
'use strict';
const { kConstruct } = require('../../core/symbols')
const { kEnumerableProperty } = require('../../core/util')
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')
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
function isHTTPWhiteSpaceCharCode(code) {
return code === 0x0a || code === 0x0d || code === 0x09 || code === 0x20;
}
/**
@ -26,56 +26,63 @@ function isHTTPWhiteSpaceCharCode (code) {
* @param {string} potentialValue
* @returns {string}
*/
function headerValueNormalize (potentialValue) {
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
let i = 0;
let j = potentialValue.length;
while (j > i && isHTTPWhiteSpaceCharCode(potentialValue.charCodeAt(j - 1))) --j
while (j > i && isHTTPWhiteSpaceCharCode(potentialValue.charCodeAt(i))) ++i
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)
return i === 0 && j === potentialValue.length ?
potentialValue
: potentialValue.substring(i, j);
}
/**
* @param {Headers} headers
* @param {Array|Object} object
*/
function fill (headers, 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]
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}.`
})
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])
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)
const keys = Object.keys(object);
for (let i = 0; i < keys.length; ++i) {
appendHeader(headers, keys[i], object[keys[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>']
})
types: [
'sequence<sequence<ByteString>>',
'record<ByteString, ByteString>',
],
});
}
}
@ -85,9 +92,9 @@ function fill (headers, object) {
* @param {string} name
* @param {string} value
*/
function appendHeader (headers, name, value) {
function appendHeader(headers, name, value) {
// 1. Normalize value.
value = headerValueNormalize(value)
value = headerValueNormalize(value);
// 2. If name is not a header name or value is not a
// header value, then throw a TypeError.
@ -95,14 +102,14 @@ function appendHeader (headers, name, value) {
throw webidl.errors.invalidArgument({
prefix: 'Headers.append',
value: name,
type: 'header name'
})
type: 'header name',
});
} else if (!isValidHeaderValue(value)) {
throw webidl.errors.invalidArgument({
prefix: 'Headers.append',
value,
type: 'header value'
})
type: 'header value',
});
}
// 3. If headerss guard is "immutable", then throw a TypeError.
@ -112,14 +119,14 @@ function appendHeader (headers, name, value) {
// TODO
// Note: undici does not implement forbidden header names
if (getHeadersGuard(headers) === 'immutable') {
throw new TypeError('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)
return getHeadersList(headers).append(name, value, false);
// 8. If headerss guard is "request-no-cors", then remove
// privileged no-CORS request headers from headers
@ -129,36 +136,36 @@ function appendHeader (headers, name, value) {
/**
* @param {Headers} target
*/
function headersListSortAndCombine (target) {
const headersList = getHeadersList(target)
function headersListSortAndCombine(target) {
const headersList = getHeadersList(target);
if (!headersList) {
return []
return [];
}
if (headersList.sortedMap) {
return 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 = []
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 names = headersList.toSortedArray();
const cookies = headersList.cookies
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)
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]
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
@ -167,7 +174,7 @@ function headersListSortAndCombine (target) {
// 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]])
headers.push([name, cookies[j]]);
}
} else {
// 2. Otherwise:
@ -178,33 +185,33 @@ function headersListSortAndCombine (target) {
// Note: This operation was done by `HeadersList#toSortedArray`.
// 3. Append (name, value) to headers.
headers.push([name, value])
headers.push([name, value]);
}
}
// 4. Return headers.
return (headersList.sortedMap = headers)
return (headersList.sortedMap = headers);
}
function compareHeaderName (a, b) {
return a[0] < b[0] ? -1 : 1
function compareHeaderName(a, b) {
return a[0] < b[0] ? -1 : 1;
}
class HeadersList {
/** @type {[string, string][]|null} */
cookies = null
cookies = null;
sortedMap
headersMap
sortedMap;
headersMap;
constructor (init) {
constructor(init) {
if (init instanceof HeadersList) {
this.headersMap = new Map(init.headersMap)
this.sortedMap = init.sortedMap
this.cookies = init.cookies === null ? null : [...init.cookies]
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
this.headersMap = new Map(init);
this.sortedMap = null;
}
}
@ -213,18 +220,18 @@ class HeadersList {
* @param {string} name
* @param {boolean} isLowerCase
*/
contains (name, 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())
return this.headersMap.has(isLowerCase ? name : name.toLowerCase());
}
clear () {
this.headersMap.clear()
this.sortedMap = null
this.cookies = null
clear() {
this.headersMap.clear();
this.sortedMap = null;
this.cookies = null;
}
/**
@ -233,27 +240,27 @@ class HeadersList {
* @param {string} value
* @param {boolean} isLowerCase
*/
append (name, value, isLowerCase) {
this.sortedMap = null
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)
const lowercaseName = isLowerCase ? name : name.toLowerCase();
const exists = this.headersMap.get(lowercaseName);
// 2. Append (name, value) to list.
if (exists) {
const delimiter = lowercaseName === 'cookie' ? '; ' : ', '
const delimiter = lowercaseName === 'cookie' ? '; ' : ', ';
this.headersMap.set(lowercaseName, {
name: exists.name,
value: `${exists.value}${delimiter}${value}`
})
value: `${exists.value}${delimiter}${value}`,
});
} else {
this.headersMap.set(lowercaseName, { name, value })
this.headersMap.set(lowercaseName, { name, value });
}
if (lowercaseName === 'set-cookie') {
(this.cookies ??= []).push(value)
(this.cookies ??= []).push(value);
}
}
@ -263,19 +270,19 @@ class HeadersList {
* @param {string} value
* @param {boolean} isLowerCase
*/
set (name, value, isLowerCase) {
this.sortedMap = null
const lowercaseName = isLowerCase ? name : name.toLowerCase()
set(name, value, isLowerCase) {
this.sortedMap = null;
const lowercaseName = isLowerCase ? name : name.toLowerCase();
if (lowercaseName === 'set-cookie') {
this.cookies = [value]
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 })
this.headersMap.set(lowercaseName, { name, value });
}
/**
@ -283,15 +290,15 @@ class HeadersList {
* @param {string} name
* @param {boolean} isLowerCase
*/
delete (name, isLowerCase) {
this.sortedMap = null
if (!isLowerCase) name = name.toLowerCase()
delete(name, isLowerCase) {
this.sortedMap = null;
if (!isLowerCase) name = name.toLowerCase();
if (name === 'set-cookie') {
this.cookies = null
this.cookies = null;
}
this.headersMap.delete(name)
this.headersMap.delete(name);
}
/**
@ -300,191 +307,203 @@ class HeadersList {
* @param {boolean} isLowerCase
* @returns {string | null}
*/
get (name, isLowerCase) {
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
return (
this.headersMap.get(isLowerCase ? name : name.toLowerCase())?.value ??
null
);
}
* [Symbol.iterator] () {
*[Symbol.iterator]() {
// use the lowercased name
for (const { 0: name, 1: { value } } of this.headersMap) {
yield [name, value]
for (const {
0: name,
1: { value },
} of this.headersMap) {
yield [name, value];
}
}
get entries () {
const headers = {}
get entries() {
const headers = {};
if (this.headersMap.size !== 0) {
for (const { name, value } of this.headersMap.values()) {
headers[name] = value
headers[name] = value;
}
}
return headers
return headers;
}
rawValues () {
return this.headersMap.values()
rawValues() {
return this.headersMap.values();
}
get entriesList () {
const headers = []
get entriesList() {
const headers = [];
if (this.headersMap.size !== 0) {
for (const { 0: lowerName, 1: { name, value } } of this.headersMap) {
for (const {
0: lowerName,
1: { name, value },
} of this.headersMap) {
if (lowerName === 'set-cookie') {
for (const cookie of this.cookies) {
headers.push([name, cookie])
headers.push([name, cookie]);
}
} else {
headers.push([name, value])
headers.push([name, value]);
}
}
}
return headers
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)
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
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
const iterator = this.headersMap[Symbol.iterator]();
const firstValue = iterator.next().value;
// set [name, value] to first index.
array[0] = [firstValue[0], firstValue[1].value]
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)
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
value = iterator.next().value;
// set [name, value] to current index.
x = array[i] = [value[0], value[1].value]
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
assert(x[1] !== null);
left = 0;
right = i;
// binary search
while (left < right) {
// middle index
pivot = left + ((right - left) >> 1)
pivot = left + ((right - left) >> 1);
// compare header name
if (array[pivot][0] <= x[0]) {
left = pivot + 1
left = pivot + 1;
} else {
right = pivot
right = pivot;
}
}
if (i !== pivot) {
j = i
j = i;
while (j > left) {
array[j] = array[--j]
array[j] = array[--j];
}
array[left] = x
array[left] = x;
}
}
/* c8 ignore next 4 */
if (!iterator.next().done) {
// This is for debugging and will never be called.
throw new TypeError('Unreachable')
throw new TypeError('Unreachable');
}
return array
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]
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)
assert(value !== null);
}
return array.sort(compareHeaderName)
return array.sort(compareHeaderName);
}
}
}
// https://fetch.spec.whatwg.org/#headers-class
class Headers {
#guard
#guard;
/**
* @type {HeadersList}
*/
#headersList
#headersList;
/**
* @param {HeadersInit|Symbol} [init]
* @returns
*/
constructor (init = undefined) {
webidl.util.markAsUncloneable(this)
constructor(init = undefined) {
webidl.util.markAsUncloneable(this);
if (init === kConstruct) {
return
return;
}
this.#headersList = new HeadersList()
this.#headersList = new HeadersList();
// The new Headers(init) constructor steps are:
// 1. Set thiss guard to "none".
this.#guard = '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)
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)
append(name, value) {
webidl.brandCheck(this, Headers);
webidl.argumentLengthCheck(arguments, 2, 'Headers.append')
webidl.argumentLengthCheck(arguments, 2, 'Headers.append');
const prefix = 'Headers.append'
name = webidl.converters.ByteString(name, prefix, 'name')
value = webidl.converters.ByteString(value, prefix, 'value')
const prefix = 'Headers.append';
name = webidl.converters.ByteString(name, prefix, 'name');
value = webidl.converters.ByteString(value, prefix, 'value');
return appendHeader(this, name, value)
return appendHeader(this, name, value);
}
// https://fetch.spec.whatwg.org/#dom-headers-delete
delete (name) {
webidl.brandCheck(this, Headers)
delete(name) {
webidl.brandCheck(this, Headers);
webidl.argumentLengthCheck(arguments, 1, 'Headers.delete')
webidl.argumentLengthCheck(arguments, 1, 'Headers.delete');
const prefix = 'Headers.delete'
name = webidl.converters.ByteString(name, prefix, 'name')
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'
})
type: 'header name',
});
}
// 2. If thiss guard is "immutable", then throw a TypeError.
@ -498,79 +517,79 @@ class Headers {
// a forbidden response-header name, return.
// Note: undici does not implement forbidden header names
if (this.#guard === 'immutable') {
throw new TypeError('immutable')
throw new TypeError('immutable');
}
// 6. If thiss header list does not contain name, then
// return.
if (!this.#headersList.contains(name, false)) {
return
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)
this.#headersList.delete(name, false);
}
// https://fetch.spec.whatwg.org/#dom-headers-get
get (name) {
webidl.brandCheck(this, Headers)
get(name) {
webidl.brandCheck(this, Headers);
webidl.argumentLengthCheck(arguments, 1, 'Headers.get')
webidl.argumentLengthCheck(arguments, 1, 'Headers.get');
const prefix = 'Headers.get'
name = webidl.converters.ByteString(name, prefix, 'name')
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'
})
type: 'header name',
});
}
// 2. Return the result of getting name from thiss header
// list.
return this.#headersList.get(name, false)
return this.#headersList.get(name, false);
}
// https://fetch.spec.whatwg.org/#dom-headers-has
has (name) {
webidl.brandCheck(this, Headers)
has(name) {
webidl.brandCheck(this, Headers);
webidl.argumentLengthCheck(arguments, 1, 'Headers.has')
webidl.argumentLengthCheck(arguments, 1, 'Headers.has');
const prefix = 'Headers.has'
name = webidl.converters.ByteString(name, prefix, 'name')
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'
})
type: 'header name',
});
}
// 2. Return true if thiss header list contains name;
// otherwise false.
return this.#headersList.contains(name, false)
return this.#headersList.contains(name, false);
}
// https://fetch.spec.whatwg.org/#dom-headers-set
set (name, value) {
webidl.brandCheck(this, Headers)
set(name, value) {
webidl.brandCheck(this, Headers);
webidl.argumentLengthCheck(arguments, 2, 'Headers.set')
webidl.argumentLengthCheck(arguments, 2, 'Headers.set');
const prefix = 'Headers.set'
name = webidl.converters.ByteString(name, prefix, 'name')
value = webidl.converters.ByteString(value, prefix, 'value')
const prefix = 'Headers.set';
name = webidl.converters.ByteString(name, prefix, 'name');
value = webidl.converters.ByteString(value, prefix, 'value');
// 1. Normalize value.
value = headerValueNormalize(value)
value = headerValueNormalize(value);
// 2. If name is not a header name or value is not a
// header value, then throw a TypeError.
@ -578,14 +597,14 @@ class Headers {
throw webidl.errors.invalidArgument({
prefix,
value: name,
type: 'header name'
})
type: 'header name',
});
} else if (!isValidHeaderValue(value)) {
throw webidl.errors.invalidArgument({
prefix,
value,
type: 'header value'
})
type: 'header value',
});
}
// 3. If thiss guard is "immutable", then throw a TypeError.
@ -598,69 +617,70 @@ class Headers {
// forbidden response-header name, return.
// Note: undici does not implement forbidden header names
if (this.#guard === 'immutable') {
throw new TypeError('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)
this.#headersList.set(name, value, false);
}
// https://fetch.spec.whatwg.org/#dom-headers-getsetcookie
getSetCookie () {
webidl.brandCheck(this, Headers)
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
const list = this.#headersList.cookies;
if (list) {
return [...list]
return [...list];
}
return []
return [];
}
[util.inspect.custom] (depth, options) {
options.depth ??= depth
[util.inspect.custom](depth, options) {
options.depth ??= depth;
return `Headers ${util.formatWithOptions(options, this.#headersList.entries)}`
return `Headers ${util.formatWithOptions(options, this.#headersList.entries)}`;
}
static getHeadersGuard (o) {
return o.#guard
static getHeadersGuard(o) {
return o.#guard;
}
static setHeadersGuard (o, guard) {
o.#guard = guard
static setHeadersGuard(o, guard) {
o.#guard = guard;
}
/**
* @param {Headers} o
*/
static getHeadersList (o) {
return o.#headersList
static getHeadersList(o) {
return o.#headersList;
}
/**
* @param {Headers} target
* @param {HeadersList} list
*/
static setHeadersList (target, list) {
target.#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')
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)
iteratorMixin('Headers', Headers, headersListSortAndCombine, 0, 1);
Object.defineProperties(Headers.prototype, {
append: kEnumerableProperty,
@ -671,40 +691,50 @@ Object.defineProperties(Headers.prototype, {
getSetCookie: kEnumerableProperty,
[Symbol.toStringTag]: {
value: 'Headers',
configurable: true
configurable: true,
},
[util.inspect.custom]: {
enumerable: false
}
})
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)
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
if (!util.types.isProxy(V) && iterator === Headers.prototype.entries) {
// Headers object
try {
return getHeadersList(V).entriesList
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['sequence<sequence<ByteString>>'](
V,
prefix,
argument,
iterator.bind(V)
);
}
return webidl.converters['record<ByteString, ByteString>'](V, prefix, argument)
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>']
})
}
types: ['sequence<sequence<ByteString>>', 'record<ByteString, ByteString>'],
});
};
module.exports = {
fill,
@ -715,5 +745,5 @@ module.exports = {
getHeadersGuard,
setHeadersGuard,
setHeadersList,
getHeadersList
}
getHeadersList,
};

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,10 +1,24 @@
'use strict'
'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 {
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,
@ -12,248 +26,248 @@ const {
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')
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')
const textEncoder = new TextEncoder('utf-8');
// https://fetch.spec.whatwg.org/#response-class
class Response {
/** @type {Headers} */
#headers
#headers;
#state
#state;
// Creates network error Response.
static error () {
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')
const responseObject = fromInnerResponse(makeNetworkError(), 'immutable');
return responseObject
return responseObject;
}
// https://fetch.spec.whatwg.org/#dom-response-json
static json (data, init = undefined) {
webidl.argumentLengthCheck(arguments, 1, 'Response.json')
static json(data, init = undefined) {
webidl.argumentLengthCheck(arguments, 1, 'Response.json');
if (init !== null) {
init = webidl.converters.ResponseInit(init)
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)
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')
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' })
initializeResponse(responseObject, init, {
body: body[0],
type: 'application/json',
});
// 5. Return responseObject.
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')
static redirect(url, status = 302) {
webidl.argumentLengthCheck(arguments, 1, 'Response.redirect');
url = webidl.converters.USVString(url)
status = webidl.converters['unsigned short'](status)
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
let parsedURL;
try {
parsedURL = new URL(url, relevantRealm.settingsObject.baseUrl)
parsedURL = new URL(url, relevantRealm.settingsObject.baseUrl);
} catch (err) {
throw new TypeError(`Failed to parse URL from ${url}`, { cause: 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}`)
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')
const responseObject = fromInnerResponse(makeResponse({}), 'immutable');
// 5. Set responseObjects responses status to status.
responseObject.#state.status = status
responseObject.#state.status = status;
// 6. Let value be parsedURL, serialized and isomorphic encoded.
const value = isomorphicEncode(URLSerializer(parsedURL))
const value = isomorphicEncode(URLSerializer(parsedURL));
// 7. Append `Location`/value to responseObjects responses header list.
responseObject.#state.headersList.append('location', value, true)
responseObject.#state.headersList.append('location', value, true);
// 8. Return responseObject.
return responseObject
return responseObject;
}
// https://fetch.spec.whatwg.org/#dom-response
constructor (body = null, init = undefined) {
webidl.util.markAsUncloneable(this)
constructor(body = null, init = undefined) {
webidl.util.markAsUncloneable(this);
if (body === kConstruct) {
return
return;
}
if (body !== null) {
body = webidl.converters.BodyInit(body)
body = webidl.converters.BodyInit(body);
}
init = webidl.converters.ResponseInit(init)
init = webidl.converters.ResponseInit(init);
// 1. Set thiss response to a new response.
this.#state = makeResponse({})
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)
this.#headers = new Headers(kConstruct);
setHeadersGuard(this.#headers, 'response');
setHeadersList(this.#headers, this.#state.headersList);
// 3. Let bodyWithType be null.
let bodyWithType = 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 }
const [extractedBody, type] = extractBody(body);
bodyWithType = { body: extractedBody, type };
}
// 5. Perform initialize a response given this, init, and bodyWithType.
initializeResponse(this, init, bodyWithType)
initializeResponse(this, init, bodyWithType);
}
// Returns responses type, e.g., "cors".
get type () {
webidl.brandCheck(this, Response)
get type() {
webidl.brandCheck(this, Response);
// The type getter steps are to return thiss responses type.
return this.#state.type
return this.#state.type;
}
// Returns responses URL, if it has one; otherwise the empty string.
get url () {
webidl.brandCheck(this, Response)
get url() {
webidl.brandCheck(this, Response);
const urlList = this.#state.urlList
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
const url = urlList[urlList.length - 1] ?? null;
if (url === null) {
return ''
return '';
}
return URLSerializer(url, true)
return URLSerializer(url, true);
}
// Returns whether response was obtained through a redirect.
get redirected () {
webidl.brandCheck(this, Response)
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
return this.#state.urlList.length > 1;
}
// Returns responses status.
get status () {
webidl.brandCheck(this, Response)
get status() {
webidl.brandCheck(this, Response);
// The status getter steps are to return thiss responses status.
return this.#state.status
return this.#state.status;
}
// Returns whether responses status is an ok status.
get ok () {
webidl.brandCheck(this, Response)
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
return this.#state.status >= 200 && this.#state.status <= 299;
}
// Returns responses status message.
get statusText () {
webidl.brandCheck(this, Response)
get statusText() {
webidl.brandCheck(this, Response);
// The statusText getter steps are to return thiss responses status
// message.
return this.#state.statusText
return this.#state.statusText;
}
// Returns responses headers as Headers.
get headers () {
webidl.brandCheck(this, Response)
get headers() {
webidl.brandCheck(this, Response);
// The headers getter steps are to return thiss headers.
return this.#headers
return this.#headers;
}
get body () {
webidl.brandCheck(this, Response)
get body() {
webidl.brandCheck(this, Response);
return this.#state.body ? this.#state.body.stream : null
return this.#state.body ? this.#state.body.stream : null;
}
get bodyUsed () {
webidl.brandCheck(this, Response)
get bodyUsed() {
webidl.brandCheck(this, Response);
return !!this.#state.body && util.isDisturbed(this.#state.body.stream)
return !!this.#state.body && util.isDisturbed(this.#state.body.stream);
}
// Returns a clone of response.
clone () {
webidl.brandCheck(this, 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.'
})
message: 'Body has already been consumed.',
});
}
// 2. Let clonedResponse be the result of cloning thiss response.
const clonedResponse = cloneResponse(this.#state)
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))
return fromInnerResponse(clonedResponse, getHeadersGuard(this.#headers));
}
[nodeUtil.inspect.custom] (depth, options) {
[nodeUtil.inspect.custom](depth, options) {
if (options.depth === null) {
options.depth = 2
options.depth = 2;
}
options.colors ??= true
options.colors ??= true;
const properties = {
status: this.status,
@ -264,50 +278,55 @@ class Response {
ok: this.ok,
redirected: this.redirected,
type: this.type,
url: this.url
}
url: this.url,
};
return `Response ${nodeUtil.formatWithOptions(options, properties)}`
return `Response ${nodeUtil.formatWithOptions(options, properties)}`;
}
/**
* @param {Response} response
*/
static getResponseHeaders (response) {
return response.#headers
static getResponseHeaders(response) {
return response.#headers;
}
/**
* @param {Response} response
* @param {Headers} newHeaders
*/
static setResponseHeaders (response, newHeaders) {
response.#headers = newHeaders
static setResponseHeaders(response, newHeaders) {
response.#headers = newHeaders;
}
/**
* @param {Response} response
*/
static getResponseState (response) {
return response.#state
static getResponseState(response) {
return response.#state;
}
/**
* @param {Response} response
* @param {any} newState
*/
static setResponseState (response, newState) {
response.#state = 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')
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)
mixinBody(Response, getResponseState);
Object.defineProperties(Response.prototype, {
type: kEnumerableProperty,
@ -322,18 +341,18 @@ Object.defineProperties(Response.prototype, {
bodyUsed: kEnumerableProperty,
[Symbol.toStringTag]: {
value: 'Response',
configurable: true
}
})
configurable: true,
},
});
Object.defineProperties(Response, {
json: kEnumerableProperty,
redirect: kEnumerableProperty,
error: kEnumerableProperty
})
error: kEnumerableProperty,
});
// https://fetch.spec.whatwg.org/#concept-response-clone
function cloneResponse (response) {
function cloneResponse(response) {
// To clone a response response, run these steps:
// 1. If response is a filtered response, then return a new identical
@ -343,23 +362,23 @@ function cloneResponse (response) {
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 })
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)
newResponse.body = cloneBody(newResponse, response.body);
}
// 4. Return newResponse.
return newResponse
return newResponse;
}
function makeResponse (init) {
function makeResponse(init) {
return {
aborted: false,
rangeRequested: false,
@ -371,55 +390,54 @@ function makeResponse (init) {
cacheState: '',
statusText: '',
...init,
headersList: init?.headersList
? new HeadersList(init?.headersList)
headersList:
init?.headersList ?
new HeadersList(init?.headersList)
: new HeadersList(),
urlList: init?.urlList ? [...init.urlList] : []
}
urlList: init?.urlList ? [...init.urlList] : [],
};
}
function makeNetworkError (reason) {
const isError = isErrorLike(reason)
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'
})
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) {
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) {
function makeFilteredResponse(response, state) {
state = {
internalResponse: response,
...state
}
...state,
};
return new Proxy(response, {
get (target, p) {
return p in state ? state[p] : target[p]
get(target, p) {
return p in state ? state[p] : target[p];
},
set (target, p, value) {
assert(!(p in state))
target[p] = value
return true
}
})
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) {
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') {
@ -430,8 +448,8 @@ function filterResponse (response, type) {
// Note: undici does not implement forbidden response-header names
return makeFilteredResponse(response, {
type: 'basic',
headersList: response.headersList
})
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
@ -441,8 +459,8 @@ function filterResponse (response, type) {
// Note: undici does not implement CORS-safelisted response-header names
return makeFilteredResponse(response, {
type: 'cors',
headersList: response.headersList
})
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
@ -453,8 +471,8 @@ function filterResponse (response, type) {
urlList: Object.freeze([]),
status: 0,
statusText: '',
body: null
})
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
@ -465,31 +483,42 @@ function filterResponse (response, type) {
status: 0,
statusText: '',
headersList: [],
body: null
})
body: null,
});
} else {
assert(false)
assert(false);
}
}
// https://fetch.spec.whatwg.org/#appropriate-network-error
function makeAppropriateNetworkError (fetchParams, err = null) {
function makeAppropriateNetworkError(fetchParams, err = null) {
// 1. Assert: fetchParams is canceled.
assert(isCancelled(fetchParams))
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 }))
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) {
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.')
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,
@ -498,23 +527,23 @@ function initializeResponse (response, init, body) {
// 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')
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
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
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)
fill(getResponseHeaders(response), init.headers);
}
// 6. If body was given, then:
@ -523,17 +552,24 @@ function initializeResponse (response, init, body) {
if (nullBodyStatus.includes(response.status)) {
throw webidl.errors.exception({
header: 'Response constructor',
message: `Invalid response status code ${response.status}`
})
message: `Invalid response status code ${response.status}`,
});
}
// 2. Set response's body to body's body.
getResponseState(response).body = body.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)
if (
body.type != null &&
!getResponseState(response).headersList.contains('content-type', true)
) {
getResponseState(response).headersList.append(
'content-type',
body.type,
true
);
}
}
}
@ -544,13 +580,13 @@ function initializeResponse (response, init, body) {
* @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)
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
@ -558,70 +594,70 @@ function fromInnerResponse (innerResponse, guard) {
// 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))
streamRegistry.register(response, new WeakRef(innerResponse.body.stream));
}
return response
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)
return webidl.converters.USVString(V, prefix, name);
}
if (webidl.is.Blob(V)) {
return V
return V;
}
if (ArrayBuffer.isView(V) || types.isArrayBuffer(V)) {
return V
return V;
}
if (webidl.is.FormData(V)) {
return V
return V;
}
if (webidl.is.URLSearchParams(V)) {
return V
return V;
}
return webidl.converters.DOMString(V, prefix, name)
}
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
return V;
}
// Note: the spec doesn't include async iterables,
// this is an undici extension.
if (V?.[Symbol.asyncIterator]) {
return V
return V;
}
return webidl.converters.XMLHttpRequestBodyInit(V, prefix, argument)
}
return webidl.converters.XMLHttpRequestBodyInit(V, prefix, argument);
};
webidl.converters.ResponseInit = webidl.dictionaryConverter([
{
key: 'status',
converter: webidl.converters['unsigned short'],
defaultValue: () => 200
defaultValue: () => 200,
},
{
key: 'statusText',
converter: webidl.converters.ByteString,
defaultValue: () => ''
defaultValue: () => '',
},
{
key: 'headers',
converter: webidl.converters.HeadersInit
}
])
converter: webidl.converters.HeadersInit,
},
]);
webidl.is.Response = webidl.util.MakeTypeAssertion(Response)
webidl.is.Response = webidl.util.MakeTypeAssertion(Response);
module.exports = {
isNetworkError,
@ -632,5 +668,5 @@ module.exports = {
Response,
cloneResponse,
fromInnerResponse,
getResponseState
}
getResponseState,
};

File diff suppressed because it is too large Load Diff

View File

@ -1,112 +1,121 @@
'use strict'
'use strict';
const { types, inspect } = require('node:util')
const { markAsUncloneable } = require('node:worker_threads')
const { toUSVString } = require('../../core/util')
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 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])
const FunctionPrototypeSymbolHasInstance = Function.call.bind(
Function.prototype[Symbol.hasInstance]
);
/** @type {import('../../../types/webidl').Webidl} */
const webidl = {
converters: {},
util: {},
errors: {},
is: {}
}
is: {},
};
webidl.errors.exception = function (message) {
return new TypeError(`${message.header}: ${message.message}`)
}
return new TypeError(`${message.header}: ${message.message}`);
};
webidl.errors.conversionFailed = function (context) {
const plural = context.types.length === 1 ? '' : ' one of'
const plural = context.types.length === 1 ? '' : ' one of';
const message =
`${context.argument} could not be converted to` +
`${plural}: ${context.types.join(', ')}.`
`${plural}: ${context.types.join(', ')}.`;
return webidl.errors.exception({
header: context.prefix,
message
})
}
message,
});
};
webidl.errors.invalidArgument = function (context) {
return webidl.errors.exception({
header: context.prefix,
message: `"${context.value}" is an invalid ${context.type}.`
})
}
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
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))
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
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
})
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'
})
}
message: 'Illegal constructor',
});
};
webidl.util.MakeTypeAssertion = function (I) {
return (O) => FunctionPrototypeSymbolHasInstance(I, O)
}
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 '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 NULL;
}
return OBJECT
return OBJECT;
}
}
}
};
webidl.util.Types = {
UNDEFINED,
@ -116,65 +125,73 @@ webidl.util.Types = {
NUMBER,
BIGINT,
NULL,
OBJECT
}
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'
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 || (() => {})
webidl.util.markAsUncloneable = markAsUncloneable || (() => {});
// https://webidl.spec.whatwg.org/#abstract-opdef-converttoint
webidl.util.ConvertToInt = function (V, bitLength, signedness, opts) {
let upperBound
let lowerBound
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
upperBound = Math.pow(2, 53) - 1;
// 2. If signedness is "unsigned", then let lowerBound be 0.
if (signedness === 'unsigned') {
lowerBound = 0
lowerBound = 0;
} else {
// 3. Otherwise let lowerBound be 2^53 + 1.
lowerBound = Math.pow(-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
lowerBound = 0;
// 2. Let upperBound be 2^bitLength 1.
upperBound = Math.pow(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
lowerBound = Math.pow(-2, bitLength) - 1;
// 2. Let upperBound be 2^bitLength 1 1.
upperBound = Math.pow(2, bitLength - 1) - 1
upperBound = Math.pow(2, bitLength - 1) - 1;
}
// 4. Let x be ? ToNumber(V).
let x = Number(V)
let x = Number(V);
// 5. If x is 0, then set x to +0.
if (x === 0) {
x = 0
x = 0;
}
// 6. If the conversion is to an IDL type associated
@ -188,24 +205,24 @@ webidl.util.ConvertToInt = function (V, bitLength, signedness, opts) {
) {
throw webidl.errors.exception({
header: 'Integer conversion',
message: `Could not convert ${webidl.util.Stringify(V)} to an integer.`
})
message: `Could not convert ${webidl.util.Stringify(V)} to an integer.`,
});
}
// 2. Set x to IntegerPart(x).
x = webidl.util.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}.`
})
message: `Value must be between ${lowerBound}-${upperBound}, got ${x}.`,
});
}
// 4. Return x.
return x
return x;
}
// 7. If x is not NaN and the conversion is to an IDL
@ -213,19 +230,19 @@ webidl.util.ConvertToInt = function (V, bitLength, signedness, opts) {
// 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)
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)
x = Math.floor(x);
} else {
x = Math.ceil(x)
x = Math.ceil(x);
}
// 3. Return x.
return x
return x;
}
// 8. If x is NaN, +0, +∞, or −∞, then return +0.
@ -235,53 +252,53 @@ webidl.util.ConvertToInt = function (V, bitLength, signedness, opts) {
x === Number.POSITIVE_INFINITY ||
x === Number.NEGATIVE_INFINITY
) {
return 0
return 0;
}
// 9. Set x to IntegerPart(x).
x = webidl.util.IntegerPart(x)
x = webidl.util.IntegerPart(x);
// 10. Set x to x modulo 2^bitLength.
x = x % Math.pow(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)
return x - Math.pow(2, bitLength);
}
// 12. Otherwise, return x.
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))
const r = Math.floor(Math.abs(n));
// 2. If n < 0, then return -1 × r.
if (n < 0) {
return -1 * r
return -1 * r;
}
// 3. Otherwise, return r.
return r
}
return r;
};
webidl.util.Stringify = function (V) {
const type = webidl.util.Type(V)
const type = webidl.util.Type(V);
switch (type) {
case SYMBOL:
return `Symbol(${V.description})`
return `Symbol(${V.description})`;
case OBJECT:
return inspect(V)
return inspect(V);
case STRING:
return `"${V}"`
return `"${V}"`;
default:
return `${V}`
return `${V}`;
}
}
};
// https://webidl.spec.whatwg.org/#es-sequence
webidl.sequenceConverter = function (converter) {
@ -290,41 +307,39 @@ webidl.sequenceConverter = function (converter) {
if (webidl.util.Type(V) !== OBJECT) {
throw webidl.errors.exception({
header: prefix,
message: `${argument} (${webidl.util.Stringify(V)}) is not iterable.`
})
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
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'
) {
if (method === undefined || typeof method.next !== 'function') {
throw webidl.errors.exception({
header: prefix,
message: `${argument} is not iterable.`
})
message: `${argument} is not iterable.`,
});
}
// https://webidl.spec.whatwg.org/#create-sequence-from-iterable
while (true) {
const { done, value } = method.next()
const { done, value } = method.next();
if (done) {
break
break;
}
seq.push(converter(value, prefix, `${argument}[${index++}]`))
seq.push(converter(value, prefix, `${argument}[${index++}]`));
}
return seq
}
}
return seq;
};
};
// https://webidl.spec.whatwg.org/#es-to-record
webidl.recordConverter = function (keyConverter, valueConverter) {
@ -333,148 +348,158 @@ webidl.recordConverter = function (keyConverter, valueConverter) {
if (webidl.util.Type(O) !== OBJECT) {
throw webidl.errors.exception({
header: prefix,
message: `${argument} ("${webidl.util.TypeValueToString(O)}") is not an Object.`
})
message: `${argument} ("${webidl.util.TypeValueToString(O)}") is not an Object.`,
});
}
// 2. Let result be a new empty instance of record<K, V>.
const result = {}
const result = {};
if (!types.isProxy(O)) {
// 1. Let desc be ? O.[[GetOwnProperty]](key).
const keys = [...Object.getOwnPropertyNames(O), ...Object.getOwnPropertySymbols(O)]
const keys = [
...Object.getOwnPropertyNames(O),
...Object.getOwnPropertySymbols(O),
];
for (const key of keys) {
const keyName = webidl.util.Stringify(key)
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}`)
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}]`)
const typedValue = valueConverter(
O[key],
prefix,
`${argument}[${keyName}]`
);
// 4. Set result[typedKey] to typedValue.
result[typedKey] = typedValue
result[typedKey] = typedValue;
}
// 5. Return result.
return result
return result;
}
// 3. Let keys be ? O.[[OwnPropertyKeys]]().
const keys = Reflect.ownKeys(O)
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)
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)
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)
const typedValue = valueConverter(O[key], prefix, argument);
// 4. Set result[typedKey] to typedValue.
result[typedKey] = typedValue
result[typedKey] = typedValue;
}
}
// 5. Return result.
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}.`
})
message: `Expected ${argument} ("${webidl.util.Stringify(V)}") to be an instance of ${name}.`,
});
}
return V
}
}
return V;
};
};
webidl.dictionaryConverter = function (converters) {
return (dictionary, prefix, argument) => {
const dict = {}
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.`
})
message: `Expected ${dictionary} to be one of: Null, Undefined, Object.`,
});
}
for (const options of converters) {
const { key, defaultValue, required, converter } = options
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}".`
})
message: `Missing required key "${key}".`,
});
}
}
let value = dictionary?.[key]
const hasDefault = defaultValue !== undefined
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()
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}`)
value = converter(value, prefix, `${argument}.${key}`);
if (
options.allowedValues &&
!options.allowedValues.includes(value)
) {
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(', ')}.`
})
message: `${value} is not an accepted type. Expected one of ${options.allowedValues.join(', ')}.`,
});
}
dict[key] = value
dict[key] = value;
}
}
return dict
}
}
return dict;
};
};
webidl.nullableConverter = function (converter) {
return (V, prefix, argument) => {
if (V === null) {
return V
return V;
}
return converter(V, prefix, argument)
}
}
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)
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) {
@ -483,22 +508,22 @@ webidl.converters.DOMString = function (V, prefix, argument, opts) {
// extended attribute, then return the DOMString value
// that represents the empty string.
if (V === null && opts?.legacyNullToEmptyString) {
return ''
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.`
})
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)
}
return String(V);
};
// https://webidl.spec.whatwg.org/#es-ByteString
webidl.converters.ByteString = function (V, prefix, argument) {
@ -506,11 +531,11 @@ webidl.converters.ByteString = function (V, prefix, argument) {
if (typeof V === 'symbol') {
throw webidl.errors.exception({
header: prefix,
message: `${argument} is a symbol, which cannot be converted to a ByteString.`
})
message: `${argument} is a symbol, which cannot be converted to a ByteString.`,
});
}
const x = String(V)
const x = String(V);
// 2. If the value of any element of x is greater than
// 255, then throw a TypeError.
@ -518,75 +543,96 @@ webidl.converters.ByteString = function (V, prefix, argument) {
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.`
)
`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
}
return x;
};
// https://webidl.spec.whatwg.org/#es-USVString
// TODO: rewrite this so we can control the errors thrown
webidl.converters.USVString = toUSVString
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)
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
}
return x;
};
// https://webidl.spec.whatwg.org/#es-any
webidl.converters.any = function (V) {
return 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)
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
}
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)
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
}
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)
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
}
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)
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
}
return x;
};
// https://webidl.spec.whatwg.org/#idl-ArrayBuffer
webidl.converters.ArrayBuffer = function (V, prefix, argument, opts) {
@ -595,15 +641,12 @@ webidl.converters.ArrayBuffer = function (V, prefix, argument, opts) {
// 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)
) {
if (webidl.util.Type(V) !== OBJECT || !types.isAnyArrayBuffer(V)) {
throw webidl.errors.conversionFailed({
prefix,
argument: `${argument} ("${webidl.util.Stringify(V)}")`,
types: ['ArrayBuffer']
})
types: ['ArrayBuffer'],
});
}
// 2. If the conversion is not to an IDL type associated
@ -613,8 +656,8 @@ webidl.converters.ArrayBuffer = function (V, prefix, argument, opts) {
if (opts?.allowShared === false && types.isSharedArrayBuffer(V)) {
throw webidl.errors.exception({
header: 'ArrayBuffer',
message: 'SharedArrayBuffer is not allowed.'
})
message: 'SharedArrayBuffer is not allowed.',
});
}
// 3. If the conversion is not to an IDL type associated
@ -624,14 +667,14 @@ webidl.converters.ArrayBuffer = function (V, prefix, argument, opts) {
if (V.resizable || V.growable) {
throw webidl.errors.exception({
header: 'ArrayBuffer',
message: 'Received a resizable ArrayBuffer.'
})
message: 'Received a resizable ArrayBuffer.',
});
}
// 4. Return the IDL ArrayBuffer value that is a
// reference to the same object as V.
return V
}
return V;
};
webidl.converters.TypedArray = function (V, T, prefix, name, opts) {
// 1. Let T be the IDL type V is being converted to.
@ -647,8 +690,8 @@ webidl.converters.TypedArray = function (V, T, prefix, name, opts) {
throw webidl.errors.conversionFailed({
prefix,
argument: `${name} ("${webidl.util.Stringify(V)}")`,
types: [T.name]
})
types: [T.name],
});
}
// 3. If the conversion is not to an IDL type associated
@ -658,8 +701,8 @@ webidl.converters.TypedArray = function (V, T, prefix, name, opts) {
if (opts?.allowShared === false && types.isSharedArrayBuffer(V.buffer)) {
throw webidl.errors.exception({
header: 'ArrayBuffer',
message: 'SharedArrayBuffer is not allowed.'
})
message: 'SharedArrayBuffer is not allowed.',
});
}
// 4. If the conversion is not to an IDL type associated
@ -669,14 +712,14 @@ webidl.converters.TypedArray = function (V, T, prefix, name, opts) {
if (V.buffer.resizable || V.buffer.growable) {
throw webidl.errors.exception({
header: 'ArrayBuffer',
message: 'Received a resizable 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
}
return V;
};
webidl.converters.DataView = function (V, prefix, name, opts) {
// 1. If Type(V) is not Object, or V does not have a
@ -684,8 +727,8 @@ webidl.converters.DataView = function (V, prefix, name, opts) {
if (webidl.util.Type(V) !== OBJECT || !types.isDataView(V)) {
throw webidl.errors.exception({
header: prefix,
message: `${name} is not a DataView.`
})
message: `${name} is not a DataView.`,
});
}
// 2. If the conversion is not to an IDL type associated
@ -695,8 +738,8 @@ webidl.converters.DataView = function (V, prefix, name, opts) {
if (opts?.allowShared === false && types.isSharedArrayBuffer(V.buffer)) {
throw webidl.errors.exception({
header: 'ArrayBuffer',
message: 'SharedArrayBuffer is not allowed.'
})
message: 'SharedArrayBuffer is not allowed.',
});
}
// 3. If the conversion is not to an IDL type associated
@ -706,35 +749,35 @@ webidl.converters.DataView = function (V, prefix, name, opts) {
if (V.buffer.resizable || V.buffer.growable) {
throw webidl.errors.exception({
header: 'ArrayBuffer',
message: 'Received a resizable ArrayBuffer.'
})
message: 'Received a resizable ArrayBuffer.',
});
}
// 4. Return the IDL DataView value that is a reference
// to the same object as V.
return 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.Blob = webidl.interfaceConverter(webidl.is.Blob, 'Blob');
webidl.converters.AbortSignal = webidl.interfaceConverter(
webidl.is.AbortSignal,
'AbortSignal'
)
);
module.exports = {
webidl
}
webidl,
};

View File

@ -1,23 +1,33 @@
'use strict'
'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')
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
let crypto;
try {
crypto = require('node:crypto')
/* c8 ignore next 3 */
} catch {
}
crypto = require('node:crypto');
/* c8 ignore next 3 */
} catch {}
/**
* @see https://websockets.spec.whatwg.org/#concept-websocket-establish
@ -26,12 +36,18 @@ try {
* @param {import('./websocket').Handler} handler
* @param {Partial<import('../../../types/websocket').WebSocketInit>} options
*/
function establishWebSocketConnection (url, protocols, client, handler, 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
const requestURL = url;
requestURL.protocol = url.protocol === 'ws:' ? 'http:' : 'https:'
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
@ -45,14 +61,14 @@ function establishWebSocketConnection (url, protocols, client, handler, options)
mode: 'websocket',
credentials: 'include',
cache: 'no-store',
redirect: 'error'
})
redirect: 'error',
});
// Note: undici extension, allow setting custom headers.
if (options.headers) {
const headersList = getHeadersList(new Headers(options.headers))
const headersList = getHeadersList(new Headers(options.headers));
request.headersList = headersList
request.headersList = headersList;
}
// 3. Append (`Upgrade`, `websocket`) to requests header list.
@ -63,31 +79,35 @@ function establishWebSocketConnection (url, protocols, client, handler, options)
// 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')
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)
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)
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)
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'
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)
request.headersList.append(
'sec-websocket-extensions',
permessageDeflate,
true
);
// 11. Fetch request with useParallelQueue set to true, and
// processResponse given response being these steps:
@ -95,27 +115,38 @@ function establishWebSocketConnection (url, protocols, client, handler, options)
request,
useParallelQueue: true,
dispatcher: options.dispatcher,
processResponse (response) {
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
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
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
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,
@ -129,8 +160,12 @@ function establishWebSocketConnection (url, protocols, client, handler, options)
// 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
failWebsocketConnection(
handler,
1002,
'Server did not set Upgrade header to "websocket".'
);
return;
}
// 3. If the response lacks a |Connection| header field or the
@ -138,8 +173,12 @@ function establishWebSocketConnection (url, protocols, client, handler, options)
// 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
failWebsocketConnection(
handler,
1002,
'Server did not set Connection header to "upgrade".'
);
return;
}
// 4. If the response lacks a |Sec-WebSocket-Accept| header field or
@ -149,11 +188,18 @@ function establishWebSocketConnection (url, protocols, client, handler, options)
// 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')
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
failWebsocketConnection(
handler,
1002,
'Incorrect hash received in Sec-WebSocket-Accept header.'
);
return;
}
// 5. If the response includes a |Sec-WebSocket-Extensions| header
@ -163,15 +209,19 @@ function establishWebSocketConnection (url, protocols, client, handler, options)
// 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
const secExtension = response.headersList.get('Sec-WebSocket-Extensions');
let extensions;
if (secExtension !== null) {
extensions = parseExtensions(secExtension)
extensions = parseExtensions(secExtension);
if (!extensions.has('permessage-deflate')) {
failWebsocketConnection(handler, 1002, 'Sec-WebSocket-Extensions header does not match.')
return
failWebsocketConnection(
handler,
1002,
'Sec-WebSocket-Extensions header does not match.'
);
return;
}
}
@ -180,10 +230,13 @@ function establishWebSocketConnection (url, protocols, client, handler, options)
// 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')
const secProtocol = response.headersList.get('Sec-WebSocket-Protocol');
if (secProtocol !== null) {
const requestProtocols = getDecodeSplit('sec-websocket-protocol', request.headersList)
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
@ -191,29 +244,33 @@ function establishWebSocketConnection (url, protocols, client, handler, options)
// 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
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)
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
})
extensions: secExtension,
});
}
handler.wasEverConnected = true
handler.onConnectionEstablished(response, extensions)
}
})
handler.wasEverConnected = true;
handler.onConnectionEstablished(response, extensions);
},
});
return controller
return controller;
}
/**
@ -222,15 +279,15 @@ function establishWebSocketConnection (url, protocols, client, handler, options)
* @param {number} [code=null]
* @param {string} [reason='']
*/
function closeWebSocketConnection (object, code, reason, validate = false) {
function closeWebSocketConnection(object, code, reason, validate = false) {
// 1. If code was not supplied, let code be null.
code ??= null
code ??= null;
// 2. If reason was not supplied, let reason be the empty string.
reason ??= ''
reason ??= '';
// 3. Validate close code and reason with code and reason.
if (validate) validateCloseCodeAndReason(code, 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)
@ -241,14 +298,17 @@ function closeWebSocketConnection (object, code, reason, validate = false) {
// 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)) {
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()
const frame = new WebsocketFrameSend();
// If neither code nor reason is present, the WebSocket Close
// message must not have a body.
@ -258,39 +318,39 @@ function closeWebSocketConnection (object, code, reason, validate = false) {
// 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
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))
assert(code === null || Number.isInteger(code));
if (code === null && reason.length === 0) {
frame.frameData = emptyBuffer
frame.frameData = emptyBuffer;
} else if (code !== null && reason === null) {
frame.frameData = Buffer.allocUnsafe(2)
frame.frameData.writeUInt16BE(code, 0)
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)
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')
frame.frameData.write(reason, 2, 'utf-8');
} else {
frame.frameData = emptyBuffer
frame.frameData = emptyBuffer;
}
object.socket.write(frame.createFrame(opcodes.CLOSE))
object.socket.write(frame.createFrame(opcodes.CLOSE));
object.closeState.add(sentCloseFrameState.SENT)
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
object.readyState = states.CLOSING;
} else {
// Set objects ready state to CLOSING (2).
object.readyState = states.CLOSING
object.readyState = states.CLOSING;
}
}
@ -300,26 +360,26 @@ function closeWebSocketConnection (object, code, reason, validate = false) {
* @param {string|undefined} reason
* @returns {void}
*/
function failWebsocketConnection (handler, code, reason) {
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)
closeWebSocketConnection(handler, code, reason, false);
}
handler.controller.abort()
handler.controller.abort();
if (handler.socket?.destroyed === false) {
handler.socket.destroy()
handler.socket.destroy();
}
handler.onFail(code, reason)
handler.onFail(code, reason);
}
module.exports = {
establishWebSocketConnection,
failWebsocketConnection,
closeWebSocketConnection
}
closeWebSocketConnection,
};

View File

@ -1,4 +1,4 @@
'use strict'
'use strict';
/**
* This is a Globally Unique Identifier unique used to validate that the
@ -6,7 +6,7 @@
* @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'
const uid = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
/**
* @type {PropertyDescriptor}
@ -14,8 +14,8 @@ const uid = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'
const staticPropertyDescriptors = {
enumerable: true,
writable: false,
configurable: false
}
configurable: false,
};
/**
* The states of the WebSocket connection.
@ -31,8 +31,8 @@ const states = {
CONNECTING: 0,
OPEN: 1,
CLOSING: 2,
CLOSED: 3
}
CLOSED: 3,
};
/**
* @readonly
@ -43,8 +43,8 @@ const states = {
*/
const sentCloseFrameState = {
SENT: 1,
RECEIVED: 2
}
RECEIVED: 2,
};
/**
* The WebSocket opcodes.
@ -65,15 +65,15 @@ const opcodes = {
BINARY: 0x2,
CLOSE: 0x8,
PING: 0x9,
PONG: 0xA
}
PONG: 0xa,
};
/**
* The maximum value for an unsigned 16-bit integer.
*
* @type {65535} 2 ** 16 - 1
*/
const maxUnsigned16Bit = 65535
const maxUnsigned16Bit = 65535;
/**
* The states of the parser.
@ -89,15 +89,15 @@ const parserStates = {
INFO: 0,
PAYLOADLENGTH_16: 2,
PAYLOADLENGTH_64: 3,
READ_DATA: 4
}
READ_DATA: 4,
};
/**
* An empty buffer.
*
* @type {Buffer}
*/
const emptyBuffer = Buffer.allocUnsafe(0)
const emptyBuffer = Buffer.allocUnsafe(0);
/**
* @readonly
@ -110,8 +110,8 @@ const sendHints = {
text: 1,
typedArray: 2,
arrayBuffer: 3,
blob: 4
}
blob: 4,
};
module.exports = {
uid,
@ -122,5 +122,5 @@ module.exports = {
maxUnsigned16Bit,
parserStates,
emptyBuffer,
sendHints
}
sendHints,
};

View File

@ -1,69 +1,73 @@
'use strict'
'use strict';
const { webidl } = require('../fetch/webidl')
const { kEnumerableProperty } = require('../../core/util')
const { kConstruct } = require('../../core/symbols')
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
#eventInit;
constructor (type, eventInitDict = {}) {
constructor(type, eventInitDict = {}) {
if (type === kConstruct) {
super(arguments[1], arguments[2])
webidl.util.markAsUncloneable(this)
return
super(arguments[1], arguments[2]);
webidl.util.markAsUncloneable(this);
return;
}
const prefix = 'MessageEvent constructor'
webidl.argumentLengthCheck(arguments, 1, prefix)
const prefix = 'MessageEvent constructor';
webidl.argumentLengthCheck(arguments, 1, prefix);
type = webidl.converters.DOMString(type, prefix, 'type')
eventInitDict = webidl.converters.MessageEventInit(eventInitDict, prefix, 'eventInitDict')
type = webidl.converters.DOMString(type, prefix, 'type');
eventInitDict = webidl.converters.MessageEventInit(
eventInitDict,
prefix,
'eventInitDict'
);
super(type, eventInitDict)
super(type, eventInitDict);
this.#eventInit = eventInitDict
webidl.util.markAsUncloneable(this)
this.#eventInit = eventInitDict;
webidl.util.markAsUncloneable(this);
}
get data () {
webidl.brandCheck(this, MessageEvent)
get data() {
webidl.brandCheck(this, MessageEvent);
return this.#eventInit.data
return this.#eventInit.data;
}
get origin () {
webidl.brandCheck(this, MessageEvent)
get origin() {
webidl.brandCheck(this, MessageEvent);
return this.#eventInit.origin
return this.#eventInit.origin;
}
get lastEventId () {
webidl.brandCheck(this, MessageEvent)
get lastEventId() {
webidl.brandCheck(this, MessageEvent);
return this.#eventInit.lastEventId
return this.#eventInit.lastEventId;
}
get source () {
webidl.brandCheck(this, MessageEvent)
get source() {
webidl.brandCheck(this, MessageEvent);
return this.#eventInit.source
return this.#eventInit.source;
}
get ports () {
webidl.brandCheck(this, MessageEvent)
get ports() {
webidl.brandCheck(this, MessageEvent);
if (!Object.isFrozen(this.#eventInit.ports)) {
Object.freeze(this.#eventInit.ports)
Object.freeze(this.#eventInit.ports);
}
return this.#eventInit.ports
return this.#eventInit.ports;
}
initMessageEvent (
initMessageEvent(
type,
bubbles = false,
cancelable = false,
@ -73,259 +77,265 @@ class MessageEvent extends Event {
source = null,
ports = []
) {
webidl.brandCheck(this, MessageEvent)
webidl.brandCheck(this, MessageEvent);
webidl.argumentLengthCheck(arguments, 1, 'MessageEvent.initMessageEvent')
webidl.argumentLengthCheck(arguments, 1, 'MessageEvent.initMessageEvent');
return new MessageEvent(type, {
bubbles, cancelable, data, origin, lastEventId, source, ports
})
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
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
const { createFastMessageEvent } = MessageEvent;
delete MessageEvent.createFastMessageEvent;
/**
* @see https://websockets.spec.whatwg.org/#the-closeevent-interface
*/
class CloseEvent extends Event {
#eventInit
#eventInit;
constructor (type, eventInitDict = {}) {
const prefix = 'CloseEvent constructor'
webidl.argumentLengthCheck(arguments, 1, prefix)
constructor(type, eventInitDict = {}) {
const prefix = 'CloseEvent constructor';
webidl.argumentLengthCheck(arguments, 1, prefix);
type = webidl.converters.DOMString(type, prefix, 'type')
eventInitDict = webidl.converters.CloseEventInit(eventInitDict)
type = webidl.converters.DOMString(type, prefix, 'type');
eventInitDict = webidl.converters.CloseEventInit(eventInitDict);
super(type, eventInitDict)
super(type, eventInitDict);
this.#eventInit = eventInitDict
webidl.util.markAsUncloneable(this)
this.#eventInit = eventInitDict;
webidl.util.markAsUncloneable(this);
}
get wasClean () {
webidl.brandCheck(this, CloseEvent)
get wasClean() {
webidl.brandCheck(this, CloseEvent);
return this.#eventInit.wasClean
return this.#eventInit.wasClean;
}
get code () {
webidl.brandCheck(this, CloseEvent)
get code() {
webidl.brandCheck(this, CloseEvent);
return this.#eventInit.code
return this.#eventInit.code;
}
get reason () {
webidl.brandCheck(this, CloseEvent)
get reason() {
webidl.brandCheck(this, CloseEvent);
return this.#eventInit.reason
return this.#eventInit.reason;
}
}
// https://html.spec.whatwg.org/multipage/webappapis.html#the-errorevent-interface
class ErrorEvent extends Event {
#eventInit
#eventInit;
constructor (type, eventInitDict) {
const prefix = 'ErrorEvent constructor'
webidl.argumentLengthCheck(arguments, 1, prefix)
constructor(type, eventInitDict) {
const prefix = 'ErrorEvent constructor';
webidl.argumentLengthCheck(arguments, 1, prefix);
super(type, eventInitDict)
webidl.util.markAsUncloneable(this)
super(type, eventInitDict);
webidl.util.markAsUncloneable(this);
type = webidl.converters.DOMString(type, prefix, 'type')
eventInitDict = webidl.converters.ErrorEventInit(eventInitDict ?? {})
type = webidl.converters.DOMString(type, prefix, 'type');
eventInitDict = webidl.converters.ErrorEventInit(eventInitDict ?? {});
this.#eventInit = eventInitDict
this.#eventInit = eventInitDict;
}
get message () {
webidl.brandCheck(this, ErrorEvent)
get message() {
webidl.brandCheck(this, ErrorEvent);
return this.#eventInit.message
return this.#eventInit.message;
}
get filename () {
webidl.brandCheck(this, ErrorEvent)
get filename() {
webidl.brandCheck(this, ErrorEvent);
return this.#eventInit.filename
return this.#eventInit.filename;
}
get lineno () {
webidl.brandCheck(this, ErrorEvent)
get lineno() {
webidl.brandCheck(this, ErrorEvent);
return this.#eventInit.lineno
return this.#eventInit.lineno;
}
get colno () {
webidl.brandCheck(this, ErrorEvent)
get colno() {
webidl.brandCheck(this, ErrorEvent);
return this.#eventInit.colno
return this.#eventInit.colno;
}
get error () {
webidl.brandCheck(this, ErrorEvent)
get error() {
webidl.brandCheck(this, ErrorEvent);
return this.#eventInit.error
return this.#eventInit.error;
}
}
Object.defineProperties(MessageEvent.prototype, {
[Symbol.toStringTag]: {
value: 'MessageEvent',
configurable: true
configurable: true,
},
data: kEnumerableProperty,
origin: kEnumerableProperty,
lastEventId: kEnumerableProperty,
source: kEnumerableProperty,
ports: kEnumerableProperty,
initMessageEvent: kEnumerableProperty
})
initMessageEvent: kEnumerableProperty,
});
Object.defineProperties(CloseEvent.prototype, {
[Symbol.toStringTag]: {
value: 'CloseEvent',
configurable: true
configurable: true,
},
reason: kEnumerableProperty,
code: kEnumerableProperty,
wasClean: kEnumerableProperty
})
wasClean: kEnumerableProperty,
});
Object.defineProperties(ErrorEvent.prototype, {
[Symbol.toStringTag]: {
value: 'ErrorEvent',
configurable: true
configurable: true,
},
message: kEnumerableProperty,
filename: kEnumerableProperty,
lineno: kEnumerableProperty,
colno: kEnumerableProperty,
error: 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
defaultValue: () => false,
},
{
key: 'cancelable',
converter: webidl.converters.boolean,
defaultValue: () => false
defaultValue: () => false,
},
{
key: 'composed',
converter: webidl.converters.boolean,
defaultValue: () => false
}
]
defaultValue: () => false,
},
];
webidl.converters.MessageEventInit = webidl.dictionaryConverter([
...eventInit,
{
key: 'data',
converter: webidl.converters.any,
defaultValue: () => null
defaultValue: () => null,
},
{
key: 'origin',
converter: webidl.converters.USVString,
defaultValue: () => ''
defaultValue: () => '',
},
{
key: 'lastEventId',
converter: webidl.converters.DOMString,
defaultValue: () => ''
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
defaultValue: () => null,
},
{
key: 'ports',
converter: webidl.converters['sequence<MessagePort>'],
defaultValue: () => new Array(0)
}
])
defaultValue: () => new Array(0),
},
]);
webidl.converters.CloseEventInit = webidl.dictionaryConverter([
...eventInit,
{
key: 'wasClean',
converter: webidl.converters.boolean,
defaultValue: () => false
defaultValue: () => false,
},
{
key: 'code',
converter: webidl.converters['unsigned short'],
defaultValue: () => 0
defaultValue: () => 0,
},
{
key: 'reason',
converter: webidl.converters.USVString,
defaultValue: () => ''
}
])
defaultValue: () => '',
},
]);
webidl.converters.ErrorEventInit = webidl.dictionaryConverter([
...eventInit,
{
key: 'message',
converter: webidl.converters.DOMString,
defaultValue: () => ''
defaultValue: () => '',
},
{
key: 'filename',
converter: webidl.converters.USVString,
defaultValue: () => ''
defaultValue: () => '',
},
{
key: 'lineno',
converter: webidl.converters['unsigned long'],
defaultValue: () => 0
defaultValue: () => 0,
},
{
key: 'colno',
converter: webidl.converters['unsigned long'],
defaultValue: () => 0
defaultValue: () => 0,
},
{
key: 'error',
converter: webidl.converters.any
}
])
converter: webidl.converters.any,
},
]);
module.exports = {
MessageEvent,
CloseEvent,
ErrorEvent,
createFastMessageEvent
}
createFastMessageEvent,
};

View File

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

View File

@ -1,70 +1,78 @@
'use strict'
'use strict';
const { createInflateRaw, Z_DEFAULT_WINDOWBITS } = require('node:zlib')
const { isValidClientWindowBits } = require('./util')
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')
const tail = Buffer.from([0x00, 0x00, 0xff, 0xff]);
const kBuffer = Symbol('kBuffer');
const kLength = Symbol('kLength');
class PerMessageDeflate {
/** @type {import('node:zlib').InflateRaw} */
#inflate
#inflate;
#options = {}
#options = {};
constructor (extensions) {
this.#options.serverNoContextTakeover = extensions.has('server_no_context_takeover')
this.#options.serverMaxWindowBits = extensions.get('server_max_window_bits')
constructor(extensions) {
this.#options.serverNoContextTakeover = extensions.has(
'server_no_context_takeover'
);
this.#options.serverMaxWindowBits = extensions.get(
'server_max_window_bits'
);
}
decompress (chunk, fin, callback) {
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
let windowBits = Z_DEFAULT_WINDOWBITS;
if (this.#options.serverMaxWindowBits) { // empty values default to 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
callback(new Error('Invalid server_max_window_bits'));
return;
}
windowBits = Number.parseInt(this.#options.serverMaxWindowBits)
windowBits = Number.parseInt(this.#options.serverMaxWindowBits);
}
this.#inflate = createInflateRaw({ windowBits })
this.#inflate[kBuffer] = []
this.#inflate[kLength] = 0
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[kBuffer].push(data);
this.#inflate[kLength] += data.length;
});
this.#inflate.on('error', (err) => {
this.#inflate = null
callback(err)
})
this.#inflate = null;
callback(err);
});
}
this.#inflate.write(chunk)
this.#inflate.write(chunk);
if (fin) {
this.#inflate.write(tail)
this.#inflate.write(tail);
}
this.#inflate.flush(() => {
const full = Buffer.concat(this.#inflate[kBuffer], this.#inflate[kLength])
const full = Buffer.concat(
this.#inflate[kBuffer],
this.#inflate[kLength]
);
this.#inflate[kBuffer].length = 0
this.#inflate[kLength] = 0
this.#inflate[kBuffer].length = 0;
this.#inflate[kLength] = 0;
callback(null, full)
})
callback(null, full);
});
}
}
module.exports = { PerMessageDeflate }
module.exports = { PerMessageDeflate };

View File

@ -1,9 +1,15 @@
'use strict'
'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 { 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,
@ -11,11 +17,11 @@ const {
utf8Decode,
isControlFrame,
isTextBinaryFrame,
isContinuationFrame
} = require('./util')
const { failWebsocketConnection } = require('./connection')
const { WebsocketFrameSend } = require('./frame')
const { PerMessageDeflate } = require('./permessage-deflate')
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>
@ -23,30 +29,33 @@ const { PerMessageDeflate } = require('./permessage-deflate')
// Copyright (c) 2016 Luigi Pinca and contributors
class ByteParser extends Writable {
#buffers = []
#fragmentsBytes = 0
#byteOffset = 0
#loop = false
#buffers = [];
#fragmentsBytes = 0;
#byteOffset = 0;
#loop = false;
#state = parserStates.INFO
#state = parserStates.INFO;
#info = {}
#fragments = []
#info = {};
#fragments = [];
/** @type {Map<string, PerMessageDeflate>} */
#extensions
#extensions;
/** @type {import('./websocket').Handler} */
#handler
#handler;
constructor (handler, extensions) {
super()
constructor(handler, extensions) {
super();
this.#handler = handler
this.#extensions = extensions == null ? new Map() : extensions
this.#handler = handler;
this.#extensions = extensions == null ? new Map() : extensions;
if (this.#extensions.has('permessage-deflate')) {
this.#extensions.set('permessage-deflate', new PerMessageDeflate(extensions))
this.#extensions.set(
'permessage-deflate',
new PerMessageDeflate(extensions)
);
}
}
@ -54,12 +63,12 @@ class ByteParser extends Writable {
* @param {Buffer} chunk
* @param {() => void} callback
*/
_write (chunk, _, callback) {
this.#buffers.push(chunk)
this.#byteOffset += chunk.length
this.#loop = true
_write(chunk, _, callback) {
this.#buffers.push(chunk);
this.#byteOffset += chunk.length;
this.#loop = true;
this.run(callback)
this.run(callback);
}
/**
@ -67,34 +76,42 @@ class ByteParser extends Writable {
* Callback is called whenever there are no more chunks buffering,
* or not enough bytes are buffered to parse.
*/
run (callback) {
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()
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 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 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
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()
failWebsocketConnection(
this.#handler,
1002,
'Invalid opcode received'
);
return callback();
}
if (masked) {
failWebsocketConnection(this.#handler, 1002, 'Frame cannot be masked')
return callback()
failWebsocketConnection(
this.#handler,
1002,
'Frame cannot be masked'
);
return callback();
}
// MUST be 0 unless an extension is negotiated that defines meanings
@ -107,80 +124,112 @@ class ByteParser extends Writable {
// 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
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
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
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
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
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
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 (
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
this.#info.payloadLength = payloadLength;
this.#state = parserStates.READ_DATA;
} else if (payloadLength === 126) {
this.#state = parserStates.PAYLOADLENGTH_16
this.#state = parserStates.PAYLOADLENGTH_16;
} else if (payloadLength === 127) {
this.#state = parserStates.PAYLOADLENGTH_64
this.#state = parserStates.PAYLOADLENGTH_64;
}
if (isTextBinaryFrame(opcode)) {
this.#info.binaryType = opcode
this.#info.compressed = rsv1 !== 0
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
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()
return callback();
}
const buffer = this.consume(2)
const buffer = this.consume(2);
this.#info.payloadLength = buffer.readUInt16BE(0)
this.#state = parserStates.READ_DATA
this.#info.payloadLength = buffer.readUInt16BE(0);
this.#state = parserStates.READ_DATA;
} else if (this.#state === parserStates.PAYLOADLENGTH_64) {
if (this.#byteOffset < 8) {
return callback()
return callback();
}
const buffer = this.consume(8)
const upper = buffer.readUInt32BE(0)
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
@ -189,62 +238,76 @@ class ByteParser extends Writable {
// 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
failWebsocketConnection(
this.#handler,
1009,
'Received payload length > 2^31 bytes.'
);
return;
}
const lower = buffer.readUInt32BE(4)
const lower = buffer.readUInt32BE(4);
this.#info.payloadLength = (upper << 8) + lower
this.#state = parserStates.READ_DATA
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()
return callback();
}
const body = this.consume(this.#info.payloadLength)
const body = this.consume(this.#info.payloadLength);
if (isControlFrame(this.#info.opcode)) {
this.#loop = this.parseControlFrame(body)
this.#state = parserStates.INFO
this.#loop = this.parseControlFrame(body);
this.#state = parserStates.INFO;
} else {
if (!this.#info.compressed) {
this.writeFragments(body)
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())
websocketMessageReceived(
this.#handler,
this.#info.binaryType,
this.consumeFragments()
);
}
this.#state = parserStates.INFO
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.#extensions
.get('permessage-deflate')
.decompress(body, this.#info.fin, (error, data) => {
if (error) {
failWebsocketConnection(this.#handler, 1007, error.message);
return;
}
this.writeFragments(data)
this.writeFragments(data);
if (!this.#info.fin) {
this.#state = parserStates.INFO
this.#loop = true
this.run(callback)
return
}
if (!this.#info.fin) {
this.#state = parserStates.INFO;
this.#loop = true;
this.run(callback);
return;
}
websocketMessageReceived(this.#handler, this.#info.binaryType, this.consumeFragments())
websocketMessageReceived(
this.#handler,
this.#info.binaryType,
this.consumeFragments()
);
this.#loop = true
this.#state = parserStates.INFO
this.run(callback)
})
this.#loop = true;
this.#state = parserStates.INFO;
this.run(callback);
});
this.#loop = false
break
this.#loop = false;
break;
}
}
}
@ -256,162 +319,169 @@ class ByteParser extends Writable {
* @param {number} n
* @returns {Buffer}
*/
consume (n) {
consume(n) {
if (n > this.#byteOffset) {
throw new Error('Called consume() before buffers satiated.')
throw new Error('Called consume() before buffers satiated.');
} else if (n === 0) {
return emptyBuffer
return emptyBuffer;
}
this.#byteOffset -= n
this.#byteOffset -= n;
const first = this.#buffers[0]
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)
this.#buffers[0] = first.subarray(n, first.length);
return first.subarray(0, n);
} else if (first.length === n) {
// prefect match
return this.#buffers.shift()
return this.#buffers.shift();
} else {
let offset = 0
let offset = 0;
// If Buffer.allocUnsafe is used, extra copies will be made because the offset is non-zero.
const buffer = Buffer.allocUnsafeSlow(n)
const buffer = Buffer.allocUnsafeSlow(n);
while (offset !== n) {
const next = this.#buffers[0]
const length = next.length
const next = this.#buffers[0];
const length = next.length;
if (length + offset === n) {
buffer.set(this.#buffers.shift(), offset)
break
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
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
buffer.set(this.#buffers.shift(), offset);
offset += length;
}
}
return buffer
return buffer;
}
}
writeFragments (fragment) {
this.#fragmentsBytes += fragment.length
this.#fragments.push(fragment)
writeFragments(fragment) {
this.#fragmentsBytes += fragment.length;
this.#fragments.push(fragment);
}
consumeFragments () {
const fragments = this.#fragments
consumeFragments() {
const fragments = this.#fragments;
if (fragments.length === 1) {
// single fragment
this.#fragmentsBytes = 0
return fragments.shift()
this.#fragmentsBytes = 0;
return fragments.shift();
}
let offset = 0
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)
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
const buffer = fragments[i];
output.set(buffer, offset);
offset += buffer.length;
}
this.#fragments = []
this.#fragmentsBytes = 0
this.#fragments = [];
this.#fragmentsBytes = 0;
return output
return output;
}
parseCloseBody (data) {
assert(data.length !== 1)
parseCloseBody(data) {
assert(data.length !== 1);
// https://datatracker.ietf.org/doc/html/rfc6455#section-7.1.5
/** @type {number|undefined} */
let code
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)
code = data.readUInt16BE(0);
}
if (code !== undefined && !isValidStatusCode(code)) {
return { code: 1002, reason: 'Invalid status code', error: true }
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)
let reason = data.subarray(2);
// Remove BOM
if (reason[0] === 0xEF && reason[1] === 0xBB && reason[2] === 0xBF) {
reason = reason.subarray(3)
if (reason[0] === 0xef && reason[1] === 0xbb && reason[2] === 0xbf) {
reason = reason.subarray(3);
}
try {
reason = utf8Decode(reason)
reason = utf8Decode(reason);
} catch {
return { code: 1007, reason: 'Invalid UTF-8', error: true }
return { code: 1007, reason: 'Invalid UTF-8', error: true };
}
return { code, reason, error: false }
return { code, reason, error: false };
}
/**
* Parses control frames.
* @param {Buffer} body
*/
parseControlFrame (body) {
const { opcode, payloadLength } = this.#info
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
failWebsocketConnection(
this.#handler,
1002,
'Received close frame with a 1-byte body.'
);
return false;
}
this.#info.closeInfo = this.parseCloseBody(body)
this.#info.closeInfo = this.parseCloseBody(body);
if (this.#info.closeInfo.error) {
const { code, reason } = this.#info.closeInfo
const { code, reason } = this.#info.closeInfo;
failWebsocketConnection(this.#handler, code, reason)
return false
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 (
!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
let body = emptyBuffer;
if (this.#info.closeInfo.code) {
body = Buffer.allocUnsafe(2)
body.writeUInt16BE(this.#info.closeInfo.code, 0)
body = Buffer.allocUnsafe(2);
body.writeUInt16BE(this.#info.closeInfo.code, 0);
}
const closeFrame = new WebsocketFrameSend(body)
const closeFrame = new WebsocketFrameSend(body);
this.#handler.socket.write(closeFrame.createFrame(opcodes.CLOSE))
this.#handler.closeState.add(sentCloseFrameState.SENT)
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)
this.#handler.readyState = states.CLOSING;
this.#handler.closeState.add(sentCloseFrameState.RECEIVED);
return false
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.
@ -419,14 +489,14 @@ class ByteParser extends Writable {
// "Application data"
if (!this.#handler.closeState.has(sentCloseFrameState.RECEIVED)) {
const frame = new WebsocketFrameSend(body)
const frame = new WebsocketFrameSend(body);
this.#handler.socket.write(frame.createFrame(opcodes.PONG))
this.#handler.socket.write(frame.createFrame(opcodes.PONG));
if (channels.ping.hasSubscribers) {
channels.ping.publish({
payload: body
})
payload: body,
});
}
}
} else if (opcode === opcodes.PONG) {
@ -436,19 +506,19 @@ class ByteParser extends Writable {
if (channels.pong.hasSubscribers) {
channels.pong.publish({
payload: body
})
payload: body,
});
}
}
return true
return true;
}
get closingInfo () {
return this.#info.closeInfo
get closingInfo() {
return this.#info.closeInfo;
}
}
module.exports = {
ByteParser
}
ByteParser,
};

View File

@ -1,8 +1,8 @@
'use strict'
'use strict';
const { WebsocketFrameSend } = require('./frame')
const { opcodes, sendHints } = require('./constants')
const FixedQueue = require('../../dispatcher/fixed-queue')
const { WebsocketFrameSend } = require('./frame');
const { opcodes, sendHints } = require('./constants');
const FixedQueue = require('../../dispatcher/fixed-queue');
/**
* @typedef {object} SendQueueNode
@ -15,95 +15,98 @@ class SendQueue {
/**
* @type {FixedQueue}
*/
#queue = new FixedQueue()
#queue = new FixedQueue();
/**
* @type {boolean}
*/
#running = false
#running = false;
/** @type {import('node:net').Socket} */
#socket
#socket;
constructor (socket) {
this.#socket = socket
constructor(socket) {
this.#socket = socket;
}
add (item, cb, hint) {
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()
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)
this.#socket.write(createFrame(item, hint), cb);
}
} else {
/** @type {SendQueueNode} */
const node = {
promise: null,
callback: cb,
frame: createFrame(item, hint)
}
this.#queue.push(node)
frame: createFrame(item, hint),
};
this.#queue.push(node);
}
return
return;
}
/** @type {SendQueueNode} */
const node = {
promise: item.arrayBuffer().then((ab) => {
node.promise = null
node.frame = createFrame(ab, hint)
node.promise = null;
node.frame = createFrame(ab, hint);
}),
callback: cb,
frame: null
}
frame: null,
};
this.#queue.push(node)
this.#queue.push(node);
if (!this.#running) {
this.#run()
this.#run();
}
}
async #run () {
this.#running = true
const queue = this.#queue
async #run() {
this.#running = true;
const queue = this.#queue;
while (!queue.isEmpty()) {
const node = queue.shift()
const node = queue.shift();
// wait pending promise
if (node.promise !== null) {
await node.promise
await node.promise;
}
// write
this.#socket.write(node.frame, node.callback)
this.#socket.write(node.frame, node.callback);
// cleanup
node.callback = node.frame = null
node.callback = node.frame = null;
}
this.#running = false
this.#running = false;
}
}
function createFrame (data, hint) {
return new WebsocketFrameSend(toBuffer(data, hint)).createFrame(hint === sendHints.text ? opcodes.TEXT : opcodes.BINARY)
function createFrame(data, hint) {
return new WebsocketFrameSend(toBuffer(data, hint)).createFrame(
hint === sendHints.text ? opcodes.TEXT : opcodes.BINARY
);
}
function toBuffer (data, hint) {
function toBuffer(data, hint) {
switch (hint) {
case sendHints.text:
case sendHints.typedArray:
return new Uint8Array(data.buffer, data.byteOffset, data.byteLength)
return new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
case sendHints.arrayBuffer:
case sendHints.blob:
return new Uint8Array(data)
return new Uint8Array(data);
}
}
module.exports = { SendQueue }
module.exports = { SendQueue };

View File

@ -1,54 +1,54 @@
'use strict'
'use strict';
const { webidl } = require('../../fetch/webidl')
const { validateCloseCodeAndReason } = require('../util')
const { kConstruct } = require('../../../core/symbols')
const { kEnumerableProperty } = require('../../../core/util')
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
#closeCode;
#reason;
constructor (message = '', init = undefined) {
message = webidl.converters.DOMString(message, 'WebSocketError', 'message')
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')
super(message, 'WebSocketError');
if (init === kConstruct) {
return
return;
} else if (init !== null) {
init = webidl.converters.WebSocketCloseInfo(init)
init = webidl.converters.WebSocketCloseInfo(init);
}
// 3. Let code be init [" closeCode "] if it exists , or null otherwise.
let code = init.closeCode ?? null
let code = init.closeCode ?? null;
// 4. Let reason be init [" reason "] if it exists , or the empty string otherwise.
const reason = init.reason ?? ''
const reason = init.reason ?? '';
// 5. Validate close code and reason with code and reason .
validateCloseCodeAndReason(code, 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
code = 1000;
}
// 7. Set this 's closeCode to code .
this.#closeCode = code
this.#closeCode = code;
// 8. Set this 's reason to reason .
this.#reason = reason
this.#reason = reason;
}
get closeCode () {
return this.#closeCode
get closeCode() {
return this.#closeCode;
}
get reason () {
return this.#reason
get reason() {
return this.#reason;
}
/**
@ -56,16 +56,16 @@ class WebSocketError extends DOMException {
* @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
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
const { createUnvalidatedWebSocketError } = WebSocketError;
delete WebSocketError.createUnvalidatedWebSocketError;
Object.defineProperties(WebSocketError.prototype, {
closeCode: kEnumerableProperty,
@ -74,10 +74,10 @@ Object.defineProperties(WebSocketError.prototype, {
value: 'WebSocketError',
writable: false,
enumerable: false,
configurable: true
}
})
configurable: true,
},
});
webidl.is.WebSocketError = webidl.util.MakeTypeAssertion(WebSocketError)
webidl.is.WebSocketError = webidl.util.MakeTypeAssertion(WebSocketError);
module.exports = { WebSocketError, createUnvalidatedWebSocketError }
module.exports = { WebSocketError, createUnvalidatedWebSocketError };

View File

@ -1,67 +1,84 @@
'use strict'
'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')
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
let emittedExperimentalWarning = false;
class WebSocketStream {
// Each WebSocketStream object has an associated url , which is a URL record .
/** @type {URL} */
#url
#url;
// Each WebSocketStream object has an associated opened promise , which is a promise.
/** @type {ReturnType<typeof createDeferredPromise>} */
#openedPromise
#openedPromise;
// Each WebSocketStream object has an associated closed promise , which is a promise.
/** @type {ReturnType<typeof createDeferredPromise>} */
#closedPromise
#closedPromise;
// Each WebSocketStream object has an associated readable stream , which is a ReadableStream .
/** @type {ReadableStream} */
#readableStream
#readableStream;
/** @type {ReadableStreamDefaultController} */
#readableStreamController
#readableStreamController;
// Each WebSocketStream object has an associated writable stream , which is a WritableStream .
/** @type {WritableStream} */
#writableStream
#writableStream;
// Each WebSocketStream object has an associated boolean handshake aborted , which is initially false.
#handshakeAborted = 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),
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),
onParserError: (err) =>
failWebsocketConnection(this.#handler, null, err.message),
onParserDrain: () => this.#handler.socket.resume(),
onSocketData: (chunk) => {
if (!this.#parser.write(chunk)) {
this.#handler.socket.pause()
this.#handler.socket.pause();
}
},
onSocketError: (err) => {
this.#handler.readyState = states.CLOSING
this.#handler.readyState = states.CLOSING;
if (channels.socketError.hasSubscribers) {
channels.socketError.publish(err)
channels.socketError.publish(err);
}
this.#handler.socket.destroy()
this.#handler.socket.destroy();
},
onSocketClose: () => this.#onSocketClose(),
@ -69,51 +86,65 @@ class WebSocketStream {
socket: null,
closeState: new Set(),
controller: null,
wasEverConnected: false
}
wasEverConnected: false,
};
/** @type {import('../receiver').ByteParser} */
#parser
#parser;
constructor (url, options = undefined) {
constructor(url, options = undefined) {
if (!emittedExperimentalWarning) {
process.emitWarning('WebSocketStream is experimental! Expect it to change at any time.', {
code: 'UNDICI-WSS'
})
emittedExperimentalWarning = true
process.emitWarning(
'WebSocketStream is experimental! Expect it to change at any time.',
{
code: 'UNDICI-WSS',
}
);
emittedExperimentalWarning = true;
}
webidl.argumentLengthCheck(arguments, 1, 'WebSocket')
webidl.argumentLengthCheck(arguments, 1, 'WebSocket');
url = webidl.converters.USVString(url)
url = webidl.converters.USVString(url);
if (options !== null) {
options = webidl.converters.WebSocketStreamOptions(options)
options = webidl.converters.WebSocketStreamOptions(options);
}
// 1. Let baseURL be this 's relevant settings object 's API base URL .
const baseURL = environmentSettingsObject.settingsObject.baseUrl
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)
const urlRecord = getURLRecord(url, baseURL);
// 3. Let protocols be options [" protocols "] if it exists , otherwise an empty sequence.
const protocols = options.protocols
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 !== 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')
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()
this.#url = urlRecord.toString();
// 6. Set this 's opened promise and closed promise to new promises.
this.#openedPromise = createDeferredPromise()
this.#closedPromise = createDeferredPromise()
this.#openedPromise = createDeferredPromise();
this.#closedPromise = createDeferredPromise();
// 7. Apply backpressure to the WebSocket.
// TODO
@ -121,38 +152,42 @@ class WebSocketStream {
// 8. If options [" signal "] exists ,
if (options.signal != null) {
// 8.1. Let signal be options [" signal "].
const signal = 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
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)
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
// 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)
// 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 })
// 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
const client = environmentSettingsObject.settingsObject;
// 10. Run this step in parallel :
// 10.1. Establish a WebSocket connection given urlRecord , protocols , and client . [FETCH]
@ -162,115 +197,122 @@ class WebSocketStream {
client,
this.#handler,
options
)
);
}
// The url getter steps are to return this 's url , serialized .
get url () {
return this.#url.toString()
get url() {
return this.#url.toString();
}
// The opened getter steps are to return this 's opened promise .
get opened () {
return this.#openedPromise.promise
get opened() {
return this.#openedPromise.promise;
}
// The closed getter steps are to return this 's closed promise .
get closed () {
return this.#closedPromise.promise
get closed() {
return this.#closedPromise.promise;
}
// The close( closeInfo ) method steps are:
close (closeInfo = undefined) {
close(closeInfo = undefined) {
if (closeInfo !== null) {
closeInfo = webidl.converters.WebSocketCloseInfo(closeInfo)
closeInfo = webidl.converters.WebSocketCloseInfo(closeInfo);
}
// 1. Let code be closeInfo [" closeCode "] if present, or null otherwise.
const code = closeInfo.closeCode ?? null
const code = closeInfo.closeCode ?? null;
// 2. Let reason be closeInfo [" reason "].
const reason = closeInfo.reason
const reason = closeInfo.reason;
// 3. Close the WebSocket with this , code , and reason .
closeWebSocketConnection(this.#handler, code, reason, true)
closeWebSocketConnection(this.#handler, code, reason, true);
}
#write (chunk) {
#write(chunk) {
// 1. Let promise be a new promise created in stream s relevant realm .
const promise = createDeferredPromise()
const promise = createDeferredPromise();
// 2. Let data be null.
let data = null
let data = null;
// 3. Let opcode be null.
let opcode = 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)
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
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
let string;
try {
string = webidl.converters.DOMString(chunk)
string = webidl.converters.DOMString(chunk);
} catch (e) {
promise.reject(e)
return
promise.reject(e);
return;
}
// 5.2. Set data to the result of UTF-8 encoding string .
data = new TextEncoder().encode(string)
data = new TextEncoder().encode(string);
// 5.3. Set opcode to a text frame opcode.
opcode = opcodes.TEXT
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)
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)
})
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
return promise;
}
/** @type {import('../websocket').Handler['onConnectionEstablished']} */
#onConnectionEstablished (response, parsedExtensions) {
this.#handler.socket = response.socket
#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))
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.#parser = parser;
// 1. Change stream s ready state to OPEN (1).
this.#handler.readyState = states.OPEN
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 ?? ''
const extensions = parsedExtensions ?? '';
// 4. Let protocol be the subprotocol in use .
const protocol = response.headersList.get('sec-websocket-protocol') ?? ''
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 .
@ -278,16 +320,19 @@ class WebSocketStream {
// 8. Set up readable with pullAlgorithm and cancelAlgorithm .
const readable = new ReadableStream({
start: (controller) => {
this.#readableStreamController = controller
this.#readableStreamController = controller;
},
pull (controller) {
let chunk
while (controller.desiredSize > 0 && (chunk = response.socket.read()) !== null) {
controller.enqueue(chunk)
pull(controller) {
let chunk;
while (
controller.desiredSize > 0 &&
(chunk = response.socket.read()) !== null
) {
controller.enqueue(chunk);
}
},
cancel: (reason) => this.#cancel(reason)
})
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 .
@ -297,29 +342,29 @@ class WebSocketStream {
const writable = new WritableStream({
write: (chunk) => this.#write(chunk),
close: () => closeWebSocketConnection(this.#handler, null, null),
abort: (reason) => this.#closeUsingReason(reason)
})
abort: (reason) => this.#closeUsingReason(reason),
});
// Set stream s readable stream to readable .
this.#readableStream = readable
this.#readableStream = readable;
// Set stream s writable stream to writable .
this.#writableStream = 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
})
writable,
});
}
/** @type {import('../websocket').Handler['onMessage']} */
#onMessage (type, data) {
#onMessage(type, data) {
// 1. If streams ready state is not OPEN (1), then return.
if (this.#handler.readyState !== states.OPEN) {
return
return;
}
// 2. Let chunk be determined by switching on type:
@ -328,45 +373,48 @@ class WebSocketStream {
// - 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
let chunk;
if (type === opcodes.TEXT) {
try {
chunk = utf8Decode(data)
chunk = utf8Decode(data);
} catch {
failWebsocketConnection(this.#handler, 'Received invalid UTF-8 in text frame.')
return
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)
chunk = new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
}
// 3. Enqueue chunk into streams readable stream.
this.#readableStreamController.enqueue(chunk)
this.#readableStreamController.enqueue(chunk);
// 4. Apply backpressure to the WebSocket.
}
/** @type {import('../websocket').Handler['onSocketClose']} */
#onSocketClose () {
#onSocketClose() {
const wasClean =
this.#handler.closeState.has(sentCloseFrameState.SENT) &&
this.#handler.closeState.has(sentCloseFrameState.RECEIVED)
this.#handler.closeState.has(sentCloseFrameState.RECEIVED);
// 1. Change the ready state to CLOSED (3).
this.#handler.readyState = states.CLOSED
this.#handler.readyState = states.CLOSED;
// 2. If stream s handshake aborted is true, then return.
if (this.#handshakeAborted) {
return
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'))
this.#openedPromise.reject(new WebSocketError('Socket never opened'));
}
const result = this.#parser.closingInfo
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
@ -376,71 +424,84 @@ class WebSocketStream {
// 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
let code = result?.code ?? 1005;
if (!this.#handler.closeState.has(sentCloseFrameState.SENT) && !this.#handler.closeState.has(sentCloseFrameState.RECEIVED)) {
code = 1006
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))
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(() => {})
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'))
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
})
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)
const error = createUnvalidatedWebSocketError(
'unclean close',
code,
reason
);
// 7.2. Error stream s readable stream with error .
this.#readableStreamController.error(error)
this.#readableStreamController.error(error);
// 7.3. Error stream s writable stream with error .
this.#writableStream.abort(error)
this.#writableStream.abort(error);
// 7.4. Reject stream s closed promise with error .
this.#closedPromise.reject(error)
this.#closedPromise.reject(error);
}
}
#closeUsingReason (reason) {
#closeUsingReason(reason) {
// 1. Let code be null.
let code = null
let code = null;
// 2. Let reasonString be the empty string.
let reasonString = ''
let reasonString = '';
// 3. If reason implements WebSocketError ,
if (webidl.is.WebSocketError(reason)) {
// 3.1. Set code to reason s closeCode .
code = reason.closeCode
code = reason.closeCode;
// 3.2. Set reasonString to reason s reason .
reasonString = reason.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)
closeWebSocketConnection(this.#handler, code, reasonString);
}
// To cancel a WebSocketStream stream given reason , close using reason giving stream and reason .
#cancel (reason) {
this.#closeUsingReason(reason)
#cancel(reason) {
this.#closeUsingReason(reason);
}
}
@ -453,33 +514,34 @@ Object.defineProperties(WebSocketStream.prototype, {
value: 'WebSocketStream',
writable: false,
enumerable: false,
configurable: true
}
})
configurable: true,
},
});
webidl.converters.WebSocketStreamOptions = webidl.dictionaryConverter([
{
key: 'protocols',
converter: webidl.sequenceConverter(webidl.converters.USVString),
defaultValue: () => []
defaultValue: () => [],
},
{
key: 'signal',
converter: webidl.nullableConverter(webidl.converters.AbortSignal),
defaultValue: () => null
}
])
defaultValue: () => null,
},
]);
webidl.converters.WebSocketCloseInfo = webidl.dictionaryConverter([
{
key: 'closeCode',
converter: (V) => webidl.converters['unsigned short'](V, { enforceRange: true })
converter: (V) =>
webidl.converters['unsigned short'](V, { enforceRange: true }),
},
{
key: 'reason',
converter: webidl.converters.USVString,
defaultValue: () => ''
}
])
defaultValue: () => '',
},
]);
module.exports = { WebSocketStream }
module.exports = { WebSocketStream };

View File

@ -1,47 +1,50 @@
'use strict'
'use strict';
const { states, opcodes } = require('./constants')
const { isUtf8 } = require('node:buffer')
const { collectASequenceOfCodePointsFast, removeHTTPWhitespace } = require('../fetch/data-url')
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) {
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
return readyState === states.CONNECTING;
}
/**
* @param {number} readyState
* @returns {boolean}
*/
function isEstablished (readyState) {
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
return readyState === states.OPEN;
}
/**
* @param {number} readyState
* @returns {boolean}
*/
function isClosing (readyState) {
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
return readyState === states.CLOSING;
}
/**
* @param {number} readyState
* @returns {boolean}
*/
function isClosed (readyState) {
return readyState === states.CLOSED
function isClosed(readyState) {
return readyState === states.CLOSED;
}
/**
@ -52,20 +55,25 @@ function isClosed (readyState) {
* @param {EventInit | undefined} eventInitDict
* @returns {void}
*/
function fireEvent (e, target, eventFactory = (type, init) => new Event(type, init), eventInitDict = {}) {
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)
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)
target.dispatchEvent(event);
}
/**
@ -75,19 +83,19 @@ function fireEvent (e, target, eventFactory = (type, init) => new Event(type, in
* @param {Buffer} data application data
* @returns {void}
*/
function websocketMessageReceived (handler, type, data) {
handler.onMessage(type, data)
function websocketMessageReceived(handler, type, data) {
handler.onMessage(type, data);
}
/**
* @param {Buffer} buffer
* @returns {ArrayBuffer}
*/
function toArrayBuffer (buffer) {
function toArrayBuffer(buffer) {
if (buffer.byteLength === buffer.buffer.byteLength) {
return buffer.buffer
return buffer.buffer;
}
return new Uint8Array(buffer).buffer
return new Uint8Array(buffer).buffer;
}
/**
@ -97,7 +105,7 @@ function toArrayBuffer (buffer) {
* @param {string} protocol
* @returns {boolean}
*/
function isValidSubprotocol (protocol) {
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
@ -105,38 +113,38 @@ function isValidSubprotocol (protocol) {
// U+007E not including separator characters as defined in
// [RFC2616] and MUST all be unique strings.
if (protocol.length === 0) {
return false
return false;
}
for (let i = 0; i < protocol.length; ++i) {
const code = protocol.charCodeAt(i)
const code = protocol.charCodeAt(i);
if (
code < 0x21 || // CTL, contains SP (0x20) and HT (0x09)
code > 0x7E ||
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 === 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 // }
code === 0x5b || // [
code === 0x5c || // \
code === 0x5d || // ]
code === 0x7b || // {
code === 0x7d // }
) {
return false
return false;
}
}
return true
return true;
}
/**
@ -144,16 +152,16 @@ function isValidSubprotocol (protocol) {
* @param {number} code
* @returns {boolean}
*/
function isValidStatusCode (code) {
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
return code >= 3000 && code <= 4999;
}
/**
@ -161,28 +169,28 @@ function isValidStatusCode (code) {
* @param {number} opcode
* @returns {boolean}
*/
function isControlFrame (opcode) {
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
function isContinuationFrame(opcode) {
return opcode === opcodes.CONTINUATION;
}
/**
* @param {number} opcode
* @returns {boolean}
*/
function isTextBinaryFrame (opcode) {
return opcode === opcodes.TEXT || opcode === opcodes.BINARY
function isTextBinaryFrame(opcode) {
return opcode === opcodes.TEXT || opcode === opcodes.BINARY;
}
/**
@ -190,8 +198,12 @@ function isTextBinaryFrame (opcode) {
* @param {number} opcode
* @returns {boolean}
*/
function isValidOpcode (opcode) {
return isTextBinaryFrame(opcode) || isContinuationFrame(opcode) || isControlFrame(opcode)
function isValidOpcode(opcode) {
return (
isTextBinaryFrame(opcode) ||
isContinuationFrame(opcode) ||
isControlFrame(opcode)
);
}
/**
@ -200,23 +212,23 @@ function isValidOpcode (opcode) {
* @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()
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)
const pair = collectASequenceOfCodePointsFast(';', extensions, position);
const [name, value = ''] = pair.split('=', 2);
extensionList.set(
removeHTTPWhitespace(name, true, false),
removeHTTPWhitespace(value, false, true)
)
);
position.position++
position.position++;
}
return extensionList
return extensionList;
}
/**
@ -225,16 +237,16 @@ function parseExtensions (extensions) {
* @param {string} value
* @returns {boolean}
*/
function isValidClientWindowBits (value) {
function isValidClientWindowBits(value) {
for (let i = 0; i < value.length; i++) {
const byte = value.charCodeAt(i)
const byte = value.charCodeAt(i);
if (byte < 0x30 || byte > 0x39) {
return false
return false;
}
}
return true
return true;
}
/**
@ -242,47 +254,47 @@ function isValidClientWindowBits (value) {
* @param {string} url
* @param {string} [baseURL]
*/
function getURLRecord (url, 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
let urlRecord;
try {
urlRecord = new URL(url, baseURL)
urlRecord = new URL(url, baseURL);
} catch (e) {
throw new DOMException(e, 'SyntaxError')
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:'
urlRecord.protocol = 'ws:';
} else if (urlRecord.protocol === 'https:') {
urlRecord.protocol = 'wss:'
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')
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')
throw new DOMException('hash', 'SyntaxError');
}
// Return urlRecord .
return urlRecord
return urlRecord;
}
// https://whatpr.org/websockets/48.html#validate-close-code-and-reason
function validateCloseCodeAndReason (code, 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')
throw new DOMException('invalid code', 'InvalidAccessError');
}
}
@ -291,10 +303,13 @@ function validateCloseCodeAndReason (code, reason) {
// 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)
const reasonBytesLength = Buffer.byteLength(reason);
if (reasonBytesLength > 123) {
throw new DOMException(`Reason must be less than 123 bytes; received ${reasonBytesLength}`, 'SyntaxError')
throw new DOMException(
`Reason must be less than 123 bytes; received ${reasonBytesLength}`,
'SyntaxError'
);
}
}
}
@ -305,16 +320,16 @@ function validateCloseCodeAndReason (code, reason) {
*/
const utf8Decode = (() => {
if (typeof process.versions.icu === 'string') {
const fatalDecoder = new TextDecoder('utf-8', { fatal: true })
return fatalDecoder.decode.bind(fatalDecoder)
const fatalDecoder = new TextDecoder('utf-8', { fatal: true });
return fatalDecoder.decode.bind(fatalDecoder);
}
return function (buffer) {
if (isUtf8(buffer)) {
return buffer.toString('utf-8')
return buffer.toString('utf-8');
}
throw new TypeError('Invalid utf-8 received.')
}
})()
throw new TypeError('Invalid utf-8 received.');
};
})();
module.exports = {
isConnecting,
@ -334,5 +349,5 @@ module.exports = {
isValidClientWindowBits,
toArrayBuffer,
getURLRecord,
validateCloseCodeAndReason
}
validateCloseCodeAndReason,
};

View File

@ -1,9 +1,15 @@
'use strict'
'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 { 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,
@ -12,16 +18,20 @@ const {
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')
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
@ -47,36 +57,38 @@ class WebSocket extends EventTarget {
open: null,
error: null,
close: null,
message: null
}
message: null,
};
#bufferedAmount = 0
#protocol = ''
#extensions = ''
#bufferedAmount = 0;
#protocol = '';
#extensions = '';
/** @type {SendQueue} */
#sendQueue
#sendQueue;
/** @type {Handler} */
#handler = {
onConnectionEstablished: (response, extensions) => this.#onConnectionEstablished(response, extensions),
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),
onParserError: (err) =>
failWebsocketConnection(this.#handler, null, err.message),
onParserDrain: () => this.#onParserDrain(),
onSocketData: (chunk) => {
if (!this.#parser.write(chunk)) {
this.#handler.socket.pause()
this.#handler.socket.pause();
}
},
onSocketError: (err) => {
this.#handler.readyState = states.CLOSING
this.#handler.readyState = states.CLOSING;
if (channels.socketError.hasSubscribers) {
channels.socketError.publish(err)
channels.socketError.publish(err);
}
this.#handler.socket.destroy()
this.#handler.socket.destroy();
},
onSocketClose: () => this.#onSocketClose(),
@ -84,60 +96,73 @@ class WebSocket extends EventTarget {
socket: null,
closeState: new Set(),
controller: null,
wasEverConnected: false
}
wasEverConnected: false,
};
#url
#binaryType
#url;
#binaryType;
/** @type {import('./receiver').ByteParser} */
#parser
#parser;
/**
* @param {string} url
* @param {string|string[]} protocols
*/
constructor (url, protocols = []) {
super()
constructor(url, protocols = []) {
super();
webidl.util.markAsUncloneable(this)
webidl.util.markAsUncloneable(this);
const prefix = 'WebSocket constructor'
webidl.argumentLengthCheck(arguments, 1, prefix)
const prefix = 'WebSocket constructor';
webidl.argumentLengthCheck(arguments, 1, prefix);
const options = webidl.converters['DOMString or sequence<DOMString> or WebSocketInit'](protocols, prefix, 'options')
const options = webidl.converters[
'DOMString or sequence<DOMString> or WebSocketInit'
](protocols, prefix, 'options');
url = webidl.converters.USVString(url)
protocols = options.protocols
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
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)
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]
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 !== 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')
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)
this.#url = new URL(urlRecord.href);
// 6. Let client be this's relevant settings object.
const client = environmentSettingsObject.settingsObject
const client = environmentSettingsObject.settingsObject;
// 7. Run this step in parallel:
// 7.1. Establish a WebSocket connection given urlRecord, protocols,
@ -148,12 +173,12 @@ class WebSocket extends EventTarget {
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
this.#handler.readyState = WebSocket.CONNECTING;
// The extensions attribute must initially return the empty string.
@ -161,7 +186,7 @@ class WebSocket extends EventTarget {
// Each WebSocket object has an associated binary type, which is a
// BinaryType. Initially it must be "blob".
this.#binaryType = 'blob'
this.#binaryType = 'blob';
}
/**
@ -169,53 +194,58 @@ class WebSocket extends EventTarget {
* @param {number|undefined} code
* @param {string|undefined} reason
*/
close (code = undefined, reason = undefined) {
webidl.brandCheck(this, WebSocket)
close(code = undefined, reason = undefined) {
webidl.brandCheck(this, WebSocket);
const prefix = 'WebSocket.close'
const prefix = 'WebSocket.close';
if (code !== undefined) {
code = webidl.converters['unsigned short'](code, prefix, 'code', { clamp: true })
code = webidl.converters['unsigned short'](code, prefix, 'code', {
clamp: true,
});
}
if (reason !== undefined) {
reason = webidl.converters.USVString(reason)
reason = webidl.converters.USVString(reason);
}
// 1. If code is the special value "missing", then set code to null.
code ??= null
code ??= null;
// 2. If reason is the special value "missing", then set reason to the empty string.
reason ??= ''
reason ??= '';
// 3. Close the WebSocket with this, code, and reason.
closeWebSocketConnection(this.#handler, code, reason, true)
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)
send(data) {
webidl.brandCheck(this, WebSocket);
const prefix = 'WebSocket.send'
webidl.argumentLengthCheck(arguments, 1, prefix)
const prefix = 'WebSocket.send';
webidl.argumentLengthCheck(arguments, 1, prefix);
data = webidl.converters.WebSocketSendData(data, prefix, 'data')
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')
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 (
!isEstablished(this.#handler.readyState) ||
isClosing(this.#handler.readyState)
) {
return;
}
// If data is a string
@ -231,12 +261,16 @@ class WebSocket extends EventTarget {
// the bufferedAmount attribute by the number of bytes needed to
// express the argument as UTF-8.
const buffer = Buffer.from(data)
const buffer = Buffer.from(data);
this.#bufferedAmount += buffer.byteLength
this.#sendQueue.add(buffer, () => {
this.#bufferedAmount -= buffer.byteLength
}, sendHints.text)
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
@ -250,10 +284,14 @@ class WebSocket extends EventTarget {
// 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)
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
@ -267,10 +305,14 @@ class WebSocket extends EventTarget {
// 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)
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
@ -283,234 +325,244 @@ class WebSocket extends EventTarget {
// 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)
this.#bufferedAmount += data.size;
this.#sendQueue.add(
data,
() => {
this.#bufferedAmount -= data.size;
},
sendHints.blob
);
}
}
get readyState () {
webidl.brandCheck(this, WebSocket)
get readyState() {
webidl.brandCheck(this, WebSocket);
// The readyState getter steps are to return this's ready state.
return this.#handler.readyState
return this.#handler.readyState;
}
get bufferedAmount () {
webidl.brandCheck(this, WebSocket)
get bufferedAmount() {
webidl.brandCheck(this, WebSocket);
return this.#bufferedAmount
return this.#bufferedAmount;
}
get url () {
webidl.brandCheck(this, WebSocket)
get url() {
webidl.brandCheck(this, WebSocket);
// The url getter steps are to return this's url, serialized.
return URLSerializer(this.#url)
return URLSerializer(this.#url);
}
get extensions () {
webidl.brandCheck(this, WebSocket)
get extensions() {
webidl.brandCheck(this, WebSocket);
return this.#extensions
return this.#extensions;
}
get protocol () {
webidl.brandCheck(this, WebSocket)
get protocol() {
webidl.brandCheck(this, WebSocket);
return this.#protocol
return this.#protocol;
}
get onopen () {
webidl.brandCheck(this, WebSocket)
get onopen() {
webidl.brandCheck(this, WebSocket);
return this.#events.open
return this.#events.open;
}
set onopen (fn) {
webidl.brandCheck(this, WebSocket)
set onopen(fn) {
webidl.brandCheck(this, WebSocket);
if (this.#events.open) {
this.removeEventListener('open', this.#events.open)
this.removeEventListener('open', this.#events.open);
}
if (typeof fn === 'function') {
this.#events.open = fn
this.addEventListener('open', fn)
this.#events.open = fn;
this.addEventListener('open', fn);
} else {
this.#events.open = null
this.#events.open = null;
}
}
get onerror () {
webidl.brandCheck(this, WebSocket)
get onerror() {
webidl.brandCheck(this, WebSocket);
return this.#events.error
return this.#events.error;
}
set onerror (fn) {
webidl.brandCheck(this, WebSocket)
set onerror(fn) {
webidl.brandCheck(this, WebSocket);
if (this.#events.error) {
this.removeEventListener('error', this.#events.error)
this.removeEventListener('error', this.#events.error);
}
if (typeof fn === 'function') {
this.#events.error = fn
this.addEventListener('error', fn)
this.#events.error = fn;
this.addEventListener('error', fn);
} else {
this.#events.error = null
this.#events.error = null;
}
}
get onclose () {
webidl.brandCheck(this, WebSocket)
get onclose() {
webidl.brandCheck(this, WebSocket);
return this.#events.close
return this.#events.close;
}
set onclose (fn) {
webidl.brandCheck(this, WebSocket)
set onclose(fn) {
webidl.brandCheck(this, WebSocket);
if (this.#events.close) {
this.removeEventListener('close', this.#events.close)
this.removeEventListener('close', this.#events.close);
}
if (typeof fn === 'function') {
this.#events.close = fn
this.addEventListener('close', fn)
this.#events.close = fn;
this.addEventListener('close', fn);
} else {
this.#events.close = null
this.#events.close = null;
}
}
get onmessage () {
webidl.brandCheck(this, WebSocket)
get onmessage() {
webidl.brandCheck(this, WebSocket);
return this.#events.message
return this.#events.message;
}
set onmessage (fn) {
webidl.brandCheck(this, WebSocket)
set onmessage(fn) {
webidl.brandCheck(this, WebSocket);
if (this.#events.message) {
this.removeEventListener('message', this.#events.message)
this.removeEventListener('message', this.#events.message);
}
if (typeof fn === 'function') {
this.#events.message = fn
this.addEventListener('message', fn)
this.#events.message = fn;
this.addEventListener('message', fn);
} else {
this.#events.message = null
this.#events.message = null;
}
}
get binaryType () {
webidl.brandCheck(this, WebSocket)
get binaryType() {
webidl.brandCheck(this, WebSocket);
return this.#binaryType
return this.#binaryType;
}
set binaryType (type) {
webidl.brandCheck(this, WebSocket)
set binaryType(type) {
webidl.brandCheck(this, WebSocket);
if (type !== 'blob' && type !== 'arraybuffer') {
this.#binaryType = 'blob'
this.#binaryType = 'blob';
} else {
this.#binaryType = type
this.#binaryType = type;
}
}
/**
* @see https://websockets.spec.whatwg.org/#feedback-from-the-protocol
*/
#onConnectionEstablished (response, parsedExtensions) {
#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
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))
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)
this.#parser = parser;
this.#sendQueue = new SendQueue(response.socket);
// 1. Change the ready state to OPEN (1).
this.#handler.readyState = states.OPEN
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')
const extensions = response.headersList.get('sec-websocket-extensions');
if (extensions !== null) {
this.#extensions = extensions
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')
const protocol = response.headersList.get('sec-websocket-protocol');
if (protocol !== null) {
this.#protocol = protocol
this.#protocol = protocol;
}
// 4. Fire an event named open at the WebSocket object.
fireEvent('open', this)
fireEvent('open', this);
}
#onFail (code, reason) {
#onFail(code, reason) {
if (reason) {
// TODO: process.nextTick
fireEvent('error', this, (type, init) => new ErrorEvent(type, init), {
error: new Error(reason),
message: reason
})
message: reason,
});
}
if (!this.#handler.wasEverConnected) {
this.#handler.readyState = states.CLOSED
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
})
wasClean: false,
code,
reason,
});
}
}
#onMessage (type, data) {
#onMessage(type, data) {
// 1. If ready state is not OPEN (1), then return.
if (this.#handler.readyState !== states.OPEN) {
return
return;
}
// 2. Let dataForEvent be determined by switching on type and binary type:
let dataForEvent
let dataForEvent;
if (type === opcodes.TEXT) {
// -> type indicates that the data is Text
// a new DOMString containing data
try {
dataForEvent = utf8Decode(data)
dataForEvent = utf8Decode(data);
} catch {
failWebsocketConnection(this.#handler, 1007, 'Received invalid UTF-8 in text frame.')
return
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])
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)
dataForEvent = toArrayBuffer(data);
}
}
@ -519,45 +571,45 @@ class WebSocket extends EventTarget {
// objects url's origin, and the data attribute initialized to dataForEvent.
fireEvent('message', this, createFastMessageEvent, {
origin: this.#url.origin,
data: dataForEvent
})
data: dataForEvent,
});
}
#onParserDrain () {
this.#handler.socket.resume()
#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 () {
#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)
this.#handler.closeState.has(sentCloseFrameState.RECEIVED);
let code = 1005
let reason = ''
let code = 1005;
let reason = '';
const result = this.#parser.closingInfo
const result = this.#parser.closingInfo;
if (result && !result.error) {
code = result.code ?? 1005
reason = result.reason
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
code = 1006;
}
// 1. Change the ready state to CLOSED (3).
this.#handler.readyState = states.CLOSED
this.#handler.readyState = states.CLOSED;
// 2. If the user agent was required to fail the WebSocket
// connection, or if the WebSocket connection was closed
@ -575,27 +627,29 @@ class WebSocket extends EventTarget {
// reason.
// TODO: process.nextTick
fireEvent('close', this, (type, init) => new CloseEvent(type, init), {
wasClean, code, reason
})
wasClean,
code,
reason,
});
if (channels.close.hasSubscribers) {
channels.close.publish({
websocket: this,
code,
reason
})
reason,
});
}
}
}
// https://websockets.spec.whatwg.org/#dom-websocket-connecting
WebSocket.CONNECTING = WebSocket.prototype.CONNECTING = states.CONNECTING
WebSocket.CONNECTING = WebSocket.prototype.CONNECTING = states.CONNECTING;
// https://websockets.spec.whatwg.org/#dom-websocket-open
WebSocket.OPEN = WebSocket.prototype.OPEN = states.OPEN
WebSocket.OPEN = WebSocket.prototype.OPEN = states.OPEN;
// https://websockets.spec.whatwg.org/#dom-websocket-closing
WebSocket.CLOSING = WebSocket.prototype.CLOSING = states.CLOSING
WebSocket.CLOSING = WebSocket.prototype.CLOSING = states.CLOSING;
// https://websockets.spec.whatwg.org/#dom-websocket-closed
WebSocket.CLOSED = WebSocket.prototype.CLOSED = states.CLOSED
WebSocket.CLOSED = WebSocket.prototype.CLOSED = states.CLOSED;
Object.defineProperties(WebSocket.prototype, {
CONNECTING: staticPropertyDescriptors,
@ -618,69 +672,82 @@ Object.defineProperties(WebSocket.prototype, {
value: 'WebSocket',
writable: false,
enumerable: false,
configurable: true
}
})
configurable: true,
},
});
Object.defineProperties(WebSocket, {
CONNECTING: staticPropertyDescriptors,
OPEN: staticPropertyDescriptors,
CLOSING: staticPropertyDescriptors,
CLOSED: 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)
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)
}
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)
defaultValue: () => new Array(0),
},
{
key: 'dispatcher',
converter: webidl.converters.any,
defaultValue: () => getGlobalDispatcher()
defaultValue: () => getGlobalDispatcher(),
},
{
key: 'headers',
converter: webidl.nullableConverter(webidl.converters.HeadersInit)
}
])
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)
}
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) }
}
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
return V;
}
if (ArrayBuffer.isView(V) || types.isArrayBuffer(V)) {
return V
return V;
}
}
return webidl.converters.USVString(V)
}
return webidl.converters.USVString(V);
};
module.exports = {
WebSocket
}
WebSocket,
};