diff --git a/README.md b/README.md index 7715ab5..fdbe658 100644 --- a/README.md +++ b/README.md @@ -1,212 +1,108 @@ # 在线预览 -https://www.gk.jsproxy.tk +https://zjcqoo.github.io/-----https://www.google.com (目前仍在更新中,最好使用隐身模式访问,避免缓存导致的问题) -# 安装部署 - -## 依赖 - -* OpenResty - -* acme.sh - -* node.js / webpack / webpack-cli - -CentOS7 可执行 `./server/setup.sh` 一键安装。 +[之前版本](https://github.com/EtherDream/jsproxy/tree/first-ver)已不再更新,但[演示服务](https://jsproxy.tk/)仍保留一段时间。 -## 配置 +# 安装 -首先需要一个域名,例如 example.com,解析 @ 和 * 到服务器 IP。 - -在项目的根目录下新建 `dnsconf` 文件: +新建一个名为 `jsproxy` 用户,在其主目录安装 nginx: ```bash -DOMAIN=example.com -DNS_ID=dns_xx -export xx_id=xxx -export xx_key=xxxxxx +useradd jsproxy -g nobody +su jsproxy + +cd ~ +git clone --depth=1 git@github.com:EtherDream/jsproxy.git server + +cd server +./setup-nginx.sh ``` -第一个为域名,后面三个参考 [acme.sh dns api](https://github.com/Neilpang/acme.sh/tree/master/dnsapi)。 +安装过程若有依赖缺失,可尝试(CentOS 为例): -执行 `./build.sh`。该过程会申请 SSL 证书,时间可能较长。 - -执行 `./server/run.sh` 开启服务。 - -访问 `https://example.com` 即可进入首页。 - -> 本项目使用了 `brotli_static` 指令,如果当前的 nginx 不支持,可在 `server/nginx.conf` 配置中将其注释,或参考 `server/setup.sh` 重新编译 nginx。 - - -## 扩展 - -编辑 `sitelist.txt` 文件,可配置站点别名,格式为 `别名 主机名`。配置完成后需要执行 `build.sh` 更新。 - -执行 `./server/run.sh reload` 重启服务。(该命令的参数和 nginx -s 意义一样,当然也可以自己管理 nginx 服务) - -访问 `https://别名.example.com` 即可进入相应站点。 - -由于 HTTPS 证书不支持多级通配,所以别名数量是有限的(好像 acme.sh 只支持 30 几个) - -对于普通的域名,例如 `www.host.com` 则转换成 `www-dot-host-dot-com.example.com` 的格式,即 `.` 变成 `-dot-`。(原本就有 `-dot-` 字符的域名暂未考虑) - - -# 功能特点 - -## 性能开销 - -本代理主要功能都运行在客户端,最大程度减少服务端计算量。前端通过 `Service Worker` 拦截和处理资源,同时注入一个 JS 到页面顶部,实现一些辅助功能。 - -服务端则非常简单,直接利用 nginx 反向代理功能,并且不修改内容(只修改 HTTP 头),避免处理内容的开销,以及原始数据解压再压缩的开销(或者不压缩时流量开销)。 - -例如现在流行的 br 压缩,压缩比高但压缩成本很大。因此让代理服务器只转发而不操作数据,可节省大量资源。 - - -## 域名模型 - -本代理将不同的目标站点作为独立的子域名,例如: - -```text -so.jsproxy.tk => stackoverflow.com -gk.jsproxy.tk => www.google.com.hk +```bash +yum install -y \ + gcc gcc-c++ \ + pcre pcre-devel \ + openssl openssl-devel \ + zlib zlib-devel ``` -这在一定程度上隔离了站点之间的数据,例如 Cookie、Storage 等。 +## 测试 -该模型支持目标站点子域和主域 Cookie 共存: +启动服务: -![](docs/sub-root-cookie.png) +```bash +./server/run.sh +``` -另外页面中的辅助脚本,也会对部分 DOM API 进行重写,模拟一个沙盒环境。 +访问:https://etherdream.github.io/jsproxy-localtest/-----https://github.com/ -例如脚本设置 Cookie 时,会触发钩子程序对赋值进行调整: +![](https://raw.githubusercontent.com/EtherDream/jsproxy-localtest/temp/preview.png) -![](docs/js-set-cookie.png) +注意,**当前项目只提供接口服务**,浏览器端脚本和页面不在本项目。这样做是为了让接口和界面分离,意义参见后续。 -类似的还有: -![](docs/domain-model.png) +# 部署 -使得代理对页面尽可能保持透明。 +参考 `gen-cert` 目录,为自己的域名申请证书,然后修改 `nginx.conf` 中域名相关的配置(默认被注释),以及 DNS 地址(默认是 114.114.114.114)。 +浏览器端项目位于:https://github.com/EtherDream/jsproxy-browser -## 路径修正 +参考备注,修改服务器域名,之后将 www 目录发布到 Web 空间即可。 -前端脚本会对资源、超链接、表单、弹窗的 URL 进行修正。 +(目前还不完善,之后将实现动态配置,无需修改 JS 代码) -后端代理会对请求头的 `Referer`、`Origin` 字段进行修正,减少被拦截的可能。 -![](docs/login1.png) +# 安全策略 -![](docs/login2.png) +如果不希望代理访问内网,可执行 `setup-ipset.sh`,避免 SSRF 风险。 -目前测试了 GitHub、Twitter 可以登陆,Google 登陆还有一些问题。 +该脚本可禁止 `jsporxy` 用户访问内网(针对 TCP)。nginx 之外的程序也生效,但不影响其他用户。 -当然请不要在测试服务器里输入隐私数据。 +# 服务管理 -# 存在问题 +重启服务:`./run.sh reload` -该代理目前仍存在较多问题,主要有: +关闭服务:`./run.sh quit` -## 普通域名模式没有子域 +参数和 nginx -s 相同。 -由于 `www-dot-host-dot-com.example.com` 并非 `host-dot-com.example.com` 的子域,因此这种模式下 cookie 和 domain 都无法支持域模型。 -未来可能会尝试把所有站点都放在同个域名下,例如 `https://example.com/host.com/path/to`,这样就无需考虑域名的问题。当然这种方案需要重写更多的 API 以确保数据隔离,甚至还要自己维护 cookie 的携带,难度比较大。 +# CHANGELOG +## v0.0.1 -## location hook +虽然目前仍为概念演示状态,但相比最初版本,有了很大变化: -由于 `window` 和 `document` 对象的 `location` 属性无法重写,导致很多网站的脚本在读写路径时会出问题。 +* 不再使用二级域名 -目前在代码层解决这个问题:通过 Service Worker 以及 API 钩子拦截 JS 代码,然后将其中的 `location` 字符串替换成 `__location`,从而将操作转到我们的对象上。 +由于二级域名的方案缺陷太多,例如 HTTPS 证书问题,DNS 性能和安全问题等,最终不再使用二级域名,而是只用单个域名,目标 URL 放在路径里。例如: -由于这种方式简单粗暴,有时会把正则、字符串、属性名的 location 也替换了,导致代码出现问题。因此之后会尝试在 AST 层面进行调整,当然缺点是比较耗时,尤其对于很大的 JS。 +https://zjcqoo.github.io/-----https://www.google.com -当然,如果 `location` 不是字面出现的,比如 `obj[key]` 形式,那么这种方案仍不可行,除非调整 `window` 和 `document`。但它们也可以不通过字面获取,例如通过 `this` 也可以获取 `window`,更别提 `eval` 等等。。。所以网站本身若真想访问 `location`,我们还是很难阻止的。 +当然这也会产生很多新问题,例如无法支持 Cookie 等、页面之前没有同源策略限制等。 -因此这里给 Web 开发者一个建议:如果想检测当前页面 URL 是否为钓鱼网站,最好不要出现字面量的 `window`、`location` 获取 URL,而是通过动态的方式进行获取,以防落入上述这种低级的陷阱。 +对于 Cookie 问题,目前通过 JS 来维护,而不用浏览器原生的(当然还有不少细节没实现)。这样的好处是前后端可以分离,前端的页面可以放在 CDN、GitHub Pages 上,我们的服务器只提供代理接口。 +这样一个页面可使用多个服务器,实现线路实时切换、负载均衡等效果。 -## 多进程问题 -由于 Service Worker 无法拦截第三方站点的框架页,因此会出现 iframe 逃脱代理的情况。 +* 服务端优化 -目前尝试对框架元素的 `src` 属性进行拦截,同时监控 DOM 创建事件,将新增的框架调整成我们的 URL。当然这里面还涉及到 `about:`、`blob:`、`data:` 等协议,会有些麻烦,暂未实现。 +安全改进:由于 Web 页面托管在第三方站点上,自己的服务器无需开启 443 端口,因此也无需 root 运行。同时支持 IP 黑名单功能,防止 SSRF 攻击。 -另外新创建的 `Worker`、`SharedWorker` 暂时也没有注入辅助 JS 代码,还在调研中。 +代码改进:接口代理使用固定的 URL(`/http` 和 `/ws`),不再使用任意路径,代码干净了很多。 -至于业务方的 `ServiceWorker`,目前是直接拒绝其使用的,因为这会和代理本身的 `ServiceWorker` 冲突。以后再调研两者是否能较好的共存。 +* 提供一个首页 -## 很多地方需要优化 - -由于目前还只是个概念验证的状态,很多代码都是临时写的,之后稳定了再重构和完善。 - -另外测试案例也没有,估计有一大堆 BUG 还没发现。 - - -# 优化探索 - -YY 一些优化方案,以后有时间探索。 - -## 流量的优先级 - -因为我们是在前端拦截流量,所以能了解每个请求的具体用途,从而可更好的设置优先级。例如在流量压力较大时,优先满足网页、脚本等流量,推迟视频、动画等流量,确保主要功能不受影响。 - -## 脚本离线分析 - -由于前端修改 JS 比较耗性能,因此可事先把各大网站的常用脚本在本地分析,然后上传到 nginx 缓存里。这样浏览器就不需要实时计算了,可以大幅降低开销。并且离线分析可以更加深入,对于动态访问 `location` 的情况也能覆盖到,甚至完全不局限于修改 `location` 的功能,而是更通用的调整,例如去广告,增加其他功能等等。 - -另外对于常用的内联脚本,也可将离线分析结果进行下发,浏览器运行时只需简单查表,避免大量在线计算。 - -## 资源本地加速 - -进一步,还考虑可以把常用网站的静态资源预先下回本地,部署到附近的 CDN 上,或者 [打包成图片上传到各大免费图床、相册里](https://yq.aliyun.com/articles/236582),提供给 Service Worker 更快的访问通道。这样可大幅加快网站访问速度,并且节省代理服务器的流量! - -## 缓存重新压缩 - -虽然大部分网站都开启了 HTTP 传输压缩,并且不少支持 br 格式,但考虑到压缩成本,很多网站并没有将压缩率调到最大。而我们的代理服务器,显然也不会为了节省那么一点流量,牺牲大量 CPU 去做解压和压缩。 - -但是,这个过程可以离线去做,尤其对于那些 CPU 过剩而流量紧缺的服务器。我们可将空闲时的 CPU 资源用于 nginx cache 最高级 br 压缩。甚至还可以对非 CORS 请求的图片进行更高程度的压缩,并且转换成 WebP 格式,进一步降低流量开销。 - -## 动态数据压缩 - -有些网页内容很大却关闭了缓存,例如 google 首页,每次访问都要重新下载一次,浪费不少流量。但是让代理服务器强制缓存也是不行的,因为页面里可能包含了用户信息,缓存的话就会串号导致隐私问题。 - -然而这些页面的绝大部分都是相同的,每次重复传输实属不必。因此,我们可预先分析出那些不涉及隐私的公共子串,将其部署在本地 CDN 上。代理在返回数据时,重复部分用索引代替,从而可减少传输流量,提高访问速度。更进一步,甚至可以尝试把网页反推回模板,这样只需传输模板变量就可以! - -对于那些流量接收免费、发送计费的服务器来说,这是个值得考虑的优化方案。 - - -# 初衷 - -春节期间由于家里电脑上不了 google 很是不爽,平时用惯了公司自带的科学上网,好久没维护自己的都不能用了,于是一气之下写了这个程序。 - -其实很久以前也尝试过类似的,但都十分简陋。这次决定做个完善的,充分用上浏览器的新技术和黑魔法,顺便再熟悉下 nginx 的技术细节。 - -当然制作过程并不顺利,遇到各种问题。因此先实现了一个简单的 google 代理,之后的问题就可以通过它解决了。于是用「开发中的代理」搜索「代理开发中」遇到的问题,然后不断改进。或许这就叫自举😂 - -尽管折腾了整个春节,但毕竟不是寒假才几天时间,所以仍是个半成品。不过用来浏览常见的编程网站是没问题的,甚至还能刷推看视频。 - -当然要做到完善还需不少时间,暂时先分享个半成品吧~ - - -# 后续 - -之后还会将它用于以下技术的研究: - -* 网站镜像 / 沙盒化 - -* 钓鱼网站攻防检测 - -* 资源访问端上加速 - -当然请勿将本项目用于访问非法用途,否则后果自负。 +虽然依旧简陋,但比之前好。 # License diff --git a/allowed-sites.txt b/allowed-sites.txt new file mode 100644 index 0000000..d2f3503 --- /dev/null +++ b/allowed-sites.txt @@ -0,0 +1,4 @@ +# no pathname +http://localhost 1; +https://etherdream.github.io 1; +https://zjcqoo.github.io 1; \ No newline at end of file diff --git a/api.conf b/api.conf new file mode 100644 index 0000000..f9823bf --- /dev/null +++ b/api.conf @@ -0,0 +1,161 @@ +if ($_origin_allowed = '') { + return 404 'ERROR: origin `$http_origin` is not allowed'; +} +if ($http_x_jsproxy) { + return 404 'ERROR: circular dependency'; +} +proxy_set_header x-jsproxy 1; +proxy_set_header Connection $http_connection; + +set $_url ''; +set $_ver ''; + + +location = /preflight { + internal; + more_set_headers + 'access-control-allow-origin: *' + 'access-control-allow-methods: GET,POST,PUT,DELETE,HEAD,OPTIONS' + 'access-control-allow-headers: --url,--referer,--cookie,--origin,--ext,--aceh,--ver,accept,accept-charset,accept-encoding,accept-language,accept-datetime,authorization,cache-control,content-length,content-type,date,if-match,if-modified-since,if-none-match,if-range,if-unmodified-since,max-forwards,pragma,range,te,upgrade,upgrade-insecure-requests,x-requested-with,chrome-proxy' + 'access-control-max-age: 1728000' + ; + return 204; +} + + +# HTTP(S) Proxy +location = /http { + if ($request_method = 'OPTIONS') { + rewrite ^ /preflight; + } + + # decode req headers + access_by_lua_block { + local hdrs, err = ngx.req.get_headers() + local extHdrs + + for k, v in pairs(hdrs) do + if k:sub(1, 2) ~= '--' then + goto continue + end + + ngx.req.clear_header(k) + k = k:sub(3) + + if k == 'url' then + ngx.var._url = v + elseif k == 'ver' then + ngx.var._ver = v + elseif k == 'aceh' then + ngx.ctx._aceh = 1 + elseif k == 'ext' then + extHdrs = require('cjson').decode(v) + else + ngx.req.set_header(k, v) + end + + ::continue:: + end + + if extHdrs then + for k, v in pairs(extHdrs) do + ngx.req.set_header(k, v) + end + end + } + + proxy_cache my_cache; + proxy_pass $_url; + + more_set_headers + 'server: $upstream_http_server' + 'content-security-policy' + 'content-security-policy-report-only' + 'x-frame-options' + ; + + # encode res headers + header_filter_by_lua_block { + local expose = '*' + local detail = (ngx.ctx._aceh == 1) + local vary = '--url' + + local h, err = ngx.resp.get_headers() + for k, v in pairs(h) do + if + -- headers to escape -- + k == 'access-control-allow-origin' or + k == 'access-control-expose-headers' or + k == 'location' or + k == 'set-cookie' + then + if type(v) == 'table' then + for i = 1, #v do + local x = i .. '-' .. k + ngx.header[x] = v[i] + + if detail then + expose = expose .. ',' .. x + end + end + else + local x = '--' .. k + ngx.header[x] = v + + if detail then + expose = expose .. ',' .. x + end + end + ngx.header[k] = nil + + elseif k == 'vary' then + if type(v) == 'table' then + vary = vary .. ', ' .. table.concat(v, ', ') + else + vary = vary .. ', ' .. v + end + + elseif detail and + -- not simple header -- + k ~= 'cache-control' and + k ~= 'cache-language' and + k ~= 'content-type' and + k ~= 'expires' and + k ~= 'last-modified' and + k ~= 'pragma' + then + expose = expose .. ',' .. k + end + end + + if detail then + expose = expose .. ',--s' + ngx.header['--t'] = '1' + end + + ngx.header['access-control-expose-headers'] = expose + ngx.header['access-control-allow-origin'] = '*' + ngx.header['vary'] = vary + ngx.header['--s'] = ngx.status + ngx.status = 200 + } +} + + +# WebSocket Proxy +location = /ws { + access_by_lua_block { + local query, err = ngx.req.get_uri_args() + + for k, v in pairs(query) do + if k == 'url__' then + ngx.var._url = v + elseif k == 'ver__' then + ngx.var._ver = v + else + ngx.req.set_header(k, v) + end + end + } + proxy_pass $_url; +} diff --git a/browser/home/index.html b/browser/home/index.html deleted file mode 100644 index 4b05112..0000000 --- a/browser/home/index.html +++ /dev/null @@ -1,27 +0,0 @@ - - - - Page Sandbox Demo - - - - - -

网页沙盒

-
- URL: - - -
- - - - \ No newline at end of file diff --git a/browser/proxy/debug.sh b/browser/proxy/debug.sh deleted file mode 100755 index 445c25f..0000000 --- a/browser/proxy/debug.sh +++ /dev/null @@ -1,3 +0,0 @@ -JS=../../server/www/x.js -rm -f $JS.br -webpack -w -o $JS --mode development \ No newline at end of file diff --git a/browser/proxy/package.json b/browser/proxy/package.json deleted file mode 100644 index e8f0025..0000000 --- a/browser/proxy/package.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "name": "jsproxy-client", - "version": "0.0.1", - "description": "", - "main": "boot.js", - "directories": { - "lib": "lib" - }, - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" - }, - "keywords": [], - "author": "EtherDream", - "license": "MIT" -} diff --git a/browser/proxy/release.sh b/browser/proxy/release.sh deleted file mode 100755 index 2a24274..0000000 --- a/browser/proxy/release.sh +++ /dev/null @@ -1,2 +0,0 @@ -webpack --mode production -brotli -f -o ../../server/www/x.js.br dist/main.js diff --git a/browser/proxy/src/fakeloc.js b/browser/proxy/src/fakeloc.js deleted file mode 100644 index 85b607a..0000000 --- a/browser/proxy/src/fakeloc.js +++ /dev/null @@ -1,158 +0,0 @@ -import * as urlx from "./urlx"; - - -/** - * @param {string} url - */ -function decOrigin(url) { - const u = new URL(url) - urlx.decUrlObj(u) - return u.origin -} - -function setup(obj, fakeLoc) { - Reflect.defineProperty(obj, '__location', { - get() { - return fakeLoc - }, - set(val) { - console.log('[jsproxy] %s set location: %s', obj, val) - fakeLoc.href = val - } - }) -} - -/** - * @param {Window} win - */ -export function init(win) { - let loc = win.location - - // TODO: iframe 场合下存在问题 - // 比如 youtube 首页缺少这个判断会报错 - if (loc.href === 'about:blank') { - loc = win.top.location - } - - const fakeLoc = Object.setPrototypeOf({ - get href() { - // console.log('[jsproxy] get location.href') - return urlx.decUrlStr(loc.href) - }, - - get protocol() { - // TODO: 未考虑非 https 的页面 URL - return loc.protocol - }, - - get host() { - // TODO: 未考虑带端口的页面 URL - // console.log('[jsproxy] get location.host') - return urlx.decHost(loc.host) - }, - - get hostname() { - // console.log('[jsproxy] get location.hostname') - return urlx.decHost(loc.hostname) - }, - - get port() { - // TODO: 未考虑带端口的页面 URL - return loc.port - }, - - get pathname() { - return loc.pathname - }, - - get search() { - return loc.search - }, - - get hash() { - return loc.hash - }, - - get origin() { - // console.log('[jsproxy] get location.origin') - return decOrigin(loc.origin) - }, - - get ancestorOrigins() { - // TODO: DOMStringList[] - // console.log('[jsproxy] get location.ancestorOrigins') - return [...loc.ancestorOrigins].map(decOrigin) - }, - - set href(val) { - console.log('[jsproxy] set location.href:', val) - loc.href = urlx.encUrlStr(val, loc) - }, - - set protocol(val) { - const u = new URL(loc) - // TODO: - }, - - set host(val) { - console.log('[jsproxy] set location.host:', val) - // TODO: - }, - - set hostname(val) { - console.log('[jsproxy] set location.hostname:', val) - loc.hostname = urlx.encHost(val) - }, - - set port(val) { - console.log('[jsproxy] set location.port:', val) - // TODO: - }, - - set pathname(val) { - loc.pathname = val - }, - - set search(val) { - loc.search = val - }, - - set hash(val) { - loc.hash = val - }, - - reload() { - loc.reload(...arguments) - }, - - replace(val) { - if (val) { - console.log('[jsproxy] location.replace:', val) - arguments[0] = urlx.encUrlStr(val, loc) - } - loc.replace(...arguments) - }, - - assign(val) { - if (val) { - console.log('[jsproxy] location.assign:', val) - arguments[0] = urlx.encUrlStr(val, loc) - } - loc.assign(...arguments) - }, - - toString() { - const val = loc.toString(...arguments) - return urlx.decUrlStr(val) - }, - - toLocaleString() { - const val = loc.toLocaleString(...arguments) - return urlx.decUrlStr(val) - }, - }, loc.constructor.prototype) - - - setup(win, fakeLoc) - setup(win.document, fakeLoc) -} diff --git a/browser/proxy/src/hook.js b/browser/proxy/src/hook.js deleted file mode 100644 index 8d14e40..0000000 --- a/browser/proxy/src/hook.js +++ /dev/null @@ -1,247 +0,0 @@ -export const RETURN = {} - -const { - apply, - getOwnPropertyDescriptor, - defineProperty, -} = Reflect - -const rawMap = new WeakMap() - - -/** - * @param {Window} win - */ -export function createHook(win) { - /** - * hook function - * - * @param {object} obj - * @param {string} key - * @param {Function} factory - */ - function func(obj, key, factory) { - const oldFn = obj[key] - if (!oldFn) { - return false - } - const newFn = factory(oldFn) - for (const k in oldFn) { - newFn[k] = oldFn[k] - } - newFn.prototype = oldFn.prototype - rawMap.set(newFn, oldFn) - obj[key] = newFn - return true - } - - /** - * hook property - * - * @param {object} obj - * @param {string} key - * @param {Function} g - * @param {Function} s - */ - function prop(obj, key, g, s) { - const desc = getOwnPropertyDescriptor(obj, key) - if (!desc) { - return false - } - if (g) { - func(desc, 'get', g) - } - if (s) { - func(desc, 'set', s) - } - defineProperty(obj, key, desc) - return true - } - - - function hookElemProp(proto, name, onget, onset) { - prop(proto, name, - getter => function() { - const val = getter.call(this) - return onget.call(this, val) - }, - setter => function(val) { - val = onset.call(this, val) - if (val == RETURN) { - return - } - setter.call(this, val) - } - ) - } - - const toLCase = ''.toLocaleLowerCase - const elemProto = win.Element.prototype - const rawGetAttr = elemProto.getAttribute - const rawSetAttr = elemProto.setAttribute - - const tagAttrHandlersMap = {} - const tagTextHandlerMap = {} - const tagKeySetMap = {} - const tagKeyGetMap = {} - - - function attr(tag, proto, ...handlers) { - let hasBind, hasAttr - let keySetMap, keyGetMap - - handlers.forEach(v => { - // 带划线的 attr 属性名,转换成驼峰形式的 prop 属性名。 - // 例如 `http-equiv` -> `httpEquiv` - const prop = v.name.replace(/-(\w)/g, - (_, char) => char.toUpperCase() - ) - hookElemProp(proto, prop, v.onget, v.onset) - - // #text - if (prop === 'innerText') { - tagTextHandlerMap[tag] = v - return - } - - // attribute - if (tagAttrHandlersMap[tag]) { - tagAttrHandlersMap[tag].push(v) - hasBind = true - } else { - tagAttrHandlersMap[tag] = [v] - tagKeySetMap[tag] = {} - tagKeyGetMap[tag] = {} - } - - if (!keySetMap) { - keySetMap = tagKeySetMap[tag] - keyGetMap = tagKeyGetMap[tag] - } - const key = toLCase.call(v.name) - keySetMap[key] = v.onset - keyGetMap[key] = v.onget - hasAttr = true - }) - - if (hasBind || !hasAttr) { - return - } - - // 如果之前调用过 setAttribute,直接返回上次设置的值; - // 如果没有调用过,则返回 onget 的回调值。 - func(proto, 'getAttribute', oldFn => function(name) { - const key = toLCase.call(name) - const onget = keyGetMap[key] - if (!onget) { - return apply(oldFn, this, arguments) - } - - const lastVal = this['_k' + key] - if (lastVal !== undefined) { - return lastVal - } - const val = apply(oldFn, this, arguments) - return onget(val) - }) - - func(proto, 'setAttribute', oldFn => function(name, val) { - const key = toLCase.call(name) - const onset = keySetMap[key] - if (onset) { - this['_k' + key] = val - - const ret = onset.call(this, val) - if (ret === RETURN) { - return - } - arguments[1] = ret - } - return apply(oldFn, this, arguments) - }) - - // TODO: setAttributeNode - // ... - } - - /** - * @param {Text} node - * @param {object} handler - * @param {Element} elem - */ - function parseNewTextNode(node, handler, elem) { - const val = node.nodeValue - const ret = handler.onset.call(elem, val) - if (ret === RETURN) { - return - } - node.nodeValue = ret - } - - /** - * @param {Element} elem - * @param {object} handler - */ - function parseNewElemNode(elem, handler) { - const name = handler.name - if (!elem.hasAttribute(name)) { - return - } - const val = rawGetAttr.call(elem, name) - const ret = handler.onset.call(elem, val) - if (ret === RETURN) { - return - } - rawSetAttr.call(elem, name, ret) - } - - /** - * @param {MutationRecord[]} mutations - */ - function parseMutations(mutations) { - mutations.forEach(mutation => { - mutation.addedNodes.forEach(node => { - switch (node.nodeType) { - case 1: // ELEMENT_NODE - const handlers = tagAttrHandlersMap[node.tagName] - handlers && handlers.forEach(v => { - parseNewElemNode(node, v) - }) - break - case 3: // TEXT_NODE - const elem = node.parentElement - if (elem) { - const handler = tagTextHandlerMap[elem.tagName] - if (handler) { - parseNewTextNode(node, handler, elem) - } - } - break - } - }) - }) - } - - - const observer = new win.MutationObserver(parseMutations) - observer.observe(win.document, { - childList: true, - subtree: true, - }) - - // win.addEventListener('DOMContentLoaded', e => { - // parseMutations(observer.takeRecords()) - // observer.disconnect() - // }) - - // hide source code - func(win.Function.prototype, 'toString', oldFn => function() { - return apply(oldFn, rawMap.get(this) || this, arguments) - }) - - return { - func, - prop, - attr, - } -} \ No newline at end of file diff --git a/browser/proxy/src/hostlist.js b/browser/proxy/src/hostlist.js deleted file mode 100644 index b7ae6d0..0000000 --- a/browser/proxy/src/hostlist.js +++ /dev/null @@ -1,37 +0,0 @@ -// THIS FILE WAS GENERATED BY build.sh -// DO NOT MODIFY -export const MY_ROOT = 'jsproxy.tk' -export const HOST_LIST = [ - ['gg', 'google.com'], - ['gc', 'google.cn'], - ['gk', 'google.com.hk'], - ['gu', 'googleusercontent.com'], - ['gs', 'googlesource.com'], - ['wk', 'wikipedia.org'], - ['m.wk', 'm.wikipedia.org'], - ['so', 'stackoverflow.com'], - ['se', 'stackexchange.com'], - ['sf', 'serverfault.com'], - ['su', 'superuser.com'], - ['au', 'askubuntu.com'], - ['gh', 'github.com'], - ['qr', 'quora.com'], - ['ux', 'unix.com'], - ['mz', 'mozilla.org'], - ['w3', 'w3schools.com'], - ['cr', 'chromium.org'], - ['my', 'myspace.com'], - ['fb', 'facebook.com'], - ['yt', 'youtube.com'], - ['tw', 'twitter.com'], - ['fl', 'flickr.com'], - ['rd', 'reddit.com'], - ['bg', 'blogger.com'], - ['wp', 'wordpress.com'], - ['md', 'medium.com'], - ['hn', 'hackernoon.com'], - ['yh', 'yahoo.com'], - ['bc', 'bbc.com'], - ['th', 'twitch.tv'], - ['sc', 'steamcommunity.com'], -] diff --git a/browser/proxy/src/index.js b/browser/proxy/src/index.js deleted file mode 100644 index 2dc11a8..0000000 --- a/browser/proxy/src/index.js +++ /dev/null @@ -1,13 +0,0 @@ -function main() { - if ('onclick' in self) { - // page env - return require('./page.js') - } - if ('onfetch' in self) { - // sw env - return require('./sw.js') - } - return require('./worker.js') -} - -main() \ No newline at end of file diff --git a/browser/proxy/src/inject.js b/browser/proxy/src/inject.js deleted file mode 100644 index 49a6a45..0000000 --- a/browser/proxy/src/inject.js +++ /dev/null @@ -1,77 +0,0 @@ -import * as urlx from "./urlx"; -import * as util from './util.js' -import * as jsfilter from './jsfilter.js' - - -const RES_HOST = urlx.getMyRootHost() -const HELPER_URL = `//${RES_HOST}/x.js` - -// 为了简化注入位置的分析,这里直接插到 HTML 开头 -// 所以页面里会出现两个 -const HTML_BEG = util.strToBytes( - `` -) - -// Worker -const WORKER_BEG = util.strToBytes( - `importScripts('${HELPER_URL}');` -) - - -/** - * @param {Response} res - * @param {Object} resOpt - */ -export function htmlRemote(res, resOpt) { - const reader = res.body.getReader() - let injected - - const stream = new ReadableStream({ - async pull(controller) { - if (!injected) { - injected = true - controller.enqueue(HTML_BEG) - } - const r = await reader.read() - if (r.done) { - controller.close() - return - } - controller.enqueue(r.value) - } - }) - return new Response(stream, resOpt) -} - - -// 处理 data、blob 协议的页面 -export function htmlLocal(uri) { - // TODO: -} - - -/** - * @param {Response} res - * @param {Object} resOpt - */ -export async function jsRemote(res, resOpt, charset) { - // 之后会分析语法树,所以不使用流模式 - const buf = await res.arrayBuffer() - const ret = await jsfilter.parseBin(buf, charset) - if (ret) { - resOpt.headers = new Headers(resOpt.headers) - resOpt.headers.set('content-type', 'text/javascript') - } - return new Response(ret || buf, resOpt) -} - - -export function workerRemote(res, resOpt, charset) { - // TODO: -} - - -// 处理 data、blob 协议的 Worker -export function workerLocal(data) { - // TODO: -} \ No newline at end of file diff --git a/browser/proxy/src/jsfilter.js b/browser/proxy/src/jsfilter.js deleted file mode 100644 index 9c555d1..0000000 --- a/browser/proxy/src/jsfilter.js +++ /dev/null @@ -1,31 +0,0 @@ -import * as util from './util.js' - - -/** - * @param {string} code - */ -export function parseSync(code) { - // TODO: parse js ast - let match - code = code.replace(/(\b)location(\b)/g, (s, $1, $2) => { - match = true - return $1 + '__location' + $2 - }) - if (match) { - return code - } -} - -/** - * @param {Uint8Array} buf - */ -export async function parseBin(buf, charset) { - const str = util.bytesToStr(buf, charset) - const ret = parseSync(str) - if (ret) { - return util.strToBytes(ret) - } - if (!util.isUtf8(charset)) { - return util.strToBytes(str) - } -} \ No newline at end of file diff --git a/browser/proxy/src/nav.js b/browser/proxy/src/nav.js deleted file mode 100644 index cf2ed56..0000000 --- a/browser/proxy/src/nav.js +++ /dev/null @@ -1,171 +0,0 @@ -import * as urlx from './urlx.js'; - -/** - * page navigate intercept - * - * @param {Window} win - * @param {Hook} hook - */ -export function init(win, hook) { - const { - location, - Reflect, - } = win - - const { - apply, - } = Reflect - - const linkProto = win.HTMLAnchorElement.prototype - const areaProto = win.HTMLAreaElement.prototype - const formProto = win.HTMLFormElement.prototype - - function hookNavAttr(tag, proto, name) { - hook.attr(tag, proto, { - name, - onget(val) { - const u = new URL(val, location) - urlx.unpack(u) - return u.href - }, - onset(val) { - const u = new URL(val, location) - urlx.pack(u, false, false) - return u.href - } - }) - } - hookNavAttr('A', linkProto, 'href') - hookNavAttr('AREA', areaProto, 'href') - hookNavAttr('FORM', formProto, 'action') - - - // TODO: - function hookLinkProp(proto) { - hook.prop(proto, 'hostname', - getter => function() { - const val = getter.call(this) - return val - }, - setter => function(val) { - console.log('[jsproxy] set link hostname:', val) - setter.call(this, val) - } - ) - - hook.prop(proto, 'host', - getter => function() { - const val = getter.call(this) - return val - }, - setter => function(val) { - console.log('[jsproxy] set link host:', val) - setter.call(this, val) - } - ) - - hook.prop(proto, 'protocol', - getter => function() { - const val = getter.call(this) - return val - }, - setter => function(val) { - console.log('[jsproxy] set link protocol:', val) - setter.call(this, val) - } - ) - - hook.prop(proto, 'port', - getter => function() { - const val = getter.call(this) - return val - }, - setter => function(val) { - console.log('[jsproxy] set link port:', val) - setter.call(this, val) - } - ) - - hook.prop(proto, 'search', - getter => function() { - const val = getter.call(this) - return val - }, - setter => function(val) { - console.log('[jsproxy] set link search:', val) - setter.call(this, val) - } - ) - } - hookLinkProp(linkProto) - hookLinkProp(areaProto) - - /** - * @param {HTMLAnchorElement | HTMLAreaElement | HTMLFormElement} el - * @param {string} prop - */ - function processElem(el, prop) { - const urlStr = el[prop] - if (urlStr) { - el[prop] = urlStr - } - } - - function linkClickHook(oldFn) { - return function() { - processElem(this, 'href') - return apply(oldFn, this, arguments) - } - } - hook.func(linkProto, 'click', linkClickHook) - hook.func(areaProto, 'click', linkClickHook) - hook.func(formProto, 'submit', oldFn => function() { - processElem(this, 'action') - return apply(oldFn, this, arguments) - }) - - - // hook window.open() - hook.func(win, 'open', oldFn => function(url) { - if (url) { - const u = new URL(url, location) - urlx.pack(u, false, false) - arguments[0] = u.href - } - return apply(oldFn, this, arguments) - }) - - - // - // hook - // - const baseProto = win.HTMLBaseElement.prototype - - hook.attr('BASE', baseProto, { - name: 'href', - onget(val) { - return urlx.decUrlStr(val) - }, - onset(val) { - // console.log('[jsproxy] set base.href:', val) - // val = getFinalUrl(val) - return urlx.encUrlStr(val, location) - } - }) - - // - // hook - // - const metaProto = win.HTMLMetaElement.prototype - - hook.attr('META', metaProto, { - name: 'http-equiv', - onget(val) { - // TODO: - return val - }, - onset(val) { - return val - } - }) -} \ No newline at end of file diff --git a/browser/proxy/src/page.js b/browser/proxy/src/page.js deleted file mode 100644 index 0670681..0000000 --- a/browser/proxy/src/page.js +++ /dev/null @@ -1,393 +0,0 @@ -import {createHook} from './hook.js' -import * as urlx from './urlx.js' -import * as util from './util.js' -import * as nav from './nav.js' -import * as jsfilter from './jsfilter.js' -import * as fakeloc from './fakeloc.js' - - -/** - * @param {Window} win - */ -function initWin(win) { - if (!win) { - return - } - try { - if (win.Math.__flag) { - return // setuped - } - win.Math.__flag = 1 - } catch (err) { - return // not same origin - } - - const { - // WeakSet, - // Reflect, - // RegExp, - // URL, - // Proxy, - document, - location, - navigator, - } = win - - const { - apply, - construct, - } = Reflect - - const isExtPageMode = urlx.isMyExtHost(location.hostname) - - // hook Function - // hook.func(window, 'Function', oldFn => function() { - // return apply(oldFn, this, arguments) - // }) - - const hook = createHook(win) - nav.init(win, hook) - - // hook window/document.location - fakeloc.init(win, hook) - - - // hook document.domain - const docProto = win.Document.prototype - - hook.prop(docProto, 'domain', - getter => function() { - const val = getter.call(this) - return urlx.decHost(val) - }, - setter => function(val) { - if (isExtPageMode) { - console.warn('[jsproxy] unsafe domain') - val = urlx.getMyExtHost() - } else { - val = urlx.encHost(val) - } - setter.call(this, val) - } - ) - - // hook document.cookie - const R_COOKIE_DOMAIN = /(?<=;\s*domain=)[^;]+/i - - hook.prop(docProto, 'cookie', null, - setter => function(val) { - val = val.replace(R_COOKIE_DOMAIN, rHost => { - if (isExtPageMode) { - return '' - } - if (rHost[0] === '.') { - rHost = rHost.substr(1) - } - const vHost = urlx.encHost(rHost) - if (urlx.isMyRootHost(vHost)) { - console.warn('[jsproxy] invalid cookie domain:', rHost, vHost) - } - return vHost - }) - setter.call(this, val) - } - ) - - // uri api - function getUriHook(getter) { - return function() { - const val = getter.call(this) - return urlx.decUrlStr(val) - } - } - hook.prop(docProto, 'referrer', getUriHook) - hook.prop(docProto, 'URL', getUriHook) - hook.prop(docProto, 'documentURI', getUriHook) - hook.prop(win.Node.prototype, 'baseURI', getUriHook) - - - // disable ServiceWorker - const swProto = win.ServiceWorkerContainer.prototype - if (swProto) { - hook.func(swProto, 'register', oldFn => function() { - console.warn('access serviceWorker.register blocked') - return new Promise(function() {}) - }) - hook.func(swProto, 'getRegistration', oldFn => function() { - console.warn('access serviceWorker.getRegistration blocked') - return new Promise(function() {}) - }) - hook.func(swProto, 'getRegistrations', oldFn => function() { - console.warn('access serviceWorker.getRegistrations blocked') - return new Promise(function() {}) - }) - } - - // - // hook history - // - function historyStateHook(oldFn) { - return function(_0, _1, url) { - if (url) { - arguments[2] = urlx.encUrlStr(url, location) - } - // console.log('[jsproxy] history.replaceState', url) - return apply(oldFn, this, arguments) - } - } - const historyProto = win.History.prototype - hook.func(historyProto, 'pushState', historyStateHook) - hook.func(historyProto, 'replaceState', historyStateHook) - - - // - hook.func(navigator, 'registerProtocolHandler', oldFn => function(_0, url, _1) { - console.log('registerProtocolHandler:', arguments) - return apply(oldFn, this, arguments) - }) - - - // hook Performance API - hook.prop(win.PerformanceEntry.prototype, 'name', getUriHook) - - // - // hook iframe - // - const iframeProto = win.HTMLIFrameElement.prototype - hook.prop(iframeProto, 'contentWindow', - getter => function() { - const win = getter.call(this) - initWin(win) - return win - } - ) - - hook.prop(iframeProto, 'contentDocument', - getter => function() { - const doc = getter.call(this) - if (doc) { - initWin(doc.defaultView) - } - return doc - } - ) - - - hook.attr('IFRAME', iframeProto, { - name: 'src', - onget(val) { - return urlx.decUrlStr(val) - }, - onset(val) { - val = urlx.encUrlStr(val, location) - console.log('[jsproxy] set