This commit is contained in:
Jason 2022-02-14 00:25:17 -05:00
parent b9b6aee734
commit 82f5f76588
66 changed files with 74967 additions and 1 deletions

2
.gitignore vendored
View file

@ -1 +1 @@
./node_modules ./node_modules

17
bundle.js Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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;

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

View 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
View 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
View 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
View 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
View file

@ -0,0 +1,4 @@
{
"prefix": "/service/",
"bare": "/bare/"
}

138
example/index.js Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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

File diff suppressed because one or more lines are too long

View 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
View 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';
};

View 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
View file

27
index.js Normal file
View 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

File diff suppressed because one or more lines are too long

743
lib/uv.handler.js Normal file
View 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
View 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
View 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

File diff suppressed because it is too large Load diff

26
package.json Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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;

View 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 };

View 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
View 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
View 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++) { // Dont 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
View 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
View 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
View 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 };

View 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
View 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
View 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
View 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
View 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-----

View 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>

View file

@ -0,0 +1,8 @@
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js', {
scope: '/'
});
navigator.serviceWorker.ready.then(() => {
location.reload()
})
};

View 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
View file

@ -0,0 +1,3 @@
function upgrade(req, socket, head) {
};

73
server/v1/util.js Normal file
View 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;
};