V3 support, WebSocketApi

brought back the old WebSocket hook API
and added hooks for send() and readyState
This commit is contained in:
David Reed 2023-06-08 17:11:11 -04:00
parent 56bf6fc6b8
commit a8c84e8926
No known key found for this signature in database
GPG key ID: 2211691D8A1EE72F
6 changed files with 179 additions and 224 deletions

14
package-lock.json generated
View file

@ -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",

View file

@ -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",

View file

@ -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);

View file

@ -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;

View file

@ -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';
/**

View file

@ -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();