mirror of
https://github.com/titaniumnetwork-dev/Ultraviolet.git
synced 2025-05-15 20:40:01 -04:00
uv
This commit is contained in:
parent
b9b6aee734
commit
82f5f76588
66 changed files with 74967 additions and 1 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -1 +1 @@
|
||||||
./node_modules
|
./node_modules
|
17
bundle.js
Normal file
17
bundle.js
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
import webpack from "webpack";
|
||||||
|
import path from "path";
|
||||||
|
|
||||||
|
const __dirname = path.resolve(path.dirname(decodeURI(new URL(import.meta.url).pathname))).slice(3);
|
||||||
|
|
||||||
|
console.log(__dirname);
|
||||||
|
|
||||||
|
webpack({
|
||||||
|
mode: 'none',
|
||||||
|
entry: path.join(__dirname, './rewrite/index.js'),
|
||||||
|
output: {
|
||||||
|
path: __dirname,
|
||||||
|
filename: './lib/uv.bundle.js',
|
||||||
|
}
|
||||||
|
}, (err, i) =>
|
||||||
|
console.log(!err ? 'Ultraviolet bundled!' : e)
|
||||||
|
);
|
44
client/dom/attr.js
Normal file
44
client/dom/attr.js
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
import EventEmitter from "../events.js";
|
||||||
|
import HookEvent from "../hook.js";
|
||||||
|
|
||||||
|
class AttrApi extends EventEmitter {
|
||||||
|
constructor(ctx) {
|
||||||
|
super();
|
||||||
|
this.ctx = ctx;
|
||||||
|
this.window = ctx.window;
|
||||||
|
this.Attr = this.window.Attr || {};
|
||||||
|
this.attrProto = this.Attr.prototype || {};
|
||||||
|
this.value = ctx.nativeMethods.getOwnPropertyDescriptor(this.attrProto, 'value');
|
||||||
|
this.name = ctx.nativeMethods.getOwnPropertyDescriptor(this.attrProto, 'name');
|
||||||
|
};
|
||||||
|
override() {
|
||||||
|
this.ctx.overrideDescriptor(this.attrProto, 'name', {
|
||||||
|
get: (target, that) => {
|
||||||
|
const event = new HookEvent({ value: target.call(that) }, target, that);
|
||||||
|
this.emit('name', event);
|
||||||
|
|
||||||
|
if (event.intercepted) return event.returnValue;
|
||||||
|
return event.data.value;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
this.ctx.overrideDescriptor(this.attrProto, 'value', {
|
||||||
|
get: (target, that) => {
|
||||||
|
const event = new HookEvent({ name: this.name.get.call(that), value: target.call(that) }, target, that);
|
||||||
|
this.emit('getValue', event);
|
||||||
|
|
||||||
|
if (event.intercepted) return event.returnValue;
|
||||||
|
return event.data.value;
|
||||||
|
},
|
||||||
|
set: (target, that, [ val ]) => {
|
||||||
|
const event = new HookEvent({ name: this.name.get.call(that), value: val }, target, that);
|
||||||
|
this.emit('setValue', event);
|
||||||
|
|
||||||
|
if (event.intercepted) return event.returnValue;
|
||||||
|
event.target.call(event.that, event.data.value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AttrApi;
|
184
client/dom/document.js
Normal file
184
client/dom/document.js
Normal file
|
@ -0,0 +1,184 @@
|
||||||
|
import EventEmitter from "../events.js";
|
||||||
|
import HookEvent from "../hook.js";
|
||||||
|
|
||||||
|
class DocumentHook extends EventEmitter {
|
||||||
|
constructor(ctx) {
|
||||||
|
super();
|
||||||
|
this.ctx = ctx;
|
||||||
|
this.window = ctx.window;
|
||||||
|
this.document = this.window.document;
|
||||||
|
this.Document = this.window.Document || {};
|
||||||
|
this.DOMParser = this.window.DOMParser || {};
|
||||||
|
this.docProto = this.Document.prototype || {};
|
||||||
|
this.domProto = this.DOMParser.prototype || {};
|
||||||
|
this.title = ctx.nativeMethods.getOwnPropertyDescriptor(this.docProto, 'title');
|
||||||
|
this.cookie = ctx.nativeMethods.getOwnPropertyDescriptor(this.docProto, 'cookie');
|
||||||
|
this.referrer = ctx.nativeMethods.getOwnPropertyDescriptor(this.docProto, 'referrer');
|
||||||
|
this.domain = ctx.nativeMethods.getOwnPropertyDescriptor(this.docProto, 'domain');
|
||||||
|
this.documentURI = ctx.nativeMethods.getOwnPropertyDescriptor(this.docProto, 'documentURI');
|
||||||
|
this.write = this.docProto.write;
|
||||||
|
this.writeln = this.docProto.writeln;
|
||||||
|
this.querySelector = this.docProto.querySelector;
|
||||||
|
this.querySelectorAll = this.docProto.querySelectorAll;
|
||||||
|
this.parseFromString = this.domProto.parseFromString;
|
||||||
|
this.URL = ctx.nativeMethods.getOwnPropertyDescriptor(this.docProto, 'URL');
|
||||||
|
};
|
||||||
|
overrideParseFromString() {
|
||||||
|
this.ctx.override(this.domProto, 'parseFromString', (target, that, args) => {
|
||||||
|
if (2 > args.length) return target.apply(that, args);
|
||||||
|
let [ string, type ] = args;
|
||||||
|
|
||||||
|
const event = new HookEvent({ string, type }, target, that);
|
||||||
|
this.emit('parseFromString', event);
|
||||||
|
|
||||||
|
if (event.intercepted) return event.returnValue;
|
||||||
|
return event.target.call(event.that, event.data.string, event.data.type);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
overrideQuerySelector() {
|
||||||
|
this.ctx.override(this.docProto, 'querySelector', (target, that, args) => {
|
||||||
|
if (!args.length) return target.apply(that, args);
|
||||||
|
let [ selectors ] = args;
|
||||||
|
|
||||||
|
const event = new HookEvent({ selectors }, target, that);
|
||||||
|
this.emit('querySelector', event);
|
||||||
|
|
||||||
|
if (event.intercepted) return event.returnValue;
|
||||||
|
return event.target.call(event.that, event.data.selectors);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
overrideDomain() {
|
||||||
|
this.ctx.overrideDescriptor(this.docProto, 'domain', {
|
||||||
|
get: (target, that) => {
|
||||||
|
const event = new HookEvent({ value: target.call(that) }, target, that);
|
||||||
|
this.emit('getDomain', event);
|
||||||
|
|
||||||
|
if (event.intercepted) return event.returnValue;
|
||||||
|
return event.data.value;
|
||||||
|
},
|
||||||
|
set: (target, that, [ val ]) => {
|
||||||
|
const event = new HookEvent({ value: val }, target, that);
|
||||||
|
this.emit('setDomain', event);
|
||||||
|
|
||||||
|
if (event.intercepted) return event.returnValue;
|
||||||
|
return event.target.call(event.that, event.data.value);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
overrideReferrer() {
|
||||||
|
this.ctx.overrideDescriptor(this.docProto, 'referrer', {
|
||||||
|
get: (target, that) => {
|
||||||
|
const event = new HookEvent({ value: target.call(that) }, target, that);
|
||||||
|
this.emit('referrer', event);
|
||||||
|
|
||||||
|
if (event.intercepted) return event.returnValue;
|
||||||
|
return event.data.value;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
overrideCreateTreeWalker() {
|
||||||
|
this.ctx.override(this.docProto, 'createTreeWalker', (target, that, args) => {
|
||||||
|
if (!args.length) return target.apply(that, args);
|
||||||
|
let [ root, show = 0xFFFFFFFF, filter, expandEntityReferences ] = args;
|
||||||
|
|
||||||
|
const event = new HookEvent({ root, show, filter, expandEntityReferences }, target, that);
|
||||||
|
this.emit('createTreeWalker', event);
|
||||||
|
|
||||||
|
if (event.intercepted) return event.returnValue;
|
||||||
|
return event.target.call(event.that, event.data.root, event.data.show, event.data.filter, event.data.expandEntityReferences);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
overrideWrite() {
|
||||||
|
this.ctx.override(this.docProto, 'write', (target, that, args) => {
|
||||||
|
if (!args.length) return target.apply(that, args);
|
||||||
|
let [ ...html ] = args;
|
||||||
|
|
||||||
|
const event = new HookEvent({ html }, target, that);
|
||||||
|
this.emit('write', event);
|
||||||
|
|
||||||
|
if (event.intercepted) return event.returnValue;
|
||||||
|
return event.target.apply(event.that, event.data.html);
|
||||||
|
});
|
||||||
|
this.ctx.override(this.docProto, 'writeln', (target, that, args) => {
|
||||||
|
if (!args.length) return target.apply(that, args);
|
||||||
|
let [ ...html ] = args;
|
||||||
|
|
||||||
|
const event = new HookEvent({ html }, target, that);
|
||||||
|
this.emit('writeln', event);
|
||||||
|
|
||||||
|
if (event.intercepted) return event.returnValue;
|
||||||
|
return event.target.apply(event.that, event.data.html);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
overrideDocumentURI() {
|
||||||
|
this.ctx.overrideDescriptor(this.docProto, 'documentURI', {
|
||||||
|
get: (target, that) => {
|
||||||
|
const event = new HookEvent({ value: target.call(that) }, target, that);
|
||||||
|
this.emit('documentURI', event);
|
||||||
|
|
||||||
|
if (event.intercepted) return event.returnValue;
|
||||||
|
return event.data.value;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
overrideURL() {
|
||||||
|
this.ctx.overrideDescriptor(this.docProto, 'URL', {
|
||||||
|
get: (target, that) => {
|
||||||
|
const event = new HookEvent({ value: target.call(that) }, target, that);
|
||||||
|
this.emit('url', event);
|
||||||
|
|
||||||
|
if (event.intercepted) return event.returnValue;
|
||||||
|
return event.data.value;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
overrideReferrer() {
|
||||||
|
this.ctx.overrideDescriptor(this.docProto, 'referrer', {
|
||||||
|
get: (target, that) => {
|
||||||
|
const event = new HookEvent({ value: target.call(that) }, target, that);
|
||||||
|
this.emit('referrer', event);
|
||||||
|
|
||||||
|
if (event.intercepted) return event.returnValue;
|
||||||
|
return event.data.value;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
overrideCookie() {
|
||||||
|
this.ctx.overrideDescriptor(this.docProto, 'cookie', {
|
||||||
|
get: (target, that) => {
|
||||||
|
const event = new HookEvent({ value: target.call(that) }, target, that);
|
||||||
|
this.emit('getCookie', event);
|
||||||
|
|
||||||
|
if (event.intercepted) return event.returnValue;
|
||||||
|
return event.data.value;
|
||||||
|
},
|
||||||
|
set: (target, that, [ value ]) => {
|
||||||
|
const event = new HookEvent({ value, }, target, that);
|
||||||
|
this.emit('setCookie', event);
|
||||||
|
|
||||||
|
if (event.intercepted) return event.returnValue;
|
||||||
|
return event.target.call(event.that, event.data.value);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
overrideTitle() {
|
||||||
|
this.ctx.overrideDescriptor(this.docProto, 'title', {
|
||||||
|
get: (target, that) => {
|
||||||
|
const event = new HookEvent({ value: target.call(that) }, target, that);
|
||||||
|
this.emit('getTitle', event);
|
||||||
|
|
||||||
|
if (event.intercepted) return event.returnValue;
|
||||||
|
return event.data.value;
|
||||||
|
},
|
||||||
|
set: (target, that, [ value ]) => {
|
||||||
|
const event = new HookEvent({ value, }, target, that);
|
||||||
|
this.emit('setTitle', event);
|
||||||
|
|
||||||
|
if (event.intercepted) return event.returnValue;
|
||||||
|
return event.target.call(event.that, event.data.value);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DocumentHook;
|
165
client/dom/element.js
Normal file
165
client/dom/element.js
Normal file
|
@ -0,0 +1,165 @@
|
||||||
|
import EventEmitter from "../events.js";
|
||||||
|
import HookEvent from "../hook.js";
|
||||||
|
|
||||||
|
class ElementApi extends EventEmitter {
|
||||||
|
constructor(ctx) {
|
||||||
|
super();
|
||||||
|
this.ctx = ctx;
|
||||||
|
this.window = ctx.window;
|
||||||
|
this.Audio = this.window.Audio;
|
||||||
|
this.Element = this.window.Element;
|
||||||
|
this.elemProto = this.Element ? this.Element.prototype : {};
|
||||||
|
this.innerHTML = ctx.nativeMethods.getOwnPropertyDescriptor(this.elemProto, 'innerHTML');
|
||||||
|
this.outerHTML = ctx.nativeMethods.getOwnPropertyDescriptor(this.elemProto, 'outerHTML');
|
||||||
|
this.setAttribute = this.elemProto.setAttribute;
|
||||||
|
this.getAttribute = this.elemProto.getAttribute;
|
||||||
|
this.removeAttribute = this.elemProto.removeAttribute;
|
||||||
|
this.hasAttribute = this.elemProto.hasAttribute;
|
||||||
|
this.querySelector = this.elemProto.querySelector;
|
||||||
|
this.querySelectorAll = this.elemProto.querySelectorAll;
|
||||||
|
this.insertAdjacentHTML = this.elemProto.insertAdjacentHTML;
|
||||||
|
this.insertAdjacentText = this.elemProto.insertAdjacentText;
|
||||||
|
};
|
||||||
|
overrideQuerySelector() {
|
||||||
|
this.ctx.override(this.elemProto, 'querySelector', (target, that, args) => {
|
||||||
|
if (!args.length) return target.apply(that, args);
|
||||||
|
let [ selectors ] = args;
|
||||||
|
|
||||||
|
const event = new HookEvent({ selectors }, target, that);
|
||||||
|
this.emit('querySelector', event);
|
||||||
|
|
||||||
|
if (event.intercepted) return event.returnValue;
|
||||||
|
return event.target.call(event.that, event.data.selectors);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
overrideAttribute() {
|
||||||
|
this.ctx.override(this.elemProto, 'getAttribute', (target, that, args) => {
|
||||||
|
if (!args.length) return target.apply(that, args);
|
||||||
|
let [ name ] = args;
|
||||||
|
|
||||||
|
const event = new HookEvent({ name }, target, that);
|
||||||
|
this.emit('getAttribute', event);
|
||||||
|
|
||||||
|
if (event.intercepted) return event.returnValue;
|
||||||
|
return event.target.call(event.that, event.data.name);
|
||||||
|
});
|
||||||
|
this.ctx.override(this.elemProto, 'setAttribute', (target, that, args) => {
|
||||||
|
if (2 > args.length) return target.apply(that, args);
|
||||||
|
let [ name, value ] = args;
|
||||||
|
|
||||||
|
const event = new HookEvent({ name, value }, target, that);
|
||||||
|
this.emit('setAttribute', event);
|
||||||
|
|
||||||
|
if (event.intercepted) return event.returnValue;
|
||||||
|
return event.target.call(event.that, event.data.name, event.data.value);
|
||||||
|
});
|
||||||
|
this.ctx.override(this.elemProto, 'hasAttribute', (target, that, args) => {
|
||||||
|
if (!args.length) return target.apply(that, args);
|
||||||
|
let [ name ] = args;
|
||||||
|
|
||||||
|
const event = new HookEvent({ name }, target, that);
|
||||||
|
this.emit('hasAttribute', event);
|
||||||
|
|
||||||
|
if (event.intercepted) return event.returnValue;
|
||||||
|
return event.target.call(event.that, event.data.name);
|
||||||
|
});
|
||||||
|
this.ctx.override(this.elemProto, 'removeAttribute', (target, that, args) => {
|
||||||
|
if (!args.length) return target.apply(that, args);
|
||||||
|
let [ name ] = args;
|
||||||
|
|
||||||
|
const event = new HookEvent({ name }, target, that);
|
||||||
|
this.emit('removeAttribute', event);
|
||||||
|
|
||||||
|
if (event.intercepted) return event.returnValue;
|
||||||
|
return event.target.call(event.that, event.data.name);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
overrideAudio() {
|
||||||
|
this.ctx.override(this.window, 'Audio', (target, that, args) => {
|
||||||
|
if (!args.length) return new target(...args);
|
||||||
|
let [ url ] = args;
|
||||||
|
|
||||||
|
const event = new HookEvent({ url }, target, that);
|
||||||
|
this.emit('audio', event);
|
||||||
|
|
||||||
|
if (event.intercepted) return event.returnValue;
|
||||||
|
return new event.target(event.data.url);
|
||||||
|
}, true);
|
||||||
|
};
|
||||||
|
overrideHtml() {
|
||||||
|
this.hookProperty(this.Element, 'innerHTML', {
|
||||||
|
get: (target, that) => {
|
||||||
|
const event = new HookEvent({ value: target.call(that) }, target, that);
|
||||||
|
this.emit('getInnerHTML', event);
|
||||||
|
|
||||||
|
if (event.intercepted) return event.returnValue;
|
||||||
|
return event.data.value;
|
||||||
|
},
|
||||||
|
set: (target, that, [ val ]) => {
|
||||||
|
const event = new HookEvent({ value: val }, target, that);
|
||||||
|
this.emit('setInnerHTML', event);
|
||||||
|
|
||||||
|
if (event.intercepted) return event.returnValue;
|
||||||
|
target.call(that, event.data.value);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
this.hookProperty(this.Element, 'outerHTML', {
|
||||||
|
get: (target, that) => {
|
||||||
|
const event = new HookEvent({ value: target.call(that) }, target, that);
|
||||||
|
this.emit('getOuterHTML', event);
|
||||||
|
|
||||||
|
if (event.intercepted) return event.returnValue;
|
||||||
|
return event.data.value;
|
||||||
|
},
|
||||||
|
set: (target, that, [ val ]) => {
|
||||||
|
const event = new HookEvent({ value: val }, target, that);
|
||||||
|
this.emit('setOuterHTML', event);
|
||||||
|
|
||||||
|
if (event.intercepted) return event.returnValue;
|
||||||
|
target.call(that, event.data.value);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
overrideInsertAdjacentHTML() {
|
||||||
|
this.ctx.override(this.elemProto, 'insertAdjacentHTML', (target, that, args) => {
|
||||||
|
if (2 > args.length) return target.apply(that, args);
|
||||||
|
let [ position, html ] = args;
|
||||||
|
|
||||||
|
const event = new HookEvent({ position, html }, target, that);
|
||||||
|
this.emit('insertAdjacentHTML', event);
|
||||||
|
|
||||||
|
if (event.intercepted) return event.returnValue;
|
||||||
|
return event.target.call(event.that, event.data.position, event.data.html);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
overrideInsertAdjacentText() {
|
||||||
|
this.ctx.override(this.elemProto, 'insertAdjacentText', (target, that, args) => {
|
||||||
|
if (2 > args.length) return target.apply(that, args);
|
||||||
|
let [ position, text ] = args;
|
||||||
|
|
||||||
|
const event = new HookEvent({ position, text }, target, that);
|
||||||
|
this.emit('insertAdjacentText', event);
|
||||||
|
|
||||||
|
if (event.intercepted) return event.returnValue;
|
||||||
|
return event.target.call(event.that, event.data.position, event.data.text);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
hookProperty(element, prop, handler) {
|
||||||
|
if (!element || !prop in element) return false;
|
||||||
|
|
||||||
|
if (this.ctx.nativeMethods.isArray(element)) {
|
||||||
|
for (const elem of element) {
|
||||||
|
this.hookProperty(elem, prop, handler);
|
||||||
|
};
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const proto = element.prototype;
|
||||||
|
|
||||||
|
this.ctx.overrideDescriptor(proto, prop, handler);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ElementApi;
|
121
client/dom/node.js
Normal file
121
client/dom/node.js
Normal file
|
@ -0,0 +1,121 @@
|
||||||
|
import EventEmitter from "../events.js";
|
||||||
|
import HookEvent from "../hook.js";
|
||||||
|
|
||||||
|
class NodeApi extends EventEmitter {
|
||||||
|
constructor(ctx) {
|
||||||
|
super();
|
||||||
|
this.ctx = ctx;
|
||||||
|
this.window = ctx.window;
|
||||||
|
this.Node = ctx.window.Node || {};
|
||||||
|
this.nodeProto = this.Node.prototype || {};
|
||||||
|
this.compareDocumentPosition = this.nodeProto.compareDocumentPosition;
|
||||||
|
this.contains = this.nodeProto.contains;
|
||||||
|
this.insertBefore = this.nodeProto.insertBefore;
|
||||||
|
this.replaceChild = this.nodeProto.replaceChild;
|
||||||
|
this.append = this.nodeProto.append;
|
||||||
|
this.appendChild = this.nodeProto.appendChild;
|
||||||
|
this.removeChild = this.nodeProto.removeChild;
|
||||||
|
|
||||||
|
this.textContent = ctx.nativeMethods.getOwnPropertyDescriptor(this.nodeProto, 'textContent');
|
||||||
|
this.parentNode = ctx.nativeMethods.getOwnPropertyDescriptor(this.nodeProto, 'parentNode');
|
||||||
|
this.parentElement = ctx.nativeMethods.getOwnPropertyDescriptor(this.nodeProto, 'parentElement');
|
||||||
|
this.childNodes = ctx.nativeMethods.getOwnPropertyDescriptor(this.nodeProto, 'childNodes');
|
||||||
|
this.baseURI = ctx.nativeMethods.getOwnPropertyDescriptor(this.nodeProto, 'baseURI');
|
||||||
|
this.previousSibling = ctx.nativeMethods.getOwnPropertyDescriptor(this.nodeProto, 'previousSibling');
|
||||||
|
this.ownerDocument = ctx.nativeMethods.getOwnPropertyDescriptor(this.nodeProto, 'ownerDocument');
|
||||||
|
};
|
||||||
|
overrideTextContent() {
|
||||||
|
this.ctx.overrideDescriptor(this.nodeProto, 'textContent', {
|
||||||
|
get: (target, that) => {
|
||||||
|
const event = new HookEvent({ value: target.call(that) }, target, that);
|
||||||
|
this.emit('getTextContent', event);
|
||||||
|
|
||||||
|
if (event.intercepted) return event.returnValue;
|
||||||
|
return event.data.value;
|
||||||
|
},
|
||||||
|
set: (target, that, [ val ]) => {
|
||||||
|
const event = new HookEvent({ value: val }, target, that);
|
||||||
|
this.emit('setTextContent', event);
|
||||||
|
|
||||||
|
if (event.intercepted) return event.returnValue;
|
||||||
|
target.call(that, event.data.value);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
overrideAppend() {
|
||||||
|
this.ctx.override(this.nodeProto, 'append', (target, that, [ ...nodes ]) => {
|
||||||
|
const event = new HookEvent({ nodes }, target, that);
|
||||||
|
this.emit('append', event);
|
||||||
|
|
||||||
|
if (event.intercepted) return event.returnValue;
|
||||||
|
return event.target.call(event.that, event.data.nodes);
|
||||||
|
});
|
||||||
|
this.ctx.override(this.nodeProto, 'appendChild', (target, that, args) => {
|
||||||
|
if (!args.length) return target.apply(that, args);
|
||||||
|
let [ node ] = args;
|
||||||
|
|
||||||
|
const event = new HookEvent({ node }, target, that);
|
||||||
|
this.emit('appendChild', event);
|
||||||
|
|
||||||
|
if (event.intercepted) return event.returnValue;
|
||||||
|
return event.target.call(event.that, event.data.node);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
overrideBaseURI() {
|
||||||
|
this.ctx.overrideDescriptor(this.nodeProto, 'baseURI', {
|
||||||
|
get: (target, that) => {
|
||||||
|
const event = new HookEvent({ value: target.call(that) }, target, that);
|
||||||
|
this.emit('baseURI', event);
|
||||||
|
|
||||||
|
if (event.intercepted) return event.returnValue;
|
||||||
|
return event.data.value;
|
||||||
|
},
|
||||||
|
})
|
||||||
|
};
|
||||||
|
overrideParent() {
|
||||||
|
this.ctx.overrideDescriptor(this.nodeProto, 'parentNode', {
|
||||||
|
get: (target, that) => {
|
||||||
|
const event = new HookEvent({ node: target.call(that) }, target, that);
|
||||||
|
this.emit('parentNode', event);
|
||||||
|
|
||||||
|
if (event.intercepted) return event.returnValue;
|
||||||
|
return event.data.node;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
this.ctx.overrideDescriptor(this.nodeProto, 'parentElement', {
|
||||||
|
get: (target, that) => {
|
||||||
|
const event = new HookEvent({ element: target.call(that) }, target, that);
|
||||||
|
this.emit('parentElement', event);
|
||||||
|
|
||||||
|
if (event.intercepted) return event.returnValue;
|
||||||
|
return event.data.node;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
overrideOwnerDocument() {
|
||||||
|
this.ctx.overrideDescriptor(this.nodeProto, 'ownerDocument', {
|
||||||
|
get: (target, that) => {
|
||||||
|
const event = new HookEvent({ document: target.call(that) }, target, that);
|
||||||
|
this.emit('ownerDocument', event);
|
||||||
|
|
||||||
|
if (event.intercepted) return event.returnValue;
|
||||||
|
return event.data.document;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
overrideCompareDocumentPosit1ion() {
|
||||||
|
this.ctx.override(this.nodeProto, 'compareDocumentPosition', (target, that, args) => {
|
||||||
|
if (!args.length) return target.apply(that, args);
|
||||||
|
let [ node ] = args;
|
||||||
|
const event = new HookEvent({ node }, target, that);
|
||||||
|
|
||||||
|
if (event.intercepted) return event.returnValue;
|
||||||
|
return event.target.call(event.that, event.data.node);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
overrideChildMethods() {
|
||||||
|
this.ctx.override(this.nodeProto, 'removeChild')
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NodeApi;
|
497
client/events.js
Normal file
497
client/events.js
Normal file
|
@ -0,0 +1,497 @@
|
||||||
|
// Copyright Joyent, Inc. and other Node contributors.
|
||||||
|
//
|
||||||
|
// Permission is hereby granted, free of charge, to any person obtaining a
|
||||||
|
// copy of this software and associated documentation files (the
|
||||||
|
// "Software"), to deal in the Software without restriction, including
|
||||||
|
// without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
// distribute, sublicense, and/or sell copies of the Software, and to permit
|
||||||
|
// persons to whom the Software is furnished to do so, subject to the
|
||||||
|
// following conditions:
|
||||||
|
//
|
||||||
|
// The above copyright notice and this permission notice shall be included
|
||||||
|
// in all copies or substantial portions of the Software.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||||
|
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
|
||||||
|
// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||||
|
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||||
|
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
|
||||||
|
// USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var R = typeof Reflect === 'object' ? Reflect : null
|
||||||
|
var ReflectApply = R && typeof R.apply === 'function'
|
||||||
|
? R.apply
|
||||||
|
: function ReflectApply(target, receiver, args) {
|
||||||
|
return Function.prototype.apply.call(target, receiver, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
var ReflectOwnKeys
|
||||||
|
if (R && typeof R.ownKeys === 'function') {
|
||||||
|
ReflectOwnKeys = R.ownKeys
|
||||||
|
} else if (Object.getOwnPropertySymbols) {
|
||||||
|
ReflectOwnKeys = function ReflectOwnKeys(target) {
|
||||||
|
return Object.getOwnPropertyNames(target)
|
||||||
|
.concat(Object.getOwnPropertySymbols(target));
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
ReflectOwnKeys = function ReflectOwnKeys(target) {
|
||||||
|
return Object.getOwnPropertyNames(target);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function ProcessEmitWarning(warning) {
|
||||||
|
if (console && console.warn) console.warn(warning);
|
||||||
|
}
|
||||||
|
|
||||||
|
var NumberIsNaN = Number.isNaN || function NumberIsNaN(value) {
|
||||||
|
return value !== value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function EventEmitter() {
|
||||||
|
EventEmitter.init.call(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default EventEmitter;
|
||||||
|
|
||||||
|
// Backwards-compat with node 0.10.x
|
||||||
|
EventEmitter.EventEmitter = EventEmitter;
|
||||||
|
|
||||||
|
EventEmitter.prototype._events = undefined;
|
||||||
|
EventEmitter.prototype._eventsCount = 0;
|
||||||
|
EventEmitter.prototype._maxListeners = undefined;
|
||||||
|
|
||||||
|
// By default EventEmitters will print a warning if more than 10 listeners are
|
||||||
|
// added to it. This is a useful default which helps finding memory leaks.
|
||||||
|
var defaultMaxListeners = 10;
|
||||||
|
|
||||||
|
function checkListener(listener) {
|
||||||
|
if (typeof listener !== 'function') {
|
||||||
|
throw new TypeError('The "listener" argument must be of type Function. Received type ' + typeof listener);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.defineProperty(EventEmitter, 'defaultMaxListeners', {
|
||||||
|
enumerable: true,
|
||||||
|
get: function() {
|
||||||
|
return defaultMaxListeners;
|
||||||
|
},
|
||||||
|
set: function(arg) {
|
||||||
|
if (typeof arg !== 'number' || arg < 0 || NumberIsNaN(arg)) {
|
||||||
|
throw new RangeError('The value of "defaultMaxListeners" is out of range. It must be a non-negative number. Received ' + arg + '.');
|
||||||
|
}
|
||||||
|
defaultMaxListeners = arg;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
EventEmitter.init = function() {
|
||||||
|
|
||||||
|
if (this._events === undefined ||
|
||||||
|
this._events === Object.getPrototypeOf(this)._events) {
|
||||||
|
this._events = Object.create(null);
|
||||||
|
this._eventsCount = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._maxListeners = this._maxListeners || undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Obviously not all Emitters should be limited to 10. This function allows
|
||||||
|
// that to be increased. Set to zero for unlimited.
|
||||||
|
EventEmitter.prototype.setMaxListeners = function setMaxListeners(n) {
|
||||||
|
if (typeof n !== 'number' || n < 0 || NumberIsNaN(n)) {
|
||||||
|
throw new RangeError('The value of "n" is out of range. It must be a non-negative number. Received ' + n + '.');
|
||||||
|
}
|
||||||
|
this._maxListeners = n;
|
||||||
|
return this;
|
||||||
|
};
|
||||||
|
|
||||||
|
function _getMaxListeners(that) {
|
||||||
|
if (that._maxListeners === undefined)
|
||||||
|
return EventEmitter.defaultMaxListeners;
|
||||||
|
return that._maxListeners;
|
||||||
|
}
|
||||||
|
|
||||||
|
EventEmitter.prototype.getMaxListeners = function getMaxListeners() {
|
||||||
|
return _getMaxListeners(this);
|
||||||
|
};
|
||||||
|
|
||||||
|
EventEmitter.prototype.emit = function emit(type) {
|
||||||
|
var args = [];
|
||||||
|
for (var i = 1; i < arguments.length; i++) args.push(arguments[i]);
|
||||||
|
var doError = (type === 'error');
|
||||||
|
|
||||||
|
var events = this._events;
|
||||||
|
if (events !== undefined)
|
||||||
|
doError = (doError && events.error === undefined);
|
||||||
|
else if (!doError)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// If there is no 'error' event listener then throw.
|
||||||
|
if (doError) {
|
||||||
|
var er;
|
||||||
|
if (args.length > 0)
|
||||||
|
er = args[0];
|
||||||
|
if (er instanceof Error) {
|
||||||
|
// Note: The comments on the `throw` lines are intentional, they show
|
||||||
|
// up in Node's output if this results in an unhandled exception.
|
||||||
|
throw er; // Unhandled 'error' event
|
||||||
|
}
|
||||||
|
// At least give some kind of context to the user
|
||||||
|
var err = new Error('Unhandled error.' + (er ? ' (' + er.message + ')' : ''));
|
||||||
|
err.context = er;
|
||||||
|
throw err; // Unhandled 'error' event
|
||||||
|
}
|
||||||
|
|
||||||
|
var handler = events[type];
|
||||||
|
|
||||||
|
if (handler === undefined)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (typeof handler === 'function') {
|
||||||
|
ReflectApply(handler, this, args);
|
||||||
|
} else {
|
||||||
|
var len = handler.length;
|
||||||
|
var listeners = arrayClone(handler, len);
|
||||||
|
for (var i = 0; i < len; ++i)
|
||||||
|
ReflectApply(listeners[i], this, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
function _addListener(target, type, listener, prepend) {
|
||||||
|
var m;
|
||||||
|
var events;
|
||||||
|
var existing;
|
||||||
|
|
||||||
|
checkListener(listener);
|
||||||
|
|
||||||
|
events = target._events;
|
||||||
|
if (events === undefined) {
|
||||||
|
events = target._events = Object.create(null);
|
||||||
|
target._eventsCount = 0;
|
||||||
|
} else {
|
||||||
|
// To avoid recursion in the case that type === "newListener"! Before
|
||||||
|
// adding it to the listeners, first emit "newListener".
|
||||||
|
if (events.newListener !== undefined) {
|
||||||
|
target.emit('newListener', type,
|
||||||
|
listener.listener ? listener.listener : listener);
|
||||||
|
|
||||||
|
// Re-assign `events` because a newListener handler could have caused the
|
||||||
|
// this._events to be assigned to a new object
|
||||||
|
events = target._events;
|
||||||
|
}
|
||||||
|
existing = events[type];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existing === undefined) {
|
||||||
|
// Optimize the case of one listener. Don't need the extra array object.
|
||||||
|
existing = events[type] = listener;
|
||||||
|
++target._eventsCount;
|
||||||
|
} else {
|
||||||
|
if (typeof existing === 'function') {
|
||||||
|
// Adding the second element, need to change to array.
|
||||||
|
existing = events[type] =
|
||||||
|
prepend ? [listener, existing] : [existing, listener];
|
||||||
|
// If we've already got an array, just append.
|
||||||
|
} else if (prepend) {
|
||||||
|
existing.unshift(listener);
|
||||||
|
} else {
|
||||||
|
existing.push(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for listener leak
|
||||||
|
m = _getMaxListeners(target);
|
||||||
|
if (m > 0 && existing.length > m && !existing.warned) {
|
||||||
|
existing.warned = true;
|
||||||
|
// No error code for this since it is a Warning
|
||||||
|
// eslint-disable-next-line no-restricted-syntax
|
||||||
|
var w = new Error('Possible EventEmitter memory leak detected. ' +
|
||||||
|
existing.length + ' ' + String(type) + ' listeners ' +
|
||||||
|
'added. Use emitter.setMaxListeners() to ' +
|
||||||
|
'increase limit');
|
||||||
|
w.name = 'MaxListenersExceededWarning';
|
||||||
|
w.emitter = target;
|
||||||
|
w.type = type;
|
||||||
|
w.count = existing.length;
|
||||||
|
ProcessEmitWarning(w);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return target;
|
||||||
|
}
|
||||||
|
|
||||||
|
EventEmitter.prototype.addListener = function addListener(type, listener) {
|
||||||
|
return _addListener(this, type, listener, false);
|
||||||
|
};
|
||||||
|
|
||||||
|
EventEmitter.prototype.on = EventEmitter.prototype.addListener;
|
||||||
|
|
||||||
|
EventEmitter.prototype.prependListener =
|
||||||
|
function prependListener(type, listener) {
|
||||||
|
return _addListener(this, type, listener, true);
|
||||||
|
};
|
||||||
|
|
||||||
|
function onceWrapper() {
|
||||||
|
if (!this.fired) {
|
||||||
|
this.target.removeListener(this.type, this.wrapFn);
|
||||||
|
this.fired = true;
|
||||||
|
if (arguments.length === 0)
|
||||||
|
return this.listener.call(this.target);
|
||||||
|
return this.listener.apply(this.target, arguments);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function _onceWrap(target, type, listener) {
|
||||||
|
var state = { fired: false, wrapFn: undefined, target: target, type: type, listener: listener };
|
||||||
|
var wrapped = onceWrapper.bind(state);
|
||||||
|
wrapped.listener = listener;
|
||||||
|
state.wrapFn = wrapped;
|
||||||
|
return wrapped;
|
||||||
|
}
|
||||||
|
|
||||||
|
EventEmitter.prototype.once = function once(type, listener) {
|
||||||
|
checkListener(listener);
|
||||||
|
this.on(type, _onceWrap(this, type, listener));
|
||||||
|
return this;
|
||||||
|
};
|
||||||
|
|
||||||
|
EventEmitter.prototype.prependOnceListener =
|
||||||
|
function prependOnceListener(type, listener) {
|
||||||
|
checkListener(listener);
|
||||||
|
this.prependListener(type, _onceWrap(this, type, listener));
|
||||||
|
return this;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Emits a 'removeListener' event if and only if the listener was removed.
|
||||||
|
EventEmitter.prototype.removeListener =
|
||||||
|
function removeListener(type, listener) {
|
||||||
|
var list, events, position, i, originalListener;
|
||||||
|
|
||||||
|
checkListener(listener);
|
||||||
|
|
||||||
|
events = this._events;
|
||||||
|
if (events === undefined)
|
||||||
|
return this;
|
||||||
|
|
||||||
|
list = events[type];
|
||||||
|
if (list === undefined)
|
||||||
|
return this;
|
||||||
|
|
||||||
|
if (list === listener || list.listener === listener) {
|
||||||
|
if (--this._eventsCount === 0)
|
||||||
|
this._events = Object.create(null);
|
||||||
|
else {
|
||||||
|
delete events[type];
|
||||||
|
if (events.removeListener)
|
||||||
|
this.emit('removeListener', type, list.listener || listener);
|
||||||
|
}
|
||||||
|
} else if (typeof list !== 'function') {
|
||||||
|
position = -1;
|
||||||
|
|
||||||
|
for (i = list.length - 1; i >= 0; i--) {
|
||||||
|
if (list[i] === listener || list[i].listener === listener) {
|
||||||
|
originalListener = list[i].listener;
|
||||||
|
position = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (position < 0)
|
||||||
|
return this;
|
||||||
|
|
||||||
|
if (position === 0)
|
||||||
|
list.shift();
|
||||||
|
else {
|
||||||
|
spliceOne(list, position);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (list.length === 1)
|
||||||
|
events[type] = list[0];
|
||||||
|
|
||||||
|
if (events.removeListener !== undefined)
|
||||||
|
this.emit('removeListener', type, originalListener || listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this;
|
||||||
|
};
|
||||||
|
|
||||||
|
EventEmitter.prototype.off = EventEmitter.prototype.removeListener;
|
||||||
|
|
||||||
|
EventEmitter.prototype.removeAllListeners =
|
||||||
|
function removeAllListeners(type) {
|
||||||
|
var listeners, events, i;
|
||||||
|
|
||||||
|
events = this._events;
|
||||||
|
if (events === undefined)
|
||||||
|
return this;
|
||||||
|
|
||||||
|
// not listening for removeListener, no need to emit
|
||||||
|
if (events.removeListener === undefined) {
|
||||||
|
if (arguments.length === 0) {
|
||||||
|
this._events = Object.create(null);
|
||||||
|
this._eventsCount = 0;
|
||||||
|
} else if (events[type] !== undefined) {
|
||||||
|
if (--this._eventsCount === 0)
|
||||||
|
this._events = Object.create(null);
|
||||||
|
else
|
||||||
|
delete events[type];
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
// emit removeListener for all listeners on all events
|
||||||
|
if (arguments.length === 0) {
|
||||||
|
var keys = Object.keys(events);
|
||||||
|
var key;
|
||||||
|
for (i = 0; i < keys.length; ++i) {
|
||||||
|
key = keys[i];
|
||||||
|
if (key === 'removeListener') continue;
|
||||||
|
this.removeAllListeners(key);
|
||||||
|
}
|
||||||
|
this.removeAllListeners('removeListener');
|
||||||
|
this._events = Object.create(null);
|
||||||
|
this._eventsCount = 0;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
listeners = events[type];
|
||||||
|
|
||||||
|
if (typeof listeners === 'function') {
|
||||||
|
this.removeListener(type, listeners);
|
||||||
|
} else if (listeners !== undefined) {
|
||||||
|
// LIFO order
|
||||||
|
for (i = listeners.length - 1; i >= 0; i--) {
|
||||||
|
this.removeListener(type, listeners[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return this;
|
||||||
|
};
|
||||||
|
|
||||||
|
function _listeners(target, type, unwrap) {
|
||||||
|
var events = target._events;
|
||||||
|
|
||||||
|
if (events === undefined)
|
||||||
|
return [];
|
||||||
|
|
||||||
|
var evlistener = events[type];
|
||||||
|
if (evlistener === undefined)
|
||||||
|
return [];
|
||||||
|
|
||||||
|
if (typeof evlistener === 'function')
|
||||||
|
return unwrap ? [evlistener.listener || evlistener] : [evlistener];
|
||||||
|
|
||||||
|
return unwrap ?
|
||||||
|
unwrapListeners(evlistener) : arrayClone(evlistener, evlistener.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
EventEmitter.prototype.listeners = function listeners(type) {
|
||||||
|
return _listeners(this, type, true);
|
||||||
|
};
|
||||||
|
|
||||||
|
EventEmitter.prototype.rawListeners = function rawListeners(type) {
|
||||||
|
return _listeners(this, type, false);
|
||||||
|
};
|
||||||
|
|
||||||
|
EventEmitter.listenerCount = function(emitter, type) {
|
||||||
|
if (typeof emitter.listenerCount === 'function') {
|
||||||
|
return emitter.listenerCount(type);
|
||||||
|
} else {
|
||||||
|
return listenerCount.call(emitter, type);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
EventEmitter.prototype.listenerCount = listenerCount;
|
||||||
|
function listenerCount(type) {
|
||||||
|
var events = this._events;
|
||||||
|
|
||||||
|
if (events !== undefined) {
|
||||||
|
var evlistener = events[type];
|
||||||
|
|
||||||
|
if (typeof evlistener === 'function') {
|
||||||
|
return 1;
|
||||||
|
} else if (evlistener !== undefined) {
|
||||||
|
return evlistener.length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
EventEmitter.prototype.eventNames = function eventNames() {
|
||||||
|
return this._eventsCount > 0 ? ReflectOwnKeys(this._events) : [];
|
||||||
|
};
|
||||||
|
|
||||||
|
function arrayClone(arr, n) {
|
||||||
|
var copy = new Array(n);
|
||||||
|
for (var i = 0; i < n; ++i)
|
||||||
|
copy[i] = arr[i];
|
||||||
|
return copy;
|
||||||
|
}
|
||||||
|
|
||||||
|
function spliceOne(list, index) {
|
||||||
|
for (; index + 1 < list.length; index++)
|
||||||
|
list[index] = list[index + 1];
|
||||||
|
list.pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
function unwrapListeners(arr) {
|
||||||
|
var ret = new Array(arr.length);
|
||||||
|
for (var i = 0; i < ret.length; ++i) {
|
||||||
|
ret[i] = arr[i].listener || arr[i];
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
function once(emitter, name) {
|
||||||
|
return new Promise(function (resolve, reject) {
|
||||||
|
function errorListener(err) {
|
||||||
|
emitter.removeListener(name, resolver);
|
||||||
|
reject(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolver() {
|
||||||
|
if (typeof emitter.removeListener === 'function') {
|
||||||
|
emitter.removeListener('error', errorListener);
|
||||||
|
}
|
||||||
|
resolve([].slice.call(arguments));
|
||||||
|
};
|
||||||
|
|
||||||
|
eventTargetAgnosticAddListener(emitter, name, resolver, { once: true });
|
||||||
|
if (name !== 'error') {
|
||||||
|
addErrorHandlerIfEventEmitter(emitter, errorListener, { once: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function addErrorHandlerIfEventEmitter(emitter, handler, flags) {
|
||||||
|
if (typeof emitter.on === 'function') {
|
||||||
|
eventTargetAgnosticAddListener(emitter, 'error', handler, flags);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function eventTargetAgnosticAddListener(emitter, name, listener, flags) {
|
||||||
|
if (typeof emitter.on === 'function') {
|
||||||
|
if (flags.once) {
|
||||||
|
emitter.once(name, listener);
|
||||||
|
} else {
|
||||||
|
emitter.on(name, listener);
|
||||||
|
}
|
||||||
|
} else if (typeof emitter.addEventListener === 'function') {
|
||||||
|
// EventTarget does not have `error` event semantics like Node
|
||||||
|
// EventEmitters, we do not listen for `error` events here.
|
||||||
|
emitter.addEventListener(name, function wrapListener(arg) {
|
||||||
|
// IE does not have builtin `{ once: true }` support so we
|
||||||
|
// have to do it manually.
|
||||||
|
if (flags.once) {
|
||||||
|
emitter.removeEventListener(name, wrapListener);
|
||||||
|
}
|
||||||
|
listener(arg);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
throw new TypeError('The "emitter" argument must be of type EventEmitter. Received type ' + typeof emitter);
|
||||||
|
}
|
||||||
|
}
|
78
client/history.js
Normal file
78
client/history.js
Normal file
|
@ -0,0 +1,78 @@
|
||||||
|
import EventEmitter from "./events.js";
|
||||||
|
import HookEvent from "./hook.js";
|
||||||
|
|
||||||
|
class History extends EventEmitter {
|
||||||
|
constructor(ctx) {
|
||||||
|
super();
|
||||||
|
this.ctx = ctx;
|
||||||
|
this.window = this.ctx.window;
|
||||||
|
this.History = this.window.History;
|
||||||
|
this.history = this.window.history;
|
||||||
|
this.historyProto = this.History ? this.History.prototype : {};
|
||||||
|
this.pushState = this.historyProto.pushState;
|
||||||
|
this.replaceState = this.historyProto.replaceState;
|
||||||
|
this.go = this.historyProto.go;
|
||||||
|
this.back = this.historyProto.back;
|
||||||
|
this.forward = this.historyProto.forward;
|
||||||
|
};
|
||||||
|
override() {
|
||||||
|
this.overridePushState();
|
||||||
|
this.overrideReplaceState();
|
||||||
|
this.overrideGo();
|
||||||
|
this.overrideForward();
|
||||||
|
this.overrideBack();
|
||||||
|
};
|
||||||
|
overridePushState() {
|
||||||
|
this.ctx.override(this.historyProto, 'pushState', (target, that, args) => {
|
||||||
|
if (2 > args.length) return target.apply(that, args);
|
||||||
|
let [ state, title, url = '' ] = args;
|
||||||
|
|
||||||
|
const event = new HookEvent({ state, title, url }, target, that);
|
||||||
|
this.emit('pushState', event);
|
||||||
|
|
||||||
|
if (event.intercepted) return event.returnValue;
|
||||||
|
return event.target.call(event.that, event.data.state, event.data.title, event.data.url);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
overrideReplaceState() {
|
||||||
|
this.ctx.override(this.historyProto, 'replaceState', (target, that, args) => {
|
||||||
|
if (2 > args.length) return target.apply(that, args);
|
||||||
|
let [ state, title, url = '' ] = args;
|
||||||
|
|
||||||
|
const event = new HookEvent({ state, title, url }, target, that);
|
||||||
|
this.emit('replaceState', event);
|
||||||
|
|
||||||
|
if (event.intercepted) return event.returnValue;
|
||||||
|
return event.target.call(event.that, event.data.state, event.data.title, event.data.url);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
overrideGo() {
|
||||||
|
this.ctx.override(this.historyProto, 'go', (target, that, [ delta ]) => {
|
||||||
|
const event = new HookEvent({ delta }, target, that);
|
||||||
|
this.emit('go', event);
|
||||||
|
|
||||||
|
if (event.intercepted) return event.returnValue;
|
||||||
|
return event.target.call(event.that, event.data.delta);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
overrideForward() {
|
||||||
|
this.ctx.override(this.historyProto, 'forward', (target, that) => {
|
||||||
|
const event = new HookEvent(null, target, that);
|
||||||
|
this.emit('forward', event);
|
||||||
|
|
||||||
|
if (event.intercepted) return event.returnValue;
|
||||||
|
return event.target.call(event.that);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
overrideBack() {
|
||||||
|
this.ctx.override(this.historyProto, 'back', (target, that) => {
|
||||||
|
const event = new HookEvent(null, target, that);
|
||||||
|
this.emit('back', event);
|
||||||
|
|
||||||
|
if (event.intercepted) return event.returnValue;
|
||||||
|
return event.target.call(event.that);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default History;
|
23
client/hook.js
Normal file
23
client/hook.js
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
class HookEvent {
|
||||||
|
#intercepted;
|
||||||
|
#returnValue;
|
||||||
|
constructor(data = {}, target = null, that = null) {
|
||||||
|
this.#intercepted = false;
|
||||||
|
this.#returnValue = null;
|
||||||
|
this.data = data;
|
||||||
|
this.target = target;
|
||||||
|
this.that = that;
|
||||||
|
};
|
||||||
|
get intercepted() {
|
||||||
|
return this.#intercepted;
|
||||||
|
};
|
||||||
|
get returnValue() {
|
||||||
|
return this.#returnValue;
|
||||||
|
};
|
||||||
|
respondWith(input) {
|
||||||
|
this.#returnValue = input;
|
||||||
|
this.#intercepted = true;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default HookEvent;
|
101
client/index.js
Normal file
101
client/index.js
Normal file
|
@ -0,0 +1,101 @@
|
||||||
|
import DocumentHook from "./dom/document.js";
|
||||||
|
import ElementApi from "./dom/element.js";
|
||||||
|
import NodeApi from "./dom/node.js";
|
||||||
|
import AttrApi from "./dom/attr.js";
|
||||||
|
import FunctionHook from "./native/function.js";
|
||||||
|
import ObjectHook from "./native/object.js";
|
||||||
|
import Fetch from "./requests/fetch.js";
|
||||||
|
import WebSocketApi from "./requests/websocket.js";
|
||||||
|
import Xhr from "./requests/xhr.js";
|
||||||
|
import EventSourceApi from "./requests/eventsource.js";
|
||||||
|
import History from "./history.js";
|
||||||
|
import LocationApi from "./location.js";
|
||||||
|
import MessageApi from "./message.js";
|
||||||
|
import NavigatorApi from "./navigator.js";
|
||||||
|
import Workers from "./worker.js";
|
||||||
|
import URLApi from "./url.js";
|
||||||
|
import EventEmitter from "./events.js";
|
||||||
|
|
||||||
|
class UVClient extends EventEmitter {
|
||||||
|
constructor(window = self, worker = !window.window) {
|
||||||
|
super();
|
||||||
|
this.window = window;
|
||||||
|
this.nativeMethods = {
|
||||||
|
fnToString: this.window.Function.prototype.toString,
|
||||||
|
defineProperty: this.window.Object.defineProperty,
|
||||||
|
getOwnPropertyDescriptor: this.window.Object.getOwnPropertyDescriptor,
|
||||||
|
getOwnPropertyDescriptors: this.window.Object.getOwnPropertyDescriptors,
|
||||||
|
isArray: this.window.Array.isArray,
|
||||||
|
setPrototypeOf: this.window.Object.setPrototypeOf,
|
||||||
|
isExtensible: this.window.Object.isExtensible,
|
||||||
|
};
|
||||||
|
this.worker = worker;
|
||||||
|
this.fetch = new Fetch(this);
|
||||||
|
this.xhr = new Xhr(this);
|
||||||
|
this.history = new History(this);
|
||||||
|
this.element = new ElementApi(this);
|
||||||
|
this.node = new NodeApi(this)
|
||||||
|
this.document = new DocumentHook(this);
|
||||||
|
this.function = new FunctionHook(this);
|
||||||
|
this.object = new ObjectHook(this);
|
||||||
|
this.message = new MessageApi(this);
|
||||||
|
this.websocket = new WebSocketApi(this);
|
||||||
|
this.navigator = new NavigatorApi(this);
|
||||||
|
this.eventSource = new EventSourceApi(this);
|
||||||
|
this.attribute = new AttrApi(this);
|
||||||
|
this.url = new URLApi(this);
|
||||||
|
this.workers = new Workers(this);
|
||||||
|
this.location = new LocationApi(this);
|
||||||
|
};
|
||||||
|
initLocation(rewriteUrl, sourceUrl) {
|
||||||
|
this.location = new LocationApi(this, sourceUrl, rewriteUrl, this.worker);
|
||||||
|
};
|
||||||
|
override(obj, prop, wrapper, construct) {
|
||||||
|
if (!prop in obj) return false;
|
||||||
|
const wrapped = this.wrap(obj, prop, wrapper, construct);
|
||||||
|
return obj[prop] = wrapped;
|
||||||
|
};
|
||||||
|
overrideDescriptor(obj, prop, wrapObj = {}) {
|
||||||
|
const wrapped = this.wrapDescriptor(obj, prop, wrapObj);
|
||||||
|
if (!wrapped) return {};
|
||||||
|
this.nativeMethods.defineProperty(obj, prop, wrapped);
|
||||||
|
return wrapped;
|
||||||
|
};
|
||||||
|
wrap(obj, prop, wrap, construct) {
|
||||||
|
const fn = obj[prop];
|
||||||
|
if (!fn) return fn;
|
||||||
|
const wrapped = 'prototype' in fn ? function attach() {
|
||||||
|
return wrap(fn, this, [...arguments]);
|
||||||
|
} : {
|
||||||
|
attach() {
|
||||||
|
return wrap(fn, this, [...arguments]);
|
||||||
|
},
|
||||||
|
}.attach;
|
||||||
|
|
||||||
|
if (!!construct) {
|
||||||
|
wrapped.prototype = fn.prototype;
|
||||||
|
wrapped.prototype.constructor = wrapped;
|
||||||
|
};
|
||||||
|
|
||||||
|
this.emit('wrap', fn, wrapped, !!construct);
|
||||||
|
|
||||||
|
return wrapped;
|
||||||
|
};
|
||||||
|
wrapDescriptor(obj, prop, wrapObj = {}) {
|
||||||
|
const descriptor = this.nativeMethods.getOwnPropertyDescriptor(obj, prop);
|
||||||
|
if (!descriptor) return false;
|
||||||
|
for (let key in wrapObj) {
|
||||||
|
if (key in descriptor) {
|
||||||
|
if (key === 'get' || key === 'set') {
|
||||||
|
descriptor[key] = this.wrap(descriptor, key, wrapObj[key]);
|
||||||
|
} else {
|
||||||
|
descriptor[key] = typeof wrapObj[key] == 'function' ? wrapObj[key](descriptor[key]) : wrapObj[key];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return descriptor;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default UVClient;
|
||||||
|
if (typeof self === 'object') self.UVClient = UVClient;
|
119
client/location.js
Normal file
119
client/location.js
Normal file
|
@ -0,0 +1,119 @@
|
||||||
|
class LocationApi {
|
||||||
|
constructor(ctx) {
|
||||||
|
this.ctx = ctx;
|
||||||
|
this.window = ctx.window;
|
||||||
|
this.location = this.window.location;
|
||||||
|
this.WorkerLocation = this.ctx.worker ? this.window.WorkerLocation : null;
|
||||||
|
this.workerLocProto = this.WorkerLocation ? this.WorkerLocation.prototype : {};
|
||||||
|
this.keys = ['href', 'protocol', 'host', 'hostname', 'port', 'pathname', 'search', 'hash', 'origin'];
|
||||||
|
this.href = this.WorkerLocation ? ctx.nativeMethods.getOwnPropertyDescriptor(this.workerLocProto, 'href') :
|
||||||
|
ctx.nativeMethods.getOwnPropertyDescriptor(this.location, 'href');
|
||||||
|
};
|
||||||
|
overrideWorkerLocation(parse) {
|
||||||
|
if (!this.WorkerLocation) return false;
|
||||||
|
const uv = this;
|
||||||
|
|
||||||
|
for (const key of this.keys) {
|
||||||
|
this.ctx.overrideDescriptor(this.workerLocProto, key, {
|
||||||
|
get: (target, that) => {
|
||||||
|
return parse(
|
||||||
|
uv.href.get.call(this.location)
|
||||||
|
)[key]
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
emulate(parse, wrap) {
|
||||||
|
const emulation = {};
|
||||||
|
const that = this;
|
||||||
|
|
||||||
|
for (const key of that.keys) {
|
||||||
|
this.ctx.nativeMethods.defineProperty(emulation, key, {
|
||||||
|
get() {
|
||||||
|
return parse(
|
||||||
|
that.href.get.call(that.location)
|
||||||
|
)[key];
|
||||||
|
},
|
||||||
|
set: key !== 'origin' ? function (val) {
|
||||||
|
switch(key) {
|
||||||
|
case 'href':
|
||||||
|
that.location.href = wrap(val);
|
||||||
|
break;
|
||||||
|
case 'hash':
|
||||||
|
that.location.hash = val;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
const url = new URL(emulation.href);
|
||||||
|
url[key] = val;
|
||||||
|
that.location.href = wrap(url.href);
|
||||||
|
};
|
||||||
|
} : undefined,
|
||||||
|
configurable: false,
|
||||||
|
enumerable: true,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
if ('reload' in this.location) {
|
||||||
|
this.ctx.nativeMethods.defineProperty(emulation, 'reload', {
|
||||||
|
value: this.ctx.wrap(this.location, 'reload', (target, that) => {
|
||||||
|
return target.call(that === emulation ? this.location : that);
|
||||||
|
}),
|
||||||
|
writable: false,
|
||||||
|
enumerable: true,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
if ('replace' in this.location) {
|
||||||
|
this.ctx.nativeMethods.defineProperty(emulation, 'replace', {
|
||||||
|
value: this.ctx.wrap(this.location, 'assign', (target, that, args) => {
|
||||||
|
if (!args.length || that !== emulation) target.call(that);
|
||||||
|
that = this.location;
|
||||||
|
let [ input ] = args;
|
||||||
|
|
||||||
|
const url = new URL(input, emulation.href);
|
||||||
|
return target.call(that === emulation ? this.location : that, wrap(url.href));
|
||||||
|
}),
|
||||||
|
writable: false,
|
||||||
|
enumerable: true,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
if ('assign' in this.location) {
|
||||||
|
this.ctx.nativeMethods.defineProperty(emulation, 'assign', {
|
||||||
|
value: this.ctx.wrap(this.location, 'assign', (target, that, args) => {
|
||||||
|
if (!args.length || that !== emulation) target.call(that);
|
||||||
|
that = this.location;
|
||||||
|
let [ input ] = args;
|
||||||
|
|
||||||
|
const url = new URL(input, emulation.href);
|
||||||
|
return target.call(that === emulation ? this.location : that, wrap(url.href));
|
||||||
|
}),
|
||||||
|
writable: false,
|
||||||
|
enumerable: true,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
this.ctx.nativeMethods.defineProperty(emulation, 'toString', {
|
||||||
|
value: this.ctx.wrap(this.location, 'toString', () => {
|
||||||
|
return emulation.href;
|
||||||
|
}),
|
||||||
|
enumerable: true,
|
||||||
|
writable: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.ctx.nativeMethods.defineProperty(emulation, Symbol.toPrimitive, {
|
||||||
|
value: () => emulation.href,
|
||||||
|
writable: false,
|
||||||
|
enumerable: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (this.ctx.window.Location) this.ctx.nativeMethods.setPrototypeOf(emulation, this.ctx.window.Location.prototype);
|
||||||
|
|
||||||
|
return emulation;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LocationApi;
|
84
client/message.js
Normal file
84
client/message.js
Normal file
|
@ -0,0 +1,84 @@
|
||||||
|
import EventEmitter from "./events.js";
|
||||||
|
import HookEvent from "./hook.js";
|
||||||
|
|
||||||
|
class MessageApi extends EventEmitter {
|
||||||
|
constructor(ctx) {
|
||||||
|
super();
|
||||||
|
this.ctx = ctx;
|
||||||
|
this.window = this.ctx.window;
|
||||||
|
this.postMessage = this.window.postMessage;
|
||||||
|
this.MessageEvent = this.window.MessageEvent || {};
|
||||||
|
this.MessagePort = this.window.MessagePort || {};
|
||||||
|
this.mpProto = this.MessagePort.prototype || {};
|
||||||
|
this.mpPostMessage = this.mpProto.postMessage;
|
||||||
|
this.messageProto = this.MessageEvent.prototype || {};
|
||||||
|
this.messageData = ctx.nativeMethods.getOwnPropertyDescriptor(this.messageProto, 'data');
|
||||||
|
this.messageOrigin = ctx.nativeMethods.getOwnPropertyDescriptor(this.messageProto, 'origin');
|
||||||
|
};
|
||||||
|
overridePostMessage() {
|
||||||
|
this.ctx.override(this.window, 'postMessage', (target, that, args) => {
|
||||||
|
if (!args.length) return target.apply(that, args);
|
||||||
|
|
||||||
|
let message;
|
||||||
|
let origin;
|
||||||
|
let transfer;
|
||||||
|
|
||||||
|
if (!this.ctx.worker) {
|
||||||
|
[ message, origin, transfer = [] ] = args;
|
||||||
|
} else {
|
||||||
|
[ message, transfer = [] ] = args;
|
||||||
|
};
|
||||||
|
|
||||||
|
const event = new HookEvent({ message, origin, transfer, worker: this.ctx.worker }, target, that);
|
||||||
|
this.emit('postMessage', event);
|
||||||
|
|
||||||
|
if (event.intercepted) return event.returnValue;
|
||||||
|
return this.ctx.worker ? event.target.call(event.that, event.data.message, event.data.transfer) : event.target.call(event.that, event.data.message, event.data.origin, event.data.transfer);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
wrapPostMessage(obj, prop, noOrigin = false) {
|
||||||
|
return this.ctx.wrap(obj, prop, (target, that, args) => {
|
||||||
|
if (this.ctx.worker ? !args.length : 2 > args) return target.apply(that, args);
|
||||||
|
let message;
|
||||||
|
let origin;
|
||||||
|
let transfer;
|
||||||
|
|
||||||
|
if (!noOrigin) {
|
||||||
|
[ message, origin, transfer = [] ] = args;
|
||||||
|
} else {
|
||||||
|
[ message, transfer = [] ] = args;
|
||||||
|
origin = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const event = new HookEvent({ message, origin, transfer, worker: this.ctx.worker }, target, obj);
|
||||||
|
this.emit('postMessage', event);
|
||||||
|
|
||||||
|
if (event.intercepted) return event.returnValue;
|
||||||
|
return noOrigin ? event.target.call(event.that, event.data.message, event.data.transfer) : event.target.call(event.that, event.data.message, event.data.origin, event.data.transfer);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
overrideMessageOrigin() {
|
||||||
|
this.ctx.overrideDescriptor(this.messageProto, 'origin', {
|
||||||
|
get: (target, that) => {
|
||||||
|
const event = new HookEvent({ value: target.call(that) }, target, that);
|
||||||
|
this.emit('origin', event);
|
||||||
|
|
||||||
|
if (event.intercepted) return event.returnValue;
|
||||||
|
return event.data.value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
overrideMessageData() {
|
||||||
|
this.ctx.overrideDescriptor(this.messageProto, 'data', {
|
||||||
|
get: (target, that) => {
|
||||||
|
const event = new HookEvent({ value: target.call(that) }, target, that);
|
||||||
|
this.emit('data', event);
|
||||||
|
|
||||||
|
if (event.intercepted) return event.returnValue;
|
||||||
|
return event.data.value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MessageApi;
|
46
client/native/function.js
Normal file
46
client/native/function.js
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
import EventEmitter from "../events.js";
|
||||||
|
import HookEvent from "../hook.js";
|
||||||
|
|
||||||
|
class FunctionHook extends EventEmitter {
|
||||||
|
constructor(ctx) {
|
||||||
|
super();
|
||||||
|
this.ctx = ctx;
|
||||||
|
this.window = ctx.window;
|
||||||
|
this.Function = this.window.Function;
|
||||||
|
this.fnProto = this.Function.prototype;
|
||||||
|
this.toString = this.fnProto.toString;
|
||||||
|
this.fnStrings = ctx.fnStrings;
|
||||||
|
this.call = this.fnProto.call;
|
||||||
|
this.apply = this.fnProto.apply;
|
||||||
|
this.bind = this.fnProto.bind;
|
||||||
|
};
|
||||||
|
overrideFunction() {
|
||||||
|
this.ctx.override(this.window, 'Function', (target, that, args) => {
|
||||||
|
if (!args.length) return target.apply(that, args);
|
||||||
|
|
||||||
|
let script = args[args.length - 1];
|
||||||
|
let fnArgs = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < args.length - 1; i++) {
|
||||||
|
fnArgs.push(args[i]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const event = new HookEvent({ script, args: fnArgs }, target, that);
|
||||||
|
this.emit('function', event);
|
||||||
|
|
||||||
|
if (event.intercepted) return event.returnValue;
|
||||||
|
return event.target.call(event.that, ...event.data.args, event.data.script);
|
||||||
|
}, true);
|
||||||
|
};
|
||||||
|
overrideToString() {
|
||||||
|
this.ctx.override(this.fnProto, 'toString', (target, that) => {
|
||||||
|
const event = new HookEvent({ fn: that }, target, that);
|
||||||
|
this.emit('toString', event);
|
||||||
|
|
||||||
|
if (event.intercepted) return event.returnValue;
|
||||||
|
return event.target.call(event.data.fn);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FunctionHook;
|
40
client/native/object.js
Normal file
40
client/native/object.js
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
import EventEmitter from "../events.js";
|
||||||
|
import HookEvent from "../hook.js";
|
||||||
|
|
||||||
|
class ObjectHook extends EventEmitter {
|
||||||
|
constructor(ctx) {
|
||||||
|
super();
|
||||||
|
this.ctx = ctx;
|
||||||
|
this.window = ctx.window;
|
||||||
|
this.Object = this.window.Object;
|
||||||
|
this.getOwnPropertyDescriptors = this.Object.getOwnPropertyDescriptors;
|
||||||
|
this.getOwnPropertyDescriptor = this.Object.getOwnPropertyDescriptor;
|
||||||
|
this.getOwnPropertyNames = this.Object.getOwnPropertyNames;
|
||||||
|
};
|
||||||
|
overrideGetPropertyNames() {
|
||||||
|
this.ctx.override(this.Object, 'getOwnPropertyNames', (target, that, args) => {
|
||||||
|
if (!args.length) return target.apply(that, args);
|
||||||
|
let [ object ] = args;
|
||||||
|
|
||||||
|
const event = new HookEvent({ names: target.call(that, object) }, target, that);
|
||||||
|
this.emit('getOwnPropertyNames', event);
|
||||||
|
|
||||||
|
if (event.intercepted) return event.returnValue;
|
||||||
|
return event.data.names;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
overrideGetOwnPropertyDescriptors() {
|
||||||
|
this.ctx.override(this.Object, 'getOwnPropertyDescriptors', (target, that, args) => {
|
||||||
|
if (!args.length) return target.apply(that, args);
|
||||||
|
let [ object ] = args;
|
||||||
|
|
||||||
|
const event = new HookEvent({ descriptors: target.call(that, object) }, target, that);
|
||||||
|
this.emit('getOwnPropertyDescriptors', event);
|
||||||
|
|
||||||
|
if (event.intercepted) return event.returnValue;
|
||||||
|
return event.data.descriptors;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ObjectHook;
|
28
client/navigator.js
Normal file
28
client/navigator.js
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
import EventEmitter from "./events.js";
|
||||||
|
import HookEvent from "./hook.js";
|
||||||
|
|
||||||
|
class NavigatorApi extends EventEmitter {
|
||||||
|
constructor(ctx) {
|
||||||
|
super();
|
||||||
|
this.ctx = ctx;
|
||||||
|
this.window = ctx.window;
|
||||||
|
this.navigator = this.window.navigator;
|
||||||
|
this.Navigator = this.window.Navigator || {};
|
||||||
|
this.navProto = this.Navigator.prototype || {};
|
||||||
|
this.sendBeacon = this.navProto.sendBeacon;
|
||||||
|
};
|
||||||
|
overrideSendBeacon() {
|
||||||
|
this.ctx.override(this.navProto, 'sendBeacon', (target, that, args) => {
|
||||||
|
if (!args.length) return target.apply(that, args);
|
||||||
|
let [ url, data = '' ] = args;
|
||||||
|
|
||||||
|
const event = new HookEvent({ url, data }, target, that);
|
||||||
|
this.emit('sendBeacon', event);
|
||||||
|
|
||||||
|
if (event.intercepted) return event.returnValue;
|
||||||
|
return event.target.call(event.that, event.data.url, event.data.data);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NavigatorApi;
|
36
client/requests/eventsource.js
Normal file
36
client/requests/eventsource.js
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
import EventEmitter from "../events.js";
|
||||||
|
import HookEvent from "../hook.js";
|
||||||
|
|
||||||
|
class EventSourceApi extends EventEmitter {
|
||||||
|
constructor(ctx) {
|
||||||
|
super();
|
||||||
|
this.ctx = ctx;
|
||||||
|
this.window = ctx.window;
|
||||||
|
this.EventSource = this.window.EventSource || {};
|
||||||
|
this.esProto = this.EventSource.prototype || {};
|
||||||
|
this.url = ctx.nativeMethods.getOwnPropertyDescriptor(this.esProto, 'url');
|
||||||
|
};
|
||||||
|
overrideConstruct() {
|
||||||
|
this.ctx.override(this.window, 'EventSource', (target, that, args) => {
|
||||||
|
if (!args.length) return new target(...args);
|
||||||
|
let [ url, config = {} ] = args;
|
||||||
|
|
||||||
|
const event = new HookEvent({ url, config }, target, that);
|
||||||
|
this.emit('construct', event);
|
||||||
|
|
||||||
|
if (event.intercepted) return event.returnValue;
|
||||||
|
return new event.target(event.data.url, event.data.config);
|
||||||
|
}, true);
|
||||||
|
};
|
||||||
|
overrideUrl() {
|
||||||
|
this.ctx.overrideDescriptor(this.esProto, 'url', {
|
||||||
|
get: (target, that) => {
|
||||||
|
const event = new HookEvent({ value: target.call(that) }, target, that);
|
||||||
|
this.emit('url', event);
|
||||||
|
return event.data.value;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EventSourceApi;
|
166
client/requests/fetch.js
Normal file
166
client/requests/fetch.js
Normal file
|
@ -0,0 +1,166 @@
|
||||||
|
import EventEmitter from "../events.js";
|
||||||
|
import HookEvent from "../hook.js";
|
||||||
|
|
||||||
|
class Fetch extends EventEmitter {
|
||||||
|
constructor(ctx) {
|
||||||
|
super();
|
||||||
|
this.ctx = ctx;
|
||||||
|
this.window = ctx.window;
|
||||||
|
this.fetch = this.window.fetch;
|
||||||
|
this.Request = this.window.Request;
|
||||||
|
this.Response = this.window.Response;
|
||||||
|
this.Headers = this.window.Headers;
|
||||||
|
this.reqProto = this.Request ? this.Request.prototype : {};
|
||||||
|
this.resProto = this.Response ? this.Response.prototype : {};
|
||||||
|
this.headersProto = this.Headers ? this.Headers.prototype : {};
|
||||||
|
this.reqUrl = ctx.nativeMethods.getOwnPropertyDescriptor(this.reqProto, 'url');
|
||||||
|
this.resUrl = ctx.nativeMethods.getOwnPropertyDescriptor(this.resProto, 'url');
|
||||||
|
this.reqHeaders = ctx.nativeMethods.getOwnPropertyDescriptor(this.reqProto, 'headers');
|
||||||
|
this.resHeaders = ctx.nativeMethods.getOwnPropertyDescriptor(this.resProto, 'headers');
|
||||||
|
};
|
||||||
|
override() {
|
||||||
|
this.overrideRequest();
|
||||||
|
this.overrideUrl();
|
||||||
|
this.overrideHeaders();
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
overrideRequest() {
|
||||||
|
if (!this.fetch) return false;
|
||||||
|
|
||||||
|
this.ctx.override(this.window, 'fetch', (target, that, args) => {
|
||||||
|
if (!args.length || args[0] instanceof this.Request) return target.apply(that, args);
|
||||||
|
|
||||||
|
let [ input, options = {} ] = args;
|
||||||
|
const event = new HookEvent({ input, options }, target, that);
|
||||||
|
|
||||||
|
this.emit('request', event);
|
||||||
|
if (event.intercepted) return event.returnValue;
|
||||||
|
|
||||||
|
return event.target.call(event.that, event.data.input, event.data.options);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.ctx.override(this.window, 'Request', (target, that, args) => {
|
||||||
|
if (!args.length) return new target(...args);
|
||||||
|
|
||||||
|
let [ input, options = {} ] = args;
|
||||||
|
const event = new HookEvent({ input, options }, target);
|
||||||
|
|
||||||
|
this.emit('request', event);
|
||||||
|
if (event.intercepted) return event.returnValue;
|
||||||
|
|
||||||
|
return new event.target(event.data.input, event.data.options);
|
||||||
|
}, true);
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
overrideUrl() {
|
||||||
|
this.ctx.overrideDescriptor(this.reqProto, 'url', {
|
||||||
|
get: (target, that) => {
|
||||||
|
const event = new HookEvent({ value: target.call(that) }, target, that);
|
||||||
|
|
||||||
|
this.emit('requestUrl', event);
|
||||||
|
if (event.intercepted) return event.returnValue;
|
||||||
|
|
||||||
|
return event.data.value;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
this.ctx.overrideDescriptor(this.resProto, 'url', {
|
||||||
|
get: (target, that) => {
|
||||||
|
const event = new HookEvent({ value: target.call(that) }, target, that);
|
||||||
|
|
||||||
|
this.emit('responseUrl', event);
|
||||||
|
if (event.intercepted) return event.returnValue;
|
||||||
|
|
||||||
|
return event.data.value;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
overrideHeaders() {
|
||||||
|
if (!this.Headers) return false;
|
||||||
|
|
||||||
|
this.ctx.overrideDescriptor(this.reqProto, 'headers', {
|
||||||
|
get: (target, that) => {
|
||||||
|
const event = new HookEvent({ value: target.call(that) }, target, that);
|
||||||
|
|
||||||
|
this.emit('requestHeaders', event);
|
||||||
|
if (event.intercepted) return event.returnValue;
|
||||||
|
|
||||||
|
return event.data.value;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
this.ctx.overrideDescriptor(this.resProto, 'headers', {
|
||||||
|
get: (target, that) => {
|
||||||
|
const event = new HookEvent({ value: target.call(that) }, target, that);
|
||||||
|
|
||||||
|
this.emit('responseHeaders', event);
|
||||||
|
if (event.intercepted) return event.returnValue;
|
||||||
|
|
||||||
|
return event.data.value;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
this.ctx.override(this.headersProto, 'get', (target, that, [ name ]) => {
|
||||||
|
if (!name) return target.call(that);
|
||||||
|
const event = new HookEvent({ name, value: target.call(that, name) }, target, that);
|
||||||
|
|
||||||
|
this.emit('getHeader', event);
|
||||||
|
if (event.intercepted) return event.returnValue;
|
||||||
|
|
||||||
|
return event.data.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.ctx.override(this.headersProto, 'set', (target, that, args) => {
|
||||||
|
if (2 > args.length) return target.apply(that, args);
|
||||||
|
|
||||||
|
let [ name, value ] = args;
|
||||||
|
const event = new HookEvent({ name, value }, target, that);
|
||||||
|
|
||||||
|
this.emit('setHeader', event);
|
||||||
|
if (event.intercepted) return event.returnValue;
|
||||||
|
|
||||||
|
return event.target.call(event.that, event.data.name, event.data.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.ctx.override(this.headersProto, 'has', (target, that, args) => {
|
||||||
|
if (!args.length) return target.call(that);
|
||||||
|
let [ name ] = args;
|
||||||
|
|
||||||
|
const event = new HookEvent({ name, value: target.call(that, name) }, target, that);
|
||||||
|
|
||||||
|
this.emit('hasHeader', event);
|
||||||
|
if (event.intercepted) return event.returnValue;
|
||||||
|
|
||||||
|
return event.data;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.ctx.override(this.headersProto, 'append', (target, that, args) => {
|
||||||
|
if (2 > args.length) return target.apply(that, args);
|
||||||
|
|
||||||
|
let [ name, value ] = args;
|
||||||
|
const event = new HookEvent({ name, value }, target, that);
|
||||||
|
|
||||||
|
this.emit('appendHeader', event);
|
||||||
|
if (event.intercepted) return event.returnValue;
|
||||||
|
|
||||||
|
return event.target.call(event.that, event.data.name, event.data.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.ctx.override(this.headersProto, 'delete', (target, that, args) => {
|
||||||
|
if (!args.length) return target.apply(that, args);
|
||||||
|
|
||||||
|
let [ name ] = args;
|
||||||
|
const event = new HookEvent({ name }, target, that);
|
||||||
|
|
||||||
|
this.emit('deleteHeader', event);
|
||||||
|
if (event.intercepted) return event.returnValue;
|
||||||
|
|
||||||
|
return event.target.call(event.that, event.data.name);
|
||||||
|
});
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Fetch;
|
49
client/requests/websocket.js
Normal file
49
client/requests/websocket.js
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
import EventEmitter from "../events.js";
|
||||||
|
import HookEvent from "../hook.js";
|
||||||
|
|
||||||
|
class WebSocketApi extends EventEmitter {
|
||||||
|
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.send = this.wsProto.send;
|
||||||
|
this.close = this.wsProto.close;
|
||||||
|
};
|
||||||
|
overrideWebSocket() {
|
||||||
|
this.ctx.override(this.window, 'WebSocket', (target, that, args) => {
|
||||||
|
if (!args.length) return new target(...args);
|
||||||
|
let [ url, protocols = [] ] = args;
|
||||||
|
|
||||||
|
if (!this.ctx.nativeMethods.isArray(protocols)) protocols = [ protocols ];
|
||||||
|
const event = new HookEvent({ url, protocols }, target, that);
|
||||||
|
this.emit('websocket', event);
|
||||||
|
|
||||||
|
if (event.intercepted) return event.returnValue;
|
||||||
|
return new event.target(event.data.url, event.data.protocols);
|
||||||
|
}, true);
|
||||||
|
};
|
||||||
|
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;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default WebSocketApi;
|
109
client/requests/xhr.js
Normal file
109
client/requests/xhr.js
Normal file
|
@ -0,0 +1,109 @@
|
||||||
|
import EventEmitter from "../events.js";
|
||||||
|
import HookEvent from "../hook.js";
|
||||||
|
|
||||||
|
class Xhr extends EventEmitter {
|
||||||
|
constructor(ctx) {
|
||||||
|
super();
|
||||||
|
this.ctx = ctx;
|
||||||
|
this.window = ctx.window;
|
||||||
|
this.XMLHttpRequest = this.window.XMLHttpRequest;
|
||||||
|
this.xhrProto = this.window.XMLHttpRequest ? this.window.XMLHttpRequest.prototype : {};
|
||||||
|
this.open = this.xhrProto.open;
|
||||||
|
this.abort = this.xhrProto.abort;
|
||||||
|
this.send = this.xhrProto.send;
|
||||||
|
this.overrideMimeType = this.xhrProto.overrideMimeType
|
||||||
|
this.getAllResponseHeaders = this.xhrProto.getAllResponseHeaders;
|
||||||
|
this.getResponseHeader = this.xhrProto.getResponseHeader;
|
||||||
|
this.setRequestHeader = this.xhrProto.setRequestHeader;
|
||||||
|
this.responseURL = ctx.nativeMethods.getOwnPropertyDescriptor(this.xhrProto, 'responseURL');
|
||||||
|
this.responseText = ctx.nativeMethods.getOwnPropertyDescriptor(this.xhrProto, 'responseText');
|
||||||
|
};
|
||||||
|
override() {
|
||||||
|
this.overrideOpen();
|
||||||
|
this.overrideSend();
|
||||||
|
this.overrideMimeType();
|
||||||
|
this.overrideGetResHeader();
|
||||||
|
this.overrideGetResHeaders();
|
||||||
|
this.overrideSetReqHeader();
|
||||||
|
};
|
||||||
|
overrideOpen() {
|
||||||
|
this.ctx.override(this.xhrProto, 'open', (target, that, args) => {
|
||||||
|
if (2 > args.length) return target.apply(that, args);
|
||||||
|
|
||||||
|
let [ method, input, async = true, user = null, password = null ] = args;
|
||||||
|
const event = new HookEvent({ method, input, async, user, password }, target, that);
|
||||||
|
|
||||||
|
this.emit('open', event);
|
||||||
|
if (event.intercepted) return event.returnValue;
|
||||||
|
|
||||||
|
return event.target.call(
|
||||||
|
event.that,
|
||||||
|
event.data.method,
|
||||||
|
event.data.input,
|
||||||
|
event.data.async,
|
||||||
|
event.data.user,
|
||||||
|
event.data.password
|
||||||
|
);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
overrideResponseUrl() {
|
||||||
|
this.ctx.overrideDescriptor(this.xhrProto, 'responseURL', {
|
||||||
|
get: (target, that) => {
|
||||||
|
const event = new HookEvent({ value: target.call(that) }, target, that);
|
||||||
|
this.emit('responseUrl', event);
|
||||||
|
|
||||||
|
if (event.intercepted) return event.returnValue;
|
||||||
|
return event.data.value;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
overrideSend() {
|
||||||
|
this.ctx.override(this.xhrProto, 'send', (target, that, [ body = null ]) => {
|
||||||
|
const event = new HookEvent({ body }, target, that);
|
||||||
|
|
||||||
|
this.emit('send', event);
|
||||||
|
if (event.intercepted) return event.returnValue;
|
||||||
|
|
||||||
|
return event.target.call(
|
||||||
|
event.that,
|
||||||
|
event.data.body,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
overrideSetReqHeader() {
|
||||||
|
this.ctx.override(this.xhrProto, 'setRequestHeader', (target, that, args) => {
|
||||||
|
if (2 > args.length) return target.apply(that, args);
|
||||||
|
|
||||||
|
let [ name, value ] = args;
|
||||||
|
const event = new HookEvent({ name, value }, target, that);
|
||||||
|
|
||||||
|
this.emit('setReqHeader', event);
|
||||||
|
if (event.intercepted) return event.returnValue;
|
||||||
|
|
||||||
|
return event.target.call(event.that, event.data.name, event.data.value);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
overrideGetResHeaders() {
|
||||||
|
this.ctx.override(this.xhrProto, 'getAllResponseHeaders', (target, that) => {
|
||||||
|
const event = new HookEvent({ value: target.call(that) }, target, that);
|
||||||
|
|
||||||
|
this.emit('getAllResponseHeaders', event);
|
||||||
|
if (event.intercepted) return event.returnValue;
|
||||||
|
|
||||||
|
return event.data.value;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
overrideGetResHeader() {
|
||||||
|
this.ctx.override(this.xhrProto, 'getResponseHeader', (target, that, args) => {
|
||||||
|
if (!args.length) return target.apply(that, args);
|
||||||
|
let [ name ] = args;
|
||||||
|
|
||||||
|
const event = new HookEvent({ name, value: target.call(that, name) }, target, that);
|
||||||
|
if (event.intercepted) return event.returnValue;
|
||||||
|
|
||||||
|
return event.data.value;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Xhr
|
37
client/url.js
Normal file
37
client/url.js
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
import EventEmitter from "./events.js";
|
||||||
|
import HookEvent from "./hook.js";
|
||||||
|
|
||||||
|
class URLApi extends EventEmitter {
|
||||||
|
constructor(ctx) {
|
||||||
|
super();
|
||||||
|
this.ctx = ctx;
|
||||||
|
this.window = this.ctx.window;
|
||||||
|
this.URL = this.window.URL || {};
|
||||||
|
this.createObjectURL = this.URL.createObjectURL;
|
||||||
|
this.revokeObjectURL = this.URL.revokeObjectURL;
|
||||||
|
};
|
||||||
|
overrideObjectURL() {
|
||||||
|
this.ctx.override(this.URL, 'createObjectURL', (target, that, args) => {
|
||||||
|
if (!args.length) return target.apply(that, args);
|
||||||
|
let [ object ] = args;
|
||||||
|
|
||||||
|
const event = new HookEvent({ object }, target, that);
|
||||||
|
this.emit('createObjectURL', event);
|
||||||
|
|
||||||
|
if (event.intercepted) return event.returnValue;
|
||||||
|
return event.target.call(event.that, event.data.object);
|
||||||
|
});
|
||||||
|
this.ctx.override(this.URL, 'revokeObjectURL', (target, that, args) => {
|
||||||
|
if (!args.length) return target.apply(that, args);
|
||||||
|
let [ url ] = args;
|
||||||
|
|
||||||
|
const event = new HookEvent({ url }, target, that);
|
||||||
|
this.emit('revokeObjectURL', event);
|
||||||
|
|
||||||
|
if (event.intercepted) return event.returnValue;
|
||||||
|
return event.target.call(event.that, event.data.url);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default URLApi;
|
66
client/worker.js
Normal file
66
client/worker.js
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
import EventEmitter from "./events.js";
|
||||||
|
import HookEvent from "./hook.js";
|
||||||
|
|
||||||
|
class Workers extends EventEmitter {
|
||||||
|
constructor(ctx) {
|
||||||
|
super();
|
||||||
|
this.ctx = ctx;
|
||||||
|
this.window = ctx.window;
|
||||||
|
this.Worker = this.window.Worker || {};
|
||||||
|
this.Worklet = this.window.Worklet || {};
|
||||||
|
this.workletProto = this.Worklet.prototype || {};
|
||||||
|
this.workerProto = this.Worker.prototype || {};
|
||||||
|
this.postMessage = this.workerProto.postMessage;
|
||||||
|
this.terminate = this.workerProto.terminate;
|
||||||
|
this.addModule = this.workletProto.addModule;
|
||||||
|
};
|
||||||
|
overrideWorker() {
|
||||||
|
this.ctx.override(this.window, 'Worker', (target, that, args) => {
|
||||||
|
if (!args.length) return new target(...args);
|
||||||
|
let [ url, options = {} ] = args;
|
||||||
|
|
||||||
|
const event = new HookEvent({ url, options }, target, that);
|
||||||
|
this.emit('worker', event);
|
||||||
|
|
||||||
|
if (event.intercepted) return event.returnValue;
|
||||||
|
return new event.target(...[ event.data.url, event.data.options ]);
|
||||||
|
}, true);
|
||||||
|
};
|
||||||
|
overrideAddModule() {
|
||||||
|
this.ctx.override(this.workletProto, 'addModule', (target, that, args) => {
|
||||||
|
if (!args.length) return target.apply(that, args);
|
||||||
|
let [ url, options = {} ] = args;
|
||||||
|
|
||||||
|
const event = new HookEvent({ url, options }, target, that);
|
||||||
|
this.emit('addModule', event);
|
||||||
|
|
||||||
|
if (event.intercepted) return event.returnValue;
|
||||||
|
return event.target.call(event.that, event.data.url, event.data.options);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
overridePostMessage() {
|
||||||
|
this.ctx.override(this.workerProto, 'postMessage', (target, that, args) => {
|
||||||
|
if (!args.length) return target.apply(that, args);
|
||||||
|
let [ message, transfer = [] ] = args;
|
||||||
|
|
||||||
|
const event = new HookEvent({ message, transfer }, target, that);
|
||||||
|
this.emit('postMessage', event);
|
||||||
|
|
||||||
|
if (event.intercepted) return event.returnValue;
|
||||||
|
return event.target.call(event.that, event.data.message, event.data.transfer);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
overrideImportScripts() {
|
||||||
|
this.ctx.override(this.window, 'importScripts', (target, that, scripts) => {
|
||||||
|
if (!scripts.length) return target.apply(that, scripts);
|
||||||
|
|
||||||
|
const event = new HookEvent({ scripts }, target, that);
|
||||||
|
this.emit('importScripts', event);
|
||||||
|
|
||||||
|
if (event.intercepted) return event.returnValue;
|
||||||
|
return event.target.apply(event.that, event.data.scripts);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Workers;
|
4
example/config.json
Normal file
4
example/config.json
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
{
|
||||||
|
"prefix": "/service/",
|
||||||
|
"bare": "/bare/"
|
||||||
|
}
|
138
example/index.js
Normal file
138
example/index.js
Normal file
|
@ -0,0 +1,138 @@
|
||||||
|
import https from "https";
|
||||||
|
import httpStatic from "node-static";
|
||||||
|
import path from "path";
|
||||||
|
import { readFileSync, createReadStream, read } from "fs";
|
||||||
|
import request from "../server/v1/request.js";
|
||||||
|
|
||||||
|
const __dirname = path.resolve(path.dirname(decodeURI(new URL(import.meta.url).pathname))).slice(3);
|
||||||
|
const config = JSON.parse(readFileSync(path.join(__dirname, './config.json'), 'utf-8'));
|
||||||
|
const file = new httpStatic.Server(path.join(__dirname, './static/'));
|
||||||
|
|
||||||
|
const server = https.createServer({
|
||||||
|
key: readFileSync(path.join(__dirname, './ssl.key')),
|
||||||
|
cert: readFileSync(path.join(__dirname, './ssl.cert')),
|
||||||
|
});
|
||||||
|
|
||||||
|
server.on('request', (req, res) => {
|
||||||
|
if (req.url.startsWith(config.bare + 'v1/')) {
|
||||||
|
return request(req, res);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (req.url.startsWith('/uv.handler.js')) {
|
||||||
|
res.writeHead(200, { "Content-Type": "application/javascript" });
|
||||||
|
createUVFileStream('uv.handler.js').pipe(res);
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (req.url.startsWith('/uv.sw.js')) {
|
||||||
|
res.writeHead(200, { "Content-Type": "application/javascript" });
|
||||||
|
createUVFileStream('uv.sw.js').pipe(res);
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (req.url.startsWith('/uv.bundle.js')) {
|
||||||
|
res.writeHead(200, { "Content-Type": "application/javascript" });
|
||||||
|
createUVFileStream('uv.bundle.js').pipe(res);
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (req.url.startsWith(config.prefix)) {
|
||||||
|
res.writeHead(200, { "Content-Type": "text/html" });
|
||||||
|
createReadStream(path.join(__dirname, './load.html')).pipe(res);
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
file.serve(req, res);
|
||||||
|
});
|
||||||
|
|
||||||
|
const impl = {
|
||||||
|
'accept-encoding': 'Accept-Encoding',
|
||||||
|
'accept-language': 'Accept-Language',
|
||||||
|
'accept': 'Accept',
|
||||||
|
'sec-websocket-extensions': 'Sec-WebSocket-Extensions',
|
||||||
|
'sec-websocket-key': 'Sec-WebSocket-Key',
|
||||||
|
'sec-websocket-version': 'Sec-WebSocket-Version'
|
||||||
|
};
|
||||||
|
|
||||||
|
server.on('upgrade', (req, socket, head) => {
|
||||||
|
if (!req.url.startsWith('/bare/v1/') || !req.headers['sec-websocket-protocol']) return socket.end();
|
||||||
|
try {
|
||||||
|
const [ bare, data ] = req.headers['sec-websocket-protocol'].split(/,\s*/g);
|
||||||
|
const {
|
||||||
|
remote,
|
||||||
|
headers,
|
||||||
|
forward_headers: forward,
|
||||||
|
} = JSON.parse(decodeProtocol(data));
|
||||||
|
|
||||||
|
for (const header of forward) {
|
||||||
|
if (req.headers[header]) headers[(impl[header] || header)] = req.headers[header];
|
||||||
|
};
|
||||||
|
|
||||||
|
const url = new URL(remote.protocol + '//' + remote.host + ':' + remote.port + remote.path);
|
||||||
|
const remoteRequest = (url.protocol === 'https:' ? https : http).request(
|
||||||
|
url,
|
||||||
|
{
|
||||||
|
headers,
|
||||||
|
method: req.method,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
remoteRequest.on('upgrade', (remoteResponse, remoteSocket, remoteHead) => {
|
||||||
|
let handshake = 'HTTP/1.1 101 Web Socket Protocol Handshake\r\n';
|
||||||
|
if (remoteResponse.headers['sec-websocket-accept']) handshake += `Sec-WebSocket-Accept: ${remoteResponse.headers['sec-websocket-accept']}\r\n`;
|
||||||
|
if (remoteResponse.headers['sec-websocket-extensions']) handshake += `Sec-WebSocket-Extensions: ${remoteResponse.headers['sec-websocket-extensions']}\r\n`;
|
||||||
|
handshake += `Sec-WebSocket-Protocol: bare\r\n`;
|
||||||
|
if (remoteResponse.headers['connection']) handshake += `Connection: ${remoteResponse.headers['connection']}\r\n`;
|
||||||
|
if (remoteResponse.headers['upgrade']) handshake += `Upgrade: ${remoteResponse.headers['upgrade']}\r\n`;
|
||||||
|
handshake += '\r\n';
|
||||||
|
socket.write(handshake);
|
||||||
|
socket.write(remoteHead);
|
||||||
|
remoteSocket.on('close', () => socket.end());
|
||||||
|
socket.on('close', () => remoteSocket.end());
|
||||||
|
remoteSocket.on('error', () => socket.end());
|
||||||
|
socket.on('error', () => remoteSocket.end());
|
||||||
|
|
||||||
|
remoteSocket.pipe(socket);
|
||||||
|
socket.pipe(remoteSocket);
|
||||||
|
});
|
||||||
|
|
||||||
|
remoteRequest.on('error', () => socket.end());
|
||||||
|
|
||||||
|
remoteRequest.end();
|
||||||
|
|
||||||
|
} catch(e) {
|
||||||
|
console.log(e);
|
||||||
|
socket.end();
|
||||||
|
};
|
||||||
|
})
|
||||||
|
|
||||||
|
function decodeProtocol(protocol){
|
||||||
|
if(typeof protocol != 'string')throw new TypeError('protocol must be a string');
|
||||||
|
|
||||||
|
let result = '';
|
||||||
|
|
||||||
|
for(let i = 0; i < protocol.length; i++){
|
||||||
|
const char = protocol[i];
|
||||||
|
|
||||||
|
if(char == '%'){
|
||||||
|
const code = parseInt(protocol.slice(i + 1, i + 3), 16);
|
||||||
|
const decoded = String.fromCharCode(code);
|
||||||
|
|
||||||
|
result += decoded;
|
||||||
|
i += 2;
|
||||||
|
}else{
|
||||||
|
result += char;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
server.listen(443);
|
||||||
|
|
||||||
|
function createUVFileStream(file) {
|
||||||
|
return createReadStream(
|
||||||
|
path.join(__dirname, '../lib/', file)
|
||||||
|
);
|
||||||
|
};
|
229
example/index.test.js
Normal file
229
example/index.test.js
Normal file
|
@ -0,0 +1,229 @@
|
||||||
|
import http from "http";
|
||||||
|
import https from "https";
|
||||||
|
import httpStatic from "node-static";
|
||||||
|
import path from "path";
|
||||||
|
import { readFileSync, createReadStream } from "fs";
|
||||||
|
import webpack from "webpack";
|
||||||
|
|
||||||
|
const __dirname = path.resolve(path.dirname(decodeURI(new URL(import.meta.url).pathname))).slice(3);
|
||||||
|
const file = new httpStatic.Server(path.join(__dirname, './static/'));
|
||||||
|
|
||||||
|
const server = https.createServer({
|
||||||
|
key: readFileSync(path.join(__dirname, './ssl.key')),
|
||||||
|
cert: readFileSync(path.join(__dirname, './ssl.cert')),
|
||||||
|
});
|
||||||
|
|
||||||
|
server.on('request', (req, res) => {
|
||||||
|
|
||||||
|
if (req.url.startsWith('/service/')) {
|
||||||
|
res.writeHead(200, { "Content-Type": "text/html", "Cache-Control": 'no-cache' });
|
||||||
|
createReadStream(path.join(__dirname, './load.html')).pipe(res);
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!req.url.startsWith('/bare/v1/')) return file.serve(req, res);
|
||||||
|
|
||||||
|
|
||||||
|
try {
|
||||||
|
const headers = JSON.parse(req.headers['x-bare-headers']);
|
||||||
|
const forward = JSON.parse((req.headers['x-bare-forward-headers'] || '[]'));
|
||||||
|
const url = new URL(req.headers['x-bare-protocol'] + '//' + req.headers['x-bare-host'] + ':' + req.headers['x-bare-port'] + req.headers['x-bare-path']);
|
||||||
|
|
||||||
|
for (const header of forward) {
|
||||||
|
if (req.headers[header]) headers[header] = req.headers[header];
|
||||||
|
};
|
||||||
|
|
||||||
|
const remoteRequest = (url.protocol === 'https:' ? https : http).request(
|
||||||
|
url,
|
||||||
|
{
|
||||||
|
headers: headers,
|
||||||
|
method: req.method,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
remoteRequest.on('response', remoteResponse => {
|
||||||
|
remoteResponse.headers['x-bare-headers'] = JSON.stringify(remoteResponse.headers);
|
||||||
|
remoteResponse.headers['x-bare-status'] = remoteResponse.statusCode.toString();
|
||||||
|
remoteResponse.headers['x-bare-status-text'] = remoteResponse.statusMessage;
|
||||||
|
remoteResponse.headers['cache-control'] = 'no-cache';
|
||||||
|
|
||||||
|
const headers = {
|
||||||
|
'x-bare-headers': JSON.stringify(remoteResponse.headers),
|
||||||
|
'x-bare-status': remoteResponse.statusCode.toString(),
|
||||||
|
'x-bare-status-text': remoteResponse.statusMessage,
|
||||||
|
'cache-control': 'no-cache',
|
||||||
|
};
|
||||||
|
|
||||||
|
if (remoteResponse.headers['content-encoding']) headers['content-encoding'] = remoteResponse.headers['content-encoding'];
|
||||||
|
if (remoteResponse.headers['content-length']) headers['content-length'] = remoteResponse.headers['content-length'];
|
||||||
|
|
||||||
|
res.writeHead(200, headers);
|
||||||
|
remoteResponse.pipe(res);
|
||||||
|
});
|
||||||
|
|
||||||
|
remoteRequest.on('error', e => {
|
||||||
|
res.writeHead(500, {});
|
||||||
|
res.end();
|
||||||
|
});
|
||||||
|
|
||||||
|
req.pipe(remoteRequest);
|
||||||
|
} catch(e) {
|
||||||
|
res.writeHead(500, {});
|
||||||
|
res.end();
|
||||||
|
};
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
const impl = {
|
||||||
|
'accept-encoding': 'Accept-Encoding',
|
||||||
|
'accept-language': 'Accept-Language',
|
||||||
|
'accept': 'Accept',
|
||||||
|
'sec-websocket-extensions': 'Sec-WebSocket-Extensions',
|
||||||
|
'sec-websocket-key': 'Sec-WebSocket-Key',
|
||||||
|
'sec-websocket-version': 'Sec-WebSocket-Version'
|
||||||
|
};
|
||||||
|
|
||||||
|
server.on('upgrade', (req, socket, head) => {
|
||||||
|
if (!req.url.startsWith('/bare/v1/') || !req.headers['sec-websocket-protocol']) return socket.end();
|
||||||
|
try {
|
||||||
|
const [ bare, data ] = req.headers['sec-websocket-protocol'].split(/,\s*/g);
|
||||||
|
const {
|
||||||
|
remote,
|
||||||
|
headers,
|
||||||
|
forward_headers: forward,
|
||||||
|
} = JSON.parse(decodeProtocol(data));
|
||||||
|
|
||||||
|
for (const header of forward) {
|
||||||
|
if (req.headers[header]) headers[(impl[header] || header)] = req.headers[header];
|
||||||
|
};
|
||||||
|
|
||||||
|
const url = new URL(remote.protocol + '//' + remote.host + ':' + remote.port + remote.path);
|
||||||
|
const remoteRequest = (url.protocol === 'https:' ? https : http).request(
|
||||||
|
url,
|
||||||
|
{
|
||||||
|
headers,
|
||||||
|
method: req.method,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
remoteRequest.on('upgrade', (remoteResponse, remoteSocket, remoteHead) => {
|
||||||
|
let handshake = 'HTTP/1.1 101 Web Socket Protocol Handshake\r\n';
|
||||||
|
if (remoteResponse.headers['sec-websocket-accept']) handshake += `Sec-WebSocket-Accept: ${remoteResponse.headers['sec-websocket-accept']}\r\n`;
|
||||||
|
if (remoteResponse.headers['sec-websocket-extensions']) handshake += `Sec-WebSocket-Extensions: ${remoteResponse.headers['sec-websocket-extensions']}\r\n`;
|
||||||
|
handshake += `Sec-WebSocket-Protocol: bare\r\n`;
|
||||||
|
if (remoteResponse.headers['connection']) handshake += `Connection: ${remoteResponse.headers['connection']}\r\n`;
|
||||||
|
if (remoteResponse.headers['upgrade']) handshake += `Upgrade: ${remoteResponse.headers['upgrade']}\r\n`;
|
||||||
|
handshake += '\r\n';
|
||||||
|
socket.write(handshake);
|
||||||
|
socket.write(remoteHead);
|
||||||
|
remoteSocket.on('close', () => socket.end());
|
||||||
|
socket.on('close', () => remoteSocket.end());
|
||||||
|
remoteSocket.on('error', () => socket.end());
|
||||||
|
socket.on('error', () => remoteSocket.end());
|
||||||
|
|
||||||
|
remoteSocket.pipe(socket);
|
||||||
|
socket.pipe(remoteSocket);
|
||||||
|
});
|
||||||
|
|
||||||
|
remoteRequest.on('error', () => socket.end());
|
||||||
|
|
||||||
|
remoteRequest.end();
|
||||||
|
|
||||||
|
} catch(e) {
|
||||||
|
console.log(e);
|
||||||
|
socket.end();
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
server.listen(443);
|
||||||
|
|
||||||
|
|
||||||
|
const valid_chars = "!#$%&'*+-.0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ^_`abcdefghijklmnopqrstuvwxyz|~";
|
||||||
|
const reserved_chars = "%";
|
||||||
|
|
||||||
|
function encodeProtocol(protocol){
|
||||||
|
protocol = protocol.toString();
|
||||||
|
|
||||||
|
let result = '';
|
||||||
|
|
||||||
|
for(let i = 0; i < protocol.length; i++){
|
||||||
|
const char = protocol[i];
|
||||||
|
|
||||||
|
if(valid_chars.includes(char) && !reserved_chars.includes(char)){
|
||||||
|
result += char;
|
||||||
|
}else{
|
||||||
|
const code = char.charCodeAt();
|
||||||
|
result += '%' + code.toString(16).padStart(2, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function decodeProtocol(protocol){
|
||||||
|
if(typeof protocol != 'string')throw new TypeError('protocol must be a string');
|
||||||
|
|
||||||
|
let result = '';
|
||||||
|
|
||||||
|
for(let i = 0; i < protocol.length; i++){
|
||||||
|
const char = protocol[i];
|
||||||
|
|
||||||
|
if(char == '%'){
|
||||||
|
const code = parseInt(protocol.slice(i + 1, i + 3), 16);
|
||||||
|
const decoded = String.fromCharCode(code);
|
||||||
|
|
||||||
|
result += decoded;
|
||||||
|
i += 2;
|
||||||
|
}else{
|
||||||
|
result += char;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function parseRawHeaders(rawHeaders = []) {
|
||||||
|
const obj = {};
|
||||||
|
|
||||||
|
for (let i = 0; i < rawHeaders.length; i+=2) {
|
||||||
|
const name = rawHeaders[i] || '';
|
||||||
|
const lowerCaseName = name.toLowerCase();
|
||||||
|
const value = rawHeaders[i + 1] || '';
|
||||||
|
|
||||||
|
if (lowerCaseName in obj) {
|
||||||
|
if (Array.isArray(obj[lowerCaseName].value)) {
|
||||||
|
obj[lowerCaseName].value.push(value);
|
||||||
|
} else {
|
||||||
|
obj[lowerCaseName].value = [ obj[lowerCaseName].value, value ];
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
obj[lowerCaseName] = { name, value };
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
return obj;
|
||||||
|
};
|
||||||
|
|
||||||
|
function compileParsedHeaders(headers = {}, prefix = false) {
|
||||||
|
const compiled = {};
|
||||||
|
|
||||||
|
for (const key in headers) {
|
||||||
|
const { name, value } = headers[key];
|
||||||
|
compiled[(prefix ? 'x-op-' : '') + name] = value;
|
||||||
|
};
|
||||||
|
|
||||||
|
return compiled;
|
||||||
|
};
|
||||||
|
|
||||||
|
webpack({
|
||||||
|
mode: 'none',
|
||||||
|
entry: path.join(__dirname, '../lib/index.js'),
|
||||||
|
output: {
|
||||||
|
path: __dirname,
|
||||||
|
filename: './static/op.bundle.js',
|
||||||
|
}
|
||||||
|
}, (err, i) =>
|
||||||
|
console.log(!err ? 'Ultraviolet bundled!' : 'Err')
|
||||||
|
);
|
16
example/load.html
Normal file
16
example/load.html
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
<!DOCTYPE HTML>
|
||||||
|
<html>
|
||||||
|
<head></head>
|
||||||
|
<body>
|
||||||
|
<script>
|
||||||
|
if ('serviceWorker' in navigator) {
|
||||||
|
navigator.serviceWorker.register('/uv.sw.js', {
|
||||||
|
scope: '/service/'
|
||||||
|
});
|
||||||
|
navigator.serviceWorker.ready.then(() => {
|
||||||
|
location.reload()
|
||||||
|
})
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
22
example/ssl.cert
Normal file
22
example/ssl.cert
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIDqzCCApOgAwIBAgIJAJnCkScWtmL0MA0GCSqGSIb3DQEBCwUAMGwxCzAJBgNV
|
||||||
|
BAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMRgwFgYDVQQKDA9UaXRhbml1bU5l
|
||||||
|
dHdvcmsxDjAMBgNVBAsMBWdhbWVyMR4wHAYDVQQDDBUqLnRpdGFuaXVtbmV0d29y
|
||||||
|
ay5vcmcwHhcNMjAwNjEzMTg0OTU2WhcNMjEwNjEzMTg0OTU2WjBsMQswCQYDVQQG
|
||||||
|
EwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEYMBYGA1UECgwPVGl0YW5pdW1OZXR3
|
||||||
|
b3JrMQ4wDAYDVQQLDAVnYW1lcjEeMBwGA1UEAwwVKi50aXRhbml1bW5ldHdvcmsu
|
||||||
|
b3JnMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwPL69+RE6r8RrFh4
|
||||||
|
njzC8ZRnLB+yNtuGw14C0dvNb5JwgdLl5g9/wK/s0V5NGlqwxlQlxQ/gUSuYEcUR
|
||||||
|
6MYjcnaUmZZe/gaKVV0fkfkuigOWhLnI5AQxx7rhkzx1ujuyJ9D2pkDtZpSvv0yn
|
||||||
|
2yrvWhJMtjuxGYip8jaLuRpbXoafvR7nrlDaNcE/GwIjnCCxsRnY2bGbxYK840mN
|
||||||
|
fuMfF2nz+fXKPuQ/9PT48e3wOo9vM5s7yKhiHYwrogqzGN4cH4sSr1FE8C7flFyT
|
||||||
|
Yw101u7fUaopfeGCo9Pg6IrfzyzE5Qb7OlqlVk2IkvXx7pPqVc6lZCJEhOX/qF9o
|
||||||
|
n3mFqwIDAQABo1AwTjAdBgNVHQ4EFgQUC561ob2kGtFQ4az6y64b98+Fy+IwHwYD
|
||||||
|
VR0jBBgwFoAUC561ob2kGtFQ4az6y64b98+Fy+IwDAYDVR0TBAUwAwEB/zANBgkq
|
||||||
|
hkiG9w0BAQsFAAOCAQEAotvUsSLSzFyxQz329tEPyH6Tmi19FQoA5ZbLg6EqeTI9
|
||||||
|
08qOByDGkSYJi0npaIlPO1I557NxRzdO0PxK3ybol6lnzuSlqCJP5nb1dr0z2Eax
|
||||||
|
wgKht9P+ap/yozU5ye05ah2nkpcaeDPnwnnWFmfsnYNfgu62EshOS+5FETWEKVUb
|
||||||
|
LXQhGInOdJq8KZvhoLZWJoUhyAqxBfW4oVvaqs+Ff96A2NNKrvbiAVYX30rVa+x0
|
||||||
|
KIl0/DoVvDx2Q6TiL396cAXdKUW7edRQcSsGFcxwIrU5lePm0V05aN+oCoEBvXBG
|
||||||
|
ArPN+a5kpGjJwfcpcBVf9cJ6IsvptGS9de3eTHoTyw==
|
||||||
|
-----END CERTIFICATE-----
|
28
example/ssl.key
Normal file
28
example/ssl.key
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
-----BEGIN PRIVATE KEY-----
|
||||||
|
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDA8vr35ETqvxGs
|
||||||
|
WHiePMLxlGcsH7I224bDXgLR281vknCB0uXmD3/Ar+zRXk0aWrDGVCXFD+BRK5gR
|
||||||
|
xRHoxiNydpSZll7+BopVXR+R+S6KA5aEucjkBDHHuuGTPHW6O7In0PamQO1mlK+/
|
||||||
|
TKfbKu9aEky2O7EZiKnyNou5Gltehp+9HueuUNo1wT8bAiOcILGxGdjZsZvFgrzj
|
||||||
|
SY1+4x8XafP59co+5D/09Pjx7fA6j28zmzvIqGIdjCuiCrMY3hwfixKvUUTwLt+U
|
||||||
|
XJNjDXTW7t9Rqil94YKj0+Doit/PLMTlBvs6WqVWTYiS9fHuk+pVzqVkIkSE5f+o
|
||||||
|
X2ifeYWrAgMBAAECggEAbihK8Ev6rKr5RBQeiPjXs2SuoppV/MvIXLHHmliLKS/J
|
||||||
|
29S0PGyM202VPtM/4dP1KMXR6nft8WmaIEsKtoKoqijZHfajtRO21pWb+JLy5wi1
|
||||||
|
XoFTGBrs8MLZFl5mODTsuZ6rsq9O2kn5LJZvHsmcbSgVc9UQfytvG0HY840ArS3g
|
||||||
|
kSDtUFb1xRui6wtCBKzHVvCT+FXhSBbwkHalmbqP6BefhJ3lW2VonkOcHDrdXPfW
|
||||||
|
CEN18IJ2v8QYgXqZP6VUlAweNXLJ33ZOl+jXGdygcOG24MFqdw0VtP0XFGk0jnSS
|
||||||
|
W6dX67BZKeZ71EKaTy02jw5LpQNXA70ismPJHQ2uQQKBgQDuROawnBIW1fC3xOle
|
||||||
|
m+JmP0eMe0eIQycxRsMXsXhYAA0wV3qYZSLZrNK2eRhmSNt+ODSmZ2Vt11dwOv5u
|
||||||
|
bo8WONrRlM097SmitS2S+8o7ASem2VKQzyRE72Y9517Q+aNBdLRVtjrRNSw/hfSu
|
||||||
|
ayLuG36+yukSH7wq7mfoUX34ZwKBgQDPTrgyyw8n5XhZT/qTTRnQJ2GTvPxDzNoJ
|
||||||
|
IAGhGJGFAb6wgLoSpGx6BC122vuRxcTjkjAiMDci5N2zNW+YZVni+F0KTVvNFfU2
|
||||||
|
pOTJUg3luRTygCra6O02PxwpbP/9KCBAKq/kYw/eBW+gxhPwP3ZrbAirvBjgBh0I
|
||||||
|
kIrFijNOHQKBgGUUAbFGZD4fwCCVflLOWnr5uUaVPcFGi6fR1w2EEgNy8iVh1vYz
|
||||||
|
YVdqg3E5aepqWgLvoRY+or64LbXEsQ70A+tvbxSdxXvR0mnd5lmGS0JAuSuE4gvg
|
||||||
|
dAhybrMwJf8NB/7KnX4G8mix3/WKxEQB2y2bqGcT+U/g+phTzuy1NXVdAoGBAIrl
|
||||||
|
jVjK4J60iswcYCEteWwT1rbr2oF60WNnxG+xTF63apJLzWAMNnoSLnwCAKgMv/xR
|
||||||
|
yFo/v9FrUnduCBUtYupFyeDLMATa/27bUEbq6VDPjw9jfFMr2TONWUsQMvvlVKZp
|
||||||
|
c2wsS0dQkRhBXr6LZsZWngCiiHAg6HcCkVgFXpapAoGBAJ/8oLGt0Ar+0MTl+gyk
|
||||||
|
xSqgHnsc5jgqhix3nIoI5oEAbfibdGmRD1S3rtWD9YsnPxMIl+6E5bOAHrmd+Zr8
|
||||||
|
O7EP+CLvbz4JXidaaa85h9ThXSG5xk1A1UTtSFrp+KolLE1Vvmjjd+R844XsM2wZ
|
||||||
|
OAHbihzk0iPPphjEWR4lU4Av
|
||||||
|
-----END PRIVATE KEY-----
|
17
example/static/index.html
Normal file
17
example/static/index.html
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/idb@7/build/umd.js"></script>
|
||||||
|
<script src="uv.bundle.js"></script>
|
||||||
|
<script src="uv.handler.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Testing</h1>
|
||||||
|
<iframe src="/"></iframe>
|
||||||
|
<script>
|
||||||
|
//console.log(__uv$get(window, 'postMessage', __uv) !== __uv$get(parent, 'postMessage', __uv));
|
||||||
|
const frame = document.querySelector('iframe').contentWindow;
|
||||||
|
addEventListener('message', e => console.log(e.source.postMessage !== window.postMessage));
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>__uv$get(frame, 'postMessage', __uv)
|
6
example/static/index.js
Normal file
6
example/static/index.js
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
importScripts('./uv.bundle.js');
|
||||||
|
importScripts('./uv.handler.js');
|
||||||
|
|
||||||
|
__uv.client.location.overrideWorkerLocation(() => new URL('https://www.google.com'));
|
||||||
|
|
||||||
|
console.log(postMessage.__uv$string);
|
24795
example/static/op.bundle.js
Normal file
24795
example/static/op.bundle.js
Normal file
File diff suppressed because one or more lines are too long
765
example/static/op.handler.js
Normal file
765
example/static/op.handler.js
Normal file
|
@ -0,0 +1,765 @@
|
||||||
|
async function __opHook(window, worker = false) {
|
||||||
|
if ('__op' in window && window.__op instanceof Rewriter) return false;
|
||||||
|
|
||||||
|
const __op = window.__op = new Rewriter({
|
||||||
|
prefix: '/service/',
|
||||||
|
window
|
||||||
|
});
|
||||||
|
|
||||||
|
__op.loc = __op.oxidation.nativeMethods.getOwnPropertyDescriptor(window, 'location')
|
||||||
|
|
||||||
|
// Website data
|
||||||
|
__op.meta.origin = location.origin;
|
||||||
|
|
||||||
|
let urlStr = __op.sourceUrl(window.location.href);
|
||||||
|
if (urlStr.startsWith('blob:')) urlStr = urlStr.slice('blob:'.length);
|
||||||
|
if (urlStr.startsWith('about:')) urlStr = window.parent.__op.meta.url.href;
|
||||||
|
|
||||||
|
__op.cookieStr = window.__opCookies || '';
|
||||||
|
__op.meta.base = __op.meta.url = new URL(urlStr);
|
||||||
|
__op.domain = __op.meta.url.host;
|
||||||
|
__op.blobUrls = new window.Map();
|
||||||
|
__op.referrer = '';
|
||||||
|
__op.cookies = [];
|
||||||
|
__op.brazenKeys = [
|
||||||
|
'__opLocation',
|
||||||
|
'__opSource',
|
||||||
|
'__opSetSource',
|
||||||
|
'__opPostMessage',
|
||||||
|
'__opEval',
|
||||||
|
'__opOriginalPM',
|
||||||
|
];
|
||||||
|
__op.sw = 'serviceWorker' in window.navigator ? window.navigator.serviceWorker.controller : false;
|
||||||
|
|
||||||
|
if (window.__opCookies) delete window.__opCookies;
|
||||||
|
if (window.__opReferrer) {
|
||||||
|
__op.referrer = __op.sourceUrl(window.__opReferrer);
|
||||||
|
delete window.__opReferrer;
|
||||||
|
};
|
||||||
|
|
||||||
|
window.__opTemp = [];
|
||||||
|
|
||||||
|
// Client-side hooking
|
||||||
|
const { oxidation: __oxidation } = __op;
|
||||||
|
|
||||||
|
let rawBase = window.document ? __oxidation.node.baseURI.get.call(window.document) : window.location.href;
|
||||||
|
let base = __op.sourceUrl(rawBase);
|
||||||
|
__oxidation.initLocation(__op.rewriteUrl.bind(__op), __op.sourceUrl.bind(__op));
|
||||||
|
|
||||||
|
__oxidation.nativeMethods.defineProperty(__op.meta, 'base', {
|
||||||
|
get() {
|
||||||
|
if (!window.document) return __op.meta.url;
|
||||||
|
|
||||||
|
if (__oxidation.node.baseURI.get.call(window.document) !== rawBase) {
|
||||||
|
rawBase = __oxidation.node.baseURI.get.call(window.document);
|
||||||
|
base = __op.sourceUrl(rawBase);
|
||||||
|
};
|
||||||
|
|
||||||
|
return base;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
HTMLMediaElement,
|
||||||
|
HTMLScriptElement,
|
||||||
|
HTMLAudioElement,
|
||||||
|
HTMLVideoElement,
|
||||||
|
HTMLInputElement,
|
||||||
|
HTMLEmbedElement,
|
||||||
|
HTMLTrackElement,
|
||||||
|
HTMLAnchorElement,
|
||||||
|
HTMLIFrameElement,
|
||||||
|
HTMLAreaElement,
|
||||||
|
HTMLLinkElement,
|
||||||
|
HTMLBaseElement,
|
||||||
|
HTMLFormElement,
|
||||||
|
HTMLImageElement,
|
||||||
|
HTMLSourceElement,
|
||||||
|
} = window;
|
||||||
|
|
||||||
|
// Fetch
|
||||||
|
__oxidation.fetch.on('request', event => {
|
||||||
|
event.data.input = __op.rewriteUrl(event.data.input);
|
||||||
|
});
|
||||||
|
|
||||||
|
__oxidation.fetch.on('requestUrl', event => {
|
||||||
|
event.data.value = __op.sourceUrl(event.data.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
__oxidation.fetch.on('responseUrl', event => {
|
||||||
|
event.data.value = __op.sourceUrl(event.data.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
// XMLHttpRequest
|
||||||
|
__oxidation.xhr.on('open', event => {
|
||||||
|
event.data.input = __op.rewriteUrl(event.data.input);
|
||||||
|
});
|
||||||
|
|
||||||
|
__oxidation.xhr.on('responseUrl', event => {
|
||||||
|
event.data.value = __op.sourceUrl(event.data.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Workers
|
||||||
|
__oxidation.workers.on('worker', event => {
|
||||||
|
event.data.url = __op.rewriteUrl(event.data.url);
|
||||||
|
});
|
||||||
|
|
||||||
|
__oxidation.workers.on('addModule', event => {
|
||||||
|
event.data.url = __op.rewriteUrl(event.data.url);
|
||||||
|
});
|
||||||
|
|
||||||
|
__oxidation.workers.on('importScripts', event => {
|
||||||
|
for (const i in event.data.scripts) {
|
||||||
|
event.data.scripts[i] = __op.rewriteUrl(event.data.scripts[i]);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
__oxidation.workers.on('postMessage', event => {
|
||||||
|
let to = event.data.origin;
|
||||||
|
|
||||||
|
event.data.origin = '*';
|
||||||
|
event.data.message = {
|
||||||
|
__data: event.data.message,
|
||||||
|
__origin: __op.meta.url.origin,
|
||||||
|
__to: to,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// Navigator
|
||||||
|
__oxidation.navigator.on('sendBeacon', event => {
|
||||||
|
event.data.url = __op.rewriteUrl(event.data.url);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Cookies
|
||||||
|
__oxidation.document.on('getCookie', event => {
|
||||||
|
event.data.value = __op.cookieStr;
|
||||||
|
});
|
||||||
|
|
||||||
|
__oxidation.document.on('setCookie', event => {
|
||||||
|
Promise.resolve(__op.cookie.setCookies(event.data.value, __op.db, __op.meta)).then(() => {
|
||||||
|
__op.cookie.db().then(db => {
|
||||||
|
__op.cookie.getCookies(db).then(cookies => {
|
||||||
|
__op.cookieStr = __op.cookie.serialize(cookies, __op.meta, true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
const cookie = __op.cookie.setCookie(event.data.value)[0];
|
||||||
|
|
||||||
|
if (!cookie.path) cookie.path = '/';
|
||||||
|
if (!cookie.domain) cookie.domain = __op.meta.url.hostname;
|
||||||
|
|
||||||
|
if (__op.cookie.validateCookie(cookie, __op.meta, true)) {
|
||||||
|
if (__op.cookieStr.length) __op.cookieStr += '; ';
|
||||||
|
__op.cookieStr += `${cookie.name}=${cookie.value}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
event.respondWith(event.data.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
// HTML
|
||||||
|
__oxidation.element.on('setInnerHTML', event => {
|
||||||
|
switch(event.that.tagName) {
|
||||||
|
case 'SCRIPT':
|
||||||
|
event.data.value = __op.js.rewrite(event.data.value);
|
||||||
|
break;
|
||||||
|
case 'STYLE':
|
||||||
|
event.data.value = __op.rewriteCSS(event.data.value);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
event.data.value = __op.rewriteHtml(event.data.value);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
__oxidation.element.on('getInnerHTML', event => {
|
||||||
|
switch(event.that.tagName) {
|
||||||
|
case 'SCRIPT':
|
||||||
|
event.data.value = __op.js.source(event.data.value);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
event.data.value = __op.sourceHtml(event.data.value);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
__oxidation.element.on('setOuterHTML', event => {
|
||||||
|
event.data.value = __op.rewriteHtml(event.data.value, { document: event.that.tagName === 'HTML' });
|
||||||
|
});
|
||||||
|
|
||||||
|
__oxidation.element.on('getOuterHTML', event => {
|
||||||
|
switch(event.that.tagName) {
|
||||||
|
case 'HEAD':
|
||||||
|
event.data.value = __op.sourceHtml(
|
||||||
|
event.data.value.replace(/<head(.*)>(.*)<\/head>/s, '<op-head$1>$2</op-head>')
|
||||||
|
).replace(/<op-head(.*)>(.*)<\/op-head>/s, '<head$1>$2</head>');
|
||||||
|
break;
|
||||||
|
case 'BODY':
|
||||||
|
event.data.value = __op.sourceHtml(
|
||||||
|
event.data.value.replace(/<body(.*)>(.*)<\/body>/s, '<op-body$1>$2</op-body>')
|
||||||
|
).replace(/<op-body(.*)>(.*)<\/op-body>/s, '<body$1>$2</body>');
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
event.data.value = __op.sourceHtml(event.data.value, { document: event.that.tagName === 'HTML' });
|
||||||
|
break;
|
||||||
|
};
|
||||||
|
|
||||||
|
//event.data.value = __op.sourceHtml(event.data.value, { document: event.that.tagName === 'HTML' });
|
||||||
|
});
|
||||||
|
|
||||||
|
__oxidation.document.on('write', event => {
|
||||||
|
if (!event.data.html.length) return false;
|
||||||
|
event.data.html = [ __op.rewriteHtml(event.data.html.join('')) ];
|
||||||
|
});
|
||||||
|
|
||||||
|
__oxidation.document.on('writeln', event => {
|
||||||
|
if (!event.data.html.length) return false;
|
||||||
|
event.data.html = [ __op.rewriteHtml(event.data.html.join('')) ];
|
||||||
|
});
|
||||||
|
|
||||||
|
__oxidation.element.on('insertAdjacentHTML', event => {
|
||||||
|
event.data.html = __op.rewriteHtml(event.data.html);
|
||||||
|
});
|
||||||
|
|
||||||
|
// EventSource
|
||||||
|
|
||||||
|
__oxidation.eventSource.on('construct', event => {
|
||||||
|
event.data.url = __op.rewriteUrl(event.data.url);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
__oxidation.eventSource.on('url', event => {
|
||||||
|
event.data.url = __op.rewriteUrl(event.data.url);
|
||||||
|
});
|
||||||
|
|
||||||
|
// History
|
||||||
|
__oxidation.history.on('replaceState', event => {
|
||||||
|
if (event.data.url) event.data.url = __op.rewriteUrl(event.data.url, '__op' in event.that ? event.that.__op.meta : __op.meta);
|
||||||
|
});
|
||||||
|
__oxidation.history.on('pushState', event => {
|
||||||
|
if (event.data.url) event.data.url = __op.rewriteUrl(event.data.url, '__op' in event.that ? event.that.__op.meta : __op.meta);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Element get set attribute methods
|
||||||
|
__oxidation.element.on('getAttribute', event => {
|
||||||
|
if (__oxidation.element.hasAttribute.call(event.that, __op.attributePrefix + '-attr-' + event.data.name)) {
|
||||||
|
event.respondWith(
|
||||||
|
event.target.call(event.that, __op.attributePrefix + '-attr-' + event.data.name)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Message
|
||||||
|
__oxidation.message.on('postMessage', event => {
|
||||||
|
let to = event.data.origin;
|
||||||
|
|
||||||
|
event.data.origin = '*';
|
||||||
|
event.data.message = {
|
||||||
|
__data: event.data.message,
|
||||||
|
__origin: __op.meta.url.origin,
|
||||||
|
__to: to,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
__oxidation.message.on('data', event => {
|
||||||
|
const { value: data } = event.data;
|
||||||
|
if (typeof data === 'object' && '__data' in data && '__origin' in data) {
|
||||||
|
event.respondWith(data.__data);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
__oxidation.message.on('origin', event => {
|
||||||
|
const data = __oxidation.message.messageData.get.call(event.that);
|
||||||
|
if (typeof data === 'object' && data.__data && data.__origin) {
|
||||||
|
event.respondWith(data.__origin);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
__oxidation.overrideDescriptor(window, 'origin', {
|
||||||
|
get: (target, that) => {
|
||||||
|
return __oxidation.location.origin;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
__oxidation.node.on('baseURI', event => {
|
||||||
|
if (event.data.value.startsWith(window.location.origin)) event.data.value = __op.sourceUrl(event.data.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
__oxidation.element.on('setAttribute', event => {
|
||||||
|
if (event.that instanceof HTMLMediaElement && event.data.name === 'src' && event.data.value.startsWith('blob:')) {
|
||||||
|
event.target.call(event.that, __op.attributePrefix + '-attr-' + event.data.name, event.data.value);
|
||||||
|
event.data.value = __op.blobUrls.get(event.data.value);
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (__op.attrs.isUrl(event.data.name)) {
|
||||||
|
event.target.call(event.that, __op.attributePrefix + '-attr-' + event.data.name, event.data.value);
|
||||||
|
event.data.value = __op.rewriteUrl(event.data.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (__op.attrs.isStyle(event.data.name)) {
|
||||||
|
event.target.call(event.that, __op.attributePrefix + '-attr-' + event.data.name, event.data.value);
|
||||||
|
event.data.value = __op.rewriteCSS(event.data.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (__op.attrs.isHtml(event.data.name)) {
|
||||||
|
event.target.call(event.that, __op.attributePrefix + '-attr-' + event.data.name, event.data.value);
|
||||||
|
event.data.value = __op.rewriteHtml(event.data.value, { ...__op.meta, document: true });
|
||||||
|
};
|
||||||
|
|
||||||
|
if (__op.attrs.isForbidden(event.data.name)) {
|
||||||
|
event.data.name = __op.attributePrefix + '-attr-' + event.data.name;
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
__oxidation.element.on('audio', event => {
|
||||||
|
event.data.url = __op.rewriteUrl(event.data.url);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Element Property Attributes
|
||||||
|
__oxidation.element.hookProperty([ HTMLAnchorElement, HTMLAreaElement, HTMLLinkElement, HTMLBaseElement ], 'href', {
|
||||||
|
get: (target, that) => {
|
||||||
|
return __op.sourceUrl(
|
||||||
|
target.call(that)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
set: (target, that, [ val ]) => {
|
||||||
|
__oxidation.element.setAttribute.call(that, __op.attributePrefix + '-attr-href', val)
|
||||||
|
target.call(that, __op.rewriteUrl(val));
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
__oxidation.element.hookProperty([ HTMLScriptElement, HTMLMediaElement, HTMLImageElement, HTMLInputElement, HTMLEmbedElement, HTMLIFrameElement, HTMLTrackElement, HTMLSourceElement ], 'src', {
|
||||||
|
get: (target, that) => {
|
||||||
|
return __op.sourceUrl(
|
||||||
|
target.call(that)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
set: (target, that, [ val ]) => {
|
||||||
|
if (new String(val).toString().trim().startsWith('blob:') && that instanceof HTMLMediaElement) {
|
||||||
|
__oxidation.element.setAttribute.call(that, __op.attributePrefix + '-attr-src', val)
|
||||||
|
return target.call(that, __op.blobUrls.get(val) || val);
|
||||||
|
};
|
||||||
|
|
||||||
|
__oxidation.element.setAttribute.call(that, __op.attributePrefix + '-attr-src', val)
|
||||||
|
target.call(that, __op.rewriteUrl(val));
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
__oxidation.element.hookProperty([ HTMLFormElement ], 'action', {
|
||||||
|
get: (target, that) => {
|
||||||
|
return __op.sourceUrl(
|
||||||
|
target.call(that)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
set: (target, that, [ val ]) => {
|
||||||
|
__oxidation.element.setAttribute.call(that, __op.attributePrefix + '-attr-action', val)
|
||||||
|
target.call(that, __op.rewriteUrl(val));
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
__oxidation.element.hookProperty(HTMLScriptElement, 'integrity', {
|
||||||
|
get: (target, that) => {
|
||||||
|
return __oxidation.element.getAttribute.call(that, __op.attributePrefix + '-attr-integrity');
|
||||||
|
},
|
||||||
|
set: (target, that, [ val ]) => {
|
||||||
|
__oxidation.element.setAttribute.call(that, __op.attributePrefix + '-attr-integrity', val);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
__oxidation.element.hookProperty(HTMLIFrameElement, 'sandbox', {
|
||||||
|
get: (target, that) => {
|
||||||
|
return __oxidation.element.getAttribute.call(that, __op.attributePrefix + '-attr-sandbox') || target.call(that);
|
||||||
|
},
|
||||||
|
set: (target, that, [ val ]) => {
|
||||||
|
__oxidation.element.setAttribute.call(that, __op.attributePrefix + '-attr-sandbox', val);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
__oxidation.element.hookProperty(HTMLIFrameElement, 'contentWindow', {
|
||||||
|
get: (target, that) => {
|
||||||
|
const win = target.call(that);
|
||||||
|
try {
|
||||||
|
if (!win.__op) __opHook(win);
|
||||||
|
return win;
|
||||||
|
} catch(e) {
|
||||||
|
return win;
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
__oxidation.element.hookProperty(HTMLIFrameElement, 'contentDocument', {
|
||||||
|
get: (target, that) => {
|
||||||
|
const doc = target.call(that);
|
||||||
|
try {
|
||||||
|
const win = doc.defaultView
|
||||||
|
if (!win.__op) __opHook(win);
|
||||||
|
return doc;
|
||||||
|
} catch(e) {
|
||||||
|
return win;
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
__oxidation.node.on('getTextContent', event => {
|
||||||
|
if (event.that.tagName === 'SCRIPT') {
|
||||||
|
event.data.value = __op.js.source(event.data.value);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
__oxidation.node.on('setTextContent', event => {
|
||||||
|
if (event.that.tagName === 'SCRIPT') {
|
||||||
|
event.data.value = __op.js.rewrite(event.data.value);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
__oxidation.object.on('getOwnPropertyNames', event => {
|
||||||
|
event.data.names = event.data.names.filter(element => !(__op.brazenKeys.includes(element)));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Document
|
||||||
|
__oxidation.document.on('getDomain', event => {
|
||||||
|
event.data.value = __op.domain;
|
||||||
|
});
|
||||||
|
__oxidation.document.on('setDomain', event => {
|
||||||
|
if (!event.data.value.toString().endsWith(__op.meta.url.hostname.split('.').slice(-2).join('.'))) return event.respondWith('');
|
||||||
|
event.respondWith(__op.domain = event.data.value);
|
||||||
|
})
|
||||||
|
|
||||||
|
__oxidation.document.on('url', event => {
|
||||||
|
event.data.value = __oxidation.location.href;
|
||||||
|
});
|
||||||
|
|
||||||
|
__oxidation.document.on('documentURI', event => {
|
||||||
|
event.data.value = __oxidation.location.href;
|
||||||
|
});
|
||||||
|
|
||||||
|
__oxidation.document.on('referrer', event => {
|
||||||
|
event.data.value = __op.referrer || __op.sourceUrl(event.data.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
__oxidation.document.on('parseFromString', event => {
|
||||||
|
if (event.data.type !== 'text/html') return false;
|
||||||
|
event.data.string = __op.rewriteHtml(event.data.string, { ...__op.meta, document: true, });
|
||||||
|
});
|
||||||
|
|
||||||
|
// Attribute (node.attributes)
|
||||||
|
__oxidation.attribute.on('getValue', event => {
|
||||||
|
if (__oxidation.element.hasAttribute.call(event.that.ownerElement, __op.attributePrefix + '-attr-' + event.data.name)) {
|
||||||
|
event.data.value = __oxidation.element.getAttribute.call(event.that.ownerElement, __op.attributePrefix + '-attr-' + event.data.name);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
__oxidation.attribute.on('setValue', event => {
|
||||||
|
if (__op.attrs.isUrl(event.data.name)) {
|
||||||
|
__oxidation.element.setAttribute.call(event.that.ownerElement, __op.attributePrefix + '-attr-' + event.data.name, event.data.value);
|
||||||
|
event.data.value = __op.rewriteUrl(event.data.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (__op.attrs.isStyle(event.data.name)) {
|
||||||
|
__oxidation.element.setAttribute.call(event.that.ownerElement, __op.attributePrefix + '-attr-' + event.data.name, event.data.value);
|
||||||
|
event.data.value = __op.rewriteCSS(event.data.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (__op.attrs.isHtml(event.data.name)) {
|
||||||
|
__oxidation.element.setAttribute.call(event.that.ownerElement, __op.attributePrefix + '-attr-' + event.data.name, event.data.value);
|
||||||
|
event.data.value = __op.rewriteHtml(event.data.value, { ...__op.meta, document: true });
|
||||||
|
};
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
// URL
|
||||||
|
__oxidation.url.on('createObjectURL', event => {
|
||||||
|
let url = event.target.call(event.that, event.data.object);
|
||||||
|
if (url.startsWith('blob:' + location.origin)) {
|
||||||
|
let newUrl = 'blob:' + __op.meta.url.origin + url.slice('blob:'.length + location.origin.length);
|
||||||
|
|
||||||
|
__op.blobUrls.set(newUrl, url);
|
||||||
|
event.respondWith(newUrl);
|
||||||
|
} else {
|
||||||
|
event.respondWith(url);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
__oxidation.url.on('revokeObjectURL', event => {
|
||||||
|
if (__op.blobUrls.has(event.data.url)) {
|
||||||
|
const old = event.data.url;
|
||||||
|
event.data.url = __op.blobUrls.get(event.data.url);
|
||||||
|
__op.blobUrls.delete(old);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
__oxidation.websocket.on('websocket', event => {
|
||||||
|
const url = new URL(event.data.url);
|
||||||
|
|
||||||
|
const headers = {
|
||||||
|
Host: url.host,
|
||||||
|
Origin: __op.meta.url.origin,
|
||||||
|
Pragma: 'no-cache',
|
||||||
|
'Cache-Control': 'no-cache',
|
||||||
|
Upgrade: 'websocket',
|
||||||
|
'User-Agent': window.navigator.userAgent,
|
||||||
|
'Connection': 'Upgrade',
|
||||||
|
};
|
||||||
|
|
||||||
|
const cookies = __op.cookie.serialize(__op.cookies, { url }, false);
|
||||||
|
|
||||||
|
if (cookies) headers.Cookie = cookies;
|
||||||
|
const protocols = [...event.data.protocols];
|
||||||
|
|
||||||
|
const remote = {
|
||||||
|
protocol: url.protocol === 'wss:' ? 'https:' : 'http:',
|
||||||
|
host: url.hostname,
|
||||||
|
port: url.port,
|
||||||
|
path: url.pathname + url.search,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (protocols.length) headers['Sec-WebSocket-Protocol'] = protocols.join(', ');
|
||||||
|
|
||||||
|
event.data.url = `wss://${window.location.host}/bare/v1/`;
|
||||||
|
event.data.protocols = [
|
||||||
|
'bare',
|
||||||
|
encodeProtocol(JSON.stringify({
|
||||||
|
remote,
|
||||||
|
headers,
|
||||||
|
forward_headers: [
|
||||||
|
'accept',
|
||||||
|
'accept-encoding',
|
||||||
|
'accept-language',
|
||||||
|
'sec-websocket-extensions',
|
||||||
|
'sec-websocket-key',
|
||||||
|
'sec-websocket-version',
|
||||||
|
]
|
||||||
|
})),
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
// Function
|
||||||
|
|
||||||
|
__oxidation.function.on('function', event => {
|
||||||
|
event.data.script = __op.js.rewrite(event.data.script);
|
||||||
|
});
|
||||||
|
|
||||||
|
__oxidation.function.on('toString', event => {
|
||||||
|
if (__oxidation.fnStrings.has(event.data.fn)) return event.respondWith(__oxidation.fnStrings.get(event.data.fn));
|
||||||
|
|
||||||
|
/*
|
||||||
|
const str = event.target.call(event.that);
|
||||||
|
let sourced;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const padding = !str.startsWith('function') && str[0] !== '(';
|
||||||
|
sourced = __op.js.source((padding ? '({' : 'x=') + str + (padding ? '})' : '')).slice(2, str.length + 2);
|
||||||
|
} catch(e) {
|
||||||
|
sourced = str;
|
||||||
|
};
|
||||||
|
|
||||||
|
event.respondWith(sourced);
|
||||||
|
*/
|
||||||
|
});
|
||||||
|
|
||||||
|
if ('WorkerGlobalScope' in window) {
|
||||||
|
__oxidation.overrideDescriptor(window.WorkerGlobalScope.prototype, 'location', {
|
||||||
|
get: (target, that) => {
|
||||||
|
if (!(that instanceof window.WorkerGlobalScope)) return target.call(that);
|
||||||
|
return __oxidation.location;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
__op.pm = null
|
||||||
|
__op.eval = __oxidation.wrap(window, 'eval', (target, that, args) => {
|
||||||
|
if (!args.length || typeof args[0] !== 'string') return target.apply(that, args);
|
||||||
|
let [ script ] = args;
|
||||||
|
|
||||||
|
script = __op.js.rewrite(script);
|
||||||
|
return target.call(that, script);
|
||||||
|
});
|
||||||
|
|
||||||
|
__oxidation.nativeMethods.defineProperty(window.Object.prototype, '__opLocation', {
|
||||||
|
configurable: true,
|
||||||
|
get() {
|
||||||
|
return (this === window.document || this === window) ? __oxidation.location : this.location;
|
||||||
|
},
|
||||||
|
set(val) {
|
||||||
|
if (this === window.document || this === window) {
|
||||||
|
__oxidation.location.href = val;
|
||||||
|
} else {
|
||||||
|
this.location = val;
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
__oxidation.nativeMethods.defineProperty(window.Object.prototype, '__opEval', {
|
||||||
|
configurable: true,
|
||||||
|
get() {
|
||||||
|
return this === window ? __op.eval : this.eval;
|
||||||
|
},
|
||||||
|
set(val) {
|
||||||
|
this.eval = val;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
__oxidation.nativeMethods.defineProperty(window.Object.prototype, '__opSource', {
|
||||||
|
writable: true,
|
||||||
|
value: __op,
|
||||||
|
});
|
||||||
|
|
||||||
|
__oxidation.nativeMethods.defineProperty(window.Object.prototype, '__opSetSource', {
|
||||||
|
configurable: true,
|
||||||
|
get() {
|
||||||
|
return (__op) => {
|
||||||
|
this.__opSource = __op;
|
||||||
|
return this;
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
__oxidation.nativeMethods.defineProperty(window.Object.prototype, '__opPostMessage', {
|
||||||
|
configurable: true,
|
||||||
|
get() {
|
||||||
|
if (window.DedicatedWorkerGlobalScope && this instanceof window.DedicatedWorkerGlobalScope || window.window && this instanceof window.Window) {
|
||||||
|
|
||||||
|
const source = this.__opSource || __op;
|
||||||
|
|
||||||
|
if (!__op.pm) __op.pm = source.oxidation.message.wrapPostMessage(this, '__opOriginalPM', !window.window);
|
||||||
|
return __op.pm;
|
||||||
|
};
|
||||||
|
|
||||||
|
return this.postMessage;
|
||||||
|
},
|
||||||
|
set(val) {
|
||||||
|
this.postMessage = val;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
__oxidation.document.on('querySelector', event => {
|
||||||
|
const elem = event.target.call(event.that, __op.css.rewriteSelector(event.data.selectors, ['src', 'href'], true));
|
||||||
|
event.respondWith((elem || event.target.call(event.that, event.data.selectors)));
|
||||||
|
});
|
||||||
|
|
||||||
|
__oxidation.element.on('querySelector', event => {
|
||||||
|
//console.log('Element:', event.data);
|
||||||
|
});
|
||||||
|
|
||||||
|
/*
|
||||||
|
if (window.document) {
|
||||||
|
__oxidation.override(__oxidation.document.docProto, 'querySelector', (target, that, args) => {
|
||||||
|
console.log(args[0]);
|
||||||
|
return target.apply(that, args);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
window.__opOriginalPM = window.postMessage;
|
||||||
|
|
||||||
|
if (window.History && !worker) __oxidation.nativeMethods.defineProperty(window.History.prototype, '__op', {
|
||||||
|
writable: true,
|
||||||
|
value: __op,
|
||||||
|
enumerable: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Hooking functions & descriptors
|
||||||
|
__oxidation.fetch.overrideRequest();
|
||||||
|
__oxidation.fetch.overrideUrl();
|
||||||
|
__oxidation.xhr.overrideOpen();
|
||||||
|
__oxidation.xhr.overrideResponseUrl();
|
||||||
|
__oxidation.element.overrideHtml();
|
||||||
|
__oxidation.element.overrideAttribute();
|
||||||
|
__oxidation.element.overrideInsertAdjacentHTML();
|
||||||
|
__oxidation.element.overrideAudio();
|
||||||
|
// __oxidation.element.overrideQuerySelector();
|
||||||
|
__oxidation.node.overrideBaseURI();
|
||||||
|
__oxidation.node.overrideTextContent();
|
||||||
|
__oxidation.attribute.override();
|
||||||
|
__oxidation.document.overrideDomain();
|
||||||
|
__oxidation.document.overrideURL();
|
||||||
|
__oxidation.document.overrideDocumentURI();
|
||||||
|
__oxidation.document.overrideWrite();
|
||||||
|
__oxidation.document.overrideReferrer();
|
||||||
|
__oxidation.document.overrideParseFromString();
|
||||||
|
//__oxidation.document.overrideQuerySelector();
|
||||||
|
__oxidation.object.overrideGetPropertyNames();
|
||||||
|
__oxidation.history.overridePushState();
|
||||||
|
__oxidation.history.overrideReplaceState();
|
||||||
|
__oxidation.eventSource.overrideConstruct();
|
||||||
|
__oxidation.eventSource.overrideUrl();
|
||||||
|
__oxidation.websocket.overrideWebSocket();
|
||||||
|
__oxidation.websocket.overrideProtocol();
|
||||||
|
__oxidation.websocket.overrideUrl();
|
||||||
|
__oxidation.url.overrideObjectURL();
|
||||||
|
__oxidation.document.overrideCookie();
|
||||||
|
__oxidation.message.overridePostMessage();
|
||||||
|
__oxidation.message.overrideMessageOrigin();
|
||||||
|
__oxidation.message.overrideMessageData();
|
||||||
|
__oxidation.workers.overrideWorker();
|
||||||
|
__oxidation.workers.overrideAddModule();
|
||||||
|
__oxidation.workers.overrideImportScripts();
|
||||||
|
__oxidation.workers.overridePostMessage();
|
||||||
|
__oxidation.navigator.overrideSendBeacon();
|
||||||
|
__oxidation.function.overrideFunction();
|
||||||
|
__oxidation.function.overrideToString();
|
||||||
|
__op.addRewrites();
|
||||||
|
|
||||||
|
__op.$wrap = function(name) {
|
||||||
|
if (name === 'location') return '__opLocation';
|
||||||
|
if (name === 'eval') return '__opEval';
|
||||||
|
|
||||||
|
return name;
|
||||||
|
};
|
||||||
|
|
||||||
|
__op.$get = function(that) {
|
||||||
|
if (that === window.location) return __oxidation.location;
|
||||||
|
if (that === window.eval) return __op.eval;
|
||||||
|
return that;
|
||||||
|
};
|
||||||
|
|
||||||
|
__op.db = await __op.cookie.db();
|
||||||
|
//_op.cookieStr = await __op.cookie.getCookies(__op.db.getAll('cookies'), __op.meta, true);
|
||||||
|
|
||||||
|
const valid_chars = "!#$%&'*+-.0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ^_`abcdefghijklmnopqrstuvwxyz|~";
|
||||||
|
const reserved_chars = "%";
|
||||||
|
|
||||||
|
function encodeProtocol(protocol){
|
||||||
|
protocol = protocol.toString();
|
||||||
|
|
||||||
|
let result = '';
|
||||||
|
|
||||||
|
for(let i = 0; i < protocol.length; i++){
|
||||||
|
const char = protocol[i];
|
||||||
|
|
||||||
|
if(valid_chars.includes(char) && !reserved_chars.includes(char)){
|
||||||
|
result += char;
|
||||||
|
}else{
|
||||||
|
const code = char.charCodeAt();
|
||||||
|
result += '%' + code.toString(16).padStart(2, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (window.Location) {
|
||||||
|
window.Location = __oxidation.location.constructor;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!worker && window.navigator && window.navigator.serviceWorker) {
|
||||||
|
window.navigator.serviceWorker.addEventListener('message', event => {
|
||||||
|
if (typeof event.data !== 'object') return false;
|
||||||
|
|
||||||
|
if (event.data.msg === 'updateCookies') {
|
||||||
|
__op.cookie.getCookies(__op.db).then(cookies => {
|
||||||
|
__op.cookies = cookies;
|
||||||
|
__op.cookieStr = __op.cookie.serialize(cookies, __op.meta, true);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!self.__op) { __opHook(self); }
|
||||||
|
|
||||||
|
|
||||||
|
function instrument(obj, prop, wrapper) {
|
||||||
|
return (...args) => obj[prop].apply(this, args);
|
||||||
|
};
|
304
example/static/op.sw.js
Normal file
304
example/static/op.sw.js
Normal file
|
@ -0,0 +1,304 @@
|
||||||
|
importScripts('/op.bundle.js');
|
||||||
|
|
||||||
|
const csp = [
|
||||||
|
'cross-origin-embedder-policy',
|
||||||
|
'cross-origin-opener-policy',
|
||||||
|
'cross-origin-resource-policy',
|
||||||
|
'content-security-policy',
|
||||||
|
'content-security-policy-report-only',
|
||||||
|
'expect-ct',
|
||||||
|
'feature-policy',
|
||||||
|
'origin-isolation',
|
||||||
|
'strict-transport-security',
|
||||||
|
'upgrade-insecure-requests',
|
||||||
|
'x-content-type-options',
|
||||||
|
'x-download-options',
|
||||||
|
'x-frame-options',
|
||||||
|
'x-permitted-cross-domain-policies',
|
||||||
|
'x-powered-by',
|
||||||
|
'x-xss-protection',
|
||||||
|
];
|
||||||
|
|
||||||
|
const headers = {
|
||||||
|
csp: [
|
||||||
|
'cross-origin-embedder-policy',
|
||||||
|
'cross-origin-opener-policy',
|
||||||
|
'cross-origin-resource-policy',
|
||||||
|
'content-security-policy',
|
||||||
|
'content-security-policy-report-only',
|
||||||
|
'expect-ct',
|
||||||
|
'feature-policy',
|
||||||
|
'origin-isolation',
|
||||||
|
'strict-transport-security',
|
||||||
|
'upgrade-insecure-requests',
|
||||||
|
'x-content-type-options',
|
||||||
|
'x-download-options',
|
||||||
|
'x-frame-options',
|
||||||
|
'x-permitted-cross-domain-policies',
|
||||||
|
'x-powered-by',
|
||||||
|
'x-xss-protection',
|
||||||
|
],
|
||||||
|
forward: [
|
||||||
|
'accept-encoding',
|
||||||
|
'accept',
|
||||||
|
'connection',
|
||||||
|
'content-length',
|
||||||
|
'content-type',
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const method = {
|
||||||
|
empty: ['GET', 'HEAD']
|
||||||
|
};
|
||||||
|
|
||||||
|
const statusCode = {
|
||||||
|
empty: [
|
||||||
|
204,
|
||||||
|
304,
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
Brazen('/bare/v1/', {
|
||||||
|
prefix: '/service/',
|
||||||
|
});
|
||||||
|
|
||||||
|
function Brazen(bare, config = {}) {
|
||||||
|
if (!bare) throw new Error('No barer server given. Cannot make connections without one.');
|
||||||
|
addEventListener('fetch', event => {
|
||||||
|
event.respondWith(handler(event.request, config, bare));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
async function handler(request, config = {}, bare) {
|
||||||
|
try {
|
||||||
|
let blob = false;
|
||||||
|
const clientRequest = request;
|
||||||
|
const _url = new URL(clientRequest.url);
|
||||||
|
const rewrite = new Rewriter(config);
|
||||||
|
const db = await rewrite.cookie.db();
|
||||||
|
let fetchUrl = bare;
|
||||||
|
|
||||||
|
if (!request.url.startsWith(location.origin) || !_url.pathname.startsWith(rewrite.prefix) || (config.skip || []).includes(request.url)) {
|
||||||
|
return fetch(request);
|
||||||
|
};
|
||||||
|
|
||||||
|
rewrite.meta.origin = location.origin;
|
||||||
|
rewrite.meta.base = rewrite.meta.url = new URL(rewrite.sourceUrl(request.url));
|
||||||
|
|
||||||
|
rewrite.html.on('element', (element, type) => {
|
||||||
|
if (type !== 'rewrite') return false;
|
||||||
|
if (element.tagName !== 'head') return false;
|
||||||
|
|
||||||
|
element.childNodes.unshift(
|
||||||
|
{
|
||||||
|
tagName: 'script',
|
||||||
|
nodeName: 'script',
|
||||||
|
childNodes: [],
|
||||||
|
attrs: [
|
||||||
|
{ name: 'src', value: '/op.handler.js', skip: true }
|
||||||
|
],
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
element.childNodes.unshift(
|
||||||
|
{
|
||||||
|
tagName: 'script',
|
||||||
|
nodeName: 'script',
|
||||||
|
childNodes: [],
|
||||||
|
attrs: [
|
||||||
|
{ name: 'src', value: '/op.bundle.js', skip: true }
|
||||||
|
],
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
element.childNodes.unshift(
|
||||||
|
{
|
||||||
|
tagName: 'script',
|
||||||
|
nodeName: 'script',
|
||||||
|
childNodes: [
|
||||||
|
{
|
||||||
|
nodeName: '#text',
|
||||||
|
value: `window.__opCookies = atob("${btoa(rewrite.cookie.serialize(element.options.cookies, rewrite.meta, true))}");\nwindow.__opReferrer = atob("${btoa(request.referrer)}");`
|
||||||
|
},
|
||||||
|
],
|
||||||
|
attrs: [],
|
||||||
|
skip: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
rewrite.addRewrites();
|
||||||
|
|
||||||
|
if (rewrite.meta.url.protocol === 'blob:') {
|
||||||
|
blob = true;
|
||||||
|
rewrite.meta.base = rewrite.meta.url = new URL(rewrite.meta.url.pathname);
|
||||||
|
fetchUrl = 'blob:' + location.origin + rewrite.meta.url.pathname;
|
||||||
|
};
|
||||||
|
|
||||||
|
const { url } = rewrite.meta;
|
||||||
|
const sendHeaders = Object.fromEntries([...request.headers.entries()]);
|
||||||
|
|
||||||
|
sendHeaders['user-agent'] = navigator.userAgent;
|
||||||
|
|
||||||
|
if (request.referrer && request.referrer.startsWith(location.origin)) {
|
||||||
|
const referer = new URL(rewrite.sourceUrl(request.referrer));
|
||||||
|
|
||||||
|
if (rewrite.meta.url.origin !== referer.origin && request.mode === 'cors') {
|
||||||
|
sendHeaders.origin = referer.origin;
|
||||||
|
};
|
||||||
|
|
||||||
|
sendHeaders.referer = referer.href;
|
||||||
|
};
|
||||||
|
|
||||||
|
//const cookies = await rewrite.cookie.getCookies(await db.getAll('cookies'), rewrite.meta) || '';
|
||||||
|
const cookies = await rewrite.cookie.getCookies(db) || [];
|
||||||
|
const cookieStr = rewrite.cookie.serialize(cookies, rewrite.meta, false);
|
||||||
|
if (cookieStr) sendHeaders.cookie = cookieStr;
|
||||||
|
|
||||||
|
const barer = {
|
||||||
|
'x-bare-protocol': url.protocol,
|
||||||
|
'x-bare-host': url.hostname,
|
||||||
|
'x-bare-path': url.pathname + url.search,
|
||||||
|
'x-bare-port': url.port,
|
||||||
|
'x-bare-headers': JSON.stringify(sendHeaders),
|
||||||
|
'x-bare-forward-headers': JSON.stringify(headers.forward),
|
||||||
|
};
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
method: request.method,
|
||||||
|
headers: !blob ? barer : request.headers,
|
||||||
|
redirect: request.redirect,
|
||||||
|
credentials: 'omit',
|
||||||
|
mode: request.mode === 'cors' ? request.mode : 'same-origin',
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!method.empty.includes(request.method.toUpperCase())) options.body = await request.blob();
|
||||||
|
|
||||||
|
const remoteRequest = !blob ? new Request(fetchUrl, options) : new Request(fetchUrl);
|
||||||
|
const remoteResponse = await fetch(remoteRequest);
|
||||||
|
|
||||||
|
if (remoteResponse.status === 500) {
|
||||||
|
return Promise.reject('Err');
|
||||||
|
};
|
||||||
|
|
||||||
|
const sendData = !blob ? getBarerResponse(remoteResponse) : {
|
||||||
|
status: remoteResponse.status,
|
||||||
|
statusText: remoteResponse.statusText,
|
||||||
|
headers: Object.fromEntries([...remoteResponse.headers.entries()]),
|
||||||
|
body: remoteResponse.body,
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const name of headers.csp) {
|
||||||
|
if (sendData.headers[name]) delete sendData.headers[name];
|
||||||
|
};
|
||||||
|
|
||||||
|
if (sendData.headers.location) {
|
||||||
|
sendData.headers.location = rewrite.rewriteUrl(sendData.headers.location);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (sendData.headers['set-cookie']) {
|
||||||
|
Promise.resolve(rewrite.cookie.setCookies(sendData.headers['set-cookie'], db, rewrite.meta)).then(() => {
|
||||||
|
self.clients.matchAll().then(function (clients){
|
||||||
|
clients.forEach(function(client){
|
||||||
|
client.postMessage({
|
||||||
|
msg: 'updateCookies',
|
||||||
|
url: rewrite.meta.url.href,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
delete sendData.headers['set-cookie'];
|
||||||
|
};
|
||||||
|
|
||||||
|
if (statusCode.empty.includes(sendData.status)) {
|
||||||
|
return new Response(null, {
|
||||||
|
headers: sendData.headers,
|
||||||
|
status: sendData.status,
|
||||||
|
statusText: sendData.statusText,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
switch(request.destination) {
|
||||||
|
case 'script':
|
||||||
|
sendData.body = rewrite.js.rewrite(
|
||||||
|
await remoteResponse.text()
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case 'worker':
|
||||||
|
sendData.body = `if (!self.__op) importScripts('/op.bundle.js', '/op.handler.js');\n`;
|
||||||
|
sendData.body += rewrite.js.rewrite(
|
||||||
|
await remoteResponse.text()
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case 'style':
|
||||||
|
sendData.body = rewrite.rewriteCSS(
|
||||||
|
await remoteResponse.text()
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
/*
|
||||||
|
case 'document':
|
||||||
|
case 'iframe':
|
||||||
|
sendData.body = rewrite.rewriteHtml(
|
||||||
|
await remoteResponse.text(),
|
||||||
|
{
|
||||||
|
document: true,
|
||||||
|
cookies,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
*/
|
||||||
|
/*
|
||||||
|
default:
|
||||||
|
if (request.mode !== 'cors' && isHtml(rewrite.meta.url, (sendData.headers['content-type'] || ''))) {
|
||||||
|
sendData.body = rewrite.rewriteHtml(
|
||||||
|
await remoteResponse.text(),
|
||||||
|
{
|
||||||
|
document: true ,
|
||||||
|
cookies,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
*/
|
||||||
|
case 'iframe':
|
||||||
|
case 'document':
|
||||||
|
if (isHtml(rewrite.meta.url, (sendData.headers['content-type'] || ''))) {
|
||||||
|
sendData.body = rewrite.rewriteHtml(
|
||||||
|
await remoteResponse.text(),
|
||||||
|
{
|
||||||
|
document: true ,
|
||||||
|
cookies,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// EventSource support
|
||||||
|
if (sendHeaders.accept === 'text/event-stream') {
|
||||||
|
sendData.headers['content-type'] = 'text/event-stream';
|
||||||
|
};
|
||||||
|
|
||||||
|
return new Response(sendData.body, {
|
||||||
|
headers: sendData.headers,
|
||||||
|
status: sendData.status,
|
||||||
|
statusText: sendData.statusText,
|
||||||
|
});
|
||||||
|
} catch(e) {
|
||||||
|
return new Response(e.toString(), {
|
||||||
|
status: 500,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
function getBarerResponse(response) {
|
||||||
|
return {
|
||||||
|
headers: JSON.parse(response.headers.get('x-bare-headers')),
|
||||||
|
status: +response.headers.get('x-bare-status'),
|
||||||
|
statusText: response.headers.get('x-bare-status-text'),
|
||||||
|
body: response.body,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
function isHtml(url, contentType = '') {
|
||||||
|
return (Rewriter.mime.contentType((contentType || url.pathname)) || 'text/html').split(';')[0] === 'text/html';
|
||||||
|
};
|
206
example/static/op.test.sw.js
Normal file
206
example/static/op.test.sw.js
Normal file
|
@ -0,0 +1,206 @@
|
||||||
|
importScripts('/op.bundle.js');
|
||||||
|
|
||||||
|
const csp = [
|
||||||
|
'cross-origin-embedder-policy',
|
||||||
|
'cross-origin-opener-policy',
|
||||||
|
'cross-origin-resource-policy',
|
||||||
|
'content-security-policy',
|
||||||
|
'content-security-policy-report-only',
|
||||||
|
'expect-ct',
|
||||||
|
'feature-policy',
|
||||||
|
'origin-isolation',
|
||||||
|
'strict-transport-security',
|
||||||
|
'upgrade-insecure-requests',
|
||||||
|
'x-content-type-options',
|
||||||
|
'x-download-options',
|
||||||
|
'x-frame-options',
|
||||||
|
'x-permitted-cross-domain-policies',
|
||||||
|
'x-powered-by',
|
||||||
|
'x-xss-protection',
|
||||||
|
];
|
||||||
|
|
||||||
|
const emptyBody = ['204'];
|
||||||
|
|
||||||
|
async function handle(request) {
|
||||||
|
try {
|
||||||
|
const parsed = new URL(request.url);
|
||||||
|
|
||||||
|
if (parsed.pathname === '/op.bundle.js' || parsed.pathname === '/favicon.ico' || parsed.pathname === '/op.handler.js') return fetch(request);
|
||||||
|
|
||||||
|
const rewrite = new Rewriter({
|
||||||
|
meta: {
|
||||||
|
origin: location.origin,
|
||||||
|
},
|
||||||
|
prefix: '/service/',
|
||||||
|
});
|
||||||
|
|
||||||
|
const db = await rewrite.cookie.db();
|
||||||
|
|
||||||
|
rewrite.html.on('element', (element, type) => {
|
||||||
|
if (type !== 'rewrite') return false;
|
||||||
|
if (element.tagName !== 'head') return false;
|
||||||
|
|
||||||
|
element.childNodes.unshift(
|
||||||
|
{
|
||||||
|
tagName: 'script',
|
||||||
|
nodeName: 'script',
|
||||||
|
childNodes: [],
|
||||||
|
attrs: [
|
||||||
|
{ name: 'src', value: '/op.handler.js', skip: true }
|
||||||
|
],
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
element.childNodes.unshift(
|
||||||
|
{
|
||||||
|
tagName: 'script',
|
||||||
|
nodeName: 'script',
|
||||||
|
childNodes: [],
|
||||||
|
attrs: [
|
||||||
|
{ name: 'src', value: '/op.bundle.js', skip: true }
|
||||||
|
],
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
rewrite.addRewrites();
|
||||||
|
|
||||||
|
if (!request.url.startsWith(location.origin) || request.url.startsWith(location.origin + rewrite.prefix)) {
|
||||||
|
let blob = false;
|
||||||
|
let requestUrl = '/fetch/asdsaadsad';
|
||||||
|
|
||||||
|
rewrite.meta.base = rewrite.meta.url = !request.url.startsWith(location.origin) ? new URL(request.url) : new URL(rewrite.sourceUrl(request.url));
|
||||||
|
|
||||||
|
if (rewrite.meta.url.protocol === 'blob:') {
|
||||||
|
blob = true;
|
||||||
|
rewrite.meta.base = rewrite.meta.url = new URL(rewrite.meta.url.pathname);
|
||||||
|
requestUrl = 'blob:' + location.origin + rewrite.meta.url.pathname;
|
||||||
|
};
|
||||||
|
|
||||||
|
const requestHeaders = request.headers instanceof Headers ? Object.fromEntries([...request.headers.entries()]) : request.headers;
|
||||||
|
let cookieStr = '';
|
||||||
|
|
||||||
|
if (request.referrer && request.referrer.startsWith(location.origin)) {
|
||||||
|
const referer = new URL(rewrite.sourceUrl(request.referrer));
|
||||||
|
|
||||||
|
if (rewrite.meta.url.origin !== referer.origin && request.mode === 'cors') {
|
||||||
|
request.headers.origin = referer.origin;
|
||||||
|
};
|
||||||
|
|
||||||
|
request.headers.referer = referer.href;
|
||||||
|
};
|
||||||
|
|
||||||
|
cookieStr = await rewrite.cookie.getCookies(await db.getAll('cookies'), rewrite.meta);
|
||||||
|
|
||||||
|
requestHeaders.cookie = cookieStr;
|
||||||
|
|
||||||
|
const headers = {
|
||||||
|
'x-tomp-protocol': rewrite.meta.url.protocol,
|
||||||
|
'x-tomp-host': rewrite.meta.url.hostname,
|
||||||
|
'x-tomp-path': rewrite.meta.url.pathname + rewrite.meta.url.search,
|
||||||
|
'x-tomp-port': rewrite.meta.url.port,
|
||||||
|
'x-tomp-headers': JSON.stringify(requestHeaders),
|
||||||
|
'x-tomp-forward-headers': JSON.stringify(['user-agent', 'accept', 'accept-encoding', 'accept', 'connection']),
|
||||||
|
};
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
method: request.method,
|
||||||
|
headers: !blob ? headers : request.headers,
|
||||||
|
redirect: request.redirect,
|
||||||
|
mode: request.mode === 'cors' ? request.mode : 'same-origin',
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!['GET', 'HEAD'].includes(request.method.toUpperCase())) options.body = await request.blob();
|
||||||
|
|
||||||
|
const newRequest = new Request(requestUrl, options);
|
||||||
|
|
||||||
|
const processed = fetch(newRequest).then(async response => {
|
||||||
|
let body = response.body;
|
||||||
|
|
||||||
|
const resHeaders = new Headers(response.headers);
|
||||||
|
|
||||||
|
const rawHeaders = response.headers.has('x-tomp-headers') ? JSON.parse(response.headers.get('x-tomp-headers')) : Object.fromEntries([...response.headers.entries()]);
|
||||||
|
const status = response.headers.get('x-tomp-status') || response.status;
|
||||||
|
const statusText = response.headers.get('x-tomp-status-text') || response.statusText;
|
||||||
|
|
||||||
|
for (let header in rawHeaders) {
|
||||||
|
if (csp.indexOf(header) > -1) delete rawHeaders[header];
|
||||||
|
};
|
||||||
|
|
||||||
|
if (rawHeaders.location) {
|
||||||
|
rawHeaders.location = rewrite.rewriteUrl(rawHeaders.location);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (rawHeaders['set-cookie']) {
|
||||||
|
Promise.resolve(rewrite.cookie.setCookies(rawHeaders['set-cookie'], db, rewrite.meta)).then(() => {
|
||||||
|
self.clients.matchAll().then(function (clients){
|
||||||
|
clients.forEach(function(client){
|
||||||
|
client.postMessage({
|
||||||
|
msg: 'updateCookies',
|
||||||
|
url: rewrite.meta.url.href,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
delete rawHeaders['set-cookie'];
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isHtml(rewrite.meta.url, (resHeaders.get('content-type') || ''))) {
|
||||||
|
body = rewrite.rewriteHtml(
|
||||||
|
await response.text(),
|
||||||
|
{
|
||||||
|
document: true
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (request.destination === 'script') {
|
||||||
|
body = rewrite.js.rewrite(
|
||||||
|
await response.text()
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (request.destination === 'worker') {
|
||||||
|
body = `if (!self.__op) importScripts('/op.bundle.js', '/op.handler.js');\n`;
|
||||||
|
body += rewrite.js.rewrite(
|
||||||
|
await response.text()
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (request.destination === 'style') {
|
||||||
|
body = rewrite.rewriteCSS(
|
||||||
|
await response.text()
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (emptyBody.includes(status)) {
|
||||||
|
return new Response(null, {
|
||||||
|
headers: rawHeaders,
|
||||||
|
status,
|
||||||
|
statusText,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return new Response(body, {
|
||||||
|
headers: rawHeaders,
|
||||||
|
status,
|
||||||
|
statusText,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return processed;
|
||||||
|
|
||||||
|
};
|
||||||
|
} catch(e) {
|
||||||
|
return new Response(e.toString());
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
self.addEventListener('fetch', event => {
|
||||||
|
if (event.request.url) event.respondWith(handle(event.request));
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
function isHtml(url, contentType = '') {
|
||||||
|
return (Rewriter.mime.contentType((contentType || url.pathname)) || 'text/html').split(';')[0] === 'text/html';
|
||||||
|
};
|
0
example/static/test.html
Normal file
0
example/static/test.html
Normal file
27
index.js
Normal file
27
index.js
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
import Ultraviolet from './rewrite/index.js'
|
||||||
|
|
||||||
|
import { generate } from 'esotope-hammerhead';
|
||||||
|
import { parseScript } from 'meriyah';
|
||||||
|
|
||||||
|
const uv = new Ultraviolet({
|
||||||
|
meta: {
|
||||||
|
url: new URL('https://www.google.com'),
|
||||||
|
base: new URL('https://www.google.com')
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/*
|
||||||
|
console.log(
|
||||||
|
uv.css.recast('body { background: url ( \n " coohcie " ) }')
|
||||||
|
)
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
uv.rewriteJS('window.eval(saasdsd)')
|
||||||
|
)
|
||||||
|
|
||||||
|
/*
|
||||||
|
const used = process.memoryUsage().heapUsed / 1024 / 1024;
|
||||||
|
console.log(`The script uses approximately ${Math.round(used * 100) / 100} MB`);
|
||||||
|
*/
|
38848
lib/uv.bundle.js
Normal file
38848
lib/uv.bundle.js
Normal file
File diff suppressed because one or more lines are too long
743
lib/uv.handler.js
Normal file
743
lib/uv.handler.js
Normal file
|
@ -0,0 +1,743 @@
|
||||||
|
async function __uvHook(window, config = {}) {
|
||||||
|
if ('__uv' in window && window.__uv instanceof Ultraviolet) return false;
|
||||||
|
|
||||||
|
|
||||||
|
const worker = !window.window;
|
||||||
|
const master = '__uv';
|
||||||
|
const methodPrefix = '__uv$';
|
||||||
|
const __uv = new Ultraviolet({
|
||||||
|
...config,
|
||||||
|
window,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { client } = __uv;
|
||||||
|
const {
|
||||||
|
HTMLMediaElement,
|
||||||
|
HTMLScriptElement,
|
||||||
|
HTMLAudioElement,
|
||||||
|
HTMLVideoElement,
|
||||||
|
HTMLInputElement,
|
||||||
|
HTMLEmbedElement,
|
||||||
|
HTMLTrackElement,
|
||||||
|
HTMLAnchorElement,
|
||||||
|
HTMLIFrameElement,
|
||||||
|
HTMLAreaElement,
|
||||||
|
HTMLLinkElement,
|
||||||
|
HTMLBaseElement,
|
||||||
|
HTMLFormElement,
|
||||||
|
HTMLImageElement,
|
||||||
|
HTMLSourceElement,
|
||||||
|
} = window;
|
||||||
|
|
||||||
|
client.nativeMethods.defineProperty(window, '__uv', {
|
||||||
|
value: __uv,
|
||||||
|
enumerable: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
__uv.meta.origin = location.origin;
|
||||||
|
__uv.location = client.location.emulate(
|
||||||
|
(href) => {
|
||||||
|
if (href.startsWith('blob:')) href = href.slice('blob:'.length);
|
||||||
|
return new URL(__uv.sourceUrl(href));
|
||||||
|
},
|
||||||
|
(href) => {
|
||||||
|
return __uv.rewriteUrl(href);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
__uv.cookieStr = window.__uv$cookies || '';
|
||||||
|
__uv.meta.url = __uv.location;
|
||||||
|
__uv.domain = __uv.meta.url.host;
|
||||||
|
__uv.blobUrls = new window.Map();
|
||||||
|
__uv.referrer = '';
|
||||||
|
__uv.cookies = [];
|
||||||
|
|
||||||
|
let rawBase = window.document ? client.node.baseURI.get.call(window.document) : window.location.href;
|
||||||
|
let base = __uv.sourceUrl(rawBase);
|
||||||
|
|
||||||
|
client.nativeMethods.defineProperty(__uv.meta, 'base', {
|
||||||
|
get() {
|
||||||
|
if (!window.document) return __uv.meta.url.href;
|
||||||
|
|
||||||
|
if (client.node.baseURI.get.call(window.document) !== rawBase) {
|
||||||
|
rawBase = client.node.baseURI.get.call(window.document);
|
||||||
|
base = __uv.sourceUrl(rawBase);
|
||||||
|
};
|
||||||
|
|
||||||
|
return base;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
__uv.methods = {
|
||||||
|
setSource: methodPrefix + 'setSource',
|
||||||
|
source: methodPrefix + 'source',
|
||||||
|
location: methodPrefix + 'location',
|
||||||
|
function: methodPrefix + 'function',
|
||||||
|
string: methodPrefix + 'string',
|
||||||
|
eval: methodPrefix + 'eval',
|
||||||
|
};
|
||||||
|
|
||||||
|
__uv.filterKeys = [
|
||||||
|
master,
|
||||||
|
__uv.methods.setSource,
|
||||||
|
__uv.methods.source,
|
||||||
|
__uv.methods.location,
|
||||||
|
__uv.methods.function,
|
||||||
|
__uv.methods.string,
|
||||||
|
__uv.methods.eval,
|
||||||
|
'Ultraviolet',
|
||||||
|
'__uvHook',
|
||||||
|
];
|
||||||
|
|
||||||
|
|
||||||
|
client.on('wrap', (target, wrapped) => {
|
||||||
|
client.nativeMethods.defineProperty(wrapped, 'name', client.nativeMethods.getOwnPropertyDescriptor(target, 'name'));
|
||||||
|
client.nativeMethods.defineProperty(wrapped, 'length', client.nativeMethods.getOwnPropertyDescriptor(target, 'length'));
|
||||||
|
|
||||||
|
client.nativeMethods.defineProperty(wrapped, __uv.methods.string, {
|
||||||
|
enumerable: false,
|
||||||
|
value: client.nativeMethods.fnToString.call(target),
|
||||||
|
});
|
||||||
|
|
||||||
|
client.nativeMethods.defineProperty(wrapped, __uv.methods.function, {
|
||||||
|
enumerable: false,
|
||||||
|
value: target,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
client.fetch.on('request', event => {
|
||||||
|
event.data.input = __uv.rewriteUrl(event.data.input);
|
||||||
|
});
|
||||||
|
|
||||||
|
client.fetch.on('requestUrl', event => {
|
||||||
|
event.data.value = __uv.sourceUrl(event.data.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
client.fetch.on('responseUrl', event => {
|
||||||
|
event.data.value = __uv.sourceUrl(event.data.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
// XMLHttpRequest
|
||||||
|
client.xhr.on('open', event => {
|
||||||
|
event.data.input = __uv.rewriteUrl(event.data.input);
|
||||||
|
});
|
||||||
|
|
||||||
|
client.xhr.on('responseUrl', event => {
|
||||||
|
event.data.value = __uv.sourceUrl(event.data.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// Workers
|
||||||
|
client.workers.on('worker', event => {
|
||||||
|
event.data.url = __uv.rewriteUrl(event.data.url);
|
||||||
|
});
|
||||||
|
|
||||||
|
client.workers.on('addModule', event => {
|
||||||
|
event.data.url = __uv.rewriteUrl(event.data.url);
|
||||||
|
});
|
||||||
|
|
||||||
|
client.workers.on('importScripts', event => {
|
||||||
|
for (const i in event.data.scripts) {
|
||||||
|
event.data.scripts[i] = __uv.rewriteUrl(event.data.scripts[i]);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
client.workers.on('postMessage', event => {
|
||||||
|
let to = event.data.origin;
|
||||||
|
|
||||||
|
event.data.origin = '*';
|
||||||
|
event.data.message = {
|
||||||
|
__data: event.data.message,
|
||||||
|
__origin: __uv.meta.url.origin,
|
||||||
|
__to: to,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Navigator
|
||||||
|
client.navigator.on('sendBeacon', event => {
|
||||||
|
event.data.url = __uv.rewriteUrl(event.data.url);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Cookies
|
||||||
|
client.document.on('getCookie', event => {
|
||||||
|
event.data.value = __uv.cookieStr;
|
||||||
|
});
|
||||||
|
|
||||||
|
client.document.on('setCookie', event => {
|
||||||
|
Promise.resolve(__uv.cookie.setCookies(event.data.value, __uv.db, __uv.meta)).then(() => {
|
||||||
|
__uv.cookie.db().then(db => {
|
||||||
|
__uv.cookie.getCookies(db).then(cookies => {
|
||||||
|
__uv.cookieStr = __uv.cookie.serialize(cookies, __uv.meta, true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
const cookie = __uv.cookie.setCookie(event.data.value)[0];
|
||||||
|
|
||||||
|
if (!cookie.path) cookie.path = '/';
|
||||||
|
if (!cookie.domain) cookie.domain = __uv.meta.url.hostname;
|
||||||
|
|
||||||
|
if (__uv.cookie.validateCookie(cookie, __uv.meta, true)) {
|
||||||
|
if (__uv.cookieStr.length) __uv.cookieStr += '; ';
|
||||||
|
__uv.cookieStr += `${cookie.name}=${cookie.value}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
event.respondWith(event.data.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
// HTML
|
||||||
|
client.element.on('setInnerHTML', event => {
|
||||||
|
switch(event.that.tagName) {
|
||||||
|
case 'SCRIPT':
|
||||||
|
event.data.value = __uv.js.rewrite(event.data.value);
|
||||||
|
break;
|
||||||
|
case 'STYLE':
|
||||||
|
event.data.value = __uv.rewriteCSS(event.data.value);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
event.data.value = __uv.rewriteHtml(event.data.value);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
client.element.on('getInnerHTML', event => {
|
||||||
|
switch(event.that.tagName) {
|
||||||
|
case 'SCRIPT':
|
||||||
|
event.data.value = __uv.js.source(event.data.value);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
event.data.value = __uv.sourceHtml(event.data.value);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
client.element.on('setOuterHTML', event => {
|
||||||
|
event.data.value = __uv.rewriteHtml(event.data.value, { document: event.that.tagName === 'HTML' });
|
||||||
|
});
|
||||||
|
|
||||||
|
client.element.on('getOuterHTML', event => {
|
||||||
|
switch(event.that.tagName) {
|
||||||
|
case 'HEAD':
|
||||||
|
event.data.value = __uv.sourceHtml(
|
||||||
|
event.data.value.replace(/<head(.*)>(.*)<\/head>/s, '<op-head$1>$2</op-head>')
|
||||||
|
).replace(/<op-head(.*)>(.*)<\/op-head>/s, '<head$1>$2</head>');
|
||||||
|
break;
|
||||||
|
case 'BODY':
|
||||||
|
event.data.value = __uv.sourceHtml(
|
||||||
|
event.data.value.replace(/<body(.*)>(.*)<\/body>/s, '<op-body$1>$2</op-body>')
|
||||||
|
).replace(/<op-body(.*)>(.*)<\/op-body>/s, '<body$1>$2</body>');
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
event.data.value = __uv.sourceHtml(event.data.value, { document: event.that.tagName === 'HTML' });
|
||||||
|
break;
|
||||||
|
};
|
||||||
|
|
||||||
|
//event.data.value = __uv.sourceHtml(event.data.value, { document: event.that.tagName === 'HTML' });
|
||||||
|
});
|
||||||
|
|
||||||
|
client.document.on('write', event => {
|
||||||
|
if (!event.data.html.length) return false;
|
||||||
|
event.data.html = [ __uv.rewriteHtml(event.data.html.join('')) ];
|
||||||
|
});
|
||||||
|
|
||||||
|
client.document.on('writeln', event => {
|
||||||
|
if (!event.data.html.length) return false;
|
||||||
|
event.data.html = [ __uv.rewriteHtml(event.data.html.join('')) ];
|
||||||
|
});
|
||||||
|
|
||||||
|
client.element.on('insertAdjacentHTML', event => {
|
||||||
|
event.data.html = __uv.rewriteHtml(event.data.html);
|
||||||
|
});
|
||||||
|
|
||||||
|
// EventSource
|
||||||
|
|
||||||
|
client.eventSource.on('construct', event => {
|
||||||
|
event.data.url = __uv.rewriteUrl(event.data.url);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
client.eventSource.on('url', event => {
|
||||||
|
event.data.url = __uv.rewriteUrl(event.data.url);
|
||||||
|
});
|
||||||
|
|
||||||
|
// History
|
||||||
|
client.history.on('replaceState', event => {
|
||||||
|
if (event.data.url) event.data.url = __uv.rewriteUrl(event.data.url, '__uv' in event.that ? event.that.__uv.meta : __uv.meta);
|
||||||
|
});
|
||||||
|
client.history.on('pushState', event => {
|
||||||
|
if (event.data.url) event.data.url = __uv.rewriteUrl(event.data.url, '__uv' in event.that ? event.that.__uv.meta : __uv.meta);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Element get set attribute methods
|
||||||
|
client.element.on('getAttribute', event => {
|
||||||
|
if (client.element.hasAttribute.call(event.that, __uv.attributePrefix + '-attr-' + event.data.name)) {
|
||||||
|
event.respondWith(
|
||||||
|
event.target.call(event.that, __uv.attributePrefix + '-attr-' + event.data.name)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Message
|
||||||
|
client.message.on('postMessage', event => {
|
||||||
|
let to = event.data.origin;
|
||||||
|
let call = __uv.call;
|
||||||
|
|
||||||
|
|
||||||
|
if (event.that) {
|
||||||
|
call = event.that.__uv$source.call;
|
||||||
|
};
|
||||||
|
|
||||||
|
event.data.origin = '*';
|
||||||
|
event.data.message = {
|
||||||
|
__data: event.data.message,
|
||||||
|
__origin: (event.that || event.target).__uv$source.location.origin,
|
||||||
|
__to: to,
|
||||||
|
};
|
||||||
|
|
||||||
|
event.respondWith(
|
||||||
|
worker ?
|
||||||
|
call(event.target, [ event.data.message, event.data.transfer ], event.that) :
|
||||||
|
call(event.target, [ event.data.message, event.data.origin, event.data.transfer ], event.that)
|
||||||
|
);
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
client.message.on('data', event => {
|
||||||
|
const { value: data } = event.data;
|
||||||
|
if (typeof data === 'object' && '__data' in data && '__origin' in data) {
|
||||||
|
event.respondWith(data.__data);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
client.message.on('origin', event => {
|
||||||
|
const data = client.message.messageData.get.call(event.that);
|
||||||
|
if (typeof data === 'object' && data.__data && data.__origin) {
|
||||||
|
event.respondWith(data.__origin);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
client.overrideDescriptor(window, 'origin', {
|
||||||
|
get: (target, that) => {
|
||||||
|
return __uv.location.origin;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
client.node.on('baseURI', event => {
|
||||||
|
if (event.data.value.startsWith(window.location.origin)) event.data.value = __uv.sourceUrl(event.data.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
client.element.on('setAttribute', event => {
|
||||||
|
if (event.that instanceof HTMLMediaElement && event.data.name === 'src' && event.data.value.startsWith('blob:')) {
|
||||||
|
event.target.call(event.that, __uv.attributePrefix + '-attr-' + event.data.name, event.data.value);
|
||||||
|
event.data.value = __uv.blobUrls.get(event.data.value);
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (__uv.attrs.isUrl(event.data.name)) {
|
||||||
|
event.target.call(event.that, __uv.attributePrefix + '-attr-' + event.data.name, event.data.value);
|
||||||
|
event.data.value = __uv.rewriteUrl(event.data.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (__uv.attrs.isStyle(event.data.name)) {
|
||||||
|
event.target.call(event.that, __uv.attributePrefix + '-attr-' + event.data.name, event.data.value);
|
||||||
|
event.data.value = __uv.rewriteCSS(event.data.value, { context: 'declarationList' });
|
||||||
|
};
|
||||||
|
|
||||||
|
if (__uv.attrs.isHtml(event.data.name)) {
|
||||||
|
event.target.call(event.that, __uv.attributePrefix + '-attr-' + event.data.name, event.data.value);
|
||||||
|
event.data.value = __uv.rewriteHtml(event.data.value, { ...__uv.meta, document: true });
|
||||||
|
};
|
||||||
|
|
||||||
|
if (__uv.attrs.isForbidden(event.data.name)) {
|
||||||
|
event.data.name = __uv.attributePrefix + '-attr-' + event.data.name;
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
client.element.on('audio', event => {
|
||||||
|
event.data.url = __uv.rewriteUrl(event.data.url);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Element Property Attributes
|
||||||
|
client.element.hookProperty([ HTMLAnchorElement, HTMLAreaElement, HTMLLinkElement, HTMLBaseElement ], 'href', {
|
||||||
|
get: (target, that) => {
|
||||||
|
return __uv.sourceUrl(
|
||||||
|
target.call(that)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
set: (target, that, [ val ]) => {
|
||||||
|
client.element.setAttribute.call(that, __uv.attributePrefix + '-attr-href', val)
|
||||||
|
target.call(that, __uv.rewriteUrl(val));
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
client.element.hookProperty([ HTMLScriptElement, HTMLMediaElement, HTMLImageElement, HTMLInputElement, HTMLEmbedElement, HTMLIFrameElement, HTMLTrackElement, HTMLSourceElement ], 'src', {
|
||||||
|
get: (target, that) => {
|
||||||
|
return __uv.sourceUrl(
|
||||||
|
target.call(that)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
set: (target, that, [ val ]) => {
|
||||||
|
if (new String(val).toString().trim().startsWith('blob:') && that instanceof HTMLMediaElement) {
|
||||||
|
client.element.setAttribute.call(that, __uv.attributePrefix + '-attr-src', val)
|
||||||
|
return target.call(that, __uv.blobUrls.get(val) || val);
|
||||||
|
};
|
||||||
|
|
||||||
|
client.element.setAttribute.call(that, __uv.attributePrefix + '-attr-src', val)
|
||||||
|
target.call(that, __uv.rewriteUrl(val));
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
client.element.hookProperty([ HTMLFormElement ], 'action', {
|
||||||
|
get: (target, that) => {
|
||||||
|
return __uv.sourceUrl(
|
||||||
|
target.call(that)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
set: (target, that, [ val ]) => {
|
||||||
|
client.element.setAttribute.call(that, __uv.attributePrefix + '-attr-action', val)
|
||||||
|
target.call(that, __uv.rewriteUrl(val));
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
client.element.hookProperty(HTMLScriptElement, 'integrity', {
|
||||||
|
get: (target, that) => {
|
||||||
|
return client.element.getAttribute.call(that, __uv.attributePrefix + '-attr-integrity');
|
||||||
|
},
|
||||||
|
set: (target, that, [ val ]) => {
|
||||||
|
client.element.setAttribute.call(that, __uv.attributePrefix + '-attr-integrity', val);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
client.element.hookProperty(HTMLIFrameElement, 'sandbox', {
|
||||||
|
get: (target, that) => {
|
||||||
|
return client.element.getAttribute.call(that, __uv.attributePrefix + '-attr-sandbox') || target.call(that);
|
||||||
|
},
|
||||||
|
set: (target, that, [ val ]) => {
|
||||||
|
client.element.setAttribute.call(that, __uv.attributePrefix + '-attr-sandbox', val);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
client.element.hookProperty(HTMLIFrameElement, 'contentWindow', {
|
||||||
|
get: (target, that) => {
|
||||||
|
const win = target.call(that);
|
||||||
|
try {
|
||||||
|
if (!win.__uv) __uvHook(win);
|
||||||
|
return win;
|
||||||
|
} catch(e) {
|
||||||
|
return win;
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
client.element.hookProperty(HTMLIFrameElement, 'contentDocument', {
|
||||||
|
get: (target, that) => {
|
||||||
|
const doc = target.call(that);
|
||||||
|
try {
|
||||||
|
const win = doc.defaultView
|
||||||
|
if (!win.__uv) __uvHook(win);
|
||||||
|
return doc;
|
||||||
|
} catch(e) {
|
||||||
|
return win;
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
client.node.on('getTextContent', event => {
|
||||||
|
if (event.that.tagName === 'SCRIPT') {
|
||||||
|
event.data.value = __uv.js.source(event.data.value);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
client.node.on('setTextContent', event => {
|
||||||
|
if (event.that.tagName === 'SCRIPT') {
|
||||||
|
event.data.value = __uv.js.rewrite(event.data.value);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Document
|
||||||
|
client.document.on('getDomain', event => {
|
||||||
|
event.data.value = __uv.domain;
|
||||||
|
});
|
||||||
|
client.document.on('setDomain', event => {
|
||||||
|
if (!event.data.value.toString().endsWith(__uv.meta.url.hostname.split('.').slice(-2).join('.'))) return event.respondWith('');
|
||||||
|
event.respondWith(__uv.domain = event.data.value);
|
||||||
|
})
|
||||||
|
|
||||||
|
client.document.on('url', event => {
|
||||||
|
event.data.value = __uv.location.href;
|
||||||
|
});
|
||||||
|
|
||||||
|
client.document.on('documentURI', event => {
|
||||||
|
event.data.value = __uv.location.href;
|
||||||
|
});
|
||||||
|
|
||||||
|
client.document.on('referrer', event => {
|
||||||
|
event.data.value = __uv.referrer || __uv.sourceUrl(event.data.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
client.document.on('parseFromString', event => {
|
||||||
|
if (event.data.type !== 'text/html') return false;
|
||||||
|
event.data.string = __uv.rewriteHtml(event.data.string, { ...__uv.meta, document: true, });
|
||||||
|
});
|
||||||
|
|
||||||
|
// Attribute (node.attributes)
|
||||||
|
client.attribute.on('getValue', event => {
|
||||||
|
if (client.element.hasAttribute.call(event.that.ownerElement, __uv.attributePrefix + '-attr-' + event.data.name)) {
|
||||||
|
event.data.value = client.element.getAttribute.call(event.that.ownerElement, __uv.attributePrefix + '-attr-' + event.data.name);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
client.attribute.on('setValue', event => {
|
||||||
|
if (__uv.attrs.isUrl(event.data.name)) {
|
||||||
|
client.element.setAttribute.call(event.that.ownerElement, __uv.attributePrefix + '-attr-' + event.data.name, event.data.value);
|
||||||
|
event.data.value = __uv.rewriteUrl(event.data.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (__uv.attrs.isStyle(event.data.name)) {
|
||||||
|
client.element.setAttribute.call(event.that.ownerElement, __uv.attributePrefix + '-attr-' + event.data.name, event.data.value);
|
||||||
|
event.data.value = __uv.rewriteCSS(event.data.value, { context: 'declarationList' });
|
||||||
|
};
|
||||||
|
|
||||||
|
if (__uv.attrs.isHtml(event.data.name)) {
|
||||||
|
client.element.setAttribute.call(event.that.ownerElement, __uv.attributePrefix + '-attr-' + event.data.name, event.data.value);
|
||||||
|
event.data.value = __uv.rewriteHtml(event.data.value, { ...__uv.meta, document: true });
|
||||||
|
};
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
client.override(window.JSON, 'stringify', (target, that, args) => {
|
||||||
|
try {
|
||||||
|
return target.apply(that, args);
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e, 'Error', args);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// URL
|
||||||
|
client.url.on('createObjectURL', event => {
|
||||||
|
let url = event.target.call(event.that, event.data.object);
|
||||||
|
if (url.startsWith('blob:' + location.origin)) {
|
||||||
|
let newUrl = 'blob:' + __uv.meta.url.origin + url.slice('blob:'.length + location.origin.length);
|
||||||
|
|
||||||
|
__uv.blobUrls.set(newUrl, url);
|
||||||
|
event.respondWith(newUrl);
|
||||||
|
} else {
|
||||||
|
event.respondWith(url);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
client.url.on('revokeObjectURL', event => {
|
||||||
|
if (__uv.blobUrls.has(event.data.url)) {
|
||||||
|
const old = event.data.url;
|
||||||
|
event.data.url = __uv.blobUrls.get(event.data.url);
|
||||||
|
__uv.blobUrls.delete(old);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
client.websocket.on('websocket', event => {
|
||||||
|
const url = new URL(event.data.url);
|
||||||
|
|
||||||
|
const headers = {
|
||||||
|
Host: url.host,
|
||||||
|
Origin: __uv.meta.url.origin,
|
||||||
|
Pragma: 'no-cache',
|
||||||
|
'Cache-Control': 'no-cache',
|
||||||
|
Upgrade: 'websocket',
|
||||||
|
'User-Agent': window.navigator.userAgent,
|
||||||
|
'Connection': 'Upgrade',
|
||||||
|
};
|
||||||
|
|
||||||
|
const cookies = __uv.cookie.serialize(__uv.cookies, { url }, false);
|
||||||
|
|
||||||
|
if (cookies) headers.Cookie = cookies;
|
||||||
|
const protocols = [...event.data.protocols];
|
||||||
|
|
||||||
|
const remote = {
|
||||||
|
protocol: url.protocol === 'wss:' ? 'https:' : 'http:',
|
||||||
|
host: url.hostname,
|
||||||
|
port: url.port,
|
||||||
|
path: url.pathname + url.search,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (protocols.length) headers['Sec-WebSocket-Protocol'] = protocols.join(', ');
|
||||||
|
|
||||||
|
event.data.url = `wss://${window.location.host}/bare/v1/`;
|
||||||
|
event.data.protocols = [
|
||||||
|
'bare',
|
||||||
|
__uv.encodeProtocol(JSON.stringify({
|
||||||
|
remote,
|
||||||
|
headers,
|
||||||
|
forward_headers: [
|
||||||
|
'accept',
|
||||||
|
'accept-encoding',
|
||||||
|
'accept-language',
|
||||||
|
'sec-websocket-extensions',
|
||||||
|
'sec-websocket-key',
|
||||||
|
'sec-websocket-version',
|
||||||
|
]
|
||||||
|
})),
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
client.function.on('function', event => {
|
||||||
|
event.data.script = __uv.rewriteJS(event.data.script);
|
||||||
|
});
|
||||||
|
|
||||||
|
client.function.on('toString', event => {
|
||||||
|
if (__uv.methods.string in event.that) event.respondWith(event.that[__uv.methods.string]);
|
||||||
|
});
|
||||||
|
|
||||||
|
client.object.on('getOwnPropertyNames', event => {
|
||||||
|
event.data.names = event.data.names.filter(element => !(__uv.filterKeys.includes(element)));
|
||||||
|
});
|
||||||
|
|
||||||
|
client.object.on('getOwnPropertyDescriptors', event => {
|
||||||
|
console.log(event.data.descriptors);
|
||||||
|
|
||||||
|
for (const forbidden of __uv.filterKeys) {
|
||||||
|
delete event.data.descriptors[forbidden];
|
||||||
|
};
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
// Hooking functions & descriptors
|
||||||
|
client.fetch.overrideRequest();
|
||||||
|
client.fetch.overrideUrl();
|
||||||
|
client.xhr.overrideOpen();
|
||||||
|
client.xhr.overrideResponseUrl();
|
||||||
|
client.element.overrideHtml();
|
||||||
|
client.element.overrideAttribute();
|
||||||
|
client.element.overrideInsertAdjacentHTML();
|
||||||
|
client.element.overrideAudio();
|
||||||
|
// client.element.overrideQuerySelector();
|
||||||
|
client.node.overrideBaseURI();
|
||||||
|
client.node.overrideTextContent();
|
||||||
|
client.attribute.override();
|
||||||
|
client.document.overrideDomain();
|
||||||
|
client.document.overrideURL();
|
||||||
|
client.document.overrideDocumentURI();
|
||||||
|
client.document.overrideWrite();
|
||||||
|
client.document.overrideReferrer();
|
||||||
|
client.document.overrideParseFromString();
|
||||||
|
//client.document.overrideQuerySelector();
|
||||||
|
client.object.overrideGetPropertyNames();
|
||||||
|
client.object.overrideGetOwnPropertyDescriptors();
|
||||||
|
client.history.overridePushState();
|
||||||
|
client.history.overrideReplaceState();
|
||||||
|
client.eventSource.overrideConstruct();
|
||||||
|
client.eventSource.overrideUrl();
|
||||||
|
client.websocket.overrideWebSocket();
|
||||||
|
client.websocket.overrideProtocol();
|
||||||
|
client.websocket.overrideUrl();
|
||||||
|
client.url.overrideObjectURL();
|
||||||
|
client.document.overrideCookie();
|
||||||
|
client.message.overridePostMessage();
|
||||||
|
client.message.overrideMessageOrigin();
|
||||||
|
client.message.overrideMessageData();
|
||||||
|
client.workers.overrideWorker();
|
||||||
|
client.workers.overrideAddModule();
|
||||||
|
client.workers.overrideImportScripts();
|
||||||
|
client.workers.overridePostMessage();
|
||||||
|
client.navigator.overrideSendBeacon();
|
||||||
|
client.function.overrideFunction();
|
||||||
|
client.function.overrideToString();
|
||||||
|
client.location.overrideWorkerLocation(
|
||||||
|
(href) => {
|
||||||
|
return new URL(__uv.sourceUrl(href));
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
client.nativeMethods.defineProperty(__uv, '$wrap', {
|
||||||
|
get() {
|
||||||
|
return function(name) {
|
||||||
|
if (name === 'location') return __uv.methods.location;
|
||||||
|
if (name === 'eval') return __uv.methods.eval;
|
||||||
|
return name;
|
||||||
|
};
|
||||||
|
},
|
||||||
|
set: val => {
|
||||||
|
console.log('waht the fuck', val);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
__uv.$get = function(that) {
|
||||||
|
if (that === window.location) return __uv.location;
|
||||||
|
if (that === window.eval) return __uv.eval;
|
||||||
|
return that;
|
||||||
|
};
|
||||||
|
|
||||||
|
__uv.eval = client.wrap(window, 'eval', (target, that, args) => {
|
||||||
|
if (!args.length || typeof args[0] !== 'string') return target.apply(that, args);
|
||||||
|
let [ script ] = args;
|
||||||
|
|
||||||
|
script = __uv.rewriteJS(script);
|
||||||
|
return target.call(that, script);
|
||||||
|
});
|
||||||
|
|
||||||
|
__uv.call = function(target, args, that) {
|
||||||
|
return that ? target.apply(that, args) : target(...args);
|
||||||
|
};
|
||||||
|
|
||||||
|
__uv.call$ = function(obj, prop, args = []) {
|
||||||
|
return obj[prop].apply(obj, args);
|
||||||
|
};
|
||||||
|
|
||||||
|
client.nativeMethods.defineProperty(window.Object.prototype, master, {
|
||||||
|
get: () => {
|
||||||
|
return __uv;
|
||||||
|
},
|
||||||
|
enumerable: false
|
||||||
|
});
|
||||||
|
|
||||||
|
client.nativeMethods.defineProperty(window.Object.prototype, __uv.methods.setSource, {
|
||||||
|
value: function(source) {
|
||||||
|
|
||||||
|
if (!client.nativeMethods.isExtensible(this)) console.log('you suck');
|
||||||
|
|
||||||
|
if (!client.nativeMethods.isExtensible(this)) return this;
|
||||||
|
|
||||||
|
client.nativeMethods.defineProperty(this, __uv.methods.source, {
|
||||||
|
value: source,
|
||||||
|
writable: true,
|
||||||
|
enumerable: false
|
||||||
|
});
|
||||||
|
|
||||||
|
return this;
|
||||||
|
},
|
||||||
|
enumerable: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
client.nativeMethods.defineProperty(window.Object.prototype, __uv.methods.source, {
|
||||||
|
value: __uv,
|
||||||
|
writable: true,
|
||||||
|
enumerable: false
|
||||||
|
});
|
||||||
|
|
||||||
|
client.nativeMethods.defineProperty(window.Object.prototype, __uv.methods.location, {
|
||||||
|
configurable: true,
|
||||||
|
get() {
|
||||||
|
|
||||||
|
return (this === window.document || this === window) ? __uv.location : this.location;
|
||||||
|
},
|
||||||
|
set(val) {
|
||||||
|
if (this === window.document || this === window) {
|
||||||
|
__uv.location.href = val;
|
||||||
|
} else {
|
||||||
|
this.location = val;
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
client.nativeMethods.defineProperty(window.Object.prototype, __uv.methods.eval, {
|
||||||
|
configurable: true,
|
||||||
|
get() {
|
||||||
|
return this === window ? __uv.eval : this.eval;
|
||||||
|
},
|
||||||
|
set(val) {
|
||||||
|
this.eval = val;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!self.__uv) {
|
||||||
|
__uvHook(self, {});
|
||||||
|
};
|
215
lib/uv.handler.test.js
Normal file
215
lib/uv.handler.test.js
Normal file
|
@ -0,0 +1,215 @@
|
||||||
|
async function __uvHook(window, config = {}) {
|
||||||
|
if ('__uv' in window && window.__uv instanceof Ultraviolet) return false;
|
||||||
|
|
||||||
|
const worker = !window.window;
|
||||||
|
const master = '__uv';
|
||||||
|
const methodPrefix = '__uv$';
|
||||||
|
|
||||||
|
const __uv = window.__uv = new Ultraviolet({
|
||||||
|
...config,
|
||||||
|
window,
|
||||||
|
});
|
||||||
|
|
||||||
|
__uv.methods = {
|
||||||
|
get: methodPrefix + 'get',
|
||||||
|
proxy: methodPrefix + 'proxy',
|
||||||
|
call: methodPrefix + 'call',
|
||||||
|
set: methodPrefix + 'set',
|
||||||
|
script: methodPrefix + 'script',
|
||||||
|
url: methodPrefix + 'url',
|
||||||
|
object: methodPrefix + 'obj',
|
||||||
|
string: methodPrefix + 'string',
|
||||||
|
function: methodPrefix + 'fn',
|
||||||
|
this: methodPrefix + 'this',
|
||||||
|
};
|
||||||
|
|
||||||
|
__uv.methodTypes = {
|
||||||
|
[methodPrefix + 'get']: 'get',
|
||||||
|
[methodPrefix + 'proxy']: 'proxy',
|
||||||
|
[methodPrefix + 'call']: 'call',
|
||||||
|
[methodPrefix + 'set']: 'set',
|
||||||
|
[methodPrefix + 'script']: 'script',
|
||||||
|
[methodPrefix + 'url']: 'url',
|
||||||
|
[methodPrefix + 'obj']: 'object',
|
||||||
|
[methodPrefix + 'string']: 'string',
|
||||||
|
[methodPrefix + 'fn']: 'function',
|
||||||
|
[methodPrefix + 'this']: 'this',
|
||||||
|
};
|
||||||
|
|
||||||
|
__uv.filterKeys = [
|
||||||
|
master,
|
||||||
|
methodPrefix + 'get',
|
||||||
|
methodPrefix + 'set',
|
||||||
|
methodPrefix + 'proxy',
|
||||||
|
methodPrefix + 'script',
|
||||||
|
methodPrefix + 'url',
|
||||||
|
methodPrefix + 'source',
|
||||||
|
methodPrefix + 'string',
|
||||||
|
methodPrefix + 'fn',
|
||||||
|
methodPrefix + 'this',
|
||||||
|
methodPrefix + 'location',
|
||||||
|
methodPrefix + 'parent',
|
||||||
|
methodPrefix + 'top',
|
||||||
|
methodPrefix + 'eval',
|
||||||
|
methodPrefix + 'setSource'
|
||||||
|
];
|
||||||
|
|
||||||
|
const { client } = __uv;
|
||||||
|
|
||||||
|
client.on('wrap', (target, wrapped) => {
|
||||||
|
client.nativeMethods.defineProperty(wrapped, 'name', client.nativeMethods.getOwnPropertyDescriptor(target, 'name'));
|
||||||
|
client.nativeMethods.defineProperty(wrapped, 'length', client.nativeMethods.getOwnPropertyDescriptor(target, 'length'));
|
||||||
|
|
||||||
|
client.nativeMethods.defineProperty(wrapped, __uv.methods.string, {
|
||||||
|
enumerable: false,
|
||||||
|
value: client.nativeMethods.fnToString.call(target),
|
||||||
|
});
|
||||||
|
|
||||||
|
client.nativeMethods.defineProperty(wrapped, __uv.methods.function, {
|
||||||
|
enumerable: false,
|
||||||
|
value: target,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
client.function.on('toString', event => {
|
||||||
|
if (__uv.methods.string in event.that) event.respondWith(event.that[__uv.methods.string]);
|
||||||
|
});
|
||||||
|
|
||||||
|
client.object.on('getOwnPropertyNames', event => {
|
||||||
|
event.data.names = event.data.names.filter(element => !(__uv.filterKeys.includes(element)));
|
||||||
|
});
|
||||||
|
|
||||||
|
client.message.on('postMessage', event => {
|
||||||
|
const source = event.that ? event.that.__uv$source : event.target.__uv$source;
|
||||||
|
const that = !event.that ? (event.target.__uv$this || event.that) : event.that;
|
||||||
|
event.respondWith(
|
||||||
|
worker ? source.call$(that, event.target, event.data.message, event.data.transfer) :
|
||||||
|
source.call$(that, event.target, event.data.message, event.data.origin, event.data.transfer)
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
client.fetch.on('request', event => {
|
||||||
|
console.log(event.that);
|
||||||
|
});
|
||||||
|
|
||||||
|
__uv.call$ = function(obj, prop, args = []) {
|
||||||
|
console.log(obj, prop, args);
|
||||||
|
return typeof prop === 'function' ? prop.call(obj, ...args) : obj[prop].call(obj, ...args);
|
||||||
|
};
|
||||||
|
|
||||||
|
function __uv$proxy(that, uv = __uv) {
|
||||||
|
return that;
|
||||||
|
};
|
||||||
|
|
||||||
|
function __uv$get(obj, prop, uv = __uv) {
|
||||||
|
if (obj.__uv$source !== uv) obj.__uv$source = uv;
|
||||||
|
|
||||||
|
const val = obj[prop];
|
||||||
|
|
||||||
|
if (typeof val === 'function' && prop === 'postMessage') {
|
||||||
|
val.__uv$source = uv;
|
||||||
|
val.__uv$this = obj;
|
||||||
|
};
|
||||||
|
|
||||||
|
return val;
|
||||||
|
};
|
||||||
|
|
||||||
|
function __uv$set(obj, prop, val, uv = __uv) {
|
||||||
|
if (obj.__uv$source !== uv) obj.__uv$source = uv;
|
||||||
|
return obj[prop] = val;
|
||||||
|
};
|
||||||
|
|
||||||
|
function __uv$call(obj, prop, args = [], uv = __uv) {
|
||||||
|
if (obj.__uv$source !== uv) obj.__uv$source = uv;
|
||||||
|
return (uv || __uv).call$(obj, prop, args);
|
||||||
|
};
|
||||||
|
|
||||||
|
function __uv$obj(obj, keys, uv = __uv) {
|
||||||
|
const emulator = {};
|
||||||
|
|
||||||
|
for (const key of keys) {
|
||||||
|
client.nativeMethods.defineProperty(emulator, key, {
|
||||||
|
get: () => __uv$get(obj, key, uv),
|
||||||
|
set: val => __uv$set(obj, key, val, uv)
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return emulator;
|
||||||
|
};
|
||||||
|
|
||||||
|
client.function.overrideToString();
|
||||||
|
client.object.overrideGetPropertyNames();
|
||||||
|
client.message.overridePostMessage();
|
||||||
|
|
||||||
|
//client.fetch.overrideRequest();
|
||||||
|
|
||||||
|
client.nativeMethods.defineProperty(window, __uv.methods.proxy, {
|
||||||
|
value: __uv$proxy,
|
||||||
|
writable: false,
|
||||||
|
enumerable: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
client.nativeMethods.defineProperty(window, __uv.methods.get, {
|
||||||
|
get() {
|
||||||
|
return __uv$get;
|
||||||
|
},
|
||||||
|
configurable: false,
|
||||||
|
enumerable: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
client.nativeMethods.defineProperty(window, __uv.methods.set, {
|
||||||
|
value: __uv$set,
|
||||||
|
writable: false,
|
||||||
|
enumerable: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
client.nativeMethods.defineProperty(window, __uv.methods.call, {
|
||||||
|
value: __uv$call,
|
||||||
|
writable: false,
|
||||||
|
enumerable: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
client.nativeMethods.defineProperty(window, __uv.methods.object, {
|
||||||
|
value: __uv$obj,
|
||||||
|
writable: false,
|
||||||
|
enumerable: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
client.nativeMethods.defineProperty(window, __uv.methods.url, {
|
||||||
|
get: () => __uv.rewriteUrl.bind(__uv),
|
||||||
|
configurable: false,
|
||||||
|
enumerable: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
client.nativeMethods.defineProperty(window, __uv.methods.script, {
|
||||||
|
get: () => __uv.rewriteJS,
|
||||||
|
configurable: false,
|
||||||
|
enumerable: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
client.nativeMethods.defineProperty(window.Object.prototype, master, {
|
||||||
|
get: () => {
|
||||||
|
return __uv;
|
||||||
|
},
|
||||||
|
enumerable: false
|
||||||
|
});
|
||||||
|
|
||||||
|
client.nativeMethods.defineProperty(window.Object.prototype, methodPrefix + 'postMessage', {
|
||||||
|
get() {
|
||||||
|
return this.postMessage;
|
||||||
|
},
|
||||||
|
set(val) {
|
||||||
|
this.postMessage = val;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
client.nativeMethods.defineProperty(window.Object.prototype, methodPrefix + 'source', {
|
||||||
|
value: __uv,
|
||||||
|
writable: true,
|
||||||
|
enumerable: false
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!self.__uv) {
|
||||||
|
__uvHook(self, {});
|
||||||
|
};
|
299
lib/uv.sw.js
Normal file
299
lib/uv.sw.js
Normal file
|
@ -0,0 +1,299 @@
|
||||||
|
importScripts('/uv.bundle.js');
|
||||||
|
|
||||||
|
const csp = [
|
||||||
|
'cross-origin-embedder-policy',
|
||||||
|
'cross-origin-opener-policy',
|
||||||
|
'cross-origin-resource-policy',
|
||||||
|
'content-security-policy',
|
||||||
|
'content-security-policy-report-only',
|
||||||
|
'expect-ct',
|
||||||
|
'feature-policy',
|
||||||
|
'origin-isolation',
|
||||||
|
'strict-transport-security',
|
||||||
|
'upgrade-insecure-requests',
|
||||||
|
'x-content-type-options',
|
||||||
|
'x-download-options',
|
||||||
|
'x-frame-options',
|
||||||
|
'x-permitted-cross-domain-policies',
|
||||||
|
'x-powered-by',
|
||||||
|
'x-xss-protection',
|
||||||
|
];
|
||||||
|
|
||||||
|
const headers = {
|
||||||
|
csp: [
|
||||||
|
'cross-origin-embedder-policy',
|
||||||
|
'cross-origin-opener-policy',
|
||||||
|
'cross-origin-resource-policy',
|
||||||
|
'content-security-policy',
|
||||||
|
'content-security-policy-report-only',
|
||||||
|
'expect-ct',
|
||||||
|
'feature-policy',
|
||||||
|
'origin-isolation',
|
||||||
|
'strict-transport-security',
|
||||||
|
'upgrade-insecure-requests',
|
||||||
|
'x-content-type-options',
|
||||||
|
'x-download-options',
|
||||||
|
'x-frame-options',
|
||||||
|
'x-permitted-cross-domain-policies',
|
||||||
|
'x-powered-by',
|
||||||
|
'x-xss-protection',
|
||||||
|
],
|
||||||
|
forward: [
|
||||||
|
'accept-encoding',
|
||||||
|
'accept',
|
||||||
|
'connection',
|
||||||
|
'content-length',
|
||||||
|
'content-type',
|
||||||
|
'user-agent',
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const scripts = {
|
||||||
|
package: '/uv.bundle.js',
|
||||||
|
handler: '/uv.handler.js',
|
||||||
|
};
|
||||||
|
|
||||||
|
const method = {
|
||||||
|
empty: ['GET', 'HEAD']
|
||||||
|
};
|
||||||
|
|
||||||
|
const statusCode = {
|
||||||
|
empty: [
|
||||||
|
204,
|
||||||
|
304,
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const handler = UVServiceWorker('/bare/v1/', {});
|
||||||
|
|
||||||
|
addEventListener('fetch',
|
||||||
|
async event => {
|
||||||
|
return event.respondWith(handler(event));
|
||||||
|
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
function UVServiceWorker(bare = '/bare/v1/', options) {
|
||||||
|
try {
|
||||||
|
return async function handler(event) {
|
||||||
|
const { request } = event;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!request.url.startsWith(location.origin + (options.prefix || '/service/'))) {
|
||||||
|
return fetch(request);
|
||||||
|
};
|
||||||
|
|
||||||
|
const requestCtx = {
|
||||||
|
url: bare,
|
||||||
|
referrer: false,
|
||||||
|
headers: {},
|
||||||
|
forward: headers.forward,
|
||||||
|
method: request.method,
|
||||||
|
body: !method.empty.includes(request.method.toUpperCase()) ? await request.blob() : null,
|
||||||
|
redirect: request.redirect,
|
||||||
|
credentials: 'omit',
|
||||||
|
mode: request.mode === 'cors' ? request.mode : 'same-origin',
|
||||||
|
blob: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
const uv = new Ultraviolet(options);
|
||||||
|
const db = await uv.cookie.db();
|
||||||
|
|
||||||
|
uv.meta.origin = location.origin;
|
||||||
|
uv.meta.base = uv.meta.url = new URL(uv.sourceUrl(request.url));
|
||||||
|
|
||||||
|
uv.html.on('element', (element, type) => {
|
||||||
|
if (type !== 'rewrite') return false;
|
||||||
|
if (element.tagName !== 'head') return false;
|
||||||
|
|
||||||
|
element.childNodes.unshift(
|
||||||
|
{
|
||||||
|
tagName: 'script',
|
||||||
|
nodeName: 'script',
|
||||||
|
childNodes: [],
|
||||||
|
attrs: [
|
||||||
|
{ name: 'src', value: scripts.handler, skip: true }
|
||||||
|
],
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
element.childNodes.unshift(
|
||||||
|
{
|
||||||
|
tagName: 'script',
|
||||||
|
nodeName: 'script',
|
||||||
|
childNodes: [],
|
||||||
|
attrs: [
|
||||||
|
{ name: 'src', value: scripts.package, skip: true }
|
||||||
|
],
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
element.childNodes.unshift(
|
||||||
|
{
|
||||||
|
tagName: 'script',
|
||||||
|
nodeName: 'script',
|
||||||
|
childNodes: [
|
||||||
|
{
|
||||||
|
nodeName: '#text',
|
||||||
|
value: `window.__uv$cookies = atob("${btoa(uv.cookie.serialize(element.options.cookies, uv.meta, true))}");\nwindow.__uv$referrer = atob("${btoa(request.referrer)}");`
|
||||||
|
},
|
||||||
|
],
|
||||||
|
attrs: [],
|
||||||
|
skip: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (uv.meta.url.protocol === 'blob:') {
|
||||||
|
requestCtx.blob = true;
|
||||||
|
uv.meta.base = uv.meta.url = new URL(uv.meta.url.pathname);
|
||||||
|
requestCtx.url = 'blob:' + location.origin + uv.meta.url.pathname;
|
||||||
|
};
|
||||||
|
|
||||||
|
requestCtx.headers = Object.fromEntries([...request.headers.entries()]);
|
||||||
|
|
||||||
|
if (request.referrer && request.referrer.startsWith(location.origin)) {
|
||||||
|
const referer = new URL(uv.sourceUrl(request.referrer));
|
||||||
|
|
||||||
|
if (uv.meta.url.origin !== referer.origin && request.mode === 'cors') {
|
||||||
|
requestCtx.headers.origin = referer.origin;
|
||||||
|
};
|
||||||
|
|
||||||
|
requestCtx.headers.referer = referer.href;
|
||||||
|
};
|
||||||
|
|
||||||
|
const cookies = await uv.cookie.getCookies(db) || [];
|
||||||
|
const cookieStr = uv.cookie.serialize(cookies, uv.meta, false);
|
||||||
|
if (cookieStr) requestCtx.headers.cookie = cookieStr;
|
||||||
|
|
||||||
|
const bareHeaders = {
|
||||||
|
'x-bare-protocol': uv.meta.url.protocol,
|
||||||
|
'x-bare-host': uv.meta.url.hostname,
|
||||||
|
'x-bare-path': uv.meta.url.pathname + uv.meta.url.search,
|
||||||
|
'x-bare-port': uv.meta.url.port,
|
||||||
|
'x-bare-headers': JSON.stringify(requestCtx.headers),
|
||||||
|
'x-bare-forward-headers': JSON.stringify(requestCtx.forward),
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchOptions = {
|
||||||
|
method: requestCtx.method,
|
||||||
|
headers: !requestCtx.blob ? bareHeaders : requestCtx.headers,
|
||||||
|
redirect: requestCtx.redirect,
|
||||||
|
credentials: requestCtx.credentials,
|
||||||
|
mode: requestCtx.mode,
|
||||||
|
};
|
||||||
|
if (requestCtx.body) fetchOptions.body = requestCtx.body;
|
||||||
|
|
||||||
|
const response = await fetch(requestCtx.url, fetchOptions);
|
||||||
|
if (response.status === 500) {
|
||||||
|
return Promise.reject('Err');
|
||||||
|
};
|
||||||
|
|
||||||
|
const sendData = !requestCtx.blob ? getBarerResponse(response) : {
|
||||||
|
status: response.status,
|
||||||
|
statusText: response.statusText,
|
||||||
|
headers: Object.fromEntries([...response.headers.entries()]),
|
||||||
|
body: response.body,
|
||||||
|
};
|
||||||
|
|
||||||
|
const responseCtx = {
|
||||||
|
headers: sendData.headers,
|
||||||
|
status: sendData.status,
|
||||||
|
statusText: sendData.statusText,
|
||||||
|
body: !statusCode.empty.includes(sendData.status) ? sendData.body : null,
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const name of headers.csp) {
|
||||||
|
if (responseCtx.headers[name]) delete responseCtx.headers[name];
|
||||||
|
};
|
||||||
|
|
||||||
|
if (responseCtx.headers.location) {
|
||||||
|
responseCtx.headers.location = uv.rewriteUrl(responseCtx.headers.location);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (responseCtx.headers['set-cookie']) {
|
||||||
|
Promise.resolve(uv.cookie.setCookies(responseCtx.headers['set-cookie'], db, uv.meta)).then(() => {
|
||||||
|
self.clients.matchAll().then(function (clients){
|
||||||
|
clients.forEach(function(client){
|
||||||
|
client.postMessage({
|
||||||
|
msg: 'updateCookies',
|
||||||
|
url: uv.meta.url.href,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
delete responseCtx.headers['set-cookie'];
|
||||||
|
};
|
||||||
|
|
||||||
|
if (responseCtx.body) {
|
||||||
|
switch(request.destination) {
|
||||||
|
case 'script':
|
||||||
|
responseCtx.body = `if (!self.__uv && self.importScripts) importScripts('/uv.bundle.js', '/uv.handler.js');\n`;
|
||||||
|
responseCtx.body += uv.js.rewrite(
|
||||||
|
await response.text()
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case 'worker':
|
||||||
|
responseCtx.body = `if (!self.__uv) importScripts('/uv.bundle.js', '/uv.handler.js');\n`;
|
||||||
|
responseCtx.body += uv.js.rewrite(
|
||||||
|
await response.text()
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case 'style':
|
||||||
|
responseCtx.body = uv.rewriteCSS(
|
||||||
|
await response.text()
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case 'iframe':
|
||||||
|
case 'document':
|
||||||
|
if (isHtml(uv.meta.url, (sendData.headers['content-type'] || ''))) {
|
||||||
|
responseCtx.body = uv.rewriteHtml(
|
||||||
|
await response.text(),
|
||||||
|
{
|
||||||
|
document: true ,
|
||||||
|
cookies,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
if (requestCtx.headers.accept === 'text/event-stream') {
|
||||||
|
requestCtx.headers['content-type'] = 'text/event-stream';
|
||||||
|
};
|
||||||
|
|
||||||
|
return new Response(responseCtx.body, {
|
||||||
|
headers: responseCtx.headers,
|
||||||
|
status: responseCtx.status,
|
||||||
|
statusText: responseCtx.statusText,
|
||||||
|
});
|
||||||
|
} catch(e) {
|
||||||
|
console.log(e);
|
||||||
|
return new Response(e.toString(), {
|
||||||
|
status: 500,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
};
|
||||||
|
} catch(e) {
|
||||||
|
console.log(e);
|
||||||
|
return (event) => {
|
||||||
|
event.respondWith(new Response(e.toString(), {
|
||||||
|
status: 500,
|
||||||
|
}))
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
function getBarerResponse(response) {
|
||||||
|
return {
|
||||||
|
headers: JSON.parse(response.headers.get('x-bare-headers')),
|
||||||
|
status: +response.headers.get('x-bare-status'),
|
||||||
|
statusText: response.headers.get('x-bare-status-text'),
|
||||||
|
body: response.body,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
function isHtml(url, contentType = '') {
|
||||||
|
return (Ultraviolet.mime.contentType((contentType || url.pathname)) || 'text/html').split(';')[0] === 'text/html';
|
||||||
|
};
|
2857
package-lock.json
generated
Normal file
2857
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
26
package.json
Normal file
26
package.json
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
{
|
||||||
|
"name": "ultraviolet",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Proxy",
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
|
},
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"type": "module",
|
||||||
|
"dependencies": {
|
||||||
|
"css-tree": "^2.0.4",
|
||||||
|
"esotope-hammerhead": "^0.6.1",
|
||||||
|
"idb": "^7.0.0",
|
||||||
|
"meriyah": "^4.2.0",
|
||||||
|
"mime-db": "^1.51.0",
|
||||||
|
"node-static": "^0.7.11",
|
||||||
|
"parse5": "^6.0.1",
|
||||||
|
"set-cookie-parser": "^2.4.8",
|
||||||
|
"webpack": "^5.68.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"eslint": "^8.8.0"
|
||||||
|
}
|
||||||
|
}
|
74
rewrite/codecs.js
Normal file
74
rewrite/codecs.js
Normal file
|
@ -0,0 +1,74 @@
|
||||||
|
// -------------------------------------------------------------
|
||||||
|
// WARNING: this file is used by both the client and the server.
|
||||||
|
// Do not use any browser or node-specific API!
|
||||||
|
// -------------------------------------------------------------
|
||||||
|
export const xor = {
|
||||||
|
encode(str){
|
||||||
|
if (!str) return str;
|
||||||
|
return encodeURIComponent(str.toString().split('').map((char, ind) => ind % 2 ? String.fromCharCode(char.charCodeAt() ^ 2) : char).join(''));
|
||||||
|
},
|
||||||
|
decode(str){
|
||||||
|
if (!str) return str;
|
||||||
|
let [ input, ...search ] = str.split('?');
|
||||||
|
|
||||||
|
return decodeURIComponent(input).split('').map((char, ind) => ind % 2 ? String.fromCharCode(char.charCodeAt(0) ^ 2) : char).join('') + (search.length ? '?' + search.join('?') : '');
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const plain = {
|
||||||
|
encode(str) {
|
||||||
|
if (!str) return str;
|
||||||
|
return encodeURIComponent(str);
|
||||||
|
},
|
||||||
|
decode(str) {
|
||||||
|
if (!str) return str;
|
||||||
|
return decodeURIComponent(str);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const base64 = {
|
||||||
|
encode(str){
|
||||||
|
if (!str) return str;
|
||||||
|
str = str.toString();
|
||||||
|
const b64chs = Array.from('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=');
|
||||||
|
let u32;
|
||||||
|
let c0;
|
||||||
|
let c1;
|
||||||
|
let c2;
|
||||||
|
let asc = '';
|
||||||
|
let pad = str.length % 3;
|
||||||
|
|
||||||
|
for (let i = 0; i < str.length;) {
|
||||||
|
if((c0 = str.charCodeAt(i++)) > 255 || (c1 = str.charCodeAt(i++)) > 255 || (c2 = str.charCodeAt(i++)) > 255)throw new TypeError('invalid character found');
|
||||||
|
u32 = (c0 << 16) | (c1 << 8) | c2;
|
||||||
|
asc += b64chs[u32 >> 18 & 63]
|
||||||
|
+ b64chs[u32 >> 12 & 63]
|
||||||
|
+ b64chs[u32 >> 6 & 63]
|
||||||
|
+ b64chs[u32 & 63];
|
||||||
|
}
|
||||||
|
|
||||||
|
return encodeURIComponent(pad ? asc.slice(0, pad - 3) + '==='.substr(pad) : asc);
|
||||||
|
},
|
||||||
|
decode(str){
|
||||||
|
if (!str) return str;
|
||||||
|
str = decodeURIComponent(str.toString());
|
||||||
|
const b64tab = {"0":52,"1":53,"2":54,"3":55,"4":56,"5":57,"6":58,"7":59,"8":60,"9":61,"A":0,"B":1,"C":2,"D":3,"E":4,"F":5,"G":6,"H":7,"I":8,"J":9,"K":10,"L":11,"M":12,"N":13,"O":14,"P":15,"Q":16,"R":17,"S":18,"T":19,"U":20,"V":21,"W":22,"X":23,"Y":24,"Z":25,"a":26,"b":27,"c":28,"d":29,"e":30,"f":31,"g":32,"h":33,"i":34,"j":35,"k":36,"l":37,"m":38,"n":39,"o":40,"p":41,"q":42,"r":43,"s":44,"t":45,"u":46,"v":47,"w":48,"x":49,"y":50,"z":51,"+":62,"/":63,"=":64};
|
||||||
|
str = str.replace(/\s+/g, '');
|
||||||
|
str += '=='.slice(2 - (str.length & 3));
|
||||||
|
let u24;
|
||||||
|
let bin = '';
|
||||||
|
let r1;
|
||||||
|
let r2;
|
||||||
|
|
||||||
|
for (let i = 0; i < str.length;) {
|
||||||
|
u24 = b64tab[str.charAt(i++)] << 18
|
||||||
|
| b64tab[str.charAt(i++)] << 12
|
||||||
|
| (r1 = b64tab[str.charAt(i++)]) << 6
|
||||||
|
| (r2 = b64tab[str.charAt(i++)]);
|
||||||
|
bin += r1 === 64 ? String.fromCharCode(u24 >> 16 & 255)
|
||||||
|
: r2 === 64 ? String.fromCharCode(u24 >> 16 & 255, u24 >> 8 & 255)
|
||||||
|
: String.fromCharCode(u24 >> 16 & 255, u24 >> 8 & 255, u24 & 255);
|
||||||
|
};
|
||||||
|
return bin;
|
||||||
|
},
|
||||||
|
};
|
75
rewrite/cookie.js
Normal file
75
rewrite/cookie.js
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
// -------------------------------------------------------------
|
||||||
|
// WARNING: this file is used by both the client and the server.
|
||||||
|
// Do not use any browser or node-specific API!
|
||||||
|
// -------------------------------------------------------------
|
||||||
|
import setCookie from 'set-cookie-parser';
|
||||||
|
|
||||||
|
function validateCookie(cookie, meta, js = false) {
|
||||||
|
if (cookie.httpOnly && !!js) return false;
|
||||||
|
|
||||||
|
if (cookie.domain.startsWith('.')) {
|
||||||
|
|
||||||
|
if (!meta.url.hostname.endsWith(cookie.domain.slice(1))) return false;
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (cookie.domain !== meta.url.hostname) return false;
|
||||||
|
if (cookie.secure && meta.url.protocol === 'http:') return false;
|
||||||
|
if (!meta.url.pathname.startsWith(cookie.path)) return false;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
async function db(openDB) {
|
||||||
|
const db = await openDB('__op', 1, {
|
||||||
|
upgrade(db, oldVersion, newVersion, transaction) {
|
||||||
|
const store = db.createObjectStore('cookies', {
|
||||||
|
keyPath: 'id',
|
||||||
|
});
|
||||||
|
store.createIndex('path', 'path');
|
||||||
|
},
|
||||||
|
});
|
||||||
|
db.transaction(['cookies'], 'readwrite').store.index('path');
|
||||||
|
return db;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
function serialize(cookies = [], meta, js) {
|
||||||
|
let str = '';
|
||||||
|
for (const cookie of cookies) {
|
||||||
|
if (!validateCookie(cookie, meta, js)) continue;
|
||||||
|
if (str.length) str += '; ';
|
||||||
|
str += cookie.name;
|
||||||
|
str += '='
|
||||||
|
str += cookie.value;
|
||||||
|
};
|
||||||
|
return str;
|
||||||
|
};
|
||||||
|
|
||||||
|
async function getCookies(db) {
|
||||||
|
return await db.getAll('cookies');
|
||||||
|
};
|
||||||
|
|
||||||
|
function setCookies(data, db, meta) {
|
||||||
|
if (!db) return false;
|
||||||
|
const cookies = setCookie(data, {
|
||||||
|
decodeValues: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
for (const cookie of cookies) {
|
||||||
|
if (!cookie.domain) cookie.domain = '.' + meta.url.hostname;
|
||||||
|
if (!cookie.path) cookie.path = '/';
|
||||||
|
|
||||||
|
if (!cookie.domain.startsWith('.')) {
|
||||||
|
cookie.domain = '.' + cookie.domain;
|
||||||
|
};
|
||||||
|
|
||||||
|
db.put('cookies', {
|
||||||
|
...cookie,
|
||||||
|
id: `${cookie.domain}@${cookie.path}@${cookie.name}`,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
export { validateCookie, getCookies, setCookies, db , serialize };
|
37
rewrite/css.js
Normal file
37
rewrite/css.js
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
import { parse, walk, generate } from "css-tree";
|
||||||
|
import EventEmitter from "./events.js";
|
||||||
|
import parsel from "./parsel.js";
|
||||||
|
|
||||||
|
class CSS extends EventEmitter {
|
||||||
|
constructor(ctx) {
|
||||||
|
super();
|
||||||
|
this.ctx = ctx;
|
||||||
|
this.meta = ctx.meta;
|
||||||
|
this.parsel = parsel;
|
||||||
|
this.parse = parse;
|
||||||
|
this.walk = walk;
|
||||||
|
this.generate = generate;
|
||||||
|
};
|
||||||
|
rewrite(str, options) {
|
||||||
|
return this.recast(str, options, 'rewrite');
|
||||||
|
};
|
||||||
|
source(str, options) {
|
||||||
|
return this.recast(str, options, 'source');
|
||||||
|
};
|
||||||
|
recast(str, options, type) {
|
||||||
|
if (!str) return str;
|
||||||
|
str = new String(str).toString();
|
||||||
|
try {
|
||||||
|
const ast = this.parse(str, { ...options, parseCustomProperty: true });
|
||||||
|
this.walk(ast, node => {
|
||||||
|
this.emit(node.type, node, options, type);
|
||||||
|
});
|
||||||
|
return this.generate(ast);
|
||||||
|
} catch(e) {
|
||||||
|
console.log(e)
|
||||||
|
return str;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CSS;
|
497
rewrite/events.js
Normal file
497
rewrite/events.js
Normal file
|
@ -0,0 +1,497 @@
|
||||||
|
// Copyright Joyent, Inc. and other Node contributors.
|
||||||
|
//
|
||||||
|
// Permission is hereby granted, free of charge, to any person obtaining a
|
||||||
|
// copy of this software and associated documentation files (the
|
||||||
|
// "Software"), to deal in the Software without restriction, including
|
||||||
|
// without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
// distribute, sublicense, and/or sell copies of the Software, and to permit
|
||||||
|
// persons to whom the Software is furnished to do so, subject to the
|
||||||
|
// following conditions:
|
||||||
|
//
|
||||||
|
// The above copyright notice and this permission notice shall be included
|
||||||
|
// in all copies or substantial portions of the Software.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||||
|
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
|
||||||
|
// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||||
|
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||||
|
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
|
||||||
|
// USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var R = typeof Reflect === 'object' ? Reflect : null
|
||||||
|
var ReflectApply = R && typeof R.apply === 'function'
|
||||||
|
? R.apply
|
||||||
|
: function ReflectApply(target, receiver, args) {
|
||||||
|
return Function.prototype.apply.call(target, receiver, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
var ReflectOwnKeys
|
||||||
|
if (R && typeof R.ownKeys === 'function') {
|
||||||
|
ReflectOwnKeys = R.ownKeys
|
||||||
|
} else if (Object.getOwnPropertySymbols) {
|
||||||
|
ReflectOwnKeys = function ReflectOwnKeys(target) {
|
||||||
|
return Object.getOwnPropertyNames(target)
|
||||||
|
.concat(Object.getOwnPropertySymbols(target));
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
ReflectOwnKeys = function ReflectOwnKeys(target) {
|
||||||
|
return Object.getOwnPropertyNames(target);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function ProcessEmitWarning(warning) {
|
||||||
|
if (console && console.warn) console.warn(warning);
|
||||||
|
}
|
||||||
|
|
||||||
|
var NumberIsNaN = Number.isNaN || function NumberIsNaN(value) {
|
||||||
|
return value !== value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function EventEmitter() {
|
||||||
|
EventEmitter.init.call(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default EventEmitter;
|
||||||
|
|
||||||
|
// Backwards-compat with node 0.10.x
|
||||||
|
EventEmitter.EventEmitter = EventEmitter;
|
||||||
|
|
||||||
|
EventEmitter.prototype._events = undefined;
|
||||||
|
EventEmitter.prototype._eventsCount = 0;
|
||||||
|
EventEmitter.prototype._maxListeners = undefined;
|
||||||
|
|
||||||
|
// By default EventEmitters will print a warning if more than 10 listeners are
|
||||||
|
// added to it. This is a useful default which helps finding memory leaks.
|
||||||
|
var defaultMaxListeners = 10;
|
||||||
|
|
||||||
|
function checkListener(listener) {
|
||||||
|
if (typeof listener !== 'function') {
|
||||||
|
throw new TypeError('The "listener" argument must be of type Function. Received type ' + typeof listener);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.defineProperty(EventEmitter, 'defaultMaxListeners', {
|
||||||
|
enumerable: true,
|
||||||
|
get: function() {
|
||||||
|
return defaultMaxListeners;
|
||||||
|
},
|
||||||
|
set: function(arg) {
|
||||||
|
if (typeof arg !== 'number' || arg < 0 || NumberIsNaN(arg)) {
|
||||||
|
throw new RangeError('The value of "defaultMaxListeners" is out of range. It must be a non-negative number. Received ' + arg + '.');
|
||||||
|
}
|
||||||
|
defaultMaxListeners = arg;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
EventEmitter.init = function() {
|
||||||
|
|
||||||
|
if (this._events === undefined ||
|
||||||
|
this._events === Object.getPrototypeOf(this)._events) {
|
||||||
|
this._events = Object.create(null);
|
||||||
|
this._eventsCount = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._maxListeners = this._maxListeners || undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Obviously not all Emitters should be limited to 10. This function allows
|
||||||
|
// that to be increased. Set to zero for unlimited.
|
||||||
|
EventEmitter.prototype.setMaxListeners = function setMaxListeners(n) {
|
||||||
|
if (typeof n !== 'number' || n < 0 || NumberIsNaN(n)) {
|
||||||
|
throw new RangeError('The value of "n" is out of range. It must be a non-negative number. Received ' + n + '.');
|
||||||
|
}
|
||||||
|
this._maxListeners = n;
|
||||||
|
return this;
|
||||||
|
};
|
||||||
|
|
||||||
|
function _getMaxListeners(that) {
|
||||||
|
if (that._maxListeners === undefined)
|
||||||
|
return EventEmitter.defaultMaxListeners;
|
||||||
|
return that._maxListeners;
|
||||||
|
}
|
||||||
|
|
||||||
|
EventEmitter.prototype.getMaxListeners = function getMaxListeners() {
|
||||||
|
return _getMaxListeners(this);
|
||||||
|
};
|
||||||
|
|
||||||
|
EventEmitter.prototype.emit = function emit(type) {
|
||||||
|
var args = [];
|
||||||
|
for (var i = 1; i < arguments.length; i++) args.push(arguments[i]);
|
||||||
|
var doError = (type === 'error');
|
||||||
|
|
||||||
|
var events = this._events;
|
||||||
|
if (events !== undefined)
|
||||||
|
doError = (doError && events.error === undefined);
|
||||||
|
else if (!doError)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// If there is no 'error' event listener then throw.
|
||||||
|
if (doError) {
|
||||||
|
var er;
|
||||||
|
if (args.length > 0)
|
||||||
|
er = args[0];
|
||||||
|
if (er instanceof Error) {
|
||||||
|
// Note: The comments on the `throw` lines are intentional, they show
|
||||||
|
// up in Node's output if this results in an unhandled exception.
|
||||||
|
throw er; // Unhandled 'error' event
|
||||||
|
}
|
||||||
|
// At least give some kind of context to the user
|
||||||
|
var err = new Error('Unhandled error.' + (er ? ' (' + er.message + ')' : ''));
|
||||||
|
err.context = er;
|
||||||
|
throw err; // Unhandled 'error' event
|
||||||
|
}
|
||||||
|
|
||||||
|
var handler = events[type];
|
||||||
|
|
||||||
|
if (handler === undefined)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (typeof handler === 'function') {
|
||||||
|
ReflectApply(handler, this, args);
|
||||||
|
} else {
|
||||||
|
var len = handler.length;
|
||||||
|
var listeners = arrayClone(handler, len);
|
||||||
|
for (var i = 0; i < len; ++i)
|
||||||
|
ReflectApply(listeners[i], this, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
function _addListener(target, type, listener, prepend) {
|
||||||
|
var m;
|
||||||
|
var events;
|
||||||
|
var existing;
|
||||||
|
|
||||||
|
checkListener(listener);
|
||||||
|
|
||||||
|
events = target._events;
|
||||||
|
if (events === undefined) {
|
||||||
|
events = target._events = Object.create(null);
|
||||||
|
target._eventsCount = 0;
|
||||||
|
} else {
|
||||||
|
// To avoid recursion in the case that type === "newListener"! Before
|
||||||
|
// adding it to the listeners, first emit "newListener".
|
||||||
|
if (events.newListener !== undefined) {
|
||||||
|
target.emit('newListener', type,
|
||||||
|
listener.listener ? listener.listener : listener);
|
||||||
|
|
||||||
|
// Re-assign `events` because a newListener handler could have caused the
|
||||||
|
// this._events to be assigned to a new object
|
||||||
|
events = target._events;
|
||||||
|
}
|
||||||
|
existing = events[type];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existing === undefined) {
|
||||||
|
// Optimize the case of one listener. Don't need the extra array object.
|
||||||
|
existing = events[type] = listener;
|
||||||
|
++target._eventsCount;
|
||||||
|
} else {
|
||||||
|
if (typeof existing === 'function') {
|
||||||
|
// Adding the second element, need to change to array.
|
||||||
|
existing = events[type] =
|
||||||
|
prepend ? [listener, existing] : [existing, listener];
|
||||||
|
// If we've already got an array, just append.
|
||||||
|
} else if (prepend) {
|
||||||
|
existing.unshift(listener);
|
||||||
|
} else {
|
||||||
|
existing.push(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for listener leak
|
||||||
|
m = _getMaxListeners(target);
|
||||||
|
if (m > 0 && existing.length > m && !existing.warned) {
|
||||||
|
existing.warned = true;
|
||||||
|
// No error code for this since it is a Warning
|
||||||
|
// eslint-disable-next-line no-restricted-syntax
|
||||||
|
var w = new Error('Possible EventEmitter memory leak detected. ' +
|
||||||
|
existing.length + ' ' + String(type) + ' listeners ' +
|
||||||
|
'added. Use emitter.setMaxListeners() to ' +
|
||||||
|
'increase limit');
|
||||||
|
w.name = 'MaxListenersExceededWarning';
|
||||||
|
w.emitter = target;
|
||||||
|
w.type = type;
|
||||||
|
w.count = existing.length;
|
||||||
|
ProcessEmitWarning(w);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return target;
|
||||||
|
}
|
||||||
|
|
||||||
|
EventEmitter.prototype.addListener = function addListener(type, listener) {
|
||||||
|
return _addListener(this, type, listener, false);
|
||||||
|
};
|
||||||
|
|
||||||
|
EventEmitter.prototype.on = EventEmitter.prototype.addListener;
|
||||||
|
|
||||||
|
EventEmitter.prototype.prependListener =
|
||||||
|
function prependListener(type, listener) {
|
||||||
|
return _addListener(this, type, listener, true);
|
||||||
|
};
|
||||||
|
|
||||||
|
function onceWrapper() {
|
||||||
|
if (!this.fired) {
|
||||||
|
this.target.removeListener(this.type, this.wrapFn);
|
||||||
|
this.fired = true;
|
||||||
|
if (arguments.length === 0)
|
||||||
|
return this.listener.call(this.target);
|
||||||
|
return this.listener.apply(this.target, arguments);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function _onceWrap(target, type, listener) {
|
||||||
|
var state = { fired: false, wrapFn: undefined, target: target, type: type, listener: listener };
|
||||||
|
var wrapped = onceWrapper.bind(state);
|
||||||
|
wrapped.listener = listener;
|
||||||
|
state.wrapFn = wrapped;
|
||||||
|
return wrapped;
|
||||||
|
}
|
||||||
|
|
||||||
|
EventEmitter.prototype.once = function once(type, listener) {
|
||||||
|
checkListener(listener);
|
||||||
|
this.on(type, _onceWrap(this, type, listener));
|
||||||
|
return this;
|
||||||
|
};
|
||||||
|
|
||||||
|
EventEmitter.prototype.prependOnceListener =
|
||||||
|
function prependOnceListener(type, listener) {
|
||||||
|
checkListener(listener);
|
||||||
|
this.prependListener(type, _onceWrap(this, type, listener));
|
||||||
|
return this;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Emits a 'removeListener' event if and only if the listener was removed.
|
||||||
|
EventEmitter.prototype.removeListener =
|
||||||
|
function removeListener(type, listener) {
|
||||||
|
var list, events, position, i, originalListener;
|
||||||
|
|
||||||
|
checkListener(listener);
|
||||||
|
|
||||||
|
events = this._events;
|
||||||
|
if (events === undefined)
|
||||||
|
return this;
|
||||||
|
|
||||||
|
list = events[type];
|
||||||
|
if (list === undefined)
|
||||||
|
return this;
|
||||||
|
|
||||||
|
if (list === listener || list.listener === listener) {
|
||||||
|
if (--this._eventsCount === 0)
|
||||||
|
this._events = Object.create(null);
|
||||||
|
else {
|
||||||
|
delete events[type];
|
||||||
|
if (events.removeListener)
|
||||||
|
this.emit('removeListener', type, list.listener || listener);
|
||||||
|
}
|
||||||
|
} else if (typeof list !== 'function') {
|
||||||
|
position = -1;
|
||||||
|
|
||||||
|
for (i = list.length - 1; i >= 0; i--) {
|
||||||
|
if (list[i] === listener || list[i].listener === listener) {
|
||||||
|
originalListener = list[i].listener;
|
||||||
|
position = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (position < 0)
|
||||||
|
return this;
|
||||||
|
|
||||||
|
if (position === 0)
|
||||||
|
list.shift();
|
||||||
|
else {
|
||||||
|
spliceOne(list, position);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (list.length === 1)
|
||||||
|
events[type] = list[0];
|
||||||
|
|
||||||
|
if (events.removeListener !== undefined)
|
||||||
|
this.emit('removeListener', type, originalListener || listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this;
|
||||||
|
};
|
||||||
|
|
||||||
|
EventEmitter.prototype.off = EventEmitter.prototype.removeListener;
|
||||||
|
|
||||||
|
EventEmitter.prototype.removeAllListeners =
|
||||||
|
function removeAllListeners(type) {
|
||||||
|
var listeners, events, i;
|
||||||
|
|
||||||
|
events = this._events;
|
||||||
|
if (events === undefined)
|
||||||
|
return this;
|
||||||
|
|
||||||
|
// not listening for removeListener, no need to emit
|
||||||
|
if (events.removeListener === undefined) {
|
||||||
|
if (arguments.length === 0) {
|
||||||
|
this._events = Object.create(null);
|
||||||
|
this._eventsCount = 0;
|
||||||
|
} else if (events[type] !== undefined) {
|
||||||
|
if (--this._eventsCount === 0)
|
||||||
|
this._events = Object.create(null);
|
||||||
|
else
|
||||||
|
delete events[type];
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
// emit removeListener for all listeners on all events
|
||||||
|
if (arguments.length === 0) {
|
||||||
|
var keys = Object.keys(events);
|
||||||
|
var key;
|
||||||
|
for (i = 0; i < keys.length; ++i) {
|
||||||
|
key = keys[i];
|
||||||
|
if (key === 'removeListener') continue;
|
||||||
|
this.removeAllListeners(key);
|
||||||
|
}
|
||||||
|
this.removeAllListeners('removeListener');
|
||||||
|
this._events = Object.create(null);
|
||||||
|
this._eventsCount = 0;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
listeners = events[type];
|
||||||
|
|
||||||
|
if (typeof listeners === 'function') {
|
||||||
|
this.removeListener(type, listeners);
|
||||||
|
} else if (listeners !== undefined) {
|
||||||
|
// LIFO order
|
||||||
|
for (i = listeners.length - 1; i >= 0; i--) {
|
||||||
|
this.removeListener(type, listeners[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return this;
|
||||||
|
};
|
||||||
|
|
||||||
|
function _listeners(target, type, unwrap) {
|
||||||
|
var events = target._events;
|
||||||
|
|
||||||
|
if (events === undefined)
|
||||||
|
return [];
|
||||||
|
|
||||||
|
var evlistener = events[type];
|
||||||
|
if (evlistener === undefined)
|
||||||
|
return [];
|
||||||
|
|
||||||
|
if (typeof evlistener === 'function')
|
||||||
|
return unwrap ? [evlistener.listener || evlistener] : [evlistener];
|
||||||
|
|
||||||
|
return unwrap ?
|
||||||
|
unwrapListeners(evlistener) : arrayClone(evlistener, evlistener.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
EventEmitter.prototype.listeners = function listeners(type) {
|
||||||
|
return _listeners(this, type, true);
|
||||||
|
};
|
||||||
|
|
||||||
|
EventEmitter.prototype.rawListeners = function rawListeners(type) {
|
||||||
|
return _listeners(this, type, false);
|
||||||
|
};
|
||||||
|
|
||||||
|
EventEmitter.listenerCount = function(emitter, type) {
|
||||||
|
if (typeof emitter.listenerCount === 'function') {
|
||||||
|
return emitter.listenerCount(type);
|
||||||
|
} else {
|
||||||
|
return listenerCount.call(emitter, type);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
EventEmitter.prototype.listenerCount = listenerCount;
|
||||||
|
function listenerCount(type) {
|
||||||
|
var events = this._events;
|
||||||
|
|
||||||
|
if (events !== undefined) {
|
||||||
|
var evlistener = events[type];
|
||||||
|
|
||||||
|
if (typeof evlistener === 'function') {
|
||||||
|
return 1;
|
||||||
|
} else if (evlistener !== undefined) {
|
||||||
|
return evlistener.length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
EventEmitter.prototype.eventNames = function eventNames() {
|
||||||
|
return this._eventsCount > 0 ? ReflectOwnKeys(this._events) : [];
|
||||||
|
};
|
||||||
|
|
||||||
|
function arrayClone(arr, n) {
|
||||||
|
var copy = new Array(n);
|
||||||
|
for (var i = 0; i < n; ++i)
|
||||||
|
copy[i] = arr[i];
|
||||||
|
return copy;
|
||||||
|
}
|
||||||
|
|
||||||
|
function spliceOne(list, index) {
|
||||||
|
for (; index + 1 < list.length; index++)
|
||||||
|
list[index] = list[index + 1];
|
||||||
|
list.pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
function unwrapListeners(arr) {
|
||||||
|
var ret = new Array(arr.length);
|
||||||
|
for (var i = 0; i < ret.length; ++i) {
|
||||||
|
ret[i] = arr[i].listener || arr[i];
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
function once(emitter, name) {
|
||||||
|
return new Promise(function (resolve, reject) {
|
||||||
|
function errorListener(err) {
|
||||||
|
emitter.removeListener(name, resolver);
|
||||||
|
reject(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolver() {
|
||||||
|
if (typeof emitter.removeListener === 'function') {
|
||||||
|
emitter.removeListener('error', errorListener);
|
||||||
|
}
|
||||||
|
resolve([].slice.call(arguments));
|
||||||
|
};
|
||||||
|
|
||||||
|
eventTargetAgnosticAddListener(emitter, name, resolver, { once: true });
|
||||||
|
if (name !== 'error') {
|
||||||
|
addErrorHandlerIfEventEmitter(emitter, errorListener, { once: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function addErrorHandlerIfEventEmitter(emitter, handler, flags) {
|
||||||
|
if (typeof emitter.on === 'function') {
|
||||||
|
eventTargetAgnosticAddListener(emitter, 'error', handler, flags);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function eventTargetAgnosticAddListener(emitter, name, listener, flags) {
|
||||||
|
if (typeof emitter.on === 'function') {
|
||||||
|
if (flags.once) {
|
||||||
|
emitter.once(name, listener);
|
||||||
|
} else {
|
||||||
|
emitter.on(name, listener);
|
||||||
|
}
|
||||||
|
} else if (typeof emitter.addEventListener === 'function') {
|
||||||
|
// EventTarget does not have `error` event semantics like Node
|
||||||
|
// EventEmitters, we do not listen for `error` events here.
|
||||||
|
emitter.addEventListener(name, function wrapListener(arg) {
|
||||||
|
// IE does not have builtin `{ once: true }` support so we
|
||||||
|
// have to do it manually.
|
||||||
|
if (flags.once) {
|
||||||
|
emitter.removeEventListener(name, wrapListener);
|
||||||
|
}
|
||||||
|
listener(arg);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
throw new TypeError('The "emitter" argument must be of type EventEmitter. Received type ' + typeof emitter);
|
||||||
|
}
|
||||||
|
}
|
237
rewrite/html.js
Normal file
237
rewrite/html.js
Normal file
|
@ -0,0 +1,237 @@
|
||||||
|
import EventEmitter from './events.js';
|
||||||
|
import { parse, parseFragment, serialize } from 'parse5';
|
||||||
|
|
||||||
|
class HTML extends EventEmitter {
|
||||||
|
constructor(ctx) {
|
||||||
|
super();
|
||||||
|
this.ctx = ctx;
|
||||||
|
this.rewriteUrl = ctx.rewriteUrl;
|
||||||
|
this.sourceUrl = ctx.sourceUrl;
|
||||||
|
};
|
||||||
|
rewrite(str, options = {}) {
|
||||||
|
if (!str) return str;
|
||||||
|
return this.recast(str, node => {
|
||||||
|
if (node.tagName) this.emit('element', node, 'rewrite');
|
||||||
|
if (node.attr) this.emit('attr', node, 'rewrite');
|
||||||
|
if (node.nodeName === '#text') this.emit('text', node, 'rewrite');
|
||||||
|
}, options)
|
||||||
|
};
|
||||||
|
source(str, options = {}) {
|
||||||
|
if (!str) return str;
|
||||||
|
return this.recast(str, node => {
|
||||||
|
if (node.tagName) this.emit('element', node, 'source');
|
||||||
|
if (node.attr) this.emit('attr', node, 'source');
|
||||||
|
if (node.nodeName === '#text') this.emit('text', node, 'source');
|
||||||
|
}, options)
|
||||||
|
};
|
||||||
|
recast(str, fn, options = {}) {
|
||||||
|
try {
|
||||||
|
const ast = (options.document ? parse : parseFragment)(new String(str).toString());
|
||||||
|
this.iterate(ast, fn, options);
|
||||||
|
return serialize(ast);
|
||||||
|
} catch(e) {
|
||||||
|
return str;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
iterate(ast, fn, fnOptions) {
|
||||||
|
if (!ast) return ast;
|
||||||
|
|
||||||
|
if (ast.tagName) {
|
||||||
|
const element = new P5Element(ast, false, fnOptions);
|
||||||
|
fn(element);
|
||||||
|
if (ast.attrs) {
|
||||||
|
for (const attr of ast.attrs) {
|
||||||
|
if (!attr.skip) fn(new AttributeEvent(element, attr, fnOptions));
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
if (ast.childNodes) {
|
||||||
|
for (const child of ast.childNodes) {
|
||||||
|
if (!child.skip) this.iterate(child, fn, fnOptions);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
if (ast.nodeName === '#text') {
|
||||||
|
fn(new TextEvent(ast, new P5Element(ast.parentNode), false, fnOptions));
|
||||||
|
};
|
||||||
|
|
||||||
|
return ast;
|
||||||
|
};
|
||||||
|
wrapSrcset(str, meta = this.ctx.meta) {
|
||||||
|
return str.split(',').map(src => {
|
||||||
|
const parts = src.trimStart().split(' ');
|
||||||
|
if (parts[0]) parts[0] = this.ctx.rewriteUrl(parts[0], meta);
|
||||||
|
return parts.join(' ');
|
||||||
|
}).join(', ');
|
||||||
|
};
|
||||||
|
unwrapSrcset(str, meta = this.ctx.meta) {
|
||||||
|
return str.split(',').map(src => {
|
||||||
|
const parts = src.trimStart().split(' ');
|
||||||
|
if (parts[0]) parts[0] = this.ctx.sourceUrl(parts[0], meta);
|
||||||
|
return parts.join(' ');
|
||||||
|
}).join(', ');
|
||||||
|
};
|
||||||
|
static parse = parse;
|
||||||
|
static parseFragment = parseFragment;
|
||||||
|
static serialize = serialize;
|
||||||
|
};
|
||||||
|
|
||||||
|
class P5Element extends EventEmitter {
|
||||||
|
constructor(node, stream = false, options = {}) {
|
||||||
|
super();
|
||||||
|
this.stream = stream;
|
||||||
|
this.node = node;
|
||||||
|
this.options = options;
|
||||||
|
};
|
||||||
|
setAttribute(name, value) {
|
||||||
|
for (const attr of this.attrs) {
|
||||||
|
if (attr.name === name) {
|
||||||
|
attr.value = value;
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
this.attrs.push(
|
||||||
|
{
|
||||||
|
name,
|
||||||
|
value,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
getAttribute(name) {
|
||||||
|
const attr = this.attrs.find(attr => attr.name === name) || {};
|
||||||
|
return attr.value;
|
||||||
|
};
|
||||||
|
hasAttribute(name) {
|
||||||
|
return !!this.attrs.find(attr => attr.name === name);
|
||||||
|
};
|
||||||
|
removeAttribute(name) {
|
||||||
|
const i = this.attrs.findIndex(attr => attr.name === name);
|
||||||
|
if (typeof i !== 'undefined') this.attrs.splice(i, 1);
|
||||||
|
};
|
||||||
|
get tagName() {
|
||||||
|
return this.node.tagName;
|
||||||
|
};
|
||||||
|
set tagName(val) {
|
||||||
|
this.node.tagName = val;
|
||||||
|
};
|
||||||
|
get childNodes() {
|
||||||
|
return !this.stream ? this.node.childNodes : null;
|
||||||
|
};
|
||||||
|
get innerHTML() {
|
||||||
|
return !this.stream ? serialize(
|
||||||
|
{
|
||||||
|
nodeName: '#document-fragment',
|
||||||
|
childNodes: this.childNodes,
|
||||||
|
}
|
||||||
|
) : null;
|
||||||
|
};
|
||||||
|
set innerHTML(val) {
|
||||||
|
if (!this.stream) this.node.childNodes = parseFragment(val).childNodes;
|
||||||
|
};
|
||||||
|
get outerHTML() {
|
||||||
|
return !this.stream ? serialize(
|
||||||
|
{
|
||||||
|
nodeName: '#document-fragment',
|
||||||
|
childNodes: [ this ],
|
||||||
|
}
|
||||||
|
) : null;
|
||||||
|
};
|
||||||
|
set outerHTML(val) {
|
||||||
|
if (!this.stream) this.parentNode.childNodes.splice(this.parentNode.childNodes.findIndex(node => node === this.node), 1, ...parseFragment(val).childNodes);
|
||||||
|
};
|
||||||
|
get textContent() {
|
||||||
|
if (this.stream) return null;
|
||||||
|
|
||||||
|
let str = '';
|
||||||
|
iterate(this.node, node => {
|
||||||
|
if (node.nodeName === '#text') str += node.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
return str;
|
||||||
|
};
|
||||||
|
set textContent(val) {
|
||||||
|
if (!this.stream) this.node.childNodes = [
|
||||||
|
{
|
||||||
|
nodeName: '#text',
|
||||||
|
value: val,
|
||||||
|
parentNode: this.node
|
||||||
|
}
|
||||||
|
];
|
||||||
|
};
|
||||||
|
get nodeName() {
|
||||||
|
return this.node.nodeName;
|
||||||
|
}
|
||||||
|
get parentNode() {
|
||||||
|
return this.node.parentNode ? new P5Element(this.node.parentNode) : null;
|
||||||
|
};
|
||||||
|
get attrs() {
|
||||||
|
return this.node.attrs;
|
||||||
|
}
|
||||||
|
get namespaceURI() {
|
||||||
|
return this.node.namespaceURI;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
class AttributeEvent {
|
||||||
|
constructor(node, attr, options = {}) {
|
||||||
|
this.attr = attr;
|
||||||
|
this.attrs = node.attrs;
|
||||||
|
this.node = node;
|
||||||
|
this.options = options;
|
||||||
|
};
|
||||||
|
delete() {
|
||||||
|
const i = this.attrs.findIndex(attr => attr === this.attr);
|
||||||
|
|
||||||
|
this.attrs.splice(i, 1);
|
||||||
|
|
||||||
|
Object.defineProperty(this, 'deleted', {
|
||||||
|
get: () => true,
|
||||||
|
});
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
get name() {
|
||||||
|
return this.attr.name;
|
||||||
|
};
|
||||||
|
|
||||||
|
set name(val) {
|
||||||
|
this.attr.name = val;
|
||||||
|
};
|
||||||
|
get value() {
|
||||||
|
return this.attr.value;
|
||||||
|
};
|
||||||
|
|
||||||
|
set value(val) {
|
||||||
|
this.attr.value = val;
|
||||||
|
};
|
||||||
|
get deleted() {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
class TextEvent {
|
||||||
|
constructor(node, element, stream = false, options = {}) {
|
||||||
|
this.stream = stream;
|
||||||
|
this.node = node;
|
||||||
|
this.element = element;
|
||||||
|
this.options = options;
|
||||||
|
};
|
||||||
|
get nodeName() {
|
||||||
|
return this.node.nodeName;
|
||||||
|
}
|
||||||
|
get parentNode() {
|
||||||
|
return this.element;
|
||||||
|
};
|
||||||
|
get value() {
|
||||||
|
return this.stream ? this.node.text : this.node.value;
|
||||||
|
};
|
||||||
|
set value(val) {
|
||||||
|
|
||||||
|
if (this.stream) this.node.text = val;
|
||||||
|
else this.node.value = val;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default HTML;
|
187
rewrite/index.js
Normal file
187
rewrite/index.js
Normal file
|
@ -0,0 +1,187 @@
|
||||||
|
import HTML from './html.js';
|
||||||
|
import CSS from './css.js';
|
||||||
|
import JS from './js.js';
|
||||||
|
import setCookie from 'set-cookie-parser';
|
||||||
|
import { xor, base64, plain } from './codecs.js';
|
||||||
|
import mimeTypes from './mime.js';
|
||||||
|
import { validateCookie, db, getCookies, setCookies, serialize } from './cookie.js';
|
||||||
|
import { attributes, isUrl, isForbidden, isHtml, isSrcset, isStyle, text } from './rewrite.html.js';
|
||||||
|
import { importStyle, url } from './rewrite.css.js';
|
||||||
|
//import { call, destructureDeclaration, dynamicImport, getProperty, importDeclaration, setProperty, sourceMethods, wrapEval, wrapIdentifier } from './rewrite.script.js';
|
||||||
|
import { dynamicImport, identifier, importDeclaration, property, unwrap, wrapEval } from './rewrite.script.test.js';
|
||||||
|
import { openDB } from 'idb';
|
||||||
|
import parsel from './parsel.js';
|
||||||
|
import UVClient from '../client/index.js';
|
||||||
|
|
||||||
|
const valid_chars = "!#$%&'*+-.0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ^_`abcdefghijklmnopqrstuvwxyz|~";
|
||||||
|
const reserved_chars = "%";
|
||||||
|
|
||||||
|
class Ultraviolet {
|
||||||
|
constructor(options = {}) {
|
||||||
|
this.prefix = options.prefix || '/service/';
|
||||||
|
//this.urlRegex = /^(#|about:|data:|mailto:|javascript:)/;
|
||||||
|
this.urlRegex = /^(#|about:|data:|mailto:)/
|
||||||
|
this.rewriteUrl = options.rewriteUrl || this.rewriteUrl;
|
||||||
|
this.sourceUrl = options.sourceUrl || this.sourceUrl;
|
||||||
|
this.encodeUrl = options.encodeUrl || this.encodeUrl;
|
||||||
|
this.decodeUrl = options.decodeUrl || this.decodeUrl;
|
||||||
|
this.vanilla = 'vanilla' in options ? options.vanilla : false;
|
||||||
|
this.meta = options.meta || {};
|
||||||
|
this.meta.base ||= undefined;
|
||||||
|
this.meta.origin ||= '';
|
||||||
|
this.meta.url ||= this.meta.base || '';
|
||||||
|
this.codec = Ultraviolet.codec;
|
||||||
|
this.html = new HTML(this);
|
||||||
|
this.css = new CSS(this);
|
||||||
|
this.js = new JS(this);
|
||||||
|
this.parsel = parsel;
|
||||||
|
this.openDB = this.constructor.openDB;
|
||||||
|
this.client = typeof self !== 'undefined' ? new UVClient((options.window || self)) : null;
|
||||||
|
this.master = '__uv';
|
||||||
|
this.dataPrefix = '__uv$';
|
||||||
|
this.attributePrefix = '__uv';
|
||||||
|
this.attrs = {
|
||||||
|
isUrl,
|
||||||
|
isForbidden,
|
||||||
|
isHtml,
|
||||||
|
isSrcset,
|
||||||
|
isStyle,
|
||||||
|
};
|
||||||
|
if (!this.vanilla) this.implementUVMiddleware();
|
||||||
|
this.cookie = {
|
||||||
|
validateCookie,
|
||||||
|
db: () => {
|
||||||
|
return db(this.constructor.openDB);
|
||||||
|
},
|
||||||
|
getCookies,
|
||||||
|
setCookies,
|
||||||
|
serialize,
|
||||||
|
setCookie,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
rewriteUrl(str, meta = this.meta) {
|
||||||
|
str = new String(str).trim();
|
||||||
|
if (!str || this.urlRegex.test(str)) return str;
|
||||||
|
|
||||||
|
if (str.startsWith('javascript:')) {
|
||||||
|
return 'javascript:' + this.js.rewrite(str.slice('javascript:'.length));
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
return meta.origin + this.prefix + this.encodeUrl(new URL(str, meta.base).href);
|
||||||
|
} catch(e) {
|
||||||
|
return meta.origin + this.prefix + this.encodeUrl(str);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
sourceUrl(str, meta = this.meta) {
|
||||||
|
if (!str || this.urlRegex.test(str)) return str;
|
||||||
|
try {
|
||||||
|
return new URL(
|
||||||
|
this.decodeUrl(str.slice(this.prefix.length + meta.origin.length)),
|
||||||
|
meta.base
|
||||||
|
).href;
|
||||||
|
} catch(e) {
|
||||||
|
return this.decodeUrl(str.slice(this.prefix.length + meta.origin.length));
|
||||||
|
};
|
||||||
|
};
|
||||||
|
encodeUrl(str) {
|
||||||
|
return encodeURIComponent(str);
|
||||||
|
};
|
||||||
|
decodeUrl(str) {
|
||||||
|
return decodeURIComponent(str);
|
||||||
|
};
|
||||||
|
encodeProtocol(protocol) {
|
||||||
|
protocol = protocol.toString();
|
||||||
|
|
||||||
|
let result = '';
|
||||||
|
|
||||||
|
for(let i = 0; i < protocol.length; i++){
|
||||||
|
const char = protocol[i];
|
||||||
|
|
||||||
|
if(valid_chars.includes(char) && !reserved_chars.includes(char)){
|
||||||
|
result += char;
|
||||||
|
}else{
|
||||||
|
const code = char.charCodeAt();
|
||||||
|
result += '%' + code.toString(16).padStart(2, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
decodeProtocol(protocol) {
|
||||||
|
if(typeof protocol != 'string')throw new TypeError('protocol must be a string');
|
||||||
|
|
||||||
|
let result = '';
|
||||||
|
|
||||||
|
for(let i = 0; i < protocol.length; i++){
|
||||||
|
const char = protocol[i];
|
||||||
|
|
||||||
|
if(char == '%'){
|
||||||
|
const code = parseInt(protocol.slice(i + 1, i + 3), 16);
|
||||||
|
const decoded = String.fromCharCode(code);
|
||||||
|
|
||||||
|
result += decoded;
|
||||||
|
i += 2;
|
||||||
|
}else{
|
||||||
|
result += char;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
implementUVMiddleware() {
|
||||||
|
// HTML
|
||||||
|
attributes(this);
|
||||||
|
text(this);
|
||||||
|
|
||||||
|
// CSS
|
||||||
|
url(this);
|
||||||
|
importStyle(this);
|
||||||
|
|
||||||
|
// JS
|
||||||
|
/*
|
||||||
|
getProperty(this);
|
||||||
|
call(this)
|
||||||
|
setProperty(this);
|
||||||
|
sourceMethods(this);
|
||||||
|
importDeclaration(this);
|
||||||
|
dynamicImport(this);
|
||||||
|
wrapEval(this);
|
||||||
|
wrapIdentifier(this)
|
||||||
|
*/
|
||||||
|
|
||||||
|
importDeclaration(this);
|
||||||
|
dynamicImport(this);
|
||||||
|
property(this);
|
||||||
|
wrapEval(this);
|
||||||
|
identifier(this);
|
||||||
|
unwrap(this);
|
||||||
|
|
||||||
|
//destructureDeclaration(this)
|
||||||
|
};
|
||||||
|
get rewriteHtml() {
|
||||||
|
return this.html.rewrite.bind(this.html);
|
||||||
|
};
|
||||||
|
get sourceHtml() {
|
||||||
|
return this.html.source.bind(this.html);
|
||||||
|
};
|
||||||
|
get rewriteCSS() {
|
||||||
|
return this.css.rewrite.bind(this.css);
|
||||||
|
};
|
||||||
|
get sourceCSS() {
|
||||||
|
return this.css.source.bind(this.css);
|
||||||
|
};
|
||||||
|
get rewriteJS() {
|
||||||
|
return this.js.rewrite.bind(this.js);
|
||||||
|
};
|
||||||
|
get sourceJS() {
|
||||||
|
return this.js.source.bind(this.js);
|
||||||
|
};
|
||||||
|
static codec = { xor, base64, plain };
|
||||||
|
static mime = mimeTypes;
|
||||||
|
static setCookie = setCookie;
|
||||||
|
static openDB = openDB;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Ultraviolet;
|
||||||
|
if (typeof self === 'object') self.Ultraviolet = Ultraviolet;
|
122
rewrite/js.js
Normal file
122
rewrite/js.js
Normal file
|
@ -0,0 +1,122 @@
|
||||||
|
import { parseScript } from 'meriyah';
|
||||||
|
// import { parse } from 'acorn-hammerhead';
|
||||||
|
import { generate } from 'esotope-hammerhead';
|
||||||
|
import EventEmitter from './events.js';
|
||||||
|
|
||||||
|
class JS extends EventEmitter {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
/*
|
||||||
|
this.parseOptions = {
|
||||||
|
allowReturnOutsideFunction: true,
|
||||||
|
allowImportExportEverywhere: true,
|
||||||
|
ecmaVersion: 2021,
|
||||||
|
};
|
||||||
|
*/
|
||||||
|
this.parseOptions = {
|
||||||
|
ranges: true,
|
||||||
|
module: true,
|
||||||
|
globalReturn: true,
|
||||||
|
};
|
||||||
|
this.generationOptions = {
|
||||||
|
format: {
|
||||||
|
quotes: 'double',
|
||||||
|
escapeless: true,
|
||||||
|
compact: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
this.parse = parseScript /*parse*/;
|
||||||
|
this.generate = generate;
|
||||||
|
};
|
||||||
|
rewrite(str, data = {}) {
|
||||||
|
return this.recast(str, data, 'rewrite');
|
||||||
|
};
|
||||||
|
source(str, data = {}) {
|
||||||
|
return this.recast(str, data, 'source');
|
||||||
|
};
|
||||||
|
recast(str, data = {}, type = '') {
|
||||||
|
try {
|
||||||
|
const output = [];
|
||||||
|
const ast = this.parse(str, this.parseOptions);
|
||||||
|
const meta = {
|
||||||
|
data,
|
||||||
|
changes: [],
|
||||||
|
input: str,
|
||||||
|
ast,
|
||||||
|
get slice() {
|
||||||
|
return slice;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let slice = 0;
|
||||||
|
|
||||||
|
this.iterate(ast, (node, parent = null) => {
|
||||||
|
if (parent && parent.inTransformer) node.isTransformer = true;
|
||||||
|
node.parent = parent;
|
||||||
|
|
||||||
|
this.emit(node.type, node, meta, type);
|
||||||
|
});
|
||||||
|
|
||||||
|
meta.changes.sort((a, b) => (a.start - b.start) || (a.end - b.end));
|
||||||
|
|
||||||
|
for (const change of meta.changes) {
|
||||||
|
if ('start' in change && typeof change.start === 'number') output.push(str.slice(slice, change.start));
|
||||||
|
if (change.node) output.push(typeof change.node === 'string' ? change.node : generate(change.node, this.generationOptions));
|
||||||
|
if ('end' in change && typeof change.end === 'number') slice = change.end;
|
||||||
|
};
|
||||||
|
output.push(str.slice(slice));
|
||||||
|
return output.join('');
|
||||||
|
} catch(e) {
|
||||||
|
return str;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
iterate(ast, handler) {
|
||||||
|
if (typeof ast != 'object' || !handler) return;
|
||||||
|
walk(ast, null, handler);
|
||||||
|
function walk(node, parent, handler) {
|
||||||
|
if (typeof node != 'object' || !handler) return;
|
||||||
|
handler(node, parent, handler);
|
||||||
|
for (const child in node) {
|
||||||
|
if (child === 'parent') continue;
|
||||||
|
if (Array.isArray(node[child])) {
|
||||||
|
node[child].forEach(entry => {
|
||||||
|
if (entry) walk(entry, node, handler)
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
if (node[child]) walk(node[child], node, handler);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
if (typeof node.iterateEnd === 'function') node.iterateEnd();
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
class NodeEvent extends EventEmitter {
|
||||||
|
constructor(node, parent = null) {
|
||||||
|
super();
|
||||||
|
this._node = node;
|
||||||
|
for (let key in node) {
|
||||||
|
Object.defineProperty(this, key, {
|
||||||
|
get: () => node[key],
|
||||||
|
sel: val => node[key] = val,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
this.parent = parent;
|
||||||
|
};
|
||||||
|
iterate(handler) {
|
||||||
|
for (const key in this._node) {
|
||||||
|
if (key === 'parent') continue;
|
||||||
|
if (Array.isArray(this._node[key])) {
|
||||||
|
this._node[key].forEach(entry => {
|
||||||
|
const child = new this.constructor(entry, this._node);
|
||||||
|
handler(child);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const child = new this.constructor(entry, this._node);
|
||||||
|
walk(this._node[key], this._node, handler);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default JS;
|
||||||
|
export { NodeEvent };
|
229
rewrite/legacy/css.test.js
Normal file
229
rewrite/legacy/css.test.js
Normal file
|
@ -0,0 +1,229 @@
|
||||||
|
import EventEmitter from "../events.js";
|
||||||
|
import parsel from "../parsel.js";
|
||||||
|
|
||||||
|
|
||||||
|
class CSS extends EventEmitter {
|
||||||
|
constructor(ctx) {
|
||||||
|
super();
|
||||||
|
this.regex = /(?<url>\burl\()|(?<import>@import\b)|(?<parathesis>[\(\)])|(?<trailingEscape>\\*)?(?<quote>['"])|(?<comment>\/\*|\*\/)/g;
|
||||||
|
this.ctx = ctx;
|
||||||
|
this.meta = ctx.meta;
|
||||||
|
this.parsel = parsel;
|
||||||
|
};
|
||||||
|
rewrite(str, options = this.meta) {
|
||||||
|
return this.rewriteChunk(str, options, {}, 'rewrite').output.join("");
|
||||||
|
};
|
||||||
|
rewriteSelector(str, attributes = [], list = true, prefix = '__op-attr-') {
|
||||||
|
if (!str) return str;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (list) {
|
||||||
|
const selectors = str.split(/(\s*$),(\s*$)/);
|
||||||
|
const processed = [];
|
||||||
|
|
||||||
|
|
||||||
|
for (const selector of selectors) {
|
||||||
|
processed.push(
|
||||||
|
this.rewriteSelector(selector, attributes, false, prefix)
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
return processed.join(', ');
|
||||||
|
};
|
||||||
|
let slice = 0;
|
||||||
|
const output = [];
|
||||||
|
const tokens = this.parsel.tokenize(str)
|
||||||
|
|
||||||
|
for (const token of tokens) {
|
||||||
|
if (token.type !== 'attribute') continue;
|
||||||
|
|
||||||
|
|
||||||
|
const [ start ] = token.pos;
|
||||||
|
const end = start + token.content.length;
|
||||||
|
|
||||||
|
if (attributes.includes(token.name)) {
|
||||||
|
output.push(
|
||||||
|
str.slice(slice, start)
|
||||||
|
);
|
||||||
|
|
||||||
|
output.push(
|
||||||
|
token.content.replace(token.name, prefix + token.name)
|
||||||
|
);
|
||||||
|
|
||||||
|
slice = end;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
output.push(
|
||||||
|
str.slice(slice)
|
||||||
|
);
|
||||||
|
|
||||||
|
return output.join('');
|
||||||
|
} catch(e) {
|
||||||
|
return str;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
rewriteChunk(chunk, options = this.meta, state = {}, type = 'rewrite') {
|
||||||
|
const regex = new RegExp(this.regex);
|
||||||
|
state.string ||= false;
|
||||||
|
state.quote ||= '';
|
||||||
|
state.previous ||= null;
|
||||||
|
state.url ||= false;
|
||||||
|
state.urlContent ||= '';
|
||||||
|
state.rewriteString ||= false;
|
||||||
|
state.comment ||= false;
|
||||||
|
state.stringContent ||= '';
|
||||||
|
|
||||||
|
const output = [];
|
||||||
|
let loc = 0;
|
||||||
|
let cutoff = chunk.length;
|
||||||
|
let match = null;
|
||||||
|
|
||||||
|
if (state.string || state.url) cutoff = 0;
|
||||||
|
|
||||||
|
while ((match = regex.exec(chunk)) !== null) {
|
||||||
|
const {
|
||||||
|
url,
|
||||||
|
parathesis,
|
||||||
|
quote,
|
||||||
|
trailingEscape,
|
||||||
|
comment,
|
||||||
|
} = match.groups;
|
||||||
|
|
||||||
|
if (state.comment) {
|
||||||
|
if (comment === '*/') {
|
||||||
|
state.comment = false;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (comment === '/*') {
|
||||||
|
state.comment = true;
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (state.string) {
|
||||||
|
if (quote) {
|
||||||
|
if (state.quote === quote) {
|
||||||
|
// Checks for backslashes that can escape the quote.
|
||||||
|
// Also checking for backslashes that can escape the backslash behind the quote. Unlikely to happen, but still possible.
|
||||||
|
if (trailingEscape && trailingEscape.length & 1) continue;
|
||||||
|
|
||||||
|
if (state.rewriteString) {
|
||||||
|
const string = state.stringContent + chunk.slice(cutoff, match.index);
|
||||||
|
|
||||||
|
output.push(
|
||||||
|
chunk.slice(loc, cutoff)
|
||||||
|
);
|
||||||
|
|
||||||
|
const url = new URLEvent(string, state, options, type);
|
||||||
|
this.emit('url', url);
|
||||||
|
|
||||||
|
output.push(
|
||||||
|
url.value
|
||||||
|
);
|
||||||
|
|
||||||
|
output.push(quote);
|
||||||
|
|
||||||
|
loc = regex.lastIndex;
|
||||||
|
cutoff = chunk.length;
|
||||||
|
|
||||||
|
chunk.slice(loc, cutoff);
|
||||||
|
|
||||||
|
state.rewriteString = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
state.quote = '';
|
||||||
|
state.string = false;
|
||||||
|
state.stringContent = '';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (quote) {
|
||||||
|
state.string = true;
|
||||||
|
state.quote = quote;
|
||||||
|
|
||||||
|
if (state.previous && state.previous.groups.import) {
|
||||||
|
state.rewriteString = true;
|
||||||
|
cutoff = regex.lastIndex;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
if (state.url && parathesis === ')' && !state.string) {
|
||||||
|
const string = (state.urlContent + chunk.slice(cutoff, match.index)).trim();
|
||||||
|
|
||||||
|
output.push(
|
||||||
|
chunk.slice(loc, cutoff)
|
||||||
|
);
|
||||||
|
|
||||||
|
if ((string[0] === '"' || string[0] === "'") && string[0] === string[string.length - 1]) {
|
||||||
|
output.push(string[0]);
|
||||||
|
|
||||||
|
const url = new URLEvent(string.slice(1, string.length - 1), state, options, type);
|
||||||
|
this.emit('url', url);
|
||||||
|
|
||||||
|
output.push(
|
||||||
|
url.value
|
||||||
|
);
|
||||||
|
|
||||||
|
output.push(string[0]);
|
||||||
|
} else {
|
||||||
|
const url = new URLEvent(string, state, options, type);
|
||||||
|
this.emit('url', url);
|
||||||
|
|
||||||
|
output.push(
|
||||||
|
url.value,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
output.push(parathesis);
|
||||||
|
|
||||||
|
loc = regex.lastIndex;
|
||||||
|
cutoff = chunk.length;
|
||||||
|
|
||||||
|
chunk.slice(loc, cutoff);
|
||||||
|
|
||||||
|
state.urlContent = '';
|
||||||
|
state.url = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (state.url) {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (url) {
|
||||||
|
state.url = true;
|
||||||
|
cutoff = regex.lastIndex;
|
||||||
|
};
|
||||||
|
|
||||||
|
state.previous = match;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (state.string) {
|
||||||
|
state.stringContent += chunk.slice(cutoff);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (state.url) {
|
||||||
|
state.urlContent += chunk.slice(cutoff);
|
||||||
|
};
|
||||||
|
|
||||||
|
output.push(
|
||||||
|
chunk.slice(loc, cutoff)
|
||||||
|
);
|
||||||
|
|
||||||
|
return { output, state };
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
class URLEvent {
|
||||||
|
constructor(value, state = {}, options = {}, type = 'rewrite') {
|
||||||
|
this.value = value;
|
||||||
|
this.state = state;
|
||||||
|
this.options = options;
|
||||||
|
this.type = type;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CSS;
|
9
rewrite/legacy/rewrite.css.test.js
Normal file
9
rewrite/legacy/rewrite.css.test.js
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
function url(ctx, meta = ctx.meta) {
|
||||||
|
const { css } = ctx;
|
||||||
|
|
||||||
|
css.on('url', event => {
|
||||||
|
event.value = ctx.rewriteUrl(event.value, meta);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export { url };
|
230
rewrite/legacy/rewrite.script.test.js
Normal file
230
rewrite/legacy/rewrite.script.test.js
Normal file
|
@ -0,0 +1,230 @@
|
||||||
|
import { Syntax } from 'esotope-hammerhead';
|
||||||
|
|
||||||
|
function property(ctx) {
|
||||||
|
const { js } = ctx;
|
||||||
|
js.on('MemberExpression', (node, data, type) => {
|
||||||
|
if (node.object.type === 'Super') return false;
|
||||||
|
|
||||||
|
if (type === 'rewrite' && computedProperty(node)) {
|
||||||
|
data.changes.push({
|
||||||
|
node: '__op.$wrap((',
|
||||||
|
start: node.property.start,
|
||||||
|
end: node.property.start,
|
||||||
|
})
|
||||||
|
node.iterateEnd = function() {
|
||||||
|
data.changes.push({
|
||||||
|
node: '))',
|
||||||
|
start: node.property.end,
|
||||||
|
end: node.property.end,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!node.computed && node.property.name === 'location' && type === 'rewrite' || node.property.name === '__opLocation' && type === 'source') {
|
||||||
|
data.changes.push({
|
||||||
|
start: node.property.start,
|
||||||
|
end: node.property.end,
|
||||||
|
node: type === 'rewrite' ? '__opLocation' : 'location'
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
if (node.object.type === Syntax.Identifier && node.object.name === 'eval') {
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
if (!node.computed && node.property.name === 'postMessage' && type === 'rewrite' || node.property.name === '__opPostMessage' && type === 'source') {
|
||||||
|
data.changes.push({
|
||||||
|
start: node.property.start,
|
||||||
|
end: node.property.end,
|
||||||
|
node: type === 'rewrite' ? '__opSetSource(__op).__opPostMessage' : 'postMessage'
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
if (!node.computed && node.property.name === 'eval' && type === 'rewrite' || node.property.name === '__opEval' && type === 'source') {
|
||||||
|
data.changes.push({
|
||||||
|
start: node.property.start,
|
||||||
|
end: node.property.end,
|
||||||
|
node: type === 'rewrite' ? '__opEval' : 'eval'
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!node.computed && node.property.name === '__opSetSource' && type === 'source' && node.parent.type === Syntax.CallExpression) {
|
||||||
|
const { parent, property } = node;
|
||||||
|
data.changes.push({
|
||||||
|
start: property.start - 1,
|
||||||
|
end: parent.end,
|
||||||
|
});
|
||||||
|
|
||||||
|
node.iterateEnd = function() {
|
||||||
|
data.changes.push({
|
||||||
|
start: property.start,
|
||||||
|
end: parent.end,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
function identifier(ctx) {
|
||||||
|
const { js } = ctx;
|
||||||
|
js.on('Identifier', (node, data, type) => {
|
||||||
|
if (type !== 'rewrite') return false;
|
||||||
|
const { parent } = node;
|
||||||
|
if (!['location', 'eval'].includes(node.name)) return false;
|
||||||
|
if (parent.type === Syntax.VariableDeclarator && parent.id === node) return false;
|
||||||
|
if ((parent.type === Syntax.AssignmentExpression || parent.type === Syntax.AssignmentPattern) && parent.left === node) return false;
|
||||||
|
if ((parent.type === Syntax.FunctionExpression || parent.type === Syntax.FunctionDeclaration) && parent.id === node) return false;
|
||||||
|
if (parent.type === Syntax.MemberExpression && parent.property === node && !parent.computed) return false;
|
||||||
|
if (node.name === 'eval' && parent.type === Syntax.CallExpression && parent.callee === node) return false;
|
||||||
|
if (parent.type === Syntax.Property && parent.key === node) return false;
|
||||||
|
if (parent.type === Syntax.Property && parent.value === node && parent.shorthand) return false;
|
||||||
|
if (parent.type === Syntax.UpdateExpression && (parent.operator === '++' || parent.operator === '--')) return false;
|
||||||
|
if ((parent.type === Syntax.FunctionExpression || parent.type === Syntax.FunctionDeclaration || parent.type === Syntax.ArrowFunctionExpression) && parent.params.indexOf(node) !== -1) return false;
|
||||||
|
if (parent.type === Syntax.MethodDefinition) return false;
|
||||||
|
if (parent.type === Syntax.ClassDeclaration) return false;
|
||||||
|
if (parent.type === Syntax.RestElement) return false;
|
||||||
|
if (parent.type === Syntax.ExportSpecifier) return false;
|
||||||
|
if (parent.type === Syntax.ImportSpecifier) return false;
|
||||||
|
|
||||||
|
data.changes.push({
|
||||||
|
start: node.start,
|
||||||
|
end: node.end,
|
||||||
|
node: '__op.$get(' + node.name + ')'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
function wrapEval(ctx) {
|
||||||
|
const { js } = ctx;
|
||||||
|
js.on('CallExpression', (node, data, type) => {
|
||||||
|
if (type !== 'rewrite') return false;
|
||||||
|
if (!node.arguments.length) return false;
|
||||||
|
if (node.callee.type !== 'Identifier') return false;
|
||||||
|
if (node.callee.name !== 'eval') return false;
|
||||||
|
|
||||||
|
const [ script ] = node.arguments;
|
||||||
|
|
||||||
|
data.changes.push({
|
||||||
|
node: '__op.js.rewrite(',
|
||||||
|
start: script.start,
|
||||||
|
end: script.start,
|
||||||
|
})
|
||||||
|
node.iterateEnd = function() {
|
||||||
|
data.changes.push({
|
||||||
|
node: ')',
|
||||||
|
start: script.end,
|
||||||
|
end: script.end,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
function importDeclaration(ctx) {
|
||||||
|
const { js } = ctx;
|
||||||
|
js.on(Syntax.Literal, (node, data, type) => {
|
||||||
|
if (!((node.parent.type === Syntax.ImportDeclaration || node.parent.type === Syntax.ExportAllDeclaration || node.parent.type === Syntax.ExportNamedDeclaration)
|
||||||
|
&& node.parent.source === node)) return false;
|
||||||
|
|
||||||
|
data.changes.push({
|
||||||
|
start: node.start + 1,
|
||||||
|
end: node.end - 1,
|
||||||
|
node: type === 'rewrite' ? ctx.rewriteUrl(node.value) : ctx.sourceUrl(node.value)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
function dynamicImport(ctx) {
|
||||||
|
const { js } = ctx;
|
||||||
|
js.on(Syntax.ImportExpression, (node, data, type) => {
|
||||||
|
if (type !== 'rewrite') return false;
|
||||||
|
data.changes.push({
|
||||||
|
node: '__op.rewriteUrl(',
|
||||||
|
start: node.source.start,
|
||||||
|
end: node.source.start,
|
||||||
|
})
|
||||||
|
node.iterateEnd = function() {
|
||||||
|
data.changes.push({
|
||||||
|
node: ')',
|
||||||
|
start: node.source.end,
|
||||||
|
end: node.source.end,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
function unwrap(ctx) {
|
||||||
|
const { js } = ctx;
|
||||||
|
js.on('CallExpression', (node, data, type) => {
|
||||||
|
if (type !== 'source') return false;
|
||||||
|
if (!isWrapped(node.callee)) return false;
|
||||||
|
|
||||||
|
switch(node.callee.property.name) {
|
||||||
|
case '$wrap':
|
||||||
|
if (!node.arguments || node.parent.type !== Syntax.MemberExpression || node.parent.property !== node) return false;
|
||||||
|
const [ property ] = node.arguments;
|
||||||
|
|
||||||
|
data.changes.push({
|
||||||
|
start: node.callee.start,
|
||||||
|
end: property.start,
|
||||||
|
});
|
||||||
|
|
||||||
|
node.iterateEnd = function() {
|
||||||
|
data.changes.push({
|
||||||
|
start: node.end - 2,
|
||||||
|
end: node.end,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
case '$get':
|
||||||
|
case 'rewriteUrl':
|
||||||
|
const [ arg ] = node.arguments;
|
||||||
|
|
||||||
|
data.changes.push({
|
||||||
|
start: node.callee.start,
|
||||||
|
end: arg.start,
|
||||||
|
});
|
||||||
|
|
||||||
|
node.iterateEnd = function() {
|
||||||
|
data.changes.push({
|
||||||
|
start: node.end - 1,
|
||||||
|
end: node.end,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
case 'rewrite':
|
||||||
|
const [ script ] = node.arguments;
|
||||||
|
data.changes.push({
|
||||||
|
start: node.callee.start,
|
||||||
|
end: script.start,
|
||||||
|
});
|
||||||
|
node.iterateEnd = function() {
|
||||||
|
data.changes.push({
|
||||||
|
start: node.end - 1,
|
||||||
|
end: node.end,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
function isWrapped(node) {
|
||||||
|
if (node.type !== Syntax.MemberExpression) return false;
|
||||||
|
if (node.property.name === 'rewrite' && isWrapped(node.object)) return true;
|
||||||
|
if (node.object.type !== Syntax.Identifier || node.object.name !== '__op') return false;
|
||||||
|
if (!['js', '$get', '$wrap', 'rewriteUrl'].includes(node.property.name)) return false;
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
function computedProperty(parent) {
|
||||||
|
if (!parent.computed) return false;
|
||||||
|
const { property: node } = parent;
|
||||||
|
if (node.type === 'Literal' && node.value !== 'location') return false;
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export { property, wrapEval, dynamicImport, importDeclaration, identifier, unwrap };
|
198
rewrite/mime.js
Normal file
198
rewrite/mime.js
Normal file
|
@ -0,0 +1,198 @@
|
||||||
|
/*!
|
||||||
|
* mime-types
|
||||||
|
* Copyright(c) 2014 Jonathan Ong
|
||||||
|
* Copyright(c) 2015 Douglas Christopher Wilson
|
||||||
|
* MIT Licensed
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use strict'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Module dependencies.
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
|
||||||
|
var $exports = {}
|
||||||
|
|
||||||
|
import db from "mime-db";
|
||||||
|
|
||||||
|
var extname = function(path = '') {
|
||||||
|
if (!path.includes('.')) return '';
|
||||||
|
const map = path.split('.');
|
||||||
|
|
||||||
|
return '.' + map[map.length - 1];
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Module variables.
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
|
||||||
|
var EXTRACT_TYPE_REGEXP = /^\s*([^;\s]*)(?:;|\s|$)/
|
||||||
|
var TEXT_TYPE_REGEXP = /^text\//i
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Module exports.
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
|
||||||
|
$exports.charset = charset
|
||||||
|
$exports.charsets = { lookup: charset }
|
||||||
|
$exports.contentType = contentType
|
||||||
|
$exports.extension = extension
|
||||||
|
$exports.extensions = Object.create(null)
|
||||||
|
$exports.lookup = lookup
|
||||||
|
$exports.types = Object.create(null)
|
||||||
|
|
||||||
|
// Populate the extensions/types maps
|
||||||
|
populateMaps($exports.extensions, $exports.types)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the default charset for a MIME type.
|
||||||
|
*
|
||||||
|
* @param {string} type
|
||||||
|
* @return {boolean|string}
|
||||||
|
*/
|
||||||
|
|
||||||
|
function charset (type) {
|
||||||
|
if (!type || typeof type !== 'string') {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: use media-typer
|
||||||
|
var match = EXTRACT_TYPE_REGEXP.exec(type)
|
||||||
|
var mime = match && db[match[1].toLowerCase()]
|
||||||
|
|
||||||
|
if (mime && mime.charset) {
|
||||||
|
return mime.charset
|
||||||
|
}
|
||||||
|
|
||||||
|
// default text/* to utf-8
|
||||||
|
if (match && TEXT_TYPE_REGEXP.test(match[1])) {
|
||||||
|
return 'UTF-8'
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a full Content-Type header given a MIME type or extension.
|
||||||
|
*
|
||||||
|
* @param {string} str
|
||||||
|
* @return {boolean|string}
|
||||||
|
*/
|
||||||
|
|
||||||
|
function contentType (str) {
|
||||||
|
// TODO: should this even be in this module?
|
||||||
|
if (!str || typeof str !== 'string') {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
var mime = str.indexOf('/') === -1
|
||||||
|
? $exports.lookup(str)
|
||||||
|
: str
|
||||||
|
|
||||||
|
if (!mime) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: use content-type or other module
|
||||||
|
if (mime.indexOf('charset') === -1) {
|
||||||
|
var charset = $exports.charset(mime)
|
||||||
|
if (charset) mime += '; charset=' + charset.toLowerCase()
|
||||||
|
}
|
||||||
|
|
||||||
|
return mime
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the default extension for a MIME type.
|
||||||
|
*
|
||||||
|
* @param {string} type
|
||||||
|
* @return {boolean|string}
|
||||||
|
*/
|
||||||
|
|
||||||
|
function extension (type) {
|
||||||
|
if (!type || typeof type !== 'string') {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: use media-typer
|
||||||
|
var match = EXTRACT_TYPE_REGEXP.exec(type)
|
||||||
|
|
||||||
|
// get extensions
|
||||||
|
var exts = match && $exports.extensions[match[1].toLowerCase()]
|
||||||
|
|
||||||
|
if (!exts || !exts.length) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return exts[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lookup the MIME type for a file path/extension.
|
||||||
|
*
|
||||||
|
* @param {string} path
|
||||||
|
* @return {boolean|string}
|
||||||
|
*/
|
||||||
|
|
||||||
|
function lookup (path) {
|
||||||
|
if (!path || typeof path !== 'string') {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// get the extension ("ext" or ".ext" or full path)
|
||||||
|
var extension = extname('x.' + path)
|
||||||
|
.toLowerCase()
|
||||||
|
.substr(1)
|
||||||
|
|
||||||
|
if (!extension) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return $exports.types[extension] || false
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Populate the extensions and types maps.
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
|
||||||
|
function populateMaps (extensions, types) {
|
||||||
|
// source preference (least -> most)
|
||||||
|
var preference = ['nginx', 'apache', undefined, 'iana']
|
||||||
|
|
||||||
|
Object.keys(db).forEach(function forEachMimeType (type) {
|
||||||
|
var mime = db[type]
|
||||||
|
var exts = mime.extensions
|
||||||
|
|
||||||
|
if (!exts || !exts.length) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// mime -> extensions
|
||||||
|
extensions[type] = exts
|
||||||
|
|
||||||
|
// extension -> mime
|
||||||
|
for (var i = 0; i < exts.length; i++) {
|
||||||
|
var extension = exts[i]
|
||||||
|
|
||||||
|
if (types[extension]) {
|
||||||
|
var from = preference.indexOf(db[types[extension]].source)
|
||||||
|
var to = preference.indexOf(mime.source)
|
||||||
|
|
||||||
|
if (types[extension] !== 'application/octet-stream' &&
|
||||||
|
(from > to || (from === to && types[extension].substr(0, 12) === 'application/'))) {
|
||||||
|
// skip the remapping
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// set the extension -> mime
|
||||||
|
types[extension] = type
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export default $exports;
|
382
rewrite/parsel.js
Normal file
382
rewrite/parsel.js
Normal file
|
@ -0,0 +1,382 @@
|
||||||
|
export default (function (exports) {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const TOKENS = {
|
||||||
|
attribute: /\[\s*(?:(?<namespace>\*|[-\w]*)\|)?(?<name>[-\w\u{0080}-\u{FFFF}]+)\s*(?:(?<operator>\W?=)\s*(?<value>.+?)\s*(?<caseSensitive>[iIsS])?\s*)?\]/gu,
|
||||||
|
id: /#(?<name>(?:[-\w\u{0080}-\u{FFFF}]|\\.)+)/gu,
|
||||||
|
class: /\.(?<name>(?:[-\w\u{0080}-\u{FFFF}]|\\.)+)/gu,
|
||||||
|
comma: /\s*,\s*/g, // must be before combinator
|
||||||
|
combinator: /\s*[\s>+~]\s*/g, // this must be after attribute
|
||||||
|
"pseudo-element": /::(?<name>[-\w\u{0080}-\u{FFFF}]+)(?:\((?<argument>¶+)\))?/gu, // this must be before pseudo-class
|
||||||
|
"pseudo-class": /:(?<name>[-\w\u{0080}-\u{FFFF}]+)(?:\((?<argument>¶+)\))?/gu,
|
||||||
|
type: /(?:(?<namespace>\*|[-\w]*)\|)?(?<name>[-\w\u{0080}-\u{FFFF}]+)|\*/gu // this must be last
|
||||||
|
};
|
||||||
|
|
||||||
|
const TOKENS_WITH_PARENS = new Set(["pseudo-class", "pseudo-element"]);
|
||||||
|
const TOKENS_WITH_STRINGS = new Set([...TOKENS_WITH_PARENS, "attribute"]);
|
||||||
|
const TRIM_TOKENS = new Set(["combinator", "comma"]);
|
||||||
|
const RECURSIVE_PSEUDO_CLASSES = new Set(["not", "is", "where", "has", "matches", "-moz-any", "-webkit-any", "nth-child", "nth-last-child"]);
|
||||||
|
|
||||||
|
const RECURSIVE_PSEUDO_CLASSES_ARGS = {
|
||||||
|
"nth-child": /(?<index>[\dn+-]+)\s+of\s+(?<subtree>.+)/
|
||||||
|
};
|
||||||
|
|
||||||
|
RECURSIVE_PSEUDO_CLASSES["nth-last-child"] = RECURSIVE_PSEUDO_CLASSES_ARGS["nth-child"];
|
||||||
|
|
||||||
|
const TOKENS_FOR_RESTORE = Object.assign({}, TOKENS);
|
||||||
|
TOKENS_FOR_RESTORE["pseudo-element"] = RegExp(TOKENS["pseudo-element"].source.replace("(?<argument>¶+)", "(?<argument>.+?)"), "gu");
|
||||||
|
TOKENS_FOR_RESTORE["pseudo-class"] = RegExp(TOKENS["pseudo-class"].source.replace("(?<argument>¶+)", "(?<argument>.+)"), "gu");
|
||||||
|
|
||||||
|
function gobbleParens(text, i) {
|
||||||
|
let str = "", stack = [];
|
||||||
|
|
||||||
|
for (; i < text.length; i++) {
|
||||||
|
let char = text[i];
|
||||||
|
|
||||||
|
if (char === "(") {
|
||||||
|
stack.push(char);
|
||||||
|
}
|
||||||
|
else if (char === ")") {
|
||||||
|
if (stack.length > 0) {
|
||||||
|
stack.pop();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
throw new Error("Closing paren without opening paren at " + i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
str += char;
|
||||||
|
|
||||||
|
if (stack.length === 0) {
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error("Opening paren without closing paren");
|
||||||
|
}
|
||||||
|
|
||||||
|
function tokenizeBy (text, grammar) {
|
||||||
|
if (!text) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
var strarr = [text];
|
||||||
|
|
||||||
|
for (var token in grammar) {
|
||||||
|
let pattern = grammar[token];
|
||||||
|
|
||||||
|
for (var i=0; i < strarr.length; i++) { // Don’t cache length as it changes during the loop
|
||||||
|
var str = strarr[i];
|
||||||
|
|
||||||
|
if (typeof str === "string") {
|
||||||
|
pattern.lastIndex = 0;
|
||||||
|
|
||||||
|
var match = pattern.exec(str);
|
||||||
|
|
||||||
|
if (match) {
|
||||||
|
let from = match.index - 1;
|
||||||
|
let args = [];
|
||||||
|
let content = match[0];
|
||||||
|
|
||||||
|
let before = str.slice(0, from + 1);
|
||||||
|
if (before) {
|
||||||
|
args.push(before);
|
||||||
|
}
|
||||||
|
|
||||||
|
args.push({
|
||||||
|
type: token,
|
||||||
|
content,
|
||||||
|
...match.groups
|
||||||
|
});
|
||||||
|
|
||||||
|
let after = str.slice(from + content.length + 1);
|
||||||
|
if (after) {
|
||||||
|
args.push(after);
|
||||||
|
}
|
||||||
|
|
||||||
|
strarr.splice(i, 1, ...args);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let offset = 0;
|
||||||
|
for (let i=0; i<strarr.length; i++) {
|
||||||
|
let token = strarr[i];
|
||||||
|
let length = token.length || token.content.length;
|
||||||
|
|
||||||
|
if (typeof token === "object") {
|
||||||
|
token.pos = [offset, offset + length];
|
||||||
|
|
||||||
|
if (TRIM_TOKENS.has(token.type)) {
|
||||||
|
token.content = token.content.trim() || " ";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
offset += length;
|
||||||
|
}
|
||||||
|
|
||||||
|
return strarr;
|
||||||
|
}
|
||||||
|
|
||||||
|
function tokenize (selector) {
|
||||||
|
if (!selector) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
selector = selector.trim(); // prevent leading/trailing whitespace be interpreted as combinators
|
||||||
|
|
||||||
|
// Replace strings with whitespace strings (to preserve offsets)
|
||||||
|
let strings = [];
|
||||||
|
// FIXME Does not account for escaped backslashes before a quote
|
||||||
|
selector = selector.replace(/(['"])(\\\1|.)+?\1/g, (str, quote, content, start) => {
|
||||||
|
strings.push({str, start});
|
||||||
|
return quote + "§".repeat(content.length) + quote;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Now that strings are out of the way, extract parens and replace them with parens with whitespace (to preserve offsets)
|
||||||
|
let parens = [], offset = 0, start;
|
||||||
|
while ((start = selector.indexOf("(", offset)) > -1) {
|
||||||
|
let str = gobbleParens(selector, start);
|
||||||
|
parens.push({str, start});
|
||||||
|
selector = selector.substring(0, start) + "(" + "¶".repeat(str.length - 2) + ")" + selector.substring(start + str.length);
|
||||||
|
offset = start + str.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now we have no nested structures and we can parse with regexes
|
||||||
|
let tokens = tokenizeBy(selector, TOKENS);
|
||||||
|
|
||||||
|
// Now restore parens and strings in reverse order
|
||||||
|
function restoreNested(strings, regex, types) {
|
||||||
|
for (let str of strings) {
|
||||||
|
for (let token of tokens) {
|
||||||
|
if (types.has(token.type) && token.pos[0] < str.start && str.start < token.pos[1]) {
|
||||||
|
let content = token.content;
|
||||||
|
token.content = token.content.replace(regex, str.str);
|
||||||
|
|
||||||
|
if (token.content !== content) { // actually changed?
|
||||||
|
// Re-evaluate groups
|
||||||
|
TOKENS_FOR_RESTORE[token.type].lastIndex = 0;
|
||||||
|
let match = TOKENS_FOR_RESTORE[token.type].exec(token.content);
|
||||||
|
let groups = match.groups;
|
||||||
|
Object.assign(token, groups);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
restoreNested(parens, /\(¶+\)/, TOKENS_WITH_PARENS);
|
||||||
|
restoreNested(strings, /(['"])§+?\1/, TOKENS_WITH_STRINGS);
|
||||||
|
|
||||||
|
return tokens;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert a flat list of tokens into a tree of complex & compound selectors
|
||||||
|
function nestTokens(tokens, {list = true} = {}) {
|
||||||
|
if (list && tokens.find(t => t.type === "comma")) {
|
||||||
|
let selectors = [], temp = [];
|
||||||
|
|
||||||
|
for (let i=0; i<tokens.length; i++) {
|
||||||
|
if (tokens[i].type === "comma") {
|
||||||
|
if (temp.length === 0) {
|
||||||
|
throw new Error("Incorrect comma at " + i);
|
||||||
|
}
|
||||||
|
|
||||||
|
selectors.push(nestTokens(temp, {list: false}));
|
||||||
|
temp.length = 0;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
temp.push(tokens[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (temp.length === 0) {
|
||||||
|
throw new Error("Trailing comma");
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
selectors.push(nestTokens(temp, {list: false}));
|
||||||
|
}
|
||||||
|
|
||||||
|
return { type: "list", list: selectors };
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i=tokens.length - 1; i>=0; i--) {
|
||||||
|
let token = tokens[i];
|
||||||
|
|
||||||
|
if (token.type === "combinator") {
|
||||||
|
let left = tokens.slice(0, i);
|
||||||
|
let right = tokens.slice(i + 1);
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: "complex",
|
||||||
|
combinator: token.content,
|
||||||
|
left: nestTokens(left),
|
||||||
|
right: nestTokens(right)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tokens.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we're here, there are no combinators, so it's just a list
|
||||||
|
return tokens.length === 1? tokens[0] : {
|
||||||
|
type: "compound",
|
||||||
|
list: [...tokens] // clone to avoid pointers messing up the AST
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Traverse an AST (or part thereof), in depth-first order
|
||||||
|
function walk(node, callback, o, parent) {
|
||||||
|
if (!node) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node.type === "complex") {
|
||||||
|
walk(node.left, callback, o, node);
|
||||||
|
walk(node.right, callback, o, node);
|
||||||
|
}
|
||||||
|
else if (node.type === "compound") {
|
||||||
|
for (let n of node.list) {
|
||||||
|
walk(n, callback, o, node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (node.subtree && o && o.subtree) {
|
||||||
|
walk(node.subtree, callback, o, node);
|
||||||
|
}
|
||||||
|
|
||||||
|
callback(node, parent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse a CSS selector
|
||||||
|
* @param selector {String} The selector to parse
|
||||||
|
* @param options.recursive {Boolean} Whether to parse the arguments of pseudo-classes like :is(), :has() etc. Defaults to true.
|
||||||
|
* @param options.list {Boolean} Whether this can be a selector list (A, B, C etc). Defaults to true.
|
||||||
|
*/
|
||||||
|
function parse(selector, {recursive = true, list = true} = {}) {
|
||||||
|
let tokens = tokenize(selector);
|
||||||
|
|
||||||
|
if (!tokens) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let ast = nestTokens(tokens, {list});
|
||||||
|
|
||||||
|
if (recursive) {
|
||||||
|
walk(ast, node => {
|
||||||
|
if (node.type === "pseudo-class" && node.argument) {
|
||||||
|
if (RECURSIVE_PSEUDO_CLASSES.has(node.name)) {
|
||||||
|
let argument = node.argument;
|
||||||
|
const childArg = RECURSIVE_PSEUDO_CLASSES_ARGS[node.name];
|
||||||
|
if (childArg) {
|
||||||
|
const match = childArg.exec(argument);
|
||||||
|
if (!match) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.assign(node, match.groups);
|
||||||
|
argument = match.groups.subtree;
|
||||||
|
}
|
||||||
|
if (argument) {
|
||||||
|
node.subtree = parse(argument, {recursive: true, list: true});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return ast;
|
||||||
|
}
|
||||||
|
|
||||||
|
function specificityToNumber(specificity, base) {
|
||||||
|
base = base || Math.max(...specificity) + 1;
|
||||||
|
|
||||||
|
return specificity[0] * base ** 2 + specificity[1] * base + specificity[2];
|
||||||
|
}
|
||||||
|
|
||||||
|
function maxIndexOf(arr) {
|
||||||
|
let max = arr[0], ret = 0;
|
||||||
|
|
||||||
|
for (let i=0; i<arr.length; i++) {
|
||||||
|
if (arr[i] > max) {
|
||||||
|
ret = i;
|
||||||
|
max = arr[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return arr.length === 0? -1 : ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate specificity of a selector.
|
||||||
|
* If the selector is a list, the max specificity is returned.
|
||||||
|
*/
|
||||||
|
function specificity(selector, {format = "array"} = {}) {
|
||||||
|
let ast = typeof selector === "object"? selector : parse(selector, {recursive: true});
|
||||||
|
|
||||||
|
if (!ast) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ast.type === "list") {
|
||||||
|
// Return max specificity
|
||||||
|
let base = 10;
|
||||||
|
let specificities = ast.list.map(s => {
|
||||||
|
let sp = specificity(s);
|
||||||
|
base = Math.max(base, ...sp);
|
||||||
|
return sp;
|
||||||
|
});
|
||||||
|
let numbers = specificities.map(s => specificityToNumber(s, base));
|
||||||
|
let i = maxIndexOf(numbers);
|
||||||
|
return specificities[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
let ret = [0, 0, 0];
|
||||||
|
|
||||||
|
walk(ast, node => {
|
||||||
|
if (node.type === "id") {
|
||||||
|
ret[0]++;
|
||||||
|
}
|
||||||
|
else if (node.type === "class" || node.type === "attribute") {
|
||||||
|
ret[1]++;
|
||||||
|
}
|
||||||
|
else if ((node.type === "type" && node.content !== "*") || node.type === "pseudo-element") {
|
||||||
|
ret[2]++;
|
||||||
|
}
|
||||||
|
else if (node.type === "pseudo-class" && node.name !== "where") {
|
||||||
|
if (RECURSIVE_PSEUDO_CLASSES.has(node.name) && node.subtree) {
|
||||||
|
// Max of argument list
|
||||||
|
let sub = specificity(node.subtree);
|
||||||
|
sub.forEach((s, i) => ret[i] += s);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
ret[1]++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.RECURSIVE_PSEUDO_CLASSES = RECURSIVE_PSEUDO_CLASSES;
|
||||||
|
exports.RECURSIVE_PSEUDO_CLASSES_ARGS = RECURSIVE_PSEUDO_CLASSES_ARGS;
|
||||||
|
exports.TOKENS = TOKENS;
|
||||||
|
exports.TRIM_TOKENS = TRIM_TOKENS;
|
||||||
|
exports.gobbleParens = gobbleParens;
|
||||||
|
exports.nestTokens = nestTokens;
|
||||||
|
exports.parse = parse;
|
||||||
|
exports.specificity = specificity;
|
||||||
|
exports.specificityToNumber = specificityToNumber;
|
||||||
|
exports.tokenize = tokenize;
|
||||||
|
exports.tokenizeBy = tokenizeBy;
|
||||||
|
exports.walk = walk;
|
||||||
|
|
||||||
|
Object.defineProperty(exports, '__esModule', { value: true });
|
||||||
|
|
||||||
|
return exports;
|
||||||
|
|
||||||
|
}({}));
|
20
rewrite/rewrite.css.js
Normal file
20
rewrite/rewrite.css.js
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
function url(ctx) {
|
||||||
|
const { css } = ctx;
|
||||||
|
css.on('Url', (node, data, type) => {
|
||||||
|
node.value = type === 'rewrite' ? ctx.rewriteUrl(node.value) : ctx.sourceUrl(node.value);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
function importStyle(ctx) {
|
||||||
|
const { css } = ctx;
|
||||||
|
css.on('Atrule', (node, data, type) => {
|
||||||
|
if (node.name !== 'import') return false;
|
||||||
|
const { data: url } = node.prelude.children.head;
|
||||||
|
// Already handling Url's
|
||||||
|
if (url.type === 'Url') return false;
|
||||||
|
url.value = type === 'rewrite' ? ctx.rewriteUrl(url.value) : ctx.sourceUrl(url.value);
|
||||||
|
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export { url, importStyle };
|
174
rewrite/rewrite.html.js
Normal file
174
rewrite/rewrite.html.js
Normal file
|
@ -0,0 +1,174 @@
|
||||||
|
function attributes(ctx, meta = ctx.meta) {
|
||||||
|
const { html, js, css, attributePrefix } = ctx;
|
||||||
|
const origPrefix = attributePrefix + '-attr-';
|
||||||
|
|
||||||
|
html.on('attr', (attr, type) => {
|
||||||
|
if (attr.node.tagName === 'base' && attr.name === 'href' && attr.options.document) {
|
||||||
|
meta.base = new URL(attr.value, meta.url);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (type === 'rewrite' && isUrl(attr.name, attr.tagName)) {
|
||||||
|
attr.node.setAttribute(origPrefix + attr.name, attr.value);
|
||||||
|
attr.value = ctx.rewriteUrl(attr.value, meta);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (type === 'rewrite' && isSrcset(attr.name)) {
|
||||||
|
attr.node.setAttribute(origPrefix + attr.name, attr.value);
|
||||||
|
attr.value = html.wrapSrcset(attr.value, meta);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
if (type === 'rewrite' && isHtml(attr.name)) {
|
||||||
|
attr.node.setAttribute(origPrefix + attr.name, attr.value);
|
||||||
|
attr.value = html.rewrite(attr.value, { ...meta, document: true });
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
if (type === 'rewrite' && isStyle(attr.name)) {
|
||||||
|
attr.node.setAttribute(origPrefix + attr.name, attr.value);
|
||||||
|
attr.value = ctx.rewriteCSS(attr.value, { context: 'declarationList', });
|
||||||
|
};
|
||||||
|
|
||||||
|
if (type === 'rewrite' && isForbidden(attr.name)) {
|
||||||
|
attr.name = origPrefix + attr.name;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (type === 'rewrite' && isEvent(attr.name)) {
|
||||||
|
attr.node.setAttribute(origPrefix + attr.name, attr.value);
|
||||||
|
attr.value = js.rewrite(attr.value, meta);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (type === 'source' && attr.name.startsWith(origPrefix)) {
|
||||||
|
if (attr.node.hasAttribute(attr.name.slice(origPrefix.length))) attr.node.removeAttribute(attr.name.slice(origPrefix.length));
|
||||||
|
attr.name = attr.name.slice(origPrefix.length);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
if (isHtml(attr.name)) {
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isStyle(attr.name)) {
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isSrcset(attr.name)) {
|
||||||
|
|
||||||
|
};
|
||||||
|
*/
|
||||||
|
});
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
function text(ctx, meta = ctx.meta) {
|
||||||
|
const { html, js, css, attributePrefix } = ctx;
|
||||||
|
|
||||||
|
html.on('text', (text, type) => {
|
||||||
|
if (text.element.tagName === 'script') {
|
||||||
|
text.value = type === 'rewrite' ? js.rewrite(text.value) : js.source(text.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (text.element.tagName === 'style') {
|
||||||
|
text.value = type === 'rewrite' ? css.rewrite(text.value) : css.source(text.value);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
function isUrl(name, tag) {
|
||||||
|
return tag === 'object' && name === 'data' || ['src', 'href', 'ping', 'movie', 'action', 'poster', 'profile', 'background'].indexOf(name) > -1;
|
||||||
|
};
|
||||||
|
function isEvent(name) {
|
||||||
|
return [
|
||||||
|
'onafterprint',
|
||||||
|
'onbeforeprint',
|
||||||
|
'onbeforeunload',
|
||||||
|
'onerror',
|
||||||
|
'onhashchange',
|
||||||
|
'onload',
|
||||||
|
'onmessage',
|
||||||
|
'onoffline',
|
||||||
|
'ononline',
|
||||||
|
'onpagehide',
|
||||||
|
'onpopstate',
|
||||||
|
'onstorage',
|
||||||
|
'onunload',
|
||||||
|
'onblur',
|
||||||
|
'onchange',
|
||||||
|
'oncontextmenu',
|
||||||
|
'onfocus',
|
||||||
|
'oninput',
|
||||||
|
'oninvalid',
|
||||||
|
'onreset',
|
||||||
|
'onsearch',
|
||||||
|
'onselect',
|
||||||
|
'onsubmit',
|
||||||
|
'onkeydown',
|
||||||
|
'onkeypress',
|
||||||
|
'onkeyup',
|
||||||
|
'onclick',
|
||||||
|
'ondblclick',
|
||||||
|
'onmousedown',
|
||||||
|
'onmousemove',
|
||||||
|
'onmouseout',
|
||||||
|
'onmouseover',
|
||||||
|
'onmouseup',
|
||||||
|
'onmousewheel',
|
||||||
|
'onwheel',
|
||||||
|
'ondrag',
|
||||||
|
'ondragend',
|
||||||
|
'ondragenter',
|
||||||
|
'ondragleave',
|
||||||
|
'ondragover',
|
||||||
|
'ondragstart',
|
||||||
|
'ondrop',
|
||||||
|
'onscroll',
|
||||||
|
'oncopy',
|
||||||
|
'oncut',
|
||||||
|
'onpaste',
|
||||||
|
'onabort',
|
||||||
|
'oncanplay',
|
||||||
|
'oncanplaythrough',
|
||||||
|
'oncuechange',
|
||||||
|
'ondurationchange',
|
||||||
|
'onemptied',
|
||||||
|
'onended',
|
||||||
|
'onerror',
|
||||||
|
'onloadeddata',
|
||||||
|
'onloadedmetadata',
|
||||||
|
'onloadstart',
|
||||||
|
'onpause',
|
||||||
|
'onplay',
|
||||||
|
'onplaying',
|
||||||
|
'onprogress',
|
||||||
|
'onratechange',
|
||||||
|
'onseeked',
|
||||||
|
'onseeking',
|
||||||
|
'onstalled',
|
||||||
|
'onsuspend',
|
||||||
|
'ontimeupdate',
|
||||||
|
'onvolumechange',
|
||||||
|
'onwaiting',
|
||||||
|
].indexOf(name) > -1;
|
||||||
|
};
|
||||||
|
|
||||||
|
function isForbidden(name) {
|
||||||
|
return ['http-equiv', 'integrity', 'sandbox', 'nonce', 'crossorigin'].indexOf(name) > -1;
|
||||||
|
};
|
||||||
|
|
||||||
|
function isHtml(name){
|
||||||
|
return name === 'srcdoc';
|
||||||
|
};
|
||||||
|
|
||||||
|
function isStyle(name) {
|
||||||
|
return name === 'style';
|
||||||
|
};
|
||||||
|
|
||||||
|
function isSrcset(name) {
|
||||||
|
return name === 'srcset' || name === 'imagesrcset';
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export { attributes, text, isUrl, isEvent, isForbidden, isHtml, isStyle, isSrcset };
|
474
rewrite/rewrite.script.js
Normal file
474
rewrite/rewrite.script.js
Normal file
|
@ -0,0 +1,474 @@
|
||||||
|
import { Syntax } from 'esotope-hammerhead';
|
||||||
|
const master = '__uv';
|
||||||
|
const methodPrefix = '__uv$';
|
||||||
|
const uvMethods = {
|
||||||
|
get: methodPrefix + 'get',
|
||||||
|
proxy: methodPrefix + 'proxy',
|
||||||
|
call: methodPrefix + 'call',
|
||||||
|
set: methodPrefix + 'set',
|
||||||
|
script: methodPrefix + 'script',
|
||||||
|
url: methodPrefix + 'url',
|
||||||
|
object: methodPrefix + 'obj'
|
||||||
|
};
|
||||||
|
const uvMethodTypes = {
|
||||||
|
[methodPrefix + 'get']: 'get',
|
||||||
|
[methodPrefix + 'proxy']: 'proxy',
|
||||||
|
[methodPrefix + 'call']: 'call',
|
||||||
|
[methodPrefix + 'set']: 'set',
|
||||||
|
[methodPrefix + 'script']: 'script',
|
||||||
|
[methodPrefix + 'url']: 'url',
|
||||||
|
[methodPrefix + 'obj']: 'object'
|
||||||
|
};
|
||||||
|
const shortHandAssignment = {
|
||||||
|
'+=': '+',
|
||||||
|
'-=': '-',
|
||||||
|
'*=': '*',
|
||||||
|
'/=': '/',
|
||||||
|
'%=': '%',
|
||||||
|
'**=': '**',
|
||||||
|
'<<=': '<<',
|
||||||
|
'>>=': '>>',
|
||||||
|
'>>>=': '>>>',
|
||||||
|
'&=': '&',
|
||||||
|
'^=': '^',
|
||||||
|
'|=': '|',
|
||||||
|
};
|
||||||
|
const assignmentOperators = ['=', '+=', '-=', '*=', '/=', '%=', '**=', '<<=', '>>=', '>>>=', '&=', '^=', '|='];
|
||||||
|
|
||||||
|
|
||||||
|
function getProperty(ctx) {
|
||||||
|
const { js } = ctx;
|
||||||
|
js.on(Syntax.MemberExpression, (node, data, type) => {
|
||||||
|
if (type !== 'rewrite') return false;
|
||||||
|
if (node.object.type === Syntax.Super)
|
||||||
|
return false;
|
||||||
|
if (node.parent.type === Syntax.AssignmentExpression && node.parent.left === node)
|
||||||
|
return false;
|
||||||
|
if (node.parent.type === Syntax.CallExpression && node.parent.callee === node)
|
||||||
|
return false;
|
||||||
|
if (node.parent.type === Syntax.UnaryExpression && node.parent.operator === 'delete')
|
||||||
|
return false;
|
||||||
|
if (node.parent.type === Syntax.UpdateExpression && (node.parent.operator === '++' || parent.operator === '--'))
|
||||||
|
return false;
|
||||||
|
if (node.parent.type === Syntax.NewExpression && node.parent.callee === node)
|
||||||
|
return false;
|
||||||
|
if (node.parent.type === Syntax.ForInStatement && node.parent.left === node) return false;
|
||||||
|
if (node.computed && node.property.type === Syntax.Literal && !shouldWrapProperty(node.property.value))
|
||||||
|
return false;
|
||||||
|
if (!node.computed && node.property.type === Syntax.Identifier && !shouldWrapProperty(node.property.name))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
data.changes.push({
|
||||||
|
node: `${uvMethods.get}((`,
|
||||||
|
start: node.start,
|
||||||
|
end: node.object.start,
|
||||||
|
})
|
||||||
|
|
||||||
|
node.object.iterateEnd = function () {
|
||||||
|
data.changes.push({
|
||||||
|
start: node.object.end,
|
||||||
|
end: node.property.start,
|
||||||
|
});
|
||||||
|
|
||||||
|
data.changes.push({
|
||||||
|
node: '), ('
|
||||||
|
});
|
||||||
|
|
||||||
|
if (node.computed) {
|
||||||
|
node.property.iterateEnd = function () {
|
||||||
|
data.changes.push({
|
||||||
|
start: node.property.end,
|
||||||
|
end: node.end,
|
||||||
|
node: `), ${master}, true)`
|
||||||
|
});
|
||||||
|
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
data.changes.push({
|
||||||
|
end: node.end,
|
||||||
|
node: '"' + node.property.name + `"), ${master}, false)`
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
};
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
function call(ctx) {
|
||||||
|
const { js } = ctx;
|
||||||
|
js.on(Syntax.CallExpression, (node, data, type) => {
|
||||||
|
if (type !== 'rewrite') return false;
|
||||||
|
if (node.callee.type !== Syntax.MemberExpression)
|
||||||
|
return false;
|
||||||
|
if (node.callee.object.type === Syntax.Super)
|
||||||
|
return false;
|
||||||
|
if (node.callee.computed && node.callee.property.type === Syntax.Literal && !shouldWrapProperty(node.callee.property.value))
|
||||||
|
return false;
|
||||||
|
if (!node.callee.computed && node.callee.property.type === Syntax.Identifier && !shouldWrapProperty(node.callee.property.name))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
const { callee } = node;
|
||||||
|
|
||||||
|
data.changes.push({
|
||||||
|
node: `${uvMethods.call}((`,
|
||||||
|
start: node.start,
|
||||||
|
end: callee.object.start,
|
||||||
|
})
|
||||||
|
|
||||||
|
callee.object.iterateEnd = function () {
|
||||||
|
data.changes.push({
|
||||||
|
start: callee.object.end,
|
||||||
|
end: callee.property.start,
|
||||||
|
});
|
||||||
|
|
||||||
|
data.changes.push({
|
||||||
|
node: '), ('
|
||||||
|
});
|
||||||
|
|
||||||
|
if (callee.computed) {
|
||||||
|
callee.property.iterateEnd = function() {
|
||||||
|
|
||||||
|
data.changes.push({
|
||||||
|
end: node.arguments.length ? node.arguments[0].start : callee.end,
|
||||||
|
start: callee.property.end,
|
||||||
|
node: '), ['
|
||||||
|
})
|
||||||
|
node.iterateEnd = function() {
|
||||||
|
data.changes.push({
|
||||||
|
end: node.end,
|
||||||
|
start: node.arguments.length ? node.arguments[node.arguments.length - 1].end : callee.end,
|
||||||
|
node: `], ${master}, true)`
|
||||||
|
})
|
||||||
|
};
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
data.changes.push({
|
||||||
|
end: node.arguments.length ? node.arguments[0].start : false,
|
||||||
|
node: '"' + callee.property.name + '"), ['
|
||||||
|
})
|
||||||
|
node.iterateEnd = function() {
|
||||||
|
data.changes.push({
|
||||||
|
end: node.end,
|
||||||
|
start: node.arguments.length ? node.arguments[node.arguments.length - 1].end : false,
|
||||||
|
node: `], ${master}, false)`
|
||||||
|
})
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
function setProperty(ctx) {
|
||||||
|
const { js } = ctx;
|
||||||
|
js.on(Syntax.AssignmentExpression, (node, data, type) => {
|
||||||
|
if (type !== 'rewrite') return false;
|
||||||
|
if (node.left.type !== Syntax.MemberExpression) return false;
|
||||||
|
if (!assignmentOperators.includes(node.operator)) return false;
|
||||||
|
if (node.left.object.type === Syntax.Super)
|
||||||
|
return false;
|
||||||
|
if (node.left.computed && node.left.property.type === Syntax.Literal && !shouldWrapProperty(node.left.property.value))
|
||||||
|
return false;
|
||||||
|
if (!node.left.computed && node.left.property.type === Syntax.Identifier && !shouldWrapProperty(node.left.property.name))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
const { left, right } = node;
|
||||||
|
|
||||||
|
data.changes.push({
|
||||||
|
node: `${uvMethods.set}((`,
|
||||||
|
start: left.object.start,
|
||||||
|
end: left.object.start,
|
||||||
|
});
|
||||||
|
|
||||||
|
left.object.iterateEnd = function () {
|
||||||
|
data.changes.push({
|
||||||
|
start: left.object.end,
|
||||||
|
end: left.property.start,
|
||||||
|
});
|
||||||
|
|
||||||
|
data.changes.push({
|
||||||
|
node: '), ('
|
||||||
|
});
|
||||||
|
|
||||||
|
if (left.computed) {
|
||||||
|
left.property.iterateEnd = function() {
|
||||||
|
data.changes.push({
|
||||||
|
end: right.start,
|
||||||
|
node: '' + left.property.name + '), '
|
||||||
|
})
|
||||||
|
if (shortHandAssignment[node.operator]) {
|
||||||
|
data.changes.push({
|
||||||
|
node: data.input.slice(left.start, left.end) + ` ${shortHandAssignment[node.operator]} `
|
||||||
|
})
|
||||||
|
};
|
||||||
|
node.iterateEnd = function() {
|
||||||
|
data.changes.push({
|
||||||
|
end: node.end,
|
||||||
|
start: right.end,
|
||||||
|
node: `, ${master}, true)`
|
||||||
|
})
|
||||||
|
};
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
data.changes.push({
|
||||||
|
end: right.start,
|
||||||
|
node: '"' + left.property.name + '"), '
|
||||||
|
})
|
||||||
|
if (shortHandAssignment[node.operator]) {
|
||||||
|
data.changes.push({
|
||||||
|
node: data.input.slice(left.start, left.end) + ` ${shortHandAssignment[node.operator]} `
|
||||||
|
})
|
||||||
|
};
|
||||||
|
node.iterateEnd = function() {
|
||||||
|
data.changes.push({
|
||||||
|
end: node.end,
|
||||||
|
start: right.end,
|
||||||
|
node: `, ${master}, false)`
|
||||||
|
})
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
function wrapEval(ctx) {
|
||||||
|
const { js } = ctx;
|
||||||
|
js.on(Syntax.CallExpression, (node, data, type) => {
|
||||||
|
if (type !== 'rewrite') return false;
|
||||||
|
if (!node.arguments.length) return false;
|
||||||
|
if (node.callee.type !== Syntax.Identifier) return false;
|
||||||
|
if (node.callee.name !== 'eval') return false;
|
||||||
|
|
||||||
|
const [ script ] = node.arguments;
|
||||||
|
|
||||||
|
data.changes.push({
|
||||||
|
node: uvMethods.script + '(',
|
||||||
|
start: script.start,
|
||||||
|
end: script.start,
|
||||||
|
})
|
||||||
|
node.iterateEnd = function() {
|
||||||
|
data.changes.push({
|
||||||
|
node: ')',
|
||||||
|
start: script.end,
|
||||||
|
end: script.end,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
function sourceMethods(ctx) {
|
||||||
|
const { js } = ctx;
|
||||||
|
js.on(Syntax.CallExpression, (node, data, type) => {
|
||||||
|
if (type !== 'source') return false;
|
||||||
|
if (node.callee.type !== Syntax.Identifier) return false;
|
||||||
|
if (!uvMethodTypes[node.callee.name]) return false;
|
||||||
|
|
||||||
|
const info = uvWrapperInfo(node, data);
|
||||||
|
|
||||||
|
switch(uvMethodTypes[node.callee.name]) {
|
||||||
|
case 'set':
|
||||||
|
data.changes.push({
|
||||||
|
node: info.computed ? `${info.object}[${info.property}] = ${info.value}` : `${info.object}.${info.property} = ${info.value}`,
|
||||||
|
start: node.start,
|
||||||
|
end: node.end,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case 'get':
|
||||||
|
data.changes.push({
|
||||||
|
node: info.computed ? `${info.object}[${info.property}]` : `${info.object}.${info.property}`,
|
||||||
|
start: node.start,
|
||||||
|
end: node.end,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case 'call':
|
||||||
|
data.changes.push({
|
||||||
|
node: info.computed ? `${info.object}[${info.property}](${info.args})` : `${info.object}.${info.property}${info.args}`,
|
||||||
|
start: node.start,
|
||||||
|
end: node.end,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case 'script':
|
||||||
|
data.changes.push({
|
||||||
|
node: info.script,
|
||||||
|
start: node.start,
|
||||||
|
end: node.end
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case 'url':
|
||||||
|
data.changes.push({
|
||||||
|
node: info.url,
|
||||||
|
start: node.start,
|
||||||
|
end: node.end
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case 'proxy':
|
||||||
|
data.changes.push({
|
||||||
|
node: info.name,
|
||||||
|
start: node.start,
|
||||||
|
end: node.end,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
function uvWrapperInfo(node, { input }) {
|
||||||
|
const method = uvMethodTypes[node.callee.name];
|
||||||
|
|
||||||
|
switch(method) {
|
||||||
|
case 'set':
|
||||||
|
{
|
||||||
|
const [ object, property, value, source, computed ] = node.arguments;
|
||||||
|
return {
|
||||||
|
method,
|
||||||
|
object: input.slice(object.start - 1, object.end + 1),
|
||||||
|
property: property.type === Syntax.Literal && !computed.value ? property.value : input.slice(property.start, property.end),
|
||||||
|
computed: !!computed.value,
|
||||||
|
value: input.slice(value.start, value.end),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
case 'get':
|
||||||
|
{
|
||||||
|
const [ object, property, source, computed ] = node.arguments;
|
||||||
|
return {
|
||||||
|
method,
|
||||||
|
object: input.slice(object.start - 1, object.end + 1),
|
||||||
|
property: property.type === Syntax.Literal && !computed.value ? property.value : input.slice(property.start, property.end),
|
||||||
|
computed: !!computed.value,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
case 'call':
|
||||||
|
{
|
||||||
|
const [ object, property, args, source, computed ] = node.arguments;
|
||||||
|
return {
|
||||||
|
method,
|
||||||
|
object: input.slice(object.start - 1, object.end + 1),
|
||||||
|
property: property.type === Syntax.Literal && !computed.value ? property.value : input.slice(property.start, property.end),
|
||||||
|
args: input.slice(args.start + 1, args.end - 1),
|
||||||
|
computed: !!computed.value,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
case 'script':
|
||||||
|
{
|
||||||
|
const [ script ] = node.arguments;
|
||||||
|
return {
|
||||||
|
script: input.slice(script.start, script.end),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case 'url':
|
||||||
|
{
|
||||||
|
const [ url ] = node.arguments;
|
||||||
|
return {
|
||||||
|
url: input.slice(url.start, url.end),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case 'proxy':
|
||||||
|
{
|
||||||
|
const [ name ] = node.arguments;
|
||||||
|
return { name };
|
||||||
|
};
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
function wrapIdentifier(ctx) {
|
||||||
|
const { js } = ctx;
|
||||||
|
js.on(Syntax.Identifier, (node, data, type) => {
|
||||||
|
if (type !== 'rewrite') return false;
|
||||||
|
const { parent } = node;
|
||||||
|
if (!shouldWrapIdentifier(node.name)) return false;
|
||||||
|
if (parent.type === Syntax.VariableDeclarator && parent.id === node) return false;
|
||||||
|
if ((parent.type === Syntax.AssignmentExpression || parent.type === Syntax.AssignmentPattern) && parent.left === node) return false;
|
||||||
|
if ((parent.type === Syntax.FunctionExpression || parent.type === Syntax.FunctionDeclaration) && parent.id === node) return false;
|
||||||
|
if (parent.type === Syntax.MemberExpression && parent.property === node && !parent.computed) return false;
|
||||||
|
if (node.name === 'eval' && parent.type === Syntax.CallExpression && parent.callee === node) return false;
|
||||||
|
if (parent.type === Syntax.Property && parent.key === node) return false;
|
||||||
|
if (parent.type === Syntax.Property && parent.value === node && parent.shorthand) return false;
|
||||||
|
if (parent.type === Syntax.UpdateExpression && (parent.operator === '++' || parent.operator === '--')) return false;
|
||||||
|
if ((parent.type === Syntax.FunctionExpression || parent.type === Syntax.FunctionDeclaration || parent.type === Syntax.ArrowFunctionExpression) && parent.params.indexOf(node) !== -1) return false;
|
||||||
|
if (parent.type === Syntax.MethodDefinition) return false;
|
||||||
|
if (parent.type === Syntax.ClassDeclaration) return false;
|
||||||
|
if (parent.type === Syntax.RestElement) return false;
|
||||||
|
if (parent.type === Syntax.ExportSpecifier) return false;
|
||||||
|
if (parent.type === Syntax.ImportSpecifier) return false;
|
||||||
|
data.changes.push({
|
||||||
|
start: node.start,
|
||||||
|
end: node.end,
|
||||||
|
node: `${uvMethods.proxy}(${node.name}, __uv)`
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
function importDeclaration(ctx) {
|
||||||
|
const { js } = ctx;
|
||||||
|
js.on(Syntax.Literal, (node, data, type) => {
|
||||||
|
if (!((node.parent.type === Syntax.ImportDeclaration || node.parent.type === Syntax.ExportAllDeclaration || node.parent.type === Syntax.ExportNamedDeclaration)
|
||||||
|
&& node.parent.source === node)) return false;
|
||||||
|
|
||||||
|
data.changes.push({
|
||||||
|
start: node.start + 1,
|
||||||
|
end: node.end - 1,
|
||||||
|
node: type === 'rewrite' ? ctx.rewriteUrl(node.value) : ctx.sourceUrl(node.value)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
function dynamicImport(ctx) {
|
||||||
|
const { js } = ctx;
|
||||||
|
js.on(Syntax.ImportExpression, (node, data, type) => {
|
||||||
|
if (type !== 'rewrite') return false;
|
||||||
|
data.changes.push({
|
||||||
|
node: uvMethods.url + '(',
|
||||||
|
start: node.source.start,
|
||||||
|
end: node.source.start,
|
||||||
|
})
|
||||||
|
node.iterateEnd = function() {
|
||||||
|
data.changes.push({
|
||||||
|
node: ')',
|
||||||
|
start: node.source.end,
|
||||||
|
end: node.source.end,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
function destructureDeclaration(ctx) {
|
||||||
|
const { js } = ctx;
|
||||||
|
js.on(Syntax.VariableDeclarator, (node, data, type) => {
|
||||||
|
if (type !== 'rewrite') return false;
|
||||||
|
if (node.id.type !== Syntax.ObjectPattern) return false;
|
||||||
|
const names = [];
|
||||||
|
|
||||||
|
for (const { key } of node.id.properties) {
|
||||||
|
names.push(key.name);
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log(names);
|
||||||
|
|
||||||
|
data.changes.push({
|
||||||
|
node: uvMethods.object + '(',
|
||||||
|
start: node.init.start,
|
||||||
|
end: node.init.start,
|
||||||
|
})
|
||||||
|
|
||||||
|
node.iterateEnd = function() {
|
||||||
|
data.changes.push({
|
||||||
|
node: ')',
|
||||||
|
start: node.init.end,
|
||||||
|
end: node.init.end,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
function shouldWrapProperty(name) {
|
||||||
|
return name === 'eval' || name === 'postMessage' || name === 'location' || name === 'parent' || name === 'top';
|
||||||
|
};
|
||||||
|
|
||||||
|
function shouldWrapIdentifier(name) {
|
||||||
|
return name === 'postMessage' || name === 'location' || name === 'parent' || name === 'top';
|
||||||
|
};
|
||||||
|
|
||||||
|
export { getProperty, destructureDeclaration, setProperty, call, sourceMethods, importDeclaration, dynamicImport, wrapIdentifier, wrapEval };
|
230
rewrite/rewrite.script.test.js
Normal file
230
rewrite/rewrite.script.test.js
Normal file
|
@ -0,0 +1,230 @@
|
||||||
|
import { Syntax } from 'esotope-hammerhead';
|
||||||
|
|
||||||
|
function property(ctx) {
|
||||||
|
const { js } = ctx;
|
||||||
|
js.on('MemberExpression', (node, data, type) => {
|
||||||
|
if (node.object.type === 'Super') return false;
|
||||||
|
|
||||||
|
if (type === 'rewrite' && computedProperty(node)) {
|
||||||
|
data.changes.push({
|
||||||
|
node: '__uv.$wrap((',
|
||||||
|
start: node.property.start,
|
||||||
|
end: node.property.start,
|
||||||
|
})
|
||||||
|
node.iterateEnd = function() {
|
||||||
|
data.changes.push({
|
||||||
|
node: '))',
|
||||||
|
start: node.property.end,
|
||||||
|
end: node.property.end,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!node.computed && node.property.name === 'location' && type === 'rewrite' || node.property.name === '__uv$location' && type === 'source') {
|
||||||
|
data.changes.push({
|
||||||
|
start: node.property.start,
|
||||||
|
end: node.property.end,
|
||||||
|
node: type === 'rewrite' ? '__uv$setSource(__uv).__uv$location' : 'location'
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
if (node.object.type === Syntax.Identifier && node.object.name === 'eval') {
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
if (!node.computed && node.property.name === 'postMessage' && type === 'rewrite') {
|
||||||
|
data.changes.push({
|
||||||
|
start: node.property.start,
|
||||||
|
end: node.property.end,
|
||||||
|
node:'__uv$setSource(__uv).postMessage',
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
if (!node.computed && node.property.name === 'eval' && type === 'rewrite' || node.property.name === '__uv$eval' && type === 'source') {
|
||||||
|
data.changes.push({
|
||||||
|
start: node.property.start,
|
||||||
|
end: node.property.end,
|
||||||
|
node: type === 'rewrite' ? '__uv$setSource(__uv).__uv$eval' : 'eval'
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!node.computed && node.property.name === '__uv$setSource' && type === 'source' && node.parent.type === Syntax.CallExpression) {
|
||||||
|
const { parent, property } = node;
|
||||||
|
data.changes.push({
|
||||||
|
start: property.start - 1,
|
||||||
|
end: parent.end,
|
||||||
|
});
|
||||||
|
|
||||||
|
node.iterateEnd = function() {
|
||||||
|
data.changes.push({
|
||||||
|
start: property.start,
|
||||||
|
end: parent.end,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
function identifier(ctx) {
|
||||||
|
const { js } = ctx;
|
||||||
|
js.on('Identifier', (node, data, type) => {
|
||||||
|
if (type !== 'rewrite') return false;
|
||||||
|
const { parent } = node;
|
||||||
|
if (!['location', 'eval'].includes(node.name)) return false;
|
||||||
|
if (parent.type === Syntax.VariableDeclarator && parent.id === node) return false;
|
||||||
|
if ((parent.type === Syntax.AssignmentExpression || parent.type === Syntax.AssignmentPattern) && parent.left === node) return false;
|
||||||
|
if ((parent.type === Syntax.FunctionExpression || parent.type === Syntax.FunctionDeclaration) && parent.id === node) return false;
|
||||||
|
if (parent.type === Syntax.MemberExpression && parent.property === node && !parent.computed) return false;
|
||||||
|
if (node.name === 'eval' && parent.type === Syntax.CallExpression && parent.callee === node) return false;
|
||||||
|
if (parent.type === Syntax.Property && parent.key === node) return false;
|
||||||
|
if (parent.type === Syntax.Property && parent.value === node && parent.shorthand) return false;
|
||||||
|
if (parent.type === Syntax.UpdateExpression && (parent.operator === '++' || parent.operator === '--')) return false;
|
||||||
|
if ((parent.type === Syntax.FunctionExpression || parent.type === Syntax.FunctionDeclaration || parent.type === Syntax.ArrowFunctionExpression) && parent.params.indexOf(node) !== -1) return false;
|
||||||
|
if (parent.type === Syntax.MethodDefinition) return false;
|
||||||
|
if (parent.type === Syntax.ClassDeclaration) return false;
|
||||||
|
if (parent.type === Syntax.RestElement) return false;
|
||||||
|
if (parent.type === Syntax.ExportSpecifier) return false;
|
||||||
|
if (parent.type === Syntax.ImportSpecifier) return false;
|
||||||
|
|
||||||
|
data.changes.push({
|
||||||
|
start: node.start,
|
||||||
|
end: node.end,
|
||||||
|
node: '__uv.$get(' + node.name + ')'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
function wrapEval(ctx) {
|
||||||
|
const { js } = ctx;
|
||||||
|
js.on('CallExpression', (node, data, type) => {
|
||||||
|
if (type !== 'rewrite') return false;
|
||||||
|
if (!node.arguments.length) return false;
|
||||||
|
if (node.callee.type !== 'Identifier') return false;
|
||||||
|
if (node.callee.name !== 'eval') return false;
|
||||||
|
|
||||||
|
const [ script ] = node.arguments;
|
||||||
|
|
||||||
|
data.changes.push({
|
||||||
|
node: '__uv.js.rewrite(',
|
||||||
|
start: script.start,
|
||||||
|
end: script.start,
|
||||||
|
})
|
||||||
|
node.iterateEnd = function() {
|
||||||
|
data.changes.push({
|
||||||
|
node: ')',
|
||||||
|
start: script.end,
|
||||||
|
end: script.end,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
function importDeclaration(ctx) {
|
||||||
|
const { js } = ctx;
|
||||||
|
js.on(Syntax.Literal, (node, data, type) => {
|
||||||
|
if (!((node.parent.type === Syntax.ImportDeclaration || node.parent.type === Syntax.ExportAllDeclaration || node.parent.type === Syntax.ExportNamedDeclaration)
|
||||||
|
&& node.parent.source === node)) return false;
|
||||||
|
|
||||||
|
data.changes.push({
|
||||||
|
start: node.start + 1,
|
||||||
|
end: node.end - 1,
|
||||||
|
node: type === 'rewrite' ? ctx.rewriteUrl(node.value) : ctx.sourceUrl(node.value)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
function dynamicImport(ctx) {
|
||||||
|
const { js } = ctx;
|
||||||
|
js.on(Syntax.ImportExpression, (node, data, type) => {
|
||||||
|
if (type !== 'rewrite') return false;
|
||||||
|
data.changes.push({
|
||||||
|
node: '__uv.rewriteUrl(',
|
||||||
|
start: node.source.start,
|
||||||
|
end: node.source.start,
|
||||||
|
})
|
||||||
|
node.iterateEnd = function() {
|
||||||
|
data.changes.push({
|
||||||
|
node: ')',
|
||||||
|
start: node.source.end,
|
||||||
|
end: node.source.end,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
function unwrap(ctx) {
|
||||||
|
const { js } = ctx;
|
||||||
|
js.on('CallExpression', (node, data, type) => {
|
||||||
|
if (type !== 'source') return false;
|
||||||
|
if (!isWrapped(node.callee)) return false;
|
||||||
|
|
||||||
|
switch(node.callee.property.name) {
|
||||||
|
case '$wrap':
|
||||||
|
if (!node.arguments || node.parent.type !== Syntax.MemberExpression || node.parent.property !== node) return false;
|
||||||
|
const [ property ] = node.arguments;
|
||||||
|
|
||||||
|
data.changes.push({
|
||||||
|
start: node.callee.start,
|
||||||
|
end: property.start,
|
||||||
|
});
|
||||||
|
|
||||||
|
node.iterateEnd = function() {
|
||||||
|
data.changes.push({
|
||||||
|
start: node.end - 2,
|
||||||
|
end: node.end,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
case '$get':
|
||||||
|
case 'rewriteUrl':
|
||||||
|
const [ arg ] = node.arguments;
|
||||||
|
|
||||||
|
data.changes.push({
|
||||||
|
start: node.callee.start,
|
||||||
|
end: arg.start,
|
||||||
|
});
|
||||||
|
|
||||||
|
node.iterateEnd = function() {
|
||||||
|
data.changes.push({
|
||||||
|
start: node.end - 1,
|
||||||
|
end: node.end,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
case 'rewrite':
|
||||||
|
const [ script ] = node.arguments;
|
||||||
|
data.changes.push({
|
||||||
|
start: node.callee.start,
|
||||||
|
end: script.start,
|
||||||
|
});
|
||||||
|
node.iterateEnd = function() {
|
||||||
|
data.changes.push({
|
||||||
|
start: node.end - 1,
|
||||||
|
end: node.end,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
function isWrapped(node) {
|
||||||
|
if (node.type !== Syntax.MemberExpression) return false;
|
||||||
|
if (node.property.name === 'rewrite' && isWrapped(node.object)) return true;
|
||||||
|
if (node.object.type !== Syntax.Identifier || node.object.name !== '__uv') return false;
|
||||||
|
if (!['js', '$get', '$wrap', 'rewriteUrl'].includes(node.property.name)) return false;
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
function computedProperty(parent) {
|
||||||
|
if (!parent.computed) return false;
|
||||||
|
const { property: node } = parent;
|
||||||
|
if (node.type === 'Literal' && node.value !== 'location') return false;
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export { property, wrapEval, dynamicImport, importDeclaration, identifier, unwrap };
|
34
server/v1/request.js
Normal file
34
server/v1/request.js
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
import http from 'http';
|
||||||
|
import https from 'https';
|
||||||
|
import { json, prepareRequest, prepareResponse } from './util.js';
|
||||||
|
|
||||||
|
function request(request, response) {
|
||||||
|
try {
|
||||||
|
const data = prepareRequest(request);
|
||||||
|
const protocol = data.protocol === 'https:' ? https : http;
|
||||||
|
|
||||||
|
const remoteRequest = protocol.request(data);
|
||||||
|
|
||||||
|
remoteRequest.on('response', remoteResponse => {
|
||||||
|
const send = prepareResponse(remoteResponse);
|
||||||
|
response.writeHead(...send);
|
||||||
|
remoteResponse.pipe(response);
|
||||||
|
});
|
||||||
|
|
||||||
|
remoteRequest.on('error', e => {
|
||||||
|
console.log(e);
|
||||||
|
json(response, 500, {
|
||||||
|
error: e.toString()
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
request.pipe(remoteRequest);
|
||||||
|
} catch(e) {
|
||||||
|
console.log(e);
|
||||||
|
json(response, 500, {
|
||||||
|
error: e.toString()
|
||||||
|
});
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default request;
|
29
server/v1/test/index.js
Normal file
29
server/v1/test/index.js
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
import https from "https";
|
||||||
|
import path from "path";
|
||||||
|
import { readFileSync, createReadStream } from "fs";
|
||||||
|
import request from "../request.js";
|
||||||
|
|
||||||
|
const __dirname = path.resolve(path.dirname(decodeURI(new URL(import.meta.url).pathname))).slice(3);
|
||||||
|
|
||||||
|
https.createServer({
|
||||||
|
key: readFileSync(path.join(__dirname, './ssl.key')),
|
||||||
|
cert: readFileSync(path.join(__dirname, './ssl.cert')),
|
||||||
|
}, (req, res) => {
|
||||||
|
if (req.url.startsWith('/sw.js')) {
|
||||||
|
res.writeHead(200, { "Content-Type": 'application/javascript' });
|
||||||
|
createReadStream(path.join(__dirname, './static/sw.js')).pipe(res);
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (req.url.startsWith('/script.js')) {
|
||||||
|
res.writeHead(200, { "Content-Type": 'application/javascript' });
|
||||||
|
createReadStream(path.join(__dirname, './static/script.js')).pipe(res);
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (req.url.startsWith('/bare/v1/')) {
|
||||||
|
return request(req, res);
|
||||||
|
};
|
||||||
|
|
||||||
|
createReadStream(path.join(__dirname, './static/index.html')).pipe(res);
|
||||||
|
}).listen(443);
|
22
server/v1/test/ssl.cert
Normal file
22
server/v1/test/ssl.cert
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIDqzCCApOgAwIBAgIJAJnCkScWtmL0MA0GCSqGSIb3DQEBCwUAMGwxCzAJBgNV
|
||||||
|
BAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMRgwFgYDVQQKDA9UaXRhbml1bU5l
|
||||||
|
dHdvcmsxDjAMBgNVBAsMBWdhbWVyMR4wHAYDVQQDDBUqLnRpdGFuaXVtbmV0d29y
|
||||||
|
ay5vcmcwHhcNMjAwNjEzMTg0OTU2WhcNMjEwNjEzMTg0OTU2WjBsMQswCQYDVQQG
|
||||||
|
EwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEYMBYGA1UECgwPVGl0YW5pdW1OZXR3
|
||||||
|
b3JrMQ4wDAYDVQQLDAVnYW1lcjEeMBwGA1UEAwwVKi50aXRhbml1bW5ldHdvcmsu
|
||||||
|
b3JnMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwPL69+RE6r8RrFh4
|
||||||
|
njzC8ZRnLB+yNtuGw14C0dvNb5JwgdLl5g9/wK/s0V5NGlqwxlQlxQ/gUSuYEcUR
|
||||||
|
6MYjcnaUmZZe/gaKVV0fkfkuigOWhLnI5AQxx7rhkzx1ujuyJ9D2pkDtZpSvv0yn
|
||||||
|
2yrvWhJMtjuxGYip8jaLuRpbXoafvR7nrlDaNcE/GwIjnCCxsRnY2bGbxYK840mN
|
||||||
|
fuMfF2nz+fXKPuQ/9PT48e3wOo9vM5s7yKhiHYwrogqzGN4cH4sSr1FE8C7flFyT
|
||||||
|
Yw101u7fUaopfeGCo9Pg6IrfzyzE5Qb7OlqlVk2IkvXx7pPqVc6lZCJEhOX/qF9o
|
||||||
|
n3mFqwIDAQABo1AwTjAdBgNVHQ4EFgQUC561ob2kGtFQ4az6y64b98+Fy+IwHwYD
|
||||||
|
VR0jBBgwFoAUC561ob2kGtFQ4az6y64b98+Fy+IwDAYDVR0TBAUwAwEB/zANBgkq
|
||||||
|
hkiG9w0BAQsFAAOCAQEAotvUsSLSzFyxQz329tEPyH6Tmi19FQoA5ZbLg6EqeTI9
|
||||||
|
08qOByDGkSYJi0npaIlPO1I557NxRzdO0PxK3ybol6lnzuSlqCJP5nb1dr0z2Eax
|
||||||
|
wgKht9P+ap/yozU5ye05ah2nkpcaeDPnwnnWFmfsnYNfgu62EshOS+5FETWEKVUb
|
||||||
|
LXQhGInOdJq8KZvhoLZWJoUhyAqxBfW4oVvaqs+Ff96A2NNKrvbiAVYX30rVa+x0
|
||||||
|
KIl0/DoVvDx2Q6TiL396cAXdKUW7edRQcSsGFcxwIrU5lePm0V05aN+oCoEBvXBG
|
||||||
|
ArPN+a5kpGjJwfcpcBVf9cJ6IsvptGS9de3eTHoTyw==
|
||||||
|
-----END CERTIFICATE-----
|
28
server/v1/test/ssl.key
Normal file
28
server/v1/test/ssl.key
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
-----BEGIN PRIVATE KEY-----
|
||||||
|
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDA8vr35ETqvxGs
|
||||||
|
WHiePMLxlGcsH7I224bDXgLR281vknCB0uXmD3/Ar+zRXk0aWrDGVCXFD+BRK5gR
|
||||||
|
xRHoxiNydpSZll7+BopVXR+R+S6KA5aEucjkBDHHuuGTPHW6O7In0PamQO1mlK+/
|
||||||
|
TKfbKu9aEky2O7EZiKnyNou5Gltehp+9HueuUNo1wT8bAiOcILGxGdjZsZvFgrzj
|
||||||
|
SY1+4x8XafP59co+5D/09Pjx7fA6j28zmzvIqGIdjCuiCrMY3hwfixKvUUTwLt+U
|
||||||
|
XJNjDXTW7t9Rqil94YKj0+Doit/PLMTlBvs6WqVWTYiS9fHuk+pVzqVkIkSE5f+o
|
||||||
|
X2ifeYWrAgMBAAECggEAbihK8Ev6rKr5RBQeiPjXs2SuoppV/MvIXLHHmliLKS/J
|
||||||
|
29S0PGyM202VPtM/4dP1KMXR6nft8WmaIEsKtoKoqijZHfajtRO21pWb+JLy5wi1
|
||||||
|
XoFTGBrs8MLZFl5mODTsuZ6rsq9O2kn5LJZvHsmcbSgVc9UQfytvG0HY840ArS3g
|
||||||
|
kSDtUFb1xRui6wtCBKzHVvCT+FXhSBbwkHalmbqP6BefhJ3lW2VonkOcHDrdXPfW
|
||||||
|
CEN18IJ2v8QYgXqZP6VUlAweNXLJ33ZOl+jXGdygcOG24MFqdw0VtP0XFGk0jnSS
|
||||||
|
W6dX67BZKeZ71EKaTy02jw5LpQNXA70ismPJHQ2uQQKBgQDuROawnBIW1fC3xOle
|
||||||
|
m+JmP0eMe0eIQycxRsMXsXhYAA0wV3qYZSLZrNK2eRhmSNt+ODSmZ2Vt11dwOv5u
|
||||||
|
bo8WONrRlM097SmitS2S+8o7ASem2VKQzyRE72Y9517Q+aNBdLRVtjrRNSw/hfSu
|
||||||
|
ayLuG36+yukSH7wq7mfoUX34ZwKBgQDPTrgyyw8n5XhZT/qTTRnQJ2GTvPxDzNoJ
|
||||||
|
IAGhGJGFAb6wgLoSpGx6BC122vuRxcTjkjAiMDci5N2zNW+YZVni+F0KTVvNFfU2
|
||||||
|
pOTJUg3luRTygCra6O02PxwpbP/9KCBAKq/kYw/eBW+gxhPwP3ZrbAirvBjgBh0I
|
||||||
|
kIrFijNOHQKBgGUUAbFGZD4fwCCVflLOWnr5uUaVPcFGi6fR1w2EEgNy8iVh1vYz
|
||||||
|
YVdqg3E5aepqWgLvoRY+or64LbXEsQ70A+tvbxSdxXvR0mnd5lmGS0JAuSuE4gvg
|
||||||
|
dAhybrMwJf8NB/7KnX4G8mix3/WKxEQB2y2bqGcT+U/g+phTzuy1NXVdAoGBAIrl
|
||||||
|
jVjK4J60iswcYCEteWwT1rbr2oF60WNnxG+xTF63apJLzWAMNnoSLnwCAKgMv/xR
|
||||||
|
yFo/v9FrUnduCBUtYupFyeDLMATa/27bUEbq6VDPjw9jfFMr2TONWUsQMvvlVKZp
|
||||||
|
c2wsS0dQkRhBXr6LZsZWngCiiHAg6HcCkVgFXpapAoGBAJ/8oLGt0Ar+0MTl+gyk
|
||||||
|
xSqgHnsc5jgqhix3nIoI5oEAbfibdGmRD1S3rtWD9YsnPxMIl+6E5bOAHrmd+Zr8
|
||||||
|
O7EP+CLvbz4JXidaaa85h9ThXSG5xk1A1UTtSFrp+KolLE1Vvmjjd+R844XsM2wZ
|
||||||
|
OAHbihzk0iPPphjEWR4lU4Av
|
||||||
|
-----END PRIVATE KEY-----
|
9
server/v1/test/static/index.html
Normal file
9
server/v1/test/static/index.html
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<script src="/script.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Loading www.google.com for testing...</h1>
|
||||||
|
</body>
|
||||||
|
</html>
|
8
server/v1/test/static/script.js
Normal file
8
server/v1/test/static/script.js
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
if ('serviceWorker' in navigator) {
|
||||||
|
navigator.serviceWorker.register('/sw.js', {
|
||||||
|
scope: '/'
|
||||||
|
});
|
||||||
|
navigator.serviceWorker.ready.then(() => {
|
||||||
|
location.reload()
|
||||||
|
})
|
||||||
|
};
|
30
server/v1/test/static/sw.js
Normal file
30
server/v1/test/static/sw.js
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
addEventListener('fetch', async event => {
|
||||||
|
|
||||||
|
const { request } = event;
|
||||||
|
const url = new URL(request.url);
|
||||||
|
|
||||||
|
const sendHeaders = {
|
||||||
|
Accept: request.headers.get('accept'),
|
||||||
|
'Accept-Language': request.headers.get('accept-language'),
|
||||||
|
Host: 'www.google.com'
|
||||||
|
};
|
||||||
|
|
||||||
|
if (request.referrer) {
|
||||||
|
sendHeaders.Referer = 'https://www.google.com' + request.referrer.slice(location.origin.length);
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log(Object.fromEntries([...request.headers.entries()]), sendHeaders);
|
||||||
|
|
||||||
|
event.respondWith(
|
||||||
|
fetch('/bare/v1/', {
|
||||||
|
headers: {
|
||||||
|
'x-bare-host': 'www.google.com',
|
||||||
|
'x-bare-port': '443',
|
||||||
|
'x-bare-protocol': 'https:',
|
||||||
|
'x-bare-path': url.pathname + url.search,
|
||||||
|
'x-bare-headers': JSON.stringify(sendHeaders),
|
||||||
|
'x-bare-forward-headers': JSON.stringify(['user-agent'])
|
||||||
|
},
|
||||||
|
})
|
||||||
|
)
|
||||||
|
});
|
3
server/v1/upgrade.js
Normal file
3
server/v1/upgrade.js
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
function upgrade(req, socket, head) {
|
||||||
|
|
||||||
|
};
|
73
server/v1/util.js
Normal file
73
server/v1/util.js
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
export function prepareResponse({ headers, statusCode, statusMessage }) {
|
||||||
|
const sendHeaders = {
|
||||||
|
'x-bare-headers': JSON.stringify(headers),
|
||||||
|
'x-bare-status': statusCode,
|
||||||
|
'x-bare-status-text': statusMessage,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (headers['content-encoding']) sendHeaders['content-encoding'] = headers['content-encoding'];
|
||||||
|
if (headers['content-length']) sendHeaders['content-length'] = headers['content-length'];
|
||||||
|
|
||||||
|
return [ 200, sendHeaders ];
|
||||||
|
};
|
||||||
|
|
||||||
|
export function prepareRequest({ headers, rawHeaders, method }) {
|
||||||
|
if (!'x-bare-headers' in headers) throw new Error('Headers missing.');
|
||||||
|
if (!('x-bare-protocol' in headers || 'x-bare-host' in headers || 'x-bare-port' in headers || 'x-bare-path' in headers)) {
|
||||||
|
throw new Error('URL key missing.');
|
||||||
|
};
|
||||||
|
|
||||||
|
const forward = JSON.parse((getBareHeader('forward-headers', headers) || '[]'));
|
||||||
|
const sendHeaders = JSON.parse((getBareHeader('headers', headers) || '{}'));
|
||||||
|
const raw = constructRawHeaders(rawHeaders);
|
||||||
|
|
||||||
|
for (const name of forward) {
|
||||||
|
if (name in raw) {
|
||||||
|
sendHeaders[raw[name].name] = raw[name].value;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
headers: sendHeaders,
|
||||||
|
host: getBareHeader('host', headers),
|
||||||
|
port: getBareHeader('port', headers),
|
||||||
|
protocol: getBareHeader('protocol', headers),
|
||||||
|
path: getBareHeader('path', headers),
|
||||||
|
localAddress: null,
|
||||||
|
agent: null,
|
||||||
|
method,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export function json(response, status, json) {
|
||||||
|
response.writeHead(status, { 'Content-Type': 'application/json' });
|
||||||
|
response.end(
|
||||||
|
typeof json === 'object' ? JSON.stringify(json) : json
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export function getBareHeader(name, headers = {}) {
|
||||||
|
return headers[`x-bare-${name}`] || null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function constructRawHeaders(rawHeaders = []) {
|
||||||
|
const obj = {};
|
||||||
|
|
||||||
|
for (let i = 0; i < rawHeaders.length; i+=2) {
|
||||||
|
const name = rawHeaders[i] || '';
|
||||||
|
const lowerCaseName = name.toLowerCase();
|
||||||
|
const value = rawHeaders[i + 1] || '';
|
||||||
|
|
||||||
|
if (lowerCaseName in obj) {
|
||||||
|
if (Array.isArray(obj[lowerCaseName].value)) {
|
||||||
|
obj[lowerCaseName].value.push(value);
|
||||||
|
} else {
|
||||||
|
obj[lowerCaseName].value = [ obj[lowerCaseName].value, value ];
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
obj[lowerCaseName] = { name, value };
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
return obj;
|
||||||
|
};
|
Loading…
Add table
Add a link
Reference in a new issue