diff --git a/.gitignore b/.gitignore index 4b90444..ae84271 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1 @@ -./node_modules +./node_modules \ No newline at end of file diff --git a/bundle.js b/bundle.js new file mode 100644 index 0000000..ee63f67 --- /dev/null +++ b/bundle.js @@ -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) +); \ No newline at end of file diff --git a/client/dom/attr.js b/client/dom/attr.js new file mode 100644 index 0000000..fa09e82 --- /dev/null +++ b/client/dom/attr.js @@ -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; \ No newline at end of file diff --git a/client/dom/document.js b/client/dom/document.js new file mode 100644 index 0000000..cbfd1f8 --- /dev/null +++ b/client/dom/document.js @@ -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; \ No newline at end of file diff --git a/client/dom/element.js b/client/dom/element.js new file mode 100644 index 0000000..260fd50 --- /dev/null +++ b/client/dom/element.js @@ -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; \ No newline at end of file diff --git a/client/dom/node.js b/client/dom/node.js new file mode 100644 index 0000000..9c646bc --- /dev/null +++ b/client/dom/node.js @@ -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; \ No newline at end of file diff --git a/client/events.js b/client/events.js new file mode 100644 index 0000000..b9be953 --- /dev/null +++ b/client/events.js @@ -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); + } +} \ No newline at end of file diff --git a/client/history.js b/client/history.js new file mode 100644 index 0000000..3b83c7c --- /dev/null +++ b/client/history.js @@ -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; \ No newline at end of file diff --git a/client/hook.js b/client/hook.js new file mode 100644 index 0000000..aa82975 --- /dev/null +++ b/client/hook.js @@ -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; \ No newline at end of file diff --git a/client/index.js b/client/index.js new file mode 100644 index 0000000..b84f493 --- /dev/null +++ b/client/index.js @@ -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; \ No newline at end of file diff --git a/client/location.js b/client/location.js new file mode 100644 index 0000000..3ec0f40 --- /dev/null +++ b/client/location.js @@ -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; \ No newline at end of file diff --git a/client/message.js b/client/message.js new file mode 100644 index 0000000..4f41717 --- /dev/null +++ b/client/message.js @@ -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; \ No newline at end of file diff --git a/client/native/function.js b/client/native/function.js new file mode 100644 index 0000000..8b85ea6 --- /dev/null +++ b/client/native/function.js @@ -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; \ No newline at end of file diff --git a/client/native/object.js b/client/native/object.js new file mode 100644 index 0000000..1cd6caa --- /dev/null +++ b/client/native/object.js @@ -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; \ No newline at end of file diff --git a/client/navigator.js b/client/navigator.js new file mode 100644 index 0000000..1bf217b --- /dev/null +++ b/client/navigator.js @@ -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; \ No newline at end of file diff --git a/client/requests/eventsource.js b/client/requests/eventsource.js new file mode 100644 index 0000000..3ab3bd6 --- /dev/null +++ b/client/requests/eventsource.js @@ -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; \ No newline at end of file diff --git a/client/requests/fetch.js b/client/requests/fetch.js new file mode 100644 index 0000000..bf3e626 --- /dev/null +++ b/client/requests/fetch.js @@ -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; \ No newline at end of file diff --git a/client/requests/websocket.js b/client/requests/websocket.js new file mode 100644 index 0000000..50d8082 --- /dev/null +++ b/client/requests/websocket.js @@ -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; \ No newline at end of file diff --git a/client/requests/xhr.js b/client/requests/xhr.js new file mode 100644 index 0000000..493b71c --- /dev/null +++ b/client/requests/xhr.js @@ -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 \ No newline at end of file diff --git a/client/url.js b/client/url.js new file mode 100644 index 0000000..bb6e747 --- /dev/null +++ b/client/url.js @@ -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; \ No newline at end of file diff --git a/client/worker.js b/client/worker.js new file mode 100644 index 0000000..f567a2b --- /dev/null +++ b/client/worker.js @@ -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; \ No newline at end of file diff --git a/example/config.json b/example/config.json new file mode 100644 index 0000000..4a9bd5a --- /dev/null +++ b/example/config.json @@ -0,0 +1,4 @@ +{ + "prefix": "/service/", + "bare": "/bare/" +} \ No newline at end of file diff --git a/example/index.js b/example/index.js new file mode 100644 index 0000000..1e56e3d --- /dev/null +++ b/example/index.js @@ -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) + ); +}; \ No newline at end of file diff --git a/example/index.test.js b/example/index.test.js new file mode 100644 index 0000000..1b585d7 --- /dev/null +++ b/example/index.test.js @@ -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') +); \ No newline at end of file diff --git a/example/load.html b/example/load.html new file mode 100644 index 0000000..f6ad2bd --- /dev/null +++ b/example/load.html @@ -0,0 +1,16 @@ + + + + + + + \ No newline at end of file diff --git a/example/ssl.cert b/example/ssl.cert new file mode 100644 index 0000000..f87b9e0 --- /dev/null +++ b/example/ssl.cert @@ -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----- \ No newline at end of file diff --git a/example/ssl.key b/example/ssl.key new file mode 100644 index 0000000..a878bfd --- /dev/null +++ b/example/ssl.key @@ -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----- \ No newline at end of file diff --git a/example/static/index.html b/example/static/index.html new file mode 100644 index 0000000..3a53252 --- /dev/null +++ b/example/static/index.html @@ -0,0 +1,17 @@ + + + + + + + + +

Testing

+ + + +__uv$get(frame, 'postMessage', __uv) \ No newline at end of file diff --git a/example/static/index.js b/example/static/index.js new file mode 100644 index 0000000..29e0b43 --- /dev/null +++ b/example/static/index.js @@ -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); \ No newline at end of file diff --git a/example/static/op.bundle.js b/example/static/op.bundle.js new file mode 100644 index 0000000..f65c52b --- /dev/null +++ b/example/static/op.bundle.js @@ -0,0 +1,24795 @@ +/******/ (() => { // webpackBootstrap +/******/ var __webpack_modules__ = ([ +/* 0 */, +/* 1 */ +/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => { + +"use strict"; +__webpack_require__.r(__webpack_exports__); +/* harmony export */ __webpack_require__.d(__webpack_exports__, { +/* harmony export */ "default": () => (__WEBPACK_DEFAULT_EXPORT__) +/* harmony export */ }); +/* harmony import */ var _events_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(2); +/* harmony import */ var parse5__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(3); + + + +class HTML extends _events_js__WEBPACK_IMPORTED_MODULE_0__["default"] { + 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 ? parse5__WEBPACK_IMPORTED_MODULE_1__.parse : parse5__WEBPACK_IMPORTED_MODULE_1__.parseFragment)(new String(str).toString()); + this.iterate(ast, fn, options); + return (0,parse5__WEBPACK_IMPORTED_MODULE_1__.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 = parse5__WEBPACK_IMPORTED_MODULE_1__.parse; + static parseFragment = parse5__WEBPACK_IMPORTED_MODULE_1__.parseFragment; + static serialize = parse5__WEBPACK_IMPORTED_MODULE_1__.serialize; +}; + +class P5Element extends _events_js__WEBPACK_IMPORTED_MODULE_0__["default"] { + 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 ? (0,parse5__WEBPACK_IMPORTED_MODULE_1__.serialize)( + { + nodeName: '#document-fragment', + childNodes: this.childNodes, + } + ) : null; + }; + set innerHTML(val) { + if (!this.stream) this.node.childNodes = (0,parse5__WEBPACK_IMPORTED_MODULE_1__.parseFragment)(val).childNodes; + }; + get outerHTML() { + return !this.stream ? (0,parse5__WEBPACK_IMPORTED_MODULE_1__.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, ...(0,parse5__WEBPACK_IMPORTED_MODULE_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; + }; +}; + +/* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (HTML); + +/***/ }), +/* 2 */ +/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => { + +"use strict"; +__webpack_require__.r(__webpack_exports__); +/* harmony export */ __webpack_require__.d(__webpack_exports__, { +/* harmony export */ "default": () => (__WEBPACK_DEFAULT_EXPORT__) +/* harmony export */ }); +// 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. + + + +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); +} + +/* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (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); + } +} + +/***/ }), +/* 3 */ +/***/ ((__unused_webpack_module, exports, __webpack_require__) => { + +"use strict"; + + +const Parser = __webpack_require__(4); +const Serializer = __webpack_require__(26); + +// Shorthands +exports.parse = function parse(html, options) { + const parser = new Parser(options); + + return parser.parse(html); +}; + +exports.parseFragment = function parseFragment(fragmentContext, html, options) { + if (typeof fragmentContext === 'string') { + options = html; + html = fragmentContext; + fragmentContext = null; + } + + const parser = new Parser(options); + + return parser.parseFragment(html, fragmentContext); +}; + +exports.serialize = function(node, options) { + const serializer = new Serializer(node, options); + + return serializer.serialize(); +}; + + +/***/ }), +/* 4 */ +/***/ ((module, __unused_webpack_exports, __webpack_require__) => { + +"use strict"; + + +const Tokenizer = __webpack_require__(5); +const OpenElementStack = __webpack_require__(10); +const FormattingElementList = __webpack_require__(12); +const LocationInfoParserMixin = __webpack_require__(13); +const ErrorReportingParserMixin = __webpack_require__(18); +const Mixin = __webpack_require__(14); +const defaultTreeAdapter = __webpack_require__(22); +const mergeOptions = __webpack_require__(23); +const doctype = __webpack_require__(24); +const foreignContent = __webpack_require__(25); +const ERR = __webpack_require__(8); +const unicode = __webpack_require__(7); +const HTML = __webpack_require__(11); + +//Aliases +const $ = HTML.TAG_NAMES; +const NS = HTML.NAMESPACES; +const ATTRS = HTML.ATTRS; + +const DEFAULT_OPTIONS = { + scriptingEnabled: true, + sourceCodeLocationInfo: false, + onParseError: null, + treeAdapter: defaultTreeAdapter +}; + +//Misc constants +const HIDDEN_INPUT_TYPE = 'hidden'; + +//Adoption agency loops iteration count +const AA_OUTER_LOOP_ITER = 8; +const AA_INNER_LOOP_ITER = 3; + +//Insertion modes +const INITIAL_MODE = 'INITIAL_MODE'; +const BEFORE_HTML_MODE = 'BEFORE_HTML_MODE'; +const BEFORE_HEAD_MODE = 'BEFORE_HEAD_MODE'; +const IN_HEAD_MODE = 'IN_HEAD_MODE'; +const IN_HEAD_NO_SCRIPT_MODE = 'IN_HEAD_NO_SCRIPT_MODE'; +const AFTER_HEAD_MODE = 'AFTER_HEAD_MODE'; +const IN_BODY_MODE = 'IN_BODY_MODE'; +const TEXT_MODE = 'TEXT_MODE'; +const IN_TABLE_MODE = 'IN_TABLE_MODE'; +const IN_TABLE_TEXT_MODE = 'IN_TABLE_TEXT_MODE'; +const IN_CAPTION_MODE = 'IN_CAPTION_MODE'; +const IN_COLUMN_GROUP_MODE = 'IN_COLUMN_GROUP_MODE'; +const IN_TABLE_BODY_MODE = 'IN_TABLE_BODY_MODE'; +const IN_ROW_MODE = 'IN_ROW_MODE'; +const IN_CELL_MODE = 'IN_CELL_MODE'; +const IN_SELECT_MODE = 'IN_SELECT_MODE'; +const IN_SELECT_IN_TABLE_MODE = 'IN_SELECT_IN_TABLE_MODE'; +const IN_TEMPLATE_MODE = 'IN_TEMPLATE_MODE'; +const AFTER_BODY_MODE = 'AFTER_BODY_MODE'; +const IN_FRAMESET_MODE = 'IN_FRAMESET_MODE'; +const AFTER_FRAMESET_MODE = 'AFTER_FRAMESET_MODE'; +const AFTER_AFTER_BODY_MODE = 'AFTER_AFTER_BODY_MODE'; +const AFTER_AFTER_FRAMESET_MODE = 'AFTER_AFTER_FRAMESET_MODE'; + +//Insertion mode reset map +const INSERTION_MODE_RESET_MAP = { + [$.TR]: IN_ROW_MODE, + [$.TBODY]: IN_TABLE_BODY_MODE, + [$.THEAD]: IN_TABLE_BODY_MODE, + [$.TFOOT]: IN_TABLE_BODY_MODE, + [$.CAPTION]: IN_CAPTION_MODE, + [$.COLGROUP]: IN_COLUMN_GROUP_MODE, + [$.TABLE]: IN_TABLE_MODE, + [$.BODY]: IN_BODY_MODE, + [$.FRAMESET]: IN_FRAMESET_MODE +}; + +//Template insertion mode switch map +const TEMPLATE_INSERTION_MODE_SWITCH_MAP = { + [$.CAPTION]: IN_TABLE_MODE, + [$.COLGROUP]: IN_TABLE_MODE, + [$.TBODY]: IN_TABLE_MODE, + [$.TFOOT]: IN_TABLE_MODE, + [$.THEAD]: IN_TABLE_MODE, + [$.COL]: IN_COLUMN_GROUP_MODE, + [$.TR]: IN_TABLE_BODY_MODE, + [$.TD]: IN_ROW_MODE, + [$.TH]: IN_ROW_MODE +}; + +//Token handlers map for insertion modes +const TOKEN_HANDLERS = { + [INITIAL_MODE]: { + [Tokenizer.CHARACTER_TOKEN]: tokenInInitialMode, + [Tokenizer.NULL_CHARACTER_TOKEN]: tokenInInitialMode, + [Tokenizer.WHITESPACE_CHARACTER_TOKEN]: ignoreToken, + [Tokenizer.COMMENT_TOKEN]: appendComment, + [Tokenizer.DOCTYPE_TOKEN]: doctypeInInitialMode, + [Tokenizer.START_TAG_TOKEN]: tokenInInitialMode, + [Tokenizer.END_TAG_TOKEN]: tokenInInitialMode, + [Tokenizer.EOF_TOKEN]: tokenInInitialMode + }, + [BEFORE_HTML_MODE]: { + [Tokenizer.CHARACTER_TOKEN]: tokenBeforeHtml, + [Tokenizer.NULL_CHARACTER_TOKEN]: tokenBeforeHtml, + [Tokenizer.WHITESPACE_CHARACTER_TOKEN]: ignoreToken, + [Tokenizer.COMMENT_TOKEN]: appendComment, + [Tokenizer.DOCTYPE_TOKEN]: ignoreToken, + [Tokenizer.START_TAG_TOKEN]: startTagBeforeHtml, + [Tokenizer.END_TAG_TOKEN]: endTagBeforeHtml, + [Tokenizer.EOF_TOKEN]: tokenBeforeHtml + }, + [BEFORE_HEAD_MODE]: { + [Tokenizer.CHARACTER_TOKEN]: tokenBeforeHead, + [Tokenizer.NULL_CHARACTER_TOKEN]: tokenBeforeHead, + [Tokenizer.WHITESPACE_CHARACTER_TOKEN]: ignoreToken, + [Tokenizer.COMMENT_TOKEN]: appendComment, + [Tokenizer.DOCTYPE_TOKEN]: misplacedDoctype, + [Tokenizer.START_TAG_TOKEN]: startTagBeforeHead, + [Tokenizer.END_TAG_TOKEN]: endTagBeforeHead, + [Tokenizer.EOF_TOKEN]: tokenBeforeHead + }, + [IN_HEAD_MODE]: { + [Tokenizer.CHARACTER_TOKEN]: tokenInHead, + [Tokenizer.NULL_CHARACTER_TOKEN]: tokenInHead, + [Tokenizer.WHITESPACE_CHARACTER_TOKEN]: insertCharacters, + [Tokenizer.COMMENT_TOKEN]: appendComment, + [Tokenizer.DOCTYPE_TOKEN]: misplacedDoctype, + [Tokenizer.START_TAG_TOKEN]: startTagInHead, + [Tokenizer.END_TAG_TOKEN]: endTagInHead, + [Tokenizer.EOF_TOKEN]: tokenInHead + }, + [IN_HEAD_NO_SCRIPT_MODE]: { + [Tokenizer.CHARACTER_TOKEN]: tokenInHeadNoScript, + [Tokenizer.NULL_CHARACTER_TOKEN]: tokenInHeadNoScript, + [Tokenizer.WHITESPACE_CHARACTER_TOKEN]: insertCharacters, + [Tokenizer.COMMENT_TOKEN]: appendComment, + [Tokenizer.DOCTYPE_TOKEN]: misplacedDoctype, + [Tokenizer.START_TAG_TOKEN]: startTagInHeadNoScript, + [Tokenizer.END_TAG_TOKEN]: endTagInHeadNoScript, + [Tokenizer.EOF_TOKEN]: tokenInHeadNoScript + }, + [AFTER_HEAD_MODE]: { + [Tokenizer.CHARACTER_TOKEN]: tokenAfterHead, + [Tokenizer.NULL_CHARACTER_TOKEN]: tokenAfterHead, + [Tokenizer.WHITESPACE_CHARACTER_TOKEN]: insertCharacters, + [Tokenizer.COMMENT_TOKEN]: appendComment, + [Tokenizer.DOCTYPE_TOKEN]: misplacedDoctype, + [Tokenizer.START_TAG_TOKEN]: startTagAfterHead, + [Tokenizer.END_TAG_TOKEN]: endTagAfterHead, + [Tokenizer.EOF_TOKEN]: tokenAfterHead + }, + [IN_BODY_MODE]: { + [Tokenizer.CHARACTER_TOKEN]: characterInBody, + [Tokenizer.NULL_CHARACTER_TOKEN]: ignoreToken, + [Tokenizer.WHITESPACE_CHARACTER_TOKEN]: whitespaceCharacterInBody, + [Tokenizer.COMMENT_TOKEN]: appendComment, + [Tokenizer.DOCTYPE_TOKEN]: ignoreToken, + [Tokenizer.START_TAG_TOKEN]: startTagInBody, + [Tokenizer.END_TAG_TOKEN]: endTagInBody, + [Tokenizer.EOF_TOKEN]: eofInBody + }, + [TEXT_MODE]: { + [Tokenizer.CHARACTER_TOKEN]: insertCharacters, + [Tokenizer.NULL_CHARACTER_TOKEN]: insertCharacters, + [Tokenizer.WHITESPACE_CHARACTER_TOKEN]: insertCharacters, + [Tokenizer.COMMENT_TOKEN]: ignoreToken, + [Tokenizer.DOCTYPE_TOKEN]: ignoreToken, + [Tokenizer.START_TAG_TOKEN]: ignoreToken, + [Tokenizer.END_TAG_TOKEN]: endTagInText, + [Tokenizer.EOF_TOKEN]: eofInText + }, + [IN_TABLE_MODE]: { + [Tokenizer.CHARACTER_TOKEN]: characterInTable, + [Tokenizer.NULL_CHARACTER_TOKEN]: characterInTable, + [Tokenizer.WHITESPACE_CHARACTER_TOKEN]: characterInTable, + [Tokenizer.COMMENT_TOKEN]: appendComment, + [Tokenizer.DOCTYPE_TOKEN]: ignoreToken, + [Tokenizer.START_TAG_TOKEN]: startTagInTable, + [Tokenizer.END_TAG_TOKEN]: endTagInTable, + [Tokenizer.EOF_TOKEN]: eofInBody + }, + [IN_TABLE_TEXT_MODE]: { + [Tokenizer.CHARACTER_TOKEN]: characterInTableText, + [Tokenizer.NULL_CHARACTER_TOKEN]: ignoreToken, + [Tokenizer.WHITESPACE_CHARACTER_TOKEN]: whitespaceCharacterInTableText, + [Tokenizer.COMMENT_TOKEN]: tokenInTableText, + [Tokenizer.DOCTYPE_TOKEN]: tokenInTableText, + [Tokenizer.START_TAG_TOKEN]: tokenInTableText, + [Tokenizer.END_TAG_TOKEN]: tokenInTableText, + [Tokenizer.EOF_TOKEN]: tokenInTableText + }, + [IN_CAPTION_MODE]: { + [Tokenizer.CHARACTER_TOKEN]: characterInBody, + [Tokenizer.NULL_CHARACTER_TOKEN]: ignoreToken, + [Tokenizer.WHITESPACE_CHARACTER_TOKEN]: whitespaceCharacterInBody, + [Tokenizer.COMMENT_TOKEN]: appendComment, + [Tokenizer.DOCTYPE_TOKEN]: ignoreToken, + [Tokenizer.START_TAG_TOKEN]: startTagInCaption, + [Tokenizer.END_TAG_TOKEN]: endTagInCaption, + [Tokenizer.EOF_TOKEN]: eofInBody + }, + [IN_COLUMN_GROUP_MODE]: { + [Tokenizer.CHARACTER_TOKEN]: tokenInColumnGroup, + [Tokenizer.NULL_CHARACTER_TOKEN]: tokenInColumnGroup, + [Tokenizer.WHITESPACE_CHARACTER_TOKEN]: insertCharacters, + [Tokenizer.COMMENT_TOKEN]: appendComment, + [Tokenizer.DOCTYPE_TOKEN]: ignoreToken, + [Tokenizer.START_TAG_TOKEN]: startTagInColumnGroup, + [Tokenizer.END_TAG_TOKEN]: endTagInColumnGroup, + [Tokenizer.EOF_TOKEN]: eofInBody + }, + [IN_TABLE_BODY_MODE]: { + [Tokenizer.CHARACTER_TOKEN]: characterInTable, + [Tokenizer.NULL_CHARACTER_TOKEN]: characterInTable, + [Tokenizer.WHITESPACE_CHARACTER_TOKEN]: characterInTable, + [Tokenizer.COMMENT_TOKEN]: appendComment, + [Tokenizer.DOCTYPE_TOKEN]: ignoreToken, + [Tokenizer.START_TAG_TOKEN]: startTagInTableBody, + [Tokenizer.END_TAG_TOKEN]: endTagInTableBody, + [Tokenizer.EOF_TOKEN]: eofInBody + }, + [IN_ROW_MODE]: { + [Tokenizer.CHARACTER_TOKEN]: characterInTable, + [Tokenizer.NULL_CHARACTER_TOKEN]: characterInTable, + [Tokenizer.WHITESPACE_CHARACTER_TOKEN]: characterInTable, + [Tokenizer.COMMENT_TOKEN]: appendComment, + [Tokenizer.DOCTYPE_TOKEN]: ignoreToken, + [Tokenizer.START_TAG_TOKEN]: startTagInRow, + [Tokenizer.END_TAG_TOKEN]: endTagInRow, + [Tokenizer.EOF_TOKEN]: eofInBody + }, + [IN_CELL_MODE]: { + [Tokenizer.CHARACTER_TOKEN]: characterInBody, + [Tokenizer.NULL_CHARACTER_TOKEN]: ignoreToken, + [Tokenizer.WHITESPACE_CHARACTER_TOKEN]: whitespaceCharacterInBody, + [Tokenizer.COMMENT_TOKEN]: appendComment, + [Tokenizer.DOCTYPE_TOKEN]: ignoreToken, + [Tokenizer.START_TAG_TOKEN]: startTagInCell, + [Tokenizer.END_TAG_TOKEN]: endTagInCell, + [Tokenizer.EOF_TOKEN]: eofInBody + }, + [IN_SELECT_MODE]: { + [Tokenizer.CHARACTER_TOKEN]: insertCharacters, + [Tokenizer.NULL_CHARACTER_TOKEN]: ignoreToken, + [Tokenizer.WHITESPACE_CHARACTER_TOKEN]: insertCharacters, + [Tokenizer.COMMENT_TOKEN]: appendComment, + [Tokenizer.DOCTYPE_TOKEN]: ignoreToken, + [Tokenizer.START_TAG_TOKEN]: startTagInSelect, + [Tokenizer.END_TAG_TOKEN]: endTagInSelect, + [Tokenizer.EOF_TOKEN]: eofInBody + }, + [IN_SELECT_IN_TABLE_MODE]: { + [Tokenizer.CHARACTER_TOKEN]: insertCharacters, + [Tokenizer.NULL_CHARACTER_TOKEN]: ignoreToken, + [Tokenizer.WHITESPACE_CHARACTER_TOKEN]: insertCharacters, + [Tokenizer.COMMENT_TOKEN]: appendComment, + [Tokenizer.DOCTYPE_TOKEN]: ignoreToken, + [Tokenizer.START_TAG_TOKEN]: startTagInSelectInTable, + [Tokenizer.END_TAG_TOKEN]: endTagInSelectInTable, + [Tokenizer.EOF_TOKEN]: eofInBody + }, + [IN_TEMPLATE_MODE]: { + [Tokenizer.CHARACTER_TOKEN]: characterInBody, + [Tokenizer.NULL_CHARACTER_TOKEN]: ignoreToken, + [Tokenizer.WHITESPACE_CHARACTER_TOKEN]: whitespaceCharacterInBody, + [Tokenizer.COMMENT_TOKEN]: appendComment, + [Tokenizer.DOCTYPE_TOKEN]: ignoreToken, + [Tokenizer.START_TAG_TOKEN]: startTagInTemplate, + [Tokenizer.END_TAG_TOKEN]: endTagInTemplate, + [Tokenizer.EOF_TOKEN]: eofInTemplate + }, + [AFTER_BODY_MODE]: { + [Tokenizer.CHARACTER_TOKEN]: tokenAfterBody, + [Tokenizer.NULL_CHARACTER_TOKEN]: tokenAfterBody, + [Tokenizer.WHITESPACE_CHARACTER_TOKEN]: whitespaceCharacterInBody, + [Tokenizer.COMMENT_TOKEN]: appendCommentToRootHtmlElement, + [Tokenizer.DOCTYPE_TOKEN]: ignoreToken, + [Tokenizer.START_TAG_TOKEN]: startTagAfterBody, + [Tokenizer.END_TAG_TOKEN]: endTagAfterBody, + [Tokenizer.EOF_TOKEN]: stopParsing + }, + [IN_FRAMESET_MODE]: { + [Tokenizer.CHARACTER_TOKEN]: ignoreToken, + [Tokenizer.NULL_CHARACTER_TOKEN]: ignoreToken, + [Tokenizer.WHITESPACE_CHARACTER_TOKEN]: insertCharacters, + [Tokenizer.COMMENT_TOKEN]: appendComment, + [Tokenizer.DOCTYPE_TOKEN]: ignoreToken, + [Tokenizer.START_TAG_TOKEN]: startTagInFrameset, + [Tokenizer.END_TAG_TOKEN]: endTagInFrameset, + [Tokenizer.EOF_TOKEN]: stopParsing + }, + [AFTER_FRAMESET_MODE]: { + [Tokenizer.CHARACTER_TOKEN]: ignoreToken, + [Tokenizer.NULL_CHARACTER_TOKEN]: ignoreToken, + [Tokenizer.WHITESPACE_CHARACTER_TOKEN]: insertCharacters, + [Tokenizer.COMMENT_TOKEN]: appendComment, + [Tokenizer.DOCTYPE_TOKEN]: ignoreToken, + [Tokenizer.START_TAG_TOKEN]: startTagAfterFrameset, + [Tokenizer.END_TAG_TOKEN]: endTagAfterFrameset, + [Tokenizer.EOF_TOKEN]: stopParsing + }, + [AFTER_AFTER_BODY_MODE]: { + [Tokenizer.CHARACTER_TOKEN]: tokenAfterAfterBody, + [Tokenizer.NULL_CHARACTER_TOKEN]: tokenAfterAfterBody, + [Tokenizer.WHITESPACE_CHARACTER_TOKEN]: whitespaceCharacterInBody, + [Tokenizer.COMMENT_TOKEN]: appendCommentToDocument, + [Tokenizer.DOCTYPE_TOKEN]: ignoreToken, + [Tokenizer.START_TAG_TOKEN]: startTagAfterAfterBody, + [Tokenizer.END_TAG_TOKEN]: tokenAfterAfterBody, + [Tokenizer.EOF_TOKEN]: stopParsing + }, + [AFTER_AFTER_FRAMESET_MODE]: { + [Tokenizer.CHARACTER_TOKEN]: ignoreToken, + [Tokenizer.NULL_CHARACTER_TOKEN]: ignoreToken, + [Tokenizer.WHITESPACE_CHARACTER_TOKEN]: whitespaceCharacterInBody, + [Tokenizer.COMMENT_TOKEN]: appendCommentToDocument, + [Tokenizer.DOCTYPE_TOKEN]: ignoreToken, + [Tokenizer.START_TAG_TOKEN]: startTagAfterAfterFrameset, + [Tokenizer.END_TAG_TOKEN]: ignoreToken, + [Tokenizer.EOF_TOKEN]: stopParsing + } +}; + +//Parser +class Parser { + constructor(options) { + this.options = mergeOptions(DEFAULT_OPTIONS, options); + + this.treeAdapter = this.options.treeAdapter; + this.pendingScript = null; + + if (this.options.sourceCodeLocationInfo) { + Mixin.install(this, LocationInfoParserMixin); + } + + if (this.options.onParseError) { + Mixin.install(this, ErrorReportingParserMixin, { onParseError: this.options.onParseError }); + } + } + + // API + parse(html) { + const document = this.treeAdapter.createDocument(); + + this._bootstrap(document, null); + this.tokenizer.write(html, true); + this._runParsingLoop(null); + + return document; + } + + parseFragment(html, fragmentContext) { + //NOTE: use