Full Code of EtherDream/jsproxy for AI

master be161ed8bbaf cached
37 files
48.3 KB
18.9k tokens
11 symbols
1 requests
Download .txt
Repository: EtherDream/jsproxy
Branch: master
Commit: be161ed8bbaf
Files: 37
Total size: 48.3 KB

Directory structure:
gitextract_nki4u8cg/

├── .gitignore
├── LICENSE
├── README.md
├── acme/
│   ├── .well-known/
│   │   └── acme-challenge/
│   │       └── test.txt
│   └── README.md
├── allowed-sites.conf
├── api.conf
├── cf-worker/
│   ├── .eslintrc.json
│   ├── README.md
│   └── index.js
├── changelogs/
│   ├── README.md
│   ├── v0.0.1.md
│   └── v0.1.0.md
├── docs/
│   ├── blogs/
│   │   └── js-hook.md
│   ├── cert-auto.md
│   ├── cert-manual.md
│   └── setup.md
├── i.sh
├── log-svc/
│   ├── README.md
│   ├── backup/
│   │   └── README.md
│   ├── backup.sh
│   └── svc.sh
├── log.conf
├── lua/
│   ├── http-body-hash.lua
│   ├── http-dec-req-hdr.lua
│   ├── http-enc-res-hdr.lua
│   └── ws-dec-req-hdr.lua
├── mime.types
├── nginx/
│   ├── .gitignore
│   └── logs/
│       └── .gitignore
├── nginx.conf
├── run.sh
├── setup-ipset.sh
├── test/
│   └── works.txt
├── www/
│   ├── .gitignore
│   └── README.md
└── www.conf

================================================
FILE CONTENTS
================================================

================================================
FILE: .gitignore
================================================
._*

================================================
FILE: LICENSE
================================================
MIT License

Copyright (c) 2019 EtherDream

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


================================================
FILE: README.md
================================================

# 更新

* 2019-07-24 [v0.1.0](https://github.com/EtherDream/jsproxy/blob/master/changelogs/v0.1.0.md) 发布,主要修复了缓存失效的问题。网络接口和之前版本不兼容,请及时更新服务端和 cfworker。

* 2019-06-22 [cfworker 无服务器版](cf-worker) 发布,长期使用演示服务的请使用该版本。

[查看更多](changelogs)


# 安装

```bash
curl https://raw.githubusercontent.com/EtherDream/jsproxy/0.1.0/i.sh | bash
```

* 自动安装目前只支持 Linux x64,并且需要 root 权限

* 安装过程中 80 端口能被外网访问(申请 HTTPS 证书)

无法满足上述条件,或想了解安装细节,可尝试[手动安装](docs/setup.md)。

测试: `https://服务器IP.xip.io:8443`(具体参考脚本输出)


### 自定义域名

将域名 `example.com` 解析到服务器 IP,然后执行:

```bash
curl https://raw.githubusercontent.com/EtherDream/jsproxy/master/i.sh | bash -s example.com
```

访问: `https://example.com:8443`


### 自定义端口

默认端口为 8443 (HTTPS) 和 8080 (HTTP) ,如需改成 443 和 80,推荐使用端口转发:

```bash
iptables -A PREROUTING -t nat -p tcp --dport 443 -j REDIRECT --to-ports 8443
iptables -A PREROUTING -t nat -p tcp --dport 80 -j REDIRECT --to-ports 8080
```

同时修改 `www.conf` 中的 `:8443` 为 `:443`。


### 使用 GitHub Pages 前端

本项目支持前后端分离,前端部分(`www` 目录下的文件)可部署在第三方 Web 服务器上。

例如演示站点的前端部署于 GitHub Pages 服务,从而可使用个性域名(*.github.io),还能减少一定的流量开销。

Fork 本项目,进入 `gh-pages` 分支(该分支内容和 `www` 目录相同),编辑 `conf.js` 文件:

* 节点列表(`node_map` 字段,包括节点 id 和节点主机)

* 默认节点(`node_default` 字段,指定节点 id)

访问 `https://用户名.github.io/jsproxy` 预览。


# 维护

```sh
# 切换到 jsproxy 用户
su - jsproxy

# 重启服务
./run.sh reload

# 关闭服务(参数和 nginx -s 相同)
./run.sh quit

# 启动服务
./run.sh

# 查看代理日志
tail server/nginx/logs/proxy.log
```

目前暂未实现开机自启动。


# 禁止外链

默认情况下,代理接口允许所有 `github.io` 子站点调用,这可能导致不必要的流量消耗。

如果希望只给自己网站使用,可编辑 `allowed-sites.conf`。(重启服务生效)


# 安全策略

如果不希望代理访问内网(避免 SSRF 风险),可执行 `setup-ipset.sh`:

```bash
/home/jsproxy/server/setup-ipset.sh
```

> 需要 root 权限,依赖 `ipset` 命令

该脚本可禁止 `jsporxy` 用户访问保留 IP 段(针对 TCP)。nginx 之外的程序也生效,但不影响其他用户。


# 相关文章

* [基于 JS Hook 技术,打造最先进的在线代理](https://github.com/EtherDream/jsproxy/blob/master/docs/blogs/js-hook.md)


# 项目特点

相比传统在线代理,本项目具有以下特点:

## 服务端开销低

传统在线代理几乎都是在服务端替换 HTML/JS/CSS 等资源中的 URL。这不仅需要对内容做大量的分析和处理,还需对流量进行解压和再压缩,消耗大量 CPU 资源。并且由于逻辑较复杂,通常使用 Python/PHP 等编程语言自己实现。

为降低服务端开销,本项目使用浏览器的一个黑科技 —— Service Worker。它能让 JS 拦截网页产生的请求,并能自定义返回内容,相当于在浏览器内部实现一个反向代理。这使得绝大部分的内容处理都可以在浏览器上完成,服务器只需纯粹的转发流量。

因此本项目服务端直接使用 nginx,并且转发过程不修改内容(只修改 HTTP 头),避免了内容处理产生的巨大开销。同时得益于 nginx 丰富的功能,很多常用需求无需重新造轮子,通过简单配置即可实现。并且无论性能还是稳定性,都远高于自己实现。

## API 虚拟化

传统在线代理大多只针对静态 URL 的替换,忽视了动态 URL 以及和 URL 相关的网页 API。例如 a.com 反向代理 google.com,但页面中 JS 读取 `document.domain` 得到的仍是 a.com。这可能导致某些业务逻辑出现问题。

为缓解这个问题,本代理在页面头部注入一个 JS,用以重写绝大部分和 URL 相关的 API,使得页面中的 JS 获取到的仍是原始 URL:

![](https://raw.githubusercontent.com/EtherDream/jsproxy-localtest/temp/hook.png)

对于有些无法重写的 API,例如 `location`,本代理会将代码中字面出现的 `location` 替换成 `__location`,从而将操作转移到自定义对象上。当然对于非字面的情况(例如 `this['lo' + 'cation']`),目前还无法处理。


# 类似项目

目前找到的都是传统后端替换 URL 的方案。当然后端替换也有不少优点,例如浏览器兼容性高,甚至低版本的 IE 都可以使用。

## zmirror

GitHub: https://github.com/aploium/zmirror

## php-proxy

GitHub: https://github.com/jenssegers/php-proxy


# 项目意义

本项目主要用于以下技术的研究:

* 网站镜像 / 沙盒化

* 钓鱼网站检测技术

* 前端资源访问加速

当然请勿将本项目用于非法用途,否则后果自负。

Demo 页面文明使用,不要进行登陆等涉及隐私的操作。


# License

MIT


================================================
FILE: acme/.well-known/acme-challenge/test.txt
================================================
ok

================================================
FILE: acme/README.md
================================================
该目录存放 HTTPS 证书申请时的 challenge(由 acme.sh 写入),用于 Let's Encrypt 服务器的验证

================================================
FILE: allowed-sites.conf
================================================
#
# 授权哪些站点可使用本服务,防止外链
#
# 本服务会校验 HTTP 请求头 origin 字段,如果不在该列表,则拒绝代理
# 每个 URL 对应一个短别名,用于日志记录
# 注意 URL 不包含路径部分(结尾没有 /)
#
http://127.0.0.1                  '127';
http://127.0.0.1:8080             '127';
http://localhost                  'lo';
http://localhost:8080             'lo';

# 接口和网站同源,这种情况下 origin 为空
''                                'mysite';

# ~ 开头为正则匹配,此处允许 github.io 所有子站点
~^https://([\w-]+)\.github\.io$   'gh-$1';

# 允许任何站点使用
# ~(.*)                             '$1';


================================================
FILE: api.conf
================================================
set                   $_level     '';
set                   $_switched  '';
set                   $_url       '';
set                   $_ver       '';
set                   $_ref       '';
set                   $_type      '';
set                   $_mode      '';
set                   $_bodyhash  '';

error_page            500 502 504 /error;

location = /error {
  internal;
  access_log          off;
  more_set_headers
    'access-control-allow-origin: *'
    'access-control-expose-headers: gateway-err--'
    'gateway-err--: {"msg": "$arg_msg", "addr": "$upstream_addr", "url": "$arg_url"}'
  ;
  return              204;
}


location = /preflight {
  internal;
  access_log          off;
  more_set_headers
    'access-control-allow-origin: *'
    'access-control-allow-methods: GET,POST,PUT,PATCH,TRACE,DELETE,HEAD,OPTIONS'
    'access-control-max-age: 1728000'
  ;
  return              204;
}

# 该接口已作废
location = /http {
  access_log          off;
  more_set_headers    'access-control-allow-origin: *';
  return              200  "该接口已作废,请更新前端脚本";
}

# HTTP(S) Proxy
location /http/ {
  # see ./allowed-sites.conf
  if ($_origin_id = '') {
    rewrite             ^   /error?msg=ORIGIN_NOT_ALLOWED;
  }
  if ($http_x_jsproxy) {
    rewrite             ^   /error?msg=CIRCULAR_DEPENDENCY;
  }
  proxy_set_header      x-jsproxy   1;
  proxy_set_header      Connection  $http_connection;
  

  if ($http_access_control_request_methods) {
    rewrite             ^   /preflight;
  }

  access_by_lua_file    ../lua/http-dec-req-hdr.lua;

  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'
    'alt-svc'
    'clear-site-data'
  ;
  header_filter_by_lua_file   ../lua/http-enc-res-hdr.lua;
  body_filter_by_lua_file     ../lua/http-body-hash.lua;
}


# WebSocket Proxy
location = /ws {
  access_by_lua_file    ../lua/ws-dec-req-hdr.lua;
  proxy_set_header      Upgrade     $http_upgrade;
  proxy_set_header      Connection  $http_connection;
  proxy_pass            $_url;
}

================================================
FILE: cf-worker/.eslintrc.json
================================================
{
  "env": {
    "browser": true,
    "es6": true,
    "serviceworker": true
  },
  "extends": "eslint:recommended",
  "globals": {
    "Atomics": "readonly",
    "SharedArrayBuffer": "readonly"
  },
  "parserOptions": {
    "ecmaVersion": 2017,
    "sourceType": "module"
  },
  "rules": {
    "no-empty": "warn",
    "no-unused-vars": "warn",
    "no-constant-condition": "warn"
  }
}

================================================
FILE: cf-worker/README.md
================================================
使用 CloudFlare Worker 免费部署


# 简介

`CloudFlare Worker` 是 CloudFlare 的边缘计算服务。开发者可通过 JavaScript 对 CDN 进行编程,从而能灵活处理 HTTP 请求。这使得很多任务可在 CDN 上完成,无需自己的服务器参与。


# 部署

首页:https://workers.cloudflare.com

注册,登陆,`Start building`,取一个子域名,`Create a Worker`。

复制 [index.js](https://raw.githubusercontent.com/EtherDream/jsproxy/master/cf-worker/index.js) 到左侧代码框,`Save and deploy`。如果正常,右侧应显示首页。

收藏地址框中的 `https://xxxx.子域名.workers.dev`,以后可直接访问。


# 计费

后退到 `overview` 页面可参看使用情况。免费版每天有 10 万次免费请求,对于个人通常足够。

如果不够用,可注册多个 Worker,在 `conf.js` 中配置多线路负载均衡。或者升级到 $5 的高级版本,每月可用 1000 万次请求(超出部分 $0.5/百万次请求)。


# 修改配置

默认情况下,静态资源从 `https://etherdream.github.io/jsproxy` 反向代理,可通过代码中 `ASSET_URL` 配置,从而可使用自定义的 `conf.js` 配置。


# 存在问题

* WebSocket 代理尚未实现

* 外链限制尚未实现

* 未充分测试,以后再完善

================================================
FILE: cf-worker/index.js
================================================
'use strict'

/**
 * static files (404.html, sw.js, conf.js)
 */
const ASSET_URL = 'https://etherdream.github.io/jsproxy'

const JS_VER = 10
const MAX_RETRY = 1

/** @type {RequestInit} */
const PREFLIGHT_INIT = {
  status: 204,
  headers: new Headers({
    'access-control-allow-origin': '*',
    'access-control-allow-methods': 'GET,POST,PUT,PATCH,TRACE,DELETE,HEAD,OPTIONS',
    'access-control-max-age': '1728000',
  }),
}

/**
 * @param {any} body
 * @param {number} status
 * @param {Object<string, string>} headers
 */
function makeRes(body, status = 200, headers = {}) {
  headers['--ver'] = JS_VER
  headers['access-control-allow-origin'] = '*'
  return new Response(body, {status, headers})
}


/**
 * @param {string} urlStr 
 */
function newUrl(urlStr) {
  try {
    return new URL(urlStr)
  } catch (err) {
    return null
  }
}


addEventListener('fetch', e => {
  const ret = fetchHandler(e)
    .catch(err => makeRes('cfworker error:\n' + err.stack, 502))
  e.respondWith(ret)
})


/**
 * @param {FetchEvent} e 
 */
async function fetchHandler(e) {
  const req = e.request
  const urlStr = req.url
  const urlObj = new URL(urlStr)
  const path = urlObj.href.substr(urlObj.origin.length)

  if (urlObj.protocol === 'http:') {
    urlObj.protocol = 'https:'
    return makeRes('', 301, {
      'strict-transport-security': 'max-age=99999999; includeSubDomains; preload',
      'location': urlObj.href,
    })
  }

  if (path.startsWith('/http/')) {
    return httpHandler(req, path.substr(6))
  }

  switch (path) {
  case '/http':
    return makeRes('请更新 cfworker 到最新版本!')
  case '/ws':
    return makeRes('not support', 400)
  case '/works':
    return makeRes('it works')
  default:
    // static files
    return fetch(ASSET_URL + path)
  }
}


/**
 * @param {Request} req
 * @param {string} pathname
 */
function httpHandler(req, pathname) {
  const reqHdrRaw = req.headers
  if (reqHdrRaw.has('x-jsproxy')) {
    return Response.error()
  }

  // preflight
  if (req.method === 'OPTIONS' &&
      reqHdrRaw.has('access-control-request-headers')
  ) {
    return new Response(null, PREFLIGHT_INIT)
  }

  let acehOld = false
  let rawSvr = ''
  let rawLen = ''
  let rawEtag = ''

  const reqHdrNew = new Headers(reqHdrRaw)
  reqHdrNew.set('x-jsproxy', '1')

  // 此处逻辑和 http-dec-req-hdr.lua 大致相同
  // https://github.com/EtherDream/jsproxy/blob/master/lua/http-dec-req-hdr.lua
  const refer = reqHdrNew.get('referer')
  const query = refer.substr(refer.indexOf('?') + 1)
  if (!query) {
    return makeRes('missing params', 403)
  }
  const param = new URLSearchParams(query)

  for (const [k, v] of Object.entries(param)) {
    if (k.substr(0, 2) === '--') {
      // 系统信息
      switch (k.substr(2)) {
      case 'aceh':
        acehOld = true
        break
      case 'raw-info':
        [rawSvr, rawLen, rawEtag] = v.split('|')
        break
      }
    } else {
      // 还原 HTTP 请求头
      if (v) {
        reqHdrNew.set(k, v)
      } else {
        reqHdrNew.delete(k)
      }
    }
  }
  if (!param.has('referer')) {
    reqHdrNew.delete('referer')
  }

  // cfworker 会把路径中的 `//` 合并成 `/`
  const urlStr = pathname.replace(/^(https?):\/+/, '$1://')
  const urlObj = newUrl(urlStr)
  if (!urlObj) {
    return makeRes('invalid proxy url: ' + urlStr, 403)
  }

  /** @type {RequestInit} */
  const reqInit = {
    method: req.method,
    headers: reqHdrNew,
    redirect: 'manual',
  }
  if (req.method === 'POST') {
    reqInit.body = req.body
  }
  return proxy(urlObj, reqInit, acehOld, rawLen, 0)
}


/**
 * 
 * @param {URL} urlObj 
 * @param {RequestInit} reqInit 
 * @param {number} retryTimes 
 */
async function proxy(urlObj, reqInit, acehOld, rawLen, retryTimes) {
  const res = await fetch(urlObj.href, reqInit)
  const resHdrOld = res.headers
  const resHdrNew = new Headers(resHdrOld)

  let expose = '*'
  
  for (const [k, v] of resHdrOld.entries()) {
    if (k === 'access-control-allow-origin' ||
        k === 'access-control-expose-headers' ||
        k === 'location' ||
        k === 'set-cookie'
    ) {
      const x = '--' + k
      resHdrNew.set(x, v)
      if (acehOld) {
        expose = expose + ',' + x
      }
      resHdrNew.delete(k)
    }
    else if (acehOld &&
      k !== 'cache-control' &&
      k !== 'content-language' &&
      k !== 'content-type' &&
      k !== 'expires' &&
      k !== 'last-modified' &&
      k !== 'pragma'
    ) {
      expose = expose + ',' + k
    }
  }

  if (acehOld) {
    expose = expose + ',--s'
    resHdrNew.set('--t', '1')
  }

  // verify
  if (rawLen) {
    const newLen = resHdrOld.get('content-length') || ''
    const badLen = (rawLen !== newLen)

    if (badLen) {
      if (retryTimes < MAX_RETRY) {
        urlObj = await parseYtVideoRedir(urlObj, newLen, res)
        if (urlObj) {
          return proxy(urlObj, reqInit, acehOld, rawLen, retryTimes + 1)
        }
      }
      return makeRes(res.body, 400, {
        '--error': `bad len: ${newLen}, except: ${rawLen}`,
        'access-control-expose-headers': '--error',
      })
    }

    if (retryTimes > 1) {
      resHdrNew.set('--retry', retryTimes)
    }
  }

  let status = res.status

  resHdrNew.set('access-control-expose-headers', expose)
  resHdrNew.set('access-control-allow-origin', '*')
  resHdrNew.set('--s', status)
  resHdrNew.set('--ver', JS_VER)

  resHdrNew.delete('content-security-policy')
  resHdrNew.delete('content-security-policy-report-only')
  resHdrNew.delete('clear-site-data')

  if (status === 301 ||
      status === 302 ||
      status === 303 ||
      status === 307 ||
      status === 308
  ) {
    status = status + 10
  }

  return new Response(res.body, {
    status,
    headers: resHdrNew,
  })
}


/**
 * @param {URL} urlObj 
 */
function isYtUrl(urlObj) {
  return (
    urlObj.host.endsWith('.googlevideo.com') &&
    urlObj.pathname.startsWith('/videoplayback')
  )
}

/**
 * @param {URL} urlObj 
 * @param {number} newLen 
 * @param {Response} res 
 */
async function parseYtVideoRedir(urlObj, newLen, res) {
  if (newLen > 2000) {
    return null
  }
  if (!isYtUrl(urlObj)) {
    return null
  }
  try {
    const data = await res.text()
    urlObj = new URL(data)
  } catch (err) {
    return null
  }
  if (!isYtUrl(urlObj)) {
    return null
  }
  return urlObj
}

================================================
FILE: changelogs/README.md
================================================
# 完整更新日志

* 2019-06-11 前端脚本调整,首页可离线访问(如果长时间加载中,尝试多刷新几次或者隐身模式访问)

* 2019-05-30 更新 cfworker,对 ytb 视频进行了优化(推荐选 1080p+,不会增加服务器压力)

* 2019-05-29 nginx 增加静态资源服务,可同时支持代理接口和首页访问

* 2019-05-27 增加 nio.io、sslip.io 后备域名,减少申请失败的几率

* 2019-05-26 安装时自动申请证书(使用 xip.io 域名),安装后即可预览

* 全新的 URL 模型,取代 [之前版本](https://github.com/EtherDream/jsproxy/tree/first-ver)。[查看详细](v0.0.1.md)


================================================
FILE: changelogs/v0.0.1.md
================================================
虽然该版本仍为概念演示状态,但相比[最初版本](https://github.com/EtherDream/jsproxy/tree/first-ver),有了很大变化:

# 不再使用子域名

使用子域名编码目标域名(例如 gg.jsproxy.tk),存在太多缺陷。例如 HTTPS 证书问题,DNS 性能和安全问题等。因此目前不再使用子域名,只用固定的域名,目标 URL 放在路径里。例如:

https://zjcqoo.github.io/-----https://www.google.com

当然这也会产生很多新问题,例如无法支持 Cookie、页面之间没有同源策略限制等。

对于 Cookie,目前通过 JS 来维护,而不用浏览器原生(当然还有不少细节没实现)。这样的好处是前后端可以分离,前端页面可以放在第三方 Web 服务器上(例如 CDN、GitHub Pages),我们的服务器只提供代理接口。

这样一个页面可使用多个服务器的代理接口,并能实现线路切换、负载均衡等效果。

同源策略方面的限制目前暂未实现,因此不要进行登陆等操作,避免隐私泄露。


# 服务端优化

安全改进:由于 Web 页面托管在第三方站点上,自己的服务器无需开启 443 端口,因此也无需 root 运行。同时支持 IP 黑名单功能,防止 SSRF 攻击。

代码改进:接口代理使用固定的 URL(参见 `api.conf`),不再使用任意路径,代码干净了很多。


# 支持更多浏览器

相比之前版本只支持 Chrome,现在还支持最新的 Safari 和 FireFox。

注意:FireFox 隐身模式下不支持 Service Worker,只能普通模式访问。


# 提供一个首页

虽然依旧简陋,但比之前好。提供了线路切换、预加载的功能。

================================================
FILE: changelogs/v0.1.0.md
================================================
# v0.1.0

## 更新内容

* 后端代理以及 cfworker 接口调整,修复缓存失效的问题

* 前端增加缓存记录,提高浏览器缓存命中率

* 前端增加 Cookie 持久化

* 前端增加 CORS 站点直连功能

* 配置调整,支持线路权重

* 更详细的服务器错误信息显示

* 增加更多的 Storage API Hook


## 代理接口调整

之前代理接口使用固定的路径 `/http`,目标 URL 设置在请求头 `--url` 字段,同时返回头配置了 `vary: --url` 字段,希望能根据不同的 `--url` 请求返回不同的缓存内容。但事实上该方案并未生效,和预想的不同,浪费了不少流量。(对 `vary` 了解不够透彻~)

为修复这个问题,目前将代理接口改成 `/http/目标 URL`,去掉了 `vary` 字段。同时将绝大部分的请求字段打包到 `Referer` 字段里,使请求头保持简单,不产生 CORS preflight。

> 如果不打包,则会频繁出现 preflight,即使配置了 `Access-Control-Max-Age` 也没用,因为 max-age 只对特定 URL 记忆,而现在的 URL 几乎每次都不同,所以必须保持请求头足够简单。至于为什么选择 `Referer` 字段,因为只有这个字段可以灵活存储数据,[其他几个字段都有些限制](https://fetch.spec.whatwg.org/#cors-unsafe-request-header-byte),容易出现 preflight。

当然这个功能目前仍在研究中,未来也许会有更好的方案。


## 节点缓存

由于切换 节点/线路 会使得最终的 URL 发生变化,从而导致无法利用已有的缓存。

目前增加了静态资源记忆功能,记住当前使用的域名。下次加载时直接使用上次的域名,从而命中浏览器缓存。

存储查看:`indexedDB` -> `.sys` -> `url-cache`


## Cookie 持久化

目前 Cookie 信息定期同步到本地存储,浏览器重启后可保持之前的会话。

存储查看:`indexedDB` -> `.sys` -> `cookie`


## CORS 站点直连

不少网站(通常是 CDN)在返回头中配置了 `access-control-allow-origin: *`,并且不校验 `origin` 和 `referer`(或者允许为空)。

对于这样的站点,前端可直接连接而无需通过代理,从而能加快访问速度,并且节省代理服务器流量。

目前收集了部分站点,只在纯前端实现。未来将尝试和服务端配合,覆盖所有这样的场合。


## 节点权重支持

不同于之前均匀分配负载,目前可配置每个线路的权重,从而能对部分线路增加或降低负载。

例如演示案例中的 cfworker 节点,使用 1 个收费版 + 多个免费版的方案。由于免费版有访问频率限制,因此使用更低的权重以减少负载。(命中比例 = 当前值 / 总值)


## 详细错误信息

目前可显示代理服务器的 DNS 解析错误、HTTP 连接错误、白名单错误等,取代之前过于简陋的报错信息。


## Storage API Hook

增加 `indexedDB` 和 `Cache Storage` 的 key 枚举、删除的 API Hook。


================================================
FILE: docs/blogs/js-hook.md
================================================
《基于 JS Hook 技术,打造最先进的在线代理》


# 什么是在线代理

所谓在线代理,就类似本项目的演示,就可通过某个网站访问另一个网站(通常无法直接访问)。不用安装任何插件,不用修改任何配置,仅仅打开一个网页即可。

类似的网站,或许大家都曾见过,并且印象中应该都不怎么好用。相比 ss/v2 这些网络层代理,在线代理的成熟度显然要低得多,只能临时凑合着用。


# 为什么在线代理不好用

因为要实现一个完善的在线代理,难度非常大!

也许你会说,用 nginx 搭个反向代理不就可以了。其实并没有这么简单。

举个例子,假如我们用 `a.com` 反向代理 `b.com`,并且 `b.com` 有如下网页:

```html
<img src="/foo.gif">
<img src="http://b.com/bar.gif">
```

第一个 img 是相对路径。由于当前实际地址是 `a.com`,因此最终访问的 URL 是 `http://a.com/foo.gif`。我们的后端服务器收到请求后,抓取 `http://b.com/foo.gif` 的内容并返回给用户。这没有问题。

第二个 img 是绝对路径,这就有问题了!浏览器会直接访问 `b.com`,根本不经过我们的后端。而 `b.com` 是无法直接访问的,于是图片加载失败。

因此后端在代理网页时,还需要对其中的内容进行处理,将那些 **绝对路径 URL** 替换成自己的地址。例如:

```html
<img src="/foo.gif">
<img src="http://a.com/proxy?url=http://b.com/bar.gif">
```

这样才能确保图 2 走我们的站点,而不是连接 `b.com` 导致逃脱代理。

由此可见,衡量一个在线代理完不完善,很重要的一点就是:能否覆盖网页中尽可能多的 URL,减少逃逸现象。

----

虽然替换网页中的 URL 并不困难,但是,这极其麻烦!

做过 Web 开发的都清楚,网页里的 URL 有千奇百怪的存在形式,可存在于 HTML、CSS、JS 甚至是动态加载的 JSON、XML 等资源中,因此后端只处理 HTML 是不够的,还必须处理各种文本资源!这对服务器是个不小的开销。

除了内容处理,其实还有很多额外开销。互联网上的文本资源大多都是压缩传输,而压缩的数据是无法直接处理的,因此还得先解压;最后处理完的数据,还得再压缩回去。一来一往,消耗不少 CPU。当然也可以不压缩,但这又会增加流量开销。

像过去的 `gzip` 压缩开销尚可接受,而如今流行的 `brotli` 压缩开销非常大。假如用户频繁访问大体积的文本资源,代理服务器 CPU 将严重消耗。

----

不过,上述问题还不是最严重的。事实上 HTML、CSS 等资源都好说,唯独 JS 是个坑 —— 因为 JS 是程序,它可以动态产生 URL。例如:

```js
var site = 'b';
document.write('<img src=http://' + site + '.com/foo.gif>');
```

遇到这种场合,任何字符串层面的替换都是无解的!

除了动态产生 URL,还有动态获取 URL 的情况。因为有很多 API 是和 URL 相关的,例如:

* `document.domain`,`document.URL`,`document.cookie`

* 超链接 `href` 属性,表单 `action` 属性,各种元素 `src` 属性

* 消息事件 `origin` 属性

* 省略数十个 ...

在我们 `a.com` 页面里调用这些 API,返回自然是 `a.com` 的 URL,而不会是 `b.com`。假如网页里的业务逻辑仍以 `b.com` 作为标准处理,很可能就会出现问题。

这类情况现实中很普遍,而传统的在线代理,对此则无能为力。


# 新概念在线代理

现在,我们尝试用更先进的技术,先解决动态 URL 的问题,然后改进服务器的开销问题。

## API Hook

先来思考:在 `a.com` 的网页里,可以让 `document.domain` 返回 `b.com` 吗?

其实可以!因为 JS 非常灵活,绝大部分的原生 API 都可以重写,所以能轻易改变默认行为。例如:

```js
var raw_open = window.open;

window.open = function(url) {
  return raw_open('http://a.com/proxy?' + url);
};
```

这个经过改造的 `open` 函数,可以在每次调用时给 url 加上 `http://a.com/proxy?` 这个前缀。这样,原始网页弹出的任何 URL,其实都是我们站点的页面!

除了函数,属性也可以重写。例如改变 `document.domain` 的 `getter` 和 `setter`:

```js
var fakeDomain = 'b.com';

Object.defineProperty(document, 'domain', {
  get() {
    return fakeDomain;
  },
  set(value) {
    fakeDomain = value;
  }
})
```

通过对函数和属性的重写,我们可以 hook 绝大多数和 URL 相关的 API,在其中对输入的参数或者输出的返回值进行调整。

这样,原本 `b.com` 的网页现在运行于 `a.com` 站点下,页面脚本获得的仍是 `b.com` 的 URL。

我们的代理似乎透明般存在,难以被原始页面感知!


## DOM Hook

增加前端脚本之后,服务端的开销反而变大了。因为除了替换 URL,还得往页面头部注入脚本代码。

既然前端脚本这么强大,可不可以将 URL 的替换也让它来实现?

我们设想下,假如服务端不替换 URL,只注入脚本,那么返回的 HTML 类似这样:

```html
<script src="helper.js"></script>
<html>
  ...
  <img src="http://b.com/foo.gif">
  <script src="http://b.com/bar.js"></script>
</html>
```

我们的脚本可以最先运行,这是个巨大的优势。但是,这能改变后续标签的 URL 属性吗?

事实上,有一个 API 可以拦截 DOM 元素的创建,它就是 `MutationObserver`。通过它,我们可以在 **DOM 元素渲染前** 修改其属性,将绝对路径的 URL 调整成我们的站点。

**尽管服务器返回的 HTML 里都是原始 URL,但资源实际上是从我们的站点加载,超链接指向的也是我们的站点!**

前端有了 `API Hook` 和 `DOM Hook`,后端也就无需处理 JSON、XML 等资源了,因为 URL 无论从何而来,最终都将传给 API,或者赋给 DOM 的属性 —— 这两者现在都能拦截!


## URL Hook

由于 `MutationObserver` 只能拦截 DOM 元素,因此仅适用于 HTML,而无法适用于其他资源,例如 CSS 中也有 URL 字符串,这仍需后端处理。

```css
@import 'http://b.com/foo.css';
.xx {
  background-image: url(http://b.com/bar.gif);
}
```

另外,即使 HTML 中只插入一行代码,服务器仍需对流量进行解压和再压缩,消耗不少 CPU 资源。这很不完美!

因此,我们的终极目标是:服务器不处理任何内容!最多只处理 HTTP 头。

----

为了实现这个目标,需要借助 HTML5 的一个黑科技 —— `Service Worker`。

`Service Worker` 是一种后台运行的服务进程,它提供的 API 允许 JS 拦截当前站点产生的所有 HTTP 请求(甚至包括浏览器地址栏的访问请求),并能控制返回结果,相当于浏览器内部的反向代理!

有了这个 API,我们可统一捕获流量。无论 URL 是绝对路径还是相对路径,无论出现在 HTML 还是 CSS,都能在 `Service Worker` 层进行拦截,然后转发到自己的服务器!

不过 `Service Worker` 本身也需通过脚本安装,那么服务端是否仍需往 HTML 中插入脚本?

其实不需要。因为 `Service Worker` 是后台进程,一旦安装可长期运行,即使网页关闭它仍在运行。所以只需让用户安装一次就可以。

当用户首次访问时,不管访问什么路径,服务端始终返回安装页面。之后,整个站点所有流量都被 `Service Worker` 接管。最终所有的内容处理,都可以由 JS 来实现!服务端只需纯粹转发数据,甚至都不用考虑解压缩,从而大幅降低 CPU 开销。

----

到此,我们实现了三种类型的 Hook:

* API Hook (重写函数和属性)

* DOM Hook (MutationObserver)

* URL Hook  (Service Worker)

现在,无论加载 URL 还是调用 API,都可被我们拦截和代理,仿佛将原始网页嵌套在一个沙盒中运行!


# 无法 Hook 的 API

由于浏览器限制,有些 API 是无法重写的,其中最典型的就是 `location` —— 无论 `window.location` 还是 `document.location` 以及 `location` 对象中的成员,都是无法重写的。

```js
Object.defineProperty(location, 'href', {
  get() {},
  set() {}
})
// Uncaught TypeError: Cannot redefine property: href
```

然而这个 API 使用频率非常高,不少网站通过它检测当前的域名是否合法,例如:

```js
if (location.host != 'b.com') {
  location.href = 'http://b.com';
}
```

如果不考虑这个接口,网站一旦发生跳转,就逃出我们的沙盒了!

然而它又无法重写,这该如何解决?尽管理论上无解,但作为实践,还是可以在一定程度上进行缓解 —— 我们可将 JS 中字面出现的 `location` 进行重命名,例如修改成 `__location`,这样就能将操作**转移到我们定义的对象上**!

```js
if (__location.host != 'b.com') {
  __location.href = 'http://b.com';
}
```

因为 `Service Worker` 掌控所有流量,所以修改 JS 资源并不困难。此外,网页中的内联脚本也可通过 `MutationObserver` 拦截和修改。

> 目前演示站点为了简单,直接使用正则替换,有时会将同名的函数、属性、字符串也进行修改,导致出现误伤。更好的方案,则是在语法树层面进行修改,当然性能开销也会更大。

不过这个方案只能缓解,而无法彻底解决。因为我们只能修改明文出现的 `location`,对于动态的场合就无解了,例如:

```js
self['lo' + 'cat' + 'ion']    // location object
this[atob('bG9jYXRpb24=')]    // location object
```

更别提 `eval` 这些了。所以,如果网页有意访问 `location`,我们是无法拦截的!

这个特征,可以给 Web 开发者一个警示:如果想检测当前页面 URL 是否合法,尽量不要出现明文的 `window`、`location` 等关键字,而是通过动态的方式访问,以防落入上述这种陷阱。如果想检测你的网站是否安全,可尝试用演示站点访问你的登录页,如果没有跳转到原始网站,说明你的 `location` 接口用得不够安全,用户极有可能在代理页面中输入账号密码,导致隐私泄露!


# 无法 Hook 的 DOM

事实上,有些特殊请求无法被 `Service Worker` 拦截,例如 **不同源的框架页面**。因此,我们仍需借助 `MutationObserver` 修改元素的 URL 属性,将跨源的页面变成同源,从而可拦截子页面的所有请求。

不过 `MutationObserver` 也有一些细节问题。例如,即使给元素设置了新的 URL,但是原始 URL 仍会加载。因为浏览器为了提高速度,有一个预加载的机制,原始 URL 在 HTML 解析阶段就开始加载了;之后修改会导致加载取消,但请求仍已产生,控制台里可看到 `cancel` 状态的请求。

有个简单的办法,倒是可以缓解这问题:设置一个 `Content-Security-Policy` 策略,让网页只允许加载自己的域名,从而阻止预加载请求。这个方案目前在演示中开启,可以看到每个页面的头部有一个 `<meta>` 标签定义的 CSP 策略。

当然 `MutationObserver` 仍存在较多问题,这里不一一叙述。事实上最完善的方案,仍是替换 HTML 里的 URL 字符串,并且最好支持流模式。这个功能以后将会实现。


# 无法 Hook 的资源

由于 `URL` 只是 `URI` 的子集,因此有些 URI 资源无法被 `Service Worker` 拦截。最典型的就是 `Data URI`。

此外,还有 `about:`、`blob:`、`javascript:` 等协议的资源加载也无法拦截。这意味着,通过这些 uri 产生的网页,其中的资源都不会被 `Service Worker` 捕获;通过这些 uri 产生的脚本,其中的 `location` 都不会被替换成 `__location`。这就会出现逃逸现象!

因此,我们还得借助 API Hook 和 DOM Hook 来覆盖这类资源的加载。(目前演示中尚未实现)

其他例如 `WebSocket` 协议 `ws:` 和 `wss:`,虽然也不会经过 `Service Worker`,但其本质仍是 HTTP,因此通过 API Hook 即可解决。


# 更多优化

得益于 `Service Worker` 超高的灵活性,我们甚至可对网络架构进行改造,将前后端进行分离:

* 前端只提供静态资源,负责首页展示、`Service Worker` 的安装以及自身脚本

* 后端只提供代理接口,负责数据转发

这样,前端可部署在第三方 Web 服务器上,例如演示站点部署于 GitHub Pages 服务。并且,一个前端可同时使用多个后端服务,从而可实现多倍的带宽加速!

在此基础上,还可以实现负载均衡、故障切换等功能,甚至很多有趣的玩法。。。

例如,我们可将各大网站的常用静态资源,预先部署到本地 CDN 上。用户遇到这些资源时可直接从 CDN 加载,大幅加快访问速度,并能减少代理服务器的流量。

例如,服务器遇到体积较大的静态资源时,只返回文件信息,让用户从流量更廉价的渠道获取完整内容。如果失败,再从原始服务器获取。这样可大幅降低服务器的流量开销。并且这个过程在 `Service Worker` 里实现,上层业务则是毫无感知的。(目前演示网站使用 CloudFlare Worker 作为大文件的下载渠道,流量费用可节省一倍)

----

不过,前后端分离的架构也存在一些缺陷。很多原本浏览器底层实现的功能,现在需要自己来实现,大幅增加了复杂度。例如 Cookie 的增删改查、同源策略的模拟等。目前演示中实现了 Cookie 基本功能,其他的暂未实现。

当然,尽管这个演示还不完善,但这种架构模型,目前是最先进的。搜了国外类似的站点,目前只有 [CroxyProxy](https://www.croxyproxy.com) 这个网站具备 `Service Worker` 和 DOM API Hook 的功能。不过这个网站似乎出现没多久,前端部分还是加密的。另外它没有前后端分离,代理接口是通过 PHP 实现的,相比 nginx 效率和体验都会打折扣,并且 `WebSocket` 也没有实现。

事实上在线代理本不复杂,就看如何使用各种黑科技和巧妙的思路来改进它~


================================================
FILE: docs/cert-auto.md
================================================
# 自动申请 HTTPS 证书

1.转发 80 端口到 8080 端口(需要 root 权限)

```bash
iptables -t nat -I PREROUTING 1 -p tcp --dport 80 -j REDIRECT --to-ports 8080
```

> 外部访问 http://服务器IP/.well-known/acme-challenge/test.txt 可验证是否正常。返回 `ok` 说明正常。

2.安装 acme.sh(无需 root 权限,在 `jsproxy` 用户下安装)

```bash
su - jsproxy
curl https://raw.githubusercontent.com/Neilpang/acme.sh/master/acme.sh | INSTALLONLINE=1  sh
```

> 部分精简系统可能没有 `openssl` 导致运行失败,需提前安装依赖(例如 `yum install -y openssl`)

3.申请证书

```bash
# 服务器公网 IP
ip=$(curl -s https://api.ipify.org)
domain=$ip.xip.io

dist=~/server/cert/$domain
mkdir -p $dist

~/.acme.sh/acme.sh \
  --issue \
  -d $domain \
  --keylength ec-256 \
  --webroot ~/server/acme

~/.acme.sh/acme.sh \
  --install-cert \
  -d $domain \
  --ecc \
  --key-file $dist/ecc.key \
  --fullchain-file $dist/ecc.cer
```

如果申请失败(例如提示 `rate limit exceeded`),尝试将 `xip.io` 换成 `nip.io`、`sslip.io` 等其他类似的域名。

4.生成配置文件:

```conf
echo "
listen                8443 ssl http2;
ssl_certificate       cert/$domain/ecc.cer;
ssl_certificate_key   cert/$domain/ecc.key;
" > ~/server/cert/cert.conf
```

重启服务:`~/server/run.sh reload`

5.验证

访问 `https://服务器IP.xip.io:8443/`,没出现证书错误即成功。

6.关闭 80 端口转发

```bash
iptables -t nat -D PREROUTING 1
```

如果 80 端口没有运行其他服务,可以不关闭。因为 Let's Encrypt 证书有效期只有 3 个月,所以 acme.sh 会定期执行续签脚本。如果 80 端口关闭则无法自动续签。

================================================
FILE: docs/cert-manual.md
================================================
# 手动申请 HTTPS 证书

在线申请:https://www.sslforfree.com


## 方案 1 —- 通过 80 端口验证

前提条件:公网 IP 能访问 80 端口,设备需要 root 权限

1.输入 `服务器IP.xip.io`

2.`Manual Verification` -> `Manually Verify Domain` -> `Download File`

3.文件保存到服务器 `~/server/acme/.well-known/acme-challenge/` 目录

4.转发 80 端口到 8080 端口(需要 root 权限)

```bash
iptables -t nat -I PREROUTING 1 -p tcp --dport 80 -j REDIRECT --to-ports 
```

当然也可以使用其他 Web 服务,只要该文件能被外部访问就可以。

5.测试链接能否访问(Verify successful upload by visiting the following links in your browser)

6.Download SSL Certificate

7.保存证书

`Certificate` 保存到 `~/server/cert/xip.io/cert`

`Private Key` 保存到 `~/server/cert/xip.io/key`

编辑文件 `~/server/cert/cert.conf`

```conf
listen                8443 ssl http2;
ssl_certificate       cert/xip.io/cert;
ssl_certificate_key   cert/xip.io/key;
```

重启服务:`~/server/run.sh reload`

8.验证

访问 `https://服务器IP.xip.io:8443/`,没出现证书错误即成功。

9.关闭 80 端口转发

```bash
iptables -t nat -D PREROUTING 1
```


## 方案 2 —- 通过 DNS 验证

前提条件:拥有域名控制权(`xip.io` 不支持)

1.输入域名

2.Manual Verification (DNS) -> Manually Verify Domain

3.根据提示,创建一个 TXT 记录

4.Download SSL Certificate

5.保存证书(和上述相同)

================================================
FILE: docs/setup.md
================================================
# 手动安装

## 创建用户

新建一个名为 `jsproxy` 用户(`nobody` 组),并切换:

```bash
groupadd nobody
useradd jsproxy -g nobody --create-home

su - jsproxy
```

非 Linux 系统,或者无 root 权限的设备,可忽略。

> 为什么要创建用户?因为使用低权限运行服务可减少风险。另外在防 SSRF 脚本 `setup-ipset.sh` 中,是通过 iptalbes 的 `uid-owner` 策略阻止 `jsprxoy` 这个特定用户访问内网的。


## 安装 nginx

本项目使用 [OpenResty](https://openresty.org/en/)。编译前需确保 make、gcc 等工具存在。

```bash
cd $(mktemp -d)

curl -O https://www.openssl.org/source/openssl-1.1.1b.tar.gz
tar zxf openssl-*

curl -O https://ftp.pcre.org/pub/pcre/pcre-8.43.tar.gz
tar zxf pcre-*

curl -O https://zlib.net/zlib-1.2.11.tar.gz
tar zxf zlib-*

curl -O https://openresty.org/download/openresty-1.15.8.1.tar.gz
tar zxf openresty-*
cd openresty-*

export PATH=$PATH:/sbin

./configure \
  --with-openssl=../openssl-1.1.1b \
  --with-pcre=../pcre-8.43 \
  --with-zlib=../zlib-1.2.11 \
  --with-http_v2_module \
  --with-http_ssl_module \
  --with-pcre-jit \
  --prefix=$HOME/openresty

make
make install
```

其中 `configure` 的参数 `--prefix` 指定 nginx 安装路径,这里为方便设为用户主目录。

> 注意编译后的 nginx 程序不能改变位置,否则会启动失败

测试能否执行:

```bash
~/openresty/nginx/sbin/nginx -h
```


## 安装代理程序

下载本项目,其本质就是一堆 nginx 配置。推荐放在 `jsproxy` 用户的主目录:

```bash
cd ~
git clone --depth=1 https://github.com/EtherDream/jsproxy.git server
```

下载静态资源文件到 `www` 目录:

```bash
cd server
rm -rf www
git clone -b gh-pages --depth=1 https://github.com/EtherDream/jsproxy.git www
```

开启服务:

```bash
./run.sh
```

更新使用 git 即可。


## 申请域名

* 免费申请:https://www.freenom.com

* 临时测试:`服务器IP.xip.io`

类似的还有 `nip.io`、`sslip.io`,自动安装脚本默认使用 `xip.io`。


## 申请证书

可通过 Let's Encrypt 申请免费的 HTTPS 证书。

* [手动申请](cert-manual.md)

* [自动申请](cert-auto.md)

也可以不申请证书,使用免费的 HTTPS 反向代理,例如 [CloudFlare](https://www.cloudflare.com/):

```text
[浏览器] --- https ---> [CloudFlare] --- http ---> [服务器]
```

这种方案不仅能节省系统资源,还能减少流量开销(静态资源可被 CloudFlare 缓存)。当然延时可能较高,并且安全性略低。

> 为什么一定要用 HTTPS?因为本项目使用了浏览器 Service Worker 技术,该 API 只能在安全环境使用,除了 localhost、127.0.0.0/8 站点可以使用 HTTP,其他必须 HTTPS。


## 支持系统

目前测试了 OSX 系统,其他还在测试中。。。


================================================
FILE: i.sh
================================================
#!/usr/bin/env bash

{ # this ensures the entire script is downloaded #

JSPROXY_VER=0.1.0
OPENRESTY_VER=1.15.8.1

SRC_URL=https://raw.githubusercontent.com/EtherDream/jsproxy/$JSPROXY_VER
BIN_URL=https://raw.githubusercontent.com/EtherDream/jsproxy-bin/master
ZIP_URL=https://codeload.github.com/EtherDream/jsproxy/tar.gz

SUPPORTED_OS="Linux-x86_64"
OS="$(uname)-$(uname -m)"
USER=$(whoami)

INSTALL_DIR=/home/jsproxy
NGX_DIR=$INSTALL_DIR/openresty

DOMAIN_SUFFIX=(
  xip.io
  nip.io
  sslip.io
)

GET_IP_API=(
  https://api.ipify.org
  https://bot.whatismyipaddress.com/
)

COLOR_RESET="\033[0m"
COLOR_RED="\033[31m"
COLOR_GREEN="\033[32m"
COLOR_YELLOW="\033[33m"

output() {
  local color=$1
  shift 1
  local sdata=$@
  local stime=$(date "+%H:%M:%S")
  printf "$color[jsproxy $stime]$COLOR_RESET $sdata\n"
}
log() {
  output $COLOR_GREEN $1
}
warn() {
  output $COLOR_YELLOW $1
}
err() {
  output $COLOR_RED $1
}

gen_cert() {
  local ip=""

  for i in ${GET_IP_API[@]}; do
    log "服务器公网 IP 获取中,通过接口 $i"
    ip=$(curl -s $i)

    if [[ ! $ip ]]; then
      warn "获取失败"
      continue
    fi

    if ! grep -qP "^\d+\.\d+\.\d+\.\d+$" <<< $ip; then
      warn "无效 IP:$ip"
      continue
    fi

    break
  done

  if [[ $ip ]]; then
    log "服务器公网 IP: $ip"
  else
    err "服务器公网 IP 获取失败,无法申请证书"
    exit 1
  fi

  log "安装 acme.sh 脚本 ..."
  curl https://raw.githubusercontent.com/Neilpang/acme.sh/master/acme.sh | INSTALLONLINE=1  sh

  local acme=~/.acme.sh/acme.sh

  local domains=()

  if [[ $@ ]]; then
    for i in $@; do
      domains+=($i)
    done
  else
    warn "未指定域名,使用公共测试域名"
    for i in ${DOMAIN_SUFFIX[@]}; do
      domains+=($ip.$i)
    done
  fi

  for domain in ${domains[@]}; do
    echo "校验域名 $domain ..."

    local ret=$(getent ahosts $domain | head -n1 | awk '{print $1}')
    if [[ $ret != $ip ]]; then
      err "域名 $domain 解析结果: $ret,非本机公网 IP: $ip"
      continue
    fi

    log "尝试为域名 $domain 申请证书 ..."

    local dist=server/cert/$domain
    mkdir -p $dist

    $acme \
      --issue \
      -d $domain \
      --keylength ec-256 \
      --webroot server/acme

    $acme \
      --install-cert \
      -d $domain \
      --ecc \
      --key-file $dist/ecc.key \
      --fullchain-file $dist/ecc.cer

    if [ -s $dist/ecc.key ] && [ -s $dist/ecc.cer ]; then
      echo "# generated by i.sh
listen                8443 ssl http2;
ssl_certificate       cert/$domain/ecc.cer;
ssl_certificate_key   cert/$domain/ecc.key;
" > server/cert/cert.conf

      local url=https://$domain:8443
      echo "
$url  'mysite';" >> server/allowed-sites.conf

      log "证书申请完成,重启服务 ..."
      server/run.sh reload

      log "在线预览: $url"
      break
    fi

    err "证书申请失败!(80 端口是否添加到防火墙)"
    rm -rf $dist
  done
}


install() {
  cd $INSTALL_DIR

  log "下载 nginx 程序 ..."
  curl -O $BIN_URL/$OS/openresty-$OPENRESTY_VER.tar.gz
  tar zxf openresty-$OPENRESTY_VER.tar.gz
  rm -f openresty-$OPENRESTY_VER.tar.gz

  local ngx_exe=$NGX_DIR/nginx/sbin/nginx
  local ngx_ver=$($ngx_exe -v 2>&1)

  if [[ "$ngx_ver" != *"nginx version:"* ]]; then
    err "$ngx_exe 无法执行!尝试编译安装"
    exit 1
  fi
  log "$ngx_ver"
  log "nginx path: $NGX_DIR"

  log "下载代理服务 ..."
  curl -o jsproxy.tar.gz $ZIP_URL/$JSPROXY_VER
  tar zxf jsproxy.tar.gz
  rm -f jsproxy.tar.gz

  log "下载静态资源 ..."
  curl -o www.tar.gz $ZIP_URL/gh-pages
  tar zxf www.tar.gz -C jsproxy-$JSPROXY_VER/www --strip-components=1
  rm -f www.tar.gz

  if [ -x server/run.sh ]; then
    warn "尝试停止当前服务 ..."
    server/run.sh quit
  fi

  if [ -d server ]; then
    local backup="$INSTALL_DIR/bak/$(date +%Y_%m_%d_%H_%M_%S)"
    warn "当前 server 目录备份到 $backup"
    mkdir -p $backup
    mv server $backup
  fi

  mv jsproxy-$JSPROXY_VER server

  log "启动服务 ..."
  server/run.sh

  log "服务已开启"
  
  shift 1
  gen_cert $@
}

main() {
  log "自动安装脚本开始执行"

  if [[ "$SUPPORTED_OS" != *"$OS"* ]]; then
    err "当前系统 $OS 不支持自动安装。尝试编译安装"
    exit 1
  fi

  if [[ "$USER" != "root" ]]; then
    err "自动安装需要 root 权限。如果无法使用 root,尝试编译安装"
    exit 1
  fi

  local cmd
  if [[ $0 == *"i.sh" ]]; then
    warn "本地调试模式"

    local dst=/home/jsproxy/i.sh
    cp $0 $dst
    chown jsproxy:nobody $dst
    if [[ $1 == "-s" ]]; then
      shift 1
    fi
    cmd="bash $dst install $@"
  else
    cmd="curl -s $SRC_URL/i.sh | bash -s install $@"
  fi

  iptables \
    -t nat \
    -I PREROUTING 1 \
    -p tcp --dport 80 \
    -j REDIRECT \
    --to-ports 8080

  if ! id -u jsproxy > /dev/null 2>&1 ; then
    log "创建用户 jsproxy ..."
    groupadd nobody > /dev/null 2>&1
    useradd jsproxy -g nobody --create-home
  fi

  log "切换到 jsproxy 用户,执行安装脚本 ..."
  su - jsproxy -c "$cmd"

  local line=$(iptables -t nat -nL --line-numbers | grep "tcp dpt:80 redir ports 8080")
  iptables -t nat -D PREROUTING ${line%% *}

  log "安装完成。后续维护参考 https://github.com/EtherDream/jsproxy"
}


if [[ $1 == "install" ]]; then
  install $@
else
  main $@
fi

} # this ensures the entire script is downloaded #


================================================
FILE: log-svc/README.md
================================================
nginx 日志备份服务

## 说明

nginx 长时间运行会导致日志文件过大,该服务定期备份日志到 `backup` 目录,并进行压缩。


## 启动

```bash
./svc.sh &
```

使用 `jsproxy` 用户运行,无需 `root`。

================================================
FILE: log-svc/backup/README.md
================================================
该目录存放临时备份的日志。

================================================
FILE: log-svc/backup.sh
================================================
#!/usr/bin/env bash
# 功能:备份 nginx 日志到 backup 目录

SVC_DIR=/home/jsproxy/server
LOG_DIR=$SVC_DIR/nginx/logs
DST_DIR=$SVC_DIR/log-svc/backup

LOG_FILE=$LOG_DIR/proxy.log
LOG_SIZE=$(( 256 * 1024 * 1024 ))

ERR_FILE=$LOG_DIR/error.log
ERR_SIZE=$(( 256 * 1024 * 1024 ))


# error.log 达到 ERR_SIZE,开始备份(目前只清理)
errsize=$(stat --printf=%s $ERR_FILE)
if (( $errsize >= $ERR_SIZE )); then
  echo > $ERR_FILE
fi

# proxy.log 达到 LOG_SIZE,开始备份
logsize=$(stat --printf=%s $LOG_FILE)
if (( $logsize < $LOG_SIZE )); then
  exit
fi

logtime=$(date "+%Y-%m-%d-%H-%M-%S")

#
# 先移走日志文件,然后创建新的日志文件,通知 nginx 重新打开
#
mv $LOG_FILE $DST_DIR/$logtime.log
touch $LOG_FILE
$SVC_DIR/run.sh reopen
sleep 1

#
# 日志压缩
# 根据实际情况调整策略,在不影响系统的前提下,充分利用剩余 CPU
#
echo "compress ..."

nice -n 19 xz $DST_DIR/*.log

echo "done"

================================================
FILE: log-svc/svc.sh
================================================
#!/usr/bin/env bash
# 功能:定时调用 backup.sh

echo "log svc running"
CUR_DIR=$(cd `dirname $0` && pwd)

# 也可用 crontab
while true
do
  $CUR_DIR/backup.sh
  sleep 60
done

================================================
FILE: log.conf
================================================
#
# 日志格式定义
# https://nginx.org/en/docs/http/ngx_http_log_module.html
#
# 分隔: tab (\t)
# 前缀: 格式版本。格式变化时递增,方便解析
# 备注:
# _origin_id
#   请求源的别名,参考 allowed-sites.conf
# _ver
#   前端配置的版本,定义于 www/conf.js
# remote_addr
#   用户 IP,目前未考虑 XFF
# _level
#   实验中。记录切换状态(首次请求为 1。切换失败再次请求为 0,表示不接受切换)
# _switched
#   实验中。记录是否切换到廉价节点(未切换则为空,有切换则记录资源体积大小)
# _bodyhash
#   返回内容的 SHA256,用于统计重复内容
# upstream_http_access_control_allow_origin
#   统计 acao = * 的站点,用于加入可直连列表
# _ref
#   请求 referer,不包括 `https://example.com/-----` 部分
# _mode
#   前端 request.mode 属性
# _type
#   前端 request.destination 属性
#
log_format              log_proxy escape=none
  '02	'
  '$time_iso8601	'
  '$_origin_id	'
  '$_ver	'
  '$remote_addr	'
  '$_level	'
  '$_switched	'
  '$upstream_cache_status	'
  '$request_time	'
  '$request_length	'
  '$bytes_sent	'
  '$request_method	'
  '$_url	'
  '$status	'
  '$_bodyhash	'
  '$upstream_http_access_control_allow_origin	'
  '$http_user_agent	'
  '$_ref	'
  '$_mode	'
  '$_type'
;

log_format              log_www escape=none
  '01	'
  '$time_iso8601	'
  '$remote_addr	'
  '$request_time	'
  '$request_method	'
  '$uri	'
  '$http_host	'
  '$status	'
  '$http_user_agent'
;


================================================
FILE: lua/http-body-hash.lua
================================================
-- ngx.arg[1] => chunk
-- ngx.arg[2] => eof


-- 大文件只返回首块 hash(用户从廉价带宽获取内容)
if ngx.ctx._switched then
  local chunk = ngx.arg[1]
  ngx.arg[1] = #chunk .. ',' .. ngx.crc32_long(chunk)
  ngx.arg[2] = true
  return
end


-- 计算 HTTP 返回数据的 hash(用于统计)
if ngx.ctx._sha256 == nil then
  local resty_sha256 = require 'resty.sha256'
  ngx.ctx._sha256 = resty_sha256:new()
end

if ngx.arg[2] then
  local digest = ngx.ctx._sha256:final()
  digest = digest:sub(1, 16)

  local str = require 'resty.string'
  ngx.var._bodyhash = str.to_hex(digest)
else
  ngx.ctx._sha256:update(ngx.arg[1])
end

================================================
FILE: lua/http-dec-req-hdr.lua
================================================
-- 还原 HTTP 请求头
local hasRawRefer = false

local hdrs = ngx.req.get_headers()
local refer = hdrs['referer']
local query = refer:sub(refer:find('?', 10, true) + 1)
local param = ngx.decode_args(query)


for k, v in pairs(param) do
  if k:sub(1, 2) == '--' then
    k = k:sub(3)

    if k == 'ver' then
      ngx.var._ver = v
    elseif k == 'type' then
      ngx.var._type = v
    elseif k == 'mode' then
      ngx.var._mode = v
    elseif k == 'aceh' then
      ngx.ctx._acehOld = true
    elseif k == 'level' then
      ngx.var._level = v
      ngx.ctx._level = tonumber(v)
    end
  else
    ngx.req.set_header(k, v)

    if k == 'referer' then
      hasRawRefer = true
      ngx.var._ref = v
    end
  end
end

if not hasRawRefer then
  ngx.req.clear_header('referer')
end

-- 删除 URL 的 '/http/' 前缀
ngx.var._url = ngx.var.request_uri:sub(7)

================================================
FILE: lua/http-enc-res-hdr.lua
================================================
-- 功能:编码 HTTP 返回头
-- 阶段:header_filter_by_lua
-- 备注:
-- aceh = HTTP 返回头的 access-control-expose-headers 字段

-- 无论浏览器是否支持,aceh 始终包含 *
local expose = '*'

-- 该值为 true 表示浏览器不支持 aceh: *,需返回详细的头部列表
local detail = ngx.ctx._acehOld


local function addHdr(k, v)
  ngx.header[k] = v
  if detail then
    expose = expose .. ',' .. k
  end
end


local function flushHdr()
  if detail then
    if status ~= 200 then
      expose = expose .. ',--s'
    end
    -- 该字段不在 aceh 中,如果浏览器能读取到,说明支持 * 通配
    ngx.header['--t'] = '1'
  end

  ngx.header['access-control-expose-headers'] = expose
  ngx.header['access-control-allow-origin'] = '*'

  local status = ngx.status

  -- 前端优先使用该字段作为状态码
  if status ~= 200 then
    ngx.header['--s'] = status
  end

  -- 保留原始状态码,便于控制台调试
  -- 例如 404 显示红色,如果统一设置成 200 则没有颜色区分
  -- 需要转义 30X 重定向,否则不符合 cors 标准
  if
    status == 301 or
    status == 302 or
    status == 303 or
    status == 307 or
    status == 308
  then
    status = status + 10
  end
  ngx.status = status
end


local function nodeSwitched()
  local status = ngx.status
  if status ~= 200 and status ~= 206 then
    return false
  end

  local level = ngx.ctx._level
  if level == nil or level == 0 then
    return false
  end

  if ngx.req.get_method() ~= 'GET' then
    return false
  end

  if ngx.header['set-cookie'] ~= nil then
    return false
  end

  local resLenStr = ngx.header['content-length']
  if resLenStr == nil then
    return false
  end

  -- 小于 400KB 的资源不走加速
  local resLenNum = tonumber(resLenStr)
  if resLenNum == nil or resLenNum < 1000 * 400 then
    return false
  end


  local addr = ngx.var.upstream_addr or ''
  local etag = ngx.header['etag'] or ''
  local last = ngx.header['last-modified'] or ''

  local info = addr .. '|' .. resLenStr .. '|' .. etag .. '|' .. last

  -- clear all res headers
  local h, err = ngx.resp.get_headers()
  for k, v in pairs(h) do
    ngx.header[k] = nil
  end

  addHdr('--raw-info', info)
  addHdr('--switched', '1')

  ngx.header['cache-control'] = 'no-cache'
  ngx.var._switched = resLenStr
  ngx.ctx._switched = true

  flushHdr()
  return true
end

-- 节点切换功能,目前还在测试中(demo 中已开启)
-- if nodeSwitched() then
--  return
-- end


local h, err = ngx.resp.get_headers()
for k, v in pairs(h) do
  if
    -- 这些头有特殊意义,需要转义 --
    k == 'access-control-allow-origin' or
    k == 'access-control-expose-headers' or
    k == 'location' or
    k == 'set-cookie'
  then
    if type(v) == 'table' then
      -- 重复的字段,例如 Set-Cookie
      -- 转换成 1-Set-Cookie, 2-Set-Cookie, ...
      for i = 1, #v do
        addHdr(i .. '-' .. k, v[i])
      end
    else
      addHdr('--' .. k, v)
    end
    ngx.header[k] = nil

  elseif detail and
    -- 非简单头无法被 fetch 读取,需添加到 aceh 列表 --
    -- https://developer.mozilla.org/en-US/docs/Glossary/Simple_response_header
    k ~= 'cache-control' and
    k ~= 'content-language' and
    k ~= 'content-type' and
    k ~= 'expires' and
    k ~= 'last-modified' and
    k ~= 'pragma'
  then
    expose = expose .. ',' .. k
  end
end

-- 不缓存非 GET 请求
if ngx.req.get_method() ~= 'GET' then
  ngx.header['cache-control'] = 'no-cache'
end

flushHdr()


================================================
FILE: lua/ws-dec-req-hdr.lua
================================================
-- 功能:还原 WebSocket 的 HTTP 请求头
-- 阶段:access_by_lua
-- 备注:JS 无法设置 ws 的头部,因此信息存储于 query

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

================================================
FILE: mime.types
================================================

types {
    text/html                                        html htm shtml;
    text/css                                         css;
    text/xml                                         xml;
    image/gif                                        gif;
    image/jpeg                                       jpeg jpg;
    application/javascript                           js;
    application/atom+xml                             atom;
    application/rss+xml                              rss;

    text/mathml                                      mml;
    text/plain                                       txt;
    text/vnd.sun.j2me.app-descriptor                 jad;
    text/vnd.wap.wml                                 wml;
    text/x-component                                 htc;

    image/png                                        png;
    image/svg+xml                                    svg svgz;
    image/tiff                                       tif tiff;
    image/vnd.wap.wbmp                               wbmp;
    image/webp                                       webp;
    image/x-icon                                     ico;
    image/x-jng                                      jng;
    image/x-ms-bmp                                   bmp;

    font/woff                                        woff;
    font/woff2                                       woff2;

    application/java-archive                         jar war ear;
    application/json                                 json;
    application/mac-binhex40                         hqx;
    application/msword                               doc;
    application/pdf                                  pdf;
    application/postscript                           ps eps ai;
    application/rtf                                  rtf;
    application/vnd.apple.mpegurl                    m3u8;
    application/vnd.google-earth.kml+xml             kml;
    application/vnd.google-earth.kmz                 kmz;
    application/vnd.ms-excel                         xls;
    application/vnd.ms-fontobject                    eot;
    application/vnd.ms-powerpoint                    ppt;
    application/vnd.oasis.opendocument.graphics      odg;
    application/vnd.oasis.opendocument.presentation  odp;
    application/vnd.oasis.opendocument.spreadsheet   ods;
    application/vnd.oasis.opendocument.text          odt;
    application/vnd.openxmlformats-officedocument.presentationml.presentation
                                                     pptx;
    application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
                                                     xlsx;
    application/vnd.openxmlformats-officedocument.wordprocessingml.document
                                                     docx;
    application/vnd.wap.wmlc                         wmlc;
    application/x-7z-compressed                      7z;
    application/x-cocoa                              cco;
    application/x-java-archive-diff                  jardiff;
    application/x-java-jnlp-file                     jnlp;
    application/x-makeself                           run;
    application/x-perl                               pl pm;
    application/x-pilot                              prc pdb;
    application/x-rar-compressed                     rar;
    application/x-redhat-package-manager             rpm;
    application/x-sea                                sea;
    application/x-shockwave-flash                    swf;
    application/x-stuffit                            sit;
    application/x-tcl                                tcl tk;
    application/x-x509-ca-cert                       der pem crt;
    application/x-xpinstall                          xpi;
    application/xhtml+xml                            xhtml;
    application/xspf+xml                             xspf;
    application/zip                                  zip;

    application/octet-stream                         bin exe dll;
    application/octet-stream                         deb;
    application/octet-stream                         dmg;
    application/octet-stream                         iso img;
    application/octet-stream                         msi msp msm;

    audio/midi                                       mid midi kar;
    audio/mpeg                                       mp3;
    audio/ogg                                        ogg;
    audio/x-m4a                                      m4a;
    audio/x-realaudio                                ra;

    video/3gpp                                       3gpp 3gp;
    video/mp2t                                       ts;
    video/mp4                                        mp4;
    video/mpeg                                       mpeg mpg;
    video/quicktime                                  mov;
    video/webm                                       webm;
    video/x-flv                                      flv;
    video/x-m4v                                      m4v;
    video/x-mng                                      mng;
    video/x-ms-asf                                   asx asf;
    video/x-ms-wmv                                   wmv;
    video/x-msvideo                                  avi;
}


================================================
FILE: nginx/.gitignore
================================================
*
!logs
!.gitignore

================================================
FILE: nginx/logs/.gitignore
================================================
*
!.gitignore

================================================
FILE: nginx.conf
================================================
http {
  include                 log.conf;
  server {
    listen                8080;
    include               cert/cert.conf;
    include               api.conf;
    include               www.conf;
  }

  # https://nginx.org/en/docs/http/ngx_http_core_module.html
  resolver                1.1.1.1 ipv6=off;
  resolver_timeout        10s;

  keepalive_timeout       60;
  keepalive_requests      2048;
  server_tokens           off;
  underscores_in_headers  on;

  # https://nginx.org/en/docs/http/ngx_http_ssl_module.html
  ssl_protocols           TLSv1.2 TLSv1.3;
  ssl_ciphers             TLS13-CHACHA20-POLY1305-SHA256:TLS13-AES-256-GCM-SHA384:TLS13-AES-128-GCM-SHA256:EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH;
  ssl_session_cache       shared:SSL:30m;
  ssl_session_timeout     1d;
  ssl_prefer_server_ciphers on;

  # https://nginx.org/en/docs/http/ngx_http_limit_req_module.html
  limit_req_log_level     warn;
  limit_req_zone          $binary_remote_addr zone=reqip:16m rate=100r/s;
  limit_req               zone=reqip burst=200 nodelay;

  access_log              logs/proxy.log log_proxy buffer=64k flush=1s;

  # https://nginx.org/cn/docs/http/ngx_http_proxy_module.html
  # 1MB = 8000key
  proxy_cache_path        cache
    levels=1:2
    keys_zone=my_cache:32m
    max_size=20g
    inactive=6h
    use_temp_path=off
  ;
  proxy_http_version      1.1;
  proxy_ssl_server_name   on;

  proxy_buffer_size       16k;
  proxy_buffers           4 32k;
  proxy_busy_buffers_size 64k;
  proxy_send_timeout      30s;
  proxy_read_timeout      30s;
  proxy_connect_timeout   10s;

  lua_load_resty_core     off;

  map $http_origin $_origin_id {
    include               allowed-sites.conf;
  }
}

# https://nginx.org/en/docs/ngx_core_module.html
events {
  worker_connections      4096;
}

================================================
FILE: run.sh
================================================
#
# 该脚本封装 nginx 调用,可在任意位置执行
#
# 启动:./run.sh 
# 重启:./run.sh -s reload
# 关闭:./run.sh -s quit 
# 
NGX_BIN=~/openresty/nginx/sbin/nginx
CUR_DIR=$(cd `dirname $0` && pwd)

if [ $1 ]; then
  PARAM="-s $1"
fi

$NGX_BIN -c $CUR_DIR/nginx.conf -p $CUR_DIR/nginx $PARAM

================================================
FILE: setup-ipset.sh
================================================
#
# 该脚本用于禁止 jsporxy 用户访问内网,防止 SSRF 攻击
# 需要 root 权限运行,依赖 ipset 命令
#
if [[ $(iptables -L | grep "anti ssrf") ]]; then
  exit
fi

ipset create ngx-ban-dstip hash:net

iptables \
  -m comment --comment "anti ssrf" \
  -A OUTPUT \
  -p tcp --syn \
  -m owner --uid-owner jsproxy \
  -m set --match-set ngx-ban-dstip dst \
  -j REJECT

# https://en.wikipedia.org/wiki/Reserved_IP_addresses
REV_NET=(
  0.0.0.0/8
  10.0.0.0/8
  100.64.0.0/10
  127.0.0.0/8
  169.254.0.0/16
  172.16.0.0/12
  192.0.0.0/24
  192.0.2.0/24
  192.88.99.0/24
  192.168.0.0/16
  198.18.0.0/15
  198.51.100.0/24
  203.0.113.0/24
  224.0.0.0/4
  240.0.0.0/4
  255.255.255.255/32
)

for v in ${REV_NET[@]}; do
  ipset add ngx-ban-dstip $v
done

# 可屏蔽更多的网段:
# ipset add ngx-ban-dstip xxx

================================================
FILE: test/works.txt
================================================
ok

================================================
FILE: www/.gitignore
================================================
*
!README.md
!.gitignore

================================================
FILE: www/README.md
================================================
该目录存放首页静态资源,内容和 gh-pages 分支相同

================================================
FILE: www.conf
================================================
#
# 提供 www 目录的静态资源服务
#
include               mime.types;
sendfile              on;
charset               utf-8;

# 安装步骤多,节省的流量不多,暂时不开
# brotli_static         on;
# gzip_static           on;
gzip                  on;

log_not_found         off;
error_page            404 = /404.html;

location = /404.html {
  internal;
  root                ../www;

  # http 重定向到 https(忽略 localhost 或 IP 访问)
  access_by_lua_block {
    if ngx.var.scheme == 'https' then
      return
    end
    local host = ngx.var.host
    if host == 'localhost' then
      return
    end
    if ngx.re.match(host, [[^\d+\.\d+\.\d+\.\d+$]]) then
      return
    end
    local url = host .. ':8443' .. ngx.var.request_uri
    ngx.redirect('https://' .. url, 301)
  }

  # 永久重定向申请: https://hstspreload.org/
  more_set_headers
    'strict-transport-security: max-age=99999999; includeSubDomains; preload'
  ;
}

location / {
  access_log          logs/access.log log_www buffer=64k flush=1s;
  root                ../www;
  index               404.html;
}

# HTTPS 证书申请验证
location /.well-known/acme-challenge/ {
  access_log          logs/acme.log combined;
  root                ../acme;
}
Download .txt
gitextract_nki4u8cg/

├── .gitignore
├── LICENSE
├── README.md
├── acme/
│   ├── .well-known/
│   │   └── acme-challenge/
│   │       └── test.txt
│   └── README.md
├── allowed-sites.conf
├── api.conf
├── cf-worker/
│   ├── .eslintrc.json
│   ├── README.md
│   └── index.js
├── changelogs/
│   ├── README.md
│   ├── v0.0.1.md
│   └── v0.1.0.md
├── docs/
│   ├── blogs/
│   │   └── js-hook.md
│   ├── cert-auto.md
│   ├── cert-manual.md
│   └── setup.md
├── i.sh
├── log-svc/
│   ├── README.md
│   ├── backup/
│   │   └── README.md
│   ├── backup.sh
│   └── svc.sh
├── log.conf
├── lua/
│   ├── http-body-hash.lua
│   ├── http-dec-req-hdr.lua
│   ├── http-enc-res-hdr.lua
│   └── ws-dec-req-hdr.lua
├── mime.types
├── nginx/
│   ├── .gitignore
│   └── logs/
│       └── .gitignore
├── nginx.conf
├── run.sh
├── setup-ipset.sh
├── test/
│   └── works.txt
├── www/
│   ├── .gitignore
│   └── README.md
└── www.conf
Download .txt
SYMBOL INDEX (11 symbols across 1 files)

FILE: cf-worker/index.js
  constant ASSET_URL (line 6) | const ASSET_URL = 'https://etherdream.github.io/jsproxy'
  constant JS_VER (line 8) | const JS_VER = 10
  constant MAX_RETRY (line 9) | const MAX_RETRY = 1
  constant PREFLIGHT_INIT (line 12) | const PREFLIGHT_INIT = {
  function makeRes (line 26) | function makeRes(body, status = 200, headers = {}) {
  function newUrl (line 36) | function newUrl(urlStr) {
  function fetchHandler (line 55) | async function fetchHandler(e) {
  function httpHandler (line 91) | function httpHandler(req, pathname) {
  function proxy (line 171) | async function proxy(urlObj, reqInit, acehOld, rawLen, retryTimes) {
  function isYtUrl (line 261) | function isYtUrl(urlObj) {
  function parseYtVideoRedir (line 273) | async function parseYtVideoRedir(urlObj, newLen, res) {
Condensed preview — 37 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (54K chars).
[
  {
    "path": ".gitignore",
    "chars": 3,
    "preview": "._*"
  },
  {
    "path": "LICENSE",
    "chars": 1067,
    "preview": "MIT License\n\nCopyright (c) 2019 EtherDream\n\nPermission is hereby granted, free of charge, to any person obtaining a copy"
  },
  {
    "path": "README.md",
    "chars": 3028,
    "preview": "\n# 更新\n\n* 2019-07-24 [v0.1.0](https://github.com/EtherDream/jsproxy/blob/master/changelogs/v0.1.0.md) 发布,主要修复了缓存失效的问题。网络接"
  },
  {
    "path": "acme/.well-known/acme-challenge/test.txt",
    "chars": 2,
    "preview": "ok"
  },
  {
    "path": "acme/README.md",
    "chars": 66,
    "preview": "该目录存放 HTTPS 证书申请时的 challenge(由 acme.sh 写入),用于 Let's Encrypt 服务器的验证"
  },
  {
    "path": "allowed-sites.conf",
    "chars": 481,
    "preview": "#\n# 授权哪些站点可使用本服务,防止外链\n#\n# 本服务会校验 HTTP 请求头 origin 字段,如果不在该列表,则拒绝代理\n# 每个 URL 对应一个短别名,用于日志记录\n# 注意 URL 不包含路径部分(结尾没有 /)\n#\nhtt"
  },
  {
    "path": "api.conf",
    "chars": 2149,
    "preview": "set                   $_level     '';\nset                   $_switched  '';\nset                   $_url       '';\nset   "
  },
  {
    "path": "cf-worker/.eslintrc.json",
    "chars": 386,
    "preview": "{\n  \"env\": {\n    \"browser\": true,\n    \"es6\": true,\n    \"serviceworker\": true\n  },\n  \"extends\": \"eslint:recommended\",\n  \""
  },
  {
    "path": "cf-worker/README.md",
    "chars": 743,
    "preview": "使用 CloudFlare Worker 免费部署\n\n\n# 简介\n\n`CloudFlare Worker` 是 CloudFlare 的边缘计算服务。开发者可通过 JavaScript 对 CDN 进行编程,从而能灵活处理 HTTP 请求。"
  },
  {
    "path": "cf-worker/index.js",
    "chars": 6268,
    "preview": "'use strict'\n\n/**\n * static files (404.html, sw.js, conf.js)\n */\nconst ASSET_URL = 'https://etherdream.github.io/jsproxy"
  },
  {
    "path": "changelogs/README.md",
    "chars": 360,
    "preview": "# 完整更新日志\n\n* 2019-06-11 前端脚本调整,首页可离线访问(如果长时间加载中,尝试多刷新几次或者隐身模式访问)\n\n* 2019-05-30 更新 cfworker,对 ytb 视频进行了优化(推荐选 1080p+,不会增加服"
  },
  {
    "path": "changelogs/v0.0.1.md",
    "chars": 777,
    "preview": "虽然该版本仍为概念演示状态,但相比[最初版本](https://github.com/EtherDream/jsproxy/tree/first-ver),有了很大变化:\n\n# 不再使用子域名\n\n使用子域名编码目标域名(例如 gg.jspr"
  },
  {
    "path": "changelogs/v0.1.0.md",
    "chars": 1442,
    "preview": "# v0.1.0\n\n## 更新内容\n\n* 后端代理以及 cfworker 接口调整,修复缓存失效的问题\n\n* 前端增加缓存记录,提高浏览器缓存命中率\n\n* 前端增加 Cookie 持久化\n\n* 前端增加 CORS 站点直连功能\n\n* 配置调"
  },
  {
    "path": "docs/blogs/js-hook.md",
    "chars": 7003,
    "preview": "《基于 JS Hook 技术,打造最先进的在线代理》\n\n\n# 什么是在线代理\n\n所谓在线代理,就类似本项目的演示,就可通过某个网站访问另一个网站(通常无法直接访问)。不用安装任何插件,不用修改任何配置,仅仅打开一个网页即可。\n\n类似的网站,"
  },
  {
    "path": "docs/cert-auto.md",
    "chars": 1306,
    "preview": "# 自动申请 HTTPS 证书\n\n1.转发 80 端口到 8080 端口(需要 root 权限)\n\n```bash\niptables -t nat -I PREROUTING 1 -p tcp --dport 80 -j REDIRECT "
  },
  {
    "path": "docs/cert-manual.md",
    "chars": 1108,
    "preview": "# 手动申请 HTTPS 证书\n\n在线申请:https://www.sslforfree.com\n\n\n## 方案 1 —- 通过 80 端口验证\n\n前提条件:公网 IP 能访问 80 端口,设备需要 root 权限\n\n1.输入 `服务器IP"
  },
  {
    "path": "docs/setup.md",
    "chars": 1992,
    "preview": "# 手动安装\n\n## 创建用户\n\n新建一个名为 `jsproxy` 用户(`nobody` 组),并切换:\n\n```bash\ngroupadd nobody\nuseradd jsproxy -g nobody --create-home\n\n"
  },
  {
    "path": "i.sh",
    "chars": 4931,
    "preview": "#!/usr/bin/env bash\n\n{ # this ensures the entire script is downloaded #\n\nJSPROXY_VER=0.1.0\nOPENRESTY_VER=1.15.8.1\n\nSRC_U"
  },
  {
    "path": "log-svc/README.md",
    "chars": 133,
    "preview": "nginx 日志备份服务\n\n## 说明\n\nnginx 长时间运行会导致日志文件过大,该服务定期备份日志到 `backup` 目录,并进行压缩。\n\n\n## 启动\n\n```bash\n./svc.sh &\n```\n\n使用 `jsproxy` 用户"
  },
  {
    "path": "log-svc/backup/README.md",
    "chars": 13,
    "preview": "该目录存放临时备份的日志。"
  },
  {
    "path": "log-svc/backup.sh",
    "chars": 782,
    "preview": "#!/usr/bin/env bash\n# 功能:备份 nginx 日志到 backup 目录\n\nSVC_DIR=/home/jsproxy/server\nLOG_DIR=$SVC_DIR/nginx/logs\nDST_DIR=$SVC_D"
  },
  {
    "path": "log-svc/svc.sh",
    "chars": 163,
    "preview": "#!/usr/bin/env bash\n# 功能:定时调用 backup.sh\n\necho \"log svc running\"\nCUR_DIR=$(cd `dirname $0` && pwd)\n\n# 也可用 crontab\nwhile t"
  },
  {
    "path": "log.conf",
    "chars": 1169,
    "preview": "#\n# 日志格式定义\n# https://nginx.org/en/docs/http/ngx_http_log_module.html\n#\n# 分隔: tab (\\t)\n# 前缀: 格式版本。格式变化时递增,方便解析\n# 备注:\n# _o"
  },
  {
    "path": "lua/http-body-hash.lua",
    "chars": 580,
    "preview": "-- ngx.arg[1] => chunk\n-- ngx.arg[2] => eof\n\n\n-- 大文件只返回首块 hash(用户从廉价带宽获取内容)\nif ngx.ctx._switched then\n  local chunk = ng"
  },
  {
    "path": "lua/http-dec-req-hdr.lua",
    "chars": 841,
    "preview": "-- 还原 HTTP 请求头\nlocal hasRawRefer = false\n\nlocal hdrs = ngx.req.get_headers()\nlocal refer = hdrs['referer']\nlocal query ="
  },
  {
    "path": "lua/http-enc-res-hdr.lua",
    "chars": 3113,
    "preview": "-- 功能:编码 HTTP 返回头\n-- 阶段:header_filter_by_lua\n-- 备注:\n-- aceh = HTTP 返回头的 access-control-expose-headers 字段\n\n-- 无论浏览器是否支持,a"
  },
  {
    "path": "lua/ws-dec-req-hdr.lua",
    "chars": 294,
    "preview": "-- 功能:还原 WebSocket 的 HTTP 请求头\n-- 阶段:access_by_lua\n-- 备注:JS 无法设置 ws 的头部,因此信息存储于 query\n\nlocal query, err = ngx.req.get_uri"
  },
  {
    "path": "mime.types",
    "chars": 5231,
    "preview": "\ntypes {\n    text/html                                        html htm shtml;\n    text/css                              "
  },
  {
    "path": "nginx/.gitignore",
    "chars": 19,
    "preview": "*\n!logs\n!.gitignore"
  },
  {
    "path": "nginx/logs/.gitignore",
    "chars": 13,
    "preview": "*\n!.gitignore"
  },
  {
    "path": "nginx.conf",
    "chars": 1805,
    "preview": "http {\n  include                 log.conf;\n  server {\n    listen                8080;\n    include               cert/cer"
  },
  {
    "path": "run.sh",
    "chars": 259,
    "preview": "#\n# 该脚本封装 nginx 调用,可在任意位置执行\n#\n# 启动:./run.sh \n# 重启:./run.sh -s reload\n# 关闭:./run.sh -s quit \n# \nNGX_BIN=~/openresty/nginx"
  },
  {
    "path": "setup-ipset.sh",
    "chars": 752,
    "preview": "#\n# 该脚本用于禁止 jsporxy 用户访问内网,防止 SSRF 攻击\n# 需要 root 权限运行,依赖 ipset 命令\n#\nif [[ $(iptables -L | grep \"anti ssrf\") ]]; then\n  ex"
  },
  {
    "path": "test/works.txt",
    "chars": 2,
    "preview": "ok"
  },
  {
    "path": "www/.gitignore",
    "chars": 24,
    "preview": "*\n!README.md\n!.gitignore"
  },
  {
    "path": "www/README.md",
    "chars": 29,
    "preview": "该目录存放首页静态资源,内容和 gh-pages 分支相同"
  },
  {
    "path": "www.conf",
    "chars": 1157,
    "preview": "#\n# 提供 www 目录的静态资源服务\n#\ninclude               mime.types;\nsendfile              on;\ncharset               utf-8;\n\n# 安装步骤多"
  }
]

About this extraction

This page contains the full source code of the EtherDream/jsproxy GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 37 files (48.3 KB), approximately 18.9k tokens, and a symbol index with 11 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!