diff --git a/src/client/client.ts b/src/client/client.ts index c09b492..d43ad6a 100644 --- a/src/client/client.ts +++ b/src/client/client.ts @@ -13,6 +13,7 @@ import { decodeUrl, encodeUrl, } from "../shared"; +import type BareClientType from "@mercuryworkshop/bare-mux"; import { createWrapFn } from "./shared/wrap"; import { NavigateEvent } from "./events"; import type { URLMeta } from "../shared/rewriters/url"; @@ -60,7 +61,7 @@ export class ScramjetClient { globalProxy: any; locationProxy: any; serviceWorker: ServiceWorkerContainer; - bare: any; + bare: BareClientType; descriptors: Record = {}; natives: Record = {}; diff --git a/src/client/shared/requests/websocket.ts b/src/client/shared/requests/websocket.ts index 7eafc46..49eb295 100644 --- a/src/client/shared/requests/websocket.ts +++ b/src/client/shared/requests/websocket.ts @@ -1,20 +1,219 @@ +import { type BareWebSocket } from "@mercuryworkshop/bare-mux"; import { ScramjetClient } from "../../client"; +type FakeWebSocketState = { + extensions: string; + protocol: string; + url: string; + binaryType: string; + barews: BareWebSocket; + + captureListeners: Record; + listeners: Record; + + onclose?: (ev: CloseEvent) => any; + onerror?: (ev: Event) => any; + onmessage?: (ev: MessageEvent) => any; + onopen?: (ev: Event) => any; +}; export default function (client: ScramjetClient, self: typeof globalThis) { + const socketmap: WeakMap = new WeakMap(); client.Proxy("WebSocket", { construct(ctx) { - ctx.return( - client.bare.createWebSocket( - ctx.args[0], - ctx.args[1], - ctx.fn as typeof WebSocket, - { - "User-Agent": self.navigator.userAgent, - Origin: client.url.origin, + const fakeWebSocket = new EventTarget() as WebSocket; + Object.setPrototypeOf(fakeWebSocket, self.WebSocket.prototype); + fakeWebSocket.constructor = ctx.fn; + + const trustEvent = (ev: Event) => + new Proxy(ev, { + get(target, prop) { + if (prop === "isTrusted") return true; + return Reflect.get(target, prop); }, - ArrayBuffer.prototype - ) + }); + + const barews = client.bare.createWebSocket( + ctx.args[0], + ctx.args[1], + null, + { + "User-Agent": self.navigator.userAgent, + Origin: client.url.origin, + } ); + + const state: FakeWebSocketState = { + extensions: "", + protocol: "", + url: ctx.args[0], + binaryType: "blob", + barews, + + captureListeners: {}, + listeners: {}, + }; + + barews.addEventListener("open", (ev) => { + const fakeev = new Event("open"); + state.onopen?.(trustEvent(fakeev)); + fakeWebSocket.dispatchEvent(fakeev); + }); + + socketmap.set(fakeWebSocket, state); + }, + }); + + client.Proxy("EventTarget.prototype.addEventListener", { + apply(ctx) { + const ws = socketmap.get(ctx.this); + if (!ws) return; // it's not a websocket ignore it + + const [type, listener, opts] = ctx.args; + + if ( + (typeof opts === "object" && opts.capture) || + (typeof opts === "boolean" && opts) + ) { + const listeners = (ws.captureListeners[type] ??= []); + listeners.push(listener); + ws.captureListeners[type] = listeners; + } else { + const listeners = (ws.listeners[type] ??= []); + listeners.push(listener); + ws.listeners[type] = listeners; + } + + ctx.return(undefined); + }, + }); + + client.Proxy("EventTarget.prototype.removeEventListener", { + apply(ctx) { + const ws = socketmap.get(ctx.this); + if (!ws) return; + + const [type, listener, opts] = ctx.args; + + if ( + (typeof opts === "object" && opts.capture) || + (typeof opts === "boolean" && opts) + ) { + const listeners = (ws.captureListeners[type] ??= []); + const idx = listeners.indexOf(listener); + if (idx !== -1) listeners.splice(idx, 1); + ws.captureListeners[type] = listeners; + } else { + const listeners = (ws.listeners[type] ??= []); + const idx = listeners.indexOf(listener); + if (idx !== -1) listeners.splice(idx, 1); + ws.listeners[type] = listeners; + } + + ctx.return(undefined); + }, + }); + + client.Trap("WebSocket.prototype.binaryType", { + get(ctx) { + const ws = socketmap.get(ctx.this); + return ws.binaryType; + }, + set(ctx, v: string) { + const ws = socketmap.get(ctx.this); + if (v === "blob" || v === "arraybuffer") ws.binaryType = v; + }, + }); + + client.Trap("WebSocket.prototype.bufferedAmount", { + get() { + return 0; + }, + }); + + client.Trap("WebSocket.prototype.extensions", { + get(ctx) { + const ws = socketmap.get(ctx.this); + return ws.extensions; + }, + }); + + client.Trap("WebSocket.prototype.onclose", { + get(ctx) { + const ws = socketmap.get(ctx.this); + return ws.onclose; + }, + set(ctx, v: (ev: CloseEvent) => any) { + const ws = socketmap.get(ctx.this); + ws.onclose = v; + }, + }); + + client.Trap("WebSocket.prototype.onerror", { + get(ctx) { + const ws = socketmap.get(ctx.this); + return ws.onerror; + }, + set(ctx, v: (ev: Event) => any) { + const ws = socketmap.get(ctx.this); + ws.onerror = v; + }, + }); + + client.Trap("WebSocket.prototype.onmessage", { + get(ctx) { + const ws = socketmap.get(ctx.this); + return ws.onmessage; + }, + set(ctx, v: (ev: MessageEvent) => any) { + const ws = socketmap.get(ctx.this); + ws.onmessage = v; + }, + }); + + client.Trap("WebSocket.prototype.onopen", { + get(ctx) { + const ws = socketmap.get(ctx.this); + return ws.onopen; + }, + set(ctx, v: (ev: Event) => any) { + const ws = socketmap.get(ctx.this); + ws.onopen = v; + }, + }); + + client.Trap("WebSocket.prototype.url", { + get(ctx) { + const ws = socketmap.get(ctx.this); + return ws.url; + }, + }); + + client.Trap("WebSocket.prototype.protocol", { + get(ctx) { + const ws = socketmap.get(ctx.this); + return ws.protocol; + }, + }); + + client.Trap("WebSocket.prototype.readyState", { + get(ctx) { + const ws = socketmap.get(ctx.this); + return ws.barews.readyState; + }, + }); + + client.Proxy("WebSocket.prototype.send", { + apply(ctx) { + const ws = socketmap.get(ctx.this); + + ctx.return(ws.barews.send(ctx.args[0])); + }, + }); + + client.Proxy("WebSocket.prototype.close", { + apply(ctx) { + const ws = socketmap.get(ctx.this); + ctx.return(ws.barews.close(ctx.args[0], ctx.args[1])); }, }); } diff --git a/src/client/shared/unproxy.ts b/src/client/shared/unproxy.ts index 058e366..9905c2a 100644 --- a/src/client/shared/unproxy.ts +++ b/src/client/shared/unproxy.ts @@ -7,6 +7,7 @@ export const order = 3; export default function (client: ScramjetClient, self: typeof window) { // an automated approach to cleaning the documentProxy from dom functions // it will trigger an illegal invocation if you pass the proxy to c++ code, we gotta hotswap it out with the real one + // admittedly this is pretty slow. worth investigating if there's ways to get back some of the lost performance for (const target of [self]) { for (const prop in target) {