@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
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
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
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
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)