@mercuryworkshop/bare-mux
Advanced tools
Comparing version 1.1.4 to 2.0.0
@@ -1,11 +0,9 @@ | ||
export * from './BareTypes'; | ||
export * from './BareClient'; | ||
export * from './Switcher'; | ||
export * from './RemoteClient'; | ||
export { BareClient as default } from './BareClient'; | ||
export * from './baretypes'; | ||
export * from './client'; | ||
export * from './connection'; | ||
export { BareClient as default } from './client'; | ||
export { WebSocketFields } from "./snapshot"; | ||
export type * from './BareTypes'; | ||
export type * from './BareClient'; | ||
export type * from './Switcher'; | ||
export type * from './RemoteClient'; | ||
export type * from './baretypes'; | ||
export type * from './client'; | ||
export type * from './connection'; | ||
export type * from "./snapshot"; |
@@ -1,405 +0,423 @@ | ||
import { v4 } from 'uuid'; | ||
(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.BareMux = {})); | ||
})(this, (function (exports) { 'use strict'; | ||
const maxRedirects = 20; | ||
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, | ||
}; | ||
// 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, | ||
}; | ||
/// <reference lib="WebWorker" /> | ||
function registerRemoteListener(channel) { | ||
navigator.serviceWorker.addEventListener("message", async ({ data }) => { | ||
if (data.type === "request") { | ||
const { remote, method, body, headers } = data; | ||
let response = await findSwitcher().active?.request(new URL(remote), method, body, headers, undefined).catch((err) => { | ||
let error = { id: data.id, type: "error", error: err }; | ||
console.error(err); | ||
channel.postMessage(error); | ||
}); | ||
if (response) { | ||
let transferred = []; | ||
if (response.body instanceof ArrayBuffer || response.body instanceof Blob || response.body instanceof ReadableStream) { | ||
transferred.push(response.body); | ||
} | ||
response.id = data.id; | ||
response.type = "response"; | ||
channel.postMessage(response, transferred); | ||
} | ||
} | ||
}); | ||
} | ||
let remote; | ||
if ("ServiceWorkerGlobalScope" in self) { | ||
addEventListener("message", async ({ data }) => { | ||
if (data.type === "response") { | ||
let promise = remote.promises.get(data.id); | ||
if (promise.resolve) { | ||
promise.resolve(data); | ||
remote.promises.delete(data.id); | ||
} | ||
} | ||
else if (data.type === "error") { | ||
let promise = remote.promises.get(data.id); | ||
if (promise.reject) { | ||
promise.reject(data.error); | ||
remote.promises.delete(data.id); | ||
} | ||
} | ||
}); | ||
} | ||
class RemoteTransport { | ||
canstart = true; | ||
ready = false; | ||
promises = new Map(); | ||
constructor() { | ||
if (!("ServiceWorkerGlobalScope" in self)) { | ||
throw new TypeError("Attempt to construct RemoteClient from outside a service worker"); | ||
} | ||
} | ||
async init() { | ||
remote = this; | ||
this.ready = true; | ||
} | ||
async meta() { } | ||
async request(remote, method, body, headers, signal) { | ||
let id = v4(); | ||
const clients = await self.clients.matchAll(); | ||
if (clients.length < 1) | ||
throw new Error("no available clients"); | ||
for (const client of clients) { | ||
client.postMessage({ | ||
type: "request", | ||
id, | ||
remote: remote.toString(), | ||
method, | ||
body, | ||
headers | ||
}); | ||
} | ||
return await new Promise((resolve, reject) => { | ||
this.promises.set(id, { resolve, reject }); | ||
}); | ||
} | ||
connect(url, origin, protocols, requestHeaders, onopen, onmessage, onclose, onerror) { | ||
throw "why are you calling connect from remoteclient"; | ||
} | ||
} | ||
async function searchForPort() { | ||
// @ts-expect-error | ||
const clients = await self.clients.matchAll({ type: "window", includeUncontrolled: true }); | ||
const promise = Promise.race([...clients.map((x) => tryGetPort(x)), new Promise((_, reject) => setTimeout(reject, 1000, new Error("")))]); | ||
try { | ||
return await promise; | ||
} | ||
catch { | ||
console.warn("bare-mux: failed to get a bare-mux SharedWorker MessagePort within 1s, retrying"); | ||
return await searchForPort(); | ||
} | ||
} | ||
function tryGetPort(client) { | ||
let channel = new MessageChannel(); | ||
return new Promise(resolve => { | ||
client.postMessage({ type: "getPort", port: channel.port2 }, [channel.port2]); | ||
channel.port1.onmessage = event => { | ||
resolve(event.data); | ||
}; | ||
}); | ||
} | ||
function createPort(path, channel, registerHandlers) { | ||
const worker = new SharedWorker(path, "bare-mux-worker"); | ||
if (registerHandlers) { | ||
// uv removes navigator.serviceWorker so this errors | ||
if (navigator.serviceWorker) { | ||
navigator.serviceWorker.addEventListener("message", event => { | ||
if (event.data.type === "getPort" && event.data.port) { | ||
console.debug("bare-mux: recieved request for port from sw"); | ||
const worker = new SharedWorker(path, "bare-mux-worker"); | ||
event.data.port.postMessage(worker.port, [worker.port]); | ||
} | ||
}); | ||
} | ||
channel.onmessage = (event) => { | ||
if (event.data.type === "getPath") { | ||
console.debug("bare-mux: recieved request for worker path from broadcast channel"); | ||
channel.postMessage({ type: "path", path: path }); | ||
} | ||
}; | ||
} | ||
return worker.port; | ||
} | ||
class WorkerConnection { | ||
constructor(workerPath) { | ||
this.channel = new BroadcastChannel("bare-mux"); | ||
this.createChannel(workerPath, true); | ||
} | ||
createChannel(workerPath, inInit) { | ||
// @ts-expect-error | ||
if (self.clients) { | ||
// running in a ServiceWorker | ||
// ask a window for the worker port, register for refreshPort | ||
this.port = searchForPort(); | ||
this.channel.onmessage = (event) => { | ||
if (event.data.type === "refreshPort") { | ||
this.port = searchForPort(); | ||
} | ||
}; | ||
} | ||
else if (workerPath && SharedWorker) { | ||
// running in a window, was passed a workerPath | ||
// create the SharedWorker and help other bare-mux clients get the workerPath | ||
if (!workerPath.startsWith("/") && !workerPath.includes("://")) | ||
throw new Error("Invalid URL. Must be absolute or start at the root."); | ||
this.port = createPort(workerPath, this.channel, inInit); | ||
} | ||
else if (SharedWorker) { | ||
// running in a window, was not passed a workerPath | ||
// ask other bare-mux clients for the workerPath | ||
this.port = new Promise(resolve => { | ||
this.channel.onmessage = (event) => { | ||
if (event.data.type === "path") { | ||
resolve(createPort(event.data.path, this.channel, inInit)); | ||
} | ||
}; | ||
this.channel.postMessage({ type: "getPath" }); | ||
}); | ||
} | ||
else { | ||
// SharedWorker does not exist | ||
throw new Error("Unable to get a channel to the SharedWorker."); | ||
} | ||
} | ||
async sendMessage(message, transferable) { | ||
if (this.port instanceof Promise) | ||
this.port = await this.port; | ||
const pingChannel = new MessageChannel(); | ||
const pingPromise = new Promise((resolve, reject) => { | ||
pingChannel.port1.onmessage = event => { | ||
if (event.data.type === "pong") { | ||
resolve(); | ||
} | ||
}; | ||
setTimeout(reject, 1500); | ||
}); | ||
this.port.postMessage({ message: { type: "ping" }, port: pingChannel.port2 }, [pingChannel.port2]); | ||
try { | ||
await pingPromise; | ||
} | ||
catch { | ||
console.warn("bare-mux: Failed to get a ping response from the worker within 1.5s. Assuming port is dead."); | ||
this.createChannel(); | ||
return await this.sendMessage(message, transferable); | ||
} | ||
const channel = new MessageChannel(); | ||
const toTransfer = [channel.port2, ...(transferable || [])]; | ||
const promise = new Promise((resolve, reject) => { | ||
channel.port1.onmessage = event => { | ||
const message = event.data; | ||
if (message.type === "error") { | ||
reject(message.error); | ||
} | ||
else { | ||
resolve(message); | ||
} | ||
}; | ||
}); | ||
this.port.postMessage({ message: message, port: channel.port2 }, toTransfer); | ||
return await promise; | ||
} | ||
} | ||
//@ts-expect-error not installing node types for this one thing | ||
self.BCC_VERSION = "1.1.4"; | ||
console.debug("BARE_MUX_VERSION: " + self.BCC_VERSION); | ||
function initTransport(name, config) { | ||
let cl = new ((0, eval)(name))(...config); | ||
cl.initpromise = cl.init(); | ||
return cl; | ||
} | ||
class Switcher { | ||
active = null; | ||
channel = new BroadcastChannel("bare-mux"); | ||
data = null; | ||
constructor() { | ||
this.channel.addEventListener("message", ({ data: { type, data } }) => { | ||
console.log(`bare-mux: ${type}`, data, `${"ServiceWorker" in globalThis}`); | ||
switch (type) { | ||
case "setremote": | ||
this.active = new RemoteTransport; | ||
break; | ||
case "set": | ||
const { name, config } = data; | ||
this.active = initTransport(name, config); | ||
break; | ||
case "find": | ||
if (this.data) { | ||
this.channel.postMessage(this.data); | ||
} | ||
break; | ||
} | ||
}); | ||
} | ||
} | ||
function findSwitcher() { | ||
if ("ServiceWorkerGlobalScope" in globalThis && globalThis.gSwitcher && !globalThis.gSwitcher.active) { | ||
globalThis.gSwitcher.channel.postMessage({ type: "find" }); | ||
} | ||
if (globalThis.gSwitcher) | ||
return globalThis.gSwitcher; | ||
if ("ServiceWorkerGlobalScope" in globalThis) { | ||
globalThis.gSwitcher = new Switcher; | ||
globalThis.gSwitcher.channel.postMessage({ type: "find" }); | ||
return globalThis.gSwitcher; | ||
} | ||
let _parent = window; | ||
for (let i = 0; i < 20; i++) { | ||
try { | ||
if (_parent == _parent.parent) { | ||
globalThis.gSwitcher = new Switcher; | ||
return globalThis.gSwitcher; | ||
} | ||
_parent = _parent.parent; | ||
if (_parent && _parent["gSwitcher"]) { | ||
console.debug("Found implementation on parent"); | ||
globalThis.gSwitcher = _parent["gSwitcher"]; | ||
return _parent["gSwitcher"]; | ||
} | ||
} | ||
catch (e) { | ||
globalThis.gSwitcher = new Switcher; | ||
globalThis.gSwitcher.channel.postMessage({ type: "find" }); | ||
return globalThis.gSwitcher; | ||
} | ||
} | ||
throw "unreachable"; | ||
} | ||
findSwitcher(); | ||
function SetTransport(name, ...config) { | ||
let switcher = findSwitcher(); | ||
switcher.active = initTransport(name, config); | ||
switcher.data = { type: "set", data: { name, config } }; | ||
switcher.channel.postMessage(switcher.data); | ||
} | ||
async function SetSingletonTransport(client) { | ||
let switcher = findSwitcher(); | ||
await client.init(); | ||
switcher.active = client; | ||
switcher.data = { type: "setremote", data: { name: client.constructor.name } }; | ||
switcher.channel.postMessage(switcher.data); | ||
} | ||
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 | ||
Object.getOwnPropertyDescriptor(WebSocket.prototype, 'readyState').get; | ||
const wsProtocols = ['ws:', 'wss:']; | ||
const statusEmpty = [101, 204, 205, 304]; | ||
const statusRedirect = [301, 302, 303, 307, 308]; | ||
class BareMuxConnection { | ||
constructor(workerPath) { | ||
this.worker = new WorkerConnection(workerPath); | ||
} | ||
async getTransport() { | ||
return (await this.worker.sendMessage({ type: "get" })).name; | ||
} | ||
async setTransport(path, options) { | ||
await this.setManualTransport(` | ||
const { default: BareTransport } = await import("${path}"); | ||
return [new BareTransport(${options.map(x => JSON.stringify(x)).join(", ")}), "${path}"]; | ||
`); | ||
} | ||
async setManualTransport(functionBody) { | ||
await this.worker.sendMessage({ | ||
type: "set", | ||
client: functionBody, | ||
}); | ||
} | ||
} | ||
class BareClient { | ||
/** | ||
* Create a BareClient. Calls to fetch and connect will wait for an implementation to be ready. | ||
*/ | ||
constructor(workerPath) { | ||
this.worker = new WorkerConnection(workerPath); | ||
} | ||
createWebSocket(remote, protocols = [], webSocketImpl, requestHeaders, arrayBufferImpl) { | ||
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 = (webSocketImpl || WebSocket); | ||
const socket = new wsImpl("ws://127.0.0.1:1", protocols); | ||
let fakeProtocol = ''; | ||
let fakeReadyState = WebSocketFields.CONNECTING; | ||
let initialErrorHappened = false; | ||
socket.addEventListener("error", (e) => { | ||
if (!initialErrorHappened) { | ||
fakeReadyState = WebSocket.CONNECTING; | ||
e.stopImmediatePropagation(); | ||
initialErrorHappened = true; | ||
} | ||
}); | ||
let initialCloseHappened = false; | ||
socket.addEventListener("close", (e) => { | ||
if (!initialCloseHappened) { | ||
e.stopImmediatePropagation(); | ||
initialCloseHappened = true; | ||
} | ||
}); | ||
// TODO socket onerror will be broken | ||
arrayBufferImpl = arrayBufferImpl || wsImpl.constructor.constructor("return ArrayBuffer")().prototype; | ||
requestHeaders = requestHeaders || {}; | ||
requestHeaders['Host'] = (new URL(remote)).host; | ||
// requestHeaders['Origin'] = origin; | ||
requestHeaders['Pragma'] = 'no-cache'; | ||
requestHeaders['Cache-Control'] = 'no-cache'; | ||
requestHeaders['Upgrade'] = 'websocket'; | ||
// requestHeaders['User-Agent'] = navigator.userAgent; | ||
requestHeaders['Connection'] = 'Upgrade'; | ||
const onopen = (protocol) => { | ||
fakeReadyState = WebSocketFields.OPEN; | ||
fakeProtocol = protocol; | ||
socket.meta = { | ||
headers: { | ||
"sec-websocket-protocol": protocol, | ||
} | ||
}; // what the fuck is a meta | ||
socket.dispatchEvent(new Event("open")); | ||
}; | ||
const onmessage = async (payload) => { | ||
if (typeof payload === "string") { | ||
socket.dispatchEvent(new MessageEvent("message", { data: payload })); | ||
} | ||
else if ("byteLength" in payload) { | ||
if (socket.binaryType === "blob") { | ||
payload = new Blob([payload]); | ||
} | ||
else { | ||
Object.setPrototypeOf(payload, arrayBufferImpl); | ||
} | ||
socket.dispatchEvent(new MessageEvent("message", { data: payload })); | ||
} | ||
else if ("arrayBuffer" in payload) { | ||
if (socket.binaryType === "arraybuffer") { | ||
payload = await payload.arrayBuffer(); | ||
Object.setPrototypeOf(payload, arrayBufferImpl); | ||
} | ||
socket.dispatchEvent(new MessageEvent("message", { data: payload })); | ||
} | ||
}; | ||
const onclose = (code, reason) => { | ||
fakeReadyState = WebSocketFields.CLOSED; | ||
socket.dispatchEvent(new CloseEvent("close", { code, reason })); | ||
}; | ||
const onerror = () => { | ||
fakeReadyState = WebSocketFields.CLOSED; | ||
socket.dispatchEvent(new Event("error")); | ||
}; | ||
const channel = new MessageChannel(); | ||
channel.port1.onmessage = event => { | ||
if (event.data.type === "open") { | ||
onopen(event.data.args[0]); | ||
} | ||
else if (event.data.type === "message") { | ||
onmessage(event.data.args[0]); | ||
} | ||
else if (event.data.type === "close") { | ||
onclose(event.data.args[0], event.data.args[1]); | ||
} | ||
else if (event.data.type === "error") { | ||
onerror( /* event.data.args[0] */); | ||
} | ||
}; | ||
this.worker.sendMessage({ | ||
type: "websocket", | ||
websocket: { | ||
url: remote.toString(), | ||
origin: origin, | ||
protocols: protocols, | ||
requestHeaders: requestHeaders, | ||
channel: channel.port2, | ||
}, | ||
}, [channel.port2]); | ||
// 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 = () => fakeReadyState; | ||
// 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."); | ||
}; | ||
// 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; | ||
let data = args[0]; | ||
// @ts-expect-error idk why it errors? | ||
if (data.buffer) | ||
data = data.buffer; | ||
channel.port1.postMessage({ type: "data", data: data }, data instanceof ArrayBuffer ? [data] : []); | ||
}; | ||
socket.close = function (code, reason) { | ||
channel.port1.postMessage({ type: "close", closeCode: code, closeReason: reason }); | ||
}; | ||
Object.defineProperty(socket, 'url', { | ||
get: () => remote.toString(), | ||
configurable: true, | ||
enumerable: true, | ||
}); | ||
const getProtocol = () => fakeProtocol; | ||
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 = 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; | ||
} | ||
for (let i = 0;; i++) { | ||
if ('host' in headers) | ||
headers.host = urlO.host; | ||
else | ||
headers.Host = urlO.host; | ||
let resp = (await this.worker.sendMessage({ | ||
type: "fetch", | ||
fetch: { | ||
remote: urlO.toString(), | ||
method: req.method, | ||
headers: headers, | ||
body: body || undefined, | ||
}, | ||
}, body ? [body] : [])).fetch; | ||
let responseobj = new Response(statusEmpty.includes(resp.status) ? undefined : resp.body, { | ||
headers: new Headers(resp.headers), | ||
status: resp.status, | ||
statusText: resp.statusText, | ||
}); | ||
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; | ||
} | ||
} | ||
} | ||
} | ||
/* | ||
* 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; | ||
} | ||
exports.BareClient = BareClient; | ||
exports.BareMuxConnection = BareMuxConnection; | ||
exports.WebSocketFields = WebSocketFields; | ||
exports.WorkerConnection = WorkerConnection; | ||
exports.default = BareClient; | ||
exports.maxRedirects = maxRedirects; | ||
exports.validProtocol = validProtocol; | ||
// get the unhooked value | ||
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 = [], webSocketImpl, requestHeaders, arrayBufferImpl) { | ||
let switcher = findSwitcher(); | ||
let client = switcher.active; | ||
if (!client) | ||
throw "there are no bare clients"; | ||
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 = (webSocketImpl || WebSocket); | ||
const socket = new wsImpl("ws://127.0.0.1:1", protocols); | ||
let fakeProtocol = ''; | ||
let fakeReadyState = WebSocketFields.CONNECTING; | ||
let initialErrorHappened = false; | ||
socket.addEventListener("error", (e) => { | ||
if (!initialErrorHappened) { | ||
fakeReadyState = WebSocket.CONNECTING; | ||
e.stopImmediatePropagation(); | ||
initialErrorHappened = true; | ||
} | ||
}); | ||
let initialCloseHappened = false; | ||
socket.addEventListener("close", (e) => { | ||
if (!initialCloseHappened) { | ||
e.stopImmediatePropagation(); | ||
initialCloseHappened = true; | ||
} | ||
}); | ||
// TODO socket onerror will be broken | ||
arrayBufferImpl = arrayBufferImpl || webSocketImpl.constructor.constructor("return ArrayBuffer")().prototype; | ||
requestHeaders['Host'] = (new URL(remote)).host; | ||
// requestHeaders['Origin'] = origin; | ||
requestHeaders['Pragma'] = 'no-cache'; | ||
requestHeaders['Cache-Control'] = 'no-cache'; | ||
requestHeaders['Upgrade'] = 'websocket'; | ||
// requestHeaders['User-Agent'] = navigator.userAgent; | ||
requestHeaders['Connection'] = 'Upgrade'; | ||
const websocket = client.connect(remote, origin, protocols, requestHeaders, (protocol) => { | ||
fakeReadyState = WebSocketFields.OPEN; | ||
fakeProtocol = protocol; | ||
socket.meta = { | ||
headers: { | ||
"sec-websocket-protocol": protocol, | ||
} | ||
}; // what the fuck is a meta | ||
socket.dispatchEvent(new Event("open")); | ||
}, async (payload) => { | ||
if (typeof payload === "string") { | ||
socket.dispatchEvent(new MessageEvent("message", { data: payload })); | ||
} | ||
else if ("byteLength" in payload) { | ||
if (socket.binaryType === "blob") { | ||
payload = new Blob([payload]); | ||
} | ||
else { | ||
Object.setPrototypeOf(payload, arrayBufferImpl); | ||
} | ||
socket.dispatchEvent(new MessageEvent("message", { data: payload })); | ||
} | ||
else if ("arrayBuffer" in payload) { | ||
if (socket.binaryType === "arraybuffer") { | ||
payload = await payload.arrayBuffer(); | ||
Object.setPrototypeOf(payload, arrayBufferImpl); | ||
} | ||
socket.dispatchEvent(new MessageEvent("message", { data: payload })); | ||
} | ||
}, (code, reason) => { | ||
fakeReadyState = WebSocketFields.CLOSED; | ||
socket.dispatchEvent(new CloseEvent("close", { code, reason })); | ||
}, () => { | ||
fakeReadyState = WebSocketFields.CLOSED; | ||
socket.dispatchEvent(new Event("error")); | ||
}); | ||
const sendData = websocket[0]; | ||
const close = websocket[1]; | ||
// 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 = () => fakeReadyState; | ||
// 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."); | ||
}; | ||
// 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]); | ||
}; | ||
socket.close = function (code, reason) { | ||
close(code, reason); | ||
}; | ||
Object.defineProperty(socket, 'url', { | ||
get: () => remote.toString(), | ||
configurable: true, | ||
enumerable: true, | ||
}); | ||
const getProtocol = () => fakeProtocol; | ||
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) { | ||
// in race conditions we trust | ||
await new Promise(r => setTimeout(r, 1000)); | ||
switcher = findSwitcher(); | ||
} | ||
if (!switcher.active) | ||
throw "there are no bare clients"; | ||
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), | ||
status: resp.status, | ||
statusText: resp.statusText, | ||
}); | ||
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; | ||
} | ||
} | ||
} | ||
} | ||
Object.defineProperty(exports, '__esModule', { value: true }); | ||
export { BareClient, SetSingletonTransport, SetTransport, WebSocketFields, BareClient as default, findSwitcher, maxRedirects, registerRemoteListener }; | ||
})); | ||
//# sourceMappingURL=index.js.map |
@@ -30,8 +30,9 @@ export declare const fetch: typeof globalThis.fetch; | ||
}; | ||
export declare const SharedWorker: { | ||
new (scriptURL: string | URL, options?: string | WorkerOptions): SharedWorker; | ||
prototype: SharedWorker; | ||
}; | ||
export declare const WebSocketFields: { | ||
prototype: { | ||
send: { | ||
(data: string | ArrayBufferLike | Blob | ArrayBufferView): void; | ||
(data: string | ArrayBufferLike | Blob | ArrayBufferView): void; | ||
}; | ||
send: (data: string | ArrayBufferLike | Blob | ArrayBufferView) => void; | ||
}; | ||
@@ -38,0 +39,0 @@ CLOSED: 3; |
declare const baremuxPath: string; | ||
export { baremuxPath }; | ||
export { baremuxPath }; |
{ | ||
"name": "@mercuryworkshop/bare-mux", | ||
"version": "1.1.4", | ||
"version": "2.0.0", | ||
"description": "", | ||
"type": "module", | ||
"author": "", | ||
"main": "dist/index.cjs", | ||
"types": "dist/index.d.ts", | ||
"files": [ | ||
"dist", | ||
"lib" | ||
], | ||
"exports": { | ||
".": { | ||
"import": "./dist/index.js", | ||
"require": "./dist/bare.cjs", | ||
"import": "./dist/module.js", | ||
"require": "./dist/index.js", | ||
"types": "./dist/index.d.ts" | ||
}, | ||
"./node": { | ||
"types": "./lib/index.d.ts", | ||
"import": "./lib/index.cjs", | ||
"require": "./lib/index.cjs" | ||
"require": "./lib/index.cjs", | ||
"types": "./lib/index.d.ts" | ||
} | ||
}, | ||
"files": [ | ||
"dist", | ||
"lib" | ||
], | ||
"devDependencies": { | ||
"@rollup/plugin-inject": "^5.0.5", | ||
"esbuild": "^0.19.11", | ||
"esbuild-plugin-d.ts": "^1.2.2", | ||
"@rollup/plugin-replace": "^5.0.5", | ||
"rollup": "^4.9.6", | ||
"rollup-plugin-typescript2": "^0.36.0", | ||
"@rollup/plugin-replace": "^5.0.5" | ||
"rollup-plugin-typescript2": "^0.36.0" | ||
}, | ||
"dependencies": { | ||
"@types/uuid": "^9.0.8", | ||
"uuid": "^9.0.1" | ||
}, | ||
"scripts": { | ||
"build": "rollup -c" | ||
"build": "rollup -c", | ||
"watch": "rollup -cw" | ||
} | ||
} |
@@ -40,10 +40,19 @@ # Bare-Mux | ||
To switch between transports, use the `SetTransport` function. | ||
Here is an example of using bare-mux: | ||
```js | ||
import { SetTransport } from '@mercuryworkshop/bare-mux'; | ||
/// As an end-user | ||
import { BareMuxConnection } from "@mercuryworkshop/bare-mux"; | ||
const conn = new BareMuxConnection("/bare-mux/worker.js"); | ||
SetTransport("EpxMod.EpoxyClient", { wisp: "wss://wisp.mercurywork.shop" }); | ||
SetTransport("BareMod.BareClient", "https://some-bare-server.com"); | ||
// Set Bare-Client transport | ||
// If your transport is an ES module and exports the class as the default export | ||
await conn.setTransport("/bare-mux/transport-module.js", ["arg1", "ws://localhost:4000"]); | ||
/// As a proxy developer | ||
import { BareClient } from "@mercuryworkshop/bare-mux"; | ||
const client = new BareClient(); | ||
// Fetch | ||
const resp = await client.fetch("https://example.com"); | ||
// Create websocket | ||
const ws = client.createWebSocket("wss://echo.websocket.events"); | ||
``` | ||
If not using a bundler, extract the npm package in releases, and include the `bare.cjs` file and call `BareMux.SetTransport`. |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Uses eval
Supply chain riskPackage uses dynamic code execution (e.g., eval()), which is a dangerous practice. This can prevent the code from running in certain environments and increases the risk that the code may contain exploits or malicious behavior.
Found 1 instance in 1 package
0
4
16
1113
58
3
50972
- Removed@types/uuid@^9.0.8
- Removeduuid@^9.0.1
- Removed@types/uuid@9.0.8(transitive)
- Removeduuid@9.0.1(transitive)