mirror of https://github.com/xtekky/gpt4free.git
Add arkose_token to OpenaiChat
This commit is contained in:
parent
79cf039a88
commit
dc04ca9306
|
@ -1,11 +1,13 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import uuid, json, time
|
||||
import uuid, json, time, os
|
||||
import tempfile, subprocess, shutil
|
||||
|
||||
from ..base_provider import AsyncGeneratorProvider
|
||||
from ..helper import get_browser, get_cookies, format_prompt, get_event_loop
|
||||
from ...typing import AsyncResult, Messages
|
||||
from ...requests import StreamSession
|
||||
from ... import debug
|
||||
|
||||
class OpenaiChat(AsyncGeneratorProvider):
|
||||
url = "https://chat.openai.com"
|
||||
|
@ -47,6 +49,7 @@ class OpenaiChat(AsyncGeneratorProvider):
|
|||
]
|
||||
data = {
|
||||
"action": "next",
|
||||
"arkose_token": await get_arkose_token(proxy),
|
||||
"messages": messages,
|
||||
"conversation_id": None,
|
||||
"parent_message_id": str(uuid.uuid4()),
|
||||
|
@ -65,7 +68,11 @@ class OpenaiChat(AsyncGeneratorProvider):
|
|||
line = json.loads(line)
|
||||
except:
|
||||
continue
|
||||
if "message" not in line or "message_type" not in line["message"]["metadata"]:
|
||||
if "message" not in line:
|
||||
continue
|
||||
if "error" in line and line["error"]:
|
||||
raise RuntimeError(line["error"])
|
||||
if "message_type" not in line["message"]["metadata"]:
|
||||
continue
|
||||
if line["message"]["metadata"]["message_type"] == "next":
|
||||
new_message = line["message"]["content"]["parts"][0]
|
||||
|
@ -134,4 +141,40 @@ class OpenaiChat(AsyncGeneratorProvider):
|
|||
("cookies", "dict[str, str]")
|
||||
]
|
||||
param = ", ".join([": ".join(p) for p in params])
|
||||
return f"g4f.provider.{cls.__name__} supports: ({param})"
|
||||
return f"g4f.provider.{cls.__name__} supports: ({param})"
|
||||
|
||||
async def get_arkose_token(proxy: str = None) -> str:
|
||||
node = shutil.which("node")
|
||||
if not node:
|
||||
if debug.logging:
|
||||
print('OpenaiChat: "node" not found')
|
||||
return
|
||||
dir = os.path.dirname(os.path.dirname(__file__))
|
||||
include = f'{dir}/npm/node_modules/funcaptcha'
|
||||
config = {
|
||||
"pkey": "3D86FBBA-9D22-402A-B512-3420086BA6CC",
|
||||
"surl": "https://tcr9i.chat.openai.com",
|
||||
"data": {},
|
||||
"headers": {
|
||||
"User-Agent": 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36'
|
||||
},
|
||||
"site": "https://chat.openai.com",
|
||||
"proxy": proxy
|
||||
}
|
||||
source = """
|
||||
fun = require({include})
|
||||
config = {config}
|
||||
fun.getToken(config).then(token => {
|
||||
console.log(token.token)
|
||||
})
|
||||
"""
|
||||
source = source.replace('{include}', json.dumps(include))
|
||||
source = source.replace('{config}', json.dumps(config))
|
||||
tmp = tempfile.NamedTemporaryFile(delete=False)
|
||||
tmp.write(source.encode())
|
||||
tmp.close()
|
||||
try:
|
||||
p = subprocess.Popen([node, tmp.name], stdout=subprocess.PIPE)
|
||||
return p.stdout.read().decode()
|
||||
finally:
|
||||
os.unlink(tmp.name)
|
|
@ -0,0 +1,39 @@
|
|||
{
|
||||
"name": "npm",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"node_modules/@fastify/busboy": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.0.0.tgz",
|
||||
"integrity": "sha512-JUFJad5lv7jxj926GPgymrWQxxjPYuJNiNjNMzqT+HiuP6Vl3dk5xzG+8sTX96np0ZAluvaMzPsjhHZ5rNuNQQ==",
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/crypto-js": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz",
|
||||
"integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q=="
|
||||
},
|
||||
"node_modules/funcaptcha": {
|
||||
"version": "1.1.7",
|
||||
"resolved": "https://registry.npmjs.org/funcaptcha/-/funcaptcha-1.1.7.tgz",
|
||||
"integrity": "sha512-xcXzOzgrV1MixYzmAbqL4EuTiBXaJPYNdCHfzyTkINCo32+Er77ukehglgaN2ITWKmgGX1kafoVn4K+1ND3nXw==",
|
||||
"dependencies": {
|
||||
"undici": "^5.22.0"
|
||||
}
|
||||
},
|
||||
"node_modules/undici": {
|
||||
"version": "5.27.0",
|
||||
"resolved": "https://registry.npmjs.org/undici/-/undici-5.27.0.tgz",
|
||||
"integrity": "sha512-l3ydWhlhOJzMVOYkymLykcRRXqbUaQriERtR70B9LzNkZ4bX52Fc8wbTDneMiwo8T+AemZXvXaTx+9o5ROxrXg==",
|
||||
"dependencies": {
|
||||
"@fastify/busboy": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
Copyright Brian White. All rights reserved.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to
|
||||
deal in the Software without restriction, including without limitation the
|
||||
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||
sell copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||
IN THE SOFTWARE.
|
|
@ -0,0 +1,271 @@
|
|||
# busboy
|
||||
|
||||
<div align="center">
|
||||
|
||||
[![Build Status](https://github.com/fastify/busboy/workflows/ci/badge.svg)](https://github.com/fastify/busboy/actions)
|
||||
[![Coverage Status](https://coveralls.io/repos/fastify/busboy/badge.svg?branch=master)](https://coveralls.io/r/fastify/busboy?branch=master)
|
||||
[![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg?style=flat)](https://standardjs.com/)
|
||||
[![Security Responsible Disclosure](https://img.shields.io/badge/Security-Responsible%20Disclosure-yellow.svg)](https://github.com/nodejs/security-wg/blob/HEAD/processes/responsible_disclosure_template.md)
|
||||
|
||||
</div>
|
||||
|
||||
<div align="center">
|
||||
|
||||
[![NPM version](https://img.shields.io/npm/v/@fastify/busboy.svg?style=flat)](https://www.npmjs.com/package/@fastify/busboy)
|
||||
[![NPM downloads](https://img.shields.io/npm/dm/@fastify/busboy.svg?style=flat)](https://www.npmjs.com/package/@fastify/busboy)
|
||||
|
||||
</div>
|
||||
|
||||
Description
|
||||
===========
|
||||
|
||||
A Node.js module for parsing incoming HTML form data.
|
||||
|
||||
This is an officially supported fork by [fastify](https://github.com/fastify/) organization of the amazing library [originally created](https://github.com/mscdex/busboy) by Brian White,
|
||||
aimed at addressing long-standing issues with it.
|
||||
|
||||
Benchmark (Mean time for 500 Kb payload, 2000 cycles, 1000 cycle warmup):
|
||||
|
||||
| Library | Version | Mean time in nanoseconds (less is better) |
|
||||
|-----------------------|---------|-------------------------------------------|
|
||||
| busboy | 0.3.1 | `340114` |
|
||||
| @fastify/busboy | 1.0.0 | `270984` |
|
||||
|
||||
[Changelog](https://github.com/fastify/busboy/blob/master/CHANGELOG.md) since busboy 0.31.
|
||||
|
||||
Requirements
|
||||
============
|
||||
|
||||
* [Node.js](http://nodejs.org/) 10+
|
||||
|
||||
|
||||
Install
|
||||
=======
|
||||
|
||||
npm i @fastify/busboy
|
||||
|
||||
|
||||
Examples
|
||||
========
|
||||
|
||||
* Parsing (multipart) with default options:
|
||||
|
||||
```javascript
|
||||
const http = require('node:http');
|
||||
const { inspect } = require('node:util');
|
||||
const Busboy = require('busboy');
|
||||
|
||||
http.createServer((req, res) => {
|
||||
if (req.method === 'POST') {
|
||||
const busboy = new Busboy({ headers: req.headers });
|
||||
busboy.on('file', (fieldname, file, filename, encoding, mimetype) => {
|
||||
console.log(`File [${fieldname}]: filename: ${filename}, encoding: ${encoding}, mimetype: ${mimetype}`);
|
||||
file.on('data', data => {
|
||||
console.log(`File [${fieldname}] got ${data.length} bytes`);
|
||||
});
|
||||
file.on('end', () => {
|
||||
console.log(`File [${fieldname}] Finished`);
|
||||
});
|
||||
});
|
||||
busboy.on('field', (fieldname, val, fieldnameTruncated, valTruncated, encoding, mimetype) => {
|
||||
console.log(`Field [${fieldname}]: value: ${inspect(val)}`);
|
||||
});
|
||||
busboy.on('finish', () => {
|
||||
console.log('Done parsing form!');
|
||||
res.writeHead(303, { Connection: 'close', Location: '/' });
|
||||
res.end();
|
||||
});
|
||||
req.pipe(busboy);
|
||||
} else if (req.method === 'GET') {
|
||||
res.writeHead(200, { Connection: 'close' });
|
||||
res.end(`<html><head></head><body>
|
||||
<form method="POST" enctype="multipart/form-data">
|
||||
<input type="text" name="textfield"><br>
|
||||
<input type="file" name="filefield"><br>
|
||||
<input type="submit">
|
||||
</form>
|
||||
</body></html>`);
|
||||
}
|
||||
}).listen(8000, () => {
|
||||
console.log('Listening for requests');
|
||||
});
|
||||
|
||||
// Example output, using http://nodejs.org/images/ryan-speaker.jpg as the file:
|
||||
//
|
||||
// Listening for requests
|
||||
// File [filefield]: filename: ryan-speaker.jpg, encoding: binary
|
||||
// File [filefield] got 11971 bytes
|
||||
// Field [textfield]: value: 'testing! :-)'
|
||||
// File [filefield] Finished
|
||||
// Done parsing form!
|
||||
```
|
||||
|
||||
* Save all incoming files to disk:
|
||||
|
||||
```javascript
|
||||
const http = require('node:http');
|
||||
const path = require('node:path');
|
||||
const os = require('node:os');
|
||||
const fs = require('node:fs');
|
||||
|
||||
const Busboy = require('busboy');
|
||||
|
||||
http.createServer(function(req, res) {
|
||||
if (req.method === 'POST') {
|
||||
const busboy = new Busboy({ headers: req.headers });
|
||||
busboy.on('file', function(fieldname, file, filename, encoding, mimetype) {
|
||||
var saveTo = path.join(os.tmpdir(), path.basename(fieldname));
|
||||
file.pipe(fs.createWriteStream(saveTo));
|
||||
});
|
||||
busboy.on('finish', function() {
|
||||
res.writeHead(200, { 'Connection': 'close' });
|
||||
res.end("That's all folks!");
|
||||
});
|
||||
return req.pipe(busboy);
|
||||
}
|
||||
res.writeHead(404);
|
||||
res.end();
|
||||
}).listen(8000, function() {
|
||||
console.log('Listening for requests');
|
||||
});
|
||||
```
|
||||
|
||||
* Parsing (urlencoded) with default options:
|
||||
|
||||
```javascript
|
||||
const http = require('node:http');
|
||||
const { inspect } = require('node:util');
|
||||
|
||||
const Busboy = require('busboy');
|
||||
|
||||
http.createServer(function(req, res) {
|
||||
if (req.method === 'POST') {
|
||||
const busboy = new Busboy({ headers: req.headers });
|
||||
busboy.on('file', function(fieldname, file, filename, encoding, mimetype) {
|
||||
console.log('File [' + fieldname + ']: filename: ' + filename);
|
||||
file.on('data', function(data) {
|
||||
console.log('File [' + fieldname + '] got ' + data.length + ' bytes');
|
||||
});
|
||||
file.on('end', function() {
|
||||
console.log('File [' + fieldname + '] Finished');
|
||||
});
|
||||
});
|
||||
busboy.on('field', function(fieldname, val, fieldnameTruncated, valTruncated) {
|
||||
console.log('Field [' + fieldname + ']: value: ' + inspect(val));
|
||||
});
|
||||
busboy.on('finish', function() {
|
||||
console.log('Done parsing form!');
|
||||
res.writeHead(303, { Connection: 'close', Location: '/' });
|
||||
res.end();
|
||||
});
|
||||
req.pipe(busboy);
|
||||
} else if (req.method === 'GET') {
|
||||
res.writeHead(200, { Connection: 'close' });
|
||||
res.end('<html><head></head><body>\
|
||||
<form method="POST">\
|
||||
<input type="text" name="textfield"><br />\
|
||||
<select name="selectfield">\
|
||||
<option value="1">1</option>\
|
||||
<option value="10">10</option>\
|
||||
<option value="100">100</option>\
|
||||
<option value="9001">9001</option>\
|
||||
</select><br />\
|
||||
<input type="checkbox" name="checkfield">Node.js rules!<br />\
|
||||
<input type="submit">\
|
||||
</form>\
|
||||
</body></html>');
|
||||
}
|
||||
}).listen(8000, function() {
|
||||
console.log('Listening for requests');
|
||||
});
|
||||
|
||||
// Example output:
|
||||
//
|
||||
// Listening for requests
|
||||
// Field [textfield]: value: 'testing! :-)'
|
||||
// Field [selectfield]: value: '9001'
|
||||
// Field [checkfield]: value: 'on'
|
||||
// Done parsing form!
|
||||
```
|
||||
|
||||
|
||||
API
|
||||
===
|
||||
|
||||
_Busboy_ is a _Writable_ stream
|
||||
|
||||
Busboy (special) events
|
||||
-----------------------
|
||||
|
||||
* **file**(< _string_ >fieldname, < _ReadableStream_ >stream, < _string_ >filename, < _string_ >transferEncoding, < _string_ >mimeType) - Emitted for each new file form field found. `transferEncoding` contains the 'Content-Transfer-Encoding' value for the file stream. `mimeType` contains the 'Content-Type' value for the file stream.
|
||||
* Note: if you listen for this event, you should always handle the `stream` no matter if you care about the file contents or not (e.g. you can simply just do `stream.resume();` if you want to discard the contents), otherwise the 'finish' event will never fire on the Busboy instance. However, if you don't care about **any** incoming files, you can simply not listen for the 'file' event at all and any/all files will be automatically and safely discarded (these discarded files do still count towards `files` and `parts` limits).
|
||||
* If a configured file size limit was reached, `stream` will both have a boolean property `truncated` (best checked at the end of the stream) and emit a 'limit' event to notify you when this happens.
|
||||
* The property `bytesRead` informs about the number of bytes that have been read so far.
|
||||
|
||||
* **field**(< _string_ >fieldname, < _string_ >value, < _boolean_ >fieldnameTruncated, < _boolean_ >valueTruncated, < _string_ >transferEncoding, < _string_ >mimeType) - Emitted for each new non-file field found.
|
||||
|
||||
* **partsLimit**() - Emitted when specified `parts` limit has been reached. No more 'file' or 'field' events will be emitted.
|
||||
|
||||
* **filesLimit**() - Emitted when specified `files` limit has been reached. No more 'file' events will be emitted.
|
||||
|
||||
* **fieldsLimit**() - Emitted when specified `fields` limit has been reached. No more 'field' events will be emitted.
|
||||
|
||||
|
||||
Busboy methods
|
||||
--------------
|
||||
|
||||
* **(constructor)**(< _object_ >config) - Creates and returns a new Busboy instance.
|
||||
|
||||
* The constructor takes the following valid `config` settings:
|
||||
|
||||
* **headers** - _object_ - These are the HTTP headers of the incoming request, which are used by individual parsers.
|
||||
|
||||
* **autoDestroy** - _boolean_ - Whether this stream should automatically call .destroy() on itself after ending. (Default: false).
|
||||
|
||||
* **highWaterMark** - _integer_ - highWaterMark to use for this Busboy instance (Default: WritableStream default).
|
||||
|
||||
* **fileHwm** - _integer_ - highWaterMark to use for file streams (Default: ReadableStream default).
|
||||
|
||||
* **defCharset** - _string_ - Default character set to use when one isn't defined (Default: 'utf8').
|
||||
|
||||
* **preservePath** - _boolean_ - If paths in the multipart 'filename' field shall be preserved. (Default: false).
|
||||
|
||||
* **isPartAFile** - __function__ - Use this function to override the default file detection functionality. It has following parameters:
|
||||
|
||||
* fieldName - __string__ The name of the field.
|
||||
|
||||
* contentType - __string__ The content-type of the part, e.g. `text/plain`, `image/jpeg`, `application/octet-stream`
|
||||
|
||||
* fileName - __string__ The name of a file supplied by the part.
|
||||
|
||||
(Default: `(fieldName, contentType, fileName) => (contentType === 'application/octet-stream' || fileName !== undefined)`)
|
||||
|
||||
* **limits** - _object_ - Various limits on incoming data. Valid properties are:
|
||||
|
||||
* **fieldNameSize** - _integer_ - Max field name size (in bytes) (Default: 100 bytes).
|
||||
|
||||
* **fieldSize** - _integer_ - Max field value size (in bytes) (Default: 1 MiB, which is 1024 x 1024 bytes).
|
||||
|
||||
* **fields** - _integer_ - Max number of non-file fields (Default: Infinity).
|
||||
|
||||
* **fileSize** - _integer_ - For multipart forms, the max file size (in bytes) (Default: Infinity).
|
||||
|
||||
* **files** - _integer_ - For multipart forms, the max number of file fields (Default: Infinity).
|
||||
|
||||
* **parts** - _integer_ - For multipart forms, the max number of parts (fields + files) (Default: Infinity).
|
||||
|
||||
* **headerPairs** - _integer_ - For multipart forms, the max number of header key=>value pairs to parse **Default:** 2000
|
||||
|
||||
* **headerSize** - _integer_ - For multipart forms, the max size of a multipart header **Default:** 81920.
|
||||
|
||||
* The constructor can throw errors:
|
||||
|
||||
* **Busboy expected an options-Object.** - Busboy expected an Object as first parameters.
|
||||
|
||||
* **Busboy expected an options-Object with headers-attribute.** - The first parameter is lacking of a headers-attribute.
|
||||
|
||||
* **Limit $limit is not a valid number** - Busboy expected the desired limit to be of type number. Busboy throws this Error to prevent a potential security issue by falling silently back to the Busboy-defaults. Potential source for this Error can be the direct use of environment variables without transforming them to the type number.
|
||||
|
||||
* **Unsupported Content-Type.** - The `Content-Type` isn't one Busboy can parse.
|
||||
|
||||
* **Missing Content-Type-header.** - The provided headers don't include `Content-Type` at all.
|
|
@ -0,0 +1,19 @@
|
|||
Copyright Brian White. All rights reserved.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to
|
||||
deal in the Software without restriction, including without limitation the
|
||||
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||
sell copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||
IN THE SOFTWARE.
|
207
g4f/Provider/npm/node_modules/@fastify/busboy/deps/dicer/lib/Dicer.js
generated
vendored
Normal file
207
g4f/Provider/npm/node_modules/@fastify/busboy/deps/dicer/lib/Dicer.js
generated
vendored
Normal file
|
@ -0,0 +1,207 @@
|
|||
'use strict'
|
||||
|
||||
const WritableStream = require('node:stream').Writable
|
||||
const inherits = require('node:util').inherits
|
||||
|
||||
const StreamSearch = require('../../streamsearch/sbmh')
|
||||
|
||||
const PartStream = require('./PartStream')
|
||||
const HeaderParser = require('./HeaderParser')
|
||||
|
||||
const DASH = 45
|
||||
const B_ONEDASH = Buffer.from('-')
|
||||
const B_CRLF = Buffer.from('\r\n')
|
||||
const EMPTY_FN = function () {}
|
||||
|
||||
function Dicer (cfg) {
|
||||
if (!(this instanceof Dicer)) { return new Dicer(cfg) }
|
||||
WritableStream.call(this, cfg)
|
||||
|
||||
if (!cfg || (!cfg.headerFirst && typeof cfg.boundary !== 'string')) { throw new TypeError('Boundary required') }
|
||||
|
||||
if (typeof cfg.boundary === 'string') { this.setBoundary(cfg.boundary) } else { this._bparser = undefined }
|
||||
|
||||
this._headerFirst = cfg.headerFirst
|
||||
|
||||
this._dashes = 0
|
||||
this._parts = 0
|
||||
this._finished = false
|
||||
this._realFinish = false
|
||||
this._isPreamble = true
|
||||
this._justMatched = false
|
||||
this._firstWrite = true
|
||||
this._inHeader = true
|
||||
this._part = undefined
|
||||
this._cb = undefined
|
||||
this._ignoreData = false
|
||||
this._partOpts = { highWaterMark: cfg.partHwm }
|
||||
this._pause = false
|
||||
|
||||
const self = this
|
||||
this._hparser = new HeaderParser(cfg)
|
||||
this._hparser.on('header', function (header) {
|
||||
self._inHeader = false
|
||||
self._part.emit('header', header)
|
||||
})
|
||||
}
|
||||
inherits(Dicer, WritableStream)
|
||||
|
||||
Dicer.prototype.emit = function (ev) {
|
||||
if (ev === 'finish' && !this._realFinish) {
|
||||
if (!this._finished) {
|
||||
const self = this
|
||||
process.nextTick(function () {
|
||||
self.emit('error', new Error('Unexpected end of multipart data'))
|
||||
if (self._part && !self._ignoreData) {
|
||||
const type = (self._isPreamble ? 'Preamble' : 'Part')
|
||||
self._part.emit('error', new Error(type + ' terminated early due to unexpected end of multipart data'))
|
||||
self._part.push(null)
|
||||
process.nextTick(function () {
|
||||
self._realFinish = true
|
||||
self.emit('finish')
|
||||
self._realFinish = false
|
||||
})
|
||||
return
|
||||
}
|
||||
self._realFinish = true
|
||||
self.emit('finish')
|
||||
self._realFinish = false
|
||||
})
|
||||
}
|
||||
} else { WritableStream.prototype.emit.apply(this, arguments) }
|
||||
}
|
||||
|
||||
Dicer.prototype._write = function (data, encoding, cb) {
|
||||
// ignore unexpected data (e.g. extra trailer data after finished)
|
||||
if (!this._hparser && !this._bparser) { return cb() }
|
||||
|
||||
if (this._headerFirst && this._isPreamble) {
|
||||
if (!this._part) {
|
||||
this._part = new PartStream(this._partOpts)
|
||||
if (this._events.preamble) { this.emit('preamble', this._part) } else { this._ignore() }
|
||||
}
|
||||
const r = this._hparser.push(data)
|
||||
if (!this._inHeader && r !== undefined && r < data.length) { data = data.slice(r) } else { return cb() }
|
||||
}
|
||||
|
||||
// allows for "easier" testing
|
||||
if (this._firstWrite) {
|
||||
this._bparser.push(B_CRLF)
|
||||
this._firstWrite = false
|
||||
}
|
||||
|
||||
this._bparser.push(data)
|
||||
|
||||
if (this._pause) { this._cb = cb } else { cb() }
|
||||
}
|
||||
|
||||
Dicer.prototype.reset = function () {
|
||||
this._part = undefined
|
||||
this._bparser = undefined
|
||||
this._hparser = undefined
|
||||
}
|
||||
|
||||
Dicer.prototype.setBoundary = function (boundary) {
|
||||
const self = this
|
||||
this._bparser = new StreamSearch('\r\n--' + boundary)
|
||||
this._bparser.on('info', function (isMatch, data, start, end) {
|
||||
self._oninfo(isMatch, data, start, end)
|
||||
})
|
||||
}
|
||||
|
||||
Dicer.prototype._ignore = function () {
|
||||
if (this._part && !this._ignoreData) {
|
||||
this._ignoreData = true
|
||||
this._part.on('error', EMPTY_FN)
|
||||
// we must perform some kind of read on the stream even though we are
|
||||
// ignoring the data, otherwise node's Readable stream will not emit 'end'
|
||||
// after pushing null to the stream
|
||||
this._part.resume()
|
||||
}
|
||||
}
|
||||
|
||||
Dicer.prototype._oninfo = function (isMatch, data, start, end) {
|
||||
let buf; const self = this; let i = 0; let r; let shouldWriteMore = true
|
||||
|
||||
if (!this._part && this._justMatched && data) {
|
||||
while (this._dashes < 2 && (start + i) < end) {
|
||||
if (data[start + i] === DASH) {
|
||||
++i
|
||||
++this._dashes
|
||||
} else {
|
||||
if (this._dashes) { buf = B_ONEDASH }
|
||||
this._dashes = 0
|
||||
break
|
||||
}
|
||||
}
|
||||
if (this._dashes === 2) {
|
||||
if ((start + i) < end && this._events.trailer) { this.emit('trailer', data.slice(start + i, end)) }
|
||||
this.reset()
|
||||
this._finished = true
|
||||
// no more parts will be added
|
||||
if (self._parts === 0) {
|
||||
self._realFinish = true
|
||||
self.emit('finish')
|
||||
self._realFinish = false
|
||||
}
|
||||
}
|
||||
if (this._dashes) { return }
|
||||
}
|
||||
if (this._justMatched) { this._justMatched = false }
|
||||
if (!this._part) {
|
||||
this._part = new PartStream(this._partOpts)
|
||||
this._part._read = function (n) {
|
||||
self._unpause()
|
||||
}
|
||||
if (this._isPreamble && this._events.preamble) { this.emit('preamble', this._part) } else if (this._isPreamble !== true && this._events.part) { this.emit('part', this._part) } else { this._ignore() }
|
||||
if (!this._isPreamble) { this._inHeader = true }
|
||||
}
|
||||
if (data && start < end && !this._ignoreData) {
|
||||
if (this._isPreamble || !this._inHeader) {
|
||||
if (buf) { shouldWriteMore = this._part.push(buf) }
|
||||
shouldWriteMore = this._part.push(data.slice(start, end))
|
||||
if (!shouldWriteMore) { this._pause = true }
|
||||
} else if (!this._isPreamble && this._inHeader) {
|
||||
if (buf) { this._hparser.push(buf) }
|
||||
r = this._hparser.push(data.slice(start, end))
|
||||
if (!this._inHeader && r !== undefined && r < end) { this._oninfo(false, data, start + r, end) }
|
||||
}
|
||||
}
|
||||
if (isMatch) {
|
||||
this._hparser.reset()
|
||||
if (this._isPreamble) { this._isPreamble = false } else {
|
||||
if (start !== end) {
|
||||
++this._parts
|
||||
this._part.on('end', function () {
|
||||
if (--self._parts === 0) {
|
||||
if (self._finished) {
|
||||
self._realFinish = true
|
||||
self.emit('finish')
|
||||
self._realFinish = false
|
||||
} else {
|
||||
self._unpause()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
this._part.push(null)
|
||||
this._part = undefined
|
||||
this._ignoreData = false
|
||||
this._justMatched = true
|
||||
this._dashes = 0
|
||||
}
|
||||
}
|
||||
|
||||
Dicer.prototype._unpause = function () {
|
||||
if (!this._pause) { return }
|
||||
|
||||
this._pause = false
|
||||
if (this._cb) {
|
||||
const cb = this._cb
|
||||
this._cb = undefined
|
||||
cb()
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Dicer
|
100
g4f/Provider/npm/node_modules/@fastify/busboy/deps/dicer/lib/HeaderParser.js
generated
vendored
Normal file
100
g4f/Provider/npm/node_modules/@fastify/busboy/deps/dicer/lib/HeaderParser.js
generated
vendored
Normal file
|
@ -0,0 +1,100 @@
|
|||
'use strict'
|
||||
|
||||
const EventEmitter = require('node:events').EventEmitter
|
||||
const inherits = require('node:util').inherits
|
||||
const getLimit = require('../../../lib/utils/getLimit')
|
||||
|
||||
const StreamSearch = require('../../streamsearch/sbmh')
|
||||
|
||||
const B_DCRLF = Buffer.from('\r\n\r\n')
|
||||
const RE_CRLF = /\r\n/g
|
||||
const RE_HDR = /^([^:]+):[ \t]?([\x00-\xFF]+)?$/ // eslint-disable-line no-control-regex
|
||||
|
||||
function HeaderParser (cfg) {
|
||||
EventEmitter.call(this)
|
||||
|
||||
cfg = cfg || {}
|
||||
const self = this
|
||||
this.nread = 0
|
||||
this.maxed = false
|
||||
this.npairs = 0
|
||||
this.maxHeaderPairs = getLimit(cfg, 'maxHeaderPairs', 2000)
|
||||
this.maxHeaderSize = getLimit(cfg, 'maxHeaderSize', 80 * 1024)
|
||||
this.buffer = ''
|
||||
this.header = {}
|
||||
this.finished = false
|
||||
this.ss = new StreamSearch(B_DCRLF)
|
||||
this.ss.on('info', function (isMatch, data, start, end) {
|
||||
if (data && !self.maxed) {
|
||||
if (self.nread + end - start >= self.maxHeaderSize) {
|
||||
end = self.maxHeaderSize - self.nread + start
|
||||
self.nread = self.maxHeaderSize
|
||||
self.maxed = true
|
||||
} else { self.nread += (end - start) }
|
||||
|
||||
self.buffer += data.toString('binary', start, end)
|
||||
}
|
||||
if (isMatch) { self._finish() }
|
||||
})
|
||||
}
|
||||
inherits(HeaderParser, EventEmitter)
|
||||
|
||||
HeaderParser.prototype.push = function (data) {
|
||||
const r = this.ss.push(data)
|
||||
if (this.finished) { return r }
|
||||
}
|
||||
|
||||
HeaderParser.prototype.reset = function () {
|
||||
this.finished = false
|
||||
this.buffer = ''
|
||||
this.header = {}
|
||||
this.ss.reset()
|
||||
}
|
||||
|
||||
HeaderParser.prototype._finish = function () {
|
||||
if (this.buffer) { this._parseHeader() }
|
||||
this.ss.matches = this.ss.maxMatches
|
||||
const header = this.header
|
||||
this.header = {}
|
||||
this.buffer = ''
|
||||
this.finished = true
|
||||
this.nread = this.npairs = 0
|
||||
this.maxed = false
|
||||
this.emit('header', header)
|
||||
}
|
||||
|
||||
HeaderParser.prototype._parseHeader = function () {
|
||||
if (this.npairs === this.maxHeaderPairs) { return }
|
||||
|
||||
const lines = this.buffer.split(RE_CRLF)
|
||||
const len = lines.length
|
||||
let m, h
|
||||
|
||||
for (var i = 0; i < len; ++i) { // eslint-disable-line no-var
|
||||
if (lines[i].length === 0) { continue }
|
||||
if (lines[i][0] === '\t' || lines[i][0] === ' ') {
|
||||
// folded header content
|
||||
// RFC2822 says to just remove the CRLF and not the whitespace following
|
||||
// it, so we follow the RFC and include the leading whitespace ...
|
||||
if (h) {
|
||||
this.header[h][this.header[h].length - 1] += lines[i]
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
const posColon = lines[i].indexOf(':')
|
||||
if (
|
||||
posColon === -1 ||
|
||||
posColon === 0
|
||||
) {
|
||||
return
|
||||
}
|
||||
m = RE_HDR.exec(lines[i])
|
||||
h = m[1].toLowerCase()
|
||||
this.header[h] = this.header[h] || []
|
||||
this.header[h].push((m[2] || ''))
|
||||
if (++this.npairs === this.maxHeaderPairs) { break }
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = HeaderParser
|
13
g4f/Provider/npm/node_modules/@fastify/busboy/deps/dicer/lib/PartStream.js
generated
vendored
Normal file
13
g4f/Provider/npm/node_modules/@fastify/busboy/deps/dicer/lib/PartStream.js
generated
vendored
Normal file
|
@ -0,0 +1,13 @@
|
|||
'use strict'
|
||||
|
||||
const inherits = require('node:util').inherits
|
||||
const ReadableStream = require('node:stream').Readable
|
||||
|
||||
function PartStream (opts) {
|
||||
ReadableStream.call(this, opts)
|
||||
}
|
||||
inherits(PartStream, ReadableStream)
|
||||
|
||||
PartStream.prototype._read = function (n) {}
|
||||
|
||||
module.exports = PartStream
|
164
g4f/Provider/npm/node_modules/@fastify/busboy/deps/dicer/lib/dicer.d.ts
generated
vendored
Normal file
164
g4f/Provider/npm/node_modules/@fastify/busboy/deps/dicer/lib/dicer.d.ts
generated
vendored
Normal file
|
@ -0,0 +1,164 @@
|
|||
// Type definitions for dicer 0.2
|
||||
// Project: https://github.com/mscdex/dicer
|
||||
// Definitions by: BendingBender <https://github.com/BendingBender>
|
||||
// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
|
||||
// TypeScript Version: 2.2
|
||||
/// <reference types="node" />
|
||||
|
||||
import stream = require("stream");
|
||||
|
||||
// tslint:disable:unified-signatures
|
||||
|
||||
/**
|
||||
* A very fast streaming multipart parser for node.js.
|
||||
* Dicer is a WritableStream
|
||||
*
|
||||
* Dicer (special) events:
|
||||
* - on('finish', ()) - Emitted when all parts have been parsed and the Dicer instance has been ended.
|
||||
* - on('part', (stream: PartStream)) - Emitted when a new part has been found.
|
||||
* - on('preamble', (stream: PartStream)) - Emitted for preamble if you should happen to need it (can usually be ignored).
|
||||
* - on('trailer', (data: Buffer)) - Emitted when trailing data was found after the terminating boundary (as with the preamble, this can usually be ignored too).
|
||||
*/
|
||||
export class Dicer extends stream.Writable {
|
||||
/**
|
||||
* Creates and returns a new Dicer instance with the following valid config settings:
|
||||
*
|
||||
* @param config The configuration to use
|
||||
*/
|
||||
constructor(config: Dicer.Config);
|
||||
/**
|
||||
* Sets the boundary to use for parsing and performs some initialization needed for parsing.
|
||||
* You should only need to use this if you set headerFirst to true in the constructor and are parsing the boundary from the preamble header.
|
||||
*
|
||||
* @param boundary The boundary to use
|
||||
*/
|
||||
setBoundary(boundary: string): void;
|
||||
addListener(event: "finish", listener: () => void): this;
|
||||
addListener(event: "part", listener: (stream: Dicer.PartStream) => void): this;
|
||||
addListener(event: "preamble", listener: (stream: Dicer.PartStream) => void): this;
|
||||
addListener(event: "trailer", listener: (data: Buffer) => void): this;
|
||||
addListener(event: "close", listener: () => void): this;
|
||||
addListener(event: "drain", listener: () => void): this;
|
||||
addListener(event: "error", listener: (err: Error) => void): this;
|
||||
addListener(event: "pipe", listener: (src: stream.Readable) => void): this;
|
||||
addListener(event: "unpipe", listener: (src: stream.Readable) => void): this;
|
||||
addListener(event: string, listener: (...args: any[]) => void): this;
|
||||
on(event: "finish", listener: () => void): this;
|
||||
on(event: "part", listener: (stream: Dicer.PartStream) => void): this;
|
||||
on(event: "preamble", listener: (stream: Dicer.PartStream) => void): this;
|
||||
on(event: "trailer", listener: (data: Buffer) => void): this;
|
||||
on(event: "close", listener: () => void): this;
|
||||
on(event: "drain", listener: () => void): this;
|
||||
on(event: "error", listener: (err: Error) => void): this;
|
||||
on(event: "pipe", listener: (src: stream.Readable) => void): this;
|
||||
on(event: "unpipe", listener: (src: stream.Readable) => void): this;
|
||||
on(event: string, listener: (...args: any[]) => void): this;
|
||||
once(event: "finish", listener: () => void): this;
|
||||
once(event: "part", listener: (stream: Dicer.PartStream) => void): this;
|
||||
once(event: "preamble", listener: (stream: Dicer.PartStream) => void): this;
|
||||
once(event: "trailer", listener: (data: Buffer) => void): this;
|
||||
once(event: "close", listener: () => void): this;
|
||||
once(event: "drain", listener: () => void): this;
|
||||
once(event: "error", listener: (err: Error) => void): this;
|
||||
once(event: "pipe", listener: (src: stream.Readable) => void): this;
|
||||
once(event: "unpipe", listener: (src: stream.Readable) => void): this;
|
||||
once(event: string, listener: (...args: any[]) => void): this;
|
||||
prependListener(event: "finish", listener: () => void): this;
|
||||
prependListener(event: "part", listener: (stream: Dicer.PartStream) => void): this;
|
||||
prependListener(event: "preamble", listener: (stream: Dicer.PartStream) => void): this;
|
||||
prependListener(event: "trailer", listener: (data: Buffer) => void): this;
|
||||
prependListener(event: "close", listener: () => void): this;
|
||||
prependListener(event: "drain", listener: () => void): this;
|
||||
prependListener(event: "error", listener: (err: Error) => void): this;
|
||||
prependListener(event: "pipe", listener: (src: stream.Readable) => void): this;
|
||||
prependListener(event: "unpipe", listener: (src: stream.Readable) => void): this;
|
||||
prependListener(event: string, listener: (...args: any[]) => void): this;
|
||||
prependOnceListener(event: "finish", listener: () => void): this;
|
||||
prependOnceListener(event: "part", listener: (stream: Dicer.PartStream) => void): this;
|
||||
prependOnceListener(event: "preamble", listener: (stream: Dicer.PartStream) => void): this;
|
||||
prependOnceListener(event: "trailer", listener: (data: Buffer) => void): this;
|
||||
prependOnceListener(event: "close", listener: () => void): this;
|
||||
prependOnceListener(event: "drain", listener: () => void): this;
|
||||
prependOnceListener(event: "error", listener: (err: Error) => void): this;
|
||||
prependOnceListener(event: "pipe", listener: (src: stream.Readable) => void): this;
|
||||
prependOnceListener(event: "unpipe", listener: (src: stream.Readable) => void): this;
|
||||
prependOnceListener(event: string, listener: (...args: any[]) => void): this;
|
||||
removeListener(event: "finish", listener: () => void): this;
|
||||
removeListener(event: "part", listener: (stream: Dicer.PartStream) => void): this;
|
||||
removeListener(event: "preamble", listener: (stream: Dicer.PartStream) => void): this;
|
||||
removeListener(event: "trailer", listener: (data: Buffer) => void): this;
|
||||
removeListener(event: "close", listener: () => void): this;
|
||||
removeListener(event: "drain", listener: () => void): this;
|
||||
removeListener(event: "error", listener: (err: Error) => void): this;
|
||||
removeListener(event: "pipe", listener: (src: stream.Readable) => void): this;
|
||||
removeListener(event: "unpipe", listener: (src: stream.Readable) => void): this;
|
||||
removeListener(event: string, listener: (...args: any[]) => void): this;
|
||||
}
|
||||
|
||||
declare namespace Dicer {
|
||||
interface Config {
|
||||
/**
|
||||
* This is the boundary used to detect the beginning of a new part.
|
||||
*/
|
||||
boundary?: string | undefined;
|
||||
/**
|
||||
* If true, preamble header parsing will be performed first.
|
||||
*/
|
||||
headerFirst?: boolean | undefined;
|
||||
/**
|
||||
* The maximum number of header key=>value pairs to parse Default: 2000 (same as node's http).
|
||||
*/
|
||||
maxHeaderPairs?: number | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* PartStream is a _ReadableStream_
|
||||
*
|
||||
* PartStream (special) events:
|
||||
* - on('header', (header: object)) - An object containing the header for this particular part. Each property value is an array of one or more string values.
|
||||
*/
|
||||
interface PartStream extends stream.Readable {
|
||||
addListener(event: "header", listener: (header: object) => void): this;
|
||||
addListener(event: "close", listener: () => void): this;
|
||||
addListener(event: "data", listener: (chunk: Buffer | string) => void): this;
|
||||
addListener(event: "end", listener: () => void): this;
|
||||
addListener(event: "readable", listener: () => void): this;
|
||||
addListener(event: "error", listener: (err: Error) => void): this;
|
||||
addListener(event: string, listener: (...args: any[]) => void): this;
|
||||
on(event: "header", listener: (header: object) => void): this;
|
||||
on(event: "close", listener: () => void): this;
|
||||
on(event: "data", listener: (chunk: Buffer | string) => void): this;
|
||||
on(event: "end", listener: () => void): this;
|
||||
on(event: "readable", listener: () => void): this;
|
||||
on(event: "error", listener: (err: Error) => void): this;
|
||||
on(event: string, listener: (...args: any[]) => void): this;
|
||||
once(event: "header", listener: (header: object) => void): this;
|
||||
once(event: "close", listener: () => void): this;
|
||||
once(event: "data", listener: (chunk: Buffer | string) => void): this;
|
||||
once(event: "end", listener: () => void): this;
|
||||
once(event: "readable", listener: () => void): this;
|
||||
once(event: "error", listener: (err: Error) => void): this;
|
||||
once(event: string, listener: (...args: any[]) => void): this;
|
||||
prependListener(event: "header", listener: (header: object) => void): this;
|
||||
prependListener(event: "close", listener: () => void): this;
|
||||
prependListener(event: "data", listener: (chunk: Buffer | string) => void): this;
|
||||
prependListener(event: "end", listener: () => void): this;
|
||||
prependListener(event: "readable", listener: () => void): this;
|
||||
prependListener(event: "error", listener: (err: Error) => void): this;
|
||||
prependListener(event: string, listener: (...args: any[]) => void): this;
|
||||
prependOnceListener(event: "header", listener: (header: object) => void): this;
|
||||
prependOnceListener(event: "close", listener: () => void): this;
|
||||
prependOnceListener(event: "data", listener: (chunk: Buffer | string) => void): this;
|
||||
prependOnceListener(event: "end", listener: () => void): this;
|
||||
prependOnceListener(event: "readable", listener: () => void): this;
|
||||
prependOnceListener(event: "error", listener: (err: Error) => void): this;
|
||||
prependOnceListener(event: string, listener: (...args: any[]) => void): this;
|
||||
removeListener(event: "header", listener: (header: object) => void): this;
|
||||
removeListener(event: "close", listener: () => void): this;
|
||||
removeListener(event: "data", listener: (chunk: Buffer | string) => void): this;
|
||||
removeListener(event: "end", listener: () => void): this;
|
||||
removeListener(event: "readable", listener: () => void): this;
|
||||
removeListener(event: "error", listener: (err: Error) => void): this;
|
||||
removeListener(event: string, listener: (...args: any[]) => void): this;
|
||||
}
|
||||
}
|
228
g4f/Provider/npm/node_modules/@fastify/busboy/deps/streamsearch/sbmh.js
generated
vendored
Normal file
228
g4f/Provider/npm/node_modules/@fastify/busboy/deps/streamsearch/sbmh.js
generated
vendored
Normal file
|
@ -0,0 +1,228 @@
|
|||
'use strict'
|
||||
|
||||
/**
|
||||
* Copyright Brian White. All rights reserved.
|
||||
*
|
||||
* @see https://github.com/mscdex/streamsearch
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to
|
||||
* deal in the Software without restriction, including without limitation the
|
||||
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||
* sell copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||
* IN THE SOFTWARE.
|
||||
*
|
||||
* Based heavily on the Streaming Boyer-Moore-Horspool C++ implementation
|
||||
* by Hongli Lai at: https://github.com/FooBarWidget/boyer-moore-horspool
|
||||
*/
|
||||
const EventEmitter = require('node:events').EventEmitter
|
||||
const inherits = require('node:util').inherits
|
||||
|
||||
function SBMH (needle) {
|
||||
if (typeof needle === 'string') {
|
||||
needle = Buffer.from(needle)
|
||||
}
|
||||
|
||||
if (!Buffer.isBuffer(needle)) {
|
||||
throw new TypeError('The needle has to be a String or a Buffer.')
|
||||
}
|
||||
|
||||
const needleLength = needle.length
|
||||
|
||||
if (needleLength === 0) {
|
||||
throw new Error('The needle cannot be an empty String/Buffer.')
|
||||
}
|
||||
|
||||
if (needleLength > 256) {
|
||||
throw new Error('The needle cannot have a length bigger than 256.')
|
||||
}
|
||||
|
||||
this.maxMatches = Infinity
|
||||
this.matches = 0
|
||||
|
||||
this._occ = new Array(256)
|
||||
.fill(needleLength) // Initialize occurrence table.
|
||||
this._lookbehind_size = 0
|
||||
this._needle = needle
|
||||
this._bufpos = 0
|
||||
|
||||
this._lookbehind = Buffer.alloc(needleLength)
|
||||
|
||||
// Populate occurrence table with analysis of the needle,
|
||||
// ignoring last letter.
|
||||
for (var i = 0; i < needleLength - 1; ++i) { // eslint-disable-line no-var
|
||||
this._occ[needle[i]] = needleLength - 1 - i
|
||||
}
|
||||
}
|
||||
inherits(SBMH, EventEmitter)
|
||||
|
||||
SBMH.prototype.reset = function () {
|
||||
this._lookbehind_size = 0
|
||||
this.matches = 0
|
||||
this._bufpos = 0
|
||||
}
|
||||
|
||||
SBMH.prototype.push = function (chunk, pos) {
|
||||
if (!Buffer.isBuffer(chunk)) {
|
||||
chunk = Buffer.from(chunk, 'binary')
|
||||
}
|
||||
const chlen = chunk.length
|
||||
this._bufpos = pos || 0
|
||||
let r
|
||||
while (r !== chlen && this.matches < this.maxMatches) { r = this._sbmh_feed(chunk) }
|
||||
return r
|
||||
}
|
||||
|
||||
SBMH.prototype._sbmh_feed = function (data) {
|
||||
const len = data.length
|
||||
const needle = this._needle
|
||||
const needleLength = needle.length
|
||||
const lastNeedleChar = needle[needleLength - 1]
|
||||
|
||||
// Positive: points to a position in `data`
|
||||
// pos == 3 points to data[3]
|
||||
// Negative: points to a position in the lookbehind buffer
|
||||
// pos == -2 points to lookbehind[lookbehind_size - 2]
|
||||
let pos = -this._lookbehind_size
|
||||
let ch
|
||||
|
||||
if (pos < 0) {
|
||||
// Lookbehind buffer is not empty. Perform Boyer-Moore-Horspool
|
||||
// search with character lookup code that considers both the
|
||||
// lookbehind buffer and the current round's haystack data.
|
||||
//
|
||||
// Loop until
|
||||
// there is a match.
|
||||
// or until
|
||||
// we've moved past the position that requires the
|
||||
// lookbehind buffer. In this case we switch to the
|
||||
// optimized loop.
|
||||
// or until
|
||||
// the character to look at lies outside the haystack.
|
||||
while (pos < 0 && pos <= len - needleLength) {
|
||||
ch = this._sbmh_lookup_char(data, pos + needleLength - 1)
|
||||
|
||||
if (
|
||||
ch === lastNeedleChar &&
|
||||
this._sbmh_memcmp(data, pos, needleLength - 1)
|
||||
) {
|
||||
this._lookbehind_size = 0
|
||||
++this.matches
|
||||
this.emit('info', true)
|
||||
|
||||
return (this._bufpos = pos + needleLength)
|
||||
}
|
||||
pos += this._occ[ch]
|
||||
}
|
||||
|
||||
// No match.
|
||||
|
||||
if (pos < 0) {
|
||||
// There's too few data for Boyer-Moore-Horspool to run,
|
||||
// so let's use a different algorithm to skip as much as
|
||||
// we can.
|
||||
// Forward pos until
|
||||
// the trailing part of lookbehind + data
|
||||
// looks like the beginning of the needle
|
||||
// or until
|
||||
// pos == 0
|
||||
while (pos < 0 && !this._sbmh_memcmp(data, pos, len - pos)) { ++pos }
|
||||
}
|
||||
|
||||
if (pos >= 0) {
|
||||
// Discard lookbehind buffer.
|
||||
this.emit('info', false, this._lookbehind, 0, this._lookbehind_size)
|
||||
this._lookbehind_size = 0
|
||||
} else {
|
||||
// Cut off part of the lookbehind buffer that has
|
||||
// been processed and append the entire haystack
|
||||
// into it.
|
||||
const bytesToCutOff = this._lookbehind_size + pos
|
||||
if (bytesToCutOff > 0) {
|
||||
// The cut off data is guaranteed not to contain the needle.
|
||||
this.emit('info', false, this._lookbehind, 0, bytesToCutOff)
|
||||
}
|
||||
|
||||
this._lookbehind.copy(this._lookbehind, 0, bytesToCutOff,
|
||||
this._lookbehind_size - bytesToCutOff)
|
||||
this._lookbehind_size -= bytesToCutOff
|
||||
|
||||
data.copy(this._lookbehind, this._lookbehind_size)
|
||||
this._lookbehind_size += len
|
||||
|
||||
this._bufpos = len
|
||||
return len
|
||||
}
|
||||
}
|
||||
|
||||
pos += (pos >= 0) * this._bufpos
|
||||
|
||||
// Lookbehind buffer is now empty. We only need to check if the
|
||||
// needle is in the haystack.
|
||||
if (data.indexOf(needle, pos) !== -1) {
|
||||
pos = data.indexOf(needle, pos)
|
||||
++this.matches
|
||||
if (pos > 0) { this.emit('info', true, data, this._bufpos, pos) } else { this.emit('info', true) }
|
||||
|
||||
return (this._bufpos = pos + needleLength)
|
||||
} else {
|
||||
pos = len - needleLength
|
||||
}
|
||||
|
||||
// There was no match. If there's trailing haystack data that we cannot
|
||||
// match yet using the Boyer-Moore-Horspool algorithm (because the trailing
|
||||
// data is less than the needle size) then match using a modified
|
||||
// algorithm that starts matching from the beginning instead of the end.
|
||||
// Whatever trailing data is left after running this algorithm is added to
|
||||
// the lookbehind buffer.
|
||||
while (
|
||||
pos < len &&
|
||||
(
|
||||
data[pos] !== needle[0] ||
|
||||
(
|
||||
(Buffer.compare(
|
||||
data.subarray(pos, pos + len - pos),
|
||||
needle.subarray(0, len - pos)
|
||||
) !== 0)
|
||||
)
|
||||
)
|
||||
) {
|
||||
++pos
|
||||
}
|
||||
if (pos < len) {
|
||||
data.copy(this._lookbehind, 0, pos, pos + (len - pos))
|
||||
this._lookbehind_size = len - pos
|
||||
}
|
||||
|
||||
// Everything until pos is guaranteed not to contain needle data.
|
||||
if (pos > 0) { this.emit('info', false, data, this._bufpos, pos < len ? pos : len) }
|
||||
|
||||
this._bufpos = len
|
||||
return len
|
||||
}
|
||||
|
||||
SBMH.prototype._sbmh_lookup_char = function (data, pos) {
|
||||
return (pos < 0)
|
||||
? this._lookbehind[this._lookbehind_size + pos]
|
||||
: data[pos]
|
||||
}
|
||||
|
||||
SBMH.prototype._sbmh_memcmp = function (data, pos, len) {
|
||||
for (var i = 0; i < len; ++i) { // eslint-disable-line no-var
|
||||
if (this._sbmh_lookup_char(data, pos + i) !== this._needle[i]) { return false }
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
module.exports = SBMH
|
|
@ -0,0 +1,196 @@
|
|||
// Definitions by: Jacob Baskin <https://github.com/jacobbaskin>
|
||||
// BendingBender <https://github.com/BendingBender>
|
||||
// Igor Savin <https://github.com/kibertoad>
|
||||
|
||||
/// <reference types="node" />
|
||||
|
||||
import * as http from 'http';
|
||||
import { Readable, Writable } from 'stream';
|
||||
export { Dicer } from "../deps/dicer/lib/dicer";
|
||||
|
||||
export const Busboy: BusboyConstructor;
|
||||
export default Busboy;
|
||||
|
||||
export interface BusboyConfig {
|
||||
/**
|
||||
* These are the HTTP headers of the incoming request, which are used by individual parsers.
|
||||
*/
|
||||
headers: BusboyHeaders;
|
||||
/**
|
||||
* `highWaterMark` to use for this Busboy instance.
|
||||
* @default WritableStream default.
|
||||
*/
|
||||
highWaterMark?: number | undefined;
|
||||
/**
|
||||
* highWaterMark to use for file streams.
|
||||
* @default ReadableStream default.
|
||||
*/
|
||||
fileHwm?: number | undefined;
|
||||
/**
|
||||
* Default character set to use when one isn't defined.
|
||||
* @default 'utf8'
|
||||
*/
|
||||
defCharset?: string | undefined;
|
||||
/**
|
||||
* Detect if a Part is a file.
|
||||
*
|
||||
* By default a file is detected if contentType
|
||||
* is application/octet-stream or fileName is not
|
||||
* undefined.
|
||||
*
|
||||
* Modify this to handle e.g. Blobs.
|
||||
*/
|
||||
isPartAFile?: (fieldName: string | undefined, contentType: string | undefined, fileName: string | undefined) => boolean;
|
||||
/**
|
||||
* If paths in the multipart 'filename' field shall be preserved.
|
||||
* @default false
|
||||
*/
|
||||
preservePath?: boolean | undefined;
|
||||
/**
|
||||
* Various limits on incoming data.
|
||||
*/
|
||||
limits?:
|
||||
| {
|
||||
/**
|
||||
* Max field name size (in bytes)
|
||||
* @default 100 bytes
|
||||
*/
|
||||
fieldNameSize?: number | undefined;
|
||||
/**
|
||||
* Max field value size (in bytes)
|
||||
* @default 1MB
|
||||
*/
|
||||
fieldSize?: number | undefined;
|
||||
/**
|
||||
* Max number of non-file fields
|
||||
* @default Infinity
|
||||
*/
|
||||
fields?: number | undefined;
|
||||
/**
|
||||
* For multipart forms, the max file size (in bytes)
|
||||
* @default Infinity
|
||||
*/
|
||||
fileSize?: number | undefined;
|
||||
/**
|
||||
* For multipart forms, the max number of file fields
|
||||
* @default Infinity
|
||||
*/
|
||||
files?: number | undefined;
|
||||
/**
|
||||
* For multipart forms, the max number of parts (fields + files)
|
||||
* @default Infinity
|
||||
*/
|
||||
parts?: number | undefined;
|
||||
/**
|
||||
* For multipart forms, the max number of header key=>value pairs to parse
|
||||
* @default 2000
|
||||
*/
|
||||
headerPairs?: number | undefined;
|
||||
|
||||
/**
|
||||
* For multipart forms, the max size of a header part
|
||||
* @default 81920
|
||||
*/
|
||||
headerSize?: number | undefined;
|
||||
}
|
||||
| undefined;
|
||||
}
|
||||
|
||||
export type BusboyHeaders = { 'content-type': string } & http.IncomingHttpHeaders;
|
||||
|
||||
export interface BusboyFileStream extends
|
||||
Readable {
|
||||
|
||||
truncated: boolean;
|
||||
|
||||
/**
|
||||
* The number of bytes that have been read so far.
|
||||
*/
|
||||
bytesRead: number;
|
||||
}
|
||||
|
||||
export interface Busboy extends Writable {
|
||||
addListener<Event extends keyof BusboyEvents>(event: Event, listener: BusboyEvents[Event]): this;
|
||||
|
||||
addListener(event: string | symbol, listener: (...args: any[]) => void): this;
|
||||
|
||||
on<Event extends keyof BusboyEvents>(event: Event, listener: BusboyEvents[Event]): this;
|
||||
|
||||
on(event: string | symbol, listener: (...args: any[]) => void): this;
|
||||
|
||||
once<Event extends keyof BusboyEvents>(event: Event, listener: BusboyEvents[Event]): this;
|
||||
|
||||
once(event: string | symbol, listener: (...args: any[]) => void): this;
|
||||
|
||||
removeListener<Event extends keyof BusboyEvents>(event: Event, listener: BusboyEvents[Event]): this;
|
||||
|
||||
removeListener(event: string | symbol, listener: (...args: any[]) => void): this;
|
||||
|
||||
off<Event extends keyof BusboyEvents>(event: Event, listener: BusboyEvents[Event]): this;
|
||||
|
||||
off(event: string | symbol, listener: (...args: any[]) => void): this;
|
||||
|
||||
prependListener<Event extends keyof BusboyEvents>(event: Event, listener: BusboyEvents[Event]): this;
|
||||
|
||||
prependListener(event: string | symbol, listener: (...args: any[]) => void): this;
|
||||
|
||||
prependOnceListener<Event extends keyof BusboyEvents>(event: Event, listener: BusboyEvents[Event]): this;
|
||||
|
||||
prependOnceListener(event: string | symbol, listener: (...args: any[]) => void): this;
|
||||
}
|
||||
|
||||
export interface BusboyEvents {
|
||||
/**
|
||||
* Emitted for each new file form field found.
|
||||
*
|
||||
* * Note: if you listen for this event, you should always handle the `stream` no matter if you care about the
|
||||
* file contents or not (e.g. you can simply just do `stream.resume();` if you want to discard the contents),
|
||||
* otherwise the 'finish' event will never fire on the Busboy instance. However, if you don't care about **any**
|
||||
* incoming files, you can simply not listen for the 'file' event at all and any/all files will be automatically
|
||||
* and safely discarded (these discarded files do still count towards `files` and `parts` limits).
|
||||
* * If a configured file size limit was reached, `stream` will both have a boolean property `truncated`
|
||||
* (best checked at the end of the stream) and emit a 'limit' event to notify you when this happens.
|
||||
*
|
||||
* @param listener.transferEncoding Contains the 'Content-Transfer-Encoding' value for the file stream.
|
||||
* @param listener.mimeType Contains the 'Content-Type' value for the file stream.
|
||||
*/
|
||||
file: (
|
||||
fieldname: string,
|
||||
stream: BusboyFileStream,
|
||||
filename: string,
|
||||
transferEncoding: string,
|
||||
mimeType: string,
|
||||
) => void;
|
||||
/**
|
||||
* Emitted for each new non-file field found.
|
||||
*/
|
||||
field: (
|
||||
fieldname: string,
|
||||
value: string,
|
||||
fieldnameTruncated: boolean,
|
||||
valueTruncated: boolean,
|
||||
transferEncoding: string,
|
||||
mimeType: string,
|
||||
) => void;
|
||||
finish: () => void;
|
||||
/**
|
||||
* Emitted when specified `parts` limit has been reached. No more 'file' or 'field' events will be emitted.
|
||||
*/
|
||||
partsLimit: () => void;
|
||||
/**
|
||||
* Emitted when specified `files` limit has been reached. No more 'file' events will be emitted.
|
||||
*/
|
||||
filesLimit: () => void;
|
||||
/**
|
||||
* Emitted when specified `fields` limit has been reached. No more 'field' events will be emitted.
|
||||
*/
|
||||
fieldsLimit: () => void;
|
||||
error: (error: unknown) => void;
|
||||
}
|
||||
|
||||
export interface BusboyConstructor {
|
||||
(options: BusboyConfig): Busboy;
|
||||
|
||||
new(options: BusboyConfig): Busboy;
|
||||
}
|
||||
|
|
@ -0,0 +1,85 @@
|
|||
'use strict'
|
||||
|
||||
const WritableStream = require('node:stream').Writable
|
||||
const { inherits } = require('node:util')
|
||||
const Dicer = require('../deps/dicer/lib/Dicer')
|
||||
|
||||
const MultipartParser = require('./types/multipart')
|
||||
const UrlencodedParser = require('./types/urlencoded')
|
||||
const parseParams = require('./utils/parseParams')
|
||||
|
||||
function Busboy (opts) {
|
||||
if (!(this instanceof Busboy)) { return new Busboy(opts) }
|
||||
|
||||
if (typeof opts !== 'object') {
|
||||
throw new TypeError('Busboy expected an options-Object.')
|
||||
}
|
||||
if (typeof opts.headers !== 'object') {
|
||||
throw new TypeError('Busboy expected an options-Object with headers-attribute.')
|
||||
}
|
||||
if (typeof opts.headers['content-type'] !== 'string') {
|
||||
throw new TypeError('Missing Content-Type-header.')
|
||||
}
|
||||
|
||||
const {
|
||||
headers,
|
||||
...streamOptions
|
||||
} = opts
|
||||
|
||||
this.opts = {
|
||||
autoDestroy: false,
|
||||
...streamOptions
|
||||
}
|
||||
WritableStream.call(this, this.opts)
|
||||
|
||||
this._done = false
|
||||
this._parser = this.getParserByHeaders(headers)
|
||||
this._finished = false
|
||||
}
|
||||
inherits(Busboy, WritableStream)
|
||||
|
||||
Busboy.prototype.emit = function (ev) {
|
||||
if (ev === 'finish') {
|
||||
if (!this._done) {
|
||||
this._parser?.end()
|
||||
return
|
||||
} else if (this._finished) {
|
||||
return
|
||||
}
|
||||
this._finished = true
|
||||
}
|
||||
WritableStream.prototype.emit.apply(this, arguments)
|
||||
}
|
||||
|
||||
Busboy.prototype.getParserByHeaders = function (headers) {
|
||||
const parsed = parseParams(headers['content-type'])
|
||||
|
||||
const cfg = {
|
||||
defCharset: this.opts.defCharset,
|
||||
fileHwm: this.opts.fileHwm,
|
||||
headers,
|
||||
highWaterMark: this.opts.highWaterMark,
|
||||
isPartAFile: this.opts.isPartAFile,
|
||||
limits: this.opts.limits,
|
||||
parsedConType: parsed,
|
||||
preservePath: this.opts.preservePath
|
||||
}
|
||||
|
||||
if (MultipartParser.detect.test(parsed[0])) {
|
||||
return new MultipartParser(this, cfg)
|
||||
}
|
||||
if (UrlencodedParser.detect.test(parsed[0])) {
|
||||
return new UrlencodedParser(this, cfg)
|
||||
}
|
||||
throw new Error('Unsupported Content-Type.')
|
||||
}
|
||||
|
||||
Busboy.prototype._write = function (chunk, encoding, cb) {
|
||||
this._parser.write(chunk, cb)
|
||||
}
|
||||
|
||||
module.exports = Busboy
|
||||
module.exports.default = Busboy
|
||||
module.exports.Busboy = Busboy
|
||||
|
||||
module.exports.Dicer = Dicer
|
306
g4f/Provider/npm/node_modules/@fastify/busboy/lib/types/multipart.js
generated
vendored
Normal file
306
g4f/Provider/npm/node_modules/@fastify/busboy/lib/types/multipart.js
generated
vendored
Normal file
|
@ -0,0 +1,306 @@
|
|||
'use strict'
|
||||
|
||||
// TODO:
|
||||
// * support 1 nested multipart level
|
||||
// (see second multipart example here:
|
||||
// http://www.w3.org/TR/html401/interact/forms.html#didx-multipartform-data)
|
||||
// * support limits.fieldNameSize
|
||||
// -- this will require modifications to utils.parseParams
|
||||
|
||||
const { Readable } = require('node:stream')
|
||||
const { inherits } = require('node:util')
|
||||
|
||||
const Dicer = require('../../deps/dicer/lib/Dicer')
|
||||
|
||||
const parseParams = require('../utils/parseParams')
|
||||
const decodeText = require('../utils/decodeText')
|
||||
const basename = require('../utils/basename')
|
||||
const getLimit = require('../utils/getLimit')
|
||||
|
||||
const RE_BOUNDARY = /^boundary$/i
|
||||
const RE_FIELD = /^form-data$/i
|
||||
const RE_CHARSET = /^charset$/i
|
||||
const RE_FILENAME = /^filename$/i
|
||||
const RE_NAME = /^name$/i
|
||||
|
||||
Multipart.detect = /^multipart\/form-data/i
|
||||
function Multipart (boy, cfg) {
|
||||
let i
|
||||
let len
|
||||
const self = this
|
||||
let boundary
|
||||
const limits = cfg.limits
|
||||
const isPartAFile = cfg.isPartAFile || ((fieldName, contentType, fileName) => (contentType === 'application/octet-stream' || fileName !== undefined))
|
||||
const parsedConType = cfg.parsedConType || []
|
||||
const defCharset = cfg.defCharset || 'utf8'
|
||||
const preservePath = cfg.preservePath
|
||||
const fileOpts = { highWaterMark: cfg.fileHwm }
|
||||
|
||||
for (i = 0, len = parsedConType.length; i < len; ++i) {
|
||||
if (Array.isArray(parsedConType[i]) &&
|
||||
RE_BOUNDARY.test(parsedConType[i][0])) {
|
||||
boundary = parsedConType[i][1]
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
function checkFinished () {
|
||||
if (nends === 0 && finished && !boy._done) {
|
||||
finished = false
|
||||
self.end()
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof boundary !== 'string') { throw new Error('Multipart: Boundary not found') }
|
||||
|
||||
const fieldSizeLimit = getLimit(limits, 'fieldSize', 1 * 1024 * 1024)
|
||||
const fileSizeLimit = getLimit(limits, 'fileSize', Infinity)
|
||||
const filesLimit = getLimit(limits, 'files', Infinity)
|
||||
const fieldsLimit = getLimit(limits, 'fields', Infinity)
|
||||
const partsLimit = getLimit(limits, 'parts', Infinity)
|
||||
const headerPairsLimit = getLimit(limits, 'headerPairs', 2000)
|
||||
const headerSizeLimit = getLimit(limits, 'headerSize', 80 * 1024)
|
||||
|
||||
let nfiles = 0
|
||||
let nfields = 0
|
||||
let nends = 0
|
||||
let curFile
|
||||
let curField
|
||||
let finished = false
|
||||
|
||||
this._needDrain = false
|
||||
this._pause = false
|
||||
this._cb = undefined
|
||||
this._nparts = 0
|
||||
this._boy = boy
|
||||
|
||||
const parserCfg = {
|
||||
boundary,
|
||||
maxHeaderPairs: headerPairsLimit,
|
||||
maxHeaderSize: headerSizeLimit,
|
||||
partHwm: fileOpts.highWaterMark,
|
||||
highWaterMark: cfg.highWaterMark
|
||||
}
|
||||
|
||||
this.parser = new Dicer(parserCfg)
|
||||
this.parser.on('drain', function () {
|
||||
self._needDrain = false
|
||||
if (self._cb && !self._pause) {
|
||||
const cb = self._cb
|
||||
self._cb = undefined
|
||||
cb()
|
||||
}
|
||||
}).on('part', function onPart (part) {
|
||||
if (++self._nparts > partsLimit) {
|
||||
self.parser.removeListener('part', onPart)
|
||||
self.parser.on('part', skipPart)
|
||||
boy.hitPartsLimit = true
|
||||
boy.emit('partsLimit')
|
||||
return skipPart(part)
|
||||
}
|
||||
|
||||
// hack because streams2 _always_ doesn't emit 'end' until nextTick, so let
|
||||
// us emit 'end' early since we know the part has ended if we are already
|
||||
// seeing the next part
|
||||
if (curField) {
|
||||
const field = curField
|
||||
field.emit('end')
|
||||
field.removeAllListeners('end')
|
||||
}
|
||||
|
||||
part.on('header', function (header) {
|
||||
let contype
|
||||
let fieldname
|
||||
let parsed
|
||||
let charset
|
||||
let encoding
|
||||
let filename
|
||||
let nsize = 0
|
||||
|
||||
if (header['content-type']) {
|
||||
parsed = parseParams(header['content-type'][0])
|
||||
if (parsed[0]) {
|
||||
contype = parsed[0].toLowerCase()
|
||||
for (i = 0, len = parsed.length; i < len; ++i) {
|
||||
if (RE_CHARSET.test(parsed[i][0])) {
|
||||
charset = parsed[i][1].toLowerCase()
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (contype === undefined) { contype = 'text/plain' }
|
||||
if (charset === undefined) { charset = defCharset }
|
||||
|
||||
if (header['content-disposition']) {
|
||||
parsed = parseParams(header['content-disposition'][0])
|
||||
if (!RE_FIELD.test(parsed[0])) { return skipPart(part) }
|
||||
for (i = 0, len = parsed.length; i < len; ++i) {
|
||||
if (RE_NAME.test(parsed[i][0])) {
|
||||
fieldname = parsed[i][1]
|
||||
} else if (RE_FILENAME.test(parsed[i][0])) {
|
||||
filename = parsed[i][1]
|
||||
if (!preservePath) { filename = basename(filename) }
|
||||
}
|
||||
}
|
||||
} else { return skipPart(part) }
|
||||
|
||||
if (header['content-transfer-encoding']) { encoding = header['content-transfer-encoding'][0].toLowerCase() } else { encoding = '7bit' }
|
||||
|
||||
let onData,
|
||||
onEnd
|
||||
|
||||
if (isPartAFile(fieldname, contype, filename)) {
|
||||
// file/binary field
|
||||
if (nfiles === filesLimit) {
|
||||
if (!boy.hitFilesLimit) {
|
||||
boy.hitFilesLimit = true
|
||||
boy.emit('filesLimit')
|
||||
}
|
||||
return skipPart(part)
|
||||
}
|
||||
|
||||
++nfiles
|
||||
|
||||
if (!boy._events.file) {
|
||||
self.parser._ignore()
|
||||
return
|
||||
}
|
||||
|
||||
++nends
|
||||
const file = new FileStream(fileOpts)
|
||||
curFile = file
|
||||
file.on('end', function () {
|
||||
--nends
|
||||
self._pause = false
|
||||
checkFinished()
|
||||
if (self._cb && !self._needDrain) {
|
||||
const cb = self._cb
|
||||
self._cb = undefined
|
||||
cb()
|
||||
}
|
||||
})
|
||||
file._read = function (n) {
|
||||
if (!self._pause) { return }
|
||||
self._pause = false
|
||||
if (self._cb && !self._needDrain) {
|
||||
const cb = self._cb
|
||||
self._cb = undefined
|
||||
cb()
|
||||
}
|
||||
}
|
||||
boy.emit('file', fieldname, file, filename, encoding, contype)
|
||||
|
||||
onData = function (data) {
|
||||
if ((nsize += data.length) > fileSizeLimit) {
|
||||
const extralen = fileSizeLimit - nsize + data.length
|
||||
if (extralen > 0) { file.push(data.slice(0, extralen)) }
|
||||
file.truncated = true
|
||||
file.bytesRead = fileSizeLimit
|
||||
part.removeAllListeners('data')
|
||||
file.emit('limit')
|
||||
return
|
||||
} else if (!file.push(data)) { self._pause = true }
|
||||
|
||||
file.bytesRead = nsize
|
||||
}
|
||||
|
||||
onEnd = function () {
|
||||
curFile = undefined
|
||||
file.push(null)
|
||||
}
|
||||
} else {
|
||||
// non-file field
|
||||
if (nfields === fieldsLimit) {
|
||||
if (!boy.hitFieldsLimit) {
|
||||
boy.hitFieldsLimit = true
|
||||
boy.emit('fieldsLimit')
|
||||
}
|
||||
return skipPart(part)
|
||||
}
|
||||
|
||||
++nfields
|
||||
++nends
|
||||
let buffer = ''
|
||||
let truncated = false
|
||||
curField = part
|
||||
|
||||
onData = function (data) {
|
||||
if ((nsize += data.length) > fieldSizeLimit) {
|
||||
const extralen = (fieldSizeLimit - (nsize - data.length))
|
||||
buffer += data.toString('binary', 0, extralen)
|
||||
truncated = true
|
||||
part.removeAllListeners('data')
|
||||
} else { buffer += data.toString('binary') }
|
||||
}
|
||||
|
||||
onEnd = function () {
|
||||
curField = undefined
|
||||
if (buffer.length) { buffer = decodeText(buffer, 'binary', charset) }
|
||||
boy.emit('field', fieldname, buffer, false, truncated, encoding, contype)
|
||||
--nends
|
||||
checkFinished()
|
||||
}
|
||||
}
|
||||
|
||||
/* As of node@2efe4ab761666 (v0.10.29+/v0.11.14+), busboy had become
|
||||
broken. Streams2/streams3 is a huge black box of confusion, but
|
||||
somehow overriding the sync state seems to fix things again (and still
|
||||
seems to work for previous node versions).
|
||||
*/
|
||||
part._readableState.sync = false
|
||||
|
||||
part.on('data', onData)
|
||||
part.on('end', onEnd)
|
||||
}).on('error', function (err) {
|
||||
if (curFile) { curFile.emit('error', err) }
|
||||
})
|
||||
}).on('error', function (err) {
|
||||
boy.emit('error', err)
|
||||
}).on('finish', function () {
|
||||
finished = true
|
||||
checkFinished()
|
||||
})
|
||||
}
|
||||
|
||||
Multipart.prototype.write = function (chunk, cb) {
|
||||
const r = this.parser.write(chunk)
|
||||
if (r && !this._pause) {
|
||||
cb()
|
||||
} else {
|
||||
this._needDrain = !r
|
||||
this._cb = cb
|
||||
}
|
||||
}
|
||||
|
||||
Multipart.prototype.end = function () {
|
||||
const self = this
|
||||
|
||||
if (self.parser.writable) {
|
||||
self.parser.end()
|
||||
} else if (!self._boy._done) {
|
||||
process.nextTick(function () {
|
||||
self._boy._done = true
|
||||
self._boy.emit('finish')
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function skipPart (part) {
|
||||
part.resume()
|
||||
}
|
||||
|
||||
function FileStream (opts) {
|
||||
Readable.call(this, opts)
|
||||
|
||||
this.bytesRead = 0
|
||||
|
||||
this.truncated = false
|
||||
}
|
||||
|
||||
inherits(FileStream, Readable)
|
||||
|
||||
FileStream.prototype._read = function (n) {}
|
||||
|
||||
module.exports = Multipart
|
190
g4f/Provider/npm/node_modules/@fastify/busboy/lib/types/urlencoded.js
generated
vendored
Normal file
190
g4f/Provider/npm/node_modules/@fastify/busboy/lib/types/urlencoded.js
generated
vendored
Normal file
|
@ -0,0 +1,190 @@
|
|||
'use strict'
|
||||
|
||||
const Decoder = require('../utils/Decoder')
|
||||
const decodeText = require('../utils/decodeText')
|
||||
const getLimit = require('../utils/getLimit')
|
||||
|
||||
const RE_CHARSET = /^charset$/i
|
||||
|
||||
UrlEncoded.detect = /^application\/x-www-form-urlencoded/i
|
||||
function UrlEncoded (boy, cfg) {
|
||||
const limits = cfg.limits
|
||||
const parsedConType = cfg.parsedConType
|
||||
this.boy = boy
|
||||
|
||||
this.fieldSizeLimit = getLimit(limits, 'fieldSize', 1 * 1024 * 1024)
|
||||
this.fieldNameSizeLimit = getLimit(limits, 'fieldNameSize', 100)
|
||||
this.fieldsLimit = getLimit(limits, 'fields', Infinity)
|
||||
|
||||
let charset
|
||||
for (var i = 0, len = parsedConType.length; i < len; ++i) { // eslint-disable-line no-var
|
||||
if (Array.isArray(parsedConType[i]) &&
|
||||
RE_CHARSET.test(parsedConType[i][0])) {
|
||||
charset = parsedConType[i][1].toLowerCase()
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (charset === undefined) { charset = cfg.defCharset || 'utf8' }
|
||||
|
||||
this.decoder = new Decoder()
|
||||
this.charset = charset
|
||||
this._fields = 0
|
||||
this._state = 'key'
|
||||
this._checkingBytes = true
|
||||
this._bytesKey = 0
|
||||
this._bytesVal = 0
|
||||
this._key = ''
|
||||
this._val = ''
|
||||
this._keyTrunc = false
|
||||
this._valTrunc = false
|
||||
this._hitLimit = false
|
||||
}
|
||||
|
||||
UrlEncoded.prototype.write = function (data, cb) {
|
||||
if (this._fields === this.fieldsLimit) {
|
||||
if (!this.boy.hitFieldsLimit) {
|
||||
this.boy.hitFieldsLimit = true
|
||||
this.boy.emit('fieldsLimit')
|
||||
}
|
||||
return cb()
|
||||
}
|
||||
|
||||
let idxeq; let idxamp; let i; let p = 0; const len = data.length
|
||||
|
||||
while (p < len) {
|
||||
if (this._state === 'key') {
|
||||
idxeq = idxamp = undefined
|
||||
for (i = p; i < len; ++i) {
|
||||
if (!this._checkingBytes) { ++p }
|
||||
if (data[i] === 0x3D/* = */) {
|
||||
idxeq = i
|
||||
break
|
||||
} else if (data[i] === 0x26/* & */) {
|
||||
idxamp = i
|
||||
break
|
||||
}
|
||||
if (this._checkingBytes && this._bytesKey === this.fieldNameSizeLimit) {
|
||||
this._hitLimit = true
|
||||
break
|
||||
} else if (this._checkingBytes) { ++this._bytesKey }
|
||||
}
|
||||
|
||||
if (idxeq !== undefined) {
|
||||
// key with assignment
|
||||
if (idxeq > p) { this._key += this.decoder.write(data.toString('binary', p, idxeq)) }
|
||||
this._state = 'val'
|
||||
|
||||
this._hitLimit = false
|
||||
this._checkingBytes = true
|
||||
this._val = ''
|
||||
this._bytesVal = 0
|
||||
this._valTrunc = false
|
||||
this.decoder.reset()
|
||||
|
||||
p = idxeq + 1
|
||||
} else if (idxamp !== undefined) {
|
||||
// key with no assignment
|
||||
++this._fields
|
||||
let key; const keyTrunc = this._keyTrunc
|
||||
if (idxamp > p) { key = (this._key += this.decoder.write(data.toString('binary', p, idxamp))) } else { key = this._key }
|
||||
|
||||
this._hitLimit = false
|
||||
this._checkingBytes = true
|
||||
this._key = ''
|
||||
this._bytesKey = 0
|
||||
this._keyTrunc = false
|
||||
this.decoder.reset()
|
||||
|
||||
if (key.length) {
|
||||
this.boy.emit('field', decodeText(key, 'binary', this.charset),
|
||||
'',
|
||||
keyTrunc,
|
||||
false)
|
||||
}
|
||||
|
||||
p = idxamp + 1
|
||||
if (this._fields === this.fieldsLimit) { return cb() }
|
||||
} else if (this._hitLimit) {
|
||||
// we may not have hit the actual limit if there are encoded bytes...
|
||||
if (i > p) { this._key += this.decoder.write(data.toString('binary', p, i)) }
|
||||
p = i
|
||||
if ((this._bytesKey = this._key.length) === this.fieldNameSizeLimit) {
|
||||
// yep, we actually did hit the limit
|
||||
this._checkingBytes = false
|
||||
this._keyTrunc = true
|
||||
}
|
||||
} else {
|
||||
if (p < len) { this._key += this.decoder.write(data.toString('binary', p)) }
|
||||
p = len
|
||||
}
|
||||
} else {
|
||||
idxamp = undefined
|
||||
for (i = p; i < len; ++i) {
|
||||
if (!this._checkingBytes) { ++p }
|
||||
if (data[i] === 0x26/* & */) {
|
||||
idxamp = i
|
||||
break
|
||||
}
|
||||
if (this._checkingBytes && this._bytesVal === this.fieldSizeLimit) {
|
||||
this._hitLimit = true
|
||||
break
|
||||
} else if (this._checkingBytes) { ++this._bytesVal }
|
||||
}
|
||||
|
||||
if (idxamp !== undefined) {
|
||||
++this._fields
|
||||
if (idxamp > p) { this._val += this.decoder.write(data.toString('binary', p, idxamp)) }
|
||||
this.boy.emit('field', decodeText(this._key, 'binary', this.charset),
|
||||
decodeText(this._val, 'binary', this.charset),
|
||||
this._keyTrunc,
|
||||
this._valTrunc)
|
||||
this._state = 'key'
|
||||
|
||||
this._hitLimit = false
|
||||
this._checkingBytes = true
|
||||
this._key = ''
|
||||
this._bytesKey = 0
|
||||
this._keyTrunc = false
|
||||
this.decoder.reset()
|
||||
|
||||
p = idxamp + 1
|
||||
if (this._fields === this.fieldsLimit) { return cb() }
|
||||
} else if (this._hitLimit) {
|
||||
// we may not have hit the actual limit if there are encoded bytes...
|
||||
if (i > p) { this._val += this.decoder.write(data.toString('binary', p, i)) }
|
||||
p = i
|
||||
if ((this._val === '' && this.fieldSizeLimit === 0) ||
|
||||
(this._bytesVal = this._val.length) === this.fieldSizeLimit) {
|
||||
// yep, we actually did hit the limit
|
||||
this._checkingBytes = false
|
||||
this._valTrunc = true
|
||||
}
|
||||
} else {
|
||||
if (p < len) { this._val += this.decoder.write(data.toString('binary', p)) }
|
||||
p = len
|
||||
}
|
||||
}
|
||||
}
|
||||
cb()
|
||||
}
|
||||
|
||||
UrlEncoded.prototype.end = function () {
|
||||
if (this.boy._done) { return }
|
||||
|
||||
if (this._state === 'key' && this._key.length > 0) {
|
||||
this.boy.emit('field', decodeText(this._key, 'binary', this.charset),
|
||||
'',
|
||||
this._keyTrunc,
|
||||
false)
|
||||
} else if (this._state === 'val') {
|
||||
this.boy.emit('field', decodeText(this._key, 'binary', this.charset),
|
||||
decodeText(this._val, 'binary', this.charset),
|
||||
this._keyTrunc,
|
||||
this._valTrunc)
|
||||
}
|
||||
this.boy._done = true
|
||||
this.boy.emit('finish')
|
||||
}
|
||||
|
||||
module.exports = UrlEncoded
|
54
g4f/Provider/npm/node_modules/@fastify/busboy/lib/utils/Decoder.js
generated
vendored
Normal file
54
g4f/Provider/npm/node_modules/@fastify/busboy/lib/utils/Decoder.js
generated
vendored
Normal file
|
@ -0,0 +1,54 @@
|
|||
'use strict'
|
||||
|
||||
const RE_PLUS = /\+/g
|
||||
|
||||
const HEX = [
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0,
|
||||
0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
|
||||
]
|
||||
|
||||
function Decoder () {
|
||||
this.buffer = undefined
|
||||
}
|
||||
Decoder.prototype.write = function (str) {
|
||||
// Replace '+' with ' ' before decoding
|
||||
str = str.replace(RE_PLUS, ' ')
|
||||
let res = ''
|
||||
let i = 0; let p = 0; const len = str.length
|
||||
for (; i < len; ++i) {
|
||||
if (this.buffer !== undefined) {
|
||||
if (!HEX[str.charCodeAt(i)]) {
|
||||
res += '%' + this.buffer
|
||||
this.buffer = undefined
|
||||
--i // retry character
|
||||
} else {
|
||||
this.buffer += str[i]
|
||||
++p
|
||||
if (this.buffer.length === 2) {
|
||||
res += String.fromCharCode(parseInt(this.buffer, 16))
|
||||
this.buffer = undefined
|
||||
}
|
||||
}
|
||||
} else if (str[i] === '%') {
|
||||
if (i > p) {
|
||||
res += str.substring(p, i)
|
||||
p = i
|
||||
}
|
||||
this.buffer = ''
|
||||
++p
|
||||
}
|
||||
}
|
||||
if (p < len && this.buffer === undefined) { res += str.substring(p) }
|
||||
return res
|
||||
}
|
||||
Decoder.prototype.reset = function () {
|
||||
this.buffer = undefined
|
||||
}
|
||||
|
||||
module.exports = Decoder
|
14
g4f/Provider/npm/node_modules/@fastify/busboy/lib/utils/basename.js
generated
vendored
Normal file
14
g4f/Provider/npm/node_modules/@fastify/busboy/lib/utils/basename.js
generated
vendored
Normal file
|
@ -0,0 +1,14 @@
|
|||
'use strict'
|
||||
|
||||
module.exports = function basename (path) {
|
||||
if (typeof path !== 'string') { return '' }
|
||||
for (var i = path.length - 1; i >= 0; --i) { // eslint-disable-line no-var
|
||||
switch (path.charCodeAt(i)) {
|
||||
case 0x2F: // '/'
|
||||
case 0x5C: // '\'
|
||||
path = path.slice(i + 1)
|
||||
return (path === '..' || path === '.' ? '' : path)
|
||||
}
|
||||
}
|
||||
return (path === '..' || path === '.' ? '' : path)
|
||||
}
|
26
g4f/Provider/npm/node_modules/@fastify/busboy/lib/utils/decodeText.js
generated
vendored
Normal file
26
g4f/Provider/npm/node_modules/@fastify/busboy/lib/utils/decodeText.js
generated
vendored
Normal file
|
@ -0,0 +1,26 @@
|
|||
'use strict'
|
||||
|
||||
// Node has always utf-8
|
||||
const utf8Decoder = new TextDecoder('utf-8')
|
||||
const textDecoders = new Map([
|
||||
['utf-8', utf8Decoder],
|
||||
['utf8', utf8Decoder]
|
||||
])
|
||||
|
||||
function decodeText (text, textEncoding, destEncoding) {
|
||||
if (text) {
|
||||
if (textDecoders.has(destEncoding)) {
|
||||
try {
|
||||
return textDecoders.get(destEncoding).decode(Buffer.from(text, textEncoding))
|
||||
} catch (e) { }
|
||||
} else {
|
||||
try {
|
||||
textDecoders.set(destEncoding, new TextDecoder(destEncoding))
|
||||
return textDecoders.get(destEncoding).decode(Buffer.from(text, textEncoding))
|
||||
} catch (e) { }
|
||||
}
|
||||
}
|
||||
return text
|
||||
}
|
||||
|
||||
module.exports = decodeText
|
16
g4f/Provider/npm/node_modules/@fastify/busboy/lib/utils/getLimit.js
generated
vendored
Normal file
16
g4f/Provider/npm/node_modules/@fastify/busboy/lib/utils/getLimit.js
generated
vendored
Normal file
|
@ -0,0 +1,16 @@
|
|||
'use strict'
|
||||
|
||||
module.exports = function getLimit (limits, name, defaultLimit) {
|
||||
if (
|
||||
!limits ||
|
||||
limits[name] === undefined ||
|
||||
limits[name] === null
|
||||
) { return defaultLimit }
|
||||
|
||||
if (
|
||||
typeof limits[name] !== 'number' ||
|
||||
isNaN(limits[name])
|
||||
) { throw new TypeError('Limit ' + name + ' is not a valid number') }
|
||||
|
||||
return limits[name]
|
||||
}
|
87
g4f/Provider/npm/node_modules/@fastify/busboy/lib/utils/parseParams.js
generated
vendored
Normal file
87
g4f/Provider/npm/node_modules/@fastify/busboy/lib/utils/parseParams.js
generated
vendored
Normal file
|
@ -0,0 +1,87 @@
|
|||
'use strict'
|
||||
|
||||
const decodeText = require('./decodeText')
|
||||
|
||||
const RE_ENCODED = /%([a-fA-F0-9]{2})/g
|
||||
|
||||
function encodedReplacer (match, byte) {
|
||||
return String.fromCharCode(parseInt(byte, 16))
|
||||
}
|
||||
|
||||
function parseParams (str) {
|
||||
const res = []
|
||||
let state = 'key'
|
||||
let charset = ''
|
||||
let inquote = false
|
||||
let escaping = false
|
||||
let p = 0
|
||||
let tmp = ''
|
||||
|
||||
for (var i = 0, len = str.length; i < len; ++i) { // eslint-disable-line no-var
|
||||
const char = str[i]
|
||||
if (char === '\\' && inquote) {
|
||||
if (escaping) { escaping = false } else {
|
||||
escaping = true
|
||||
continue
|
||||
}
|
||||
} else if (char === '"') {
|
||||
if (!escaping) {
|
||||
if (inquote) {
|
||||
inquote = false
|
||||
state = 'key'
|
||||
} else { inquote = true }
|
||||
continue
|
||||
} else { escaping = false }
|
||||
} else {
|
||||
if (escaping && inquote) { tmp += '\\' }
|
||||
escaping = false
|
||||
if ((state === 'charset' || state === 'lang') && char === "'") {
|
||||
if (state === 'charset') {
|
||||
state = 'lang'
|
||||
charset = tmp.substring(1)
|
||||
} else { state = 'value' }
|
||||
tmp = ''
|
||||
continue
|
||||
} else if (state === 'key' &&
|
||||
(char === '*' || char === '=') &&
|
||||
res.length) {
|
||||
if (char === '*') { state = 'charset' } else { state = 'value' }
|
||||
res[p] = [tmp, undefined]
|
||||
tmp = ''
|
||||
continue
|
||||
} else if (!inquote && char === ';') {
|
||||
state = 'key'
|
||||
if (charset) {
|
||||
if (tmp.length) {
|
||||
tmp = decodeText(tmp.replace(RE_ENCODED, encodedReplacer),
|
||||
'binary',
|
||||
charset)
|
||||
}
|
||||
charset = ''
|
||||
} else if (tmp.length) {
|
||||
tmp = decodeText(tmp, 'binary', 'utf8')
|
||||
}
|
||||
if (res[p] === undefined) { res[p] = tmp } else { res[p][1] = tmp }
|
||||
tmp = ''
|
||||
++p
|
||||
continue
|
||||
} else if (!inquote && (char === ' ' || char === '\t')) { continue }
|
||||
}
|
||||
tmp += char
|
||||
}
|
||||
if (charset && tmp.length) {
|
||||
tmp = decodeText(tmp.replace(RE_ENCODED, encodedReplacer),
|
||||
'binary',
|
||||
charset)
|
||||
} else if (tmp) {
|
||||
tmp = decodeText(tmp, 'binary', 'utf8')
|
||||
}
|
||||
|
||||
if (res[p] === undefined) {
|
||||
if (tmp) { res[p] = tmp }
|
||||
} else { res[p][1] = tmp }
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
module.exports = parseParams
|
|
@ -0,0 +1,89 @@
|
|||
{
|
||||
"name": "@fastify/busboy",
|
||||
"version": "2.0.0",
|
||||
"private": false,
|
||||
"author": "Brian White <mscdex@mscdex.net>",
|
||||
"contributors": [
|
||||
{
|
||||
"name": "Igor Savin",
|
||||
"email": "kibertoad@gmail.com",
|
||||
"url": "https://github.com/kibertoad"
|
||||
},
|
||||
{
|
||||
"name": "Aras Abbasi",
|
||||
"email": "aras.abbasi@gmail.com",
|
||||
"url": "https://github.com/uzlopak"
|
||||
}
|
||||
],
|
||||
"description": "A streaming parser for HTML form data for node.js",
|
||||
"main": "lib/main",
|
||||
"types": "lib/main.d.ts",
|
||||
"scripts": {
|
||||
"bench:busboy": "cd benchmarks && npm install && npm run benchmark-fastify",
|
||||
"bench:dicer": "node bench/dicer/dicer-bench-multipart-parser.js",
|
||||
"coveralls": "nyc report --reporter=lcov",
|
||||
"lint": "npm run lint:standard",
|
||||
"lint:everything": "npm run lint && npm run test:types",
|
||||
"lint:fix": "standard --fix",
|
||||
"lint:standard": "standard --verbose | snazzy",
|
||||
"test:mocha": "mocha test",
|
||||
"test:types": "tsd",
|
||||
"test:coverage": "nyc npm run test",
|
||||
"test": "npm run test:mocha"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.1.0",
|
||||
"busboy": "^1.0.0",
|
||||
"chai": "^4.3.6",
|
||||
"eslint": "^8.23.0",
|
||||
"eslint-config-standard": "^17.0.0",
|
||||
"eslint-plugin-n": "^16.0.0",
|
||||
"mocha": "^10.0.0",
|
||||
"nyc": "^15.1.0",
|
||||
"photofinish": "^1.8.0",
|
||||
"snazzy": "^9.0.0",
|
||||
"standard": "^17.0.0",
|
||||
"tsd": "^0.29.0",
|
||||
"typescript": "^5.0.2"
|
||||
},
|
||||
"keywords": [
|
||||
"uploads",
|
||||
"forms",
|
||||
"multipart",
|
||||
"form-data"
|
||||
],
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/fastify/busboy.git"
|
||||
},
|
||||
"tsd": {
|
||||
"directory": "test/types",
|
||||
"compilerOptions": {
|
||||
"esModuleInterop": false,
|
||||
"module": "commonjs",
|
||||
"target": "ES2017"
|
||||
}
|
||||
},
|
||||
"standard": {
|
||||
"globals": [
|
||||
"describe",
|
||||
"it"
|
||||
],
|
||||
"ignore": [
|
||||
"bench"
|
||||
]
|
||||
},
|
||||
"files": [
|
||||
"README.md",
|
||||
"LICENSE",
|
||||
"lib/*",
|
||||
"deps/encoding/*",
|
||||
"deps/dicer/lib",
|
||||
"deps/streamsearch/",
|
||||
"deps/dicer/LICENSE"
|
||||
]
|
||||
}
|
|
@ -1421,7 +1421,10 @@
|
|||
*
|
||||
* var base64String = CryptoJS.enc.Base64url.stringify(wordArray);
|
||||
*/
|
||||
stringify: function (wordArray, urlSafe=true) {
|
||||
stringify: function (wordArray, urlSafe) {
|
||||
if (urlSafe === undefined) {
|
||||
urlSafe = true
|
||||
}
|
||||
// Shortcuts
|
||||
var words = wordArray.words;
|
||||
var sigBytes = wordArray.sigBytes;
|
||||
|
@ -1470,7 +1473,11 @@
|
|||
*
|
||||
* var wordArray = CryptoJS.enc.Base64url.parse(base64String);
|
||||
*/
|
||||
parse: function (base64Str, urlSafe=true) {
|
||||
parse: function (base64Str, urlSafe) {
|
||||
if (urlSafe === undefined) {
|
||||
urlSafe = true
|
||||
}
|
||||
|
||||
// Shortcuts
|
||||
var base64StrLength = base64Str.length;
|
||||
var map = urlSafe ? this._safe_map : this._map;
|
||||
|
@ -1517,6 +1524,7 @@
|
|||
}
|
||||
}());
|
||||
|
||||
|
||||
(function (Math) {
|
||||
// Shortcuts
|
||||
var C = CryptoJS;
|
||||
|
@ -1579,7 +1587,7 @@
|
|||
var M_offset_14 = M[offset + 14];
|
||||
var M_offset_15 = M[offset + 15];
|
||||
|
||||
// Working varialbes
|
||||
// Working variables
|
||||
var a = H[0];
|
||||
var b = H[1];
|
||||
var c = H[2];
|
||||
|
@ -3206,7 +3214,7 @@
|
|||
var Base = C_lib.Base;
|
||||
var WordArray = C_lib.WordArray;
|
||||
var C_algo = C.algo;
|
||||
var SHA1 = C_algo.SHA1;
|
||||
var SHA256 = C_algo.SHA256;
|
||||
var HMAC = C_algo.HMAC;
|
||||
|
||||
/**
|
||||
|
@ -3217,13 +3225,13 @@
|
|||
* Configuration options.
|
||||
*
|
||||
* @property {number} keySize The key size in words to generate. Default: 4 (128 bits)
|
||||
* @property {Hasher} hasher The hasher to use. Default: SHA1
|
||||
* @property {number} iterations The number of iterations to perform. Default: 1
|
||||
* @property {Hasher} hasher The hasher to use. Default: SHA256
|
||||
* @property {number} iterations The number of iterations to perform. Default: 250000
|
||||
*/
|
||||
cfg: Base.extend({
|
||||
keySize: 128/32,
|
||||
hasher: SHA1,
|
||||
iterations: 1
|
||||
hasher: SHA256,
|
||||
iterations: 250000
|
||||
}),
|
||||
|
||||
/**
|
||||
|
@ -4209,14 +4217,19 @@
|
|||
* var derivedParams = CryptoJS.kdf.OpenSSL.execute('Password', 256/32, 128/32);
|
||||
* var derivedParams = CryptoJS.kdf.OpenSSL.execute('Password', 256/32, 128/32, 'saltsalt');
|
||||
*/
|
||||
execute: function (password, keySize, ivSize, salt) {
|
||||
execute: function (password, keySize, ivSize, salt, hasher) {
|
||||
// Generate random salt
|
||||
if (!salt) {
|
||||
salt = WordArray.random(64/8);
|
||||
}
|
||||
|
||||
// Derive key and IV
|
||||
var key = EvpKDF.create({ keySize: keySize + ivSize }).compute(password, salt);
|
||||
if (!hasher) {
|
||||
var key = EvpKDF.create({ keySize: keySize + ivSize }).compute(password, salt);
|
||||
} else {
|
||||
var key = EvpKDF.create({ keySize: keySize + ivSize, hasher: hasher }).compute(password, salt);
|
||||
}
|
||||
|
||||
|
||||
// Separate key and IV
|
||||
var iv = WordArray.create(key.words.slice(keySize), ivSize * 4);
|
||||
|
@ -4263,7 +4276,7 @@
|
|||
cfg = this.cfg.extend(cfg);
|
||||
|
||||
// Derive key and other params
|
||||
var derivedParams = cfg.kdf.execute(password, cipher.keySize, cipher.ivSize);
|
||||
var derivedParams = cfg.kdf.execute(password, cipher.keySize, cipher.ivSize, cfg.salt, cfg.hasher);
|
||||
|
||||
// Add IV to config
|
||||
cfg.iv = derivedParams.iv;
|
||||
|
@ -4302,7 +4315,7 @@
|
|||
ciphertext = this._parse(ciphertext, cfg.format);
|
||||
|
||||
// Derive key and other params
|
||||
var derivedParams = cfg.kdf.execute(password, cipher.keySize, cipher.ivSize, ciphertext.salt);
|
||||
var derivedParams = cfg.kdf.execute(password, cipher.keySize, cipher.ivSize, ciphertext.salt, cfg.hasher);
|
||||
|
||||
// Add IV to config
|
||||
cfg.iv = derivedParams.iv;
|
||||
|
@ -6186,6 +6199,459 @@
|
|||
}());
|
||||
|
||||
|
||||
(function () {
|
||||
// Shortcuts
|
||||
var C = CryptoJS;
|
||||
var C_lib = C.lib;
|
||||
var BlockCipher = C_lib.BlockCipher;
|
||||
var C_algo = C.algo;
|
||||
|
||||
const N = 16;
|
||||
|
||||
//Origin pbox and sbox, derived from PI
|
||||
const ORIG_P = [
|
||||
0x243F6A88, 0x85A308D3, 0x13198A2E, 0x03707344,
|
||||
0xA4093822, 0x299F31D0, 0x082EFA98, 0xEC4E6C89,
|
||||
0x452821E6, 0x38D01377, 0xBE5466CF, 0x34E90C6C,
|
||||
0xC0AC29B7, 0xC97C50DD, 0x3F84D5B5, 0xB5470917,
|
||||
0x9216D5D9, 0x8979FB1B
|
||||
];
|
||||
|
||||
const ORIG_S = [
|
||||
[ 0xD1310BA6, 0x98DFB5AC, 0x2FFD72DB, 0xD01ADFB7,
|
||||
0xB8E1AFED, 0x6A267E96, 0xBA7C9045, 0xF12C7F99,
|
||||
0x24A19947, 0xB3916CF7, 0x0801F2E2, 0x858EFC16,
|
||||
0x636920D8, 0x71574E69, 0xA458FEA3, 0xF4933D7E,
|
||||
0x0D95748F, 0x728EB658, 0x718BCD58, 0x82154AEE,
|
||||
0x7B54A41D, 0xC25A59B5, 0x9C30D539, 0x2AF26013,
|
||||
0xC5D1B023, 0x286085F0, 0xCA417918, 0xB8DB38EF,
|
||||
0x8E79DCB0, 0x603A180E, 0x6C9E0E8B, 0xB01E8A3E,
|
||||
0xD71577C1, 0xBD314B27, 0x78AF2FDA, 0x55605C60,
|
||||
0xE65525F3, 0xAA55AB94, 0x57489862, 0x63E81440,
|
||||
0x55CA396A, 0x2AAB10B6, 0xB4CC5C34, 0x1141E8CE,
|
||||
0xA15486AF, 0x7C72E993, 0xB3EE1411, 0x636FBC2A,
|
||||
0x2BA9C55D, 0x741831F6, 0xCE5C3E16, 0x9B87931E,
|
||||
0xAFD6BA33, 0x6C24CF5C, 0x7A325381, 0x28958677,
|
||||
0x3B8F4898, 0x6B4BB9AF, 0xC4BFE81B, 0x66282193,
|
||||
0x61D809CC, 0xFB21A991, 0x487CAC60, 0x5DEC8032,
|
||||
0xEF845D5D, 0xE98575B1, 0xDC262302, 0xEB651B88,
|
||||
0x23893E81, 0xD396ACC5, 0x0F6D6FF3, 0x83F44239,
|
||||
0x2E0B4482, 0xA4842004, 0x69C8F04A, 0x9E1F9B5E,
|
||||
0x21C66842, 0xF6E96C9A, 0x670C9C61, 0xABD388F0,
|
||||
0x6A51A0D2, 0xD8542F68, 0x960FA728, 0xAB5133A3,
|
||||
0x6EEF0B6C, 0x137A3BE4, 0xBA3BF050, 0x7EFB2A98,
|
||||
0xA1F1651D, 0x39AF0176, 0x66CA593E, 0x82430E88,
|
||||
0x8CEE8619, 0x456F9FB4, 0x7D84A5C3, 0x3B8B5EBE,
|
||||
0xE06F75D8, 0x85C12073, 0x401A449F, 0x56C16AA6,
|
||||
0x4ED3AA62, 0x363F7706, 0x1BFEDF72, 0x429B023D,
|
||||
0x37D0D724, 0xD00A1248, 0xDB0FEAD3, 0x49F1C09B,
|
||||
0x075372C9, 0x80991B7B, 0x25D479D8, 0xF6E8DEF7,
|
||||
0xE3FE501A, 0xB6794C3B, 0x976CE0BD, 0x04C006BA,
|
||||
0xC1A94FB6, 0x409F60C4, 0x5E5C9EC2, 0x196A2463,
|
||||
0x68FB6FAF, 0x3E6C53B5, 0x1339B2EB, 0x3B52EC6F,
|
||||
0x6DFC511F, 0x9B30952C, 0xCC814544, 0xAF5EBD09,
|
||||
0xBEE3D004, 0xDE334AFD, 0x660F2807, 0x192E4BB3,
|
||||
0xC0CBA857, 0x45C8740F, 0xD20B5F39, 0xB9D3FBDB,
|
||||
0x5579C0BD, 0x1A60320A, 0xD6A100C6, 0x402C7279,
|
||||
0x679F25FE, 0xFB1FA3CC, 0x8EA5E9F8, 0xDB3222F8,
|
||||
0x3C7516DF, 0xFD616B15, 0x2F501EC8, 0xAD0552AB,
|
||||
0x323DB5FA, 0xFD238760, 0x53317B48, 0x3E00DF82,
|
||||
0x9E5C57BB, 0xCA6F8CA0, 0x1A87562E, 0xDF1769DB,
|
||||
0xD542A8F6, 0x287EFFC3, 0xAC6732C6, 0x8C4F5573,
|
||||
0x695B27B0, 0xBBCA58C8, 0xE1FFA35D, 0xB8F011A0,
|
||||
0x10FA3D98, 0xFD2183B8, 0x4AFCB56C, 0x2DD1D35B,
|
||||
0x9A53E479, 0xB6F84565, 0xD28E49BC, 0x4BFB9790,
|
||||
0xE1DDF2DA, 0xA4CB7E33, 0x62FB1341, 0xCEE4C6E8,
|
||||
0xEF20CADA, 0x36774C01, 0xD07E9EFE, 0x2BF11FB4,
|
||||
0x95DBDA4D, 0xAE909198, 0xEAAD8E71, 0x6B93D5A0,
|
||||
0xD08ED1D0, 0xAFC725E0, 0x8E3C5B2F, 0x8E7594B7,
|
||||
0x8FF6E2FB, 0xF2122B64, 0x8888B812, 0x900DF01C,
|
||||
0x4FAD5EA0, 0x688FC31C, 0xD1CFF191, 0xB3A8C1AD,
|
||||
0x2F2F2218, 0xBE0E1777, 0xEA752DFE, 0x8B021FA1,
|
||||
0xE5A0CC0F, 0xB56F74E8, 0x18ACF3D6, 0xCE89E299,
|
||||
0xB4A84FE0, 0xFD13E0B7, 0x7CC43B81, 0xD2ADA8D9,
|
||||
0x165FA266, 0x80957705, 0x93CC7314, 0x211A1477,
|
||||
0xE6AD2065, 0x77B5FA86, 0xC75442F5, 0xFB9D35CF,
|
||||
0xEBCDAF0C, 0x7B3E89A0, 0xD6411BD3, 0xAE1E7E49,
|
||||
0x00250E2D, 0x2071B35E, 0x226800BB, 0x57B8E0AF,
|
||||
0x2464369B, 0xF009B91E, 0x5563911D, 0x59DFA6AA,
|
||||
0x78C14389, 0xD95A537F, 0x207D5BA2, 0x02E5B9C5,
|
||||
0x83260376, 0x6295CFA9, 0x11C81968, 0x4E734A41,
|
||||
0xB3472DCA, 0x7B14A94A, 0x1B510052, 0x9A532915,
|
||||
0xD60F573F, 0xBC9BC6E4, 0x2B60A476, 0x81E67400,
|
||||
0x08BA6FB5, 0x571BE91F, 0xF296EC6B, 0x2A0DD915,
|
||||
0xB6636521, 0xE7B9F9B6, 0xFF34052E, 0xC5855664,
|
||||
0x53B02D5D, 0xA99F8FA1, 0x08BA4799, 0x6E85076A ],
|
||||
[ 0x4B7A70E9, 0xB5B32944, 0xDB75092E, 0xC4192623,
|
||||
0xAD6EA6B0, 0x49A7DF7D, 0x9CEE60B8, 0x8FEDB266,
|
||||
0xECAA8C71, 0x699A17FF, 0x5664526C, 0xC2B19EE1,
|
||||
0x193602A5, 0x75094C29, 0xA0591340, 0xE4183A3E,
|
||||
0x3F54989A, 0x5B429D65, 0x6B8FE4D6, 0x99F73FD6,
|
||||
0xA1D29C07, 0xEFE830F5, 0x4D2D38E6, 0xF0255DC1,
|
||||
0x4CDD2086, 0x8470EB26, 0x6382E9C6, 0x021ECC5E,
|
||||
0x09686B3F, 0x3EBAEFC9, 0x3C971814, 0x6B6A70A1,
|
||||
0x687F3584, 0x52A0E286, 0xB79C5305, 0xAA500737,
|
||||
0x3E07841C, 0x7FDEAE5C, 0x8E7D44EC, 0x5716F2B8,
|
||||
0xB03ADA37, 0xF0500C0D, 0xF01C1F04, 0x0200B3FF,
|
||||
0xAE0CF51A, 0x3CB574B2, 0x25837A58, 0xDC0921BD,
|
||||
0xD19113F9, 0x7CA92FF6, 0x94324773, 0x22F54701,
|
||||
0x3AE5E581, 0x37C2DADC, 0xC8B57634, 0x9AF3DDA7,
|
||||
0xA9446146, 0x0FD0030E, 0xECC8C73E, 0xA4751E41,
|
||||
0xE238CD99, 0x3BEA0E2F, 0x3280BBA1, 0x183EB331,
|
||||
0x4E548B38, 0x4F6DB908, 0x6F420D03, 0xF60A04BF,
|
||||
0x2CB81290, 0x24977C79, 0x5679B072, 0xBCAF89AF,
|
||||
0xDE9A771F, 0xD9930810, 0xB38BAE12, 0xDCCF3F2E,
|
||||
0x5512721F, 0x2E6B7124, 0x501ADDE6, 0x9F84CD87,
|
||||
0x7A584718, 0x7408DA17, 0xBC9F9ABC, 0xE94B7D8C,
|
||||
0xEC7AEC3A, 0xDB851DFA, 0x63094366, 0xC464C3D2,
|
||||
0xEF1C1847, 0x3215D908, 0xDD433B37, 0x24C2BA16,
|
||||
0x12A14D43, 0x2A65C451, 0x50940002, 0x133AE4DD,
|
||||
0x71DFF89E, 0x10314E55, 0x81AC77D6, 0x5F11199B,
|
||||
0x043556F1, 0xD7A3C76B, 0x3C11183B, 0x5924A509,
|
||||
0xF28FE6ED, 0x97F1FBFA, 0x9EBABF2C, 0x1E153C6E,
|
||||
0x86E34570, 0xEAE96FB1, 0x860E5E0A, 0x5A3E2AB3,
|
||||
0x771FE71C, 0x4E3D06FA, 0x2965DCB9, 0x99E71D0F,
|
||||
0x803E89D6, 0x5266C825, 0x2E4CC978, 0x9C10B36A,
|
||||
0xC6150EBA, 0x94E2EA78, 0xA5FC3C53, 0x1E0A2DF4,
|
||||
0xF2F74EA7, 0x361D2B3D, 0x1939260F, 0x19C27960,
|
||||
0x5223A708, 0xF71312B6, 0xEBADFE6E, 0xEAC31F66,
|
||||
0xE3BC4595, 0xA67BC883, 0xB17F37D1, 0x018CFF28,
|
||||
0xC332DDEF, 0xBE6C5AA5, 0x65582185, 0x68AB9802,
|
||||
0xEECEA50F, 0xDB2F953B, 0x2AEF7DAD, 0x5B6E2F84,
|
||||
0x1521B628, 0x29076170, 0xECDD4775, 0x619F1510,
|
||||
0x13CCA830, 0xEB61BD96, 0x0334FE1E, 0xAA0363CF,
|
||||
0xB5735C90, 0x4C70A239, 0xD59E9E0B, 0xCBAADE14,
|
||||
0xEECC86BC, 0x60622CA7, 0x9CAB5CAB, 0xB2F3846E,
|
||||
0x648B1EAF, 0x19BDF0CA, 0xA02369B9, 0x655ABB50,
|
||||
0x40685A32, 0x3C2AB4B3, 0x319EE9D5, 0xC021B8F7,
|
||||
0x9B540B19, 0x875FA099, 0x95F7997E, 0x623D7DA8,
|
||||
0xF837889A, 0x97E32D77, 0x11ED935F, 0x16681281,
|
||||
0x0E358829, 0xC7E61FD6, 0x96DEDFA1, 0x7858BA99,
|
||||
0x57F584A5, 0x1B227263, 0x9B83C3FF, 0x1AC24696,
|
||||
0xCDB30AEB, 0x532E3054, 0x8FD948E4, 0x6DBC3128,
|
||||
0x58EBF2EF, 0x34C6FFEA, 0xFE28ED61, 0xEE7C3C73,
|
||||
0x5D4A14D9, 0xE864B7E3, 0x42105D14, 0x203E13E0,
|
||||
0x45EEE2B6, 0xA3AAABEA, 0xDB6C4F15, 0xFACB4FD0,
|
||||
0xC742F442, 0xEF6ABBB5, 0x654F3B1D, 0x41CD2105,
|
||||
0xD81E799E, 0x86854DC7, 0xE44B476A, 0x3D816250,
|
||||
0xCF62A1F2, 0x5B8D2646, 0xFC8883A0, 0xC1C7B6A3,
|
||||
0x7F1524C3, 0x69CB7492, 0x47848A0B, 0x5692B285,
|
||||
0x095BBF00, 0xAD19489D, 0x1462B174, 0x23820E00,
|
||||
0x58428D2A, 0x0C55F5EA, 0x1DADF43E, 0x233F7061,
|
||||
0x3372F092, 0x8D937E41, 0xD65FECF1, 0x6C223BDB,
|
||||
0x7CDE3759, 0xCBEE7460, 0x4085F2A7, 0xCE77326E,
|
||||
0xA6078084, 0x19F8509E, 0xE8EFD855, 0x61D99735,
|
||||
0xA969A7AA, 0xC50C06C2, 0x5A04ABFC, 0x800BCADC,
|
||||
0x9E447A2E, 0xC3453484, 0xFDD56705, 0x0E1E9EC9,
|
||||
0xDB73DBD3, 0x105588CD, 0x675FDA79, 0xE3674340,
|
||||
0xC5C43465, 0x713E38D8, 0x3D28F89E, 0xF16DFF20,
|
||||
0x153E21E7, 0x8FB03D4A, 0xE6E39F2B, 0xDB83ADF7 ],
|
||||
[ 0xE93D5A68, 0x948140F7, 0xF64C261C, 0x94692934,
|
||||
0x411520F7, 0x7602D4F7, 0xBCF46B2E, 0xD4A20068,
|
||||
0xD4082471, 0x3320F46A, 0x43B7D4B7, 0x500061AF,
|
||||
0x1E39F62E, 0x97244546, 0x14214F74, 0xBF8B8840,
|
||||
0x4D95FC1D, 0x96B591AF, 0x70F4DDD3, 0x66A02F45,
|
||||
0xBFBC09EC, 0x03BD9785, 0x7FAC6DD0, 0x31CB8504,
|
||||
0x96EB27B3, 0x55FD3941, 0xDA2547E6, 0xABCA0A9A,
|
||||
0x28507825, 0x530429F4, 0x0A2C86DA, 0xE9B66DFB,
|
||||
0x68DC1462, 0xD7486900, 0x680EC0A4, 0x27A18DEE,
|
||||
0x4F3FFEA2, 0xE887AD8C, 0xB58CE006, 0x7AF4D6B6,
|
||||
0xAACE1E7C, 0xD3375FEC, 0xCE78A399, 0x406B2A42,
|
||||
0x20FE9E35, 0xD9F385B9, 0xEE39D7AB, 0x3B124E8B,
|
||||
0x1DC9FAF7, 0x4B6D1856, 0x26A36631, 0xEAE397B2,
|
||||
0x3A6EFA74, 0xDD5B4332, 0x6841E7F7, 0xCA7820FB,
|
||||
0xFB0AF54E, 0xD8FEB397, 0x454056AC, 0xBA489527,
|
||||
0x55533A3A, 0x20838D87, 0xFE6BA9B7, 0xD096954B,
|
||||
0x55A867BC, 0xA1159A58, 0xCCA92963, 0x99E1DB33,
|
||||
0xA62A4A56, 0x3F3125F9, 0x5EF47E1C, 0x9029317C,
|
||||
0xFDF8E802, 0x04272F70, 0x80BB155C, 0x05282CE3,
|
||||
0x95C11548, 0xE4C66D22, 0x48C1133F, 0xC70F86DC,
|
||||
0x07F9C9EE, 0x41041F0F, 0x404779A4, 0x5D886E17,
|
||||
0x325F51EB, 0xD59BC0D1, 0xF2BCC18F, 0x41113564,
|
||||
0x257B7834, 0x602A9C60, 0xDFF8E8A3, 0x1F636C1B,
|
||||
0x0E12B4C2, 0x02E1329E, 0xAF664FD1, 0xCAD18115,
|
||||
0x6B2395E0, 0x333E92E1, 0x3B240B62, 0xEEBEB922,
|
||||
0x85B2A20E, 0xE6BA0D99, 0xDE720C8C, 0x2DA2F728,
|
||||
0xD0127845, 0x95B794FD, 0x647D0862, 0xE7CCF5F0,
|
||||
0x5449A36F, 0x877D48FA, 0xC39DFD27, 0xF33E8D1E,
|
||||
0x0A476341, 0x992EFF74, 0x3A6F6EAB, 0xF4F8FD37,
|
||||
0xA812DC60, 0xA1EBDDF8, 0x991BE14C, 0xDB6E6B0D,
|
||||
0xC67B5510, 0x6D672C37, 0x2765D43B, 0xDCD0E804,
|
||||
0xF1290DC7, 0xCC00FFA3, 0xB5390F92, 0x690FED0B,
|
||||
0x667B9FFB, 0xCEDB7D9C, 0xA091CF0B, 0xD9155EA3,
|
||||
0xBB132F88, 0x515BAD24, 0x7B9479BF, 0x763BD6EB,
|
||||
0x37392EB3, 0xCC115979, 0x8026E297, 0xF42E312D,
|
||||
0x6842ADA7, 0xC66A2B3B, 0x12754CCC, 0x782EF11C,
|
||||
0x6A124237, 0xB79251E7, 0x06A1BBE6, 0x4BFB6350,
|
||||
0x1A6B1018, 0x11CAEDFA, 0x3D25BDD8, 0xE2E1C3C9,
|
||||
0x44421659, 0x0A121386, 0xD90CEC6E, 0xD5ABEA2A,
|
||||
0x64AF674E, 0xDA86A85F, 0xBEBFE988, 0x64E4C3FE,
|
||||
0x9DBC8057, 0xF0F7C086, 0x60787BF8, 0x6003604D,
|
||||
0xD1FD8346, 0xF6381FB0, 0x7745AE04, 0xD736FCCC,
|
||||
0x83426B33, 0xF01EAB71, 0xB0804187, 0x3C005E5F,
|
||||
0x77A057BE, 0xBDE8AE24, 0x55464299, 0xBF582E61,
|
||||
0x4E58F48F, 0xF2DDFDA2, 0xF474EF38, 0x8789BDC2,
|
||||
0x5366F9C3, 0xC8B38E74, 0xB475F255, 0x46FCD9B9,
|
||||
0x7AEB2661, 0x8B1DDF84, 0x846A0E79, 0x915F95E2,
|
||||
0x466E598E, 0x20B45770, 0x8CD55591, 0xC902DE4C,
|
||||
0xB90BACE1, 0xBB8205D0, 0x11A86248, 0x7574A99E,
|
||||
0xB77F19B6, 0xE0A9DC09, 0x662D09A1, 0xC4324633,
|
||||
0xE85A1F02, 0x09F0BE8C, 0x4A99A025, 0x1D6EFE10,
|
||||
0x1AB93D1D, 0x0BA5A4DF, 0xA186F20F, 0x2868F169,
|
||||
0xDCB7DA83, 0x573906FE, 0xA1E2CE9B, 0x4FCD7F52,
|
||||
0x50115E01, 0xA70683FA, 0xA002B5C4, 0x0DE6D027,
|
||||
0x9AF88C27, 0x773F8641, 0xC3604C06, 0x61A806B5,
|
||||
0xF0177A28, 0xC0F586E0, 0x006058AA, 0x30DC7D62,
|
||||
0x11E69ED7, 0x2338EA63, 0x53C2DD94, 0xC2C21634,
|
||||
0xBBCBEE56, 0x90BCB6DE, 0xEBFC7DA1, 0xCE591D76,
|
||||
0x6F05E409, 0x4B7C0188, 0x39720A3D, 0x7C927C24,
|
||||
0x86E3725F, 0x724D9DB9, 0x1AC15BB4, 0xD39EB8FC,
|
||||
0xED545578, 0x08FCA5B5, 0xD83D7CD3, 0x4DAD0FC4,
|
||||
0x1E50EF5E, 0xB161E6F8, 0xA28514D9, 0x6C51133C,
|
||||
0x6FD5C7E7, 0x56E14EC4, 0x362ABFCE, 0xDDC6C837,
|
||||
0xD79A3234, 0x92638212, 0x670EFA8E, 0x406000E0 ],
|
||||
[ 0x3A39CE37, 0xD3FAF5CF, 0xABC27737, 0x5AC52D1B,
|
||||
0x5CB0679E, 0x4FA33742, 0xD3822740, 0x99BC9BBE,
|
||||
0xD5118E9D, 0xBF0F7315, 0xD62D1C7E, 0xC700C47B,
|
||||
0xB78C1B6B, 0x21A19045, 0xB26EB1BE, 0x6A366EB4,
|
||||
0x5748AB2F, 0xBC946E79, 0xC6A376D2, 0x6549C2C8,
|
||||
0x530FF8EE, 0x468DDE7D, 0xD5730A1D, 0x4CD04DC6,
|
||||
0x2939BBDB, 0xA9BA4650, 0xAC9526E8, 0xBE5EE304,
|
||||
0xA1FAD5F0, 0x6A2D519A, 0x63EF8CE2, 0x9A86EE22,
|
||||
0xC089C2B8, 0x43242EF6, 0xA51E03AA, 0x9CF2D0A4,
|
||||
0x83C061BA, 0x9BE96A4D, 0x8FE51550, 0xBA645BD6,
|
||||
0x2826A2F9, 0xA73A3AE1, 0x4BA99586, 0xEF5562E9,
|
||||
0xC72FEFD3, 0xF752F7DA, 0x3F046F69, 0x77FA0A59,
|
||||
0x80E4A915, 0x87B08601, 0x9B09E6AD, 0x3B3EE593,
|
||||
0xE990FD5A, 0x9E34D797, 0x2CF0B7D9, 0x022B8B51,
|
||||
0x96D5AC3A, 0x017DA67D, 0xD1CF3ED6, 0x7C7D2D28,
|
||||
0x1F9F25CF, 0xADF2B89B, 0x5AD6B472, 0x5A88F54C,
|
||||
0xE029AC71, 0xE019A5E6, 0x47B0ACFD, 0xED93FA9B,
|
||||
0xE8D3C48D, 0x283B57CC, 0xF8D56629, 0x79132E28,
|
||||
0x785F0191, 0xED756055, 0xF7960E44, 0xE3D35E8C,
|
||||
0x15056DD4, 0x88F46DBA, 0x03A16125, 0x0564F0BD,
|
||||
0xC3EB9E15, 0x3C9057A2, 0x97271AEC, 0xA93A072A,
|
||||
0x1B3F6D9B, 0x1E6321F5, 0xF59C66FB, 0x26DCF319,
|
||||
0x7533D928, 0xB155FDF5, 0x03563482, 0x8ABA3CBB,
|
||||
0x28517711, 0xC20AD9F8, 0xABCC5167, 0xCCAD925F,
|
||||
0x4DE81751, 0x3830DC8E, 0x379D5862, 0x9320F991,
|
||||
0xEA7A90C2, 0xFB3E7BCE, 0x5121CE64, 0x774FBE32,
|
||||
0xA8B6E37E, 0xC3293D46, 0x48DE5369, 0x6413E680,
|
||||
0xA2AE0810, 0xDD6DB224, 0x69852DFD, 0x09072166,
|
||||
0xB39A460A, 0x6445C0DD, 0x586CDECF, 0x1C20C8AE,
|
||||
0x5BBEF7DD, 0x1B588D40, 0xCCD2017F, 0x6BB4E3BB,
|
||||
0xDDA26A7E, 0x3A59FF45, 0x3E350A44, 0xBCB4CDD5,
|
||||
0x72EACEA8, 0xFA6484BB, 0x8D6612AE, 0xBF3C6F47,
|
||||
0xD29BE463, 0x542F5D9E, 0xAEC2771B, 0xF64E6370,
|
||||
0x740E0D8D, 0xE75B1357, 0xF8721671, 0xAF537D5D,
|
||||
0x4040CB08, 0x4EB4E2CC, 0x34D2466A, 0x0115AF84,
|
||||
0xE1B00428, 0x95983A1D, 0x06B89FB4, 0xCE6EA048,
|
||||
0x6F3F3B82, 0x3520AB82, 0x011A1D4B, 0x277227F8,
|
||||
0x611560B1, 0xE7933FDC, 0xBB3A792B, 0x344525BD,
|
||||
0xA08839E1, 0x51CE794B, 0x2F32C9B7, 0xA01FBAC9,
|
||||
0xE01CC87E, 0xBCC7D1F6, 0xCF0111C3, 0xA1E8AAC7,
|
||||
0x1A908749, 0xD44FBD9A, 0xD0DADECB, 0xD50ADA38,
|
||||
0x0339C32A, 0xC6913667, 0x8DF9317C, 0xE0B12B4F,
|
||||
0xF79E59B7, 0x43F5BB3A, 0xF2D519FF, 0x27D9459C,
|
||||
0xBF97222C, 0x15E6FC2A, 0x0F91FC71, 0x9B941525,
|
||||
0xFAE59361, 0xCEB69CEB, 0xC2A86459, 0x12BAA8D1,
|
||||
0xB6C1075E, 0xE3056A0C, 0x10D25065, 0xCB03A442,
|
||||
0xE0EC6E0E, 0x1698DB3B, 0x4C98A0BE, 0x3278E964,
|
||||
0x9F1F9532, 0xE0D392DF, 0xD3A0342B, 0x8971F21E,
|
||||
0x1B0A7441, 0x4BA3348C, 0xC5BE7120, 0xC37632D8,
|
||||
0xDF359F8D, 0x9B992F2E, 0xE60B6F47, 0x0FE3F11D,
|
||||
0xE54CDA54, 0x1EDAD891, 0xCE6279CF, 0xCD3E7E6F,
|
||||
0x1618B166, 0xFD2C1D05, 0x848FD2C5, 0xF6FB2299,
|
||||
0xF523F357, 0xA6327623, 0x93A83531, 0x56CCCD02,
|
||||
0xACF08162, 0x5A75EBB5, 0x6E163697, 0x88D273CC,
|
||||
0xDE966292, 0x81B949D0, 0x4C50901B, 0x71C65614,
|
||||
0xE6C6C7BD, 0x327A140A, 0x45E1D006, 0xC3F27B9A,
|
||||
0xC9AA53FD, 0x62A80F00, 0xBB25BFE2, 0x35BDD2F6,
|
||||
0x71126905, 0xB2040222, 0xB6CBCF7C, 0xCD769C2B,
|
||||
0x53113EC0, 0x1640E3D3, 0x38ABBD60, 0x2547ADF0,
|
||||
0xBA38209C, 0xF746CE76, 0x77AFA1C5, 0x20756060,
|
||||
0x85CBFE4E, 0x8AE88DD8, 0x7AAAF9B0, 0x4CF9AA7E,
|
||||
0x1948C25C, 0x02FB8A8C, 0x01C36AE4, 0xD6EBE1F9,
|
||||
0x90D4F869, 0xA65CDEA0, 0x3F09252D, 0xC208E69F,
|
||||
0xB74E6132, 0xCE77E25B, 0x578FDFE3, 0x3AC372E6 ]
|
||||
];
|
||||
|
||||
var BLOWFISH_CTX = {
|
||||
pbox: [],
|
||||
sbox: []
|
||||
}
|
||||
|
||||
function F(ctx, x){
|
||||
let a = (x >> 24) & 0xFF;
|
||||
let b = (x >> 16) & 0xFF;
|
||||
let c = (x >> 8) & 0xFF;
|
||||
let d = x & 0xFF;
|
||||
|
||||
let y = ctx.sbox[0][a] + ctx.sbox[1][b];
|
||||
y = y ^ ctx.sbox[2][c];
|
||||
y = y + ctx.sbox[3][d];
|
||||
|
||||
return y;
|
||||
}
|
||||
|
||||
function BlowFish_Encrypt(ctx, left, right){
|
||||
let Xl = left;
|
||||
let Xr = right;
|
||||
let temp;
|
||||
|
||||
for(let i = 0; i < N; ++i){
|
||||
Xl = Xl ^ ctx.pbox[i];
|
||||
Xr = F(ctx, Xl) ^ Xr;
|
||||
|
||||
temp = Xl;
|
||||
Xl = Xr;
|
||||
Xr = temp;
|
||||
}
|
||||
|
||||
temp = Xl;
|
||||
Xl = Xr;
|
||||
Xr = temp;
|
||||
|
||||
Xr = Xr ^ ctx.pbox[N];
|
||||
Xl = Xl ^ ctx.pbox[N + 1];
|
||||
|
||||
return {left: Xl, right: Xr};
|
||||
}
|
||||
|
||||
function BlowFish_Decrypt(ctx, left, right){
|
||||
let Xl = left;
|
||||
let Xr = right;
|
||||
let temp;
|
||||
|
||||
for(let i = N + 1; i > 1; --i){
|
||||
Xl = Xl ^ ctx.pbox[i];
|
||||
Xr = F(ctx, Xl) ^ Xr;
|
||||
|
||||
temp = Xl;
|
||||
Xl = Xr;
|
||||
Xr = temp;
|
||||
}
|
||||
|
||||
temp = Xl;
|
||||
Xl = Xr;
|
||||
Xr = temp;
|
||||
|
||||
Xr = Xr ^ ctx.pbox[1];
|
||||
Xl = Xl ^ ctx.pbox[0];
|
||||
|
||||
return {left: Xl, right: Xr};
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialization ctx's pbox and sbox.
|
||||
*
|
||||
* @param {Object} ctx The object has pbox and sbox.
|
||||
* @param {Array} key An array of 32-bit words.
|
||||
* @param {int} keysize The length of the key.
|
||||
*
|
||||
* @example
|
||||
*
|
||||
* BlowFishInit(BLOWFISH_CTX, key, 128/32);
|
||||
*/
|
||||
function BlowFishInit(ctx, key, keysize)
|
||||
{
|
||||
for(let Row = 0; Row < 4; Row++)
|
||||
{
|
||||
ctx.sbox[Row] = [];
|
||||
for(let Col = 0; Col < 256; Col++)
|
||||
{
|
||||
ctx.sbox[Row][Col] = ORIG_S[Row][Col];
|
||||
}
|
||||
}
|
||||
|
||||
let keyIndex = 0;
|
||||
for(let index = 0; index < N + 2; index++)
|
||||
{
|
||||
ctx.pbox[index] = ORIG_P[index] ^ key[keyIndex];
|
||||
keyIndex++;
|
||||
if(keyIndex >= keysize)
|
||||
{
|
||||
keyIndex = 0;
|
||||
}
|
||||
}
|
||||
|
||||
let Data1 = 0;
|
||||
let Data2 = 0;
|
||||
let res = 0;
|
||||
for(let i = 0; i < N + 2; i += 2)
|
||||
{
|
||||
res = BlowFish_Encrypt(ctx, Data1, Data2);
|
||||
Data1 = res.left;
|
||||
Data2 = res.right;
|
||||
ctx.pbox[i] = Data1;
|
||||
ctx.pbox[i + 1] = Data2;
|
||||
}
|
||||
|
||||
for(let i = 0; i < 4; i++)
|
||||
{
|
||||
for(let j = 0; j < 256; j += 2)
|
||||
{
|
||||
res = BlowFish_Encrypt(ctx, Data1, Data2);
|
||||
Data1 = res.left;
|
||||
Data2 = res.right;
|
||||
ctx.sbox[i][j] = Data1;
|
||||
ctx.sbox[i][j + 1] = Data2;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Blowfish block cipher algorithm.
|
||||
*/
|
||||
var Blowfish = C_algo.Blowfish = BlockCipher.extend({
|
||||
_doReset: function () {
|
||||
// Skip reset of nRounds has been set before and key did not change
|
||||
if (this._keyPriorReset === this._key) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Shortcuts
|
||||
var key = this._keyPriorReset = this._key;
|
||||
var keyWords = key.words;
|
||||
var keySize = key.sigBytes / 4;
|
||||
|
||||
//Initialization pbox and sbox
|
||||
BlowFishInit(BLOWFISH_CTX, keyWords, keySize);
|
||||
},
|
||||
|
||||
encryptBlock: function (M, offset) {
|
||||
var res = BlowFish_Encrypt(BLOWFISH_CTX, M[offset], M[offset + 1]);
|
||||
M[offset] = res.left;
|
||||
M[offset + 1] = res.right;
|
||||
},
|
||||
|
||||
decryptBlock: function (M, offset) {
|
||||
var res = BlowFish_Decrypt(BLOWFISH_CTX, M[offset], M[offset + 1]);
|
||||
M[offset] = res.left;
|
||||
M[offset + 1] = res.right;
|
||||
},
|
||||
|
||||
blockSize: 64/32,
|
||||
|
||||
keySize: 128/32,
|
||||
|
||||
ivSize: 64/32
|
||||
});
|
||||
|
||||
/**
|
||||
* Shortcut functions to the cipher's object interface.
|
||||
*
|
||||
* @example
|
||||
*
|
||||
* var ciphertext = CryptoJS.Blowfish.encrypt(message, key, cfg);
|
||||
* var plaintext = CryptoJS.Blowfish.decrypt(ciphertext, key, cfg);
|
||||
*/
|
||||
C.Blowfish = BlockCipher._createHelper(Blowfish);
|
||||
}());
|
||||
|
||||
|
||||
return CryptoJS;
|
||||
|
||||
}));
|
|
@ -0,0 +1,43 @@
|
|||
Copyright (c) 2023 noahcoolboy
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
"Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject
|
||||
to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR
|
||||
ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
|
||||
CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
|
||||
"Commons Clause" License Condition v1.0
|
||||
|
||||
The Software is provided to you by the Licensor under the License,
|
||||
as defined below, subject to the following condition.
|
||||
|
||||
Without limiting other conditions in the License, the grant of
|
||||
rights under the License will not include, and the License does not
|
||||
grant to you, the right to Sell the Software.
|
||||
|
||||
For purposes of the foregoing, "Sell" means practicing any or all
|
||||
of the rights granted to you under the License to provide to third
|
||||
parties, for a fee or other consideration (including without
|
||||
limitation fees for hosting or consulting/ support services related
|
||||
to the Software), a product or service whose value derives, entirely
|
||||
or substantially, from the functionality of the Software. Any license
|
||||
notice or attribution required by the License must also include
|
||||
this Commons Clause License Condition notice.
|
||||
|
||||
Software: All funcaptcha's associated files.
|
||||
License: MIT
|
||||
Licensor: noahcoolboy
|
|
@ -0,0 +1,115 @@
|
|||
# funcaptcha
|
||||
A library used to interact with funcaptchas.
|
||||
## Installation
|
||||
This package is available on npm.
|
||||
Simply run: `npm install funcaptcha`
|
||||
## Usage And Documentation
|
||||
Require the library like any other
|
||||
```js
|
||||
const fun = require("funcaptcha")
|
||||
```
|
||||
|
||||
You must first fetch a token using getToken
|
||||
```js
|
||||
const token = await fun.getToken({
|
||||
pkey: "476068BF-9607-4799-B53D-966BE98E2B81", // The public key
|
||||
surl: "https://roblox-api.arkoselabs.com", // OPTIONAL: Some websites can have a custom service URL
|
||||
data: { // OPTIONAL
|
||||
blob: "blob" // Some websites can have custom data passed: here it is data[blob]
|
||||
},
|
||||
headers: { // OPTIONAL
|
||||
// You can pass custom headers if you have to, but keep
|
||||
// in mind to pass a user agent when doing that
|
||||
"User-Agent": 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.0.0 Safari/537.36'
|
||||
},
|
||||
site: "https://www.roblox.com/login", // The site which contains the funcaptcha
|
||||
proxy: "http://127.0.0.1:8888" // OPTIONAL: A proxy to fetch the token, usually not required
|
||||
// NOTE: The proxy will only be used for fetching the token, and not future requests such as getting images and answering captchas
|
||||
})
|
||||
```
|
||||
|
||||
You can then create a new session
|
||||
```js
|
||||
// Token, in this case, may either be a string (if you already know it) or an object you received from getToken (it will strip the token out of the object)
|
||||
const session = new fun.Session(token, {
|
||||
proxy: "http://127.0.0.1:8888", // OPTIONAL: A proxy used to get images and answer captchas, usually not required
|
||||
userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.0.0 Safari/537.36" // OPTIONAL: Custom user agent for all future requests
|
||||
})
|
||||
|
||||
// If you would like to let a user solve the captcha in their browser
|
||||
// NOTE: Embed URLs will not work unless put in an iframe.
|
||||
console.log(session.getEmbedUrl())
|
||||
|
||||
// Suppressed captchas are instantly valid an do not require for you to load a challenge (it will error)
|
||||
// These can occur when using a trusted IP and good fingerprint
|
||||
// You can check if a captcha is suppressed by doing the following:
|
||||
console.log(session.tokenInfo.sup == "1")
|
||||
```
|
||||
|
||||
One session can get you 10 funcaptcha challenges, you will have to get another session after that.
|
||||
```js
|
||||
let challenge = await session.getChallenge()
|
||||
// Please view https://pastebin.com/raw/Gi6yKwyD to see all the data you can find
|
||||
console.log(challenge.gameType) // Gets the game type (ball, tiles, matchkey, etc...)
|
||||
console.log(challenge.variant) // The game variant, eg: apple, rotated, maze, dice_pair, dart, card, 3d_rollball_animals, etc...
|
||||
console.log(challenge.instruction) // Self explanatory
|
||||
console.log(challenge.waves) // Wave count
|
||||
console.log(challenge.wave) // Current wave number
|
||||
|
||||
// You can then use these functions
|
||||
await challenge.getImage()
|
||||
|
||||
// For game type 1, where you have to rotate a circle to put the image in the correct orientation
|
||||
// In this game type, the angle increment can vary. It can be found with challenge.increment
|
||||
await challenge.answer(3) // Usually 0-6, but can be 0-5 or 0-6 depending on challenge.increment (clockwise)
|
||||
await challenge.answer(51.4) // You can input the raw angle as well (clockwise, negative for counter clockwise)
|
||||
|
||||
// For game type 3, where you have to pick one of 6 tiles
|
||||
await challenge.answer(2) // 0-5, please see https://github.com/noahcoolboy/roblox-funcaptcha/raw/master/img.gif
|
||||
|
||||
// For game type 4, where you pick an image from a selection of images which matches the prompt compared to the image on the left
|
||||
// The answer should be between 0 and challenge.difficulty
|
||||
await challenge.answer(2) // Pick the third image
|
||||
```
|
||||
|
||||
## Full Example
|
||||
```js
|
||||
const fs = require("fs")
|
||||
const fun = require("funcaptcha")
|
||||
const readline = require("readline")
|
||||
let rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout
|
||||
})
|
||||
|
||||
function ask(question) {
|
||||
return new Promise((resolve, reject) => {
|
||||
rl.question(question, (answer) => {
|
||||
resolve(answer)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fun.getToken({
|
||||
pkey: "69A21A01-CC7B-B9C6-0F9A-E7FA06677FFC",
|
||||
}).then(async token => {
|
||||
let session = new fun.Session(token)
|
||||
let challenge = await session.getChallenge()
|
||||
console.log(challenge.data.game_data.game_variant)
|
||||
console.log(challenge.data.game_data.customGUI.api_breaker)
|
||||
|
||||
for(let x = 0; x < challenge.data.game_data.waves; x++) {
|
||||
fs.writeFileSync(`${x}.gif`, await challenge.getImage())
|
||||
console.log(await challenge.answer(parseInt(await ask("Answer: "))))
|
||||
}
|
||||
console.log("Done!")
|
||||
})
|
||||
```
|
||||
|
||||
|
||||
## Support Me
|
||||
Care to support my work?
|
||||
* BTC: 38pbL2kX2f6oXGVvc6WFF2BY9VpUCLH7FG
|
||||
* LTC: M81EXhLSRXNKigqNuz5r7nanAvXmJmjFht
|
||||
* XRP: rw2ciyaNshpHe7bCHo4bRWq6pqqynnWKQg:865667163
|
||||
* Ko-Fi (PayPal): [noahcoolboy](https://ko-fi.com/noahcoolboy)
|
|
@ -0,0 +1,28 @@
|
|||
export interface GetTokenOptions {
|
||||
pkey: string;
|
||||
surl?: string;
|
||||
data?: {
|
||||
[key: string]: string;
|
||||
};
|
||||
headers?: {
|
||||
[key: string]: string;
|
||||
};
|
||||
site?: string;
|
||||
location?: string;
|
||||
proxy?: string;
|
||||
language?: string;
|
||||
}
|
||||
export interface GetTokenResult {
|
||||
challenge_url: string;
|
||||
challenge_url_cdn: string;
|
||||
challenge_url_cdn_sri: string;
|
||||
disable_default_styling: boolean | null;
|
||||
iframe_height: number | null;
|
||||
iframe_width: number | null;
|
||||
kbio: boolean;
|
||||
mbio: boolean;
|
||||
noscript: string;
|
||||
tbio: boolean;
|
||||
token: string;
|
||||
}
|
||||
export declare function getToken(options: GetTokenOptions): Promise<GetTokenResult>;
|
|
@ -0,0 +1,45 @@
|
|||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.getToken = void 0;
|
||||
const http_1 = require("./http");
|
||||
const util_1 = require("./util");
|
||||
async function getToken(options) {
|
||||
options = {
|
||||
surl: "https://client-api.arkoselabs.com",
|
||||
data: {},
|
||||
...options,
|
||||
};
|
||||
if (!options.headers)
|
||||
options.headers = { "User-Agent": util_1.default.DEFAULT_USER_AGENT };
|
||||
else if (!Object.keys(options.headers).map(v => v.toLowerCase()).includes("user-agent"))
|
||||
options.headers["User-Agent"] = util_1.default.DEFAULT_USER_AGENT;
|
||||
options.headers["Accept-Language"] = "en-US,en;q=0.9";
|
||||
options.headers["Sec-Fetch-Site"] = "same-origin";
|
||||
options.headers["Accept"] = "*/*";
|
||||
options.headers["Content-Type"] = "application/x-www-form-urlencoded; charset=UTF-8";
|
||||
options.headers["sec-fetch-mode"] = "cors";
|
||||
if (options.site) {
|
||||
options.headers["Origin"] = options.surl;
|
||||
options.headers["Referer"] = `${options.surl}/v2/${options.pkey}/1.5.5/enforcement.fbfc14b0d793c6ef8359e0e4b4a91f67.html`;
|
||||
}
|
||||
let ua = options.headers[Object.keys(options.headers).find(v => v.toLowerCase() == "user-agent")];
|
||||
let res = await (0, http_1.default)(options.surl, {
|
||||
method: "POST",
|
||||
path: "/fc/gt2/public_key/" + options.pkey,
|
||||
body: util_1.default.constructFormData({
|
||||
bda: util_1.default.getBda(ua, options),
|
||||
public_key: options.pkey,
|
||||
site: options.site ? new URL(options.site).origin : undefined,
|
||||
userbrowser: ua,
|
||||
capi_version: "1.5.5",
|
||||
capi_mode: "inline",
|
||||
style_theme: "default",
|
||||
rnd: Math.random().toString(),
|
||||
...Object.fromEntries(Object.keys(options.data).map(v => ["data[" + v + "]", options.data[v]])),
|
||||
language: options.language || "en",
|
||||
}),
|
||||
headers: options.headers,
|
||||
}, options.proxy);
|
||||
return JSON.parse(res.body.toString());
|
||||
}
|
||||
exports.getToken = getToken;
|
|
@ -0,0 +1,82 @@
|
|||
/// <reference types="node" />
|
||||
import { TokenInfo } from "./session";
|
||||
interface ChallengeOptions {
|
||||
userAgent?: string;
|
||||
proxy?: string;
|
||||
}
|
||||
interface ChallengeData {
|
||||
token: string;
|
||||
tokenInfo: TokenInfo;
|
||||
session_token: string;
|
||||
challengeID: string;
|
||||
challengeURL: string;
|
||||
game_data: {
|
||||
gameType: number;
|
||||
customGUI: {
|
||||
is_using_api_breaker_v2: boolean;
|
||||
_guiFontColr: string;
|
||||
_challenge_imgs: string[];
|
||||
api_breaker: string;
|
||||
encrypted_mode: number;
|
||||
example_images: {
|
||||
correct: string;
|
||||
incorrect: string;
|
||||
};
|
||||
};
|
||||
waves: number;
|
||||
game_variant?: string;
|
||||
game_difficulty?: number;
|
||||
puzzle_name?: string;
|
||||
instruction_string?: string;
|
||||
};
|
||||
game_sid: string;
|
||||
lang: string;
|
||||
string_table: {
|
||||
[key: string]: string;
|
||||
};
|
||||
string_table_prefixes: string[];
|
||||
}
|
||||
interface AnswerResponse {
|
||||
response: "not answered" | "answered";
|
||||
solved?: boolean;
|
||||
incorrect_guess?: number;
|
||||
score?: number;
|
||||
decryption_key?: string;
|
||||
time_end?: number;
|
||||
time_end_seconds?: number;
|
||||
}
|
||||
export declare abstract class Challenge {
|
||||
data: ChallengeData;
|
||||
imgs: Promise<Buffer>[];
|
||||
wave: number;
|
||||
protected key: Promise<string>;
|
||||
protected userAgent: string;
|
||||
protected proxy: string;
|
||||
constructor(data: ChallengeData, challengeOptions: ChallengeOptions);
|
||||
getImage(): Promise<Buffer>;
|
||||
protected getKey(): Promise<string>;
|
||||
abstract answer(answer: number): Promise<AnswerResponse>;
|
||||
get gameType(): number;
|
||||
get variant(): string;
|
||||
get instruction(): string;
|
||||
get waves(): number;
|
||||
}
|
||||
export declare class Challenge1 extends Challenge {
|
||||
private answerHistory;
|
||||
increment: any;
|
||||
constructor(data: ChallengeData, challengeOptions: ChallengeOptions);
|
||||
private round;
|
||||
answer(answer: number): Promise<AnswerResponse>;
|
||||
}
|
||||
export declare class Challenge3 extends Challenge {
|
||||
private answerHistory;
|
||||
constructor(data: ChallengeData, challengeOptions: ChallengeOptions);
|
||||
answer(tile: number): Promise<AnswerResponse>;
|
||||
}
|
||||
export declare class Challenge4 extends Challenge {
|
||||
private answerHistory;
|
||||
constructor(data: ChallengeData, challengeOptions: ChallengeOptions);
|
||||
answer(index: number): Promise<AnswerResponse>;
|
||||
get difficulty(): number;
|
||||
}
|
||||
export {};
|
|
@ -0,0 +1,194 @@
|
|||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.Challenge4 = exports.Challenge3 = exports.Challenge1 = exports.Challenge = void 0;
|
||||
const http_1 = require("./http");
|
||||
const util_1 = require("./util");
|
||||
const crypt_1 = require("./crypt");
|
||||
const console_1 = require("console");
|
||||
class Challenge {
|
||||
constructor(data, challengeOptions) {
|
||||
this.wave = 0;
|
||||
this.data = data;
|
||||
this.userAgent = challengeOptions.userAgent;
|
||||
this.proxy = challengeOptions.proxy;
|
||||
// Preload images
|
||||
this.imgs = data.game_data.customGUI._challenge_imgs.map(async (v) => {
|
||||
let req = await (0, http_1.default)(v, {
|
||||
method: "GET",
|
||||
path: undefined,
|
||||
headers: {
|
||||
"User-Agent": this.userAgent,
|
||||
"Referer": this.data.tokenInfo.surl
|
||||
},
|
||||
});
|
||||
return req.body;
|
||||
});
|
||||
if (data.game_data.customGUI.encrypted_mode) {
|
||||
// Preload decryption key
|
||||
this.key = this.getKey();
|
||||
}
|
||||
}
|
||||
async getImage() {
|
||||
let img = await this.imgs[this.wave];
|
||||
try {
|
||||
JSON.parse(img.toString()); // Image is encrypted
|
||||
img = Buffer.from(await crypt_1.default.decrypt(img.toString(), await this.getKey()), "base64");
|
||||
}
|
||||
catch (err) {
|
||||
// Image is not encrypted
|
||||
// All good!
|
||||
}
|
||||
return img;
|
||||
}
|
||||
async getKey() {
|
||||
if (this.key)
|
||||
return await this.key;
|
||||
let response = await (0, http_1.default)(this.data.tokenInfo.surl, {
|
||||
method: "POST",
|
||||
path: "/fc/ekey/",
|
||||
headers: {
|
||||
"User-Agent": this.userAgent,
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
"Referer": this.data.tokenInfo.surl,
|
||||
},
|
||||
body: util_1.default.constructFormData({
|
||||
session_token: this.data.session_token,
|
||||
game_token: this.data.challengeID,
|
||||
}),
|
||||
}, this.proxy);
|
||||
this.key = JSON.parse(response.body.toString()).decryption_key;
|
||||
return this.key;
|
||||
}
|
||||
get gameType() {
|
||||
return this.data.game_data.gameType;
|
||||
}
|
||||
get variant() {
|
||||
return this.data.game_data.game_variant || this.data.game_data.instruction_string;
|
||||
}
|
||||
get instruction() {
|
||||
return this.data.string_table[`${this.data.game_data.gameType}.instructions-${this.variant}`] || this.data.string_table[`${this.data.game_data.gameType}.touch_done_info${this.data.game_data.game_variant ? `_${this.data.game_data.game_variant}` : ""}`];
|
||||
}
|
||||
get waves() {
|
||||
return this.data.game_data.waves;
|
||||
}
|
||||
}
|
||||
exports.Challenge = Challenge;
|
||||
class Challenge1 extends Challenge {
|
||||
constructor(data, challengeOptions) {
|
||||
super(data, challengeOptions);
|
||||
this.answerHistory = [];
|
||||
// But WHY?!
|
||||
let clr = data.game_data.customGUI._guiFontColr;
|
||||
this.increment = parseInt(clr ? clr.replace("#", "").substring(3) : "28", 16);
|
||||
this.increment = this.increment > 113 ? this.increment / 10 : this.increment;
|
||||
}
|
||||
round(num) {
|
||||
return (Math.round(num * 10) / 10).toFixed(2);
|
||||
}
|
||||
async answer(answer) {
|
||||
if (answer >= 0 && answer <= Math.round(360 / 51.4) - 1)
|
||||
this.answerHistory.push(this.round(answer * this.increment));
|
||||
else
|
||||
this.answerHistory.push(this.round(answer));
|
||||
let encrypted = await crypt_1.default.encrypt(this.answerHistory.toString(), this.data.session_token);
|
||||
let req = await (0, http_1.default)(this.data.tokenInfo.surl, {
|
||||
method: "POST",
|
||||
path: "/fc/ca/",
|
||||
headers: {
|
||||
"User-Agent": this.userAgent,
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
"Referer": this.data.challengeURL
|
||||
},
|
||||
body: util_1.default.constructFormData({
|
||||
session_token: this.data.session_token,
|
||||
game_token: this.data.challengeID,
|
||||
guess: encrypted,
|
||||
}),
|
||||
}, this.proxy);
|
||||
let reqData = JSON.parse(req.body.toString());
|
||||
this.key = reqData.decryption_key || "";
|
||||
this.wave++;
|
||||
return reqData;
|
||||
}
|
||||
}
|
||||
exports.Challenge1 = Challenge1;
|
||||
class Challenge3 extends Challenge {
|
||||
constructor(data, challengeOptions) {
|
||||
super(data, challengeOptions);
|
||||
this.answerHistory = [];
|
||||
}
|
||||
async answer(tile) {
|
||||
(0, console_1.assert)(tile >= 0 && tile <= 5, "Tile must be between 0 and 5");
|
||||
let pos = util_1.default.tileToLoc(tile);
|
||||
this.answerHistory.push(util_1.default.solveBreaker(!!this.data.game_data.customGUI.is_using_api_breaker_v2, this.data.game_data.customGUI.api_breaker, 3, pos));
|
||||
let encrypted = await crypt_1.default.encrypt(JSON.stringify(this.answerHistory), this.data.session_token);
|
||||
let requestedId = await crypt_1.default.encrypt(JSON.stringify({}), `REQUESTED${this.data.session_token}ID`);
|
||||
let { cookie: tCookie, value: tValue } = util_1.default.getTimestamp();
|
||||
let req = await (0, http_1.default)(this.data.tokenInfo.surl, {
|
||||
method: "POST",
|
||||
path: "/fc/ca/",
|
||||
headers: {
|
||||
"User-Agent": this.userAgent,
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
"X-Newrelic-Timestamp": tValue,
|
||||
"X-Requested-ID": requestedId,
|
||||
"Cookie": tCookie,
|
||||
"Referer": this.data.challengeURL
|
||||
},
|
||||
body: util_1.default.constructFormData({
|
||||
session_token: this.data.session_token,
|
||||
game_token: this.data.challengeID,
|
||||
guess: encrypted,
|
||||
analytics_tier: this.data.tokenInfo.at,
|
||||
sid: this.data.tokenInfo.r,
|
||||
bio: this.data.tokenInfo.mbio && "eyJtYmlvIjoiMTI1MCwwLDE0NywyMDQ7MTg5NCwwLDE1MSwyMDA7MTk2MCwxLDE1MiwxOTk7MjAyOSwyLDE1MiwxOTk7MjU3NSwwLDE1NSwxOTU7MjU4NSwwLDE1NiwxOTA7MjU5NSwwLDE1OCwxODU7MjYwNCwwLDE1OSwxODA7MjYxMywwLDE2MCwxNzU7MjYyMSwwLDE2MSwxNzA7MjYzMCwwLDE2MywxNjU7MjY0MCwwLDE2NCwxNjA7MjY1MCwwLDE2NSwxNTU7MjY2NCwwLDE2NiwxNTA7MjY3NywwLDE2NiwxNDQ7MjY5NCwwLDE2NywxMzk7MjcyMCwwLDE2NywxMzM7Mjc1NCwwLDE2NywxMjc7Mjc4MywwLDE2NywxMjE7MjgxMiwwLDE2NywxMTU7Mjg0MywwLDE2NywxMDk7Mjg2MywwLDE2NywxMDM7Mjg3NSwwLDE2Niw5ODsyOTA1LDAsMTY1LDkzOzMyMzIsMCwxNjUsOTk7MzI2MiwwLDE2NSwxMDU7MzI5OSwwLDE2NCwxMTA7MzM0MCwwLDE2MSwxMTU7MzM3MiwwLDE1NywxMjA7MzM5NSwwLDE1MywxMjQ7MzQwOCwwLDE0OCwxMjc7MzQyMCwwLDE0MywxMzA7MzQyOSwwLDEzOCwxMzE7MzQ0MSwwLDEzMywxMzQ7MzQ1MCwwLDEyOCwxMzU7MzQ2MSwwLDEyMywxMzg7MzQ3NiwwLDExOCwxNDA7MzQ4OSwwLDExMywxNDI7MzUwMywwLDEwOCwxNDM7MzUxOCwwLDEwMywxNDQ7MzUzNCwwLDk4LDE0NTszNTU2LDAsOTMsMTQ2OzM2MTUsMCw4OCwxNDg7MzY2MiwwLDgzLDE1MTszNjgzLDAsNzgsMTU0OzM3MDEsMCw3MywxNTc7MzcyNSwwLDY5LDE2MTszNzkzLDEsNjgsMTYyOzM4NTEsMiw2OCwxNjI7IiwidGJpbyI6IiIsImtiaW8iOiIifQ=="
|
||||
}),
|
||||
}, this.proxy);
|
||||
let reqData = JSON.parse(req.body.toString());
|
||||
this.key = reqData.decryption_key || "";
|
||||
this.wave++;
|
||||
return reqData;
|
||||
}
|
||||
}
|
||||
exports.Challenge3 = Challenge3;
|
||||
class Challenge4 extends Challenge {
|
||||
constructor(data, challengeOptions) {
|
||||
super(data, challengeOptions);
|
||||
this.answerHistory = [];
|
||||
}
|
||||
async answer(index) {
|
||||
(0, console_1.assert)(index >= 0 && index <= this.data.game_data.game_difficulty - 1, "Index must be between 0 and " + (this.data.game_data.game_difficulty - 1));
|
||||
this.answerHistory.push(util_1.default.solveBreaker(!!this.data.game_data.customGUI.is_using_api_breaker_v2, this.data.game_data.customGUI.api_breaker, 4, { index }));
|
||||
let encrypted = await crypt_1.default.encrypt(JSON.stringify(this.answerHistory), this.data.session_token);
|
||||
let requestedId = await crypt_1.default.encrypt(JSON.stringify({}), `REQUESTED${this.data.session_token}ID`);
|
||||
let { cookie: tCookie, value: tValue } = util_1.default.getTimestamp();
|
||||
let req = await (0, http_1.default)(this.data.tokenInfo.surl, {
|
||||
method: "POST",
|
||||
path: "/fc/ca/",
|
||||
headers: {
|
||||
"User-Agent": this.userAgent,
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
"X-Newrelic-Timestamp": tValue,
|
||||
"X-Requested-ID": requestedId,
|
||||
"Cookie": tCookie,
|
||||
"Referer": this.data.challengeURL
|
||||
},
|
||||
body: util_1.default.constructFormData({
|
||||
session_token: this.data.session_token,
|
||||
game_token: this.data.challengeID,
|
||||
guess: encrypted,
|
||||
analytics_tier: this.data.tokenInfo.at,
|
||||
sid: this.data.tokenInfo.r,
|
||||
bio: this.data.tokenInfo.mbio && "eyJtYmlvIjoiMTI1MCwwLDE0NywyMDQ7MTg5NCwwLDE1MSwyMDA7MTk2MCwxLDE1MiwxOTk7MjAyOSwyLDE1MiwxOTk7MjU3NSwwLDE1NSwxOTU7MjU4NSwwLDE1NiwxOTA7MjU5NSwwLDE1OCwxODU7MjYwNCwwLDE1OSwxODA7MjYxMywwLDE2MCwxNzU7MjYyMSwwLDE2MSwxNzA7MjYzMCwwLDE2MywxNjU7MjY0MCwwLDE2NCwxNjA7MjY1MCwwLDE2NSwxNTU7MjY2NCwwLDE2NiwxNTA7MjY3NywwLDE2NiwxNDQ7MjY5NCwwLDE2NywxMzk7MjcyMCwwLDE2NywxMzM7Mjc1NCwwLDE2NywxMjc7Mjc4MywwLDE2NywxMjE7MjgxMiwwLDE2NywxMTU7Mjg0MywwLDE2NywxMDk7Mjg2MywwLDE2NywxMDM7Mjg3NSwwLDE2Niw5ODsyOTA1LDAsMTY1LDkzOzMyMzIsMCwxNjUsOTk7MzI2MiwwLDE2NSwxMDU7MzI5OSwwLDE2NCwxMTA7MzM0MCwwLDE2MSwxMTU7MzM3MiwwLDE1NywxMjA7MzM5NSwwLDE1MywxMjQ7MzQwOCwwLDE0OCwxMjc7MzQyMCwwLDE0MywxMzA7MzQyOSwwLDEzOCwxMzE7MzQ0MSwwLDEzMywxMzQ7MzQ1MCwwLDEyOCwxMzU7MzQ2MSwwLDEyMywxMzg7MzQ3NiwwLDExOCwxNDA7MzQ4OSwwLDExMywxNDI7MzUwMywwLDEwOCwxNDM7MzUxOCwwLDEwMywxNDQ7MzUzNCwwLDk4LDE0NTszNTU2LDAsOTMsMTQ2OzM2MTUsMCw4OCwxNDg7MzY2MiwwLDgzLDE1MTszNjgzLDAsNzgsMTU0OzM3MDEsMCw3MywxNTc7MzcyNSwwLDY5LDE2MTszNzkzLDEsNjgsMTYyOzM4NTEsMiw2OCwxNjI7IiwidGJpbyI6IiIsImtiaW8iOiIifQ=="
|
||||
}),
|
||||
}, this.proxy);
|
||||
let reqData = JSON.parse(req.body.toString());
|
||||
this.key = reqData.decryption_key || "";
|
||||
this.wave++;
|
||||
return reqData;
|
||||
}
|
||||
get difficulty() {
|
||||
return this.data.game_data.game_difficulty;
|
||||
}
|
||||
}
|
||||
exports.Challenge4 = Challenge4;
|
|
@ -0,0 +1,7 @@
|
|||
declare function encrypt(data: string, key: string): string;
|
||||
declare function decrypt(rawData: string, key: string): string;
|
||||
declare const _default: {
|
||||
encrypt: typeof encrypt;
|
||||
decrypt: typeof decrypt;
|
||||
};
|
||||
export default _default;
|
|
@ -0,0 +1,51 @@
|
|||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const crypto_1 = require("crypto");
|
||||
function encrypt(data, key) {
|
||||
let salt = "";
|
||||
let salted = "";
|
||||
let dx = Buffer.alloc(0);
|
||||
// Generate salt, as 8 random lowercase letters
|
||||
salt = String.fromCharCode(...Array(8).fill(0).map(_ => Math.floor(Math.random() * 26) + 97));
|
||||
// Our final key and iv come from the key and salt being repeatedly hashed
|
||||
// dx = md5(md5(md5(key + salt) + key + salt) + key + salt)
|
||||
// For each round of hashing, we append the result to salted, resulting in a 96 character string
|
||||
// The first 64 characters are the key, and the last 32 are the iv
|
||||
for (let x = 0; x < 3; x++) {
|
||||
dx = (0, crypto_1.createHash)("md5")
|
||||
.update(Buffer.concat([
|
||||
Buffer.from(dx),
|
||||
Buffer.from(key),
|
||||
Buffer.from(salt),
|
||||
]))
|
||||
.digest();
|
||||
salted += dx.toString("hex");
|
||||
}
|
||||
let aes = (0, crypto_1.createCipheriv)("aes-256-cbc", Buffer.from(salted.substring(0, 64), "hex"), // Key
|
||||
Buffer.from(salted.substring(64, 64 + 32), "hex") // IV
|
||||
);
|
||||
return JSON.stringify({
|
||||
ct: aes.update(data, null, "base64") + aes.final("base64"),
|
||||
iv: salted.substring(64, 64 + 32),
|
||||
s: Buffer.from(salt).toString("hex"),
|
||||
});
|
||||
}
|
||||
function decrypt(rawData, key) {
|
||||
let data = JSON.parse(rawData);
|
||||
// We get our decryption key by doing the inverse of the encryption process
|
||||
let dk = Buffer.concat([Buffer.from(key), Buffer.from(data.s, "hex")]);
|
||||
let arr = [Buffer.from((0, crypto_1.createHash)("md5").update(dk).digest()).toString("hex")];
|
||||
let result = arr[0];
|
||||
for (let x = 1; x < 3; x++) {
|
||||
arr.push(Buffer.from((0, crypto_1.createHash)("md5")
|
||||
.update(Buffer.concat([Buffer.from(arr[x - 1], "hex"), dk]))
|
||||
.digest()).toString("hex"));
|
||||
result += arr[x];
|
||||
}
|
||||
let aes = (0, crypto_1.createDecipheriv)("aes-256-cbc", Buffer.from(result.substring(0, 64), "hex"), Buffer.from(data.iv, "hex"));
|
||||
return aes.update(data.ct, "base64", "utf8") + aes.final("utf8");
|
||||
}
|
||||
exports.default = {
|
||||
encrypt,
|
||||
decrypt,
|
||||
};
|
|
@ -0,0 +1,63 @@
|
|||
declare const baseFingerprint: {
|
||||
DNT: string;
|
||||
L: string;
|
||||
D: number;
|
||||
PR: number;
|
||||
S: number[];
|
||||
AS: number[];
|
||||
TO: number;
|
||||
SS: boolean;
|
||||
LS: boolean;
|
||||
IDB: boolean;
|
||||
B: boolean;
|
||||
ODB: boolean;
|
||||
CPUC: string;
|
||||
PK: string;
|
||||
CFP: string;
|
||||
FR: boolean;
|
||||
FOS: boolean;
|
||||
FB: boolean;
|
||||
JSF: string[];
|
||||
P: string[];
|
||||
T: (number | boolean)[];
|
||||
H: number;
|
||||
SWF: boolean;
|
||||
};
|
||||
declare function getFingerprint(): {
|
||||
DNT: string;
|
||||
L: string;
|
||||
D: number;
|
||||
PR: number;
|
||||
S: number[];
|
||||
AS: number[];
|
||||
TO: number;
|
||||
SS: boolean;
|
||||
LS: boolean;
|
||||
IDB: boolean;
|
||||
B: boolean;
|
||||
ODB: boolean;
|
||||
CPUC: string;
|
||||
PK: string;
|
||||
CFP: string;
|
||||
FR: boolean;
|
||||
FOS: boolean;
|
||||
FB: boolean;
|
||||
JSF: string[];
|
||||
P: string[];
|
||||
T: (number | boolean)[];
|
||||
H: number;
|
||||
SWF: boolean;
|
||||
};
|
||||
declare function prepareF(fingerprint: any): string;
|
||||
declare function prepareFe(fingerprint: any): any[];
|
||||
declare function getEnhancedFingerprint(fp: typeof baseFingerprint, ua: string, opts: any): {
|
||||
key: string;
|
||||
value: any;
|
||||
}[];
|
||||
declare const _default: {
|
||||
getFingerprint: typeof getFingerprint;
|
||||
prepareF: typeof prepareF;
|
||||
prepareFe: typeof prepareFe;
|
||||
getEnhancedFingerprint: typeof getEnhancedFingerprint;
|
||||
};
|
||||
export default _default;
|
|
@ -0,0 +1,304 @@
|
|||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const murmur_1 = require("./murmur");
|
||||
const crypto_1 = require("crypto");
|
||||
const baseFingerprint = {
|
||||
DNT: "unknown",
|
||||
L: "en-US",
|
||||
D: 24,
|
||||
PR: 1,
|
||||
S: [1920, 1200],
|
||||
AS: [1920, 1200],
|
||||
TO: 9999,
|
||||
SS: true,
|
||||
LS: true,
|
||||
IDB: true,
|
||||
B: false,
|
||||
ODB: true,
|
||||
CPUC: "unknown",
|
||||
PK: "Win32",
|
||||
CFP: `canvas winding:yes~canvas fp:data:image/png;base64,${Buffer.from(Math.random().toString()).toString("base64")}`,
|
||||
FR: false,
|
||||
FOS: false,
|
||||
FB: false,
|
||||
JSF: [
|
||||
"Andale Mono",
|
||||
"Arial",
|
||||
"Arial Black",
|
||||
"Arial Hebrew",
|
||||
"Arial MT",
|
||||
"Arial Narrow",
|
||||
"Arial Rounded MT Bold",
|
||||
"Arial Unicode MS",
|
||||
"Bitstream Vera Sans Mono",
|
||||
"Book Antiqua",
|
||||
"Bookman Old Style",
|
||||
"Calibri",
|
||||
"Cambria",
|
||||
"Cambria Math",
|
||||
"Century",
|
||||
"Century Gothic",
|
||||
"Century Schoolbook",
|
||||
"Comic Sans",
|
||||
"Comic Sans MS",
|
||||
"Consolas",
|
||||
"Courier",
|
||||
"Courier New",
|
||||
"Garamond",
|
||||
"Geneva",
|
||||
"Georgia",
|
||||
"Helvetica",
|
||||
"Helvetica Neue",
|
||||
"Impact",
|
||||
"Lucida Bright",
|
||||
"Lucida Calligraphy",
|
||||
"Lucida Console",
|
||||
"Lucida Fax",
|
||||
"LUCIDA GRANDE",
|
||||
"Lucida Handwriting",
|
||||
"Lucida Sans",
|
||||
"Lucida Sans Typewriter",
|
||||
"Lucida Sans Unicode",
|
||||
"Microsoft Sans Serif",
|
||||
"Monaco",
|
||||
"Monotype Corsiva",
|
||||
"MS Gothic",
|
||||
"MS Outlook",
|
||||
"MS PGothic",
|
||||
"MS Reference Sans Serif",
|
||||
"MS Sans Serif",
|
||||
"MS Serif",
|
||||
"MYRIAD",
|
||||
"MYRIAD PRO",
|
||||
"Palatino",
|
||||
"Palatino Linotype",
|
||||
"Segoe Print",
|
||||
"Segoe Script",
|
||||
"Segoe UI",
|
||||
"Segoe UI Light",
|
||||
"Segoe UI Semibold",
|
||||
"Segoe UI Symbol",
|
||||
"Tahoma",
|
||||
"Times",
|
||||
"Times New Roman",
|
||||
"Times New Roman PS",
|
||||
"Trebuchet MS",
|
||||
"Verdana",
|
||||
"Wingdings",
|
||||
"Wingdings 2",
|
||||
"Wingdings 3",
|
||||
],
|
||||
P: [
|
||||
"Chrome PDF Plugin::Portable Document Format::application/x-google-chrome-pdf~pdf",
|
||||
"Chrome PDF Viewer::::application/pdf~pdf",
|
||||
"Native Client::::application/x-nacl~,application/x-pnacl~",
|
||||
],
|
||||
T: [0, false, false],
|
||||
H: 24,
|
||||
SWF: false, // Flash support
|
||||
};
|
||||
const languages = [
|
||||
"af", "af-ZA", "ar", "ar-AE", "ar-BH", "ar-DZ", "ar-EG", "ar-IQ", "ar-JO", "ar-KW", "ar-LB", "ar-LY", "ar-MA", "ar-OM", "ar-QA", "ar-SA",
|
||||
"ar-SY", "ar-TN", "ar-YE", "az", "az-AZ", "az-AZ", "be", "be-BY", "bg", "bg-BG", "bs-BA", "ca", "ca-ES", "cs", "cs-CZ", "cy",
|
||||
"cy-GB", "da", "da-DK", "de", "de-AT", "de-CH", "de-DE", "de-LI", "de-LU", "dv", "dv-MV", "el", "el-GR", "en", "en-AU", "en-BZ",
|
||||
"en-CA", "en-CB", "en-GB", "en-IE", "en-JM", "en-NZ", "en-PH", "en-TT", "en-US", "en-ZA", "en-ZW", "eo", "es", "es-AR", "es-BO", "es-CL",
|
||||
"es-CO", "es-CR", "es-DO", "es-EC", "es-ES", "es-ES", "es-GT", "es-HN", "es-MX", "es-NI", "es-PA", "es-PE", "es-PR", "es-PY", "es-SV", "es-UY",
|
||||
"es-VE", "et", "et-EE", "eu", "eu-ES", "fa", "fa-IR", "fi", "fi-FI", "fo", "fo-FO", "fr", "fr-BE", "fr-CA", "fr-CH", "fr-FR",
|
||||
"fr-LU", "fr-MC", "gl", "gl-ES", "gu", "gu-IN", "he", "he-IL", "hi", "hi-IN", "hr", "hr-BA", "hr-HR", "hu", "hu-HU", "hy",
|
||||
"hy-AM", "id", "id-ID", "is", "is-IS", "it", "it-CH", "it-IT", "ja", "ja-JP", "ka", "ka-GE", "kk", "kk-KZ", "kn", "kn-IN",
|
||||
"ko", "ko-KR", "kok", "kok-IN", "ky", "ky-KG", "lt", "lt-LT", "lv", "lv-LV", "mi", "mi-NZ", "mk", "mk-MK", "mn", "mn-MN",
|
||||
"mr", "mr-IN", "ms", "ms-BN", "ms-MY", "mt", "mt-MT", "nb", "nb-NO", "nl", "nl-BE", "nl-NL", "nn-NO", "ns", "ns-ZA", "pa",
|
||||
"pa-IN", "pl", "pl-PL", "ps", "ps-AR", "pt", "pt-BR", "pt-PT", "qu", "qu-BO", "qu-EC", "qu-PE", "ro", "ro-RO", "ru", "ru-RU",
|
||||
"sa", "sa-IN", "se", "se-FI", "se-FI", "se-FI", "se-NO", "se-NO", "se-NO", "se-SE", "se-SE", "se-SE", "sk", "sk-SK", "sl", "sl-SI",
|
||||
"sq", "sq-AL", "sr-BA", "sr-BA", "sr-SP", "sr-SP", "sv", "sv-FI", "sv-SE", "sw", "sw-KE", "syr", "syr-SY", "ta", "ta-IN", "te",
|
||||
"te-IN", "th", "th-TH", "tl", "tl-PH", "tn", "tn-ZA", "tr", "tr-TR", "tt", "tt-RU", "ts", "uk", "uk-UA", "ur", "ur-PK",
|
||||
"uz", "uz-UZ", "uz-UZ", "vi", "vi-VN", "xh", "xh-ZA", "zh", "zh-CN", "zh-HK", "zh-MO", "zh-SG", "zh-TW", "zu", "zu-ZA"
|
||||
];
|
||||
let screenRes = [
|
||||
[1920, 1080],
|
||||
[1920, 1200],
|
||||
[2048, 1080],
|
||||
[2560, 1440],
|
||||
[1366, 768],
|
||||
[1440, 900],
|
||||
[1536, 864],
|
||||
[1680, 1050],
|
||||
[1280, 1024],
|
||||
[1280, 800],
|
||||
[1280, 720],
|
||||
[1600, 1200],
|
||||
[1600, 900],
|
||||
];
|
||||
function randomScreenRes() {
|
||||
return screenRes[Math.floor(Math.random() * screenRes.length)];
|
||||
}
|
||||
// Get fingerprint
|
||||
function getFingerprint() {
|
||||
let fingerprint = { ...baseFingerprint }; // Create a copy of the base fingerprint
|
||||
// Randomization time!
|
||||
fingerprint["DNT"] = "unknown";
|
||||
fingerprint["L"] = languages[Math.floor(Math.random() * languages.length)];
|
||||
fingerprint["D"] = [8, 24][Math.floor(Math.random() * 2)];
|
||||
fingerprint["PR"] = Math.round(Math.random() * 100) / 100 * 2 + 0.5;
|
||||
fingerprint["S"] = randomScreenRes();
|
||||
fingerprint["AS"] = fingerprint.S;
|
||||
fingerprint["TO"] = (Math.floor(Math.random() * 24) - 12) * 60;
|
||||
fingerprint["SS"] = Math.random() > 0.5;
|
||||
fingerprint["LS"] = Math.random() > 0.5;
|
||||
fingerprint["IDB"] = Math.random() > 0.5;
|
||||
fingerprint["B"] = Math.random() > 0.5;
|
||||
fingerprint["ODB"] = Math.random() > 0.5;
|
||||
fingerprint["CPUC"] = "unknown";
|
||||
fingerprint["PK"] = "Win32";
|
||||
fingerprint["CFP"] = "canvas winding:yes~canvas fp:data:image/png;base64," + (0, crypto_1.randomBytes)(128).toString("base64");
|
||||
fingerprint["FR"] = false; // Fake Resolution
|
||||
fingerprint["FOS"] = false; // Fake Operating System
|
||||
fingerprint["FB"] = false; // Fake Browser
|
||||
fingerprint["JSF"] = fingerprint["JSF"].filter(() => Math.random() > 0.5);
|
||||
fingerprint["P"] = fingerprint["P"].filter(() => Math.random() > 0.5);
|
||||
fingerprint["T"] = [
|
||||
Math.floor(Math.random() * 8),
|
||||
Math.random() > 0.5,
|
||||
Math.random() > 0.5,
|
||||
];
|
||||
fingerprint["H"] = 2 ** Math.floor(Math.random() * 6);
|
||||
fingerprint["SWF"] = fingerprint["SWF"]; // RIP Flash
|
||||
return fingerprint;
|
||||
}
|
||||
function prepareF(fingerprint) {
|
||||
let f = [];
|
||||
let keys = Object.keys(fingerprint);
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
if (fingerprint[keys[i]].join)
|
||||
f.push(fingerprint[keys[i]].join(";"));
|
||||
else
|
||||
f.push(fingerprint[keys[i]]);
|
||||
}
|
||||
return f.join("~~~");
|
||||
}
|
||||
function prepareFe(fingerprint) {
|
||||
let fe = [];
|
||||
let keys = Object.keys(fingerprint);
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
switch (keys[i]) {
|
||||
case "CFP":
|
||||
fe.push(`${keys[i]}:${cfpHash(fingerprint[keys[i]])}`);
|
||||
break;
|
||||
case "P":
|
||||
fe.push(`${keys[i]}:${fingerprint[keys[i]].map((v) => v.split("::")[0])}`);
|
||||
break;
|
||||
default:
|
||||
fe.push(`${keys[i]}:${fingerprint[keys[i]]}`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return fe;
|
||||
}
|
||||
function cfpHash(H8W) {
|
||||
var l8W, U8W;
|
||||
if (!H8W)
|
||||
return "";
|
||||
if (Array.prototype.reduce)
|
||||
return H8W.split("").reduce(function (p8W, z8W) {
|
||||
p8W = (p8W << 5) - p8W + z8W.charCodeAt(0);
|
||||
return p8W & p8W;
|
||||
}, 0);
|
||||
l8W = 0;
|
||||
if (H8W.length === 0)
|
||||
return l8W;
|
||||
for (var k8W = 0; k8W < H8W.length; k8W++) {
|
||||
U8W = H8W.charCodeAt(k8W);
|
||||
l8W = (l8W << 5) - l8W + U8W;
|
||||
l8W = l8W & l8W;
|
||||
}
|
||||
return l8W;
|
||||
}
|
||||
let baseEnhancedFingerprint = {
|
||||
"webgl_extensions": "ANGLE_instanced_arrays;EXT_blend_minmax;EXT_color_buffer_half_float;EXT_disjoint_timer_query;EXT_float_blend;EXT_frag_depth;EXT_shader_texture_lod;EXT_texture_compression_bptc;EXT_texture_compression_rgtc;EXT_texture_filter_anisotropic;EXT_sRGB;KHR_parallel_shader_compile;OES_element_index_uint;OES_fbo_render_mipmap;OES_standard_derivatives;OES_texture_float;OES_texture_float_linear;OES_texture_half_float;OES_texture_half_float_linear;OES_vertex_array_object;WEBGL_color_buffer_float;WEBGL_compressed_texture_s3tc;WEBGL_compressed_texture_s3tc_srgb;WEBGL_debug_renderer_info;WEBGL_debug_shaders;WEBGL_depth_texture;WEBGL_draw_buffers;WEBGL_lose_context;WEBGL_multi_draw",
|
||||
"webgl_extensions_hash": "58a5a04a5bef1a78fa88d5c5098bd237",
|
||||
"webgl_renderer": "WebKit WebGL",
|
||||
"webgl_vendor": "WebKit",
|
||||
"webgl_version": "WebGL 1.0 (OpenGL ES 2.0 Chromium)",
|
||||
"webgl_shading_language_version": "WebGL GLSL ES 1.0 (OpenGL ES GLSL ES 1.0 Chromium)",
|
||||
"webgl_aliased_line_width_range": "[1, 1]",
|
||||
"webgl_aliased_point_size_range": "[1, 1023]",
|
||||
"webgl_antialiasing": "yes",
|
||||
"webgl_bits": "8,8,24,8,8,0",
|
||||
"webgl_max_params": "16,64,16384,4096,8192,32,8192,31,16,32,4096",
|
||||
"webgl_max_viewport_dims": "[8192, 8192]",
|
||||
"webgl_unmasked_vendor": "Google Inc. (Google)",
|
||||
"webgl_unmasked_renderer": "ANGLE (Google, Vulkan 1.3.0 (SwiftShader Device (Subzero) (0x0000C0DE)), SwiftShader driver)",
|
||||
"webgl_vsf_params": "23,127,127,23,127,127,23,127,127",
|
||||
"webgl_vsi_params": "0,31,30,0,31,30,0,31,30",
|
||||
"webgl_fsf_params": "23,127,127,23,127,127,23,127,127",
|
||||
"webgl_fsi_params": "0,31,30,0,31,30,0,31,30",
|
||||
"webgl_hash_webgl": null,
|
||||
"user_agent_data_brands": "Chromium,Google Chrome,Not=A?Brand",
|
||||
"user_agent_data_mobile": null,
|
||||
"navigator_connection_downlink": null,
|
||||
"navigator_connection_downlink_max": null,
|
||||
"network_info_rtt": null,
|
||||
"network_info_save_data": false,
|
||||
"network_info_rtt_type": null,
|
||||
"screen_pixel_depth": 24,
|
||||
"navigator_device_memory": 0.5,
|
||||
"navigator_languages": "en-US,fr-BE,fr,en-BE,en",
|
||||
"window_inner_width": 0,
|
||||
"window_inner_height": 0,
|
||||
"window_outer_width": 2195,
|
||||
"window_outer_height": 1195,
|
||||
"browser_detection_firefox": false,
|
||||
"browser_detection_brave": false,
|
||||
"audio_codecs": "{\"ogg\":\"probably\",\"mp3\":\"probably\",\"wav\":\"probably\",\"m4a\":\"maybe\",\"aac\":\"probably\"}",
|
||||
"video_codecs": "{\"ogg\":\"probably\",\"h264\":\"probably\",\"webm\":\"probably\",\"mpeg4v\":\"\",\"mpeg4a\":\"\",\"theora\":\"\"}",
|
||||
"media_query_dark_mode": true,
|
||||
"headless_browser_phantom": false,
|
||||
"headless_browser_selenium": false,
|
||||
"headless_browser_nightmare_js": false,
|
||||
"document__referrer": "https://www.roblox.com/",
|
||||
"window__ancestor_origins": [
|
||||
"https://www.roblox.com",
|
||||
],
|
||||
"window__tree_index": [
|
||||
0
|
||||
],
|
||||
"window__tree_structure": "[[]]",
|
||||
"window__location_href": "https://roblox-api.arkoselabs.com/v2/1.5.5/enforcement.fbfc14b0d793c6ef8359e0e4b4a91f67.html#476068BF-9607-4799-B53D-966BE98E2B81",
|
||||
"client_config__sitedata_location_href": "https://www.roblox.com/arkose/iframe",
|
||||
"client_config__surl": "https://roblox-api.arkoselabs.com",
|
||||
"client_config__language": null,
|
||||
"navigator_battery_charging": true,
|
||||
"audio_fingerprint": "124.04347527516074"
|
||||
};
|
||||
function getEnhancedFingerprint(fp, ua, opts) {
|
||||
let fingerprint = { ...baseEnhancedFingerprint };
|
||||
fingerprint.webgl_extensions = fingerprint.webgl_extensions.split(";").filter(_ => Math.random() > 0.5).join(";");
|
||||
fingerprint.webgl_extensions_hash = (0, murmur_1.default)(fingerprint.webgl_extensions, 0);
|
||||
fingerprint.screen_pixel_depth = fp.D;
|
||||
fingerprint.navigator_languages = fp.L;
|
||||
fingerprint.window_outer_height = fp.S[0];
|
||||
fingerprint.window_outer_width = fp.S[1];
|
||||
fingerprint.window_inner_height = fp.S[0];
|
||||
fingerprint.window_inner_width = fp.S[1];
|
||||
fingerprint.screen_pixel_depth = fp.D;
|
||||
fingerprint.browser_detection_firefox = !!ua.match(/Firefox\/\d+/);
|
||||
fingerprint.browser_detection_brave = !!ua.match(/Brave\/\d+/);
|
||||
fingerprint.media_query_dark_mode = Math.random() > 0.9;
|
||||
fingerprint.webgl_hash_webgl = (0, murmur_1.default)(Object.entries(fingerprint).filter(([k, v]) => k.startsWith("webgl_") && k != "webgl_hash_webgl").map(([k, v]) => v).join(","), 0);
|
||||
fingerprint.client_config__language = opts.language || null;
|
||||
fingerprint.window__location_href = `${opts.surl}/v2/1.5.5/enforcement.fbfc14b0d793c6ef8359e0e4b4a91f67.html#${opts.pkey}`;
|
||||
if (opts.site) {
|
||||
fingerprint.document__referrer = opts.site;
|
||||
fingerprint.window__ancestor_origins = [opts.site];
|
||||
fingerprint.client_config__sitedata_location_href = opts.site;
|
||||
}
|
||||
fingerprint.client_config__surl = opts.surl || "https://client-api.arkoselabs.com";
|
||||
fingerprint.audio_fingerprint = (124.04347527516074 + Math.random() * 0.001 - 0.0005).toString();
|
||||
return Object.entries(fingerprint).map(([k, v]) => ({ key: k, value: v }));
|
||||
}
|
||||
exports.default = {
|
||||
getFingerprint,
|
||||
prepareF,
|
||||
prepareFe,
|
||||
getEnhancedFingerprint,
|
||||
};
|
|
@ -0,0 +1,7 @@
|
|||
/// <reference types="node" />
|
||||
import { RequestOptions } from "undici/types/dispatcher";
|
||||
declare function req(url: string, options: RequestOptions, proxy?: string): Promise<{
|
||||
headers: import("undici/types/header").IncomingHttpHeaders;
|
||||
body: Buffer;
|
||||
}>;
|
||||
export default req;
|
|
@ -0,0 +1,25 @@
|
|||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const undici_1 = require("undici");
|
||||
async function req(url, options, proxy) {
|
||||
let auth = undefined;
|
||||
if (proxy) {
|
||||
let proxyUrl = new URL(proxy);
|
||||
if (proxyUrl.username && proxyUrl.password) {
|
||||
auth = Buffer.from(proxyUrl.username + ":" + proxyUrl.password).toString("base64");
|
||||
}
|
||||
}
|
||||
let dispatcher = proxy ? new undici_1.ProxyAgent({
|
||||
uri: proxy,
|
||||
auth
|
||||
}) : undefined;
|
||||
let req = await (0, undici_1.request)(url, {
|
||||
...options,
|
||||
dispatcher,
|
||||
});
|
||||
return {
|
||||
headers: req.headers,
|
||||
body: Buffer.from(await req.body.arrayBuffer()),
|
||||
};
|
||||
}
|
||||
exports.default = req;
|
|
@ -0,0 +1,2 @@
|
|||
export * from "./api";
|
||||
export * from "./session";
|
|
@ -0,0 +1,18 @@
|
|||
"use strict";
|
||||
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
var desc = Object.getOwnPropertyDescriptor(m, k);
|
||||
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
||||
desc = { enumerable: true, get: function() { return m[k]; } };
|
||||
}
|
||||
Object.defineProperty(o, k2, desc);
|
||||
}) : (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
o[k2] = m[k];
|
||||
}));
|
||||
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
||||
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
__exportStar(require("./api"), exports);
|
||||
__exportStar(require("./session"), exports);
|
|
@ -0,0 +1,2 @@
|
|||
declare var x64hash128: (t: any, r: any) => string;
|
||||
export default x64hash128;
|
|
@ -0,0 +1,186 @@
|
|||
"use strict";
|
||||
// MurmurHash3 related functions
|
||||
// https://github.com/markogresak/fingerprintjs2/blob/master/src/x64hash128.js
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
// Given two 64bit ints (as an array of two 32bit ints) returns the two
|
||||
// added together as a 64bit int (as an array of two 32bit ints).
|
||||
var x64Add = function (t, r) {
|
||||
(t = [t[0] >>> 16, 65535 & t[0], t[1] >>> 16, 65535 & t[1]]),
|
||||
(r = [r[0] >>> 16, 65535 & r[0], r[1] >>> 16, 65535 & r[1]]);
|
||||
var e = [0, 0, 0, 0];
|
||||
return ((e[3] += t[3] + r[3]),
|
||||
(e[2] += e[3] >>> 16),
|
||||
(e[3] &= 65535),
|
||||
(e[2] += t[2] + r[2]),
|
||||
(e[1] += e[2] >>> 16),
|
||||
(e[2] &= 65535),
|
||||
(e[1] += t[1] + r[1]),
|
||||
(e[0] += e[1] >>> 16),
|
||||
(e[1] &= 65535),
|
||||
(e[0] += t[0] + r[0]),
|
||||
(e[0] &= 65535),
|
||||
[(e[0] << 16) | e[1], (e[2] << 16) | e[3]]);
|
||||
},
|
||||
// Given two 64bit ints (as an array of two 32bit ints) returns the two
|
||||
// multiplied together as a 64bit int (as an array of two 32bit ints).
|
||||
x64Multiply = function (t, r) {
|
||||
(t = [t[0] >>> 16, 65535 & t[0], t[1] >>> 16, 65535 & t[1]]),
|
||||
(r = [r[0] >>> 16, 65535 & r[0], r[1] >>> 16, 65535 & r[1]]);
|
||||
var e = [0, 0, 0, 0];
|
||||
return ((e[3] += t[3] * r[3]),
|
||||
(e[2] += e[3] >>> 16),
|
||||
(e[3] &= 65535),
|
||||
(e[2] += t[2] * r[3]),
|
||||
(e[1] += e[2] >>> 16),
|
||||
(e[2] &= 65535),
|
||||
(e[2] += t[3] * r[2]),
|
||||
(e[1] += e[2] >>> 16),
|
||||
(e[2] &= 65535),
|
||||
(e[1] += t[1] * r[3]),
|
||||
(e[0] += e[1] >>> 16),
|
||||
(e[1] &= 65535),
|
||||
(e[1] += t[2] * r[2]),
|
||||
(e[0] += e[1] >>> 16),
|
||||
(e[1] &= 65535),
|
||||
(e[1] += t[3] * r[1]),
|
||||
(e[0] += e[1] >>> 16),
|
||||
(e[1] &= 65535),
|
||||
(e[0] += t[0] * r[3] + t[1] * r[2] + t[2] * r[1] + t[3] * r[0]),
|
||||
(e[0] &= 65535),
|
||||
[(e[0] << 16) | e[1], (e[2] << 16) | e[3]]);
|
||||
},
|
||||
// Given a 64bit int (as an array of two 32bit ints) and an int
|
||||
// representing a number of bit positions, returns the 64bit int (as an
|
||||
// array of two 32bit ints) rotated left by that number of positions.
|
||||
x64Rotl = function (t, r) {
|
||||
return 32 === (r %= 64)
|
||||
? [t[1], t[0]]
|
||||
: r < 32
|
||||
? [
|
||||
(t[0] << r) | (t[1] >>> (32 - r)),
|
||||
(t[1] << r) | (t[0] >>> (32 - r)),
|
||||
]
|
||||
: ((r -= 32),
|
||||
[
|
||||
(t[1] << r) | (t[0] >>> (32 - r)),
|
||||
(t[0] << r) | (t[1] >>> (32 - r)),
|
||||
]);
|
||||
},
|
||||
// Given a 64bit int (as an array of two 32bit ints) and an int
|
||||
// representing a number of bit positions, returns the 64bit int (as an
|
||||
// array of two 32bit ints) shifted left by that number of positions.
|
||||
x64LeftShift = function (t, r) {
|
||||
return 0 === (r %= 64)
|
||||
? t
|
||||
: r < 32
|
||||
? [(t[0] << r) | (t[1] >>> (32 - r)), t[1] << r]
|
||||
: [t[1] << (r - 32), 0];
|
||||
},
|
||||
// Given two 64bit ints (as an array of two 32bit ints) returns the two
|
||||
// xored together as a 64bit int (as an array of two 32bit ints).
|
||||
x64Xor = function (t, r) {
|
||||
return [t[0] ^ r[0], t[1] ^ r[1]];
|
||||
},
|
||||
// Given a block, returns murmurHash3's final x64 mix of that block.
|
||||
// (`[0, h[0] >>> 1]` is a 33 bit unsigned right shift. This is the
|
||||
// only place where we need to right shift 64bit ints.)
|
||||
x64Fmix = function (t) {
|
||||
return ((t = x64Xor(t, [0, t[0] >>> 1])),
|
||||
(t = x64Multiply(t, [4283543511, 3981806797])),
|
||||
(t = x64Xor(t, [0, t[0] >>> 1])),
|
||||
(t = x64Multiply(t, [3301882366, 444984403])),
|
||||
(t = x64Xor(t, [0, t[0] >>> 1])));
|
||||
},
|
||||
// Given a string and an optional seed as an int, returns a 128 bit
|
||||
// hash using the x64 flavor of MurmurHash3, as an unsigned hex.
|
||||
x64hash128 = function (t, r) {
|
||||
r = r || 0;
|
||||
for (var e = (t = t || "").length % 16, o = t.length - e, x = [0, r], c = [0, r], h = [0, 0], a = [0, 0], d = [2277735313, 289559509], i = [1291169091, 658871167], l = 0; l < o; l += 16)
|
||||
(h = [
|
||||
(255 & t.charCodeAt(l + 4)) |
|
||||
((255 & t.charCodeAt(l + 5)) << 8) |
|
||||
((255 & t.charCodeAt(l + 6)) << 16) |
|
||||
((255 & t.charCodeAt(l + 7)) << 24),
|
||||
(255 & t.charCodeAt(l)) |
|
||||
((255 & t.charCodeAt(l + 1)) << 8) |
|
||||
((255 & t.charCodeAt(l + 2)) << 16) |
|
||||
((255 & t.charCodeAt(l + 3)) << 24),
|
||||
]),
|
||||
(a = [
|
||||
(255 & t.charCodeAt(l + 12)) |
|
||||
((255 & t.charCodeAt(l + 13)) << 8) |
|
||||
((255 & t.charCodeAt(l + 14)) << 16) |
|
||||
((255 & t.charCodeAt(l + 15)) << 24),
|
||||
(255 & t.charCodeAt(l + 8)) |
|
||||
((255 & t.charCodeAt(l + 9)) << 8) |
|
||||
((255 & t.charCodeAt(l + 10)) << 16) |
|
||||
((255 & t.charCodeAt(l + 11)) << 24),
|
||||
]),
|
||||
(h = x64Multiply(h, d)),
|
||||
(h = x64Rotl(h, 31)),
|
||||
(h = x64Multiply(h, i)),
|
||||
(x = x64Xor(x, h)),
|
||||
(x = x64Rotl(x, 27)),
|
||||
(x = x64Add(x, c)),
|
||||
(x = x64Add(x64Multiply(x, [0, 5]), [0, 1390208809])),
|
||||
(a = x64Multiply(a, i)),
|
||||
(a = x64Rotl(a, 33)),
|
||||
(a = x64Multiply(a, d)),
|
||||
(c = x64Xor(c, a)),
|
||||
(c = x64Rotl(c, 31)),
|
||||
(c = x64Add(c, x)),
|
||||
(c = x64Add(x64Multiply(c, [0, 5]), [0, 944331445]));
|
||||
switch (((h = [0, 0]), (a = [0, 0]), e)) {
|
||||
case 15:
|
||||
a = x64Xor(a, x64LeftShift([0, t.charCodeAt(l + 14)], 48));
|
||||
case 14:
|
||||
a = x64Xor(a, x64LeftShift([0, t.charCodeAt(l + 13)], 40));
|
||||
case 13:
|
||||
a = x64Xor(a, x64LeftShift([0, t.charCodeAt(l + 12)], 32));
|
||||
case 12:
|
||||
a = x64Xor(a, x64LeftShift([0, t.charCodeAt(l + 11)], 24));
|
||||
case 11:
|
||||
a = x64Xor(a, x64LeftShift([0, t.charCodeAt(l + 10)], 16));
|
||||
case 10:
|
||||
a = x64Xor(a, x64LeftShift([0, t.charCodeAt(l + 9)], 8));
|
||||
case 9:
|
||||
(a = x64Xor(a, [0, t.charCodeAt(l + 8)])),
|
||||
(a = x64Multiply(a, i)),
|
||||
(a = x64Rotl(a, 33)),
|
||||
(a = x64Multiply(a, d)),
|
||||
(c = x64Xor(c, a));
|
||||
case 8:
|
||||
h = x64Xor(h, x64LeftShift([0, t.charCodeAt(l + 7)], 56));
|
||||
case 7:
|
||||
h = x64Xor(h, x64LeftShift([0, t.charCodeAt(l + 6)], 48));
|
||||
case 6:
|
||||
h = x64Xor(h, x64LeftShift([0, t.charCodeAt(l + 5)], 40));
|
||||
case 5:
|
||||
h = x64Xor(h, x64LeftShift([0, t.charCodeAt(l + 4)], 32));
|
||||
case 4:
|
||||
h = x64Xor(h, x64LeftShift([0, t.charCodeAt(l + 3)], 24));
|
||||
case 3:
|
||||
h = x64Xor(h, x64LeftShift([0, t.charCodeAt(l + 2)], 16));
|
||||
case 2:
|
||||
h = x64Xor(h, x64LeftShift([0, t.charCodeAt(l + 1)], 8));
|
||||
case 1:
|
||||
(h = x64Xor(h, [0, t.charCodeAt(l)])),
|
||||
(h = x64Multiply(h, d)),
|
||||
(h = x64Rotl(h, 31)),
|
||||
(h = x64Multiply(h, i)),
|
||||
(x = x64Xor(x, h));
|
||||
}
|
||||
return ((x = x64Xor(x, [0, t.length])),
|
||||
(c = x64Xor(c, [0, t.length])),
|
||||
(x = x64Add(x, c)),
|
||||
(c = x64Add(c, x)),
|
||||
(x = x64Fmix(x)),
|
||||
(c = x64Fmix(c)),
|
||||
(x = x64Add(x, c)),
|
||||
(c = x64Add(c, x)),
|
||||
("00000000" + (x[0] >>> 0).toString(16)).slice(-8) +
|
||||
("00000000" + (x[1] >>> 0).toString(16)).slice(-8) +
|
||||
("00000000" + (c[0] >>> 0).toString(16)).slice(-8) +
|
||||
("00000000" + (c[1] >>> 0).toString(16)).slice(-8));
|
||||
};
|
||||
exports.default = x64hash128;
|
|
@ -0,0 +1,36 @@
|
|||
import { GetTokenResult } from "./api";
|
||||
import { Challenge } from "./challenge";
|
||||
export interface TokenInfo {
|
||||
token: string;
|
||||
r: string;
|
||||
metabgclr: string;
|
||||
mainbgclr: string;
|
||||
guitextcolor: string;
|
||||
metaiconclr: string;
|
||||
meta_height: string;
|
||||
meta_width: string;
|
||||
meta: string;
|
||||
pk: string;
|
||||
dc: string;
|
||||
at: string;
|
||||
cdn_url: string;
|
||||
lurl: string;
|
||||
surl: string;
|
||||
smurl: string;
|
||||
kbio: boolean;
|
||||
mbio: boolean;
|
||||
tbio: boolean;
|
||||
}
|
||||
export interface SessionOptions {
|
||||
userAgent?: string;
|
||||
proxy?: string;
|
||||
}
|
||||
export declare class Session {
|
||||
token: string;
|
||||
tokenInfo: TokenInfo;
|
||||
private userAgent;
|
||||
private proxy;
|
||||
constructor(token: string | GetTokenResult, sessionOptions?: SessionOptions);
|
||||
getChallenge(): Promise<Challenge>;
|
||||
getEmbedUrl(): string;
|
||||
}
|
|
@ -0,0 +1,77 @@
|
|||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.Session = void 0;
|
||||
const challenge_1 = require("./challenge");
|
||||
const http_1 = require("./http");
|
||||
const util_1 = require("./util");
|
||||
let parseToken = (token) => Object.fromEntries(token
|
||||
.split("|")
|
||||
.map((v) => v.split("=").map((v) => decodeURIComponent(v))));
|
||||
class Session {
|
||||
constructor(token, sessionOptions) {
|
||||
var _a;
|
||||
if (typeof token === "string") {
|
||||
this.token = token;
|
||||
}
|
||||
else {
|
||||
this.token = token.token;
|
||||
}
|
||||
if (!this.token.startsWith("token="))
|
||||
this.token = "token=" + this.token;
|
||||
this.tokenInfo = parseToken(this.token);
|
||||
this.tokenInfo.mbio = typeof (token) !== "string" ? (_a = token.mbio) !== null && _a !== void 0 ? _a : false : false;
|
||||
this.userAgent = (sessionOptions === null || sessionOptions === void 0 ? void 0 : sessionOptions.userAgent) || util_1.default.DEFAULT_USER_AGENT;
|
||||
this.proxy = sessionOptions === null || sessionOptions === void 0 ? void 0 : sessionOptions.proxy;
|
||||
}
|
||||
async getChallenge() {
|
||||
let res = await (0, http_1.default)(this.tokenInfo.surl, {
|
||||
path: "/fc/gfct/",
|
||||
method: "POST",
|
||||
body: util_1.default.constructFormData({
|
||||
sid: this.tokenInfo.r,
|
||||
render_type: "canvas",
|
||||
token: this.tokenInfo.token,
|
||||
analytics_tier: this.tokenInfo.at,
|
||||
"data%5Bstatus%5D": "init",
|
||||
lang: "en",
|
||||
apiBreakerVersion: "green"
|
||||
}),
|
||||
headers: {
|
||||
"User-Agent": this.userAgent,
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
"Accept-Language": "en-US,en;q=0.9",
|
||||
"Sec-Fetch-Site": "same-origin",
|
||||
"Referer": this.getEmbedUrl()
|
||||
},
|
||||
}, this.proxy);
|
||||
let data = JSON.parse(res.body.toString());
|
||||
data.token = this.token;
|
||||
data.tokenInfo = this.tokenInfo;
|
||||
if (data.game_data.gameType == 1) {
|
||||
return new challenge_1.Challenge1(data, {
|
||||
proxy: this.proxy,
|
||||
userAgent: this.userAgent,
|
||||
});
|
||||
}
|
||||
else if (data.game_data.gameType == 3) {
|
||||
return new challenge_1.Challenge3(data, {
|
||||
proxy: this.proxy,
|
||||
userAgent: this.userAgent,
|
||||
});
|
||||
}
|
||||
else if (data.game_data.gameType == 4) {
|
||||
return new challenge_1.Challenge4(data, {
|
||||
proxy: this.proxy,
|
||||
userAgent: this.userAgent,
|
||||
});
|
||||
}
|
||||
else {
|
||||
throw new Error("Unsupported game type: " + data.game_data.gameType);
|
||||
}
|
||||
//return res.body.toString()
|
||||
}
|
||||
getEmbedUrl() {
|
||||
return `${this.tokenInfo.surl}/fc/gc/?${util_1.default.constructFormData(this.tokenInfo)}`;
|
||||
}
|
||||
}
|
||||
exports.Session = Session;
|
|
@ -0,0 +1,190 @@
|
|||
interface TimestampData {
|
||||
cookie: string;
|
||||
value: string;
|
||||
}
|
||||
interface TileLoc {
|
||||
x: number;
|
||||
y: number;
|
||||
px: number;
|
||||
py: number;
|
||||
}
|
||||
declare function tileToLoc(tile: number): TileLoc;
|
||||
declare function constructFormData(data: {}): string;
|
||||
declare function random(): string;
|
||||
declare function getTimestamp(): TimestampData;
|
||||
declare function getBda(userAgent: string, opts: object): string;
|
||||
declare function solveBreaker(v2: boolean, breaker: {
|
||||
value: string[];
|
||||
key: string;
|
||||
} | string, gameType: number, value: object): any;
|
||||
declare const _default: {
|
||||
DEFAULT_USER_AGENT: string;
|
||||
tileToLoc: typeof tileToLoc;
|
||||
constructFormData: typeof constructFormData;
|
||||
getBda: typeof getBda;
|
||||
apiBreakers: {
|
||||
v1: {
|
||||
3: {
|
||||
default: (c: any) => any;
|
||||
method_1: (c: any) => {
|
||||
x: any;
|
||||
y: any;
|
||||
};
|
||||
method_2: (c: any) => {
|
||||
x: any;
|
||||
y: number;
|
||||
};
|
||||
method_3: (c: any) => {
|
||||
a: any;
|
||||
b: any;
|
||||
};
|
||||
method_4: (c: any) => any[];
|
||||
method_5: (c: any) => number[];
|
||||
};
|
||||
4: {
|
||||
default: (c: any) => any;
|
||||
};
|
||||
};
|
||||
v2: {
|
||||
3: {
|
||||
value: {
|
||||
alpha: (c: any) => {
|
||||
x: any;
|
||||
y: number;
|
||||
px: any;
|
||||
py: any;
|
||||
};
|
||||
beta: (c: any) => {
|
||||
x: any;
|
||||
y: any;
|
||||
py: any;
|
||||
px: any;
|
||||
};
|
||||
gamma: (c: any) => {
|
||||
x: any;
|
||||
y: number;
|
||||
px: any;
|
||||
py: any;
|
||||
};
|
||||
delta: (c: any) => {
|
||||
x: any;
|
||||
y: any;
|
||||
px: any;
|
||||
py: any;
|
||||
};
|
||||
epsilon: (c: any) => {
|
||||
x: number;
|
||||
y: number;
|
||||
px: any;
|
||||
py: any;
|
||||
};
|
||||
zeta: (c: any) => {
|
||||
x: any;
|
||||
y: any;
|
||||
px: any;
|
||||
py: any;
|
||||
};
|
||||
method_1: (c: any) => {
|
||||
x: any;
|
||||
y: any;
|
||||
px: any;
|
||||
py: any;
|
||||
};
|
||||
method_2: (c: any) => {
|
||||
x: any;
|
||||
y: number;
|
||||
px: any;
|
||||
py: any;
|
||||
};
|
||||
method_3: (c: any) => {
|
||||
x: number;
|
||||
y: number;
|
||||
px: any;
|
||||
py: any;
|
||||
};
|
||||
};
|
||||
key: {
|
||||
alpha: (c: any) => any[];
|
||||
beta: (c: any) => string;
|
||||
gamma: (c: any) => string;
|
||||
delta: (c: any) => any[];
|
||||
epsilon: (c: any) => {
|
||||
answer: {
|
||||
x: any;
|
||||
y: any;
|
||||
px: any;
|
||||
py: any;
|
||||
};
|
||||
};
|
||||
zeta: (c: any) => any[];
|
||||
method_1: (c: any) => {
|
||||
a: any;
|
||||
b: any;
|
||||
px: any;
|
||||
py: any;
|
||||
};
|
||||
method_2: (c: any) => any[];
|
||||
method_3: (c: any) => any[];
|
||||
};
|
||||
};
|
||||
4: {
|
||||
value: {
|
||||
alpha: (c: any) => {
|
||||
index: number;
|
||||
};
|
||||
beta: (c: any) => {
|
||||
index: number;
|
||||
};
|
||||
gamma: (c: any) => {
|
||||
index: number;
|
||||
};
|
||||
delta: (c: any) => {
|
||||
index: number;
|
||||
};
|
||||
epsilon: (c: any) => {
|
||||
index: number;
|
||||
};
|
||||
zeta: (c: any) => {
|
||||
index: any;
|
||||
};
|
||||
va: (c: any) => {
|
||||
index: any;
|
||||
};
|
||||
vb: (c: any) => {
|
||||
index: number;
|
||||
};
|
||||
vc: (c: any) => {
|
||||
index: number;
|
||||
};
|
||||
vd: (c: any) => {
|
||||
index: number;
|
||||
};
|
||||
};
|
||||
key: {
|
||||
alpha: (c: any) => any[];
|
||||
beta: (c: any) => {
|
||||
size: number;
|
||||
id: any;
|
||||
limit: number;
|
||||
req_timestamp: number;
|
||||
};
|
||||
gamma: (c: any) => any;
|
||||
delta: (c: any) => {
|
||||
index: any;
|
||||
};
|
||||
epsilon: (c: any) => any;
|
||||
zeta: (c: any) => any[];
|
||||
ka: (c: any) => any;
|
||||
kb: (c: any) => any[];
|
||||
kc: (c: any) => {
|
||||
guess: any;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
getTimestamp: typeof getTimestamp;
|
||||
random: typeof random;
|
||||
solveBreaker: typeof solveBreaker;
|
||||
};
|
||||
export default _default;
|
|
@ -0,0 +1,172 @@
|
|||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const fingerprint_1 = require("./fingerprint");
|
||||
const murmur_1 = require("./murmur");
|
||||
const crypt_1 = require("./crypt");
|
||||
const DEFAULT_USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36";
|
||||
let apiBreakers = {
|
||||
v1: {
|
||||
3: {
|
||||
default: (c) => c,
|
||||
method_1: (c) => ({ x: c.y, y: c.x }),
|
||||
method_2: (c) => ({ x: c.x, y: (c.y + c.x) * c.x }),
|
||||
method_3: (c) => ({ a: c.x, b: c.y }),
|
||||
method_4: (c) => [c.x, c.y],
|
||||
method_5: (c) => [c.y, c.x].map((v) => Math.sqrt(v)),
|
||||
},
|
||||
4: {
|
||||
default: (c) => c
|
||||
}
|
||||
},
|
||||
v2: {
|
||||
3: {
|
||||
value: {
|
||||
alpha: (c) => ({ x: c.x, y: (c.y + c.x) * c.x, px: c.px, py: c.py }),
|
||||
beta: (c) => ({ x: c.y, y: c.x, py: c.px, px: c.py }),
|
||||
gamma: (c) => ({ x: c.y + 1, y: -c.x, px: c.px, py: c.py }),
|
||||
delta: (c) => ({ x: c.y + 0.25, y: c.x + 0.5, px: c.px, py: c.py }),
|
||||
epsilon: (c) => ({ x: c.x * 0.5, y: c.y * 5, px: c.px, py: c.py }),
|
||||
zeta: (c) => ({ x: c.x + 1, y: c.y + 2, px: c.px, py: c.py }),
|
||||
method_1: (c) => ({ x: c.x, y: c.y, px: c.px, py: c.py }),
|
||||
method_2: (c) => ({ x: c.y, y: (c.y + c.x) * c.x, px: c.px, py: c.py }),
|
||||
method_3: (c) => ({ x: Math.sqrt(c.x), y: Math.sqrt(c.y), px: c.px, py: c.py }),
|
||||
},
|
||||
key: {
|
||||
alpha: (c) => [c.y, c.px, c.py, c.x],
|
||||
beta: (c) => JSON.stringify({ x: c.x, y: c.y, px: c.px, py: c.py }),
|
||||
gamma: (c) => [c.x, c.y, c.px, c.py].join(" "),
|
||||
delta: (c) => [1, c.x, 2, c.y, 3, c.px, 4, c.py],
|
||||
epsilon: (c) => ({ answer: { x: c.x, y: c.y, px: c.px, py: c.py } }),
|
||||
zeta: (c) => [c.x, [c.y, [c.px, [c.py]]]],
|
||||
method_1: (c) => ({ a: c.x, b: c.y, px: c.px, py: c.py }),
|
||||
method_2: (c) => [c.x, c.y],
|
||||
method_3: (c) => [c.y, c.x],
|
||||
}
|
||||
},
|
||||
4: {
|
||||
value: {
|
||||
// @ts-ignore
|
||||
alpha: (c) => ({ index: String(c.index) + 1 - 2 }),
|
||||
beta: (c) => ({ index: -c.index }),
|
||||
gamma: (c) => ({ index: 3 * (3 - c.index) }),
|
||||
delta: (c) => ({ index: 7 * c.index }),
|
||||
epsilon: (c) => ({ index: 2 * c.index }),
|
||||
zeta: (c) => ({ index: c.index ? 100 / c.index : c.index }),
|
||||
va: (c) => ({ index: c.index + 3 }),
|
||||
vb: (c) => ({ index: -c.index }),
|
||||
vc: (c) => ({ index: 10 - c.index }),
|
||||
vd: (c) => ({ index: 3 * c.index }),
|
||||
},
|
||||
key: {
|
||||
alpha: (c) => [Math.round(100 * Math.random()), c.index, Math.round(100 * Math.random())],
|
||||
beta: (c) => ({ size: 50 - c.index, id: c.index, limit: 10 * c.index, req_timestamp: Date.now() }),
|
||||
gamma: (c) => c.index,
|
||||
delta: (c) => ({ index: c.index }),
|
||||
epsilon: (c) => {
|
||||
const arr = [];
|
||||
const len = Math.round(5 * Math.random()) + 1;
|
||||
const rand = Math.round(Math.random() * len);
|
||||
for (let i = 0; i < len; i++) {
|
||||
arr.push(i === rand ? c.index : Math.round(10 * Math.random()));
|
||||
}
|
||||
arr.push(rand);
|
||||
return arr;
|
||||
},
|
||||
zeta: (c) => Array(Math.round(5 * Math.random()) + 1).concat(c.index),
|
||||
ka: (c) => c.index,
|
||||
kb: (c) => [c.index],
|
||||
kc: (c) => ({ guess: c.index }),
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
function tileToLoc(tile) {
|
||||
let xClick = (tile % 3) * 100 + (tile % 3) * 3 + 3 + 10 + Math.floor(Math.random() * 80);
|
||||
let yClick = Math.floor(tile / 3) * 100 + Math.floor(tile / 3) * 3 + 3 + 10 + Math.floor(Math.random() * 80);
|
||||
return {
|
||||
x: xClick,
|
||||
y: yClick,
|
||||
px: xClick / 300,
|
||||
py: yClick / 200,
|
||||
};
|
||||
}
|
||||
function constructFormData(data) {
|
||||
return Object.keys(data)
|
||||
.filter((v) => data[v] !== undefined)
|
||||
.map((k) => `${k}=${encodeURIComponent(data[k])}`)
|
||||
.join("&");
|
||||
}
|
||||
function random() {
|
||||
return Array(32)
|
||||
.fill(0)
|
||||
.map(() => "0123456789abcdef"[Math.floor(Math.random() * 16)])
|
||||
.join("");
|
||||
}
|
||||
function getTimestamp() {
|
||||
const time = (new Date()).getTime().toString();
|
||||
const value = `${time.substring(0, 7)}00${time.substring(7, 13)}`;
|
||||
return { cookie: `timestamp=${value};path=/;secure;samesite=none`, value };
|
||||
}
|
||||
function getBda(userAgent, opts) {
|
||||
let fp = fingerprint_1.default.getFingerprint();
|
||||
let fe = fingerprint_1.default.prepareFe(fp);
|
||||
let bda = [
|
||||
{ key: "api_type", value: "js" },
|
||||
{ key: "p", value: 1 },
|
||||
{ key: "f", value: (0, murmur_1.default)(fingerprint_1.default.prepareF(fingerprint_1.default), 31) },
|
||||
{
|
||||
key: "n",
|
||||
value: Buffer.from(Math.round(Date.now() / (1000 - 0)).toString()).toString("base64"),
|
||||
},
|
||||
{ key: "wh", value: `${random()}|${random()}` },
|
||||
{
|
||||
"key": "enhanced_fp",
|
||||
"value": fingerprint_1.default.getEnhancedFingerprint(fp, userAgent, opts)
|
||||
},
|
||||
{ key: "fe", value: fe },
|
||||
{ key: "ife_hash", value: (0, murmur_1.default)(fe.join(", "), 38) },
|
||||
{ key: "cs", value: 1 },
|
||||
{
|
||||
key: "jsbd",
|
||||
value: JSON.stringify({
|
||||
HL: 4,
|
||||
DT: "",
|
||||
NWD: "false",
|
||||
DOTO: 1,
|
||||
DMTO: 1,
|
||||
}),
|
||||
},
|
||||
];
|
||||
let time = new Date().getTime() / 1000;
|
||||
let key = userAgent + Math.round(time - (time % 21600));
|
||||
let s = JSON.stringify(bda);
|
||||
let encrypted = crypt_1.default.encrypt(s, key);
|
||||
return Buffer.from(encrypted).toString("base64");
|
||||
}
|
||||
function solveBreaker(v2, breaker = "default", gameType, value) {
|
||||
if (!v2 && typeof breaker === "string")
|
||||
return (apiBreakers.v1[gameType][breaker || "default"] || ((v) => v))(value);
|
||||
if (typeof breaker !== "string") {
|
||||
let b = apiBreakers.v2[gameType];
|
||||
let v = breaker.value.reduce((acc, cur) => {
|
||||
if (b.value[cur])
|
||||
return b.value[cur](acc);
|
||||
else
|
||||
return cur;
|
||||
}, value);
|
||||
return b.key[breaker.key](v);
|
||||
}
|
||||
else {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
exports.default = {
|
||||
DEFAULT_USER_AGENT,
|
||||
tileToLoc,
|
||||
constructFormData,
|
||||
getBda,
|
||||
apiBreakers,
|
||||
getTimestamp,
|
||||
random,
|
||||
solveBreaker
|
||||
};
|
|
@ -0,0 +1,27 @@
|
|||
{
|
||||
"name": "funcaptcha",
|
||||
"version": "1.1.7",
|
||||
"description": "A library used to interact with funcaptchas.",
|
||||
"author": "noahcoolboy",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
"funcaptcha"
|
||||
],
|
||||
"main": "lib/index.js",
|
||||
"types": "lib/index.d.ts",
|
||||
"scripts": {
|
||||
"build": "rimraf lib && tsc",
|
||||
"test": "node test/test.js",
|
||||
"benchmark": "node test/benchmark.js",
|
||||
"roblox": "node test/roblox.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"undici": "^5.22.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^17.0.42",
|
||||
"rimraf": "^3.0.2",
|
||||
"typescript": "^4.7.4"
|
||||
},
|
||||
"repository": "github:noahcoolboy/funcaptcha"
|
||||
}
|
|
@ -0,0 +1,85 @@
|
|||
import request from "./http";
|
||||
import util from "./util";
|
||||
|
||||
export interface GetTokenOptions {
|
||||
pkey: string;
|
||||
// Service URL
|
||||
surl?: string;
|
||||
data?: { [key: string]: string };
|
||||
headers?: { [key: string]: string };
|
||||
site?: string;
|
||||
// Page URL
|
||||
location?: string;
|
||||
proxy?: string;
|
||||
language?: string;
|
||||
}
|
||||
|
||||
export interface GetTokenResult {
|
||||
challenge_url: string;
|
||||
challenge_url_cdn: string;
|
||||
challenge_url_cdn_sri: string;
|
||||
disable_default_styling: boolean | null;
|
||||
iframe_height: number | null;
|
||||
iframe_width: number | null;
|
||||
// Enable keyboard biometrics
|
||||
kbio: boolean;
|
||||
// Enable mouse biometrics
|
||||
mbio: boolean;
|
||||
noscript: string;
|
||||
// Enable touch biometrics
|
||||
tbio: boolean;
|
||||
// The token for the funcaptcha. Can be used 10 times before having to get a new token.
|
||||
token: string;
|
||||
}
|
||||
|
||||
export async function getToken(
|
||||
options: GetTokenOptions
|
||||
): Promise<GetTokenResult> {
|
||||
options = {
|
||||
surl: "https://client-api.arkoselabs.com",
|
||||
data: {},
|
||||
...options,
|
||||
};
|
||||
|
||||
if (!options.headers)
|
||||
options.headers = { "User-Agent": util.DEFAULT_USER_AGENT };
|
||||
else if (!Object.keys(options.headers).map(v => v.toLowerCase()).includes("user-agent"))
|
||||
options.headers["User-Agent"] = util.DEFAULT_USER_AGENT;
|
||||
|
||||
options.headers["Accept-Language"] = "en-US,en;q=0.9";
|
||||
options.headers["Sec-Fetch-Site"] = "same-origin";
|
||||
options.headers["Accept"] = "*/*";
|
||||
options.headers["Content-Type"] = "application/x-www-form-urlencoded; charset=UTF-8";
|
||||
options.headers["sec-fetch-mode"] = "cors"
|
||||
|
||||
if (options.site) {
|
||||
options.headers["Origin"] = options.surl
|
||||
options.headers["Referer"] = `${options.surl}/v2/${options.pkey}/1.5.5/enforcement.fbfc14b0d793c6ef8359e0e4b4a91f67.html`
|
||||
}
|
||||
|
||||
let ua = options.headers[Object.keys(options.headers).find(v => v.toLowerCase() == "user-agent")]
|
||||
|
||||
let res = await request(
|
||||
options.surl,
|
||||
{
|
||||
method: "POST",
|
||||
path: "/fc/gt2/public_key/" + options.pkey,
|
||||
body: util.constructFormData({
|
||||
bda: util.getBda(ua, options),
|
||||
public_key: options.pkey,
|
||||
site: options.site ? new URL(options.site).origin : undefined,
|
||||
userbrowser: ua,
|
||||
capi_version: "1.5.5",
|
||||
capi_mode: "inline",
|
||||
style_theme: "default",
|
||||
rnd: Math.random().toString(),
|
||||
...Object.fromEntries(Object.keys(options.data).map(v => ["data[" + v + "]", options.data[v]])),
|
||||
language: options.language || "en",
|
||||
}),
|
||||
headers: options.headers,
|
||||
},
|
||||
options.proxy
|
||||
);
|
||||
|
||||
return JSON.parse(res.body.toString());
|
||||
}
|
|
@ -0,0 +1,296 @@
|
|||
import request from "./http";
|
||||
import { TokenInfo } from "./session";
|
||||
import util from "./util";
|
||||
import crypt from "./crypt";
|
||||
import { assert } from "console";
|
||||
|
||||
interface ChallengeOptions {
|
||||
userAgent?: string;
|
||||
proxy?: string;
|
||||
}
|
||||
|
||||
interface ChallengeData {
|
||||
token: string;
|
||||
tokenInfo: TokenInfo;
|
||||
session_token: string;
|
||||
challengeID: string;
|
||||
challengeURL: string;
|
||||
game_data: {
|
||||
gameType: number;
|
||||
customGUI: {
|
||||
is_using_api_breaker_v2: boolean;
|
||||
_guiFontColr: string;
|
||||
_challenge_imgs: string[];
|
||||
api_breaker: string;
|
||||
encrypted_mode: number;
|
||||
example_images: {
|
||||
correct: string;
|
||||
incorrect: string;
|
||||
}
|
||||
};
|
||||
waves: number;
|
||||
game_variant?: string; // For gametype 3
|
||||
game_difficulty?: number;
|
||||
puzzle_name?: string; // For gametype 4
|
||||
instruction_string?: string; // For gametype 4
|
||||
};
|
||||
game_sid: string;
|
||||
lang: string;
|
||||
string_table: {
|
||||
[key: string]: string;
|
||||
},
|
||||
string_table_prefixes: string[]
|
||||
}
|
||||
|
||||
interface AnswerResponse {
|
||||
response: "not answered" | "answered";
|
||||
solved?: boolean;
|
||||
incorrect_guess?: number;
|
||||
score?: number;
|
||||
decryption_key?: string;
|
||||
time_end?: number;
|
||||
time_end_seconds?: number;
|
||||
}
|
||||
|
||||
export abstract class Challenge {
|
||||
public data: ChallengeData;
|
||||
public imgs: Promise<Buffer>[];
|
||||
public wave: number = 0;
|
||||
protected key: Promise<string>;
|
||||
protected userAgent: string;
|
||||
protected proxy: string;
|
||||
|
||||
constructor(data: ChallengeData, challengeOptions: ChallengeOptions) {
|
||||
this.data = data;
|
||||
this.userAgent = challengeOptions.userAgent;
|
||||
this.proxy = challengeOptions.proxy;
|
||||
|
||||
// Preload images
|
||||
this.imgs = data.game_data.customGUI._challenge_imgs.map(async (v) => {
|
||||
let req = await request(v, {
|
||||
method: "GET",
|
||||
path: undefined,
|
||||
headers: {
|
||||
"User-Agent": this.userAgent,
|
||||
"Referer": this.data.tokenInfo.surl
|
||||
},
|
||||
});
|
||||
return req.body;
|
||||
});
|
||||
|
||||
if(data.game_data.customGUI.encrypted_mode) {
|
||||
// Preload decryption key
|
||||
this.key = this.getKey();
|
||||
}
|
||||
}
|
||||
|
||||
async getImage(): Promise<Buffer> {
|
||||
let img = await this.imgs[this.wave];
|
||||
try {
|
||||
JSON.parse(img.toString()); // Image is encrypted
|
||||
img = Buffer.from(
|
||||
await crypt.decrypt(img.toString(), await this.getKey()),
|
||||
"base64"
|
||||
);
|
||||
} catch (err) {
|
||||
// Image is not encrypted
|
||||
// All good!
|
||||
}
|
||||
return img;
|
||||
}
|
||||
|
||||
protected async getKey() {
|
||||
if (this.key) return await this.key;
|
||||
let response = await request(
|
||||
this.data.tokenInfo.surl,
|
||||
{
|
||||
method: "POST",
|
||||
path: "/fc/ekey/",
|
||||
headers: {
|
||||
"User-Agent": this.userAgent,
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
"Referer": this.data.tokenInfo.surl,
|
||||
},
|
||||
body: util.constructFormData({
|
||||
session_token: this.data.session_token,
|
||||
game_token: this.data.challengeID,
|
||||
}),
|
||||
},
|
||||
this.proxy
|
||||
);
|
||||
this.key = JSON.parse(response.body.toString()).decryption_key;
|
||||
return this.key;
|
||||
}
|
||||
|
||||
abstract answer(answer: number): Promise<AnswerResponse>;
|
||||
|
||||
get gameType() {
|
||||
return this.data.game_data.gameType;
|
||||
}
|
||||
|
||||
get variant() {
|
||||
return this.data.game_data.game_variant || this.data.game_data.instruction_string;
|
||||
}
|
||||
|
||||
get instruction() {
|
||||
return this.data.string_table[`${this.data.game_data.gameType}.instructions-${this.variant}`] || this.data.string_table[`${this.data.game_data.gameType}.touch_done_info${this.data.game_data.game_variant ? `_${this.data.game_data.game_variant}` : ""}`];
|
||||
}
|
||||
|
||||
get waves() {
|
||||
return this.data.game_data.waves;
|
||||
}
|
||||
}
|
||||
|
||||
export class Challenge1 extends Challenge {
|
||||
private answerHistory = [];
|
||||
public increment;
|
||||
|
||||
constructor(data: ChallengeData, challengeOptions: ChallengeOptions) {
|
||||
super(data, challengeOptions);
|
||||
|
||||
// But WHY?!
|
||||
let clr = data.game_data.customGUI._guiFontColr
|
||||
this.increment = parseInt(clr ? clr.replace("#", "").substring(3) : "28", 16)
|
||||
this.increment = this.increment > 113 ? this.increment / 10 : this.increment
|
||||
}
|
||||
|
||||
private round(num: number): string {
|
||||
return (Math.round(num * 10) / 10).toFixed(2);
|
||||
}
|
||||
|
||||
async answer(answer: number): Promise<AnswerResponse> {
|
||||
if(answer >= 0 && answer <= Math.round(360 / 51.4) - 1)
|
||||
this.answerHistory.push(this.round(answer * this.increment));
|
||||
else
|
||||
this.answerHistory.push(this.round(answer))
|
||||
|
||||
let encrypted = await crypt.encrypt(
|
||||
this.answerHistory.toString(),
|
||||
this.data.session_token
|
||||
);
|
||||
let req = await request(
|
||||
this.data.tokenInfo.surl,
|
||||
{
|
||||
method: "POST",
|
||||
path: "/fc/ca/",
|
||||
headers: {
|
||||
"User-Agent": this.userAgent,
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
"Referer": this.data.challengeURL
|
||||
},
|
||||
body: util.constructFormData({
|
||||
session_token: this.data.session_token,
|
||||
game_token: this.data.challengeID,
|
||||
guess: encrypted,
|
||||
}),
|
||||
},
|
||||
this.proxy
|
||||
);
|
||||
let reqData = JSON.parse(req.body.toString());
|
||||
this.key = reqData.decryption_key || "";
|
||||
this.wave++;
|
||||
return reqData;
|
||||
}
|
||||
}
|
||||
|
||||
export class Challenge3 extends Challenge {
|
||||
private answerHistory = [];
|
||||
|
||||
constructor(data: ChallengeData, challengeOptions: ChallengeOptions) {
|
||||
super(data, challengeOptions);
|
||||
}
|
||||
|
||||
async answer(tile: number): Promise<AnswerResponse> {
|
||||
assert(tile >= 0 && tile <= 5, "Tile must be between 0 and 5");
|
||||
|
||||
let pos = util.tileToLoc(tile);
|
||||
this.answerHistory.push(util.solveBreaker(!!this.data.game_data.customGUI.is_using_api_breaker_v2, this.data.game_data.customGUI.api_breaker, 3, pos))
|
||||
|
||||
let encrypted = await crypt.encrypt(
|
||||
JSON.stringify(this.answerHistory),
|
||||
this.data.session_token
|
||||
);
|
||||
let requestedId = await crypt.encrypt(JSON.stringify({}), `REQUESTED${this.data.session_token}ID`);
|
||||
let { cookie: tCookie, value: tValue } = util.getTimestamp();
|
||||
let req = await request(
|
||||
this.data.tokenInfo.surl,
|
||||
{
|
||||
method: "POST",
|
||||
path: "/fc/ca/",
|
||||
headers: {
|
||||
"User-Agent": this.userAgent,
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
"X-Newrelic-Timestamp": tValue,
|
||||
"X-Requested-ID": requestedId,
|
||||
"Cookie": tCookie,
|
||||
"Referer": this.data.challengeURL
|
||||
},
|
||||
body: util.constructFormData({
|
||||
session_token: this.data.session_token,
|
||||
game_token: this.data.challengeID,
|
||||
guess: encrypted,
|
||||
analytics_tier: this.data.tokenInfo.at,
|
||||
sid: this.data.tokenInfo.r,
|
||||
bio: this.data.tokenInfo.mbio && "eyJtYmlvIjoiMTI1MCwwLDE0NywyMDQ7MTg5NCwwLDE1MSwyMDA7MTk2MCwxLDE1MiwxOTk7MjAyOSwyLDE1MiwxOTk7MjU3NSwwLDE1NSwxOTU7MjU4NSwwLDE1NiwxOTA7MjU5NSwwLDE1OCwxODU7MjYwNCwwLDE1OSwxODA7MjYxMywwLDE2MCwxNzU7MjYyMSwwLDE2MSwxNzA7MjYzMCwwLDE2MywxNjU7MjY0MCwwLDE2NCwxNjA7MjY1MCwwLDE2NSwxNTU7MjY2NCwwLDE2NiwxNTA7MjY3NywwLDE2NiwxNDQ7MjY5NCwwLDE2NywxMzk7MjcyMCwwLDE2NywxMzM7Mjc1NCwwLDE2NywxMjc7Mjc4MywwLDE2NywxMjE7MjgxMiwwLDE2NywxMTU7Mjg0MywwLDE2NywxMDk7Mjg2MywwLDE2NywxMDM7Mjg3NSwwLDE2Niw5ODsyOTA1LDAsMTY1LDkzOzMyMzIsMCwxNjUsOTk7MzI2MiwwLDE2NSwxMDU7MzI5OSwwLDE2NCwxMTA7MzM0MCwwLDE2MSwxMTU7MzM3MiwwLDE1NywxMjA7MzM5NSwwLDE1MywxMjQ7MzQwOCwwLDE0OCwxMjc7MzQyMCwwLDE0MywxMzA7MzQyOSwwLDEzOCwxMzE7MzQ0MSwwLDEzMywxMzQ7MzQ1MCwwLDEyOCwxMzU7MzQ2MSwwLDEyMywxMzg7MzQ3NiwwLDExOCwxNDA7MzQ4OSwwLDExMywxNDI7MzUwMywwLDEwOCwxNDM7MzUxOCwwLDEwMywxNDQ7MzUzNCwwLDk4LDE0NTszNTU2LDAsOTMsMTQ2OzM2MTUsMCw4OCwxNDg7MzY2MiwwLDgzLDE1MTszNjgzLDAsNzgsMTU0OzM3MDEsMCw3MywxNTc7MzcyNSwwLDY5LDE2MTszNzkzLDEsNjgsMTYyOzM4NTEsMiw2OCwxNjI7IiwidGJpbyI6IiIsImtiaW8iOiIifQ=="
|
||||
}),
|
||||
},
|
||||
this.proxy
|
||||
);
|
||||
let reqData = JSON.parse(req.body.toString());
|
||||
this.key = reqData.decryption_key || "";
|
||||
this.wave++;
|
||||
return reqData;
|
||||
}
|
||||
}
|
||||
|
||||
export class Challenge4 extends Challenge {
|
||||
private answerHistory = [];
|
||||
|
||||
constructor(data: ChallengeData, challengeOptions: ChallengeOptions) {
|
||||
super(data, challengeOptions);
|
||||
}
|
||||
|
||||
async answer(index: number): Promise<AnswerResponse> {
|
||||
assert(index >= 0 && index <= this.data.game_data.game_difficulty - 1, "Index must be between 0 and " + (this.data.game_data.game_difficulty - 1));
|
||||
this.answerHistory.push(util.solveBreaker(!!this.data.game_data.customGUI.is_using_api_breaker_v2, this.data.game_data.customGUI.api_breaker, 4, { index }))
|
||||
|
||||
let encrypted = await crypt.encrypt(
|
||||
JSON.stringify(this.answerHistory),
|
||||
this.data.session_token
|
||||
);
|
||||
let requestedId = await crypt.encrypt(JSON.stringify({}), `REQUESTED${this.data.session_token}ID`);
|
||||
let { cookie: tCookie, value: tValue } = util.getTimestamp();
|
||||
let req = await request(
|
||||
this.data.tokenInfo.surl,
|
||||
{
|
||||
method: "POST",
|
||||
path: "/fc/ca/",
|
||||
headers: {
|
||||
"User-Agent": this.userAgent,
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
"X-Newrelic-Timestamp": tValue,
|
||||
"X-Requested-ID": requestedId,
|
||||
"Cookie": tCookie,
|
||||
"Referer": this.data.challengeURL
|
||||
},
|
||||
body: util.constructFormData({
|
||||
session_token: this.data.session_token,
|
||||
game_token: this.data.challengeID,
|
||||
guess: encrypted,
|
||||
analytics_tier: this.data.tokenInfo.at,
|
||||
sid: this.data.tokenInfo.r,
|
||||
bio: this.data.tokenInfo.mbio && "eyJtYmlvIjoiMTI1MCwwLDE0NywyMDQ7MTg5NCwwLDE1MSwyMDA7MTk2MCwxLDE1MiwxOTk7MjAyOSwyLDE1MiwxOTk7MjU3NSwwLDE1NSwxOTU7MjU4NSwwLDE1NiwxOTA7MjU5NSwwLDE1OCwxODU7MjYwNCwwLDE1OSwxODA7MjYxMywwLDE2MCwxNzU7MjYyMSwwLDE2MSwxNzA7MjYzMCwwLDE2MywxNjU7MjY0MCwwLDE2NCwxNjA7MjY1MCwwLDE2NSwxNTU7MjY2NCwwLDE2NiwxNTA7MjY3NywwLDE2NiwxNDQ7MjY5NCwwLDE2NywxMzk7MjcyMCwwLDE2NywxMzM7Mjc1NCwwLDE2NywxMjc7Mjc4MywwLDE2NywxMjE7MjgxMiwwLDE2NywxMTU7Mjg0MywwLDE2NywxMDk7Mjg2MywwLDE2NywxMDM7Mjg3NSwwLDE2Niw5ODsyOTA1LDAsMTY1LDkzOzMyMzIsMCwxNjUsOTk7MzI2MiwwLDE2NSwxMDU7MzI5OSwwLDE2NCwxMTA7MzM0MCwwLDE2MSwxMTU7MzM3MiwwLDE1NywxMjA7MzM5NSwwLDE1MywxMjQ7MzQwOCwwLDE0OCwxMjc7MzQyMCwwLDE0MywxMzA7MzQyOSwwLDEzOCwxMzE7MzQ0MSwwLDEzMywxMzQ7MzQ1MCwwLDEyOCwxMzU7MzQ2MSwwLDEyMywxMzg7MzQ3NiwwLDExOCwxNDA7MzQ4OSwwLDExMywxNDI7MzUwMywwLDEwOCwxNDM7MzUxOCwwLDEwMywxNDQ7MzUzNCwwLDk4LDE0NTszNTU2LDAsOTMsMTQ2OzM2MTUsMCw4OCwxNDg7MzY2MiwwLDgzLDE1MTszNjgzLDAsNzgsMTU0OzM3MDEsMCw3MywxNTc7MzcyNSwwLDY5LDE2MTszNzkzLDEsNjgsMTYyOzM4NTEsMiw2OCwxNjI7IiwidGJpbyI6IiIsImtiaW8iOiIifQ=="
|
||||
}),
|
||||
},
|
||||
this.proxy
|
||||
);
|
||||
let reqData = JSON.parse(req.body.toString());
|
||||
this.key = reqData.decryption_key || "";
|
||||
this.wave++;
|
||||
return reqData;
|
||||
}
|
||||
|
||||
get difficulty(): number {
|
||||
return this.data.game_data.game_difficulty;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,78 @@
|
|||
import { createHash, createCipheriv, createDecipheriv } from "crypto";
|
||||
|
||||
interface EncryptionData {
|
||||
ct: string;
|
||||
iv: string;
|
||||
s: string;
|
||||
}
|
||||
|
||||
function encrypt(data: string, key: string): string {
|
||||
let salt = "";
|
||||
let salted = "";
|
||||
let dx = Buffer.alloc(0);
|
||||
|
||||
// Generate salt, as 8 random lowercase letters
|
||||
salt = String.fromCharCode(...Array(8).fill(0).map(_ => Math.floor(Math.random() * 26) + 97))
|
||||
|
||||
// Our final key and iv come from the key and salt being repeatedly hashed
|
||||
// dx = md5(md5(md5(key + salt) + key + salt) + key + salt)
|
||||
// For each round of hashing, we append the result to salted, resulting in a 96 character string
|
||||
// The first 64 characters are the key, and the last 32 are the iv
|
||||
for (let x = 0; x < 3; x++) {
|
||||
dx = createHash("md5")
|
||||
.update(
|
||||
Buffer.concat([
|
||||
Buffer.from(dx),
|
||||
Buffer.from(key),
|
||||
Buffer.from(salt),
|
||||
])
|
||||
)
|
||||
.digest();
|
||||
|
||||
salted += dx.toString("hex");
|
||||
}
|
||||
|
||||
let aes = createCipheriv(
|
||||
"aes-256-cbc",
|
||||
Buffer.from(salted.substring(0, 64), "hex"), // Key
|
||||
Buffer.from(salted.substring(64, 64 + 32), "hex") // IV
|
||||
);
|
||||
|
||||
return JSON.stringify({
|
||||
ct: aes.update(data, null, "base64") + aes.final("base64"),
|
||||
iv: salted.substring(64, 64 + 32),
|
||||
s: Buffer.from(salt).toString("hex"),
|
||||
});
|
||||
}
|
||||
|
||||
function decrypt(rawData: string, key: string): string {
|
||||
let data: EncryptionData = JSON.parse(rawData);
|
||||
|
||||
// We get our decryption key by doing the inverse of the encryption process
|
||||
let dk = Buffer.concat([Buffer.from(key), Buffer.from(data.s, "hex")]);
|
||||
let arr = [Buffer.from(createHash("md5").update(dk).digest()).toString("hex")];
|
||||
let result = arr[0];
|
||||
|
||||
for (let x = 1; x < 3; x++) {
|
||||
arr.push(
|
||||
Buffer.from(
|
||||
createHash("md5")
|
||||
.update(Buffer.concat([Buffer.from(arr[x - 1], "hex"), dk]))
|
||||
.digest()
|
||||
).toString("hex")
|
||||
);
|
||||
result += arr[x];
|
||||
}
|
||||
|
||||
let aes = createDecipheriv(
|
||||
"aes-256-cbc",
|
||||
Buffer.from(result.substring(0, 64), "hex"),
|
||||
Buffer.from(data.iv, "hex")
|
||||
);
|
||||
return aes.update(data.ct, "base64", "utf8") + aes.final("utf8");
|
||||
}
|
||||
|
||||
export default {
|
||||
encrypt,
|
||||
decrypt,
|
||||
};
|
|
@ -0,0 +1,321 @@
|
|||
import x64hash128 from "./murmur";
|
||||
import { randomBytes } from "crypto";
|
||||
|
||||
const baseFingerprint = {
|
||||
DNT: "unknown", // Do not track On/Off | Previous Value: 1
|
||||
L: "en-US", // Browser language
|
||||
D: 24, // Screen color depth (in bits)
|
||||
PR: 1, // Pixel ratio
|
||||
S: [1920, 1200], // Screen resolution
|
||||
AS: [1920, 1200], // Available screen resolution
|
||||
TO: 9999, // Timezone offset
|
||||
SS: true, // Screen orientation (landscape/portrait)
|
||||
LS: true, // Local storage available
|
||||
IDB: true, // IndexedDB available
|
||||
B: false, // addBehaviour support
|
||||
ODB: true, // OpenDatabase support
|
||||
CPUC: "unknown", // CPU Class
|
||||
PK: "Win32", // Platform
|
||||
CFP: `canvas winding:yes~canvas fp:data:image/png;base64,${Buffer.from(
|
||||
Math.random().toString()
|
||||
).toString("base64")}`, // Canvas fingerprint (if canvas is supported)
|
||||
FR: false, // Fake screen resolution?
|
||||
FOS: false, // Fake OS?
|
||||
FB: false, // Fake Browser?
|
||||
JSF: [
|
||||
"Andale Mono",
|
||||
"Arial",
|
||||
"Arial Black",
|
||||
"Arial Hebrew",
|
||||
"Arial MT",
|
||||
"Arial Narrow",
|
||||
"Arial Rounded MT Bold",
|
||||
"Arial Unicode MS",
|
||||
"Bitstream Vera Sans Mono",
|
||||
"Book Antiqua",
|
||||
"Bookman Old Style",
|
||||
"Calibri",
|
||||
"Cambria",
|
||||
"Cambria Math",
|
||||
"Century",
|
||||
"Century Gothic",
|
||||
"Century Schoolbook",
|
||||
"Comic Sans",
|
||||
"Comic Sans MS",
|
||||
"Consolas",
|
||||
"Courier",
|
||||
"Courier New",
|
||||
"Garamond",
|
||||
"Geneva",
|
||||
"Georgia",
|
||||
"Helvetica",
|
||||
"Helvetica Neue",
|
||||
"Impact",
|
||||
"Lucida Bright",
|
||||
"Lucida Calligraphy",
|
||||
"Lucida Console",
|
||||
"Lucida Fax",
|
||||
"LUCIDA GRANDE",
|
||||
"Lucida Handwriting",
|
||||
"Lucida Sans",
|
||||
"Lucida Sans Typewriter",
|
||||
"Lucida Sans Unicode",
|
||||
"Microsoft Sans Serif",
|
||||
"Monaco",
|
||||
"Monotype Corsiva",
|
||||
"MS Gothic",
|
||||
"MS Outlook",
|
||||
"MS PGothic",
|
||||
"MS Reference Sans Serif",
|
||||
"MS Sans Serif",
|
||||
"MS Serif",
|
||||
"MYRIAD",
|
||||
"MYRIAD PRO",
|
||||
"Palatino",
|
||||
"Palatino Linotype",
|
||||
"Segoe Print",
|
||||
"Segoe Script",
|
||||
"Segoe UI",
|
||||
"Segoe UI Light",
|
||||
"Segoe UI Semibold",
|
||||
"Segoe UI Symbol",
|
||||
"Tahoma",
|
||||
"Times",
|
||||
"Times New Roman",
|
||||
"Times New Roman PS",
|
||||
"Trebuchet MS",
|
||||
"Verdana",
|
||||
"Wingdings",
|
||||
"Wingdings 2",
|
||||
"Wingdings 3",
|
||||
], // Available fonts
|
||||
P: [
|
||||
"Chrome PDF Plugin::Portable Document Format::application/x-google-chrome-pdf~pdf",
|
||||
"Chrome PDF Viewer::::application/pdf~pdf",
|
||||
"Native Client::::application/x-nacl~,application/x-pnacl~",
|
||||
], // Plugins
|
||||
T: [0, false, false], // Touch screen (maxTouchPoints, TouchEvent event listener support, ontouchstart support)
|
||||
H: 24, // Cpu threads
|
||||
SWF: false, // Flash support
|
||||
};
|
||||
|
||||
const languages = [
|
||||
"af", "af-ZA", "ar", "ar-AE", "ar-BH", "ar-DZ", "ar-EG", "ar-IQ", "ar-JO", "ar-KW", "ar-LB", "ar-LY", "ar-MA", "ar-OM", "ar-QA", "ar-SA",
|
||||
"ar-SY", "ar-TN", "ar-YE", "az", "az-AZ", "az-AZ", "be", "be-BY", "bg", "bg-BG", "bs-BA", "ca", "ca-ES", "cs", "cs-CZ", "cy",
|
||||
"cy-GB", "da", "da-DK", "de", "de-AT", "de-CH", "de-DE", "de-LI", "de-LU", "dv", "dv-MV", "el", "el-GR", "en", "en-AU", "en-BZ",
|
||||
"en-CA", "en-CB", "en-GB", "en-IE", "en-JM", "en-NZ", "en-PH", "en-TT", "en-US", "en-ZA", "en-ZW", "eo", "es", "es-AR", "es-BO", "es-CL",
|
||||
"es-CO", "es-CR", "es-DO", "es-EC", "es-ES", "es-ES", "es-GT", "es-HN", "es-MX", "es-NI", "es-PA", "es-PE", "es-PR", "es-PY", "es-SV", "es-UY",
|
||||
"es-VE", "et", "et-EE", "eu", "eu-ES", "fa", "fa-IR", "fi", "fi-FI", "fo", "fo-FO", "fr", "fr-BE", "fr-CA", "fr-CH", "fr-FR",
|
||||
"fr-LU", "fr-MC", "gl", "gl-ES", "gu", "gu-IN", "he", "he-IL", "hi", "hi-IN", "hr", "hr-BA", "hr-HR", "hu", "hu-HU", "hy",
|
||||
"hy-AM", "id", "id-ID", "is", "is-IS", "it", "it-CH", "it-IT", "ja", "ja-JP", "ka", "ka-GE", "kk", "kk-KZ", "kn", "kn-IN",
|
||||
"ko", "ko-KR", "kok", "kok-IN", "ky", "ky-KG", "lt", "lt-LT", "lv", "lv-LV", "mi", "mi-NZ", "mk", "mk-MK", "mn", "mn-MN",
|
||||
"mr", "mr-IN", "ms", "ms-BN", "ms-MY", "mt", "mt-MT", "nb", "nb-NO", "nl", "nl-BE", "nl-NL", "nn-NO", "ns", "ns-ZA", "pa",
|
||||
"pa-IN", "pl", "pl-PL", "ps", "ps-AR", "pt", "pt-BR", "pt-PT", "qu", "qu-BO", "qu-EC", "qu-PE", "ro", "ro-RO", "ru", "ru-RU",
|
||||
"sa", "sa-IN", "se", "se-FI", "se-FI", "se-FI", "se-NO", "se-NO", "se-NO", "se-SE", "se-SE", "se-SE", "sk", "sk-SK", "sl", "sl-SI",
|
||||
"sq", "sq-AL", "sr-BA", "sr-BA", "sr-SP", "sr-SP", "sv", "sv-FI", "sv-SE", "sw", "sw-KE", "syr", "syr-SY", "ta", "ta-IN", "te",
|
||||
"te-IN", "th", "th-TH", "tl", "tl-PH", "tn", "tn-ZA", "tr", "tr-TR", "tt", "tt-RU", "ts", "uk", "uk-UA", "ur", "ur-PK",
|
||||
"uz", "uz-UZ", "uz-UZ", "vi", "vi-VN", "xh", "xh-ZA", "zh", "zh-CN", "zh-HK", "zh-MO", "zh-SG", "zh-TW", "zu", "zu-ZA"
|
||||
];
|
||||
|
||||
let screenRes = [
|
||||
[1920, 1080],
|
||||
[1920, 1200],
|
||||
[2048, 1080],
|
||||
[2560, 1440],
|
||||
[1366, 768],
|
||||
[1440, 900],
|
||||
[1536, 864],
|
||||
[1680, 1050],
|
||||
[1280, 1024],
|
||||
[1280, 800],
|
||||
[1280, 720],
|
||||
[1600, 1200],
|
||||
[1600, 900],
|
||||
];
|
||||
function randomScreenRes() {
|
||||
return screenRes[Math.floor(Math.random() * screenRes.length)];
|
||||
}
|
||||
|
||||
// Get fingerprint
|
||||
function getFingerprint() {
|
||||
let fingerprint = { ...baseFingerprint }; // Create a copy of the base fingerprint
|
||||
|
||||
// Randomization time!
|
||||
fingerprint["DNT"] = "unknown";
|
||||
fingerprint["L"] = languages[Math.floor(Math.random() * languages.length)];
|
||||
fingerprint["D"] = [8, 24][
|
||||
Math.floor(Math.random() * 2)
|
||||
];
|
||||
fingerprint["PR"] = Math.round(Math.random() * 100) / 100 * 2 + 0.5;
|
||||
fingerprint["S"] = randomScreenRes();
|
||||
fingerprint["AS"] = fingerprint.S;
|
||||
fingerprint["TO"] = (Math.floor(Math.random() * 24) - 12) * 60;
|
||||
fingerprint["SS"] = Math.random() > 0.5;
|
||||
fingerprint["LS"] = Math.random() > 0.5;
|
||||
fingerprint["IDB"] = Math.random() > 0.5;
|
||||
fingerprint["B"] = Math.random() > 0.5;
|
||||
fingerprint["ODB"] = Math.random() > 0.5;
|
||||
fingerprint["CPUC"] = "unknown";
|
||||
fingerprint["PK"] = "Win32"
|
||||
fingerprint["CFP"] = "canvas winding:yes~canvas fp:data:image/png;base64," + randomBytes(128).toString("base64");
|
||||
fingerprint["FR"] = false; // Fake Resolution
|
||||
fingerprint["FOS"] = false; // Fake Operating System
|
||||
fingerprint["FB"] = false; // Fake Browser
|
||||
fingerprint["JSF"] = fingerprint["JSF"].filter(() => Math.random() > 0.5);
|
||||
fingerprint["P"] = fingerprint["P"].filter(() => Math.random() > 0.5);
|
||||
fingerprint["T"] = [
|
||||
Math.floor(Math.random() * 8),
|
||||
Math.random() > 0.5,
|
||||
Math.random() > 0.5,
|
||||
];
|
||||
fingerprint["H"] = 2 ** Math.floor(Math.random() * 6);
|
||||
fingerprint["SWF"] = fingerprint["SWF"]; // RIP Flash
|
||||
|
||||
return fingerprint;
|
||||
}
|
||||
|
||||
function prepareF(fingerprint) {
|
||||
let f = [];
|
||||
let keys = Object.keys(fingerprint);
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
if (fingerprint[keys[i]].join) f.push(fingerprint[keys[i]].join(";"));
|
||||
else f.push(fingerprint[keys[i]]);
|
||||
}
|
||||
return f.join("~~~");
|
||||
}
|
||||
|
||||
function prepareFe(fingerprint) {
|
||||
let fe = [];
|
||||
let keys = Object.keys(fingerprint);
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
switch (keys[i]) {
|
||||
case "CFP":
|
||||
fe.push(`${keys[i]}:${cfpHash(fingerprint[keys[i]])}`);
|
||||
break;
|
||||
case "P":
|
||||
fe.push(
|
||||
`${keys[i]}:${fingerprint[keys[i]].map(
|
||||
(v) => v.split("::")[0]
|
||||
)}`
|
||||
);
|
||||
break;
|
||||
default:
|
||||
fe.push(`${keys[i]}:${fingerprint[keys[i]]}`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return fe;
|
||||
}
|
||||
|
||||
function cfpHash(H8W) {
|
||||
var l8W, U8W;
|
||||
if (!H8W) return "";
|
||||
if (Array.prototype.reduce)
|
||||
return H8W.split("").reduce(function (p8W, z8W) {
|
||||
p8W = (p8W << 5) - p8W + z8W.charCodeAt(0);
|
||||
return p8W & p8W;
|
||||
}, 0);
|
||||
l8W = 0;
|
||||
if (H8W.length === 0) return l8W;
|
||||
for (var k8W = 0; k8W < H8W.length; k8W++) {
|
||||
U8W = H8W.charCodeAt(k8W);
|
||||
l8W = (l8W << 5) - l8W + U8W;
|
||||
l8W = l8W & l8W;
|
||||
}
|
||||
return l8W;
|
||||
}
|
||||
|
||||
let baseEnhancedFingerprint = {
|
||||
"webgl_extensions": "ANGLE_instanced_arrays;EXT_blend_minmax;EXT_color_buffer_half_float;EXT_disjoint_timer_query;EXT_float_blend;EXT_frag_depth;EXT_shader_texture_lod;EXT_texture_compression_bptc;EXT_texture_compression_rgtc;EXT_texture_filter_anisotropic;EXT_sRGB;KHR_parallel_shader_compile;OES_element_index_uint;OES_fbo_render_mipmap;OES_standard_derivatives;OES_texture_float;OES_texture_float_linear;OES_texture_half_float;OES_texture_half_float_linear;OES_vertex_array_object;WEBGL_color_buffer_float;WEBGL_compressed_texture_s3tc;WEBGL_compressed_texture_s3tc_srgb;WEBGL_debug_renderer_info;WEBGL_debug_shaders;WEBGL_depth_texture;WEBGL_draw_buffers;WEBGL_lose_context;WEBGL_multi_draw",
|
||||
"webgl_extensions_hash": "58a5a04a5bef1a78fa88d5c5098bd237",
|
||||
"webgl_renderer": "WebKit WebGL",
|
||||
"webgl_vendor": "WebKit",
|
||||
"webgl_version": "WebGL 1.0 (OpenGL ES 2.0 Chromium)",
|
||||
"webgl_shading_language_version": "WebGL GLSL ES 1.0 (OpenGL ES GLSL ES 1.0 Chromium)",
|
||||
"webgl_aliased_line_width_range": "[1, 1]",
|
||||
"webgl_aliased_point_size_range": "[1, 1023]",
|
||||
"webgl_antialiasing": "yes",
|
||||
"webgl_bits": "8,8,24,8,8,0",
|
||||
"webgl_max_params": "16,64,16384,4096,8192,32,8192,31,16,32,4096",
|
||||
"webgl_max_viewport_dims": "[8192, 8192]",
|
||||
"webgl_unmasked_vendor": "Google Inc. (Google)",
|
||||
"webgl_unmasked_renderer": "ANGLE (Google, Vulkan 1.3.0 (SwiftShader Device (Subzero) (0x0000C0DE)), SwiftShader driver)",
|
||||
"webgl_vsf_params": "23,127,127,23,127,127,23,127,127",
|
||||
"webgl_vsi_params": "0,31,30,0,31,30,0,31,30",
|
||||
"webgl_fsf_params": "23,127,127,23,127,127,23,127,127",
|
||||
"webgl_fsi_params": "0,31,30,0,31,30,0,31,30",
|
||||
"webgl_hash_webgl": null,
|
||||
"user_agent_data_brands": "Chromium,Google Chrome,Not=A?Brand",
|
||||
"user_agent_data_mobile": null,
|
||||
"navigator_connection_downlink": null,
|
||||
"navigator_connection_downlink_max": null,
|
||||
"network_info_rtt": null,
|
||||
"network_info_save_data": false,
|
||||
"network_info_rtt_type": null,
|
||||
"screen_pixel_depth": 24,
|
||||
"navigator_device_memory": 0.5,
|
||||
"navigator_languages": "en-US,fr-BE,fr,en-BE,en",
|
||||
"window_inner_width": 0,
|
||||
"window_inner_height": 0,
|
||||
"window_outer_width": 2195,
|
||||
"window_outer_height": 1195,
|
||||
"browser_detection_firefox": false,
|
||||
"browser_detection_brave": false,
|
||||
"audio_codecs": "{\"ogg\":\"probably\",\"mp3\":\"probably\",\"wav\":\"probably\",\"m4a\":\"maybe\",\"aac\":\"probably\"}",
|
||||
"video_codecs": "{\"ogg\":\"probably\",\"h264\":\"probably\",\"webm\":\"probably\",\"mpeg4v\":\"\",\"mpeg4a\":\"\",\"theora\":\"\"}",
|
||||
"media_query_dark_mode": true,
|
||||
"headless_browser_phantom": false,
|
||||
"headless_browser_selenium": false,
|
||||
"headless_browser_nightmare_js": false,
|
||||
"document__referrer": "https://www.roblox.com/",
|
||||
"window__ancestor_origins": [
|
||||
"https://www.roblox.com",
|
||||
],
|
||||
"window__tree_index": [
|
||||
0
|
||||
],
|
||||
"window__tree_structure": "[[]]",
|
||||
"window__location_href": "https://roblox-api.arkoselabs.com/v2/1.5.5/enforcement.fbfc14b0d793c6ef8359e0e4b4a91f67.html#476068BF-9607-4799-B53D-966BE98E2B81",
|
||||
"client_config__sitedata_location_href": "https://www.roblox.com/arkose/iframe",
|
||||
"client_config__surl": "https://roblox-api.arkoselabs.com",
|
||||
"client_config__language": null,
|
||||
"navigator_battery_charging": true,
|
||||
"audio_fingerprint": "124.04347527516074"
|
||||
}
|
||||
function getEnhancedFingerprint(fp: typeof baseFingerprint, ua: string, opts: any) {
|
||||
let fingerprint = { ...baseEnhancedFingerprint };
|
||||
|
||||
fingerprint.webgl_extensions = fingerprint.webgl_extensions.split(";").filter(_ => Math.random() > 0.5).join(";");
|
||||
fingerprint.webgl_extensions_hash = x64hash128(fingerprint.webgl_extensions, 0);
|
||||
fingerprint.screen_pixel_depth = fp.D;
|
||||
fingerprint.navigator_languages = fp.L;
|
||||
fingerprint.window_outer_height = fp.S[0];
|
||||
fingerprint.window_outer_width = fp.S[1];
|
||||
fingerprint.window_inner_height = fp.S[0];
|
||||
fingerprint.window_inner_width = fp.S[1];
|
||||
fingerprint.screen_pixel_depth = fp.D;
|
||||
fingerprint.browser_detection_firefox = !!ua.match(/Firefox\/\d+/)
|
||||
fingerprint.browser_detection_brave = !!ua.match(/Brave\/\d+/)
|
||||
fingerprint.media_query_dark_mode = Math.random() > 0.9;
|
||||
fingerprint.webgl_hash_webgl = x64hash128(Object.entries(fingerprint).filter(([k, v]) => k.startsWith("webgl_") && k != "webgl_hash_webgl").map(([k, v]) => v).join(","), 0);
|
||||
|
||||
fingerprint.client_config__language = opts.language || null;
|
||||
fingerprint.window__location_href = `${opts.surl}/v2/1.5.5/enforcement.fbfc14b0d793c6ef8359e0e4b4a91f67.html#${opts.pkey}`
|
||||
if (opts.site) {
|
||||
fingerprint.document__referrer = opts.site;
|
||||
fingerprint.window__ancestor_origins = [opts.site];
|
||||
fingerprint.client_config__sitedata_location_href = opts.site;
|
||||
}
|
||||
|
||||
fingerprint.client_config__surl = opts.surl || "https://client-api.arkoselabs.com";
|
||||
fingerprint.audio_fingerprint = (124.04347527516074 + Math.random() * 0.001 - 0.0005).toString();
|
||||
|
||||
return Object.entries(fingerprint).map(([k, v]) => ({ key: k, value: v }));
|
||||
}
|
||||
|
||||
export default {
|
||||
getFingerprint,
|
||||
prepareF,
|
||||
prepareFe,
|
||||
getEnhancedFingerprint,
|
||||
};
|
|
@ -0,0 +1,28 @@
|
|||
import { request, ProxyAgent } from "undici";
|
||||
// @ts-ignore
|
||||
import { RequestOptions } from "undici/types/dispatcher";
|
||||
|
||||
async function req(url: string, options: RequestOptions, proxy?: string) {
|
||||
let auth = undefined;
|
||||
if (proxy) {
|
||||
let proxyUrl = new URL(proxy);
|
||||
if(proxyUrl.username && proxyUrl.password) {
|
||||
auth = Buffer.from(proxyUrl.username + ":" + proxyUrl.password).toString("base64")
|
||||
}
|
||||
}
|
||||
let dispatcher = proxy ? new ProxyAgent({
|
||||
uri: proxy,
|
||||
auth
|
||||
}) : undefined;
|
||||
|
||||
let req = await request(url, {
|
||||
...options,
|
||||
dispatcher,
|
||||
});
|
||||
return {
|
||||
headers: req.headers,
|
||||
body: Buffer.from(await req.body.arrayBuffer()),
|
||||
};
|
||||
}
|
||||
|
||||
export default req;
|
|
@ -0,0 +1,2 @@
|
|||
export * from "./api";
|
||||
export * from "./session";
|
|
@ -0,0 +1,205 @@
|
|||
// MurmurHash3 related functions
|
||||
// https://github.com/markogresak/fingerprintjs2/blob/master/src/x64hash128.js
|
||||
|
||||
// Given two 64bit ints (as an array of two 32bit ints) returns the two
|
||||
// added together as a 64bit int (as an array of two 32bit ints).
|
||||
var x64Add = function (t, r) {
|
||||
(t = [t[0] >>> 16, 65535 & t[0], t[1] >>> 16, 65535 & t[1]]),
|
||||
(r = [r[0] >>> 16, 65535 & r[0], r[1] >>> 16, 65535 & r[1]]);
|
||||
var e = [0, 0, 0, 0];
|
||||
return (
|
||||
(e[3] += t[3] + r[3]),
|
||||
(e[2] += e[3] >>> 16),
|
||||
(e[3] &= 65535),
|
||||
(e[2] += t[2] + r[2]),
|
||||
(e[1] += e[2] >>> 16),
|
||||
(e[2] &= 65535),
|
||||
(e[1] += t[1] + r[1]),
|
||||
(e[0] += e[1] >>> 16),
|
||||
(e[1] &= 65535),
|
||||
(e[0] += t[0] + r[0]),
|
||||
(e[0] &= 65535),
|
||||
[(e[0] << 16) | e[1], (e[2] << 16) | e[3]]
|
||||
);
|
||||
},
|
||||
// Given two 64bit ints (as an array of two 32bit ints) returns the two
|
||||
// multiplied together as a 64bit int (as an array of two 32bit ints).
|
||||
x64Multiply = function (t, r) {
|
||||
(t = [t[0] >>> 16, 65535 & t[0], t[1] >>> 16, 65535 & t[1]]),
|
||||
(r = [r[0] >>> 16, 65535 & r[0], r[1] >>> 16, 65535 & r[1]]);
|
||||
var e = [0, 0, 0, 0];
|
||||
return (
|
||||
(e[3] += t[3] * r[3]),
|
||||
(e[2] += e[3] >>> 16),
|
||||
(e[3] &= 65535),
|
||||
(e[2] += t[2] * r[3]),
|
||||
(e[1] += e[2] >>> 16),
|
||||
(e[2] &= 65535),
|
||||
(e[2] += t[3] * r[2]),
|
||||
(e[1] += e[2] >>> 16),
|
||||
(e[2] &= 65535),
|
||||
(e[1] += t[1] * r[3]),
|
||||
(e[0] += e[1] >>> 16),
|
||||
(e[1] &= 65535),
|
||||
(e[1] += t[2] * r[2]),
|
||||
(e[0] += e[1] >>> 16),
|
||||
(e[1] &= 65535),
|
||||
(e[1] += t[3] * r[1]),
|
||||
(e[0] += e[1] >>> 16),
|
||||
(e[1] &= 65535),
|
||||
(e[0] += t[0] * r[3] + t[1] * r[2] + t[2] * r[1] + t[3] * r[0]),
|
||||
(e[0] &= 65535),
|
||||
[(e[0] << 16) | e[1], (e[2] << 16) | e[3]]
|
||||
);
|
||||
},
|
||||
// Given a 64bit int (as an array of two 32bit ints) and an int
|
||||
// representing a number of bit positions, returns the 64bit int (as an
|
||||
// array of two 32bit ints) rotated left by that number of positions.
|
||||
x64Rotl = function (t, r) {
|
||||
return 32 === (r %= 64)
|
||||
? [t[1], t[0]]
|
||||
: r < 32
|
||||
? [
|
||||
(t[0] << r) | (t[1] >>> (32 - r)),
|
||||
(t[1] << r) | (t[0] >>> (32 - r)),
|
||||
]
|
||||
: ((r -= 32),
|
||||
[
|
||||
(t[1] << r) | (t[0] >>> (32 - r)),
|
||||
(t[0] << r) | (t[1] >>> (32 - r)),
|
||||
]);
|
||||
},
|
||||
// Given a 64bit int (as an array of two 32bit ints) and an int
|
||||
// representing a number of bit positions, returns the 64bit int (as an
|
||||
// array of two 32bit ints) shifted left by that number of positions.
|
||||
x64LeftShift = function (t, r) {
|
||||
return 0 === (r %= 64)
|
||||
? t
|
||||
: r < 32
|
||||
? [(t[0] << r) | (t[1] >>> (32 - r)), t[1] << r]
|
||||
: [t[1] << (r - 32), 0];
|
||||
},
|
||||
// Given two 64bit ints (as an array of two 32bit ints) returns the two
|
||||
// xored together as a 64bit int (as an array of two 32bit ints).
|
||||
x64Xor = function (t, r) {
|
||||
return [t[0] ^ r[0], t[1] ^ r[1]];
|
||||
},
|
||||
// Given a block, returns murmurHash3's final x64 mix of that block.
|
||||
// (`[0, h[0] >>> 1]` is a 33 bit unsigned right shift. This is the
|
||||
// only place where we need to right shift 64bit ints.)
|
||||
x64Fmix = function (t) {
|
||||
return (
|
||||
(t = x64Xor(t, [0, t[0] >>> 1])),
|
||||
(t = x64Multiply(t, [4283543511, 3981806797])),
|
||||
(t = x64Xor(t, [0, t[0] >>> 1])),
|
||||
(t = x64Multiply(t, [3301882366, 444984403])),
|
||||
(t = x64Xor(t, [0, t[0] >>> 1]))
|
||||
);
|
||||
},
|
||||
// Given a string and an optional seed as an int, returns a 128 bit
|
||||
// hash using the x64 flavor of MurmurHash3, as an unsigned hex.
|
||||
x64hash128 = function (t, r) {
|
||||
r = r || 0;
|
||||
for (
|
||||
var e = (t = t || "").length % 16,
|
||||
o = t.length - e,
|
||||
x = [0, r],
|
||||
c = [0, r],
|
||||
h = [0, 0],
|
||||
a = [0, 0],
|
||||
d = [2277735313, 289559509],
|
||||
i = [1291169091, 658871167],
|
||||
l = 0;
|
||||
l < o;
|
||||
l += 16
|
||||
)
|
||||
(h = [
|
||||
(255 & t.charCodeAt(l + 4)) |
|
||||
((255 & t.charCodeAt(l + 5)) << 8) |
|
||||
((255 & t.charCodeAt(l + 6)) << 16) |
|
||||
((255 & t.charCodeAt(l + 7)) << 24),
|
||||
(255 & t.charCodeAt(l)) |
|
||||
((255 & t.charCodeAt(l + 1)) << 8) |
|
||||
((255 & t.charCodeAt(l + 2)) << 16) |
|
||||
((255 & t.charCodeAt(l + 3)) << 24),
|
||||
]),
|
||||
(a = [
|
||||
(255 & t.charCodeAt(l + 12)) |
|
||||
((255 & t.charCodeAt(l + 13)) << 8) |
|
||||
((255 & t.charCodeAt(l + 14)) << 16) |
|
||||
((255 & t.charCodeAt(l + 15)) << 24),
|
||||
(255 & t.charCodeAt(l + 8)) |
|
||||
((255 & t.charCodeAt(l + 9)) << 8) |
|
||||
((255 & t.charCodeAt(l + 10)) << 16) |
|
||||
((255 & t.charCodeAt(l + 11)) << 24),
|
||||
]),
|
||||
(h = x64Multiply(h, d)),
|
||||
(h = x64Rotl(h, 31)),
|
||||
(h = x64Multiply(h, i)),
|
||||
(x = x64Xor(x, h)),
|
||||
(x = x64Rotl(x, 27)),
|
||||
(x = x64Add(x, c)),
|
||||
(x = x64Add(x64Multiply(x, [0, 5]), [0, 1390208809])),
|
||||
(a = x64Multiply(a, i)),
|
||||
(a = x64Rotl(a, 33)),
|
||||
(a = x64Multiply(a, d)),
|
||||
(c = x64Xor(c, a)),
|
||||
(c = x64Rotl(c, 31)),
|
||||
(c = x64Add(c, x)),
|
||||
(c = x64Add(x64Multiply(c, [0, 5]), [0, 944331445]));
|
||||
switch (((h = [0, 0]), (a = [0, 0]), e)) {
|
||||
case 15:
|
||||
a = x64Xor(a, x64LeftShift([0, t.charCodeAt(l + 14)], 48));
|
||||
case 14:
|
||||
a = x64Xor(a, x64LeftShift([0, t.charCodeAt(l + 13)], 40));
|
||||
case 13:
|
||||
a = x64Xor(a, x64LeftShift([0, t.charCodeAt(l + 12)], 32));
|
||||
case 12:
|
||||
a = x64Xor(a, x64LeftShift([0, t.charCodeAt(l + 11)], 24));
|
||||
case 11:
|
||||
a = x64Xor(a, x64LeftShift([0, t.charCodeAt(l + 10)], 16));
|
||||
case 10:
|
||||
a = x64Xor(a, x64LeftShift([0, t.charCodeAt(l + 9)], 8));
|
||||
case 9:
|
||||
(a = x64Xor(a, [0, t.charCodeAt(l + 8)])),
|
||||
(a = x64Multiply(a, i)),
|
||||
(a = x64Rotl(a, 33)),
|
||||
(a = x64Multiply(a, d)),
|
||||
(c = x64Xor(c, a));
|
||||
case 8:
|
||||
h = x64Xor(h, x64LeftShift([0, t.charCodeAt(l + 7)], 56));
|
||||
case 7:
|
||||
h = x64Xor(h, x64LeftShift([0, t.charCodeAt(l + 6)], 48));
|
||||
case 6:
|
||||
h = x64Xor(h, x64LeftShift([0, t.charCodeAt(l + 5)], 40));
|
||||
case 5:
|
||||
h = x64Xor(h, x64LeftShift([0, t.charCodeAt(l + 4)], 32));
|
||||
case 4:
|
||||
h = x64Xor(h, x64LeftShift([0, t.charCodeAt(l + 3)], 24));
|
||||
case 3:
|
||||
h = x64Xor(h, x64LeftShift([0, t.charCodeAt(l + 2)], 16));
|
||||
case 2:
|
||||
h = x64Xor(h, x64LeftShift([0, t.charCodeAt(l + 1)], 8));
|
||||
case 1:
|
||||
(h = x64Xor(h, [0, t.charCodeAt(l)])),
|
||||
(h = x64Multiply(h, d)),
|
||||
(h = x64Rotl(h, 31)),
|
||||
(h = x64Multiply(h, i)),
|
||||
(x = x64Xor(x, h));
|
||||
}
|
||||
return (
|
||||
(x = x64Xor(x, [0, t.length])),
|
||||
(c = x64Xor(c, [0, t.length])),
|
||||
(x = x64Add(x, c)),
|
||||
(c = x64Add(c, x)),
|
||||
(x = x64Fmix(x)),
|
||||
(c = x64Fmix(c)),
|
||||
(x = x64Add(x, c)),
|
||||
(c = x64Add(c, x)),
|
||||
("00000000" + (x[0] >>> 0).toString(16)).slice(-8) +
|
||||
("00000000" + (x[1] >>> 0).toString(16)).slice(-8) +
|
||||
("00000000" + (c[0] >>> 0).toString(16)).slice(-8) +
|
||||
("00000000" + (c[1] >>> 0).toString(16)).slice(-8)
|
||||
);
|
||||
};
|
||||
export default x64hash128;
|
|
@ -0,0 +1,125 @@
|
|||
import { GetTokenResult } from "./api";
|
||||
import { Challenge, Challenge1, Challenge3, Challenge4 } from "./challenge";
|
||||
import http from "./http";
|
||||
import util from "./util";
|
||||
|
||||
export interface TokenInfo {
|
||||
token: string;
|
||||
r: string;
|
||||
metabgclr: string;
|
||||
mainbgclr: string;
|
||||
guitextcolor: string;
|
||||
metaiconclr: string;
|
||||
meta_height: string;
|
||||
meta_width: string;
|
||||
meta: string;
|
||||
pk: string;
|
||||
dc: string;
|
||||
at: string;
|
||||
cdn_url: string;
|
||||
lurl: string;
|
||||
surl: string;
|
||||
smurl: string;
|
||||
// Enable keyboard biometrics
|
||||
kbio: boolean;
|
||||
// Enable mouse biometrics
|
||||
mbio: boolean;
|
||||
// Enable touch biometrics
|
||||
tbio: boolean;
|
||||
}
|
||||
|
||||
export interface SessionOptions {
|
||||
userAgent?: string;
|
||||
proxy?: string;
|
||||
}
|
||||
|
||||
let parseToken = (token: string): TokenInfo =>
|
||||
Object.fromEntries(
|
||||
token
|
||||
.split("|")
|
||||
.map((v) => v.split("=").map((v) => decodeURIComponent(v)))
|
||||
);
|
||||
|
||||
export class Session {
|
||||
public token: string;
|
||||
public tokenInfo: TokenInfo;
|
||||
private userAgent: string;
|
||||
private proxy: string;
|
||||
|
||||
constructor(
|
||||
token: string | GetTokenResult,
|
||||
sessionOptions?: SessionOptions
|
||||
) {
|
||||
if (typeof token === "string") {
|
||||
this.token = token;
|
||||
} else {
|
||||
this.token = token.token;
|
||||
}
|
||||
if (!this.token.startsWith("token="))
|
||||
this.token = "token=" + this.token;
|
||||
|
||||
this.tokenInfo = parseToken(this.token);
|
||||
this.tokenInfo.mbio = typeof(token) !== "string" ? token.mbio ?? false : false
|
||||
this.userAgent = sessionOptions?.userAgent || util.DEFAULT_USER_AGENT;
|
||||
this.proxy = sessionOptions?.proxy;
|
||||
}
|
||||
|
||||
async getChallenge(): Promise<Challenge> {
|
||||
let res = await http(
|
||||
this.tokenInfo.surl,
|
||||
{
|
||||
path: "/fc/gfct/",
|
||||
method: "POST",
|
||||
body: util.constructFormData({
|
||||
sid: this.tokenInfo.r,
|
||||
render_type: "canvas",
|
||||
token: this.tokenInfo.token,
|
||||
analytics_tier: this.tokenInfo.at,
|
||||
"data%5Bstatus%5D": "init",
|
||||
lang: "en",
|
||||
apiBreakerVersion: "green"
|
||||
}),
|
||||
headers: {
|
||||
"User-Agent": this.userAgent,
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
"Accept-Language": "en-US,en;q=0.9",
|
||||
"Sec-Fetch-Site": "same-origin",
|
||||
"Referer": this.getEmbedUrl()
|
||||
},
|
||||
},
|
||||
this.proxy
|
||||
);
|
||||
|
||||
let data = JSON.parse(res.body.toString());
|
||||
data.token = this.token;
|
||||
data.tokenInfo = this.tokenInfo;
|
||||
|
||||
if (data.game_data.gameType == 1) {
|
||||
return new Challenge1(data, {
|
||||
proxy: this.proxy,
|
||||
userAgent: this.userAgent,
|
||||
});
|
||||
} else if (data.game_data.gameType == 3) {
|
||||
return new Challenge3(data, {
|
||||
proxy: this.proxy,
|
||||
userAgent: this.userAgent,
|
||||
});
|
||||
} else if (data.game_data.gameType == 4) {
|
||||
return new Challenge4(data, {
|
||||
proxy: this.proxy,
|
||||
userAgent: this.userAgent,
|
||||
});
|
||||
} else {
|
||||
throw new Error(
|
||||
"Unsupported game type: " + data.game_data.gameType
|
||||
);
|
||||
}
|
||||
//return res.body.toString()
|
||||
}
|
||||
|
||||
getEmbedUrl(): string {
|
||||
return `${this.tokenInfo.surl}/fc/gc/?${util.constructFormData(
|
||||
this.tokenInfo
|
||||
)}`;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,197 @@
|
|||
import fingerprint from "./fingerprint";
|
||||
import murmur from "./murmur";
|
||||
import crypt from "./crypt";
|
||||
|
||||
interface TimestampData {
|
||||
cookie: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
const DEFAULT_USER_AGENT =
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36";
|
||||
|
||||
let apiBreakers = {
|
||||
v1: {
|
||||
3: {
|
||||
default: (c) => c,
|
||||
method_1: (c) => ({ x: c.y, y: c.x }),
|
||||
method_2: (c) => ({ x: c.x, y: (c.y + c.x) * c.x }),
|
||||
method_3: (c) => ({ a: c.x, b: c.y }),
|
||||
method_4: (c) => [c.x, c.y],
|
||||
method_5: (c) => [c.y, c.x].map((v) => Math.sqrt(v)),
|
||||
},
|
||||
4: {
|
||||
default: (c) => c
|
||||
}
|
||||
},
|
||||
v2: {
|
||||
3: {
|
||||
value: {
|
||||
alpha: (c) => ({ x: c.x, y: (c.y + c.x) * c.x, px: c.px, py: c.py }),
|
||||
beta: (c) => ({ x: c.y, y: c.x, py: c.px, px: c.py }),
|
||||
gamma: (c) => ({ x: c.y + 1, y: -c.x, px: c.px, py: c.py }),
|
||||
delta: (c) => ({ x: c.y + 0.25, y: c.x + 0.5, px: c.px, py: c.py }),
|
||||
epsilon: (c) => ({ x: c.x * 0.5, y: c.y * 5, px: c.px, py: c.py }),
|
||||
zeta: (c) => ({ x: c.x + 1, y: c.y + 2, px: c.px, py: c.py }),
|
||||
method_1: (c) => ({ x: c.x, y: c.y, px: c.px, py: c.py }),
|
||||
method_2: (c) => ({ x: c.y, y: (c.y + c.x) * c.x, px: c.px, py: c.py }),
|
||||
method_3: (c) => ({ x: Math.sqrt(c.x), y: Math.sqrt(c.y), px: c.px, py: c.py }),
|
||||
},
|
||||
key: {
|
||||
alpha: (c) => [c.y, c.px, c.py, c.x],
|
||||
beta: (c) => JSON.stringify({ x: c.x, y: c.y, px: c.px, py: c.py }),
|
||||
gamma: (c) => [c.x, c.y, c.px, c.py].join(" "),
|
||||
delta: (c) => [1, c.x, 2, c.y, 3, c.px, 4, c.py],
|
||||
epsilon: (c) => ({ answer: { x: c.x, y: c.y, px: c.px, py: c.py } }),
|
||||
zeta: (c) => [c.x, [c.y, [c.px, [c.py]]]],
|
||||
method_1: (c) => ({ a: c.x, b: c.y, px: c.px, py: c.py }),
|
||||
method_2: (c) => [c.x, c.y],
|
||||
method_3: (c) => [c.y, c.x],
|
||||
}
|
||||
},
|
||||
4: {
|
||||
value: {
|
||||
// @ts-ignore
|
||||
alpha: (c) => ({ index: String(c.index) + 1 - 2 }),
|
||||
beta: (c) => ({ index: -c.index }),
|
||||
gamma: (c) => ({ index: 3 * (3 - c.index) }),
|
||||
delta: (c) => ({ index: 7 * c.index }),
|
||||
epsilon: (c) => ({ index: 2 * c.index }),
|
||||
zeta: (c) => ({ index: c.index ? 100 / c.index : c.index }),
|
||||
va: (c) => ({ index: c.index + 3 }),
|
||||
vb: (c) => ({ index: -c.index }),
|
||||
vc: (c) => ({ index: 10 - c.index }),
|
||||
vd: (c) => ({ index: 3 * c.index }),
|
||||
},
|
||||
key: {
|
||||
alpha: (c) => [Math.round(100 * Math.random()), c.index, Math.round(100 * Math.random())],
|
||||
beta: (c) => ({ size: 50 - c.index, id: c.index, limit: 10 * c.index, req_timestamp: Date.now() }),
|
||||
gamma: (c) => c.index,
|
||||
delta: (c) => ({ index: c.index }),
|
||||
epsilon: (c) => {
|
||||
const arr: any = [];
|
||||
const len = Math.round(5 * Math.random()) + 1;
|
||||
const rand = Math.round(Math.random() * len);
|
||||
for (let i = 0; i < len; i++) {
|
||||
arr.push(i === rand ? c.index : Math.round(10 * Math.random()));
|
||||
}
|
||||
arr.push(rand);
|
||||
return arr;
|
||||
},
|
||||
zeta: (c) => Array(Math.round(5 * Math.random()) + 1).concat(c.index),
|
||||
ka: (c) => c.index,
|
||||
kb: (c) => [c.index],
|
||||
kc: (c) => ({ guess: c.index }),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface TileLoc {
|
||||
x: number;
|
||||
y: number;
|
||||
px: number;
|
||||
py: number;
|
||||
}
|
||||
function tileToLoc(tile: number): TileLoc {
|
||||
let xClick = (tile % 3) * 100 + (tile % 3) * 3 + 3 + 10 + Math.floor(Math.random() * 80);
|
||||
let yClick = Math.floor(tile / 3) * 100 + Math.floor(tile / 3) * 3 + 3 + 10 + Math.floor(Math.random() * 80);
|
||||
return {
|
||||
x: xClick,
|
||||
y: yClick,
|
||||
px: xClick / 300,
|
||||
py: yClick / 200,
|
||||
}
|
||||
}
|
||||
|
||||
function constructFormData(data: {}): string {
|
||||
return Object.keys(data)
|
||||
.filter((v) => data[v] !== undefined)
|
||||
.map((k) => `${k}=${encodeURIComponent(data[k])}`)
|
||||
.join("&");
|
||||
}
|
||||
|
||||
function random(): string {
|
||||
return Array(32)
|
||||
.fill(0)
|
||||
.map(() => "0123456789abcdef"[Math.floor(Math.random() * 16)])
|
||||
.join("");
|
||||
}
|
||||
|
||||
function getTimestamp(): TimestampData {
|
||||
const time = (new Date()).getTime().toString()
|
||||
const value = `${time.substring(0, 7)}00${time.substring(7, 13)}`
|
||||
|
||||
return { cookie: `timestamp=${value};path=/;secure;samesite=none`, value }
|
||||
}
|
||||
|
||||
function getBda(userAgent: string, opts: object): string {
|
||||
let fp = fingerprint.getFingerprint();
|
||||
let fe = fingerprint.prepareFe(fp);
|
||||
|
||||
let bda = [
|
||||
{ key: "api_type", value: "js" },
|
||||
{ key: "p", value: 1 },
|
||||
{ key: "f", value: murmur(fingerprint.prepareF(fingerprint), 31) },
|
||||
{
|
||||
key: "n",
|
||||
value: Buffer.from(
|
||||
Math.round(Date.now() / (1000 - 0)).toString()
|
||||
).toString("base64"),
|
||||
},
|
||||
{ key: "wh", value: `${random()}|${random()}` },
|
||||
{
|
||||
"key": "enhanced_fp",
|
||||
"value": fingerprint.getEnhancedFingerprint(fp, userAgent, opts)
|
||||
},
|
||||
{ key: "fe", value: fe },
|
||||
{ key: "ife_hash", value: murmur(fe.join(", "), 38) },
|
||||
{ key: "cs", value: 1 },
|
||||
{
|
||||
key: "jsbd",
|
||||
value: JSON.stringify({
|
||||
HL: 4,
|
||||
DT: "",
|
||||
NWD: "false",
|
||||
DOTO: 1,
|
||||
DMTO: 1,
|
||||
}),
|
||||
},
|
||||
];
|
||||
|
||||
let time = new Date().getTime() / 1000;
|
||||
let key = userAgent + Math.round(time - (time % 21600));
|
||||
|
||||
let s = JSON.stringify(bda);
|
||||
let encrypted = crypt.encrypt(s, key);
|
||||
return Buffer.from(encrypted).toString("base64");
|
||||
}
|
||||
|
||||
function solveBreaker(v2: boolean, breaker: { value: string[], key: string } | string = "default", gameType: number, value: object) {
|
||||
if (!v2 && typeof breaker === "string")
|
||||
return (apiBreakers.v1[gameType][breaker || "default"] || ((v: any) => v))(value)
|
||||
|
||||
if (typeof breaker !== "string") {
|
||||
let b = apiBreakers.v2[gameType]
|
||||
let v = breaker.value.reduce((acc, cur) => {
|
||||
if (b.value[cur])
|
||||
return b.value[cur](acc)
|
||||
else
|
||||
return cur
|
||||
}, value)
|
||||
return b.key[breaker.key](v)
|
||||
} else {
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
DEFAULT_USER_AGENT,
|
||||
tileToLoc,
|
||||
constructFormData,
|
||||
getBda,
|
||||
apiBreakers,
|
||||
getTimestamp,
|
||||
random,
|
||||
solveBreaker
|
||||
};
|
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) Matteo Collina and Undici contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
|
@ -0,0 +1,443 @@
|
|||
# undici
|
||||
|
||||
[![Node CI](https://github.com/nodejs/undici/actions/workflows/nodejs.yml/badge.svg)](https://github.com/nodejs/undici/actions/workflows/nodejs.yml) [![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg?style=flat)](http://standardjs.com/) [![npm version](https://badge.fury.io/js/undici.svg)](https://badge.fury.io/js/undici) [![codecov](https://codecov.io/gh/nodejs/undici/branch/main/graph/badge.svg?token=yZL6LtXkOA)](https://codecov.io/gh/nodejs/undici)
|
||||
|
||||
An HTTP/1.1 client, written from scratch for Node.js.
|
||||
|
||||
> Undici means eleven in Italian. 1.1 -> 11 -> Eleven -> Undici.
|
||||
It is also a Stranger Things reference.
|
||||
|
||||
Have a question about using Undici? Open a [Q&A Discussion](https://github.com/nodejs/undici/discussions/new) or join our official OpenJS [Slack](https://openjs-foundation.slack.com/archives/C01QF9Q31QD) channel.
|
||||
|
||||
## Install
|
||||
|
||||
```
|
||||
npm i undici
|
||||
```
|
||||
|
||||
## Benchmarks
|
||||
|
||||
The benchmark is a simple `hello world` [example](benchmarks/benchmark.js) using a
|
||||
number of unix sockets (connections) with a pipelining depth of 10 running on Node 20.6.0.
|
||||
|
||||
### Connections 1
|
||||
|
||||
|
||||
| Tests | Samples | Result | Tolerance | Difference with slowest |
|
||||
|---------------------|---------|---------------|-----------|-------------------------|
|
||||
| http - no keepalive | 15 | 5.32 req/sec | ± 2.61 % | - |
|
||||
| http - keepalive | 10 | 5.35 req/sec | ± 2.47 % | + 0.44 % |
|
||||
| undici - fetch | 15 | 41.85 req/sec | ± 2.49 % | + 686.04 % |
|
||||
| undici - pipeline | 40 | 50.36 req/sec | ± 2.77 % | + 845.92 % |
|
||||
| undici - stream | 15 | 60.58 req/sec | ± 2.75 % | + 1037.72 % |
|
||||
| undici - request | 10 | 61.19 req/sec | ± 2.60 % | + 1049.24 % |
|
||||
| undici - dispatch | 20 | 64.84 req/sec | ± 2.81 % | + 1117.81 % |
|
||||
|
||||
|
||||
### Connections 50
|
||||
|
||||
| Tests | Samples | Result | Tolerance | Difference with slowest |
|
||||
|---------------------|---------|------------------|-----------|-------------------------|
|
||||
| undici - fetch | 30 | 2107.19 req/sec | ± 2.69 % | - |
|
||||
| http - no keepalive | 10 | 2698.90 req/sec | ± 2.68 % | + 28.08 % |
|
||||
| http - keepalive | 10 | 4639.49 req/sec | ± 2.55 % | + 120.17 % |
|
||||
| undici - pipeline | 40 | 6123.33 req/sec | ± 2.97 % | + 190.59 % |
|
||||
| undici - stream | 50 | 9426.51 req/sec | ± 2.92 % | + 347.35 % |
|
||||
| undici - request | 10 | 10162.88 req/sec | ± 2.13 % | + 382.29 % |
|
||||
| undici - dispatch | 50 | 11191.11 req/sec | ± 2.98 % | + 431.09 % |
|
||||
|
||||
|
||||
## Quick Start
|
||||
|
||||
```js
|
||||
import { request } from 'undici'
|
||||
|
||||
const {
|
||||
statusCode,
|
||||
headers,
|
||||
trailers,
|
||||
body
|
||||
} = await request('http://localhost:3000/foo')
|
||||
|
||||
console.log('response received', statusCode)
|
||||
console.log('headers', headers)
|
||||
|
||||
for await (const data of body) {
|
||||
console.log('data', data)
|
||||
}
|
||||
|
||||
console.log('trailers', trailers)
|
||||
```
|
||||
|
||||
## Body Mixins
|
||||
|
||||
The `body` mixins are the most common way to format the request/response body. Mixins include:
|
||||
|
||||
- [`.formData()`](https://fetch.spec.whatwg.org/#dom-body-formdata)
|
||||
- [`.json()`](https://fetch.spec.whatwg.org/#dom-body-json)
|
||||
- [`.text()`](https://fetch.spec.whatwg.org/#dom-body-text)
|
||||
|
||||
Example usage:
|
||||
|
||||
```js
|
||||
import { request } from 'undici'
|
||||
|
||||
const {
|
||||
statusCode,
|
||||
headers,
|
||||
trailers,
|
||||
body
|
||||
} = await request('http://localhost:3000/foo')
|
||||
|
||||
console.log('response received', statusCode)
|
||||
console.log('headers', headers)
|
||||
console.log('data', await body.json())
|
||||
console.log('trailers', trailers)
|
||||
```
|
||||
|
||||
_Note: Once a mixin has been called then the body cannot be reused, thus calling additional mixins on `.body`, e.g. `.body.json(); .body.text()` will result in an error `TypeError: unusable` being thrown and returned through the `Promise` rejection._
|
||||
|
||||
Should you need to access the `body` in plain-text after using a mixin, the best practice is to use the `.text()` mixin first and then manually parse the text to the desired format.
|
||||
|
||||
For more information about their behavior, please reference the body mixin from the [Fetch Standard](https://fetch.spec.whatwg.org/#body-mixin).
|
||||
|
||||
## Common API Methods
|
||||
|
||||
This section documents our most commonly used API methods. Additional APIs are documented in their own files within the [docs](./docs/) folder and are accessible via the navigation list on the left side of the docs site.
|
||||
|
||||
### `undici.request([url, options]): Promise`
|
||||
|
||||
Arguments:
|
||||
|
||||
* **url** `string | URL | UrlObject`
|
||||
* **options** [`RequestOptions`](./docs/api/Dispatcher.md#parameter-requestoptions)
|
||||
* **dispatcher** `Dispatcher` - Default: [getGlobalDispatcher](#undicigetglobaldispatcher)
|
||||
* **method** `String` - Default: `PUT` if `options.body`, otherwise `GET`
|
||||
* **maxRedirections** `Integer` - Default: `0`
|
||||
|
||||
Returns a promise with the result of the `Dispatcher.request` method.
|
||||
|
||||
Calls `options.dispatcher.request(options)`.
|
||||
|
||||
See [Dispatcher.request](./docs/api/Dispatcher.md#dispatcherrequestoptions-callback) for more details.
|
||||
|
||||
### `undici.stream([url, options, ]factory): Promise`
|
||||
|
||||
Arguments:
|
||||
|
||||
* **url** `string | URL | UrlObject`
|
||||
* **options** [`StreamOptions`](./docs/api/Dispatcher.md#parameter-streamoptions)
|
||||
* **dispatcher** `Dispatcher` - Default: [getGlobalDispatcher](#undicigetglobaldispatcher)
|
||||
* **method** `String` - Default: `PUT` if `options.body`, otherwise `GET`
|
||||
* **maxRedirections** `Integer` - Default: `0`
|
||||
* **factory** `Dispatcher.stream.factory`
|
||||
|
||||
Returns a promise with the result of the `Dispatcher.stream` method.
|
||||
|
||||
Calls `options.dispatcher.stream(options, factory)`.
|
||||
|
||||
See [Dispatcher.stream](docs/api/Dispatcher.md#dispatcherstreamoptions-factory-callback) for more details.
|
||||
|
||||
### `undici.pipeline([url, options, ]handler): Duplex`
|
||||
|
||||
Arguments:
|
||||
|
||||
* **url** `string | URL | UrlObject`
|
||||
* **options** [`PipelineOptions`](docs/api/Dispatcher.md#parameter-pipelineoptions)
|
||||
* **dispatcher** `Dispatcher` - Default: [getGlobalDispatcher](#undicigetglobaldispatcher)
|
||||
* **method** `String` - Default: `PUT` if `options.body`, otherwise `GET`
|
||||
* **maxRedirections** `Integer` - Default: `0`
|
||||
* **handler** `Dispatcher.pipeline.handler`
|
||||
|
||||
Returns: `stream.Duplex`
|
||||
|
||||
Calls `options.dispatch.pipeline(options, handler)`.
|
||||
|
||||
See [Dispatcher.pipeline](docs/api/Dispatcher.md#dispatcherpipelineoptions-handler) for more details.
|
||||
|
||||
### `undici.connect([url, options]): Promise`
|
||||
|
||||
Starts two-way communications with the requested resource using [HTTP CONNECT](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/CONNECT).
|
||||
|
||||
Arguments:
|
||||
|
||||
* **url** `string | URL | UrlObject`
|
||||
* **options** [`ConnectOptions`](docs/api/Dispatcher.md#parameter-connectoptions)
|
||||
* **dispatcher** `Dispatcher` - Default: [getGlobalDispatcher](#undicigetglobaldispatcher)
|
||||
* **maxRedirections** `Integer` - Default: `0`
|
||||
* **callback** `(err: Error | null, data: ConnectData | null) => void` (optional)
|
||||
|
||||
Returns a promise with the result of the `Dispatcher.connect` method.
|
||||
|
||||
Calls `options.dispatch.connect(options)`.
|
||||
|
||||
See [Dispatcher.connect](docs/api/Dispatcher.md#dispatcherconnectoptions-callback) for more details.
|
||||
|
||||
### `undici.fetch(input[, init]): Promise`
|
||||
|
||||
Implements [fetch](https://fetch.spec.whatwg.org/#fetch-method).
|
||||
|
||||
* https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch
|
||||
* https://fetch.spec.whatwg.org/#fetch-method
|
||||
|
||||
Only supported on Node 16.8+.
|
||||
|
||||
Basic usage example:
|
||||
|
||||
```js
|
||||
import { fetch } from 'undici'
|
||||
|
||||
|
||||
const res = await fetch('https://example.com')
|
||||
const json = await res.json()
|
||||
console.log(json)
|
||||
```
|
||||
|
||||
You can pass an optional dispatcher to `fetch` as:
|
||||
|
||||
```js
|
||||
import { fetch, Agent } from 'undici'
|
||||
|
||||
const res = await fetch('https://example.com', {
|
||||
// Mocks are also supported
|
||||
dispatcher: new Agent({
|
||||
keepAliveTimeout: 10,
|
||||
keepAliveMaxTimeout: 10
|
||||
})
|
||||
})
|
||||
const json = await res.json()
|
||||
console.log(json)
|
||||
```
|
||||
|
||||
#### `request.body`
|
||||
|
||||
A body can be of the following types:
|
||||
|
||||
- ArrayBuffer
|
||||
- ArrayBufferView
|
||||
- AsyncIterables
|
||||
- Blob
|
||||
- Iterables
|
||||
- String
|
||||
- URLSearchParams
|
||||
- FormData
|
||||
|
||||
In this implementation of fetch, ```request.body``` now accepts ```Async Iterables```. It is not present in the [Fetch Standard.](https://fetch.spec.whatwg.org)
|
||||
|
||||
```js
|
||||
import { fetch } from 'undici'
|
||||
|
||||
const data = {
|
||||
async *[Symbol.asyncIterator]() {
|
||||
yield 'hello'
|
||||
yield 'world'
|
||||
},
|
||||
}
|
||||
|
||||
await fetch('https://example.com', { body: data, method: 'POST', duplex: 'half' })
|
||||
```
|
||||
|
||||
#### `request.duplex`
|
||||
|
||||
- half
|
||||
|
||||
In this implementation of fetch, `request.duplex` must be set if `request.body` is `ReadableStream` or `Async Iterables`. And fetch requests are currently always be full duplex. More detail refer to [Fetch Standard.](https://fetch.spec.whatwg.org/#dom-requestinit-duplex)
|
||||
|
||||
#### `response.body`
|
||||
|
||||
Nodejs has two kinds of streams: [web streams](https://nodejs.org/dist/latest-v16.x/docs/api/webstreams.html), which follow the API of the WHATWG web standard found in browsers, and an older Node-specific [streams API](https://nodejs.org/api/stream.html). `response.body` returns a readable web stream. If you would prefer to work with a Node stream you can convert a web stream using `.fromWeb()`.
|
||||
|
||||
```js
|
||||
import { fetch } from 'undici'
|
||||
import { Readable } from 'node:stream'
|
||||
|
||||
const response = await fetch('https://example.com')
|
||||
const readableWebStream = response.body
|
||||
const readableNodeStream = Readable.fromWeb(readableWebStream)
|
||||
```
|
||||
|
||||
#### Specification Compliance
|
||||
|
||||
This section documents parts of the [Fetch Standard](https://fetch.spec.whatwg.org) that Undici does
|
||||
not support or does not fully implement.
|
||||
|
||||
##### Garbage Collection
|
||||
|
||||
* https://fetch.spec.whatwg.org/#garbage-collection
|
||||
|
||||
The [Fetch Standard](https://fetch.spec.whatwg.org) allows users to skip consuming the response body by relying on
|
||||
[garbage collection](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Memory_Management#garbage_collection) to release connection resources. Undici does not do the same. Therefore, it is important to always either consume or cancel the response body.
|
||||
|
||||
Garbage collection in Node is less aggressive and deterministic
|
||||
(due to the lack of clear idle periods that browsers have through the rendering refresh rate)
|
||||
which means that leaving the release of connection resources to the garbage collector can lead
|
||||
to excessive connection usage, reduced performance (due to less connection re-use), and even
|
||||
stalls or deadlocks when running out of connections.
|
||||
|
||||
```js
|
||||
// Do
|
||||
const headers = await fetch(url)
|
||||
.then(async res => {
|
||||
for await (const chunk of res.body) {
|
||||
// force consumption of body
|
||||
}
|
||||
return res.headers
|
||||
})
|
||||
|
||||
// Do not
|
||||
const headers = await fetch(url)
|
||||
.then(res => res.headers)
|
||||
```
|
||||
|
||||
However, if you want to get only headers, it might be better to use `HEAD` request method. Usage of this method will obviate the need for consumption or cancelling of the response body. See [MDN - HTTP - HTTP request methods - HEAD](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/HEAD) for more details.
|
||||
|
||||
```js
|
||||
const headers = await fetch(url, { method: 'HEAD' })
|
||||
.then(res => res.headers)
|
||||
```
|
||||
|
||||
##### Forbidden and Safelisted Header Names
|
||||
|
||||
* https://fetch.spec.whatwg.org/#cors-safelisted-response-header-name
|
||||
* https://fetch.spec.whatwg.org/#forbidden-header-name
|
||||
* https://fetch.spec.whatwg.org/#forbidden-response-header-name
|
||||
* https://github.com/wintercg/fetch/issues/6
|
||||
|
||||
The [Fetch Standard](https://fetch.spec.whatwg.org) requires implementations to exclude certain headers from requests and responses. In browser environments, some headers are forbidden so the user agent remains in full control over them. In Undici, these constraints are removed to give more control to the user.
|
||||
|
||||
### `undici.upgrade([url, options]): Promise`
|
||||
|
||||
Upgrade to a different protocol. See [MDN - HTTP - Protocol upgrade mechanism](https://developer.mozilla.org/en-US/docs/Web/HTTP/Protocol_upgrade_mechanism) for more details.
|
||||
|
||||
Arguments:
|
||||
|
||||
* **url** `string | URL | UrlObject`
|
||||
* **options** [`UpgradeOptions`](docs/api/Dispatcher.md#parameter-upgradeoptions)
|
||||
* **dispatcher** `Dispatcher` - Default: [getGlobalDispatcher](#undicigetglobaldispatcher)
|
||||
* **maxRedirections** `Integer` - Default: `0`
|
||||
* **callback** `(error: Error | null, data: UpgradeData) => void` (optional)
|
||||
|
||||
Returns a promise with the result of the `Dispatcher.upgrade` method.
|
||||
|
||||
Calls `options.dispatcher.upgrade(options)`.
|
||||
|
||||
See [Dispatcher.upgrade](docs/api/Dispatcher.md#dispatcherupgradeoptions-callback) for more details.
|
||||
|
||||
### `undici.setGlobalDispatcher(dispatcher)`
|
||||
|
||||
* dispatcher `Dispatcher`
|
||||
|
||||
Sets the global dispatcher used by Common API Methods.
|
||||
|
||||
### `undici.getGlobalDispatcher()`
|
||||
|
||||
Gets the global dispatcher used by Common API Methods.
|
||||
|
||||
Returns: `Dispatcher`
|
||||
|
||||
### `undici.setGlobalOrigin(origin)`
|
||||
|
||||
* origin `string | URL | undefined`
|
||||
|
||||
Sets the global origin used in `fetch`.
|
||||
|
||||
If `undefined` is passed, the global origin will be reset. This will cause `Response.redirect`, `new Request()`, and `fetch` to throw an error when a relative path is passed.
|
||||
|
||||
```js
|
||||
setGlobalOrigin('http://localhost:3000')
|
||||
|
||||
const response = await fetch('/api/ping')
|
||||
|
||||
console.log(response.url) // http://localhost:3000/api/ping
|
||||
```
|
||||
|
||||
### `undici.getGlobalOrigin()`
|
||||
|
||||
Gets the global origin used in `fetch`.
|
||||
|
||||
Returns: `URL`
|
||||
|
||||
### `UrlObject`
|
||||
|
||||
* **port** `string | number` (optional)
|
||||
* **path** `string` (optional)
|
||||
* **pathname** `string` (optional)
|
||||
* **hostname** `string` (optional)
|
||||
* **origin** `string` (optional)
|
||||
* **protocol** `string` (optional)
|
||||
* **search** `string` (optional)
|
||||
|
||||
## Specification Compliance
|
||||
|
||||
This section documents parts of the HTTP/1.1 specification that Undici does
|
||||
not support or does not fully implement.
|
||||
|
||||
### Expect
|
||||
|
||||
Undici does not support the `Expect` request header field. The request
|
||||
body is always immediately sent and the `100 Continue` response will be
|
||||
ignored.
|
||||
|
||||
Refs: https://tools.ietf.org/html/rfc7231#section-5.1.1
|
||||
|
||||
### Pipelining
|
||||
|
||||
Undici will only use pipelining if configured with a `pipelining` factor
|
||||
greater than `1`.
|
||||
|
||||
Undici always assumes that connections are persistent and will immediately
|
||||
pipeline requests, without checking whether the connection is persistent.
|
||||
Hence, automatic fallback to HTTP/1.0 or HTTP/1.1 without pipelining is
|
||||
not supported.
|
||||
|
||||
Undici will immediately pipeline when retrying requests after a failed
|
||||
connection. However, Undici will not retry the first remaining requests in
|
||||
the prior pipeline and instead error the corresponding callback/promise/stream.
|
||||
|
||||
Undici will abort all running requests in the pipeline when any of them are
|
||||
aborted.
|
||||
|
||||
* Refs: https://tools.ietf.org/html/rfc2616#section-8.1.2.2
|
||||
* Refs: https://tools.ietf.org/html/rfc7230#section-6.3.2
|
||||
|
||||
### Manual Redirect
|
||||
|
||||
Since it is not possible to manually follow an HTTP redirect on the server-side,
|
||||
Undici returns the actual response instead of an `opaqueredirect` filtered one
|
||||
when invoked with a `manual` redirect. This aligns `fetch()` with the other
|
||||
implementations in Deno and Cloudflare Workers.
|
||||
|
||||
Refs: https://fetch.spec.whatwg.org/#atomic-http-redirect-handling
|
||||
|
||||
## Workarounds
|
||||
|
||||
### Network address family autoselection.
|
||||
|
||||
If you experience problem when connecting to a remote server that is resolved by your DNS servers to a IPv6 (AAAA record)
|
||||
first, there are chances that your local router or ISP might have problem connecting to IPv6 networks. In that case
|
||||
undici will throw an error with code `UND_ERR_CONNECT_TIMEOUT`.
|
||||
|
||||
If the target server resolves to both a IPv6 and IPv4 (A records) address and you are using a compatible Node version
|
||||
(18.3.0 and above), you can fix the problem by providing the `autoSelectFamily` option (support by both `undici.request`
|
||||
and `undici.Agent`) which will enable the family autoselection algorithm when establishing the connection.
|
||||
|
||||
## Collaborators
|
||||
|
||||
* [__Daniele Belardi__](https://github.com/dnlup), <https://www.npmjs.com/~dnlup>
|
||||
* [__Ethan Arrowood__](https://github.com/ethan-arrowood), <https://www.npmjs.com/~ethan_arrowood>
|
||||
* [__Matteo Collina__](https://github.com/mcollina), <https://www.npmjs.com/~matteo.collina>
|
||||
* [__Matthew Aitken__](https://github.com/KhafraDev), <https://www.npmjs.com/~khaf>
|
||||
* [__Robert Nagy__](https://github.com/ronag), <https://www.npmjs.com/~ronag>
|
||||
* [__Szymon Marczak__](https://github.com/szmarczak), <https://www.npmjs.com/~szmarczak>
|
||||
* [__Tomas Della Vedova__](https://github.com/delvedor), <https://www.npmjs.com/~delvedor>
|
||||
|
||||
### Releasers
|
||||
|
||||
* [__Ethan Arrowood__](https://github.com/ethan-arrowood), <https://www.npmjs.com/~ethan_arrowood>
|
||||
* [__Matteo Collina__](https://github.com/mcollina), <https://www.npmjs.com/~matteo.collina>
|
||||
* [__Robert Nagy__](https://github.com/ronag), <https://www.npmjs.com/~ronag>
|
||||
* [__Matthew Aitken__](https://github.com/KhafraDev), <https://www.npmjs.com/~khaf>
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
|
@ -0,0 +1,15 @@
|
|||
'use strict'
|
||||
|
||||
const fetchImpl = require('./lib/fetch').fetch
|
||||
|
||||
module.exports.fetch = function fetch (resource, init = undefined) {
|
||||
return fetchImpl(resource, init).catch((err) => {
|
||||
Error.captureStackTrace(err, this)
|
||||
throw err
|
||||
})
|
||||
}
|
||||
module.exports.FormData = require('./lib/fetch/formdata').FormData
|
||||
module.exports.Headers = require('./lib/fetch/headers').Headers
|
||||
module.exports.Response = require('./lib/fetch/response').Response
|
||||
module.exports.Request = require('./lib/fetch/request').Request
|
||||
module.exports.WebSocket = require('./lib/websocket/websocket').WebSocket
|
|
@ -0,0 +1,3 @@
|
|||
export * from './types/index'
|
||||
import Undici from './types/index'
|
||||
export default Undici
|
|
@ -0,0 +1,165 @@
|
|||
'use strict'
|
||||
|
||||
const Client = require('./lib/client')
|
||||
const Dispatcher = require('./lib/dispatcher')
|
||||
const errors = require('./lib/core/errors')
|
||||
const Pool = require('./lib/pool')
|
||||
const BalancedPool = require('./lib/balanced-pool')
|
||||
const Agent = require('./lib/agent')
|
||||
const util = require('./lib/core/util')
|
||||
const { InvalidArgumentError } = errors
|
||||
const api = require('./lib/api')
|
||||
const buildConnector = require('./lib/core/connect')
|
||||
const MockClient = require('./lib/mock/mock-client')
|
||||
const MockAgent = require('./lib/mock/mock-agent')
|
||||
const MockPool = require('./lib/mock/mock-pool')
|
||||
const mockErrors = require('./lib/mock/mock-errors')
|
||||
const ProxyAgent = require('./lib/proxy-agent')
|
||||
const { getGlobalDispatcher, setGlobalDispatcher } = require('./lib/global')
|
||||
const DecoratorHandler = require('./lib/handler/DecoratorHandler')
|
||||
const RedirectHandler = require('./lib/handler/RedirectHandler')
|
||||
const createRedirectInterceptor = require('./lib/interceptor/redirectInterceptor')
|
||||
|
||||
let hasCrypto
|
||||
try {
|
||||
require('crypto')
|
||||
hasCrypto = true
|
||||
} catch {
|
||||
hasCrypto = false
|
||||
}
|
||||
|
||||
Object.assign(Dispatcher.prototype, api)
|
||||
|
||||
module.exports.Dispatcher = Dispatcher
|
||||
module.exports.Client = Client
|
||||
module.exports.Pool = Pool
|
||||
module.exports.BalancedPool = BalancedPool
|
||||
module.exports.Agent = Agent
|
||||
module.exports.ProxyAgent = ProxyAgent
|
||||
|
||||
module.exports.DecoratorHandler = DecoratorHandler
|
||||
module.exports.RedirectHandler = RedirectHandler
|
||||
module.exports.createRedirectInterceptor = createRedirectInterceptor
|
||||
|
||||
module.exports.buildConnector = buildConnector
|
||||
module.exports.errors = errors
|
||||
|
||||
function makeDispatcher (fn) {
|
||||
return (url, opts, handler) => {
|
||||
if (typeof opts === 'function') {
|
||||
handler = opts
|
||||
opts = null
|
||||
}
|
||||
|
||||
if (!url || (typeof url !== 'string' && typeof url !== 'object' && !(url instanceof URL))) {
|
||||
throw new InvalidArgumentError('invalid url')
|
||||
}
|
||||
|
||||
if (opts != null && typeof opts !== 'object') {
|
||||
throw new InvalidArgumentError('invalid opts')
|
||||
}
|
||||
|
||||
if (opts && opts.path != null) {
|
||||
if (typeof opts.path !== 'string') {
|
||||
throw new InvalidArgumentError('invalid opts.path')
|
||||
}
|
||||
|
||||
let path = opts.path
|
||||
if (!opts.path.startsWith('/')) {
|
||||
path = `/${path}`
|
||||
}
|
||||
|
||||
url = new URL(util.parseOrigin(url).origin + path)
|
||||
} else {
|
||||
if (!opts) {
|
||||
opts = typeof url === 'object' ? url : {}
|
||||
}
|
||||
|
||||
url = util.parseURL(url)
|
||||
}
|
||||
|
||||
const { agent, dispatcher = getGlobalDispatcher() } = opts
|
||||
|
||||
if (agent) {
|
||||
throw new InvalidArgumentError('unsupported opts.agent. Did you mean opts.client?')
|
||||
}
|
||||
|
||||
return fn.call(dispatcher, {
|
||||
...opts,
|
||||
origin: url.origin,
|
||||
path: url.search ? `${url.pathname}${url.search}` : url.pathname,
|
||||
method: opts.method || (opts.body ? 'PUT' : 'GET')
|
||||
}, handler)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports.setGlobalDispatcher = setGlobalDispatcher
|
||||
module.exports.getGlobalDispatcher = getGlobalDispatcher
|
||||
|
||||
if (util.nodeMajor > 16 || (util.nodeMajor === 16 && util.nodeMinor >= 8)) {
|
||||
let fetchImpl = null
|
||||
module.exports.fetch = async function fetch (resource) {
|
||||
if (!fetchImpl) {
|
||||
fetchImpl = require('./lib/fetch').fetch
|
||||
}
|
||||
|
||||
try {
|
||||
return await fetchImpl(...arguments)
|
||||
} catch (err) {
|
||||
if (typeof err === 'object') {
|
||||
Error.captureStackTrace(err, this)
|
||||
}
|
||||
|
||||
throw err
|
||||
}
|
||||
}
|
||||
module.exports.Headers = require('./lib/fetch/headers').Headers
|
||||
module.exports.Response = require('./lib/fetch/response').Response
|
||||
module.exports.Request = require('./lib/fetch/request').Request
|
||||
module.exports.FormData = require('./lib/fetch/formdata').FormData
|
||||
module.exports.File = require('./lib/fetch/file').File
|
||||
module.exports.FileReader = require('./lib/fileapi/filereader').FileReader
|
||||
|
||||
const { setGlobalOrigin, getGlobalOrigin } = require('./lib/fetch/global')
|
||||
|
||||
module.exports.setGlobalOrigin = setGlobalOrigin
|
||||
module.exports.getGlobalOrigin = getGlobalOrigin
|
||||
|
||||
const { CacheStorage } = require('./lib/cache/cachestorage')
|
||||
const { kConstruct } = require('./lib/cache/symbols')
|
||||
|
||||
// Cache & CacheStorage are tightly coupled with fetch. Even if it may run
|
||||
// in an older version of Node, it doesn't have any use without fetch.
|
||||
module.exports.caches = new CacheStorage(kConstruct)
|
||||
}
|
||||
|
||||
if (util.nodeMajor >= 16) {
|
||||
const { deleteCookie, getCookies, getSetCookies, setCookie } = require('./lib/cookies')
|
||||
|
||||
module.exports.deleteCookie = deleteCookie
|
||||
module.exports.getCookies = getCookies
|
||||
module.exports.getSetCookies = getSetCookies
|
||||
module.exports.setCookie = setCookie
|
||||
|
||||
const { parseMIMEType, serializeAMimeType } = require('./lib/fetch/dataURL')
|
||||
|
||||
module.exports.parseMIMEType = parseMIMEType
|
||||
module.exports.serializeAMimeType = serializeAMimeType
|
||||
}
|
||||
|
||||
if (util.nodeMajor >= 18 && hasCrypto) {
|
||||
const { WebSocket } = require('./lib/websocket/websocket')
|
||||
|
||||
module.exports.WebSocket = WebSocket
|
||||
}
|
||||
|
||||
module.exports.request = makeDispatcher(api.request)
|
||||
module.exports.stream = makeDispatcher(api.stream)
|
||||
module.exports.pipeline = makeDispatcher(api.pipeline)
|
||||
module.exports.connect = makeDispatcher(api.connect)
|
||||
module.exports.upgrade = makeDispatcher(api.upgrade)
|
||||
|
||||
module.exports.MockClient = MockClient
|
||||
module.exports.MockPool = MockPool
|
||||
module.exports.MockAgent = MockAgent
|
||||
module.exports.mockErrors = mockErrors
|
|
@ -0,0 +1,148 @@
|
|||
'use strict'
|
||||
|
||||
const { InvalidArgumentError } = require('./core/errors')
|
||||
const { kClients, kRunning, kClose, kDestroy, kDispatch, kInterceptors } = require('./core/symbols')
|
||||
const DispatcherBase = require('./dispatcher-base')
|
||||
const Pool = require('./pool')
|
||||
const Client = require('./client')
|
||||
const util = require('./core/util')
|
||||
const createRedirectInterceptor = require('./interceptor/redirectInterceptor')
|
||||
const { WeakRef, FinalizationRegistry } = require('./compat/dispatcher-weakref')()
|
||||
|
||||
const kOnConnect = Symbol('onConnect')
|
||||
const kOnDisconnect = Symbol('onDisconnect')
|
||||
const kOnConnectionError = Symbol('onConnectionError')
|
||||
const kMaxRedirections = Symbol('maxRedirections')
|
||||
const kOnDrain = Symbol('onDrain')
|
||||
const kFactory = Symbol('factory')
|
||||
const kFinalizer = Symbol('finalizer')
|
||||
const kOptions = Symbol('options')
|
||||
|
||||
function defaultFactory (origin, opts) {
|
||||
return opts && opts.connections === 1
|
||||
? new Client(origin, opts)
|
||||
: new Pool(origin, opts)
|
||||
}
|
||||
|
||||
class Agent extends DispatcherBase {
|
||||
constructor ({ factory = defaultFactory, maxRedirections = 0, connect, ...options } = {}) {
|
||||
super()
|
||||
|
||||
if (typeof factory !== 'function') {
|
||||
throw new InvalidArgumentError('factory must be a function.')
|
||||
}
|
||||
|
||||
if (connect != null && typeof connect !== 'function' && typeof connect !== 'object') {
|
||||
throw new InvalidArgumentError('connect must be a function or an object')
|
||||
}
|
||||
|
||||
if (!Number.isInteger(maxRedirections) || maxRedirections < 0) {
|
||||
throw new InvalidArgumentError('maxRedirections must be a positive number')
|
||||
}
|
||||
|
||||
if (connect && typeof connect !== 'function') {
|
||||
connect = { ...connect }
|
||||
}
|
||||
|
||||
this[kInterceptors] = options.interceptors && options.interceptors.Agent && Array.isArray(options.interceptors.Agent)
|
||||
? options.interceptors.Agent
|
||||
: [createRedirectInterceptor({ maxRedirections })]
|
||||
|
||||
this[kOptions] = { ...util.deepClone(options), connect }
|
||||
this[kOptions].interceptors = options.interceptors
|
||||
? { ...options.interceptors }
|
||||
: undefined
|
||||
this[kMaxRedirections] = maxRedirections
|
||||
this[kFactory] = factory
|
||||
this[kClients] = new Map()
|
||||
this[kFinalizer] = new FinalizationRegistry(/* istanbul ignore next: gc is undeterministic */ key => {
|
||||
const ref = this[kClients].get(key)
|
||||
if (ref !== undefined && ref.deref() === undefined) {
|
||||
this[kClients].delete(key)
|
||||
}
|
||||
})
|
||||
|
||||
const agent = this
|
||||
|
||||
this[kOnDrain] = (origin, targets) => {
|
||||
agent.emit('drain', origin, [agent, ...targets])
|
||||
}
|
||||
|
||||
this[kOnConnect] = (origin, targets) => {
|
||||
agent.emit('connect', origin, [agent, ...targets])
|
||||
}
|
||||
|
||||
this[kOnDisconnect] = (origin, targets, err) => {
|
||||
agent.emit('disconnect', origin, [agent, ...targets], err)
|
||||
}
|
||||
|
||||
this[kOnConnectionError] = (origin, targets, err) => {
|
||||
agent.emit('connectionError', origin, [agent, ...targets], err)
|
||||
}
|
||||
}
|
||||
|
||||
get [kRunning] () {
|
||||
let ret = 0
|
||||
for (const ref of this[kClients].values()) {
|
||||
const client = ref.deref()
|
||||
/* istanbul ignore next: gc is undeterministic */
|
||||
if (client) {
|
||||
ret += client[kRunning]
|
||||
}
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
[kDispatch] (opts, handler) {
|
||||
let key
|
||||
if (opts.origin && (typeof opts.origin === 'string' || opts.origin instanceof URL)) {
|
||||
key = String(opts.origin)
|
||||
} else {
|
||||
throw new InvalidArgumentError('opts.origin must be a non-empty string or URL.')
|
||||
}
|
||||
|
||||
const ref = this[kClients].get(key)
|
||||
|
||||
let dispatcher = ref ? ref.deref() : null
|
||||
if (!dispatcher) {
|
||||
dispatcher = this[kFactory](opts.origin, this[kOptions])
|
||||
.on('drain', this[kOnDrain])
|
||||
.on('connect', this[kOnConnect])
|
||||
.on('disconnect', this[kOnDisconnect])
|
||||
.on('connectionError', this[kOnConnectionError])
|
||||
|
||||
this[kClients].set(key, new WeakRef(dispatcher))
|
||||
this[kFinalizer].register(dispatcher, key)
|
||||
}
|
||||
|
||||
return dispatcher.dispatch(opts, handler)
|
||||
}
|
||||
|
||||
async [kClose] () {
|
||||
const closePromises = []
|
||||
for (const ref of this[kClients].values()) {
|
||||
const client = ref.deref()
|
||||
/* istanbul ignore else: gc is undeterministic */
|
||||
if (client) {
|
||||
closePromises.push(client.close())
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.all(closePromises)
|
||||
}
|
||||
|
||||
async [kDestroy] (err) {
|
||||
const destroyPromises = []
|
||||
for (const ref of this[kClients].values()) {
|
||||
const client = ref.deref()
|
||||
/* istanbul ignore else: gc is undeterministic */
|
||||
if (client) {
|
||||
destroyPromises.push(client.destroy(err))
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.all(destroyPromises)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Agent
|
|
@ -0,0 +1,54 @@
|
|||
const { addAbortListener } = require('../core/util')
|
||||
const { RequestAbortedError } = require('../core/errors')
|
||||
|
||||
const kListener = Symbol('kListener')
|
||||
const kSignal = Symbol('kSignal')
|
||||
|
||||
function abort (self) {
|
||||
if (self.abort) {
|
||||
self.abort()
|
||||
} else {
|
||||
self.onError(new RequestAbortedError())
|
||||
}
|
||||
}
|
||||
|
||||
function addSignal (self, signal) {
|
||||
self[kSignal] = null
|
||||
self[kListener] = null
|
||||
|
||||
if (!signal) {
|
||||
return
|
||||
}
|
||||
|
||||
if (signal.aborted) {
|
||||
abort(self)
|
||||
return
|
||||
}
|
||||
|
||||
self[kSignal] = signal
|
||||
self[kListener] = () => {
|
||||
abort(self)
|
||||
}
|
||||
|
||||
addAbortListener(self[kSignal], self[kListener])
|
||||
}
|
||||
|
||||
function removeSignal (self) {
|
||||
if (!self[kSignal]) {
|
||||
return
|
||||
}
|
||||
|
||||
if ('removeEventListener' in self[kSignal]) {
|
||||
self[kSignal].removeEventListener('abort', self[kListener])
|
||||
} else {
|
||||
self[kSignal].removeListener('abort', self[kListener])
|
||||
}
|
||||
|
||||
self[kSignal] = null
|
||||
self[kListener] = null
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
addSignal,
|
||||
removeSignal
|
||||
}
|
|
@ -0,0 +1,104 @@
|
|||
'use strict'
|
||||
|
||||
const { AsyncResource } = require('async_hooks')
|
||||
const { InvalidArgumentError, RequestAbortedError, SocketError } = require('../core/errors')
|
||||
const util = require('../core/util')
|
||||
const { addSignal, removeSignal } = require('./abort-signal')
|
||||
|
||||
class ConnectHandler extends AsyncResource {
|
||||
constructor (opts, callback) {
|
||||
if (!opts || typeof opts !== 'object') {
|
||||
throw new InvalidArgumentError('invalid opts')
|
||||
}
|
||||
|
||||
if (typeof callback !== 'function') {
|
||||
throw new InvalidArgumentError('invalid callback')
|
||||
}
|
||||
|
||||
const { signal, opaque, responseHeaders } = opts
|
||||
|
||||
if (signal && typeof signal.on !== 'function' && typeof signal.addEventListener !== 'function') {
|
||||
throw new InvalidArgumentError('signal must be an EventEmitter or EventTarget')
|
||||
}
|
||||
|
||||
super('UNDICI_CONNECT')
|
||||
|
||||
this.opaque = opaque || null
|
||||
this.responseHeaders = responseHeaders || null
|
||||
this.callback = callback
|
||||
this.abort = null
|
||||
|
||||
addSignal(this, signal)
|
||||
}
|
||||
|
||||
onConnect (abort, context) {
|
||||
if (!this.callback) {
|
||||
throw new RequestAbortedError()
|
||||
}
|
||||
|
||||
this.abort = abort
|
||||
this.context = context
|
||||
}
|
||||
|
||||
onHeaders () {
|
||||
throw new SocketError('bad connect', null)
|
||||
}
|
||||
|
||||
onUpgrade (statusCode, rawHeaders, socket) {
|
||||
const { callback, opaque, context } = this
|
||||
|
||||
removeSignal(this)
|
||||
|
||||
this.callback = null
|
||||
|
||||
let headers = rawHeaders
|
||||
// Indicates is an HTTP2Session
|
||||
if (headers != null) {
|
||||
headers = this.responseHeaders === 'raw' ? util.parseRawHeaders(rawHeaders) : util.parseHeaders(rawHeaders)
|
||||
}
|
||||
|
||||
this.runInAsyncScope(callback, null, null, {
|
||||
statusCode,
|
||||
headers,
|
||||
socket,
|
||||
opaque,
|
||||
context
|
||||
})
|
||||
}
|
||||
|
||||
onError (err) {
|
||||
const { callback, opaque } = this
|
||||
|
||||
removeSignal(this)
|
||||
|
||||
if (callback) {
|
||||
this.callback = null
|
||||
queueMicrotask(() => {
|
||||
this.runInAsyncScope(callback, null, err, { opaque })
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function connect (opts, callback) {
|
||||
if (callback === undefined) {
|
||||
return new Promise((resolve, reject) => {
|
||||
connect.call(this, opts, (err, data) => {
|
||||
return err ? reject(err) : resolve(data)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
try {
|
||||
const connectHandler = new ConnectHandler(opts, callback)
|
||||
this.dispatch({ ...opts, method: 'CONNECT' }, connectHandler)
|
||||
} catch (err) {
|
||||
if (typeof callback !== 'function') {
|
||||
throw err
|
||||
}
|
||||
const opaque = opts && opts.opaque
|
||||
queueMicrotask(() => callback(err, { opaque }))
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = connect
|
|
@ -0,0 +1,249 @@
|
|||
'use strict'
|
||||
|
||||
const {
|
||||
Readable,
|
||||
Duplex,
|
||||
PassThrough
|
||||
} = require('stream')
|
||||
const {
|
||||
InvalidArgumentError,
|
||||
InvalidReturnValueError,
|
||||
RequestAbortedError
|
||||
} = require('../core/errors')
|
||||
const util = require('../core/util')
|
||||
const { AsyncResource } = require('async_hooks')
|
||||
const { addSignal, removeSignal } = require('./abort-signal')
|
||||
const assert = require('assert')
|
||||
|
||||
const kResume = Symbol('resume')
|
||||
|
||||
class PipelineRequest extends Readable {
|
||||
constructor () {
|
||||
super({ autoDestroy: true })
|
||||
|
||||
this[kResume] = null
|
||||
}
|
||||
|
||||
_read () {
|
||||
const { [kResume]: resume } = this
|
||||
|
||||
if (resume) {
|
||||
this[kResume] = null
|
||||
resume()
|
||||
}
|
||||
}
|
||||
|
||||
_destroy (err, callback) {
|
||||
this._read()
|
||||
|
||||
callback(err)
|
||||
}
|
||||
}
|
||||
|
||||
class PipelineResponse extends Readable {
|
||||
constructor (resume) {
|
||||
super({ autoDestroy: true })
|
||||
this[kResume] = resume
|
||||
}
|
||||
|
||||
_read () {
|
||||
this[kResume]()
|
||||
}
|
||||
|
||||
_destroy (err, callback) {
|
||||
if (!err && !this._readableState.endEmitted) {
|
||||
err = new RequestAbortedError()
|
||||
}
|
||||
|
||||
callback(err)
|
||||
}
|
||||
}
|
||||
|
||||
class PipelineHandler extends AsyncResource {
|
||||
constructor (opts, handler) {
|
||||
if (!opts || typeof opts !== 'object') {
|
||||
throw new InvalidArgumentError('invalid opts')
|
||||
}
|
||||
|
||||
if (typeof handler !== 'function') {
|
||||
throw new InvalidArgumentError('invalid handler')
|
||||
}
|
||||
|
||||
const { signal, method, opaque, onInfo, responseHeaders } = opts
|
||||
|
||||
if (signal && typeof signal.on !== 'function' && typeof signal.addEventListener !== 'function') {
|
||||
throw new InvalidArgumentError('signal must be an EventEmitter or EventTarget')
|
||||
}
|
||||
|
||||
if (method === 'CONNECT') {
|
||||
throw new InvalidArgumentError('invalid method')
|
||||
}
|
||||
|
||||
if (onInfo && typeof onInfo !== 'function') {
|
||||
throw new InvalidArgumentError('invalid onInfo callback')
|
||||
}
|
||||
|
||||
super('UNDICI_PIPELINE')
|
||||
|
||||
this.opaque = opaque || null
|
||||
this.responseHeaders = responseHeaders || null
|
||||
this.handler = handler
|
||||
this.abort = null
|
||||
this.context = null
|
||||
this.onInfo = onInfo || null
|
||||
|
||||
this.req = new PipelineRequest().on('error', util.nop)
|
||||
|
||||
this.ret = new Duplex({
|
||||
readableObjectMode: opts.objectMode,
|
||||
autoDestroy: true,
|
||||
read: () => {
|
||||
const { body } = this
|
||||
|
||||
if (body && body.resume) {
|
||||
body.resume()
|
||||
}
|
||||
},
|
||||
write: (chunk, encoding, callback) => {
|
||||
const { req } = this
|
||||
|
||||
if (req.push(chunk, encoding) || req._readableState.destroyed) {
|
||||
callback()
|
||||
} else {
|
||||
req[kResume] = callback
|
||||
}
|
||||
},
|
||||
destroy: (err, callback) => {
|
||||
const { body, req, res, ret, abort } = this
|
||||
|
||||
if (!err && !ret._readableState.endEmitted) {
|
||||
err = new RequestAbortedError()
|
||||
}
|
||||
|
||||
if (abort && err) {
|
||||
abort()
|
||||
}
|
||||
|
||||
util.destroy(body, err)
|
||||
util.destroy(req, err)
|
||||
util.destroy(res, err)
|
||||
|
||||
removeSignal(this)
|
||||
|
||||
callback(err)
|
||||
}
|
||||
}).on('prefinish', () => {
|
||||
const { req } = this
|
||||
|
||||
// Node < 15 does not call _final in same tick.
|
||||
req.push(null)
|
||||
})
|
||||
|
||||
this.res = null
|
||||
|
||||
addSignal(this, signal)
|
||||
}
|
||||
|
||||
onConnect (abort, context) {
|
||||
const { ret, res } = this
|
||||
|
||||
assert(!res, 'pipeline cannot be retried')
|
||||
|
||||
if (ret.destroyed) {
|
||||
throw new RequestAbortedError()
|
||||
}
|
||||
|
||||
this.abort = abort
|
||||
this.context = context
|
||||
}
|
||||
|
||||
onHeaders (statusCode, rawHeaders, resume) {
|
||||
const { opaque, handler, context } = this
|
||||
|
||||
if (statusCode < 200) {
|
||||
if (this.onInfo) {
|
||||
const headers = this.responseHeaders === 'raw' ? util.parseRawHeaders(rawHeaders) : util.parseHeaders(rawHeaders)
|
||||
this.onInfo({ statusCode, headers })
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
this.res = new PipelineResponse(resume)
|
||||
|
||||
let body
|
||||
try {
|
||||
this.handler = null
|
||||
const headers = this.responseHeaders === 'raw' ? util.parseRawHeaders(rawHeaders) : util.parseHeaders(rawHeaders)
|
||||
body = this.runInAsyncScope(handler, null, {
|
||||
statusCode,
|
||||
headers,
|
||||
opaque,
|
||||
body: this.res,
|
||||
context
|
||||
})
|
||||
} catch (err) {
|
||||
this.res.on('error', util.nop)
|
||||
throw err
|
||||
}
|
||||
|
||||
if (!body || typeof body.on !== 'function') {
|
||||
throw new InvalidReturnValueError('expected Readable')
|
||||
}
|
||||
|
||||
body
|
||||
.on('data', (chunk) => {
|
||||
const { ret, body } = this
|
||||
|
||||
if (!ret.push(chunk) && body.pause) {
|
||||
body.pause()
|
||||
}
|
||||
})
|
||||
.on('error', (err) => {
|
||||
const { ret } = this
|
||||
|
||||
util.destroy(ret, err)
|
||||
})
|
||||
.on('end', () => {
|
||||
const { ret } = this
|
||||
|
||||
ret.push(null)
|
||||
})
|
||||
.on('close', () => {
|
||||
const { ret } = this
|
||||
|
||||
if (!ret._readableState.ended) {
|
||||
util.destroy(ret, new RequestAbortedError())
|
||||
}
|
||||
})
|
||||
|
||||
this.body = body
|
||||
}
|
||||
|
||||
onData (chunk) {
|
||||
const { res } = this
|
||||
return res.push(chunk)
|
||||
}
|
||||
|
||||
onComplete (trailers) {
|
||||
const { res } = this
|
||||
res.push(null)
|
||||
}
|
||||
|
||||
onError (err) {
|
||||
const { ret } = this
|
||||
this.handler = null
|
||||
util.destroy(ret, err)
|
||||
}
|
||||
}
|
||||
|
||||
function pipeline (opts, handler) {
|
||||
try {
|
||||
const pipelineHandler = new PipelineHandler(opts, handler)
|
||||
this.dispatch({ ...opts, body: pipelineHandler.req }, pipelineHandler)
|
||||
return pipelineHandler.ret
|
||||
} catch (err) {
|
||||
return new PassThrough().destroy(err)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = pipeline
|
|
@ -0,0 +1,179 @@
|
|||
'use strict'
|
||||
|
||||
const Readable = require('./readable')
|
||||
const {
|
||||
InvalidArgumentError,
|
||||
RequestAbortedError
|
||||
} = require('../core/errors')
|
||||
const util = require('../core/util')
|
||||
const { getResolveErrorBodyCallback } = require('./util')
|
||||
const { AsyncResource } = require('async_hooks')
|
||||
const { addSignal, removeSignal } = require('./abort-signal')
|
||||
|
||||
class RequestHandler extends AsyncResource {
|
||||
constructor (opts, callback) {
|
||||
if (!opts || typeof opts !== 'object') {
|
||||
throw new InvalidArgumentError('invalid opts')
|
||||
}
|
||||
|
||||
const { signal, method, opaque, body, onInfo, responseHeaders, throwOnError, highWaterMark } = opts
|
||||
|
||||
try {
|
||||
if (typeof callback !== 'function') {
|
||||
throw new InvalidArgumentError('invalid callback')
|
||||
}
|
||||
|
||||
if (highWaterMark && (typeof highWaterMark !== 'number' || highWaterMark < 0)) {
|
||||
throw new InvalidArgumentError('invalid highWaterMark')
|
||||
}
|
||||
|
||||
if (signal && typeof signal.on !== 'function' && typeof signal.addEventListener !== 'function') {
|
||||
throw new InvalidArgumentError('signal must be an EventEmitter or EventTarget')
|
||||
}
|
||||
|
||||
if (method === 'CONNECT') {
|
||||
throw new InvalidArgumentError('invalid method')
|
||||
}
|
||||
|
||||
if (onInfo && typeof onInfo !== 'function') {
|
||||
throw new InvalidArgumentError('invalid onInfo callback')
|
||||
}
|
||||
|
||||
super('UNDICI_REQUEST')
|
||||
} catch (err) {
|
||||
if (util.isStream(body)) {
|
||||
util.destroy(body.on('error', util.nop), err)
|
||||
}
|
||||
throw err
|
||||
}
|
||||
|
||||
this.responseHeaders = responseHeaders || null
|
||||
this.opaque = opaque || null
|
||||
this.callback = callback
|
||||
this.res = null
|
||||
this.abort = null
|
||||
this.body = body
|
||||
this.trailers = {}
|
||||
this.context = null
|
||||
this.onInfo = onInfo || null
|
||||
this.throwOnError = throwOnError
|
||||
this.highWaterMark = highWaterMark
|
||||
|
||||
if (util.isStream(body)) {
|
||||
body.on('error', (err) => {
|
||||
this.onError(err)
|
||||
})
|
||||
}
|
||||
|
||||
addSignal(this, signal)
|
||||
}
|
||||
|
||||
onConnect (abort, context) {
|
||||
if (!this.callback) {
|
||||
throw new RequestAbortedError()
|
||||
}
|
||||
|
||||
this.abort = abort
|
||||
this.context = context
|
||||
}
|
||||
|
||||
onHeaders (statusCode, rawHeaders, resume, statusMessage) {
|
||||
const { callback, opaque, abort, context, responseHeaders, highWaterMark } = this
|
||||
|
||||
const headers = responseHeaders === 'raw' ? util.parseRawHeaders(rawHeaders) : util.parseHeaders(rawHeaders)
|
||||
|
||||
if (statusCode < 200) {
|
||||
if (this.onInfo) {
|
||||
this.onInfo({ statusCode, headers })
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
const parsedHeaders = responseHeaders === 'raw' ? util.parseHeaders(rawHeaders) : headers
|
||||
const contentType = parsedHeaders['content-type']
|
||||
const body = new Readable({ resume, abort, contentType, highWaterMark })
|
||||
|
||||
this.callback = null
|
||||
this.res = body
|
||||
if (callback !== null) {
|
||||
if (this.throwOnError && statusCode >= 400) {
|
||||
this.runInAsyncScope(getResolveErrorBodyCallback, null,
|
||||
{ callback, body, contentType, statusCode, statusMessage, headers }
|
||||
)
|
||||
} else {
|
||||
this.runInAsyncScope(callback, null, null, {
|
||||
statusCode,
|
||||
headers,
|
||||
trailers: this.trailers,
|
||||
opaque,
|
||||
body,
|
||||
context
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onData (chunk) {
|
||||
const { res } = this
|
||||
return res.push(chunk)
|
||||
}
|
||||
|
||||
onComplete (trailers) {
|
||||
const { res } = this
|
||||
|
||||
removeSignal(this)
|
||||
|
||||
util.parseHeaders(trailers, this.trailers)
|
||||
|
||||
res.push(null)
|
||||
}
|
||||
|
||||
onError (err) {
|
||||
const { res, callback, body, opaque } = this
|
||||
|
||||
removeSignal(this)
|
||||
|
||||
if (callback) {
|
||||
// TODO: Does this need queueMicrotask?
|
||||
this.callback = null
|
||||
queueMicrotask(() => {
|
||||
this.runInAsyncScope(callback, null, err, { opaque })
|
||||
})
|
||||
}
|
||||
|
||||
if (res) {
|
||||
this.res = null
|
||||
// Ensure all queued handlers are invoked before destroying res.
|
||||
queueMicrotask(() => {
|
||||
util.destroy(res, err)
|
||||
})
|
||||
}
|
||||
|
||||
if (body) {
|
||||
this.body = null
|
||||
util.destroy(body, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function request (opts, callback) {
|
||||
if (callback === undefined) {
|
||||
return new Promise((resolve, reject) => {
|
||||
request.call(this, opts, (err, data) => {
|
||||
return err ? reject(err) : resolve(data)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
try {
|
||||
this.dispatch(opts, new RequestHandler(opts, callback))
|
||||
} catch (err) {
|
||||
if (typeof callback !== 'function') {
|
||||
throw err
|
||||
}
|
||||
const opaque = opts && opts.opaque
|
||||
queueMicrotask(() => callback(err, { opaque }))
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = request
|
|
@ -0,0 +1,220 @@
|
|||
'use strict'
|
||||
|
||||
const { finished, PassThrough } = require('stream')
|
||||
const {
|
||||
InvalidArgumentError,
|
||||
InvalidReturnValueError,
|
||||
RequestAbortedError
|
||||
} = require('../core/errors')
|
||||
const util = require('../core/util')
|
||||
const { getResolveErrorBodyCallback } = require('./util')
|
||||
const { AsyncResource } = require('async_hooks')
|
||||
const { addSignal, removeSignal } = require('./abort-signal')
|
||||
|
||||
class StreamHandler extends AsyncResource {
|
||||
constructor (opts, factory, callback) {
|
||||
if (!opts || typeof opts !== 'object') {
|
||||
throw new InvalidArgumentError('invalid opts')
|
||||
}
|
||||
|
||||
const { signal, method, opaque, body, onInfo, responseHeaders, throwOnError } = opts
|
||||
|
||||
try {
|
||||
if (typeof callback !== 'function') {
|
||||
throw new InvalidArgumentError('invalid callback')
|
||||
}
|
||||
|
||||
if (typeof factory !== 'function') {
|
||||
throw new InvalidArgumentError('invalid factory')
|
||||
}
|
||||
|
||||
if (signal && typeof signal.on !== 'function' && typeof signal.addEventListener !== 'function') {
|
||||
throw new InvalidArgumentError('signal must be an EventEmitter or EventTarget')
|
||||
}
|
||||
|
||||
if (method === 'CONNECT') {
|
||||
throw new InvalidArgumentError('invalid method')
|
||||
}
|
||||
|
||||
if (onInfo && typeof onInfo !== 'function') {
|
||||
throw new InvalidArgumentError('invalid onInfo callback')
|
||||
}
|
||||
|
||||
super('UNDICI_STREAM')
|
||||
} catch (err) {
|
||||
if (util.isStream(body)) {
|
||||
util.destroy(body.on('error', util.nop), err)
|
||||
}
|
||||
throw err
|
||||
}
|
||||
|
||||
this.responseHeaders = responseHeaders || null
|
||||
this.opaque = opaque || null
|
||||
this.factory = factory
|
||||
this.callback = callback
|
||||
this.res = null
|
||||
this.abort = null
|
||||
this.context = null
|
||||
this.trailers = null
|
||||
this.body = body
|
||||
this.onInfo = onInfo || null
|
||||
this.throwOnError = throwOnError || false
|
||||
|
||||
if (util.isStream(body)) {
|
||||
body.on('error', (err) => {
|
||||
this.onError(err)
|
||||
})
|
||||
}
|
||||
|
||||
addSignal(this, signal)
|
||||
}
|
||||
|
||||
onConnect (abort, context) {
|
||||
if (!this.callback) {
|
||||
throw new RequestAbortedError()
|
||||
}
|
||||
|
||||
this.abort = abort
|
||||
this.context = context
|
||||
}
|
||||
|
||||
onHeaders (statusCode, rawHeaders, resume, statusMessage) {
|
||||
const { factory, opaque, context, callback, responseHeaders } = this
|
||||
|
||||
const headers = responseHeaders === 'raw' ? util.parseRawHeaders(rawHeaders) : util.parseHeaders(rawHeaders)
|
||||
|
||||
if (statusCode < 200) {
|
||||
if (this.onInfo) {
|
||||
this.onInfo({ statusCode, headers })
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
this.factory = null
|
||||
|
||||
let res
|
||||
|
||||
if (this.throwOnError && statusCode >= 400) {
|
||||
const parsedHeaders = responseHeaders === 'raw' ? util.parseHeaders(rawHeaders) : headers
|
||||
const contentType = parsedHeaders['content-type']
|
||||
res = new PassThrough()
|
||||
|
||||
this.callback = null
|
||||
this.runInAsyncScope(getResolveErrorBodyCallback, null,
|
||||
{ callback, body: res, contentType, statusCode, statusMessage, headers }
|
||||
)
|
||||
} else {
|
||||
if (factory === null) {
|
||||
return
|
||||
}
|
||||
|
||||
res = this.runInAsyncScope(factory, null, {
|
||||
statusCode,
|
||||
headers,
|
||||
opaque,
|
||||
context
|
||||
})
|
||||
|
||||
if (
|
||||
!res ||
|
||||
typeof res.write !== 'function' ||
|
||||
typeof res.end !== 'function' ||
|
||||
typeof res.on !== 'function'
|
||||
) {
|
||||
throw new InvalidReturnValueError('expected Writable')
|
||||
}
|
||||
|
||||
// TODO: Avoid finished. It registers an unnecessary amount of listeners.
|
||||
finished(res, { readable: false }, (err) => {
|
||||
const { callback, res, opaque, trailers, abort } = this
|
||||
|
||||
this.res = null
|
||||
if (err || !res.readable) {
|
||||
util.destroy(res, err)
|
||||
}
|
||||
|
||||
this.callback = null
|
||||
this.runInAsyncScope(callback, null, err || null, { opaque, trailers })
|
||||
|
||||
if (err) {
|
||||
abort()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
res.on('drain', resume)
|
||||
|
||||
this.res = res
|
||||
|
||||
const needDrain = res.writableNeedDrain !== undefined
|
||||
? res.writableNeedDrain
|
||||
: res._writableState && res._writableState.needDrain
|
||||
|
||||
return needDrain !== true
|
||||
}
|
||||
|
||||
onData (chunk) {
|
||||
const { res } = this
|
||||
|
||||
return res ? res.write(chunk) : true
|
||||
}
|
||||
|
||||
onComplete (trailers) {
|
||||
const { res } = this
|
||||
|
||||
removeSignal(this)
|
||||
|
||||
if (!res) {
|
||||
return
|
||||
}
|
||||
|
||||
this.trailers = util.parseHeaders(trailers)
|
||||
|
||||
res.end()
|
||||
}
|
||||
|
||||
onError (err) {
|
||||
const { res, callback, opaque, body } = this
|
||||
|
||||
removeSignal(this)
|
||||
|
||||
this.factory = null
|
||||
|
||||
if (res) {
|
||||
this.res = null
|
||||
util.destroy(res, err)
|
||||
} else if (callback) {
|
||||
this.callback = null
|
||||
queueMicrotask(() => {
|
||||
this.runInAsyncScope(callback, null, err, { opaque })
|
||||
})
|
||||
}
|
||||
|
||||
if (body) {
|
||||
this.body = null
|
||||
util.destroy(body, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function stream (opts, factory, callback) {
|
||||
if (callback === undefined) {
|
||||
return new Promise((resolve, reject) => {
|
||||
stream.call(this, opts, factory, (err, data) => {
|
||||
return err ? reject(err) : resolve(data)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
try {
|
||||
this.dispatch(opts, new StreamHandler(opts, factory, callback))
|
||||
} catch (err) {
|
||||
if (typeof callback !== 'function') {
|
||||
throw err
|
||||
}
|
||||
const opaque = opts && opts.opaque
|
||||
queueMicrotask(() => callback(err, { opaque }))
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = stream
|
|
@ -0,0 +1,105 @@
|
|||
'use strict'
|
||||
|
||||
const { InvalidArgumentError, RequestAbortedError, SocketError } = require('../core/errors')
|
||||
const { AsyncResource } = require('async_hooks')
|
||||
const util = require('../core/util')
|
||||
const { addSignal, removeSignal } = require('./abort-signal')
|
||||
const assert = require('assert')
|
||||
|
||||
class UpgradeHandler extends AsyncResource {
|
||||
constructor (opts, callback) {
|
||||
if (!opts || typeof opts !== 'object') {
|
||||
throw new InvalidArgumentError('invalid opts')
|
||||
}
|
||||
|
||||
if (typeof callback !== 'function') {
|
||||
throw new InvalidArgumentError('invalid callback')
|
||||
}
|
||||
|
||||
const { signal, opaque, responseHeaders } = opts
|
||||
|
||||
if (signal && typeof signal.on !== 'function' && typeof signal.addEventListener !== 'function') {
|
||||
throw new InvalidArgumentError('signal must be an EventEmitter or EventTarget')
|
||||
}
|
||||
|
||||
super('UNDICI_UPGRADE')
|
||||
|
||||
this.responseHeaders = responseHeaders || null
|
||||
this.opaque = opaque || null
|
||||
this.callback = callback
|
||||
this.abort = null
|
||||
this.context = null
|
||||
|
||||
addSignal(this, signal)
|
||||
}
|
||||
|
||||
onConnect (abort, context) {
|
||||
if (!this.callback) {
|
||||
throw new RequestAbortedError()
|
||||
}
|
||||
|
||||
this.abort = abort
|
||||
this.context = null
|
||||
}
|
||||
|
||||
onHeaders () {
|
||||
throw new SocketError('bad upgrade', null)
|
||||
}
|
||||
|
||||
onUpgrade (statusCode, rawHeaders, socket) {
|
||||
const { callback, opaque, context } = this
|
||||
|
||||
assert.strictEqual(statusCode, 101)
|
||||
|
||||
removeSignal(this)
|
||||
|
||||
this.callback = null
|
||||
const headers = this.responseHeaders === 'raw' ? util.parseRawHeaders(rawHeaders) : util.parseHeaders(rawHeaders)
|
||||
this.runInAsyncScope(callback, null, null, {
|
||||
headers,
|
||||
socket,
|
||||
opaque,
|
||||
context
|
||||
})
|
||||
}
|
||||
|
||||
onError (err) {
|
||||
const { callback, opaque } = this
|
||||
|
||||
removeSignal(this)
|
||||
|
||||
if (callback) {
|
||||
this.callback = null
|
||||
queueMicrotask(() => {
|
||||
this.runInAsyncScope(callback, null, err, { opaque })
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function upgrade (opts, callback) {
|
||||
if (callback === undefined) {
|
||||
return new Promise((resolve, reject) => {
|
||||
upgrade.call(this, opts, (err, data) => {
|
||||
return err ? reject(err) : resolve(data)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
try {
|
||||
const upgradeHandler = new UpgradeHandler(opts, callback)
|
||||
this.dispatch({
|
||||
...opts,
|
||||
method: opts.method || 'GET',
|
||||
upgrade: opts.protocol || 'Websocket'
|
||||
}, upgradeHandler)
|
||||
} catch (err) {
|
||||
if (typeof callback !== 'function') {
|
||||
throw err
|
||||
}
|
||||
const opaque = opts && opts.opaque
|
||||
queueMicrotask(() => callback(err, { opaque }))
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = upgrade
|
|
@ -0,0 +1,7 @@
|
|||
'use strict'
|
||||
|
||||
module.exports.request = require('./api-request')
|
||||
module.exports.stream = require('./api-stream')
|
||||
module.exports.pipeline = require('./api-pipeline')
|
||||
module.exports.upgrade = require('./api-upgrade')
|
||||
module.exports.connect = require('./api-connect')
|
|
@ -0,0 +1,307 @@
|
|||
// Ported from https://github.com/nodejs/undici/pull/907
|
||||
|
||||
'use strict'
|
||||
|
||||
const assert = require('assert')
|
||||
const { Readable } = require('stream')
|
||||
const { RequestAbortedError, NotSupportedError, InvalidArgumentError } = require('../core/errors')
|
||||
const util = require('../core/util')
|
||||
const { ReadableStreamFrom, toUSVString } = require('../core/util')
|
||||
|
||||
let Blob
|
||||
|
||||
const kConsume = Symbol('kConsume')
|
||||
const kReading = Symbol('kReading')
|
||||
const kBody = Symbol('kBody')
|
||||
const kAbort = Symbol('abort')
|
||||
const kContentType = Symbol('kContentType')
|
||||
|
||||
module.exports = class BodyReadable extends Readable {
|
||||
constructor ({
|
||||
resume,
|
||||
abort,
|
||||
contentType = '',
|
||||
highWaterMark = 64 * 1024 // Same as nodejs fs streams.
|
||||
}) {
|
||||
super({
|
||||
autoDestroy: true,
|
||||
read: resume,
|
||||
highWaterMark
|
||||
})
|
||||
|
||||
this._readableState.dataEmitted = false
|
||||
|
||||
this[kAbort] = abort
|
||||
this[kConsume] = null
|
||||
this[kBody] = null
|
||||
this[kContentType] = contentType
|
||||
|
||||
// Is stream being consumed through Readable API?
|
||||
// This is an optimization so that we avoid checking
|
||||
// for 'data' and 'readable' listeners in the hot path
|
||||
// inside push().
|
||||
this[kReading] = false
|
||||
}
|
||||
|
||||
destroy (err) {
|
||||
if (this.destroyed) {
|
||||
// Node < 16
|
||||
return this
|
||||
}
|
||||
|
||||
if (!err && !this._readableState.endEmitted) {
|
||||
err = new RequestAbortedError()
|
||||
}
|
||||
|
||||
if (err) {
|
||||
this[kAbort]()
|
||||
}
|
||||
|
||||
return super.destroy(err)
|
||||
}
|
||||
|
||||
emit (ev, ...args) {
|
||||
if (ev === 'data') {
|
||||
// Node < 16.7
|
||||
this._readableState.dataEmitted = true
|
||||
} else if (ev === 'error') {
|
||||
// Node < 16
|
||||
this._readableState.errorEmitted = true
|
||||
}
|
||||
return super.emit(ev, ...args)
|
||||
}
|
||||
|
||||
on (ev, ...args) {
|
||||
if (ev === 'data' || ev === 'readable') {
|
||||
this[kReading] = true
|
||||
}
|
||||
return super.on(ev, ...args)
|
||||
}
|
||||
|
||||
addListener (ev, ...args) {
|
||||
return this.on(ev, ...args)
|
||||
}
|
||||
|
||||
off (ev, ...args) {
|
||||
const ret = super.off(ev, ...args)
|
||||
if (ev === 'data' || ev === 'readable') {
|
||||
this[kReading] = (
|
||||
this.listenerCount('data') > 0 ||
|
||||
this.listenerCount('readable') > 0
|
||||
)
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
removeListener (ev, ...args) {
|
||||
return this.off(ev, ...args)
|
||||
}
|
||||
|
||||
push (chunk) {
|
||||
if (this[kConsume] && chunk !== null && this.readableLength === 0) {
|
||||
consumePush(this[kConsume], chunk)
|
||||
return this[kReading] ? super.push(chunk) : true
|
||||
}
|
||||
return super.push(chunk)
|
||||
}
|
||||
|
||||
// https://fetch.spec.whatwg.org/#dom-body-text
|
||||
async text () {
|
||||
return consume(this, 'text')
|
||||
}
|
||||
|
||||
// https://fetch.spec.whatwg.org/#dom-body-json
|
||||
async json () {
|
||||
return consume(this, 'json')
|
||||
}
|
||||
|
||||
// https://fetch.spec.whatwg.org/#dom-body-blob
|
||||
async blob () {
|
||||
return consume(this, 'blob')
|
||||
}
|
||||
|
||||
// https://fetch.spec.whatwg.org/#dom-body-arraybuffer
|
||||
async arrayBuffer () {
|
||||
return consume(this, 'arrayBuffer')
|
||||
}
|
||||
|
||||
// https://fetch.spec.whatwg.org/#dom-body-formdata
|
||||
async formData () {
|
||||
// TODO: Implement.
|
||||
throw new NotSupportedError()
|
||||
}
|
||||
|
||||
// https://fetch.spec.whatwg.org/#dom-body-bodyused
|
||||
get bodyUsed () {
|
||||
return util.isDisturbed(this)
|
||||
}
|
||||
|
||||
// https://fetch.spec.whatwg.org/#dom-body-body
|
||||
get body () {
|
||||
if (!this[kBody]) {
|
||||
this[kBody] = ReadableStreamFrom(this)
|
||||
if (this[kConsume]) {
|
||||
// TODO: Is this the best way to force a lock?
|
||||
this[kBody].getReader() // Ensure stream is locked.
|
||||
assert(this[kBody].locked)
|
||||
}
|
||||
}
|
||||
return this[kBody]
|
||||
}
|
||||
|
||||
async dump (opts) {
|
||||
let limit = opts && Number.isFinite(opts.limit) ? opts.limit : 262144
|
||||
const signal = opts && opts.signal
|
||||
const abortFn = () => {
|
||||
this.destroy()
|
||||
}
|
||||
let signalListenerCleanup
|
||||
if (signal) {
|
||||
if (typeof signal !== 'object' || !('aborted' in signal)) {
|
||||
throw new InvalidArgumentError('signal must be an AbortSignal')
|
||||
}
|
||||
util.throwIfAborted(signal)
|
||||
signalListenerCleanup = util.addAbortListener(signal, abortFn)
|
||||
}
|
||||
try {
|
||||
for await (const chunk of this) {
|
||||
util.throwIfAborted(signal)
|
||||
limit -= Buffer.byteLength(chunk)
|
||||
if (limit < 0) {
|
||||
return
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
util.throwIfAborted(signal)
|
||||
} finally {
|
||||
if (typeof signalListenerCleanup === 'function') {
|
||||
signalListenerCleanup()
|
||||
} else if (signalListenerCleanup) {
|
||||
signalListenerCleanup[Symbol.dispose]()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// https://streams.spec.whatwg.org/#readablestream-locked
|
||||
function isLocked (self) {
|
||||
// Consume is an implicit lock.
|
||||
return (self[kBody] && self[kBody].locked === true) || self[kConsume]
|
||||
}
|
||||
|
||||
// https://fetch.spec.whatwg.org/#body-unusable
|
||||
function isUnusable (self) {
|
||||
return util.isDisturbed(self) || isLocked(self)
|
||||
}
|
||||
|
||||
async function consume (stream, type) {
|
||||
if (isUnusable(stream)) {
|
||||
throw new TypeError('unusable')
|
||||
}
|
||||
|
||||
assert(!stream[kConsume])
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
stream[kConsume] = {
|
||||
type,
|
||||
stream,
|
||||
resolve,
|
||||
reject,
|
||||
length: 0,
|
||||
body: []
|
||||
}
|
||||
|
||||
stream
|
||||
.on('error', function (err) {
|
||||
consumeFinish(this[kConsume], err)
|
||||
})
|
||||
.on('close', function () {
|
||||
if (this[kConsume].body !== null) {
|
||||
consumeFinish(this[kConsume], new RequestAbortedError())
|
||||
}
|
||||
})
|
||||
|
||||
process.nextTick(consumeStart, stream[kConsume])
|
||||
})
|
||||
}
|
||||
|
||||
function consumeStart (consume) {
|
||||
if (consume.body === null) {
|
||||
return
|
||||
}
|
||||
|
||||
const { _readableState: state } = consume.stream
|
||||
|
||||
for (const chunk of state.buffer) {
|
||||
consumePush(consume, chunk)
|
||||
}
|
||||
|
||||
if (state.endEmitted) {
|
||||
consumeEnd(this[kConsume])
|
||||
} else {
|
||||
consume.stream.on('end', function () {
|
||||
consumeEnd(this[kConsume])
|
||||
})
|
||||
}
|
||||
|
||||
consume.stream.resume()
|
||||
|
||||
while (consume.stream.read() != null) {
|
||||
// Loop
|
||||
}
|
||||
}
|
||||
|
||||
function consumeEnd (consume) {
|
||||
const { type, body, resolve, stream, length } = consume
|
||||
|
||||
try {
|
||||
if (type === 'text') {
|
||||
resolve(toUSVString(Buffer.concat(body)))
|
||||
} else if (type === 'json') {
|
||||
resolve(JSON.parse(Buffer.concat(body)))
|
||||
} else if (type === 'arrayBuffer') {
|
||||
const dst = new Uint8Array(length)
|
||||
|
||||
let pos = 0
|
||||
for (const buf of body) {
|
||||
dst.set(buf, pos)
|
||||
pos += buf.byteLength
|
||||
}
|
||||
|
||||
resolve(dst.buffer)
|
||||
} else if (type === 'blob') {
|
||||
if (!Blob) {
|
||||
Blob = require('buffer').Blob
|
||||
}
|
||||
resolve(new Blob(body, { type: stream[kContentType] }))
|
||||
}
|
||||
|
||||
consumeFinish(consume)
|
||||
} catch (err) {
|
||||
stream.destroy(err)
|
||||
}
|
||||
}
|
||||
|
||||
function consumePush (consume, chunk) {
|
||||
consume.length += chunk.length
|
||||
consume.body.push(chunk)
|
||||
}
|
||||
|
||||
function consumeFinish (consume, err) {
|
||||
if (consume.body === null) {
|
||||
return
|
||||
}
|
||||
|
||||
if (err) {
|
||||
consume.reject(err)
|
||||
} else {
|
||||
consume.resolve()
|
||||
}
|
||||
|
||||
consume.type = null
|
||||
consume.stream = null
|
||||
consume.resolve = null
|
||||
consume.reject = null
|
||||
consume.length = 0
|
||||
consume.body = null
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
const assert = require('assert')
|
||||
const {
|
||||
ResponseStatusCodeError
|
||||
} = require('../core/errors')
|
||||
const { toUSVString } = require('../core/util')
|
||||
|
||||
async function getResolveErrorBodyCallback ({ callback, body, contentType, statusCode, statusMessage, headers }) {
|
||||
assert(body)
|
||||
|
||||
let chunks = []
|
||||
let limit = 0
|
||||
|
||||
for await (const chunk of body) {
|
||||
chunks.push(chunk)
|
||||
limit += chunk.length
|
||||
if (limit > 128 * 1024) {
|
||||
chunks = null
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (statusCode === 204 || !contentType || !chunks) {
|
||||
process.nextTick(callback, new ResponseStatusCodeError(`Response status code ${statusCode}${statusMessage ? `: ${statusMessage}` : ''}`, statusCode, headers))
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
if (contentType.startsWith('application/json')) {
|
||||
const payload = JSON.parse(toUSVString(Buffer.concat(chunks)))
|
||||
process.nextTick(callback, new ResponseStatusCodeError(`Response status code ${statusCode}${statusMessage ? `: ${statusMessage}` : ''}`, statusCode, headers, payload))
|
||||
return
|
||||
}
|
||||
|
||||
if (contentType.startsWith('text/')) {
|
||||
const payload = toUSVString(Buffer.concat(chunks))
|
||||
process.nextTick(callback, new ResponseStatusCodeError(`Response status code ${statusCode}${statusMessage ? `: ${statusMessage}` : ''}`, statusCode, headers, payload))
|
||||
return
|
||||
}
|
||||
} catch (err) {
|
||||
// Process in a fallback if error
|
||||
}
|
||||
|
||||
process.nextTick(callback, new ResponseStatusCodeError(`Response status code ${statusCode}${statusMessage ? `: ${statusMessage}` : ''}`, statusCode, headers))
|
||||
}
|
||||
|
||||
module.exports = { getResolveErrorBodyCallback }
|
|
@ -0,0 +1,190 @@
|
|||
'use strict'
|
||||
|
||||
const {
|
||||
BalancedPoolMissingUpstreamError,
|
||||
InvalidArgumentError
|
||||
} = require('./core/errors')
|
||||
const {
|
||||
PoolBase,
|
||||
kClients,
|
||||
kNeedDrain,
|
||||
kAddClient,
|
||||
kRemoveClient,
|
||||
kGetDispatcher
|
||||
} = require('./pool-base')
|
||||
const Pool = require('./pool')
|
||||
const { kUrl, kInterceptors } = require('./core/symbols')
|
||||
const { parseOrigin } = require('./core/util')
|
||||
const kFactory = Symbol('factory')
|
||||
|
||||
const kOptions = Symbol('options')
|
||||
const kGreatestCommonDivisor = Symbol('kGreatestCommonDivisor')
|
||||
const kCurrentWeight = Symbol('kCurrentWeight')
|
||||
const kIndex = Symbol('kIndex')
|
||||
const kWeight = Symbol('kWeight')
|
||||
const kMaxWeightPerServer = Symbol('kMaxWeightPerServer')
|
||||
const kErrorPenalty = Symbol('kErrorPenalty')
|
||||
|
||||
function getGreatestCommonDivisor (a, b) {
|
||||
if (b === 0) return a
|
||||
return getGreatestCommonDivisor(b, a % b)
|
||||
}
|
||||
|
||||
function defaultFactory (origin, opts) {
|
||||
return new Pool(origin, opts)
|
||||
}
|
||||
|
||||
class BalancedPool extends PoolBase {
|
||||
constructor (upstreams = [], { factory = defaultFactory, ...opts } = {}) {
|
||||
super()
|
||||
|
||||
this[kOptions] = opts
|
||||
this[kIndex] = -1
|
||||
this[kCurrentWeight] = 0
|
||||
|
||||
this[kMaxWeightPerServer] = this[kOptions].maxWeightPerServer || 100
|
||||
this[kErrorPenalty] = this[kOptions].errorPenalty || 15
|
||||
|
||||
if (!Array.isArray(upstreams)) {
|
||||
upstreams = [upstreams]
|
||||
}
|
||||
|
||||
if (typeof factory !== 'function') {
|
||||
throw new InvalidArgumentError('factory must be a function.')
|
||||
}
|
||||
|
||||
this[kInterceptors] = opts.interceptors && opts.interceptors.BalancedPool && Array.isArray(opts.interceptors.BalancedPool)
|
||||
? opts.interceptors.BalancedPool
|
||||
: []
|
||||
this[kFactory] = factory
|
||||
|
||||
for (const upstream of upstreams) {
|
||||
this.addUpstream(upstream)
|
||||
}
|
||||
this._updateBalancedPoolStats()
|
||||
}
|
||||
|
||||
addUpstream (upstream) {
|
||||
const upstreamOrigin = parseOrigin(upstream).origin
|
||||
|
||||
if (this[kClients].find((pool) => (
|
||||
pool[kUrl].origin === upstreamOrigin &&
|
||||
pool.closed !== true &&
|
||||
pool.destroyed !== true
|
||||
))) {
|
||||
return this
|
||||
}
|
||||
const pool = this[kFactory](upstreamOrigin, Object.assign({}, this[kOptions]))
|
||||
|
||||
this[kAddClient](pool)
|
||||
pool.on('connect', () => {
|
||||
pool[kWeight] = Math.min(this[kMaxWeightPerServer], pool[kWeight] + this[kErrorPenalty])
|
||||
})
|
||||
|
||||
pool.on('connectionError', () => {
|
||||
pool[kWeight] = Math.max(1, pool[kWeight] - this[kErrorPenalty])
|
||||
this._updateBalancedPoolStats()
|
||||
})
|
||||
|
||||
pool.on('disconnect', (...args) => {
|
||||
const err = args[2]
|
||||
if (err && err.code === 'UND_ERR_SOCKET') {
|
||||
// decrease the weight of the pool.
|
||||
pool[kWeight] = Math.max(1, pool[kWeight] - this[kErrorPenalty])
|
||||
this._updateBalancedPoolStats()
|
||||
}
|
||||
})
|
||||
|
||||
for (const client of this[kClients]) {
|
||||
client[kWeight] = this[kMaxWeightPerServer]
|
||||
}
|
||||
|
||||
this._updateBalancedPoolStats()
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
_updateBalancedPoolStats () {
|
||||
this[kGreatestCommonDivisor] = this[kClients].map(p => p[kWeight]).reduce(getGreatestCommonDivisor, 0)
|
||||
}
|
||||
|
||||
removeUpstream (upstream) {
|
||||
const upstreamOrigin = parseOrigin(upstream).origin
|
||||
|
||||
const pool = this[kClients].find((pool) => (
|
||||
pool[kUrl].origin === upstreamOrigin &&
|
||||
pool.closed !== true &&
|
||||
pool.destroyed !== true
|
||||
))
|
||||
|
||||
if (pool) {
|
||||
this[kRemoveClient](pool)
|
||||
}
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
get upstreams () {
|
||||
return this[kClients]
|
||||
.filter(dispatcher => dispatcher.closed !== true && dispatcher.destroyed !== true)
|
||||
.map((p) => p[kUrl].origin)
|
||||
}
|
||||
|
||||
[kGetDispatcher] () {
|
||||
// We validate that pools is greater than 0,
|
||||
// otherwise we would have to wait until an upstream
|
||||
// is added, which might never happen.
|
||||
if (this[kClients].length === 0) {
|
||||
throw new BalancedPoolMissingUpstreamError()
|
||||
}
|
||||
|
||||
const dispatcher = this[kClients].find(dispatcher => (
|
||||
!dispatcher[kNeedDrain] &&
|
||||
dispatcher.closed !== true &&
|
||||
dispatcher.destroyed !== true
|
||||
))
|
||||
|
||||
if (!dispatcher) {
|
||||
return
|
||||
}
|
||||
|
||||
const allClientsBusy = this[kClients].map(pool => pool[kNeedDrain]).reduce((a, b) => a && b, true)
|
||||
|
||||
if (allClientsBusy) {
|
||||
return
|
||||
}
|
||||
|
||||
let counter = 0
|
||||
|
||||
let maxWeightIndex = this[kClients].findIndex(pool => !pool[kNeedDrain])
|
||||
|
||||
while (counter++ < this[kClients].length) {
|
||||
this[kIndex] = (this[kIndex] + 1) % this[kClients].length
|
||||
const pool = this[kClients][this[kIndex]]
|
||||
|
||||
// find pool index with the largest weight
|
||||
if (pool[kWeight] > this[kClients][maxWeightIndex][kWeight] && !pool[kNeedDrain]) {
|
||||
maxWeightIndex = this[kIndex]
|
||||
}
|
||||
|
||||
// decrease the current weight every `this[kClients].length`.
|
||||
if (this[kIndex] === 0) {
|
||||
// Set the current weight to the next lower weight.
|
||||
this[kCurrentWeight] = this[kCurrentWeight] - this[kGreatestCommonDivisor]
|
||||
|
||||
if (this[kCurrentWeight] <= 0) {
|
||||
this[kCurrentWeight] = this[kMaxWeightPerServer]
|
||||
}
|
||||
}
|
||||
if (pool[kWeight] >= this[kCurrentWeight] && (!pool[kNeedDrain])) {
|
||||
return pool
|
||||
}
|
||||
}
|
||||
|
||||
this[kCurrentWeight] = this[kClients][maxWeightIndex][kWeight]
|
||||
this[kIndex] = maxWeightIndex
|
||||
return this[kClients][maxWeightIndex]
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = BalancedPool
|
|
@ -0,0 +1,838 @@
|
|||
'use strict'
|
||||
|
||||
const { kConstruct } = require('./symbols')
|
||||
const { urlEquals, fieldValues: getFieldValues } = require('./util')
|
||||
const { kEnumerableProperty, isDisturbed } = require('../core/util')
|
||||
const { kHeadersList } = require('../core/symbols')
|
||||
const { webidl } = require('../fetch/webidl')
|
||||
const { Response, cloneResponse } = require('../fetch/response')
|
||||
const { Request } = require('../fetch/request')
|
||||
const { kState, kHeaders, kGuard, kRealm } = require('../fetch/symbols')
|
||||
const { fetching } = require('../fetch/index')
|
||||
const { urlIsHttpHttpsScheme, createDeferredPromise, readAllBytes } = require('../fetch/util')
|
||||
const assert = require('assert')
|
||||
const { getGlobalDispatcher } = require('../global')
|
||||
|
||||
/**
|
||||
* @see https://w3c.github.io/ServiceWorker/#dfn-cache-batch-operation
|
||||
* @typedef {Object} CacheBatchOperation
|
||||
* @property {'delete' | 'put'} type
|
||||
* @property {any} request
|
||||
* @property {any} response
|
||||
* @property {import('../../types/cache').CacheQueryOptions} options
|
||||
*/
|
||||
|
||||
/**
|
||||
* @see https://w3c.github.io/ServiceWorker/#dfn-request-response-list
|
||||
* @typedef {[any, any][]} requestResponseList
|
||||
*/
|
||||
|
||||
class Cache {
|
||||
/**
|
||||
* @see https://w3c.github.io/ServiceWorker/#dfn-relevant-request-response-list
|
||||
* @type {requestResponseList}
|
||||
*/
|
||||
#relevantRequestResponseList
|
||||
|
||||
constructor () {
|
||||
if (arguments[0] !== kConstruct) {
|
||||
webidl.illegalConstructor()
|
||||
}
|
||||
|
||||
this.#relevantRequestResponseList = arguments[1]
|
||||
}
|
||||
|
||||
async match (request, options = {}) {
|
||||
webidl.brandCheck(this, Cache)
|
||||
webidl.argumentLengthCheck(arguments, 1, { header: 'Cache.match' })
|
||||
|
||||
request = webidl.converters.RequestInfo(request)
|
||||
options = webidl.converters.CacheQueryOptions(options)
|
||||
|
||||
const p = await this.matchAll(request, options)
|
||||
|
||||
if (p.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
return p[0]
|
||||
}
|
||||
|
||||
async matchAll (request = undefined, options = {}) {
|
||||
webidl.brandCheck(this, Cache)
|
||||
|
||||
if (request !== undefined) request = webidl.converters.RequestInfo(request)
|
||||
options = webidl.converters.CacheQueryOptions(options)
|
||||
|
||||
// 1.
|
||||
let r = null
|
||||
|
||||
// 2.
|
||||
if (request !== undefined) {
|
||||
if (request instanceof Request) {
|
||||
// 2.1.1
|
||||
r = request[kState]
|
||||
|
||||
// 2.1.2
|
||||
if (r.method !== 'GET' && !options.ignoreMethod) {
|
||||
return []
|
||||
}
|
||||
} else if (typeof request === 'string') {
|
||||
// 2.2.1
|
||||
r = new Request(request)[kState]
|
||||
}
|
||||
}
|
||||
|
||||
// 5.
|
||||
// 5.1
|
||||
const responses = []
|
||||
|
||||
// 5.2
|
||||
if (request === undefined) {
|
||||
// 5.2.1
|
||||
for (const requestResponse of this.#relevantRequestResponseList) {
|
||||
responses.push(requestResponse[1])
|
||||
}
|
||||
} else { // 5.3
|
||||
// 5.3.1
|
||||
const requestResponses = this.#queryCache(r, options)
|
||||
|
||||
// 5.3.2
|
||||
for (const requestResponse of requestResponses) {
|
||||
responses.push(requestResponse[1])
|
||||
}
|
||||
}
|
||||
|
||||
// 5.4
|
||||
// We don't implement CORs so we don't need to loop over the responses, yay!
|
||||
|
||||
// 5.5.1
|
||||
const responseList = []
|
||||
|
||||
// 5.5.2
|
||||
for (const response of responses) {
|
||||
// 5.5.2.1
|
||||
const responseObject = new Response(response.body?.source ?? null)
|
||||
const body = responseObject[kState].body
|
||||
responseObject[kState] = response
|
||||
responseObject[kState].body = body
|
||||
responseObject[kHeaders][kHeadersList] = response.headersList
|
||||
responseObject[kHeaders][kGuard] = 'immutable'
|
||||
|
||||
responseList.push(responseObject)
|
||||
}
|
||||
|
||||
// 6.
|
||||
return Object.freeze(responseList)
|
||||
}
|
||||
|
||||
async add (request) {
|
||||
webidl.brandCheck(this, Cache)
|
||||
webidl.argumentLengthCheck(arguments, 1, { header: 'Cache.add' })
|
||||
|
||||
request = webidl.converters.RequestInfo(request)
|
||||
|
||||
// 1.
|
||||
const requests = [request]
|
||||
|
||||
// 2.
|
||||
const responseArrayPromise = this.addAll(requests)
|
||||
|
||||
// 3.
|
||||
return await responseArrayPromise
|
||||
}
|
||||
|
||||
async addAll (requests) {
|
||||
webidl.brandCheck(this, Cache)
|
||||
webidl.argumentLengthCheck(arguments, 1, { header: 'Cache.addAll' })
|
||||
|
||||
requests = webidl.converters['sequence<RequestInfo>'](requests)
|
||||
|
||||
// 1.
|
||||
const responsePromises = []
|
||||
|
||||
// 2.
|
||||
const requestList = []
|
||||
|
||||
// 3.
|
||||
for (const request of requests) {
|
||||
if (typeof request === 'string') {
|
||||
continue
|
||||
}
|
||||
|
||||
// 3.1
|
||||
const r = request[kState]
|
||||
|
||||
// 3.2
|
||||
if (!urlIsHttpHttpsScheme(r.url) || r.method !== 'GET') {
|
||||
throw webidl.errors.exception({
|
||||
header: 'Cache.addAll',
|
||||
message: 'Expected http/s scheme when method is not GET.'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 4.
|
||||
/** @type {ReturnType<typeof fetching>[]} */
|
||||
const fetchControllers = []
|
||||
|
||||
// 5.
|
||||
for (const request of requests) {
|
||||
// 5.1
|
||||
const r = new Request(request)[kState]
|
||||
|
||||
// 5.2
|
||||
if (!urlIsHttpHttpsScheme(r.url)) {
|
||||
throw webidl.errors.exception({
|
||||
header: 'Cache.addAll',
|
||||
message: 'Expected http/s scheme.'
|
||||
})
|
||||
}
|
||||
|
||||
// 5.4
|
||||
r.initiator = 'fetch'
|
||||
r.destination = 'subresource'
|
||||
|
||||
// 5.5
|
||||
requestList.push(r)
|
||||
|
||||
// 5.6
|
||||
const responsePromise = createDeferredPromise()
|
||||
|
||||
// 5.7
|
||||
fetchControllers.push(fetching({
|
||||
request: r,
|
||||
dispatcher: getGlobalDispatcher(),
|
||||
processResponse (response) {
|
||||
// 1.
|
||||
if (response.type === 'error' || response.status === 206 || response.status < 200 || response.status > 299) {
|
||||
responsePromise.reject(webidl.errors.exception({
|
||||
header: 'Cache.addAll',
|
||||
message: 'Received an invalid status code or the request failed.'
|
||||
}))
|
||||
} else if (response.headersList.contains('vary')) { // 2.
|
||||
// 2.1
|
||||
const fieldValues = getFieldValues(response.headersList.get('vary'))
|
||||
|
||||
// 2.2
|
||||
for (const fieldValue of fieldValues) {
|
||||
// 2.2.1
|
||||
if (fieldValue === '*') {
|
||||
responsePromise.reject(webidl.errors.exception({
|
||||
header: 'Cache.addAll',
|
||||
message: 'invalid vary field value'
|
||||
}))
|
||||
|
||||
for (const controller of fetchControllers) {
|
||||
controller.abort()
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
processResponseEndOfBody (response) {
|
||||
// 1.
|
||||
if (response.aborted) {
|
||||
responsePromise.reject(new DOMException('aborted', 'AbortError'))
|
||||
return
|
||||
}
|
||||
|
||||
// 2.
|
||||
responsePromise.resolve(response)
|
||||
}
|
||||
}))
|
||||
|
||||
// 5.8
|
||||
responsePromises.push(responsePromise.promise)
|
||||
}
|
||||
|
||||
// 6.
|
||||
const p = Promise.all(responsePromises)
|
||||
|
||||
// 7.
|
||||
const responses = await p
|
||||
|
||||
// 7.1
|
||||
const operations = []
|
||||
|
||||
// 7.2
|
||||
let index = 0
|
||||
|
||||
// 7.3
|
||||
for (const response of responses) {
|
||||
// 7.3.1
|
||||
/** @type {CacheBatchOperation} */
|
||||
const operation = {
|
||||
type: 'put', // 7.3.2
|
||||
request: requestList[index], // 7.3.3
|
||||
response // 7.3.4
|
||||
}
|
||||
|
||||
operations.push(operation) // 7.3.5
|
||||
|
||||
index++ // 7.3.6
|
||||
}
|
||||
|
||||
// 7.5
|
||||
const cacheJobPromise = createDeferredPromise()
|
||||
|
||||
// 7.6.1
|
||||
let errorData = null
|
||||
|
||||
// 7.6.2
|
||||
try {
|
||||
this.#batchCacheOperations(operations)
|
||||
} catch (e) {
|
||||
errorData = e
|
||||
}
|
||||
|
||||
// 7.6.3
|
||||
queueMicrotask(() => {
|
||||
// 7.6.3.1
|
||||
if (errorData === null) {
|
||||
cacheJobPromise.resolve(undefined)
|
||||
} else {
|
||||
// 7.6.3.2
|
||||
cacheJobPromise.reject(errorData)
|
||||
}
|
||||
})
|
||||
|
||||
// 7.7
|
||||
return cacheJobPromise.promise
|
||||
}
|
||||
|
||||
async put (request, response) {
|
||||
webidl.brandCheck(this, Cache)
|
||||
webidl.argumentLengthCheck(arguments, 2, { header: 'Cache.put' })
|
||||
|
||||
request = webidl.converters.RequestInfo(request)
|
||||
response = webidl.converters.Response(response)
|
||||
|
||||
// 1.
|
||||
let innerRequest = null
|
||||
|
||||
// 2.
|
||||
if (request instanceof Request) {
|
||||
innerRequest = request[kState]
|
||||
} else { // 3.
|
||||
innerRequest = new Request(request)[kState]
|
||||
}
|
||||
|
||||
// 4.
|
||||
if (!urlIsHttpHttpsScheme(innerRequest.url) || innerRequest.method !== 'GET') {
|
||||
throw webidl.errors.exception({
|
||||
header: 'Cache.put',
|
||||
message: 'Expected an http/s scheme when method is not GET'
|
||||
})
|
||||
}
|
||||
|
||||
// 5.
|
||||
const innerResponse = response[kState]
|
||||
|
||||
// 6.
|
||||
if (innerResponse.status === 206) {
|
||||
throw webidl.errors.exception({
|
||||
header: 'Cache.put',
|
||||
message: 'Got 206 status'
|
||||
})
|
||||
}
|
||||
|
||||
// 7.
|
||||
if (innerResponse.headersList.contains('vary')) {
|
||||
// 7.1.
|
||||
const fieldValues = getFieldValues(innerResponse.headersList.get('vary'))
|
||||
|
||||
// 7.2.
|
||||
for (const fieldValue of fieldValues) {
|
||||
// 7.2.1
|
||||
if (fieldValue === '*') {
|
||||
throw webidl.errors.exception({
|
||||
header: 'Cache.put',
|
||||
message: 'Got * vary field value'
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 8.
|
||||
if (innerResponse.body && (isDisturbed(innerResponse.body.stream) || innerResponse.body.stream.locked)) {
|
||||
throw webidl.errors.exception({
|
||||
header: 'Cache.put',
|
||||
message: 'Response body is locked or disturbed'
|
||||
})
|
||||
}
|
||||
|
||||
// 9.
|
||||
const clonedResponse = cloneResponse(innerResponse)
|
||||
|
||||
// 10.
|
||||
const bodyReadPromise = createDeferredPromise()
|
||||
|
||||
// 11.
|
||||
if (innerResponse.body != null) {
|
||||
// 11.1
|
||||
const stream = innerResponse.body.stream
|
||||
|
||||
// 11.2
|
||||
const reader = stream.getReader()
|
||||
|
||||
// 11.3
|
||||
readAllBytes(reader).then(bodyReadPromise.resolve, bodyReadPromise.reject)
|
||||
} else {
|
||||
bodyReadPromise.resolve(undefined)
|
||||
}
|
||||
|
||||
// 12.
|
||||
/** @type {CacheBatchOperation[]} */
|
||||
const operations = []
|
||||
|
||||
// 13.
|
||||
/** @type {CacheBatchOperation} */
|
||||
const operation = {
|
||||
type: 'put', // 14.
|
||||
request: innerRequest, // 15.
|
||||
response: clonedResponse // 16.
|
||||
}
|
||||
|
||||
// 17.
|
||||
operations.push(operation)
|
||||
|
||||
// 19.
|
||||
const bytes = await bodyReadPromise.promise
|
||||
|
||||
if (clonedResponse.body != null) {
|
||||
clonedResponse.body.source = bytes
|
||||
}
|
||||
|
||||
// 19.1
|
||||
const cacheJobPromise = createDeferredPromise()
|
||||
|
||||
// 19.2.1
|
||||
let errorData = null
|
||||
|
||||
// 19.2.2
|
||||
try {
|
||||
this.#batchCacheOperations(operations)
|
||||
} catch (e) {
|
||||
errorData = e
|
||||
}
|
||||
|
||||
// 19.2.3
|
||||
queueMicrotask(() => {
|
||||
// 19.2.3.1
|
||||
if (errorData === null) {
|
||||
cacheJobPromise.resolve()
|
||||
} else { // 19.2.3.2
|
||||
cacheJobPromise.reject(errorData)
|
||||
}
|
||||
})
|
||||
|
||||
return cacheJobPromise.promise
|
||||
}
|
||||
|
||||
async delete (request, options = {}) {
|
||||
webidl.brandCheck(this, Cache)
|
||||
webidl.argumentLengthCheck(arguments, 1, { header: 'Cache.delete' })
|
||||
|
||||
request = webidl.converters.RequestInfo(request)
|
||||
options = webidl.converters.CacheQueryOptions(options)
|
||||
|
||||
/**
|
||||
* @type {Request}
|
||||
*/
|
||||
let r = null
|
||||
|
||||
if (request instanceof Request) {
|
||||
r = request[kState]
|
||||
|
||||
if (r.method !== 'GET' && !options.ignoreMethod) {
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
assert(typeof request === 'string')
|
||||
|
||||
r = new Request(request)[kState]
|
||||
}
|
||||
|
||||
/** @type {CacheBatchOperation[]} */
|
||||
const operations = []
|
||||
|
||||
/** @type {CacheBatchOperation} */
|
||||
const operation = {
|
||||
type: 'delete',
|
||||
request: r,
|
||||
options
|
||||
}
|
||||
|
||||
operations.push(operation)
|
||||
|
||||
const cacheJobPromise = createDeferredPromise()
|
||||
|
||||
let errorData = null
|
||||
let requestResponses
|
||||
|
||||
try {
|
||||
requestResponses = this.#batchCacheOperations(operations)
|
||||
} catch (e) {
|
||||
errorData = e
|
||||
}
|
||||
|
||||
queueMicrotask(() => {
|
||||
if (errorData === null) {
|
||||
cacheJobPromise.resolve(!!requestResponses?.length)
|
||||
} else {
|
||||
cacheJobPromise.reject(errorData)
|
||||
}
|
||||
})
|
||||
|
||||
return cacheJobPromise.promise
|
||||
}
|
||||
|
||||
/**
|
||||
* @see https://w3c.github.io/ServiceWorker/#dom-cache-keys
|
||||
* @param {any} request
|
||||
* @param {import('../../types/cache').CacheQueryOptions} options
|
||||
* @returns {readonly Request[]}
|
||||
*/
|
||||
async keys (request = undefined, options = {}) {
|
||||
webidl.brandCheck(this, Cache)
|
||||
|
||||
if (request !== undefined) request = webidl.converters.RequestInfo(request)
|
||||
options = webidl.converters.CacheQueryOptions(options)
|
||||
|
||||
// 1.
|
||||
let r = null
|
||||
|
||||
// 2.
|
||||
if (request !== undefined) {
|
||||
// 2.1
|
||||
if (request instanceof Request) {
|
||||
// 2.1.1
|
||||
r = request[kState]
|
||||
|
||||
// 2.1.2
|
||||
if (r.method !== 'GET' && !options.ignoreMethod) {
|
||||
return []
|
||||
}
|
||||
} else if (typeof request === 'string') { // 2.2
|
||||
r = new Request(request)[kState]
|
||||
}
|
||||
}
|
||||
|
||||
// 4.
|
||||
const promise = createDeferredPromise()
|
||||
|
||||
// 5.
|
||||
// 5.1
|
||||
const requests = []
|
||||
|
||||
// 5.2
|
||||
if (request === undefined) {
|
||||
// 5.2.1
|
||||
for (const requestResponse of this.#relevantRequestResponseList) {
|
||||
// 5.2.1.1
|
||||
requests.push(requestResponse[0])
|
||||
}
|
||||
} else { // 5.3
|
||||
// 5.3.1
|
||||
const requestResponses = this.#queryCache(r, options)
|
||||
|
||||
// 5.3.2
|
||||
for (const requestResponse of requestResponses) {
|
||||
// 5.3.2.1
|
||||
requests.push(requestResponse[0])
|
||||
}
|
||||
}
|
||||
|
||||
// 5.4
|
||||
queueMicrotask(() => {
|
||||
// 5.4.1
|
||||
const requestList = []
|
||||
|
||||
// 5.4.2
|
||||
for (const request of requests) {
|
||||
const requestObject = new Request('https://a')
|
||||
requestObject[kState] = request
|
||||
requestObject[kHeaders][kHeadersList] = request.headersList
|
||||
requestObject[kHeaders][kGuard] = 'immutable'
|
||||
requestObject[kRealm] = request.client
|
||||
|
||||
// 5.4.2.1
|
||||
requestList.push(requestObject)
|
||||
}
|
||||
|
||||
// 5.4.3
|
||||
promise.resolve(Object.freeze(requestList))
|
||||
})
|
||||
|
||||
return promise.promise
|
||||
}
|
||||
|
||||
/**
|
||||
* @see https://w3c.github.io/ServiceWorker/#batch-cache-operations-algorithm
|
||||
* @param {CacheBatchOperation[]} operations
|
||||
* @returns {requestResponseList}
|
||||
*/
|
||||
#batchCacheOperations (operations) {
|
||||
// 1.
|
||||
const cache = this.#relevantRequestResponseList
|
||||
|
||||
// 2.
|
||||
const backupCache = [...cache]
|
||||
|
||||
// 3.
|
||||
const addedItems = []
|
||||
|
||||
// 4.1
|
||||
const resultList = []
|
||||
|
||||
try {
|
||||
// 4.2
|
||||
for (const operation of operations) {
|
||||
// 4.2.1
|
||||
if (operation.type !== 'delete' && operation.type !== 'put') {
|
||||
throw webidl.errors.exception({
|
||||
header: 'Cache.#batchCacheOperations',
|
||||
message: 'operation type does not match "delete" or "put"'
|
||||
})
|
||||
}
|
||||
|
||||
// 4.2.2
|
||||
if (operation.type === 'delete' && operation.response != null) {
|
||||
throw webidl.errors.exception({
|
||||
header: 'Cache.#batchCacheOperations',
|
||||
message: 'delete operation should not have an associated response'
|
||||
})
|
||||
}
|
||||
|
||||
// 4.2.3
|
||||
if (this.#queryCache(operation.request, operation.options, addedItems).length) {
|
||||
throw new DOMException('???', 'InvalidStateError')
|
||||
}
|
||||
|
||||
// 4.2.4
|
||||
let requestResponses
|
||||
|
||||
// 4.2.5
|
||||
if (operation.type === 'delete') {
|
||||
// 4.2.5.1
|
||||
requestResponses = this.#queryCache(operation.request, operation.options)
|
||||
|
||||
// TODO: the spec is wrong, this is needed to pass WPTs
|
||||
if (requestResponses.length === 0) {
|
||||
return []
|
||||
}
|
||||
|
||||
// 4.2.5.2
|
||||
for (const requestResponse of requestResponses) {
|
||||
const idx = cache.indexOf(requestResponse)
|
||||
assert(idx !== -1)
|
||||
|
||||
// 4.2.5.2.1
|
||||
cache.splice(idx, 1)
|
||||
}
|
||||
} else if (operation.type === 'put') { // 4.2.6
|
||||
// 4.2.6.1
|
||||
if (operation.response == null) {
|
||||
throw webidl.errors.exception({
|
||||
header: 'Cache.#batchCacheOperations',
|
||||
message: 'put operation should have an associated response'
|
||||
})
|
||||
}
|
||||
|
||||
// 4.2.6.2
|
||||
const r = operation.request
|
||||
|
||||
// 4.2.6.3
|
||||
if (!urlIsHttpHttpsScheme(r.url)) {
|
||||
throw webidl.errors.exception({
|
||||
header: 'Cache.#batchCacheOperations',
|
||||
message: 'expected http or https scheme'
|
||||
})
|
||||
}
|
||||
|
||||
// 4.2.6.4
|
||||
if (r.method !== 'GET') {
|
||||
throw webidl.errors.exception({
|
||||
header: 'Cache.#batchCacheOperations',
|
||||
message: 'not get method'
|
||||
})
|
||||
}
|
||||
|
||||
// 4.2.6.5
|
||||
if (operation.options != null) {
|
||||
throw webidl.errors.exception({
|
||||
header: 'Cache.#batchCacheOperations',
|
||||
message: 'options must not be defined'
|
||||
})
|
||||
}
|
||||
|
||||
// 4.2.6.6
|
||||
requestResponses = this.#queryCache(operation.request)
|
||||
|
||||
// 4.2.6.7
|
||||
for (const requestResponse of requestResponses) {
|
||||
const idx = cache.indexOf(requestResponse)
|
||||
assert(idx !== -1)
|
||||
|
||||
// 4.2.6.7.1
|
||||
cache.splice(idx, 1)
|
||||
}
|
||||
|
||||
// 4.2.6.8
|
||||
cache.push([operation.request, operation.response])
|
||||
|
||||
// 4.2.6.10
|
||||
addedItems.push([operation.request, operation.response])
|
||||
}
|
||||
|
||||
// 4.2.7
|
||||
resultList.push([operation.request, operation.response])
|
||||
}
|
||||
|
||||
// 4.3
|
||||
return resultList
|
||||
} catch (e) { // 5.
|
||||
// 5.1
|
||||
this.#relevantRequestResponseList.length = 0
|
||||
|
||||
// 5.2
|
||||
this.#relevantRequestResponseList = backupCache
|
||||
|
||||
// 5.3
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @see https://w3c.github.io/ServiceWorker/#query-cache
|
||||
* @param {any} requestQuery
|
||||
* @param {import('../../types/cache').CacheQueryOptions} options
|
||||
* @param {requestResponseList} targetStorage
|
||||
* @returns {requestResponseList}
|
||||
*/
|
||||
#queryCache (requestQuery, options, targetStorage) {
|
||||
/** @type {requestResponseList} */
|
||||
const resultList = []
|
||||
|
||||
const storage = targetStorage ?? this.#relevantRequestResponseList
|
||||
|
||||
for (const requestResponse of storage) {
|
||||
const [cachedRequest, cachedResponse] = requestResponse
|
||||
if (this.#requestMatchesCachedItem(requestQuery, cachedRequest, cachedResponse, options)) {
|
||||
resultList.push(requestResponse)
|
||||
}
|
||||
}
|
||||
|
||||
return resultList
|
||||
}
|
||||
|
||||
/**
|
||||
* @see https://w3c.github.io/ServiceWorker/#request-matches-cached-item-algorithm
|
||||
* @param {any} requestQuery
|
||||
* @param {any} request
|
||||
* @param {any | null} response
|
||||
* @param {import('../../types/cache').CacheQueryOptions | undefined} options
|
||||
* @returns {boolean}
|
||||
*/
|
||||
#requestMatchesCachedItem (requestQuery, request, response = null, options) {
|
||||
// if (options?.ignoreMethod === false && request.method === 'GET') {
|
||||
// return false
|
||||
// }
|
||||
|
||||
const queryURL = new URL(requestQuery.url)
|
||||
|
||||
const cachedURL = new URL(request.url)
|
||||
|
||||
if (options?.ignoreSearch) {
|
||||
cachedURL.search = ''
|
||||
|
||||
queryURL.search = ''
|
||||
}
|
||||
|
||||
if (!urlEquals(queryURL, cachedURL, true)) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (
|
||||
response == null ||
|
||||
options?.ignoreVary ||
|
||||
!response.headersList.contains('vary')
|
||||
) {
|
||||
return true
|
||||
}
|
||||
|
||||
const fieldValues = getFieldValues(response.headersList.get('vary'))
|
||||
|
||||
for (const fieldValue of fieldValues) {
|
||||
if (fieldValue === '*') {
|
||||
return false
|
||||
}
|
||||
|
||||
const requestValue = request.headersList.get(fieldValue)
|
||||
const queryValue = requestQuery.headersList.get(fieldValue)
|
||||
|
||||
// If one has the header and the other doesn't, or one has
|
||||
// a different value than the other, return false
|
||||
if (requestValue !== queryValue) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
Object.defineProperties(Cache.prototype, {
|
||||
[Symbol.toStringTag]: {
|
||||
value: 'Cache',
|
||||
configurable: true
|
||||
},
|
||||
match: kEnumerableProperty,
|
||||
matchAll: kEnumerableProperty,
|
||||
add: kEnumerableProperty,
|
||||
addAll: kEnumerableProperty,
|
||||
put: kEnumerableProperty,
|
||||
delete: kEnumerableProperty,
|
||||
keys: kEnumerableProperty
|
||||
})
|
||||
|
||||
const cacheQueryOptionConverters = [
|
||||
{
|
||||
key: 'ignoreSearch',
|
||||
converter: webidl.converters.boolean,
|
||||
defaultValue: false
|
||||
},
|
||||
{
|
||||
key: 'ignoreMethod',
|
||||
converter: webidl.converters.boolean,
|
||||
defaultValue: false
|
||||
},
|
||||
{
|
||||
key: 'ignoreVary',
|
||||
converter: webidl.converters.boolean,
|
||||
defaultValue: false
|
||||
}
|
||||
]
|
||||
|
||||
webidl.converters.CacheQueryOptions = webidl.dictionaryConverter(cacheQueryOptionConverters)
|
||||
|
||||
webidl.converters.MultiCacheQueryOptions = webidl.dictionaryConverter([
|
||||
...cacheQueryOptionConverters,
|
||||
{
|
||||
key: 'cacheName',
|
||||
converter: webidl.converters.DOMString
|
||||
}
|
||||
])
|
||||
|
||||
webidl.converters.Response = webidl.interfaceConverter(Response)
|
||||
|
||||
webidl.converters['sequence<RequestInfo>'] = webidl.sequenceConverter(
|
||||
webidl.converters.RequestInfo
|
||||
)
|
||||
|
||||
module.exports = {
|
||||
Cache
|
||||
}
|
|
@ -0,0 +1,144 @@
|
|||
'use strict'
|
||||
|
||||
const { kConstruct } = require('./symbols')
|
||||
const { Cache } = require('./cache')
|
||||
const { webidl } = require('../fetch/webidl')
|
||||
const { kEnumerableProperty } = require('../core/util')
|
||||
|
||||
class CacheStorage {
|
||||
/**
|
||||
* @see https://w3c.github.io/ServiceWorker/#dfn-relevant-name-to-cache-map
|
||||
* @type {Map<string, import('./cache').requestResponseList}
|
||||
*/
|
||||
#caches = new Map()
|
||||
|
||||
constructor () {
|
||||
if (arguments[0] !== kConstruct) {
|
||||
webidl.illegalConstructor()
|
||||
}
|
||||
}
|
||||
|
||||
async match (request, options = {}) {
|
||||
webidl.brandCheck(this, CacheStorage)
|
||||
webidl.argumentLengthCheck(arguments, 1, { header: 'CacheStorage.match' })
|
||||
|
||||
request = webidl.converters.RequestInfo(request)
|
||||
options = webidl.converters.MultiCacheQueryOptions(options)
|
||||
|
||||
// 1.
|
||||
if (options.cacheName != null) {
|
||||
// 1.1.1.1
|
||||
if (this.#caches.has(options.cacheName)) {
|
||||
// 1.1.1.1.1
|
||||
const cacheList = this.#caches.get(options.cacheName)
|
||||
const cache = new Cache(kConstruct, cacheList)
|
||||
|
||||
return await cache.match(request, options)
|
||||
}
|
||||
} else { // 2.
|
||||
// 2.2
|
||||
for (const cacheList of this.#caches.values()) {
|
||||
const cache = new Cache(kConstruct, cacheList)
|
||||
|
||||
// 2.2.1.2
|
||||
const response = await cache.match(request, options)
|
||||
|
||||
if (response !== undefined) {
|
||||
return response
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @see https://w3c.github.io/ServiceWorker/#cache-storage-has
|
||||
* @param {string} cacheName
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
async has (cacheName) {
|
||||
webidl.brandCheck(this, CacheStorage)
|
||||
webidl.argumentLengthCheck(arguments, 1, { header: 'CacheStorage.has' })
|
||||
|
||||
cacheName = webidl.converters.DOMString(cacheName)
|
||||
|
||||
// 2.1.1
|
||||
// 2.2
|
||||
return this.#caches.has(cacheName)
|
||||
}
|
||||
|
||||
/**
|
||||
* @see https://w3c.github.io/ServiceWorker/#dom-cachestorage-open
|
||||
* @param {string} cacheName
|
||||
* @returns {Promise<Cache>}
|
||||
*/
|
||||
async open (cacheName) {
|
||||
webidl.brandCheck(this, CacheStorage)
|
||||
webidl.argumentLengthCheck(arguments, 1, { header: 'CacheStorage.open' })
|
||||
|
||||
cacheName = webidl.converters.DOMString(cacheName)
|
||||
|
||||
// 2.1
|
||||
if (this.#caches.has(cacheName)) {
|
||||
// await caches.open('v1') !== await caches.open('v1')
|
||||
|
||||
// 2.1.1
|
||||
const cache = this.#caches.get(cacheName)
|
||||
|
||||
// 2.1.1.1
|
||||
return new Cache(kConstruct, cache)
|
||||
}
|
||||
|
||||
// 2.2
|
||||
const cache = []
|
||||
|
||||
// 2.3
|
||||
this.#caches.set(cacheName, cache)
|
||||
|
||||
// 2.4
|
||||
return new Cache(kConstruct, cache)
|
||||
}
|
||||
|
||||
/**
|
||||
* @see https://w3c.github.io/ServiceWorker/#cache-storage-delete
|
||||
* @param {string} cacheName
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
async delete (cacheName) {
|
||||
webidl.brandCheck(this, CacheStorage)
|
||||
webidl.argumentLengthCheck(arguments, 1, { header: 'CacheStorage.delete' })
|
||||
|
||||
cacheName = webidl.converters.DOMString(cacheName)
|
||||
|
||||
return this.#caches.delete(cacheName)
|
||||
}
|
||||
|
||||
/**
|
||||
* @see https://w3c.github.io/ServiceWorker/#cache-storage-keys
|
||||
* @returns {string[]}
|
||||
*/
|
||||
async keys () {
|
||||
webidl.brandCheck(this, CacheStorage)
|
||||
|
||||
// 2.1
|
||||
const keys = this.#caches.keys()
|
||||
|
||||
// 2.2
|
||||
return [...keys]
|
||||
}
|
||||
}
|
||||
|
||||
Object.defineProperties(CacheStorage.prototype, {
|
||||
[Symbol.toStringTag]: {
|
||||
value: 'CacheStorage',
|
||||
configurable: true
|
||||
},
|
||||
match: kEnumerableProperty,
|
||||
has: kEnumerableProperty,
|
||||
open: kEnumerableProperty,
|
||||
delete: kEnumerableProperty,
|
||||
keys: kEnumerableProperty
|
||||
})
|
||||
|
||||
module.exports = {
|
||||
CacheStorage
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
'use strict'
|
||||
|
||||
module.exports = {
|
||||
kConstruct: Symbol('constructable')
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
'use strict'
|
||||
|
||||
const assert = require('assert')
|
||||
const { URLSerializer } = require('../fetch/dataURL')
|
||||
const { isValidHeaderName } = require('../fetch/util')
|
||||
|
||||
/**
|
||||
* @see https://url.spec.whatwg.org/#concept-url-equals
|
||||
* @param {URL} A
|
||||
* @param {URL} B
|
||||
* @param {boolean | undefined} excludeFragment
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function urlEquals (A, B, excludeFragment = false) {
|
||||
const serializedA = URLSerializer(A, excludeFragment)
|
||||
|
||||
const serializedB = URLSerializer(B, excludeFragment)
|
||||
|
||||
return serializedA === serializedB
|
||||
}
|
||||
|
||||
/**
|
||||
* @see https://github.com/chromium/chromium/blob/694d20d134cb553d8d89e5500b9148012b1ba299/content/browser/cache_storage/cache_storage_cache.cc#L260-L262
|
||||
* @param {string} header
|
||||
*/
|
||||
function fieldValues (header) {
|
||||
assert(header !== null)
|
||||
|
||||
const values = []
|
||||
|
||||
for (let value of header.split(',')) {
|
||||
value = value.trim()
|
||||
|
||||
if (!value.length) {
|
||||
continue
|
||||
} else if (!isValidHeaderName(value)) {
|
||||
continue
|
||||
}
|
||||
|
||||
values.push(value)
|
||||
}
|
||||
|
||||
return values
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
urlEquals,
|
||||
fieldValues
|
||||
}
|
File diff suppressed because it is too large
Load Diff
48
g4f/Provider/npm/node_modules/undici/lib/compat/dispatcher-weakref.js
generated
vendored
Normal file
48
g4f/Provider/npm/node_modules/undici/lib/compat/dispatcher-weakref.js
generated
vendored
Normal file
|
@ -0,0 +1,48 @@
|
|||
'use strict'
|
||||
|
||||
/* istanbul ignore file: only for Node 12 */
|
||||
|
||||
const { kConnected, kSize } = require('../core/symbols')
|
||||
|
||||
class CompatWeakRef {
|
||||
constructor (value) {
|
||||
this.value = value
|
||||
}
|
||||
|
||||
deref () {
|
||||
return this.value[kConnected] === 0 && this.value[kSize] === 0
|
||||
? undefined
|
||||
: this.value
|
||||
}
|
||||
}
|
||||
|
||||
class CompatFinalizer {
|
||||
constructor (finalizer) {
|
||||
this.finalizer = finalizer
|
||||
}
|
||||
|
||||
register (dispatcher, key) {
|
||||
if (dispatcher.on) {
|
||||
dispatcher.on('disconnect', () => {
|
||||
if (dispatcher[kConnected] === 0 && dispatcher[kSize] === 0) {
|
||||
this.finalizer(key)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = function () {
|
||||
// FIXME: remove workaround when the Node bug is fixed
|
||||
// https://github.com/nodejs/node/issues/49344#issuecomment-1741776308
|
||||
if (process.env.NODE_V8_COVERAGE) {
|
||||
return {
|
||||
WeakRef: CompatWeakRef,
|
||||
FinalizationRegistry: CompatFinalizer
|
||||
}
|
||||
}
|
||||
return {
|
||||
WeakRef: global.WeakRef || CompatWeakRef,
|
||||
FinalizationRegistry: global.FinalizationRegistry || CompatFinalizer
|
||||
}
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
'use strict'
|
||||
|
||||
// https://wicg.github.io/cookie-store/#cookie-maximum-attribute-value-size
|
||||
const maxAttributeValueSize = 1024
|
||||
|
||||
// https://wicg.github.io/cookie-store/#cookie-maximum-name-value-pair-size
|
||||
const maxNameValuePairSize = 4096
|
||||
|
||||
module.exports = {
|
||||
maxAttributeValueSize,
|
||||
maxNameValuePairSize
|
||||
}
|
|
@ -0,0 +1,184 @@
|
|||
'use strict'
|
||||
|
||||
const { parseSetCookie } = require('./parse')
|
||||
const { stringify, getHeadersList } = require('./util')
|
||||
const { webidl } = require('../fetch/webidl')
|
||||
const { Headers } = require('../fetch/headers')
|
||||
|
||||
/**
|
||||
* @typedef {Object} Cookie
|
||||
* @property {string} name
|
||||
* @property {string} value
|
||||
* @property {Date|number|undefined} expires
|
||||
* @property {number|undefined} maxAge
|
||||
* @property {string|undefined} domain
|
||||
* @property {string|undefined} path
|
||||
* @property {boolean|undefined} secure
|
||||
* @property {boolean|undefined} httpOnly
|
||||
* @property {'Strict'|'Lax'|'None'} sameSite
|
||||
* @property {string[]} unparsed
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {Headers} headers
|
||||
* @returns {Record<string, string>}
|
||||
*/
|
||||
function getCookies (headers) {
|
||||
webidl.argumentLengthCheck(arguments, 1, { header: 'getCookies' })
|
||||
|
||||
webidl.brandCheck(headers, Headers, { strict: false })
|
||||
|
||||
const cookie = headers.get('cookie')
|
||||
const out = {}
|
||||
|
||||
if (!cookie) {
|
||||
return out
|
||||
}
|
||||
|
||||
for (const piece of cookie.split(';')) {
|
||||
const [name, ...value] = piece.split('=')
|
||||
|
||||
out[name.trim()] = value.join('=')
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Headers} headers
|
||||
* @param {string} name
|
||||
* @param {{ path?: string, domain?: string }|undefined} attributes
|
||||
* @returns {void}
|
||||
*/
|
||||
function deleteCookie (headers, name, attributes) {
|
||||
webidl.argumentLengthCheck(arguments, 2, { header: 'deleteCookie' })
|
||||
|
||||
webidl.brandCheck(headers, Headers, { strict: false })
|
||||
|
||||
name = webidl.converters.DOMString(name)
|
||||
attributes = webidl.converters.DeleteCookieAttributes(attributes)
|
||||
|
||||
// Matches behavior of
|
||||
// https://github.com/denoland/deno_std/blob/63827b16330b82489a04614027c33b7904e08be5/http/cookie.ts#L278
|
||||
setCookie(headers, {
|
||||
name,
|
||||
value: '',
|
||||
expires: new Date(0),
|
||||
...attributes
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Headers} headers
|
||||
* @returns {Cookie[]}
|
||||
*/
|
||||
function getSetCookies (headers) {
|
||||
webidl.argumentLengthCheck(arguments, 1, { header: 'getSetCookies' })
|
||||
|
||||
webidl.brandCheck(headers, Headers, { strict: false })
|
||||
|
||||
const cookies = getHeadersList(headers).cookies
|
||||
|
||||
if (!cookies) {
|
||||
return []
|
||||
}
|
||||
|
||||
// In older versions of undici, cookies is a list of name:value.
|
||||
return cookies.map((pair) => parseSetCookie(Array.isArray(pair) ? pair[1] : pair))
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Headers} headers
|
||||
* @param {Cookie} cookie
|
||||
* @returns {void}
|
||||
*/
|
||||
function setCookie (headers, cookie) {
|
||||
webidl.argumentLengthCheck(arguments, 2, { header: 'setCookie' })
|
||||
|
||||
webidl.brandCheck(headers, Headers, { strict: false })
|
||||
|
||||
cookie = webidl.converters.Cookie(cookie)
|
||||
|
||||
const str = stringify(cookie)
|
||||
|
||||
if (str) {
|
||||
headers.append('Set-Cookie', stringify(cookie))
|
||||
}
|
||||
}
|
||||
|
||||
webidl.converters.DeleteCookieAttributes = webidl.dictionaryConverter([
|
||||
{
|
||||
converter: webidl.nullableConverter(webidl.converters.DOMString),
|
||||
key: 'path',
|
||||
defaultValue: null
|
||||
},
|
||||
{
|
||||
converter: webidl.nullableConverter(webidl.converters.DOMString),
|
||||
key: 'domain',
|
||||
defaultValue: null
|
||||
}
|
||||
])
|
||||
|
||||
webidl.converters.Cookie = webidl.dictionaryConverter([
|
||||
{
|
||||
converter: webidl.converters.DOMString,
|
||||
key: 'name'
|
||||
},
|
||||
{
|
||||
converter: webidl.converters.DOMString,
|
||||
key: 'value'
|
||||
},
|
||||
{
|
||||
converter: webidl.nullableConverter((value) => {
|
||||
if (typeof value === 'number') {
|
||||
return webidl.converters['unsigned long long'](value)
|
||||
}
|
||||
|
||||
return new Date(value)
|
||||
}),
|
||||
key: 'expires',
|
||||
defaultValue: null
|
||||
},
|
||||
{
|
||||
converter: webidl.nullableConverter(webidl.converters['long long']),
|
||||
key: 'maxAge',
|
||||
defaultValue: null
|
||||
},
|
||||
{
|
||||
converter: webidl.nullableConverter(webidl.converters.DOMString),
|
||||
key: 'domain',
|
||||
defaultValue: null
|
||||
},
|
||||
{
|
||||
converter: webidl.nullableConverter(webidl.converters.DOMString),
|
||||
key: 'path',
|
||||
defaultValue: null
|
||||
},
|
||||
{
|
||||
converter: webidl.nullableConverter(webidl.converters.boolean),
|
||||
key: 'secure',
|
||||
defaultValue: null
|
||||
},
|
||||
{
|
||||
converter: webidl.nullableConverter(webidl.converters.boolean),
|
||||
key: 'httpOnly',
|
||||
defaultValue: null
|
||||
},
|
||||
{
|
||||
converter: webidl.converters.USVString,
|
||||
key: 'sameSite',
|
||||
allowedValues: ['Strict', 'Lax', 'None']
|
||||
},
|
||||
{
|
||||
converter: webidl.sequenceConverter(webidl.converters.DOMString),
|
||||
key: 'unparsed',
|
||||
defaultValue: []
|
||||
}
|
||||
])
|
||||
|
||||
module.exports = {
|
||||
getCookies,
|
||||
deleteCookie,
|
||||
getSetCookies,
|
||||
setCookie
|
||||
}
|
|
@ -0,0 +1,317 @@
|
|||
'use strict'
|
||||
|
||||
const { maxNameValuePairSize, maxAttributeValueSize } = require('./constants')
|
||||
const { isCTLExcludingHtab } = require('./util')
|
||||
const { collectASequenceOfCodePointsFast } = require('../fetch/dataURL')
|
||||
const assert = require('assert')
|
||||
|
||||
/**
|
||||
* @description Parses the field-value attributes of a set-cookie header string.
|
||||
* @see https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#section-5.4
|
||||
* @param {string} header
|
||||
* @returns if the header is invalid, null will be returned
|
||||
*/
|
||||
function parseSetCookie (header) {
|
||||
// 1. If the set-cookie-string contains a %x00-08 / %x0A-1F / %x7F
|
||||
// character (CTL characters excluding HTAB): Abort these steps and
|
||||
// ignore the set-cookie-string entirely.
|
||||
if (isCTLExcludingHtab(header)) {
|
||||
return null
|
||||
}
|
||||
|
||||
let nameValuePair = ''
|
||||
let unparsedAttributes = ''
|
||||
let name = ''
|
||||
let value = ''
|
||||
|
||||
// 2. If the set-cookie-string contains a %x3B (";") character:
|
||||
if (header.includes(';')) {
|
||||
// 1. The name-value-pair string consists of the characters up to,
|
||||
// but not including, the first %x3B (";"), and the unparsed-
|
||||
// attributes consist of the remainder of the set-cookie-string
|
||||
// (including the %x3B (";") in question).
|
||||
const position = { position: 0 }
|
||||
|
||||
nameValuePair = collectASequenceOfCodePointsFast(';', header, position)
|
||||
unparsedAttributes = header.slice(position.position)
|
||||
} else {
|
||||
// Otherwise:
|
||||
|
||||
// 1. The name-value-pair string consists of all the characters
|
||||
// contained in the set-cookie-string, and the unparsed-
|
||||
// attributes is the empty string.
|
||||
nameValuePair = header
|
||||
}
|
||||
|
||||
// 3. If the name-value-pair string lacks a %x3D ("=") character, then
|
||||
// the name string is empty, and the value string is the value of
|
||||
// name-value-pair.
|
||||
if (!nameValuePair.includes('=')) {
|
||||
value = nameValuePair
|
||||
} else {
|
||||
// Otherwise, the name string consists of the characters up to, but
|
||||
// not including, the first %x3D ("=") character, and the (possibly
|
||||
// empty) value string consists of the characters after the first
|
||||
// %x3D ("=") character.
|
||||
const position = { position: 0 }
|
||||
name = collectASequenceOfCodePointsFast(
|
||||
'=',
|
||||
nameValuePair,
|
||||
position
|
||||
)
|
||||
value = nameValuePair.slice(position.position + 1)
|
||||
}
|
||||
|
||||
// 4. Remove any leading or trailing WSP characters from the name
|
||||
// string and the value string.
|
||||
name = name.trim()
|
||||
value = value.trim()
|
||||
|
||||
// 5. If the sum of the lengths of the name string and the value string
|
||||
// is more than 4096 octets, abort these steps and ignore the set-
|
||||
// cookie-string entirely.
|
||||
if (name.length + value.length > maxNameValuePairSize) {
|
||||
return null
|
||||
}
|
||||
|
||||
// 6. The cookie-name is the name string, and the cookie-value is the
|
||||
// value string.
|
||||
return {
|
||||
name, value, ...parseUnparsedAttributes(unparsedAttributes)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the remaining attributes of a set-cookie header
|
||||
* @see https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#section-5.4
|
||||
* @param {string} unparsedAttributes
|
||||
* @param {[Object.<string, unknown>]={}} cookieAttributeList
|
||||
*/
|
||||
function parseUnparsedAttributes (unparsedAttributes, cookieAttributeList = {}) {
|
||||
// 1. If the unparsed-attributes string is empty, skip the rest of
|
||||
// these steps.
|
||||
if (unparsedAttributes.length === 0) {
|
||||
return cookieAttributeList
|
||||
}
|
||||
|
||||
// 2. Discard the first character of the unparsed-attributes (which
|
||||
// will be a %x3B (";") character).
|
||||
assert(unparsedAttributes[0] === ';')
|
||||
unparsedAttributes = unparsedAttributes.slice(1)
|
||||
|
||||
let cookieAv = ''
|
||||
|
||||
// 3. If the remaining unparsed-attributes contains a %x3B (";")
|
||||
// character:
|
||||
if (unparsedAttributes.includes(';')) {
|
||||
// 1. Consume the characters of the unparsed-attributes up to, but
|
||||
// not including, the first %x3B (";") character.
|
||||
cookieAv = collectASequenceOfCodePointsFast(
|
||||
';',
|
||||
unparsedAttributes,
|
||||
{ position: 0 }
|
||||
)
|
||||
unparsedAttributes = unparsedAttributes.slice(cookieAv.length)
|
||||
} else {
|
||||
// Otherwise:
|
||||
|
||||
// 1. Consume the remainder of the unparsed-attributes.
|
||||
cookieAv = unparsedAttributes
|
||||
unparsedAttributes = ''
|
||||
}
|
||||
|
||||
// Let the cookie-av string be the characters consumed in this step.
|
||||
|
||||
let attributeName = ''
|
||||
let attributeValue = ''
|
||||
|
||||
// 4. If the cookie-av string contains a %x3D ("=") character:
|
||||
if (cookieAv.includes('=')) {
|
||||
// 1. The (possibly empty) attribute-name string consists of the
|
||||
// characters up to, but not including, the first %x3D ("=")
|
||||
// character, and the (possibly empty) attribute-value string
|
||||
// consists of the characters after the first %x3D ("=")
|
||||
// character.
|
||||
const position = { position: 0 }
|
||||
|
||||
attributeName = collectASequenceOfCodePointsFast(
|
||||
'=',
|
||||
cookieAv,
|
||||
position
|
||||
)
|
||||
attributeValue = cookieAv.slice(position.position + 1)
|
||||
} else {
|
||||
// Otherwise:
|
||||
|
||||
// 1. The attribute-name string consists of the entire cookie-av
|
||||
// string, and the attribute-value string is empty.
|
||||
attributeName = cookieAv
|
||||
}
|
||||
|
||||
// 5. Remove any leading or trailing WSP characters from the attribute-
|
||||
// name string and the attribute-value string.
|
||||
attributeName = attributeName.trim()
|
||||
attributeValue = attributeValue.trim()
|
||||
|
||||
// 6. If the attribute-value is longer than 1024 octets, ignore the
|
||||
// cookie-av string and return to Step 1 of this algorithm.
|
||||
if (attributeValue.length > maxAttributeValueSize) {
|
||||
return parseUnparsedAttributes(unparsedAttributes, cookieAttributeList)
|
||||
}
|
||||
|
||||
// 7. Process the attribute-name and attribute-value according to the
|
||||
// requirements in the following subsections. (Notice that
|
||||
// attributes with unrecognized attribute-names are ignored.)
|
||||
const attributeNameLowercase = attributeName.toLowerCase()
|
||||
|
||||
// https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#section-5.4.1
|
||||
// If the attribute-name case-insensitively matches the string
|
||||
// "Expires", the user agent MUST process the cookie-av as follows.
|
||||
if (attributeNameLowercase === 'expires') {
|
||||
// 1. Let the expiry-time be the result of parsing the attribute-value
|
||||
// as cookie-date (see Section 5.1.1).
|
||||
const expiryTime = new Date(attributeValue)
|
||||
|
||||
// 2. If the attribute-value failed to parse as a cookie date, ignore
|
||||
// the cookie-av.
|
||||
|
||||
cookieAttributeList.expires = expiryTime
|
||||
} else if (attributeNameLowercase === 'max-age') {
|
||||
// https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#section-5.4.2
|
||||
// If the attribute-name case-insensitively matches the string "Max-
|
||||
// Age", the user agent MUST process the cookie-av as follows.
|
||||
|
||||
// 1. If the first character of the attribute-value is not a DIGIT or a
|
||||
// "-" character, ignore the cookie-av.
|
||||
const charCode = attributeValue.charCodeAt(0)
|
||||
|
||||
if ((charCode < 48 || charCode > 57) && attributeValue[0] !== '-') {
|
||||
return parseUnparsedAttributes(unparsedAttributes, cookieAttributeList)
|
||||
}
|
||||
|
||||
// 2. If the remainder of attribute-value contains a non-DIGIT
|
||||
// character, ignore the cookie-av.
|
||||
if (!/^\d+$/.test(attributeValue)) {
|
||||
return parseUnparsedAttributes(unparsedAttributes, cookieAttributeList)
|
||||
}
|
||||
|
||||
// 3. Let delta-seconds be the attribute-value converted to an integer.
|
||||
const deltaSeconds = Number(attributeValue)
|
||||
|
||||
// 4. Let cookie-age-limit be the maximum age of the cookie (which
|
||||
// SHOULD be 400 days or less, see Section 4.1.2.2).
|
||||
|
||||
// 5. Set delta-seconds to the smaller of its present value and cookie-
|
||||
// age-limit.
|
||||
// deltaSeconds = Math.min(deltaSeconds * 1000, maxExpiresMs)
|
||||
|
||||
// 6. If delta-seconds is less than or equal to zero (0), let expiry-
|
||||
// time be the earliest representable date and time. Otherwise, let
|
||||
// the expiry-time be the current date and time plus delta-seconds
|
||||
// seconds.
|
||||
// const expiryTime = deltaSeconds <= 0 ? Date.now() : Date.now() + deltaSeconds
|
||||
|
||||
// 7. Append an attribute to the cookie-attribute-list with an
|
||||
// attribute-name of Max-Age and an attribute-value of expiry-time.
|
||||
cookieAttributeList.maxAge = deltaSeconds
|
||||
} else if (attributeNameLowercase === 'domain') {
|
||||
// https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#section-5.4.3
|
||||
// If the attribute-name case-insensitively matches the string "Domain",
|
||||
// the user agent MUST process the cookie-av as follows.
|
||||
|
||||
// 1. Let cookie-domain be the attribute-value.
|
||||
let cookieDomain = attributeValue
|
||||
|
||||
// 2. If cookie-domain starts with %x2E ("."), let cookie-domain be
|
||||
// cookie-domain without its leading %x2E (".").
|
||||
if (cookieDomain[0] === '.') {
|
||||
cookieDomain = cookieDomain.slice(1)
|
||||
}
|
||||
|
||||
// 3. Convert the cookie-domain to lower case.
|
||||
cookieDomain = cookieDomain.toLowerCase()
|
||||
|
||||
// 4. Append an attribute to the cookie-attribute-list with an
|
||||
// attribute-name of Domain and an attribute-value of cookie-domain.
|
||||
cookieAttributeList.domain = cookieDomain
|
||||
} else if (attributeNameLowercase === 'path') {
|
||||
// https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#section-5.4.4
|
||||
// If the attribute-name case-insensitively matches the string "Path",
|
||||
// the user agent MUST process the cookie-av as follows.
|
||||
|
||||
// 1. If the attribute-value is empty or if the first character of the
|
||||
// attribute-value is not %x2F ("/"):
|
||||
let cookiePath = ''
|
||||
if (attributeValue.length === 0 || attributeValue[0] !== '/') {
|
||||
// 1. Let cookie-path be the default-path.
|
||||
cookiePath = '/'
|
||||
} else {
|
||||
// Otherwise:
|
||||
|
||||
// 1. Let cookie-path be the attribute-value.
|
||||
cookiePath = attributeValue
|
||||
}
|
||||
|
||||
// 2. Append an attribute to the cookie-attribute-list with an
|
||||
// attribute-name of Path and an attribute-value of cookie-path.
|
||||
cookieAttributeList.path = cookiePath
|
||||
} else if (attributeNameLowercase === 'secure') {
|
||||
// https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#section-5.4.5
|
||||
// If the attribute-name case-insensitively matches the string "Secure",
|
||||
// the user agent MUST append an attribute to the cookie-attribute-list
|
||||
// with an attribute-name of Secure and an empty attribute-value.
|
||||
|
||||
cookieAttributeList.secure = true
|
||||
} else if (attributeNameLowercase === 'httponly') {
|
||||
// https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#section-5.4.6
|
||||
// If the attribute-name case-insensitively matches the string
|
||||
// "HttpOnly", the user agent MUST append an attribute to the cookie-
|
||||
// attribute-list with an attribute-name of HttpOnly and an empty
|
||||
// attribute-value.
|
||||
|
||||
cookieAttributeList.httpOnly = true
|
||||
} else if (attributeNameLowercase === 'samesite') {
|
||||
// https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#section-5.4.7
|
||||
// If the attribute-name case-insensitively matches the string
|
||||
// "SameSite", the user agent MUST process the cookie-av as follows:
|
||||
|
||||
// 1. Let enforcement be "Default".
|
||||
let enforcement = 'Default'
|
||||
|
||||
const attributeValueLowercase = attributeValue.toLowerCase()
|
||||
// 2. If cookie-av's attribute-value is a case-insensitive match for
|
||||
// "None", set enforcement to "None".
|
||||
if (attributeValueLowercase.includes('none')) {
|
||||
enforcement = 'None'
|
||||
}
|
||||
|
||||
// 3. If cookie-av's attribute-value is a case-insensitive match for
|
||||
// "Strict", set enforcement to "Strict".
|
||||
if (attributeValueLowercase.includes('strict')) {
|
||||
enforcement = 'Strict'
|
||||
}
|
||||
|
||||
// 4. If cookie-av's attribute-value is a case-insensitive match for
|
||||
// "Lax", set enforcement to "Lax".
|
||||
if (attributeValueLowercase.includes('lax')) {
|
||||
enforcement = 'Lax'
|
||||
}
|
||||
|
||||
// 5. Append an attribute to the cookie-attribute-list with an
|
||||
// attribute-name of "SameSite" and an attribute-value of
|
||||
// enforcement.
|
||||
cookieAttributeList.sameSite = enforcement
|
||||
} else {
|
||||
cookieAttributeList.unparsed ??= []
|
||||
|
||||
cookieAttributeList.unparsed.push(`${attributeName}=${attributeValue}`)
|
||||
}
|
||||
|
||||
// 8. Return to Step 1 of this algorithm.
|
||||
return parseUnparsedAttributes(unparsedAttributes, cookieAttributeList)
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
parseSetCookie,
|
||||
parseUnparsedAttributes
|
||||
}
|
|
@ -0,0 +1,291 @@
|
|||
'use strict'
|
||||
|
||||
const assert = require('assert')
|
||||
const { kHeadersList } = require('../core/symbols')
|
||||
|
||||
function isCTLExcludingHtab (value) {
|
||||
if (value.length === 0) {
|
||||
return false
|
||||
}
|
||||
|
||||
for (const char of value) {
|
||||
const code = char.charCodeAt(0)
|
||||
|
||||
if (
|
||||
(code >= 0x00 || code <= 0x08) ||
|
||||
(code >= 0x0A || code <= 0x1F) ||
|
||||
code === 0x7F
|
||||
) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
CHAR = <any US-ASCII character (octets 0 - 127)>
|
||||
token = 1*<any CHAR except CTLs or separators>
|
||||
separators = "(" | ")" | "<" | ">" | "@"
|
||||
| "," | ";" | ":" | "\" | <">
|
||||
| "/" | "[" | "]" | "?" | "="
|
||||
| "{" | "}" | SP | HT
|
||||
* @param {string} name
|
||||
*/
|
||||
function validateCookieName (name) {
|
||||
for (const char of name) {
|
||||
const code = char.charCodeAt(0)
|
||||
|
||||
if (
|
||||
(code <= 0x20 || code > 0x7F) ||
|
||||
char === '(' ||
|
||||
char === ')' ||
|
||||
char === '>' ||
|
||||
char === '<' ||
|
||||
char === '@' ||
|
||||
char === ',' ||
|
||||
char === ';' ||
|
||||
char === ':' ||
|
||||
char === '\\' ||
|
||||
char === '"' ||
|
||||
char === '/' ||
|
||||
char === '[' ||
|
||||
char === ']' ||
|
||||
char === '?' ||
|
||||
char === '=' ||
|
||||
char === '{' ||
|
||||
char === '}'
|
||||
) {
|
||||
throw new Error('Invalid cookie name')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
cookie-value = *cookie-octet / ( DQUOTE *cookie-octet DQUOTE )
|
||||
cookie-octet = %x21 / %x23-2B / %x2D-3A / %x3C-5B / %x5D-7E
|
||||
; US-ASCII characters excluding CTLs,
|
||||
; whitespace DQUOTE, comma, semicolon,
|
||||
; and backslash
|
||||
* @param {string} value
|
||||
*/
|
||||
function validateCookieValue (value) {
|
||||
for (const char of value) {
|
||||
const code = char.charCodeAt(0)
|
||||
|
||||
if (
|
||||
code < 0x21 || // exclude CTLs (0-31)
|
||||
code === 0x22 ||
|
||||
code === 0x2C ||
|
||||
code === 0x3B ||
|
||||
code === 0x5C ||
|
||||
code > 0x7E // non-ascii
|
||||
) {
|
||||
throw new Error('Invalid header value')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* path-value = <any CHAR except CTLs or ";">
|
||||
* @param {string} path
|
||||
*/
|
||||
function validateCookiePath (path) {
|
||||
for (const char of path) {
|
||||
const code = char.charCodeAt(0)
|
||||
|
||||
if (code < 0x21 || char === ';') {
|
||||
throw new Error('Invalid cookie path')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* I have no idea why these values aren't allowed to be honest,
|
||||
* but Deno tests these. - Khafra
|
||||
* @param {string} domain
|
||||
*/
|
||||
function validateCookieDomain (domain) {
|
||||
if (
|
||||
domain.startsWith('-') ||
|
||||
domain.endsWith('.') ||
|
||||
domain.endsWith('-')
|
||||
) {
|
||||
throw new Error('Invalid cookie domain')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @see https://www.rfc-editor.org/rfc/rfc7231#section-7.1.1.1
|
||||
* @param {number|Date} date
|
||||
IMF-fixdate = day-name "," SP date1 SP time-of-day SP GMT
|
||||
; fixed length/zone/capitalization subset of the format
|
||||
; see Section 3.3 of [RFC5322]
|
||||
|
||||
day-name = %x4D.6F.6E ; "Mon", case-sensitive
|
||||
/ %x54.75.65 ; "Tue", case-sensitive
|
||||
/ %x57.65.64 ; "Wed", case-sensitive
|
||||
/ %x54.68.75 ; "Thu", case-sensitive
|
||||
/ %x46.72.69 ; "Fri", case-sensitive
|
||||
/ %x53.61.74 ; "Sat", case-sensitive
|
||||
/ %x53.75.6E ; "Sun", case-sensitive
|
||||
date1 = day SP month SP year
|
||||
; e.g., 02 Jun 1982
|
||||
|
||||
day = 2DIGIT
|
||||
month = %x4A.61.6E ; "Jan", case-sensitive
|
||||
/ %x46.65.62 ; "Feb", case-sensitive
|
||||
/ %x4D.61.72 ; "Mar", case-sensitive
|
||||
/ %x41.70.72 ; "Apr", case-sensitive
|
||||
/ %x4D.61.79 ; "May", case-sensitive
|
||||
/ %x4A.75.6E ; "Jun", case-sensitive
|
||||
/ %x4A.75.6C ; "Jul", case-sensitive
|
||||
/ %x41.75.67 ; "Aug", case-sensitive
|
||||
/ %x53.65.70 ; "Sep", case-sensitive
|
||||
/ %x4F.63.74 ; "Oct", case-sensitive
|
||||
/ %x4E.6F.76 ; "Nov", case-sensitive
|
||||
/ %x44.65.63 ; "Dec", case-sensitive
|
||||
year = 4DIGIT
|
||||
|
||||
GMT = %x47.4D.54 ; "GMT", case-sensitive
|
||||
|
||||
time-of-day = hour ":" minute ":" second
|
||||
; 00:00:00 - 23:59:60 (leap second)
|
||||
|
||||
hour = 2DIGIT
|
||||
minute = 2DIGIT
|
||||
second = 2DIGIT
|
||||
*/
|
||||
function toIMFDate (date) {
|
||||
if (typeof date === 'number') {
|
||||
date = new Date(date)
|
||||
}
|
||||
|
||||
const days = [
|
||||
'Sun', 'Mon', 'Tue', 'Wed',
|
||||
'Thu', 'Fri', 'Sat'
|
||||
]
|
||||
|
||||
const months = [
|
||||
'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
|
||||
'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'
|
||||
]
|
||||
|
||||
const dayName = days[date.getUTCDay()]
|
||||
const day = date.getUTCDate().toString().padStart(2, '0')
|
||||
const month = months[date.getUTCMonth()]
|
||||
const year = date.getUTCFullYear()
|
||||
const hour = date.getUTCHours().toString().padStart(2, '0')
|
||||
const minute = date.getUTCMinutes().toString().padStart(2, '0')
|
||||
const second = date.getUTCSeconds().toString().padStart(2, '0')
|
||||
|
||||
return `${dayName}, ${day} ${month} ${year} ${hour}:${minute}:${second} GMT`
|
||||
}
|
||||
|
||||
/**
|
||||
max-age-av = "Max-Age=" non-zero-digit *DIGIT
|
||||
; In practice, both expires-av and max-age-av
|
||||
; are limited to dates representable by the
|
||||
; user agent.
|
||||
* @param {number} maxAge
|
||||
*/
|
||||
function validateCookieMaxAge (maxAge) {
|
||||
if (maxAge < 0) {
|
||||
throw new Error('Invalid cookie max-age')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @see https://www.rfc-editor.org/rfc/rfc6265#section-4.1.1
|
||||
* @param {import('./index').Cookie} cookie
|
||||
*/
|
||||
function stringify (cookie) {
|
||||
if (cookie.name.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
validateCookieName(cookie.name)
|
||||
validateCookieValue(cookie.value)
|
||||
|
||||
const out = [`${cookie.name}=${cookie.value}`]
|
||||
|
||||
// https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-cookie-prefixes-00#section-3.1
|
||||
// https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-cookie-prefixes-00#section-3.2
|
||||
if (cookie.name.startsWith('__Secure-')) {
|
||||
cookie.secure = true
|
||||
}
|
||||
|
||||
if (cookie.name.startsWith('__Host-')) {
|
||||
cookie.secure = true
|
||||
cookie.domain = null
|
||||
cookie.path = '/'
|
||||
}
|
||||
|
||||
if (cookie.secure) {
|
||||
out.push('Secure')
|
||||
}
|
||||
|
||||
if (cookie.httpOnly) {
|
||||
out.push('HttpOnly')
|
||||
}
|
||||
|
||||
if (typeof cookie.maxAge === 'number') {
|
||||
validateCookieMaxAge(cookie.maxAge)
|
||||
out.push(`Max-Age=${cookie.maxAge}`)
|
||||
}
|
||||
|
||||
if (cookie.domain) {
|
||||
validateCookieDomain(cookie.domain)
|
||||
out.push(`Domain=${cookie.domain}`)
|
||||
}
|
||||
|
||||
if (cookie.path) {
|
||||
validateCookiePath(cookie.path)
|
||||
out.push(`Path=${cookie.path}`)
|
||||
}
|
||||
|
||||
if (cookie.expires && cookie.expires.toString() !== 'Invalid Date') {
|
||||
out.push(`Expires=${toIMFDate(cookie.expires)}`)
|
||||
}
|
||||
|
||||
if (cookie.sameSite) {
|
||||
out.push(`SameSite=${cookie.sameSite}`)
|
||||
}
|
||||
|
||||
for (const part of cookie.unparsed) {
|
||||
if (!part.includes('=')) {
|
||||
throw new Error('Invalid unparsed')
|
||||
}
|
||||
|
||||
const [key, ...value] = part.split('=')
|
||||
|
||||
out.push(`${key.trim()}=${value.join('=')}`)
|
||||
}
|
||||
|
||||
return out.join('; ')
|
||||
}
|
||||
|
||||
let kHeadersListNode
|
||||
|
||||
function getHeadersList (headers) {
|
||||
if (headers[kHeadersList]) {
|
||||
return headers[kHeadersList]
|
||||
}
|
||||
|
||||
if (!kHeadersListNode) {
|
||||
kHeadersListNode = Object.getOwnPropertySymbols(headers).find(
|
||||
(symbol) => symbol.description === 'headers list'
|
||||
)
|
||||
|
||||
assert(kHeadersListNode, 'Headers cannot be parsed')
|
||||
}
|
||||
|
||||
const headersList = headers[kHeadersListNode]
|
||||
assert(headersList)
|
||||
|
||||
return headersList
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
isCTLExcludingHtab,
|
||||
stringify,
|
||||
getHeadersList
|
||||
}
|
|
@ -0,0 +1,189 @@
|
|||
'use strict'
|
||||
|
||||
const net = require('net')
|
||||
const assert = require('assert')
|
||||
const util = require('./util')
|
||||
const { InvalidArgumentError, ConnectTimeoutError } = require('./errors')
|
||||
|
||||
let tls // include tls conditionally since it is not always available
|
||||
|
||||
// TODO: session re-use does not wait for the first
|
||||
// connection to resolve the session and might therefore
|
||||
// resolve the same servername multiple times even when
|
||||
// re-use is enabled.
|
||||
|
||||
let SessionCache
|
||||
// FIXME: remove workaround when the Node bug is fixed
|
||||
// https://github.com/nodejs/node/issues/49344#issuecomment-1741776308
|
||||
if (global.FinalizationRegistry && !process.env.NODE_V8_COVERAGE) {
|
||||
SessionCache = class WeakSessionCache {
|
||||
constructor (maxCachedSessions) {
|
||||
this._maxCachedSessions = maxCachedSessions
|
||||
this._sessionCache = new Map()
|
||||
this._sessionRegistry = new global.FinalizationRegistry((key) => {
|
||||
if (this._sessionCache.size < this._maxCachedSessions) {
|
||||
return
|
||||
}
|
||||
|
||||
const ref = this._sessionCache.get(key)
|
||||
if (ref !== undefined && ref.deref() === undefined) {
|
||||
this._sessionCache.delete(key)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
get (sessionKey) {
|
||||
const ref = this._sessionCache.get(sessionKey)
|
||||
return ref ? ref.deref() : null
|
||||
}
|
||||
|
||||
set (sessionKey, session) {
|
||||
if (this._maxCachedSessions === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
this._sessionCache.set(sessionKey, new WeakRef(session))
|
||||
this._sessionRegistry.register(session, sessionKey)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
SessionCache = class SimpleSessionCache {
|
||||
constructor (maxCachedSessions) {
|
||||
this._maxCachedSessions = maxCachedSessions
|
||||
this._sessionCache = new Map()
|
||||
}
|
||||
|
||||
get (sessionKey) {
|
||||
return this._sessionCache.get(sessionKey)
|
||||
}
|
||||
|
||||
set (sessionKey, session) {
|
||||
if (this._maxCachedSessions === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
if (this._sessionCache.size >= this._maxCachedSessions) {
|
||||
// remove the oldest session
|
||||
const { value: oldestKey } = this._sessionCache.keys().next()
|
||||
this._sessionCache.delete(oldestKey)
|
||||
}
|
||||
|
||||
this._sessionCache.set(sessionKey, session)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function buildConnector ({ allowH2, maxCachedSessions, socketPath, timeout, ...opts }) {
|
||||
if (maxCachedSessions != null && (!Number.isInteger(maxCachedSessions) || maxCachedSessions < 0)) {
|
||||
throw new InvalidArgumentError('maxCachedSessions must be a positive integer or zero')
|
||||
}
|
||||
|
||||
const options = { path: socketPath, ...opts }
|
||||
const sessionCache = new SessionCache(maxCachedSessions == null ? 100 : maxCachedSessions)
|
||||
timeout = timeout == null ? 10e3 : timeout
|
||||
allowH2 = allowH2 != null ? allowH2 : false
|
||||
return function connect ({ hostname, host, protocol, port, servername, localAddress, httpSocket }, callback) {
|
||||
let socket
|
||||
if (protocol === 'https:') {
|
||||
if (!tls) {
|
||||
tls = require('tls')
|
||||
}
|
||||
servername = servername || options.servername || util.getServerName(host) || null
|
||||
|
||||
const sessionKey = servername || hostname
|
||||
const session = sessionCache.get(sessionKey) || null
|
||||
|
||||
assert(sessionKey)
|
||||
|
||||
socket = tls.connect({
|
||||
highWaterMark: 16384, // TLS in node can't have bigger HWM anyway...
|
||||
...options,
|
||||
servername,
|
||||
session,
|
||||
localAddress,
|
||||
// TODO(HTTP/2): Add support for h2c
|
||||
ALPNProtocols: allowH2 ? ['http/1.1', 'h2'] : ['http/1.1'],
|
||||
socket: httpSocket, // upgrade socket connection
|
||||
port: port || 443,
|
||||
host: hostname
|
||||
})
|
||||
|
||||
socket
|
||||
.on('session', function (session) {
|
||||
// TODO (fix): Can a session become invalid once established? Don't think so?
|
||||
sessionCache.set(sessionKey, session)
|
||||
})
|
||||
} else {
|
||||
assert(!httpSocket, 'httpSocket can only be sent on TLS update')
|
||||
socket = net.connect({
|
||||
highWaterMark: 64 * 1024, // Same as nodejs fs streams.
|
||||
...options,
|
||||
localAddress,
|
||||
port: port || 80,
|
||||
host: hostname
|
||||
})
|
||||
}
|
||||
|
||||
// Set TCP keep alive options on the socket here instead of in connect() for the case of assigning the socket
|
||||
if (options.keepAlive == null || options.keepAlive) {
|
||||
const keepAliveInitialDelay = options.keepAliveInitialDelay === undefined ? 60e3 : options.keepAliveInitialDelay
|
||||
socket.setKeepAlive(true, keepAliveInitialDelay)
|
||||
}
|
||||
|
||||
const cancelTimeout = setupTimeout(() => onConnectTimeout(socket), timeout)
|
||||
|
||||
socket
|
||||
.setNoDelay(true)
|
||||
.once(protocol === 'https:' ? 'secureConnect' : 'connect', function () {
|
||||
cancelTimeout()
|
||||
|
||||
if (callback) {
|
||||
const cb = callback
|
||||
callback = null
|
||||
cb(null, this)
|
||||
}
|
||||
})
|
||||
.on('error', function (err) {
|
||||
cancelTimeout()
|
||||
|
||||
if (callback) {
|
||||
const cb = callback
|
||||
callback = null
|
||||
cb(err)
|
||||
}
|
||||
})
|
||||
|
||||
return socket
|
||||
}
|
||||
}
|
||||
|
||||
function setupTimeout (onConnectTimeout, timeout) {
|
||||
if (!timeout) {
|
||||
return () => {}
|
||||
}
|
||||
|
||||
let s1 = null
|
||||
let s2 = null
|
||||
const timeoutId = setTimeout(() => {
|
||||
// setImmediate is added to make sure that we priotorise socket error events over timeouts
|
||||
s1 = setImmediate(() => {
|
||||
if (process.platform === 'win32') {
|
||||
// Windows needs an extra setImmediate probably due to implementation differences in the socket logic
|
||||
s2 = setImmediate(() => onConnectTimeout())
|
||||
} else {
|
||||
onConnectTimeout()
|
||||
}
|
||||
})
|
||||
}, timeout)
|
||||
return () => {
|
||||
clearTimeout(timeoutId)
|
||||
clearImmediate(s1)
|
||||
clearImmediate(s2)
|
||||
}
|
||||
}
|
||||
|
||||
function onConnectTimeout (socket) {
|
||||
util.destroy(socket, new ConnectTimeoutError())
|
||||
}
|
||||
|
||||
module.exports = buildConnector
|
|
@ -0,0 +1,216 @@
|
|||
'use strict'
|
||||
|
||||
class UndiciError extends Error {
|
||||
constructor (message) {
|
||||
super(message)
|
||||
this.name = 'UndiciError'
|
||||
this.code = 'UND_ERR'
|
||||
}
|
||||
}
|
||||
|
||||
class ConnectTimeoutError extends UndiciError {
|
||||
constructor (message) {
|
||||
super(message)
|
||||
Error.captureStackTrace(this, ConnectTimeoutError)
|
||||
this.name = 'ConnectTimeoutError'
|
||||
this.message = message || 'Connect Timeout Error'
|
||||
this.code = 'UND_ERR_CONNECT_TIMEOUT'
|
||||
}
|
||||
}
|
||||
|
||||
class HeadersTimeoutError extends UndiciError {
|
||||
constructor (message) {
|
||||
super(message)
|
||||
Error.captureStackTrace(this, HeadersTimeoutError)
|
||||
this.name = 'HeadersTimeoutError'
|
||||
this.message = message || 'Headers Timeout Error'
|
||||
this.code = 'UND_ERR_HEADERS_TIMEOUT'
|
||||
}
|
||||
}
|
||||
|
||||
class HeadersOverflowError extends UndiciError {
|
||||
constructor (message) {
|
||||
super(message)
|
||||
Error.captureStackTrace(this, HeadersOverflowError)
|
||||
this.name = 'HeadersOverflowError'
|
||||
this.message = message || 'Headers Overflow Error'
|
||||
this.code = 'UND_ERR_HEADERS_OVERFLOW'
|
||||
}
|
||||
}
|
||||
|
||||
class BodyTimeoutError extends UndiciError {
|
||||
constructor (message) {
|
||||
super(message)
|
||||
Error.captureStackTrace(this, BodyTimeoutError)
|
||||
this.name = 'BodyTimeoutError'
|
||||
this.message = message || 'Body Timeout Error'
|
||||
this.code = 'UND_ERR_BODY_TIMEOUT'
|
||||
}
|
||||
}
|
||||
|
||||
class ResponseStatusCodeError extends UndiciError {
|
||||
constructor (message, statusCode, headers, body) {
|
||||
super(message)
|
||||
Error.captureStackTrace(this, ResponseStatusCodeError)
|
||||
this.name = 'ResponseStatusCodeError'
|
||||
this.message = message || 'Response Status Code Error'
|
||||
this.code = 'UND_ERR_RESPONSE_STATUS_CODE'
|
||||
this.body = body
|
||||
this.status = statusCode
|
||||
this.statusCode = statusCode
|
||||
this.headers = headers
|
||||
}
|
||||
}
|
||||
|
||||
class InvalidArgumentError extends UndiciError {
|
||||
constructor (message) {
|
||||
super(message)
|
||||
Error.captureStackTrace(this, InvalidArgumentError)
|
||||
this.name = 'InvalidArgumentError'
|
||||
this.message = message || 'Invalid Argument Error'
|
||||
this.code = 'UND_ERR_INVALID_ARG'
|
||||
}
|
||||
}
|
||||
|
||||
class InvalidReturnValueError extends UndiciError {
|
||||
constructor (message) {
|
||||
super(message)
|
||||
Error.captureStackTrace(this, InvalidReturnValueError)
|
||||
this.name = 'InvalidReturnValueError'
|
||||
this.message = message || 'Invalid Return Value Error'
|
||||
this.code = 'UND_ERR_INVALID_RETURN_VALUE'
|
||||
}
|
||||
}
|
||||
|
||||
class RequestAbortedError extends UndiciError {
|
||||
constructor (message) {
|
||||
super(message)
|
||||
Error.captureStackTrace(this, RequestAbortedError)
|
||||
this.name = 'AbortError'
|
||||
this.message = message || 'Request aborted'
|
||||
this.code = 'UND_ERR_ABORTED'
|
||||
}
|
||||
}
|
||||
|
||||
class InformationalError extends UndiciError {
|
||||
constructor (message) {
|
||||
super(message)
|
||||
Error.captureStackTrace(this, InformationalError)
|
||||
this.name = 'InformationalError'
|
||||
this.message = message || 'Request information'
|
||||
this.code = 'UND_ERR_INFO'
|
||||
}
|
||||
}
|
||||
|
||||
class RequestContentLengthMismatchError extends UndiciError {
|
||||
constructor (message) {
|
||||
super(message)
|
||||
Error.captureStackTrace(this, RequestContentLengthMismatchError)
|
||||
this.name = 'RequestContentLengthMismatchError'
|
||||
this.message = message || 'Request body length does not match content-length header'
|
||||
this.code = 'UND_ERR_REQ_CONTENT_LENGTH_MISMATCH'
|
||||
}
|
||||
}
|
||||
|
||||
class ResponseContentLengthMismatchError extends UndiciError {
|
||||
constructor (message) {
|
||||
super(message)
|
||||
Error.captureStackTrace(this, ResponseContentLengthMismatchError)
|
||||
this.name = 'ResponseContentLengthMismatchError'
|
||||
this.message = message || 'Response body length does not match content-length header'
|
||||
this.code = 'UND_ERR_RES_CONTENT_LENGTH_MISMATCH'
|
||||
}
|
||||
}
|
||||
|
||||
class ClientDestroyedError extends UndiciError {
|
||||
constructor (message) {
|
||||
super(message)
|
||||
Error.captureStackTrace(this, ClientDestroyedError)
|
||||
this.name = 'ClientDestroyedError'
|
||||
this.message = message || 'The client is destroyed'
|
||||
this.code = 'UND_ERR_DESTROYED'
|
||||
}
|
||||
}
|
||||
|
||||
class ClientClosedError extends UndiciError {
|
||||
constructor (message) {
|
||||
super(message)
|
||||
Error.captureStackTrace(this, ClientClosedError)
|
||||
this.name = 'ClientClosedError'
|
||||
this.message = message || 'The client is closed'
|
||||
this.code = 'UND_ERR_CLOSED'
|
||||
}
|
||||
}
|
||||
|
||||
class SocketError extends UndiciError {
|
||||
constructor (message, socket) {
|
||||
super(message)
|
||||
Error.captureStackTrace(this, SocketError)
|
||||
this.name = 'SocketError'
|
||||
this.message = message || 'Socket error'
|
||||
this.code = 'UND_ERR_SOCKET'
|
||||
this.socket = socket
|
||||
}
|
||||
}
|
||||
|
||||
class NotSupportedError extends UndiciError {
|
||||
constructor (message) {
|
||||
super(message)
|
||||
Error.captureStackTrace(this, NotSupportedError)
|
||||
this.name = 'NotSupportedError'
|
||||
this.message = message || 'Not supported error'
|
||||
this.code = 'UND_ERR_NOT_SUPPORTED'
|
||||
}
|
||||
}
|
||||
|
||||
class BalancedPoolMissingUpstreamError extends UndiciError {
|
||||
constructor (message) {
|
||||
super(message)
|
||||
Error.captureStackTrace(this, NotSupportedError)
|
||||
this.name = 'MissingUpstreamError'
|
||||
this.message = message || 'No upstream has been added to the BalancedPool'
|
||||
this.code = 'UND_ERR_BPL_MISSING_UPSTREAM'
|
||||
}
|
||||
}
|
||||
|
||||
class HTTPParserError extends Error {
|
||||
constructor (message, code, data) {
|
||||
super(message)
|
||||
Error.captureStackTrace(this, HTTPParserError)
|
||||
this.name = 'HTTPParserError'
|
||||
this.code = code ? `HPE_${code}` : undefined
|
||||
this.data = data ? data.toString() : undefined
|
||||
}
|
||||
}
|
||||
|
||||
class ResponseExceededMaxSizeError extends UndiciError {
|
||||
constructor (message) {
|
||||
super(message)
|
||||
Error.captureStackTrace(this, ResponseExceededMaxSizeError)
|
||||
this.name = 'ResponseExceededMaxSizeError'
|
||||
this.message = message || 'Response content exceeded max size'
|
||||
this.code = 'UND_ERR_RES_EXCEEDED_MAX_SIZE'
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
HTTPParserError,
|
||||
UndiciError,
|
||||
HeadersTimeoutError,
|
||||
HeadersOverflowError,
|
||||
BodyTimeoutError,
|
||||
RequestContentLengthMismatchError,
|
||||
ConnectTimeoutError,
|
||||
ResponseStatusCodeError,
|
||||
InvalidArgumentError,
|
||||
InvalidReturnValueError,
|
||||
RequestAbortedError,
|
||||
ClientDestroyedError,
|
||||
ClientClosedError,
|
||||
InformationalError,
|
||||
SocketError,
|
||||
NotSupportedError,
|
||||
ResponseContentLengthMismatchError,
|
||||
BalancedPoolMissingUpstreamError,
|
||||
ResponseExceededMaxSizeError
|
||||
}
|
|
@ -0,0 +1,443 @@
|
|||
'use strict'
|
||||
|
||||
const {
|
||||
InvalidArgumentError,
|
||||
NotSupportedError
|
||||
} = require('./errors')
|
||||
const assert = require('assert')
|
||||
const { kHTTP2BuildRequest, kHTTP2CopyHeaders, kHTTP1BuildRequest } = require('./symbols')
|
||||
const util = require('./util')
|
||||
|
||||
// tokenRegExp and headerCharRegex have been lifted from
|
||||
// https://github.com/nodejs/node/blob/main/lib/_http_common.js
|
||||
|
||||
/**
|
||||
* Verifies that the given val is a valid HTTP token
|
||||
* per the rules defined in RFC 7230
|
||||
* See https://tools.ietf.org/html/rfc7230#section-3.2.6
|
||||
*/
|
||||
const tokenRegExp = /^[\^_`a-zA-Z\-0-9!#$%&'*+.|~]+$/
|
||||
|
||||
/**
|
||||
* Matches if val contains an invalid field-vchar
|
||||
* field-value = *( field-content / obs-fold )
|
||||
* field-content = field-vchar [ 1*( SP / HTAB ) field-vchar ]
|
||||
* field-vchar = VCHAR / obs-text
|
||||
*/
|
||||
const headerCharRegex = /[^\t\x20-\x7e\x80-\xff]/
|
||||
|
||||
// Verifies that a given path is valid does not contain control chars \x00 to \x20
|
||||
const invalidPathRegex = /[^\u0021-\u00ff]/
|
||||
|
||||
const kHandler = Symbol('handler')
|
||||
|
||||
const channels = {}
|
||||
|
||||
let extractBody
|
||||
|
||||
try {
|
||||
const diagnosticsChannel = require('diagnostics_channel')
|
||||
channels.create = diagnosticsChannel.channel('undici:request:create')
|
||||
channels.bodySent = diagnosticsChannel.channel('undici:request:bodySent')
|
||||
channels.headers = diagnosticsChannel.channel('undici:request:headers')
|
||||
channels.trailers = diagnosticsChannel.channel('undici:request:trailers')
|
||||
channels.error = diagnosticsChannel.channel('undici:request:error')
|
||||
} catch {
|
||||
channels.create = { hasSubscribers: false }
|
||||
channels.bodySent = { hasSubscribers: false }
|
||||
channels.headers = { hasSubscribers: false }
|
||||
channels.trailers = { hasSubscribers: false }
|
||||
channels.error = { hasSubscribers: false }
|
||||
}
|
||||
|
||||
class Request {
|
||||
constructor (origin, {
|
||||
path,
|
||||
method,
|
||||
body,
|
||||
headers,
|
||||
query,
|
||||
idempotent,
|
||||
blocking,
|
||||
upgrade,
|
||||
headersTimeout,
|
||||
bodyTimeout,
|
||||
reset,
|
||||
throwOnError,
|
||||
expectContinue
|
||||
}, handler) {
|
||||
if (typeof path !== 'string') {
|
||||
throw new InvalidArgumentError('path must be a string')
|
||||
} else if (
|
||||
path[0] !== '/' &&
|
||||
!(path.startsWith('http://') || path.startsWith('https://')) &&
|
||||
method !== 'CONNECT'
|
||||
) {
|
||||
throw new InvalidArgumentError('path must be an absolute URL or start with a slash')
|
||||
} else if (invalidPathRegex.exec(path) !== null) {
|
||||
throw new InvalidArgumentError('invalid request path')
|
||||
}
|
||||
|
||||
if (typeof method !== 'string') {
|
||||
throw new InvalidArgumentError('method must be a string')
|
||||
} else if (tokenRegExp.exec(method) === null) {
|
||||
throw new InvalidArgumentError('invalid request method')
|
||||
}
|
||||
|
||||
if (upgrade && typeof upgrade !== 'string') {
|
||||
throw new InvalidArgumentError('upgrade must be a string')
|
||||
}
|
||||
|
||||
if (headersTimeout != null && (!Number.isFinite(headersTimeout) || headersTimeout < 0)) {
|
||||
throw new InvalidArgumentError('invalid headersTimeout')
|
||||
}
|
||||
|
||||
if (bodyTimeout != null && (!Number.isFinite(bodyTimeout) || bodyTimeout < 0)) {
|
||||
throw new InvalidArgumentError('invalid bodyTimeout')
|
||||
}
|
||||
|
||||
if (reset != null && typeof reset !== 'boolean') {
|
||||
throw new InvalidArgumentError('invalid reset')
|
||||
}
|
||||
|
||||
if (expectContinue != null && typeof expectContinue !== 'boolean') {
|
||||
throw new InvalidArgumentError('invalid expectContinue')
|
||||
}
|
||||
|
||||
this.headersTimeout = headersTimeout
|
||||
|
||||
this.bodyTimeout = bodyTimeout
|
||||
|
||||
this.throwOnError = throwOnError === true
|
||||
|
||||
this.method = method
|
||||
|
||||
if (body == null) {
|
||||
this.body = null
|
||||
} else if (util.isStream(body)) {
|
||||
this.body = body
|
||||
} else if (util.isBuffer(body)) {
|
||||
this.body = body.byteLength ? body : null
|
||||
} else if (ArrayBuffer.isView(body)) {
|
||||
this.body = body.buffer.byteLength ? Buffer.from(body.buffer, body.byteOffset, body.byteLength) : null
|
||||
} else if (body instanceof ArrayBuffer) {
|
||||
this.body = body.byteLength ? Buffer.from(body) : null
|
||||
} else if (typeof body === 'string') {
|
||||
this.body = body.length ? Buffer.from(body) : null
|
||||
} else if (util.isFormDataLike(body) || util.isIterable(body) || util.isBlobLike(body)) {
|
||||
this.body = body
|
||||
} else {
|
||||
throw new InvalidArgumentError('body must be a string, a Buffer, a Readable stream, an iterable, or an async iterable')
|
||||
}
|
||||
|
||||
this.completed = false
|
||||
|
||||
this.aborted = false
|
||||
|
||||
this.upgrade = upgrade || null
|
||||
|
||||
this.path = query ? util.buildURL(path, query) : path
|
||||
|
||||
this.origin = origin
|
||||
|
||||
this.idempotent = idempotent == null
|
||||
? method === 'HEAD' || method === 'GET'
|
||||
: idempotent
|
||||
|
||||
this.blocking = blocking == null ? false : blocking
|
||||
|
||||
this.reset = reset == null ? null : reset
|
||||
|
||||
this.host = null
|
||||
|
||||
this.contentLength = null
|
||||
|
||||
this.contentType = null
|
||||
|
||||
this.headers = ''
|
||||
|
||||
// Only for H2
|
||||
this.expectContinue = expectContinue != null ? expectContinue : false
|
||||
|
||||
if (Array.isArray(headers)) {
|
||||
if (headers.length % 2 !== 0) {
|
||||
throw new InvalidArgumentError('headers array must be even')
|
||||
}
|
||||
for (let i = 0; i < headers.length; i += 2) {
|
||||
processHeader(this, headers[i], headers[i + 1])
|
||||
}
|
||||
} else if (headers && typeof headers === 'object') {
|
||||
const keys = Object.keys(headers)
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
const key = keys[i]
|
||||
processHeader(this, key, headers[key])
|
||||
}
|
||||
} else if (headers != null) {
|
||||
throw new InvalidArgumentError('headers must be an object or an array')
|
||||
}
|
||||
|
||||
if (util.isFormDataLike(this.body)) {
|
||||
if (util.nodeMajor < 16 || (util.nodeMajor === 16 && util.nodeMinor < 8)) {
|
||||
throw new InvalidArgumentError('Form-Data bodies are only supported in node v16.8 and newer.')
|
||||
}
|
||||
|
||||
if (!extractBody) {
|
||||
extractBody = require('../fetch/body.js').extractBody
|
||||
}
|
||||
|
||||
const [bodyStream, contentType] = extractBody(body)
|
||||
if (this.contentType == null) {
|
||||
this.contentType = contentType
|
||||
this.headers += `content-type: ${contentType}\r\n`
|
||||
}
|
||||
this.body = bodyStream.stream
|
||||
this.contentLength = bodyStream.length
|
||||
} else if (util.isBlobLike(body) && this.contentType == null && body.type) {
|
||||
this.contentType = body.type
|
||||
this.headers += `content-type: ${body.type}\r\n`
|
||||
}
|
||||
|
||||
util.validateHandler(handler, method, upgrade)
|
||||
|
||||
this.servername = util.getServerName(this.host)
|
||||
|
||||
this[kHandler] = handler
|
||||
|
||||
if (channels.create.hasSubscribers) {
|
||||
channels.create.publish({ request: this })
|
||||
}
|
||||
}
|
||||
|
||||
onBodySent (chunk) {
|
||||
if (this[kHandler].onBodySent) {
|
||||
try {
|
||||
this[kHandler].onBodySent(chunk)
|
||||
} catch (err) {
|
||||
this.onError(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onRequestSent () {
|
||||
if (channels.bodySent.hasSubscribers) {
|
||||
channels.bodySent.publish({ request: this })
|
||||
}
|
||||
|
||||
if (this[kHandler].onRequestSent) {
|
||||
try {
|
||||
this[kHandler].onRequestSent()
|
||||
} catch (err) {
|
||||
this.onError(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onConnect (abort) {
|
||||
assert(!this.aborted)
|
||||
assert(!this.completed)
|
||||
|
||||
return this[kHandler].onConnect(abort)
|
||||
}
|
||||
|
||||
onHeaders (statusCode, headers, resume, statusText) {
|
||||
assert(!this.aborted)
|
||||
assert(!this.completed)
|
||||
|
||||
if (channels.headers.hasSubscribers) {
|
||||
channels.headers.publish({ request: this, response: { statusCode, headers, statusText } })
|
||||
}
|
||||
|
||||
return this[kHandler].onHeaders(statusCode, headers, resume, statusText)
|
||||
}
|
||||
|
||||
onData (chunk) {
|
||||
assert(!this.aborted)
|
||||
assert(!this.completed)
|
||||
|
||||
return this[kHandler].onData(chunk)
|
||||
}
|
||||
|
||||
onUpgrade (statusCode, headers, socket) {
|
||||
assert(!this.aborted)
|
||||
assert(!this.completed)
|
||||
|
||||
return this[kHandler].onUpgrade(statusCode, headers, socket)
|
||||
}
|
||||
|
||||
onComplete (trailers) {
|
||||
assert(!this.aborted)
|
||||
|
||||
this.completed = true
|
||||
if (channels.trailers.hasSubscribers) {
|
||||
channels.trailers.publish({ request: this, trailers })
|
||||
}
|
||||
return this[kHandler].onComplete(trailers)
|
||||
}
|
||||
|
||||
onError (error) {
|
||||
if (channels.error.hasSubscribers) {
|
||||
channels.error.publish({ request: this, error })
|
||||
}
|
||||
|
||||
if (this.aborted) {
|
||||
return
|
||||
}
|
||||
this.aborted = true
|
||||
return this[kHandler].onError(error)
|
||||
}
|
||||
|
||||
// TODO: adjust to support H2
|
||||
addHeader (key, value) {
|
||||
processHeader(this, key, value)
|
||||
return this
|
||||
}
|
||||
|
||||
static [kHTTP1BuildRequest] (origin, opts, handler) {
|
||||
// TODO: Migrate header parsing here, to make Requests
|
||||
// HTTP agnostic
|
||||
return new Request(origin, opts, handler)
|
||||
}
|
||||
|
||||
static [kHTTP2BuildRequest] (origin, opts, handler) {
|
||||
const headers = opts.headers
|
||||
opts = { ...opts, headers: null }
|
||||
|
||||
const request = new Request(origin, opts, handler)
|
||||
|
||||
request.headers = {}
|
||||
|
||||
if (Array.isArray(headers)) {
|
||||
if (headers.length % 2 !== 0) {
|
||||
throw new InvalidArgumentError('headers array must be even')
|
||||
}
|
||||
for (let i = 0; i < headers.length; i += 2) {
|
||||
processHeader(request, headers[i], headers[i + 1], true)
|
||||
}
|
||||
} else if (headers && typeof headers === 'object') {
|
||||
const keys = Object.keys(headers)
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
const key = keys[i]
|
||||
processHeader(request, key, headers[key], true)
|
||||
}
|
||||
} else if (headers != null) {
|
||||
throw new InvalidArgumentError('headers must be an object or an array')
|
||||
}
|
||||
|
||||
return request
|
||||
}
|
||||
|
||||
static [kHTTP2CopyHeaders] (raw) {
|
||||
const rawHeaders = raw.split('\r\n')
|
||||
const headers = {}
|
||||
|
||||
for (const header of rawHeaders) {
|
||||
const [key, value] = header.split(': ')
|
||||
|
||||
if (value == null || value.length === 0) continue
|
||||
|
||||
if (headers[key]) headers[key] += `,${value}`
|
||||
else headers[key] = value
|
||||
}
|
||||
|
||||
return headers
|
||||
}
|
||||
}
|
||||
|
||||
function processHeaderValue (key, val, skipAppend) {
|
||||
if (val && typeof val === 'object') {
|
||||
throw new InvalidArgumentError(`invalid ${key} header`)
|
||||
}
|
||||
|
||||
val = val != null ? `${val}` : ''
|
||||
|
||||
if (headerCharRegex.exec(val) !== null) {
|
||||
throw new InvalidArgumentError(`invalid ${key} header`)
|
||||
}
|
||||
|
||||
return skipAppend ? val : `${key}: ${val}\r\n`
|
||||
}
|
||||
|
||||
function processHeader (request, key, val, skipAppend = false) {
|
||||
if (val && (typeof val === 'object' && !Array.isArray(val))) {
|
||||
throw new InvalidArgumentError(`invalid ${key} header`)
|
||||
} else if (val === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
if (
|
||||
request.host === null &&
|
||||
key.length === 4 &&
|
||||
key.toLowerCase() === 'host'
|
||||
) {
|
||||
if (headerCharRegex.exec(val) !== null) {
|
||||
throw new InvalidArgumentError(`invalid ${key} header`)
|
||||
}
|
||||
// Consumed by Client
|
||||
request.host = val
|
||||
} else if (
|
||||
request.contentLength === null &&
|
||||
key.length === 14 &&
|
||||
key.toLowerCase() === 'content-length'
|
||||
) {
|
||||
request.contentLength = parseInt(val, 10)
|
||||
if (!Number.isFinite(request.contentLength)) {
|
||||
throw new InvalidArgumentError('invalid content-length header')
|
||||
}
|
||||
} else if (
|
||||
request.contentType === null &&
|
||||
key.length === 12 &&
|
||||
key.toLowerCase() === 'content-type'
|
||||
) {
|
||||
request.contentType = val
|
||||
if (skipAppend) request.headers[key] = processHeaderValue(key, val, skipAppend)
|
||||
else request.headers += processHeaderValue(key, val)
|
||||
} else if (
|
||||
key.length === 17 &&
|
||||
key.toLowerCase() === 'transfer-encoding'
|
||||
) {
|
||||
throw new InvalidArgumentError('invalid transfer-encoding header')
|
||||
} else if (
|
||||
key.length === 10 &&
|
||||
key.toLowerCase() === 'connection'
|
||||
) {
|
||||
const value = typeof val === 'string' ? val.toLowerCase() : null
|
||||
if (value !== 'close' && value !== 'keep-alive') {
|
||||
throw new InvalidArgumentError('invalid connection header')
|
||||
} else if (value === 'close') {
|
||||
request.reset = true
|
||||
}
|
||||
} else if (
|
||||
key.length === 10 &&
|
||||
key.toLowerCase() === 'keep-alive'
|
||||
) {
|
||||
throw new InvalidArgumentError('invalid keep-alive header')
|
||||
} else if (
|
||||
key.length === 7 &&
|
||||
key.toLowerCase() === 'upgrade'
|
||||
) {
|
||||
throw new InvalidArgumentError('invalid upgrade header')
|
||||
} else if (
|
||||
key.length === 6 &&
|
||||
key.toLowerCase() === 'expect'
|
||||
) {
|
||||
throw new NotSupportedError('expect header not supported')
|
||||
} else if (tokenRegExp.exec(key) === null) {
|
||||
throw new InvalidArgumentError('invalid header key')
|
||||
} else {
|
||||
if (Array.isArray(val)) {
|
||||
for (let i = 0; i < val.length; i++) {
|
||||
if (skipAppend) {
|
||||
if (request.headers[key]) request.headers[key] += `,${processHeaderValue(key, val[i], skipAppend)}`
|
||||
else request.headers[key] = processHeaderValue(key, val[i], skipAppend)
|
||||
} else {
|
||||
request.headers += processHeaderValue(key, val[i])
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (skipAppend) request.headers[key] = processHeaderValue(key, val, skipAppend)
|
||||
else request.headers += processHeaderValue(key, val)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Request
|
|
@ -0,0 +1,61 @@
|
|||
module.exports = {
|
||||
kClose: Symbol('close'),
|
||||
kDestroy: Symbol('destroy'),
|
||||
kDispatch: Symbol('dispatch'),
|
||||
kUrl: Symbol('url'),
|
||||
kWriting: Symbol('writing'),
|
||||
kResuming: Symbol('resuming'),
|
||||
kQueue: Symbol('queue'),
|
||||
kConnect: Symbol('connect'),
|
||||
kConnecting: Symbol('connecting'),
|
||||
kHeadersList: Symbol('headers list'),
|
||||
kKeepAliveDefaultTimeout: Symbol('default keep alive timeout'),
|
||||
kKeepAliveMaxTimeout: Symbol('max keep alive timeout'),
|
||||
kKeepAliveTimeoutThreshold: Symbol('keep alive timeout threshold'),
|
||||
kKeepAliveTimeoutValue: Symbol('keep alive timeout'),
|
||||
kKeepAlive: Symbol('keep alive'),
|
||||
kHeadersTimeout: Symbol('headers timeout'),
|
||||
kBodyTimeout: Symbol('body timeout'),
|
||||
kServerName: Symbol('server name'),
|
||||
kLocalAddress: Symbol('local address'),
|
||||
kHost: Symbol('host'),
|
||||
kNoRef: Symbol('no ref'),
|
||||
kBodyUsed: Symbol('used'),
|
||||
kRunning: Symbol('running'),
|
||||
kBlocking: Symbol('blocking'),
|
||||
kPending: Symbol('pending'),
|
||||
kSize: Symbol('size'),
|
||||
kBusy: Symbol('busy'),
|
||||
kQueued: Symbol('queued'),
|
||||
kFree: Symbol('free'),
|
||||
kConnected: Symbol('connected'),
|
||||
kClosed: Symbol('closed'),
|
||||
kNeedDrain: Symbol('need drain'),
|
||||
kReset: Symbol('reset'),
|
||||
kDestroyed: Symbol.for('nodejs.stream.destroyed'),
|
||||
kMaxHeadersSize: Symbol('max headers size'),
|
||||
kRunningIdx: Symbol('running index'),
|
||||
kPendingIdx: Symbol('pending index'),
|
||||
kError: Symbol('error'),
|
||||
kClients: Symbol('clients'),
|
||||
kClient: Symbol('client'),
|
||||
kParser: Symbol('parser'),
|
||||
kOnDestroyed: Symbol('destroy callbacks'),
|
||||
kPipelining: Symbol('pipelining'),
|
||||
kSocket: Symbol('socket'),
|
||||
kHostHeader: Symbol('host header'),
|
||||
kConnector: Symbol('connector'),
|
||||
kStrictContentLength: Symbol('strict content length'),
|
||||
kMaxRedirections: Symbol('maxRedirections'),
|
||||
kMaxRequests: Symbol('maxRequestsPerClient'),
|
||||
kProxy: Symbol('proxy agent options'),
|
||||
kCounter: Symbol('socket request counter'),
|
||||
kInterceptors: Symbol('dispatch interceptors'),
|
||||
kMaxResponseSize: Symbol('max response size'),
|
||||
kHTTP2Session: Symbol('http2Session'),
|
||||
kHTTP2SessionState: Symbol('http2Session state'),
|
||||
kHTTP2BuildRequest: Symbol('http2 build request'),
|
||||
kHTTP1BuildRequest: Symbol('http1 build request'),
|
||||
kHTTP2CopyHeaders: Symbol('http2 copy headers'),
|
||||
kHTTPConnVersion: Symbol('http connection version')
|
||||
}
|
|
@ -0,0 +1,503 @@
|
|||
'use strict'
|
||||
|
||||
const assert = require('assert')
|
||||
const { kDestroyed, kBodyUsed } = require('./symbols')
|
||||
const { IncomingMessage } = require('http')
|
||||
const stream = require('stream')
|
||||
const net = require('net')
|
||||
const { InvalidArgumentError } = require('./errors')
|
||||
const { Blob } = require('buffer')
|
||||
const nodeUtil = require('util')
|
||||
const { stringify } = require('querystring')
|
||||
|
||||
const [nodeMajor, nodeMinor] = process.versions.node.split('.').map(v => Number(v))
|
||||
|
||||
function nop () {}
|
||||
|
||||
function isStream (obj) {
|
||||
return obj && typeof obj === 'object' && typeof obj.pipe === 'function' && typeof obj.on === 'function'
|
||||
}
|
||||
|
||||
// based on https://github.com/node-fetch/fetch-blob/blob/8ab587d34080de94140b54f07168451e7d0b655e/index.js#L229-L241 (MIT License)
|
||||
function isBlobLike (object) {
|
||||
return (Blob && object instanceof Blob) || (
|
||||
object &&
|
||||
typeof object === 'object' &&
|
||||
(typeof object.stream === 'function' ||
|
||||
typeof object.arrayBuffer === 'function') &&
|
||||
/^(Blob|File)$/.test(object[Symbol.toStringTag])
|
||||
)
|
||||
}
|
||||
|
||||
function buildURL (url, queryParams) {
|
||||
if (url.includes('?') || url.includes('#')) {
|
||||
throw new Error('Query params cannot be passed when url already contains "?" or "#".')
|
||||
}
|
||||
|
||||
const stringified = stringify(queryParams)
|
||||
|
||||
if (stringified) {
|
||||
url += '?' + stringified
|
||||
}
|
||||
|
||||
return url
|
||||
}
|
||||
|
||||
function parseURL (url) {
|
||||
if (typeof url === 'string') {
|
||||
url = new URL(url)
|
||||
|
||||
if (!/^https?:/.test(url.origin || url.protocol)) {
|
||||
throw new InvalidArgumentError('Invalid URL protocol: the URL must start with `http:` or `https:`.')
|
||||
}
|
||||
|
||||
return url
|
||||
}
|
||||
|
||||
if (!url || typeof url !== 'object') {
|
||||
throw new InvalidArgumentError('Invalid URL: The URL argument must be a non-null object.')
|
||||
}
|
||||
|
||||
if (!/^https?:/.test(url.origin || url.protocol)) {
|
||||
throw new InvalidArgumentError('Invalid URL protocol: the URL must start with `http:` or `https:`.')
|
||||
}
|
||||
|
||||
if (!(url instanceof URL)) {
|
||||
if (url.port != null && url.port !== '' && !Number.isFinite(parseInt(url.port))) {
|
||||
throw new InvalidArgumentError('Invalid URL: port must be a valid integer or a string representation of an integer.')
|
||||
}
|
||||
|
||||
if (url.path != null && typeof url.path !== 'string') {
|
||||
throw new InvalidArgumentError('Invalid URL path: the path must be a string or null/undefined.')
|
||||
}
|
||||
|
||||
if (url.pathname != null && typeof url.pathname !== 'string') {
|
||||
throw new InvalidArgumentError('Invalid URL pathname: the pathname must be a string or null/undefined.')
|
||||
}
|
||||
|
||||
if (url.hostname != null && typeof url.hostname !== 'string') {
|
||||
throw new InvalidArgumentError('Invalid URL hostname: the hostname must be a string or null/undefined.')
|
||||
}
|
||||
|
||||
if (url.origin != null && typeof url.origin !== 'string') {
|
||||
throw new InvalidArgumentError('Invalid URL origin: the origin must be a string or null/undefined.')
|
||||
}
|
||||
|
||||
const port = url.port != null
|
||||
? url.port
|
||||
: (url.protocol === 'https:' ? 443 : 80)
|
||||
let origin = url.origin != null
|
||||
? url.origin
|
||||
: `${url.protocol}//${url.hostname}:${port}`
|
||||
let path = url.path != null
|
||||
? url.path
|
||||
: `${url.pathname || ''}${url.search || ''}`
|
||||
|
||||
if (origin.endsWith('/')) {
|
||||
origin = origin.substring(0, origin.length - 1)
|
||||
}
|
||||
|
||||
if (path && !path.startsWith('/')) {
|
||||
path = `/${path}`
|
||||
}
|
||||
// new URL(path, origin) is unsafe when `path` contains an absolute URL
|
||||
// From https://developer.mozilla.org/en-US/docs/Web/API/URL/URL:
|
||||
// If first parameter is a relative URL, second param is required, and will be used as the base URL.
|
||||
// If first parameter is an absolute URL, a given second param will be ignored.
|
||||
url = new URL(origin + path)
|
||||
}
|
||||
|
||||
return url
|
||||
}
|
||||
|
||||
function parseOrigin (url) {
|
||||
url = parseURL(url)
|
||||
|
||||
if (url.pathname !== '/' || url.search || url.hash) {
|
||||
throw new InvalidArgumentError('invalid url')
|
||||
}
|
||||
|
||||
return url
|
||||
}
|
||||
|
||||
function getHostname (host) {
|
||||
if (host[0] === '[') {
|
||||
const idx = host.indexOf(']')
|
||||
|
||||
assert(idx !== -1)
|
||||
return host.substr(1, idx - 1)
|
||||
}
|
||||
|
||||
const idx = host.indexOf(':')
|
||||
if (idx === -1) return host
|
||||
|
||||
return host.substr(0, idx)
|
||||
}
|
||||
|
||||
// IP addresses are not valid server names per RFC6066
|
||||
// > Currently, the only server names supported are DNS hostnames
|
||||
function getServerName (host) {
|
||||
if (!host) {
|
||||
return null
|
||||
}
|
||||
|
||||
assert.strictEqual(typeof host, 'string')
|
||||
|
||||
const servername = getHostname(host)
|
||||
if (net.isIP(servername)) {
|
||||
return ''
|
||||
}
|
||||
|
||||
return servername
|
||||
}
|
||||
|
||||
function deepClone (obj) {
|
||||
return JSON.parse(JSON.stringify(obj))
|
||||
}
|
||||
|
||||
function isAsyncIterable (obj) {
|
||||
return !!(obj != null && typeof obj[Symbol.asyncIterator] === 'function')
|
||||
}
|
||||
|
||||
function isIterable (obj) {
|
||||
return !!(obj != null && (typeof obj[Symbol.iterator] === 'function' || typeof obj[Symbol.asyncIterator] === 'function'))
|
||||
}
|
||||
|
||||
function bodyLength (body) {
|
||||
if (body == null) {
|
||||
return 0
|
||||
} else if (isStream(body)) {
|
||||
const state = body._readableState
|
||||
return state && state.objectMode === false && state.ended === true && Number.isFinite(state.length)
|
||||
? state.length
|
||||
: null
|
||||
} else if (isBlobLike(body)) {
|
||||
return body.size != null ? body.size : null
|
||||
} else if (isBuffer(body)) {
|
||||
return body.byteLength
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
function isDestroyed (stream) {
|
||||
return !stream || !!(stream.destroyed || stream[kDestroyed])
|
||||
}
|
||||
|
||||
function isReadableAborted (stream) {
|
||||
const state = stream && stream._readableState
|
||||
return isDestroyed(stream) && state && !state.endEmitted
|
||||
}
|
||||
|
||||
function destroy (stream, err) {
|
||||
if (!isStream(stream) || isDestroyed(stream)) {
|
||||
return
|
||||
}
|
||||
|
||||
if (typeof stream.destroy === 'function') {
|
||||
if (Object.getPrototypeOf(stream).constructor === IncomingMessage) {
|
||||
// See: https://github.com/nodejs/node/pull/38505/files
|
||||
stream.socket = null
|
||||
}
|
||||
|
||||
stream.destroy(err)
|
||||
} else if (err) {
|
||||
process.nextTick((stream, err) => {
|
||||
stream.emit('error', err)
|
||||
}, stream, err)
|
||||
}
|
||||
|
||||
if (stream.destroyed !== true) {
|
||||
stream[kDestroyed] = true
|
||||
}
|
||||
}
|
||||
|
||||
const KEEPALIVE_TIMEOUT_EXPR = /timeout=(\d+)/
|
||||
function parseKeepAliveTimeout (val) {
|
||||
const m = val.toString().match(KEEPALIVE_TIMEOUT_EXPR)
|
||||
return m ? parseInt(m[1], 10) * 1000 : null
|
||||
}
|
||||
|
||||
function parseHeaders (headers, obj = {}) {
|
||||
// For H2 support
|
||||
if (!Array.isArray(headers)) return headers
|
||||
|
||||
for (let i = 0; i < headers.length; i += 2) {
|
||||
const key = headers[i].toString().toLowerCase()
|
||||
let val = obj[key]
|
||||
|
||||
if (!val) {
|
||||
if (Array.isArray(headers[i + 1])) {
|
||||
obj[key] = headers[i + 1]
|
||||
} else {
|
||||
obj[key] = headers[i + 1].toString('utf8')
|
||||
}
|
||||
} else {
|
||||
if (!Array.isArray(val)) {
|
||||
val = [val]
|
||||
obj[key] = val
|
||||
}
|
||||
val.push(headers[i + 1].toString('utf8'))
|
||||
}
|
||||
}
|
||||
|
||||
// See https://github.com/nodejs/node/pull/46528
|
||||
if ('content-length' in obj && 'content-disposition' in obj) {
|
||||
obj['content-disposition'] = Buffer.from(obj['content-disposition']).toString('latin1')
|
||||
}
|
||||
|
||||
return obj
|
||||
}
|
||||
|
||||
function parseRawHeaders (headers) {
|
||||
const ret = []
|
||||
let hasContentLength = false
|
||||
let contentDispositionIdx = -1
|
||||
|
||||
for (let n = 0; n < headers.length; n += 2) {
|
||||
const key = headers[n + 0].toString()
|
||||
const val = headers[n + 1].toString('utf8')
|
||||
|
||||
if (key.length === 14 && (key === 'content-length' || key.toLowerCase() === 'content-length')) {
|
||||
ret.push(key, val)
|
||||
hasContentLength = true
|
||||
} else if (key.length === 19 && (key === 'content-disposition' || key.toLowerCase() === 'content-disposition')) {
|
||||
contentDispositionIdx = ret.push(key, val) - 1
|
||||
} else {
|
||||
ret.push(key, val)
|
||||
}
|
||||
}
|
||||
|
||||
// See https://github.com/nodejs/node/pull/46528
|
||||
if (hasContentLength && contentDispositionIdx !== -1) {
|
||||
ret[contentDispositionIdx] = Buffer.from(ret[contentDispositionIdx]).toString('latin1')
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
function isBuffer (buffer) {
|
||||
// See, https://github.com/mcollina/undici/pull/319
|
||||
return buffer instanceof Uint8Array || Buffer.isBuffer(buffer)
|
||||
}
|
||||
|
||||
function validateHandler (handler, method, upgrade) {
|
||||
if (!handler || typeof handler !== 'object') {
|
||||
throw new InvalidArgumentError('handler must be an object')
|
||||
}
|
||||
|
||||
if (typeof handler.onConnect !== 'function') {
|
||||
throw new InvalidArgumentError('invalid onConnect method')
|
||||
}
|
||||
|
||||
if (typeof handler.onError !== 'function') {
|
||||
throw new InvalidArgumentError('invalid onError method')
|
||||
}
|
||||
|
||||
if (typeof handler.onBodySent !== 'function' && handler.onBodySent !== undefined) {
|
||||
throw new InvalidArgumentError('invalid onBodySent method')
|
||||
}
|
||||
|
||||
if (upgrade || method === 'CONNECT') {
|
||||
if (typeof handler.onUpgrade !== 'function') {
|
||||
throw new InvalidArgumentError('invalid onUpgrade method')
|
||||
}
|
||||
} else {
|
||||
if (typeof handler.onHeaders !== 'function') {
|
||||
throw new InvalidArgumentError('invalid onHeaders method')
|
||||
}
|
||||
|
||||
if (typeof handler.onData !== 'function') {
|
||||
throw new InvalidArgumentError('invalid onData method')
|
||||
}
|
||||
|
||||
if (typeof handler.onComplete !== 'function') {
|
||||
throw new InvalidArgumentError('invalid onComplete method')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A body is disturbed if it has been read from and it cannot
|
||||
// be re-used without losing state or data.
|
||||
function isDisturbed (body) {
|
||||
return !!(body && (
|
||||
stream.isDisturbed
|
||||
? stream.isDisturbed(body) || body[kBodyUsed] // TODO (fix): Why is body[kBodyUsed] needed?
|
||||
: body[kBodyUsed] ||
|
||||
body.readableDidRead ||
|
||||
(body._readableState && body._readableState.dataEmitted) ||
|
||||
isReadableAborted(body)
|
||||
))
|
||||
}
|
||||
|
||||
function isErrored (body) {
|
||||
return !!(body && (
|
||||
stream.isErrored
|
||||
? stream.isErrored(body)
|
||||
: /state: 'errored'/.test(nodeUtil.inspect(body)
|
||||
)))
|
||||
}
|
||||
|
||||
function isReadable (body) {
|
||||
return !!(body && (
|
||||
stream.isReadable
|
||||
? stream.isReadable(body)
|
||||
: /state: 'readable'/.test(nodeUtil.inspect(body)
|
||||
)))
|
||||
}
|
||||
|
||||
function getSocketInfo (socket) {
|
||||
return {
|
||||
localAddress: socket.localAddress,
|
||||
localPort: socket.localPort,
|
||||
remoteAddress: socket.remoteAddress,
|
||||
remotePort: socket.remotePort,
|
||||
remoteFamily: socket.remoteFamily,
|
||||
timeout: socket.timeout,
|
||||
bytesWritten: socket.bytesWritten,
|
||||
bytesRead: socket.bytesRead
|
||||
}
|
||||
}
|
||||
|
||||
async function * convertIterableToBuffer (iterable) {
|
||||
for await (const chunk of iterable) {
|
||||
yield Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)
|
||||
}
|
||||
}
|
||||
|
||||
let ReadableStream
|
||||
function ReadableStreamFrom (iterable) {
|
||||
if (!ReadableStream) {
|
||||
ReadableStream = require('stream/web').ReadableStream
|
||||
}
|
||||
|
||||
if (ReadableStream.from) {
|
||||
return ReadableStream.from(convertIterableToBuffer(iterable))
|
||||
}
|
||||
|
||||
let iterator
|
||||
return new ReadableStream(
|
||||
{
|
||||
async start () {
|
||||
iterator = iterable[Symbol.asyncIterator]()
|
||||
},
|
||||
async pull (controller) {
|
||||
const { done, value } = await iterator.next()
|
||||
if (done) {
|
||||
queueMicrotask(() => {
|
||||
controller.close()
|
||||
})
|
||||
} else {
|
||||
const buf = Buffer.isBuffer(value) ? value : Buffer.from(value)
|
||||
controller.enqueue(new Uint8Array(buf))
|
||||
}
|
||||
return controller.desiredSize > 0
|
||||
},
|
||||
async cancel (reason) {
|
||||
await iterator.return()
|
||||
}
|
||||
},
|
||||
0
|
||||
)
|
||||
}
|
||||
|
||||
// The chunk should be a FormData instance and contains
|
||||
// all the required methods.
|
||||
function isFormDataLike (object) {
|
||||
return (
|
||||
object &&
|
||||
typeof object === 'object' &&
|
||||
typeof object.append === 'function' &&
|
||||
typeof object.delete === 'function' &&
|
||||
typeof object.get === 'function' &&
|
||||
typeof object.getAll === 'function' &&
|
||||
typeof object.has === 'function' &&
|
||||
typeof object.set === 'function' &&
|
||||
object[Symbol.toStringTag] === 'FormData'
|
||||
)
|
||||
}
|
||||
|
||||
function throwIfAborted (signal) {
|
||||
if (!signal) { return }
|
||||
if (typeof signal.throwIfAborted === 'function') {
|
||||
signal.throwIfAborted()
|
||||
} else {
|
||||
if (signal.aborted) {
|
||||
// DOMException not available < v17.0.0
|
||||
const err = new Error('The operation was aborted')
|
||||
err.name = 'AbortError'
|
||||
throw err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let events
|
||||
function addAbortListener (signal, listener) {
|
||||
if (typeof Symbol.dispose === 'symbol') {
|
||||
if (!events) {
|
||||
events = require('events')
|
||||
}
|
||||
if (typeof events.addAbortListener === 'function' && 'aborted' in signal) {
|
||||
return events.addAbortListener(signal, listener)
|
||||
}
|
||||
}
|
||||
if ('addEventListener' in signal) {
|
||||
signal.addEventListener('abort', listener, { once: true })
|
||||
return () => signal.removeEventListener('abort', listener)
|
||||
}
|
||||
signal.addListener('abort', listener)
|
||||
return () => signal.removeListener('abort', listener)
|
||||
}
|
||||
|
||||
const hasToWellFormed = !!String.prototype.toWellFormed
|
||||
|
||||
/**
|
||||
* @param {string} val
|
||||
*/
|
||||
function toUSVString (val) {
|
||||
if (hasToWellFormed) {
|
||||
return `${val}`.toWellFormed()
|
||||
} else if (nodeUtil.toUSVString) {
|
||||
return nodeUtil.toUSVString(val)
|
||||
}
|
||||
|
||||
return `${val}`
|
||||
}
|
||||
|
||||
const kEnumerableProperty = Object.create(null)
|
||||
kEnumerableProperty.enumerable = true
|
||||
|
||||
module.exports = {
|
||||
kEnumerableProperty,
|
||||
nop,
|
||||
isDisturbed,
|
||||
isErrored,
|
||||
isReadable,
|
||||
toUSVString,
|
||||
isReadableAborted,
|
||||
isBlobLike,
|
||||
parseOrigin,
|
||||
parseURL,
|
||||
getServerName,
|
||||
isStream,
|
||||
isIterable,
|
||||
isAsyncIterable,
|
||||
isDestroyed,
|
||||
parseRawHeaders,
|
||||
parseHeaders,
|
||||
parseKeepAliveTimeout,
|
||||
destroy,
|
||||
bodyLength,
|
||||
deepClone,
|
||||
ReadableStreamFrom,
|
||||
isBuffer,
|
||||
validateHandler,
|
||||
getSocketInfo,
|
||||
isFormDataLike,
|
||||
buildURL,
|
||||
throwIfAborted,
|
||||
addAbortListener,
|
||||
nodeMajor,
|
||||
nodeMinor,
|
||||
nodeHasAutoSelectFamily: nodeMajor > 18 || (nodeMajor === 18 && nodeMinor >= 13)
|
||||
}
|
|
@ -0,0 +1,192 @@
|
|||
'use strict'
|
||||
|
||||
const Dispatcher = require('./dispatcher')
|
||||
const {
|
||||
ClientDestroyedError,
|
||||
ClientClosedError,
|
||||
InvalidArgumentError
|
||||
} = require('./core/errors')
|
||||
const { kDestroy, kClose, kDispatch, kInterceptors } = require('./core/symbols')
|
||||
|
||||
const kDestroyed = Symbol('destroyed')
|
||||
const kClosed = Symbol('closed')
|
||||
const kOnDestroyed = Symbol('onDestroyed')
|
||||
const kOnClosed = Symbol('onClosed')
|
||||
const kInterceptedDispatch = Symbol('Intercepted Dispatch')
|
||||
|
||||
class DispatcherBase extends Dispatcher {
|
||||
constructor () {
|
||||
super()
|
||||
|
||||
this[kDestroyed] = false
|
||||
this[kOnDestroyed] = null
|
||||
this[kClosed] = false
|
||||
this[kOnClosed] = []
|
||||
}
|
||||
|
||||
get destroyed () {
|
||||
return this[kDestroyed]
|
||||
}
|
||||
|
||||
get closed () {
|
||||
return this[kClosed]
|
||||
}
|
||||
|
||||
get interceptors () {
|
||||
return this[kInterceptors]
|
||||
}
|
||||
|
||||
set interceptors (newInterceptors) {
|
||||
if (newInterceptors) {
|
||||
for (let i = newInterceptors.length - 1; i >= 0; i--) {
|
||||
const interceptor = this[kInterceptors][i]
|
||||
if (typeof interceptor !== 'function') {
|
||||
throw new InvalidArgumentError('interceptor must be an function')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this[kInterceptors] = newInterceptors
|
||||
}
|
||||
|
||||
close (callback) {
|
||||
if (callback === undefined) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.close((err, data) => {
|
||||
return err ? reject(err) : resolve(data)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
if (typeof callback !== 'function') {
|
||||
throw new InvalidArgumentError('invalid callback')
|
||||
}
|
||||
|
||||
if (this[kDestroyed]) {
|
||||
queueMicrotask(() => callback(new ClientDestroyedError(), null))
|
||||
return
|
||||
}
|
||||
|
||||
if (this[kClosed]) {
|
||||
if (this[kOnClosed]) {
|
||||
this[kOnClosed].push(callback)
|
||||
} else {
|
||||
queueMicrotask(() => callback(null, null))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
this[kClosed] = true
|
||||
this[kOnClosed].push(callback)
|
||||
|
||||
const onClosed = () => {
|
||||
const callbacks = this[kOnClosed]
|
||||
this[kOnClosed] = null
|
||||
for (let i = 0; i < callbacks.length; i++) {
|
||||
callbacks[i](null, null)
|
||||
}
|
||||
}
|
||||
|
||||
// Should not error.
|
||||
this[kClose]()
|
||||
.then(() => this.destroy())
|
||||
.then(() => {
|
||||
queueMicrotask(onClosed)
|
||||
})
|
||||
}
|
||||
|
||||
destroy (err, callback) {
|
||||
if (typeof err === 'function') {
|
||||
callback = err
|
||||
err = null
|
||||
}
|
||||
|
||||
if (callback === undefined) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.destroy(err, (err, data) => {
|
||||
return err ? /* istanbul ignore next: should never error */ reject(err) : resolve(data)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
if (typeof callback !== 'function') {
|
||||
throw new InvalidArgumentError('invalid callback')
|
||||
}
|
||||
|
||||
if (this[kDestroyed]) {
|
||||
if (this[kOnDestroyed]) {
|
||||
this[kOnDestroyed].push(callback)
|
||||
} else {
|
||||
queueMicrotask(() => callback(null, null))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (!err) {
|
||||
err = new ClientDestroyedError()
|
||||
}
|
||||
|
||||
this[kDestroyed] = true
|
||||
this[kOnDestroyed] = this[kOnDestroyed] || []
|
||||
this[kOnDestroyed].push(callback)
|
||||
|
||||
const onDestroyed = () => {
|
||||
const callbacks = this[kOnDestroyed]
|
||||
this[kOnDestroyed] = null
|
||||
for (let i = 0; i < callbacks.length; i++) {
|
||||
callbacks[i](null, null)
|
||||
}
|
||||
}
|
||||
|
||||
// Should not error.
|
||||
this[kDestroy](err).then(() => {
|
||||
queueMicrotask(onDestroyed)
|
||||
})
|
||||
}
|
||||
|
||||
[kInterceptedDispatch] (opts, handler) {
|
||||
if (!this[kInterceptors] || this[kInterceptors].length === 0) {
|
||||
this[kInterceptedDispatch] = this[kDispatch]
|
||||
return this[kDispatch](opts, handler)
|
||||
}
|
||||
|
||||
let dispatch = this[kDispatch].bind(this)
|
||||
for (let i = this[kInterceptors].length - 1; i >= 0; i--) {
|
||||
dispatch = this[kInterceptors][i](dispatch)
|
||||
}
|
||||
this[kInterceptedDispatch] = dispatch
|
||||
return dispatch(opts, handler)
|
||||
}
|
||||
|
||||
dispatch (opts, handler) {
|
||||
if (!handler || typeof handler !== 'object') {
|
||||
throw new InvalidArgumentError('handler must be an object')
|
||||
}
|
||||
|
||||
try {
|
||||
if (!opts || typeof opts !== 'object') {
|
||||
throw new InvalidArgumentError('opts must be an object.')
|
||||
}
|
||||
|
||||
if (this[kDestroyed] || this[kOnDestroyed]) {
|
||||
throw new ClientDestroyedError()
|
||||
}
|
||||
|
||||
if (this[kClosed]) {
|
||||
throw new ClientClosedError()
|
||||
}
|
||||
|
||||
return this[kInterceptedDispatch](opts, handler)
|
||||
} catch (err) {
|
||||
if (typeof handler.onError !== 'function') {
|
||||
throw new InvalidArgumentError('invalid onError method')
|
||||
}
|
||||
|
||||
handler.onError(err)
|
||||
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = DispatcherBase
|
|
@ -0,0 +1,19 @@
|
|||
'use strict'
|
||||
|
||||
const EventEmitter = require('events')
|
||||
|
||||
class Dispatcher extends EventEmitter {
|
||||
dispatch () {
|
||||
throw new Error('not implemented')
|
||||
}
|
||||
|
||||
close () {
|
||||
throw new Error('not implemented')
|
||||
}
|
||||
|
||||
destroy () {
|
||||
throw new Error('not implemented')
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Dispatcher
|
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2020 Ethan Arrowood
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
|
@ -0,0 +1,605 @@
|
|||
'use strict'
|
||||
|
||||
const Busboy = require('@fastify/busboy')
|
||||
const util = require('../core/util')
|
||||
const {
|
||||
ReadableStreamFrom,
|
||||
isBlobLike,
|
||||
isReadableStreamLike,
|
||||
readableStreamClose,
|
||||
createDeferredPromise,
|
||||
fullyReadBody
|
||||
} = require('./util')
|
||||
const { FormData } = require('./formdata')
|
||||
const { kState } = require('./symbols')
|
||||
const { webidl } = require('./webidl')
|
||||
const { DOMException, structuredClone } = require('./constants')
|
||||
const { Blob, File: NativeFile } = require('buffer')
|
||||
const { kBodyUsed } = require('../core/symbols')
|
||||
const assert = require('assert')
|
||||
const { isErrored } = require('../core/util')
|
||||
const { isUint8Array, isArrayBuffer } = require('util/types')
|
||||
const { File: UndiciFile } = require('./file')
|
||||
const { parseMIMEType, serializeAMimeType } = require('./dataURL')
|
||||
|
||||
let ReadableStream = globalThis.ReadableStream
|
||||
|
||||
/** @type {globalThis['File']} */
|
||||
const File = NativeFile ?? UndiciFile
|
||||
const textEncoder = new TextEncoder()
|
||||
const textDecoder = new TextDecoder()
|
||||
|
||||
// https://fetch.spec.whatwg.org/#concept-bodyinit-extract
|
||||
function extractBody (object, keepalive = false) {
|
||||
if (!ReadableStream) {
|
||||
ReadableStream = require('stream/web').ReadableStream
|
||||
}
|
||||
|
||||
// 1. Let stream be null.
|
||||
let stream = null
|
||||
|
||||
// 2. If object is a ReadableStream object, then set stream to object.
|
||||
if (object instanceof ReadableStream) {
|
||||
stream = object
|
||||
} else if (isBlobLike(object)) {
|
||||
// 3. Otherwise, if object is a Blob object, set stream to the
|
||||
// result of running object’s get stream.
|
||||
stream = object.stream()
|
||||
} else {
|
||||
// 4. Otherwise, set stream to a new ReadableStream object, and set
|
||||
// up stream.
|
||||
stream = new ReadableStream({
|
||||
async pull (controller) {
|
||||
controller.enqueue(
|
||||
typeof source === 'string' ? textEncoder.encode(source) : source
|
||||
)
|
||||
queueMicrotask(() => readableStreamClose(controller))
|
||||
},
|
||||
start () {},
|
||||
type: undefined
|
||||
})
|
||||
}
|
||||
|
||||
// 5. Assert: stream is a ReadableStream object.
|
||||
assert(isReadableStreamLike(stream))
|
||||
|
||||
// 6. Let action be null.
|
||||
let action = null
|
||||
|
||||
// 7. Let source be null.
|
||||
let source = null
|
||||
|
||||
// 8. Let length be null.
|
||||
let length = null
|
||||
|
||||
// 9. Let type be null.
|
||||
let type = null
|
||||
|
||||
// 10. Switch on object:
|
||||
if (typeof object === 'string') {
|
||||
// Set source to the UTF-8 encoding of object.
|
||||
// Note: setting source to a Uint8Array here breaks some mocking assumptions.
|
||||
source = object
|
||||
|
||||
// Set type to `text/plain;charset=UTF-8`.
|
||||
type = 'text/plain;charset=UTF-8'
|
||||
} else if (object instanceof URLSearchParams) {
|
||||
// URLSearchParams
|
||||
|
||||
// spec says to run application/x-www-form-urlencoded on body.list
|
||||
// this is implemented in Node.js as apart of an URLSearchParams instance toString method
|
||||
// See: https://github.com/nodejs/node/blob/e46c680bf2b211bbd52cf959ca17ee98c7f657f5/lib/internal/url.js#L490
|
||||
// and https://github.com/nodejs/node/blob/e46c680bf2b211bbd52cf959ca17ee98c7f657f5/lib/internal/url.js#L1100
|
||||
|
||||
// Set source to the result of running the application/x-www-form-urlencoded serializer with object’s list.
|
||||
source = object.toString()
|
||||
|
||||
// Set type to `application/x-www-form-urlencoded;charset=UTF-8`.
|
||||
type = 'application/x-www-form-urlencoded;charset=UTF-8'
|
||||
} else if (isArrayBuffer(object)) {
|
||||
// BufferSource/ArrayBuffer
|
||||
|
||||
// Set source to a copy of the bytes held by object.
|
||||
source = new Uint8Array(object.slice())
|
||||
} else if (ArrayBuffer.isView(object)) {
|
||||
// BufferSource/ArrayBufferView
|
||||
|
||||
// Set source to a copy of the bytes held by object.
|
||||
source = new Uint8Array(object.buffer.slice(object.byteOffset, object.byteOffset + object.byteLength))
|
||||
} else if (util.isFormDataLike(object)) {
|
||||
const boundary = `----formdata-undici-0${`${Math.floor(Math.random() * 1e11)}`.padStart(11, '0')}`
|
||||
const prefix = `--${boundary}\r\nContent-Disposition: form-data`
|
||||
|
||||
/*! formdata-polyfill. MIT License. Jimmy Wärting <https://jimmy.warting.se/opensource> */
|
||||
const escape = (str) =>
|
||||
str.replace(/\n/g, '%0A').replace(/\r/g, '%0D').replace(/"/g, '%22')
|
||||
const normalizeLinefeeds = (value) => value.replace(/\r?\n|\r/g, '\r\n')
|
||||
|
||||
// Set action to this step: run the multipart/form-data
|
||||
// encoding algorithm, with object’s entry list and UTF-8.
|
||||
// - This ensures that the body is immutable and can't be changed afterwords
|
||||
// - That the content-length is calculated in advance.
|
||||
// - And that all parts are pre-encoded and ready to be sent.
|
||||
|
||||
const blobParts = []
|
||||
const rn = new Uint8Array([13, 10]) // '\r\n'
|
||||
length = 0
|
||||
let hasUnknownSizeValue = false
|
||||
|
||||
for (const [name, value] of object) {
|
||||
if (typeof value === 'string') {
|
||||
const chunk = textEncoder.encode(prefix +
|
||||
`; name="${escape(normalizeLinefeeds(name))}"` +
|
||||
`\r\n\r\n${normalizeLinefeeds(value)}\r\n`)
|
||||
blobParts.push(chunk)
|
||||
length += chunk.byteLength
|
||||
} else {
|
||||
const chunk = textEncoder.encode(`${prefix}; name="${escape(normalizeLinefeeds(name))}"` +
|
||||
(value.name ? `; filename="${escape(value.name)}"` : '') + '\r\n' +
|
||||
`Content-Type: ${
|
||||
value.type || 'application/octet-stream'
|
||||
}\r\n\r\n`)
|
||||
blobParts.push(chunk, value, rn)
|
||||
if (typeof value.size === 'number') {
|
||||
length += chunk.byteLength + value.size + rn.byteLength
|
||||
} else {
|
||||
hasUnknownSizeValue = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const chunk = textEncoder.encode(`--${boundary}--`)
|
||||
blobParts.push(chunk)
|
||||
length += chunk.byteLength
|
||||
if (hasUnknownSizeValue) {
|
||||
length = null
|
||||
}
|
||||
|
||||
// Set source to object.
|
||||
source = object
|
||||
|
||||
action = async function * () {
|
||||
for (const part of blobParts) {
|
||||
if (part.stream) {
|
||||
yield * part.stream()
|
||||
} else {
|
||||
yield part
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Set type to `multipart/form-data; boundary=`,
|
||||
// followed by the multipart/form-data boundary string generated
|
||||
// by the multipart/form-data encoding algorithm.
|
||||
type = 'multipart/form-data; boundary=' + boundary
|
||||
} else if (isBlobLike(object)) {
|
||||
// Blob
|
||||
|
||||
// Set source to object.
|
||||
source = object
|
||||
|
||||
// Set length to object’s size.
|
||||
length = object.size
|
||||
|
||||
// If object’s type attribute is not the empty byte sequence, set
|
||||
// type to its value.
|
||||
if (object.type) {
|
||||
type = object.type
|
||||
}
|
||||
} else if (typeof object[Symbol.asyncIterator] === 'function') {
|
||||
// If keepalive is true, then throw a TypeError.
|
||||
if (keepalive) {
|
||||
throw new TypeError('keepalive')
|
||||
}
|
||||
|
||||
// If object is disturbed or locked, then throw a TypeError.
|
||||
if (util.isDisturbed(object) || object.locked) {
|
||||
throw new TypeError(
|
||||
'Response body object should not be disturbed or locked'
|
||||
)
|
||||
}
|
||||
|
||||
stream =
|
||||
object instanceof ReadableStream ? object : ReadableStreamFrom(object)
|
||||
}
|
||||
|
||||
// 11. If source is a byte sequence, then set action to a
|
||||
// step that returns source and length to source’s length.
|
||||
if (typeof source === 'string' || util.isBuffer(source)) {
|
||||
length = Buffer.byteLength(source)
|
||||
}
|
||||
|
||||
// 12. If action is non-null, then run these steps in in parallel:
|
||||
if (action != null) {
|
||||
// Run action.
|
||||
let iterator
|
||||
stream = new ReadableStream({
|
||||
async start () {
|
||||
iterator = action(object)[Symbol.asyncIterator]()
|
||||
},
|
||||
async pull (controller) {
|
||||
const { value, done } = await iterator.next()
|
||||
if (done) {
|
||||
// When running action is done, close stream.
|
||||
queueMicrotask(() => {
|
||||
controller.close()
|
||||
})
|
||||
} 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)) {
|
||||
controller.enqueue(new Uint8Array(value))
|
||||
}
|
||||
}
|
||||
return controller.desiredSize > 0
|
||||
},
|
||||
async cancel (reason) {
|
||||
await iterator.return()
|
||||
},
|
||||
type: undefined
|
||||
})
|
||||
}
|
||||
|
||||
// 13. Let body be a body whose stream is stream, source is source,
|
||||
// and length is length.
|
||||
const body = { stream, source, length }
|
||||
|
||||
// 14. Return (body, type).
|
||||
return [body, type]
|
||||
}
|
||||
|
||||
// https://fetch.spec.whatwg.org/#bodyinit-safely-extract
|
||||
function safelyExtractBody (object, keepalive = false) {
|
||||
if (!ReadableStream) {
|
||||
// istanbul ignore next
|
||||
ReadableStream = require('stream/web').ReadableStream
|
||||
}
|
||||
|
||||
// To safely extract a body and a `Content-Type` value from
|
||||
// a byte sequence or BodyInit object object, run these steps:
|
||||
|
||||
// 1. If object is a ReadableStream object, then:
|
||||
if (object instanceof ReadableStream) {
|
||||
// Assert: object is neither disturbed nor locked.
|
||||
// istanbul ignore next
|
||||
assert(!util.isDisturbed(object), 'The body has already been consumed.')
|
||||
// istanbul ignore next
|
||||
assert(!object.locked, 'The stream is locked.')
|
||||
}
|
||||
|
||||
// 2. Return the results of extracting object.
|
||||
return extractBody(object, keepalive)
|
||||
}
|
||||
|
||||
function cloneBody (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 body’s stream.
|
||||
const [out1, out2] = body.stream.tee()
|
||||
const out2Clone = structuredClone(out2, { transfer: [out2] })
|
||||
// This, for whatever reasons, unrefs out2Clone which allows
|
||||
// the process to exit by itself.
|
||||
const [, finalClone] = out2Clone.tee()
|
||||
|
||||
// 2. Set body’s stream to out1.
|
||||
body.stream = out1
|
||||
|
||||
// 3. Return a body whose stream is out2 and other members are copied from body.
|
||||
return {
|
||||
stream: finalClone,
|
||||
length: body.length,
|
||||
source: body.source
|
||||
}
|
||||
}
|
||||
|
||||
async function * consumeBody (body) {
|
||||
if (body) {
|
||||
if (isUint8Array(body)) {
|
||||
yield body
|
||||
} else {
|
||||
const stream = body.stream
|
||||
|
||||
if (util.isDisturbed(stream)) {
|
||||
throw new TypeError('The body has already been consumed.')
|
||||
}
|
||||
|
||||
if (stream.locked) {
|
||||
throw new TypeError('The stream is locked.')
|
||||
}
|
||||
|
||||
// Compat.
|
||||
stream[kBodyUsed] = true
|
||||
|
||||
yield * stream
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function throwIfAborted (state) {
|
||||
if (state.aborted) {
|
||||
throw new DOMException('The operation was aborted.', 'AbortError')
|
||||
}
|
||||
}
|
||||
|
||||
function bodyMixinMethods (instance) {
|
||||
const methods = {
|
||||
blob () {
|
||||
// The blob() method steps are to return the result of
|
||||
// running consume body with this and the following step
|
||||
// given a byte sequence bytes: return a Blob whose
|
||||
// contents are bytes and whose type attribute is this’s
|
||||
// MIME type.
|
||||
return specConsumeBody(this, (bytes) => {
|
||||
let mimeType = bodyMimeType(this)
|
||||
|
||||
if (mimeType === 'failure') {
|
||||
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)
|
||||
},
|
||||
|
||||
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 specConsumeBody(this, (bytes) => {
|
||||
return new Uint8Array(bytes).buffer
|
||||
}, instance)
|
||||
},
|
||||
|
||||
text () {
|
||||
// The text() method steps are to return the result of running
|
||||
// consume body with this and UTF-8 decode.
|
||||
return specConsumeBody(this, utf8DecodeBytes, instance)
|
||||
},
|
||||
|
||||
json () {
|
||||
// The json() method steps are to return the result of running
|
||||
// consume body with this and parse JSON from bytes.
|
||||
return specConsumeBody(this, parseJSONFromBytes, instance)
|
||||
},
|
||||
|
||||
async formData () {
|
||||
webidl.brandCheck(this, instance)
|
||||
|
||||
throwIfAborted(this[kState])
|
||||
|
||||
const contentType = this.headers.get('Content-Type')
|
||||
|
||||
// If mimeType’s essence is "multipart/form-data", then:
|
||||
if (/multipart\/form-data/.test(contentType)) {
|
||||
const headers = {}
|
||||
for (const [key, value] of this.headers) headers[key.toLowerCase()] = value
|
||||
|
||||
const responseFormData = new FormData()
|
||||
|
||||
let busboy
|
||||
|
||||
try {
|
||||
busboy = new Busboy({
|
||||
headers,
|
||||
preservePath: true
|
||||
})
|
||||
} catch (err) {
|
||||
throw new DOMException(`${err}`, 'AbortError')
|
||||
}
|
||||
|
||||
busboy.on('field', (name, value) => {
|
||||
responseFormData.append(name, value)
|
||||
})
|
||||
busboy.on('file', (name, value, filename, encoding, mimeType) => {
|
||||
const chunks = []
|
||||
|
||||
if (encoding === 'base64' || encoding.toLowerCase() === 'base64') {
|
||||
let base64chunk = ''
|
||||
|
||||
value.on('data', (chunk) => {
|
||||
base64chunk += chunk.toString().replace(/[\r\n]/gm, '')
|
||||
|
||||
const end = base64chunk.length - base64chunk.length % 4
|
||||
chunks.push(Buffer.from(base64chunk.slice(0, end), 'base64'))
|
||||
|
||||
base64chunk = base64chunk.slice(end)
|
||||
})
|
||||
value.on('end', () => {
|
||||
chunks.push(Buffer.from(base64chunk, 'base64'))
|
||||
responseFormData.append(name, new File(chunks, filename, { type: mimeType }))
|
||||
})
|
||||
} else {
|
||||
value.on('data', (chunk) => {
|
||||
chunks.push(chunk)
|
||||
})
|
||||
value.on('end', () => {
|
||||
responseFormData.append(name, new File(chunks, filename, { type: mimeType }))
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
const busboyResolve = new Promise((resolve, reject) => {
|
||||
busboy.on('finish', resolve)
|
||||
busboy.on('error', (err) => reject(new TypeError(err)))
|
||||
})
|
||||
|
||||
if (this.body !== null) for await (const chunk of consumeBody(this[kState].body)) busboy.write(chunk)
|
||||
busboy.end()
|
||||
await busboyResolve
|
||||
|
||||
return responseFormData
|
||||
} else if (/application\/x-www-form-urlencoded/.test(contentType)) {
|
||||
// Otherwise, if mimeType’s essence is "application/x-www-form-urlencoded", then:
|
||||
|
||||
// 1. Let entries be the result of parsing bytes.
|
||||
let entries
|
||||
try {
|
||||
let text = ''
|
||||
// application/x-www-form-urlencoded parser will keep the BOM.
|
||||
// https://url.spec.whatwg.org/#concept-urlencoded-parser
|
||||
// Note that streaming decoder is stateful and cannot be reused
|
||||
const streamingDecoder = new TextDecoder('utf-8', { ignoreBOM: true })
|
||||
|
||||
for await (const chunk of consumeBody(this[kState].body)) {
|
||||
if (!isUint8Array(chunk)) {
|
||||
throw new TypeError('Expected Uint8Array chunk')
|
||||
}
|
||||
text += streamingDecoder.decode(chunk, { stream: true })
|
||||
}
|
||||
text += streamingDecoder.decode()
|
||||
entries = new URLSearchParams(text)
|
||||
} catch (err) {
|
||||
// istanbul ignore next: Unclear when new URLSearchParams can fail on a string.
|
||||
// 2. If entries is failure, then throw a TypeError.
|
||||
throw Object.assign(new TypeError(), { cause: err })
|
||||
}
|
||||
|
||||
// 3. Return a new FormData object whose entries are entries.
|
||||
const formData = new FormData()
|
||||
for (const [name, value] of entries) {
|
||||
formData.append(name, value)
|
||||
}
|
||||
return formData
|
||||
} else {
|
||||
// Wait a tick before checking if the request has been aborted.
|
||||
// Otherwise, a TypeError can be thrown when an AbortError should.
|
||||
await Promise.resolve()
|
||||
|
||||
throwIfAborted(this[kState])
|
||||
|
||||
// Otherwise, throw a TypeError.
|
||||
throw webidl.errors.exception({
|
||||
header: `${instance.name}.formData`,
|
||||
message: 'Could not parse content as FormData.'
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return methods
|
||||
}
|
||||
|
||||
function mixinBody (prototype) {
|
||||
Object.assign(prototype.prototype, bodyMixinMethods(prototype))
|
||||
}
|
||||
|
||||
/**
|
||||
* @see https://fetch.spec.whatwg.org/#concept-body-consume-body
|
||||
* @param {Response|Request} object
|
||||
* @param {(value: unknown) => unknown} convertBytesToJSValue
|
||||
* @param {Response|Request} instance
|
||||
*/
|
||||
async function specConsumeBody (object, convertBytesToJSValue, instance) {
|
||||
webidl.brandCheck(object, instance)
|
||||
|
||||
throwIfAborted(object[kState])
|
||||
|
||||
// 1. If object is unusable, then return a promise rejected
|
||||
// with a TypeError.
|
||||
if (bodyUnusable(object[kState].body)) {
|
||||
throw new TypeError('Body is unusable')
|
||||
}
|
||||
|
||||
// 2. Let promise be a new promise.
|
||||
const promise = createDeferredPromise()
|
||||
|
||||
// 3. Let errorSteps given error be to reject promise with error.
|
||||
const errorSteps = (error) => promise.reject(error)
|
||||
|
||||
// 4. Let successSteps given a byte sequence data be to resolve
|
||||
// promise with the result of running convertBytesToJSValue
|
||||
// with data. If that threw an exception, then run errorSteps
|
||||
// with that exception.
|
||||
const successSteps = (data) => {
|
||||
try {
|
||||
promise.resolve(convertBytesToJSValue(data))
|
||||
} catch (e) {
|
||||
errorSteps(e)
|
||||
}
|
||||
}
|
||||
|
||||
// 5. If object’s body is null, then run successSteps with an
|
||||
// empty byte sequence.
|
||||
if (object[kState].body == null) {
|
||||
successSteps(new Uint8Array())
|
||||
return promise.promise
|
||||
}
|
||||
|
||||
// 6. Otherwise, fully read object’s body given successSteps,
|
||||
// errorSteps, and object’s relevant global object.
|
||||
await fullyReadBody(object[kState].body, successSteps, errorSteps)
|
||||
|
||||
// 7. Return promise.
|
||||
return promise.promise
|
||||
}
|
||||
|
||||
// https://fetch.spec.whatwg.org/#body-unusable
|
||||
function bodyUnusable (body) {
|
||||
// An object including the Body interface mixin is
|
||||
// said to be unusable if its body is non-null and
|
||||
// its body’s stream is disturbed or locked.
|
||||
return body != null && (body.stream.locked || util.isDisturbed(body.stream))
|
||||
}
|
||||
|
||||
/**
|
||||
* @see https://encoding.spec.whatwg.org/#utf-8-decode
|
||||
* @param {Buffer} buffer
|
||||
*/
|
||||
function utf8DecodeBytes (buffer) {
|
||||
if (buffer.length === 0) {
|
||||
return ''
|
||||
}
|
||||
|
||||
// 1. Let buffer be the result of peeking three bytes from
|
||||
// ioQueue, converted to a byte sequence.
|
||||
|
||||
// 2. If buffer is 0xEF 0xBB 0xBF, then read three
|
||||
// bytes from ioQueue. (Do nothing with those bytes.)
|
||||
if (buffer[0] === 0xEF && buffer[1] === 0xBB && buffer[2] === 0xBF) {
|
||||
buffer = buffer.subarray(3)
|
||||
}
|
||||
|
||||
// 3. Process a queue with an instance of UTF-8’s
|
||||
// decoder, ioQueue, output, and "replacement".
|
||||
const output = textDecoder.decode(buffer)
|
||||
|
||||
// 4. Return output.
|
||||
return output
|
||||
}
|
||||
|
||||
/**
|
||||
* @see https://infra.spec.whatwg.org/#parse-json-bytes-to-a-javascript-value
|
||||
* @param {Uint8Array} bytes
|
||||
*/
|
||||
function parseJSONFromBytes (bytes) {
|
||||
return JSON.parse(utf8DecodeBytes(bytes))
|
||||
}
|
||||
|
||||
/**
|
||||
* @see https://fetch.spec.whatwg.org/#concept-body-mime-type
|
||||
* @param {import('./response').Response|import('./request').Request} object
|
||||
*/
|
||||
function bodyMimeType (object) {
|
||||
const { headersList } = object[kState]
|
||||
const contentType = headersList.get('content-type')
|
||||
|
||||
if (contentType === null) {
|
||||
return 'failure'
|
||||
}
|
||||
|
||||
return parseMIMEType(contentType)
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
extractBody,
|
||||
safelyExtractBody,
|
||||
cloneBody,
|
||||
mixinBody
|
||||
}
|
|
@ -0,0 +1,151 @@
|
|||
'use strict'
|
||||
|
||||
const { MessageChannel, receiveMessageOnPort } = require('worker_threads')
|
||||
|
||||
const corsSafeListedMethods = ['GET', 'HEAD', 'POST']
|
||||
const corsSafeListedMethodsSet = new Set(corsSafeListedMethods)
|
||||
|
||||
const nullBodyStatus = [101, 204, 205, 304]
|
||||
|
||||
const redirectStatus = [301, 302, 303, 307, 308]
|
||||
const redirectStatusSet = new Set(redirectStatus)
|
||||
|
||||
// https://fetch.spec.whatwg.org/#block-bad-port
|
||||
const 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', '5060', '5061', '6000', '6566', '6665', '6666', '6667', '6668', '6669', '6697',
|
||||
'10080'
|
||||
]
|
||||
|
||||
const badPortsSet = new Set(badPorts)
|
||||
|
||||
// https://w3c.github.io/webappsec-referrer-policy/#referrer-policies
|
||||
const referrerPolicy = [
|
||||
'',
|
||||
'no-referrer',
|
||||
'no-referrer-when-downgrade',
|
||||
'same-origin',
|
||||
'origin',
|
||||
'strict-origin',
|
||||
'origin-when-cross-origin',
|
||||
'strict-origin-when-cross-origin',
|
||||
'unsafe-url'
|
||||
]
|
||||
const referrerPolicySet = new Set(referrerPolicy)
|
||||
|
||||
const requestRedirect = ['follow', 'manual', 'error']
|
||||
|
||||
const safeMethods = ['GET', 'HEAD', 'OPTIONS', 'TRACE']
|
||||
const safeMethodsSet = new Set(safeMethods)
|
||||
|
||||
const requestMode = ['navigate', 'same-origin', 'no-cors', 'cors']
|
||||
|
||||
const requestCredentials = ['omit', 'same-origin', 'include']
|
||||
|
||||
const requestCache = [
|
||||
'default',
|
||||
'no-store',
|
||||
'reload',
|
||||
'no-cache',
|
||||
'force-cache',
|
||||
'only-if-cached'
|
||||
]
|
||||
|
||||
// https://fetch.spec.whatwg.org/#request-body-header-name
|
||||
const requestBodyHeader = [
|
||||
'content-encoding',
|
||||
'content-language',
|
||||
'content-location',
|
||||
'content-type',
|
||||
// See https://github.com/nodejs/undici/issues/2021
|
||||
// 'Content-Length' is a forbidden header name, which is typically
|
||||
// removed in the Headers implementation. However, undici doesn't
|
||||
// filter out headers, so we add it here.
|
||||
'content-length'
|
||||
]
|
||||
|
||||
// https://fetch.spec.whatwg.org/#enumdef-requestduplex
|
||||
const requestDuplex = [
|
||||
'half'
|
||||
]
|
||||
|
||||
// http://fetch.spec.whatwg.org/#forbidden-method
|
||||
const forbiddenMethods = ['CONNECT', 'TRACE', 'TRACK']
|
||||
const forbiddenMethodsSet = new Set(forbiddenMethods)
|
||||
|
||||
const subresource = [
|
||||
'audio',
|
||||
'audioworklet',
|
||||
'font',
|
||||
'image',
|
||||
'manifest',
|
||||
'paintworklet',
|
||||
'script',
|
||||
'style',
|
||||
'track',
|
||||
'video',
|
||||
'xslt',
|
||||
''
|
||||
]
|
||||
const subresourceSet = new Set(subresource)
|
||||
|
||||
/** @type {globalThis['DOMException']} */
|
||||
const DOMException = globalThis.DOMException ?? (() => {
|
||||
// DOMException was only made a global in Node v17.0.0,
|
||||
// but fetch supports >= v16.8.
|
||||
try {
|
||||
atob('~')
|
||||
} catch (err) {
|
||||
return Object.getPrototypeOf(err).constructor
|
||||
}
|
||||
})()
|
||||
|
||||
let channel
|
||||
|
||||
/** @type {globalThis['structuredClone']} */
|
||||
const structuredClone =
|
||||
globalThis.structuredClone ??
|
||||
// https://github.com/nodejs/node/blob/b27ae24dcc4251bad726d9d84baf678d1f707fed/lib/internal/structured_clone.js
|
||||
// structuredClone was added in v17.0.0, but fetch supports v16.8
|
||||
function structuredClone (value, options = undefined) {
|
||||
if (arguments.length === 0) {
|
||||
throw new TypeError('missing argument')
|
||||
}
|
||||
|
||||
if (!channel) {
|
||||
channel = new MessageChannel()
|
||||
}
|
||||
channel.port1.unref()
|
||||
channel.port2.unref()
|
||||
channel.port1.postMessage(value, options?.transfer)
|
||||
return receiveMessageOnPort(channel.port2).message
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
DOMException,
|
||||
structuredClone,
|
||||
subresource,
|
||||
forbiddenMethods,
|
||||
requestBodyHeader,
|
||||
referrerPolicy,
|
||||
requestRedirect,
|
||||
requestMode,
|
||||
requestCredentials,
|
||||
requestCache,
|
||||
redirectStatus,
|
||||
corsSafeListedMethods,
|
||||
nullBodyStatus,
|
||||
safeMethods,
|
||||
badPorts,
|
||||
requestDuplex,
|
||||
subresourceSet,
|
||||
badPortsSet,
|
||||
redirectStatusSet,
|
||||
corsSafeListedMethodsSet,
|
||||
safeMethodsSet,
|
||||
forbiddenMethodsSet,
|
||||
referrerPolicySet
|
||||
}
|
|
@ -0,0 +1,630 @@
|
|||
const assert = require('assert')
|
||||
const { atob } = require('buffer')
|
||||
const { isomorphicDecode } = require('./util')
|
||||
|
||||
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
|
||||
/**
|
||||
* @see https://mimesniff.spec.whatwg.org/#http-quoted-string-token-code-point
|
||||
*/
|
||||
const HTTP_QUOTED_STRING_TOKENS = /[\u0009|\u0020-\u007E|\u0080-\u00FF]/ // eslint-disable-line
|
||||
|
||||
// https://fetch.spec.whatwg.org/#data-url-processor
|
||||
/** @param {URL} dataURL */
|
||||
function dataURLProcessor (dataURL) {
|
||||
// 1. Assert: dataURL’s scheme is "data".
|
||||
assert(dataURL.protocol === 'data:')
|
||||
|
||||
// 2. Let input be the result of running the URL
|
||||
// serializer on dataURL with exclude fragment
|
||||
// set to true.
|
||||
let input = URLSerializer(dataURL, true)
|
||||
|
||||
// 3. Remove the leading "data:" string from input.
|
||||
input = input.slice(5)
|
||||
|
||||
// 4. Let position point at the start of input.
|
||||
const position = { position: 0 }
|
||||
|
||||
// 5. Let mimeType be the result of collecting a
|
||||
// sequence of code points that are not equal
|
||||
// to U+002C (,), given position.
|
||||
let mimeType = collectASequenceOfCodePointsFast(
|
||||
',',
|
||||
input,
|
||||
position
|
||||
)
|
||||
|
||||
// 6. Strip leading and trailing ASCII whitespace
|
||||
// from mimeType.
|
||||
// Undici implementation note: we need to store the
|
||||
// length because if the mimetype has spaces removed,
|
||||
// the wrong amount will be sliced from the input in
|
||||
// step #9
|
||||
const mimeTypeLength = mimeType.length
|
||||
mimeType = removeASCIIWhitespace(mimeType, true, true)
|
||||
|
||||
// 7. If position is past the end of input, then
|
||||
// return failure
|
||||
if (position.position >= input.length) {
|
||||
return 'failure'
|
||||
}
|
||||
|
||||
// 8. Advance position by 1.
|
||||
position.position++
|
||||
|
||||
// 9. Let encodedBody be the remainder of input.
|
||||
const encodedBody = input.slice(mimeTypeLength + 1)
|
||||
|
||||
// 10. Let body be the percent-decoding of encodedBody.
|
||||
let body = stringPercentDecode(encodedBody)
|
||||
|
||||
// 11. If mimeType ends with U+003B (;), followed by
|
||||
// zero or more U+0020 SPACE, followed by an ASCII
|
||||
// case-insensitive match for "base64", then:
|
||||
if (/;(\u0020){0,}base64$/i.test(mimeType)) {
|
||||
// 1. Let stringBody be the isomorphic decode of body.
|
||||
const stringBody = isomorphicDecode(body)
|
||||
|
||||
// 2. Set body to the forgiving-base64 decode of
|
||||
// stringBody.
|
||||
body = forgivingBase64(stringBody)
|
||||
|
||||
// 3. If body is failure, then return failure.
|
||||
if (body === 'failure') {
|
||||
return 'failure'
|
||||
}
|
||||
|
||||
// 4. Remove the last 6 code points from mimeType.
|
||||
mimeType = mimeType.slice(0, -6)
|
||||
|
||||
// 5. Remove trailing U+0020 SPACE code points from mimeType,
|
||||
// if any.
|
||||
mimeType = mimeType.replace(/(\u0020)+$/, '')
|
||||
|
||||
// 6. Remove the last U+003B (;) code point from mimeType.
|
||||
mimeType = mimeType.slice(0, -1)
|
||||
}
|
||||
|
||||
// 12. If mimeType starts with U+003B (;), then prepend
|
||||
// "text/plain" to mimeType.
|
||||
if (mimeType.startsWith(';')) {
|
||||
mimeType = 'text/plain' + mimeType
|
||||
}
|
||||
|
||||
// 13. Let mimeTypeRecord be the result of parsing
|
||||
// mimeType.
|
||||
let mimeTypeRecord = parseMIMEType(mimeType)
|
||||
|
||||
// 14. If mimeTypeRecord is failure, then set
|
||||
// mimeTypeRecord to text/plain;charset=US-ASCII.
|
||||
if (mimeTypeRecord === 'failure') {
|
||||
mimeTypeRecord = parseMIMEType('text/plain;charset=US-ASCII')
|
||||
}
|
||||
|
||||
// 15. Return a new data: URL struct whose MIME
|
||||
// type is mimeTypeRecord and body is body.
|
||||
// https://fetch.spec.whatwg.org/#data-url-struct
|
||||
return { mimeType: mimeTypeRecord, body }
|
||||
}
|
||||
|
||||
// https://url.spec.whatwg.org/#concept-url-serializer
|
||||
/**
|
||||
* @param {URL} url
|
||||
* @param {boolean} excludeFragment
|
||||
*/
|
||||
function URLSerializer (url, excludeFragment = false) {
|
||||
const href = url.href
|
||||
|
||||
if (!excludeFragment) {
|
||||
return href
|
||||
}
|
||||
|
||||
const hash = href.lastIndexOf('#')
|
||||
if (hash === -1) {
|
||||
return href
|
||||
}
|
||||
return href.slice(0, hash)
|
||||
}
|
||||
|
||||
// https://infra.spec.whatwg.org/#collect-a-sequence-of-code-points
|
||||
/**
|
||||
* @param {(char: string) => boolean} condition
|
||||
* @param {string} input
|
||||
* @param {{ position: number }} position
|
||||
*/
|
||||
function collectASequenceOfCodePoints (condition, input, position) {
|
||||
// 1. Let result be the empty string.
|
||||
let result = ''
|
||||
|
||||
// 2. While position doesn’t point past the end of input and the
|
||||
// code point at position within input meets the condition condition:
|
||||
while (position.position < input.length && condition(input[position.position])) {
|
||||
// 1. Append that code point to the end of result.
|
||||
result += input[position.position]
|
||||
|
||||
// 2. Advance position by 1.
|
||||
position.position++
|
||||
}
|
||||
|
||||
// 3. Return result.
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* A faster collectASequenceOfCodePoints that only works when comparing a single character.
|
||||
* @param {string} char
|
||||
* @param {string} input
|
||||
* @param {{ position: number }} position
|
||||
*/
|
||||
function collectASequenceOfCodePointsFast (char, input, position) {
|
||||
const idx = input.indexOf(char, position.position)
|
||||
const start = position.position
|
||||
|
||||
if (idx === -1) {
|
||||
position.position = input.length
|
||||
return input.slice(start)
|
||||
}
|
||||
|
||||
position.position = idx
|
||||
return input.slice(start, position.position)
|
||||
}
|
||||
|
||||
// https://url.spec.whatwg.org/#string-percent-decode
|
||||
/** @param {string} input */
|
||||
function stringPercentDecode (input) {
|
||||
// 1. Let bytes be the UTF-8 encoding of input.
|
||||
const bytes = encoder.encode(input)
|
||||
|
||||
// 2. Return the percent-decoding of bytes.
|
||||
return percentDecode(bytes)
|
||||
}
|
||||
|
||||
// https://url.spec.whatwg.org/#percent-decode
|
||||
/** @param {Uint8Array} input */
|
||||
function percentDecode (input) {
|
||||
// 1. Let output be an empty byte sequence.
|
||||
/** @type {number[]} */
|
||||
const output = []
|
||||
|
||||
// 2. For each byte byte in input:
|
||||
for (let i = 0; i < input.length; i++) {
|
||||
const byte = input[i]
|
||||
|
||||
// 1. If byte is not 0x25 (%), then append byte to output.
|
||||
if (byte !== 0x25) {
|
||||
output.push(byte)
|
||||
|
||||
// 2. Otherwise, if byte is 0x25 (%) and the next two bytes
|
||||
// after byte in input are not in the ranges
|
||||
// 0x30 (0) to 0x39 (9), 0x41 (A) to 0x46 (F),
|
||||
// and 0x61 (a) to 0x66 (f), all inclusive, append byte
|
||||
// to output.
|
||||
} else if (
|
||||
byte === 0x25 &&
|
||||
!/^[0-9A-Fa-f]{2}$/i.test(String.fromCharCode(input[i + 1], input[i + 2]))
|
||||
) {
|
||||
output.push(0x25)
|
||||
|
||||
// 3. Otherwise:
|
||||
} else {
|
||||
// 1. Let bytePoint be the two bytes after byte in input,
|
||||
// decoded, and then interpreted as hexadecimal number.
|
||||
const nextTwoBytes = String.fromCharCode(input[i + 1], input[i + 2])
|
||||
const bytePoint = Number.parseInt(nextTwoBytes, 16)
|
||||
|
||||
// 2. Append a byte whose value is bytePoint to output.
|
||||
output.push(bytePoint)
|
||||
|
||||
// 3. Skip the next two bytes in input.
|
||||
i += 2
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Return output.
|
||||
return Uint8Array.from(output)
|
||||
}
|
||||
|
||||
// https://mimesniff.spec.whatwg.org/#parse-a-mime-type
|
||||
/** @param {string} input */
|
||||
function parseMIMEType (input) {
|
||||
// 1. Remove any leading and trailing HTTP whitespace
|
||||
// from input.
|
||||
input = removeHTTPWhitespace(input, true, true)
|
||||
|
||||
// 2. Let position be a position variable for input,
|
||||
// initially pointing at the start of input.
|
||||
const position = { position: 0 }
|
||||
|
||||
// 3. Let type be the result of collecting a sequence
|
||||
// of code points that are not U+002F (/) from
|
||||
// input, given position.
|
||||
const type = collectASequenceOfCodePointsFast(
|
||||
'/',
|
||||
input,
|
||||
position
|
||||
)
|
||||
|
||||
// 4. If type is the empty string or does not solely
|
||||
// contain HTTP token code points, then return failure.
|
||||
// https://mimesniff.spec.whatwg.org/#http-token-code-point
|
||||
if (type.length === 0 || !HTTP_TOKEN_CODEPOINTS.test(type)) {
|
||||
return 'failure'
|
||||
}
|
||||
|
||||
// 5. If position is past the end of input, then return
|
||||
// failure
|
||||
if (position.position > input.length) {
|
||||
return 'failure'
|
||||
}
|
||||
|
||||
// 6. Advance position by 1. (This skips past U+002F (/).)
|
||||
position.position++
|
||||
|
||||
// 7. Let subtype be the result of collecting a sequence of
|
||||
// code points that are not U+003B (;) from input, given
|
||||
// position.
|
||||
let subtype = collectASequenceOfCodePointsFast(
|
||||
';',
|
||||
input,
|
||||
position
|
||||
)
|
||||
|
||||
// 8. Remove any trailing HTTP whitespace from subtype.
|
||||
subtype = removeHTTPWhitespace(subtype, false, true)
|
||||
|
||||
// 9. If subtype is the empty string or does not solely
|
||||
// contain HTTP token code points, then return failure.
|
||||
if (subtype.length === 0 || !HTTP_TOKEN_CODEPOINTS.test(subtype)) {
|
||||
return 'failure'
|
||||
}
|
||||
|
||||
const typeLowercase = type.toLowerCase()
|
||||
const subtypeLowercase = subtype.toLowerCase()
|
||||
|
||||
// 10. Let mimeType be a new MIME type record whose type
|
||||
// is type, in ASCII lowercase, and subtype is subtype,
|
||||
// in ASCII lowercase.
|
||||
// https://mimesniff.spec.whatwg.org/#mime-type
|
||||
const mimeType = {
|
||||
type: typeLowercase,
|
||||
subtype: subtypeLowercase,
|
||||
/** @type {Map<string, string>} */
|
||||
parameters: new Map(),
|
||||
// https://mimesniff.spec.whatwg.org/#mime-type-essence
|
||||
essence: `${typeLowercase}/${subtypeLowercase}`
|
||||
}
|
||||
|
||||
// 11. While position is not past the end of input:
|
||||
while (position.position < input.length) {
|
||||
// 1. Advance position by 1. (This skips past U+003B (;).)
|
||||
position.position++
|
||||
|
||||
// 2. Collect a sequence of code points that are HTTP
|
||||
// whitespace from input given position.
|
||||
collectASequenceOfCodePoints(
|
||||
// https://fetch.spec.whatwg.org/#http-whitespace
|
||||
char => HTTP_WHITESPACE_REGEX.test(char),
|
||||
input,
|
||||
position
|
||||
)
|
||||
|
||||
// 3. Let parameterName be the result of collecting a
|
||||
// sequence of code points that are not U+003B (;)
|
||||
// or U+003D (=) from input, given position.
|
||||
let parameterName = collectASequenceOfCodePoints(
|
||||
(char) => char !== ';' && char !== '=',
|
||||
input,
|
||||
position
|
||||
)
|
||||
|
||||
// 4. Set parameterName to parameterName, in ASCII
|
||||
// lowercase.
|
||||
parameterName = parameterName.toLowerCase()
|
||||
|
||||
// 5. If position is not past the end of input, then:
|
||||
if (position.position < input.length) {
|
||||
// 1. If the code point at position within input is
|
||||
// U+003B (;), then continue.
|
||||
if (input[position.position] === ';') {
|
||||
continue
|
||||
}
|
||||
|
||||
// 2. Advance position by 1. (This skips past U+003D (=).)
|
||||
position.position++
|
||||
}
|
||||
|
||||
// 6. If position is past the end of input, then break.
|
||||
if (position.position > input.length) {
|
||||
break
|
||||
}
|
||||
|
||||
// 7. Let parameterValue be null.
|
||||
let parameterValue = null
|
||||
|
||||
// 8. If the code point at position within input is
|
||||
// U+0022 ("), then:
|
||||
if (input[position.position] === '"') {
|
||||
// 1. Set parameterValue to the result of collecting
|
||||
// an HTTP quoted string from input, given position
|
||||
// and the extract-value flag.
|
||||
parameterValue = collectAnHTTPQuotedString(input, position, true)
|
||||
|
||||
// 2. Collect a sequence of code points that are not
|
||||
// U+003B (;) from input, given position.
|
||||
collectASequenceOfCodePointsFast(
|
||||
';',
|
||||
input,
|
||||
position
|
||||
)
|
||||
|
||||
// 9. Otherwise:
|
||||
} else {
|
||||
// 1. Set parameterValue to the result of collecting
|
||||
// a sequence of code points that are not U+003B (;)
|
||||
// from input, given position.
|
||||
parameterValue = collectASequenceOfCodePointsFast(
|
||||
';',
|
||||
input,
|
||||
position
|
||||
)
|
||||
|
||||
// 2. Remove any trailing HTTP whitespace from parameterValue.
|
||||
parameterValue = removeHTTPWhitespace(parameterValue, false, true)
|
||||
|
||||
// 3. If parameterValue is the empty string, then continue.
|
||||
if (parameterValue.length === 0) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// 10. If all of the following are true
|
||||
// - parameterName is not the empty string
|
||||
// - parameterName solely contains HTTP token code points
|
||||
// - parameterValue solely contains HTTP quoted-string token code points
|
||||
// - mimeType’s parameters[parameterName] does not exist
|
||||
// then set mimeType’s parameters[parameterName] to parameterValue.
|
||||
if (
|
||||
parameterName.length !== 0 &&
|
||||
HTTP_TOKEN_CODEPOINTS.test(parameterName) &&
|
||||
(parameterValue.length === 0 || HTTP_QUOTED_STRING_TOKENS.test(parameterValue)) &&
|
||||
!mimeType.parameters.has(parameterName)
|
||||
) {
|
||||
mimeType.parameters.set(parameterName, parameterValue)
|
||||
}
|
||||
}
|
||||
|
||||
// 12. Return mimeType.
|
||||
return mimeType
|
||||
}
|
||||
|
||||
// https://infra.spec.whatwg.org/#forgiving-base64-decode
|
||||
/** @param {string} data */
|
||||
function forgivingBase64 (data) {
|
||||
// 1. Remove all ASCII whitespace from data.
|
||||
data = data.replace(/[\u0009\u000A\u000C\u000D\u0020]/g, '') // eslint-disable-line
|
||||
|
||||
// 2. If data’s code point length divides by 4 leaving
|
||||
// no remainder, then:
|
||||
if (data.length % 4 === 0) {
|
||||
// 1. If data ends with one or two U+003D (=) code points,
|
||||
// then remove them from data.
|
||||
data = data.replace(/=?=$/, '')
|
||||
}
|
||||
|
||||
// 3. If data’s code point length divides by 4 leaving
|
||||
// a remainder of 1, then return failure.
|
||||
if (data.length % 4 === 1) {
|
||||
return 'failure'
|
||||
}
|
||||
|
||||
// 4. If data contains a code point that is not one of
|
||||
// U+002B (+)
|
||||
// U+002F (/)
|
||||
// ASCII alphanumeric
|
||||
// then return failure.
|
||||
if (/[^+/0-9A-Za-z]/.test(data)) {
|
||||
return 'failure'
|
||||
}
|
||||
|
||||
const binary = atob(data)
|
||||
const bytes = new Uint8Array(binary.length)
|
||||
|
||||
for (let byte = 0; byte < binary.length; byte++) {
|
||||
bytes[byte] = binary.charCodeAt(byte)
|
||||
}
|
||||
|
||||
return bytes
|
||||
}
|
||||
|
||||
// https://fetch.spec.whatwg.org/#collect-an-http-quoted-string
|
||||
// tests: https://fetch.spec.whatwg.org/#example-http-quoted-string
|
||||
/**
|
||||
* @param {string} input
|
||||
* @param {{ position: number }} position
|
||||
* @param {boolean?} extractValue
|
||||
*/
|
||||
function collectAnHTTPQuotedString (input, position, extractValue) {
|
||||
// 1. Let positionStart be position.
|
||||
const positionStart = position.position
|
||||
|
||||
// 2. Let value be the empty string.
|
||||
let value = ''
|
||||
|
||||
// 3. Assert: the code point at position within input
|
||||
// is U+0022 (").
|
||||
assert(input[position.position] === '"')
|
||||
|
||||
// 4. Advance position by 1.
|
||||
position.position++
|
||||
|
||||
// 5. While true:
|
||||
while (true) {
|
||||
// 1. Append the result of collecting a sequence of code points
|
||||
// that are not U+0022 (") or U+005C (\) from input, given
|
||||
// position, to value.
|
||||
value += collectASequenceOfCodePoints(
|
||||
(char) => char !== '"' && char !== '\\',
|
||||
input,
|
||||
position
|
||||
)
|
||||
|
||||
// 2. If position is past the end of input, then break.
|
||||
if (position.position >= input.length) {
|
||||
break
|
||||
}
|
||||
|
||||
// 3. Let quoteOrBackslash be the code point at position within
|
||||
// input.
|
||||
const quoteOrBackslash = input[position.position]
|
||||
|
||||
// 4. Advance position by 1.
|
||||
position.position++
|
||||
|
||||
// 5. If quoteOrBackslash is U+005C (\), then:
|
||||
if (quoteOrBackslash === '\\') {
|
||||
// 1. If position is past the end of input, then append
|
||||
// U+005C (\) to value and break.
|
||||
if (position.position >= input.length) {
|
||||
value += '\\'
|
||||
break
|
||||
}
|
||||
|
||||
// 2. Append the code point at position within input to value.
|
||||
value += input[position.position]
|
||||
|
||||
// 3. Advance position by 1.
|
||||
position.position++
|
||||
|
||||
// 6. Otherwise:
|
||||
} else {
|
||||
// 1. Assert: quoteOrBackslash is U+0022 (").
|
||||
assert(quoteOrBackslash === '"')
|
||||
|
||||
// 2. Break.
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// 6. If the extract-value flag is set, then return value.
|
||||
if (extractValue) {
|
||||
return value
|
||||
}
|
||||
|
||||
// 7. Return the code points from positionStart to position,
|
||||
// inclusive, within input.
|
||||
return input.slice(positionStart, position.position)
|
||||
}
|
||||
|
||||
/**
|
||||
* @see https://mimesniff.spec.whatwg.org/#serialize-a-mime-type
|
||||
*/
|
||||
function serializeAMimeType (mimeType) {
|
||||
assert(mimeType !== 'failure')
|
||||
const { parameters, essence } = mimeType
|
||||
|
||||
// 1. Let serialization be the concatenation of mimeType’s
|
||||
// type, U+002F (/), and mimeType’s subtype.
|
||||
let serialization = essence
|
||||
|
||||
// 2. For each name → value of mimeType’s parameters:
|
||||
for (let [name, value] of parameters.entries()) {
|
||||
// 1. Append U+003B (;) to serialization.
|
||||
serialization += ';'
|
||||
|
||||
// 2. Append name to serialization.
|
||||
serialization += name
|
||||
|
||||
// 3. Append U+003D (=) to serialization.
|
||||
serialization += '='
|
||||
|
||||
// 4. If value does not solely contain HTTP token code
|
||||
// points or value is the empty string, then:
|
||||
if (!HTTP_TOKEN_CODEPOINTS.test(value)) {
|
||||
// 1. Precede each occurence of U+0022 (") or
|
||||
// U+005C (\) in value with U+005C (\).
|
||||
value = value.replace(/(\\|")/g, '\\$1')
|
||||
|
||||
// 2. Prepend U+0022 (") to value.
|
||||
value = '"' + value
|
||||
|
||||
// 3. Append U+0022 (") to value.
|
||||
value += '"'
|
||||
}
|
||||
|
||||
// 5. Append value to serialization.
|
||||
serialization += value
|
||||
}
|
||||
|
||||
// 3. Return serialization.
|
||||
return serialization
|
||||
}
|
||||
|
||||
/**
|
||||
* @see https://fetch.spec.whatwg.org/#http-whitespace
|
||||
* @param {string} char
|
||||
*/
|
||||
function isHTTPWhiteSpace (char) {
|
||||
return char === '\r' || char === '\n' || char === '\t' || char === ' '
|
||||
}
|
||||
|
||||
/**
|
||||
* @see https://fetch.spec.whatwg.org/#http-whitespace
|
||||
* @param {string} str
|
||||
*/
|
||||
function removeHTTPWhitespace (str, leading = true, trailing = true) {
|
||||
let lead = 0
|
||||
let trail = str.length - 1
|
||||
|
||||
if (leading) {
|
||||
for (; lead < str.length && isHTTPWhiteSpace(str[lead]); lead++);
|
||||
}
|
||||
|
||||
if (trailing) {
|
||||
for (; trail > 0 && isHTTPWhiteSpace(str[trail]); trail--);
|
||||
}
|
||||
|
||||
return str.slice(lead, trail + 1)
|
||||
}
|
||||
|
||||
/**
|
||||
* @see https://infra.spec.whatwg.org/#ascii-whitespace
|
||||
* @param {string} char
|
||||
*/
|
||||
function isASCIIWhitespace (char) {
|
||||
return char === '\r' || char === '\n' || char === '\t' || char === '\f' || char === ' '
|
||||
}
|
||||
|
||||
/**
|
||||
* @see https://infra.spec.whatwg.org/#strip-leading-and-trailing-ascii-whitespace
|
||||
*/
|
||||
function removeASCIIWhitespace (str, leading = true, trailing = true) {
|
||||
let lead = 0
|
||||
let trail = str.length - 1
|
||||
|
||||
if (leading) {
|
||||
for (; lead < str.length && isASCIIWhitespace(str[lead]); lead++);
|
||||
}
|
||||
|
||||
if (trailing) {
|
||||
for (; trail > 0 && isASCIIWhitespace(str[trail]); trail--);
|
||||
}
|
||||
|
||||
return str.slice(lead, trail + 1)
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
dataURLProcessor,
|
||||
URLSerializer,
|
||||
collectASequenceOfCodePoints,
|
||||
collectASequenceOfCodePointsFast,
|
||||
stringPercentDecode,
|
||||
parseMIMEType,
|
||||
collectAnHTTPQuotedString,
|
||||
serializeAMimeType
|
||||
}
|
|
@ -0,0 +1,344 @@
|
|||
'use strict'
|
||||
|
||||
const { Blob, File: NativeFile } = require('buffer')
|
||||
const { types } = require('util')
|
||||
const { kState } = require('./symbols')
|
||||
const { isBlobLike } = require('./util')
|
||||
const { webidl } = require('./webidl')
|
||||
const { parseMIMEType, serializeAMimeType } = require('./dataURL')
|
||||
const { kEnumerableProperty } = require('../core/util')
|
||||
const encoder = new TextEncoder()
|
||||
|
||||
class File extends Blob {
|
||||
constructor (fileBits, fileName, options = {}) {
|
||||
// The File constructor is invoked with two or three parameters, depending
|
||||
// on whether the optional dictionary parameter is used. When the File()
|
||||
// constructor is invoked, user agents must run the following steps:
|
||||
webidl.argumentLengthCheck(arguments, 2, { header: 'File constructor' })
|
||||
|
||||
fileBits = webidl.converters['sequence<BlobPart>'](fileBits)
|
||||
fileName = webidl.converters.USVString(fileName)
|
||||
options = webidl.converters.FilePropertyBag(options)
|
||||
|
||||
// 1. Let bytes be the result of processing blob parts given fileBits and
|
||||
// options.
|
||||
// Note: Blob handles this for us
|
||||
|
||||
// 2. Let n be the fileName argument to the constructor.
|
||||
const n = fileName
|
||||
|
||||
// 3. Process FilePropertyBag dictionary argument by running the following
|
||||
// substeps:
|
||||
|
||||
// 1. If the type member is provided and is not the empty string, let t
|
||||
// be set to the type dictionary member. If t contains any characters
|
||||
// outside the range U+0020 to U+007E, then set t to the empty string
|
||||
// and return from these substeps.
|
||||
// 2. Convert every character in t to ASCII lowercase.
|
||||
let t = options.type
|
||||
let d
|
||||
|
||||
// eslint-disable-next-line no-labels
|
||||
substep: {
|
||||
if (t) {
|
||||
t = parseMIMEType(t)
|
||||
|
||||
if (t === 'failure') {
|
||||
t = ''
|
||||
// eslint-disable-next-line no-labels
|
||||
break substep
|
||||
}
|
||||
|
||||
t = serializeAMimeType(t).toLowerCase()
|
||||
}
|
||||
|
||||
// 3. If the lastModified member is provided, let d be set to the
|
||||
// lastModified dictionary member. If it is not provided, set d to the
|
||||
// current date and time represented as the number of milliseconds since
|
||||
// the Unix Epoch (which is the equivalent of Date.now() [ECMA-262]).
|
||||
d = options.lastModified
|
||||
}
|
||||
|
||||
// 4. Return a new File object F such that:
|
||||
// F refers to the bytes byte sequence.
|
||||
// F.size is set to the number of total bytes in bytes.
|
||||
// F.name is set to n.
|
||||
// F.type is set to t.
|
||||
// F.lastModified is set to d.
|
||||
|
||||
super(processBlobParts(fileBits, options), { type: t })
|
||||
this[kState] = {
|
||||
name: n,
|
||||
lastModified: d,
|
||||
type: t
|
||||
}
|
||||
}
|
||||
|
||||
get name () {
|
||||
webidl.brandCheck(this, File)
|
||||
|
||||
return this[kState].name
|
||||
}
|
||||
|
||||
get lastModified () {
|
||||
webidl.brandCheck(this, File)
|
||||
|
||||
return this[kState].lastModified
|
||||
}
|
||||
|
||||
get type () {
|
||||
webidl.brandCheck(this, File)
|
||||
|
||||
return this[kState].type
|
||||
}
|
||||
}
|
||||
|
||||
class FileLike {
|
||||
constructor (blobLike, fileName, options = {}) {
|
||||
// TODO: argument idl type check
|
||||
|
||||
// The File constructor is invoked with two or three parameters, depending
|
||||
// on whether the optional dictionary parameter is used. When the File()
|
||||
// constructor is invoked, user agents must run the following steps:
|
||||
|
||||
// 1. Let bytes be the result of processing blob parts given fileBits and
|
||||
// options.
|
||||
|
||||
// 2. Let n be the fileName argument to the constructor.
|
||||
const n = fileName
|
||||
|
||||
// 3. Process FilePropertyBag dictionary argument by running the following
|
||||
// substeps:
|
||||
|
||||
// 1. If the type member is provided and is not the empty string, let t
|
||||
// be set to the type dictionary member. If t contains any characters
|
||||
// outside the range U+0020 to U+007E, then set t to the empty string
|
||||
// and return from these substeps.
|
||||
// TODO
|
||||
const t = options.type
|
||||
|
||||
// 2. Convert every character in t to ASCII lowercase.
|
||||
// TODO
|
||||
|
||||
// 3. If the lastModified member is provided, let d be set to the
|
||||
// lastModified dictionary member. If it is not provided, set d to the
|
||||
// current date and time represented as the number of milliseconds since
|
||||
// the Unix Epoch (which is the equivalent of Date.now() [ECMA-262]).
|
||||
const d = options.lastModified ?? Date.now()
|
||||
|
||||
// 4. Return a new File object F such that:
|
||||
// F refers to the bytes byte sequence.
|
||||
// F.size is set to the number of total bytes in bytes.
|
||||
// F.name is set to n.
|
||||
// F.type is set to t.
|
||||
// F.lastModified is set to d.
|
||||
|
||||
this[kState] = {
|
||||
blobLike,
|
||||
name: n,
|
||||
type: t,
|
||||
lastModified: d
|
||||
}
|
||||
}
|
||||
|
||||
stream (...args) {
|
||||
webidl.brandCheck(this, FileLike)
|
||||
|
||||
return this[kState].blobLike.stream(...args)
|
||||
}
|
||||
|
||||
arrayBuffer (...args) {
|
||||
webidl.brandCheck(this, FileLike)
|
||||
|
||||
return this[kState].blobLike.arrayBuffer(...args)
|
||||
}
|
||||
|
||||
slice (...args) {
|
||||
webidl.brandCheck(this, FileLike)
|
||||
|
||||
return this[kState].blobLike.slice(...args)
|
||||
}
|
||||
|
||||
text (...args) {
|
||||
webidl.brandCheck(this, FileLike)
|
||||
|
||||
return this[kState].blobLike.text(...args)
|
||||
}
|
||||
|
||||
get size () {
|
||||
webidl.brandCheck(this, FileLike)
|
||||
|
||||
return this[kState].blobLike.size
|
||||
}
|
||||
|
||||
get type () {
|
||||
webidl.brandCheck(this, FileLike)
|
||||
|
||||
return this[kState].blobLike.type
|
||||
}
|
||||
|
||||
get name () {
|
||||
webidl.brandCheck(this, FileLike)
|
||||
|
||||
return this[kState].name
|
||||
}
|
||||
|
||||
get lastModified () {
|
||||
webidl.brandCheck(this, FileLike)
|
||||
|
||||
return this[kState].lastModified
|
||||
}
|
||||
|
||||
get [Symbol.toStringTag] () {
|
||||
return 'File'
|
||||
}
|
||||
}
|
||||
|
||||
Object.defineProperties(File.prototype, {
|
||||
[Symbol.toStringTag]: {
|
||||
value: 'File',
|
||||
configurable: true
|
||||
},
|
||||
name: kEnumerableProperty,
|
||||
lastModified: kEnumerableProperty
|
||||
})
|
||||
|
||||
webidl.converters.Blob = webidl.interfaceConverter(Blob)
|
||||
|
||||
webidl.converters.BlobPart = function (V, opts) {
|
||||
if (webidl.util.Type(V) === 'Object') {
|
||||
if (isBlobLike(V)) {
|
||||
return webidl.converters.Blob(V, { strict: false })
|
||||
}
|
||||
|
||||
if (
|
||||
ArrayBuffer.isView(V) ||
|
||||
types.isAnyArrayBuffer(V)
|
||||
) {
|
||||
return webidl.converters.BufferSource(V, opts)
|
||||
}
|
||||
}
|
||||
|
||||
return webidl.converters.USVString(V, opts)
|
||||
}
|
||||
|
||||
webidl.converters['sequence<BlobPart>'] = webidl.sequenceConverter(
|
||||
webidl.converters.BlobPart
|
||||
)
|
||||
|
||||
// https://www.w3.org/TR/FileAPI/#dfn-FilePropertyBag
|
||||
webidl.converters.FilePropertyBag = webidl.dictionaryConverter([
|
||||
{
|
||||
key: 'lastModified',
|
||||
converter: webidl.converters['long long'],
|
||||
get defaultValue () {
|
||||
return Date.now()
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'type',
|
||||
converter: webidl.converters.DOMString,
|
||||
defaultValue: ''
|
||||
},
|
||||
{
|
||||
key: 'endings',
|
||||
converter: (value) => {
|
||||
value = webidl.converters.DOMString(value)
|
||||
value = value.toLowerCase()
|
||||
|
||||
if (value !== 'native') {
|
||||
value = 'transparent'
|
||||
}
|
||||
|
||||
return value
|
||||
},
|
||||
defaultValue: 'transparent'
|
||||
}
|
||||
])
|
||||
|
||||
/**
|
||||
* @see https://www.w3.org/TR/FileAPI/#process-blob-parts
|
||||
* @param {(NodeJS.TypedArray|Blob|string)[]} parts
|
||||
* @param {{ type: string, endings: string }} options
|
||||
*/
|
||||
function processBlobParts (parts, options) {
|
||||
// 1. Let bytes be an empty sequence of bytes.
|
||||
/** @type {NodeJS.TypedArray[]} */
|
||||
const bytes = []
|
||||
|
||||
// 2. For each element in parts:
|
||||
for (const element of parts) {
|
||||
// 1. If element is a USVString, run the following substeps:
|
||||
if (typeof element === 'string') {
|
||||
// 1. Let s be element.
|
||||
let s = element
|
||||
|
||||
// 2. If the endings member of options is "native", set s
|
||||
// to the result of converting line endings to native
|
||||
// of element.
|
||||
if (options.endings === 'native') {
|
||||
s = convertLineEndingsNative(s)
|
||||
}
|
||||
|
||||
// 3. Append the result of UTF-8 encoding s to bytes.
|
||||
bytes.push(encoder.encode(s))
|
||||
} else if (
|
||||
types.isAnyArrayBuffer(element) ||
|
||||
types.isTypedArray(element)
|
||||
) {
|
||||
// 2. If element is a BufferSource, get a copy of the
|
||||
// bytes held by the buffer source, and append those
|
||||
// bytes to bytes.
|
||||
if (!element.buffer) { // ArrayBuffer
|
||||
bytes.push(new Uint8Array(element))
|
||||
} else {
|
||||
bytes.push(
|
||||
new Uint8Array(element.buffer, element.byteOffset, element.byteLength)
|
||||
)
|
||||
}
|
||||
} else if (isBlobLike(element)) {
|
||||
// 3. If element is a Blob, append the bytes it represents
|
||||
// to bytes.
|
||||
bytes.push(element)
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Return bytes.
|
||||
return bytes
|
||||
}
|
||||
|
||||
/**
|
||||
* @see https://www.w3.org/TR/FileAPI/#convert-line-endings-to-native
|
||||
* @param {string} s
|
||||
*/
|
||||
function convertLineEndingsNative (s) {
|
||||
// 1. Let native line ending be be the code point U+000A LF.
|
||||
let nativeLineEnding = '\n'
|
||||
|
||||
// 2. If the underlying platform’s conventions are to
|
||||
// represent newlines as a carriage return and line feed
|
||||
// sequence, set native line ending to the code point
|
||||
// U+000D CR followed by the code point U+000A LF.
|
||||
if (process.platform === 'win32') {
|
||||
nativeLineEnding = '\r\n'
|
||||
}
|
||||
|
||||
return s.replace(/\r?\n/g, nativeLineEnding)
|
||||
}
|
||||
|
||||
// If this function is moved to ./util.js, some tools (such as
|
||||
// rollup) will warn about circular dependencies. See:
|
||||
// https://github.com/nodejs/undici/issues/1629
|
||||
function isFileLike (object) {
|
||||
return (
|
||||
(NativeFile && object instanceof NativeFile) ||
|
||||
object instanceof File || (
|
||||
object &&
|
||||
(typeof object.stream === 'function' ||
|
||||
typeof object.arrayBuffer === 'function') &&
|
||||
object[Symbol.toStringTag] === 'File'
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
module.exports = { File, FileLike, isFileLike }
|
|
@ -0,0 +1,265 @@
|
|||
'use strict'
|
||||
|
||||
const { isBlobLike, toUSVString, makeIterator } = require('./util')
|
||||
const { kState } = require('./symbols')
|
||||
const { File: UndiciFile, FileLike, isFileLike } = require('./file')
|
||||
const { webidl } = require('./webidl')
|
||||
const { Blob, File: NativeFile } = require('buffer')
|
||||
|
||||
/** @type {globalThis['File']} */
|
||||
const File = NativeFile ?? UndiciFile
|
||||
|
||||
// https://xhr.spec.whatwg.org/#formdata
|
||||
class FormData {
|
||||
constructor (form) {
|
||||
if (form !== undefined) {
|
||||
throw webidl.errors.conversionFailed({
|
||||
prefix: 'FormData constructor',
|
||||
argument: 'Argument 1',
|
||||
types: ['undefined']
|
||||
})
|
||||
}
|
||||
|
||||
this[kState] = []
|
||||
}
|
||||
|
||||
append (name, value, filename = undefined) {
|
||||
webidl.brandCheck(this, FormData)
|
||||
|
||||
webidl.argumentLengthCheck(arguments, 2, { header: 'FormData.append' })
|
||||
|
||||
if (arguments.length === 3 && !isBlobLike(value)) {
|
||||
throw new TypeError(
|
||||
"Failed to execute 'append' on 'FormData': parameter 2 is not of type 'Blob'"
|
||||
)
|
||||
}
|
||||
|
||||
// 1. Let value be value if given; otherwise blobValue.
|
||||
|
||||
name = webidl.converters.USVString(name)
|
||||
value = isBlobLike(value)
|
||||
? webidl.converters.Blob(value, { strict: false })
|
||||
: webidl.converters.USVString(value)
|
||||
filename = arguments.length === 3
|
||||
? webidl.converters.USVString(filename)
|
||||
: undefined
|
||||
|
||||
// 2. Let entry be the result of creating an entry with
|
||||
// name, value, and filename if given.
|
||||
const entry = makeEntry(name, value, filename)
|
||||
|
||||
// 3. Append entry to this’s entry list.
|
||||
this[kState].push(entry)
|
||||
}
|
||||
|
||||
delete (name) {
|
||||
webidl.brandCheck(this, FormData)
|
||||
|
||||
webidl.argumentLengthCheck(arguments, 1, { header: 'FormData.delete' })
|
||||
|
||||
name = webidl.converters.USVString(name)
|
||||
|
||||
// The delete(name) method steps are to remove all entries whose name
|
||||
// is name from this’s entry list.
|
||||
this[kState] = this[kState].filter(entry => entry.name !== name)
|
||||
}
|
||||
|
||||
get (name) {
|
||||
webidl.brandCheck(this, FormData)
|
||||
|
||||
webidl.argumentLengthCheck(arguments, 1, { header: 'FormData.get' })
|
||||
|
||||
name = webidl.converters.USVString(name)
|
||||
|
||||
// 1. If there is no entry whose name is name in this’s entry list,
|
||||
// then return null.
|
||||
const idx = this[kState].findIndex((entry) => entry.name === name)
|
||||
if (idx === -1) {
|
||||
return null
|
||||
}
|
||||
|
||||
// 2. Return the value of the first entry whose name is name from
|
||||
// this’s entry list.
|
||||
return this[kState][idx].value
|
||||
}
|
||||
|
||||
getAll (name) {
|
||||
webidl.brandCheck(this, FormData)
|
||||
|
||||
webidl.argumentLengthCheck(arguments, 1, { header: 'FormData.getAll' })
|
||||
|
||||
name = webidl.converters.USVString(name)
|
||||
|
||||
// 1. If there is no entry whose name is name in this’s entry list,
|
||||
// then return the empty list.
|
||||
// 2. Return the values of all entries whose name is name, in order,
|
||||
// from this’s entry list.
|
||||
return this[kState]
|
||||
.filter((entry) => entry.name === name)
|
||||
.map((entry) => entry.value)
|
||||
}
|
||||
|
||||
has (name) {
|
||||
webidl.brandCheck(this, FormData)
|
||||
|
||||
webidl.argumentLengthCheck(arguments, 1, { header: 'FormData.has' })
|
||||
|
||||
name = webidl.converters.USVString(name)
|
||||
|
||||
// The has(name) method steps are to return true if there is an entry
|
||||
// whose name is name in this’s entry list; otherwise false.
|
||||
return this[kState].findIndex((entry) => entry.name === name) !== -1
|
||||
}
|
||||
|
||||
set (name, value, filename = undefined) {
|
||||
webidl.brandCheck(this, FormData)
|
||||
|
||||
webidl.argumentLengthCheck(arguments, 2, { header: 'FormData.set' })
|
||||
|
||||
if (arguments.length === 3 && !isBlobLike(value)) {
|
||||
throw new TypeError(
|
||||
"Failed to execute 'set' on 'FormData': parameter 2 is not of type 'Blob'"
|
||||
)
|
||||
}
|
||||
|
||||
// The set(name, value) and set(name, blobValue, filename) method steps
|
||||
// are:
|
||||
|
||||
// 1. Let value be value if given; otherwise blobValue.
|
||||
|
||||
name = webidl.converters.USVString(name)
|
||||
value = isBlobLike(value)
|
||||
? webidl.converters.Blob(value, { strict: false })
|
||||
: webidl.converters.USVString(value)
|
||||
filename = arguments.length === 3
|
||||
? toUSVString(filename)
|
||||
: undefined
|
||||
|
||||
// 2. Let entry be the result of creating an entry with name, value, and
|
||||
// filename if given.
|
||||
const entry = makeEntry(name, value, filename)
|
||||
|
||||
// 3. If there are entries in this’s entry list whose name is name, then
|
||||
// replace the first such entry with entry and remove the others.
|
||||
const idx = this[kState].findIndex((entry) => entry.name === name)
|
||||
if (idx !== -1) {
|
||||
this[kState] = [
|
||||
...this[kState].slice(0, idx),
|
||||
entry,
|
||||
...this[kState].slice(idx + 1).filter((entry) => entry.name !== name)
|
||||
]
|
||||
} else {
|
||||
// 4. Otherwise, append entry to this’s entry list.
|
||||
this[kState].push(entry)
|
||||
}
|
||||
}
|
||||
|
||||
entries () {
|
||||
webidl.brandCheck(this, FormData)
|
||||
|
||||
return makeIterator(
|
||||
() => this[kState].map(pair => [pair.name, pair.value]),
|
||||
'FormData',
|
||||
'key+value'
|
||||
)
|
||||
}
|
||||
|
||||
keys () {
|
||||
webidl.brandCheck(this, FormData)
|
||||
|
||||
return makeIterator(
|
||||
() => this[kState].map(pair => [pair.name, pair.value]),
|
||||
'FormData',
|
||||
'key'
|
||||
)
|
||||
}
|
||||
|
||||
values () {
|
||||
webidl.brandCheck(this, FormData)
|
||||
|
||||
return makeIterator(
|
||||
() => this[kState].map(pair => [pair.name, pair.value]),
|
||||
'FormData',
|
||||
'value'
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {(value: string, key: string, self: FormData) => void} callbackFn
|
||||
* @param {unknown} thisArg
|
||||
*/
|
||||
forEach (callbackFn, thisArg = globalThis) {
|
||||
webidl.brandCheck(this, FormData)
|
||||
|
||||
webidl.argumentLengthCheck(arguments, 1, { header: 'FormData.forEach' })
|
||||
|
||||
if (typeof callbackFn !== 'function') {
|
||||
throw new TypeError(
|
||||
"Failed to execute 'forEach' on 'FormData': parameter 1 is not of type 'Function'."
|
||||
)
|
||||
}
|
||||
|
||||
for (const [key, value] of this) {
|
||||
callbackFn.apply(thisArg, [value, key, this])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FormData.prototype[Symbol.iterator] = FormData.prototype.entries
|
||||
|
||||
Object.defineProperties(FormData.prototype, {
|
||||
[Symbol.toStringTag]: {
|
||||
value: 'FormData',
|
||||
configurable: true
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* @see https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#create-an-entry
|
||||
* @param {string} name
|
||||
* @param {string|Blob} value
|
||||
* @param {?string} filename
|
||||
* @returns
|
||||
*/
|
||||
function makeEntry (name, value, filename) {
|
||||
// 1. Set name to the result of converting name into a scalar value string.
|
||||
// "To convert a string into a scalar value string, replace any surrogates
|
||||
// with U+FFFD."
|
||||
// see: https://nodejs.org/dist/latest-v18.x/docs/api/buffer.html#buftostringencoding-start-end
|
||||
name = Buffer.from(name).toString('utf8')
|
||||
|
||||
// 2. If value is a string, then set value to the result of converting
|
||||
// value into a scalar value string.
|
||||
if (typeof value === 'string') {
|
||||
value = Buffer.from(value).toString('utf8')
|
||||
} else {
|
||||
// 3. Otherwise:
|
||||
|
||||
// 1. If value is not a File object, then set value to a new File object,
|
||||
// representing the same bytes, whose name attribute value is "blob"
|
||||
if (!isFileLike(value)) {
|
||||
value = value instanceof Blob
|
||||
? new File([value], 'blob', { type: value.type })
|
||||
: new FileLike(value, 'blob', { type: value.type })
|
||||
}
|
||||
|
||||
// 2. If filename is given, then set value to a new File object,
|
||||
// representing the same bytes, whose name attribute is filename.
|
||||
if (filename !== undefined) {
|
||||
/** @type {FilePropertyBag} */
|
||||
const options = {
|
||||
type: value.type,
|
||||
lastModified: value.lastModified
|
||||
}
|
||||
|
||||
value = (NativeFile && value instanceof NativeFile) || value instanceof UndiciFile
|
||||
? new File([value], filename, options)
|
||||
: new FileLike(value, filename, options)
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Return an entry whose name is name and whose value is value.
|
||||
return { name, value }
|
||||
}
|
||||
|
||||
module.exports = { FormData }
|
|
@ -0,0 +1,40 @@
|
|||
'use strict'
|
||||
|
||||
// In case of breaking changes, increase the version
|
||||
// number to avoid conflicts.
|
||||
const globalOrigin = Symbol.for('undici.globalOrigin.1')
|
||||
|
||||
function getGlobalOrigin () {
|
||||
return globalThis[globalOrigin]
|
||||
}
|
||||
|
||||
function setGlobalOrigin (newOrigin) {
|
||||
if (newOrigin === undefined) {
|
||||
Object.defineProperty(globalThis, globalOrigin, {
|
||||
value: undefined,
|
||||
writable: true,
|
||||
enumerable: false,
|
||||
configurable: false
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
const parsedURL = new URL(newOrigin)
|
||||
|
||||
if (parsedURL.protocol !== 'http:' && parsedURL.protocol !== 'https:') {
|
||||
throw new TypeError(`Only http & https urls are allowed, received ${parsedURL.protocol}`)
|
||||
}
|
||||
|
||||
Object.defineProperty(globalThis, globalOrigin, {
|
||||
value: parsedURL,
|
||||
writable: true,
|
||||
enumerable: false,
|
||||
configurable: false
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getGlobalOrigin,
|
||||
setGlobalOrigin
|
||||
}
|
|
@ -0,0 +1,552 @@
|
|||
// https://github.com/Ethan-Arrowood/undici-fetch
|
||||
|
||||
'use strict'
|
||||
|
||||
const { kHeadersList } = require('../core/symbols')
|
||||
const { kGuard } = require('./symbols')
|
||||
const { kEnumerableProperty } = require('../core/util')
|
||||
const {
|
||||
makeIterator,
|
||||
isValidHeaderName,
|
||||
isValidHeaderValue
|
||||
} = require('./util')
|
||||
const { webidl } = require('./webidl')
|
||||
const assert = require('assert')
|
||||
|
||||
const kHeadersMap = Symbol('headers map')
|
||||
const kHeadersSortedMap = Symbol('headers map sorted')
|
||||
|
||||
/**
|
||||
* @see https://fetch.spec.whatwg.org/#concept-header-value-normalize
|
||||
* @param {string} potentialValue
|
||||
*/
|
||||
function headerValueNormalize (potentialValue) {
|
||||
// To normalize a byte sequence potentialValue, remove
|
||||
// any leading and trailing HTTP whitespace bytes from
|
||||
// potentialValue.
|
||||
|
||||
// Trimming the end with `.replace()` and a RegExp is typically subject to
|
||||
// ReDoS. This is safer and faster.
|
||||
let i = potentialValue.length
|
||||
while (/[\r\n\t ]/.test(potentialValue.charAt(--i)));
|
||||
return potentialValue.slice(0, i + 1).replace(/^[\r\n\t ]+/, '')
|
||||
}
|
||||
|
||||
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 (const header of object) {
|
||||
// 1. If header does not contain exactly two items, then throw a TypeError.
|
||||
if (header.length !== 2) {
|
||||
throw webidl.errors.exception({
|
||||
header: 'Headers constructor',
|
||||
message: `expected name/value pair to be length 2, found ${header.length}.`
|
||||
})
|
||||
}
|
||||
|
||||
// 2. Append (header’s first item, header’s second item) to headers.
|
||||
headers.append(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
|
||||
for (const [key, value] of Object.entries(object)) {
|
||||
headers.append(key, value)
|
||||
}
|
||||
} else {
|
||||
throw webidl.errors.conversionFailed({
|
||||
prefix: 'Headers constructor',
|
||||
argument: 'Argument 1',
|
||||
types: ['sequence<sequence<ByteString>>', 'record<ByteString, ByteString>']
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
class HeadersList {
|
||||
/** @type {[string, string][]|null} */
|
||||
cookies = null
|
||||
|
||||
constructor (init) {
|
||||
if (init instanceof HeadersList) {
|
||||
this[kHeadersMap] = new Map(init[kHeadersMap])
|
||||
this[kHeadersSortedMap] = init[kHeadersSortedMap]
|
||||
this.cookies = init.cookies
|
||||
} else {
|
||||
this[kHeadersMap] = new Map(init)
|
||||
this[kHeadersSortedMap] = null
|
||||
}
|
||||
}
|
||||
|
||||
// https://fetch.spec.whatwg.org/#header-list-contains
|
||||
contains (name) {
|
||||
// A header list list contains a header name name if list
|
||||
// contains a header whose name is a byte-case-insensitive
|
||||
// match for name.
|
||||
name = name.toLowerCase()
|
||||
|
||||
return this[kHeadersMap].has(name)
|
||||
}
|
||||
|
||||
clear () {
|
||||
this[kHeadersMap].clear()
|
||||
this[kHeadersSortedMap] = null
|
||||
this.cookies = null
|
||||
}
|
||||
|
||||
// https://fetch.spec.whatwg.org/#concept-header-list-append
|
||||
append (name, value) {
|
||||
this[kHeadersSortedMap] = null
|
||||
|
||||
// 1. If list contains name, then set name to the first such
|
||||
// header’s name.
|
||||
const lowercaseName = name.toLowerCase()
|
||||
const exists = this[kHeadersMap].get(lowercaseName)
|
||||
|
||||
// 2. Append (name, value) to list.
|
||||
if (exists) {
|
||||
const delimiter = lowercaseName === 'cookie' ? '; ' : ', '
|
||||
this[kHeadersMap].set(lowercaseName, {
|
||||
name: exists.name,
|
||||
value: `${exists.value}${delimiter}${value}`
|
||||
})
|
||||
} else {
|
||||
this[kHeadersMap].set(lowercaseName, { name, value })
|
||||
}
|
||||
|
||||
if (lowercaseName === 'set-cookie') {
|
||||
this.cookies ??= []
|
||||
this.cookies.push(value)
|
||||
}
|
||||
}
|
||||
|
||||
// https://fetch.spec.whatwg.org/#concept-header-list-set
|
||||
set (name, value) {
|
||||
this[kHeadersSortedMap] = null
|
||||
const lowercaseName = name.toLowerCase()
|
||||
|
||||
if (lowercaseName === 'set-cookie') {
|
||||
this.cookies = [value]
|
||||
}
|
||||
|
||||
// 1. If list contains name, then set the value of
|
||||
// the first such header to value and remove the
|
||||
// others.
|
||||
// 2. Otherwise, append header (name, value) to list.
|
||||
return this[kHeadersMap].set(lowercaseName, { name, value })
|
||||
}
|
||||
|
||||
// https://fetch.spec.whatwg.org/#concept-header-list-delete
|
||||
delete (name) {
|
||||
this[kHeadersSortedMap] = null
|
||||
|
||||
name = name.toLowerCase()
|
||||
|
||||
if (name === 'set-cookie') {
|
||||
this.cookies = null
|
||||
}
|
||||
|
||||
return this[kHeadersMap].delete(name)
|
||||
}
|
||||
|
||||
// https://fetch.spec.whatwg.org/#concept-header-list-get
|
||||
get (name) {
|
||||
// 1. If list does not contain name, then return null.
|
||||
if (!this.contains(name)) {
|
||||
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[kHeadersMap].get(name.toLowerCase())?.value ?? null
|
||||
}
|
||||
|
||||
* [Symbol.iterator] () {
|
||||
// use the lowercased name
|
||||
for (const [name, { value }] of this[kHeadersMap]) {
|
||||
yield [name, value]
|
||||
}
|
||||
}
|
||||
|
||||
get entries () {
|
||||
const headers = {}
|
||||
|
||||
if (this[kHeadersMap].size) {
|
||||
for (const { name, value } of this[kHeadersMap].values()) {
|
||||
headers[name] = value
|
||||
}
|
||||
}
|
||||
|
||||
return headers
|
||||
}
|
||||
}
|
||||
|
||||
// https://fetch.spec.whatwg.org/#headers-class
|
||||
class Headers {
|
||||
constructor (init = undefined) {
|
||||
this[kHeadersList] = new HeadersList()
|
||||
|
||||
// The new Headers(init) constructor steps are:
|
||||
|
||||
// 1. Set this’s guard to "none".
|
||||
this[kGuard] = 'none'
|
||||
|
||||
// 2. If init is given, then fill this with init.
|
||||
if (init !== undefined) {
|
||||
init = webidl.converters.HeadersInit(init)
|
||||
fill(this, init)
|
||||
}
|
||||
}
|
||||
|
||||
// https://fetch.spec.whatwg.org/#dom-headers-append
|
||||
append (name, value) {
|
||||
webidl.brandCheck(this, Headers)
|
||||
|
||||
webidl.argumentLengthCheck(arguments, 2, { header: 'Headers.append' })
|
||||
|
||||
name = webidl.converters.ByteString(name)
|
||||
value = webidl.converters.ByteString(value)
|
||||
|
||||
// 1. Normalize value.
|
||||
value = headerValueNormalize(value)
|
||||
|
||||
// 2. If name is not a header name or value is not a
|
||||
// header value, then throw a TypeError.
|
||||
if (!isValidHeaderName(name)) {
|
||||
throw webidl.errors.invalidArgument({
|
||||
prefix: 'Headers.append',
|
||||
value: name,
|
||||
type: 'header name'
|
||||
})
|
||||
} else if (!isValidHeaderValue(value)) {
|
||||
throw webidl.errors.invalidArgument({
|
||||
prefix: 'Headers.append',
|
||||
value,
|
||||
type: 'header value'
|
||||
})
|
||||
}
|
||||
|
||||
// 3. If headers’s guard is "immutable", then throw a TypeError.
|
||||
// 4. Otherwise, if headers’s guard is "request" and name is a
|
||||
// forbidden header name, return.
|
||||
// Note: undici does not implement forbidden header names
|
||||
if (this[kGuard] === 'immutable') {
|
||||
throw new TypeError('immutable')
|
||||
} else if (this[kGuard] === 'request-no-cors') {
|
||||
// 5. Otherwise, if headers’s guard is "request-no-cors":
|
||||
// TODO
|
||||
}
|
||||
|
||||
// 6. Otherwise, if headers’s guard is "response" and name is a
|
||||
// forbidden response-header name, return.
|
||||
|
||||
// 7. Append (name, value) to headers’s header list.
|
||||
// 8. If headers’s guard is "request-no-cors", then remove
|
||||
// privileged no-CORS request headers from headers
|
||||
return this[kHeadersList].append(name, value)
|
||||
}
|
||||
|
||||
// https://fetch.spec.whatwg.org/#dom-headers-delete
|
||||
delete (name) {
|
||||
webidl.brandCheck(this, Headers)
|
||||
|
||||
webidl.argumentLengthCheck(arguments, 1, { header: 'Headers.delete' })
|
||||
|
||||
name = webidl.converters.ByteString(name)
|
||||
|
||||
// 1. If name is not a header name, then throw a TypeError.
|
||||
if (!isValidHeaderName(name)) {
|
||||
throw webidl.errors.invalidArgument({
|
||||
prefix: 'Headers.delete',
|
||||
value: name,
|
||||
type: 'header name'
|
||||
})
|
||||
}
|
||||
|
||||
// 2. If this’s guard is "immutable", then throw a TypeError.
|
||||
// 3. Otherwise, if this’s guard is "request" and name is a
|
||||
// forbidden header name, return.
|
||||
// 4. Otherwise, if this’s guard is "request-no-cors", name
|
||||
// is not a no-CORS-safelisted request-header name, and
|
||||
// name is not a privileged no-CORS request-header name,
|
||||
// return.
|
||||
// 5. Otherwise, if this’s guard is "response" and name is
|
||||
// a forbidden response-header name, return.
|
||||
// Note: undici does not implement forbidden header names
|
||||
if (this[kGuard] === 'immutable') {
|
||||
throw new TypeError('immutable')
|
||||
} else if (this[kGuard] === 'request-no-cors') {
|
||||
// TODO
|
||||
}
|
||||
|
||||
// 6. If this’s header list does not contain name, then
|
||||
// return.
|
||||
if (!this[kHeadersList].contains(name)) {
|
||||
return
|
||||
}
|
||||
|
||||
// 7. Delete name from this’s header list.
|
||||
// 8. If this’s guard is "request-no-cors", then remove
|
||||
// privileged no-CORS request headers from this.
|
||||
return this[kHeadersList].delete(name)
|
||||
}
|
||||
|
||||
// https://fetch.spec.whatwg.org/#dom-headers-get
|
||||
get (name) {
|
||||
webidl.brandCheck(this, Headers)
|
||||
|
||||
webidl.argumentLengthCheck(arguments, 1, { header: 'Headers.get' })
|
||||
|
||||
name = webidl.converters.ByteString(name)
|
||||
|
||||
// 1. If name is not a header name, then throw a TypeError.
|
||||
if (!isValidHeaderName(name)) {
|
||||
throw webidl.errors.invalidArgument({
|
||||
prefix: 'Headers.get',
|
||||
value: name,
|
||||
type: 'header name'
|
||||
})
|
||||
}
|
||||
|
||||
// 2. Return the result of getting name from this’s header
|
||||
// list.
|
||||
return this[kHeadersList].get(name)
|
||||
}
|
||||
|
||||
// https://fetch.spec.whatwg.org/#dom-headers-has
|
||||
has (name) {
|
||||
webidl.brandCheck(this, Headers)
|
||||
|
||||
webidl.argumentLengthCheck(arguments, 1, { header: 'Headers.has' })
|
||||
|
||||
name = webidl.converters.ByteString(name)
|
||||
|
||||
// 1. If name is not a header name, then throw a TypeError.
|
||||
if (!isValidHeaderName(name)) {
|
||||
throw webidl.errors.invalidArgument({
|
||||
prefix: 'Headers.has',
|
||||
value: name,
|
||||
type: 'header name'
|
||||
})
|
||||
}
|
||||
|
||||
// 2. Return true if this’s header list contains name;
|
||||
// otherwise false.
|
||||
return this[kHeadersList].contains(name)
|
||||
}
|
||||
|
||||
// https://fetch.spec.whatwg.org/#dom-headers-set
|
||||
set (name, value) {
|
||||
webidl.brandCheck(this, Headers)
|
||||
|
||||
webidl.argumentLengthCheck(arguments, 2, { header: 'Headers.set' })
|
||||
|
||||
name = webidl.converters.ByteString(name)
|
||||
value = webidl.converters.ByteString(value)
|
||||
|
||||
// 1. Normalize value.
|
||||
value = headerValueNormalize(value)
|
||||
|
||||
// 2. If name is not a header name or value is not a
|
||||
// header value, then throw a TypeError.
|
||||
if (!isValidHeaderName(name)) {
|
||||
throw webidl.errors.invalidArgument({
|
||||
prefix: 'Headers.set',
|
||||
value: name,
|
||||
type: 'header name'
|
||||
})
|
||||
} else if (!isValidHeaderValue(value)) {
|
||||
throw webidl.errors.invalidArgument({
|
||||
prefix: 'Headers.set',
|
||||
value,
|
||||
type: 'header value'
|
||||
})
|
||||
}
|
||||
|
||||
// 3. If this’s guard is "immutable", then throw a TypeError.
|
||||
// 4. Otherwise, if this’s guard is "request" and name is a
|
||||
// forbidden header name, return.
|
||||
// 5. Otherwise, if this’s guard is "request-no-cors" and
|
||||
// name/value is not a no-CORS-safelisted request-header,
|
||||
// return.
|
||||
// 6. Otherwise, if this’s guard is "response" and name is a
|
||||
// forbidden response-header name, return.
|
||||
// Note: undici does not implement forbidden header names
|
||||
if (this[kGuard] === 'immutable') {
|
||||
throw new TypeError('immutable')
|
||||
} else if (this[kGuard] === 'request-no-cors') {
|
||||
// TODO
|
||||
}
|
||||
|
||||
// 7. Set (name, value) in this’s header list.
|
||||
// 8. If this’s guard is "request-no-cors", then remove
|
||||
// privileged no-CORS request headers from this
|
||||
return this[kHeadersList].set(name, value)
|
||||
}
|
||||
|
||||
// https://fetch.spec.whatwg.org/#dom-headers-getsetcookie
|
||||
getSetCookie () {
|
||||
webidl.brandCheck(this, Headers)
|
||||
|
||||
// 1. If this’s header list does not contain `Set-Cookie`, then return « ».
|
||||
// 2. Return the values of all headers in this’s header list whose name is
|
||||
// a byte-case-insensitive match for `Set-Cookie`, in order.
|
||||
|
||||
const list = this[kHeadersList].cookies
|
||||
|
||||
if (list) {
|
||||
return [...list]
|
||||
}
|
||||
|
||||
return []
|
||||
}
|
||||
|
||||
// https://fetch.spec.whatwg.org/#concept-header-list-sort-and-combine
|
||||
get [kHeadersSortedMap] () {
|
||||
if (this[kHeadersList][kHeadersSortedMap]) {
|
||||
return this[kHeadersList][kHeadersSortedMap]
|
||||
}
|
||||
|
||||
// 1. Let headers be an empty list of headers with the key being the name
|
||||
// and value the value.
|
||||
const headers = []
|
||||
|
||||
// 2. Let names be the result of convert header names to a sorted-lowercase
|
||||
// set with all the names of the headers in list.
|
||||
const names = [...this[kHeadersList]].sort((a, b) => a[0] < b[0] ? -1 : 1)
|
||||
const cookies = this[kHeadersList].cookies
|
||||
|
||||
// 3. For each name of names:
|
||||
for (const [name, value] of names) {
|
||||
// 1. If name is `set-cookie`, then:
|
||||
if (name === 'set-cookie') {
|
||||
// 1. Let values be a list of all values of headers in list whose name
|
||||
// is a byte-case-insensitive match for name, in order.
|
||||
|
||||
// 2. For each value of values:
|
||||
// 1. Append (name, value) to headers.
|
||||
for (const value of cookies) {
|
||||
headers.push([name, value])
|
||||
}
|
||||
} else {
|
||||
// 2. Otherwise:
|
||||
|
||||
// 1. Let value be the result of getting name from list.
|
||||
|
||||
// 2. Assert: value is non-null.
|
||||
assert(value !== null)
|
||||
|
||||
// 3. Append (name, value) to headers.
|
||||
headers.push([name, value])
|
||||
}
|
||||
}
|
||||
|
||||
this[kHeadersList][kHeadersSortedMap] = headers
|
||||
|
||||
// 4. Return headers.
|
||||
return headers
|
||||
}
|
||||
|
||||
keys () {
|
||||
webidl.brandCheck(this, Headers)
|
||||
|
||||
return makeIterator(
|
||||
() => [...this[kHeadersSortedMap].values()],
|
||||
'Headers',
|
||||
'key'
|
||||
)
|
||||
}
|
||||
|
||||
values () {
|
||||
webidl.brandCheck(this, Headers)
|
||||
|
||||
return makeIterator(
|
||||
() => [...this[kHeadersSortedMap].values()],
|
||||
'Headers',
|
||||
'value'
|
||||
)
|
||||
}
|
||||
|
||||
entries () {
|
||||
webidl.brandCheck(this, Headers)
|
||||
|
||||
return makeIterator(
|
||||
() => [...this[kHeadersSortedMap].values()],
|
||||
'Headers',
|
||||
'key+value'
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {(value: string, key: string, self: Headers) => void} callbackFn
|
||||
* @param {unknown} thisArg
|
||||
*/
|
||||
forEach (callbackFn, thisArg = globalThis) {
|
||||
webidl.brandCheck(this, Headers)
|
||||
|
||||
webidl.argumentLengthCheck(arguments, 1, { header: 'Headers.forEach' })
|
||||
|
||||
if (typeof callbackFn !== 'function') {
|
||||
throw new TypeError(
|
||||
"Failed to execute 'forEach' on 'Headers': parameter 1 is not of type 'Function'."
|
||||
)
|
||||
}
|
||||
|
||||
for (const [key, value] of this) {
|
||||
callbackFn.apply(thisArg, [value, key, this])
|
||||
}
|
||||
}
|
||||
|
||||
[Symbol.for('nodejs.util.inspect.custom')] () {
|
||||
webidl.brandCheck(this, Headers)
|
||||
|
||||
return this[kHeadersList]
|
||||
}
|
||||
}
|
||||
|
||||
Headers.prototype[Symbol.iterator] = Headers.prototype.entries
|
||||
|
||||
Object.defineProperties(Headers.prototype, {
|
||||
append: kEnumerableProperty,
|
||||
delete: kEnumerableProperty,
|
||||
get: kEnumerableProperty,
|
||||
has: kEnumerableProperty,
|
||||
set: kEnumerableProperty,
|
||||
getSetCookie: kEnumerableProperty,
|
||||
keys: kEnumerableProperty,
|
||||
values: kEnumerableProperty,
|
||||
entries: kEnumerableProperty,
|
||||
forEach: kEnumerableProperty,
|
||||
[Symbol.iterator]: { enumerable: false },
|
||||
[Symbol.toStringTag]: {
|
||||
value: 'Headers',
|
||||
configurable: true
|
||||
}
|
||||
})
|
||||
|
||||
webidl.converters.HeadersInit = function (V) {
|
||||
if (webidl.util.Type(V) === 'Object') {
|
||||
if (V[Symbol.iterator]) {
|
||||
return webidl.converters['sequence<sequence<ByteString>>'](V)
|
||||
}
|
||||
|
||||
return webidl.converters['record<ByteString, ByteString>'](V)
|
||||
}
|
||||
|
||||
throw webidl.errors.conversionFailed({
|
||||
prefix: 'Headers constructor',
|
||||
argument: 'Argument 1',
|
||||
types: ['sequence<sequence<ByteString>>', 'record<ByteString, ByteString>']
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
fill,
|
||||
Headers,
|
||||
HeadersList
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,944 @@
|
|||
/* globals AbortController */
|
||||
|
||||
'use strict'
|
||||
|
||||
const { extractBody, mixinBody, cloneBody } = require('./body')
|
||||
const { Headers, fill: fillHeaders, HeadersList } = require('./headers')
|
||||
const { FinalizationRegistry } = require('../compat/dispatcher-weakref')()
|
||||
const util = require('../core/util')
|
||||
const {
|
||||
isValidHTTPToken,
|
||||
sameOrigin,
|
||||
normalizeMethod,
|
||||
makePolicyContainer
|
||||
} = require('./util')
|
||||
const {
|
||||
forbiddenMethodsSet,
|
||||
corsSafeListedMethodsSet,
|
||||
referrerPolicy,
|
||||
requestRedirect,
|
||||
requestMode,
|
||||
requestCredentials,
|
||||
requestCache,
|
||||
requestDuplex
|
||||
} = require('./constants')
|
||||
const { kEnumerableProperty } = util
|
||||
const { kHeaders, kSignal, kState, kGuard, kRealm } = require('./symbols')
|
||||
const { webidl } = require('./webidl')
|
||||
const { getGlobalOrigin } = require('./global')
|
||||
const { URLSerializer } = require('./dataURL')
|
||||
const { kHeadersList } = require('../core/symbols')
|
||||
const assert = require('assert')
|
||||
const { getMaxListeners, setMaxListeners, getEventListeners, defaultMaxListeners } = require('events')
|
||||
|
||||
let TransformStream = globalThis.TransformStream
|
||||
|
||||
const kInit = Symbol('init')
|
||||
const kAbortController = Symbol('abortController')
|
||||
|
||||
const requestFinalizer = new FinalizationRegistry(({ signal, abort }) => {
|
||||
signal.removeEventListener('abort', abort)
|
||||
})
|
||||
|
||||
// https://fetch.spec.whatwg.org/#request-class
|
||||
class Request {
|
||||
// https://fetch.spec.whatwg.org/#dom-request
|
||||
constructor (input, init = {}) {
|
||||
if (input === kInit) {
|
||||
return
|
||||
}
|
||||
|
||||
webidl.argumentLengthCheck(arguments, 1, { header: 'Request constructor' })
|
||||
|
||||
input = webidl.converters.RequestInfo(input)
|
||||
init = webidl.converters.RequestInit(init)
|
||||
|
||||
// https://html.spec.whatwg.org/multipage/webappapis.html#environment-settings-object
|
||||
this[kRealm] = {
|
||||
settingsObject: {
|
||||
baseUrl: getGlobalOrigin(),
|
||||
get origin () {
|
||||
return this.baseUrl?.origin
|
||||
},
|
||||
policyContainer: makePolicyContainer()
|
||||
}
|
||||
}
|
||||
|
||||
// 1. Let request be null.
|
||||
let request = null
|
||||
|
||||
// 2. Let fallbackMode be null.
|
||||
let fallbackMode = null
|
||||
|
||||
// 3. Let baseURL be this’s relevant settings object’s API base URL.
|
||||
const baseUrl = this[kRealm].settingsObject.baseUrl
|
||||
|
||||
// 4. Let signal be null.
|
||||
let signal = null
|
||||
|
||||
// 5. If input is a string, then:
|
||||
if (typeof input === 'string') {
|
||||
// 1. Let parsedURL be the result of parsing input with baseURL.
|
||||
// 2. If parsedURL is failure, then throw a TypeError.
|
||||
let parsedURL
|
||||
try {
|
||||
parsedURL = new URL(input, baseUrl)
|
||||
} catch (err) {
|
||||
throw new TypeError('Failed to parse URL from ' + input, { cause: err })
|
||||
}
|
||||
|
||||
// 3. If parsedURL includes credentials, then throw a TypeError.
|
||||
if (parsedURL.username || parsedURL.password) {
|
||||
throw new TypeError(
|
||||
'Request cannot be constructed from a URL that includes credentials: ' +
|
||||
input
|
||||
)
|
||||
}
|
||||
|
||||
// 4. Set request to a new request whose URL is parsedURL.
|
||||
request = makeRequest({ urlList: [parsedURL] })
|
||||
|
||||
// 5. Set fallbackMode to "cors".
|
||||
fallbackMode = 'cors'
|
||||
} else {
|
||||
// 6. Otherwise:
|
||||
|
||||
// 7. Assert: input is a Request object.
|
||||
assert(input instanceof Request)
|
||||
|
||||
// 8. Set request to input’s request.
|
||||
request = input[kState]
|
||||
|
||||
// 9. Set signal to input’s signal.
|
||||
signal = input[kSignal]
|
||||
}
|
||||
|
||||
// 7. Let origin be this’s relevant settings object’s origin.
|
||||
const origin = this[kRealm].settingsObject.origin
|
||||
|
||||
// 8. Let window be "client".
|
||||
let window = 'client'
|
||||
|
||||
// 9. If request’s window is an environment settings object and its origin
|
||||
// is same origin with origin, then set window to request’s window.
|
||||
if (
|
||||
request.window?.constructor?.name === 'EnvironmentSettingsObject' &&
|
||||
sameOrigin(request.window, origin)
|
||||
) {
|
||||
window = request.window
|
||||
}
|
||||
|
||||
// 10. If init["window"] exists and is non-null, then throw a TypeError.
|
||||
if (init.window != null) {
|
||||
throw new TypeError(`'window' option '${window}' must be null`)
|
||||
}
|
||||
|
||||
// 11. If init["window"] exists, then set window to "no-window".
|
||||
if ('window' in init) {
|
||||
window = 'no-window'
|
||||
}
|
||||
|
||||
// 12. Set request to a new request with the following properties:
|
||||
request = makeRequest({
|
||||
// URL request’s URL.
|
||||
// undici implementation note: this is set as the first item in request's urlList in makeRequest
|
||||
// method request’s method.
|
||||
method: request.method,
|
||||
// header list A copy of request’s header list.
|
||||
// undici implementation note: headersList is cloned in makeRequest
|
||||
headersList: request.headersList,
|
||||
// unsafe-request flag Set.
|
||||
unsafeRequest: request.unsafeRequest,
|
||||
// client This’s relevant settings object.
|
||||
client: this[kRealm].settingsObject,
|
||||
// window window.
|
||||
window,
|
||||
// priority request’s priority.
|
||||
priority: request.priority,
|
||||
// origin request’s origin. The propagation of the origin is only significant for navigation requests
|
||||
// being handled by a service worker. In this scenario a request can have an origin that is different
|
||||
// from the current client.
|
||||
origin: request.origin,
|
||||
// referrer request’s referrer.
|
||||
referrer: request.referrer,
|
||||
// referrer policy request’s referrer policy.
|
||||
referrerPolicy: request.referrerPolicy,
|
||||
// mode request’s mode.
|
||||
mode: request.mode,
|
||||
// credentials mode request’s credentials mode.
|
||||
credentials: request.credentials,
|
||||
// cache mode request’s cache mode.
|
||||
cache: request.cache,
|
||||
// redirect mode request’s redirect mode.
|
||||
redirect: request.redirect,
|
||||
// integrity metadata request’s integrity metadata.
|
||||
integrity: request.integrity,
|
||||
// keepalive request’s keepalive.
|
||||
keepalive: request.keepalive,
|
||||
// reload-navigation flag request’s reload-navigation flag.
|
||||
reloadNavigation: request.reloadNavigation,
|
||||
// history-navigation flag request’s history-navigation flag.
|
||||
historyNavigation: request.historyNavigation,
|
||||
// URL list A clone of request’s URL list.
|
||||
urlList: [...request.urlList]
|
||||
})
|
||||
|
||||
// 13. If init is not empty, then:
|
||||
if (Object.keys(init).length > 0) {
|
||||
// 1. If request’s mode is "navigate", then set it to "same-origin".
|
||||
if (request.mode === 'navigate') {
|
||||
request.mode = 'same-origin'
|
||||
}
|
||||
|
||||
// 2. Unset request’s reload-navigation flag.
|
||||
request.reloadNavigation = false
|
||||
|
||||
// 3. Unset request’s history-navigation flag.
|
||||
request.historyNavigation = false
|
||||
|
||||
// 4. Set request’s origin to "client".
|
||||
request.origin = 'client'
|
||||
|
||||
// 5. Set request’s referrer to "client"
|
||||
request.referrer = 'client'
|
||||
|
||||
// 6. Set request’s referrer policy to the empty string.
|
||||
request.referrerPolicy = ''
|
||||
|
||||
// 7. Set request’s URL to request’s current URL.
|
||||
request.url = request.urlList[request.urlList.length - 1]
|
||||
|
||||
// 8. Set request’s URL list to « request’s URL ».
|
||||
request.urlList = [request.url]
|
||||
}
|
||||
|
||||
// 14. If init["referrer"] exists, then:
|
||||
if (init.referrer !== undefined) {
|
||||
// 1. Let referrer be init["referrer"].
|
||||
const referrer = init.referrer
|
||||
|
||||
// 2. If referrer is the empty string, then set request’s referrer to "no-referrer".
|
||||
if (referrer === '') {
|
||||
request.referrer = 'no-referrer'
|
||||
} else {
|
||||
// 1. Let parsedReferrer be the result of parsing referrer with
|
||||
// baseURL.
|
||||
// 2. If parsedReferrer is failure, then throw a TypeError.
|
||||
let parsedReferrer
|
||||
try {
|
||||
parsedReferrer = new URL(referrer, baseUrl)
|
||||
} catch (err) {
|
||||
throw new TypeError(`Referrer "${referrer}" is not a valid URL.`, { cause: err })
|
||||
}
|
||||
|
||||
// 3. If one of the following is true
|
||||
// - parsedReferrer’s scheme is "about" and path is the string "client"
|
||||
// - parsedReferrer’s origin is not same origin with origin
|
||||
// then set request’s referrer to "client".
|
||||
if (
|
||||
(parsedReferrer.protocol === 'about:' && parsedReferrer.hostname === 'client') ||
|
||||
(origin && !sameOrigin(parsedReferrer, this[kRealm].settingsObject.baseUrl))
|
||||
) {
|
||||
request.referrer = 'client'
|
||||
} else {
|
||||
// 4. Otherwise, set request’s referrer to parsedReferrer.
|
||||
request.referrer = parsedReferrer
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 15. If init["referrerPolicy"] exists, then set request’s referrer policy
|
||||
// to it.
|
||||
if (init.referrerPolicy !== undefined) {
|
||||
request.referrerPolicy = init.referrerPolicy
|
||||
}
|
||||
|
||||
// 16. Let mode be init["mode"] if it exists, and fallbackMode otherwise.
|
||||
let mode
|
||||
if (init.mode !== undefined) {
|
||||
mode = init.mode
|
||||
} else {
|
||||
mode = fallbackMode
|
||||
}
|
||||
|
||||
// 17. If mode is "navigate", then throw a TypeError.
|
||||
if (mode === 'navigate') {
|
||||
throw webidl.errors.exception({
|
||||
header: 'Request constructor',
|
||||
message: 'invalid request mode navigate.'
|
||||
})
|
||||
}
|
||||
|
||||
// 18. If mode is non-null, set request’s mode to mode.
|
||||
if (mode != null) {
|
||||
request.mode = mode
|
||||
}
|
||||
|
||||
// 19. If init["credentials"] exists, then set request’s credentials mode
|
||||
// to it.
|
||||
if (init.credentials !== undefined) {
|
||||
request.credentials = init.credentials
|
||||
}
|
||||
|
||||
// 18. If init["cache"] exists, then set request’s cache mode to it.
|
||||
if (init.cache !== undefined) {
|
||||
request.cache = init.cache
|
||||
}
|
||||
|
||||
// 21. If request’s cache mode is "only-if-cached" and request’s mode is
|
||||
// not "same-origin", then throw a TypeError.
|
||||
if (request.cache === 'only-if-cached' && request.mode !== 'same-origin') {
|
||||
throw new TypeError(
|
||||
"'only-if-cached' can be set only with 'same-origin' mode"
|
||||
)
|
||||
}
|
||||
|
||||
// 22. If init["redirect"] exists, then set request’s redirect mode to it.
|
||||
if (init.redirect !== undefined) {
|
||||
request.redirect = init.redirect
|
||||
}
|
||||
|
||||
// 23. If init["integrity"] exists, then set request’s integrity metadata to it.
|
||||
if (init.integrity !== undefined && init.integrity != null) {
|
||||
request.integrity = String(init.integrity)
|
||||
}
|
||||
|
||||
// 24. If init["keepalive"] exists, then set request’s keepalive to it.
|
||||
if (init.keepalive !== undefined) {
|
||||
request.keepalive = Boolean(init.keepalive)
|
||||
}
|
||||
|
||||
// 25. If init["method"] exists, then:
|
||||
if (init.method !== undefined) {
|
||||
// 1. Let method be init["method"].
|
||||
let method = init.method
|
||||
|
||||
// 2. If method is not a method or method is a forbidden method, then
|
||||
// throw a TypeError.
|
||||
if (!isValidHTTPToken(init.method)) {
|
||||
throw TypeError(`'${init.method}' is not a valid HTTP method.`)
|
||||
}
|
||||
|
||||
if (forbiddenMethodsSet.has(method.toUpperCase())) {
|
||||
throw TypeError(`'${init.method}' HTTP method is unsupported.`)
|
||||
}
|
||||
|
||||
// 3. Normalize method.
|
||||
method = normalizeMethod(init.method)
|
||||
|
||||
// 4. Set request’s method to method.
|
||||
request.method = method
|
||||
}
|
||||
|
||||
// 26. If init["signal"] exists, then set signal to it.
|
||||
if (init.signal !== undefined) {
|
||||
signal = init.signal
|
||||
}
|
||||
|
||||
// 27. Set this’s request to request.
|
||||
this[kState] = request
|
||||
|
||||
// 28. Set this’s signal to a new AbortSignal object with this’s relevant
|
||||
// Realm.
|
||||
// TODO: could this be simplified with AbortSignal.any
|
||||
// (https://dom.spec.whatwg.org/#dom-abortsignal-any)
|
||||
const ac = new AbortController()
|
||||
this[kSignal] = ac.signal
|
||||
this[kSignal][kRealm] = this[kRealm]
|
||||
|
||||
// 29. If signal is not null, then make this’s signal follow signal.
|
||||
if (signal != null) {
|
||||
if (
|
||||
!signal ||
|
||||
typeof signal.aborted !== 'boolean' ||
|
||||
typeof signal.addEventListener !== 'function'
|
||||
) {
|
||||
throw new TypeError(
|
||||
"Failed to construct 'Request': member signal is not of type AbortSignal."
|
||||
)
|
||||
}
|
||||
|
||||
if (signal.aborted) {
|
||||
ac.abort(signal.reason)
|
||||
} else {
|
||||
// Keep a strong ref to ac while request object
|
||||
// is alive. This is needed to prevent AbortController
|
||||
// from being prematurely garbage collected.
|
||||
// See, https://github.com/nodejs/undici/issues/1926.
|
||||
this[kAbortController] = ac
|
||||
|
||||
const acRef = new WeakRef(ac)
|
||||
const abort = function () {
|
||||
const ac = acRef.deref()
|
||||
if (ac !== undefined) {
|
||||
ac.abort(this.reason)
|
||||
}
|
||||
}
|
||||
|
||||
// Third-party AbortControllers may not work with these.
|
||||
// See, https://github.com/nodejs/undici/pull/1910#issuecomment-1464495619.
|
||||
try {
|
||||
// If the max amount of listeners is equal to the default, increase it
|
||||
// This is only available in node >= v19.9.0
|
||||
if (typeof getMaxListeners === 'function' && getMaxListeners(signal) === defaultMaxListeners) {
|
||||
setMaxListeners(100, signal)
|
||||
} else if (getEventListeners(signal, 'abort').length >= defaultMaxListeners) {
|
||||
setMaxListeners(100, signal)
|
||||
}
|
||||
} catch {}
|
||||
|
||||
util.addAbortListener(signal, abort)
|
||||
requestFinalizer.register(ac, { signal, abort })
|
||||
}
|
||||
}
|
||||
|
||||
// 30. Set this’s headers to a new Headers object with this’s relevant
|
||||
// Realm, whose header list is request’s header list and guard is
|
||||
// "request".
|
||||
this[kHeaders] = new Headers()
|
||||
this[kHeaders][kHeadersList] = request.headersList
|
||||
this[kHeaders][kGuard] = 'request'
|
||||
this[kHeaders][kRealm] = this[kRealm]
|
||||
|
||||
// 31. If this’s request’s mode is "no-cors", then:
|
||||
if (mode === 'no-cors') {
|
||||
// 1. If this’s request’s method is not a CORS-safelisted method,
|
||||
// then throw a TypeError.
|
||||
if (!corsSafeListedMethodsSet.has(request.method)) {
|
||||
throw new TypeError(
|
||||
`'${request.method} is unsupported in no-cors mode.`
|
||||
)
|
||||
}
|
||||
|
||||
// 2. Set this’s headers’s guard to "request-no-cors".
|
||||
this[kHeaders][kGuard] = 'request-no-cors'
|
||||
}
|
||||
|
||||
// 32. If init is not empty, then:
|
||||
if (Object.keys(init).length !== 0) {
|
||||
// 1. Let headers be a copy of this’s headers and its associated header
|
||||
// list.
|
||||
let headers = new Headers(this[kHeaders])
|
||||
|
||||
// 2. If init["headers"] exists, then set headers to init["headers"].
|
||||
if (init.headers !== undefined) {
|
||||
headers = init.headers
|
||||
}
|
||||
|
||||
// 3. Empty this’s headers’s header list.
|
||||
this[kHeaders][kHeadersList].clear()
|
||||
|
||||
// 4. If headers is a Headers object, then for each header in its header
|
||||
// list, append header’s name/header’s value to this’s headers.
|
||||
if (headers.constructor.name === 'Headers') {
|
||||
for (const [key, val] of headers) {
|
||||
this[kHeaders].append(key, val)
|
||||
}
|
||||
} else {
|
||||
// 5. Otherwise, fill this’s headers with headers.
|
||||
fillHeaders(this[kHeaders], headers)
|
||||
}
|
||||
}
|
||||
|
||||
// 33. Let inputBody be input’s request’s body if input is a Request
|
||||
// object; otherwise null.
|
||||
const inputBody = input instanceof Request ? input[kState].body : null
|
||||
|
||||
// 34. If either init["body"] exists and is non-null or inputBody is
|
||||
// non-null, and request’s method is `GET` or `HEAD`, then throw a
|
||||
// TypeError.
|
||||
if (
|
||||
(init.body != null || inputBody != null) &&
|
||||
(request.method === 'GET' || request.method === 'HEAD')
|
||||
) {
|
||||
throw new TypeError('Request with GET/HEAD method cannot have body.')
|
||||
}
|
||||
|
||||
// 35. Let initBody be null.
|
||||
let initBody = null
|
||||
|
||||
// 36. If init["body"] exists and is non-null, then:
|
||||
if (init.body != null) {
|
||||
// 1. Let Content-Type be null.
|
||||
// 2. Set initBody and Content-Type to the result of extracting
|
||||
// init["body"], with keepalive set to request’s keepalive.
|
||||
const [extractedBody, contentType] = extractBody(
|
||||
init.body,
|
||||
request.keepalive
|
||||
)
|
||||
initBody = extractedBody
|
||||
|
||||
// 3, If Content-Type is non-null and this’s headers’s header list does
|
||||
// not contain `Content-Type`, then append `Content-Type`/Content-Type to
|
||||
// this’s headers.
|
||||
if (contentType && !this[kHeaders][kHeadersList].contains('content-type')) {
|
||||
this[kHeaders].append('content-type', contentType)
|
||||
}
|
||||
}
|
||||
|
||||
// 37. Let inputOrInitBody be initBody if it is non-null; otherwise
|
||||
// inputBody.
|
||||
const inputOrInitBody = initBody ?? inputBody
|
||||
|
||||
// 38. If inputOrInitBody is non-null and inputOrInitBody’s source is
|
||||
// null, then:
|
||||
if (inputOrInitBody != null && inputOrInitBody.source == null) {
|
||||
// 1. If initBody is non-null and init["duplex"] does not exist,
|
||||
// then throw a TypeError.
|
||||
if (initBody != null && init.duplex == null) {
|
||||
throw new TypeError('RequestInit: duplex option is required when sending a body.')
|
||||
}
|
||||
|
||||
// 2. If this’s request’s mode is neither "same-origin" nor "cors",
|
||||
// then throw a TypeError.
|
||||
if (request.mode !== 'same-origin' && request.mode !== 'cors') {
|
||||
throw new TypeError(
|
||||
'If request is made from ReadableStream, mode should be "same-origin" or "cors"'
|
||||
)
|
||||
}
|
||||
|
||||
// 3. Set this’s request’s use-CORS-preflight flag.
|
||||
request.useCORSPreflightFlag = true
|
||||
}
|
||||
|
||||
// 39. Let finalBody be inputOrInitBody.
|
||||
let finalBody = inputOrInitBody
|
||||
|
||||
// 40. If initBody is null and inputBody is non-null, then:
|
||||
if (initBody == null && inputBody != null) {
|
||||
// 1. If input is unusable, then throw a TypeError.
|
||||
if (util.isDisturbed(inputBody.stream) || inputBody.stream.locked) {
|
||||
throw new TypeError(
|
||||
'Cannot construct a Request with a Request object that has already been used.'
|
||||
)
|
||||
}
|
||||
|
||||
// 2. Set finalBody to the result of creating a proxy for inputBody.
|
||||
if (!TransformStream) {
|
||||
TransformStream = require('stream/web').TransformStream
|
||||
}
|
||||
|
||||
// https://streams.spec.whatwg.org/#readablestream-create-a-proxy
|
||||
const identityTransform = new TransformStream()
|
||||
inputBody.stream.pipeThrough(identityTransform)
|
||||
finalBody = {
|
||||
source: inputBody.source,
|
||||
length: inputBody.length,
|
||||
stream: identityTransform.readable
|
||||
}
|
||||
}
|
||||
|
||||
// 41. Set this’s request’s body to finalBody.
|
||||
this[kState].body = finalBody
|
||||
}
|
||||
|
||||
// Returns request’s HTTP method, which is "GET" by default.
|
||||
get method () {
|
||||
webidl.brandCheck(this, Request)
|
||||
|
||||
// The method getter steps are to return this’s request’s method.
|
||||
return this[kState].method
|
||||
}
|
||||
|
||||
// Returns the URL of request as a string.
|
||||
get url () {
|
||||
webidl.brandCheck(this, Request)
|
||||
|
||||
// The url getter steps are to return this’s request’s URL, serialized.
|
||||
return URLSerializer(this[kState].url)
|
||||
}
|
||||
|
||||
// Returns a Headers object consisting of the headers associated with request.
|
||||
// Note that headers added in the network layer by the user agent will not
|
||||
// be accounted for in this object, e.g., the "Host" header.
|
||||
get headers () {
|
||||
webidl.brandCheck(this, Request)
|
||||
|
||||
// The headers getter steps are to return this’s headers.
|
||||
return this[kHeaders]
|
||||
}
|
||||
|
||||
// Returns the kind of resource requested by request, e.g., "document"
|
||||
// or "script".
|
||||
get destination () {
|
||||
webidl.brandCheck(this, Request)
|
||||
|
||||
// The destination getter are to return this’s request’s destination.
|
||||
return this[kState].destination
|
||||
}
|
||||
|
||||
// Returns the referrer of request. Its value can be a same-origin URL if
|
||||
// explicitly set in init, the empty string to indicate no referrer, and
|
||||
// "about:client" when defaulting to the global’s default. This is used
|
||||
// during fetching to determine the value of the `Referer` header of the
|
||||
// request being made.
|
||||
get referrer () {
|
||||
webidl.brandCheck(this, Request)
|
||||
|
||||
// 1. If this’s request’s referrer is "no-referrer", then return the
|
||||
// empty string.
|
||||
if (this[kState].referrer === 'no-referrer') {
|
||||
return ''
|
||||
}
|
||||
|
||||
// 2. If this’s request’s referrer is "client", then return
|
||||
// "about:client".
|
||||
if (this[kState].referrer === 'client') {
|
||||
return 'about:client'
|
||||
}
|
||||
|
||||
// Return this’s request’s referrer, serialized.
|
||||
return this[kState].referrer.toString()
|
||||
}
|
||||
|
||||
// Returns the referrer policy associated with request.
|
||||
// This is used during fetching to compute the value of the request’s
|
||||
// referrer.
|
||||
get referrerPolicy () {
|
||||
webidl.brandCheck(this, Request)
|
||||
|
||||
// The referrerPolicy getter steps are to return this’s request’s referrer policy.
|
||||
return this[kState].referrerPolicy
|
||||
}
|
||||
|
||||
// Returns the mode associated with request, which is a string indicating
|
||||
// whether the request will use CORS, or will be restricted to same-origin
|
||||
// URLs.
|
||||
get mode () {
|
||||
webidl.brandCheck(this, Request)
|
||||
|
||||
// The mode getter steps are to return this’s request’s mode.
|
||||
return this[kState].mode
|
||||
}
|
||||
|
||||
// Returns the credentials mode associated with request,
|
||||
// which is a string indicating whether credentials will be sent with the
|
||||
// request always, never, or only when sent to a same-origin URL.
|
||||
get credentials () {
|
||||
// The credentials getter steps are to return this’s request’s credentials mode.
|
||||
return this[kState].credentials
|
||||
}
|
||||
|
||||
// Returns the cache mode associated with request,
|
||||
// which is a string indicating how the request will
|
||||
// interact with the browser’s cache when fetching.
|
||||
get cache () {
|
||||
webidl.brandCheck(this, Request)
|
||||
|
||||
// The cache getter steps are to return this’s request’s cache mode.
|
||||
return this[kState].cache
|
||||
}
|
||||
|
||||
// Returns the redirect mode associated with request,
|
||||
// which is a string indicating how redirects for the
|
||||
// request will be handled during fetching. A request
|
||||
// will follow redirects by default.
|
||||
get redirect () {
|
||||
webidl.brandCheck(this, Request)
|
||||
|
||||
// The redirect getter steps are to return this’s request’s redirect mode.
|
||||
return this[kState].redirect
|
||||
}
|
||||
|
||||
// Returns request’s subresource integrity metadata, which is a
|
||||
// cryptographic hash of the resource being fetched. Its value
|
||||
// consists of multiple hashes separated by whitespace. [SRI]
|
||||
get integrity () {
|
||||
webidl.brandCheck(this, Request)
|
||||
|
||||
// The integrity getter steps are to return this’s request’s integrity
|
||||
// metadata.
|
||||
return this[kState].integrity
|
||||
}
|
||||
|
||||
// Returns a boolean indicating whether or not request can outlive the
|
||||
// global in which it was created.
|
||||
get keepalive () {
|
||||
webidl.brandCheck(this, Request)
|
||||
|
||||
// The keepalive getter steps are to return this’s request’s keepalive.
|
||||
return this[kState].keepalive
|
||||
}
|
||||
|
||||
// Returns a boolean indicating whether or not request is for a reload
|
||||
// navigation.
|
||||
get isReloadNavigation () {
|
||||
webidl.brandCheck(this, Request)
|
||||
|
||||
// The isReloadNavigation getter steps are to return true if this’s
|
||||
// request’s reload-navigation flag is set; otherwise false.
|
||||
return this[kState].reloadNavigation
|
||||
}
|
||||
|
||||
// Returns a boolean indicating whether or not request is for a history
|
||||
// navigation (a.k.a. back-foward navigation).
|
||||
get isHistoryNavigation () {
|
||||
webidl.brandCheck(this, Request)
|
||||
|
||||
// The isHistoryNavigation getter steps are to return true if this’s request’s
|
||||
// history-navigation flag is set; otherwise false.
|
||||
return this[kState].historyNavigation
|
||||
}
|
||||
|
||||
// Returns the signal associated with request, which is an AbortSignal
|
||||
// object indicating whether or not request has been aborted, and its
|
||||
// abort event handler.
|
||||
get signal () {
|
||||
webidl.brandCheck(this, Request)
|
||||
|
||||
// The signal getter steps are to return this’s signal.
|
||||
return this[kSignal]
|
||||
}
|
||||
|
||||
get body () {
|
||||
webidl.brandCheck(this, Request)
|
||||
|
||||
return this[kState].body ? this[kState].body.stream : null
|
||||
}
|
||||
|
||||
get bodyUsed () {
|
||||
webidl.brandCheck(this, Request)
|
||||
|
||||
return !!this[kState].body && util.isDisturbed(this[kState].body.stream)
|
||||
}
|
||||
|
||||
get duplex () {
|
||||
webidl.brandCheck(this, Request)
|
||||
|
||||
return 'half'
|
||||
}
|
||||
|
||||
// Returns a clone of request.
|
||||
clone () {
|
||||
webidl.brandCheck(this, Request)
|
||||
|
||||
// 1. If this is unusable, then throw a TypeError.
|
||||
if (this.bodyUsed || this.body?.locked) {
|
||||
throw new TypeError('unusable')
|
||||
}
|
||||
|
||||
// 2. Let clonedRequest be the result of cloning this’s request.
|
||||
const clonedRequest = cloneRequest(this[kState])
|
||||
|
||||
// 3. Let clonedRequestObject be the result of creating a Request object,
|
||||
// given clonedRequest, this’s headers’s guard, and this’s relevant Realm.
|
||||
const clonedRequestObject = new Request(kInit)
|
||||
clonedRequestObject[kState] = clonedRequest
|
||||
clonedRequestObject[kRealm] = this[kRealm]
|
||||
clonedRequestObject[kHeaders] = new Headers()
|
||||
clonedRequestObject[kHeaders][kHeadersList] = clonedRequest.headersList
|
||||
clonedRequestObject[kHeaders][kGuard] = this[kHeaders][kGuard]
|
||||
clonedRequestObject[kHeaders][kRealm] = this[kHeaders][kRealm]
|
||||
|
||||
// 4. Make clonedRequestObject’s signal follow this’s signal.
|
||||
const ac = new AbortController()
|
||||
if (this.signal.aborted) {
|
||||
ac.abort(this.signal.reason)
|
||||
} else {
|
||||
util.addAbortListener(
|
||||
this.signal,
|
||||
() => {
|
||||
ac.abort(this.signal.reason)
|
||||
}
|
||||
)
|
||||
}
|
||||
clonedRequestObject[kSignal] = ac.signal
|
||||
|
||||
// 4. Return clonedRequestObject.
|
||||
return clonedRequestObject
|
||||
}
|
||||
}
|
||||
|
||||
mixinBody(Request)
|
||||
|
||||
function makeRequest (init) {
|
||||
// https://fetch.spec.whatwg.org/#requests
|
||||
const request = {
|
||||
method: 'GET',
|
||||
localURLsOnly: false,
|
||||
unsafeRequest: false,
|
||||
body: null,
|
||||
client: null,
|
||||
reservedClient: null,
|
||||
replacesClientId: '',
|
||||
window: 'client',
|
||||
keepalive: false,
|
||||
serviceWorkers: 'all',
|
||||
initiator: '',
|
||||
destination: '',
|
||||
priority: null,
|
||||
origin: 'client',
|
||||
policyContainer: 'client',
|
||||
referrer: 'client',
|
||||
referrerPolicy: '',
|
||||
mode: 'no-cors',
|
||||
useCORSPreflightFlag: false,
|
||||
credentials: 'same-origin',
|
||||
useCredentials: false,
|
||||
cache: 'default',
|
||||
redirect: 'follow',
|
||||
integrity: '',
|
||||
cryptoGraphicsNonceMetadata: '',
|
||||
parserMetadata: '',
|
||||
reloadNavigation: false,
|
||||
historyNavigation: false,
|
||||
userActivation: false,
|
||||
taintedOrigin: false,
|
||||
redirectCount: 0,
|
||||
responseTainting: 'basic',
|
||||
preventNoCacheCacheControlHeaderModification: false,
|
||||
done: false,
|
||||
timingAllowFailed: false,
|
||||
...init,
|
||||
headersList: init.headersList
|
||||
? new HeadersList(init.headersList)
|
||||
: new HeadersList()
|
||||
}
|
||||
request.url = request.urlList[0]
|
||||
return request
|
||||
}
|
||||
|
||||
// https://fetch.spec.whatwg.org/#concept-request-clone
|
||||
function cloneRequest (request) {
|
||||
// To clone a request request, run these steps:
|
||||
|
||||
// 1. Let newRequest be a copy of request, except for its body.
|
||||
const newRequest = makeRequest({ ...request, body: null })
|
||||
|
||||
// 2. If request’s body is non-null, set newRequest’s body to the
|
||||
// result of cloning request’s body.
|
||||
if (request.body != null) {
|
||||
newRequest.body = cloneBody(request.body)
|
||||
}
|
||||
|
||||
// 3. Return newRequest.
|
||||
return newRequest
|
||||
}
|
||||
|
||||
Object.defineProperties(Request.prototype, {
|
||||
method: kEnumerableProperty,
|
||||
url: kEnumerableProperty,
|
||||
headers: kEnumerableProperty,
|
||||
redirect: kEnumerableProperty,
|
||||
clone: kEnumerableProperty,
|
||||
signal: kEnumerableProperty,
|
||||
duplex: kEnumerableProperty,
|
||||
destination: kEnumerableProperty,
|
||||
body: kEnumerableProperty,
|
||||
bodyUsed: kEnumerableProperty,
|
||||
isHistoryNavigation: kEnumerableProperty,
|
||||
isReloadNavigation: kEnumerableProperty,
|
||||
keepalive: kEnumerableProperty,
|
||||
integrity: kEnumerableProperty,
|
||||
cache: kEnumerableProperty,
|
||||
credentials: kEnumerableProperty,
|
||||
attribute: kEnumerableProperty,
|
||||
referrerPolicy: kEnumerableProperty,
|
||||
referrer: kEnumerableProperty,
|
||||
mode: kEnumerableProperty,
|
||||
[Symbol.toStringTag]: {
|
||||
value: 'Request',
|
||||
configurable: true
|
||||
}
|
||||
})
|
||||
|
||||
webidl.converters.Request = webidl.interfaceConverter(
|
||||
Request
|
||||
)
|
||||
|
||||
// https://fetch.spec.whatwg.org/#requestinfo
|
||||
webidl.converters.RequestInfo = function (V) {
|
||||
if (typeof V === 'string') {
|
||||
return webidl.converters.USVString(V)
|
||||
}
|
||||
|
||||
if (V instanceof Request) {
|
||||
return webidl.converters.Request(V)
|
||||
}
|
||||
|
||||
return webidl.converters.USVString(V)
|
||||
}
|
||||
|
||||
webidl.converters.AbortSignal = webidl.interfaceConverter(
|
||||
AbortSignal
|
||||
)
|
||||
|
||||
// https://fetch.spec.whatwg.org/#requestinit
|
||||
webidl.converters.RequestInit = webidl.dictionaryConverter([
|
||||
{
|
||||
key: 'method',
|
||||
converter: webidl.converters.ByteString
|
||||
},
|
||||
{
|
||||
key: 'headers',
|
||||
converter: webidl.converters.HeadersInit
|
||||
},
|
||||
{
|
||||
key: 'body',
|
||||
converter: webidl.nullableConverter(
|
||||
webidl.converters.BodyInit
|
||||
)
|
||||
},
|
||||
{
|
||||
key: 'referrer',
|
||||
converter: webidl.converters.USVString
|
||||
},
|
||||
{
|
||||
key: 'referrerPolicy',
|
||||
converter: webidl.converters.DOMString,
|
||||
// https://w3c.github.io/webappsec-referrer-policy/#referrer-policy
|
||||
allowedValues: referrerPolicy
|
||||
},
|
||||
{
|
||||
key: 'mode',
|
||||
converter: webidl.converters.DOMString,
|
||||
// https://fetch.spec.whatwg.org/#concept-request-mode
|
||||
allowedValues: requestMode
|
||||
},
|
||||
{
|
||||
key: 'credentials',
|
||||
converter: webidl.converters.DOMString,
|
||||
// https://fetch.spec.whatwg.org/#requestcredentials
|
||||
allowedValues: requestCredentials
|
||||
},
|
||||
{
|
||||
key: 'cache',
|
||||
converter: webidl.converters.DOMString,
|
||||
// https://fetch.spec.whatwg.org/#requestcache
|
||||
allowedValues: requestCache
|
||||
},
|
||||
{
|
||||
key: 'redirect',
|
||||
converter: webidl.converters.DOMString,
|
||||
// https://fetch.spec.whatwg.org/#requestredirect
|
||||
allowedValues: requestRedirect
|
||||
},
|
||||
{
|
||||
key: 'integrity',
|
||||
converter: webidl.converters.DOMString
|
||||
},
|
||||
{
|
||||
key: 'keepalive',
|
||||
converter: webidl.converters.boolean
|
||||
},
|
||||
{
|
||||
key: 'signal',
|
||||
converter: webidl.nullableConverter(
|
||||
(signal) => webidl.converters.AbortSignal(
|
||||
signal,
|
||||
{ strict: false }
|
||||
)
|
||||
)
|
||||
},
|
||||
{
|
||||
key: 'window',
|
||||
converter: webidl.converters.any
|
||||
},
|
||||
{
|
||||
key: 'duplex',
|
||||
converter: webidl.converters.DOMString,
|
||||
allowedValues: requestDuplex
|
||||
}
|
||||
])
|
||||
|
||||
module.exports = { Request, makeRequest }
|
|
@ -0,0 +1,575 @@
|
|||
'use strict'
|
||||
|
||||
const { Headers, HeadersList, fill } = require('./headers')
|
||||
const { extractBody, cloneBody, mixinBody } = require('./body')
|
||||
const util = require('../core/util')
|
||||
const { kEnumerableProperty } = util
|
||||
const {
|
||||
isValidReasonPhrase,
|
||||
isCancelled,
|
||||
isAborted,
|
||||
isBlobLike,
|
||||
serializeJavascriptValueToJSONString,
|
||||
isErrorLike,
|
||||
isomorphicEncode
|
||||
} = require('./util')
|
||||
const {
|
||||
redirectStatusSet,
|
||||
nullBodyStatus,
|
||||
DOMException
|
||||
} = require('./constants')
|
||||
const { kState, kHeaders, kGuard, kRealm } = require('./symbols')
|
||||
const { webidl } = require('./webidl')
|
||||
const { FormData } = require('./formdata')
|
||||
const { getGlobalOrigin } = require('./global')
|
||||
const { URLSerializer } = require('./dataURL')
|
||||
const { kHeadersList } = require('../core/symbols')
|
||||
const assert = require('assert')
|
||||
const { types } = require('util')
|
||||
|
||||
const ReadableStream = globalThis.ReadableStream || require('stream/web').ReadableStream
|
||||
const textEncoder = new TextEncoder('utf-8')
|
||||
|
||||
// https://fetch.spec.whatwg.org/#response-class
|
||||
class Response {
|
||||
// Creates network error Response.
|
||||
static error () {
|
||||
// TODO
|
||||
const relevantRealm = { settingsObject: {} }
|
||||
|
||||
// The static error() method steps are to return the result of creating a
|
||||
// Response object, given a new network error, "immutable", and this’s
|
||||
// relevant Realm.
|
||||
const responseObject = new Response()
|
||||
responseObject[kState] = makeNetworkError()
|
||||
responseObject[kRealm] = relevantRealm
|
||||
responseObject[kHeaders][kHeadersList] = responseObject[kState].headersList
|
||||
responseObject[kHeaders][kGuard] = 'immutable'
|
||||
responseObject[kHeaders][kRealm] = relevantRealm
|
||||
return responseObject
|
||||
}
|
||||
|
||||
// https://fetch.spec.whatwg.org/#dom-response-json
|
||||
static json (data, init = {}) {
|
||||
webidl.argumentLengthCheck(arguments, 1, { header: 'Response.json' })
|
||||
|
||||
if (init !== null) {
|
||||
init = webidl.converters.ResponseInit(init)
|
||||
}
|
||||
|
||||
// 1. Let bytes the result of running serialize a JavaScript value to JSON bytes on data.
|
||||
const bytes = textEncoder.encode(
|
||||
serializeJavascriptValueToJSONString(data)
|
||||
)
|
||||
|
||||
// 2. Let body be the result of extracting bytes.
|
||||
const body = extractBody(bytes)
|
||||
|
||||
// 3. Let responseObject be the result of creating a Response object, given a new response,
|
||||
// "response", and this’s relevant Realm.
|
||||
const relevantRealm = { settingsObject: {} }
|
||||
const responseObject = new Response()
|
||||
responseObject[kRealm] = relevantRealm
|
||||
responseObject[kHeaders][kGuard] = 'response'
|
||||
responseObject[kHeaders][kRealm] = relevantRealm
|
||||
|
||||
// 4. Perform initialize a response given responseObject, init, and (body, "application/json").
|
||||
initializeResponse(responseObject, init, { body: body[0], type: 'application/json' })
|
||||
|
||||
// 5. Return responseObject.
|
||||
return responseObject
|
||||
}
|
||||
|
||||
// Creates a redirect Response that redirects to url with status status.
|
||||
static redirect (url, status = 302) {
|
||||
const relevantRealm = { settingsObject: {} }
|
||||
|
||||
webidl.argumentLengthCheck(arguments, 1, { header: 'Response.redirect' })
|
||||
|
||||
url = webidl.converters.USVString(url)
|
||||
status = webidl.converters['unsigned short'](status)
|
||||
|
||||
// 1. Let parsedURL be the result of parsing url with current settings
|
||||
// object’s API base URL.
|
||||
// 2. If parsedURL is failure, then throw a TypeError.
|
||||
// TODO: base-URL?
|
||||
let parsedURL
|
||||
try {
|
||||
parsedURL = new URL(url, getGlobalOrigin())
|
||||
} catch (err) {
|
||||
throw Object.assign(new TypeError('Failed to parse URL from ' + url), {
|
||||
cause: err
|
||||
})
|
||||
}
|
||||
|
||||
// 3. If status is not a redirect status, then throw a RangeError.
|
||||
if (!redirectStatusSet.has(status)) {
|
||||
throw new RangeError('Invalid status code ' + status)
|
||||
}
|
||||
|
||||
// 4. Let responseObject be the result of creating a Response object,
|
||||
// given a new response, "immutable", and this’s relevant Realm.
|
||||
const responseObject = new Response()
|
||||
responseObject[kRealm] = relevantRealm
|
||||
responseObject[kHeaders][kGuard] = 'immutable'
|
||||
responseObject[kHeaders][kRealm] = relevantRealm
|
||||
|
||||
// 5. Set responseObject’s response’s status to status.
|
||||
responseObject[kState].status = status
|
||||
|
||||
// 6. Let value be parsedURL, serialized and isomorphic encoded.
|
||||
const value = isomorphicEncode(URLSerializer(parsedURL))
|
||||
|
||||
// 7. Append `Location`/value to responseObject’s response’s header list.
|
||||
responseObject[kState].headersList.append('location', value)
|
||||
|
||||
// 8. Return responseObject.
|
||||
return responseObject
|
||||
}
|
||||
|
||||
// https://fetch.spec.whatwg.org/#dom-response
|
||||
constructor (body = null, init = {}) {
|
||||
if (body !== null) {
|
||||
body = webidl.converters.BodyInit(body)
|
||||
}
|
||||
|
||||
init = webidl.converters.ResponseInit(init)
|
||||
|
||||
// TODO
|
||||
this[kRealm] = { settingsObject: {} }
|
||||
|
||||
// 1. Set this’s response to a new response.
|
||||
this[kState] = makeResponse({})
|
||||
|
||||
// 2. Set this’s headers to a new Headers object with this’s relevant
|
||||
// Realm, whose header list is this’s response’s header list and guard
|
||||
// is "response".
|
||||
this[kHeaders] = new Headers()
|
||||
this[kHeaders][kGuard] = 'response'
|
||||
this[kHeaders][kHeadersList] = this[kState].headersList
|
||||
this[kHeaders][kRealm] = this[kRealm]
|
||||
|
||||
// 3. Let bodyWithType be null.
|
||||
let bodyWithType = null
|
||||
|
||||
// 4. If body is non-null, then set bodyWithType to the result of extracting body.
|
||||
if (body != null) {
|
||||
const [extractedBody, type] = extractBody(body)
|
||||
bodyWithType = { body: extractedBody, type }
|
||||
}
|
||||
|
||||
// 5. Perform initialize a response given this, init, and bodyWithType.
|
||||
initializeResponse(this, init, bodyWithType)
|
||||
}
|
||||
|
||||
// Returns response’s type, e.g., "cors".
|
||||
get type () {
|
||||
webidl.brandCheck(this, Response)
|
||||
|
||||
// The type getter steps are to return this’s response’s type.
|
||||
return this[kState].type
|
||||
}
|
||||
|
||||
// Returns response’s URL, if it has one; otherwise the empty string.
|
||||
get url () {
|
||||
webidl.brandCheck(this, Response)
|
||||
|
||||
const urlList = this[kState].urlList
|
||||
|
||||
// The url getter steps are to return the empty string if this’s
|
||||
// response’s URL is null; otherwise this’s response’s URL,
|
||||
// serialized with exclude fragment set to true.
|
||||
const url = urlList[urlList.length - 1] ?? null
|
||||
|
||||
if (url === null) {
|
||||
return ''
|
||||
}
|
||||
|
||||
return URLSerializer(url, true)
|
||||
}
|
||||
|
||||
// Returns whether response was obtained through a redirect.
|
||||
get redirected () {
|
||||
webidl.brandCheck(this, Response)
|
||||
|
||||
// The redirected getter steps are to return true if this’s response’s URL
|
||||
// list has more than one item; otherwise false.
|
||||
return this[kState].urlList.length > 1
|
||||
}
|
||||
|
||||
// Returns response’s status.
|
||||
get status () {
|
||||
webidl.brandCheck(this, Response)
|
||||
|
||||
// The status getter steps are to return this’s response’s status.
|
||||
return this[kState].status
|
||||
}
|
||||
|
||||
// Returns whether response’s status is an ok status.
|
||||
get ok () {
|
||||
webidl.brandCheck(this, Response)
|
||||
|
||||
// The ok getter steps are to return true if this’s response’s status is an
|
||||
// ok status; otherwise false.
|
||||
return this[kState].status >= 200 && this[kState].status <= 299
|
||||
}
|
||||
|
||||
// Returns response’s status message.
|
||||
get statusText () {
|
||||
webidl.brandCheck(this, Response)
|
||||
|
||||
// The statusText getter steps are to return this’s response’s status
|
||||
// message.
|
||||
return this[kState].statusText
|
||||
}
|
||||
|
||||
// Returns response’s headers as Headers.
|
||||
get headers () {
|
||||
webidl.brandCheck(this, Response)
|
||||
|
||||
// The headers getter steps are to return this’s headers.
|
||||
return this[kHeaders]
|
||||
}
|
||||
|
||||
get body () {
|
||||
webidl.brandCheck(this, Response)
|
||||
|
||||
return this[kState].body ? this[kState].body.stream : null
|
||||
}
|
||||
|
||||
get bodyUsed () {
|
||||
webidl.brandCheck(this, Response)
|
||||
|
||||
return !!this[kState].body && util.isDisturbed(this[kState].body.stream)
|
||||
}
|
||||
|
||||
// Returns a clone of response.
|
||||
clone () {
|
||||
webidl.brandCheck(this, Response)
|
||||
|
||||
// 1. If this is unusable, then throw a TypeError.
|
||||
if (this.bodyUsed || (this.body && this.body.locked)) {
|
||||
throw webidl.errors.exception({
|
||||
header: 'Response.clone',
|
||||
message: 'Body has already been consumed.'
|
||||
})
|
||||
}
|
||||
|
||||
// 2. Let clonedResponse be the result of cloning this’s response.
|
||||
const clonedResponse = cloneResponse(this[kState])
|
||||
|
||||
// 3. Return the result of creating a Response object, given
|
||||
// clonedResponse, this’s headers’s guard, and this’s relevant Realm.
|
||||
const clonedResponseObject = new Response()
|
||||
clonedResponseObject[kState] = clonedResponse
|
||||
clonedResponseObject[kRealm] = this[kRealm]
|
||||
clonedResponseObject[kHeaders][kHeadersList] = clonedResponse.headersList
|
||||
clonedResponseObject[kHeaders][kGuard] = this[kHeaders][kGuard]
|
||||
clonedResponseObject[kHeaders][kRealm] = this[kHeaders][kRealm]
|
||||
|
||||
return clonedResponseObject
|
||||
}
|
||||
}
|
||||
|
||||
mixinBody(Response)
|
||||
|
||||
Object.defineProperties(Response.prototype, {
|
||||
type: kEnumerableProperty,
|
||||
url: kEnumerableProperty,
|
||||
status: kEnumerableProperty,
|
||||
ok: kEnumerableProperty,
|
||||
redirected: kEnumerableProperty,
|
||||
statusText: kEnumerableProperty,
|
||||
headers: kEnumerableProperty,
|
||||
clone: kEnumerableProperty,
|
||||
body: kEnumerableProperty,
|
||||
bodyUsed: kEnumerableProperty,
|
||||
[Symbol.toStringTag]: {
|
||||
value: 'Response',
|
||||
configurable: true
|
||||
}
|
||||
})
|
||||
|
||||
Object.defineProperties(Response, {
|
||||
json: kEnumerableProperty,
|
||||
redirect: kEnumerableProperty,
|
||||
error: kEnumerableProperty
|
||||
})
|
||||
|
||||
// https://fetch.spec.whatwg.org/#concept-response-clone
|
||||
function cloneResponse (response) {
|
||||
// To clone a response response, run these steps:
|
||||
|
||||
// 1. If response is a filtered response, then return a new identical
|
||||
// filtered response whose internal response is a clone of response’s
|
||||
// internal response.
|
||||
if (response.internalResponse) {
|
||||
return filterResponse(
|
||||
cloneResponse(response.internalResponse),
|
||||
response.type
|
||||
)
|
||||
}
|
||||
|
||||
// 2. Let newResponse be a copy of response, except for its body.
|
||||
const newResponse = makeResponse({ ...response, body: null })
|
||||
|
||||
// 3. If response’s body is non-null, then set newResponse’s body to the
|
||||
// result of cloning response’s body.
|
||||
if (response.body != null) {
|
||||
newResponse.body = cloneBody(response.body)
|
||||
}
|
||||
|
||||
// 4. Return newResponse.
|
||||
return newResponse
|
||||
}
|
||||
|
||||
function makeResponse (init) {
|
||||
return {
|
||||
aborted: false,
|
||||
rangeRequested: false,
|
||||
timingAllowPassed: false,
|
||||
requestIncludesCredentials: false,
|
||||
type: 'default',
|
||||
status: 200,
|
||||
timingInfo: null,
|
||||
cacheState: '',
|
||||
statusText: '',
|
||||
...init,
|
||||
headersList: init.headersList
|
||||
? new HeadersList(init.headersList)
|
||||
: new HeadersList(),
|
||||
urlList: init.urlList ? [...init.urlList] : []
|
||||
}
|
||||
}
|
||||
|
||||
function makeNetworkError (reason) {
|
||||
const isError = isErrorLike(reason)
|
||||
return makeResponse({
|
||||
type: 'error',
|
||||
status: 0,
|
||||
error: isError
|
||||
? reason
|
||||
: new Error(reason ? String(reason) : reason),
|
||||
aborted: reason && reason.name === 'AbortError'
|
||||
})
|
||||
}
|
||||
|
||||
function makeFilteredResponse (response, state) {
|
||||
state = {
|
||||
internalResponse: response,
|
||||
...state
|
||||
}
|
||||
|
||||
return new Proxy(response, {
|
||||
get (target, p) {
|
||||
return p in state ? state[p] : target[p]
|
||||
},
|
||||
set (target, p, value) {
|
||||
assert(!(p in state))
|
||||
target[p] = value
|
||||
return true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// https://fetch.spec.whatwg.org/#concept-filtered-response
|
||||
function filterResponse (response, type) {
|
||||
// Set response to the following filtered response with response as its
|
||||
// internal response, depending on request’s response tainting:
|
||||
if (type === 'basic') {
|
||||
// A basic filtered response is a filtered response whose type is "basic"
|
||||
// and header list excludes any headers in internal response’s header list
|
||||
// whose name is a forbidden response-header name.
|
||||
|
||||
// Note: undici does not implement forbidden response-header names
|
||||
return makeFilteredResponse(response, {
|
||||
type: 'basic',
|
||||
headersList: response.headersList
|
||||
})
|
||||
} else if (type === 'cors') {
|
||||
// A CORS filtered response is a filtered response whose type is "cors"
|
||||
// and header list excludes any headers in internal response’s header
|
||||
// list whose name is not a CORS-safelisted response-header name, given
|
||||
// internal response’s CORS-exposed header-name list.
|
||||
|
||||
// Note: undici does not implement CORS-safelisted response-header names
|
||||
return makeFilteredResponse(response, {
|
||||
type: 'cors',
|
||||
headersList: response.headersList
|
||||
})
|
||||
} else if (type === 'opaque') {
|
||||
// An opaque filtered response is a filtered response whose type is
|
||||
// "opaque", URL list is the empty list, status is 0, status message
|
||||
// is the empty byte sequence, header list is empty, and body is null.
|
||||
|
||||
return makeFilteredResponse(response, {
|
||||
type: 'opaque',
|
||||
urlList: Object.freeze([]),
|
||||
status: 0,
|
||||
statusText: '',
|
||||
body: null
|
||||
})
|
||||
} else if (type === 'opaqueredirect') {
|
||||
// An opaque-redirect filtered response is a filtered response whose type
|
||||
// is "opaqueredirect", status is 0, status message is the empty byte
|
||||
// sequence, header list is empty, and body is null.
|
||||
|
||||
return makeFilteredResponse(response, {
|
||||
type: 'opaqueredirect',
|
||||
status: 0,
|
||||
statusText: '',
|
||||
headersList: [],
|
||||
body: null
|
||||
})
|
||||
} else {
|
||||
assert(false)
|
||||
}
|
||||
}
|
||||
|
||||
// https://fetch.spec.whatwg.org/#appropriate-network-error
|
||||
function makeAppropriateNetworkError (fetchParams, err = null) {
|
||||
// 1. Assert: fetchParams is canceled.
|
||||
assert(isCancelled(fetchParams))
|
||||
|
||||
// 2. Return an aborted network error if fetchParams is aborted;
|
||||
// otherwise return a network error.
|
||||
return isAborted(fetchParams)
|
||||
? makeNetworkError(Object.assign(new DOMException('The operation was aborted.', 'AbortError'), { cause: err }))
|
||||
: makeNetworkError(Object.assign(new DOMException('Request was cancelled.'), { cause: err }))
|
||||
}
|
||||
|
||||
// https://whatpr.org/fetch/1392.html#initialize-a-response
|
||||
function initializeResponse (response, init, body) {
|
||||
// 1. If init["status"] is not in the range 200 to 599, inclusive, then
|
||||
// throw a RangeError.
|
||||
if (init.status !== null && (init.status < 200 || init.status > 599)) {
|
||||
throw new RangeError('init["status"] must be in the range of 200 to 599, inclusive.')
|
||||
}
|
||||
|
||||
// 2. If init["statusText"] does not match the reason-phrase token production,
|
||||
// then throw a TypeError.
|
||||
if ('statusText' in init && init.statusText != null) {
|
||||
// See, https://datatracker.ietf.org/doc/html/rfc7230#section-3.1.2:
|
||||
// reason-phrase = *( HTAB / SP / VCHAR / obs-text )
|
||||
if (!isValidReasonPhrase(String(init.statusText))) {
|
||||
throw new TypeError('Invalid statusText')
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Set response’s response’s status to init["status"].
|
||||
if ('status' in init && init.status != null) {
|
||||
response[kState].status = init.status
|
||||
}
|
||||
|
||||
// 4. Set response’s response’s status message to init["statusText"].
|
||||
if ('statusText' in init && init.statusText != null) {
|
||||
response[kState].statusText = init.statusText
|
||||
}
|
||||
|
||||
// 5. If init["headers"] exists, then fill response’s headers with init["headers"].
|
||||
if ('headers' in init && init.headers != null) {
|
||||
fill(response[kHeaders], init.headers)
|
||||
}
|
||||
|
||||
// 6. If body was given, then:
|
||||
if (body) {
|
||||
// 1. If response's status is a null body status, then throw a TypeError.
|
||||
if (nullBodyStatus.includes(response.status)) {
|
||||
throw webidl.errors.exception({
|
||||
header: 'Response constructor',
|
||||
message: 'Invalid response status code ' + response.status
|
||||
})
|
||||
}
|
||||
|
||||
// 2. Set response's body to body's body.
|
||||
response[kState].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 && !response[kState].headersList.contains('Content-Type')) {
|
||||
response[kState].headersList.append('content-type', body.type)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
webidl.converters.ReadableStream = webidl.interfaceConverter(
|
||||
ReadableStream
|
||||
)
|
||||
|
||||
webidl.converters.FormData = webidl.interfaceConverter(
|
||||
FormData
|
||||
)
|
||||
|
||||
webidl.converters.URLSearchParams = webidl.interfaceConverter(
|
||||
URLSearchParams
|
||||
)
|
||||
|
||||
// https://fetch.spec.whatwg.org/#typedefdef-xmlhttprequestbodyinit
|
||||
webidl.converters.XMLHttpRequestBodyInit = function (V) {
|
||||
if (typeof V === 'string') {
|
||||
return webidl.converters.USVString(V)
|
||||
}
|
||||
|
||||
if (isBlobLike(V)) {
|
||||
return webidl.converters.Blob(V, { strict: false })
|
||||
}
|
||||
|
||||
if (
|
||||
types.isAnyArrayBuffer(V) ||
|
||||
types.isTypedArray(V) ||
|
||||
types.isDataView(V)
|
||||
) {
|
||||
return webidl.converters.BufferSource(V)
|
||||
}
|
||||
|
||||
if (util.isFormDataLike(V)) {
|
||||
return webidl.converters.FormData(V, { strict: false })
|
||||
}
|
||||
|
||||
if (V instanceof URLSearchParams) {
|
||||
return webidl.converters.URLSearchParams(V)
|
||||
}
|
||||
|
||||
return webidl.converters.DOMString(V)
|
||||
}
|
||||
|
||||
// https://fetch.spec.whatwg.org/#bodyinit
|
||||
webidl.converters.BodyInit = function (V) {
|
||||
if (V instanceof ReadableStream) {
|
||||
return webidl.converters.ReadableStream(V)
|
||||
}
|
||||
|
||||
// Note: the spec doesn't include async iterables,
|
||||
// this is an undici extension.
|
||||
if (V?.[Symbol.asyncIterator]) {
|
||||
return V
|
||||
}
|
||||
|
||||
return webidl.converters.XMLHttpRequestBodyInit(V)
|
||||
}
|
||||
|
||||
webidl.converters.ResponseInit = webidl.dictionaryConverter([
|
||||
{
|
||||
key: 'status',
|
||||
converter: webidl.converters['unsigned short'],
|
||||
defaultValue: 200
|
||||
},
|
||||
{
|
||||
key: 'statusText',
|
||||
converter: webidl.converters.ByteString,
|
||||
defaultValue: ''
|
||||
},
|
||||
{
|
||||
key: 'headers',
|
||||
converter: webidl.converters.HeadersInit
|
||||
}
|
||||
])
|
||||
|
||||
module.exports = {
|
||||
makeNetworkError,
|
||||
makeResponse,
|
||||
makeAppropriateNetworkError,
|
||||
filterResponse,
|
||||
Response,
|
||||
cloneResponse
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
'use strict'
|
||||
|
||||
module.exports = {
|
||||
kUrl: Symbol('url'),
|
||||
kHeaders: Symbol('headers'),
|
||||
kSignal: Symbol('signal'),
|
||||
kState: Symbol('state'),
|
||||
kGuard: Symbol('guard'),
|
||||
kRealm: Symbol('realm')
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,648 @@
|
|||
'use strict'
|
||||
|
||||
const { types } = require('util')
|
||||
const { hasOwn, toUSVString } = require('./util')
|
||||
|
||||
/** @type {import('../../types/webidl').Webidl} */
|
||||
const webidl = {}
|
||||
webidl.converters = {}
|
||||
webidl.util = {}
|
||||
webidl.errors = {}
|
||||
|
||||
webidl.errors.exception = function (message) {
|
||||
return new TypeError(`${message.header}: ${message.message}`)
|
||||
}
|
||||
|
||||
webidl.errors.conversionFailed = function (context) {
|
||||
const plural = context.types.length === 1 ? '' : ' one of'
|
||||
const message =
|
||||
`${context.argument} could not be converted to` +
|
||||
`${plural}: ${context.types.join(', ')}.`
|
||||
|
||||
return webidl.errors.exception({
|
||||
header: context.prefix,
|
||||
message
|
||||
})
|
||||
}
|
||||
|
||||
webidl.errors.invalidArgument = function (context) {
|
||||
return webidl.errors.exception({
|
||||
header: context.prefix,
|
||||
message: `"${context.value}" is an invalid ${context.type}.`
|
||||
})
|
||||
}
|
||||
|
||||
// https://webidl.spec.whatwg.org/#implements
|
||||
webidl.brandCheck = function (V, I, opts = undefined) {
|
||||
if (opts?.strict !== false && !(V instanceof I)) {
|
||||
throw new TypeError('Illegal invocation')
|
||||
} else {
|
||||
return V?.[Symbol.toStringTag] === I.prototype[Symbol.toStringTag]
|
||||
}
|
||||
}
|
||||
|
||||
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.`,
|
||||
...ctx
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
webidl.illegalConstructor = function () {
|
||||
throw webidl.errors.exception({
|
||||
header: 'TypeError',
|
||||
message: 'Illegal constructor'
|
||||
})
|
||||
}
|
||||
|
||||
// https://tc39.es/ecma262/#sec-ecmascript-data-types-and-values
|
||||
webidl.util.Type = function (V) {
|
||||
switch (typeof V) {
|
||||
case 'undefined': return 'Undefined'
|
||||
case 'boolean': return 'Boolean'
|
||||
case 'string': return 'String'
|
||||
case 'symbol': return 'Symbol'
|
||||
case 'number': return 'Number'
|
||||
case 'bigint': return 'BigInt'
|
||||
case 'function':
|
||||
case 'object': {
|
||||
if (V === null) {
|
||||
return 'Null'
|
||||
}
|
||||
|
||||
return 'Object'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// https://webidl.spec.whatwg.org/#abstract-opdef-converttoint
|
||||
webidl.util.ConvertToInt = function (V, bitLength, signedness, opts = {}) {
|
||||
let upperBound
|
||||
let lowerBound
|
||||
|
||||
// 1. If bitLength is 64, then:
|
||||
if (bitLength === 64) {
|
||||
// 1. Let upperBound be 2^53 − 1.
|
||||
upperBound = Math.pow(2, 53) - 1
|
||||
|
||||
// 2. If signedness is "unsigned", then let lowerBound be 0.
|
||||
if (signedness === 'unsigned') {
|
||||
lowerBound = 0
|
||||
} else {
|
||||
// 3. Otherwise let lowerBound be −2^53 + 1.
|
||||
lowerBound = Math.pow(-2, 53) + 1
|
||||
}
|
||||
} else if (signedness === 'unsigned') {
|
||||
// 2. Otherwise, if signedness is "unsigned", then:
|
||||
|
||||
// 1. Let lowerBound be 0.
|
||||
lowerBound = 0
|
||||
|
||||
// 2. Let upperBound be 2^bitLength − 1.
|
||||
upperBound = Math.pow(2, bitLength) - 1
|
||||
} else {
|
||||
// 3. Otherwise:
|
||||
|
||||
// 1. Let lowerBound be -2^bitLength − 1.
|
||||
lowerBound = Math.pow(-2, bitLength) - 1
|
||||
|
||||
// 2. Let upperBound be 2^bitLength − 1 − 1.
|
||||
upperBound = Math.pow(2, bitLength - 1) - 1
|
||||
}
|
||||
|
||||
// 4. Let x be ? ToNumber(V).
|
||||
let x = Number(V)
|
||||
|
||||
// 5. If x is −0, then set x to +0.
|
||||
if (x === 0) {
|
||||
x = 0
|
||||
}
|
||||
|
||||
// 6. If the conversion is to an IDL type associated
|
||||
// with the [EnforceRange] extended attribute, then:
|
||||
if (opts.enforceRange === true) {
|
||||
// 1. If x is NaN, +∞, or −∞, then throw a TypeError.
|
||||
if (
|
||||
Number.isNaN(x) ||
|
||||
x === Number.POSITIVE_INFINITY ||
|
||||
x === Number.NEGATIVE_INFINITY
|
||||
) {
|
||||
throw webidl.errors.exception({
|
||||
header: 'Integer conversion',
|
||||
message: `Could not convert ${V} to an integer.`
|
||||
})
|
||||
}
|
||||
|
||||
// 2. Set x to IntegerPart(x).
|
||||
x = webidl.util.IntegerPart(x)
|
||||
|
||||
// 3. If x < lowerBound or x > upperBound, then
|
||||
// throw a TypeError.
|
||||
if (x < lowerBound || x > upperBound) {
|
||||
throw webidl.errors.exception({
|
||||
header: 'Integer conversion',
|
||||
message: `Value must be between ${lowerBound}-${upperBound}, got ${x}.`
|
||||
})
|
||||
}
|
||||
|
||||
// 4. Return x.
|
||||
return x
|
||||
}
|
||||
|
||||
// 7. If x is not NaN and the conversion is to an IDL
|
||||
// type associated with the [Clamp] extended
|
||||
// attribute, then:
|
||||
if (!Number.isNaN(x) && opts.clamp === true) {
|
||||
// 1. Set x to min(max(x, lowerBound), upperBound).
|
||||
x = Math.min(Math.max(x, lowerBound), upperBound)
|
||||
|
||||
// 2. Round x to the nearest integer, choosing the
|
||||
// even integer if it lies halfway between two,
|
||||
// and choosing +0 rather than −0.
|
||||
if (Math.floor(x) % 2 === 0) {
|
||||
x = Math.floor(x)
|
||||
} else {
|
||||
x = Math.ceil(x)
|
||||
}
|
||||
|
||||
// 3. Return x.
|
||||
return x
|
||||
}
|
||||
|
||||
// 8. If x is NaN, +0, +∞, or −∞, then return +0.
|
||||
if (
|
||||
Number.isNaN(x) ||
|
||||
(x === 0 && Object.is(0, x)) ||
|
||||
x === Number.POSITIVE_INFINITY ||
|
||||
x === Number.NEGATIVE_INFINITY
|
||||
) {
|
||||
return 0
|
||||
}
|
||||
|
||||
// 9. Set x to IntegerPart(x).
|
||||
x = webidl.util.IntegerPart(x)
|
||||
|
||||
// 10. Set x to x modulo 2^bitLength.
|
||||
x = x % Math.pow(2, bitLength)
|
||||
|
||||
// 11. If signedness is "signed" and x ≥ 2^bitLength − 1,
|
||||
// then return x − 2^bitLength.
|
||||
if (signedness === 'signed' && x >= Math.pow(2, bitLength) - 1) {
|
||||
return x - Math.pow(2, bitLength)
|
||||
}
|
||||
|
||||
// 12. Otherwise, return x.
|
||||
return x
|
||||
}
|
||||
|
||||
// https://webidl.spec.whatwg.org/#abstract-opdef-integerpart
|
||||
webidl.util.IntegerPart = function (n) {
|
||||
// 1. Let r be floor(abs(n)).
|
||||
const r = Math.floor(Math.abs(n))
|
||||
|
||||
// 2. If n < 0, then return -1 × r.
|
||||
if (n < 0) {
|
||||
return -1 * r
|
||||
}
|
||||
|
||||
// 3. Otherwise, return r.
|
||||
return r
|
||||
}
|
||||
|
||||
// https://webidl.spec.whatwg.org/#es-sequence
|
||||
webidl.sequenceConverter = function (converter) {
|
||||
return (V) => {
|
||||
// 1. If Type(V) is not Object, throw a TypeError.
|
||||
if (webidl.util.Type(V) !== 'Object') {
|
||||
throw webidl.errors.exception({
|
||||
header: 'Sequence',
|
||||
message: `Value of type ${webidl.util.Type(V)} is not an Object.`
|
||||
})
|
||||
}
|
||||
|
||||
// 2. Let method be ? GetMethod(V, @@iterator).
|
||||
/** @type {Generator} */
|
||||
const method = V?.[Symbol.iterator]?.()
|
||||
const seq = []
|
||||
|
||||
// 3. If method is undefined, throw a TypeError.
|
||||
if (
|
||||
method === undefined ||
|
||||
typeof method.next !== 'function'
|
||||
) {
|
||||
throw webidl.errors.exception({
|
||||
header: 'Sequence',
|
||||
message: 'Object is not an iterator.'
|
||||
})
|
||||
}
|
||||
|
||||
// https://webidl.spec.whatwg.org/#create-sequence-from-iterable
|
||||
while (true) {
|
||||
const { done, value } = method.next()
|
||||
|
||||
if (done) {
|
||||
break
|
||||
}
|
||||
|
||||
seq.push(converter(value))
|
||||
}
|
||||
|
||||
return seq
|
||||
}
|
||||
}
|
||||
|
||||
// https://webidl.spec.whatwg.org/#es-to-record
|
||||
webidl.recordConverter = function (keyConverter, valueConverter) {
|
||||
return (O) => {
|
||||
// 1. If Type(O) is not Object, throw a TypeError.
|
||||
if (webidl.util.Type(O) !== 'Object') {
|
||||
throw webidl.errors.exception({
|
||||
header: 'Record',
|
||||
message: `Value of type ${webidl.util.Type(O)} is not an Object.`
|
||||
})
|
||||
}
|
||||
|
||||
// 2. Let result be a new empty instance of record<K, V>.
|
||||
const result = {}
|
||||
|
||||
if (!types.isProxy(O)) {
|
||||
// Object.keys only returns enumerable properties
|
||||
const keys = Object.keys(O)
|
||||
|
||||
for (const key of keys) {
|
||||
// 1. Let typedKey be key converted to an IDL value of type K.
|
||||
const typedKey = keyConverter(key)
|
||||
|
||||
// 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])
|
||||
|
||||
// 4. Set result[typedKey] to typedValue.
|
||||
result[typedKey] = typedValue
|
||||
}
|
||||
|
||||
// 5. Return result.
|
||||
return result
|
||||
}
|
||||
|
||||
// 3. Let keys be ? O.[[OwnPropertyKeys]]().
|
||||
const keys = Reflect.ownKeys(O)
|
||||
|
||||
// 4. For each key of keys.
|
||||
for (const key of keys) {
|
||||
// 1. Let desc be ? O.[[GetOwnProperty]](key).
|
||||
const desc = Reflect.getOwnPropertyDescriptor(O, key)
|
||||
|
||||
// 2. If desc is not undefined and desc.[[Enumerable]] is true:
|
||||
if (desc?.enumerable) {
|
||||
// 1. Let typedKey be key converted to an IDL value of type K.
|
||||
const typedKey = keyConverter(key)
|
||||
|
||||
// 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])
|
||||
|
||||
// 4. Set result[typedKey] to typedValue.
|
||||
result[typedKey] = typedValue
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Return result.
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
webidl.interfaceConverter = function (i) {
|
||||
return (V, opts = {}) => {
|
||||
if (opts.strict !== false && !(V instanceof i)) {
|
||||
throw webidl.errors.exception({
|
||||
header: i.name,
|
||||
message: `Expected ${V} to be an instance of ${i.name}.`
|
||||
})
|
||||
}
|
||||
|
||||
return V
|
||||
}
|
||||
}
|
||||
|
||||
webidl.dictionaryConverter = function (converters) {
|
||||
return (dictionary) => {
|
||||
const type = webidl.util.Type(dictionary)
|
||||
const dict = {}
|
||||
|
||||
if (type === 'Null' || type === 'Undefined') {
|
||||
return dict
|
||||
} else if (type !== 'Object') {
|
||||
throw webidl.errors.exception({
|
||||
header: 'Dictionary',
|
||||
message: `Expected ${dictionary} to be one of: Null, Undefined, Object.`
|
||||
})
|
||||
}
|
||||
|
||||
for (const options of converters) {
|
||||
const { key, defaultValue, required, converter } = options
|
||||
|
||||
if (required === true) {
|
||||
if (!hasOwn(dictionary, key)) {
|
||||
throw webidl.errors.exception({
|
||||
header: 'Dictionary',
|
||||
message: `Missing required key "${key}".`
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
let value = dictionary[key]
|
||||
const hasDefault = hasOwn(options, 'defaultValue')
|
||||
|
||||
// Only use defaultValue if value is undefined and
|
||||
// a defaultValue options was provided.
|
||||
if (hasDefault && value !== null) {
|
||||
value = 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)
|
||||
|
||||
if (
|
||||
options.allowedValues &&
|
||||
!options.allowedValues.includes(value)
|
||||
) {
|
||||
throw webidl.errors.exception({
|
||||
header: 'Dictionary',
|
||||
message: `${value} is not an accepted type. Expected one of ${options.allowedValues.join(', ')}.`
|
||||
})
|
||||
}
|
||||
|
||||
dict[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
return dict
|
||||
}
|
||||
}
|
||||
|
||||
webidl.nullableConverter = function (converter) {
|
||||
return (V) => {
|
||||
if (V === null) {
|
||||
return V
|
||||
}
|
||||
|
||||
return converter(V)
|
||||
}
|
||||
}
|
||||
|
||||
// https://webidl.spec.whatwg.org/#es-DOMString
|
||||
webidl.converters.DOMString = function (V, opts = {}) {
|
||||
// 1. If V is null and the conversion is to an IDL type
|
||||
// associated with the [LegacyNullToEmptyString]
|
||||
// extended attribute, then return the DOMString value
|
||||
// that represents the empty string.
|
||||
if (V === null && opts.legacyNullToEmptyString) {
|
||||
return ''
|
||||
}
|
||||
|
||||
// 2. Let x be ? ToString(V).
|
||||
if (typeof V === 'symbol') {
|
||||
throw new TypeError('Could not convert argument of type symbol to string.')
|
||||
}
|
||||
|
||||
// 3. Return the IDL DOMString value that represents the
|
||||
// same sequence of code units as the one the
|
||||
// ECMAScript String value x represents.
|
||||
return String(V)
|
||||
}
|
||||
|
||||
// https://webidl.spec.whatwg.org/#es-ByteString
|
||||
webidl.converters.ByteString = function (V) {
|
||||
// 1. Let x be ? ToString(V).
|
||||
// Note: DOMString converter perform ? ToString(V)
|
||||
const x = webidl.converters.DOMString(V)
|
||||
|
||||
// 2. If the value of any element of x is greater than
|
||||
// 255, then throw a TypeError.
|
||||
for (let index = 0; index < x.length; index++) {
|
||||
const charCode = x.charCodeAt(index)
|
||||
|
||||
if (charCode > 255) {
|
||||
throw new TypeError(
|
||||
'Cannot convert argument to a ByteString because the character at ' +
|
||||
`index ${index} has a value of ${charCode} which is greater than 255.`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Return an IDL ByteString value whose length is the
|
||||
// length of x, and where the value of each element is
|
||||
// the value of the corresponding element of x.
|
||||
return x
|
||||
}
|
||||
|
||||
// https://webidl.spec.whatwg.org/#es-USVString
|
||||
webidl.converters.USVString = toUSVString
|
||||
|
||||
// https://webidl.spec.whatwg.org/#es-boolean
|
||||
webidl.converters.boolean = function (V) {
|
||||
// 1. Let x be the result of computing ToBoolean(V).
|
||||
const x = Boolean(V)
|
||||
|
||||
// 2. Return the IDL boolean value that is the one that represents
|
||||
// the same truth value as the ECMAScript Boolean value x.
|
||||
return x
|
||||
}
|
||||
|
||||
// https://webidl.spec.whatwg.org/#es-any
|
||||
webidl.converters.any = function (V) {
|
||||
return V
|
||||
}
|
||||
|
||||
// https://webidl.spec.whatwg.org/#es-long-long
|
||||
webidl.converters['long long'] = function (V) {
|
||||
// 1. Let x be ? ConvertToInt(V, 64, "signed").
|
||||
const x = webidl.util.ConvertToInt(V, 64, 'signed')
|
||||
|
||||
// 2. Return the IDL long long value that represents
|
||||
// the same numeric value as x.
|
||||
return x
|
||||
}
|
||||
|
||||
// https://webidl.spec.whatwg.org/#es-unsigned-long-long
|
||||
webidl.converters['unsigned long long'] = function (V) {
|
||||
// 1. Let x be ? ConvertToInt(V, 64, "unsigned").
|
||||
const x = webidl.util.ConvertToInt(V, 64, 'unsigned')
|
||||
|
||||
// 2. Return the IDL unsigned long long value that
|
||||
// represents the same numeric value as x.
|
||||
return x
|
||||
}
|
||||
|
||||
// https://webidl.spec.whatwg.org/#es-unsigned-long
|
||||
webidl.converters['unsigned long'] = function (V) {
|
||||
// 1. Let x be ? ConvertToInt(V, 32, "unsigned").
|
||||
const x = webidl.util.ConvertToInt(V, 32, 'unsigned')
|
||||
|
||||
// 2. Return the IDL unsigned long value that
|
||||
// represents the same numeric value as x.
|
||||
return x
|
||||
}
|
||||
|
||||
// https://webidl.spec.whatwg.org/#es-unsigned-short
|
||||
webidl.converters['unsigned short'] = function (V, opts) {
|
||||
// 1. Let x be ? ConvertToInt(V, 16, "unsigned").
|
||||
const x = webidl.util.ConvertToInt(V, 16, 'unsigned', opts)
|
||||
|
||||
// 2. Return the IDL unsigned short value that represents
|
||||
// the same numeric value as x.
|
||||
return x
|
||||
}
|
||||
|
||||
// https://webidl.spec.whatwg.org/#idl-ArrayBuffer
|
||||
webidl.converters.ArrayBuffer = function (V, opts = {}) {
|
||||
// 1. If Type(V) is not Object, or V does not have an
|
||||
// [[ArrayBufferData]] internal slot, then throw a
|
||||
// TypeError.
|
||||
// see: https://tc39.es/ecma262/#sec-properties-of-the-arraybuffer-instances
|
||||
// see: https://tc39.es/ecma262/#sec-properties-of-the-sharedarraybuffer-instances
|
||||
if (
|
||||
webidl.util.Type(V) !== 'Object' ||
|
||||
!types.isAnyArrayBuffer(V)
|
||||
) {
|
||||
throw webidl.errors.conversionFailed({
|
||||
prefix: `${V}`,
|
||||
argument: `${V}`,
|
||||
types: ['ArrayBuffer']
|
||||
})
|
||||
}
|
||||
|
||||
// 2. If the conversion is not to an IDL type associated
|
||||
// with the [AllowShared] extended attribute, and
|
||||
// IsSharedArrayBuffer(V) is true, then throw a
|
||||
// TypeError.
|
||||
if (opts.allowShared === false && types.isSharedArrayBuffer(V)) {
|
||||
throw webidl.errors.exception({
|
||||
header: 'ArrayBuffer',
|
||||
message: 'SharedArrayBuffer is not allowed.'
|
||||
})
|
||||
}
|
||||
|
||||
// 3. If the conversion is not to an IDL type associated
|
||||
// with the [AllowResizable] extended attribute, and
|
||||
// IsResizableArrayBuffer(V) is true, then throw a
|
||||
// TypeError.
|
||||
// Note: resizable ArrayBuffers are currently a proposal.
|
||||
|
||||
// 4. Return the IDL ArrayBuffer value that is a
|
||||
// reference to the same object as V.
|
||||
return V
|
||||
}
|
||||
|
||||
webidl.converters.TypedArray = function (V, T, opts = {}) {
|
||||
// 1. Let T be the IDL type V is being converted to.
|
||||
|
||||
// 2. If Type(V) is not Object, or V does not have a
|
||||
// [[TypedArrayName]] internal slot with a value
|
||||
// equal to T’s name, then throw a TypeError.
|
||||
if (
|
||||
webidl.util.Type(V) !== 'Object' ||
|
||||
!types.isTypedArray(V) ||
|
||||
V.constructor.name !== T.name
|
||||
) {
|
||||
throw webidl.errors.conversionFailed({
|
||||
prefix: `${T.name}`,
|
||||
argument: `${V}`,
|
||||
types: [T.name]
|
||||
})
|
||||
}
|
||||
|
||||
// 3. If the conversion is not to an IDL type associated
|
||||
// with the [AllowShared] extended attribute, and
|
||||
// IsSharedArrayBuffer(V.[[ViewedArrayBuffer]]) is
|
||||
// true, then throw a TypeError.
|
||||
if (opts.allowShared === false && types.isSharedArrayBuffer(V.buffer)) {
|
||||
throw webidl.errors.exception({
|
||||
header: 'ArrayBuffer',
|
||||
message: 'SharedArrayBuffer is not allowed.'
|
||||
})
|
||||
}
|
||||
|
||||
// 4. If the conversion is not to an IDL type associated
|
||||
// with the [AllowResizable] extended attribute, and
|
||||
// IsResizableArrayBuffer(V.[[ViewedArrayBuffer]]) is
|
||||
// true, then throw a TypeError.
|
||||
// Note: resizable array buffers are currently a proposal
|
||||
|
||||
// 5. Return the IDL value of type T that is a reference
|
||||
// to the same object as V.
|
||||
return V
|
||||
}
|
||||
|
||||
webidl.converters.DataView = function (V, opts = {}) {
|
||||
// 1. If Type(V) is not Object, or V does not have a
|
||||
// [[DataView]] internal slot, then throw a TypeError.
|
||||
if (webidl.util.Type(V) !== 'Object' || !types.isDataView(V)) {
|
||||
throw webidl.errors.exception({
|
||||
header: 'DataView',
|
||||
message: 'Object is not a DataView.'
|
||||
})
|
||||
}
|
||||
|
||||
// 2. If the conversion is not to an IDL type associated
|
||||
// with the [AllowShared] extended attribute, and
|
||||
// IsSharedArrayBuffer(V.[[ViewedArrayBuffer]]) is true,
|
||||
// then throw a TypeError.
|
||||
if (opts.allowShared === false && types.isSharedArrayBuffer(V.buffer)) {
|
||||
throw webidl.errors.exception({
|
||||
header: 'ArrayBuffer',
|
||||
message: 'SharedArrayBuffer is not allowed.'
|
||||
})
|
||||
}
|
||||
|
||||
// 3. If the conversion is not to an IDL type associated
|
||||
// with the [AllowResizable] extended attribute, and
|
||||
// IsResizableArrayBuffer(V.[[ViewedArrayBuffer]]) is
|
||||
// true, then throw a TypeError.
|
||||
// Note: resizable ArrayBuffers are currently a proposal
|
||||
|
||||
// 4. Return the IDL DataView value that is a reference
|
||||
// to the same object as V.
|
||||
return V
|
||||
}
|
||||
|
||||
// https://webidl.spec.whatwg.org/#BufferSource
|
||||
webidl.converters.BufferSource = function (V, opts = {}) {
|
||||
if (types.isAnyArrayBuffer(V)) {
|
||||
return webidl.converters.ArrayBuffer(V, opts)
|
||||
}
|
||||
|
||||
if (types.isTypedArray(V)) {
|
||||
return webidl.converters.TypedArray(V, V.constructor)
|
||||
}
|
||||
|
||||
if (types.isDataView(V)) {
|
||||
return webidl.converters.DataView(V, opts)
|
||||
}
|
||||
|
||||
throw new TypeError(`Could not convert ${V} to a BufferSource.`)
|
||||
}
|
||||
|
||||
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
|
||||
)
|
||||
|
||||
module.exports = {
|
||||
webidl
|
||||
}
|
|
@ -0,0 +1,290 @@
|
|||
'use strict'
|
||||
|
||||
/**
|
||||
* @see https://encoding.spec.whatwg.org/#concept-encoding-get
|
||||
* @param {string|undefined} label
|
||||
*/
|
||||
function getEncoding (label) {
|
||||
if (!label) {
|
||||
return 'failure'
|
||||
}
|
||||
|
||||
// 1. Remove any leading and trailing ASCII whitespace from label.
|
||||
// 2. If label is an ASCII case-insensitive match for any of the
|
||||
// labels listed in the table below, then return the
|
||||
// corresponding encoding; otherwise return failure.
|
||||
switch (label.trim().toLowerCase()) {
|
||||
case 'unicode-1-1-utf-8':
|
||||
case 'unicode11utf8':
|
||||
case 'unicode20utf8':
|
||||
case 'utf-8':
|
||||
case 'utf8':
|
||||
case 'x-unicode20utf8':
|
||||
return 'UTF-8'
|
||||
case '866':
|
||||
case 'cp866':
|
||||
case 'csibm866':
|
||||
case 'ibm866':
|
||||
return 'IBM866'
|
||||
case 'csisolatin2':
|
||||
case 'iso-8859-2':
|
||||
case 'iso-ir-101':
|
||||
case 'iso8859-2':
|
||||
case 'iso88592':
|
||||
case 'iso_8859-2':
|
||||
case 'iso_8859-2:1987':
|
||||
case 'l2':
|
||||
case 'latin2':
|
||||
return 'ISO-8859-2'
|
||||
case 'csisolatin3':
|
||||
case 'iso-8859-3':
|
||||
case 'iso-ir-109':
|
||||
case 'iso8859-3':
|
||||
case 'iso88593':
|
||||
case 'iso_8859-3':
|
||||
case 'iso_8859-3:1988':
|
||||
case 'l3':
|
||||
case 'latin3':
|
||||
return 'ISO-8859-3'
|
||||
case 'csisolatin4':
|
||||
case 'iso-8859-4':
|
||||
case 'iso-ir-110':
|
||||
case 'iso8859-4':
|
||||
case 'iso88594':
|
||||
case 'iso_8859-4':
|
||||
case 'iso_8859-4:1988':
|
||||
case 'l4':
|
||||
case 'latin4':
|
||||
return 'ISO-8859-4'
|
||||
case 'csisolatincyrillic':
|
||||
case 'cyrillic':
|
||||
case 'iso-8859-5':
|
||||
case 'iso-ir-144':
|
||||
case 'iso8859-5':
|
||||
case 'iso88595':
|
||||
case 'iso_8859-5':
|
||||
case 'iso_8859-5:1988':
|
||||
return 'ISO-8859-5'
|
||||
case 'arabic':
|
||||
case 'asmo-708':
|
||||
case 'csiso88596e':
|
||||
case 'csiso88596i':
|
||||
case 'csisolatinarabic':
|
||||
case 'ecma-114':
|
||||
case 'iso-8859-6':
|
||||
case 'iso-8859-6-e':
|
||||
case 'iso-8859-6-i':
|
||||
case 'iso-ir-127':
|
||||
case 'iso8859-6':
|
||||
case 'iso88596':
|
||||
case 'iso_8859-6':
|
||||
case 'iso_8859-6:1987':
|
||||
return 'ISO-8859-6'
|
||||
case 'csisolatingreek':
|
||||
case 'ecma-118':
|
||||
case 'elot_928':
|
||||
case 'greek':
|
||||
case 'greek8':
|
||||
case 'iso-8859-7':
|
||||
case 'iso-ir-126':
|
||||
case 'iso8859-7':
|
||||
case 'iso88597':
|
||||
case 'iso_8859-7':
|
||||
case 'iso_8859-7:1987':
|
||||
case 'sun_eu_greek':
|
||||
return 'ISO-8859-7'
|
||||
case 'csiso88598e':
|
||||
case 'csisolatinhebrew':
|
||||
case 'hebrew':
|
||||
case 'iso-8859-8':
|
||||
case 'iso-8859-8-e':
|
||||
case 'iso-ir-138':
|
||||
case 'iso8859-8':
|
||||
case 'iso88598':
|
||||
case 'iso_8859-8':
|
||||
case 'iso_8859-8:1988':
|
||||
case 'visual':
|
||||
return 'ISO-8859-8'
|
||||
case 'csiso88598i':
|
||||
case 'iso-8859-8-i':
|
||||
case 'logical':
|
||||
return 'ISO-8859-8-I'
|
||||
case 'csisolatin6':
|
||||
case 'iso-8859-10':
|
||||
case 'iso-ir-157':
|
||||
case 'iso8859-10':
|
||||
case 'iso885910':
|
||||
case 'l6':
|
||||
case 'latin6':
|
||||
return 'ISO-8859-10'
|
||||
case 'iso-8859-13':
|
||||
case 'iso8859-13':
|
||||
case 'iso885913':
|
||||
return 'ISO-8859-13'
|
||||
case 'iso-8859-14':
|
||||
case 'iso8859-14':
|
||||
case 'iso885914':
|
||||
return 'ISO-8859-14'
|
||||
case 'csisolatin9':
|
||||
case 'iso-8859-15':
|
||||
case 'iso8859-15':
|
||||
case 'iso885915':
|
||||
case 'iso_8859-15':
|
||||
case 'l9':
|
||||
return 'ISO-8859-15'
|
||||
case 'iso-8859-16':
|
||||
return 'ISO-8859-16'
|
||||
case 'cskoi8r':
|
||||
case 'koi':
|
||||
case 'koi8':
|
||||
case 'koi8-r':
|
||||
case 'koi8_r':
|
||||
return 'KOI8-R'
|
||||
case 'koi8-ru':
|
||||
case 'koi8-u':
|
||||
return 'KOI8-U'
|
||||
case 'csmacintosh':
|
||||
case 'mac':
|
||||
case 'macintosh':
|
||||
case 'x-mac-roman':
|
||||
return 'macintosh'
|
||||
case 'iso-8859-11':
|
||||
case 'iso8859-11':
|
||||
case 'iso885911':
|
||||
case 'tis-620':
|
||||
case 'windows-874':
|
||||
return 'windows-874'
|
||||
case 'cp1250':
|
||||
case 'windows-1250':
|
||||
case 'x-cp1250':
|
||||
return 'windows-1250'
|
||||
case 'cp1251':
|
||||
case 'windows-1251':
|
||||
case 'x-cp1251':
|
||||
return 'windows-1251'
|
||||
case 'ansi_x3.4-1968':
|
||||
case 'ascii':
|
||||
case 'cp1252':
|
||||
case 'cp819':
|
||||
case 'csisolatin1':
|
||||
case 'ibm819':
|
||||
case 'iso-8859-1':
|
||||
case 'iso-ir-100':
|
||||
case 'iso8859-1':
|
||||
case 'iso88591':
|
||||
case 'iso_8859-1':
|
||||
case 'iso_8859-1:1987':
|
||||
case 'l1':
|
||||
case 'latin1':
|
||||
case 'us-ascii':
|
||||
case 'windows-1252':
|
||||
case 'x-cp1252':
|
||||
return 'windows-1252'
|
||||
case 'cp1253':
|
||||
case 'windows-1253':
|
||||
case 'x-cp1253':
|
||||
return 'windows-1253'
|
||||
case 'cp1254':
|
||||
case 'csisolatin5':
|
||||
case 'iso-8859-9':
|
||||
case 'iso-ir-148':
|
||||
case 'iso8859-9':
|
||||
case 'iso88599':
|
||||
case 'iso_8859-9':
|
||||
case 'iso_8859-9:1989':
|
||||
case 'l5':
|
||||
case 'latin5':
|
||||
case 'windows-1254':
|
||||
case 'x-cp1254':
|
||||
return 'windows-1254'
|
||||
case 'cp1255':
|
||||
case 'windows-1255':
|
||||
case 'x-cp1255':
|
||||
return 'windows-1255'
|
||||
case 'cp1256':
|
||||
case 'windows-1256':
|
||||
case 'x-cp1256':
|
||||
return 'windows-1256'
|
||||
case 'cp1257':
|
||||
case 'windows-1257':
|
||||
case 'x-cp1257':
|
||||
return 'windows-1257'
|
||||
case 'cp1258':
|
||||
case 'windows-1258':
|
||||
case 'x-cp1258':
|
||||
return 'windows-1258'
|
||||
case 'x-mac-cyrillic':
|
||||
case 'x-mac-ukrainian':
|
||||
return 'x-mac-cyrillic'
|
||||
case 'chinese':
|
||||
case 'csgb2312':
|
||||
case 'csiso58gb231280':
|
||||
case 'gb2312':
|
||||
case 'gb_2312':
|
||||
case 'gb_2312-80':
|
||||
case 'gbk':
|
||||
case 'iso-ir-58':
|
||||
case 'x-gbk':
|
||||
return 'GBK'
|
||||
case 'gb18030':
|
||||
return 'gb18030'
|
||||
case 'big5':
|
||||
case 'big5-hkscs':
|
||||
case 'cn-big5':
|
||||
case 'csbig5':
|
||||
case 'x-x-big5':
|
||||
return 'Big5'
|
||||
case 'cseucpkdfmtjapanese':
|
||||
case 'euc-jp':
|
||||
case 'x-euc-jp':
|
||||
return 'EUC-JP'
|
||||
case 'csiso2022jp':
|
||||
case 'iso-2022-jp':
|
||||
return 'ISO-2022-JP'
|
||||
case 'csshiftjis':
|
||||
case 'ms932':
|
||||
case 'ms_kanji':
|
||||
case 'shift-jis':
|
||||
case 'shift_jis':
|
||||
case 'sjis':
|
||||
case 'windows-31j':
|
||||
case 'x-sjis':
|
||||
return 'Shift_JIS'
|
||||
case 'cseuckr':
|
||||
case 'csksc56011987':
|
||||
case 'euc-kr':
|
||||
case 'iso-ir-149':
|
||||
case 'korean':
|
||||
case 'ks_c_5601-1987':
|
||||
case 'ks_c_5601-1989':
|
||||
case 'ksc5601':
|
||||
case 'ksc_5601':
|
||||
case 'windows-949':
|
||||
return 'EUC-KR'
|
||||
case 'csiso2022kr':
|
||||
case 'hz-gb-2312':
|
||||
case 'iso-2022-cn':
|
||||
case 'iso-2022-cn-ext':
|
||||
case 'iso-2022-kr':
|
||||
case 'replacement':
|
||||
return 'replacement'
|
||||
case 'unicodefffe':
|
||||
case 'utf-16be':
|
||||
return 'UTF-16BE'
|
||||
case 'csunicode':
|
||||
case 'iso-10646-ucs-2':
|
||||
case 'ucs-2':
|
||||
case 'unicode':
|
||||
case 'unicodefeff':
|
||||
case 'utf-16':
|
||||
case 'utf-16le':
|
||||
return 'UTF-16LE'
|
||||
case 'x-user-defined':
|
||||
return 'x-user-defined'
|
||||
default: return 'failure'
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getEncoding
|
||||
}
|
|
@ -0,0 +1,344 @@
|
|||
'use strict'
|
||||
|
||||
const {
|
||||
staticPropertyDescriptors,
|
||||
readOperation,
|
||||
fireAProgressEvent
|
||||
} = require('./util')
|
||||
const {
|
||||
kState,
|
||||
kError,
|
||||
kResult,
|
||||
kEvents,
|
||||
kAborted
|
||||
} = require('./symbols')
|
||||
const { webidl } = require('../fetch/webidl')
|
||||
const { kEnumerableProperty } = require('../core/util')
|
||||
|
||||
class FileReader extends EventTarget {
|
||||
constructor () {
|
||||
super()
|
||||
|
||||
this[kState] = 'empty'
|
||||
this[kResult] = null
|
||||
this[kError] = null
|
||||
this[kEvents] = {
|
||||
loadend: null,
|
||||
error: null,
|
||||
abort: null,
|
||||
load: null,
|
||||
progress: null,
|
||||
loadstart: null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @see https://w3c.github.io/FileAPI/#dfn-readAsArrayBuffer
|
||||
* @param {import('buffer').Blob} blob
|
||||
*/
|
||||
readAsArrayBuffer (blob) {
|
||||
webidl.brandCheck(this, FileReader)
|
||||
|
||||
webidl.argumentLengthCheck(arguments, 1, { header: 'FileReader.readAsArrayBuffer' })
|
||||
|
||||
blob = webidl.converters.Blob(blob, { strict: false })
|
||||
|
||||
// The readAsArrayBuffer(blob) method, when invoked,
|
||||
// must initiate a read operation for blob with ArrayBuffer.
|
||||
readOperation(this, blob, 'ArrayBuffer')
|
||||
}
|
||||
|
||||
/**
|
||||
* @see https://w3c.github.io/FileAPI/#readAsBinaryString
|
||||
* @param {import('buffer').Blob} blob
|
||||
*/
|
||||
readAsBinaryString (blob) {
|
||||
webidl.brandCheck(this, FileReader)
|
||||
|
||||
webidl.argumentLengthCheck(arguments, 1, { header: 'FileReader.readAsBinaryString' })
|
||||
|
||||
blob = webidl.converters.Blob(blob, { strict: false })
|
||||
|
||||
// The readAsBinaryString(blob) method, when invoked,
|
||||
// must initiate a read operation for blob with BinaryString.
|
||||
readOperation(this, blob, 'BinaryString')
|
||||
}
|
||||
|
||||
/**
|
||||
* @see https://w3c.github.io/FileAPI/#readAsDataText
|
||||
* @param {import('buffer').Blob} blob
|
||||
* @param {string?} encoding
|
||||
*/
|
||||
readAsText (blob, encoding = undefined) {
|
||||
webidl.brandCheck(this, FileReader)
|
||||
|
||||
webidl.argumentLengthCheck(arguments, 1, { header: 'FileReader.readAsText' })
|
||||
|
||||
blob = webidl.converters.Blob(blob, { strict: false })
|
||||
|
||||
if (encoding !== undefined) {
|
||||
encoding = webidl.converters.DOMString(encoding)
|
||||
}
|
||||
|
||||
// The readAsText(blob, encoding) method, when invoked,
|
||||
// must initiate a read operation for blob with Text and encoding.
|
||||
readOperation(this, blob, 'Text', encoding)
|
||||
}
|
||||
|
||||
/**
|
||||
* @see https://w3c.github.io/FileAPI/#dfn-readAsDataURL
|
||||
* @param {import('buffer').Blob} blob
|
||||
*/
|
||||
readAsDataURL (blob) {
|
||||
webidl.brandCheck(this, FileReader)
|
||||
|
||||
webidl.argumentLengthCheck(arguments, 1, { header: 'FileReader.readAsDataURL' })
|
||||
|
||||
blob = webidl.converters.Blob(blob, { strict: false })
|
||||
|
||||
// The readAsDataURL(blob) method, when invoked, must
|
||||
// initiate a read operation for blob with DataURL.
|
||||
readOperation(this, blob, 'DataURL')
|
||||
}
|
||||
|
||||
/**
|
||||
* @see https://w3c.github.io/FileAPI/#dfn-abort
|
||||
*/
|
||||
abort () {
|
||||
// 1. If this's state is "empty" or if this's state is
|
||||
// "done" set this's result to null and terminate
|
||||
// this algorithm.
|
||||
if (this[kState] === 'empty' || this[kState] === 'done') {
|
||||
this[kResult] = null
|
||||
return
|
||||
}
|
||||
|
||||
// 2. If this's state is "loading" set this's state to
|
||||
// "done" and set this's result to null.
|
||||
if (this[kState] === 'loading') {
|
||||
this[kState] = 'done'
|
||||
this[kResult] = null
|
||||
}
|
||||
|
||||
// 3. If there are any tasks from this on the file reading
|
||||
// task source in an affiliated task queue, then remove
|
||||
// those tasks from that task queue.
|
||||
this[kAborted] = true
|
||||
|
||||
// 4. Terminate the algorithm for the read method being processed.
|
||||
// TODO
|
||||
|
||||
// 5. Fire a progress event called abort at this.
|
||||
fireAProgressEvent('abort', this)
|
||||
|
||||
// 6. If this's state is not "loading", fire a progress
|
||||
// event called loadend at this.
|
||||
if (this[kState] !== 'loading') {
|
||||
fireAProgressEvent('loadend', this)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @see https://w3c.github.io/FileAPI/#dom-filereader-readystate
|
||||
*/
|
||||
get readyState () {
|
||||
webidl.brandCheck(this, FileReader)
|
||||
|
||||
switch (this[kState]) {
|
||||
case 'empty': return this.EMPTY
|
||||
case 'loading': return this.LOADING
|
||||
case 'done': return this.DONE
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @see https://w3c.github.io/FileAPI/#dom-filereader-result
|
||||
*/
|
||||
get result () {
|
||||
webidl.brandCheck(this, FileReader)
|
||||
|
||||
// The result attribute’s getter, when invoked, must return
|
||||
// this's result.
|
||||
return this[kResult]
|
||||
}
|
||||
|
||||
/**
|
||||
* @see https://w3c.github.io/FileAPI/#dom-filereader-error
|
||||
*/
|
||||
get error () {
|
||||
webidl.brandCheck(this, FileReader)
|
||||
|
||||
// The error attribute’s getter, when invoked, must return
|
||||
// this's error.
|
||||
return this[kError]
|
||||
}
|
||||
|
||||
get onloadend () {
|
||||
webidl.brandCheck(this, FileReader)
|
||||
|
||||
return this[kEvents].loadend
|
||||
}
|
||||
|
||||
set onloadend (fn) {
|
||||
webidl.brandCheck(this, FileReader)
|
||||
|
||||
if (this[kEvents].loadend) {
|
||||
this.removeEventListener('loadend', this[kEvents].loadend)
|
||||
}
|
||||
|
||||
if (typeof fn === 'function') {
|
||||
this[kEvents].loadend = fn
|
||||
this.addEventListener('loadend', fn)
|
||||
} else {
|
||||
this[kEvents].loadend = null
|
||||
}
|
||||
}
|
||||
|
||||
get onerror () {
|
||||
webidl.brandCheck(this, FileReader)
|
||||
|
||||
return this[kEvents].error
|
||||
}
|
||||
|
||||
set onerror (fn) {
|
||||
webidl.brandCheck(this, FileReader)
|
||||
|
||||
if (this[kEvents].error) {
|
||||
this.removeEventListener('error', this[kEvents].error)
|
||||
}
|
||||
|
||||
if (typeof fn === 'function') {
|
||||
this[kEvents].error = fn
|
||||
this.addEventListener('error', fn)
|
||||
} else {
|
||||
this[kEvents].error = null
|
||||
}
|
||||
}
|
||||
|
||||
get onloadstart () {
|
||||
webidl.brandCheck(this, FileReader)
|
||||
|
||||
return this[kEvents].loadstart
|
||||
}
|
||||
|
||||
set onloadstart (fn) {
|
||||
webidl.brandCheck(this, FileReader)
|
||||
|
||||
if (this[kEvents].loadstart) {
|
||||
this.removeEventListener('loadstart', this[kEvents].loadstart)
|
||||
}
|
||||
|
||||
if (typeof fn === 'function') {
|
||||
this[kEvents].loadstart = fn
|
||||
this.addEventListener('loadstart', fn)
|
||||
} else {
|
||||
this[kEvents].loadstart = null
|
||||
}
|
||||
}
|
||||
|
||||
get onprogress () {
|
||||
webidl.brandCheck(this, FileReader)
|
||||
|
||||
return this[kEvents].progress
|
||||
}
|
||||
|
||||
set onprogress (fn) {
|
||||
webidl.brandCheck(this, FileReader)
|
||||
|
||||
if (this[kEvents].progress) {
|
||||
this.removeEventListener('progress', this[kEvents].progress)
|
||||
}
|
||||
|
||||
if (typeof fn === 'function') {
|
||||
this[kEvents].progress = fn
|
||||
this.addEventListener('progress', fn)
|
||||
} else {
|
||||
this[kEvents].progress = null
|
||||
}
|
||||
}
|
||||
|
||||
get onload () {
|
||||
webidl.brandCheck(this, FileReader)
|
||||
|
||||
return this[kEvents].load
|
||||
}
|
||||
|
||||
set onload (fn) {
|
||||
webidl.brandCheck(this, FileReader)
|
||||
|
||||
if (this[kEvents].load) {
|
||||
this.removeEventListener('load', this[kEvents].load)
|
||||
}
|
||||
|
||||
if (typeof fn === 'function') {
|
||||
this[kEvents].load = fn
|
||||
this.addEventListener('load', fn)
|
||||
} else {
|
||||
this[kEvents].load = null
|
||||
}
|
||||
}
|
||||
|
||||
get onabort () {
|
||||
webidl.brandCheck(this, FileReader)
|
||||
|
||||
return this[kEvents].abort
|
||||
}
|
||||
|
||||
set onabort (fn) {
|
||||
webidl.brandCheck(this, FileReader)
|
||||
|
||||
if (this[kEvents].abort) {
|
||||
this.removeEventListener('abort', this[kEvents].abort)
|
||||
}
|
||||
|
||||
if (typeof fn === 'function') {
|
||||
this[kEvents].abort = fn
|
||||
this.addEventListener('abort', fn)
|
||||
} else {
|
||||
this[kEvents].abort = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// https://w3c.github.io/FileAPI/#dom-filereader-empty
|
||||
FileReader.EMPTY = FileReader.prototype.EMPTY = 0
|
||||
// https://w3c.github.io/FileAPI/#dom-filereader-loading
|
||||
FileReader.LOADING = FileReader.prototype.LOADING = 1
|
||||
// https://w3c.github.io/FileAPI/#dom-filereader-done
|
||||
FileReader.DONE = FileReader.prototype.DONE = 2
|
||||
|
||||
Object.defineProperties(FileReader.prototype, {
|
||||
EMPTY: staticPropertyDescriptors,
|
||||
LOADING: staticPropertyDescriptors,
|
||||
DONE: staticPropertyDescriptors,
|
||||
readAsArrayBuffer: kEnumerableProperty,
|
||||
readAsBinaryString: kEnumerableProperty,
|
||||
readAsText: kEnumerableProperty,
|
||||
readAsDataURL: kEnumerableProperty,
|
||||
abort: kEnumerableProperty,
|
||||
readyState: kEnumerableProperty,
|
||||
result: kEnumerableProperty,
|
||||
error: kEnumerableProperty,
|
||||
onloadstart: kEnumerableProperty,
|
||||
onprogress: kEnumerableProperty,
|
||||
onload: kEnumerableProperty,
|
||||
onabort: kEnumerableProperty,
|
||||
onerror: kEnumerableProperty,
|
||||
onloadend: kEnumerableProperty,
|
||||
[Symbol.toStringTag]: {
|
||||
value: 'FileReader',
|
||||
writable: false,
|
||||
enumerable: false,
|
||||
configurable: true
|
||||
}
|
||||
})
|
||||
|
||||
Object.defineProperties(FileReader, {
|
||||
EMPTY: staticPropertyDescriptors,
|
||||
LOADING: staticPropertyDescriptors,
|
||||
DONE: staticPropertyDescriptors
|
||||
})
|
||||
|
||||
module.exports = {
|
||||
FileReader
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue