├── .github └── ISSUE_TEMPLATE │ ├── bug_report.md │ └── config.yml ├── .gitignore ├── LICENSE ├── README.md ├── bundle.js ├── client ├── dom │ ├── attr.js │ ├── document.js │ ├── element.js │ ├── node.js │ └── style.js ├── events.js ├── history.js ├── hook.js ├── index.js ├── location.js ├── message.js ├── native │ ├── function.js │ └── object.js ├── navigator.js ├── requests │ ├── eventsource.js │ ├── fetch.js │ ├── websocket.js │ └── xhr.js ├── storage.js ├── url.js └── worker.js ├── lib └── uv.bundle.js ├── package-lock.json ├── package.json ├── rewrite ├── codecs.js ├── cookie.js ├── css.js ├── events.js ├── html.js ├── index.js ├── js.js ├── mime.js ├── parsel.js ├── rewrite.css.js ├── rewrite.html.js └── rewrite.script.js └── uv.png /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | Issue tracker is **ONLY** used for reporting bugs. New features should be discussed on our Discord server. 11 | 12 | 13 | 14 | 15 | ## Expected Behavior 16 | 17 | 18 | ## Current Behavior 19 | 20 | 21 | ## Possible Solution 22 | 23 | 24 | ## Steps to Reproduce 25 | 26 | 27 | 1. 28 | 2. 29 | 3. 30 | 4. 31 | 32 | ## Context (Environment) 33 | 34 | 35 | 36 | 37 | 38 | ## Detailed Description 39 | 40 | 41 | ## Possible Implementation 42 | 43 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Community Support 4 | url: https://discord.gg/unblock 5 | about: Please ask and answer questions here. 6 | - name: Heroku, Repl.it, Blocked site issues 7 | url: https://www.youtube.com/watch?v=BLUkgRAy_Vo 8 | about: Do not create issues for these. 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ./node_modules -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Titanium Network 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MOVED to https://github.com/titaniumnetwork-dev/Ultraviolet 2 | -------------------------------------------------------------------------------- /bundle.js: -------------------------------------------------------------------------------- 1 | import webpack from "webpack"; 2 | import path from "path"; 3 | 4 | const __dirname = path.resolve(path.dirname(decodeURI(new URL(import.meta.url).pathname))).slice(3); 5 | 6 | console.log(path.resolve(path.dirname(decodeURI(new URL(import.meta.url).pathname))), __dirname); 7 | 8 | webpack({ 9 | mode: 'none', 10 | entry: path.join(__dirname, './rewrite/index.js'), 11 | output: { 12 | path: __dirname, 13 | filename: './lib/uv.bundle.js', 14 | } 15 | }, (err, i) => 16 | console.log(!err ? 'Ultraviolet bundled!' : e) 17 | ); -------------------------------------------------------------------------------- /client/dom/attr.js: -------------------------------------------------------------------------------- 1 | import EventEmitter from "../events.js"; 2 | import HookEvent from "../hook.js"; 3 | 4 | class AttrApi extends EventEmitter { 5 | constructor(ctx) { 6 | super(); 7 | this.ctx = ctx; 8 | this.window = ctx.window; 9 | this.Attr = this.window.Attr || {}; 10 | this.attrProto = this.Attr.prototype || {}; 11 | this.value = ctx.nativeMethods.getOwnPropertyDescriptor(this.attrProto, 'value'); 12 | this.name = ctx.nativeMethods.getOwnPropertyDescriptor(this.attrProto, 'name'); 13 | this.getNamedItem = this.attrProto.getNamedItem || null; 14 | this.setNamedItem = this.attrProto.setNamedItem || null; 15 | this.removeNamedItem = this.attrProto.removeNamedItem || null; 16 | this.getNamedItemNS = this.attrProto.getNamedItemNS || null; 17 | this.setNamedItemNS = this.attrProto.setNamedItemNS || null; 18 | this.removeNamedItemNS = this.attrProto.removeNamedItemNS || null; 19 | this.item = this.attrProto.item || null; 20 | }; 21 | overrideNameValue() { 22 | this.ctx.overrideDescriptor(this.attrProto, 'name', { 23 | get: (target, that) => { 24 | const event = new HookEvent({ value: target.call(that) }, target, that); 25 | this.emit('name', event); 26 | 27 | if (event.intercepted) return event.returnValue; 28 | return event.data.value; 29 | }, 30 | }); 31 | 32 | this.ctx.overrideDescriptor(this.attrProto, 'value', { 33 | get: (target, that) => { 34 | const event = new HookEvent({ name: this.name.get.call(that), value: target.call(that) }, target, that); 35 | this.emit('getValue', event); 36 | 37 | if (event.intercepted) return event.returnValue; 38 | return event.data.value; 39 | }, 40 | set: (target, that, [ val ]) => { 41 | const event = new HookEvent({ name: this.name.get.call(that), value: val }, target, that); 42 | this.emit('setValue', event); 43 | 44 | if (event.intercepted) return event.returnValue; 45 | event.target.call(event.that, event.data.value); 46 | } 47 | }); 48 | }; 49 | overrideItemMethods() { 50 | this.ctx.override(this.attrProto, 'getNamedItem', (target, that, args) => { 51 | if (!args.length) return target.apply(that, args); 52 | let [ name ] = args; 53 | 54 | const event = new HookEvent({ name }, target, that); 55 | this.emit('getNamedItem', event); 56 | 57 | if (event.intercepted) return event.returnValue; 58 | return event.target.call(event.that, event.data.name); 59 | }); 60 | this.ctx.override(this.attrProto, 'setNamedItem', (target, that, args) => { 61 | if (2 > args.length) return target.apply(that, args); 62 | let [ name, value ] = args; 63 | 64 | const event = new HookEvent({ name, value }, target, that); 65 | this.emit('setNamedItem', event); 66 | 67 | if (event.intercepted) return event.returnValue; 68 | return event.target.call(event.that, event.data.name, event.data.value); 69 | }); 70 | this.ctx.override(this.attrProto, 'removeNamedItem', (target, that, args) => { 71 | if (!args.length) return target.apply(that, args); 72 | let [ name ] = args; 73 | 74 | const event = new HookEvent({ name }, target, that); 75 | this.emit('removeNamedItem', event); 76 | 77 | if (event.intercepted) return event.returnValue; 78 | return event.target.call(event.that, event.data.name); 79 | }); 80 | this.ctx.override(this.attrProto, 'item', (target, that, args) => { 81 | if (!args.length) return target.apply(that, args); 82 | let [ index ] = args; 83 | 84 | const event = new HookEvent({ index }, target, that); 85 | this.emit('item', event); 86 | 87 | if (event.intercepted) return event.returnValue; 88 | return event.target.call(event.that, event.data.name); 89 | }); 90 | this.ctx.override(this.attrProto, 'getNamedItemNS', (target, that, args) => { 91 | if (2 > args.length) return target.apply(that, args); 92 | let [ namespace, localName ] = args; 93 | 94 | const event = new HookEvent({ namespace, localName }, target, that); 95 | this.emit('getNamedItemNS', event); 96 | 97 | if (event.intercepted) return event.returnValue; 98 | return event.target.call(event.that, event.data.namespace, event.data.localName); 99 | }); 100 | this.ctx.override(this.attrProto, 'setNamedItemNS', (target, that, args) => { 101 | if (!args.length) return target.apply(that, args); 102 | let [ attr ] = args; 103 | 104 | const event = new HookEvent({ attr }, target, that); 105 | this.emit('setNamedItemNS', event); 106 | 107 | if (event.intercepted) return event.returnValue; 108 | return event.target.call(event.that, event.data.name); 109 | }); 110 | this.ctx.override(this.attrProto, 'removeNamedItemNS', (target, that, args) => { 111 | if (2 > args.length) return target.apply(that, args); 112 | let [ namespace, localName ] = args; 113 | 114 | const event = new HookEvent({ namespace, localName }, target, that); 115 | this.emit('removeNamedItemNS', event); 116 | 117 | if (event.intercepted) return event.returnValue; 118 | return event.target.call(event.that, event.data.namespace, event.data.localName); 119 | }); 120 | }; 121 | }; 122 | 123 | export default AttrApi; -------------------------------------------------------------------------------- /client/dom/document.js: -------------------------------------------------------------------------------- 1 | import EventEmitter from "../events.js"; 2 | import HookEvent from "../hook.js"; 3 | 4 | class DocumentHook extends EventEmitter { 5 | constructor(ctx) { 6 | super(); 7 | this.ctx = ctx; 8 | this.window = ctx.window; 9 | this.document = this.window.document; 10 | this.Document = this.window.Document || {}; 11 | this.DOMParser = this.window.DOMParser || {}; 12 | this.docProto = this.Document.prototype || {}; 13 | this.domProto = this.DOMParser.prototype || {}; 14 | this.title = ctx.nativeMethods.getOwnPropertyDescriptor(this.docProto, 'title'); 15 | this.cookie = ctx.nativeMethods.getOwnPropertyDescriptor(this.docProto, 'cookie'); 16 | this.referrer = ctx.nativeMethods.getOwnPropertyDescriptor(this.docProto, 'referrer'); 17 | this.domain = ctx.nativeMethods.getOwnPropertyDescriptor(this.docProto, 'domain'); 18 | this.documentURI = ctx.nativeMethods.getOwnPropertyDescriptor(this.docProto, 'documentURI'); 19 | this.write = this.docProto.write; 20 | this.writeln = this.docProto.writeln; 21 | this.querySelector = this.docProto.querySelector; 22 | this.querySelectorAll = this.docProto.querySelectorAll; 23 | this.parseFromString = this.domProto.parseFromString; 24 | this.URL = ctx.nativeMethods.getOwnPropertyDescriptor(this.docProto, 'URL'); 25 | }; 26 | overrideParseFromString() { 27 | this.ctx.override(this.domProto, 'parseFromString', (target, that, args) => { 28 | if (2 > args.length) return target.apply(that, args); 29 | let [ string, type ] = args; 30 | 31 | const event = new HookEvent({ string, type }, target, that); 32 | this.emit('parseFromString', event); 33 | 34 | if (event.intercepted) return event.returnValue; 35 | return event.target.call(event.that, event.data.string, event.data.type); 36 | }); 37 | }; 38 | overrideQuerySelector() { 39 | this.ctx.override(this.docProto, 'querySelector', (target, that, args) => { 40 | if (!args.length) return target.apply(that, args); 41 | let [ selectors ] = args; 42 | 43 | const event = new HookEvent({ selectors }, target, that); 44 | this.emit('querySelector', event); 45 | 46 | if (event.intercepted) return event.returnValue; 47 | return event.target.call(event.that, event.data.selectors); 48 | }); 49 | }; 50 | overrideDomain() { 51 | this.ctx.overrideDescriptor(this.docProto, 'domain', { 52 | get: (target, that) => { 53 | const event = new HookEvent({ value: target.call(that) }, target, that); 54 | this.emit('getDomain', event); 55 | 56 | if (event.intercepted) return event.returnValue; 57 | return event.data.value; 58 | }, 59 | set: (target, that, [ val ]) => { 60 | const event = new HookEvent({ value: val }, target, that); 61 | this.emit('setDomain', event); 62 | 63 | if (event.intercepted) return event.returnValue; 64 | return event.target.call(event.that, event.data.value); 65 | }, 66 | }); 67 | }; 68 | overrideReferrer() { 69 | this.ctx.overrideDescriptor(this.docProto, 'referrer', { 70 | get: (target, that) => { 71 | const event = new HookEvent({ value: target.call(that) }, target, that); 72 | this.emit('referrer', event); 73 | 74 | if (event.intercepted) return event.returnValue; 75 | return event.data.value; 76 | }, 77 | }); 78 | }; 79 | overrideCreateTreeWalker() { 80 | this.ctx.override(this.docProto, 'createTreeWalker', (target, that, args) => { 81 | if (!args.length) return target.apply(that, args); 82 | let [ root, show = 0xFFFFFFFF, filter, expandEntityReferences ] = args; 83 | 84 | const event = new HookEvent({ root, show, filter, expandEntityReferences }, target, that); 85 | this.emit('createTreeWalker', event); 86 | 87 | if (event.intercepted) return event.returnValue; 88 | return event.target.call(event.that, event.data.root, event.data.show, event.data.filter, event.data.expandEntityReferences); 89 | }); 90 | }; 91 | overrideWrite() { 92 | this.ctx.override(this.docProto, 'write', (target, that, args) => { 93 | if (!args.length) return target.apply(that, args); 94 | let [ ...html ] = args; 95 | 96 | const event = new HookEvent({ html }, target, that); 97 | this.emit('write', event); 98 | 99 | if (event.intercepted) return event.returnValue; 100 | return event.target.apply(event.that, event.data.html); 101 | }); 102 | this.ctx.override(this.docProto, 'writeln', (target, that, args) => { 103 | if (!args.length) return target.apply(that, args); 104 | let [ ...html ] = args; 105 | 106 | const event = new HookEvent({ html }, target, that); 107 | this.emit('writeln', event); 108 | 109 | if (event.intercepted) return event.returnValue; 110 | return event.target.apply(event.that, event.data.html); 111 | }); 112 | }; 113 | overrideDocumentURI() { 114 | this.ctx.overrideDescriptor(this.docProto, 'documentURI', { 115 | get: (target, that) => { 116 | const event = new HookEvent({ value: target.call(that) }, target, that); 117 | this.emit('documentURI', event); 118 | 119 | if (event.intercepted) return event.returnValue; 120 | return event.data.value; 121 | }, 122 | }); 123 | }; 124 | overrideURL() { 125 | this.ctx.overrideDescriptor(this.docProto, 'URL', { 126 | get: (target, that) => { 127 | const event = new HookEvent({ value: target.call(that) }, target, that); 128 | this.emit('url', event); 129 | 130 | if (event.intercepted) return event.returnValue; 131 | return event.data.value; 132 | }, 133 | }); 134 | }; 135 | overrideReferrer() { 136 | this.ctx.overrideDescriptor(this.docProto, 'referrer', { 137 | get: (target, that) => { 138 | const event = new HookEvent({ value: target.call(that) }, target, that); 139 | this.emit('referrer', event); 140 | 141 | if (event.intercepted) return event.returnValue; 142 | return event.data.value; 143 | }, 144 | }); 145 | }; 146 | overrideCookie() { 147 | this.ctx.overrideDescriptor(this.docProto, 'cookie', { 148 | get: (target, that) => { 149 | const event = new HookEvent({ value: target.call(that) }, target, that); 150 | this.emit('getCookie', event); 151 | 152 | if (event.intercepted) return event.returnValue; 153 | return event.data.value; 154 | }, 155 | set: (target, that, [ value ]) => { 156 | const event = new HookEvent({ value, }, target, that); 157 | this.emit('setCookie', event); 158 | 159 | if (event.intercepted) return event.returnValue; 160 | return event.target.call(event.that, event.data.value); 161 | }, 162 | }); 163 | }; 164 | overrideTitle() { 165 | this.ctx.overrideDescriptor(this.docProto, 'title', { 166 | get: (target, that) => { 167 | const event = new HookEvent({ value: target.call(that) }, target, that); 168 | this.emit('getTitle', event); 169 | 170 | if (event.intercepted) return event.returnValue; 171 | return event.data.value; 172 | }, 173 | set: (target, that, [ value ]) => { 174 | const event = new HookEvent({ value, }, target, that); 175 | this.emit('setTitle', event); 176 | 177 | if (event.intercepted) return event.returnValue; 178 | return event.target.call(event.that, event.data.value); 179 | }, 180 | }); 181 | }; 182 | }; 183 | 184 | export default DocumentHook; -------------------------------------------------------------------------------- /client/dom/element.js: -------------------------------------------------------------------------------- 1 | import EventEmitter from "../events.js"; 2 | import HookEvent from "../hook.js"; 3 | 4 | class ElementApi extends EventEmitter { 5 | constructor(ctx) { 6 | super(); 7 | this.ctx = ctx; 8 | this.window = ctx.window; 9 | this.Audio = this.window.Audio; 10 | this.Element = this.window.Element; 11 | this.elemProto = this.Element ? this.Element.prototype : {}; 12 | this.innerHTML = ctx.nativeMethods.getOwnPropertyDescriptor(this.elemProto, 'innerHTML'); 13 | this.outerHTML = ctx.nativeMethods.getOwnPropertyDescriptor(this.elemProto, 'outerHTML'); 14 | this.setAttribute = this.elemProto.setAttribute; 15 | this.getAttribute = this.elemProto.getAttribute; 16 | this.removeAttribute = this.elemProto.removeAttribute; 17 | this.hasAttribute = this.elemProto.hasAttribute; 18 | this.querySelector = this.elemProto.querySelector; 19 | this.querySelectorAll = this.elemProto.querySelectorAll; 20 | this.insertAdjacentHTML = this.elemProto.insertAdjacentHTML; 21 | this.insertAdjacentText = this.elemProto.insertAdjacentText; 22 | }; 23 | overrideQuerySelector() { 24 | this.ctx.override(this.elemProto, 'querySelector', (target, that, args) => { 25 | if (!args.length) return target.apply(that, args); 26 | let [ selectors ] = args; 27 | 28 | const event = new HookEvent({ selectors }, target, that); 29 | this.emit('querySelector', event); 30 | 31 | if (event.intercepted) return event.returnValue; 32 | return event.target.call(event.that, event.data.selectors); 33 | }); 34 | }; 35 | overrideAttribute() { 36 | this.ctx.override(this.elemProto, 'getAttribute', (target, that, args) => { 37 | if (!args.length) return target.apply(that, args); 38 | let [ name ] = args; 39 | 40 | const event = new HookEvent({ name }, target, that); 41 | this.emit('getAttribute', event); 42 | 43 | if (event.intercepted) return event.returnValue; 44 | return event.target.call(event.that, event.data.name); 45 | }); 46 | this.ctx.override(this.elemProto, 'setAttribute', (target, that, args) => { 47 | if (2 > args.length) return target.apply(that, args); 48 | let [ name, value ] = args; 49 | 50 | const event = new HookEvent({ name, value }, target, that); 51 | this.emit('setAttribute', event); 52 | 53 | if (event.intercepted) return event.returnValue; 54 | return event.target.call(event.that, event.data.name, event.data.value); 55 | }); 56 | this.ctx.override(this.elemProto, 'hasAttribute', (target, that, args) => { 57 | if (!args.length) return target.apply(that, args); 58 | let [ name ] = args; 59 | 60 | const event = new HookEvent({ name }, target, that); 61 | this.emit('hasAttribute', event); 62 | 63 | if (event.intercepted) return event.returnValue; 64 | return event.target.call(event.that, event.data.name); 65 | }); 66 | this.ctx.override(this.elemProto, 'removeAttribute', (target, that, args) => { 67 | if (!args.length) return target.apply(that, args); 68 | let [ name ] = args; 69 | 70 | const event = new HookEvent({ name }, target, that); 71 | this.emit('removeAttribute', event); 72 | 73 | if (event.intercepted) return event.returnValue; 74 | return event.target.call(event.that, event.data.name); 75 | }); 76 | }; 77 | overrideAudio() { 78 | this.ctx.override(this.window, 'Audio', (target, that, args) => { 79 | if (!args.length) return new target(...args); 80 | let [ url ] = args; 81 | 82 | const event = new HookEvent({ url }, target, that); 83 | this.emit('audio', event); 84 | 85 | if (event.intercepted) return event.returnValue; 86 | return new event.target(event.data.url); 87 | }, true); 88 | }; 89 | overrideHtml() { 90 | this.hookProperty(this.Element, 'innerHTML', { 91 | get: (target, that) => { 92 | const event = new HookEvent({ value: target.call(that) }, target, that); 93 | this.emit('getInnerHTML', event); 94 | 95 | if (event.intercepted) return event.returnValue; 96 | return event.data.value; 97 | }, 98 | set: (target, that, [ val ]) => { 99 | const event = new HookEvent({ value: val }, target, that); 100 | this.emit('setInnerHTML', event); 101 | 102 | if (event.intercepted) return event.returnValue; 103 | target.call(that, event.data.value); 104 | }, 105 | }); 106 | this.hookProperty(this.Element, 'outerHTML', { 107 | get: (target, that) => { 108 | const event = new HookEvent({ value: target.call(that) }, target, that); 109 | this.emit('getOuterHTML', event); 110 | 111 | if (event.intercepted) return event.returnValue; 112 | return event.data.value; 113 | }, 114 | set: (target, that, [ val ]) => { 115 | const event = new HookEvent({ value: val }, target, that); 116 | this.emit('setOuterHTML', event); 117 | 118 | if (event.intercepted) return event.returnValue; 119 | target.call(that, event.data.value); 120 | }, 121 | }); 122 | }; 123 | overrideInsertAdjacentHTML() { 124 | this.ctx.override(this.elemProto, 'insertAdjacentHTML', (target, that, args) => { 125 | if (2 > args.length) return target.apply(that, args); 126 | let [ position, html ] = args; 127 | 128 | const event = new HookEvent({ position, html }, target, that); 129 | this.emit('insertAdjacentHTML', event); 130 | 131 | if (event.intercepted) return event.returnValue; 132 | return event.target.call(event.that, event.data.position, event.data.html); 133 | }); 134 | }; 135 | overrideInsertAdjacentText() { 136 | this.ctx.override(this.elemProto, 'insertAdjacentText', (target, that, args) => { 137 | if (2 > args.length) return target.apply(that, args); 138 | let [ position, text ] = args; 139 | 140 | const event = new HookEvent({ position, text }, target, that); 141 | this.emit('insertAdjacentText', event); 142 | 143 | if (event.intercepted) return event.returnValue; 144 | return event.target.call(event.that, event.data.position, event.data.text); 145 | }); 146 | }; 147 | hookProperty(element, prop, handler) { 148 | if (!element || !prop in element) return false; 149 | 150 | if (this.ctx.nativeMethods.isArray(element)) { 151 | for (const elem of element) { 152 | this.hookProperty(elem, prop, handler); 153 | }; 154 | return true; 155 | }; 156 | 157 | const proto = element.prototype; 158 | 159 | this.ctx.overrideDescriptor(proto, prop, handler); 160 | 161 | return true; 162 | }; 163 | }; 164 | 165 | export default ElementApi; -------------------------------------------------------------------------------- /client/dom/node.js: -------------------------------------------------------------------------------- 1 | import EventEmitter from "../events.js"; 2 | import HookEvent from "../hook.js"; 3 | 4 | class NodeApi extends EventEmitter { 5 | constructor(ctx) { 6 | super(); 7 | this.ctx = ctx; 8 | this.window = ctx.window; 9 | this.Node = ctx.window.Node || {}; 10 | this.nodeProto = this.Node.prototype || {}; 11 | this.compareDocumentPosition = this.nodeProto.compareDocumentPosition; 12 | this.contains = this.nodeProto.contains; 13 | this.insertBefore = this.nodeProto.insertBefore; 14 | this.replaceChild = this.nodeProto.replaceChild; 15 | this.append = this.nodeProto.append; 16 | this.appendChild = this.nodeProto.appendChild; 17 | this.removeChild = this.nodeProto.removeChild; 18 | 19 | this.textContent = ctx.nativeMethods.getOwnPropertyDescriptor(this.nodeProto, 'textContent'); 20 | this.parentNode = ctx.nativeMethods.getOwnPropertyDescriptor(this.nodeProto, 'parentNode'); 21 | this.parentElement = ctx.nativeMethods.getOwnPropertyDescriptor(this.nodeProto, 'parentElement'); 22 | this.childNodes = ctx.nativeMethods.getOwnPropertyDescriptor(this.nodeProto, 'childNodes'); 23 | this.baseURI = ctx.nativeMethods.getOwnPropertyDescriptor(this.nodeProto, 'baseURI'); 24 | this.previousSibling = ctx.nativeMethods.getOwnPropertyDescriptor(this.nodeProto, 'previousSibling'); 25 | this.ownerDocument = ctx.nativeMethods.getOwnPropertyDescriptor(this.nodeProto, 'ownerDocument'); 26 | }; 27 | overrideTextContent() { 28 | this.ctx.overrideDescriptor(this.nodeProto, 'textContent', { 29 | get: (target, that) => { 30 | const event = new HookEvent({ value: target.call(that) }, target, that); 31 | this.emit('getTextContent', event); 32 | 33 | if (event.intercepted) return event.returnValue; 34 | return event.data.value; 35 | }, 36 | set: (target, that, [ val ]) => { 37 | const event = new HookEvent({ value: val }, target, that); 38 | this.emit('setTextContent', event); 39 | 40 | if (event.intercepted) return event.returnValue; 41 | target.call(that, event.data.value); 42 | }, 43 | }); 44 | }; 45 | overrideAppend() { 46 | this.ctx.override(this.nodeProto, 'append', (target, that, [ ...nodes ]) => { 47 | const event = new HookEvent({ nodes }, target, that); 48 | this.emit('append', event); 49 | 50 | if (event.intercepted) return event.returnValue; 51 | return event.target.call(event.that, event.data.nodes); 52 | }); 53 | this.ctx.override(this.nodeProto, 'appendChild', (target, that, args) => { 54 | if (!args.length) return target.apply(that, args); 55 | let [ node ] = args; 56 | 57 | const event = new HookEvent({ node }, target, that); 58 | this.emit('appendChild', event); 59 | 60 | if (event.intercepted) return event.returnValue; 61 | return event.target.call(event.that, event.data.node); 62 | }); 63 | }; 64 | overrideBaseURI() { 65 | this.ctx.overrideDescriptor(this.nodeProto, 'baseURI', { 66 | get: (target, that) => { 67 | const event = new HookEvent({ value: target.call(that) }, target, that); 68 | this.emit('baseURI', event); 69 | 70 | if (event.intercepted) return event.returnValue; 71 | return event.data.value; 72 | }, 73 | }) 74 | }; 75 | overrideParent() { 76 | this.ctx.overrideDescriptor(this.nodeProto, 'parentNode', { 77 | get: (target, that) => { 78 | const event = new HookEvent({ node: target.call(that) }, target, that); 79 | this.emit('parentNode', event); 80 | 81 | if (event.intercepted) return event.returnValue; 82 | return event.data.node; 83 | }, 84 | }); 85 | this.ctx.overrideDescriptor(this.nodeProto, 'parentElement', { 86 | get: (target, that) => { 87 | const event = new HookEvent({ element: target.call(that) }, target, that); 88 | this.emit('parentElement', event); 89 | 90 | if (event.intercepted) return event.returnValue; 91 | return event.data.node; 92 | }, 93 | }); 94 | }; 95 | overrideOwnerDocument() { 96 | this.ctx.overrideDescriptor(this.nodeProto, 'ownerDocument', { 97 | get: (target, that) => { 98 | const event = new HookEvent({ document: target.call(that) }, target, that); 99 | this.emit('ownerDocument', event); 100 | 101 | if (event.intercepted) return event.returnValue; 102 | return event.data.document; 103 | }, 104 | }); 105 | }; 106 | overrideCompareDocumentPosit1ion() { 107 | this.ctx.override(this.nodeProto, 'compareDocumentPosition', (target, that, args) => { 108 | if (!args.length) return target.apply(that, args); 109 | let [ node ] = args; 110 | const event = new HookEvent({ node }, target, that); 111 | 112 | if (event.intercepted) return event.returnValue; 113 | return event.target.call(event.that, event.data.node); 114 | }); 115 | }; 116 | overrideChildMethods() { 117 | this.ctx.override(this.nodeProto, 'removeChild') 118 | }; 119 | }; 120 | 121 | export default NodeApi; -------------------------------------------------------------------------------- /client/dom/style.js: -------------------------------------------------------------------------------- 1 | import EventEmitter from "../events.js"; 2 | import HookEvent from "../hook.js"; 3 | 4 | class StyleApi extends EventEmitter { 5 | constructor(ctx) { 6 | super(); 7 | this.ctx = ctx; 8 | this.window = ctx.window; 9 | this.CSSStyleDeclaration = this.window.CSSStyleDeclaration || {}; 10 | this.cssStyleProto = this.CSSStyleDeclaration.prototype || {}; 11 | this.getPropertyValue = this.cssStyleProto.getPropertyValue || null; 12 | this.setProperty = this.cssStyleProto.setProperty || null; 13 | this.cssText - ctx.nativeMethods.getOwnPropertyDescriptors(this.cssStyleProto, 'cssText'); 14 | this.urlProps = ['background', 'backgroundImage', 'borderImage', 'borderImageSource', 'listStyle', 'listStyleImage', 'cursor']; 15 | this.dashedUrlProps = ['background', 'background-image', 'border-image', 'border-image-source', 'list-style', 'list-style-image', 'cursor']; 16 | this.propToDashed = { 17 | background: 'background', 18 | backgroundImage: 'background-image', 19 | borderImage: 'border-image', 20 | borderImageSource: 'border-image-source', 21 | listStyle: 'list-style', 22 | listStyleImage: 'list-style-image', 23 | cursor: 'cursor' 24 | }; 25 | }; 26 | overrideSetGetProperty() { 27 | this.ctx.override(this.cssStyleProto, 'getPropertyValue', (target, that, args) => { 28 | if (!args.length) return target.apply(that, args); 29 | 30 | let [ property ] = args; 31 | 32 | const event = new HookEvent({ property }, target, that); 33 | this.emit('getPropertyValue', event); 34 | 35 | if (event.intercepted) return event.returnValue; 36 | return event.target.call(event.that, event.data.property); 37 | }); 38 | this.ctx.override(this.cssStyleProto, 'setProperty', (target, that, args) => { 39 | if (2 > args.length) return target.apply(that, args); 40 | let [ property, value ] = args; 41 | 42 | const event = new HookEvent({ property, value }, target, that); 43 | this.emit('setProperty', event); 44 | 45 | if (event.intercepted) return event.returnValue; 46 | return event.target.call(event.that, event.data.property, event.data.value); 47 | }); 48 | }; 49 | overrideCssText() { 50 | this.ctx.overrideDescriptor(this.cssStyleProto, 'cssText', { 51 | get: (target, that) => { 52 | const event = new HookEvent({ value: target.call(that) }, target, that); 53 | this.emit('getCssText', event); 54 | 55 | if (event.intercepted) return event.returnValue; 56 | return event.data.value; 57 | }, 58 | set: (target, that, [ val ]) => { 59 | const event = new HookEvent({ value: val }, target, that); 60 | this.emit('setCssText', event); 61 | 62 | if (event.intercepted) return event.returnValue; 63 | return event.target.call(event.that, event.data.value); 64 | }, 65 | }); 66 | }; 67 | }; 68 | 69 | export default StyleApi; -------------------------------------------------------------------------------- /client/events.js: -------------------------------------------------------------------------------- 1 | // Copyright Joyent, Inc. and other Node contributors. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a 4 | // copy of this software and associated documentation files (the 5 | // "Software"), to deal in the Software without restriction, including 6 | // without limitation the rights to use, copy, modify, merge, publish, 7 | // distribute, sublicense, and/or sell copies of the Software, and to permit 8 | // persons to whom the Software is furnished to do so, subject to the 9 | // following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included 12 | // in all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 15 | // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN 17 | // NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 18 | // DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 19 | // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE 20 | // USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | 22 | 'use strict'; 23 | 24 | var R = typeof Reflect === 'object' ? Reflect : null 25 | var ReflectApply = R && typeof R.apply === 'function' 26 | ? R.apply 27 | : function ReflectApply(target, receiver, args) { 28 | return Function.prototype.apply.call(target, receiver, args); 29 | } 30 | 31 | var ReflectOwnKeys 32 | if (R && typeof R.ownKeys === 'function') { 33 | ReflectOwnKeys = R.ownKeys 34 | } else if (Object.getOwnPropertySymbols) { 35 | ReflectOwnKeys = function ReflectOwnKeys(target) { 36 | return Object.getOwnPropertyNames(target) 37 | .concat(Object.getOwnPropertySymbols(target)); 38 | }; 39 | } else { 40 | ReflectOwnKeys = function ReflectOwnKeys(target) { 41 | return Object.getOwnPropertyNames(target); 42 | }; 43 | } 44 | 45 | function ProcessEmitWarning(warning) { 46 | if (console && console.warn) console.warn(warning); 47 | } 48 | 49 | var NumberIsNaN = Number.isNaN || function NumberIsNaN(value) { 50 | return value !== value; 51 | } 52 | 53 | function EventEmitter() { 54 | EventEmitter.init.call(this); 55 | } 56 | 57 | export default EventEmitter; 58 | 59 | // Backwards-compat with node 0.10.x 60 | EventEmitter.EventEmitter = EventEmitter; 61 | 62 | EventEmitter.prototype._events = undefined; 63 | EventEmitter.prototype._eventsCount = 0; 64 | EventEmitter.prototype._maxListeners = undefined; 65 | 66 | // By default EventEmitters will print a warning if more than 10 listeners are 67 | // added to it. This is a useful default which helps finding memory leaks. 68 | var defaultMaxListeners = 10; 69 | 70 | function checkListener(listener) { 71 | if (typeof listener !== 'function') { 72 | throw new TypeError('The "listener" argument must be of type Function. Received type ' + typeof listener); 73 | } 74 | } 75 | 76 | Object.defineProperty(EventEmitter, 'defaultMaxListeners', { 77 | enumerable: true, 78 | get: function() { 79 | return defaultMaxListeners; 80 | }, 81 | set: function(arg) { 82 | if (typeof arg !== 'number' || arg < 0 || NumberIsNaN(arg)) { 83 | throw new RangeError('The value of "defaultMaxListeners" is out of range. It must be a non-negative number. Received ' + arg + '.'); 84 | } 85 | defaultMaxListeners = arg; 86 | } 87 | }); 88 | 89 | EventEmitter.init = function() { 90 | 91 | if (this._events === undefined || 92 | this._events === Object.getPrototypeOf(this)._events) { 93 | this._events = Object.create(null); 94 | this._eventsCount = 0; 95 | } 96 | 97 | this._maxListeners = this._maxListeners || undefined; 98 | }; 99 | 100 | // Obviously not all Emitters should be limited to 10. This function allows 101 | // that to be increased. Set to zero for unlimited. 102 | EventEmitter.prototype.setMaxListeners = function setMaxListeners(n) { 103 | if (typeof n !== 'number' || n < 0 || NumberIsNaN(n)) { 104 | throw new RangeError('The value of "n" is out of range. It must be a non-negative number. Received ' + n + '.'); 105 | } 106 | this._maxListeners = n; 107 | return this; 108 | }; 109 | 110 | function _getMaxListeners(that) { 111 | if (that._maxListeners === undefined) 112 | return EventEmitter.defaultMaxListeners; 113 | return that._maxListeners; 114 | } 115 | 116 | EventEmitter.prototype.getMaxListeners = function getMaxListeners() { 117 | return _getMaxListeners(this); 118 | }; 119 | 120 | EventEmitter.prototype.emit = function emit(type) { 121 | var args = []; 122 | for (var i = 1; i < arguments.length; i++) args.push(arguments[i]); 123 | var doError = (type === 'error'); 124 | 125 | var events = this._events; 126 | if (events !== undefined) 127 | doError = (doError && events.error === undefined); 128 | else if (!doError) 129 | return false; 130 | 131 | // If there is no 'error' event listener then throw. 132 | if (doError) { 133 | var er; 134 | if (args.length > 0) 135 | er = args[0]; 136 | if (er instanceof Error) { 137 | // Note: The comments on the `throw` lines are intentional, they show 138 | // up in Node's output if this results in an unhandled exception. 139 | throw er; // Unhandled 'error' event 140 | } 141 | // At least give some kind of context to the user 142 | var err = new Error('Unhandled error.' + (er ? ' (' + er.message + ')' : '')); 143 | err.context = er; 144 | throw err; // Unhandled 'error' event 145 | } 146 | 147 | var handler = events[type]; 148 | 149 | if (handler === undefined) 150 | return false; 151 | 152 | if (typeof handler === 'function') { 153 | ReflectApply(handler, this, args); 154 | } else { 155 | var len = handler.length; 156 | var listeners = arrayClone(handler, len); 157 | for (var i = 0; i < len; ++i) 158 | ReflectApply(listeners[i], this, args); 159 | } 160 | 161 | return true; 162 | }; 163 | 164 | function _addListener(target, type, listener, prepend) { 165 | var m; 166 | var events; 167 | var existing; 168 | 169 | checkListener(listener); 170 | 171 | events = target._events; 172 | if (events === undefined) { 173 | events = target._events = Object.create(null); 174 | target._eventsCount = 0; 175 | } else { 176 | // To avoid recursion in the case that type === "newListener"! Before 177 | // adding it to the listeners, first emit "newListener". 178 | if (events.newListener !== undefined) { 179 | target.emit('newListener', type, 180 | listener.listener ? listener.listener : listener); 181 | 182 | // Re-assign `events` because a newListener handler could have caused the 183 | // this._events to be assigned to a new object 184 | events = target._events; 185 | } 186 | existing = events[type]; 187 | } 188 | 189 | if (existing === undefined) { 190 | // Optimize the case of one listener. Don't need the extra array object. 191 | existing = events[type] = listener; 192 | ++target._eventsCount; 193 | } else { 194 | if (typeof existing === 'function') { 195 | // Adding the second element, need to change to array. 196 | existing = events[type] = 197 | prepend ? [listener, existing] : [existing, listener]; 198 | // If we've already got an array, just append. 199 | } else if (prepend) { 200 | existing.unshift(listener); 201 | } else { 202 | existing.push(listener); 203 | } 204 | 205 | // Check for listener leak 206 | m = _getMaxListeners(target); 207 | if (m > 0 && existing.length > m && !existing.warned) { 208 | existing.warned = true; 209 | // No error code for this since it is a Warning 210 | // eslint-disable-next-line no-restricted-syntax 211 | var w = new Error('Possible EventEmitter memory leak detected. ' + 212 | existing.length + ' ' + String(type) + ' listeners ' + 213 | 'added. Use emitter.setMaxListeners() to ' + 214 | 'increase limit'); 215 | w.name = 'MaxListenersExceededWarning'; 216 | w.emitter = target; 217 | w.type = type; 218 | w.count = existing.length; 219 | ProcessEmitWarning(w); 220 | } 221 | } 222 | 223 | return target; 224 | } 225 | 226 | EventEmitter.prototype.addListener = function addListener(type, listener) { 227 | return _addListener(this, type, listener, false); 228 | }; 229 | 230 | EventEmitter.prototype.on = EventEmitter.prototype.addListener; 231 | 232 | EventEmitter.prototype.prependListener = 233 | function prependListener(type, listener) { 234 | return _addListener(this, type, listener, true); 235 | }; 236 | 237 | function onceWrapper() { 238 | if (!this.fired) { 239 | this.target.removeListener(this.type, this.wrapFn); 240 | this.fired = true; 241 | if (arguments.length === 0) 242 | return this.listener.call(this.target); 243 | return this.listener.apply(this.target, arguments); 244 | } 245 | } 246 | 247 | function _onceWrap(target, type, listener) { 248 | var state = { fired: false, wrapFn: undefined, target: target, type: type, listener: listener }; 249 | var wrapped = onceWrapper.bind(state); 250 | wrapped.listener = listener; 251 | state.wrapFn = wrapped; 252 | return wrapped; 253 | } 254 | 255 | EventEmitter.prototype.once = function once(type, listener) { 256 | checkListener(listener); 257 | this.on(type, _onceWrap(this, type, listener)); 258 | return this; 259 | }; 260 | 261 | EventEmitter.prototype.prependOnceListener = 262 | function prependOnceListener(type, listener) { 263 | checkListener(listener); 264 | this.prependListener(type, _onceWrap(this, type, listener)); 265 | return this; 266 | }; 267 | 268 | // Emits a 'removeListener' event if and only if the listener was removed. 269 | EventEmitter.prototype.removeListener = 270 | function removeListener(type, listener) { 271 | var list, events, position, i, originalListener; 272 | 273 | checkListener(listener); 274 | 275 | events = this._events; 276 | if (events === undefined) 277 | return this; 278 | 279 | list = events[type]; 280 | if (list === undefined) 281 | return this; 282 | 283 | if (list === listener || list.listener === listener) { 284 | if (--this._eventsCount === 0) 285 | this._events = Object.create(null); 286 | else { 287 | delete events[type]; 288 | if (events.removeListener) 289 | this.emit('removeListener', type, list.listener || listener); 290 | } 291 | } else if (typeof list !== 'function') { 292 | position = -1; 293 | 294 | for (i = list.length - 1; i >= 0; i--) { 295 | if (list[i] === listener || list[i].listener === listener) { 296 | originalListener = list[i].listener; 297 | position = i; 298 | break; 299 | } 300 | } 301 | 302 | if (position < 0) 303 | return this; 304 | 305 | if (position === 0) 306 | list.shift(); 307 | else { 308 | spliceOne(list, position); 309 | } 310 | 311 | if (list.length === 1) 312 | events[type] = list[0]; 313 | 314 | if (events.removeListener !== undefined) 315 | this.emit('removeListener', type, originalListener || listener); 316 | } 317 | 318 | return this; 319 | }; 320 | 321 | EventEmitter.prototype.off = EventEmitter.prototype.removeListener; 322 | 323 | EventEmitter.prototype.removeAllListeners = 324 | function removeAllListeners(type) { 325 | var listeners, events, i; 326 | 327 | events = this._events; 328 | if (events === undefined) 329 | return this; 330 | 331 | // not listening for removeListener, no need to emit 332 | if (events.removeListener === undefined) { 333 | if (arguments.length === 0) { 334 | this._events = Object.create(null); 335 | this._eventsCount = 0; 336 | } else if (events[type] !== undefined) { 337 | if (--this._eventsCount === 0) 338 | this._events = Object.create(null); 339 | else 340 | delete events[type]; 341 | } 342 | return this; 343 | } 344 | 345 | // emit removeListener for all listeners on all events 346 | if (arguments.length === 0) { 347 | var keys = Object.keys(events); 348 | var key; 349 | for (i = 0; i < keys.length; ++i) { 350 | key = keys[i]; 351 | if (key === 'removeListener') continue; 352 | this.removeAllListeners(key); 353 | } 354 | this.removeAllListeners('removeListener'); 355 | this._events = Object.create(null); 356 | this._eventsCount = 0; 357 | return this; 358 | } 359 | 360 | listeners = events[type]; 361 | 362 | if (typeof listeners === 'function') { 363 | this.removeListener(type, listeners); 364 | } else if (listeners !== undefined) { 365 | // LIFO order 366 | for (i = listeners.length - 1; i >= 0; i--) { 367 | this.removeListener(type, listeners[i]); 368 | } 369 | } 370 | 371 | return this; 372 | }; 373 | 374 | function _listeners(target, type, unwrap) { 375 | var events = target._events; 376 | 377 | if (events === undefined) 378 | return []; 379 | 380 | var evlistener = events[type]; 381 | if (evlistener === undefined) 382 | return []; 383 | 384 | if (typeof evlistener === 'function') 385 | return unwrap ? [evlistener.listener || evlistener] : [evlistener]; 386 | 387 | return unwrap ? 388 | unwrapListeners(evlistener) : arrayClone(evlistener, evlistener.length); 389 | } 390 | 391 | EventEmitter.prototype.listeners = function listeners(type) { 392 | return _listeners(this, type, true); 393 | }; 394 | 395 | EventEmitter.prototype.rawListeners = function rawListeners(type) { 396 | return _listeners(this, type, false); 397 | }; 398 | 399 | EventEmitter.listenerCount = function(emitter, type) { 400 | if (typeof emitter.listenerCount === 'function') { 401 | return emitter.listenerCount(type); 402 | } else { 403 | return listenerCount.call(emitter, type); 404 | } 405 | }; 406 | 407 | EventEmitter.prototype.listenerCount = listenerCount; 408 | function listenerCount(type) { 409 | var events = this._events; 410 | 411 | if (events !== undefined) { 412 | var evlistener = events[type]; 413 | 414 | if (typeof evlistener === 'function') { 415 | return 1; 416 | } else if (evlistener !== undefined) { 417 | return evlistener.length; 418 | } 419 | } 420 | 421 | return 0; 422 | } 423 | 424 | EventEmitter.prototype.eventNames = function eventNames() { 425 | return this._eventsCount > 0 ? ReflectOwnKeys(this._events) : []; 426 | }; 427 | 428 | function arrayClone(arr, n) { 429 | var copy = new Array(n); 430 | for (var i = 0; i < n; ++i) 431 | copy[i] = arr[i]; 432 | return copy; 433 | } 434 | 435 | function spliceOne(list, index) { 436 | for (; index + 1 < list.length; index++) 437 | list[index] = list[index + 1]; 438 | list.pop(); 439 | } 440 | 441 | function unwrapListeners(arr) { 442 | var ret = new Array(arr.length); 443 | for (var i = 0; i < ret.length; ++i) { 444 | ret[i] = arr[i].listener || arr[i]; 445 | } 446 | return ret; 447 | } 448 | 449 | function once(emitter, name) { 450 | return new Promise(function (resolve, reject) { 451 | function errorListener(err) { 452 | emitter.removeListener(name, resolver); 453 | reject(err); 454 | } 455 | 456 | function resolver() { 457 | if (typeof emitter.removeListener === 'function') { 458 | emitter.removeListener('error', errorListener); 459 | } 460 | resolve([].slice.call(arguments)); 461 | }; 462 | 463 | eventTargetAgnosticAddListener(emitter, name, resolver, { once: true }); 464 | if (name !== 'error') { 465 | addErrorHandlerIfEventEmitter(emitter, errorListener, { once: true }); 466 | } 467 | }); 468 | } 469 | 470 | function addErrorHandlerIfEventEmitter(emitter, handler, flags) { 471 | if (typeof emitter.on === 'function') { 472 | eventTargetAgnosticAddListener(emitter, 'error', handler, flags); 473 | } 474 | } 475 | 476 | function eventTargetAgnosticAddListener(emitter, name, listener, flags) { 477 | if (typeof emitter.on === 'function') { 478 | if (flags.once) { 479 | emitter.once(name, listener); 480 | } else { 481 | emitter.on(name, listener); 482 | } 483 | } else if (typeof emitter.addEventListener === 'function') { 484 | // EventTarget does not have `error` event semantics like Node 485 | // EventEmitters, we do not listen for `error` events here. 486 | emitter.addEventListener(name, function wrapListener(arg) { 487 | // IE does not have builtin `{ once: true }` support so we 488 | // have to do it manually. 489 | if (flags.once) { 490 | emitter.removeEventListener(name, wrapListener); 491 | } 492 | listener(arg); 493 | }); 494 | } else { 495 | throw new TypeError('The "emitter" argument must be of type EventEmitter. Received type ' + typeof emitter); 496 | } 497 | } -------------------------------------------------------------------------------- /client/history.js: -------------------------------------------------------------------------------- 1 | import EventEmitter from "./events.js"; 2 | import HookEvent from "./hook.js"; 3 | 4 | class History extends EventEmitter { 5 | constructor(ctx) { 6 | super(); 7 | this.ctx = ctx; 8 | this.window = this.ctx.window; 9 | this.History = this.window.History; 10 | this.history = this.window.history; 11 | this.historyProto = this.History ? this.History.prototype : {}; 12 | this.pushState = this.historyProto.pushState; 13 | this.replaceState = this.historyProto.replaceState; 14 | this.go = this.historyProto.go; 15 | this.back = this.historyProto.back; 16 | this.forward = this.historyProto.forward; 17 | }; 18 | override() { 19 | this.overridePushState(); 20 | this.overrideReplaceState(); 21 | this.overrideGo(); 22 | this.overrideForward(); 23 | this.overrideBack(); 24 | }; 25 | overridePushState() { 26 | this.ctx.override(this.historyProto, 'pushState', (target, that, args) => { 27 | if (2 > args.length) return target.apply(that, args); 28 | let [ state, title, url = '' ] = args; 29 | 30 | const event = new HookEvent({ state, title, url }, target, that); 31 | this.emit('pushState', event); 32 | 33 | if (event.intercepted) return event.returnValue; 34 | return event.target.call(event.that, event.data.state, event.data.title, event.data.url); 35 | }); 36 | }; 37 | overrideReplaceState() { 38 | this.ctx.override(this.historyProto, 'replaceState', (target, that, args) => { 39 | if (2 > args.length) return target.apply(that, args); 40 | let [ state, title, url = '' ] = args; 41 | 42 | const event = new HookEvent({ state, title, url }, target, that); 43 | this.emit('replaceState', event); 44 | 45 | if (event.intercepted) return event.returnValue; 46 | return event.target.call(event.that, event.data.state, event.data.title, event.data.url); 47 | }); 48 | }; 49 | overrideGo() { 50 | this.ctx.override(this.historyProto, 'go', (target, that, [ delta ]) => { 51 | const event = new HookEvent({ delta }, target, that); 52 | this.emit('go', event); 53 | 54 | if (event.intercepted) return event.returnValue; 55 | return event.target.call(event.that, event.data.delta); 56 | }); 57 | }; 58 | overrideForward() { 59 | this.ctx.override(this.historyProto, 'forward', (target, that) => { 60 | const event = new HookEvent(null, target, that); 61 | this.emit('forward', event); 62 | 63 | if (event.intercepted) return event.returnValue; 64 | return event.target.call(event.that); 65 | }); 66 | }; 67 | overrideBack() { 68 | this.ctx.override(this.historyProto, 'back', (target, that) => { 69 | const event = new HookEvent(null, target, that); 70 | this.emit('back', event); 71 | 72 | if (event.intercepted) return event.returnValue; 73 | return event.target.call(event.that); 74 | }); 75 | }; 76 | }; 77 | 78 | export default History; -------------------------------------------------------------------------------- /client/hook.js: -------------------------------------------------------------------------------- 1 | class HookEvent { 2 | #intercepted; 3 | #returnValue; 4 | constructor(data = {}, target = null, that = null) { 5 | this.#intercepted = false; 6 | this.#returnValue = null; 7 | this.data = data; 8 | this.target = target; 9 | this.that = that; 10 | }; 11 | get intercepted() { 12 | return this.#intercepted; 13 | }; 14 | get returnValue() { 15 | return this.#returnValue; 16 | }; 17 | respondWith(input) { 18 | this.#returnValue = input; 19 | this.#intercepted = true; 20 | }; 21 | }; 22 | 23 | export default HookEvent; -------------------------------------------------------------------------------- /client/index.js: -------------------------------------------------------------------------------- 1 | import DocumentHook from "./dom/document.js"; 2 | import ElementApi from "./dom/element.js"; 3 | import NodeApi from "./dom/node.js"; 4 | import AttrApi from "./dom/attr.js"; 5 | import FunctionHook from "./native/function.js"; 6 | import ObjectHook from "./native/object.js"; 7 | import Fetch from "./requests/fetch.js"; 8 | import WebSocketApi from "./requests/websocket.js"; 9 | import Xhr from "./requests/xhr.js"; 10 | import EventSourceApi from "./requests/eventsource.js"; 11 | import History from "./history.js"; 12 | import LocationApi from "./location.js"; 13 | import MessageApi from "./message.js"; 14 | import NavigatorApi from "./navigator.js"; 15 | import Workers from "./worker.js"; 16 | import URLApi from "./url.js"; 17 | import EventEmitter from "./events.js"; 18 | import StorageApi from "./storage.js"; 19 | import StyleApi from "./dom/style.js"; 20 | 21 | class UVClient extends EventEmitter { 22 | constructor(window = self, worker = !window.window) { 23 | super(); 24 | this.window = window; 25 | this.nativeMethods = { 26 | fnToString: this.window.Function.prototype.toString, 27 | defineProperty: this.window.Object.defineProperty, 28 | getOwnPropertyDescriptor: this.window.Object.getOwnPropertyDescriptor, 29 | getOwnPropertyDescriptors: this.window.Object.getOwnPropertyDescriptors, 30 | getOwnPropertyNames: this.window.Object.getOwnPropertyNames, 31 | keys: this.window.Object.keys, 32 | getOwnPropertySymbols: this.window.Object.getOwnPropertySymbols, 33 | isArray: this.window.Array.isArray, 34 | setPrototypeOf: this.window.Object.setPrototypeOf, 35 | isExtensible: this.window.Object.isExtensible, 36 | Map: this.window.Map, 37 | Proxy: this.window.Proxy, 38 | }; 39 | this.worker = worker; 40 | this.fetch = new Fetch(this); 41 | this.xhr = new Xhr(this); 42 | this.history = new History(this); 43 | this.element = new ElementApi(this); 44 | this.node = new NodeApi(this) 45 | this.document = new DocumentHook(this); 46 | this.function = new FunctionHook(this); 47 | this.object = new ObjectHook(this); 48 | this.message = new MessageApi(this); 49 | this.websocket = new WebSocketApi(this); 50 | this.navigator = new NavigatorApi(this); 51 | this.eventSource = new EventSourceApi(this); 52 | this.attribute = new AttrApi(this); 53 | this.url = new URLApi(this); 54 | this.workers = new Workers(this); 55 | this.location = new LocationApi(this); 56 | this.storage = new StorageApi(this); 57 | this.style = new StyleApi(this); 58 | }; 59 | initLocation(rewriteUrl, sourceUrl) { 60 | this.location = new LocationApi(this, sourceUrl, rewriteUrl, this.worker); 61 | }; 62 | override(obj, prop, wrapper, construct) { 63 | if (!prop in obj) return false; 64 | const wrapped = this.wrap(obj, prop, wrapper, construct); 65 | return obj[prop] = wrapped; 66 | }; 67 | overrideDescriptor(obj, prop, wrapObj = {}) { 68 | const wrapped = this.wrapDescriptor(obj, prop, wrapObj); 69 | if (!wrapped) return {}; 70 | this.nativeMethods.defineProperty(obj, prop, wrapped); 71 | return wrapped; 72 | }; 73 | wrap(obj, prop, wrap, construct) { 74 | const fn = obj[prop]; 75 | if (!fn) return fn; 76 | const wrapped = 'prototype' in fn ? function attach() { 77 | return wrap(fn, this, [...arguments]); 78 | } : { 79 | attach() { 80 | return wrap(fn, this, [...arguments]); 81 | }, 82 | }.attach; 83 | 84 | if (!!construct) { 85 | wrapped.prototype = fn.prototype; 86 | wrapped.prototype.constructor = wrapped; 87 | }; 88 | 89 | this.emit('wrap', fn, wrapped, !!construct); 90 | 91 | return wrapped; 92 | }; 93 | wrapDescriptor(obj, prop, wrapObj = {}) { 94 | const descriptor = this.nativeMethods.getOwnPropertyDescriptor(obj, prop); 95 | if (!descriptor) return false; 96 | for (let key in wrapObj) { 97 | if (key in descriptor) { 98 | if (key === 'get' || key === 'set') { 99 | descriptor[key] = this.wrap(descriptor, key, wrapObj[key]); 100 | } else { 101 | descriptor[key] = typeof wrapObj[key] == 'function' ? wrapObj[key](descriptor[key]) : wrapObj[key]; 102 | }; 103 | } 104 | }; 105 | return descriptor; 106 | }; 107 | }; 108 | 109 | export default UVClient; 110 | if (typeof self === 'object') self.UVClient = UVClient; -------------------------------------------------------------------------------- /client/location.js: -------------------------------------------------------------------------------- 1 | import EventEmitter from "./events.js"; 2 | 3 | class LocationApi extends EventEmitter { 4 | constructor(ctx) { 5 | super(); 6 | this.ctx = ctx; 7 | this.window = ctx.window; 8 | this.location = this.window.location; 9 | this.WorkerLocation = this.ctx.worker ? this.window.WorkerLocation : null; 10 | this.workerLocProto = this.WorkerLocation ? this.WorkerLocation.prototype : {}; 11 | this.keys = ['href', 'protocol', 'host', 'hostname', 'port', 'pathname', 'search', 'hash', 'origin']; 12 | this.HashChangeEvent = this.window.HashChangeEvent || null; 13 | this.href = this.WorkerLocation ? ctx.nativeMethods.getOwnPropertyDescriptor(this.workerLocProto, 'href') : 14 | ctx.nativeMethods.getOwnPropertyDescriptor(this.location, 'href'); 15 | }; 16 | overrideWorkerLocation(parse) { 17 | if (!this.WorkerLocation) return false; 18 | const uv = this; 19 | 20 | for (const key of this.keys) { 21 | this.ctx.overrideDescriptor(this.workerLocProto, key, { 22 | get: (target, that) => { 23 | return parse( 24 | uv.href.get.call(this.location) 25 | )[key] 26 | }, 27 | }); 28 | }; 29 | 30 | return true; 31 | }; 32 | emulate(parse, wrap) { 33 | const emulation = {}; 34 | const that = this; 35 | 36 | for (const key of that.keys) { 37 | this.ctx.nativeMethods.defineProperty(emulation, key, { 38 | get() { 39 | return parse( 40 | that.href.get.call(that.location) 41 | )[key]; 42 | }, 43 | set: key !== 'origin' ? function (val) { 44 | switch(key) { 45 | case 'href': 46 | that.location.href = wrap(val); 47 | break; 48 | case 'hash': 49 | that.emit('hashchange', emulation.href, (val.trim().startsWith('#') ? new URL(val.trim(), emulation.href).href : new URL('#' + val.trim(), emulation.href).href), that); 50 | break; 51 | default: 52 | const url = new URL(emulation.href); 53 | url[key] = val; 54 | that.location.href = wrap(url.href); 55 | }; 56 | } : undefined, 57 | configurable: false, 58 | enumerable: true, 59 | }); 60 | }; 61 | 62 | if ('reload' in this.location) { 63 | this.ctx.nativeMethods.defineProperty(emulation, 'reload', { 64 | value: this.ctx.wrap(this.location, 'reload', (target, that) => { 65 | return target.call(that === emulation ? this.location : that); 66 | }), 67 | writable: false, 68 | enumerable: true, 69 | }); 70 | }; 71 | 72 | if ('replace' in this.location) { 73 | this.ctx.nativeMethods.defineProperty(emulation, 'replace', { 74 | value: this.ctx.wrap(this.location, 'assign', (target, that, args) => { 75 | if (!args.length || that !== emulation) target.call(that); 76 | that = this.location; 77 | let [ input ] = args; 78 | 79 | const url = new URL(input, emulation.href); 80 | return target.call(that === emulation ? this.location : that, wrap(url.href)); 81 | }), 82 | writable: false, 83 | enumerable: true, 84 | }); 85 | }; 86 | 87 | if ('assign' in this.location) { 88 | this.ctx.nativeMethods.defineProperty(emulation, 'assign', { 89 | value: this.ctx.wrap(this.location, 'assign', (target, that, args) => { 90 | if (!args.length || that !== emulation) target.call(that); 91 | that = this.location; 92 | let [ input ] = args; 93 | 94 | const url = new URL(input, emulation.href); 95 | return target.call(that === emulation ? this.location : that, wrap(url.href)); 96 | }), 97 | writable: false, 98 | enumerable: true, 99 | }); 100 | }; 101 | 102 | if ('ancestorOrigins' in this.location) { 103 | this.ctx.nativeMethods.defineProperty(emulation, 'ancestorOrigins', { 104 | get() { 105 | const arr = []; 106 | if (that.window.DOMStringList) that.ctx.nativeMethods.setPrototypeOf(arr, that.window.DOMStringList.prototype); 107 | return arr; 108 | }, 109 | set: undefined, 110 | enumerable: true, 111 | }); 112 | }; 113 | 114 | 115 | this.ctx.nativeMethods.defineProperty(emulation, 'toString', { 116 | value: this.ctx.wrap(this.location, 'toString', () => { 117 | return emulation.href; 118 | }), 119 | enumerable: true, 120 | writable: false, 121 | }); 122 | 123 | this.ctx.nativeMethods.defineProperty(emulation, Symbol.toPrimitive, { 124 | value: () => emulation.href, 125 | writable: false, 126 | enumerable: false, 127 | }); 128 | 129 | if (this.ctx.window.Location) this.ctx.nativeMethods.setPrototypeOf(emulation, this.ctx.window.Location.prototype); 130 | 131 | return emulation; 132 | }; 133 | }; 134 | 135 | export default LocationApi; -------------------------------------------------------------------------------- /client/message.js: -------------------------------------------------------------------------------- 1 | import EventEmitter from "./events.js"; 2 | import HookEvent from "./hook.js"; 3 | 4 | class MessageApi extends EventEmitter { 5 | constructor(ctx) { 6 | super(); 7 | this.ctx = ctx; 8 | this.window = this.ctx.window; 9 | this.postMessage = this.window.postMessage; 10 | this.MessageEvent = this.window.MessageEvent || {}; 11 | this.MessagePort = this.window.MessagePort || {}; 12 | this.mpProto = this.MessagePort.prototype || {}; 13 | this.mpPostMessage = this.mpProto.postMessage; 14 | this.messageProto = this.MessageEvent.prototype || {}; 15 | this.messageData = ctx.nativeMethods.getOwnPropertyDescriptor(this.messageProto, 'data'); 16 | this.messageOrigin = ctx.nativeMethods.getOwnPropertyDescriptor(this.messageProto, 'origin'); 17 | }; 18 | overridePostMessage() { 19 | this.ctx.override(this.window, 'postMessage', (target, that, args) => { 20 | if (!args.length) return target.apply(that, args); 21 | 22 | let message; 23 | let origin; 24 | let transfer; 25 | 26 | if (!this.ctx.worker) { 27 | [ message, origin, transfer = [] ] = args; 28 | } else { 29 | [ message, transfer = [] ] = args; 30 | }; 31 | 32 | const event = new HookEvent({ message, origin, transfer, worker: this.ctx.worker }, target, that); 33 | this.emit('postMessage', event); 34 | 35 | if (event.intercepted) return event.returnValue; 36 | 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); 37 | }); 38 | }; 39 | wrapPostMessage(obj, prop, noOrigin = false) { 40 | return this.ctx.wrap(obj, prop, (target, that, args) => { 41 | if (this.ctx.worker ? !args.length : 2 > args) return target.apply(that, args); 42 | let message; 43 | let origin; 44 | let transfer; 45 | 46 | if (!noOrigin) { 47 | [ message, origin, transfer = [] ] = args; 48 | } else { 49 | [ message, transfer = [] ] = args; 50 | origin = null; 51 | }; 52 | 53 | const event = new HookEvent({ message, origin, transfer, worker: this.ctx.worker }, target, obj); 54 | this.emit('postMessage', event); 55 | 56 | if (event.intercepted) return event.returnValue; 57 | 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); 58 | }); 59 | }; 60 | overrideMessageOrigin() { 61 | this.ctx.overrideDescriptor(this.messageProto, 'origin', { 62 | get: (target, that) => { 63 | const event = new HookEvent({ value: target.call(that) }, target, that); 64 | this.emit('origin', event); 65 | 66 | if (event.intercepted) return event.returnValue; 67 | return event.data.value; 68 | } 69 | }); 70 | }; 71 | overrideMessageData() { 72 | this.ctx.overrideDescriptor(this.messageProto, 'data', { 73 | get: (target, that) => { 74 | const event = new HookEvent({ value: target.call(that) }, target, that); 75 | this.emit('data', event); 76 | 77 | if (event.intercepted) return event.returnValue; 78 | return event.data.value; 79 | } 80 | }); 81 | }; 82 | }; 83 | 84 | export default MessageApi; -------------------------------------------------------------------------------- /client/native/function.js: -------------------------------------------------------------------------------- 1 | import EventEmitter from "../events.js"; 2 | import HookEvent from "../hook.js"; 3 | 4 | class FunctionHook extends EventEmitter { 5 | constructor(ctx) { 6 | super(); 7 | this.ctx = ctx; 8 | this.window = ctx.window; 9 | this.Function = this.window.Function; 10 | this.fnProto = this.Function.prototype; 11 | this.toString = this.fnProto.toString; 12 | this.fnStrings = ctx.fnStrings; 13 | this.call = this.fnProto.call; 14 | this.apply = this.fnProto.apply; 15 | this.bind = this.fnProto.bind; 16 | }; 17 | overrideFunction() { 18 | this.ctx.override(this.window, 'Function', (target, that, args) => { 19 | if (!args.length) return target.apply(that, args); 20 | 21 | let script = args[args.length - 1]; 22 | let fnArgs = []; 23 | 24 | for (let i = 0; i < args.length - 1; i++) { 25 | fnArgs.push(args[i]); 26 | }; 27 | 28 | const event = new HookEvent({ script, args: fnArgs }, target, that); 29 | this.emit('function', event); 30 | 31 | if (event.intercepted) return event.returnValue; 32 | return event.target.call(event.that, ...event.data.args, event.data.script); 33 | }, true); 34 | }; 35 | overrideToString() { 36 | this.ctx.override(this.fnProto, 'toString', (target, that) => { 37 | const event = new HookEvent({ fn: that }, target, that); 38 | this.emit('toString', event); 39 | 40 | if (event.intercepted) return event.returnValue; 41 | return event.target.call(event.data.fn); 42 | }); 43 | }; 44 | }; 45 | 46 | export default FunctionHook; -------------------------------------------------------------------------------- /client/native/object.js: -------------------------------------------------------------------------------- 1 | import EventEmitter from "../events.js"; 2 | import HookEvent from "../hook.js"; 3 | 4 | class ObjectHook extends EventEmitter { 5 | constructor(ctx) { 6 | super(); 7 | this.ctx = ctx; 8 | this.window = ctx.window; 9 | this.Object = this.window.Object; 10 | this.getOwnPropertyDescriptors = this.Object.getOwnPropertyDescriptors; 11 | this.getOwnPropertyDescriptor = this.Object.getOwnPropertyDescriptor; 12 | this.getOwnPropertyNames = this.Object.getOwnPropertyNames; 13 | }; 14 | overrideGetPropertyNames() { 15 | this.ctx.override(this.Object, 'getOwnPropertyNames', (target, that, args) => { 16 | if (!args.length) return target.apply(that, args); 17 | let [ object ] = args; 18 | 19 | const event = new HookEvent({ names: target.call(that, object) }, target, that); 20 | this.emit('getOwnPropertyNames', event); 21 | 22 | if (event.intercepted) return event.returnValue; 23 | return event.data.names; 24 | }); 25 | }; 26 | overrideGetOwnPropertyDescriptors() { 27 | this.ctx.override(this.Object, 'getOwnPropertyDescriptors', (target, that, args) => { 28 | if (!args.length) return target.apply(that, args); 29 | let [ object ] = args; 30 | 31 | const event = new HookEvent({ descriptors: target.call(that, object) }, target, that); 32 | this.emit('getOwnPropertyDescriptors', event); 33 | 34 | if (event.intercepted) return event.returnValue; 35 | return event.data.descriptors; 36 | }); 37 | }; 38 | }; 39 | 40 | export default ObjectHook; -------------------------------------------------------------------------------- /client/navigator.js: -------------------------------------------------------------------------------- 1 | import EventEmitter from "./events.js"; 2 | import HookEvent from "./hook.js"; 3 | 4 | class NavigatorApi extends EventEmitter { 5 | constructor(ctx) { 6 | super(); 7 | this.ctx = ctx; 8 | this.window = ctx.window; 9 | this.navigator = this.window.navigator; 10 | this.Navigator = this.window.Navigator || {}; 11 | this.navProto = this.Navigator.prototype || {}; 12 | this.sendBeacon = this.navProto.sendBeacon; 13 | }; 14 | overrideSendBeacon() { 15 | this.ctx.override(this.navProto, 'sendBeacon', (target, that, args) => { 16 | if (!args.length) return target.apply(that, args); 17 | let [ url, data = '' ] = args; 18 | 19 | const event = new HookEvent({ url, data }, target, that); 20 | this.emit('sendBeacon', event); 21 | 22 | if (event.intercepted) return event.returnValue; 23 | return event.target.call(event.that, event.data.url, event.data.data); 24 | }); 25 | }; 26 | }; 27 | 28 | export default NavigatorApi; -------------------------------------------------------------------------------- /client/requests/eventsource.js: -------------------------------------------------------------------------------- 1 | import EventEmitter from "../events.js"; 2 | import HookEvent from "../hook.js"; 3 | 4 | class EventSourceApi extends EventEmitter { 5 | constructor(ctx) { 6 | super(); 7 | this.ctx = ctx; 8 | this.window = ctx.window; 9 | this.EventSource = this.window.EventSource || {}; 10 | this.esProto = this.EventSource.prototype || {}; 11 | this.url = ctx.nativeMethods.getOwnPropertyDescriptor(this.esProto, 'url'); 12 | this.CONNECTING = 0; 13 | this.OPEN = 1; 14 | this.CLOSED = 2; 15 | }; 16 | overrideConstruct() { 17 | this.ctx.override(this.window, 'EventSource', (target, that, args) => { 18 | if (!args.length) return new target(...args); 19 | let [ url, config = {} ] = args; 20 | 21 | const event = new HookEvent({ url, config }, target, that); 22 | this.emit('construct', event); 23 | 24 | if (event.intercepted) return event.returnValue; 25 | return new event.target(event.data.url, event.data.config); 26 | }, true); 27 | 28 | if ('EventSource' in this.window) { 29 | this.window.EventSource.CONNECTING = this.CONNECTING; 30 | this.window.EventSource.OPEN = this.OPEN; 31 | this.window.EventSource.CLOSED = this.CLOSED; 32 | }; 33 | }; 34 | overrideUrl() { 35 | this.ctx.overrideDescriptor(this.esProto, 'url', { 36 | get: (target, that) => { 37 | const event = new HookEvent({ value: target.call(that) }, target, that); 38 | this.emit('url', event); 39 | return event.data.value; 40 | }, 41 | }); 42 | }; 43 | }; 44 | 45 | export default EventSourceApi; -------------------------------------------------------------------------------- /client/requests/fetch.js: -------------------------------------------------------------------------------- 1 | import EventEmitter from "../events.js"; 2 | import HookEvent from "../hook.js"; 3 | 4 | class Fetch extends EventEmitter { 5 | constructor(ctx) { 6 | super(); 7 | this.ctx = ctx; 8 | this.window = ctx.window; 9 | this.fetch = this.window.fetch; 10 | this.Request = this.window.Request; 11 | this.Response = this.window.Response; 12 | this.Headers = this.window.Headers; 13 | this.reqProto = this.Request ? this.Request.prototype : {}; 14 | this.resProto = this.Response ? this.Response.prototype : {}; 15 | this.headersProto = this.Headers ? this.Headers.prototype : {}; 16 | this.reqUrl = ctx.nativeMethods.getOwnPropertyDescriptor(this.reqProto, 'url'); 17 | this.resUrl = ctx.nativeMethods.getOwnPropertyDescriptor(this.resProto, 'url'); 18 | this.reqHeaders = ctx.nativeMethods.getOwnPropertyDescriptor(this.reqProto, 'headers'); 19 | this.resHeaders = ctx.nativeMethods.getOwnPropertyDescriptor(this.resProto, 'headers'); 20 | }; 21 | override() { 22 | this.overrideRequest(); 23 | this.overrideUrl(); 24 | this.overrideHeaders(); 25 | return true; 26 | }; 27 | overrideRequest() { 28 | if (!this.fetch) return false; 29 | 30 | this.ctx.override(this.window, 'fetch', (target, that, args) => { 31 | if (!args.length || args[0] instanceof this.Request) return target.apply(that, args); 32 | 33 | let [ input, options = {} ] = args; 34 | const event = new HookEvent({ input, options }, target, that); 35 | 36 | this.emit('request', event); 37 | if (event.intercepted) return event.returnValue; 38 | 39 | return event.target.call(event.that, event.data.input, event.data.options); 40 | }); 41 | 42 | this.ctx.override(this.window, 'Request', (target, that, args) => { 43 | if (!args.length) return new target(...args); 44 | 45 | let [ input, options = {} ] = args; 46 | const event = new HookEvent({ input, options }, target); 47 | 48 | this.emit('request', event); 49 | if (event.intercepted) return event.returnValue; 50 | 51 | return new event.target(event.data.input, event.data.options); 52 | }, true); 53 | return true; 54 | }; 55 | overrideUrl() { 56 | this.ctx.overrideDescriptor(this.reqProto, 'url', { 57 | get: (target, that) => { 58 | const event = new HookEvent({ value: target.call(that) }, target, that); 59 | 60 | this.emit('requestUrl', event); 61 | if (event.intercepted) return event.returnValue; 62 | 63 | return event.data.value; 64 | }, 65 | }); 66 | 67 | this.ctx.overrideDescriptor(this.resProto, 'url', { 68 | get: (target, that) => { 69 | const event = new HookEvent({ value: target.call(that) }, target, that); 70 | 71 | this.emit('responseUrl', event); 72 | if (event.intercepted) return event.returnValue; 73 | 74 | return event.data.value; 75 | }, 76 | }); 77 | return true; 78 | }; 79 | overrideHeaders() { 80 | if (!this.Headers) return false; 81 | 82 | this.ctx.overrideDescriptor(this.reqProto, 'headers', { 83 | get: (target, that) => { 84 | const event = new HookEvent({ value: target.call(that) }, target, that); 85 | 86 | this.emit('requestHeaders', event); 87 | if (event.intercepted) return event.returnValue; 88 | 89 | return event.data.value; 90 | }, 91 | }); 92 | 93 | this.ctx.overrideDescriptor(this.resProto, 'headers', { 94 | get: (target, that) => { 95 | const event = new HookEvent({ value: target.call(that) }, target, that); 96 | 97 | this.emit('responseHeaders', event); 98 | if (event.intercepted) return event.returnValue; 99 | 100 | return event.data.value; 101 | }, 102 | }); 103 | 104 | this.ctx.override(this.headersProto, 'get', (target, that, [ name ]) => { 105 | if (!name) return target.call(that); 106 | const event = new HookEvent({ name, value: target.call(that, name) }, target, that); 107 | 108 | this.emit('getHeader', event); 109 | if (event.intercepted) return event.returnValue; 110 | 111 | return event.data.value; 112 | }); 113 | 114 | this.ctx.override(this.headersProto, 'set', (target, that, args) => { 115 | if (2 > args.length) return target.apply(that, args); 116 | 117 | let [ name, value ] = args; 118 | const event = new HookEvent({ name, value }, target, that); 119 | 120 | this.emit('setHeader', event); 121 | if (event.intercepted) return event.returnValue; 122 | 123 | return event.target.call(event.that, event.data.name, event.data.value); 124 | }); 125 | 126 | this.ctx.override(this.headersProto, 'has', (target, that, args) => { 127 | if (!args.length) return target.call(that); 128 | let [ name ] = args; 129 | 130 | const event = new HookEvent({ name, value: target.call(that, name) }, target, that); 131 | 132 | this.emit('hasHeader', event); 133 | if (event.intercepted) return event.returnValue; 134 | 135 | return event.data; 136 | }); 137 | 138 | this.ctx.override(this.headersProto, 'append', (target, that, args) => { 139 | if (2 > args.length) return target.apply(that, args); 140 | 141 | let [ name, value ] = args; 142 | const event = new HookEvent({ name, value }, target, that); 143 | 144 | this.emit('appendHeader', event); 145 | if (event.intercepted) return event.returnValue; 146 | 147 | return event.target.call(event.that, event.data.name, event.data.value); 148 | }); 149 | 150 | this.ctx.override(this.headersProto, 'delete', (target, that, args) => { 151 | if (!args.length) return target.apply(that, args); 152 | 153 | let [ name ] = args; 154 | const event = new HookEvent({ name }, target, that); 155 | 156 | this.emit('deleteHeader', event); 157 | if (event.intercepted) return event.returnValue; 158 | 159 | return event.target.call(event.that, event.data.name); 160 | }); 161 | 162 | return true; 163 | }; 164 | }; 165 | 166 | export default Fetch; -------------------------------------------------------------------------------- /client/requests/websocket.js: -------------------------------------------------------------------------------- 1 | import EventEmitter from "../events.js"; 2 | import HookEvent from "../hook.js"; 3 | 4 | class WebSocketApi extends EventEmitter { 5 | constructor(ctx) { 6 | super(); 7 | this.ctx = ctx; 8 | this.window = ctx.window; 9 | this.WebSocket = this.window.WebSocket || {}; 10 | this.wsProto = this.WebSocket.prototype || {}; 11 | this.url = ctx.nativeMethods.getOwnPropertyDescriptor(this.wsProto, 'url'); 12 | this.protocol = ctx.nativeMethods.getOwnPropertyDescriptor(this.wsProto, 'protocol'); 13 | this.send = this.wsProto.send; 14 | this.close = this.wsProto.close; 15 | this.CONNECTING = 0; 16 | this.OPEN = 1; 17 | this.CLOSING = 2; 18 | this.CLOSED = 3; 19 | }; 20 | overrideWebSocket() { 21 | this.ctx.override(this.window, 'WebSocket', (target, that, args) => { 22 | if (!args.length) return new target(...args); 23 | let [ url, protocols = [] ] = args; 24 | 25 | if (!this.ctx.nativeMethods.isArray(protocols)) protocols = [ protocols ]; 26 | const event = new HookEvent({ url, protocols }, target, that); 27 | this.emit('websocket', event); 28 | 29 | if (event.intercepted) return event.returnValue; 30 | return new event.target(event.data.url, event.data.protocols); 31 | }, true); 32 | 33 | this.window.WebSocket.CONNECTING = this.CONNECTING; 34 | this.window.WebSocket.OPEN = this.OPEN; 35 | this.window.WebSocket.CLOSING = this.CLOSING; 36 | this.window.WebSocket.CLOSED = this.CLOSED; 37 | }; 38 | overrideUrl() { 39 | this.ctx.overrideDescriptor(this.wsProto, 'url', { 40 | get: (target, that) => { 41 | const event = new HookEvent({ value: target.call(that) }, target, that); 42 | this.emit('url', event); 43 | return event.data.value; 44 | }, 45 | }); 46 | }; 47 | overrideProtocol() { 48 | this.ctx.overrideDescriptor(this.wsProto, 'protocol', { 49 | get: (target, that) => { 50 | const event = new HookEvent({ value: target.call(that) }, target, that); 51 | this.emit('protocol', event); 52 | return event.data.value; 53 | }, 54 | }); 55 | }; 56 | }; 57 | 58 | export default WebSocketApi; -------------------------------------------------------------------------------- /client/requests/xhr.js: -------------------------------------------------------------------------------- 1 | import EventEmitter from "../events.js"; 2 | import HookEvent from "../hook.js"; 3 | 4 | class Xhr extends EventEmitter { 5 | constructor(ctx) { 6 | super(); 7 | this.ctx = ctx; 8 | this.window = ctx.window; 9 | this.XMLHttpRequest = this.window.XMLHttpRequest; 10 | this.xhrProto = this.window.XMLHttpRequest ? this.window.XMLHttpRequest.prototype : {}; 11 | this.open = this.xhrProto.open; 12 | this.abort = this.xhrProto.abort; 13 | this.send = this.xhrProto.send; 14 | this.overrideMimeType = this.xhrProto.overrideMimeType 15 | this.getAllResponseHeaders = this.xhrProto.getAllResponseHeaders; 16 | this.getResponseHeader = this.xhrProto.getResponseHeader; 17 | this.setRequestHeader = this.xhrProto.setRequestHeader; 18 | this.responseURL = ctx.nativeMethods.getOwnPropertyDescriptor(this.xhrProto, 'responseURL'); 19 | this.responseText = ctx.nativeMethods.getOwnPropertyDescriptor(this.xhrProto, 'responseText'); 20 | }; 21 | override() { 22 | this.overrideOpen(); 23 | this.overrideSend(); 24 | this.overrideMimeType(); 25 | this.overrideGetResHeader(); 26 | this.overrideGetResHeaders(); 27 | this.overrideSetReqHeader(); 28 | }; 29 | overrideOpen() { 30 | this.ctx.override(this.xhrProto, 'open', (target, that, args) => { 31 | if (2 > args.length) return target.apply(that, args); 32 | 33 | let [ method, input, async = true, user = null, password = null ] = args; 34 | const event = new HookEvent({ method, input, async, user, password }, target, that); 35 | 36 | this.emit('open', event); 37 | if (event.intercepted) return event.returnValue; 38 | 39 | return event.target.call( 40 | event.that, 41 | event.data.method, 42 | event.data.input, 43 | event.data.async, 44 | event.data.user, 45 | event.data.password 46 | ); 47 | }); 48 | }; 49 | overrideResponseUrl() { 50 | this.ctx.overrideDescriptor(this.xhrProto, 'responseURL', { 51 | get: (target, that) => { 52 | const event = new HookEvent({ value: target.call(that) }, target, that); 53 | this.emit('responseUrl', event); 54 | 55 | if (event.intercepted) return event.returnValue; 56 | return event.data.value; 57 | }, 58 | }); 59 | }; 60 | overrideSend() { 61 | this.ctx.override(this.xhrProto, 'send', (target, that, [ body = null ]) => { 62 | const event = new HookEvent({ body }, target, that); 63 | 64 | this.emit('send', event); 65 | if (event.intercepted) return event.returnValue; 66 | 67 | return event.target.call( 68 | event.that, 69 | event.data.body, 70 | ); 71 | }); 72 | }; 73 | overrideSetReqHeader() { 74 | this.ctx.override(this.xhrProto, 'setRequestHeader', (target, that, args) => { 75 | if (2 > args.length) return target.apply(that, args); 76 | 77 | let [ name, value ] = args; 78 | const event = new HookEvent({ name, value }, target, that); 79 | 80 | this.emit('setReqHeader', event); 81 | if (event.intercepted) return event.returnValue; 82 | 83 | return event.target.call(event.that, event.data.name, event.data.value); 84 | }); 85 | }; 86 | overrideGetResHeaders() { 87 | this.ctx.override(this.xhrProto, 'getAllResponseHeaders', (target, that) => { 88 | const event = new HookEvent({ value: target.call(that) }, target, that); 89 | 90 | this.emit('getAllResponseHeaders', event); 91 | if (event.intercepted) return event.returnValue; 92 | 93 | return event.data.value; 94 | }); 95 | }; 96 | overrideGetResHeader() { 97 | this.ctx.override(this.xhrProto, 'getResponseHeader', (target, that, args) => { 98 | if (!args.length) return target.apply(that, args); 99 | let [ name ] = args; 100 | 101 | const event = new HookEvent({ name, value: target.call(that, name) }, target, that); 102 | if (event.intercepted) return event.returnValue; 103 | 104 | return event.data.value; 105 | }); 106 | }; 107 | }; 108 | 109 | export default Xhr -------------------------------------------------------------------------------- /client/storage.js: -------------------------------------------------------------------------------- 1 | import EventEmitter from "./events.js"; 2 | import HookEvent from "./hook.js"; 3 | 4 | class StorageApi extends EventEmitter { 5 | constructor(ctx) { 6 | super(); 7 | this.ctx = ctx; 8 | this.window = ctx.window; 9 | this.localStorage = this.window.localStorage || null; 10 | this.sessionStorage = this.window.sessionStorage || null; 11 | this.Storage = this.window.Storage || {}; 12 | this.storeProto = this.Storage.prototype || {}; 13 | this.getItem = this.storeProto.getItem || null; 14 | this.setItem = this.storeProto.setItem || null; 15 | this.removeItem = this.storeProto.removeItem || null; 16 | this.clear = this.storeProto.clear || null; 17 | this.key = this.storeProto.key || null; 18 | this.methods = ['key', 'getItem', 'setItem', 'removeItem', 'clear']; 19 | this.wrappers = new ctx.nativeMethods.Map(); 20 | }; 21 | overrideMethods() { 22 | this.ctx.override(this.storeProto, 'getItem', (target, that, args) => { 23 | if (!args.length) return target.apply((this.wrappers.get(that) || that), args); 24 | let [ name ] = args; 25 | 26 | const event = new HookEvent({ name }, target, (this.wrappers.get(that) || that)); 27 | this.emit('getItem', event); 28 | 29 | if (event.intercepted) return event.returnValue; 30 | return event.target.call(event.that, event.data.name); 31 | }); 32 | this.ctx.override(this.storeProto, 'setItem', (target, that, args) => { 33 | if (2 > args.length) return target.apply((this.wrappers.get(that) || that), args); 34 | let [ name, value ] = args; 35 | 36 | const event = new HookEvent({ name, value }, target, (this.wrappers.get(that) || that)); 37 | this.emit('setItem', event); 38 | 39 | if (event.intercepted) return event.returnValue; 40 | return event.target.call(event.that, event.data.name, event.data.value); 41 | }); 42 | this.ctx.override(this.storeProto, 'removeItem', (target, that, args) => { 43 | if (!args.length) return target.apply((this.wrappers.get(that) || that), args); 44 | let [ name ] = args; 45 | 46 | const event = new HookEvent({ name }, target, (this.wrappers.get(that) || that)); 47 | this.emit('removeItem', event); 48 | 49 | if (event.intercepted) return event.returnValue; 50 | return event.target.call(event.that, event.data.name); 51 | }); 52 | this.ctx.override(this.storeProto, 'clear', (target, that) => { 53 | const event = new HookEvent(null, target, (this.wrappers.get(that) || that)); 54 | this.emit('clear', event); 55 | 56 | if (event.intercepted) return event.returnValue; 57 | return event.target.call(event.that); 58 | }); 59 | this.ctx.override(this.storeProto, 'key', (target, that, args) => { 60 | if (!args.length) return target.apply((this.wrappers.get(that) || that), args); 61 | let [ index ] = args; 62 | 63 | const event = new HookEvent({ index }, target, (this.wrappers.get(that) || that)); 64 | this.emit('key', event); 65 | 66 | if (event.intercepted) return event.returnValue; 67 | return event.target.call(event.that, event.data.index); 68 | }); 69 | }; 70 | overrideLength() { 71 | this.ctx.overrideDescriptor(this.storeProto, 'length', { 72 | get: (target, that) => { 73 | const event = new HookEvent({ length: target.call((this.wrappers.get(that) || that)) }, target, (this.wrappers.get(that) || that)); 74 | this.emit('length', event); 75 | 76 | if (event.intercepted) return event.returnValue; 77 | return event.data.length; 78 | }, 79 | }); 80 | }; 81 | emulate(storage, obj = {}) { 82 | this.ctx.nativeMethods.setPrototypeOf(obj, this.storeProto); 83 | 84 | const proxy = new this.ctx.window.Proxy(obj, { 85 | get: (target, prop) => { 86 | if (prop in this.storeProto || typeof prop === 'symbol') return storage[prop]; 87 | 88 | const event = new HookEvent({ name: prop }, null, storage); 89 | this.emit('get', event); 90 | 91 | if (event.intercepted) return event.returnValue; 92 | return storage[event.data.name]; 93 | }, 94 | set: (target, prop, value) => { 95 | if (prop in this.storeProto || typeof prop === 'symbol') return storage[prop] = value; 96 | 97 | const event = new HookEvent({ name: prop, value }, null, storage); 98 | this.emit('set', event); 99 | 100 | if (event.intercepted) return event.returnValue; 101 | 102 | return storage[event.data.name] = event.data.value; 103 | }, 104 | deleteProperty: (target, prop) => { 105 | if (typeof prop === 'symbol') return delete storage[prop]; 106 | 107 | const event = new HookEvent({ name: prop }, null, storage); 108 | this.emit('delete', event); 109 | 110 | if (event.intercepted) return event.returnValue; 111 | 112 | return delete storage[event.data.name]; 113 | }, 114 | }); 115 | 116 | this.wrappers.set(proxy, storage); 117 | this.ctx.nativeMethods.setPrototypeOf(proxy, this.storeProto); 118 | 119 | return proxy; 120 | }; 121 | 122 | }; 123 | 124 | export default StorageApi; 125 | -------------------------------------------------------------------------------- /client/url.js: -------------------------------------------------------------------------------- 1 | import EventEmitter from "./events.js"; 2 | import HookEvent from "./hook.js"; 3 | 4 | class URLApi extends EventEmitter { 5 | constructor(ctx) { 6 | super(); 7 | this.ctx = ctx; 8 | this.window = this.ctx.window; 9 | this.URL = this.window.URL || {}; 10 | this.createObjectURL = this.URL.createObjectURL; 11 | this.revokeObjectURL = this.URL.revokeObjectURL; 12 | }; 13 | overrideObjectURL() { 14 | this.ctx.override(this.URL, 'createObjectURL', (target, that, args) => { 15 | if (!args.length) return target.apply(that, args); 16 | let [ object ] = args; 17 | 18 | const event = new HookEvent({ object }, target, that); 19 | this.emit('createObjectURL', event); 20 | 21 | if (event.intercepted) return event.returnValue; 22 | return event.target.call(event.that, event.data.object); 23 | }); 24 | this.ctx.override(this.URL, 'revokeObjectURL', (target, that, args) => { 25 | if (!args.length) return target.apply(that, args); 26 | let [ url ] = args; 27 | 28 | const event = new HookEvent({ url }, target, that); 29 | this.emit('revokeObjectURL', event); 30 | 31 | if (event.intercepted) return event.returnValue; 32 | return event.target.call(event.that, event.data.url); 33 | }); 34 | }; 35 | }; 36 | 37 | export default URLApi; -------------------------------------------------------------------------------- /client/worker.js: -------------------------------------------------------------------------------- 1 | import EventEmitter from "./events.js"; 2 | import HookEvent from "./hook.js"; 3 | 4 | class Workers extends EventEmitter { 5 | constructor(ctx) { 6 | super(); 7 | this.ctx = ctx; 8 | this.window = ctx.window; 9 | this.Worker = this.window.Worker || {}; 10 | this.Worklet = this.window.Worklet || {}; 11 | this.workletProto = this.Worklet.prototype || {}; 12 | this.workerProto = this.Worker.prototype || {}; 13 | this.postMessage = this.workerProto.postMessage; 14 | this.terminate = this.workerProto.terminate; 15 | this.addModule = this.workletProto.addModule; 16 | }; 17 | overrideWorker() { 18 | this.ctx.override(this.window, 'Worker', (target, that, args) => { 19 | if (!args.length) return new target(...args); 20 | let [ url, options = {} ] = args; 21 | 22 | const event = new HookEvent({ url, options }, target, that); 23 | this.emit('worker', event); 24 | 25 | if (event.intercepted) return event.returnValue; 26 | return new event.target(...[ event.data.url, event.data.options ]); 27 | }, true); 28 | }; 29 | overrideAddModule() { 30 | this.ctx.override(this.workletProto, 'addModule', (target, that, args) => { 31 | if (!args.length) return target.apply(that, args); 32 | let [ url, options = {} ] = args; 33 | 34 | const event = new HookEvent({ url, options }, target, that); 35 | this.emit('addModule', event); 36 | 37 | if (event.intercepted) return event.returnValue; 38 | return event.target.call(event.that, event.data.url, event.data.options); 39 | }); 40 | }; 41 | overridePostMessage() { 42 | this.ctx.override(this.workerProto, 'postMessage', (target, that, args) => { 43 | if (!args.length) return target.apply(that, args); 44 | let [ message, transfer = [] ] = args; 45 | 46 | const event = new HookEvent({ message, transfer }, target, that); 47 | this.emit('postMessage', event); 48 | 49 | if (event.intercepted) return event.returnValue; 50 | return event.target.call(event.that, event.data.message, event.data.transfer); 51 | }); 52 | }; 53 | overrideImportScripts() { 54 | this.ctx.override(this.window, 'importScripts', (target, that, scripts) => { 55 | if (!scripts.length) return target.apply(that, scripts); 56 | 57 | const event = new HookEvent({ scripts }, target, that); 58 | this.emit('importScripts', event); 59 | 60 | if (event.intercepted) return event.returnValue; 61 | return event.target.apply(event.that, event.data.scripts); 62 | }); 63 | }; 64 | }; 65 | 66 | export default Workers; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ultraviolet", 3 | "version": "1.0.0", 4 | "description": "Proxy", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "type": "module", 12 | "dependencies": { 13 | "bowser": "^2.11.0", 14 | "css-tree": "^2.0.4", 15 | "esotope-hammerhead": "^0.6.1", 16 | "idb": "^7.0.0", 17 | "meriyah": "^4.2.0", 18 | "mime-db": "^1.51.0", 19 | "parse5": "^6.0.1", 20 | "set-cookie-parser": "^2.4.8", 21 | "webpack": "^5.68.0" 22 | }, 23 | "devDependencies": { 24 | "eslint": "^8.8.0" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /rewrite/codecs.js: -------------------------------------------------------------------------------- 1 | // ------------------------------------------------------------- 2 | // WARNING: this file is used by both the client and the server. 3 | // Do not use any browser or node-specific API! 4 | // ------------------------------------------------------------- 5 | export const xor = { 6 | encode(str){ 7 | if (!str) return str; 8 | return encodeURIComponent(str.toString().split('').map((char, ind) => ind % 2 ? String.fromCharCode(char.charCodeAt() ^ 2) : char).join('')); 9 | }, 10 | decode(str){ 11 | if (!str) return str; 12 | let [ input, ...search ] = str.split('?'); 13 | 14 | return decodeURIComponent(input).split('').map((char, ind) => ind % 2 ? String.fromCharCode(char.charCodeAt(0) ^ 2) : char).join('') + (search.length ? '?' + search.join('?') : ''); 15 | }, 16 | }; 17 | 18 | export const plain = { 19 | encode(str) { 20 | if (!str) return str; 21 | return encodeURIComponent(str); 22 | }, 23 | decode(str) { 24 | if (!str) return str; 25 | return decodeURIComponent(str); 26 | }, 27 | }; 28 | 29 | export const base64 = { 30 | encode(str){ 31 | if (!str) return str; 32 | str = str.toString(); 33 | const b64chs = Array.from('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='); 34 | let u32; 35 | let c0; 36 | let c1; 37 | let c2; 38 | let asc = ''; 39 | let pad = str.length % 3; 40 | 41 | for (let i = 0; i < str.length;) { 42 | if((c0 = str.charCodeAt(i++)) > 255 || (c1 = str.charCodeAt(i++)) > 255 || (c2 = str.charCodeAt(i++)) > 255)throw new TypeError('invalid character found'); 43 | u32 = (c0 << 16) | (c1 << 8) | c2; 44 | asc += b64chs[u32 >> 18 & 63] 45 | + b64chs[u32 >> 12 & 63] 46 | + b64chs[u32 >> 6 & 63] 47 | + b64chs[u32 & 63]; 48 | } 49 | 50 | return encodeURIComponent(pad ? asc.slice(0, pad - 3) + '==='.substr(pad) : asc); 51 | }, 52 | decode(str){ 53 | if (!str) return str; 54 | str = decodeURIComponent(str.toString()); 55 | const b64tab = {"0":52,"1":53,"2":54,"3":55,"4":56,"5":57,"6":58,"7":59,"8":60,"9":61,"A":0,"B":1,"C":2,"D":3,"E":4,"F":5,"G":6,"H":7,"I":8,"J":9,"K":10,"L":11,"M":12,"N":13,"O":14,"P":15,"Q":16,"R":17,"S":18,"T":19,"U":20,"V":21,"W":22,"X":23,"Y":24,"Z":25,"a":26,"b":27,"c":28,"d":29,"e":30,"f":31,"g":32,"h":33,"i":34,"j":35,"k":36,"l":37,"m":38,"n":39,"o":40,"p":41,"q":42,"r":43,"s":44,"t":45,"u":46,"v":47,"w":48,"x":49,"y":50,"z":51,"+":62,"/":63,"=":64}; 56 | str = str.replace(/\s+/g, ''); 57 | str += '=='.slice(2 - (str.length & 3)); 58 | let u24; 59 | let bin = ''; 60 | let r1; 61 | let r2; 62 | 63 | for (let i = 0; i < str.length;) { 64 | u24 = b64tab[str.charAt(i++)] << 18 65 | | b64tab[str.charAt(i++)] << 12 66 | | (r1 = b64tab[str.charAt(i++)]) << 6 67 | | (r2 = b64tab[str.charAt(i++)]); 68 | bin += r1 === 64 ? String.fromCharCode(u24 >> 16 & 255) 69 | : r2 === 64 ? String.fromCharCode(u24 >> 16 & 255, u24 >> 8 & 255) 70 | : String.fromCharCode(u24 >> 16 & 255, u24 >> 8 & 255, u24 & 255); 71 | }; 72 | return bin; 73 | }, 74 | }; -------------------------------------------------------------------------------- /rewrite/cookie.js: -------------------------------------------------------------------------------- 1 | // ------------------------------------------------------------- 2 | // WARNING: this file is used by both the client and the server. 3 | // Do not use any browser or node-specific API! 4 | // ------------------------------------------------------------- 5 | import setCookie from 'set-cookie-parser'; 6 | 7 | function validateCookie(cookie, meta, js = false) { 8 | if (cookie.httpOnly && !!js) return false; 9 | 10 | if (cookie.domain.startsWith('.')) { 11 | 12 | if (!meta.url.hostname.endsWith(cookie.domain.slice(1))) return false; 13 | return true; 14 | }; 15 | 16 | if (cookie.domain !== meta.url.hostname) return false; 17 | if (cookie.secure && meta.url.protocol === 'http:') return false; 18 | if (!meta.url.pathname.startsWith(cookie.path)) return false; 19 | 20 | return true; 21 | }; 22 | 23 | async function db(openDB) { 24 | const db = await openDB('__op', 1, { 25 | upgrade(db, oldVersion, newVersion, transaction) { 26 | const store = db.createObjectStore('cookies', { 27 | keyPath: 'id', 28 | }); 29 | store.createIndex('path', 'path'); 30 | }, 31 | }); 32 | db.transaction(['cookies'], 'readwrite').store.index('path'); 33 | return db; 34 | }; 35 | 36 | 37 | function serialize(cookies = [], meta, js) { 38 | let str = ''; 39 | for (const cookie of cookies) { 40 | if (!validateCookie(cookie, meta, js)) continue; 41 | if (str.length) str += '; '; 42 | str += cookie.name; 43 | str += '=' 44 | str += cookie.value; 45 | }; 46 | return str; 47 | }; 48 | 49 | async function getCookies(db) { 50 | const now = new Date(); 51 | return (await db.getAll('cookies')).filter(cookie => { 52 | 53 | let expired = false; 54 | if (cookie.set) { 55 | if (cookie.maxAge) { 56 | expired = (cookie.set.getTime() + (cookie.maxAge * 1e3)) < now; 57 | } else if (cookie.expires) { 58 | expired = new Date(cookie.expires.toLocaleString()) < now; 59 | }; 60 | }; 61 | 62 | if (expired) { 63 | db.delete('cookies', cookie.id); 64 | return false; 65 | }; 66 | 67 | return true; 68 | }); 69 | }; 70 | 71 | function setCookies(data, db, meta) { 72 | if (!db) return false; 73 | 74 | const cookies = setCookie(data, { 75 | decodeValues: false, 76 | }) 77 | 78 | for (const cookie of cookies) { 79 | if (!cookie.domain) cookie.domain = '.' + meta.url.hostname; 80 | if (!cookie.path) cookie.path = '/'; 81 | 82 | if (!cookie.domain.startsWith('.')) { 83 | cookie.domain = '.' + cookie.domain; 84 | }; 85 | 86 | db.put('cookies', { 87 | ...cookie, 88 | id: `${cookie.domain}@${cookie.path}@${cookie.name}`, 89 | set: new Date(Date.now()), 90 | }); 91 | }; 92 | return true; 93 | }; 94 | 95 | export { validateCookie, getCookies, setCookies, db , serialize }; -------------------------------------------------------------------------------- /rewrite/css.js: -------------------------------------------------------------------------------- 1 | import { parse, walk, generate } from "css-tree"; 2 | import EventEmitter from "./events.js"; 3 | import parsel from "./parsel.js"; 4 | 5 | class CSS extends EventEmitter { 6 | constructor(ctx) { 7 | super(); 8 | this.ctx = ctx; 9 | this.meta = ctx.meta; 10 | this.parsel = parsel; 11 | this.parse = parse; 12 | this.walk = walk; 13 | this.generate = generate; 14 | }; 15 | rewrite(str, options) { 16 | return this.recast(str, options, 'rewrite'); 17 | }; 18 | source(str, options) { 19 | return this.recast(str, options, 'source'); 20 | }; 21 | recast(str, options, type) { 22 | if (!str) return str; 23 | str = new String(str).toString(); 24 | try { 25 | const ast = this.parse(str, { ...options, parseCustomProperty: true }); 26 | this.walk(ast, node => { 27 | this.emit(node.type, node, options, type); 28 | }); 29 | return this.generate(ast); 30 | } catch(e) { 31 | return str; 32 | }; 33 | }; 34 | }; 35 | 36 | export default CSS; -------------------------------------------------------------------------------- /rewrite/events.js: -------------------------------------------------------------------------------- 1 | // Copyright Joyent, Inc. and other Node contributors. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a 4 | // copy of this software and associated documentation files (the 5 | // "Software"), to deal in the Software without restriction, including 6 | // without limitation the rights to use, copy, modify, merge, publish, 7 | // distribute, sublicense, and/or sell copies of the Software, and to permit 8 | // persons to whom the Software is furnished to do so, subject to the 9 | // following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included 12 | // in all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 15 | // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN 17 | // NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 18 | // DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 19 | // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE 20 | // USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | 22 | 'use strict'; 23 | 24 | var R = typeof Reflect === 'object' ? Reflect : null 25 | var ReflectApply = R && typeof R.apply === 'function' 26 | ? R.apply 27 | : function ReflectApply(target, receiver, args) { 28 | return Function.prototype.apply.call(target, receiver, args); 29 | } 30 | 31 | var ReflectOwnKeys 32 | if (R && typeof R.ownKeys === 'function') { 33 | ReflectOwnKeys = R.ownKeys 34 | } else if (Object.getOwnPropertySymbols) { 35 | ReflectOwnKeys = function ReflectOwnKeys(target) { 36 | return Object.getOwnPropertyNames(target) 37 | .concat(Object.getOwnPropertySymbols(target)); 38 | }; 39 | } else { 40 | ReflectOwnKeys = function ReflectOwnKeys(target) { 41 | return Object.getOwnPropertyNames(target); 42 | }; 43 | } 44 | 45 | function ProcessEmitWarning(warning) { 46 | if (console && console.warn) console.warn(warning); 47 | } 48 | 49 | var NumberIsNaN = Number.isNaN || function NumberIsNaN(value) { 50 | return value !== value; 51 | } 52 | 53 | function EventEmitter() { 54 | EventEmitter.init.call(this); 55 | } 56 | 57 | export default EventEmitter; 58 | 59 | // Backwards-compat with node 0.10.x 60 | EventEmitter.EventEmitter = EventEmitter; 61 | 62 | EventEmitter.prototype._events = undefined; 63 | EventEmitter.prototype._eventsCount = 0; 64 | EventEmitter.prototype._maxListeners = undefined; 65 | 66 | // By default EventEmitters will print a warning if more than 10 listeners are 67 | // added to it. This is a useful default which helps finding memory leaks. 68 | var defaultMaxListeners = 10; 69 | 70 | function checkListener(listener) { 71 | if (typeof listener !== 'function') { 72 | throw new TypeError('The "listener" argument must be of type Function. Received type ' + typeof listener); 73 | } 74 | } 75 | 76 | Object.defineProperty(EventEmitter, 'defaultMaxListeners', { 77 | enumerable: true, 78 | get: function() { 79 | return defaultMaxListeners; 80 | }, 81 | set: function(arg) { 82 | if (typeof arg !== 'number' || arg < 0 || NumberIsNaN(arg)) { 83 | throw new RangeError('The value of "defaultMaxListeners" is out of range. It must be a non-negative number. Received ' + arg + '.'); 84 | } 85 | defaultMaxListeners = arg; 86 | } 87 | }); 88 | 89 | EventEmitter.init = function() { 90 | 91 | if (this._events === undefined || 92 | this._events === Object.getPrototypeOf(this)._events) { 93 | this._events = Object.create(null); 94 | this._eventsCount = 0; 95 | } 96 | 97 | this._maxListeners = this._maxListeners || undefined; 98 | }; 99 | 100 | // Obviously not all Emitters should be limited to 10. This function allows 101 | // that to be increased. Set to zero for unlimited. 102 | EventEmitter.prototype.setMaxListeners = function setMaxListeners(n) { 103 | if (typeof n !== 'number' || n < 0 || NumberIsNaN(n)) { 104 | throw new RangeError('The value of "n" is out of range. It must be a non-negative number. Received ' + n + '.'); 105 | } 106 | this._maxListeners = n; 107 | return this; 108 | }; 109 | 110 | function _getMaxListeners(that) { 111 | if (that._maxListeners === undefined) 112 | return EventEmitter.defaultMaxListeners; 113 | return that._maxListeners; 114 | } 115 | 116 | EventEmitter.prototype.getMaxListeners = function getMaxListeners() { 117 | return _getMaxListeners(this); 118 | }; 119 | 120 | EventEmitter.prototype.emit = function emit(type) { 121 | var args = []; 122 | for (var i = 1; i < arguments.length; i++) args.push(arguments[i]); 123 | var doError = (type === 'error'); 124 | 125 | var events = this._events; 126 | if (events !== undefined) 127 | doError = (doError && events.error === undefined); 128 | else if (!doError) 129 | return false; 130 | 131 | // If there is no 'error' event listener then throw. 132 | if (doError) { 133 | var er; 134 | if (args.length > 0) 135 | er = args[0]; 136 | if (er instanceof Error) { 137 | // Note: The comments on the `throw` lines are intentional, they show 138 | // up in Node's output if this results in an unhandled exception. 139 | throw er; // Unhandled 'error' event 140 | } 141 | // At least give some kind of context to the user 142 | var err = new Error('Unhandled error.' + (er ? ' (' + er.message + ')' : '')); 143 | err.context = er; 144 | throw err; // Unhandled 'error' event 145 | } 146 | 147 | var handler = events[type]; 148 | 149 | if (handler === undefined) 150 | return false; 151 | 152 | if (typeof handler === 'function') { 153 | ReflectApply(handler, this, args); 154 | } else { 155 | var len = handler.length; 156 | var listeners = arrayClone(handler, len); 157 | for (var i = 0; i < len; ++i) 158 | ReflectApply(listeners[i], this, args); 159 | } 160 | 161 | return true; 162 | }; 163 | 164 | function _addListener(target, type, listener, prepend) { 165 | var m; 166 | var events; 167 | var existing; 168 | 169 | checkListener(listener); 170 | 171 | events = target._events; 172 | if (events === undefined) { 173 | events = target._events = Object.create(null); 174 | target._eventsCount = 0; 175 | } else { 176 | // To avoid recursion in the case that type === "newListener"! Before 177 | // adding it to the listeners, first emit "newListener". 178 | if (events.newListener !== undefined) { 179 | target.emit('newListener', type, 180 | listener.listener ? listener.listener : listener); 181 | 182 | // Re-assign `events` because a newListener handler could have caused the 183 | // this._events to be assigned to a new object 184 | events = target._events; 185 | } 186 | existing = events[type]; 187 | } 188 | 189 | if (existing === undefined) { 190 | // Optimize the case of one listener. Don't need the extra array object. 191 | existing = events[type] = listener; 192 | ++target._eventsCount; 193 | } else { 194 | if (typeof existing === 'function') { 195 | // Adding the second element, need to change to array. 196 | existing = events[type] = 197 | prepend ? [listener, existing] : [existing, listener]; 198 | // If we've already got an array, just append. 199 | } else if (prepend) { 200 | existing.unshift(listener); 201 | } else { 202 | existing.push(listener); 203 | } 204 | 205 | // Check for listener leak 206 | m = _getMaxListeners(target); 207 | if (m > 0 && existing.length > m && !existing.warned) { 208 | existing.warned = true; 209 | // No error code for this since it is a Warning 210 | // eslint-disable-next-line no-restricted-syntax 211 | var w = new Error('Possible EventEmitter memory leak detected. ' + 212 | existing.length + ' ' + String(type) + ' listeners ' + 213 | 'added. Use emitter.setMaxListeners() to ' + 214 | 'increase limit'); 215 | w.name = 'MaxListenersExceededWarning'; 216 | w.emitter = target; 217 | w.type = type; 218 | w.count = existing.length; 219 | ProcessEmitWarning(w); 220 | } 221 | } 222 | 223 | return target; 224 | } 225 | 226 | EventEmitter.prototype.addListener = function addListener(type, listener) { 227 | return _addListener(this, type, listener, false); 228 | }; 229 | 230 | EventEmitter.prototype.on = EventEmitter.prototype.addListener; 231 | 232 | EventEmitter.prototype.prependListener = 233 | function prependListener(type, listener) { 234 | return _addListener(this, type, listener, true); 235 | }; 236 | 237 | function onceWrapper() { 238 | if (!this.fired) { 239 | this.target.removeListener(this.type, this.wrapFn); 240 | this.fired = true; 241 | if (arguments.length === 0) 242 | return this.listener.call(this.target); 243 | return this.listener.apply(this.target, arguments); 244 | } 245 | } 246 | 247 | function _onceWrap(target, type, listener) { 248 | var state = { fired: false, wrapFn: undefined, target: target, type: type, listener: listener }; 249 | var wrapped = onceWrapper.bind(state); 250 | wrapped.listener = listener; 251 | state.wrapFn = wrapped; 252 | return wrapped; 253 | } 254 | 255 | EventEmitter.prototype.once = function once(type, listener) { 256 | checkListener(listener); 257 | this.on(type, _onceWrap(this, type, listener)); 258 | return this; 259 | }; 260 | 261 | EventEmitter.prototype.prependOnceListener = 262 | function prependOnceListener(type, listener) { 263 | checkListener(listener); 264 | this.prependListener(type, _onceWrap(this, type, listener)); 265 | return this; 266 | }; 267 | 268 | // Emits a 'removeListener' event if and only if the listener was removed. 269 | EventEmitter.prototype.removeListener = 270 | function removeListener(type, listener) { 271 | var list, events, position, i, originalListener; 272 | 273 | checkListener(listener); 274 | 275 | events = this._events; 276 | if (events === undefined) 277 | return this; 278 | 279 | list = events[type]; 280 | if (list === undefined) 281 | return this; 282 | 283 | if (list === listener || list.listener === listener) { 284 | if (--this._eventsCount === 0) 285 | this._events = Object.create(null); 286 | else { 287 | delete events[type]; 288 | if (events.removeListener) 289 | this.emit('removeListener', type, list.listener || listener); 290 | } 291 | } else if (typeof list !== 'function') { 292 | position = -1; 293 | 294 | for (i = list.length - 1; i >= 0; i--) { 295 | if (list[i] === listener || list[i].listener === listener) { 296 | originalListener = list[i].listener; 297 | position = i; 298 | break; 299 | } 300 | } 301 | 302 | if (position < 0) 303 | return this; 304 | 305 | if (position === 0) 306 | list.shift(); 307 | else { 308 | spliceOne(list, position); 309 | } 310 | 311 | if (list.length === 1) 312 | events[type] = list[0]; 313 | 314 | if (events.removeListener !== undefined) 315 | this.emit('removeListener', type, originalListener || listener); 316 | } 317 | 318 | return this; 319 | }; 320 | 321 | EventEmitter.prototype.off = EventEmitter.prototype.removeListener; 322 | 323 | EventEmitter.prototype.removeAllListeners = 324 | function removeAllListeners(type) { 325 | var listeners, events, i; 326 | 327 | events = this._events; 328 | if (events === undefined) 329 | return this; 330 | 331 | // not listening for removeListener, no need to emit 332 | if (events.removeListener === undefined) { 333 | if (arguments.length === 0) { 334 | this._events = Object.create(null); 335 | this._eventsCount = 0; 336 | } else if (events[type] !== undefined) { 337 | if (--this._eventsCount === 0) 338 | this._events = Object.create(null); 339 | else 340 | delete events[type]; 341 | } 342 | return this; 343 | } 344 | 345 | // emit removeListener for all listeners on all events 346 | if (arguments.length === 0) { 347 | var keys = Object.keys(events); 348 | var key; 349 | for (i = 0; i < keys.length; ++i) { 350 | key = keys[i]; 351 | if (key === 'removeListener') continue; 352 | this.removeAllListeners(key); 353 | } 354 | this.removeAllListeners('removeListener'); 355 | this._events = Object.create(null); 356 | this._eventsCount = 0; 357 | return this; 358 | } 359 | 360 | listeners = events[type]; 361 | 362 | if (typeof listeners === 'function') { 363 | this.removeListener(type, listeners); 364 | } else if (listeners !== undefined) { 365 | // LIFO order 366 | for (i = listeners.length - 1; i >= 0; i--) { 367 | this.removeListener(type, listeners[i]); 368 | } 369 | } 370 | 371 | return this; 372 | }; 373 | 374 | function _listeners(target, type, unwrap) { 375 | var events = target._events; 376 | 377 | if (events === undefined) 378 | return []; 379 | 380 | var evlistener = events[type]; 381 | if (evlistener === undefined) 382 | return []; 383 | 384 | if (typeof evlistener === 'function') 385 | return unwrap ? [evlistener.listener || evlistener] : [evlistener]; 386 | 387 | return unwrap ? 388 | unwrapListeners(evlistener) : arrayClone(evlistener, evlistener.length); 389 | } 390 | 391 | EventEmitter.prototype.listeners = function listeners(type) { 392 | return _listeners(this, type, true); 393 | }; 394 | 395 | EventEmitter.prototype.rawListeners = function rawListeners(type) { 396 | return _listeners(this, type, false); 397 | }; 398 | 399 | EventEmitter.listenerCount = function(emitter, type) { 400 | if (typeof emitter.listenerCount === 'function') { 401 | return emitter.listenerCount(type); 402 | } else { 403 | return listenerCount.call(emitter, type); 404 | } 405 | }; 406 | 407 | EventEmitter.prototype.listenerCount = listenerCount; 408 | function listenerCount(type) { 409 | var events = this._events; 410 | 411 | if (events !== undefined) { 412 | var evlistener = events[type]; 413 | 414 | if (typeof evlistener === 'function') { 415 | return 1; 416 | } else if (evlistener !== undefined) { 417 | return evlistener.length; 418 | } 419 | } 420 | 421 | return 0; 422 | } 423 | 424 | EventEmitter.prototype.eventNames = function eventNames() { 425 | return this._eventsCount > 0 ? ReflectOwnKeys(this._events) : []; 426 | }; 427 | 428 | function arrayClone(arr, n) { 429 | var copy = new Array(n); 430 | for (var i = 0; i < n; ++i) 431 | copy[i] = arr[i]; 432 | return copy; 433 | } 434 | 435 | function spliceOne(list, index) { 436 | for (; index + 1 < list.length; index++) 437 | list[index] = list[index + 1]; 438 | list.pop(); 439 | } 440 | 441 | function unwrapListeners(arr) { 442 | var ret = new Array(arr.length); 443 | for (var i = 0; i < ret.length; ++i) { 444 | ret[i] = arr[i].listener || arr[i]; 445 | } 446 | return ret; 447 | } 448 | 449 | function once(emitter, name) { 450 | return new Promise(function (resolve, reject) { 451 | function errorListener(err) { 452 | emitter.removeListener(name, resolver); 453 | reject(err); 454 | } 455 | 456 | function resolver() { 457 | if (typeof emitter.removeListener === 'function') { 458 | emitter.removeListener('error', errorListener); 459 | } 460 | resolve([].slice.call(arguments)); 461 | }; 462 | 463 | eventTargetAgnosticAddListener(emitter, name, resolver, { once: true }); 464 | if (name !== 'error') { 465 | addErrorHandlerIfEventEmitter(emitter, errorListener, { once: true }); 466 | } 467 | }); 468 | } 469 | 470 | function addErrorHandlerIfEventEmitter(emitter, handler, flags) { 471 | if (typeof emitter.on === 'function') { 472 | eventTargetAgnosticAddListener(emitter, 'error', handler, flags); 473 | } 474 | } 475 | 476 | function eventTargetAgnosticAddListener(emitter, name, listener, flags) { 477 | if (typeof emitter.on === 'function') { 478 | if (flags.once) { 479 | emitter.once(name, listener); 480 | } else { 481 | emitter.on(name, listener); 482 | } 483 | } else if (typeof emitter.addEventListener === 'function') { 484 | // EventTarget does not have `error` event semantics like Node 485 | // EventEmitters, we do not listen for `error` events here. 486 | emitter.addEventListener(name, function wrapListener(arg) { 487 | // IE does not have builtin `{ once: true }` support so we 488 | // have to do it manually. 489 | if (flags.once) { 490 | emitter.removeEventListener(name, wrapListener); 491 | } 492 | listener(arg); 493 | }); 494 | } else { 495 | throw new TypeError('The "emitter" argument must be of type EventEmitter. Received type ' + typeof emitter); 496 | } 497 | } -------------------------------------------------------------------------------- /rewrite/html.js: -------------------------------------------------------------------------------- 1 | import EventEmitter from './events.js'; 2 | import { parse, parseFragment, serialize } from 'parse5'; 3 | 4 | class HTML extends EventEmitter { 5 | constructor(ctx) { 6 | super(); 7 | this.ctx = ctx; 8 | this.rewriteUrl = ctx.rewriteUrl; 9 | this.sourceUrl = ctx.sourceUrl; 10 | }; 11 | rewrite(str, options = {}) { 12 | if (!str) return str; 13 | return this.recast(str, node => { 14 | if (node.tagName) this.emit('element', node, 'rewrite'); 15 | if (node.attr) this.emit('attr', node, 'rewrite'); 16 | if (node.nodeName === '#text') this.emit('text', node, 'rewrite'); 17 | }, options) 18 | }; 19 | source(str, options = {}) { 20 | if (!str) return str; 21 | return this.recast(str, node => { 22 | if (node.tagName) this.emit('element', node, 'source'); 23 | if (node.attr) this.emit('attr', node, 'source'); 24 | if (node.nodeName === '#text') this.emit('text', node, 'source'); 25 | }, options) 26 | }; 27 | recast(str, fn, options = {}) { 28 | try { 29 | const ast = (options.document ? parse : parseFragment)(new String(str).toString()); 30 | this.iterate(ast, fn, options); 31 | return serialize(ast); 32 | } catch(e) { 33 | return str; 34 | }; 35 | }; 36 | iterate(ast, fn, fnOptions) { 37 | if (!ast) return ast; 38 | 39 | if (ast.tagName) { 40 | const element = new P5Element(ast, false, fnOptions); 41 | fn(element); 42 | if (ast.attrs) { 43 | for (const attr of ast.attrs) { 44 | if (!attr.skip) fn(new AttributeEvent(element, attr, fnOptions)); 45 | }; 46 | }; 47 | }; 48 | 49 | if (ast.childNodes) { 50 | for (const child of ast.childNodes) { 51 | if (!child.skip) this.iterate(child, fn, fnOptions); 52 | }; 53 | }; 54 | 55 | if (ast.nodeName === '#text') { 56 | fn(new TextEvent(ast, new P5Element(ast.parentNode), false, fnOptions)); 57 | }; 58 | 59 | return ast; 60 | }; 61 | wrapSrcset(str, meta = this.ctx.meta) { 62 | return str.split(',').map(src => { 63 | const parts = src.trimStart().split(' '); 64 | if (parts[0]) parts[0] = this.ctx.rewriteUrl(parts[0], meta); 65 | return parts.join(' '); 66 | }).join(', '); 67 | }; 68 | unwrapSrcset(str, meta = this.ctx.meta) { 69 | return str.split(',').map(src => { 70 | const parts = src.trimStart().split(' '); 71 | if (parts[0]) parts[0] = this.ctx.sourceUrl(parts[0], meta); 72 | return parts.join(' '); 73 | }).join(', '); 74 | }; 75 | static parse = parse; 76 | static parseFragment = parseFragment; 77 | static serialize = serialize; 78 | }; 79 | 80 | class P5Element extends EventEmitter { 81 | constructor(node, stream = false, options = {}) { 82 | super(); 83 | this.stream = stream; 84 | this.node = node; 85 | this.options = options; 86 | }; 87 | setAttribute(name, value) { 88 | for (const attr of this.attrs) { 89 | if (attr.name === name) { 90 | attr.value = value; 91 | return true; 92 | }; 93 | }; 94 | 95 | this.attrs.push( 96 | { 97 | name, 98 | value, 99 | } 100 | ); 101 | }; 102 | getAttribute(name) { 103 | const attr = this.attrs.find(attr => attr.name === name) || {}; 104 | return attr.value; 105 | }; 106 | hasAttribute(name) { 107 | return !!this.attrs.find(attr => attr.name === name); 108 | }; 109 | removeAttribute(name) { 110 | const i = this.attrs.findIndex(attr => attr.name === name); 111 | if (typeof i !== 'undefined') this.attrs.splice(i, 1); 112 | }; 113 | get tagName() { 114 | return this.node.tagName; 115 | }; 116 | set tagName(val) { 117 | this.node.tagName = val; 118 | }; 119 | get childNodes() { 120 | return !this.stream ? this.node.childNodes : null; 121 | }; 122 | get innerHTML() { 123 | return !this.stream ? serialize( 124 | { 125 | nodeName: '#document-fragment', 126 | childNodes: this.childNodes, 127 | } 128 | ) : null; 129 | }; 130 | set innerHTML(val) { 131 | if (!this.stream) this.node.childNodes = parseFragment(val).childNodes; 132 | }; 133 | get outerHTML() { 134 | return !this.stream ? serialize( 135 | { 136 | nodeName: '#document-fragment', 137 | childNodes: [ this ], 138 | } 139 | ) : null; 140 | }; 141 | set outerHTML(val) { 142 | if (!this.stream) this.parentNode.childNodes.splice(this.parentNode.childNodes.findIndex(node => node === this.node), 1, ...parseFragment(val).childNodes); 143 | }; 144 | get textContent() { 145 | if (this.stream) return null; 146 | 147 | let str = ''; 148 | iterate(this.node, node => { 149 | if (node.nodeName === '#text') str += node.value; 150 | }); 151 | 152 | return str; 153 | }; 154 | set textContent(val) { 155 | if (!this.stream) this.node.childNodes = [ 156 | { 157 | nodeName: '#text', 158 | value: val, 159 | parentNode: this.node 160 | } 161 | ]; 162 | }; 163 | get nodeName() { 164 | return this.node.nodeName; 165 | } 166 | get parentNode() { 167 | return this.node.parentNode ? new P5Element(this.node.parentNode) : null; 168 | }; 169 | get attrs() { 170 | return this.node.attrs; 171 | } 172 | get namespaceURI() { 173 | return this.node.namespaceURI; 174 | } 175 | }; 176 | 177 | class AttributeEvent { 178 | constructor(node, attr, options = {}) { 179 | this.attr = attr; 180 | this.attrs = node.attrs; 181 | this.node = node; 182 | this.options = options; 183 | }; 184 | delete() { 185 | const i = this.attrs.findIndex(attr => attr === this.attr); 186 | 187 | this.attrs.splice(i, 1); 188 | 189 | Object.defineProperty(this, 'deleted', { 190 | get: () => true, 191 | }); 192 | 193 | return true; 194 | }; 195 | get name() { 196 | return this.attr.name; 197 | }; 198 | 199 | set name(val) { 200 | this.attr.name = val; 201 | }; 202 | get value() { 203 | return this.attr.value; 204 | }; 205 | 206 | set value(val) { 207 | this.attr.value = val; 208 | }; 209 | get deleted() { 210 | return false; 211 | }; 212 | }; 213 | 214 | class TextEvent { 215 | constructor(node, element, stream = false, options = {}) { 216 | this.stream = stream; 217 | this.node = node; 218 | this.element = element; 219 | this.options = options; 220 | }; 221 | get nodeName() { 222 | return this.node.nodeName; 223 | } 224 | get parentNode() { 225 | return this.element; 226 | }; 227 | get value() { 228 | return this.stream ? this.node.text : this.node.value; 229 | }; 230 | set value(val) { 231 | 232 | if (this.stream) this.node.text = val; 233 | else this.node.value = val; 234 | }; 235 | }; 236 | 237 | export default HTML; -------------------------------------------------------------------------------- /rewrite/index.js: -------------------------------------------------------------------------------- 1 | import HTML from './html.js'; 2 | import CSS from './css.js'; 3 | import JS from './js.js'; 4 | import setCookie from 'set-cookie-parser'; 5 | import { xor, base64, plain } from './codecs.js'; 6 | import mimeTypes from './mime.js'; 7 | import { validateCookie, db, getCookies, setCookies, serialize } from './cookie.js'; 8 | import { attributes, isUrl, isForbidden, isHtml, isSrcset, isStyle, text, injectHead, createInjection } from './rewrite.html.js'; 9 | import { importStyle, url } from './rewrite.css.js'; 10 | //import { call, destructureDeclaration, dynamicImport, getProperty, importDeclaration, setProperty, sourceMethods, wrapEval, wrapIdentifier } from './rewrite.script.js'; 11 | import { dynamicImport, identifier, importDeclaration, property, unwrap, wrapEval } from './rewrite.script.js'; 12 | import { openDB } from 'idb'; 13 | import parsel from './parsel.js'; 14 | import UVClient from '../client/index.js'; 15 | import Bowser from 'bowser'; 16 | 17 | 18 | const valid_chars = "!#$%&'*+-.0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ^_`abcdefghijklmnopqrstuvwxyz|~"; 19 | const reserved_chars = "%"; 20 | 21 | class Ultraviolet { 22 | constructor(options = {}) { 23 | this.prefix = options.prefix || '/service/'; 24 | //this.urlRegex = /^(#|about:|data:|mailto:|javascript:)/; 25 | this.urlRegex = /^(#|about:|data:|mailto:)/ 26 | this.rewriteUrl = options.rewriteUrl || this.rewriteUrl; 27 | this.sourceUrl = options.sourceUrl || this.sourceUrl; 28 | this.encodeUrl = options.encodeUrl || this.encodeUrl; 29 | this.decodeUrl = options.decodeUrl || this.decodeUrl; 30 | this.vanilla = 'vanilla' in options ? options.vanilla : false; 31 | this.meta = options.meta || {}; 32 | this.meta.base ||= undefined; 33 | this.meta.origin ||= ''; 34 | this.bundleScript = options.bundleScript || '/uv.bundle.js'; 35 | this.handlerScript = options.handlerScript || '/uv.handler.js'; 36 | this.configScript = options.handlerScript || '/uv.config.js'; 37 | this.meta.url ||= this.meta.base || ''; 38 | this.codec = Ultraviolet.codec; 39 | this.html = new HTML(this); 40 | this.css = new CSS(this); 41 | this.js = new JS(this); 42 | this.parsel = parsel; 43 | this.openDB = this.constructor.openDB; 44 | this.Bowser = this.constructor.Bowser; 45 | this.client = typeof self !== 'undefined' ? new UVClient((options.window || self)) : null; 46 | this.master = '__uv'; 47 | this.dataPrefix = '__uv$'; 48 | this.attributePrefix = '__uv'; 49 | this.createHtmlInject = createInjection; 50 | this.attrs = { 51 | isUrl, 52 | isForbidden, 53 | isHtml, 54 | isSrcset, 55 | isStyle, 56 | }; 57 | if (!this.vanilla) this.implementUVMiddleware(); 58 | this.cookie = { 59 | validateCookie, 60 | db: () => { 61 | return db(this.constructor.openDB); 62 | }, 63 | getCookies, 64 | setCookies, 65 | serialize, 66 | setCookie, 67 | }; 68 | }; 69 | rewriteUrl(str, meta = this.meta) { 70 | str = new String(str).trim(); 71 | if (!str || this.urlRegex.test(str)) return str; 72 | 73 | if (str.startsWith('javascript:')) { 74 | return 'javascript:' + this.js.rewrite(str.slice('javascript:'.length)); 75 | }; 76 | 77 | try { 78 | return meta.origin + this.prefix + this.encodeUrl(new URL(str, meta.base).href); 79 | } catch(e) { 80 | return meta.origin + this.prefix + this.encodeUrl(str); 81 | }; 82 | }; 83 | sourceUrl(str, meta = this.meta) { 84 | if (!str || this.urlRegex.test(str)) return str; 85 | try { 86 | return new URL( 87 | this.decodeUrl(str.slice(this.prefix.length + meta.origin.length)), 88 | meta.base 89 | ).href; 90 | } catch(e) { 91 | return this.decodeUrl(str.slice(this.prefix.length + meta.origin.length)); 92 | }; 93 | }; 94 | encodeUrl(str) { 95 | return encodeURIComponent(str); 96 | }; 97 | decodeUrl(str) { 98 | return decodeURIComponent(str); 99 | }; 100 | encodeProtocol(protocol) { 101 | protocol = protocol.toString(); 102 | 103 | let result = ''; 104 | 105 | for(let i = 0; i < protocol.length; i++){ 106 | const char = protocol[i]; 107 | 108 | if(valid_chars.includes(char) && !reserved_chars.includes(char)){ 109 | result += char; 110 | }else{ 111 | const code = char.charCodeAt(); 112 | result += '%' + code.toString(16).padStart(2, 0); 113 | } 114 | } 115 | 116 | return result; 117 | }; 118 | decodeProtocol(protocol) { 119 | if(typeof protocol != 'string')throw new TypeError('protocol must be a string'); 120 | 121 | let result = ''; 122 | 123 | for(let i = 0; i < protocol.length; i++){ 124 | const char = protocol[i]; 125 | 126 | if(char == '%'){ 127 | const code = parseInt(protocol.slice(i + 1, i + 3), 16); 128 | const decoded = String.fromCharCode(code); 129 | 130 | result += decoded; 131 | i += 2; 132 | }else{ 133 | result += char; 134 | } 135 | } 136 | 137 | return result; 138 | } 139 | implementUVMiddleware() { 140 | // HTML 141 | attributes(this); 142 | text(this); 143 | injectHead(this); 144 | // CSS 145 | url(this); 146 | importStyle(this); 147 | // JS 148 | importDeclaration(this); 149 | dynamicImport(this); 150 | property(this); 151 | wrapEval(this); 152 | identifier(this); 153 | unwrap(this); 154 | }; 155 | get rewriteHtml() { 156 | return this.html.rewrite.bind(this.html); 157 | }; 158 | get sourceHtml() { 159 | return this.html.source.bind(this.html); 160 | }; 161 | get rewriteCSS() { 162 | return this.css.rewrite.bind(this.css); 163 | }; 164 | get sourceCSS() { 165 | return this.css.source.bind(this.css); 166 | }; 167 | get rewriteJS() { 168 | return this.js.rewrite.bind(this.js); 169 | }; 170 | get sourceJS() { 171 | return this.js.source.bind(this.js); 172 | }; 173 | static codec = { xor, base64, plain }; 174 | static mime = mimeTypes; 175 | static setCookie = setCookie; 176 | static openDB = openDB; 177 | static Bowser = Bowser; 178 | }; 179 | 180 | export default Ultraviolet; 181 | if (typeof self === 'object') self.Ultraviolet = Ultraviolet; -------------------------------------------------------------------------------- /rewrite/js.js: -------------------------------------------------------------------------------- 1 | import { parseScript } from 'meriyah'; 2 | // import { parse } from 'acorn-hammerhead'; 3 | import { generate } from 'esotope-hammerhead'; 4 | import EventEmitter from './events.js'; 5 | 6 | class JS extends EventEmitter { 7 | constructor() { 8 | super(); 9 | /* 10 | this.parseOptions = { 11 | allowReturnOutsideFunction: true, 12 | allowImportExportEverywhere: true, 13 | ecmaVersion: 2021, 14 | }; 15 | */ 16 | this.parseOptions = { 17 | ranges: true, 18 | module: true, 19 | globalReturn: true, 20 | }; 21 | this.generationOptions = { 22 | format: { 23 | quotes: 'double', 24 | escapeless: true, 25 | compact: true, 26 | }, 27 | }; 28 | this.parse = parseScript /*parse*/; 29 | this.generate = generate; 30 | }; 31 | rewrite(str, data = {}) { 32 | return this.recast(str, data, 'rewrite'); 33 | }; 34 | source(str, data = {}) { 35 | return this.recast(str, data, 'source'); 36 | }; 37 | recast(str, data = {}, type = '') { 38 | try { 39 | const output = []; 40 | const ast = this.parse(str, this.parseOptions); 41 | const meta = { 42 | data, 43 | changes: [], 44 | input: str, 45 | ast, 46 | get slice() { 47 | return slice; 48 | }, 49 | }; 50 | let slice = 0; 51 | 52 | this.iterate(ast, (node, parent = null) => { 53 | if (parent && parent.inTransformer) node.isTransformer = true; 54 | node.parent = parent; 55 | 56 | this.emit(node.type, node, meta, type); 57 | }); 58 | 59 | meta.changes.sort((a, b) => (a.start - b.start) || (a.end - b.end)); 60 | 61 | for (const change of meta.changes) { 62 | if ('start' in change && typeof change.start === 'number') output.push(str.slice(slice, change.start)); 63 | if (change.node) output.push(typeof change.node === 'string' ? change.node : generate(change.node, this.generationOptions)); 64 | if ('end' in change && typeof change.end === 'number') slice = change.end; 65 | }; 66 | output.push(str.slice(slice)); 67 | return output.join(''); 68 | } catch(e) { 69 | return str; 70 | }; 71 | }; 72 | iterate(ast, handler) { 73 | if (typeof ast != 'object' || !handler) return; 74 | walk(ast, null, handler); 75 | function walk(node, parent, handler) { 76 | if (typeof node != 'object' || !handler) return; 77 | handler(node, parent, handler); 78 | for (const child in node) { 79 | if (child === 'parent') continue; 80 | if (Array.isArray(node[child])) { 81 | node[child].forEach(entry => { 82 | if (entry) walk(entry, node, handler) 83 | }); 84 | } else { 85 | if (node[child]) walk(node[child], node, handler); 86 | }; 87 | }; 88 | if (typeof node.iterateEnd === 'function') node.iterateEnd(); 89 | }; 90 | }; 91 | }; 92 | 93 | export default JS; -------------------------------------------------------------------------------- /rewrite/mime.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * mime-types 3 | * Copyright(c) 2014 Jonathan Ong 4 | * Copyright(c) 2015 Douglas Christopher Wilson 5 | * MIT Licensed 6 | */ 7 | 8 | 'use strict' 9 | 10 | /** 11 | * Module dependencies. 12 | * @private 13 | */ 14 | 15 | var $exports = {} 16 | 17 | import db from "mime-db"; 18 | 19 | var extname = function(path = '') { 20 | if (!path.includes('.')) return ''; 21 | const map = path.split('.'); 22 | 23 | return '.' + map[map.length - 1]; 24 | }; 25 | 26 | /** 27 | * Module variables. 28 | * @private 29 | */ 30 | 31 | var EXTRACT_TYPE_REGEXP = /^\s*([^;\s]*)(?:;|\s|$)/ 32 | var TEXT_TYPE_REGEXP = /^text\//i 33 | 34 | /** 35 | * Module exports. 36 | * @public 37 | */ 38 | 39 | $exports.charset = charset 40 | $exports.charsets = { lookup: charset } 41 | $exports.contentType = contentType 42 | $exports.extension = extension 43 | $exports.extensions = Object.create(null) 44 | $exports.lookup = lookup 45 | $exports.types = Object.create(null) 46 | 47 | // Populate the extensions/types maps 48 | populateMaps($exports.extensions, $exports.types) 49 | 50 | /** 51 | * Get the default charset for a MIME type. 52 | * 53 | * @param {string} type 54 | * @return {boolean|string} 55 | */ 56 | 57 | function charset (type) { 58 | if (!type || typeof type !== 'string') { 59 | return false 60 | } 61 | 62 | // TODO: use media-typer 63 | var match = EXTRACT_TYPE_REGEXP.exec(type) 64 | var mime = match && db[match[1].toLowerCase()] 65 | 66 | if (mime && mime.charset) { 67 | return mime.charset 68 | } 69 | 70 | // default text/* to utf-8 71 | if (match && TEXT_TYPE_REGEXP.test(match[1])) { 72 | return 'UTF-8' 73 | } 74 | 75 | return false 76 | } 77 | 78 | /** 79 | * Create a full Content-Type header given a MIME type or extension. 80 | * 81 | * @param {string} str 82 | * @return {boolean|string} 83 | */ 84 | 85 | function contentType (str) { 86 | // TODO: should this even be in this module? 87 | if (!str || typeof str !== 'string') { 88 | return false 89 | } 90 | 91 | var mime = str.indexOf('/') === -1 92 | ? $exports.lookup(str) 93 | : str 94 | 95 | if (!mime) { 96 | return false 97 | } 98 | 99 | // TODO: use content-type or other module 100 | if (mime.indexOf('charset') === -1) { 101 | var charset = $exports.charset(mime) 102 | if (charset) mime += '; charset=' + charset.toLowerCase() 103 | } 104 | 105 | return mime 106 | } 107 | 108 | /** 109 | * Get the default extension for a MIME type. 110 | * 111 | * @param {string} type 112 | * @return {boolean|string} 113 | */ 114 | 115 | function extension (type) { 116 | if (!type || typeof type !== 'string') { 117 | return false 118 | } 119 | 120 | // TODO: use media-typer 121 | var match = EXTRACT_TYPE_REGEXP.exec(type) 122 | 123 | // get extensions 124 | var exts = match && $exports.extensions[match[1].toLowerCase()] 125 | 126 | if (!exts || !exts.length) { 127 | return false 128 | } 129 | 130 | return exts[0] 131 | } 132 | 133 | /** 134 | * Lookup the MIME type for a file path/extension. 135 | * 136 | * @param {string} path 137 | * @return {boolean|string} 138 | */ 139 | 140 | function lookup (path) { 141 | if (!path || typeof path !== 'string') { 142 | return false 143 | } 144 | 145 | // get the extension ("ext" or ".ext" or full path) 146 | var extension = extname('x.' + path) 147 | .toLowerCase() 148 | .substr(1) 149 | 150 | if (!extension) { 151 | return false 152 | } 153 | 154 | return $exports.types[extension] || false 155 | } 156 | 157 | /** 158 | * Populate the extensions and types maps. 159 | * @private 160 | */ 161 | 162 | function populateMaps (extensions, types) { 163 | // source preference (least -> most) 164 | var preference = ['nginx', 'apache', undefined, 'iana'] 165 | 166 | Object.keys(db).forEach(function forEachMimeType (type) { 167 | var mime = db[type] 168 | var exts = mime.extensions 169 | 170 | if (!exts || !exts.length) { 171 | return 172 | } 173 | 174 | // mime -> extensions 175 | extensions[type] = exts 176 | 177 | // extension -> mime 178 | for (var i = 0; i < exts.length; i++) { 179 | var extension = exts[i] 180 | 181 | if (types[extension]) { 182 | var from = preference.indexOf(db[types[extension]].source) 183 | var to = preference.indexOf(mime.source) 184 | 185 | if (types[extension] !== 'application/octet-stream' && 186 | (from > to || (from === to && types[extension].substr(0, 12) === 'application/'))) { 187 | // skip the remapping 188 | continue 189 | } 190 | } 191 | 192 | // set the extension -> mime 193 | types[extension] = type 194 | } 195 | }) 196 | } 197 | 198 | export default $exports; -------------------------------------------------------------------------------- /rewrite/parsel.js: -------------------------------------------------------------------------------- 1 | export default (function (exports) { 2 | 'use strict'; 3 | 4 | const TOKENS = { 5 | attribute: /\[\s*(?:(?\*|[-\w]*)\|)?(?[-\w\u{0080}-\u{FFFF}]+)\s*(?:(?\W?=)\s*(?.+?)\s*(?[iIsS])?\s*)?\]/gu, 6 | id: /#(?(?:[-\w\u{0080}-\u{FFFF}]|\\.)+)/gu, 7 | class: /\.(?(?:[-\w\u{0080}-\u{FFFF}]|\\.)+)/gu, 8 | comma: /\s*,\s*/g, // must be before combinator 9 | combinator: /\s*[\s>+~]\s*/g, // this must be after attribute 10 | "pseudo-element": /::(?[-\w\u{0080}-\u{FFFF}]+)(?:\((?¶+)\))?/gu, // this must be before pseudo-class 11 | "pseudo-class": /:(?[-\w\u{0080}-\u{FFFF}]+)(?:\((?¶+)\))?/gu, 12 | type: /(?:(?\*|[-\w]*)\|)?(?[-\w\u{0080}-\u{FFFF}]+)|\*/gu // this must be last 13 | }; 14 | 15 | const TOKENS_WITH_PARENS = new Set(["pseudo-class", "pseudo-element"]); 16 | const TOKENS_WITH_STRINGS = new Set([...TOKENS_WITH_PARENS, "attribute"]); 17 | const TRIM_TOKENS = new Set(["combinator", "comma"]); 18 | const RECURSIVE_PSEUDO_CLASSES = new Set(["not", "is", "where", "has", "matches", "-moz-any", "-webkit-any", "nth-child", "nth-last-child"]); 19 | 20 | const RECURSIVE_PSEUDO_CLASSES_ARGS = { 21 | "nth-child": /(?[\dn+-]+)\s+of\s+(?.+)/ 22 | }; 23 | 24 | RECURSIVE_PSEUDO_CLASSES["nth-last-child"] = RECURSIVE_PSEUDO_CLASSES_ARGS["nth-child"]; 25 | 26 | const TOKENS_FOR_RESTORE = Object.assign({}, TOKENS); 27 | TOKENS_FOR_RESTORE["pseudo-element"] = RegExp(TOKENS["pseudo-element"].source.replace("(?¶+)", "(?.+?)"), "gu"); 28 | TOKENS_FOR_RESTORE["pseudo-class"] = RegExp(TOKENS["pseudo-class"].source.replace("(?¶+)", "(?.+)"), "gu"); 29 | 30 | function gobbleParens(text, i) { 31 | let str = "", stack = []; 32 | 33 | for (; i < text.length; i++) { 34 | let char = text[i]; 35 | 36 | if (char === "(") { 37 | stack.push(char); 38 | } 39 | else if (char === ")") { 40 | if (stack.length > 0) { 41 | stack.pop(); 42 | } 43 | else { 44 | throw new Error("Closing paren without opening paren at " + i); 45 | } 46 | } 47 | 48 | str += char; 49 | 50 | if (stack.length === 0) { 51 | return str; 52 | } 53 | } 54 | 55 | throw new Error("Opening paren without closing paren"); 56 | } 57 | 58 | function tokenizeBy (text, grammar) { 59 | if (!text) { 60 | return []; 61 | } 62 | 63 | var strarr = [text]; 64 | 65 | for (var token in grammar) { 66 | let pattern = grammar[token]; 67 | 68 | for (var i=0; i < strarr.length; i++) { // Don’t cache length as it changes during the loop 69 | var str = strarr[i]; 70 | 71 | if (typeof str === "string") { 72 | pattern.lastIndex = 0; 73 | 74 | var match = pattern.exec(str); 75 | 76 | if (match) { 77 | let from = match.index - 1; 78 | let args = []; 79 | let content = match[0]; 80 | 81 | let before = str.slice(0, from + 1); 82 | if (before) { 83 | args.push(before); 84 | } 85 | 86 | args.push({ 87 | type: token, 88 | content, 89 | ...match.groups 90 | }); 91 | 92 | let after = str.slice(from + content.length + 1); 93 | if (after) { 94 | args.push(after); 95 | } 96 | 97 | strarr.splice(i, 1, ...args); 98 | } 99 | 100 | } 101 | } 102 | } 103 | 104 | let offset = 0; 105 | for (let i=0; i { 134 | strings.push({str, start}); 135 | return quote + "§".repeat(content.length) + quote; 136 | }); 137 | 138 | // Now that strings are out of the way, extract parens and replace them with parens with whitespace (to preserve offsets) 139 | let parens = [], offset = 0, start; 140 | while ((start = selector.indexOf("(", offset)) > -1) { 141 | let str = gobbleParens(selector, start); 142 | parens.push({str, start}); 143 | selector = selector.substring(0, start) + "(" + "¶".repeat(str.length - 2) + ")" + selector.substring(start + str.length); 144 | offset = start + str.length; 145 | } 146 | 147 | // Now we have no nested structures and we can parse with regexes 148 | let tokens = tokenizeBy(selector, TOKENS); 149 | 150 | // Now restore parens and strings in reverse order 151 | function restoreNested(strings, regex, types) { 152 | for (let str of strings) { 153 | for (let token of tokens) { 154 | if (types.has(token.type) && token.pos[0] < str.start && str.start < token.pos[1]) { 155 | let content = token.content; 156 | token.content = token.content.replace(regex, str.str); 157 | 158 | if (token.content !== content) { // actually changed? 159 | // Re-evaluate groups 160 | TOKENS_FOR_RESTORE[token.type].lastIndex = 0; 161 | let match = TOKENS_FOR_RESTORE[token.type].exec(token.content); 162 | let groups = match.groups; 163 | Object.assign(token, groups); 164 | } 165 | } 166 | } 167 | } 168 | } 169 | 170 | restoreNested(parens, /\(¶+\)/, TOKENS_WITH_PARENS); 171 | restoreNested(strings, /(['"])§+?\1/, TOKENS_WITH_STRINGS); 172 | 173 | return tokens; 174 | } 175 | 176 | // Convert a flat list of tokens into a tree of complex & compound selectors 177 | function nestTokens(tokens, {list = true} = {}) { 178 | if (list && tokens.find(t => t.type === "comma")) { 179 | let selectors = [], temp = []; 180 | 181 | for (let i=0; i=0; i--) { 206 | let token = tokens[i]; 207 | 208 | if (token.type === "combinator") { 209 | let left = tokens.slice(0, i); 210 | let right = tokens.slice(i + 1); 211 | 212 | return { 213 | type: "complex", 214 | combinator: token.content, 215 | left: nestTokens(left), 216 | right: nestTokens(right) 217 | }; 218 | } 219 | } 220 | 221 | if (tokens.length === 0) { 222 | return null; 223 | } 224 | 225 | // If we're here, there are no combinators, so it's just a list 226 | return tokens.length === 1? tokens[0] : { 227 | type: "compound", 228 | list: [...tokens] // clone to avoid pointers messing up the AST 229 | }; 230 | } 231 | 232 | // Traverse an AST (or part thereof), in depth-first order 233 | function walk(node, callback, o, parent) { 234 | if (!node) { 235 | return; 236 | } 237 | 238 | if (node.type === "complex") { 239 | walk(node.left, callback, o, node); 240 | walk(node.right, callback, o, node); 241 | } 242 | else if (node.type === "compound") { 243 | for (let n of node.list) { 244 | walk(n, callback, o, node); 245 | } 246 | } 247 | else if (node.subtree && o && o.subtree) { 248 | walk(node.subtree, callback, o, node); 249 | } 250 | 251 | callback(node, parent); 252 | } 253 | 254 | /** 255 | * Parse a CSS selector 256 | * @param selector {String} The selector to parse 257 | * @param options.recursive {Boolean} Whether to parse the arguments of pseudo-classes like :is(), :has() etc. Defaults to true. 258 | * @param options.list {Boolean} Whether this can be a selector list (A, B, C etc). Defaults to true. 259 | */ 260 | function parse(selector, {recursive = true, list = true} = {}) { 261 | let tokens = tokenize(selector); 262 | 263 | if (!tokens) { 264 | return null; 265 | } 266 | 267 | let ast = nestTokens(tokens, {list}); 268 | 269 | if (recursive) { 270 | walk(ast, node => { 271 | if (node.type === "pseudo-class" && node.argument) { 272 | if (RECURSIVE_PSEUDO_CLASSES.has(node.name)) { 273 | let argument = node.argument; 274 | const childArg = RECURSIVE_PSEUDO_CLASSES_ARGS[node.name]; 275 | if (childArg) { 276 | const match = childArg.exec(argument); 277 | if (!match) { 278 | return; 279 | } 280 | 281 | Object.assign(node, match.groups); 282 | argument = match.groups.subtree; 283 | } 284 | if (argument) { 285 | node.subtree = parse(argument, {recursive: true, list: true}); 286 | } 287 | } 288 | } 289 | }); 290 | } 291 | 292 | return ast; 293 | } 294 | 295 | function specificityToNumber(specificity, base) { 296 | base = base || Math.max(...specificity) + 1; 297 | 298 | return specificity[0] * base ** 2 + specificity[1] * base + specificity[2]; 299 | } 300 | 301 | function maxIndexOf(arr) { 302 | let max = arr[0], ret = 0; 303 | 304 | for (let i=0; i max) { 306 | ret = i; 307 | max = arr[i]; 308 | } 309 | } 310 | 311 | return arr.length === 0? -1 : ret; 312 | } 313 | 314 | /** 315 | * Calculate specificity of a selector. 316 | * If the selector is a list, the max specificity is returned. 317 | */ 318 | function specificity(selector, {format = "array"} = {}) { 319 | let ast = typeof selector === "object"? selector : parse(selector, {recursive: true}); 320 | 321 | if (!ast) { 322 | return null; 323 | } 324 | 325 | if (ast.type === "list") { 326 | // Return max specificity 327 | let base = 10; 328 | let specificities = ast.list.map(s => { 329 | let sp = specificity(s); 330 | base = Math.max(base, ...sp); 331 | return sp; 332 | }); 333 | let numbers = specificities.map(s => specificityToNumber(s, base)); 334 | let i = maxIndexOf(numbers); 335 | return specificities[i]; 336 | } 337 | 338 | let ret = [0, 0, 0]; 339 | 340 | walk(ast, node => { 341 | if (node.type === "id") { 342 | ret[0]++; 343 | } 344 | else if (node.type === "class" || node.type === "attribute") { 345 | ret[1]++; 346 | } 347 | else if ((node.type === "type" && node.content !== "*") || node.type === "pseudo-element") { 348 | ret[2]++; 349 | } 350 | else if (node.type === "pseudo-class" && node.name !== "where") { 351 | if (RECURSIVE_PSEUDO_CLASSES.has(node.name) && node.subtree) { 352 | // Max of argument list 353 | let sub = specificity(node.subtree); 354 | sub.forEach((s, i) => ret[i] += s); 355 | } 356 | else { 357 | ret[1]++; 358 | } 359 | } 360 | }); 361 | 362 | return ret; 363 | } 364 | 365 | exports.RECURSIVE_PSEUDO_CLASSES = RECURSIVE_PSEUDO_CLASSES; 366 | exports.RECURSIVE_PSEUDO_CLASSES_ARGS = RECURSIVE_PSEUDO_CLASSES_ARGS; 367 | exports.TOKENS = TOKENS; 368 | exports.TRIM_TOKENS = TRIM_TOKENS; 369 | exports.gobbleParens = gobbleParens; 370 | exports.nestTokens = nestTokens; 371 | exports.parse = parse; 372 | exports.specificity = specificity; 373 | exports.specificityToNumber = specificityToNumber; 374 | exports.tokenize = tokenize; 375 | exports.tokenizeBy = tokenizeBy; 376 | exports.walk = walk; 377 | 378 | Object.defineProperty(exports, '__esModule', { value: true }); 379 | 380 | return exports; 381 | 382 | }({})); -------------------------------------------------------------------------------- /rewrite/rewrite.css.js: -------------------------------------------------------------------------------- 1 | function url(ctx) { 2 | const { css } = ctx; 3 | css.on('Url', (node, data, type) => { 4 | node.value = type === 'rewrite' ? ctx.rewriteUrl(node.value) : ctx.sourceUrl(node.value); 5 | }); 6 | }; 7 | 8 | function importStyle(ctx) { 9 | const { css } = ctx; 10 | css.on('Atrule', (node, data, type) => { 11 | if (node.name !== 'import') return false; 12 | const { data: url } = node.prelude.children.head; 13 | // Already handling Url's 14 | if (url.type === 'Url') return false; 15 | url.value = type === 'rewrite' ? ctx.rewriteUrl(url.value) : ctx.sourceUrl(url.value); 16 | 17 | }); 18 | }; 19 | 20 | export { url, importStyle }; -------------------------------------------------------------------------------- /rewrite/rewrite.html.js: -------------------------------------------------------------------------------- 1 | function attributes(ctx, meta = ctx.meta) { 2 | const { html, js, css, attributePrefix, handlerScript, bundleScript } = ctx; 3 | const origPrefix = attributePrefix + '-attr-'; 4 | 5 | html.on('attr', (attr, type) => { 6 | if (attr.node.tagName === 'base' && attr.name === 'href' && attr.options.document) { 7 | meta.base = new URL(attr.value, meta.url); 8 | }; 9 | 10 | if (type === 'rewrite' && isUrl(attr.name, attr.tagName)) { 11 | attr.node.setAttribute(origPrefix + attr.name, attr.value); 12 | attr.value = ctx.rewriteUrl(attr.value, meta); 13 | }; 14 | 15 | if (type === 'rewrite' && isSrcset(attr.name)) { 16 | attr.node.setAttribute(origPrefix + attr.name, attr.value); 17 | attr.value = html.wrapSrcset(attr.value, meta); 18 | }; 19 | 20 | 21 | if (type === 'rewrite' && isHtml(attr.name)) { 22 | attr.node.setAttribute(origPrefix + attr.name, attr.value); 23 | attr.value = html.rewrite(attr.value, { 24 | ...meta, 25 | document: true, 26 | injectHead: attr.options.injectHead || [], 27 | }); 28 | }; 29 | 30 | 31 | if (type === 'rewrite' && isStyle(attr.name)) { 32 | attr.node.setAttribute(origPrefix + attr.name, attr.value); 33 | attr.value = ctx.rewriteCSS(attr.value, { context: 'declarationList', }); 34 | }; 35 | 36 | if (type === 'rewrite' && isForbidden(attr.name)) { 37 | attr.name = origPrefix + attr.name; 38 | }; 39 | 40 | if (type === 'rewrite' && isEvent(attr.name)) { 41 | attr.node.setAttribute(origPrefix + attr.name, attr.value); 42 | attr.value = js.rewrite(attr.value, meta); 43 | }; 44 | 45 | if (type === 'source' && attr.name.startsWith(origPrefix)) { 46 | if (attr.node.hasAttribute(attr.name.slice(origPrefix.length))) attr.node.removeAttribute(attr.name.slice(origPrefix.length)); 47 | attr.name = attr.name.slice(origPrefix.length); 48 | }; 49 | 50 | 51 | /* 52 | if (isHtml(attr.name)) { 53 | 54 | }; 55 | 56 | if (isStyle(attr.name)) { 57 | 58 | }; 59 | 60 | if (isSrcset(attr.name)) { 61 | 62 | }; 63 | */ 64 | }); 65 | 66 | }; 67 | 68 | 69 | function text(ctx, meta = ctx.meta) { 70 | const { html, js, css, attributePrefix } = ctx; 71 | 72 | html.on('text', (text, type) => { 73 | if (text.element.tagName === 'script') { 74 | text.value = type === 'rewrite' ? js.rewrite(text.value) : js.source(text.value); 75 | }; 76 | 77 | if (text.element.tagName === 'style') { 78 | text.value = type === 'rewrite' ? css.rewrite(text.value) : css.source(text.value); 79 | }; 80 | }); 81 | return true; 82 | }; 83 | 84 | function isUrl(name, tag) { 85 | return tag === 'object' && name === 'data' || ['src', 'href', 'ping', 'movie', 'action', 'poster', 'profile', 'background'].indexOf(name) > -1; 86 | }; 87 | function isEvent(name) { 88 | return [ 89 | 'onafterprint', 90 | 'onbeforeprint', 91 | 'onbeforeunload', 92 | 'onerror', 93 | 'onhashchange', 94 | 'onload', 95 | 'onmessage', 96 | 'onoffline', 97 | 'ononline', 98 | 'onpagehide', 99 | 'onpopstate', 100 | 'onstorage', 101 | 'onunload', 102 | 'onblur', 103 | 'onchange', 104 | 'oncontextmenu', 105 | 'onfocus', 106 | 'oninput', 107 | 'oninvalid', 108 | 'onreset', 109 | 'onsearch', 110 | 'onselect', 111 | 'onsubmit', 112 | 'onkeydown', 113 | 'onkeypress', 114 | 'onkeyup', 115 | 'onclick', 116 | 'ondblclick', 117 | 'onmousedown', 118 | 'onmousemove', 119 | 'onmouseout', 120 | 'onmouseover', 121 | 'onmouseup', 122 | 'onmousewheel', 123 | 'onwheel', 124 | 'ondrag', 125 | 'ondragend', 126 | 'ondragenter', 127 | 'ondragleave', 128 | 'ondragover', 129 | 'ondragstart', 130 | 'ondrop', 131 | 'onscroll', 132 | 'oncopy', 133 | 'oncut', 134 | 'onpaste', 135 | 'onabort', 136 | 'oncanplay', 137 | 'oncanplaythrough', 138 | 'oncuechange', 139 | 'ondurationchange', 140 | 'onemptied', 141 | 'onended', 142 | 'onerror', 143 | 'onloadeddata', 144 | 'onloadedmetadata', 145 | 'onloadstart', 146 | 'onpause', 147 | 'onplay', 148 | 'onplaying', 149 | 'onprogress', 150 | 'onratechange', 151 | 'onseeked', 152 | 'onseeking', 153 | 'onstalled', 154 | 'onsuspend', 155 | 'ontimeupdate', 156 | 'onvolumechange', 157 | 'onwaiting', 158 | ].indexOf(name) > -1; 159 | }; 160 | 161 | function injectHead(ctx) { 162 | const { html, js, css, attributePrefix } = ctx; 163 | const origPrefix = attributePrefix + '-attr-'; 164 | html.on('element', (element, type) => { 165 | if (type !== 'rewrite') return false; 166 | if (element.tagName !== 'head') return false; 167 | if (!('injectHead' in element.options)) return false; 168 | 169 | element.childNodes.unshift( 170 | ...element.options.injectHead 171 | ); 172 | }); 173 | }; 174 | 175 | function createInjection(handler = '/uv.handler.js', bundle = '/uv.bundle.js', config = '/uv.config.js', cookies = '', referrer = '') { 176 | return [ 177 | { 178 | tagName: 'script', 179 | nodeName: 'script', 180 | childNodes: [ 181 | { 182 | nodeName: '#text', 183 | value: `window.__uv$cookies = atob("${btoa(cookies)}");\nwindow.__uv$referrer = atob("${btoa(referrer)}");` 184 | }, 185 | ], 186 | attrs: [ 187 | { 188 | name: '__uv-script', 189 | value: '1', 190 | skip: true, 191 | } 192 | ], 193 | skip: true, 194 | }, 195 | { 196 | tagName: 'script', 197 | nodeName: 'script', 198 | childNodes: [], 199 | attrs: [ 200 | { name: 'src', value: bundle, skip: true }, 201 | { 202 | name: '__uv-script', 203 | value: '1', 204 | skip: true, 205 | } 206 | ], 207 | }, 208 | { 209 | tagName: 'script', 210 | nodeName: 'script', 211 | childNodes: [], 212 | attrs: [ 213 | { name: 'src', value: config, skip: true }, 214 | { 215 | name: '__uv-script', 216 | value: '1', 217 | skip: true, 218 | } 219 | ], 220 | }, 221 | { 222 | tagName: 'script', 223 | nodeName: 'script', 224 | childNodes: [], 225 | attrs: [ 226 | { name: 'src', value: handler, skip: true }, 227 | { 228 | name: '__uv-script', 229 | value: '1', 230 | skip: true, 231 | } 232 | ], 233 | } 234 | ]; 235 | }; 236 | 237 | function isForbidden(name) { 238 | return ['http-equiv', 'integrity', 'sandbox', 'nonce', 'crossorigin'].indexOf(name) > -1; 239 | }; 240 | 241 | function isHtml(name){ 242 | return name === 'srcdoc'; 243 | }; 244 | 245 | function isStyle(name) { 246 | return name === 'style'; 247 | }; 248 | 249 | function isSrcset(name) { 250 | return name === 'srcset' || name === 'imagesrcset'; 251 | }; 252 | 253 | 254 | export { attributes, createInjection, text, isUrl, isEvent, isForbidden, isHtml, isStyle, isSrcset, injectHead }; -------------------------------------------------------------------------------- /rewrite/rewrite.script.js: -------------------------------------------------------------------------------- 1 | import { Syntax } from 'esotope-hammerhead'; 2 | 3 | function property(ctx) { 4 | const { js } = ctx; 5 | js.on('MemberExpression', (node, data, type) => { 6 | if (node.object.type === 'Super') return false; 7 | 8 | if (type === 'rewrite' && computedProperty(node)) { 9 | data.changes.push({ 10 | node: '__uv.$wrap((', 11 | start: node.property.start, 12 | end: node.property.start, 13 | }) 14 | node.iterateEnd = function() { 15 | data.changes.push({ 16 | node: '))', 17 | start: node.property.end, 18 | end: node.property.end, 19 | }); 20 | }; 21 | 22 | }; 23 | 24 | if (!node.computed && node.property.name === 'location' && type === 'rewrite' || node.property.name === '__uv$location' && type === 'source') { 25 | data.changes.push({ 26 | start: node.property.start, 27 | end: node.property.end, 28 | node: type === 'rewrite' ? '__uv$setSource(__uv).__uv$location' : 'location' 29 | }); 30 | }; 31 | 32 | 33 | if (!node.computed && node.property.name === 'top' && type === 'rewrite' || node.property.name === '__uv$top' && type === 'source') { 34 | data.changes.push({ 35 | start: node.property.start, 36 | end: node.property.end, 37 | node: type === 'rewrite' ? '__uv$setSource(__uv).__uv$top' : 'top' 38 | }); 39 | }; 40 | 41 | if (!node.computed && node.property.name === 'parent' && type === 'rewrite' || node.property.name === '__uv$parent' && type === 'source') { 42 | data.changes.push({ 43 | start: node.property.start, 44 | end: node.property.end, 45 | node: type === 'rewrite' ? '__uv$setSource(__uv).__uv$parent' : 'parent' 46 | }); 47 | }; 48 | 49 | 50 | if (!node.computed && node.property.name === 'postMessage' && type === 'rewrite') { 51 | data.changes.push({ 52 | start: node.property.start, 53 | end: node.property.end, 54 | node:'__uv$setSource(__uv).postMessage', 55 | }); 56 | }; 57 | 58 | 59 | if (!node.computed && node.property.name === 'eval' && type === 'rewrite' || node.property.name === '__uv$eval' && type === 'source') { 60 | data.changes.push({ 61 | start: node.property.start, 62 | end: node.property.end, 63 | node: type === 'rewrite' ? '__uv$setSource(__uv).__uv$eval' : 'eval' 64 | }); 65 | }; 66 | 67 | if (!node.computed && node.property.name === '__uv$setSource' && type === 'source' && node.parent.type === Syntax.CallExpression) { 68 | const { parent, property } = node; 69 | data.changes.push({ 70 | start: property.start - 1, 71 | end: parent.end, 72 | }); 73 | 74 | node.iterateEnd = function() { 75 | data.changes.push({ 76 | start: property.start, 77 | end: parent.end, 78 | }); 79 | }; 80 | }; 81 | }); 82 | }; 83 | 84 | function identifier(ctx) { 85 | const { js } = ctx; 86 | js.on('Identifier', (node, data, type) => { 87 | if (type !== 'rewrite') return false; 88 | const { parent } = node; 89 | if (!['location', 'eval', 'parent', 'top'].includes(node.name)) return false; 90 | if (parent.type === Syntax.VariableDeclarator && parent.id === node) return false; 91 | if ((parent.type === Syntax.AssignmentExpression || parent.type === Syntax.AssignmentPattern) && parent.left === node) return false; 92 | if ((parent.type === Syntax.FunctionExpression || parent.type === Syntax.FunctionDeclaration) && parent.id === node) return false; 93 | if (parent.type === Syntax.MemberExpression && parent.property === node && !parent.computed) return false; 94 | if (node.name === 'eval' && parent.type === Syntax.CallExpression && parent.callee === node) return false; 95 | if (parent.type === Syntax.Property && parent.key === node) return false; 96 | if (parent.type === Syntax.Property && parent.value === node && parent.shorthand) return false; 97 | if (parent.type === Syntax.UpdateExpression && (parent.operator === '++' || parent.operator === '--')) return false; 98 | if ((parent.type === Syntax.FunctionExpression || parent.type === Syntax.FunctionDeclaration || parent.type === Syntax.ArrowFunctionExpression) && parent.params.indexOf(node) !== -1) return false; 99 | if (parent.type === Syntax.MethodDefinition) return false; 100 | if (parent.type === Syntax.ClassDeclaration) return false; 101 | if (parent.type === Syntax.RestElement) return false; 102 | if (parent.type === Syntax.ExportSpecifier) return false; 103 | if (parent.type === Syntax.ImportSpecifier) return false; 104 | 105 | data.changes.push({ 106 | start: node.start, 107 | end: node.end, 108 | node: '__uv.$get(' + node.name + ')' 109 | }); 110 | }); 111 | }; 112 | 113 | function wrapEval(ctx) { 114 | const { js } = ctx; 115 | js.on('CallExpression', (node, data, type) => { 116 | if (type !== 'rewrite') return false; 117 | if (!node.arguments.length) return false; 118 | if (node.callee.type !== 'Identifier') return false; 119 | if (node.callee.name !== 'eval') return false; 120 | 121 | const [ script ] = node.arguments; 122 | 123 | data.changes.push({ 124 | node: '__uv.js.rewrite(', 125 | start: script.start, 126 | end: script.start, 127 | }) 128 | node.iterateEnd = function() { 129 | data.changes.push({ 130 | node: ')', 131 | start: script.end, 132 | end: script.end, 133 | }); 134 | }; 135 | }); 136 | }; 137 | 138 | function importDeclaration(ctx) { 139 | const { js } = ctx; 140 | js.on(Syntax.Literal, (node, data, type) => { 141 | if (!((node.parent.type === Syntax.ImportDeclaration || node.parent.type === Syntax.ExportAllDeclaration || node.parent.type === Syntax.ExportNamedDeclaration) 142 | && node.parent.source === node)) return false; 143 | 144 | data.changes.push({ 145 | start: node.start + 1, 146 | end: node.end - 1, 147 | node: type === 'rewrite' ? ctx.rewriteUrl(node.value) : ctx.sourceUrl(node.value) 148 | }); 149 | }); 150 | }; 151 | 152 | function dynamicImport(ctx) { 153 | const { js } = ctx; 154 | js.on(Syntax.ImportExpression, (node, data, type) => { 155 | if (type !== 'rewrite') return false; 156 | data.changes.push({ 157 | node: '__uv.rewriteUrl(', 158 | start: node.source.start, 159 | end: node.source.start, 160 | }) 161 | node.iterateEnd = function() { 162 | data.changes.push({ 163 | node: ')', 164 | start: node.source.end, 165 | end: node.source.end, 166 | }); 167 | }; 168 | }); 169 | }; 170 | 171 | function unwrap(ctx) { 172 | const { js } = ctx; 173 | js.on('CallExpression', (node, data, type) => { 174 | if (type !== 'source') return false; 175 | if (!isWrapped(node.callee)) return false; 176 | 177 | switch(node.callee.property.name) { 178 | case '$wrap': 179 | if (!node.arguments || node.parent.type !== Syntax.MemberExpression || node.parent.property !== node) return false; 180 | const [ property ] = node.arguments; 181 | 182 | data.changes.push({ 183 | start: node.callee.start, 184 | end: property.start, 185 | }); 186 | 187 | node.iterateEnd = function() { 188 | data.changes.push({ 189 | start: node.end - 2, 190 | end: node.end, 191 | }); 192 | }; 193 | break; 194 | case '$get': 195 | case 'rewriteUrl': 196 | const [ arg ] = node.arguments; 197 | 198 | data.changes.push({ 199 | start: node.callee.start, 200 | end: arg.start, 201 | }); 202 | 203 | node.iterateEnd = function() { 204 | data.changes.push({ 205 | start: node.end - 1, 206 | end: node.end, 207 | }); 208 | }; 209 | break; 210 | case 'rewrite': 211 | const [ script ] = node.arguments; 212 | data.changes.push({ 213 | start: node.callee.start, 214 | end: script.start, 215 | }); 216 | node.iterateEnd = function() { 217 | data.changes.push({ 218 | start: node.end - 1, 219 | end: node.end, 220 | }); 221 | }; 222 | }; 223 | 224 | }); 225 | }; 226 | 227 | function isWrapped(node) { 228 | if (node.type !== Syntax.MemberExpression) return false; 229 | if (node.property.name === 'rewrite' && isWrapped(node.object)) return true; 230 | if (node.object.type !== Syntax.Identifier || node.object.name !== '__uv') return false; 231 | if (!['js', '$get', '$wrap', 'rewriteUrl'].includes(node.property.name)) return false; 232 | return true; 233 | }; 234 | 235 | function computedProperty(parent) { 236 | if (!parent.computed) return false; 237 | const { property: node } = parent; 238 | if (node.type === 'Literal' && !['location', 'top', 'parent']) return false; 239 | return true; 240 | }; 241 | 242 | 243 | export { property, wrapEval, dynamicImport, importDeclaration, identifier, unwrap }; -------------------------------------------------------------------------------- /uv.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/titaniumnetwork-dev/UV-OLD/dd028900b32652559aab842e471b2320aadc9a95/uv.png --------------------------------------------------------------------------------