diff --git a/package-lock.json b/package-lock.json index b9fb653..73a009d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "1.0.11", "license": "MIT", "dependencies": { - "@tomphttp/bare-client": "^1.1.2-beta.3", + "@tomphttp/bare-client": "^2.0.0-beta", "css-tree": "^2.0.4", "esotope-hammerhead": "^0.6.1", "events": "^3.3.0", @@ -190,9 +190,9 @@ } }, "node_modules/@tomphttp/bare-client": { - "version": "1.1.2-beta.3", - "resolved": "https://registry.npmjs.org/@tomphttp/bare-client/-/bare-client-1.1.2-beta.3.tgz", - "integrity": "sha512-WyIVnSAqzfrLejmOhh/l/LtDOeK+SHnBGi/z+QyliVP1T1JxoNE5eecwxlV+osM9J6FTAYVGNHr8/5bubaIj6Q==" + "version": "2.0.0-beta", + "resolved": "https://registry.npmjs.org/@tomphttp/bare-client/-/bare-client-2.0.0-beta.tgz", + "integrity": "sha512-M2ap0V4DwIdc+gtiiAN8GFqiXDi81iOc+fu4JZGQTIa4Y4gIQVN9bFybFm0hz23QjfqSiFYfHO9o/BhQOo5bSQ==" }, "node_modules/@types/eslint": { "version": "8.4.6", @@ -2964,9 +2964,9 @@ } }, "@tomphttp/bare-client": { - "version": "1.1.2-beta.3", - "resolved": "https://registry.npmjs.org/@tomphttp/bare-client/-/bare-client-1.1.2-beta.3.tgz", - "integrity": "sha512-WyIVnSAqzfrLejmOhh/l/LtDOeK+SHnBGi/z+QyliVP1T1JxoNE5eecwxlV+osM9J6FTAYVGNHr8/5bubaIj6Q==" + "version": "2.0.0-beta", + "resolved": "https://registry.npmjs.org/@tomphttp/bare-client/-/bare-client-2.0.0-beta.tgz", + "integrity": "sha512-M2ap0V4DwIdc+gtiiAN8GFqiXDi81iOc+fu4JZGQTIa4Y4gIQVN9bFybFm0hz23QjfqSiFYfHO9o/BhQOo5bSQ==" }, "@types/eslint": { "version": "8.4.6", diff --git a/package.json b/package.json index 538d6a0..796093d 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "watch": "cross-env NODE_ENV=development webpack-cli --watch" }, "dependencies": { - "@tomphttp/bare-client": "^1.1.2-beta.3", + "@tomphttp/bare-client": "^2.0.0-beta", "css-tree": "^2.0.4", "esotope-hammerhead": "^0.6.1", "events": "^3.3.0", diff --git a/src/client/index.js b/src/client/index.js index edec6c9..181e345 100644 --- a/src/client/index.js +++ b/src/client/index.js @@ -17,12 +17,19 @@ import EventEmitter from 'events'; import StorageApi from './storage.js'; import StyleApi from './dom/style.js'; import IDBApi from './idb.js'; +import WebSocketApi from './requests/websocket.js'; class UVClient extends EventEmitter { - constructor(window = self, worker = !window.window) { + /** + * + * @param {typeof globalThis} window + * @param {import('@tomphttp/bare-client').BareClient} bareClient + * @param {boolean} worker + */ + constructor(window = self, bareClient, worker = !window.window) { super(); /** - * @type {typeof self} + * @type {typeof globalThis} */ this.window = window; this.nativeMethods = { @@ -42,6 +49,7 @@ class UVClient extends EventEmitter { Proxy: this.window.Proxy, }; this.worker = worker; + this.bareClient = bareClient; this.fetch = new Fetch(this); this.xhr = new Xhr(this); this.idb = new IDBApi(this); @@ -51,6 +59,7 @@ class UVClient extends EventEmitter { this.document = new DocumentHook(this); this.function = new FunctionHook(this); this.object = new ObjectHook(this); + this.websocket = new WebSocketApi(this); this.message = new MessageApi(this); this.navigator = new NavigatorApi(this); this.eventSource = new EventSourceApi(this); diff --git a/src/client/requests/websocket.js b/src/client/requests/websocket.js new file mode 100644 index 0000000..334d642 --- /dev/null +++ b/src/client/requests/websocket.js @@ -0,0 +1,108 @@ +import EventEmitter from 'events'; +import HookEvent from '../hook.js'; + +/** + * @typedef {import('../index').default} UVClient + */ + +class WebSocketApi extends EventEmitter { + /** + * + * @param {UVClient} ctx + */ + constructor(ctx) { + super(); + this.ctx = ctx; + this.window = ctx.window; + this.WebSocket = this.window.WebSocket || {}; + this.wsProto = this.WebSocket.prototype || {}; + this.url = ctx.nativeMethods.getOwnPropertyDescriptor( + this.wsProto, + 'url' + ); + this.protocol = ctx.nativeMethods.getOwnPropertyDescriptor( + this.wsProto, + 'protocol' + ); + this.readyState = ctx.nativeMethods.getOwnPropertyDescriptor( + this.wsProto, + 'readyState' + ); + this.send = this.wsProto.send; + this.CONNECTING = WebSocket.CONNECTING; + this.OPEN = WebSocket.OPEN; + this.CLOSING = WebSocket.CLOSING; + this.CLOSED = WebSocket.CLOSED; + } + overrideWebSocket() { + this.ctx.override( + this.window, + 'WebSocket', + (target, that, args) => { + if (!args.length) return new target(...args); + // just give the listeners direct access to the arguments + // an error occurs with too little arguments, listeners should be able to catch that + const event = new HookEvent({ args }, target, that); + this.emit('websocket', event); + + if (event.intercepted) return event.returnValue; + return new event.target(event.data.url, event.data.protocols); + }, + true + ); + + this.window.WebSocket.CONNECTING = this.CONNECTING; + this.window.WebSocket.OPEN = this.OPEN; + this.window.WebSocket.CLOSING = this.CLOSING; + this.window.WebSocket.CLOSED = this.CLOSED; + } + overrideURL() { + this.ctx.overrideDescriptor(this.wsProto, 'url', { + get: (target, that) => { + const event = new HookEvent( + { value: target.call(that) }, + target, + that + ); + this.emit('url', event); + return event.data.value; + }, + }); + } + overrideProtocol() { + this.ctx.overrideDescriptor(this.wsProto, 'protocol', { + get: (target, that) => { + const event = new HookEvent( + { value: target.call(that) }, + target, + that + ); + this.emit('protocol', event); + return event.data.value; + }, + }); + } + overrideReadyState() { + this.ctx.overrideDescriptor(this.wsProto, 'readyState', { + get: (target, that) => { + const event = new HookEvent( + { value: target.call(that) }, + target, + that + ); + this.emit('readyState', event); + return event.data.value; + }, + }); + } + overrideSend() { + this.ctx.override(this.wsProto, 'send', (target, that, args) => { + const event = new HookEvent({ args }, target, that); + this.emit('send', event); + if (event.intercepted) return event.returnValue; + return event.target.call(event.that, event.data.args); + }); + } +} + +export default WebSocketApi; diff --git a/src/rewrite/index.js b/src/rewrite/index.js index 4354a23..2ac8ce3 100644 --- a/src/rewrite/index.js +++ b/src/rewrite/index.js @@ -34,7 +34,7 @@ import { wrapEval, } from './rewrite.script.js'; import { openDB } from 'idb'; -import BareClient from '@tomphttp/bare-client'; +import { BareClient } from '@tomphttp/bare-client'; import EventEmitter from 'events'; /** diff --git a/src/uv.handler.js b/src/uv.handler.js index 3924ed9..76bc4fc 100644 --- a/src/uv.handler.js +++ b/src/uv.handler.js @@ -62,7 +62,10 @@ function __uvHook(window) { config.construct(__uv, worker ? 'worker' : 'window'); }*/ - const client = new UVClient(window); + // websockets + const bareClient = new Ultraviolet.BareClient(__uv$bareURL, __uv$bareData); + + const client = new UVClient(window, bareClient, worker); const { HTMLMediaElement, HTMLScriptElement, @@ -108,11 +111,6 @@ function __uvHook(window) { __uv.localStorageObj = {}; __uv.sessionStorageObj = {}; - // websockets - const bareClient = new Ultraviolet.BareClient(__uv$bareURL, __uv$bareData); - - __uv.bareClient = bareClient; - if (__uv.location.href === 'about:srcdoc') { __uv.meta = window.parent.__uv.meta; } @@ -1017,223 +1015,57 @@ function __uvHook(window) { } }); - function eventTarget(target, event) { - const property = `on${event}`; - const listeners = new WeakMap(); + client.websocket.on('websocket', async (event) => { + const requestHeaders = Object.create(null); + requestHeaders['Origin'] = __uv.meta.url.origin; + requestHeaders['User-Agent'] = navigator.userAgent; - Reflect.defineProperty(target, property, { - enumerable: true, - configurable: true, - get() { - if (listeners.has(this)) { - return listeners.get(this); - } else { - return null; - } - }, - set(value) { - if (typeof value == 'function') { - if (listeners.has(this)) { - this.removeEventListener(event, listeners.get(this)); - } + if (cookieStr !== '') requestHeaders['Cookie'] = cookieStr.toString(); - listeners.set(this, value); - this.addEventListener(event, value); - } + const socket = bareClient.createWebSocket( + event.data.args[0], + event.data.args[1], + requestHeaders, + (socket, getReadyState) => { + socket.__uv$getReadyState = getReadyState; }, + (socket, getSendError) => { + socket.__uv$getSendError = getSendError; + }, + event.target + ); + + socket.addEventListener('meta', (event) => { + event.preventDefault(); + // prevent event from being exposed to clients + event.stopPropagation(); + socket.__uv$socketMeta = event.meta; }); - } - const wsProtocols = ['ws:', 'wss:']; + event.respondWith(socket); + }); - class MockWebSocket extends EventTarget { - /** - * @type {import("@tomphttp/bare-client").BareWebSocket} - */ - #socket; - #ready; - #binaryType = 'blob'; - #protocol = ''; - #extensions = ''; - #url = ''; - /** - * - * @param {URL} remote - * @param {any} protocol - */ - async #open(url, protocol) { - const requestHeaders = {}; - Reflect.setPrototypeOf(requestHeaders, null); + client.websocket.on('url', (event) => { + if ('__uv$socketMeta' in event.that) + event.data.value = event.that.__uv$socketMeta.url; + }); - requestHeaders['Origin'] = __uv.meta.url.origin; - requestHeaders['User-Agent'] = navigator.userAgent; + client.websocket.on('protocol', (event) => { + if ('__uv$socketMeta' in event.that) + event.data.value = event.that.__uv$socketMeta.protocol; + }); - if (cookieStr !== '') - requestHeaders['Cookie'] = cookieStr.toString(); + client.websocket.on('readyState', (event) => { + if ('__uv$getReadyState' in event.that) + event.data.value = event.that.__uv$getReadyState(); + }); - this.#socket = await bareClient.createWebSocket( - url, - requestHeaders, - protocol - ); - - this.#socket.binaryType = this.#binaryType; - - this.#socket.addEventListener('message', (event) => { - this.dispatchEvent(new MessageEvent('message', event)); - }); - - this.#socket.addEventListener('open', async (event) => { - this.dispatchEvent(new Event('open', event)); - }); - - this.#socket.addEventListener('error', (event) => { - this.dispatchEvent(new ErrorEvent('error', event)); - }); - - this.#socket.addEventListener('close', (event) => { - this.dispatchEvent(new Event('close', event)); - }); - - const meta = await this.#socket.meta; - - if (meta.headers.has('sec-websocket-protocol')) - this.#protocol = meta.headers.get('sec-websocket-protocol'); - - if (meta.headers.has('sec-websocket-extensions')) - this.#extensions = meta.headers.get('sec-websocket-extensions'); - - let setCookie = meta.rawHeaders['set-cookie'] || []; - if (!Array.isArray(setCookie)) setCookie = []; - // trip the hook - for (const cookie of setCookie) document.cookie = cookie; + client.websocket.on('send', (event) => { + if ('__uv$getSendError' in event.that) { + const error = event.that.__uv$getSendError(); + if (error) throw error; } - get url() { - return this.#url; - } - constructor(...args) { - super(); - - if (!args.length) - throw new DOMException( - `Failed to construct 'WebSocket': 1 argument required, but only 0 present.` - ); - - const [url, protocol] = args; - - let parsed; - - try { - parsed = new URL(url); - } catch (err) { - throw new DOMException( - `Faiiled to construct 'WebSocket': The URL '${url}' is invalid.` - ); - } - - if (!wsProtocols.includes(parsed.protocol)) { - throw new DOMException( - `Failed to construct 'WebSocket': The URL's scheme must be either 'ws' or 'wss'. '${parsed.protocol}' is not allowed.` - ); - } - - this.#ready = this.#open(parsed, protocol); - } - get protocol() { - return this.#protocol; - } - get extensions() { - return this.#extensions; - } - get readyState() { - if (this.#socket) { - return this.#socket.readyState; - } else { - return MockWebSocket.CONNECTING; - } - } - get binaryType() { - return this.#binaryType; - } - set binaryType(value) { - this.#binaryType = value; - - if (this.#socket) { - this.#socket.binaryType = value; - } - } - send(data) { - if (!this.#socket) { - throw new DOMException( - `Failed to execute 'send' on 'WebSocket': Still in CONNECTING state.` - ); - } - this.#socket.send(data); - } - close(code, reason) { - if (typeof code !== 'undefined') { - if (typeof code !== 'number') { - code = 0; - } - - if (code !== 1000 && (code < 3000 || code > 4999)) { - throw new DOMException( - `Failed to execute 'close' on 'WebSocket': The code must be either 1000, or between 3000 and 4999. ${code} is neither.` - ); - } - } - - this.#ready.then(() => this.#socket.close(code, reason)); - } - } - - eventTarget(MockWebSocket.prototype, 'close'); - eventTarget(MockWebSocket.prototype, 'open'); - eventTarget(MockWebSocket.prototype, 'message'); - eventTarget(MockWebSocket.prototype, 'error'); - - for (const hook of [ - 'url', - 'protocol', - 'extensions', - 'readyState', - 'binaryType', - ]) { - const officialDesc = Object.getOwnPropertyDescriptor( - window.WebSocket.prototype, - hook - ); - const customDesc = Object.getOwnPropertyDescriptor( - MockWebSocket.prototype, - hook - ); - - if (customDesc?.get && officialDesc?.get) - client.emit('wrap', customDesc.get, officialDesc.get); - - if (customDesc?.set && officialDesc?.set) - client.emit('wrap', customDesc.get, officialDesc.get); - } - - client.emit( - 'wrap', - window.WebSocket.prototype.send, - MockWebSocket.prototype.send - ); - client.emit( - 'wrap', - window.WebSocket.prototype.close, - MockWebSocket.prototype.close - ); - - client.override( - window, - 'WebSocket', - (target, that, args) => new MockWebSocket(...args), - true - ); - - MockWebSocket.prototype.constructor = window.WebSocket; + }); client.function.on('function', (event) => { event.data.script = __uv.rewriteJS(event.data.script); @@ -1425,6 +1257,12 @@ function __uvHook(window) { client.history.overrideReplaceState(); client.eventSource.overrideConstruct(); client.eventSource.overrideUrl(); + client.websocket.overrideWebSocket(); + client.websocket.overrideProtocol(); + client.websocket.overrideURL(); + client.websocket.overrideReadyState(); + client.websocket.overrideProtocol(); + client.websocket.overrideSend(); client.url.overrideObjectURL(); client.document.overrideCookie(); client.message.overridePostMessage();