Merge pull request #1267 from hlohaus/any

Add AiChatOnline, ChatgptDemoAi, ChatgptNext Providers
This commit is contained in:
Tekky 2023-11-18 11:24:39 +00:00 committed by GitHub
commit 2fcb3f949b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
202 changed files with 455 additions and 29104 deletions

View File

@ -38,9 +38,9 @@ from .helper import format_prompt
class ChatGpt(AsyncGeneratorProvider):
url = "https://chat-gpt.com"
url = "https://chat-gpt.com"
working = True
supports_gpt_35_turbo = True
working = True
@classmethod
async def create_async_generator(

View File

@ -1,7 +1,6 @@
from __future__ import annotations
import json
import browser_cookie3
from ..typing import AsyncResult, Messages
from ..requests import StreamSession
@ -10,7 +9,7 @@ from .base_provider import AsyncGeneratorProvider, format_prompt, get_cookies
class AItianhu(AsyncGeneratorProvider):
url = "https://www.aitianhu.com"
working = True
working = False
supports_gpt_35_turbo = True
@classmethod

View File

@ -5,7 +5,7 @@ import random
from ..typing import CreateResult, Messages
from .base_provider import BaseProvider
from .helper import WebDriver, format_prompt, get_browser
from .helper import WebDriver, format_prompt, get_browser, get_random_string
from .. import debug
class AItianhuSpace(BaseProvider):
@ -31,8 +31,7 @@ class AItianhuSpace(BaseProvider):
if not model:
model = "gpt-3.5-turbo"
if not domain:
chars = 'abcdefghijklmnopqrstuvwxyz0123456789'
rand = ''.join(random.choice(chars) for _ in range(6))
rand = get_random_string(6)
domain = random.choice(cls._domains)
domain = f"{rand}.{domain}"
if debug.logging:
@ -65,10 +64,11 @@ document.getElementById('sheet').addEventListener('click', () => {{
driver.switch_to.window(window_handle)
break
# Wait for page load
wait.until(EC.visibility_of_element_located((By.CSS_SELECTOR, "textarea.n-input__textarea-el")))
try:
# Add hook in XMLHttpRequest
# Register hook in XMLHttpRequest
script = """
const _http_request_open = XMLHttpRequest.prototype.open;
window._last_message = window._message = "";
@ -90,11 +90,11 @@ XMLHttpRequest.prototype.open = function(method, url) {
"""
driver.execute_script(script)
# Input and submit prompt
# Submit prompt
driver.find_element(By.CSS_SELECTOR, "textarea.n-input__textarea-el").send_keys(prompt)
driver.find_element(By.CSS_SELECTOR, "button.n-button.n-button--primary-type.n-button--medium-type").click()
# Yield response
# Read response
while True:
chunk = driver.execute_script("""
if (window._message && window._message != window._last_message) {

View File

@ -0,0 +1,59 @@
from __future__ import annotations
import json
from aiohttp import ClientSession
from ..typing import AsyncResult, Messages
from .base_provider import AsyncGeneratorProvider
from .helper import get_random_string
class AiChatOnline(AsyncGeneratorProvider):
url = "https://aichatonline.org"
working = True
supports_gpt_35_turbo = True
supports_message_history = False
@classmethod
async def create_async_generator(
cls,
model: str,
messages: Messages,
proxy: str = None,
**kwargs
) -> AsyncResult:
headers = {
"User-Agent": "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/119.0",
"Accept": "text/event-stream",
"Accept-Language": "de,en-US;q=0.7,en;q=0.3",
"Accept-Encoding": "gzip, deflate, br",
"Referer": f"{cls.url}/chatgpt/chat/",
"Content-Type": "application/json",
"Origin": cls.url,
"Alt-Used": "aichatonline.org",
"Connection": "keep-alive",
"Sec-Fetch-Dest": "empty",
"Sec-Fetch-Mode": "cors",
"Sec-Fetch-Site": "same-origin",
"TE": "trailers"
}
async with ClientSession(headers=headers) as session:
data = {
"botId": "default",
"customId": None,
"session": get_random_string(16),
"chatId": get_random_string(),
"contextId": 7,
"messages": messages,
"newMessage": messages[-1]["content"],
"newImageId": None,
"stream": True
}
async with session.post(f"{cls.url}/chatgpt/wp-json/mwai-ui/v1/chats/submit", json=data, proxy=proxy) as response:
response.raise_for_status()
async for chunk in response.content:
if chunk.startswith(b"data: "):
data = json.loads(chunk[6:])
if data["type"] == "live":
yield data["data"]
elif data["type"] == "end":
break

View File

@ -0,0 +1,55 @@
from __future__ import annotations
import json
from aiohttp import ClientSession
from ..typing import AsyncResult, Messages
from .base_provider import AsyncGeneratorProvider
from .helper import get_random_string
class ChatgptDemoAi(AsyncGeneratorProvider):
url = "https://chat.chatgptdemo.ai"
working = True
supports_gpt_35_turbo = True
supports_message_history = True
@classmethod
async def create_async_generator(
cls,
model: str,
messages: Messages,
proxy: str = None,
**kwargs
) -> AsyncResult:
headers = {
"User-Agent": "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/119.0",
"Accept": "*/*",
"Accept-Language": "de,en-US;q=0.7,en;q=0.3",
"Accept-Encoding": "gzip, deflate, br",
"Referer": f"{cls.url}/",
"Content-Type": "application/json",
"Origin": cls.url,
"Connection": "keep-alive",
"Sec-Fetch-Dest": "empty",
"Sec-Fetch-Mode": "cors",
"Sec-Fetch-Site": "same-origin",
"TE": "trailers"
}
async with ClientSession(headers=headers) as session:
data = {
"botId": "default",
"customId": "8824fe9bdb323a5d585a3223aaa0cb6e",
"session": "N/A",
"chatId": get_random_string(12),
"contextId": 2,
"messages": messages,
"newMessage": messages[-1]["content"],
"stream": True
}
async with session.post(f"{cls.url}/wp-json/mwai-ui/v1/chats/submit", json=data, proxy=proxy) as response:
response.raise_for_status()
async for chunk in response.content:
if chunk.startswith(b"data: "):
data = json.loads(chunk[6:])
if data["type"] == "live":
yield data["data"]

View File

@ -1,9 +1,6 @@
#cloudflare block
from __future__ import annotations
import re
from aiohttp import ClientSession
from ..requests import StreamSession
from ..typing import Messages

View File

@ -0,0 +1,61 @@
from __future__ import annotations
import json
from aiohttp import ClientSession
from ..typing import AsyncResult, Messages
from .base_provider import AsyncGeneratorProvider
from .helper import format_prompt
class ChatgptNext(AsyncGeneratorProvider):
url = "https://www.chatgpt-free.cc"
working = True
supports_gpt_35_turbo = True
@classmethod
async def create_async_generator(
cls,
model: str,
messages: Messages,
proxy: str = None,
**kwargs
) -> AsyncResult:
if not model:
model = "gpt-3.5-turbo"
headers = {
"User-Agent": "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/119.0",
"Accept": "text/event-stream",
"Accept-Language": "de,en-US;q=0.7,en;q=0.3",
"Accept-Encoding": "gzip, deflate, br",
"Content-Type": "application/json",
"Referer": "https://chat.fstha.com/",
"x-requested-with": "XMLHttpRequest",
"Origin": "https://chat.fstha.com",
"Sec-Fetch-Dest": "empty",
"Sec-Fetch-Mode": "cors",
"Sec-Fetch-Site": "same-origin",
"Authorization": "Bearer ak-chatgpt-nice",
"Connection": "keep-alive",
"Alt-Used": "chat.fstha.com",
}
async with ClientSession(headers=headers) as session:
data = {
"messages": messages,
"stream": True,
"model": model,
"temperature": 0.5,
"presence_penalty": 0,
"frequency_penalty": 0,
"top_p": 1,
**kwargs
}
async with session.post(f"https://chat.fstha.com/api/openai/v1/chat/completions", json=data, proxy=proxy) as response:
response.raise_for_status()
async for chunk in response.content:
if chunk.startswith(b"data: [DONE]"):
break
if chunk.startswith(b"data: "):
content = json.loads(chunk[6:])["choices"][0]["delta"].get("content")
if content:
yield content

View File

@ -79,7 +79,6 @@ class ChatgptX(AsyncGeneratorProvider):
data = {
"user_id": user_id,
"chats_id": chat_id,
"prompt": format_prompt(messages),
"current_model": "gpt3",
"conversions_id": chat["conversions_id"],
"ass_conversions_id": chat["ass_conversions_id"],

View File

@ -1,11 +1,11 @@
from __future__ import annotations
import uuid, time, random, string, json
import uuid, time, random, json
from aiohttp import ClientSession
from ..typing import AsyncResult, Messages
from .base_provider import AsyncGeneratorProvider
from .helper import format_prompt
from .helper import format_prompt, get_random_string
class FakeGpt(AsyncGeneratorProvider):
@ -39,7 +39,7 @@ class FakeGpt(AsyncGeneratorProvider):
token_ids = [t["token_id"] for t in list if t["count"] == 0]
data = {
"token_key": random.choice(token_ids),
"session_password": random_string()
"session_password": get_random_string()
}
async with session.post(f"{cls.url}/auth/login", data=data, proxy=proxy) as response:
response.raise_for_status()
@ -88,7 +88,4 @@ class FakeGpt(AsyncGeneratorProvider):
except:
continue
if not last_message:
raise RuntimeError("No valid response")
def random_string(length: int = 10):
return ''.join(random.choice(string.ascii_lowercase + string.digits) for _ in range(length))
raise RuntimeError("No valid response")

View File

@ -1,5 +1,3 @@
# cloudflare block
from __future__ import annotations
from ..requests import StreamSession

View File

@ -1,6 +1,9 @@
from __future__ import annotations
import secrets, json
import secrets
import json
from aiohttp import ClientSession
from ..typing import AsyncResult, Messages
from .base_provider import AsyncGeneratorProvider
from .helper import format_prompt

View File

@ -1,10 +1,10 @@
from __future__ import annotations
import secrets
from aiohttp import ClientSession
from ..typing import AsyncResult, Messages
from .base_provider import AsyncGeneratorProvider
from .helper import get_random_hex
class SearchTypes():
quick = "quick"
@ -55,7 +55,7 @@ class Hashnode(AsyncGeneratorProvider):
response.raise_for_status()
cls._sources = (await response.json())["result"]
data = {
"chatId": secrets.token_hex(16).zfill(32),
"chatId": get_random_hex(),
"history": messages,
"prompt": prompt,
"searchType": search_type,

View File

@ -1,12 +1,11 @@
from __future__ import annotations
import random
import string
import json
from aiohttp import ClientSession
from ..typing import AsyncResult, Messages
from .base_provider import AsyncGeneratorProvider
from .helper import get_random_string
class Koala(AsyncGeneratorProvider):
url = "https://koala.sh"
@ -32,7 +31,7 @@ class Koala(AsyncGeneratorProvider):
"Referer": f"{cls.url}/chat",
"Content-Type": "application/json",
"Flag-Real-Time-Data": "false",
"Visitor-ID": random_string(),
"Visitor-ID": get_random_string(20),
"Origin": cls.url,
"Alt-Used": "koala.sh",
"Connection": "keep-alive",
@ -62,10 +61,4 @@ class Koala(AsyncGeneratorProvider):
response.raise_for_status()
async for chunk in response.content:
if chunk.startswith(b"data: "):
yield json.loads(chunk[6:])
def random_string(length: int = 20):
return ''.join(random.choice(
string.ascii_letters + string.digits
) for _ in range(length))
yield json.loads(chunk[6:])

View File

@ -14,6 +14,12 @@ models = {
"maxLength": 24000,
"tokenLimit": 8000,
},
"gpt-4-0613": {
"id": "gpt-4-0613",
"name": "GPT-4",
"maxLength": 32000,
"tokenLimit": 8000,
},
"gpt-3.5-turbo": {
"id": "gpt-3.5-turbo",
"name": "GPT-3.5",

View File

@ -21,10 +21,9 @@ class MyShell(BaseProvider):
proxy: str = None,
timeout: int = 120,
browser: WebDriver = None,
headless: bool = True,
**kwargs
) -> CreateResult:
driver = browser if browser else get_browser("", headless, proxy)
driver = browser if browser else get_browser("", False, proxy)
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
@ -80,6 +79,8 @@ return content;
yield chunk
elif chunk != "":
break
else:
time.sleep(0.1)
finally:
if not browser:
driver.close()

View File

@ -1,17 +1,17 @@
from __future__ import annotations
import random, string, json
import json
from aiohttp import ClientSession
from ..typing import AsyncResult, Messages
from .base_provider import AsyncGeneratorProvider
from .helper import get_random_string
class NoowAi(AsyncGeneratorProvider):
url = "https://noowai.com"
supports_message_history = True
supports_gpt_35_turbo = True
working = True
working = False
@classmethod
async def create_async_generator(
@ -43,7 +43,7 @@ class NoowAi(AsyncGeneratorProvider):
"botId": "default",
"customId": "d49bc3670c3d858458576d75c8ea0f5d",
"session": "N/A",
"chatId": random_string(),
"chatId": get_random_string(),
"contextId": 25,
"messages": messages,
"newMessage": messages[-1]["content"],
@ -63,7 +63,4 @@ class NoowAi(AsyncGeneratorProvider):
elif line["type"] == "end":
break
elif line["type"] == "error":
raise RuntimeError(line["data"])
def random_string(length: int = 10):
return ''.join(random.choice(string.ascii_lowercase + string.digits) for _ in range(length))
raise RuntimeError(line["data"])

58
g4f/Provider/OnlineGpt.py Normal file
View File

@ -0,0 +1,58 @@
from __future__ import annotations
import json
from aiohttp import ClientSession
from ..typing import AsyncResult, Messages
from .base_provider import AsyncGeneratorProvider
from .helper import get_random_string
class OnlineGpt(AsyncGeneratorProvider):
url = "https://onlinegpt.org"
working = True
supports_gpt_35_turbo = True
supports_message_history = False
@classmethod
async def create_async_generator(
cls,
model: str,
messages: Messages,
proxy: str = None,
**kwargs
) -> AsyncResult:
headers = {
"User-Agent": "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/119.0",
"Accept": "text/event-stream",
"Accept-Language": "de,en-US;q=0.7,en;q=0.3",
"Accept-Encoding": "gzip, deflate, br",
"Referer": f"{cls.url}/chat/",
"Content-Type": "application/json",
"Origin": cls.url,
"Alt-Used": "onlinegpt.org",
"Connection": "keep-alive",
"Sec-Fetch-Dest": "empty",
"Sec-Fetch-Mode": "cors",
"Sec-Fetch-Site": "same-origin",
"TE": "trailers"
}
async with ClientSession(headers=headers) as session:
data = {
"botId": "default",
"customId": None,
"session": get_random_string(12),
"chatId": get_random_string(),
"contextId": 9,
"messages": messages,
"newMessage": messages[-1]["content"],
"newImageId": None,
"stream": True
}
async with session.post(f"{cls.url}/chatgpt/wp-json/mwai-ui/v1/chats/submit", json=data, proxy=proxy) as response:
response.raise_for_status()
async for chunk in response.content:
if chunk.startswith(b"data: "):
data = json.loads(chunk[6:])
if data["type"] == "live":
yield data["data"]

View File

@ -5,7 +5,7 @@ from aiohttp import ClientSession
from ..typing import Messages, AsyncResult
from .base_provider import AsyncGeneratorProvider
from .helper import get_random_string
class Opchatgpts(AsyncGeneratorProvider):
url = "https://opchatgpts.net"
@ -36,7 +36,7 @@ class Opchatgpts(AsyncGeneratorProvider):
) as session:
data = {
"botId": "default",
"chatId": random_string(),
"chatId": get_random_string(),
"contextId": 28,
"customId": None,
"messages": messages,
@ -68,7 +68,4 @@ class Opchatgpts(AsyncGeneratorProvider):
("proxy", "str"),
]
param = ", ".join([": ".join(p) for p in params])
return f"g4f.provider.{cls.__name__} supports: ({param})"
def random_string(length: int = 10):
return ''.join(random.choice(string.ascii_lowercase + string.digits) for _ in range(length))
return f"g4f.provider.{cls.__name__} supports: ({param})"

View File

@ -23,49 +23,31 @@ class Phind(BaseProvider):
timeout: int = 120,
browser: WebDriver = None,
creative_mode: bool = None,
headless: bool = True,
**kwargs
) -> CreateResult:
driver = browser if browser else get_browser("", headless, proxy)
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
prompt = quote(format_prompt(messages))
driver.get(f"{cls.url}/search?q={prompt}&source=searchbox")
# Need to change settinge
if model.startswith("gpt-4") or creative_mode:
wait = WebDriverWait(driver, timeout)
# Open settings dropdown
wait.until(EC.visibility_of_element_located((By.CSS_SELECTOR, "button.text-dark.dropdown-toggle")))
driver.find_element(By.CSS_SELECTOR, "button.text-dark.dropdown-toggle").click()
# Wait for dropdown toggle
wait.until(EC.visibility_of_element_located((By.XPATH, "//button[text()='GPT-4']")))
# Enable GPT-4
if model.startswith("gpt-4"):
driver.find_element(By.XPATH, "//button[text()='GPT-4']").click()
# Enable creative mode
if creative_mode or creative_mode == None:
driver.find_element(By.ID, "Creative Mode").click()
# Submit changes
driver.find_element(By.CSS_SELECTOR, ".search-bar-input-group button[type='submit']").click()
# Wait for page reload
wait.until(EC.visibility_of_element_located((By.CSS_SELECTOR, ".search-container")))
try:
# Add fetch hook
script = """
driver = browser if browser else get_browser("", False, proxy)
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
prompt = quote(format_prompt(messages))
driver.get(f"{cls.url}/search?q={prompt}&source=searchbox")
# Register fetch hook
driver.execute_script("""
window._fetch = window.fetch;
window.fetch = (url, options) => {
// Call parent fetch method
const result = window._fetch(url, options);
if (url != "/api/infer/answer") return result;
if (url != "/api/infer/answer") {
return result;
}
// Load response reader
result.then((response) => {
if (!response.body.locked) {
window.reader = response.body.getReader();
window._reader = response.body.getReader();
}
});
// Return dummy response
@ -73,12 +55,31 @@ window.fetch = (url, options) => {
resolve(new Response(new ReadableStream()))
});
}
"""
# Read response from reader
driver.execute_script(script)
script = """
if(window.reader) {
chunk = await window.reader.read();
""")
# Need to change settings
if model.startswith("gpt-4") or creative_mode:
wait = WebDriverWait(driver, timeout)
# Open settings dropdown
wait.until(EC.visibility_of_element_located((By.CSS_SELECTOR, "button.text-dark.dropdown-toggle")))
driver.find_element(By.CSS_SELECTOR, "button.text-dark.dropdown-toggle").click()
# Wait for dropdown toggle
wait.until(EC.visibility_of_element_located((By.XPATH, "//button[text()='GPT-4']")))
# Enable GPT-4
if model.startswith("gpt-4"):
driver.find_element(By.XPATH, "//button[text()='GPT-4']").click()
# Enable creative mode
if creative_mode or creative_mode == None:
driver.find_element(By.ID, "Creative Mode").click()
# Submit changes
driver.find_element(By.CSS_SELECTOR, ".search-bar-input-group button[type='submit']").click()
# Wait for page reload
wait.until(EC.visibility_of_element_located((By.CSS_SELECTOR, ".search-container")))
while True:
chunk = driver.execute_script("""
if(window._reader) {
chunk = await window._reader.read();
if (chunk['done']) return null;
text = (new TextDecoder()).decode(chunk['value']);
content = '';
@ -95,9 +96,7 @@ if(window.reader) {
} else {
return ''
}
"""
while True:
chunk = driver.execute_script(script)
""")
if chunk:
yield chunk
elif chunk != "":

94
g4f/Provider/TalkAi.py Normal file
View File

@ -0,0 +1,94 @@
from __future__ import annotations
import time, json, time
from ..typing import CreateResult, Messages
from .base_provider import BaseProvider
from .helper import WebDriver, get_browser
class TalkAi(BaseProvider):
url = "https://talkai.info"
working = True
supports_gpt_35_turbo = True
supports_stream = True
@classmethod
def create_completion(
cls,
model: str,
messages: Messages,
stream: bool,
proxy: str = None,
browser: WebDriver = None,
**kwargs
) -> CreateResult:
driver = browser if browser else get_browser("", False, proxy)
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
try:
driver.get(f"{cls.url}/chat/")
# Wait for page load
WebDriverWait(driver, 240).until(
EC.presence_of_element_located((By.CSS_SELECTOR, "body.chat-page"))
)
data = {
"type": "chat",
"message": messages[-1]["content"],
"messagesHistory": [{
"from": "you" if message["role"] == "user" else "chatGPT",
"content": message["content"]
} for message in messages],
"model": model if model else "gpt-3.5-turbo",
"max_tokens": 256,
"temperature": 1,
"top_p": 1,
"presence_penalty": 0,
"frequency_penalty": 0,
**kwargs
}
script = """
const response = await fetch("/chat/send2/", {
"headers": {
"Accept": "application/json",
"Content-Type": "application/json",
},
"body": {body},
"method": "POST"
});
window._reader = response.body.pipeThrough(new TextDecoderStream()).getReader();
"""
driver.execute_script(
script.replace("{body}", json.dumps(json.dumps(data)))
)
# Read response
while True:
chunk = driver.execute_script("""
chunk = await window._reader.read();
if (chunk["done"]) {
return null;
}
content = "";
lines = chunk["value"].split("\\n")
lines.forEach((line, index) => {
if (line.startsWith('data: ')) {
content += line.substring('data: '.length);
}
});
return content;
""")
if chunk:
yield chunk.replace("\\n", "\n")
elif chunk != "":
break
else:
time.sleep(0.1)
finally:
if not browser:
driver.close()
time.sleep(0.1)
driver.quit()

View File

@ -2,6 +2,7 @@ from __future__ import annotations
from .AiAsk import AiAsk
from .Aichat import Aichat
from .AiChatOnline import AiChatOnline
from .AItianhu import AItianhu
from .AItianhuSpace import AItianhuSpace
from .Berlin import Berlin
@ -12,8 +13,10 @@ from .ChatForAi import ChatForAi
from .Chatgpt4Online import Chatgpt4Online
from .ChatgptAi import ChatgptAi
from .ChatgptDemo import ChatgptDemo
from .ChatgptDemoAi import ChatgptDemoAi
from .ChatgptFree import ChatgptFree
from .ChatgptLogin import ChatgptLogin
from .ChatgptNext import ChatgptNext
from .ChatgptX import ChatgptX
from .DeepInfra import DeepInfra
from .FakeGpt import FakeGpt
@ -29,9 +32,11 @@ from .Liaobots import Liaobots
from .Llama2 import Llama2
from .MyShell import MyShell
from .NoowAi import NoowAi
from .OnlineGpt import OnlineGpt
from .Opchatgpts import Opchatgpts
from .PerplexityAi import PerplexityAi
from .Phind import Phind
from .TalkAi import TalkAi
from .Vercel import Vercel
from .Ylokh import Ylokh
from .You import You

View File

@ -3,6 +3,9 @@ from __future__ import annotations
import sys
import asyncio
import webbrowser
import random
import string
import secrets
from os import path
from asyncio import AbstractEventLoop
from platformdirs import user_config_dir
@ -120,12 +123,10 @@ def get_cookies(domain_name=''):
def format_prompt(messages: Messages, add_special_tokens=False) -> str:
if not add_special_tokens and len(messages) <= 1:
return messages[0]["content"]
formatted = "\n".join(
[
f'{message["role"].capitalize()}: {message["content"]}'
for message in messages
]
)
formatted = "\n".join([
f'{message["role"].capitalize()}: {message["content"]}'
for message in messages
])
return f"{formatted}\nAssistant:"
@ -137,10 +138,19 @@ def get_browser(
) -> Chrome:
if user_data_dir == None:
user_data_dir = user_config_dir("g4f")
if proxy:
if not options:
options = ChromeOptions()
options.add_argument(f'--proxy-server={proxy}')
return Chrome(options=options, user_data_dir=user_data_dir, headless=headless)
return Chrome(user_data_dir=user_data_dir, options=options, headless=headless)
def get_random_string(length: int = 10) -> str:
return ''.join(
random.choice(string.ascii_lowercase + string.digits)
for _ in range(length)
)
def get_random_hex() -> str:
return secrets.token_hex(16).zfill(32)

View File

@ -3,37 +3,10 @@
"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"
}
}
}
}

View File

@ -1,19 +0,0 @@
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.

View File

@ -1,271 +0,0 @@
# 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.

View File

@ -1,19 +0,0 @@
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.

View File

@ -1,207 +0,0 @@
'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

View File

@ -1,100 +0,0 @@
'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

View File

@ -1,13 +0,0 @@
'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

View File

@ -1,164 +0,0 @@
// 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;
}
}

View File

@ -1,228 +0,0 @@
'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

View File

@ -1,196 +0,0 @@
// 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;
}

View File

@ -1,85 +0,0 @@
'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

View File

@ -1,306 +0,0 @@
'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

View File

@ -1,190 +0,0 @@
'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

View File

@ -1,54 +0,0 @@
'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

View File

@ -1,14 +0,0 @@
'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)
}

View File

@ -1,26 +0,0 @@
'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

View File

@ -1,16 +0,0 @@
'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]
}

View File

@ -1,87 +0,0 @@
'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

View File

@ -1,89 +0,0 @@
{
"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"
]
}

View File

@ -1,7 +1,13 @@
# crypto-js [![Build Status](https://travis-ci.org/brix/crypto-js.svg?branch=develop)](https://travis-ci.org/brix/crypto-js)
# crypto-js
JavaScript library of crypto standards.
## Discontinued
Active development of CryptoJS has been discontinued. This library is no longer maintained.
Nowadays, NodeJS and modern browsers have a native `Crypto` module. The latest version of CryptoJS already uses the native Crypto module for random number generation, since `Math.random()` is not crypto-safe. Further development of CryptoJS would result in it only being a wrapper of native Crypto. Therefore, development and maintenance has been discontinued, it is time to go for the native `crypto` module.
## Node.js (Install)
Requirements:
@ -212,6 +218,14 @@ console.log(decryptedData); // [{id: 1}, {id: 2}]
## Release notes
### 4.2.0
Change default hash algorithm and iteration's for PBKDF2 to prevent weak security by using the default configuration.
Custom KDF Hasher
Blowfish support
### 4.1.1
Fix module order in bundled release.

View File

@ -1,43 +0,0 @@
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

View File

@ -1,115 +0,0 @@
# 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)

View File

@ -1,28 +0,0 @@
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>;

View File

@ -1,45 +0,0 @@
"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;

View File

@ -1,82 +0,0 @@
/// <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 {};

View File

@ -1,194 +0,0 @@
"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;

View File

@ -1,7 +0,0 @@
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;

View File

@ -1,51 +0,0 @@
"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,
};

View File

@ -1,63 +0,0 @@
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;

View File

@ -1,304 +0,0 @@
"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,
};

View File

@ -1,7 +0,0 @@
/// <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;

View File

@ -1,25 +0,0 @@
"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;

View File

@ -1,2 +0,0 @@
export * from "./api";
export * from "./session";

View File

@ -1,18 +0,0 @@
"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);

View File

@ -1,2 +0,0 @@
declare var x64hash128: (t: any, r: any) => string;
export default x64hash128;

View File

@ -1,186 +0,0 @@
"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;

View File

@ -1,36 +0,0 @@
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;
}

View File

@ -1,77 +0,0 @@
"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;

View File

@ -1,190 +0,0 @@
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;

View File

@ -1,172 +0,0 @@
"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
};

View File

@ -1,27 +0,0 @@
{
"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"
}

View File

@ -1,85 +0,0 @@
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());
}

View File

@ -1,296 +0,0 @@
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;
}
}

View File

@ -1,78 +0,0 @@
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,
};

View File

@ -1,321 +0,0 @@
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,
};

View File

@ -1,28 +0,0 @@
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;

View File

@ -1,2 +0,0 @@
export * from "./api";
export * from "./session";

View File

@ -1,205 +0,0 @@
// 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;

View File

@ -1,125 +0,0 @@
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
)}`;
}
}

View File

@ -1,197 +0,0 @@
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
};

View File

@ -1,21 +0,0 @@
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.

View File

@ -1,443 +0,0 @@
# 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

View File

@ -1,15 +0,0 @@
'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

View File

@ -1,3 +0,0 @@
export * from './types/index'
import Undici from './types/index'
export default Undici

View File

@ -1,165 +0,0 @@
'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

View File

@ -1,148 +0,0 @@
'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

View File

@ -1,54 +0,0 @@
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
}

View File

@ -1,104 +0,0 @@
'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

View File

@ -1,249 +0,0 @@
'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

View File

@ -1,179 +0,0 @@
'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

View File

@ -1,220 +0,0 @@
'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

View File

@ -1,105 +0,0 @@
'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

View File

@ -1,7 +0,0 @@
'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')

View File

@ -1,307 +0,0 @@
// 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
}

View File

@ -1,46 +0,0 @@
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 }

View File

@ -1,190 +0,0 @@
'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

View File

@ -1,838 +0,0 @@
'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
}

View File

@ -1,144 +0,0 @@
'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
}

View File

@ -1,5 +0,0 @@
'use strict'
module.exports = {
kConstruct: Symbol('constructable')
}

View File

@ -1,49 +0,0 @@
'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

View File

@ -1,48 +0,0 @@
'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
}
}

View File

@ -1,12 +0,0 @@
'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
}

View File

@ -1,184 +0,0 @@
'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
}

View File

@ -1,317 +0,0 @@
'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
}

View File

@ -1,291 +0,0 @@
'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
}

View File

@ -1,189 +0,0 @@
'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

View File

@ -1,216 +0,0 @@
'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
}

Some files were not shown because too many files have changed in this diff Show More