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