├── .gitignore ├── .vscode ├── launch.json └── tasks.json ├── LICENSE ├── README.md ├── src ├── home │ ├── 404.html │ ├── README.md │ ├── assets │ │ ├── bundle.debug.js │ │ ├── cors_v1.txt │ │ ├── ico │ │ │ ├── blogger.png │ │ │ ├── facebook.png │ │ │ ├── flickr.png │ │ │ ├── gist.png │ │ │ ├── google.png │ │ │ ├── quora.png │ │ │ ├── reddit.png │ │ │ ├── twitch.png │ │ │ ├── twitter.png │ │ │ ├── wiki.png │ │ │ └── youtube.png │ │ └── index_v3.html │ ├── build.sh │ ├── conf.js │ └── sw.js └── proxy │ ├── .eslintrc.json │ ├── .package-lock.json │ ├── .package.json │ ├── README.md │ ├── debug.sh │ ├── gen-tld │ ├── README.md │ └── gen.js │ ├── jsconfig.json │ ├── release.sh │ └── src │ ├── cdn.js │ ├── client.js │ ├── cookie.js │ ├── database.js │ ├── env.js │ ├── fakeloc.js │ ├── hook.js │ ├── index.js │ ├── inject.js │ ├── jsfilter.js │ ├── msg.js │ ├── network.js │ ├── page.js │ ├── path.js │ ├── route.js │ ├── signal.js │ ├── storage.js │ ├── sw.js │ ├── tld-data.js │ ├── tld.js │ ├── urlx.js │ └── util.js └── www ├── 404.html ├── assets ├── README.md ├── cors_v1.txt ├── ico │ ├── blogger.png │ ├── facebook.png │ ├── flickr.png │ ├── gist.png │ ├── google.png │ ├── quora.png │ ├── reddit.png │ ├── twitch.png │ ├── twitter.png │ ├── wiki.png │ └── youtube.png └── index_v3.html ├── conf.js └── sw.js /.gitignore: -------------------------------------------------------------------------------- 1 | ._* 2 | node_modules 3 | src/home/assets/bundle.debug.js 4 | www/assets/bundle.*.js -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // 使用 IntelliSense 了解相关属性。 3 | // 悬停以查看现有属性的描述。 4 | // 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "clang build and debug active file", 9 | "type": "cppdbg", 10 | "request": "launch", 11 | "program": "${fileDirname}/${fileBasenameNoExtension}", 12 | "args": [], 13 | "stopAtEntry": false, 14 | "cwd": "${workspaceFolder}", 15 | "environment": [], 16 | "externalConsole": false, 17 | "MIMode": "lldb", 18 | "preLaunchTask": "clang build active file" 19 | } 20 | ] 21 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "tasks": [ 3 | { 4 | "type": "shell", 5 | "label": "clang build active file", 6 | "command": "/usr/bin/clang", 7 | "args": [ 8 | "-g", 9 | "${file}", 10 | "-o", 11 | "${fileDirname}/${fileBasenameNoExtension}" 12 | ], 13 | "options": { 14 | "cwd": "/usr/bin" 15 | } 16 | } 17 | ], 18 | "version": "2.0.0" 19 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 EtherDream 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 | JsProxy 浏览器端程序 2 | 3 | 4 | # 修改配置 5 | 6 | 修改 `www/conf.js` 配置,发布 `www` 目录即可。 7 | 8 | 9 | ## TODO 10 | 11 | * 增加网络错误重试、优先选择空闲节点功能 12 | 13 | * 在 SW 中替换 HTML 的 URL 属性,并支持流模式 14 | 15 | * CDN 文件使用 brotli 压缩 16 | 17 | * 使用 AST 修改 JS 代码 18 | 19 | * 动态页面压缩传输(反模板引擎,只传输变量,模板本身存储在 CDN) 20 | 21 | * 使用更多的免费图床作为 CDN 资源存储,并支持 Hash 校验 22 | 23 | * 计算程序使用 wasm 实现 24 | 25 | * 支持 blob/data/javascript 协议 26 | 27 | * 增加 qos 功能,优先满足资料查询网站流量 28 | 29 | * 改进同源策略的安全性,增加部分 API 的授权界面 30 | 31 | * 重新设计首页,增加更多功能 32 | 33 | * 完整的测试案例 34 | 35 | 36 | # 已知问题 37 | 38 | * 文件下载对话框取消后 SW 仍在下载(fetch.signal 的 onabort 未触发,可能是浏览器 BUG) 39 | 40 | * Chrome 图片无法保存 41 | 42 | * 非 UTF8 编码的 JS 会出现乱码(MIME 未指定 charset 的情况下出现) 43 | 44 | * Google 登陆页无法下一步 45 | 46 | * Google reCAPTCHA 无法执行 47 | 48 | * Google Maps 切换到卫星地图后卡死 49 | 50 | * iOS Safari 无法播放 Youtube 视频 51 | 52 | * twitter 在 Chrome 普通模式下无法登陆,但隐身模式可以 53 | 54 | * twitter iframe 经常加载不出来 55 | 56 | * SVG 脚本没有处理 57 | 58 | * Youtube 视频全屏播放会卡住 59 | 60 | * twitch.tv 首页报错(JS 代码修改导致错误,需要在 AST 层面修改) -------------------------------------------------------------------------------- /src/home/404.html: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 74 | 75 | -------------------------------------------------------------------------------- /src/home/README.md: -------------------------------------------------------------------------------- 1 | ## 404.html 2 | 3 | 该页面用于安装 Service Worker。 4 | 5 | 因为本项目只有一个 404.html,所以访问任意路径都会触发该页面。通过该页面安装 Service Worker 然后自动刷新,后续所有流量都可被 Service Worker 接管。 6 | 7 | 8 | ## sw.js 9 | 10 | Service Worker 脚本。 11 | 12 | 虽然 Web 标准规定 Service Worker 脚本必须和站点同源,但是 JS 可通过 `importScripts` 加载站外脚本。因此可将主体代码部署在 CDN,该脚本仅仅作为一个加载器。 13 | 14 | 15 | ## build.sh 16 | 17 | 压缩当前目录 404.html 到 ../../www/404.html 18 | 19 | 20 | ## conf.js 21 | 22 | 配置文件。该文件首次运行时动态加载 23 | 24 | 25 | ## assets 26 | 27 | 该目录存放静态资源,可部署到 CDN。 -------------------------------------------------------------------------------- /src/home/assets/cors_v1.txt: -------------------------------------------------------------------------------- 1 | # HTTP 返回头存在 access-control-allow-origin: * 的站点,不走代理直接连接 2 | # 收集了部分,实验中... 3 | 4 | # google 5 | ssl.google-analytics.com 6 | 7 | # [public] 8 | cdn.jsdelivr.net 9 | unpkg.com 10 | cdnjs.cloudflare.com 11 | cdn.bootcss.com 12 | use.fontawesome.com 13 | fast.fonts.net 14 | script.hotjar.com 15 | 16 | # github 17 | github.githubassets.com 18 | avatars0.githubusercontent.com 19 | avatars1.githubusercontent.com 20 | avatars2.githubusercontent.com 21 | avatars3.githubusercontent.com 22 | 23 | desktop.github.com 24 | 25 | # flickr 26 | status.flickr.net 27 | 28 | # ali 29 | at.alicdn.com 30 | img.alicdn.com 31 | g.alicdn.com 32 | i.alicdn.com 33 | atanx.alicdn.com 34 | wwc.alicdn.com 35 | gw.alicdn.com 36 | assets.alicdn.com 37 | aeis.alicdn.com 38 | atanx.alicdn.com 39 | hudong.alicdn.com 40 | gma.alicdn.com 41 | 42 | sc01.alicdn.com 43 | sc02.alicdn.com 44 | sc03.alicdn.com 45 | sc04.alicdn.com 46 | 47 | cbu01.alicdn.com 48 | cbu02.alicdn.com 49 | cbu03.alicdn.com 50 | cbu04.alicdn.com 51 | 52 | # baidu 53 | # img*.bdimg.com 54 | img0.bdimg.com 55 | img1.bdimg.com 56 | img2.bdimg.com 57 | img3.bdimg.com 58 | img4.bdimg.com 59 | img5.bdimg.com 60 | 61 | webmap0.bdimg.com 62 | webmap1.bdimg.com 63 | iknowpc.bdimg.com 64 | bkssl.bdimg.com 65 | baikebcs.bdimg.com 66 | gh.bdstatic.com 67 | 68 | # qq 69 | 3gimg.qq.com 70 | combo.b.qq.com 71 | 72 | # taotiao 73 | images.taboola.com 74 | images.taboola.com.cn 75 | images-dup.taboola.com 76 | 77 | # zhihu 78 | static.zhihu.com 79 | pic1.zhimg.com 80 | pic2.zhimg.com 81 | pic3.zhimg.com 82 | pic4.zhimg.com 83 | pic5.zhimg.com 84 | pic7.zhimg.com 85 | 86 | # jd 87 | img11.360buyimg.com 88 | 89 | # jianshu 90 | upload.jianshu.io 91 | upload-images.jianshu.io 92 | cdn2.jianshu.io 93 | 94 | # 163 95 | urswebzj.nosdn.127.net 96 | static.ws.126.net 97 | img1.cache.netease.com 98 | img2.cache.netease.com 99 | img3.cache.netease.com 100 | img4.cache.netease.com 101 | img5.cache.netease.com 102 | img6.cache.netease.com 103 | 104 | # sina 105 | js.t.sinajs.cn 106 | mjs.sinaimg.cn 107 | h5.sinaimg.cn 108 | 109 | # sohu 110 | 0d077ef9e74d8.cdn.sohucs.com 111 | 39d0825d09f05.cdn.sohucs.com 112 | 5b0988e595225.cdn.sohucs.com 113 | caaceed4aeaf2.cdn.sohucs.com 114 | 115 | img01.sogoucdn.com 116 | img02.sogoucdn.com 117 | img03.sogoucdn.com 118 | img04.sogoucdn.com 119 | img05.sogoucdn.com 120 | 121 | # hupu 122 | w1.hoopchina.com.cn 123 | w2.hoopchina.com.cn 124 | w3.hoopchina.com.cn 125 | w4.hoopchina.com.cn 126 | shihuo.hupucdn.com 127 | 128 | # uc 129 | image.uc.cn 130 | 131 | # ... 132 | static.cnodejs.org 133 | static2.cnodejs.org 134 | 2b.zol-img.com.cn 135 | img.pconline.com.cn 136 | angular.cn 137 | img1.dxycdn.com 138 | cdn.kastatic.org 139 | static.geetest.com 140 | cdn.registerdisney.go.com 141 | secure-us.imrworldwide.com 142 | img1.doubanio.com 143 | qnwww2.autoimg.cn 144 | qnwww3.autoimg.cn 145 | s.autoimg.cn 146 | 147 | hb.imgix.net 148 | main.qcloudimg.com 149 | vz-cdn2.contentabc.com 150 | twemoji.maxcdn.com 151 | fgn.cdn.serverable.com 152 | 153 | s1.hdslb.com 154 | s2.hdslb.com 155 | s3.hdslb.com 156 | 157 | # cnblogs 158 | common.cnblogs.com 159 | mathjax.cnblogs.com 160 | 161 | # csdn 162 | csdnimg.cn 163 | g.csdnimg.cn 164 | img-ads.csdn.net 165 | img-bss.csdn.net 166 | img-blog.csdn.net 167 | 168 | # ... 169 | static.geekbang.org 170 | static001.infoq.cn 171 | static.docs.com 172 | cdn1.developermedia.com 173 | cdn2.developermedia.com 174 | cdn.optimizely.com 175 | cdn.ampproject.org 176 | 177 | camshowverse.to 178 | static.camshowhub-cdn.to 179 | 180 | xqimg.imedao.com 181 | xavatar.imedao.com 182 | 183 | # ??? 184 | img-l3.xvideos-cdn.com 185 | static-egc.xvideos-cdn.com 186 | img-hw.xvideos-cdn.com 187 | img-hw.xnxx-cdn.com 188 | static-egc.xnxx-cdn.com 189 | di.phncdn.com 190 | cv.phncdn.com 191 | roomimg.stream.highwebmedia.com 192 | w3.cdn.anvato.net -------------------------------------------------------------------------------- /src/home/assets/ico/blogger.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EtherDream/jsproxy-browser/a7a6d740e2c64b1a029fe9ced8ffe9f786e154dd/src/home/assets/ico/blogger.png -------------------------------------------------------------------------------- /src/home/assets/ico/facebook.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EtherDream/jsproxy-browser/a7a6d740e2c64b1a029fe9ced8ffe9f786e154dd/src/home/assets/ico/facebook.png -------------------------------------------------------------------------------- /src/home/assets/ico/flickr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EtherDream/jsproxy-browser/a7a6d740e2c64b1a029fe9ced8ffe9f786e154dd/src/home/assets/ico/flickr.png -------------------------------------------------------------------------------- /src/home/assets/ico/gist.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EtherDream/jsproxy-browser/a7a6d740e2c64b1a029fe9ced8ffe9f786e154dd/src/home/assets/ico/gist.png -------------------------------------------------------------------------------- /src/home/assets/ico/google.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EtherDream/jsproxy-browser/a7a6d740e2c64b1a029fe9ced8ffe9f786e154dd/src/home/assets/ico/google.png -------------------------------------------------------------------------------- /src/home/assets/ico/quora.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EtherDream/jsproxy-browser/a7a6d740e2c64b1a029fe9ced8ffe9f786e154dd/src/home/assets/ico/quora.png -------------------------------------------------------------------------------- /src/home/assets/ico/reddit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EtherDream/jsproxy-browser/a7a6d740e2c64b1a029fe9ced8ffe9f786e154dd/src/home/assets/ico/reddit.png -------------------------------------------------------------------------------- /src/home/assets/ico/twitch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EtherDream/jsproxy-browser/a7a6d740e2c64b1a029fe9ced8ffe9f786e154dd/src/home/assets/ico/twitch.png -------------------------------------------------------------------------------- /src/home/assets/ico/twitter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EtherDream/jsproxy-browser/a7a6d740e2c64b1a029fe9ced8ffe9f786e154dd/src/home/assets/ico/twitter.png -------------------------------------------------------------------------------- /src/home/assets/ico/wiki.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EtherDream/jsproxy-browser/a7a6d740e2c64b1a029fe9ced8ffe9f786e154dd/src/home/assets/ico/wiki.png -------------------------------------------------------------------------------- /src/home/assets/ico/youtube.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EtherDream/jsproxy-browser/a7a6d740e2c64b1a029fe9ced8ffe9f786e154dd/src/home/assets/ico/youtube.png -------------------------------------------------------------------------------- /src/home/assets/index_v3.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Page Sandbox 5 | 6 | 7 | 8 | 29 | 30 | 31 |
32 | 33 |
34 |
35 | 36 |
37 |
38 | 切换线路: 39 | 40 |
41 |
42 |
43 | 167 | 168 | -------------------------------------------------------------------------------- /src/home/build.sh: -------------------------------------------------------------------------------- 1 | # 压缩 404.html 2 | html-minifier \ 3 | --collapse-whitespace \ 4 | --remove-comments \ 5 | --remove-redundant-attributes \ 6 | --remove-script-type-attributes \ 7 | --remove-tag-whitespace \ 8 | --use-short-doctype \ 9 | --remove-attribute-quotes \ 10 | --minify-css true \ 11 | --minify-js '{"toplevel": true, "ie8": true}' \ 12 | -o ../../www/404.html \ 13 | 404.html 14 | -------------------------------------------------------------------------------- /src/home/conf.js: -------------------------------------------------------------------------------- 1 | jsproxy_config({ 2 | // 当前配置的版本(记录在日志中,用于排查问题) 3 | // 每次修改配置,该值需要增加,否则不会生效。 4 | // 默认每隔 5 分钟自动下载配置,若想立即验证,可通过隐私模式访问。 5 | ver: '105', 6 | 7 | // 通过 CDN 加速常用网站的静态资源(实验中) 8 | static_boost: { 9 | enable: true, 10 | ver: 56 11 | }, 12 | 13 | // 节点配置 14 | node_map: { 15 | 'demo-hk': { 16 | label: '演示服务-香港节点', 17 | lines: { 18 | // 主机:权重 19 | 'node-aliyun-hk-0.etherdream.com:8443': 1, 20 | 'node-aliyun-hk-1.etherdream.com:8443': 1, 21 | 'node-aliyun-hk-2.etherdream.com:8443': 1, 22 | } 23 | }, 24 | 'demo-sg': { 25 | label: '演示服务-新加坡节点', 26 | lines: { 27 | 'node-aliyun-sg.etherdream.com:8443': 1, 28 | }, 29 | }, 30 | 'demo-la': { 31 | label: '演示服务-洛杉矶节点', 32 | lines: { 33 | 'node-bwh-la.etherdream.com:8443': 1, 34 | }, 35 | }, 36 | 'mysite': { 37 | label: '当前站点', 38 | lines: { 39 | [location.host]: 1, 40 | } 41 | }, 42 | // 该节点用于加载大体积的静态资源 43 | 'cfworker': { 44 | label: '', 45 | hidden: true, 46 | lines: { 47 | 'node-cfworker-2.etherdream.com': 1, 48 | } 49 | } 50 | }, 51 | 52 | /** 53 | * 默认节点 54 | */ 55 | node_default: 'demo-hk', 56 | 57 | /** 58 | * 加速节点 59 | */ 60 | node_acc: 'cfworker', 61 | 62 | /** 63 | * 静态资源 CDN 地址 64 | * 用于加速 `assets` 目录中的资源访问 65 | */ 66 | // assets_cdn: 'https://cdn.jsdelivr.net/gh/zjcqoo/zjcqoo.github.io@master/assets/', 67 | 68 | // 本地测试时打开,否则访问的是线上的 69 | assets_cdn: 'assets/', 70 | 71 | // 首页路径 72 | index_path: 'index_v3.html', 73 | 74 | // 支持 CORS 的站点列表(实验中...) 75 | direct_host_list: 'cors_v1.txt', 76 | 77 | /** 78 | * 自定义注入页面的 HTML 79 | */ 80 | inject_html: '', 81 | 82 | /** 83 | * URL 自定义处理(设计中) 84 | */ 85 | url_handler: { 86 | 'https://www.baidu.com/img/baidu_resultlogo@2.png': { 87 | replace: 'https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_92x30dp.png' 88 | }, 89 | 'https://www.pornhub.com/': { 90 | redir: 'https://php.net/' 91 | }, 92 | 'http://haha.com/': { 93 | content: 'Hello World' 94 | }, 95 | } 96 | }) -------------------------------------------------------------------------------- /src/home/sw.js: -------------------------------------------------------------------------------- 1 | jsproxy_config=x=>{__CONF__=x;importScripts(__FILE__=x.assets_cdn+'bundle.debug.js')};importScripts('conf.js') -------------------------------------------------------------------------------- /src/proxy/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es6": true, 5 | "serviceworker": true 6 | }, 7 | "extends": "eslint:recommended", 8 | "globals": { 9 | "Atomics": "readonly", 10 | "SharedArrayBuffer": "readonly" 11 | }, 12 | "parserOptions": { 13 | "ecmaVersion": 2017, 14 | "sourceType": "module" 15 | }, 16 | "rules": { 17 | "no-console": "warn", 18 | "no-empty": "warn", 19 | "no-unused-vars": "warn", 20 | "no-debugger": "warn", 21 | "no-constant-condition": "warn" 22 | } 23 | } -------------------------------------------------------------------------------- /src/proxy/.package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jsproxy-client", 3 | "version": "0.0.1", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "@babel/code-frame": { 8 | "version": "7.0.0", 9 | "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.0.0.tgz", 10 | "integrity": "sha512-OfC2uemaknXr87bdLUkWog7nYuliM9Ij5HUcajsVcMCpQrcLmtxRbVFTIqmcSkSeYRBFBRxs2FiUqFJDLdiebA==", 11 | "dev": true, 12 | "requires": { 13 | "@babel/highlight": "^7.0.0" 14 | } 15 | }, 16 | "@babel/highlight": { 17 | "version": "7.0.0", 18 | "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.0.0.tgz", 19 | "integrity": "sha512-UFMC4ZeFC48Tpvj7C8UgLvtkaUuovQX+5xNWrsIoMG8o2z+XFKjKaN9iVmS84dPwVN00W4wPmqvYoZF3EGAsfw==", 20 | "dev": true, 21 | "requires": { 22 | "chalk": "^2.0.0", 23 | "esutils": "^2.0.2", 24 | "js-tokens": "^4.0.0" 25 | } 26 | }, 27 | "acorn": { 28 | "version": "6.1.1", 29 | "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.1.1.tgz", 30 | "integrity": "sha512-jPTiwtOxaHNaAPg/dmrJ/beuzLRnXtB0kQPQ8JpotKJgTB6rX6c8mlf315941pyjBSaPg8NHXS9fhP4u17DpGA==", 31 | "dev": true 32 | }, 33 | "acorn-jsx": { 34 | "version": "5.0.1", 35 | "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.0.1.tgz", 36 | "integrity": "sha512-HJ7CfNHrfJLlNTzIEUTj43LNWGkqpRLxm3YjAlcD0ACydk9XynzYsCBHxut+iqt+1aBXkx9UP/w/ZqMr13XIzg==", 37 | "dev": true 38 | }, 39 | "ajv": { 40 | "version": "6.10.0", 41 | "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.0.tgz", 42 | "integrity": "sha512-nffhOpkymDECQyR0mnsUtoCE8RlX38G0rYP+wgLWFyZuUyuuojSSvi/+euOiQBIn63whYwYVIIH1TvE3tu4OEg==", 43 | "dev": true, 44 | "requires": { 45 | "fast-deep-equal": "^2.0.1", 46 | "fast-json-stable-stringify": "^2.0.0", 47 | "json-schema-traverse": "^0.4.1", 48 | "uri-js": "^4.2.2" 49 | } 50 | }, 51 | "ansi-escapes": { 52 | "version": "3.2.0", 53 | "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz", 54 | "integrity": "sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==", 55 | "dev": true 56 | }, 57 | "ansi-regex": { 58 | "version": "3.0.0", 59 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", 60 | "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", 61 | "dev": true 62 | }, 63 | "ansi-styles": { 64 | "version": "3.2.1", 65 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", 66 | "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", 67 | "dev": true, 68 | "requires": { 69 | "color-convert": "^1.9.0" 70 | } 71 | }, 72 | "argparse": { 73 | "version": "1.0.10", 74 | "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", 75 | "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", 76 | "dev": true, 77 | "requires": { 78 | "sprintf-js": "~1.0.2" 79 | } 80 | }, 81 | "astral-regex": { 82 | "version": "1.0.0", 83 | "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz", 84 | "integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==", 85 | "dev": true 86 | }, 87 | "balanced-match": { 88 | "version": "1.0.0", 89 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", 90 | "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", 91 | "dev": true 92 | }, 93 | "brace-expansion": { 94 | "version": "1.1.11", 95 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", 96 | "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", 97 | "dev": true, 98 | "requires": { 99 | "balanced-match": "^1.0.0", 100 | "concat-map": "0.0.1" 101 | } 102 | }, 103 | "callsites": { 104 | "version": "3.1.0", 105 | "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", 106 | "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", 107 | "dev": true 108 | }, 109 | "chalk": { 110 | "version": "2.4.2", 111 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", 112 | "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", 113 | "dev": true, 114 | "requires": { 115 | "ansi-styles": "^3.2.1", 116 | "escape-string-regexp": "^1.0.5", 117 | "supports-color": "^5.3.0" 118 | } 119 | }, 120 | "chardet": { 121 | "version": "0.7.0", 122 | "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", 123 | "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", 124 | "dev": true 125 | }, 126 | "cli-cursor": { 127 | "version": "2.1.0", 128 | "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", 129 | "integrity": "sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU=", 130 | "dev": true, 131 | "requires": { 132 | "restore-cursor": "^2.0.0" 133 | } 134 | }, 135 | "cli-width": { 136 | "version": "2.2.0", 137 | "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.0.tgz", 138 | "integrity": "sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk=", 139 | "dev": true 140 | }, 141 | "color-convert": { 142 | "version": "1.9.3", 143 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", 144 | "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", 145 | "dev": true, 146 | "requires": { 147 | "color-name": "1.1.3" 148 | } 149 | }, 150 | "color-name": { 151 | "version": "1.1.3", 152 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", 153 | "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", 154 | "dev": true 155 | }, 156 | "concat-map": { 157 | "version": "0.0.1", 158 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", 159 | "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", 160 | "dev": true 161 | }, 162 | "cross-spawn": { 163 | "version": "6.0.5", 164 | "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", 165 | "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", 166 | "dev": true, 167 | "requires": { 168 | "nice-try": "^1.0.4", 169 | "path-key": "^2.0.1", 170 | "semver": "^5.5.0", 171 | "shebang-command": "^1.2.0", 172 | "which": "^1.2.9" 173 | } 174 | }, 175 | "debug": { 176 | "version": "4.1.1", 177 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", 178 | "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", 179 | "dev": true, 180 | "requires": { 181 | "ms": "^2.1.1" 182 | } 183 | }, 184 | "deep-is": { 185 | "version": "0.1.3", 186 | "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", 187 | "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", 188 | "dev": true 189 | }, 190 | "doctrine": { 191 | "version": "3.0.0", 192 | "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", 193 | "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", 194 | "dev": true, 195 | "requires": { 196 | "esutils": "^2.0.2" 197 | } 198 | }, 199 | "emoji-regex": { 200 | "version": "7.0.3", 201 | "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", 202 | "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", 203 | "dev": true 204 | }, 205 | "escape-string-regexp": { 206 | "version": "1.0.5", 207 | "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", 208 | "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", 209 | "dev": true 210 | }, 211 | "eslint": { 212 | "version": "5.16.0", 213 | "resolved": "https://registry.npmjs.org/eslint/-/eslint-5.16.0.tgz", 214 | "integrity": "sha512-S3Rz11i7c8AA5JPv7xAH+dOyq/Cu/VXHiHXBPOU1k/JAM5dXqQPt3qcrhpHSorXmrpu2g0gkIBVXAqCpzfoZIg==", 215 | "dev": true, 216 | "requires": { 217 | "@babel/code-frame": "^7.0.0", 218 | "ajv": "^6.9.1", 219 | "chalk": "^2.1.0", 220 | "cross-spawn": "^6.0.5", 221 | "debug": "^4.0.1", 222 | "doctrine": "^3.0.0", 223 | "eslint-scope": "^4.0.3", 224 | "eslint-utils": "^1.3.1", 225 | "eslint-visitor-keys": "^1.0.0", 226 | "espree": "^5.0.1", 227 | "esquery": "^1.0.1", 228 | "esutils": "^2.0.2", 229 | "file-entry-cache": "^5.0.1", 230 | "functional-red-black-tree": "^1.0.1", 231 | "glob": "^7.1.2", 232 | "globals": "^11.7.0", 233 | "ignore": "^4.0.6", 234 | "import-fresh": "^3.0.0", 235 | "imurmurhash": "^0.1.4", 236 | "inquirer": "^6.2.2", 237 | "js-yaml": "^3.13.0", 238 | "json-stable-stringify-without-jsonify": "^1.0.1", 239 | "levn": "^0.3.0", 240 | "lodash": "^4.17.11", 241 | "minimatch": "^3.0.4", 242 | "mkdirp": "^0.5.1", 243 | "natural-compare": "^1.4.0", 244 | "optionator": "^0.8.2", 245 | "path-is-inside": "^1.0.2", 246 | "progress": "^2.0.0", 247 | "regexpp": "^2.0.1", 248 | "semver": "^5.5.1", 249 | "strip-ansi": "^4.0.0", 250 | "strip-json-comments": "^2.0.1", 251 | "table": "^5.2.3", 252 | "text-table": "^0.2.0" 253 | } 254 | }, 255 | "eslint-scope": { 256 | "version": "4.0.3", 257 | "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-4.0.3.tgz", 258 | "integrity": "sha512-p7VutNr1O/QrxysMo3E45FjYDTeXBy0iTltPFNSqKAIfjDSXC+4dj+qfyuD8bfAXrW/y6lW3O76VaYNPKfpKrg==", 259 | "dev": true, 260 | "requires": { 261 | "esrecurse": "^4.1.0", 262 | "estraverse": "^4.1.1" 263 | } 264 | }, 265 | "eslint-utils": { 266 | "version": "1.3.1", 267 | "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.3.1.tgz", 268 | "integrity": "sha512-Z7YjnIldX+2XMcjr7ZkgEsOj/bREONV60qYeB/bjMAqqqZ4zxKyWX+BOUkdmRmA9riiIPVvo5x86m5elviOk0Q==", 269 | "dev": true 270 | }, 271 | "eslint-visitor-keys": { 272 | "version": "1.0.0", 273 | "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz", 274 | "integrity": "sha512-qzm/XxIbxm/FHyH341ZrbnMUpe+5Bocte9xkmFMzPMjRaZMcXww+MpBptFvtU+79L362nqiLhekCxCxDPaUMBQ==", 275 | "dev": true 276 | }, 277 | "espree": { 278 | "version": "5.0.1", 279 | "resolved": "https://registry.npmjs.org/espree/-/espree-5.0.1.tgz", 280 | "integrity": "sha512-qWAZcWh4XE/RwzLJejfcofscgMc9CamR6Tn1+XRXNzrvUSSbiAjGOI/fggztjIi7y9VLPqnICMIPiGyr8JaZ0A==", 281 | "dev": true, 282 | "requires": { 283 | "acorn": "^6.0.7", 284 | "acorn-jsx": "^5.0.0", 285 | "eslint-visitor-keys": "^1.0.0" 286 | } 287 | }, 288 | "esprima": { 289 | "version": "4.0.1", 290 | "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", 291 | "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", 292 | "dev": true 293 | }, 294 | "esquery": { 295 | "version": "1.0.1", 296 | "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.0.1.tgz", 297 | "integrity": "sha512-SmiyZ5zIWH9VM+SRUReLS5Q8a7GxtRdxEBVZpm98rJM7Sb+A9DVCndXfkeFUd3byderg+EbDkfnevfCwynWaNA==", 298 | "dev": true, 299 | "requires": { 300 | "estraverse": "^4.0.0" 301 | } 302 | }, 303 | "esrecurse": { 304 | "version": "4.2.1", 305 | "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.2.1.tgz", 306 | "integrity": "sha512-64RBB++fIOAXPw3P9cy89qfMlvZEXZkqqJkjqqXIvzP5ezRZjW+lPWjw35UX/3EhUPFYbg5ER4JYgDw4007/DQ==", 307 | "dev": true, 308 | "requires": { 309 | "estraverse": "^4.1.0" 310 | } 311 | }, 312 | "estraverse": { 313 | "version": "4.2.0", 314 | "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.2.0.tgz", 315 | "integrity": "sha1-De4/7TH81GlhjOc0IJn8GvoL2xM=", 316 | "dev": true 317 | }, 318 | "esutils": { 319 | "version": "2.0.2", 320 | "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", 321 | "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=", 322 | "dev": true 323 | }, 324 | "external-editor": { 325 | "version": "3.0.3", 326 | "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.0.3.tgz", 327 | "integrity": "sha512-bn71H9+qWoOQKyZDo25mOMVpSmXROAsTJVVVYzrrtol3d4y+AsKjf4Iwl2Q+IuT0kFSQ1qo166UuIwqYq7mGnA==", 328 | "dev": true, 329 | "requires": { 330 | "chardet": "^0.7.0", 331 | "iconv-lite": "^0.4.24", 332 | "tmp": "^0.0.33" 333 | } 334 | }, 335 | "fast-deep-equal": { 336 | "version": "2.0.1", 337 | "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", 338 | "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=", 339 | "dev": true 340 | }, 341 | "fast-json-stable-stringify": { 342 | "version": "2.0.0", 343 | "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", 344 | "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=", 345 | "dev": true 346 | }, 347 | "fast-levenshtein": { 348 | "version": "2.0.6", 349 | "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", 350 | "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", 351 | "dev": true 352 | }, 353 | "figures": { 354 | "version": "2.0.0", 355 | "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz", 356 | "integrity": "sha1-OrGi0qYsi/tDGgyUy3l6L84nyWI=", 357 | "dev": true, 358 | "requires": { 359 | "escape-string-regexp": "^1.0.5" 360 | } 361 | }, 362 | "file-entry-cache": { 363 | "version": "5.0.1", 364 | "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-5.0.1.tgz", 365 | "integrity": "sha512-bCg29ictuBaKUwwArK4ouCaqDgLZcysCFLmM/Yn/FDoqndh/9vNuQfXRDvTuXKLxfD/JtZQGKFT8MGcJBK644g==", 366 | "dev": true, 367 | "requires": { 368 | "flat-cache": "^2.0.1" 369 | } 370 | }, 371 | "flat-cache": { 372 | "version": "2.0.1", 373 | "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-2.0.1.tgz", 374 | "integrity": "sha512-LoQe6yDuUMDzQAEH8sgmh4Md6oZnc/7PjtwjNFSzveXqSHt6ka9fPBuso7IGf9Rz4uqnSnWiFH2B/zj24a5ReA==", 375 | "dev": true, 376 | "requires": { 377 | "flatted": "^2.0.0", 378 | "rimraf": "2.6.3", 379 | "write": "1.0.3" 380 | } 381 | }, 382 | "flatted": { 383 | "version": "2.0.0", 384 | "resolved": "https://registry.npmjs.org/flatted/-/flatted-2.0.0.tgz", 385 | "integrity": "sha512-R+H8IZclI8AAkSBRQJLVOsxwAoHd6WC40b4QTNWIjzAa6BXOBfQcM587MXDTVPeYaopFNWHUFLx7eNmHDSxMWg==", 386 | "dev": true 387 | }, 388 | "fs.realpath": { 389 | "version": "1.0.0", 390 | "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", 391 | "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", 392 | "dev": true 393 | }, 394 | "functional-red-black-tree": { 395 | "version": "1.0.1", 396 | "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", 397 | "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", 398 | "dev": true 399 | }, 400 | "glob": { 401 | "version": "7.1.3", 402 | "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz", 403 | "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", 404 | "dev": true, 405 | "requires": { 406 | "fs.realpath": "^1.0.0", 407 | "inflight": "^1.0.4", 408 | "inherits": "2", 409 | "minimatch": "^3.0.4", 410 | "once": "^1.3.0", 411 | "path-is-absolute": "^1.0.0" 412 | } 413 | }, 414 | "globals": { 415 | "version": "11.12.0", 416 | "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", 417 | "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", 418 | "dev": true 419 | }, 420 | "has-flag": { 421 | "version": "3.0.0", 422 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", 423 | "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", 424 | "dev": true 425 | }, 426 | "iconv-lite": { 427 | "version": "0.4.24", 428 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", 429 | "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", 430 | "dev": true, 431 | "requires": { 432 | "safer-buffer": ">= 2.1.2 < 3" 433 | } 434 | }, 435 | "ignore": { 436 | "version": "4.0.6", 437 | "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", 438 | "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", 439 | "dev": true 440 | }, 441 | "import-fresh": { 442 | "version": "3.0.0", 443 | "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.0.0.tgz", 444 | "integrity": "sha512-pOnA9tfM3Uwics+SaBLCNyZZZbK+4PTu0OPZtLlMIrv17EdBoC15S9Kn8ckJ9TZTyKb3ywNE5y1yeDxxGA7nTQ==", 445 | "dev": true, 446 | "requires": { 447 | "parent-module": "^1.0.0", 448 | "resolve-from": "^4.0.0" 449 | } 450 | }, 451 | "imurmurhash": { 452 | "version": "0.1.4", 453 | "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", 454 | "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", 455 | "dev": true 456 | }, 457 | "inflight": { 458 | "version": "1.0.6", 459 | "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", 460 | "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", 461 | "dev": true, 462 | "requires": { 463 | "once": "^1.3.0", 464 | "wrappy": "1" 465 | } 466 | }, 467 | "inherits": { 468 | "version": "2.0.3", 469 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", 470 | "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", 471 | "dev": true 472 | }, 473 | "inquirer": { 474 | "version": "6.3.1", 475 | "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-6.3.1.tgz", 476 | "integrity": "sha512-MmL624rfkFt4TG9y/Jvmt8vdmOo836U7Y0Hxr2aFk3RelZEGX4Igk0KabWrcaaZaTv9uzglOqWh1Vly+FAWAXA==", 477 | "dev": true, 478 | "requires": { 479 | "ansi-escapes": "^3.2.0", 480 | "chalk": "^2.4.2", 481 | "cli-cursor": "^2.1.0", 482 | "cli-width": "^2.0.0", 483 | "external-editor": "^3.0.3", 484 | "figures": "^2.0.0", 485 | "lodash": "^4.17.11", 486 | "mute-stream": "0.0.7", 487 | "run-async": "^2.2.0", 488 | "rxjs": "^6.4.0", 489 | "string-width": "^2.1.0", 490 | "strip-ansi": "^5.1.0", 491 | "through": "^2.3.6" 492 | }, 493 | "dependencies": { 494 | "ansi-regex": { 495 | "version": "4.1.0", 496 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", 497 | "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", 498 | "dev": true 499 | }, 500 | "strip-ansi": { 501 | "version": "5.2.0", 502 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", 503 | "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", 504 | "dev": true, 505 | "requires": { 506 | "ansi-regex": "^4.1.0" 507 | } 508 | } 509 | } 510 | }, 511 | "is-fullwidth-code-point": { 512 | "version": "2.0.0", 513 | "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", 514 | "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", 515 | "dev": true 516 | }, 517 | "is-promise": { 518 | "version": "2.1.0", 519 | "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.1.0.tgz", 520 | "integrity": "sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=", 521 | "dev": true 522 | }, 523 | "isexe": { 524 | "version": "2.0.0", 525 | "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", 526 | "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", 527 | "dev": true 528 | }, 529 | "js-tokens": { 530 | "version": "4.0.0", 531 | "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", 532 | "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", 533 | "dev": true 534 | }, 535 | "js-yaml": { 536 | "version": "3.13.1", 537 | "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", 538 | "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", 539 | "dev": true, 540 | "requires": { 541 | "argparse": "^1.0.7", 542 | "esprima": "^4.0.0" 543 | } 544 | }, 545 | "json-schema-traverse": { 546 | "version": "0.4.1", 547 | "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", 548 | "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", 549 | "dev": true 550 | }, 551 | "json-stable-stringify-without-jsonify": { 552 | "version": "1.0.1", 553 | "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", 554 | "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", 555 | "dev": true 556 | }, 557 | "levn": { 558 | "version": "0.3.0", 559 | "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", 560 | "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", 561 | "dev": true, 562 | "requires": { 563 | "prelude-ls": "~1.1.2", 564 | "type-check": "~0.3.2" 565 | } 566 | }, 567 | "lodash": { 568 | "version": "4.17.11", 569 | "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz", 570 | "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==", 571 | "dev": true 572 | }, 573 | "mimic-fn": { 574 | "version": "1.2.0", 575 | "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", 576 | "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==", 577 | "dev": true 578 | }, 579 | "minimatch": { 580 | "version": "3.0.4", 581 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", 582 | "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", 583 | "dev": true, 584 | "requires": { 585 | "brace-expansion": "^1.1.7" 586 | } 587 | }, 588 | "minimist": { 589 | "version": "0.0.8", 590 | "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", 591 | "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", 592 | "dev": true 593 | }, 594 | "mkdirp": { 595 | "version": "0.5.1", 596 | "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", 597 | "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", 598 | "dev": true, 599 | "requires": { 600 | "minimist": "0.0.8" 601 | } 602 | }, 603 | "ms": { 604 | "version": "2.1.1", 605 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", 606 | "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", 607 | "dev": true 608 | }, 609 | "mute-stream": { 610 | "version": "0.0.7", 611 | "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz", 612 | "integrity": "sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=", 613 | "dev": true 614 | }, 615 | "natural-compare": { 616 | "version": "1.4.0", 617 | "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", 618 | "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", 619 | "dev": true 620 | }, 621 | "nice-try": { 622 | "version": "1.0.5", 623 | "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", 624 | "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", 625 | "dev": true 626 | }, 627 | "once": { 628 | "version": "1.4.0", 629 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 630 | "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", 631 | "dev": true, 632 | "requires": { 633 | "wrappy": "1" 634 | } 635 | }, 636 | "onetime": { 637 | "version": "2.0.1", 638 | "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz", 639 | "integrity": "sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ=", 640 | "dev": true, 641 | "requires": { 642 | "mimic-fn": "^1.0.0" 643 | } 644 | }, 645 | "optionator": { 646 | "version": "0.8.2", 647 | "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.2.tgz", 648 | "integrity": "sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q=", 649 | "dev": true, 650 | "requires": { 651 | "deep-is": "~0.1.3", 652 | "fast-levenshtein": "~2.0.4", 653 | "levn": "~0.3.0", 654 | "prelude-ls": "~1.1.2", 655 | "type-check": "~0.3.2", 656 | "wordwrap": "~1.0.0" 657 | } 658 | }, 659 | "os-tmpdir": { 660 | "version": "1.0.2", 661 | "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", 662 | "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", 663 | "dev": true 664 | }, 665 | "parent-module": { 666 | "version": "1.0.1", 667 | "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", 668 | "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", 669 | "dev": true, 670 | "requires": { 671 | "callsites": "^3.0.0" 672 | } 673 | }, 674 | "path-is-absolute": { 675 | "version": "1.0.1", 676 | "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", 677 | "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", 678 | "dev": true 679 | }, 680 | "path-is-inside": { 681 | "version": "1.0.2", 682 | "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", 683 | "integrity": "sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=", 684 | "dev": true 685 | }, 686 | "path-key": { 687 | "version": "2.0.1", 688 | "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", 689 | "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", 690 | "dev": true 691 | }, 692 | "prelude-ls": { 693 | "version": "1.1.2", 694 | "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", 695 | "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", 696 | "dev": true 697 | }, 698 | "progress": { 699 | "version": "2.0.3", 700 | "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", 701 | "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", 702 | "dev": true 703 | }, 704 | "punycode": { 705 | "version": "2.1.1", 706 | "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", 707 | "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", 708 | "dev": true 709 | }, 710 | "regexpp": { 711 | "version": "2.0.1", 712 | "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-2.0.1.tgz", 713 | "integrity": "sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw==", 714 | "dev": true 715 | }, 716 | "resolve-from": { 717 | "version": "4.0.0", 718 | "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", 719 | "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", 720 | "dev": true 721 | }, 722 | "restore-cursor": { 723 | "version": "2.0.0", 724 | "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", 725 | "integrity": "sha1-n37ih/gv0ybU/RYpI9YhKe7g368=", 726 | "dev": true, 727 | "requires": { 728 | "onetime": "^2.0.0", 729 | "signal-exit": "^3.0.2" 730 | } 731 | }, 732 | "rimraf": { 733 | "version": "2.6.3", 734 | "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", 735 | "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", 736 | "dev": true, 737 | "requires": { 738 | "glob": "^7.1.3" 739 | } 740 | }, 741 | "run-async": { 742 | "version": "2.3.0", 743 | "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.3.0.tgz", 744 | "integrity": "sha1-A3GrSuC91yDUFm19/aZP96RFpsA=", 745 | "dev": true, 746 | "requires": { 747 | "is-promise": "^2.1.0" 748 | } 749 | }, 750 | "rxjs": { 751 | "version": "6.5.1", 752 | "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.5.1.tgz", 753 | "integrity": "sha512-y0j31WJc83wPu31vS1VlAFW5JGrnGC+j+TtGAa1fRQphy48+fDYiDmX8tjGloToEsMkxnouOg/1IzXGKkJnZMg==", 754 | "dev": true, 755 | "requires": { 756 | "tslib": "^1.9.0" 757 | } 758 | }, 759 | "safer-buffer": { 760 | "version": "2.1.2", 761 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", 762 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", 763 | "dev": true 764 | }, 765 | "semver": { 766 | "version": "5.7.0", 767 | "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz", 768 | "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==", 769 | "dev": true 770 | }, 771 | "shebang-command": { 772 | "version": "1.2.0", 773 | "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", 774 | "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", 775 | "dev": true, 776 | "requires": { 777 | "shebang-regex": "^1.0.0" 778 | } 779 | }, 780 | "shebang-regex": { 781 | "version": "1.0.0", 782 | "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", 783 | "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", 784 | "dev": true 785 | }, 786 | "signal-exit": { 787 | "version": "3.0.2", 788 | "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", 789 | "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", 790 | "dev": true 791 | }, 792 | "slice-ansi": { 793 | "version": "2.1.0", 794 | "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-2.1.0.tgz", 795 | "integrity": "sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ==", 796 | "dev": true, 797 | "requires": { 798 | "ansi-styles": "^3.2.0", 799 | "astral-regex": "^1.0.0", 800 | "is-fullwidth-code-point": "^2.0.0" 801 | } 802 | }, 803 | "sprintf-js": { 804 | "version": "1.0.3", 805 | "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", 806 | "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", 807 | "dev": true 808 | }, 809 | "string-width": { 810 | "version": "2.1.1", 811 | "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", 812 | "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", 813 | "dev": true, 814 | "requires": { 815 | "is-fullwidth-code-point": "^2.0.0", 816 | "strip-ansi": "^4.0.0" 817 | } 818 | }, 819 | "strip-ansi": { 820 | "version": "4.0.0", 821 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", 822 | "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", 823 | "dev": true, 824 | "requires": { 825 | "ansi-regex": "^3.0.0" 826 | } 827 | }, 828 | "strip-json-comments": { 829 | "version": "2.0.1", 830 | "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", 831 | "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", 832 | "dev": true 833 | }, 834 | "supports-color": { 835 | "version": "5.5.0", 836 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", 837 | "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", 838 | "dev": true, 839 | "requires": { 840 | "has-flag": "^3.0.0" 841 | } 842 | }, 843 | "table": { 844 | "version": "5.2.3", 845 | "resolved": "https://registry.npmjs.org/table/-/table-5.2.3.tgz", 846 | "integrity": "sha512-N2RsDAMvDLvYwFcwbPyF3VmVSSkuF+G1e+8inhBLtHpvwXGw4QRPEZhihQNeEN0i1up6/f6ObCJXNdlRG3YVyQ==", 847 | "dev": true, 848 | "requires": { 849 | "ajv": "^6.9.1", 850 | "lodash": "^4.17.11", 851 | "slice-ansi": "^2.1.0", 852 | "string-width": "^3.0.0" 853 | }, 854 | "dependencies": { 855 | "ansi-regex": { 856 | "version": "4.1.0", 857 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", 858 | "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", 859 | "dev": true 860 | }, 861 | "string-width": { 862 | "version": "3.1.0", 863 | "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", 864 | "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", 865 | "dev": true, 866 | "requires": { 867 | "emoji-regex": "^7.0.1", 868 | "is-fullwidth-code-point": "^2.0.0", 869 | "strip-ansi": "^5.1.0" 870 | } 871 | }, 872 | "strip-ansi": { 873 | "version": "5.2.0", 874 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", 875 | "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", 876 | "dev": true, 877 | "requires": { 878 | "ansi-regex": "^4.1.0" 879 | } 880 | } 881 | } 882 | }, 883 | "text-table": { 884 | "version": "0.2.0", 885 | "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", 886 | "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", 887 | "dev": true 888 | }, 889 | "through": { 890 | "version": "2.3.8", 891 | "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", 892 | "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", 893 | "dev": true 894 | }, 895 | "tmp": { 896 | "version": "0.0.33", 897 | "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", 898 | "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", 899 | "dev": true, 900 | "requires": { 901 | "os-tmpdir": "~1.0.2" 902 | } 903 | }, 904 | "tslib": { 905 | "version": "1.9.3", 906 | "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.3.tgz", 907 | "integrity": "sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ==", 908 | "dev": true 909 | }, 910 | "type-check": { 911 | "version": "0.3.2", 912 | "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", 913 | "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", 914 | "dev": true, 915 | "requires": { 916 | "prelude-ls": "~1.1.2" 917 | } 918 | }, 919 | "uri-js": { 920 | "version": "4.2.2", 921 | "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", 922 | "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", 923 | "dev": true, 924 | "requires": { 925 | "punycode": "^2.1.0" 926 | } 927 | }, 928 | "which": { 929 | "version": "1.3.1", 930 | "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", 931 | "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", 932 | "dev": true, 933 | "requires": { 934 | "isexe": "^2.0.0" 935 | } 936 | }, 937 | "wordwrap": { 938 | "version": "1.0.0", 939 | "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", 940 | "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=", 941 | "dev": true 942 | }, 943 | "wrappy": { 944 | "version": "1.0.2", 945 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 946 | "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", 947 | "dev": true 948 | }, 949 | "write": { 950 | "version": "1.0.3", 951 | "resolved": "https://registry.npmjs.org/write/-/write-1.0.3.tgz", 952 | "integrity": "sha512-/lg70HAjtkUgWPVZhZcm+T4hkL8Zbtp1nFNOn3lRrxnlv50SRBv7cR7RqR+GMsd3hUXy9hWBo4CHTbFTcOYwig==", 953 | "dev": true, 954 | "requires": { 955 | "mkdirp": "^0.5.1" 956 | } 957 | } 958 | } 959 | } 960 | -------------------------------------------------------------------------------- /src/proxy/.package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jsproxy-client", 3 | "version": "0.0.1", 4 | "description": "", 5 | "main": "boot.js", 6 | "directories": { 7 | "lib": "lib" 8 | }, 9 | "scripts": { 10 | "test": "echo \"Error: no test specified\" && exit 1" 11 | }, 12 | "keywords": [], 13 | "author": "EtherDream", 14 | "license": "MIT", 15 | "dependencies": {}, 16 | "devDependencies": { 17 | "eslint": "^5.16.0" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/proxy/README.md: -------------------------------------------------------------------------------- 1 | 浏览器脚本的源文件 2 | 3 | # src 4 | 5 | 源文件目录 6 | 7 | 8 | # debug.sh 9 | 10 | 开发模式。 11 | 12 | # release.sh 13 | 14 | 发布脚本。 15 | -------------------------------------------------------------------------------- /src/proxy/debug.sh: -------------------------------------------------------------------------------- 1 | webpack \ 2 | --o "../home/assets/bundle.debug.js" \ 3 | --mode development -w -------------------------------------------------------------------------------- /src/proxy/gen-tld/README.md: -------------------------------------------------------------------------------- 1 | 顶级域名后缀生成脚本 2 | 3 | # 生成 4 | 5 | ```bash 6 | node gen 7 | ``` 8 | 9 | # TODO 10 | 11 | 该列表较大,导致最终 JS 增加上百 KB 的体积。 12 | 13 | 未考虑 `!` 和 `*.` 开头的域名。 14 | 15 | 未来考虑将结果保存为单独的文件,JS 运行时动态加载。 -------------------------------------------------------------------------------- /src/proxy/gen-tld/gen.js: -------------------------------------------------------------------------------- 1 | const https = require('https') 2 | const fs = require('fs') 3 | const {stdout} = process 4 | 5 | const SRC_PATH = 'https://publicsuffix.org/list/effective_tld_names.dat' 6 | const DST_PATH = '../src/tld-data.js' 7 | 8 | 9 | stdout.write('loading') 10 | 11 | https.get(SRC_PATH, res => { 12 | let str = '' 13 | res.on('data', s => { 14 | str += s 15 | stdout.write('.'); 16 | }).on('end', _ => { 17 | proc(str) 18 | }).setEncoding('utf8') 19 | }) 20 | 21 | 22 | function proc(str) { 23 | const list = str 24 | .split('\n') 25 | .filter(v => v) 26 | .filter(v => !v.startsWith('//')) 27 | .filter(v => !v.startsWith('!')) 28 | .map(v => v.replace('*.', '')) 29 | .sort((a, b) => a > b ? 1 : -1) 30 | .join(',') 31 | 32 | const result = `\ 33 | // THIS FILE WAS GENERATED BY './tools/tld/gen.js' 34 | export default '${list}' 35 | ` 36 | fs.writeFileSync(DST_PATH, result) 37 | 38 | console.log('\nok') 39 | } 40 | -------------------------------------------------------------------------------- /src/proxy/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "checkJs": true, 4 | "lib": ["webworker", "dom", "es2017"], 5 | "target": "es2017" 6 | }, 7 | "include": [ 8 | "src/*" 9 | ] 10 | } -------------------------------------------------------------------------------- /src/proxy/release.sh: -------------------------------------------------------------------------------- 1 | DST=../../www/assets 2 | 3 | rm $DST/bundle.*.js 4 | 5 | webpack \ 6 | --o "$DST/bundle.[hash:8].js" \ 7 | --mode production 8 | 9 | cd $DST 10 | 11 | for i in bundle.*.js; do 12 | printf "\ 13 | jsproxy_config=\ 14 | x=>{\ 15 | __CONF__=x;\ 16 | importScripts(__FILE__=x.assets_cdn+'$i')\ 17 | };\ 18 | importScripts('conf.js')\ 19 | " > ../sw.js 20 | done -------------------------------------------------------------------------------- /src/proxy/src/cdn.js: -------------------------------------------------------------------------------- 1 | import * as util from './util' 2 | 3 | // 暂时先用 jsdelivr 试验。之后换成速度很快、容量更大的免费图床 4 | const CDN = 'https://cdn.jsdelivr.net/npm/jsproxy-cache-01@0.0.' 5 | 6 | let mCurVer = -1 7 | 8 | /** @type {Map} */ 9 | let mUrlHashVerMap = new Map() 10 | 11 | /** @type {Set} */ 12 | let mDirectHostSet = new Set() 13 | 14 | 15 | async function loadDirectList(conf) { 16 | const url = conf.assets_cdn + conf.direct_host_list 17 | const res = await fetch(url) 18 | const txt = await res.text() 19 | 20 | for (const host of txt.split('\n')) { 21 | if (host && host[0] !== '#') { 22 | mDirectHostSet.add(host) 23 | } 24 | } 25 | } 26 | 27 | async function loadStaticList(conf) { 28 | const info = conf.static_boost 29 | if (!info || !info.enable) { 30 | return 31 | } 32 | const latest = info.ver 33 | if (mCurVer >= latest) { 34 | return 35 | } 36 | mCurVer = latest 37 | console.log('[jsproxy] cdn cache ver:', latest) 38 | 39 | const res = await fetch(CDN + latest + '/full') 40 | const buf = await res.arrayBuffer() 41 | const u32 = new Uint32Array(buf) 42 | 43 | let p = 0 44 | for (let ver = 0; ver <= latest; ver++) { 45 | const num = u32[p++] 46 | 47 | for (let i = 0; i < num; i++) { 48 | const urlHash = u32[p++] 49 | mUrlHashVerMap.set(urlHash, ver) 50 | } 51 | } 52 | } 53 | 54 | 55 | export function setConf(conf) { 56 | return Promise.all([ 57 | loadStaticList(conf), 58 | loadDirectList(conf), 59 | ]) 60 | } 61 | 62 | /** 63 | * @param {string} host 64 | */ 65 | export function isDirectHost(host) { 66 | return mDirectHostSet.has(host) 67 | } 68 | 69 | 70 | /** 71 | * @param {string} url 72 | */ 73 | export async function proxyDirect(url) { 74 | try { 75 | const res = await fetch(url, { 76 | referrerPolicy: 'no-referrer', 77 | }) 78 | const {status} = res 79 | if (status === 200 || status === 206) { 80 | return res 81 | } 82 | console.warn('direct status:', status, url) 83 | } catch (err) { 84 | console.warn('direct fail:', url) 85 | } 86 | } 87 | 88 | 89 | /** 90 | * @param {number} urlHash 91 | */ 92 | export function getFileVer(urlHash) { 93 | return mUrlHashVerMap.get(urlHash) 94 | } 95 | 96 | 97 | /** 98 | * @param {number} urlHash 99 | * @param {number} urlVer 100 | */ 101 | async function proxyStaticMain(urlHash, urlVer) { 102 | const hashHex = util.numToHex(urlHash, 8) 103 | const res = await fetch(CDN + urlVer + '/' + hashHex + '.txt') 104 | if (res.status !== 200) { 105 | throw 'bad status: ' + res.status 106 | } 107 | const buf = await res.arrayBuffer() 108 | const b = new Uint8Array(buf) 109 | 110 | const hdrLen = b[0] << 8 | b[1] 111 | const hdrBuf = b.subarray(2, 2 + hdrLen) 112 | const hdrStr = util.bytesToStr(hdrBuf) 113 | const hdrObj = JSON.parse(hdrStr) 114 | 115 | const body = b.subarray(2 + hdrLen) 116 | hdrObj['date'] = new Date().toUTCString() 117 | 118 | return new Response(body, { 119 | headers: hdrObj 120 | }) 121 | } 122 | 123 | 124 | /** 125 | * @param {number} urlHash 126 | * @param {number} urlVer 127 | */ 128 | export async function proxyStatic(urlHash, urlVer) { 129 | // TODO: 使用多个 CDN 130 | try { 131 | return await proxyStaticMain(urlHash, urlVer) 132 | } catch(err) { 133 | console.warn('cdn fail:', err) 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/proxy/src/client.js: -------------------------------------------------------------------------------- 1 | import * as urlx from './urlx.js' 2 | import * as route from './route.js' 3 | import * as env from './env.js' 4 | import * as hook from './hook.js' 5 | import {createFakeLoc} from './fakeloc.js' 6 | import {createStorage} from './storage.js' 7 | 8 | 9 | const { 10 | apply, 11 | construct, 12 | } = Reflect 13 | 14 | 15 | /** 16 | * Hook 页面和 Worker 相同的 API 17 | * 18 | * @param {Window} global WindowOrWorkerGlobalScope 19 | * @param {string} origin 20 | */ 21 | export function init(global, origin) { 22 | // lockNative(win) 23 | 24 | // hook Storage API 25 | createStorage(global, origin) 26 | 27 | // hook Location API 28 | const fakeLoc = createFakeLoc(global) 29 | 30 | // hook Performance API 31 | const perfProto = global['PerformanceEntry'].prototype 32 | hook.prop(perfProto, 'name', 33 | getter => function() { 34 | const val = getter.call(this) 35 | if (/^https?:/.test(val)) { 36 | return urlx.decUrlStrAbs(val) 37 | } 38 | return val 39 | } 40 | ) 41 | 42 | 43 | // hook AJAX API 44 | const xhrProto = global['XMLHttpRequest'].prototype 45 | hook.func(xhrProto, 'open', oldFn => function(_0, url) { 46 | if (url) { 47 | arguments[1] = urlx.encUrlStrRel(url, this) 48 | } 49 | return apply(oldFn, this, arguments) 50 | }) 51 | 52 | hook.prop(xhrProto, 'responseURL', 53 | getter => function(oldFn) { 54 | const val = getter.call(this) 55 | return urlx.decUrlStrRel(val, this) 56 | } 57 | ) 58 | 59 | 60 | hook.func(global, 'fetch', oldFn => function(v) { 61 | if (v) { 62 | if (v.url) { 63 | // v is Request 64 | const newUrl = urlx.encUrlStrAbs(v.url) 65 | arguments[0] = new Request(newUrl, v) 66 | } else { 67 | // v is string 68 | // TODO: 字符串不传引用,无法获取创建时的 constructor 69 | arguments[0] = urlx.encUrlStrRel(v, v) 70 | } 71 | } 72 | return apply(oldFn, this, arguments) 73 | }) 74 | 75 | 76 | hook.func(global, 'WebSocket', oldFn => function(url) { 77 | const urlObj = urlx.newUrl(url) 78 | if (urlObj) { 79 | const {ori} = env.get(this) 80 | if (ori) { 81 | const args = { 82 | 'origin': ori.origin, 83 | } 84 | arguments[0] = route.genWsUrl(urlObj, args) 85 | } 86 | } 87 | return construct(oldFn, arguments) 88 | }) 89 | 90 | /** 91 | * @param {string} type 92 | */ 93 | function hookWorker(type) { 94 | hook.func(global, type, oldFn => function(url) { 95 | if (url) { 96 | console.log('[jsproxy] new %s: %s', type, url) 97 | arguments[0] = urlx.encUrlStrRel(url, this) 98 | } 99 | return construct(oldFn, arguments) 100 | }) 101 | } 102 | 103 | hookWorker('Worker') 104 | hookWorker('SharedWorker') 105 | 106 | 107 | hook.func(global, 'importScripts', oldFn => function(...args) { 108 | const urls = args.map(urlx.encUrlStrRel) 109 | console.log('[jsproxy] importScripts:', urls) 110 | return apply(oldFn, this, urls) 111 | }) 112 | } -------------------------------------------------------------------------------- /src/proxy/src/cookie.js: -------------------------------------------------------------------------------- 1 | import {Database} from './database.js' 2 | 3 | /** @type {Set} */ 4 | let mDirtySet = new Set() 5 | 6 | 7 | function Cookie() { 8 | this.id = '' 9 | this.name = '' 10 | this.value = '' 11 | this.domain = '' 12 | this.hostOnly = false 13 | this.path = '' 14 | this.expires = NaN 15 | this.isExpired = false 16 | this.secure = false 17 | this.httpOnly = false 18 | this.sameSite = '' 19 | } 20 | 21 | /** 22 | * @param {Cookie} src 23 | * @param {Cookie} dst 24 | */ 25 | function copy(dst, src) { 26 | dst.id = src.id 27 | dst.name = src.name 28 | dst.value = src.value 29 | dst.domain = src.domain 30 | dst.hostOnly = src.hostOnly 31 | dst.path = src.path 32 | dst.expires = src.expires 33 | dst.isExpired = src.isExpired 34 | dst.secure = src.secure 35 | dst.httpOnly = src.httpOnly 36 | dst.sameSite = src.sameSite 37 | } 38 | 39 | 40 | /** 41 | * @param {string} cookiePath 42 | * @param {string} urlPath 43 | */ 44 | function isSubPath(cookiePath, urlPath) { 45 | if (urlPath === cookiePath) { 46 | return true 47 | } 48 | if (!cookiePath.endsWith('/')) { 49 | cookiePath += '/' 50 | } 51 | return urlPath.startsWith(cookiePath) 52 | } 53 | 54 | 55 | /** 56 | * @param {string} cookieDomain 57 | * @param {string} urlDomain 58 | */ 59 | function isSubDomain(cookieDomain, urlDomain) { 60 | return urlDomain === cookieDomain || 61 | urlDomain.endsWith('.' + cookieDomain) 62 | } 63 | 64 | 65 | /** 66 | * @param {Cookie} item 67 | * @param {number} now 68 | */ 69 | function isExpire(item, now) { 70 | const v = item.expires 71 | return !isNaN(v) && v < now 72 | } 73 | 74 | 75 | class CookieDomainNode { 76 | constructor() { 77 | /** @type {Cookie[]} */ 78 | this.items = null 79 | 80 | /** @type {Object} */ 81 | this.children = {} 82 | } 83 | 84 | /** 85 | * @param {string} name 86 | */ 87 | nextChild(name) { 88 | return this.children[name] || ( 89 | this.children[name] = new CookieDomainNode 90 | ) 91 | } 92 | 93 | /** 94 | * @param {string} name 95 | */ 96 | getChild(name) { 97 | return this.children[name] 98 | } 99 | 100 | /** 101 | * @param {Cookie} cookie 102 | */ 103 | addCookie(cookie) { 104 | if (this.items) { 105 | this.items.push(cookie) 106 | } else { 107 | this.items = [cookie] 108 | } 109 | } 110 | } 111 | 112 | /** @type {Map} */ 113 | const mIdCookieMap = new Map() 114 | 115 | const mCookieNodeRoot = new CookieDomainNode() 116 | 117 | 118 | 119 | export function getNonHttpOnlyItems() { 120 | const ret = [] 121 | for (const item of mIdCookieMap.values()) { 122 | if (!item.httpOnly) { 123 | ret.push(item) 124 | } 125 | } 126 | return ret 127 | } 128 | 129 | 130 | /** 131 | * @param {string} str 132 | * @param {URL} urlObj 133 | * @param {number} now 134 | */ 135 | export function parse(str, urlObj, now) { 136 | const item = new Cookie() 137 | const arr = str.split(';') 138 | 139 | for (let i = 0; i < arr.length; i++) { 140 | let key, val 141 | const s = arr[i].trim() 142 | const p = s.indexOf('=') 143 | 144 | if (p !== -1) { 145 | key = s.substr(0, p) 146 | val = s.substr(p + 1) 147 | } else { 148 | // 149 | // cookie = 's; secure; httponly' 150 | // 0: { key: '', val: 's' } 151 | // 1: { key: 'secure', val: '' } 152 | // 2: { key: 'httponly', val: '' } 153 | // 154 | key = (i === 0) ? '' : s 155 | val = (i === 0) ? s : '' 156 | } 157 | 158 | if (i === 0) { 159 | item.name = key 160 | item.value = val 161 | continue 162 | } 163 | 164 | switch (key.toLocaleLowerCase()) { 165 | case 'expires': 166 | if (isNaN(item.expires)) { 167 | item.expires = Date.parse(val) 168 | } 169 | break 170 | case 'domain': 171 | if (val[0] === '.') { 172 | val = val.substr(1) 173 | } 174 | item.domain = val 175 | break 176 | case 'path': 177 | item.path = val 178 | break 179 | case 'httponly': 180 | item.httpOnly = true 181 | break 182 | case 'secure': 183 | item.secure = true 184 | break 185 | case 'max-age': 186 | item.expires = now + (+val) * 1000 187 | break 188 | case 'samesite': 189 | item.sameSite = val 190 | break 191 | } 192 | } 193 | 194 | if (isExpire(item, now)) { 195 | item.isExpired = true 196 | } 197 | 198 | // https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Set-Cookie 199 | if (item.name.startsWith('__Secure-')) { 200 | if (!( 201 | urlObj.protocol === 'https:' && 202 | item.secure 203 | )) { 204 | return 205 | } 206 | } 207 | if (item.name.startsWith('__Host-')) { 208 | if (!( 209 | urlObj.protocol === 'https:' && 210 | item.secure && 211 | item.domain === '' && 212 | item.path === '/' 213 | )) { 214 | return 215 | } 216 | } 217 | 218 | // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#Compatibility_notes 219 | if (item.secure && urlObj.protocol === 'http:') { 220 | return 221 | } 222 | 223 | // check hostname 224 | const domain = urlObj.hostname 225 | 226 | if (item.domain) { 227 | if (!isSubDomain(item.domain, domain)) { 228 | console.warn('[jsproxy] invalid cookie domain! `%s` ⊄ `%s`', 229 | item.domain, domain) 230 | return 231 | } 232 | } else { 233 | item.domain = domain 234 | item.hostOnly = true 235 | } 236 | 237 | // check pathname 238 | const path = urlObj.pathname 239 | 240 | if (item.path) { 241 | if (!isSubPath(item.path, path)) { 242 | console.warn('[jsproxy] invalid cookie path! `%s` ⊄ `%s`', 243 | item.path, path) 244 | return 245 | } 246 | } else { 247 | item.path = path 248 | } 249 | 250 | item.id = (item.secure ? ';' : '') + 251 | item.name + ';' + 252 | item.domain + 253 | item.path 254 | 255 | return item 256 | } 257 | 258 | 259 | /** 260 | * @param {Cookie} item 261 | */ 262 | export function set(item) { 263 | // console.log('set:', item) 264 | const id = item.id 265 | const matched = mIdCookieMap.get(id) 266 | 267 | if (matched) { 268 | if (item.isExpired) { 269 | // delete 270 | mIdCookieMap.delete(id) 271 | matched.isExpired = true 272 | // TODO: remove node 273 | } else { 274 | // update 275 | copy(matched, item) 276 | } 277 | mDirtySet.add(matched) 278 | } else { 279 | // create 280 | const labels = item.domain.split('.') 281 | let labelPos = labels.length 282 | let node = mCookieNodeRoot 283 | do { 284 | node = node.nextChild(labels[--labelPos]) 285 | } while (labelPos !== 0) 286 | 287 | node.addCookie(item) 288 | mIdCookieMap.set(id, item) 289 | 290 | mDirtySet.add(item) 291 | } 292 | } 293 | 294 | 295 | /** 296 | * @param {URL} urlObj 297 | */ 298 | export function query(urlObj) { 299 | const ret = [] 300 | const now = Date.now() 301 | const domain = urlObj.hostname 302 | const path = urlObj.pathname 303 | const isHttps = (urlObj.protocol === 'https:') 304 | 305 | const labels = domain.split('.') 306 | let labelPos = labels.length 307 | let node = mCookieNodeRoot 308 | 309 | do { 310 | node = node.getChild(labels[--labelPos]) 311 | if (!node) { 312 | break 313 | } 314 | const items = node.items 315 | if (!items) { 316 | continue 317 | } 318 | for (let i = 0; i < items.length; i++) { 319 | const item = items[i] 320 | // https url | secure flag | carry 321 | // ✔ | ✔ | ✔ 322 | // ✔ | ✘ | ✔ 323 | // ✘ | ✘ | ✔ 324 | // ✘ | ✔ | ✘ 325 | if (!isHttps && item.secure) { 326 | continue 327 | } 328 | // HostOnly Cookie 需匹配完整域名 329 | if (item.hostOnly && labelPos !== 0) { 330 | continue 331 | } 332 | if (!isSubPath(item.path, path)) { 333 | continue 334 | } 335 | if (item.isExpired) { 336 | continue 337 | } 338 | if (isExpire(item, now)) { 339 | item.isExpired = true 340 | continue 341 | } 342 | // TODO: same site 343 | 344 | let str = item.value 345 | if (item.name) { 346 | str = item.name + '=' + str 347 | } 348 | ret.push(str) 349 | } 350 | } while (labelPos !== 0) 351 | 352 | return ret.join('; ') 353 | } 354 | 355 | 356 | /** @type {Database} */ 357 | let mDB 358 | 359 | export async function setDB(db) { 360 | mDB = db 361 | 362 | const now = Date.now() 363 | await mDB.enum('cookie', v => { 364 | if (isExpire(v, now)) { 365 | mDB.delete('cookie', v.id) 366 | } else { 367 | set(v) 368 | } 369 | return true 370 | }) 371 | 372 | setInterval(save, 1000 * 3) 373 | } 374 | 375 | 376 | export async function save() { 377 | if (mDirtySet.size === 0) { 378 | return 379 | } 380 | 381 | const tmp = mDirtySet 382 | mDirtySet = new Set() 383 | 384 | for (const item of tmp) { 385 | if (item.isExpired) { 386 | await mDB.delete('cookie', item.id) 387 | } else if (!isNaN(item.expires)) { 388 | // 不保存 session cookie 389 | await mDB.put('cookie', item) 390 | } 391 | } 392 | } 393 | -------------------------------------------------------------------------------- /src/proxy/src/database.js: -------------------------------------------------------------------------------- 1 | import {Signal} from './signal.js' 2 | 3 | 4 | export class Database { 5 | /** 6 | * @param {string} name 7 | */ 8 | constructor(name) { 9 | this._name = name 10 | 11 | /** @type {IDBDatabase} */ 12 | this._db = null 13 | } 14 | 15 | /** 16 | * @param {string} table 17 | * @param {IDBTransactionMode} mode 18 | */ 19 | _getStore(table, mode) { 20 | return this._db 21 | .transaction(table, mode) 22 | .objectStore(table) 23 | } 24 | 25 | /** 26 | * @param {Object} opts 27 | */ 28 | open(opts) { 29 | const s = new Signal() 30 | const req = indexedDB.open(this._name) 31 | 32 | req.onsuccess = (e) => { 33 | const idb = req.result 34 | this._db = idb 35 | 36 | idb.onclose = (e) => { 37 | console.warn('[jsproxy] indexedDB disconnected, reopen...') 38 | this.open(opts) 39 | } 40 | s.notify() 41 | } 42 | req.onerror = (e) => { 43 | console.warn('req.onerror:', e) 44 | s.abort(req.error) 45 | } 46 | req.onupgradeneeded = (e) => { 47 | const idb = req.result 48 | for (const [k, v] of Object.entries(opts)) { 49 | idb.createObjectStore(k, v) 50 | } 51 | } 52 | return s.wait() 53 | } 54 | 55 | 56 | close() { 57 | this._db.close() 58 | } 59 | 60 | /** 61 | * @param {string} table 62 | * @param {any} key 63 | */ 64 | get(table, key) { 65 | const s = new Signal() 66 | const obj = this._getStore(table, 'readonly') 67 | const req = obj.get(key) 68 | 69 | req.onsuccess = (e) => { 70 | s.notify(req.result) 71 | } 72 | req.onerror = (e) => { 73 | s.abort(req.error) 74 | } 75 | return s.wait() 76 | } 77 | 78 | /** 79 | * @param {string} table 80 | * @param {any} record 81 | */ 82 | put(table, record) { 83 | const s = new Signal() 84 | const obj = this._getStore(table, 'readwrite') 85 | const req = obj.put(record) 86 | 87 | req.onsuccess = (e) => { 88 | s.notify() 89 | } 90 | req.onerror = (e) => { 91 | s.abort(req.error) 92 | } 93 | return s.wait() 94 | } 95 | 96 | /** 97 | * @param {string} table 98 | * @param {any} key 99 | */ 100 | delete(table, key) { 101 | const s = new Signal() 102 | const obj = this._getStore(table, 'readwrite') 103 | const req = obj.delete(key) 104 | 105 | req.onsuccess = (e) => { 106 | s.notify() 107 | } 108 | req.onerror = (e) => { 109 | s.abort(req.error) 110 | } 111 | return s.wait() 112 | } 113 | 114 | /** 115 | * @param {string} table 116 | * @param {(any) => boolean} callback 117 | */ 118 | enum(table, callback, ...args) { 119 | const s = new Signal() 120 | const obj = this._getStore(table, 'readonly') 121 | const req = obj.openCursor(...args) 122 | 123 | req.onsuccess = (e) => { 124 | const {result} = req 125 | if (result) { 126 | if (callback(result.value) !== false) { 127 | result.continue() 128 | } 129 | } else { 130 | s.notify() 131 | } 132 | } 133 | req.onerror = (e) => { 134 | s.abort(req.error) 135 | } 136 | return s.wait() 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/proxy/src/env.js: -------------------------------------------------------------------------------- 1 | export const ENV_PAGE = 1 2 | export const ENV_WORKER = 2 3 | export const ENV_SW = 3 4 | 5 | let mEnvType = 0 6 | 7 | export function setEnvType(v) { 8 | mEnvType = v 9 | } 10 | 11 | export function isSwEnv() { 12 | return mEnvType === ENV_SW 13 | } 14 | 15 | export function isWorkerEnv() { 16 | return mEnvType === ENV_WORKER 17 | } 18 | 19 | 20 | /** 21 | * @type {WeakMap} 22 | */ 23 | const objInfoMap = new WeakMap() 24 | 25 | export function add(win, info) { 26 | objInfoMap.set(win.Function, info) 27 | } 28 | 29 | export function get(obj) { 30 | const Function = obj.constructor.constructor 31 | return objInfoMap.get(Function) 32 | } -------------------------------------------------------------------------------- /src/proxy/src/fakeloc.js: -------------------------------------------------------------------------------- 1 | import * as urlx from "./urlx"; 2 | 3 | const { 4 | defineProperty, 5 | setPrototypeOf, 6 | } = Object 7 | 8 | 9 | function setup(obj, fakeLoc) { 10 | defineProperty(obj, '__location', { 11 | get() { 12 | return fakeLoc 13 | }, 14 | set(val) { 15 | console.log('[jsproxy] %s set location: %s', obj, val) 16 | fakeLoc.href = val 17 | } 18 | }) 19 | } 20 | 21 | 22 | /** 23 | * @param {Window} global WindowOrWorkerGlobalScope 24 | */ 25 | export function createFakeLoc(global) { 26 | const location = global.location 27 | let ancestorOrigins 28 | 29 | /** 30 | * @param {Location | URL} loc 31 | */ 32 | function getPageUrlObj(loc) { 33 | return new URL(urlx.decUrlObj(loc)) 34 | } 35 | 36 | 37 | // 不缓存 location 属性,因为 beforeunload 事件会影响赋值 38 | const locObj = { 39 | get href() { 40 | // console.log('[jsproxy] get location.href') 41 | return getPageUrlObj(location).href 42 | }, 43 | 44 | // TODO: 精简合并 45 | get protocol() { 46 | return getPageUrlObj(location).protocol 47 | }, 48 | 49 | get host() { 50 | return getPageUrlObj(location).host 51 | }, 52 | 53 | get hostname() { 54 | return getPageUrlObj(location).hostname 55 | }, 56 | 57 | get port() { 58 | return getPageUrlObj(location).port 59 | }, 60 | 61 | get pathname() { 62 | return getPageUrlObj(location).pathname 63 | }, 64 | 65 | get search() { 66 | return getPageUrlObj(location).search 67 | }, 68 | 69 | get hash() { 70 | return getPageUrlObj(location).hash 71 | }, 72 | 73 | get origin() { 74 | return getPageUrlObj(location).origin 75 | }, 76 | 77 | toString() { 78 | return this.href 79 | }, 80 | 81 | toLocaleString() { 82 | return this.href 83 | }, 84 | 85 | // TODO: Worker 中没有以下属性 86 | get ancestorOrigins() { 87 | if (!ancestorOrigins) { 88 | // TODO: DOMStringList[] 89 | ancestorOrigins = [] 90 | 91 | let p = global 92 | while ((p = p.parent) !== top) { 93 | const u = getPageUrlObj(p.location) 94 | ancestorOrigins.unshift(u.origin) 95 | } 96 | } 97 | return ancestorOrigins 98 | }, 99 | 100 | set href(val) { 101 | console.log('[jsproxy] set location.href:', val) 102 | location.href = urlx.encUrlStrRel(val, this) 103 | }, 104 | 105 | set protocol(val) { 106 | console.log('[jsproxy] set location.protocol:', val) 107 | const urlObj = getPageUrlObj(location) 108 | urlObj.href = val 109 | location.href = urlx.encUrlObj(urlObj) 110 | }, 111 | 112 | set host(val) { 113 | console.log('[jsproxy] set location.host:', val) 114 | const urlObj = getPageUrlObj(location) 115 | urlObj.host = val 116 | location.href = urlx.encUrlObj(urlObj) 117 | }, 118 | 119 | set hostname(val) { 120 | console.log('[jsproxy] set location.hostname:', val) 121 | const urlObj = getPageUrlObj(location) 122 | urlObj.hostname = val 123 | location.href = urlx.encUrlObj(urlObj) 124 | }, 125 | 126 | set port(val) { 127 | console.log('[jsproxy] set location.port:', val) 128 | const urlObj = getPageUrlObj(location) 129 | urlObj.port = val 130 | location.href = urlx.encUrlObj(urlObj) 131 | }, 132 | 133 | set pathname(val) { 134 | console.log('[jsproxy] set location.pathname:', val) 135 | const urlObj = getPageUrlObj(location) 136 | urlObj.pathname = val 137 | location.href = urlx.encUrlObj(urlObj) 138 | }, 139 | 140 | set search(val) { 141 | location.search = val 142 | }, 143 | 144 | set hash(val) { 145 | location.hash = val 146 | }, 147 | 148 | reload() { 149 | console.warn('[jsproxy] location.reload') 150 | // @ts-ignore 151 | return location.reload(...arguments) 152 | }, 153 | 154 | replace(val) { 155 | if (val) { 156 | console.warn('[jsproxy] location.replace:', val) 157 | arguments[0] = urlx.encUrlStrRel(val, this) 158 | } 159 | // @ts-ignore 160 | return location.replace(...arguments) 161 | }, 162 | 163 | assign(val) { 164 | if (val) { 165 | console.warn('[jsproxy] location.assign:', val) 166 | arguments[0] = urlx.encUrlStrRel(val, this) 167 | } 168 | // @ts-ignore 169 | return location.assign(...arguments) 170 | }, 171 | } 172 | 173 | const locProto = location.constructor.prototype 174 | const fakeLoc = setPrototypeOf(locObj, locProto) 175 | setup(global, fakeLoc) 176 | 177 | // 非 Worker 环境 178 | const Document = global['Document'] 179 | if (Document) { 180 | // TODO: document.hasOwnProperty('location') 原本是 true 181 | setup(Document.prototype, fakeLoc) 182 | } 183 | 184 | return fakeLoc 185 | } 186 | -------------------------------------------------------------------------------- /src/proxy/src/hook.js: -------------------------------------------------------------------------------- 1 | const { 2 | getOwnPropertyDescriptor, 3 | defineProperty, 4 | apply, 5 | } = Reflect 6 | 7 | 8 | export const DROP = {} 9 | 10 | /** 11 | * hook function 12 | * 13 | * @param {object} obj 14 | * @param {string} key 15 | * @param {(oldFn: Function) => Function} factory 16 | */ 17 | export function func(obj, key, factory) { 18 | /** @type {Function} */ 19 | const oldFn = obj[key] 20 | if (!oldFn) { 21 | return false 22 | } 23 | 24 | const newFn = factory(oldFn) 25 | 26 | Object.keys(oldFn).forEach(k => { 27 | newFn[k] = oldFn[k] 28 | }) 29 | 30 | const proto = oldFn.prototype 31 | if (proto) { 32 | newFn.prototype = proto 33 | } 34 | 35 | obj[key] = newFn 36 | return true 37 | } 38 | 39 | /** 40 | * hook property 41 | * 42 | * @param {object} obj 43 | * @param {string} key 44 | * @param {(oldFn: () => any) => Function=} g 45 | * @param {(oldFn: () => void) => Function=} s 46 | */ 47 | export function prop(obj, key, g, s) { 48 | const desc = getOwnPropertyDescriptor(obj, key) 49 | if (!desc) { 50 | return false 51 | } 52 | if (g) { 53 | func(desc, 'get', g) 54 | } 55 | if (s) { 56 | func(desc, 'set', s) 57 | } 58 | defineProperty(obj, key, desc) 59 | return true 60 | } 61 | 62 | 63 | /** 64 | * @param {Window} win WindowOrWorkerGlobalScope 65 | */ 66 | export function createDomHook(win) { 67 | /** 68 | * @param {object} proto 69 | * @param {string} name 70 | * @param {Function} onget 71 | * @param {Function} onset 72 | */ 73 | function hookElemProp(proto, name, onget, onset) { 74 | prop(proto, name, 75 | getter => function() { 76 | const val = getter.call(this) 77 | return onget.call(this, val) 78 | }, 79 | setter => function(val) { 80 | val = onset.call(this, val) 81 | if (val === DROP) { 82 | return 83 | } 84 | setter.call(this, val) 85 | } 86 | ) 87 | } 88 | 89 | const elemProto = win['Element'].prototype 90 | const rawGetAttr = elemProto.getAttribute 91 | const rawSetAttr = elemProto.setAttribute 92 | 93 | const tagAttrHandlersMap = {} 94 | const tagTextHandlerMap = {} 95 | const tagKeySetMap = {} 96 | const tagKeyGetMap = {} 97 | 98 | /** 99 | * @param {string} tag 100 | * @param {object} proto 101 | * @param {...any} handlers 102 | */ 103 | function attr(tag, proto, ...handlers) { 104 | /** @type {boolean} */ 105 | let hasBind 106 | 107 | /** @type {boolean} */ 108 | let hasAttr 109 | 110 | let keySetMap 111 | let keyGetMap 112 | 113 | // TODO: 未考虑上下文 114 | 115 | handlers.forEach(v => { 116 | // 带划线的 attr 属性名,转换成驼峰形式的 prop 属性名。 117 | // 例如 `http-equiv` -> `httpEquiv` 118 | const prop = v.name.replace(/-(\w)/g, 119 | (_, char) => char.toUpperCase() 120 | ) 121 | hookElemProp(proto, prop, v.onget, v.onset) 122 | 123 | // #text 124 | if (prop === 'innerText') { 125 | tagTextHandlerMap[tag] = v 126 | return 127 | } 128 | 129 | // attribute 130 | if (tagAttrHandlersMap[tag]) { 131 | tagAttrHandlersMap[tag].push(v) 132 | hasBind = true 133 | } else { 134 | tagAttrHandlersMap[tag] = [v] 135 | tagKeySetMap[tag] = {} 136 | tagKeyGetMap[tag] = {} 137 | } 138 | 139 | if (!keySetMap) { 140 | keySetMap = tagKeySetMap[tag] 141 | keyGetMap = tagKeyGetMap[tag] 142 | } 143 | const key = v.name.toLocaleLowerCase() 144 | keySetMap[key] = v.onset 145 | keyGetMap[key] = v.onget 146 | hasAttr = true 147 | }) 148 | 149 | if (hasBind || !hasAttr) { 150 | return 151 | } 152 | 153 | // 如果之前调用过 setAttribute,直接返回上次设置的值; 154 | // 如果没有调用过,则返回 onget 的回调值。 155 | func(proto, 'getAttribute', oldFn => function(name) { 156 | const key = (name + '').toLocaleLowerCase() 157 | 158 | const onget = keyGetMap[key] 159 | if (!onget) { 160 | return apply(oldFn, this, arguments) 161 | } 162 | 163 | const lastVal = this['_k' + key] 164 | if (lastVal !== undefined) { 165 | return lastVal 166 | } 167 | const val = apply(oldFn, this, arguments) 168 | return onget.call(this, val) 169 | }) 170 | 171 | func(proto, 'setAttribute', oldFn => function(name, val) { 172 | const key = (name + '').toLocaleLowerCase() 173 | const onset = keySetMap[key] 174 | if (onset) { 175 | this['_k' + key] = val 176 | 177 | const ret = onset.call(this, val) 178 | if (ret === DROP) { 179 | return 180 | } 181 | arguments[1] = ret 182 | } 183 | return apply(oldFn, this, arguments) 184 | }) 185 | 186 | func(proto, 'setAttributeNode', oldFn => function(node) { 187 | console.warn('setAttributeNode:', node, this) 188 | // TODO: 189 | return apply(oldFn, this, arguments) 190 | }) 191 | 192 | // ... 193 | } 194 | 195 | /** 196 | * @param {Node} node 197 | * @param {object} handler 198 | * @param {Element} elem 199 | */ 200 | function parseNewTextNode(node, handler, elem) { 201 | // console.log('parseTextNode') 202 | const val = node.nodeValue 203 | const ret = handler.onset.call(elem, val, true) 204 | if (ret === DROP) { 205 | return 206 | } 207 | node.nodeValue = ret 208 | } 209 | 210 | /** 211 | * @param {Element} elem 212 | * @param {object} handler 213 | */ 214 | function parseNewElemNode(elem, handler) { 215 | const name = handler.name 216 | if (!elem.hasAttribute(name)) { 217 | return 218 | } 219 | const val = rawGetAttr.call(elem, name) 220 | const ret = handler.onset.call(elem, val, true) 221 | if (ret === DROP) { 222 | return 223 | } 224 | rawSetAttr.call(elem, name, ret) 225 | } 226 | 227 | 228 | /** 229 | * @param {Node} node 230 | */ 231 | function addNode(node) { 232 | const type = node.nodeType 233 | if (type === 1) { 234 | /** @type {Element} */ 235 | // @ts-ignore 236 | const elem = node 237 | const handlers = tagAttrHandlersMap[elem.tagName] 238 | handlers && handlers.forEach(v => { 239 | parseNewElemNode(elem, v) 240 | }) 241 | } 242 | else if (type === 3) { 243 | // TEXT_NODE 244 | const parent = node.parentElement 245 | if (parent) { 246 | const handler = tagTextHandlerMap[parent.tagName] 247 | if (handler) { 248 | parseNewTextNode(node, handler, parent) 249 | } 250 | } 251 | } 252 | } 253 | 254 | /** 255 | * @param {Node} node 256 | */ 257 | function delNode(node) { 258 | // TODO: 增加节点删除后的回调 259 | } 260 | 261 | return { 262 | attr, 263 | addNode, 264 | delNode, 265 | } 266 | } -------------------------------------------------------------------------------- /src/proxy/src/index.js: -------------------------------------------------------------------------------- 1 | import * as env from "./env"; 2 | 3 | 4 | function pageEnv(win) { 5 | env.setEnvType(env.ENV_PAGE) 6 | 7 | if (win === top) { 8 | // 开放一个接口,可供 iframe 调用 9 | win.__init__ = function(win) { 10 | page.init(win) 11 | console.log('[jsproxy] child page inited.', win.location.href) 12 | } 13 | 14 | // 用于记录 postMessage 发起者的 win 15 | let lastSrcWin 16 | win.__set_srcWin = function(obj) { 17 | lastSrcWin = obj || win 18 | return [] 19 | } 20 | win.__get_srcWin = function() { 21 | const ret = lastSrcWin 22 | lastSrcWin = null 23 | return ret 24 | } 25 | 26 | // eslint-disable-next-line no-undef 27 | const page = require('./page.js') 28 | page.init(win) 29 | 30 | console.log('[jsproxy] top page inited') 31 | } else { 32 | // 子页面直接调用 top 提供的接口,无需重复初始化 33 | top['__init__'](win) 34 | 35 | win.__set_srcWin = function() { 36 | return top['__set_srcWin'](win) 37 | } 38 | } 39 | } 40 | 41 | function swEnv() { 42 | env.setEnvType(env.ENV_SW) 43 | // eslint-disable-next-line no-undef 44 | require('./sw.js') 45 | } 46 | 47 | function workerEnv(global) { 48 | env.setEnvType(env.ENV_WORKER) 49 | 50 | // eslint-disable-next-line no-undef 51 | require('./client.js').init(global, location.origin) 52 | global.__set_srcWin = function() { 53 | return [] 54 | } 55 | } 56 | 57 | function main(global) { 58 | if ('onclick' in global) { 59 | pageEnv(global) 60 | } else if ('onfetch' in global) { 61 | swEnv() 62 | } else { 63 | workerEnv(global) 64 | } 65 | } 66 | 67 | main(self) -------------------------------------------------------------------------------- /src/proxy/src/inject.js: -------------------------------------------------------------------------------- 1 | import * as path from './path.js' 2 | import * as util from "./util" 3 | // import * as jsfilter from './jsfilter.js' 4 | 5 | let mConf 6 | 7 | 8 | const WORKER_INJECT = util.strToBytes(`\ 9 | if (typeof importScripts === 'function' && !self.window && !self.__PATH__) { 10 | self.__PATH__ = '${path.ROOT}'; 11 | importScripts('${path.HELPER}'); 12 | } 13 | `) 14 | 15 | 16 | export function getWorkerCode() { 17 | return WORKER_INJECT 18 | } 19 | 20 | 21 | export function setConf(conf) { 22 | mConf = conf 23 | } 24 | 25 | 26 | const PADDING = ' '.repeat(500) 27 | 28 | const CSP = `\ 29 | 'self' \ 30 | 'unsafe-inline' \ 31 | file: \ 32 | data: \ 33 | blob: \ 34 | mediastream: \ 35 | filesystem: \ 36 | chrome-extension-resource: \ 37 | ` 38 | 39 | /** 40 | * @param {URL} urlObj 41 | * @param {number} pageId 42 | */ 43 | export function getHtmlCode(urlObj, pageId) { 44 | const icoUrl = path.PREFIX + urlObj.origin + '/favicon.ico' 45 | const custom = mConf.inject_html || '' 46 | 47 | return util.strToBytes(`\ 48 | 49 | 50 | 51 | 52 | 53 | 54 | ${custom} 55 | 56 | 57 | `) 58 | } 59 | -------------------------------------------------------------------------------- /src/proxy/src/jsfilter.js: -------------------------------------------------------------------------------- 1 | import * as util from './util.js' 2 | 3 | 4 | /** 5 | * @param {string} code 6 | */ 7 | export function parseStr(code) { 8 | // TODO: parse js ast 9 | let match = false 10 | 11 | code = code.replace(/(\b)location(\b)/g, (_, $1, $2) => { 12 | match = true 13 | return $1 + '__location' + $2 14 | }) 15 | code = code.replace(/postMessage\s*\(/g, s => { 16 | match = true 17 | return s + `...(self.__set_srcWin?__set_srcWin():[]), ` 18 | }) 19 | if (match) { 20 | return code 21 | } 22 | return null 23 | } 24 | 25 | /** 26 | * @param {Uint8Array} buf 27 | * @param {string} charset 28 | */ 29 | export function parseBin(buf, charset) { 30 | const str = util.bytesToStr(buf, charset) 31 | const ret = parseStr(str) 32 | if (ret !== null) { 33 | return util.strToBytes(ret) 34 | } 35 | if (charset && !util.isUtf8(charset)) { 36 | return util.strToBytes(str) 37 | } 38 | return null 39 | } -------------------------------------------------------------------------------- /src/proxy/src/msg.js: -------------------------------------------------------------------------------- 1 | export const PAGE_INFO_PULL = 1 2 | export const SW_INFO_PUSH = 2 3 | 4 | export const PAGE_COOKIE_PUSH = 3 5 | export const SW_COOKIE_PUSH = 4 6 | 7 | export const PAGE_INIT_BEG = 5 8 | export const PAGE_INIT_END = 6 9 | 10 | export const PAGE_CONF_SET = 110 11 | export const PAGE_CONF_GET = 111 12 | export const PAGE_RELOAD_CONF = 112 13 | 14 | export const SW_CONF_RETURN = 112 15 | export const SW_CONF_CHANGE = 113 16 | 17 | export const PAGE_READY_CHECK = 200 18 | export const SW_READY = 201 -------------------------------------------------------------------------------- /src/proxy/src/network.js: -------------------------------------------------------------------------------- 1 | import * as route from './route.js' 2 | import * as cookie from './cookie.js' 3 | import * as urlx from './urlx.js' 4 | import * as util from './util' 5 | import * as tld from './tld.js' 6 | import * as cdn from './cdn.js' 7 | import {Database} from './database.js' 8 | 9 | 10 | const REFER_ORIGIN = location.origin + '/' 11 | const ENABLE_3RD_COOKIE = true 12 | 13 | /** @type {Database} */ 14 | let mDB 15 | 16 | 17 | // 部分浏览器不支持 access-control-expose-headers: * 18 | // https://developer.mz.jsproxy.tk/en-US/docs/Web/HTTP/Headers/Access-Control-Expose-Headers#Compatibility_notes 19 | // 20 | // 如果返回所有字段名,长度会很大。 21 | // 因此请求头中设置 aceh__ 标记,告知服务器是否要返回所有字段名。 22 | let mIsAcehOld = true 23 | 24 | // TODO: 25 | let mConf 26 | 27 | 28 | export function setConf(conf) { 29 | mConf = conf 30 | cdn.setConf(conf) 31 | } 32 | 33 | 34 | export async function setDB(db) { 35 | mDB = db 36 | // clear expires 37 | } 38 | 39 | 40 | /** 41 | * @param {string} url 42 | */ 43 | function getUrlCache(url) { 44 | return mDB.get('url-cache', url) 45 | } 46 | 47 | 48 | /** 49 | * @param {string} url 50 | * @param {string} host 51 | * @param {string} info 52 | * @param {number} expires 53 | */ 54 | async function setUrlCache(url, host, info, expires) { 55 | await mDB.put('url-cache', {url, host, info, expires}) 56 | } 57 | 58 | 59 | /** 60 | * @param {string} url 61 | */ 62 | async function delUrlCache(url) { 63 | await mDB.delete('url-cache', url) 64 | } 65 | 66 | 67 | /** 68 | * @param {URL} targetUrlObj 69 | * @param {URL} clientUrlObj 70 | * @param {Request} req 71 | */ 72 | function getReqCookie(targetUrlObj, clientUrlObj, req) { 73 | const cred = req.credentials 74 | if (cred === 'omit') { 75 | return '' 76 | } 77 | if (cred === 'same-origin') { 78 | // TODO: 79 | const targetTld = tld.getTld(targetUrlObj.hostname) 80 | const clientTld = tld.getTld(clientUrlObj.hostname) 81 | if (targetTld !== clientTld) { 82 | return '' 83 | } 84 | } 85 | return cookie.query(targetUrlObj) 86 | } 87 | 88 | 89 | /** 90 | * @param {Headers} header 91 | */ 92 | function parseResCache(header) { 93 | const cacheStr = header.get('cache-control') 94 | if (cacheStr) { 95 | if (/no-cache/i.test(cacheStr)) { 96 | return -1 97 | } 98 | const m = cacheStr.match(/(?:^|,\s*)max-age=["]?(\d+)/i) 99 | if (m) { 100 | const sec = +m[1] 101 | if (sec > 0) { 102 | return sec 103 | } 104 | } 105 | } 106 | const expires = header.get('expires') 107 | if (expires) { 108 | const ts = Date.parse(expires) 109 | if (ts > 0) { 110 | return (ts - Date.now()) / 1000 | 0 111 | } 112 | } 113 | return 0 114 | } 115 | 116 | 117 | /** 118 | * @param {string[]} cookieStrArr 119 | * @param {URL} urlObj 120 | * @param {URL} cliUrlObj 121 | */ 122 | function procResCookie(cookieStrArr, urlObj, cliUrlObj) { 123 | if (!ENABLE_3RD_COOKIE) { 124 | const urlTld = tld.getTld(urlObj.hostname) 125 | const cliTld = tld.getTld(cliUrlObj.hostname) 126 | if (cliTld !== urlTld) { 127 | return 128 | } 129 | } 130 | 131 | const ret = [] 132 | const now = Date.now() 133 | 134 | for (const str of cookieStrArr) { 135 | const item = cookie.parse(str, urlObj, now) 136 | if (!item) { 137 | continue 138 | } 139 | cookie.set(item) 140 | if (!item.httpOnly) { 141 | ret.push(item) 142 | } 143 | } 144 | return ret 145 | } 146 | 147 | 148 | /** 149 | * @param {Response} res 150 | */ 151 | function getResInfo(res) { 152 | const rawHeaders = res.headers 153 | let status = res.status 154 | 155 | /** @type {string[]} */ 156 | const cookieStrArr = [] 157 | const headers = new Headers() 158 | 159 | rawHeaders.forEach((val, key) => { 160 | if (key === 'access-control-allow-origin' || 161 | key === 'access-control-expose-headers') { 162 | return 163 | } 164 | if (key === '--s') { 165 | status = +val 166 | return 167 | } 168 | if (key === '--t') { 169 | return 170 | } 171 | // 还原重名字段 172 | // 0-key: v1 173 | // 1-key: v2 174 | // => 175 | // key: v1, v2 176 | // 177 | // 对于 set-cookie 单独存储,因为合并会破坏 cookie 格式: 178 | // var h = new Headers() 179 | // h.append('set-cookie', 'hello') 180 | // h.append('set-cookie', 'world') 181 | // h.get('set-cookie') // "hello, world" 182 | // 183 | const m = key.match(/^\d+-(.+)/) 184 | if (m) { 185 | key = m[1] 186 | if (key === 'set-cookie') { 187 | cookieStrArr.push(val) 188 | } else { 189 | headers.append(key, val) 190 | } 191 | return 192 | } 193 | 194 | // 还原转义字段(`--key` => `key`) 195 | if (key.startsWith('--')) { 196 | key = key.substr(2) 197 | } 198 | 199 | // 单个 set-cookie 返回头 200 | if (key === 'set-cookie') { 201 | cookieStrArr.push(val) 202 | return 203 | } 204 | 205 | headers.set(key, val) 206 | }) 207 | 208 | return {status, headers, cookieStrArr} 209 | } 210 | 211 | 212 | // https://fetch.spec.whatwg.org/#cors-unsafe-request-header-byte 213 | const R_UNSAFE_REQ_HDR_CHAR = 214 | // eslint-disable-next-line no-control-regex 215 | /[\x00-\x08\x0a-\x1f\x22\x28\x29\x3a\x3c\x3e\x3f\x40\x5b\x5c\x5d\x7b\x7d\x7f]/ 216 | 217 | /** 218 | * @param {string} key 219 | * @param {string} val 220 | */ 221 | function isSimpleReqHdr(key, val) { 222 | if (key === 'content-type') { 223 | return ( 224 | val === 'application/x-www-form-urlencoded' || 225 | val === 'multipart/form-data' || 226 | val === 'text/plain' 227 | ) 228 | } 229 | if (key === 'accept' || 230 | key === 'accept-language' || 231 | key === 'content-language' 232 | ) { 233 | // 标准是总和小于 1024,这里保守一些 234 | return val.length < 256 && 235 | !R_UNSAFE_REQ_HDR_CHAR.test(val) 236 | } 237 | } 238 | 239 | 240 | /** 241 | * @param {Request} req 242 | * @param {URL} urlObj 243 | * @param {URL} cliUrlObj 244 | */ 245 | function initReqHdr(req, urlObj, cliUrlObj) { 246 | const reqHdr = new Headers() 247 | const reqMap = { 248 | '--ver': mConf.ver, 249 | '--mode': req.mode, 250 | '--type': req.destination || '', 251 | 'origin': '', 252 | } 253 | if (mIsAcehOld) { 254 | reqMap['--aceh'] = '1' 255 | } 256 | 257 | req.headers.forEach((val, key) => { 258 | if (key === 'user-agent') { 259 | return 260 | } 261 | if (isSimpleReqHdr(key, val)) { 262 | reqHdr.set(key, val) 263 | } else { 264 | reqMap[key] = val 265 | } 266 | }) 267 | 268 | if (reqMap['origin']) { 269 | reqMap['origin'] = cliUrlObj.origin 270 | } 271 | 272 | const referer = req.referrer 273 | if (referer) { 274 | // TODO: CSS 引用图片的 referer 不是页面 URL,而是 CSS URL 275 | if (referer === REFER_ORIGIN) { 276 | // Referrer Policy: origin 277 | reqMap['referer'] = cliUrlObj.origin + '/' 278 | } else { 279 | reqMap['referer'] = urlx.decUrlStrAbs(referer) 280 | } 281 | } 282 | 283 | reqMap['cookie'] = getReqCookie(urlObj, cliUrlObj, req) 284 | 285 | return {reqHdr, reqMap} 286 | } 287 | 288 | /** 289 | * @param {RequestInit} reqOpt 290 | * @param {Object} info 291 | */ 292 | function updateReqHeaders(reqOpt, info) { 293 | reqOpt.referrer = '/?' + new URLSearchParams(info) 294 | } 295 | 296 | 297 | const MAX_RETRY = 5 298 | 299 | /** 300 | * @param {Request} req 301 | * @param {URL} urlObj 302 | * @param {URL} cliUrlObj 303 | */ 304 | export async function launch(req, urlObj, cliUrlObj) { 305 | const {method} = req 306 | 307 | /** @type {RequestInit} */ 308 | const reqOpt = { 309 | mode: 'cors', 310 | method, 311 | } 312 | 313 | if (method === 'POST' && !req.bodyUsed) { 314 | if (req.body) { 315 | reqOpt.body = req.body 316 | } else { 317 | const buf = await req.arrayBuffer() 318 | if (buf.byteLength > 0) { 319 | reqOpt.body = buf 320 | } 321 | } 322 | } 323 | 324 | if (req.signal) { 325 | reqOpt.signal = req.signal 326 | } 327 | 328 | if (!urlx.isHttpProto(urlObj.protocol)) { 329 | // 非 HTTP 协议的资源,直接访问 330 | // 例如 youtube 引用了 chrome-extension: 协议的脚本 331 | const res = await fetch(req) 332 | return {res} 333 | } 334 | 335 | const url = urlObj.href 336 | const urlHash = util.strHash(url) 337 | let host = '' 338 | let rawInfo = '' 339 | 340 | const {reqHdr, reqMap} = initReqHdr(req, urlObj, cliUrlObj) 341 | reqOpt.headers = reqHdr 342 | 343 | while (method === 'GET') { 344 | // 该资源是否加载过? 345 | const r = await getUrlCache(url) 346 | if (r && r.host) { 347 | const now = util.getTimeSeconds() 348 | if (now < r.expires) { 349 | // 使用之前的节点,提高缓存命中率 350 | host = r.host 351 | rawInfo = r.info 352 | break 353 | } 354 | } 355 | 356 | // 支持 CORS 的站点,可直连 357 | if (cdn.isDirectHost(urlObj.host)) { 358 | console.log('direct hit:', url) 359 | const res = await cdn.proxyDirect(url) 360 | if (res) { 361 | setUrlCache(url, '', '', 0) 362 | return {res} 363 | } 364 | } 365 | 366 | // 常用静态资源 CDN 加速 367 | const ver = cdn.getFileVer(urlHash) 368 | if (ver >= 0) { 369 | console.log('cdn hit:', url) 370 | const res = await cdn.proxyStatic(urlHash, ver) 371 | if (res) { 372 | setUrlCache(url, '', '', 0) 373 | return {res} 374 | } 375 | } 376 | 377 | break 378 | } 379 | 380 | // TODO: 此处逻辑需要优化 381 | let level = 1 382 | 383 | // 如果缓存未命中产生请求,服务器不做节点切换 384 | if (host) { 385 | level = 0 386 | } 387 | 388 | /** @type {Response} */ 389 | let res 390 | 391 | /** @type {Headers} */ 392 | let resHdr 393 | 394 | 395 | for (let i = 0; i < MAX_RETRY; i++) { 396 | if (i === 0 && host) { 397 | // 使用缓存的主机 398 | } else { 399 | host = route.getHost(urlHash, level) 400 | } 401 | 402 | const rawUrl = urlx.delHash(urlObj.href) 403 | let proxyUrl = route.genUrl(host, 'http') + '/' + rawUrl 404 | 405 | // 即使未命中缓存,在请求“加速节点”时也能带上文件信息 406 | if (rawInfo) { 407 | reqMap['--raw-info'] = rawInfo 408 | } else { 409 | delete reqMap['--raw-info'] 410 | } 411 | 412 | res = null 413 | try { 414 | reqMap['--level'] = level 415 | updateReqHeaders(reqOpt, reqMap) 416 | res = await fetch(proxyUrl, reqOpt) 417 | } catch (err) { 418 | console.warn('fetch fail:', proxyUrl) 419 | break 420 | // TODO: 重试其他线路 421 | // route.setFailHost(host) 422 | } 423 | resHdr = res.headers 424 | 425 | // 检测浏览器是否支持 aceh: * 426 | if (mIsAcehOld && resHdr.has('--t')) { 427 | mIsAcehOld = false 428 | delete reqMap['--aceh'] 429 | } 430 | 431 | // 是否切换节点 432 | if (resHdr.has('--switched')) { 433 | rawInfo = resHdr.get('--raw-info') 434 | level++ 435 | continue 436 | } 437 | 438 | // 目前只有加速节点会返回该信息 439 | const resErr = resHdr.get('--error') 440 | if (resErr) { 441 | console.warn('[jsproxy] cfworker fail:', resErr) 442 | rawInfo = '' 443 | level = 0 444 | continue 445 | } 446 | 447 | break 448 | } 449 | 450 | if (!res) { 451 | return 452 | } 453 | 454 | const { 455 | status, headers, cookieStrArr 456 | } = getResInfo(res) 457 | 458 | 459 | if (method === 'GET' && status === 200) { 460 | const cacheSec = parseResCache(headers) 461 | if (cacheSec >= 0) { 462 | const expires = util.getTimeSeconds() + cacheSec + 1000 463 | setUrlCache(url, host, rawInfo, expires) 464 | } 465 | } 466 | 467 | // 处理 HTTP 返回头的 refresh 字段 468 | // http://www.otsukare.info/2015/03/26/refresh-http-header 469 | const refresh = headers.get('refresh') 470 | if (refresh) { 471 | const newVal = urlx.replaceHttpRefresh(refresh, url) 472 | if (newVal !== refresh) { 473 | console.log('[jsproxy] http refresh:', refresh) 474 | headers.set('refresh', newVal) 475 | } 476 | } 477 | 478 | let cookies 479 | if (cookieStrArr.length) { 480 | const items = procResCookie(cookieStrArr, urlObj, cliUrlObj) 481 | if (items.length) { 482 | cookies = items 483 | } 484 | } 485 | 486 | return {res, status, headers, cookies} 487 | } 488 | -------------------------------------------------------------------------------- /src/proxy/src/page.js: -------------------------------------------------------------------------------- 1 | import * as MSG from './msg.js' 2 | import * as route from './route.js' 3 | import * as util from './util.js' 4 | import * as urlx from './urlx.js' 5 | import * as hook from './hook.js' 6 | import * as cookie from './cookie.js' 7 | import * as jsfilter from './jsfilter.js' 8 | import * as env from './env.js' 9 | import * as client from './client.js' 10 | 11 | 12 | const { 13 | apply, 14 | } = Reflect 15 | 16 | 17 | function initDoc(win, domHook) { 18 | const document = win.document 19 | 20 | const headElem = document.head 21 | const baseElemList = document.getElementsByTagName('base') 22 | const baseElem = baseElemList[0] 23 | 24 | document.__baseElem = baseElem 25 | 26 | // 27 | // 监控元素创建和删除 28 | // 29 | const nodeSet = new WeakSet() 30 | 31 | function onNodeAdd(node) { 32 | if (nodeSet.has(node)) { 33 | return 34 | } 35 | nodeSet.add(node) 36 | 37 | const nodes = node.childNodes 38 | for (let i = 0, n = nodes.length; i < n; i++) { 39 | onNodeAdd(nodes[i]) 40 | } 41 | domHook.addNode(node) 42 | } 43 | 44 | 45 | function onNodeDel(node) { 46 | nodeSet.delete(node) 47 | 48 | const nodes = node.childNodes 49 | for (let i = 0, n = nodes.length; i < n; i++) { 50 | onNodeDel(nodes[i]) 51 | } 52 | domHook.delNode(node) 53 | 54 | // TODO: 逻辑优化 55 | if (node === baseElem) { 56 | // 默认的 元素可能会被删除,需要及时补上 57 | headElem.insertBefore(baseElem, headElem.firstChild) 58 | console.warn('[jsproxy] base elem restored') 59 | } 60 | } 61 | 62 | /** 63 | * @param {MutationRecord[]} mutations 64 | */ 65 | function parseMutations(mutations) { 66 | mutations.forEach(mutation => { 67 | mutation.addedNodes.forEach(onNodeAdd) 68 | mutation.removedNodes.forEach(onNodeDel) 69 | }) 70 | } 71 | 72 | const observer = new win.MutationObserver(parseMutations) 73 | observer.observe(document, { 74 | childList: true, 75 | subtree: true, 76 | }) 77 | } 78 | 79 | 80 | /** 81 | * Hook 页面 API 82 | * 83 | * @param {Window} win 84 | */ 85 | export function init(win) { 86 | if (!win) { 87 | return 88 | } 89 | try { 90 | win['x'] 91 | } catch (err) { 92 | // TODO: 不应该出现 93 | console.warn('not same origin') 94 | return 95 | } 96 | 97 | const document = win.document 98 | 99 | // 该 window 之前已初始化过,现在只需更新 document。 100 | // 例如 iframe 加载完成之前,读取 contentWindow 得到的是空白页, 101 | // 加载完成后,document 对象会变化,但 window 上的属性仍保留。 102 | const info = env.get(win['Math']) 103 | if (info) { 104 | const {doc, domHook} = info 105 | if (doc !== document) { 106 | // 加载完成后,初始化实际页面的 document 107 | initDoc(win, domHook) 108 | info[1] = document 109 | } 110 | return 111 | } 112 | 113 | 114 | const { 115 | location, 116 | navigator, 117 | } = win 118 | 119 | 120 | // 源路径(空白页继承上级页面) 121 | const oriUrlObj = new URL(document.baseURI) 122 | 123 | const domHook = hook.createDomHook(win) 124 | 125 | // 关联当前页面上下文信息 126 | env.add(win, { 127 | loc: location, 128 | doc: document, 129 | ori: oriUrlObj, 130 | domHook, 131 | }) 132 | 133 | // hook 页面和 Worker 相同的 API 134 | client.init(win, oriUrlObj.origin) 135 | 136 | // 首次安装 document 137 | // 如果访问加载中的页面,返回 about:blank 空白页 138 | initDoc(win, domHook) 139 | 140 | 141 | const sw = navigator.serviceWorker 142 | const swCtl = sw.controller 143 | 144 | function sendMsgToSw(cmd, val) { 145 | swCtl && swCtl.postMessage([cmd, val]) 146 | } 147 | 148 | 149 | // TODO: 这部分逻辑需要优化 150 | let readyCallback 151 | 152 | function pageAsyncInit() { 153 | const curScript = document.currentScript 154 | if (!curScript) { 155 | return 156 | } 157 | // curScript.remove() 158 | 159 | const pageId = +curScript.dataset['id'] 160 | // console.log('PAGE wait id:', pageId) 161 | 162 | if (!pageId) { 163 | console.warn('[jsproxy] missing page id') 164 | return 165 | } 166 | 167 | readyCallback = function() { 168 | sendMsgToSw(MSG.PAGE_INIT_END, pageId) 169 | } 170 | 171 | sendMsgToSw(MSG.PAGE_INIT_BEG, pageId) 172 | 173 | // do async init 174 | // if (win === top) { 175 | sendMsgToSw(MSG.PAGE_INFO_PULL) 176 | // } else { 177 | // readyCallback() 178 | // } 179 | } 180 | pageAsyncInit() 181 | 182 | 183 | sw.addEventListener('message', e => { 184 | const [cmd, val] = e.data 185 | switch (cmd) { 186 | case MSG.SW_COOKIE_PUSH: 187 | // console.log('PAGE MSG.SW_COOKIE_PUSH:', val) 188 | val.forEach(cookie.set) 189 | break 190 | 191 | case MSG.SW_INFO_PUSH: 192 | // console.log('PAGE MSG.SW_INFO_PUSH:', val) 193 | val.cookies.forEach(cookie.set) 194 | route.setConf(val.conf) 195 | readyCallback() 196 | break 197 | 198 | case MSG.SW_CONF_CHANGE: 199 | route.setConf(val) 200 | break 201 | } 202 | e.stopImmediatePropagation() 203 | }, true) 204 | 205 | sw.startMessages && sw.startMessages() 206 | 207 | // 208 | // hook ServiceWorker 209 | // 210 | const swProto = win['ServiceWorkerContainer'].prototype 211 | if (swProto) { 212 | hook.func(swProto, 'register', oldFn => function() { 213 | console.warn('access serviceWorker.register blocked') 214 | return new Promise(function() {}) 215 | }) 216 | hook.func(swProto, 'getRegistration', oldFn => function() { 217 | console.warn('access serviceWorker.getRegistration blocked') 218 | return new Promise(function() {}) 219 | }) 220 | hook.func(swProto, 'getRegistrations', oldFn => function() { 221 | console.warn('access serviceWorker.getRegistrations blocked') 222 | return new Promise(function() {}) 223 | }) 224 | } 225 | 226 | /** 227 | * History API 228 | * @param {string} name 229 | */ 230 | function hookHistory(name) { 231 | const proto = win['History'].prototype 232 | 233 | hook.func(proto, name, oldFn => 234 | /** 235 | * @param {*} data 236 | * @param {string} title 237 | * @param {string} url 相对或绝对路径 238 | */ 239 | function(data, title, url) { 240 | console.log('[jsproxy] history.%s: %s', name, url) 241 | 242 | const {loc, doc} = env.get(this) 243 | if (doc && url) { 244 | const dstUrlObj = urlx.newUrl(url, doc.baseURI) 245 | if (dstUrlObj) { 246 | // 当前页面 URL 247 | const srcUrlStr = urlx.decUrlObj(loc) 248 | const srcUrlObj = new URL(srcUrlStr) 249 | 250 | if (srcUrlObj.origin !== dstUrlObj.origin) { 251 | throw Error(`\ 252 | Failed to execute '${name}' on 'History': \ 253 | A history state object with URL '${url}' \ 254 | cannot be created in a document with \ 255 | origin '${srcUrlObj.origin}' and URL '${srcUrlStr}'.` 256 | ) 257 | } 258 | arguments[2] = urlx.encUrlObj(dstUrlObj) 259 | } 260 | } 261 | return apply(oldFn, this, arguments) 262 | }) 263 | } 264 | hookHistory('pushState') 265 | hookHistory('replaceState') 266 | 267 | // 268 | // hook window.open() 269 | // 270 | hook.func(win, 'open', oldFn => function(url) { 271 | if (url) { 272 | arguments[0] = urlx.encUrlStrRel(url, url) 273 | } 274 | return apply(oldFn, this, arguments) 275 | }) 276 | 277 | // 278 | // hook window.frames[...] 279 | // 280 | const frames = win.frames 281 | 282 | // @ts-ignore 283 | win.frames = new Proxy(frames, { 284 | get(_, key) { 285 | if (typeof key === 'number') { 286 | console.log('get frames index:', key) 287 | const win = frames[key] 288 | init(win) 289 | return win 290 | } else { 291 | return frames[key] 292 | } 293 | } 294 | }) 295 | 296 | // 297 | hook.func(navigator, 'registerProtocolHandler', oldFn => function(_0, url, _1) { 298 | console.log('registerProtocolHandler:', arguments) 299 | return apply(oldFn, this, arguments) 300 | }) 301 | 302 | 303 | // 304 | // hook document.domain 305 | // 306 | const docProto = win['Document'].prototype 307 | let domain = oriUrlObj.hostname 308 | 309 | hook.prop(docProto, 'domain', 310 | getter => function() { 311 | return domain 312 | }, 313 | setter => function(val) { 314 | console.log('[jsproxy] set document.domain:', val) 315 | domain = val 316 | // TODO: 317 | setter.call(this, location.hostname) 318 | } 319 | ) 320 | 321 | // 322 | // hook document.cookie 323 | // 324 | hook.prop(docProto, 'cookie', 325 | getter => function() { 326 | // console.log('[jsproxy] get document.cookie') 327 | const {ori} = env.get(this) 328 | return cookie.query(ori) 329 | }, 330 | setter => function(val) { 331 | // console.log('[jsproxy] set document.cookie:', val) 332 | const {ori} = env.get(this) 333 | const item = cookie.parse(val, ori, Date.now()) 334 | if (item) { 335 | cookie.set(item) 336 | sendMsgToSw(MSG.PAGE_COOKIE_PUSH, item) 337 | } 338 | } 339 | ) 340 | 341 | // hook uri api 342 | function getUriHook(getter) { 343 | return function() { 344 | const val = getter.call(this) 345 | return val && urlx.decUrlStrAbs(val) 346 | } 347 | } 348 | 349 | hook.prop(docProto, 'referrer', getUriHook) 350 | hook.prop(docProto, 'URL', getUriHook) 351 | hook.prop(docProto, 'documentURI', getUriHook) 352 | 353 | const nodeProto = win['Node'].prototype 354 | hook.prop(nodeProto, 'baseURI', getUriHook) 355 | 356 | 357 | // hook Message API 358 | const msgEventProto = win['MessageEvent'].prototype 359 | hook.prop(msgEventProto, 'origin', 360 | getter => function() { 361 | const {ori} = env.get(this) 362 | return ori.origin 363 | } 364 | ) 365 | 366 | 367 | hook.func(win, 'postMessage', oldFn => function(msg, origin) { 368 | const srcWin = top['__get_srcWin']() || this 369 | // console.log(srcWin) 370 | if (origin && origin !== '*') { 371 | arguments[1] = '*' 372 | } 373 | return apply(oldFn, srcWin, arguments) 374 | }) 375 | 376 | 377 | // 378 | // hook 379 | // 380 | const metaProto = win['HTMLMetaElement'].prototype 381 | 382 | domHook.attr('META', metaProto, 383 | { 384 | name: 'http-equiv', 385 | onget(val) { 386 | // TODO: 387 | return val 388 | }, 389 | onset(val) { 390 | let newVal 391 | 392 | switch (val.toLowerCase()) { 393 | case 'refresh': 394 | newVal = urlx.replaceHttpRefresh(this.content, this) 395 | if (newVal !== val) { 396 | console.warn('[jsproxy] meta redir') 397 | this.content = newVal 398 | } 399 | break 400 | case 'content-security-policy': 401 | console.warn('[jsproxy] meta csp removed') 402 | this.remove() 403 | break 404 | case 'content-type': 405 | this.remove() 406 | break 407 | } 408 | return val 409 | } 410 | }, 411 | { 412 | name: 'charset', 413 | onget(val) { 414 | return val 415 | }, 416 | onset(val) { 417 | return 'utf-8' 418 | } 419 | } 420 | ) 421 | 422 | // 423 | // hook 元素的 URL 属性,JS 读取时伪装成原始值 424 | // 425 | function hookAttr(tag, proto, name) { 426 | domHook.attr(tag, proto, { 427 | name, 428 | onget(val) { 429 | if (val === null) { 430 | return '' 431 | } 432 | return urlx.decUrlStrRel(val, this) 433 | }, 434 | onset(val) { 435 | if (val === '') { 436 | return val 437 | } 438 | return urlx.encUrlStrRel(val, this) 439 | } 440 | }) 441 | } 442 | 443 | const anchorProto = win['HTMLAnchorElement'].prototype 444 | hookAttr('A', anchorProto, 'href') 445 | 446 | const areaProto = win['HTMLAreaElement'].prototype 447 | hookAttr('AREA', areaProto, 'href') 448 | 449 | const formProto = win['HTMLFormElement'].prototype 450 | hookAttr('FORM', formProto, 'action') 451 | 452 | const scriptProto = win['HTMLScriptElement'].prototype 453 | const linkProto = win['HTMLLinkElement'].prototype 454 | 455 | // 防止混合内容 456 | if (oriUrlObj.protocol === 'http:') { 457 | hookAttr('SCRIPT', scriptProto, 'src') 458 | hookAttr('LINK', linkProto, 'href') 459 | } 460 | 461 | // const imgProto = win.HTMLImageElement.prototype 462 | // hookAttr('IMG', imgProto, 'src') 463 | 464 | const embedProto = win['HTMLEmbedElement'].prototype 465 | hookAttr('EMBED', embedProto, 'src') 466 | 467 | const objectProto = win['HTMLObjectElement'].prototype 468 | hookAttr('OBJECT', objectProto, 'data') 469 | 470 | const iframeProto = win['HTMLIFrameElement'].prototype 471 | hookAttr('IFRAME', iframeProto, 'src') 472 | 473 | const frameProto = win['HTMLFrameElement'].prototype 474 | hookAttr('FRAME', frameProto, 'src') 475 | 476 | 477 | // 更新默认的 baseURI 478 | // https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base#Usage_notes 479 | const baseProto = win['HTMLBaseElement'].prototype 480 | domHook.attr('BASE', baseProto, 481 | { 482 | name: 'href', 483 | onget(val) { 484 | return this.__href || val 485 | }, 486 | onset(val) { 487 | // TODO: 逻辑优化 488 | const baseElem = this.ownerDocument.__baseElem 489 | if (!baseElem || baseElem === this) { 490 | return val 491 | } 492 | console.log('[jsproxy] baseURI updated:', val) 493 | const urlObj = urlx.newUrl(val, baseElem.href) 494 | baseElem.href = urlObj.href 495 | this.__href = val 496 | return '' 497 | } 498 | }) 499 | 500 | 501 | // 502 | // hook frame 503 | // 504 | hook.prop(iframeProto, 'contentWindow', 505 | getter => function() { 506 | // TODO: origin check 507 | const win = getter.call(this) 508 | init(win) 509 | return win 510 | } 511 | ) 512 | 513 | hook.prop(iframeProto, 'contentDocument', 514 | getter => function() { 515 | // TODO: origin check 516 | const doc = getter.call(this) 517 | if (doc) { 518 | init(doc.defaultView) 519 | } 520 | return doc 521 | } 522 | ) 523 | 524 | // 525 | // hook 超链接的 host、pathname 等属性 526 | // 这类属性只有 property 没有 attribute 527 | // 528 | function hookAnchorUrlProp(proto) { 529 | /** 530 | * @param {string} key 531 | */ 532 | function setupProp(key) { 533 | hook.prop(proto, key, 534 | getter => function() { 535 | // 读取 href 时会经过 hook 处理,得到的已是原始 URL 536 | const urlObj = new URL(this.href) 537 | return urlObj[key] 538 | }, 539 | setter => function(val) { 540 | // console.log('[jsproxy] set link %s: %s', key, val) 541 | const urlObj = new URL(this.href) 542 | urlObj[key] = val 543 | this.href = urlObj.href 544 | } 545 | ) 546 | } 547 | setupProp('protocol') 548 | setupProp('hostname') 549 | setupProp('host') 550 | setupProp('port') 551 | setupProp('pathname') 552 | setupProp('origin') 553 | } 554 | hookAnchorUrlProp(anchorProto) 555 | hookAnchorUrlProp(areaProto) 556 | 557 | 558 | // 该 form 可能没有经过 MutationObserver 处理 559 | hook.func(formProto, 'submit', oldFn => function() { 560 | this.action = this.action 561 | return apply(oldFn, this, arguments) 562 | }) 563 | 564 | 565 | // 566 | // 监控 离屏元素.click() 方式打开页面 567 | // 例如: 568 | // var s = document.createElement('div') 569 | // s.innerHTML = '' 570 | // s.getElementsByTagName('img')[0].click() 571 | // 572 | const htmlProto = win['HTMLElement'].prototype 573 | 574 | hook.func(htmlProto, 'click', oldFn => function() { 575 | /** @type {HTMLAnchorElement} */ 576 | let el = this 577 | 578 | // 添加到文档时已经过 MutationObserver 处理 579 | // 无需调整 href 属性 580 | if (el.isConnected) { 581 | return 582 | } 583 | while (el) { 584 | const tag = el.tagName 585 | if (tag === 'A' || tag === 'AREA') { 586 | // eslint-disable-next-line no-self-assign 587 | el.href = el.href 588 | break 589 | } 590 | // @ts-ignore 591 | el = el.parentNode 592 | } 593 | return apply(oldFn, this, arguments) 594 | }) 595 | 596 | 597 | // 598 | // 脚本元素处理 599 | // 600 | /** @type {WeakMap} */ 601 | const integrityMap = new WeakMap() 602 | 603 | /** @type {WeakMap} */ 604 | const charsetMap = new WeakMap() 605 | 606 | 607 | domHook.attr('SCRIPT', scriptProto, 608 | // 统一使用 UTF-8 编码 609 | // JS 未提供 UTF-8 转非 UTF-8 的 API,导致编码转换比较麻烦, 610 | // 因此 SW 将所有 JS 资源都转换成 UTF-8 编码。 611 | { 612 | name: 'charset', 613 | onget(val) { 614 | return charsetMap.get(this) || val 615 | }, 616 | onset(val) { 617 | if (!util.isUtf8(val)) { 618 | val = 'utf-8' 619 | } 620 | charsetMap.set(this, val) 621 | return val 622 | } 623 | }, 624 | // 禁止设置内容校验 625 | //(调整静态 HTML 时控制台会有告警,但不会阻止运行) 626 | { 627 | name: 'integrity', 628 | onget(val) { 629 | return integrityMap.get(this) || '' 630 | }, 631 | onset(val) { 632 | integrityMap.set(this, val) 633 | return '' 634 | } 635 | }, 636 | // 监控动态创建的脚本 637 | //(设置 innerHTML 时同样会触发) 638 | { 639 | name: 'innerText', 640 | onget(val) { 641 | return val 642 | }, 643 | onset(val, isInit) { 644 | const ret = updateScriptText(this, val) 645 | if (ret === null) { 646 | return isInit ? hook.DROP : val 647 | } 648 | return ret 649 | } 650 | }) 651 | 652 | // text 属性只有 prop 没有 attr 653 | hook.prop(scriptProto, 'text', 654 | getter => function() { 655 | return getter.call(this) 656 | }, 657 | setter => function(val) { 658 | const ret = updateScriptText(this, val) 659 | if (ret === null) { 660 | setter.call(this, val) 661 | } 662 | setter.call(this, ret) 663 | } 664 | ) 665 | 666 | 667 | /** @type {WeakSet} */ 668 | const parsedSet = new WeakSet() 669 | 670 | /** 671 | * @param {HTMLScriptElement} elem 672 | */ 673 | function updateScriptText(elem, code) { 674 | // 有些脚本仅用于存储数据(例如模块字符串),无需处理 675 | const type = elem.type 676 | if (type && !util.isJsMime(type)) { 677 | return null 678 | } 679 | if (parsedSet.has(elem)) { 680 | return null 681 | } 682 | parsedSet.add(elem) 683 | 684 | return jsfilter.parseStr(code) 685 | } 686 | 687 | 688 | /** 689 | * 处理 形式的脚本 690 | * @param {string} eventName 691 | */ 692 | function hookEvent(eventName) { 693 | const scanedSet = new WeakSet() 694 | 695 | function scanElement(el) { 696 | if (scanedSet.has(el)) { 697 | return 698 | } 699 | scanedSet.add(el) 700 | 701 | // 非元素节点 702 | if (el.nodeType != 1 /*Node.ELEMENT_NODE*/) { 703 | return 704 | } 705 | // 扫描内联代码 706 | if (el[eventName]) { 707 | const code = el.getAttribute(eventName) 708 | if (code) { 709 | const ret = jsfilter.parseStr(code) 710 | if (ret) { 711 | el[eventName] = ret 712 | console.log('[jsproxy] jsfilter onevent:', eventName) 713 | } 714 | } 715 | } 716 | // 扫描上级元素 717 | scanElement(el.parentNode) 718 | } 719 | 720 | document.addEventListener(eventName.substr(2), e => { 721 | scanElement(e.target) 722 | }, true) 723 | } 724 | 725 | hookEvent('onerror') 726 | hookEvent('onload') 727 | hookEvent('onclick') 728 | // Object.keys(htmlProto).forEach(v => { 729 | // if (v.startsWith('on')) { 730 | // hookEvent(v) 731 | // } 732 | // }) 733 | } 734 | -------------------------------------------------------------------------------- /src/proxy/src/path.js: -------------------------------------------------------------------------------- 1 | 2 | export const ROOT = getRootPath() 3 | export const HOME = ROOT + 'index.html' 4 | export const CONF = ROOT + 'conf.js' 5 | export const ICON = ROOT + 'favicon.ico' 6 | export const HELPER = ROOT + '__sys__/helper.js' 7 | export const ASSETS = ROOT + '__sys__/assets/' 8 | export const PREFIX = ROOT + '-----' 9 | 10 | 11 | function getRootPath() { 12 | // 13 | // 如果运行在代理页面,当前路径: 14 | // https://example.com/path/to/-----url 15 | // 如果运行在 SW,当前路径: 16 | // https://example.com/path/to/sw.js 17 | // 如果运行在 Worker,当前路径: 18 | // __PATH__ 19 | // 返回: 20 | // https://example.com/path/to/ 21 | // 22 | /** @type {string} */ 23 | const envPath = self['__PATH__'] 24 | if (envPath) { 25 | return envPath 26 | } 27 | let url = location.href 28 | const pos = url.indexOf('/-----http') 29 | if (pos === -1) { 30 | // sw 31 | url = url.replace(/[^/]+$/, '') 32 | } else { 33 | // page 34 | url = url.substr(0, pos) 35 | } 36 | return url.replace(/\/*$/, '/') 37 | } -------------------------------------------------------------------------------- /src/proxy/src/route.js: -------------------------------------------------------------------------------- 1 | import * as urlx from './urlx.js' 2 | import * as util from './util' 3 | 4 | 5 | let mConf 6 | let mNodeLinesMap 7 | 8 | /** 9 | * @param {number} urlHash 10 | * @param {string} id 11 | * @returns {string} 12 | */ 13 | function getHostByNodeId(urlHash, id) { 14 | let a = 0 15 | for (const {weight, host} of mNodeLinesMap[id]) { 16 | if ((a += weight) > urlHash) { 17 | return host 18 | } 19 | } 20 | } 21 | 22 | 23 | /** 24 | * @param {string} host 25 | */ 26 | function isLocalhost(host) { 27 | return /^(localhost|127\.\d+\.\d+\.\d+)([:/]|$)/.test(host) 28 | } 29 | 30 | 31 | /** 32 | * @param {string} host 33 | * @param {string} scheme 34 | */ 35 | export function genUrl(host, scheme) { 36 | const s = isLocalhost(host) ? '' : 's' 37 | return `${scheme}${s}://${host}/${scheme}` 38 | } 39 | 40 | 41 | /** 42 | * @param {number} urlHash 43 | * @param {number} level 44 | */ 45 | export function getHost(urlHash, level) { 46 | let node = mConf.node_default 47 | 48 | // 实验中... 49 | if (level === 2) { 50 | node = mConf.node_acc 51 | } 52 | 53 | return getHostByNodeId(urlHash, node) 54 | } 55 | 56 | 57 | // export function setFailHost(host) { 58 | 59 | // } 60 | 61 | 62 | /** 63 | * @param {URL} urlObj 64 | * @param {Object} args 65 | */ 66 | export function genWsUrl(urlObj, args) { 67 | let scheme = 'https' 68 | 69 | switch (urlObj.protocol) { 70 | case 'wss:': 71 | break 72 | case 'ws:': 73 | scheme = 'http' 74 | break 75 | default: 76 | return null 77 | } 78 | 79 | const t = urlx.delScheme(urlx.delHash(urlObj.href)) 80 | 81 | args['url__'] = scheme + '://' + t 82 | args['ver__'] = mConf.ver 83 | 84 | const urlHash = util.strHash(urlObj.href) 85 | const host = getHostByNodeId(urlHash, mConf.node_default) 86 | return genUrl(host, 'ws') + '?' + new URLSearchParams(args) 87 | } 88 | 89 | 90 | /** 91 | * @param {object} conf 92 | */ 93 | export function setConf(conf) { 94 | mConf = conf 95 | mNodeLinesMap = {} 96 | 97 | for (const [id, info] of Object.entries(conf.node_map)) { 98 | const lines = [] 99 | let weightSum = 0 100 | 101 | for (const [host, weight] of Object.entries(info.lines)) { 102 | weightSum += weight 103 | lines.push({host, weight}) 104 | } 105 | 106 | // 权重值按比例转换成 0 ~ 2^32 之间的整数,方便后续计算 107 | for (const v of lines) { 108 | v.weight = (v.weight / weightSum * 0xFFFFFFFF) >>> 0 109 | } 110 | lines.sort((a, b) => b.weight - a.weight) 111 | 112 | mNodeLinesMap[id] = lines 113 | } 114 | } -------------------------------------------------------------------------------- /src/proxy/src/signal.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Promise 简单封装 3 | * 4 | * 封装前 5 | * ``` 6 | * function get(...) { 7 | * return new Promise(function(resolve, reject) { 8 | * ... 9 | * function callback(err, result) { 10 | * if (err) { 11 | * reject(err) 12 | * } else { 13 | * resolve(result) 14 | * } 15 | * } 16 | * ... 17 | * } 18 | * } 19 | * ... 20 | * await get(...) 21 | * ``` 22 | * 23 | * 24 | * 封装后 25 | * ``` 26 | * function get(...) { 27 | * ... 28 | * const s = new Signal() 29 | * function callback(err, result) { 30 | * if (err) { 31 | * s.abort(err) 32 | * } else { 33 | * s.notify(result) 34 | * } 35 | * } 36 | * ... 37 | * return s.wait() 38 | * } 39 | * ... 40 | * await get(...) 41 | * ``` 42 | */ 43 | export class Signal { 44 | constructor() { 45 | this._promise = new Promise((resolve, reject) => { 46 | this._resolve = resolve 47 | this._reject = reject 48 | }) 49 | } 50 | 51 | wait() { 52 | return this._promise 53 | } 54 | 55 | notify(arg) { 56 | this._resolve(arg) 57 | } 58 | 59 | abort(arg) { 60 | this._reject(arg) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/proxy/src/storage.js: -------------------------------------------------------------------------------- 1 | import * as hook from './hook.js' 2 | import * as urlx from './urlx.js' 3 | 4 | 5 | const { 6 | apply, 7 | defineProperty, 8 | ownKeys, 9 | getOwnPropertyDescriptor, 10 | } = Reflect 11 | 12 | const undefined = void 0 13 | 14 | 15 | /** 16 | * @param {WindowOrWorkerGlobalScope} win 17 | * @param {string} name 18 | * @param {string} prefix 19 | */ 20 | function setup(win, name, prefix) { 21 | /** @type {Storage} */ 22 | const raw = win[name] 23 | if (!raw) { 24 | return 25 | } 26 | const prefixLen = prefix.length 27 | 28 | const nativeMap = { 29 | getItem, 30 | setItem, 31 | removeItem, 32 | clear, 33 | key, 34 | constructor: raw.constructor, 35 | toString: () => raw.toString(), 36 | [Symbol.toStringTag]: 'Storage', 37 | get length() { 38 | return getAllKeys().length 39 | }, 40 | } 41 | 42 | /** 43 | * @param {*} key 44 | */ 45 | function getItem(key) { 46 | return raw.getItem(prefix + key) 47 | } 48 | 49 | /** 50 | * @param {*} key 51 | * @param {string} val 52 | */ 53 | function setItem(key, val) { 54 | // TODO: 同步到 indexedDB 55 | raw.setItem(prefix + key, val) 56 | } 57 | 58 | /** 59 | * @param {*} key 60 | */ 61 | function removeItem(key) { 62 | return raw.removeItem(prefix + key) 63 | } 64 | 65 | function clear() { 66 | getAllKeys().forEach(removeItem) 67 | } 68 | 69 | /** 70 | * @param {*} val 71 | */ 72 | function key(val) { 73 | // TODO: 无需遍历所有 74 | const arr = getAllKeys() 75 | const ret = arr[val | 0] 76 | if (ret === undefined) { 77 | return null 78 | } 79 | return ret 80 | } 81 | 82 | 83 | /** 84 | * @returns {string[]} 85 | */ 86 | function getAllKeys() { 87 | const ret = [] 88 | const keys = ownKeys(raw) 89 | for (let i = 0; i < keys.length; i++) { 90 | const key = keys[i] 91 | if (typeof key !== 'string') { 92 | continue 93 | } 94 | if (!key.startsWith(prefix)) { 95 | continue 96 | } 97 | ret.push(key.substr(prefixLen)) 98 | } 99 | return ret 100 | } 101 | 102 | const storage = new Proxy(raw, { 103 | get(obj, key) { 104 | const val = nativeMap[key] 105 | if (val !== undefined) { 106 | return val 107 | } 108 | console.log('[jsproxy] %s get: %s', name, key) 109 | const ret = getItem(key) 110 | if (ret === null) { 111 | return undefined 112 | } 113 | return ret 114 | }, 115 | set(obj, key, val) { 116 | if (key in nativeMap) { 117 | nativeMap[key] = val 118 | return 119 | } 120 | console.log('[jsproxy] %s set: %s = %s', name, key, val) 121 | setItem(key, val) 122 | return true 123 | }, 124 | deleteProperty(obj, key) { 125 | console.log('[jsproxy] %s del: %s', name, key) 126 | removeItem(key) 127 | return true 128 | }, 129 | has(obj, key) { 130 | console.log('[jsproxy] %s has: %s', name, key) 131 | if (typeof key === 'string') { 132 | return (prefix + key) in obj 133 | } 134 | return false 135 | }, 136 | // enumerate(obj) { 137 | // console.log('[jsproxy] %s enumerate: %s', name) 138 | // // TODO: 139 | // }, 140 | ownKeys(obj) { 141 | // console.log('[jsproxy] %s ownKeys', name) 142 | return getAllKeys() 143 | }, 144 | // defineProperty(obj, key, desc) { 145 | // // console.log('[jsproxy] %s defineProperty: %s', name, key) 146 | // // TODO: 147 | // }, 148 | getOwnPropertyDescriptor(obj, key) { 149 | // console.log('[jsproxy] %s getOwnPropertyDescriptor: %s', name, key) 150 | if (typeof key === 'string') { 151 | return getOwnPropertyDescriptor(raw, prefix + key) 152 | } 153 | } 154 | }) 155 | 156 | defineProperty(win, name, {value: storage}) 157 | } 158 | 159 | 160 | /** 161 | * @param {WindowOrWorkerGlobalScope} global 162 | * @param {string} origin 163 | */ 164 | export function createStorage(global, origin) { 165 | const prefixStr = origin + '$' 166 | const prefixLen = prefixStr.length 167 | 168 | 169 | function delPrefix(str) { 170 | return str.substr(prefixLen) 171 | } 172 | 173 | function delPrefixGetter(oldFn) { 174 | return function() { 175 | const val = oldFn.call(this) 176 | return val && delPrefix(val) 177 | } 178 | } 179 | 180 | // 181 | // Web Storage 182 | // 183 | setup(global, 'localStorage', prefixStr) 184 | setup(global, 'sessionStorage', prefixStr) 185 | 186 | const StorageEventProto = global['StorageEvent'].prototype 187 | 188 | hook.prop(StorageEventProto, 'key', delPrefixGetter) 189 | hook.prop(StorageEventProto, 'url', 190 | getter => function() { 191 | const val = getter.call(this) 192 | return urlx.decUrlStrAbs(val) 193 | } 194 | ) 195 | // TODO: StorageEventProto.storageArea 196 | 197 | // 198 | // Storage API 199 | // 200 | function addPrefixHook(oldFn) { 201 | return function(name) { 202 | if (arguments.length > 0) { 203 | arguments[0] = prefixStr + name 204 | } 205 | return apply(oldFn, this, arguments) 206 | } 207 | } 208 | 209 | // indexedDB 210 | const IDBFactoryProto = global['IDBFactory'].prototype 211 | hook.func(IDBFactoryProto, 'open', addPrefixHook) 212 | 213 | hook.func(IDBFactoryProto, 'databases', oldFn => async function() { 214 | /** @type { {name: string, version: number}[] } */ 215 | const arr = await apply(oldFn, this, arguments) 216 | const ret = [] 217 | for (const v of arr) { 218 | if (v.name[0] !== '.') { 219 | v.name = delPrefix(v.name) 220 | ret.push(v) 221 | } 222 | } 223 | return ret 224 | }) 225 | 226 | // delete 227 | hook.func(IDBFactoryProto, 'deleteDatabase', addPrefixHook) 228 | 229 | const IDBDatabaseProto = global['IDBDatabase'].prototype 230 | hook.prop(IDBDatabaseProto, 'name', delPrefixGetter) 231 | 232 | 233 | // Cache Storage 234 | const cacheStorageProto = global['CacheStorage'].prototype 235 | hook.func(cacheStorageProto, 'open', addPrefixHook) 236 | 237 | hook.func(cacheStorageProto, 'keys', oldFn => async function() { 238 | /** @type {string[]} */ 239 | const arr = await apply(oldFn, this, arguments) 240 | const ret = [] 241 | for (const v of arr) { 242 | if (v[0] !== '.') { 243 | ret.push(delPrefix(v)) 244 | } 245 | } 246 | return ret 247 | }) 248 | 249 | hook.func(cacheStorageProto, 'delete', addPrefixHook) 250 | 251 | // WebSQL 252 | hook.func(global, 'openDatabase', addPrefixHook) 253 | } -------------------------------------------------------------------------------- /src/proxy/src/sw.js: -------------------------------------------------------------------------------- 1 | import * as path from './path.js' 2 | import * as route from './route.js' 3 | import * as urlx from './urlx.js' 4 | import * as util from './util.js' 5 | import * as cookie from './cookie.js' 6 | import * as network from './network.js' 7 | import * as MSG from './msg.js' 8 | import * as jsfilter from './jsfilter.js' 9 | import * as inject from './inject.js' 10 | import {Signal} from './signal.js' 11 | import {Database} from './database.js' 12 | 13 | 14 | const CONF_UPDATE_TIMER = 1000 * 60 * 5 15 | 16 | let mConf 17 | const MAX_REDIR = 5 18 | 19 | /** @type {ServiceWorkerGlobalScope} */ 20 | // @ts-ignore 21 | const global = self 22 | const clients = global.clients 23 | 24 | let mUrlHandler 25 | 26 | 27 | /** 28 | * @param {*} target 29 | * @param {number} cmd 30 | * @param {*=} val 31 | */ 32 | function sendMsg(target, cmd, val) { 33 | if (target) { 34 | target.postMessage([cmd, val]) 35 | } else { 36 | console.warn('invalid target', cmd, val) 37 | } 38 | } 39 | 40 | 41 | // 也可以用 clientId 关联,但兼容性不高 42 | let pageCounter = 0 43 | 44 | /** @type {Map} */ 45 | const pageWaitMap = new Map() 46 | 47 | function genPageId() { 48 | return ++pageCounter 49 | } 50 | 51 | /** 52 | * @param {number} pageId 53 | */ 54 | function pageWait(pageId) { 55 | const s = new Signal() 56 | // 设置最大等待时间 57 | // 有些页面不会执行 JS(例如查看源文件),导致永久等待 58 | const timer = setTimeout(_ => { 59 | pageWaitMap.delete(pageId) 60 | s.notify(false) 61 | }, 2000) 62 | 63 | pageWaitMap.set(pageId, [s, timer]) 64 | return s.wait() 65 | } 66 | 67 | /** 68 | * @param {number} id 69 | * @param {boolean} isDone 70 | */ 71 | function pageNotify(id, isDone) { 72 | const arr = pageWaitMap.get(id) 73 | if (!arr) { 74 | console.warn('[jsproxy] unknown page id:', id) 75 | return 76 | } 77 | const [s, timer] = arr 78 | if (isDone) { 79 | pageWaitMap.delete(id) 80 | s.notify(true) 81 | } else { 82 | // 页面已开始初始化,关闭定时器 83 | clearTimeout(timer) 84 | } 85 | } 86 | 87 | 88 | function makeHtmlRes(body, status = 200) { 89 | return new Response(body, { 90 | status, 91 | headers: { 92 | 'content-type': 'text/html; charset=utf-8', 93 | } 94 | }) 95 | } 96 | 97 | 98 | /** 99 | * @param {Response} res 100 | * @param {ResponseInit} resOpt 101 | * @param {URL} urlObj 102 | */ 103 | function processHtml(res, resOpt, urlObj) { 104 | const reader = res.body.getReader() 105 | let injected = false 106 | 107 | const stream = new ReadableStream({ 108 | async pull(controller) { 109 | if (!injected) { 110 | injected = true 111 | 112 | // 注入页面顶部的代码 113 | const pageId = genPageId() 114 | const buf = inject.getHtmlCode(urlObj, pageId) 115 | controller.enqueue(buf) 116 | 117 | // 留一些时间给页面做异步初始化 118 | const done = await pageWait(pageId) 119 | if (!done) { 120 | console.warn('[jsproxy] page wait timeout. id: %d url: %s', 121 | pageId, urlObj.href) 122 | } 123 | } 124 | const r = await reader.read() 125 | if (r.done) { 126 | controller.close() 127 | } else { 128 | controller.enqueue(r.value) 129 | } 130 | } 131 | }) 132 | return new Response(stream, resOpt) 133 | } 134 | 135 | 136 | /** 137 | * @param {ArrayBuffer} buf 138 | * @param {string} charset 139 | */ 140 | function processJs(buf, charset) { 141 | const u8 = new Uint8Array(buf) 142 | const ret = jsfilter.parseBin(u8, charset) || u8 143 | return util.concatBufs([inject.getWorkerCode(), ret]) 144 | } 145 | 146 | 147 | /** 148 | * @param {*} cmd 149 | * @param {*} msg 150 | * @param {string=} srcId 151 | */ 152 | async function sendMsgToPages(cmd, msg, srcId) { 153 | // 通知页面更新 Cookie 154 | const pages = await clients.matchAll({type: 'window'}) 155 | 156 | for (const page of pages) { 157 | if (page.frameType !== 'top-level') { 158 | continue 159 | } 160 | if (srcId && page.id === srcId) { 161 | continue 162 | } 163 | sendMsg(page, cmd, msg) 164 | } 165 | } 166 | 167 | 168 | /** @type Map */ 169 | const mIdUrlMap = new Map() 170 | 171 | /** 172 | * @param {string} id 173 | */ 174 | async function getUrlByClientId(id) { 175 | const client = await clients.get(id) 176 | if (!client) { 177 | return 178 | } 179 | const urlStr = urlx.decUrlStrAbs(client.url) 180 | mIdUrlMap.set(id, urlStr) 181 | return urlStr 182 | } 183 | 184 | 185 | /** 186 | * @param {string} jsonStr 187 | * @param {number} status 188 | * @param {URL} urlObj 189 | */ 190 | function parseGatewayError(jsonStr, status, urlObj) { 191 | let ret = '' 192 | const { 193 | msg, addr, url 194 | } = JSON.parse(jsonStr) 195 | 196 | switch (status) { 197 | case 204: 198 | switch (msg) { 199 | case 'ORIGIN_NOT_ALLOWED': 200 | ret = '当前域名不在服务器外链白名单' 201 | break 202 | case 'CIRCULAR_DEPENDENCY': 203 | ret = '当前请求出现循环代理' 204 | break 205 | case 'SITE_MOVE': 206 | ret = `当前站点移动到: ${url}` 207 | break 208 | } 209 | break 210 | case 500: 211 | ret = '代理服务器内部错误' 212 | break 213 | case 502: 214 | if (addr) { 215 | ret = `代理服务器无法连接网站 ${urlObj.origin} (${addr})` 216 | } else { 217 | ret = `代理服务器无法解析域名 ${urlObj.host}` 218 | } 219 | break 220 | case 504: 221 | ret = `代理服务器连接网站超时 ${urlObj.origin}` 222 | if (addr) { 223 | ret += ` (${addr})` 224 | } 225 | break 226 | } 227 | return makeHtmlRes(ret) 228 | } 229 | 230 | 231 | /** 232 | * @param {Request} req 233 | * @param {URL} urlObj 234 | * @param {URL} cliUrlObj 235 | * @param {number} redirNum 236 | * @returns {Promise} 237 | */ 238 | async function forward(req, urlObj, cliUrlObj, redirNum) { 239 | const r = await network.launch(req, urlObj, cliUrlObj) 240 | if (!r) { 241 | return makeHtmlRes('load fail') 242 | } 243 | let { 244 | res, status, headers, cookies 245 | } = r 246 | 247 | if (cookies) { 248 | sendMsgToPages(MSG.SW_COOKIE_PUSH, cookies) 249 | } 250 | 251 | if (!status) { 252 | status = res.status || 200 253 | } 254 | 255 | let headersMutable = true 256 | if (!headers) { 257 | headers = res.headers 258 | headersMutable = false 259 | } 260 | 261 | /** 262 | * @param {string} k 263 | * @param {string} v 264 | */ 265 | const setHeader = (k, v) => { 266 | if (!headersMutable) { 267 | headers = new Headers(headers) 268 | headersMutable = true 269 | } 270 | headers.set(k, v) 271 | } 272 | 273 | // 网关错误 274 | const gwErr = headers.get('gateway-err--') 275 | if (gwErr) { 276 | return parseGatewayError(gwErr, status, urlObj) 277 | } 278 | 279 | /** @type {ResponseInit} */ 280 | const resOpt = {status, headers} 281 | 282 | // 空响应 283 | // https://fetch.spec.whatwg.org/#statuses 284 | if (status === 101 || 285 | status === 204 || 286 | status === 205 || 287 | status === 304 288 | ) { 289 | return new Response(null, resOpt) 290 | } 291 | 292 | // 处理重定向 293 | if (status === 301 || 294 | status === 302 || 295 | status === 303 || 296 | status === 307 || 297 | status === 308 298 | ) { 299 | const locStr = headers.get('location') 300 | const locObj = locStr && urlx.newUrl(locStr, urlObj) 301 | if (locObj) { 302 | // 跟随模式,返回最终数据 303 | if (req.redirect === 'follow') { 304 | if (++redirNum === MAX_REDIR) { 305 | return makeHtmlRes('重定向过多', 500) 306 | } 307 | return forward(req, locObj, cliUrlObj, redirNum) 308 | } 309 | // 不跟随模式(例如页面跳转),返回 30X 状态 310 | setHeader('location', urlx.encUrlObj(locObj)) 311 | } 312 | 313 | // firefox, safari 保留内容会提示页面损坏 314 | return new Response(null, resOpt) 315 | } 316 | 317 | // 318 | // 提取 mime 和 charset(不存在则为 undefined) 319 | // 可能存在多个段,并且值可能包含引号。例如: 320 | // content-type: text/html; ...; charset="gbk" 321 | // 322 | const ctVal = headers.get('content-type') || '' 323 | const [, mime, charset] = ctVal 324 | .toLocaleLowerCase() 325 | .match(/([^;]*)(?:.*?charset=['"]?([^'"]+))?/) 326 | 327 | 328 | const type = req.destination 329 | if (type === 'script' || 330 | type === 'worker' || 331 | type === 'sharedworker' 332 | ) { 333 | const buf = await res.arrayBuffer() 334 | const ret = processJs(buf, charset) 335 | 336 | setHeader('content-type', 'text/javascript') 337 | return new Response(ret, resOpt) 338 | } 339 | 340 | if (req.mode === 'navigate' && mime === 'text/html') { 341 | return processHtml(res, resOpt, urlObj) 342 | } 343 | 344 | return new Response(res.body, resOpt) 345 | } 346 | 347 | 348 | async function proxy(e, urlObj) { 349 | // 使用 e.resultingClientId 有问题 350 | const id = e.clientId 351 | let cliUrlStr 352 | if (id) { 353 | cliUrlStr = mIdUrlMap.get(id) || await getUrlByClientId(id) 354 | } 355 | if (!cliUrlStr) { 356 | cliUrlStr = urlObj.href 357 | } 358 | const cliUrlObj = new URL(cliUrlStr) 359 | 360 | try { 361 | return await forward(e.request, urlObj, cliUrlObj, 0) 362 | } catch (err) { 363 | console.error(err) 364 | return makeHtmlRes('前端脚本错误
' + err.stack + '
', 500) 365 | } 366 | } 367 | 368 | /** @type {Database} */ 369 | let mDB 370 | 371 | async function initDB() { 372 | mDB = new Database('.sys') 373 | await mDB.open({ 374 | 'url-cache': { 375 | keyPath: 'url' 376 | }, 377 | 'cookie': { 378 | keyPath: 'id' 379 | } 380 | }) 381 | 382 | await network.setDB(mDB) 383 | await cookie.setDB(mDB) 384 | } 385 | 386 | 387 | /** 388 | * @param {FetchEvent} e 389 | */ 390 | async function onFetch(e) { 391 | if (!mConf) { 392 | await initConf() 393 | } 394 | // TODO: 逻辑优化 395 | if (!mDB) { 396 | await initDB() 397 | } 398 | const req = e.request 399 | const urlStr = urlx.delHash(req.url) 400 | 401 | // 首页(例如 https://zjcqoo.github.io/) 402 | if (urlStr === path.ROOT || urlStr === path.HOME) { 403 | let indexPath = mConf.assets_cdn + mConf.index_path 404 | if (!mConf.index_path) { 405 | // 临时代码。防止配置文件未更新的情况下首页无法加载 406 | indexPath = mConf.assets_cdn + 'index_v3.html' 407 | } 408 | const res = await fetch(indexPath) 409 | return makeHtmlRes(res.body) 410 | } 411 | 412 | // 图标、配置(例如 https://zjcqoo.github.io/conf.js) 413 | if (urlStr === path.CONF || urlStr === path.ICON) { 414 | return fetch(urlStr) 415 | } 416 | 417 | // 注入页面的脚本(例如 https://zjcqoo.github.io/__sys__/helper.js) 418 | if (urlStr === path.HELPER) { 419 | return fetch(self['__FILE__']) 420 | } 421 | 422 | // 静态资源(例如 https://zjcqoo.github.io/__sys__/assets/ico/google.png) 423 | if (urlStr.startsWith(path.ASSETS)) { 424 | const filePath = urlStr.substr(path.ASSETS.length) 425 | return fetch(mConf.assets_cdn + filePath) 426 | } 427 | 428 | if (req.mode === 'navigate') { 429 | const newUrl = urlx.adjustNav(urlStr) 430 | if (newUrl) { 431 | return Response.redirect(newUrl, 301) 432 | } 433 | } 434 | 435 | let targetUrlStr = urlx.decUrlStrAbs(urlStr) 436 | 437 | const handler = mUrlHandler[targetUrlStr] 438 | if (handler) { 439 | const { 440 | redir, 441 | content, 442 | replace, 443 | } = handler 444 | 445 | if (redir) { 446 | return Response.redirect('/-----' + redir) 447 | } 448 | if (content) { 449 | return makeHtmlRes(content) 450 | } 451 | if (replace) { 452 | targetUrlStr = replace 453 | } 454 | } 455 | 456 | const targetUrlObj = urlx.newUrl(targetUrlStr) 457 | 458 | if (targetUrlObj) { 459 | return proxy(e, targetUrlObj) 460 | } 461 | return makeHtmlRes('invalid url: ' + targetUrlStr, 500) 462 | } 463 | 464 | 465 | function parseUrlHandler(handler) { 466 | const map = {} 467 | if (!handler) { 468 | return map 469 | } 470 | for (const [match, rule] of Object.entries(handler)) { 471 | // TODO: 支持通配符和正则 472 | map[match] = rule 473 | } 474 | return map 475 | } 476 | 477 | // TODO: 逻辑优化 478 | function updateConf(conf, force) { 479 | if (!force && mConf) { 480 | if (conf.ver <= mConf.ver) { 481 | return 482 | } 483 | if (conf.node_map[mConf.node_default]) { 484 | conf.node_default = mConf.node_default 485 | } else { 486 | console.warn('default node %s -> %s', 487 | mConf.node_default, conf.node_default) 488 | } 489 | sendMsgToPages(MSG.SW_CONF_CHANGE, mConf) 490 | } 491 | inject.setConf(conf) 492 | route.setConf(conf) 493 | network.setConf(conf) 494 | 495 | mUrlHandler = parseUrlHandler(conf.url_handler) 496 | /*await*/ saveConf(conf) 497 | 498 | mConf = conf 499 | } 500 | 501 | 502 | async function readConf() { 503 | const cache = await caches.open('.sys') 504 | const req = new Request('/conf.json') 505 | const res = await cache.match(req) 506 | if (res) { 507 | return res.json() 508 | } 509 | } 510 | 511 | async function saveConf(conf) { 512 | const json = JSON.stringify(conf) 513 | const cache = await caches.open('.sys') 514 | const req = new Request('/conf.json') 515 | const res = new Response(json); 516 | return cache.put(req, res) 517 | } 518 | 519 | async function loadConf() { 520 | const res = await fetch('conf.js') 521 | const txt = await res.text() 522 | self['jsproxy_config'] = updateConf 523 | Function(txt)() 524 | } 525 | 526 | 527 | /** @type {Signal[]} */ 528 | let mConfInitQueue 529 | 530 | async function initConf() { 531 | if (mConfInitQueue) { 532 | const s = new Signal() 533 | mConfInitQueue.push(s) 534 | return s.wait() 535 | } 536 | mConfInitQueue = [] 537 | 538 | let conf 539 | try { 540 | conf = await readConf() 541 | } catch (err) { 542 | console.warn('load conf fail:', err) 543 | } 544 | if (!conf) { 545 | conf = self['__CONF__'] 546 | } 547 | if (conf) { 548 | updateConf(conf) 549 | } else { 550 | conf = await loadConf() 551 | } 552 | 553 | // 定期更新配置 554 | setInterval(loadConf, CONF_UPDATE_TIMER) 555 | 556 | mConfInitQueue.forEach(s => s.notify()) 557 | mConfInitQueue = null 558 | } 559 | 560 | 561 | global.addEventListener('fetch', e => { 562 | e.respondWith(onFetch(e)) 563 | }) 564 | 565 | 566 | global.addEventListener('message', e => { 567 | // console.log('sw msg:', e.data) 568 | const [cmd, val] = e.data 569 | const src = e.source 570 | 571 | switch (cmd) { 572 | case MSG.PAGE_COOKIE_PUSH: 573 | cookie.set(val) 574 | // @ts-ignore 575 | sendMsgToPages(MSG.SW_COOKIE_PUSH, [val], src.id) 576 | break 577 | 578 | case MSG.PAGE_INFO_PULL: 579 | // console.log('SW MSG.COOKIE_PULL:', src.id) 580 | sendMsg(src, MSG.SW_INFO_PUSH, { 581 | cookies: cookie.getNonHttpOnlyItems(), 582 | conf: mConf, 583 | }) 584 | break 585 | 586 | case MSG.PAGE_INIT_BEG: 587 | // console.log('SW MSG.PAGE_INIT_BEG:', val) 588 | pageNotify(val, false) 589 | break 590 | 591 | case MSG.PAGE_INIT_END: 592 | // console.log('SW MSG.PAGE_INIT_END:', val) 593 | pageNotify(val, true) 594 | break 595 | 596 | case MSG.PAGE_CONF_GET: 597 | if (mConf) { 598 | sendMsg(src, MSG.SW_CONF_RETURN, mConf) 599 | } else { 600 | initConf().then(_ => { 601 | sendMsg(src, MSG.SW_CONF_RETURN, mConf) 602 | }) 603 | } 604 | break 605 | 606 | case MSG.PAGE_CONF_SET: 607 | updateConf(val, true) 608 | sendMsgToPages(MSG.SW_CONF_CHANGE, mConf) 609 | break 610 | 611 | case MSG.PAGE_RELOAD_CONF: 612 | /*await*/ loadConf() 613 | break 614 | 615 | case MSG.PAGE_READY_CHECK: 616 | sendMsg(src, MSG.SW_READY) 617 | /*await*/ loadConf() 618 | break 619 | } 620 | }) 621 | 622 | 623 | global.addEventListener('install', e => { 624 | console.log('oninstall:', e) 625 | e.waitUntil(global.skipWaiting()) 626 | }) 627 | 628 | 629 | global.addEventListener('activate', e => { 630 | console.log('onactivate:', e) 631 | sendMsgToPages(MSG.SW_READY, 1) 632 | 633 | e.waitUntil(clients.claim()) 634 | }) 635 | 636 | 637 | console.log('[jsproxy] sw inited') 638 | -------------------------------------------------------------------------------- /src/proxy/src/tld.js: -------------------------------------------------------------------------------- 1 | // https://publicsuffix.org/list/effective_tld_names.dat 2 | import tldData from './tld-data.js' 3 | import {isIPv4} from './util.js' 4 | 5 | 6 | /** @type {Map} */ 7 | const mTldCache = new Map() 8 | const mTldSet = new Set(tldData.split(',')) 9 | 10 | /** 11 | * @param {string} domain 12 | */ 13 | function getDomainTld(domain) { 14 | if (isTld(domain)) { 15 | return domain 16 | } 17 | let pos = 0 18 | for (;;) { 19 | // a.b.c -> b.c 20 | pos = domain.indexOf('.', pos + 1) 21 | if (pos === -1) { 22 | return '' 23 | } 24 | const str = domain.substr(pos + 1) 25 | if (isTld(str)) { 26 | return str 27 | } 28 | } 29 | } 30 | 31 | /** 32 | * @param {string} domain 33 | */ 34 | export function getTld(domain) { 35 | let ret = mTldCache.get(domain) 36 | if (ret !== undefined) { 37 | return ret 38 | } 39 | 40 | if (isIPv4(domain)) { 41 | ret = domain 42 | } else { 43 | ret = getDomainTld(domain) 44 | } 45 | 46 | mTldCache.set(domain, ret) 47 | return ret 48 | } 49 | 50 | 51 | /** 52 | * @param {string} domain 53 | */ 54 | export function isTld(domain) { 55 | return mTldSet.has(domain) 56 | } -------------------------------------------------------------------------------- /src/proxy/src/urlx.js: -------------------------------------------------------------------------------- 1 | import * as util from './util.js' 2 | import * as env from './env.js' 3 | import * as path from './path.js' 4 | import * as tld from './tld.js' 5 | 6 | 7 | const PREFIX = path.PREFIX 8 | const PREFIX_LEN = PREFIX.length 9 | const ROOT_LEN = path.ROOT.length 10 | 11 | /** 12 | * @param {string} url 13 | */ 14 | export function isHttpProto(url) { 15 | return /^https?:/.test(url) 16 | } 17 | 18 | 19 | /** 20 | * @param {string} url 21 | */ 22 | function isInternalUrl(url) { 23 | return !isHttpProto(url) || url.startsWith(PREFIX) 24 | } 25 | 26 | 27 | /** 28 | * @param {string} url 29 | * @param {string | URL=} baseUrl 30 | */ 31 | export function newUrl(url, baseUrl) { 32 | try { 33 | // [safari] baseUrl 不能为空 34 | return baseUrl 35 | ? new URL(url, baseUrl) 36 | : new URL(url) 37 | } catch (err) { 38 | } 39 | } 40 | 41 | 42 | /** 43 | * @param {URL | Location} urlObj 44 | */ 45 | export function encUrlObj(urlObj) { 46 | const fullUrl = urlObj.href 47 | if (isInternalUrl(fullUrl)) { 48 | return fullUrl 49 | } 50 | return PREFIX + fullUrl 51 | } 52 | 53 | const IS_SW = env.isSwEnv() 54 | const IS_WORKER = env.isWorkerEnv() 55 | const WORKER_URL = IS_WORKER && decUrlStrAbs(location.href) 56 | 57 | /** 58 | * @param {string} url 59 | * @param {*} relObj 60 | */ 61 | export function encUrlStrRel(url, relObj) { 62 | let baseUrl 63 | 64 | if (IS_SW) { 65 | baseUrl = relObj 66 | } else if (IS_WORKER) { 67 | baseUrl = WORKER_URL 68 | } else { 69 | const {doc} = env.get(relObj) 70 | baseUrl = doc.baseURI 71 | } 72 | 73 | const urlObj = newUrl(url, baseUrl) 74 | if (!urlObj) { 75 | return url 76 | } 77 | return encUrlObj(urlObj) 78 | } 79 | 80 | 81 | /** 82 | * @param {string} url 83 | */ 84 | export function encUrlStrAbs(url) { 85 | const urlObj = newUrl(url) 86 | if (!urlObj) { 87 | return url 88 | } 89 | return encUrlObj(urlObj) 90 | } 91 | 92 | 93 | /** 94 | * @param {URL | Location} urlObj 95 | */ 96 | export function decUrlObj(urlObj) { 97 | const fullUrl = urlObj.href 98 | if (!fullUrl.startsWith(PREFIX)) { 99 | return fullUrl 100 | } 101 | return fullUrl.substr(PREFIX_LEN) 102 | } 103 | 104 | 105 | /** 106 | * @param {string} url 107 | * @param {*} relObj 108 | */ 109 | export function decUrlStrRel(url, relObj) { 110 | let baseUrl 111 | 112 | if (IS_WORKER) { 113 | baseUrl = WORKER_URL 114 | } else { 115 | const {doc} = env.get(relObj) 116 | baseUrl = doc.baseURI 117 | } 118 | 119 | const urlObj = newUrl(url, baseUrl) 120 | if (!urlObj) { 121 | return url 122 | } 123 | return decUrlObj(urlObj) 124 | } 125 | 126 | 127 | /** 128 | * @param {string} url 129 | */ 130 | export function decUrlStrAbs(url) { 131 | const urlObj = newUrl(url) 132 | if (!urlObj) { 133 | return url 134 | } 135 | return decUrlObj(urlObj) 136 | } 137 | 138 | 139 | 140 | /** 141 | * @param {string} url 142 | */ 143 | export function delHash(url) { 144 | const p = url.indexOf('#') 145 | return (p === -1) ? url : url.substr(0, p) 146 | } 147 | 148 | 149 | /** 150 | * @param {string} url 151 | */ 152 | export function delScheme(url) { 153 | const p = url.indexOf('://') 154 | return (p === -1) ? url : url.substr(p + 3) 155 | } 156 | 157 | 158 | /** 159 | * @param {string} val 160 | */ 161 | export function replaceHttpRefresh(val, relObj) { 162 | return val.replace(/(;\s*url=)(.+)/i, (_, $1, url) => { 163 | return $1 + encUrlStrRel(url, relObj) 164 | }) 165 | } 166 | 167 | 168 | /** 169 | * URL 导航调整 170 | * 171 | * 标准 172 | * https://example.com/-----https://www.google.com/ 173 | * 174 | * 无路径 175 | * https://example.com/-----https://www.google.com 176 | * 177 | * 无协议 178 | * https://example.com/-----www.google.com 179 | * 180 | * 任意数量的分隔符 181 | * https://example.com/---https://www.google.com 182 | * https://example.com/---------https://www.google.com 183 | * https://example.com/https://www.google.com 184 | * 185 | * 重复 186 | * https://example.com/-----https://example.com/-----https://www.google.com 187 | * 188 | * 别名 189 | * https://example.com/google 190 | * 191 | * 192 | * 搜索 193 | * https://example.com/-----xxx 194 | * -> 195 | * https://www.google.com/search?q=xxx 196 | */ 197 | const DEFAULT_ALIAS = { 198 | 'www.google.com': ['google', 'gg', 'g'], 199 | 'www.youtube.com': ['youtube', 'yt', 'y'], 200 | 'www.wikipedia.org': ['wikipedia', 'wiki', 'wk', 'w'], 201 | 'www.facebook.com': ['facebook', 'fb', 'f'], 202 | 'twitter.com': ['twitter', 'tw', 't'], 203 | } 204 | 205 | const DEFAULT_SEARCH = 'https://www.google.com/search?q=%s' 206 | 207 | /** @type {Map} */ 208 | let aliasDomainMap 209 | 210 | /** 211 | * @param {string} alias 212 | */ 213 | function getAliasUrl(alias) { 214 | if (!aliasDomainMap) { 215 | aliasDomainMap = new Map() 216 | for (const [domain, aliasArr] of Object.entries(DEFAULT_ALIAS)) { 217 | for (const v of aliasArr) { 218 | aliasDomainMap.set(v, domain) 219 | } 220 | } 221 | } 222 | 223 | const domain = aliasDomainMap.get(alias) 224 | if (domain) { 225 | return 'https://' + domain + '/' 226 | } 227 | } 228 | 229 | 230 | /** 231 | * @param {string} part 232 | */ 233 | function padUrl(part) { 234 | // TODO: HSTS 235 | const urlStr = isHttpProto(part) ? part : `http://${part}` 236 | const urlObj = newUrl(urlStr) 237 | if (!urlObj) { 238 | return 239 | } 240 | const {hostname} = urlObj 241 | 242 | // http://localhost 243 | if (!hostname.includes('.')) { 244 | return 245 | } 246 | 247 | // http://a.b 248 | if (!tld.getTld(hostname)) { 249 | return 250 | } 251 | 252 | // 数字会被当做 IP 地址: 253 | // new URL('http://1024').href == 'http://0.0.4.0' 254 | // 这种情况应该搜索,而不是访问 255 | // 只有出现完整的 IP 才访问 256 | if (util.isIPv4(hostname) && !urlStr.includes(hostname)) { 257 | return 258 | } 259 | 260 | return urlObj.href 261 | } 262 | 263 | 264 | /** 265 | * @param {string} urlStr 266 | */ 267 | export function adjustNav(urlStr) { 268 | // 分隔符 `-----` 之后的部分 269 | const rawUrlStr = urlStr.substr(PREFIX_LEN) 270 | const rawUrlObj = newUrl(rawUrlStr) 271 | 272 | if (rawUrlObj) { 273 | // 循环引用 274 | const m = rawUrlStr.match(/\/-----(https?:\/\/.+)$/) 275 | if (m) { 276 | return PREFIX + m[1] 277 | } 278 | // 标准格式(大概率) 279 | if (isHttpProto(rawUrlObj.protocol) && 280 | PREFIX + rawUrlObj.href === urlStr 281 | ) { 282 | return 283 | } 284 | } 285 | 286 | // 任意数量 `-` 之后的部分 287 | const part = urlStr.substr(ROOT_LEN).replace(/^-*/, '') 288 | 289 | const ret = getAliasUrl(part) || padUrl(part) 290 | if (ret) { 291 | return PREFIX + ret 292 | } 293 | 294 | const keyword = part.replace(/&/g, '%26') 295 | return PREFIX + DEFAULT_SEARCH.replace('%s', keyword) 296 | } 297 | -------------------------------------------------------------------------------- /src/proxy/src/util.js: -------------------------------------------------------------------------------- 1 | const ENC = new TextEncoder() 2 | 3 | /** 4 | * @param {string} str 5 | */ 6 | export function strToBytes(str) { 7 | return ENC.encode(str) 8 | } 9 | 10 | /** 11 | * @param {BufferSource} bytes 12 | * @param {string} charset 13 | */ 14 | export function bytesToStr(bytes, charset = 'utf-8') { 15 | return new TextDecoder(charset).decode(bytes) 16 | } 17 | 18 | /** 19 | * @param {string} label 20 | */ 21 | export function isUtf8(label) { 22 | return /^utf-?8$/i.test(label) 23 | } 24 | 25 | 26 | const R_IP = /^(?:\d+\.){0,3}\d+$/ 27 | 28 | /** 29 | * @param {string} str 30 | */ 31 | export function isIPv4(str) { 32 | return R_IP.test(str) 33 | } 34 | 35 | 36 | const JS_MIME_SET = new Set([ 37 | 'text/javascript', 38 | 'application/javascript', 39 | 'application/ecmascript', 40 | 'application/x-ecmascript', 41 | 'module', 42 | ]) 43 | 44 | /** 45 | * @param {string} mime 46 | */ 47 | export function isJsMime(mime) { 48 | return JS_MIME_SET.has(mime) 49 | } 50 | 51 | 52 | /** 53 | * 将多个 Uint8Array 拼接成一个 54 | * @param {Uint8Array[]} bufs 55 | */ 56 | export function concatBufs(bufs) { 57 | let size = 0 58 | bufs.forEach(v => { 59 | size += v.length 60 | }) 61 | 62 | let ret = new Uint8Array(size) 63 | let pos = 0 64 | bufs.forEach(v => { 65 | ret.set(v, pos) 66 | pos += v.length 67 | }) 68 | return ret 69 | } 70 | 71 | 72 | /** 73 | * @param {string} str 74 | */ 75 | export function strHash(str) { 76 | let sum = 0 77 | for (let i = 0, n = str.length; i < n; i++) { 78 | sum = (sum * 31 + str.charCodeAt(i)) >>> 0 79 | } 80 | return sum 81 | } 82 | 83 | 84 | /** 85 | * @param {number} num 86 | * @param {number} len 87 | */ 88 | export function numToHex(num, len) { 89 | return ('00000000' + num.toString(16)).slice(-len) 90 | } 91 | 92 | 93 | /** 94 | * @param {number} ms 95 | */ 96 | export async function sleep(ms) { 97 | return new Promise(y => setTimeout(y, ms)) 98 | } 99 | 100 | 101 | export function getTimeSeconds() { 102 | return (Date.now() / 1000) | 0 103 | } -------------------------------------------------------------------------------- /www/404.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /www/assets/README.md: -------------------------------------------------------------------------------- 1 | 该目录的文件可通过 CDN 加速 -------------------------------------------------------------------------------- /www/assets/cors_v1.txt: -------------------------------------------------------------------------------- 1 | # HTTP 返回头存在 access-control-allow-origin: * 的站点,不走代理直接连接 2 | # 收集了部分,实验中... 3 | 4 | # google 5 | ssl.google-analytics.com 6 | 7 | # [public] 8 | cdn.jsdelivr.net 9 | unpkg.com 10 | cdnjs.cloudflare.com 11 | cdn.bootcss.com 12 | use.fontawesome.com 13 | fast.fonts.net 14 | script.hotjar.com 15 | 16 | # github 17 | github.githubassets.com 18 | avatars0.githubusercontent.com 19 | avatars1.githubusercontent.com 20 | avatars2.githubusercontent.com 21 | avatars3.githubusercontent.com 22 | 23 | desktop.github.com 24 | 25 | # flickr 26 | status.flickr.net 27 | 28 | # ali 29 | at.alicdn.com 30 | img.alicdn.com 31 | g.alicdn.com 32 | i.alicdn.com 33 | atanx.alicdn.com 34 | wwc.alicdn.com 35 | gw.alicdn.com 36 | assets.alicdn.com 37 | aeis.alicdn.com 38 | atanx.alicdn.com 39 | hudong.alicdn.com 40 | gma.alicdn.com 41 | 42 | sc01.alicdn.com 43 | sc02.alicdn.com 44 | sc03.alicdn.com 45 | sc04.alicdn.com 46 | 47 | cbu01.alicdn.com 48 | cbu02.alicdn.com 49 | cbu03.alicdn.com 50 | cbu04.alicdn.com 51 | 52 | # baidu 53 | # img*.bdimg.com 54 | img0.bdimg.com 55 | img1.bdimg.com 56 | img2.bdimg.com 57 | img3.bdimg.com 58 | img4.bdimg.com 59 | img5.bdimg.com 60 | 61 | webmap0.bdimg.com 62 | webmap1.bdimg.com 63 | iknowpc.bdimg.com 64 | bkssl.bdimg.com 65 | baikebcs.bdimg.com 66 | gh.bdstatic.com 67 | 68 | # qq 69 | 3gimg.qq.com 70 | combo.b.qq.com 71 | 72 | # bilibili 73 | upos-hz-mirrorakam.akamaized.net 74 | 75 | # toutiao 76 | images.taboola.com 77 | images.taboola.com.cn 78 | images-dup.taboola.com 79 | 80 | # zhihu 81 | static.zhihu.com 82 | pic1.zhimg.com 83 | pic2.zhimg.com 84 | pic3.zhimg.com 85 | pic4.zhimg.com 86 | pic5.zhimg.com 87 | pic7.zhimg.com 88 | 89 | # jd 90 | img11.360buyimg.com 91 | 92 | # jianshu 93 | upload.jianshu.io 94 | upload-images.jianshu.io 95 | cdn2.jianshu.io 96 | 97 | # 163 98 | urswebzj.nosdn.127.net 99 | static.ws.126.net 100 | img1.cache.netease.com 101 | img2.cache.netease.com 102 | img3.cache.netease.com 103 | img4.cache.netease.com 104 | img5.cache.netease.com 105 | img6.cache.netease.com 106 | 107 | # sina 108 | js.t.sinajs.cn 109 | mjs.sinaimg.cn 110 | h5.sinaimg.cn 111 | 112 | # sohu 113 | 0d077ef9e74d8.cdn.sohucs.com 114 | 39d0825d09f05.cdn.sohucs.com 115 | 5b0988e595225.cdn.sohucs.com 116 | caaceed4aeaf2.cdn.sohucs.com 117 | 118 | img01.sogoucdn.com 119 | img02.sogoucdn.com 120 | img03.sogoucdn.com 121 | img04.sogoucdn.com 122 | img05.sogoucdn.com 123 | 124 | # hupu 125 | w1.hoopchina.com.cn 126 | w2.hoopchina.com.cn 127 | w3.hoopchina.com.cn 128 | w4.hoopchina.com.cn 129 | shihuo.hupucdn.com 130 | 131 | # uc 132 | image.uc.cn 133 | 134 | # ... 135 | static.cnodejs.org 136 | static2.cnodejs.org 137 | 2b.zol-img.com.cn 138 | img.pconline.com.cn 139 | angular.cn 140 | img1.dxycdn.com 141 | cdn.kastatic.org 142 | static.geetest.com 143 | cdn.registerdisney.go.com 144 | secure-us.imrworldwide.com 145 | img1.doubanio.com 146 | qnwww2.autoimg.cn 147 | qnwww3.autoimg.cn 148 | s.autoimg.cn 149 | 150 | hb.imgix.net 151 | main.qcloudimg.com 152 | vz-cdn2.contentabc.com 153 | twemoji.maxcdn.com 154 | fgn.cdn.serverable.com 155 | 156 | s1.hdslb.com 157 | s2.hdslb.com 158 | s3.hdslb.com 159 | 160 | # cnblogs 161 | common.cnblogs.com 162 | mathjax.cnblogs.com 163 | 164 | # csdn 165 | csdnimg.cn 166 | g.csdnimg.cn 167 | img-ads.csdn.net 168 | img-bss.csdn.net 169 | img-blog.csdn.net 170 | 171 | # ... 172 | static.geekbang.org 173 | static001.infoq.cn 174 | static.docs.com 175 | cdn1.developermedia.com 176 | cdn2.developermedia.com 177 | cdn.optimizely.com 178 | cdn.ampproject.org 179 | 180 | camshowverse.to 181 | static.camshowhub-cdn.to 182 | 183 | xqimg.imedao.com 184 | xavatar.imedao.com 185 | 186 | # ??? 187 | img-l3.xvideos-cdn.com 188 | static-egc.xvideos-cdn.com 189 | img-hw.xvideos-cdn.com 190 | img-hw.xnxx-cdn.com 191 | static-egc.xnxx-cdn.com 192 | di.phncdn.com 193 | cv.phncdn.com 194 | roomimg.stream.highwebmedia.com 195 | w3.cdn.anvato.net -------------------------------------------------------------------------------- /www/assets/ico/blogger.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EtherDream/jsproxy-browser/a7a6d740e2c64b1a029fe9ced8ffe9f786e154dd/www/assets/ico/blogger.png -------------------------------------------------------------------------------- /www/assets/ico/facebook.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EtherDream/jsproxy-browser/a7a6d740e2c64b1a029fe9ced8ffe9f786e154dd/www/assets/ico/facebook.png -------------------------------------------------------------------------------- /www/assets/ico/flickr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EtherDream/jsproxy-browser/a7a6d740e2c64b1a029fe9ced8ffe9f786e154dd/www/assets/ico/flickr.png -------------------------------------------------------------------------------- /www/assets/ico/gist.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EtherDream/jsproxy-browser/a7a6d740e2c64b1a029fe9ced8ffe9f786e154dd/www/assets/ico/gist.png -------------------------------------------------------------------------------- /www/assets/ico/google.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EtherDream/jsproxy-browser/a7a6d740e2c64b1a029fe9ced8ffe9f786e154dd/www/assets/ico/google.png -------------------------------------------------------------------------------- /www/assets/ico/quora.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EtherDream/jsproxy-browser/a7a6d740e2c64b1a029fe9ced8ffe9f786e154dd/www/assets/ico/quora.png -------------------------------------------------------------------------------- /www/assets/ico/reddit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EtherDream/jsproxy-browser/a7a6d740e2c64b1a029fe9ced8ffe9f786e154dd/www/assets/ico/reddit.png -------------------------------------------------------------------------------- /www/assets/ico/twitch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EtherDream/jsproxy-browser/a7a6d740e2c64b1a029fe9ced8ffe9f786e154dd/www/assets/ico/twitch.png -------------------------------------------------------------------------------- /www/assets/ico/twitter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EtherDream/jsproxy-browser/a7a6d740e2c64b1a029fe9ced8ffe9f786e154dd/www/assets/ico/twitter.png -------------------------------------------------------------------------------- /www/assets/ico/wiki.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EtherDream/jsproxy-browser/a7a6d740e2c64b1a029fe9ced8ffe9f786e154dd/www/assets/ico/wiki.png -------------------------------------------------------------------------------- /www/assets/ico/youtube.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EtherDream/jsproxy-browser/a7a6d740e2c64b1a029fe9ced8ffe9f786e154dd/www/assets/ico/youtube.png -------------------------------------------------------------------------------- /www/assets/index_v3.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Page Sandbox 5 | 6 | 7 | 8 | 29 | 30 | 31 |
32 | 33 |
34 |
35 | 36 |
37 |
38 | 切换线路: 39 | 40 |
41 |
42 |
43 | 167 | 168 | -------------------------------------------------------------------------------- /www/conf.js: -------------------------------------------------------------------------------- 1 | jsproxy_config({ 2 | // 当前配置的版本(记录在日志中,用于排查问题) 3 | // 每次修改配置,该值需要增加,否则不会生效。 4 | // 默认每隔 5 分钟自动下载配置,若想立即验证,可通过隐私模式访问。 5 | ver: '107', 6 | 7 | // 通过 CDN 加速常用网站的静态资源(实验中) 8 | static_boost: { 9 | enable: true, 10 | ver: 60 11 | }, 12 | 13 | // 节点配置 14 | node_map: { 15 | 'demo-hk': { 16 | label: '演示服务-香港节点', 17 | lines: { 18 | // 主机:权重 19 | 'node-aliyun-hk-0.etherdream.com:8443': 1, 20 | 'node-aliyun-hk-1.etherdream.com:8443': 1, 21 | 'node-aliyun-hk-2.etherdream.com:8443': 1, 22 | } 23 | }, 24 | 'demo-sg': { 25 | label: '演示服务-新加坡节点', 26 | lines: { 27 | 'node-aliyun-sg.etherdream.com:8443': 1, 28 | }, 29 | }, 30 | 'mysite': { 31 | label: '当前站点', 32 | lines: { 33 | [location.host]: 1, 34 | } 35 | }, 36 | // 该节点用于加载大体积的静态资源 37 | 'cfworker': { 38 | label: '', 39 | hidden: true, 40 | lines: { 41 | // 收费版(高权重) 42 | 'node-cfworker.etherdream.com': 4, 43 | 44 | // 免费版(低权重,分摊一些成本) 45 | // 每个账号每天 10 万次免费请求,但有频率限制 46 | 'b.007.workers.dev': 1, 47 | 'b.hehe.workers.dev': 1, 48 | 'b.lulu.workers.dev': 1, 49 | 'b.jsproxy.workers.dev': 1, 50 | } 51 | } 52 | }, 53 | 54 | /** 55 | * 默认节点 56 | */ 57 | // node_default: 'mysite', 58 | node_default: /jsproxy-demo\.\w+$/.test(location.host) ? 'demo-hk' : 'mysite', 59 | 60 | /** 61 | * 加速节点 62 | */ 63 | node_acc: 'cfworker', 64 | 65 | /** 66 | * 静态资源 CDN 地址 67 | * 用于加速 `assets` 目录中的资源访问 68 | */ 69 | assets_cdn: 'https://cdn.jsdelivr.net/gh/zjcqoo/zjcqoo.github.io@master/assets/', 70 | 71 | // 本地测试时打开,否则访问的是线上的 72 | // assets_cdn: 'assets/', 73 | 74 | // 首页路径 75 | index_path: 'index_v3.html', 76 | 77 | // 支持 CORS 的站点列表(实验中...) 78 | direct_host_list: 'cors_v1.txt', 79 | 80 | /** 81 | * 自定义注入页面的 HTML 82 | */ 83 | inject_html: '', 84 | 85 | /** 86 | * URL 自定义处理(设计中) 87 | */ 88 | url_handler: { 89 | 'https://www.baidu.com/img/baidu_resultlogo@2.png': { 90 | replace: 'https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_92x30dp.png' 91 | }, 92 | 'https://www.pornhub.com/': { 93 | redir: 'https://php.net/' 94 | }, 95 | 'http://haha.com/': { 96 | content: 'Hello World' 97 | }, 98 | } 99 | }) -------------------------------------------------------------------------------- /www/sw.js: -------------------------------------------------------------------------------- 1 | jsproxy_config=x=>{__CONF__=x;importScripts(__FILE__=x.assets_cdn+'bundle.c33e24c5.js')};importScripts('conf.js') --------------------------------------------------------------------------------