mirror of
https://github.com/MercuryWorkshop/bare-mux.git
synced 2025-05-14 14:50:03 -04:00
gyghhhhhhhh
This commit is contained in:
commit
86abdca21e
23 changed files with 1464 additions and 0 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
node_modules
|
61
dist/BareClient.d.ts
vendored
Normal file
61
dist/BareClient.d.ts
vendored
Normal file
|
@ -0,0 +1,61 @@
|
|||
import { BareHeaders } from './BareTypes';
|
||||
export type WebSocketImpl = {
|
||||
new (...args: ConstructorParameters<typeof WebSocket>): WebSocket;
|
||||
};
|
||||
export declare namespace BareWebSocket {
|
||||
type GetReadyStateCallback = () => number;
|
||||
type GetSendErrorCallback = () => Error | undefined;
|
||||
type GetProtocolCallback = () => string;
|
||||
type HeadersType = BareHeaders | Headers | undefined;
|
||||
type HeadersProvider = BareHeaders | (() => BareHeaders | Promise<BareHeaders>);
|
||||
interface Options {
|
||||
/**
|
||||
* A provider of request headers to pass to the remote.
|
||||
* Usually one of `User-Agent`, `Origin`, and `Cookie`
|
||||
* Can be just the headers object or an synchronous/asynchronous function that returns the headers object
|
||||
*/
|
||||
headers?: BareWebSocket.HeadersProvider;
|
||||
/**
|
||||
* A hook executed by this function with helper arguments for hooking the readyState property. If a hook isn't provided, bare-client will hook the property on the instance. Hooking it on an instance basis is good for small projects, but ideally the class should be hooked by the user of bare-client.
|
||||
*/
|
||||
readyStateHook?: ((socket: WebSocket, getReadyState: BareWebSocket.GetReadyStateCallback) => void) | undefined;
|
||||
/**
|
||||
* A hook executed by this function with helper arguments for determining if the send function should throw an error. If a hook isn't provided, bare-client will hook the function on the instance.
|
||||
*/
|
||||
sendErrorHook?: ((socket: WebSocket, getSendError: BareWebSocket.GetSendErrorCallback) => void) | undefined;
|
||||
/**
|
||||
* A hook executed by this function with the URL. If a hook isn't provided, bare-client will hook the URL.
|
||||
*/
|
||||
urlHook?: ((socket: WebSocket, url: URL) => void) | undefined;
|
||||
/**
|
||||
* A hook executed by this function with a helper for getting the current fake protocol. If a hook isn't provided, bare-client will hook the protocol.
|
||||
*/
|
||||
protocolHook?: ((socket: WebSocket, getProtocol: BareWebSocket.GetProtocolCallback) => void) | undefined;
|
||||
/**
|
||||
* A callback executed by this function with an array of cookies. This is called once the metadata from the server is received.
|
||||
*/
|
||||
setCookiesCallback?: ((setCookies: string[]) => void) | undefined;
|
||||
webSocketImpl?: WebSocket;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* A Response with additional properties.
|
||||
*/
|
||||
export interface BareResponse extends Response {
|
||||
rawResponse: Response;
|
||||
rawHeaders: BareHeaders;
|
||||
}
|
||||
/**
|
||||
* A BareResponse with additional properties.
|
||||
*/
|
||||
export interface BareResponseFetch extends BareResponse {
|
||||
finalURL: string;
|
||||
}
|
||||
export declare class BareClient {
|
||||
/**
|
||||
* Create a BareClient. Calls to fetch and connect will wait for an implementation to be ready.
|
||||
*/
|
||||
constructor();
|
||||
createWebSocket(remote: string | URL, protocols: string | string[] | undefined, options: BareWebSocket.Options, origin: string): WebSocket;
|
||||
fetch(url: string | URL, init?: RequestInit): Promise<BareResponseFetch>;
|
||||
}
|
22
dist/BareTypes.d.ts
vendored
Normal file
22
dist/BareTypes.d.ts
vendored
Normal file
|
@ -0,0 +1,22 @@
|
|||
export type BareHeaders = Record<string, string | string[]>;
|
||||
export type BareMeta = {};
|
||||
export type TransferrableResponse = {
|
||||
body: ReadableStream | ArrayBuffer | Blob | string;
|
||||
headers: BareHeaders;
|
||||
status: number;
|
||||
statusText: string;
|
||||
};
|
||||
export interface BareTransport {
|
||||
init: () => Promise<void>;
|
||||
ready: boolean;
|
||||
connect: (url: URL, origin: string, protocols: string[], onopen: (protocol: string) => void, onmessage: (data: Blob | ArrayBuffer | string) => void, onclose: (code: number, reason: string) => void, onerror: (error: string) => void) => (data: Blob | ArrayBuffer | string) => void;
|
||||
request: (remote: URL, method: string, body: BodyInit | null, headers: BareHeaders, signal: AbortSignal | undefined) => Promise<TransferrableResponse>;
|
||||
meta: () => BareMeta;
|
||||
}
|
||||
export interface BareWebSocketMeta {
|
||||
protocol: string;
|
||||
setCookies: string[];
|
||||
}
|
||||
export type BareHTTPProtocol = 'blob:' | 'http:' | 'https:' | string;
|
||||
export type BareWSProtocol = 'ws:' | 'wss:' | string;
|
||||
export declare const maxRedirects = 20;
|
0
dist/RemoteClient.d.ts
vendored
Normal file
0
dist/RemoteClient.d.ts
vendored
Normal file
26
dist/Switcher.d.ts
vendored
Normal file
26
dist/Switcher.d.ts
vendored
Normal file
|
@ -0,0 +1,26 @@
|
|||
import { BareTransport } from "./BareTypes";
|
||||
declare global {
|
||||
interface ServiceWorkerGlobalScope {
|
||||
gSwitcher: Switcher;
|
||||
BCC_VERSION: string;
|
||||
BCC_DEBUG: boolean;
|
||||
}
|
||||
interface WorkerGlobalScope {
|
||||
gSwitcher: Switcher;
|
||||
BCC_VERSION: string;
|
||||
BCC_DEBUG: boolean;
|
||||
}
|
||||
interface Window {
|
||||
gSwitcher: Switcher;
|
||||
BCC_VERSION: string;
|
||||
BCC_DEBUG: boolean;
|
||||
}
|
||||
}
|
||||
declare class Switcher {
|
||||
transports: Record<string, BareTransport>;
|
||||
active: BareTransport | null;
|
||||
}
|
||||
export declare function findSwitcher(): Switcher;
|
||||
export declare function AddTransport(name: string, client: BareTransport): void;
|
||||
export declare function SetTransport(name: string): void;
|
||||
export {};
|
297
dist/bare.cjs
vendored
Normal file
297
dist/bare.cjs
vendored
Normal file
|
@ -0,0 +1,297 @@
|
|||
(function (global, factory) {
|
||||
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
|
||||
typeof define === 'function' && define.amd ? define(['exports'], factory) :
|
||||
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.bare = {}));
|
||||
})(this, (function (exports) { 'use strict';
|
||||
|
||||
const maxRedirects = 20;
|
||||
|
||||
// The user likely has overwritten all networking functions after importing bare-client
|
||||
// It is our responsibility to make sure components of Bare-Client are using native networking functions
|
||||
const fetch = globalThis.fetch;
|
||||
const WebSocket = globalThis.WebSocket;
|
||||
const Request = globalThis.Request;
|
||||
const Response = globalThis.Response;
|
||||
const WebSocketFields = {
|
||||
prototype: {
|
||||
send: WebSocket.prototype.send,
|
||||
},
|
||||
CLOSED: WebSocket.CLOSED,
|
||||
CLOSING: WebSocket.CLOSING,
|
||||
CONNECTING: WebSocket.CONNECTING,
|
||||
OPEN: WebSocket.OPEN,
|
||||
};
|
||||
|
||||
self.BCC_VERSION = "2.1.3";
|
||||
console.warn("BCC_VERSION: " + self.BCC_VERSION);
|
||||
if (!("gTransports" in globalThis)) {
|
||||
globalThis.gTransports = {};
|
||||
}
|
||||
class Switcher {
|
||||
transports = {};
|
||||
active = null;
|
||||
}
|
||||
function findSwitcher() {
|
||||
if (globalThis.gSwitcher)
|
||||
return globalThis.gSwitcher;
|
||||
for (let i = 0; i < 20; i++) {
|
||||
try {
|
||||
parent = parent.parent;
|
||||
if (parent && parent["gSwitcher"]) {
|
||||
console.warn("found implementation on parent");
|
||||
globalThis.gSwitcher = parent["gSwitcher"];
|
||||
return parent["gSwitcher"];
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
globalThis.gSwitcher = new Switcher;
|
||||
return globalThis.gSwitcher;
|
||||
}
|
||||
}
|
||||
throw "unreachable";
|
||||
}
|
||||
|
||||
/*
|
||||
* WebSocket helpers
|
||||
*/
|
||||
const validChars = "!#$%&'*+-.0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ^_`abcdefghijklmnopqrstuvwxyz|~";
|
||||
function validProtocol(protocol) {
|
||||
for (let i = 0; i < protocol.length; i++) {
|
||||
const char = protocol[i];
|
||||
if (!validChars.includes(char)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// get the unhooked value
|
||||
const getRealReadyState = Object.getOwnPropertyDescriptor(WebSocket.prototype, 'readyState').get;
|
||||
const wsProtocols = ['ws:', 'wss:'];
|
||||
const statusEmpty = [101, 204, 205, 304];
|
||||
const statusRedirect = [301, 302, 303, 307, 308];
|
||||
class BareClient {
|
||||
/**
|
||||
* Create a BareClient. Calls to fetch and connect will wait for an implementation to be ready.
|
||||
*/
|
||||
constructor() { }
|
||||
createWebSocket(remote, protocols = [], options, origin) {
|
||||
let switcher = findSwitcher();
|
||||
let client = switcher.active;
|
||||
if (!client)
|
||||
throw "invalid switcher";
|
||||
if (!client.ready)
|
||||
throw new TypeError('You need to wait for the client to finish fetching the manifest before creating any WebSockets. Try caching the manifest data before making this request.');
|
||||
try {
|
||||
remote = new URL(remote);
|
||||
}
|
||||
catch (err) {
|
||||
throw new DOMException(`Faiiled to construct 'WebSocket': The URL '${remote}' is invalid.`);
|
||||
}
|
||||
if (!wsProtocols.includes(remote.protocol))
|
||||
throw new DOMException(`Failed to construct 'WebSocket': The URL's scheme must be either 'ws' or 'wss'. '${remote.protocol}' is not allowed.`);
|
||||
if (!Array.isArray(protocols))
|
||||
protocols = [protocols];
|
||||
protocols = protocols.map(String);
|
||||
for (const proto of protocols)
|
||||
if (!validProtocol(proto))
|
||||
throw new DOMException(`Failed to construct 'WebSocket': The subprotocol '${proto}' is invalid.`);
|
||||
let wsImpl = (options.webSocketImpl || WebSocket);
|
||||
const socket = new wsImpl("wss:null", protocols);
|
||||
let fakeProtocol = '';
|
||||
let fakeReadyState = WebSocketFields.CONNECTING;
|
||||
let initialErrorHappened = false;
|
||||
socket.addEventListener("error", (e) => {
|
||||
if (!initialErrorHappened) {
|
||||
fakeReadyState = WebSocket.CONNECTING;
|
||||
e.stopImmediatePropagation();
|
||||
initialErrorHappened = true;
|
||||
}
|
||||
});
|
||||
const sendData = client.connect(remote, origin, protocols, (protocol) => {
|
||||
fakeReadyState = WebSocketFields.OPEN;
|
||||
fakeProtocol = protocol;
|
||||
socket.dispatchEvent(new Event("open"));
|
||||
}, (payload) => {
|
||||
if (typeof payload === "string") {
|
||||
socket.dispatchEvent(new MessageEvent("message", { data: payload }));
|
||||
}
|
||||
else if (payload instanceof ArrayBuffer) {
|
||||
Object.setPrototypeOf(payload, ArrayBuffer);
|
||||
socket.dispatchEvent(new MessageEvent("message", { data: payload }));
|
||||
}
|
||||
else if (payload instanceof Blob) {
|
||||
socket.dispatchEvent(new MessageEvent("message", { data: payload }));
|
||||
}
|
||||
}, (code, reason) => {
|
||||
fakeReadyState = WebSocketFields.CLOSED;
|
||||
socket.dispatchEvent(new CloseEvent("close", { code, reason }));
|
||||
}, () => {
|
||||
fakeReadyState = WebSocketFields.CLOSED;
|
||||
});
|
||||
// const socket = this.client.connect(
|
||||
// remote,
|
||||
// protocols,
|
||||
// async () => {
|
||||
// const resolvedHeaders =
|
||||
// typeof options.headers === 'function'
|
||||
// ? await options.headers()
|
||||
// : options.headers || {};
|
||||
//
|
||||
// const requestHeaders: BareHeaders =
|
||||
// resolvedHeaders instanceof Headers
|
||||
// ? Object.fromEntries(resolvedHeaders)
|
||||
// : resolvedHeaders;
|
||||
//
|
||||
// // user is expected to specify user-agent and origin
|
||||
// // both are in spec
|
||||
//
|
||||
// requestHeaders['Host'] = (remote as URL).host;
|
||||
// // requestHeaders['Origin'] = origin;
|
||||
// requestHeaders['Pragma'] = 'no-cache';
|
||||
// requestHeaders['Cache-Control'] = 'no-cache';
|
||||
// requestHeaders['Upgrade'] = 'websocket';
|
||||
// // requestHeaders['User-Agent'] = navigator.userAgent;
|
||||
// requestHeaders['Connection'] = 'Upgrade';
|
||||
//
|
||||
// return requestHeaders;
|
||||
// },
|
||||
// (meta) => {
|
||||
// fakeProtocol = meta.protocol;
|
||||
// if (options.setCookiesCallback)
|
||||
// options.setCookiesCallback(meta.setCookies);
|
||||
// },
|
||||
// (readyState) => {
|
||||
// fakeReadyState = readyState;
|
||||
// },
|
||||
// options.webSocketImpl || WebSocket
|
||||
// );
|
||||
// protocol is always an empty before connecting
|
||||
// updated when we receive the metadata
|
||||
// this value doesn't change when it's CLOSING or CLOSED etc
|
||||
const getReadyState = () => {
|
||||
const realReadyState = getRealReadyState.call(socket);
|
||||
// readyState should only be faked when the real readyState is OPEN
|
||||
return realReadyState === WebSocketFields.OPEN
|
||||
? fakeReadyState
|
||||
: realReadyState;
|
||||
};
|
||||
if (options.readyStateHook)
|
||||
options.readyStateHook(socket, getReadyState);
|
||||
else {
|
||||
// we have to hook .readyState ourselves
|
||||
Object.defineProperty(socket, 'readyState', {
|
||||
get: getReadyState,
|
||||
configurable: true,
|
||||
enumerable: true,
|
||||
});
|
||||
}
|
||||
/**
|
||||
* @returns The error that should be thrown if send() were to be called on this socket according to the fake readyState value
|
||||
*/
|
||||
const getSendError = () => {
|
||||
const readyState = getReadyState();
|
||||
if (readyState === WebSocketFields.CONNECTING)
|
||||
return new DOMException("Failed to execute 'send' on 'WebSocket': Still in CONNECTING state.");
|
||||
};
|
||||
if (options.sendErrorHook)
|
||||
options.sendErrorHook(socket, getSendError);
|
||||
else {
|
||||
// we have to hook .send ourselves
|
||||
// use ...args to avoid giving the number of args a quantity
|
||||
// no arguments will trip the following error: TypeError: Failed to execute 'send' on 'WebSocket': 1 argument required, but only 0 present.
|
||||
socket.send = function (...args) {
|
||||
const error = getSendError();
|
||||
if (error)
|
||||
throw error;
|
||||
sendData(args[0]);
|
||||
};
|
||||
}
|
||||
if (options.urlHook)
|
||||
options.urlHook(socket, remote);
|
||||
else
|
||||
Object.defineProperty(socket, 'url', {
|
||||
get: () => remote.toString(),
|
||||
configurable: true,
|
||||
enumerable: true,
|
||||
});
|
||||
const getProtocol = () => fakeProtocol;
|
||||
if (options.protocolHook)
|
||||
options.protocolHook(socket, getProtocol);
|
||||
else
|
||||
Object.defineProperty(socket, 'protocol', {
|
||||
get: getProtocol,
|
||||
configurable: true,
|
||||
enumerable: true,
|
||||
});
|
||||
return socket;
|
||||
}
|
||||
async fetch(url, init) {
|
||||
// Only create an instance of Request to parse certain parameters of init such as method, headers, redirect
|
||||
// But use init values whenever possible
|
||||
const req = new Request(url, init);
|
||||
// try to use init.headers because it may contain capitalized headers
|
||||
// furthermore, important headers on the Request class are blocked...
|
||||
// we should try to preserve the capitalization due to quirks with earlier servers
|
||||
const inputHeaders = init?.headers || req.headers;
|
||||
const headers = inputHeaders instanceof Headers
|
||||
? Object.fromEntries(inputHeaders)
|
||||
: inputHeaders;
|
||||
const body = init?.body || req.body;
|
||||
let urlO = new URL(req.url);
|
||||
if (urlO.protocol.startsWith('blob:')) {
|
||||
const response = await fetch(urlO);
|
||||
const result = new Response(response.body, response);
|
||||
result.rawHeaders = Object.fromEntries(response.headers);
|
||||
result.rawResponse = response;
|
||||
return result;
|
||||
}
|
||||
let switcher = findSwitcher();
|
||||
if (!switcher.active)
|
||||
throw "invalid";
|
||||
const client = switcher.active;
|
||||
if (!client.ready)
|
||||
await client.init();
|
||||
for (let i = 0;; i++) {
|
||||
if ('host' in headers)
|
||||
headers.host = urlO.host;
|
||||
else
|
||||
headers.Host = urlO.host;
|
||||
let resp = await client.request(urlO, req.method, body, headers, req.signal);
|
||||
let responseobj = new Response(statusEmpty.includes(resp.status) ? undefined : resp.body, {
|
||||
headers: new Headers(resp.headers)
|
||||
});
|
||||
responseobj.rawHeaders = resp.headers;
|
||||
responseobj.rawResponse = new Response(resp.body);
|
||||
responseobj.finalURL = urlO.toString();
|
||||
const redirect = init?.redirect || req.redirect;
|
||||
if (statusRedirect.includes(responseobj.status)) {
|
||||
switch (redirect) {
|
||||
case 'follow': {
|
||||
const location = responseobj.headers.get('location');
|
||||
if (maxRedirects > i && location !== null) {
|
||||
urlO = new URL(location, urlO);
|
||||
continue;
|
||||
}
|
||||
else
|
||||
throw new TypeError('Failed to fetch');
|
||||
}
|
||||
case 'error':
|
||||
throw new TypeError('Failed to fetch');
|
||||
case 'manual':
|
||||
return responseobj;
|
||||
}
|
||||
}
|
||||
else {
|
||||
return responseobj;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
exports.BareClient = BareClient;
|
||||
exports.WebSocketFields = WebSocketFields;
|
||||
exports.maxRedirects = maxRedirects;
|
||||
|
||||
}));
|
||||
//# sourceMappingURL=bare.cjs.map
|
1
dist/bare.cjs.map
vendored
Normal file
1
dist/bare.cjs.map
vendored
Normal file
File diff suppressed because one or more lines are too long
3
dist/index.d.ts
vendored
Normal file
3
dist/index.d.ts
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
export * from './BareTypes';
|
||||
export * from './BareClient';
|
||||
export { WebSocketFields } from "./snapshot";
|
287
dist/index.js
vendored
Normal file
287
dist/index.js
vendored
Normal file
|
@ -0,0 +1,287 @@
|
|||
const maxRedirects = 20;
|
||||
|
||||
// The user likely has overwritten all networking functions after importing bare-client
|
||||
// It is our responsibility to make sure components of Bare-Client are using native networking functions
|
||||
const fetch = globalThis.fetch;
|
||||
const WebSocket = globalThis.WebSocket;
|
||||
const Request = globalThis.Request;
|
||||
const Response = globalThis.Response;
|
||||
const WebSocketFields = {
|
||||
prototype: {
|
||||
send: WebSocket.prototype.send,
|
||||
},
|
||||
CLOSED: WebSocket.CLOSED,
|
||||
CLOSING: WebSocket.CLOSING,
|
||||
CONNECTING: WebSocket.CONNECTING,
|
||||
OPEN: WebSocket.OPEN,
|
||||
};
|
||||
|
||||
self.BCC_VERSION = "2.1.3";
|
||||
console.warn("BCC_VERSION: " + self.BCC_VERSION);
|
||||
if (!("gTransports" in globalThis)) {
|
||||
globalThis.gTransports = {};
|
||||
}
|
||||
class Switcher {
|
||||
transports = {};
|
||||
active = null;
|
||||
}
|
||||
function findSwitcher() {
|
||||
if (globalThis.gSwitcher)
|
||||
return globalThis.gSwitcher;
|
||||
for (let i = 0; i < 20; i++) {
|
||||
try {
|
||||
parent = parent.parent;
|
||||
if (parent && parent["gSwitcher"]) {
|
||||
console.warn("found implementation on parent");
|
||||
globalThis.gSwitcher = parent["gSwitcher"];
|
||||
return parent["gSwitcher"];
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
globalThis.gSwitcher = new Switcher;
|
||||
return globalThis.gSwitcher;
|
||||
}
|
||||
}
|
||||
throw "unreachable";
|
||||
}
|
||||
|
||||
/*
|
||||
* WebSocket helpers
|
||||
*/
|
||||
const validChars = "!#$%&'*+-.0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ^_`abcdefghijklmnopqrstuvwxyz|~";
|
||||
function validProtocol(protocol) {
|
||||
for (let i = 0; i < protocol.length; i++) {
|
||||
const char = protocol[i];
|
||||
if (!validChars.includes(char)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// get the unhooked value
|
||||
const getRealReadyState = Object.getOwnPropertyDescriptor(WebSocket.prototype, 'readyState').get;
|
||||
const wsProtocols = ['ws:', 'wss:'];
|
||||
const statusEmpty = [101, 204, 205, 304];
|
||||
const statusRedirect = [301, 302, 303, 307, 308];
|
||||
class BareClient {
|
||||
/**
|
||||
* Create a BareClient. Calls to fetch and connect will wait for an implementation to be ready.
|
||||
*/
|
||||
constructor() { }
|
||||
createWebSocket(remote, protocols = [], options, origin) {
|
||||
let switcher = findSwitcher();
|
||||
let client = switcher.active;
|
||||
if (!client)
|
||||
throw "invalid switcher";
|
||||
if (!client.ready)
|
||||
throw new TypeError('You need to wait for the client to finish fetching the manifest before creating any WebSockets. Try caching the manifest data before making this request.');
|
||||
try {
|
||||
remote = new URL(remote);
|
||||
}
|
||||
catch (err) {
|
||||
throw new DOMException(`Faiiled to construct 'WebSocket': The URL '${remote}' is invalid.`);
|
||||
}
|
||||
if (!wsProtocols.includes(remote.protocol))
|
||||
throw new DOMException(`Failed to construct 'WebSocket': The URL's scheme must be either 'ws' or 'wss'. '${remote.protocol}' is not allowed.`);
|
||||
if (!Array.isArray(protocols))
|
||||
protocols = [protocols];
|
||||
protocols = protocols.map(String);
|
||||
for (const proto of protocols)
|
||||
if (!validProtocol(proto))
|
||||
throw new DOMException(`Failed to construct 'WebSocket': The subprotocol '${proto}' is invalid.`);
|
||||
let wsImpl = (options.webSocketImpl || WebSocket);
|
||||
const socket = new wsImpl("wss:null", protocols);
|
||||
let fakeProtocol = '';
|
||||
let fakeReadyState = WebSocketFields.CONNECTING;
|
||||
let initialErrorHappened = false;
|
||||
socket.addEventListener("error", (e) => {
|
||||
if (!initialErrorHappened) {
|
||||
fakeReadyState = WebSocket.CONNECTING;
|
||||
e.stopImmediatePropagation();
|
||||
initialErrorHappened = true;
|
||||
}
|
||||
});
|
||||
const sendData = client.connect(remote, origin, protocols, (protocol) => {
|
||||
fakeReadyState = WebSocketFields.OPEN;
|
||||
fakeProtocol = protocol;
|
||||
socket.dispatchEvent(new Event("open"));
|
||||
}, (payload) => {
|
||||
if (typeof payload === "string") {
|
||||
socket.dispatchEvent(new MessageEvent("message", { data: payload }));
|
||||
}
|
||||
else if (payload instanceof ArrayBuffer) {
|
||||
Object.setPrototypeOf(payload, ArrayBuffer);
|
||||
socket.dispatchEvent(new MessageEvent("message", { data: payload }));
|
||||
}
|
||||
else if (payload instanceof Blob) {
|
||||
socket.dispatchEvent(new MessageEvent("message", { data: payload }));
|
||||
}
|
||||
}, (code, reason) => {
|
||||
fakeReadyState = WebSocketFields.CLOSED;
|
||||
socket.dispatchEvent(new CloseEvent("close", { code, reason }));
|
||||
}, () => {
|
||||
fakeReadyState = WebSocketFields.CLOSED;
|
||||
});
|
||||
// const socket = this.client.connect(
|
||||
// remote,
|
||||
// protocols,
|
||||
// async () => {
|
||||
// const resolvedHeaders =
|
||||
// typeof options.headers === 'function'
|
||||
// ? await options.headers()
|
||||
// : options.headers || {};
|
||||
//
|
||||
// const requestHeaders: BareHeaders =
|
||||
// resolvedHeaders instanceof Headers
|
||||
// ? Object.fromEntries(resolvedHeaders)
|
||||
// : resolvedHeaders;
|
||||
//
|
||||
// // user is expected to specify user-agent and origin
|
||||
// // both are in spec
|
||||
//
|
||||
// requestHeaders['Host'] = (remote as URL).host;
|
||||
// // requestHeaders['Origin'] = origin;
|
||||
// requestHeaders['Pragma'] = 'no-cache';
|
||||
// requestHeaders['Cache-Control'] = 'no-cache';
|
||||
// requestHeaders['Upgrade'] = 'websocket';
|
||||
// // requestHeaders['User-Agent'] = navigator.userAgent;
|
||||
// requestHeaders['Connection'] = 'Upgrade';
|
||||
//
|
||||
// return requestHeaders;
|
||||
// },
|
||||
// (meta) => {
|
||||
// fakeProtocol = meta.protocol;
|
||||
// if (options.setCookiesCallback)
|
||||
// options.setCookiesCallback(meta.setCookies);
|
||||
// },
|
||||
// (readyState) => {
|
||||
// fakeReadyState = readyState;
|
||||
// },
|
||||
// options.webSocketImpl || WebSocket
|
||||
// );
|
||||
// protocol is always an empty before connecting
|
||||
// updated when we receive the metadata
|
||||
// this value doesn't change when it's CLOSING or CLOSED etc
|
||||
const getReadyState = () => {
|
||||
const realReadyState = getRealReadyState.call(socket);
|
||||
// readyState should only be faked when the real readyState is OPEN
|
||||
return realReadyState === WebSocketFields.OPEN
|
||||
? fakeReadyState
|
||||
: realReadyState;
|
||||
};
|
||||
if (options.readyStateHook)
|
||||
options.readyStateHook(socket, getReadyState);
|
||||
else {
|
||||
// we have to hook .readyState ourselves
|
||||
Object.defineProperty(socket, 'readyState', {
|
||||
get: getReadyState,
|
||||
configurable: true,
|
||||
enumerable: true,
|
||||
});
|
||||
}
|
||||
/**
|
||||
* @returns The error that should be thrown if send() were to be called on this socket according to the fake readyState value
|
||||
*/
|
||||
const getSendError = () => {
|
||||
const readyState = getReadyState();
|
||||
if (readyState === WebSocketFields.CONNECTING)
|
||||
return new DOMException("Failed to execute 'send' on 'WebSocket': Still in CONNECTING state.");
|
||||
};
|
||||
if (options.sendErrorHook)
|
||||
options.sendErrorHook(socket, getSendError);
|
||||
else {
|
||||
// we have to hook .send ourselves
|
||||
// use ...args to avoid giving the number of args a quantity
|
||||
// no arguments will trip the following error: TypeError: Failed to execute 'send' on 'WebSocket': 1 argument required, but only 0 present.
|
||||
socket.send = function (...args) {
|
||||
const error = getSendError();
|
||||
if (error)
|
||||
throw error;
|
||||
sendData(args[0]);
|
||||
};
|
||||
}
|
||||
if (options.urlHook)
|
||||
options.urlHook(socket, remote);
|
||||
else
|
||||
Object.defineProperty(socket, 'url', {
|
||||
get: () => remote.toString(),
|
||||
configurable: true,
|
||||
enumerable: true,
|
||||
});
|
||||
const getProtocol = () => fakeProtocol;
|
||||
if (options.protocolHook)
|
||||
options.protocolHook(socket, getProtocol);
|
||||
else
|
||||
Object.defineProperty(socket, 'protocol', {
|
||||
get: getProtocol,
|
||||
configurable: true,
|
||||
enumerable: true,
|
||||
});
|
||||
return socket;
|
||||
}
|
||||
async fetch(url, init) {
|
||||
// Only create an instance of Request to parse certain parameters of init such as method, headers, redirect
|
||||
// But use init values whenever possible
|
||||
const req = new Request(url, init);
|
||||
// try to use init.headers because it may contain capitalized headers
|
||||
// furthermore, important headers on the Request class are blocked...
|
||||
// we should try to preserve the capitalization due to quirks with earlier servers
|
||||
const inputHeaders = init?.headers || req.headers;
|
||||
const headers = inputHeaders instanceof Headers
|
||||
? Object.fromEntries(inputHeaders)
|
||||
: inputHeaders;
|
||||
const body = init?.body || req.body;
|
||||
let urlO = new URL(req.url);
|
||||
if (urlO.protocol.startsWith('blob:')) {
|
||||
const response = await fetch(urlO);
|
||||
const result = new Response(response.body, response);
|
||||
result.rawHeaders = Object.fromEntries(response.headers);
|
||||
result.rawResponse = response;
|
||||
return result;
|
||||
}
|
||||
let switcher = findSwitcher();
|
||||
if (!switcher.active)
|
||||
throw "invalid";
|
||||
const client = switcher.active;
|
||||
if (!client.ready)
|
||||
await client.init();
|
||||
for (let i = 0;; i++) {
|
||||
if ('host' in headers)
|
||||
headers.host = urlO.host;
|
||||
else
|
||||
headers.Host = urlO.host;
|
||||
let resp = await client.request(urlO, req.method, body, headers, req.signal);
|
||||
let responseobj = new Response(statusEmpty.includes(resp.status) ? undefined : resp.body, {
|
||||
headers: new Headers(resp.headers)
|
||||
});
|
||||
responseobj.rawHeaders = resp.headers;
|
||||
responseobj.rawResponse = new Response(resp.body);
|
||||
responseobj.finalURL = urlO.toString();
|
||||
const redirect = init?.redirect || req.redirect;
|
||||
if (statusRedirect.includes(responseobj.status)) {
|
||||
switch (redirect) {
|
||||
case 'follow': {
|
||||
const location = responseobj.headers.get('location');
|
||||
if (maxRedirects > i && location !== null) {
|
||||
urlO = new URL(location, urlO);
|
||||
continue;
|
||||
}
|
||||
else
|
||||
throw new TypeError('Failed to fetch');
|
||||
}
|
||||
case 'error':
|
||||
throw new TypeError('Failed to fetch');
|
||||
case 'manual':
|
||||
return responseobj;
|
||||
}
|
||||
}
|
||||
else {
|
||||
return responseobj;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export { BareClient, WebSocketFields, maxRedirects };
|
||||
//# sourceMappingURL=index.js.map
|
1
dist/index.js.map
vendored
Normal file
1
dist/index.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
38
dist/snapshot.d.ts
vendored
Normal file
38
dist/snapshot.d.ts
vendored
Normal file
|
@ -0,0 +1,38 @@
|
|||
export declare const fetch: typeof globalThis.fetch;
|
||||
export declare const WebSocket: {
|
||||
new (url: string | URL, protocols?: string | string[] | undefined): WebSocket;
|
||||
prototype: WebSocket;
|
||||
readonly CONNECTING: 0;
|
||||
readonly OPEN: 1;
|
||||
readonly CLOSING: 2;
|
||||
readonly CLOSED: 3;
|
||||
};
|
||||
export declare const Request: {
|
||||
new (input: URL | RequestInfo, init?: RequestInit | undefined): Request;
|
||||
prototype: Request;
|
||||
};
|
||||
export declare const Response: {
|
||||
new (body?: BodyInit | null | undefined, init?: ResponseInit | undefined): Response;
|
||||
prototype: Response;
|
||||
error(): Response;
|
||||
json(data: any, init?: ResponseInit | undefined): Response;
|
||||
redirect(url: string | URL, status?: number | undefined): Response;
|
||||
};
|
||||
export declare const XMLHttpRequest: {
|
||||
new (): XMLHttpRequest;
|
||||
prototype: XMLHttpRequest;
|
||||
readonly UNSENT: 0;
|
||||
readonly OPENED: 1;
|
||||
readonly HEADERS_RECEIVED: 2;
|
||||
readonly LOADING: 3;
|
||||
readonly DONE: 4;
|
||||
};
|
||||
export declare const WebSocketFields: {
|
||||
prototype: {
|
||||
send: (data: string | Blob | ArrayBufferView | ArrayBufferLike) => void;
|
||||
};
|
||||
CLOSED: 3;
|
||||
CLOSING: 2;
|
||||
CONNECTING: 0;
|
||||
OPEN: 1;
|
||||
};
|
1
dist/webSocket.d.ts
vendored
Normal file
1
dist/webSocket.d.ts
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
export declare function validProtocol(protocol: string): boolean;
|
1
index.js
Normal file
1
index.js
Normal file
|
@ -0,0 +1 @@
|
|||
"use strict";export*from"./BareTypes";export*from"./BareClient";export{WebSocketFields}from"./snapshot";
|
33
package.json
Normal file
33
package.json
Normal file
|
@ -0,0 +1,33 @@
|
|||
{
|
||||
"name": "@mercuryworkshop/bare-mux",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"author": "",
|
||||
"license": "MIT",
|
||||
"exports": {
|
||||
"import": "./dist/index.js",
|
||||
"require": "./dist/index.cjs"
|
||||
},
|
||||
"browser": "dist/index.cjs",
|
||||
"main": "dist/index.cjs",
|
||||
"module": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"devDependencies": {
|
||||
"@rollup/plugin-inject": "^5.0.5",
|
||||
"esbuild": "^0.19.11",
|
||||
"esbuild-plugin-d.ts": "^1.2.2",
|
||||
"rollup": "^4.9.6",
|
||||
"rollup-plugin-typescript2": "^0.36.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/uuid": "^9.0.8",
|
||||
"uuid": "^9.0.1"
|
||||
}
|
||||
}
|
56
rollup.config.js
Normal file
56
rollup.config.js
Normal file
|
@ -0,0 +1,56 @@
|
|||
import inject from '@rollup/plugin-inject';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import typescript from 'rollup-plugin-typescript2';
|
||||
|
||||
/**
|
||||
* @typedef {import('rollup').OutputOptions} OutputOptions
|
||||
* @typedef {import('rollup').RollupOptions} RollupOptions
|
||||
*/
|
||||
|
||||
/**
|
||||
* @returns {RollupOptions['plugins']!}
|
||||
*/
|
||||
const commonPlugins = () => [
|
||||
typescript(),
|
||||
inject(
|
||||
Object.fromEntries(
|
||||
['fetch', 'Request', 'Response', 'WebSocket', 'XMLHttpRequest'].map(
|
||||
(name) => [
|
||||
name,
|
||||
[fileURLToPath(new URL('./src/snapshot.ts', import.meta.url)), name],
|
||||
]
|
||||
)
|
||||
)
|
||||
),
|
||||
];
|
||||
|
||||
/**
|
||||
* @type {RollupOptions[]}
|
||||
*/
|
||||
const configs = [
|
||||
// import
|
||||
{
|
||||
input: 'src/index.ts',
|
||||
output: {
|
||||
file: `dist/index.js`,
|
||||
format: 'esm',
|
||||
sourcemap: true,
|
||||
exports: 'named',
|
||||
},
|
||||
plugins: commonPlugins(),
|
||||
},
|
||||
// require
|
||||
{
|
||||
input: 'src/index.ts',
|
||||
output: {
|
||||
file: `dist/bare.cjs`,
|
||||
format: 'umd',
|
||||
name: 'bare',
|
||||
sourcemap: true,
|
||||
exports: 'auto',
|
||||
},
|
||||
plugins: commonPlugins(),
|
||||
},
|
||||
];
|
||||
|
||||
export default configs;
|
373
src/BareClient.ts
Normal file
373
src/BareClient.ts
Normal file
|
@ -0,0 +1,373 @@
|
|||
import { BareHeaders, maxRedirects } from './BareTypes';
|
||||
import { findSwitcher } from './Switcher';
|
||||
import { WebSocketFields } from './snapshot.js';
|
||||
import { validProtocol } from './webSocket';
|
||||
|
||||
|
||||
// get the unhooked value
|
||||
const getRealReadyState = Object.getOwnPropertyDescriptor(
|
||||
WebSocket.prototype,
|
||||
'readyState'
|
||||
)!.get!;
|
||||
|
||||
const wsProtocols = ['ws:', 'wss:'];
|
||||
const statusEmpty = [101, 204, 205, 304];
|
||||
|
||||
const statusRedirect = [301, 302, 303, 307, 308];
|
||||
|
||||
export type WebSocketImpl = {
|
||||
new(...args: ConstructorParameters<typeof WebSocket>): WebSocket;
|
||||
};
|
||||
|
||||
export namespace BareWebSocket {
|
||||
export type GetReadyStateCallback = () => number;
|
||||
export type GetSendErrorCallback = () => Error | undefined;
|
||||
export type GetProtocolCallback = () => string;
|
||||
export type HeadersType = BareHeaders | Headers | undefined;
|
||||
export type HeadersProvider =
|
||||
| BareHeaders
|
||||
| (() => BareHeaders | Promise<BareHeaders>);
|
||||
|
||||
export interface Options {
|
||||
/**
|
||||
* A provider of request headers to pass to the remote.
|
||||
* Usually one of `User-Agent`, `Origin`, and `Cookie`
|
||||
* Can be just the headers object or an synchronous/asynchronous function that returns the headers object
|
||||
*/
|
||||
headers?: BareWebSocket.HeadersProvider;
|
||||
/**
|
||||
* A hook executed by this function with helper arguments for hooking the readyState property. If a hook isn't provided, bare-client will hook the property on the instance. Hooking it on an instance basis is good for small projects, but ideally the class should be hooked by the user of bare-client.
|
||||
*/
|
||||
readyStateHook?:
|
||||
| ((
|
||||
socket: WebSocket,
|
||||
getReadyState: BareWebSocket.GetReadyStateCallback
|
||||
) => void)
|
||||
| undefined;
|
||||
/**
|
||||
* A hook executed by this function with helper arguments for determining if the send function should throw an error. If a hook isn't provided, bare-client will hook the function on the instance.
|
||||
*/
|
||||
sendErrorHook?:
|
||||
| ((
|
||||
socket: WebSocket,
|
||||
getSendError: BareWebSocket.GetSendErrorCallback
|
||||
) => void)
|
||||
| undefined;
|
||||
/**
|
||||
* A hook executed by this function with the URL. If a hook isn't provided, bare-client will hook the URL.
|
||||
*/
|
||||
urlHook?: ((socket: WebSocket, url: URL) => void) | undefined;
|
||||
/**
|
||||
* A hook executed by this function with a helper for getting the current fake protocol. If a hook isn't provided, bare-client will hook the protocol.
|
||||
*/
|
||||
protocolHook?:
|
||||
| ((
|
||||
socket: WebSocket,
|
||||
getProtocol: BareWebSocket.GetProtocolCallback
|
||||
) => void)
|
||||
| undefined;
|
||||
/**
|
||||
* A callback executed by this function with an array of cookies. This is called once the metadata from the server is received.
|
||||
*/
|
||||
setCookiesCallback?: ((setCookies: string[]) => void) | undefined;
|
||||
webSocketImpl?: WebSocket;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A Response with additional properties.
|
||||
*/
|
||||
export interface BareResponse extends Response {
|
||||
rawResponse: Response;
|
||||
rawHeaders: BareHeaders;
|
||||
}
|
||||
/**
|
||||
* A BareResponse with additional properties.
|
||||
*/
|
||||
export interface BareResponseFetch extends BareResponse {
|
||||
finalURL: string;
|
||||
}
|
||||
export class BareClient {
|
||||
|
||||
/**
|
||||
* Create a BareClient. Calls to fetch and connect will wait for an implementation to be ready.
|
||||
*/
|
||||
constructor() { }
|
||||
|
||||
createWebSocket(
|
||||
remote: string | URL,
|
||||
protocols: string | string[] | undefined = [],
|
||||
options: BareWebSocket.Options,
|
||||
origin: string,
|
||||
): WebSocket {
|
||||
let switcher = findSwitcher();
|
||||
let client = switcher.active;
|
||||
if (!client) throw "invalid switcher";
|
||||
|
||||
if (!client.ready)
|
||||
throw new TypeError(
|
||||
'You need to wait for the client to finish fetching the manifest before creating any WebSockets. Try caching the manifest data before making this request.'
|
||||
);
|
||||
|
||||
try {
|
||||
remote = new URL(remote);
|
||||
} catch (err) {
|
||||
throw new DOMException(
|
||||
`Faiiled to construct 'WebSocket': The URL '${remote}' is invalid.`
|
||||
);
|
||||
}
|
||||
|
||||
if (!wsProtocols.includes(remote.protocol))
|
||||
throw new DOMException(
|
||||
`Failed to construct 'WebSocket': The URL's scheme must be either 'ws' or 'wss'. '${remote.protocol}' is not allowed.`
|
||||
);
|
||||
|
||||
if (!Array.isArray(protocols)) protocols = [protocols];
|
||||
|
||||
protocols = protocols.map(String);
|
||||
|
||||
for (const proto of protocols)
|
||||
if (!validProtocol(proto))
|
||||
throw new DOMException(
|
||||
`Failed to construct 'WebSocket': The subprotocol '${proto}' is invalid.`
|
||||
);
|
||||
|
||||
|
||||
let wsImpl = (options.webSocketImpl || WebSocket) as WebSocketImpl;
|
||||
const socket = new wsImpl("wss:null", protocols);
|
||||
|
||||
let fakeProtocol = '';
|
||||
|
||||
let fakeReadyState: number = WebSocketFields.CONNECTING;
|
||||
|
||||
let initialErrorHappened = false;
|
||||
socket.addEventListener("error", (e) => {
|
||||
if (!initialErrorHappened) {
|
||||
fakeReadyState = WebSocket.CONNECTING;
|
||||
e.stopImmediatePropagation();
|
||||
initialErrorHappened = true;
|
||||
}
|
||||
});
|
||||
|
||||
const sendData = client.connect(
|
||||
remote,
|
||||
origin,
|
||||
protocols,
|
||||
(protocol: string) => {
|
||||
fakeReadyState = WebSocketFields.OPEN;
|
||||
fakeProtocol = protocol;
|
||||
socket.dispatchEvent(new Event("open"));
|
||||
},
|
||||
(payload) => {
|
||||
if (typeof payload === "string") {
|
||||
socket.dispatchEvent(new MessageEvent("message", { data: payload }));
|
||||
} else if (payload instanceof ArrayBuffer) {
|
||||
Object.setPrototypeOf(payload, ArrayBuffer);
|
||||
|
||||
socket.dispatchEvent(new MessageEvent("message", { data: payload }));
|
||||
} else if (payload instanceof Blob) {
|
||||
socket.dispatchEvent(new MessageEvent("message", { data: payload }));
|
||||
}
|
||||
},
|
||||
(code, reason) => {
|
||||
fakeReadyState = WebSocketFields.CLOSED;
|
||||
socket.dispatchEvent(new CloseEvent("close", { code, reason }));
|
||||
},
|
||||
() => {
|
||||
fakeReadyState = WebSocketFields.CLOSED;
|
||||
},
|
||||
)
|
||||
|
||||
// const socket = this.client.connect(
|
||||
// remote,
|
||||
// protocols,
|
||||
// async () => {
|
||||
// const resolvedHeaders =
|
||||
// typeof options.headers === 'function'
|
||||
// ? await options.headers()
|
||||
// : options.headers || {};
|
||||
//
|
||||
// const requestHeaders: BareHeaders =
|
||||
// resolvedHeaders instanceof Headers
|
||||
// ? Object.fromEntries(resolvedHeaders)
|
||||
// : resolvedHeaders;
|
||||
//
|
||||
// // user is expected to specify user-agent and origin
|
||||
// // both are in spec
|
||||
//
|
||||
// requestHeaders['Host'] = (remote as URL).host;
|
||||
// // requestHeaders['Origin'] = origin;
|
||||
// requestHeaders['Pragma'] = 'no-cache';
|
||||
// requestHeaders['Cache-Control'] = 'no-cache';
|
||||
// requestHeaders['Upgrade'] = 'websocket';
|
||||
// // requestHeaders['User-Agent'] = navigator.userAgent;
|
||||
// requestHeaders['Connection'] = 'Upgrade';
|
||||
//
|
||||
// return requestHeaders;
|
||||
// },
|
||||
// (meta) => {
|
||||
// fakeProtocol = meta.protocol;
|
||||
// if (options.setCookiesCallback)
|
||||
// options.setCookiesCallback(meta.setCookies);
|
||||
// },
|
||||
// (readyState) => {
|
||||
// fakeReadyState = readyState;
|
||||
// },
|
||||
// options.webSocketImpl || WebSocket
|
||||
// );
|
||||
|
||||
// protocol is always an empty before connecting
|
||||
// updated when we receive the metadata
|
||||
// this value doesn't change when it's CLOSING or CLOSED etc
|
||||
const getReadyState = () => {
|
||||
const realReadyState = getRealReadyState.call(socket);
|
||||
// readyState should only be faked when the real readyState is OPEN
|
||||
return realReadyState === WebSocketFields.OPEN
|
||||
? fakeReadyState
|
||||
: realReadyState;
|
||||
};
|
||||
|
||||
if (options.readyStateHook) options.readyStateHook(socket, getReadyState);
|
||||
else {
|
||||
// we have to hook .readyState ourselves
|
||||
|
||||
Object.defineProperty(socket, 'readyState', {
|
||||
get: getReadyState,
|
||||
configurable: true,
|
||||
enumerable: true,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns The error that should be thrown if send() were to be called on this socket according to the fake readyState value
|
||||
*/
|
||||
const getSendError = () => {
|
||||
const readyState = getReadyState();
|
||||
|
||||
if (readyState === WebSocketFields.CONNECTING)
|
||||
return new DOMException(
|
||||
"Failed to execute 'send' on 'WebSocket': Still in CONNECTING state."
|
||||
);
|
||||
};
|
||||
|
||||
if (options.sendErrorHook) options.sendErrorHook(socket, getSendError);
|
||||
else {
|
||||
// we have to hook .send ourselves
|
||||
// use ...args to avoid giving the number of args a quantity
|
||||
// no arguments will trip the following error: TypeError: Failed to execute 'send' on 'WebSocket': 1 argument required, but only 0 present.
|
||||
socket.send = function(...args) {
|
||||
const error = getSendError();
|
||||
|
||||
if (error) throw error;
|
||||
sendData(args[0] as any);
|
||||
};
|
||||
}
|
||||
|
||||
if (options.urlHook) options.urlHook(socket, remote);
|
||||
else
|
||||
Object.defineProperty(socket, 'url', {
|
||||
get: () => remote.toString(),
|
||||
configurable: true,
|
||||
enumerable: true,
|
||||
});
|
||||
|
||||
const getProtocol = () => fakeProtocol;
|
||||
|
||||
if (options.protocolHook) options.protocolHook(socket, getProtocol);
|
||||
else
|
||||
Object.defineProperty(socket, 'protocol', {
|
||||
get: getProtocol,
|
||||
configurable: true,
|
||||
enumerable: true,
|
||||
});
|
||||
|
||||
return socket;
|
||||
}
|
||||
|
||||
async fetch(
|
||||
url: string | URL,
|
||||
init?: RequestInit
|
||||
): Promise<BareResponseFetch> {
|
||||
// Only create an instance of Request to parse certain parameters of init such as method, headers, redirect
|
||||
// But use init values whenever possible
|
||||
const req = new Request(url, init);
|
||||
|
||||
|
||||
// try to use init.headers because it may contain capitalized headers
|
||||
// furthermore, important headers on the Request class are blocked...
|
||||
// we should try to preserve the capitalization due to quirks with earlier servers
|
||||
const inputHeaders = init?.headers || req.headers;
|
||||
|
||||
const headers: BareHeaders =
|
||||
inputHeaders instanceof Headers
|
||||
? Object.fromEntries(inputHeaders)
|
||||
: (inputHeaders as BareHeaders);
|
||||
|
||||
|
||||
const body = init?.body || req.body;
|
||||
|
||||
let urlO = new URL(req.url);
|
||||
|
||||
if (urlO.protocol.startsWith('blob:')) {
|
||||
const response = await fetch(urlO);
|
||||
const result: Response & Partial<BareResponse> = new Response(
|
||||
response.body,
|
||||
response
|
||||
);
|
||||
|
||||
result.rawHeaders = Object.fromEntries(response.headers);
|
||||
result.rawResponse = response;
|
||||
|
||||
return result as BareResponseFetch;
|
||||
}
|
||||
|
||||
let switcher = findSwitcher();
|
||||
if (!switcher.active) throw "invalid";
|
||||
const client = switcher.active;
|
||||
if (!client.ready) await client.init();
|
||||
|
||||
for (let i = 0; ; i++) {
|
||||
if ('host' in headers) headers.host = urlO.host;
|
||||
else headers.Host = urlO.host;
|
||||
|
||||
|
||||
let resp = await client.request(
|
||||
urlO,
|
||||
req.method,
|
||||
body,
|
||||
headers,
|
||||
req.signal
|
||||
);
|
||||
|
||||
let responseobj: BareResponse & Partial<BareResponseFetch> = new Response(
|
||||
statusEmpty.includes(resp.status) ? undefined : resp.body, {
|
||||
headers: new Headers(resp.headers as HeadersInit)
|
||||
}) as BareResponse;
|
||||
responseobj.rawHeaders = resp.headers;
|
||||
responseobj.rawResponse = new Response(resp.body);
|
||||
|
||||
|
||||
responseobj.finalURL = urlO.toString();
|
||||
|
||||
const redirect = init?.redirect || req.redirect;
|
||||
|
||||
if (statusRedirect.includes(responseobj.status)) {
|
||||
switch (redirect) {
|
||||
case 'follow': {
|
||||
const location = responseobj.headers.get('location');
|
||||
if (maxRedirects > i && location !== null) {
|
||||
urlO = new URL(location, urlO);
|
||||
continue;
|
||||
} else throw new TypeError('Failed to fetch');
|
||||
}
|
||||
case 'error':
|
||||
throw new TypeError('Failed to fetch');
|
||||
case 'manual':
|
||||
return responseobj as BareResponseFetch;
|
||||
}
|
||||
} else {
|
||||
return responseobj as BareResponseFetch;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
49
src/BareTypes.ts
Normal file
49
src/BareTypes.ts
Normal file
|
@ -0,0 +1,49 @@
|
|||
export type BareHeaders = Record<string, string | string[]>;
|
||||
|
||||
export type BareMeta =
|
||||
{
|
||||
// ???
|
||||
};
|
||||
|
||||
export type TransferrableResponse =
|
||||
{
|
||||
body: ReadableStream | ArrayBuffer | Blob | string,
|
||||
headers: BareHeaders,
|
||||
status: number,
|
||||
statusText: string
|
||||
}
|
||||
|
||||
export interface BareTransport {
|
||||
init: () => Promise<void>;
|
||||
ready: boolean;
|
||||
connect: (
|
||||
url: URL,
|
||||
origin: string,
|
||||
protocols: string[],
|
||||
onopen: (protocol: string) => void,
|
||||
onmessage: (data: Blob | ArrayBuffer | string) => void,
|
||||
onclose: (code: number, reason: string) => void,
|
||||
onerror: (error: string) => void,
|
||||
) => (data: Blob | ArrayBuffer | string) => void;
|
||||
|
||||
request: (
|
||||
remote: URL,
|
||||
method: string,
|
||||
body: BodyInit | null,
|
||||
headers: BareHeaders,
|
||||
signal: AbortSignal | undefined
|
||||
) => Promise<TransferrableResponse>;
|
||||
|
||||
meta: () => BareMeta
|
||||
}
|
||||
export interface BareWebSocketMeta {
|
||||
protocol: string;
|
||||
setCookies: string[];
|
||||
}
|
||||
|
||||
export type BareHTTPProtocol = 'blob:' | 'http:' | 'https:' | string;
|
||||
export type BareWSProtocol = 'ws:' | 'wss:' | string;
|
||||
|
||||
export const maxRedirects = 20;
|
||||
|
||||
|
91
src/RemoteClient.ts
Normal file
91
src/RemoteClient.ts
Normal file
|
@ -0,0 +1,91 @@
|
|||
// /// <reference lib="WebWorker" />
|
||||
// import { v4 as uuid } from 'uuid';
|
||||
//
|
||||
// declare const self: ServiceWorkerGlobalScope;
|
||||
// export default class RemoteClient extends Client {
|
||||
// static singleton: RemoteClient;
|
||||
// private callbacks: Record<string, (message: Record<string, any>) => void> = {};
|
||||
//
|
||||
// private uid = uuid();
|
||||
// constructor() {
|
||||
// if (RemoteClient.singleton) return RemoteClient.singleton;
|
||||
// super();
|
||||
// // this should be fine
|
||||
// // if (!("ServiceWorkerGlobalScope" in self)) {
|
||||
// // throw new TypeError("Attempt to construct RemoteClient from outside a service worker")
|
||||
// // }
|
||||
//
|
||||
// addEventListener("message", (event) => {
|
||||
// if (event.data.__remote_target === this.uid) {
|
||||
// const callback = this.callbacks[event.data.__remote_id];
|
||||
// callback(event.data.__remote_value);
|
||||
// }
|
||||
// });
|
||||
//
|
||||
// RemoteClient.singleton = this;
|
||||
// }
|
||||
//
|
||||
// async send(message: Record<string, any>, id?: string) {
|
||||
// const clients = await self.clients.matchAll();
|
||||
// if (clients.length < 1)
|
||||
// throw new Error("no available clients");
|
||||
//
|
||||
// for (const client of clients) {
|
||||
// client.postMessage({
|
||||
// __remote_target: this.uid,
|
||||
// __remote_id: id,
|
||||
// __remote_value: message
|
||||
// })
|
||||
// }
|
||||
//
|
||||
// }
|
||||
//
|
||||
// async sendWithResponse(message: Record<string, any>): Promise<any> {
|
||||
// const id = uuid();
|
||||
// return new Promise((resolve) => {
|
||||
// this.callbacks[id] = resolve;
|
||||
// this.send(message, id);
|
||||
// });
|
||||
// }
|
||||
//
|
||||
// connect(
|
||||
// ...args: any
|
||||
// ) {
|
||||
// throw "why are you calling connect from remoteclient"
|
||||
// }
|
||||
// async request(
|
||||
// method: BareMethod,
|
||||
// requestHeaders: BareHeaders,
|
||||
// body: BodyInit | null,
|
||||
// remote: URL,
|
||||
// cache: BareCache | undefined,
|
||||
// duplex: string | undefined,
|
||||
// signal: AbortSignal | undefined
|
||||
// ): Promise<BareResponse> {
|
||||
//
|
||||
// const response = await this.sendWithResponse({
|
||||
// type: "request",
|
||||
// options: {
|
||||
// method,
|
||||
// requestHeaders,
|
||||
// body,
|
||||
// remote: remote.toString(),
|
||||
// },
|
||||
// });
|
||||
// // const readResponse = await this.readBareResponse(response);
|
||||
//
|
||||
// const result: Response & Partial<BareResponse> = new Response(
|
||||
// statusEmpty.includes(response.status!) ? undefined : response.body,
|
||||
// {
|
||||
// status: response.status,
|
||||
// statusText: response.statusText ?? undefined,
|
||||
// headers: new Headers(response.headers as HeadersInit),
|
||||
// }
|
||||
// );
|
||||
//
|
||||
// result.rawHeaders = response.rawHeaders;
|
||||
// result.rawResponse = response;
|
||||
//
|
||||
// return result as BareResponse;
|
||||
// }
|
||||
// }
|
67
src/Switcher.ts
Normal file
67
src/Switcher.ts
Normal file
|
@ -0,0 +1,67 @@
|
|||
import { BareTransport } from "./BareTypes";
|
||||
|
||||
self.BCC_VERSION = "2.1.3";
|
||||
console.warn("BCC_VERSION: " + self.BCC_VERSION);
|
||||
|
||||
if (!("gTransports" in globalThis)) {
|
||||
globalThis.gTransports = {};
|
||||
}
|
||||
|
||||
|
||||
declare global {
|
||||
interface ServiceWorkerGlobalScope {
|
||||
gSwitcher: Switcher;
|
||||
BCC_VERSION: string;
|
||||
BCC_DEBUG: boolean;
|
||||
}
|
||||
interface WorkerGlobalScope {
|
||||
gSwitcher: Switcher;
|
||||
BCC_VERSION: string;
|
||||
BCC_DEBUG: boolean;
|
||||
}
|
||||
interface Window {
|
||||
gSwitcher: Switcher;
|
||||
BCC_VERSION: string;
|
||||
BCC_DEBUG: boolean;
|
||||
}
|
||||
}
|
||||
|
||||
class Switcher {
|
||||
transports: Record<string, BareTransport> = {};
|
||||
active: BareTransport | null = null;
|
||||
}
|
||||
|
||||
export function findSwitcher(): Switcher {
|
||||
if (globalThis.gSwitcher) return globalThis.gSwitcher;
|
||||
|
||||
for (let i = 0; i < 20; i++) {
|
||||
try {
|
||||
parent = parent.parent;
|
||||
if (parent && parent["gSwitcher"]) {
|
||||
console.warn("found implementation on parent");
|
||||
globalThis.gSwitcher = parent["gSwitcher"];
|
||||
return parent["gSwitcher"];
|
||||
}
|
||||
} catch (e) {
|
||||
|
||||
globalThis.gSwitcher = new Switcher;
|
||||
return globalThis.gSwitcher;
|
||||
}
|
||||
}
|
||||
|
||||
throw "unreachable";
|
||||
}
|
||||
|
||||
export function AddTransport(name: string, client: BareTransport) {
|
||||
|
||||
let switcher = findSwitcher();
|
||||
|
||||
switcher.transports[name] = client;
|
||||
if (!switcher.active)
|
||||
switcher.active = switcher.transports[name];
|
||||
}
|
||||
|
||||
export function SetTransport(name: string) {
|
||||
let switcher = findSwitcher();
|
||||
switcher.active = switcher.transports[name];
|
||||
}
|
3
src/index.ts
Normal file
3
src/index.ts
Normal file
|
@ -0,0 +1,3 @@
|
|||
export * from './BareTypes';
|
||||
export * from './BareClient';
|
||||
export { WebSocketFields } from "./snapshot";
|
18
src/snapshot.ts
Normal file
18
src/snapshot.ts
Normal file
|
@ -0,0 +1,18 @@
|
|||
// The user likely has overwritten all networking functions after importing bare-client
|
||||
// It is our responsibility to make sure components of Bare-Client are using native networking functions
|
||||
|
||||
export const fetch = globalThis.fetch;
|
||||
export const WebSocket = globalThis.WebSocket;
|
||||
export const Request = globalThis.Request;
|
||||
export const Response = globalThis.Response;
|
||||
export const XMLHttpRequest = globalThis.XMLHttpRequest;
|
||||
|
||||
export const WebSocketFields = {
|
||||
prototype: {
|
||||
send: WebSocket.prototype.send,
|
||||
},
|
||||
CLOSED: WebSocket.CLOSED,
|
||||
CLOSING: WebSocket.CLOSING,
|
||||
CONNECTING: WebSocket.CONNECTING,
|
||||
OPEN: WebSocket.OPEN,
|
||||
};
|
18
src/webSocket.ts
Normal file
18
src/webSocket.ts
Normal file
|
@ -0,0 +1,18 @@
|
|||
/*
|
||||
* WebSocket helpers
|
||||
*/
|
||||
|
||||
const validChars =
|
||||
"!#$%&'*+-.0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ^_`abcdefghijklmnopqrstuvwxyz|~";
|
||||
|
||||
export function validProtocol(protocol: string): boolean {
|
||||
for (let i = 0; i < protocol.length; i++) {
|
||||
const char = protocol[i];
|
||||
|
||||
if (!validChars.includes(char)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
17
tsconfig.json
Normal file
17
tsconfig.json
Normal file
|
@ -0,0 +1,17 @@
|
|||
|
||||
{
|
||||
"compilerOptions": {
|
||||
"outDir": "build",
|
||||
"sourceMap": true,
|
||||
"target": "ES2022",
|
||||
"lib": ["DOM", "DOM.Iterable", "ES2019"],
|
||||
"strict": true,
|
||||
"stripInternal": true,
|
||||
"module": "ES6",
|
||||
"moduleResolution": "node",
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"noImplicitAny":false,
|
||||
"declaration": true
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue