mirror of
https://github.com/titaniumnetwork-dev/Ultraviolet.git
synced 2025-05-16 13:00:01 -04:00
V3 support, WebSocketApi
brought back the old WebSocket hook API and added hooks for send() and readyState
This commit is contained in:
parent
56bf6fc6b8
commit
a8c84e8926
6 changed files with 179 additions and 224 deletions
14
package-lock.json
generated
14
package-lock.json
generated
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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);
|
||||
|
|
108
src/client/requests/websocket.js
Normal file
108
src/client/requests/websocket.js
Normal 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;
|
|
@ -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';
|
||||
|
||||
/**
|
||||
|
|
|
@ -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();
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue