- 后端
- 网络
- 通信
【免费下载链接】jsproxy
An online proxy based on ServiceWorker
本文围绕 jsproxy 仓库中的cf-worker模块展开:先按 cf-worker/README.md 的步骤在 CloudFlare 上免费部署一套无需自有服务器的在线代理节点,再逐段剖析 cf-worker/index.js 的路由分发、请求头还原、CORS 转义与 YouTube 视频重定向修复等核心实现,并对照 nginx 侧的 api.conf 与 OpenResty 脚本说明两条代理链路如何保持协议一致。读完你可以独立完成一个零服务器成本的代理节点部署,并理解它与服务端 nginx 版在接口协议上的对应关系。
一、CloudFlare Worker 简介
CloudFlare Worker是 CloudFlare 提供的边缘计算服务。开发者可通过 JavaScript 对 CDN 进行编程,从而灵活处理 HTTP 请求,使得很多任务可以在 CDN 上完成,而无需自己的服务器参与。
对 jsproxy 而言,这正是它"无服务器版"(cfworker 节点)的载体:把代理逻辑压缩进一个单文件 Worker 脚本,直接跑在 CloudFlare 的边缘节点上。仓库根目录 README 的更新日志(README.md)中,2019-06-22 发布的"cfworker 无服务器版"即指本模块,并建议长期使用演示服务的读者迁移到该版本。
jsproxy 的整体架构是"前端 ServiceWorker + 后端纯转发":浏览器中的 ServiceWorker 拦截并改写页面请求,把目标 URL 打包进Referer请求头、以/http/目标URL的形式打到代理节点;节点(nginx 版或 cf-worker 版)只负责转发流量并改写 HTTP 头。cf-worker 就是这条链路中"可完全托管在 CDN 上"的节点实现。
二、部署步骤
部署入口是 CloudFlare 官方的 Workers 控制台(workers.cloudflare.com,可从 CloudFlare 官网进入)。操作流程如下:
- 注册并登录 CloudFlare 账号;
- 点击
Start building,为自己的 Worker 取一个子域名; - 选择
Create a Worker创建新 Worker; - 把本仓库 cf-worker/index.js 的全部内容复制到控制台左侧的代码框;
- 点击
Save and deploy完成部署。
部署成功后,右侧应显示 Worker 的首页。收藏地址栏中的https://xxxx.子域名.workers.dev形式的访问地址,以后可直接访问。
部署后的可用端点
Worker 的路由逻辑集中在 fetchHandler 中,与仓库 test/works.txt(内容为ok)的命名相呼应,部署后可验证以下路径:
| 路径 | 行为 | 源码依据 |
|---|---|---|
/works | 返回文本it works,用于连通性自检 | cf-worker/index.js#L78-L79 |
/http/... | HTTP(S) 代理主接口,见后文解析 | cf-worker/index.js#L69-L71 |
/http | 返回"请更新 cfworker 到最新版本!",作为版本探测接口 | cf-worker/index.js#L73-L75 |
/ws | 返回not support(400),WebSocket 代理尚未实现 | cf-worker/index.js#L76-L77 |
| 其他路径 | 作为静态资源从ASSET_URL反向代理 | cf-worker/index.js#L80-L83 |
另外,所有以http:协议的请求会被 301 重定向到https:,并附带strict-transport-security头(见 cf-worker/index.js#L61-L67),因此实际使用时统一走 HTTPS。
三、计费与扩容
回到控制台的overview页面可以查看用量。关键计费事实(以 cf-worker/README.md 为准):
- 免费版:每天 10 万次免费请求,对个人使用通常足够;
- 扩容方式一:注册多个 Worker(多个免费子域名),在前端
conf.js中配置多线路负载均衡,把流量摊到多个节点; - 扩容方式二:升级到 $5/月 的高级版本,每月可用 1000 万次请求,超出部分 $0.5/百万次请求。
多节点方案在 changelogs/v0.1.0.md 中有配套的权重设计:前端conf.js的node_map(节点 id 与节点主机)与node_default(默认节点)之外,v0.1.0 为每条线路增加了权重配置,"命中比例 = 当前值 / 总值"。演示案例中 cfworker 节点即采用"1 个收费版 + 多个免费版"的组合——由于免费版有访问频率限制,通过更低权重减少其负载。也就是说,用多个免费 Worker 堆量的策略是官方文档明确支持的用法。
四、修改配置:ASSET_URL 与自定义 conf.js
Worker 脚本顶部的常量直接决定前端资源从哪里加载:
// cf-worker/index.js const ASSET_URL = 'https://etherdream.github.io/jsproxy' const JS_VER = 10 const MAX_RETRY = 1ASSET_URL:默认情况下,Worker 首页所需的静态资源(404.html、sw.js、conf.js等,对应 nginx 版部署中的 www 目录内容)从https://etherdream.github.io/jsproxy反向代理。修改这个常量指向自己的静态资源站(如自己的 GitHub Pages),即可使用自定义的conf.js——也就是配置自己的node_map节点列表、node_default默认节点与线路权重。Worker 对非代理路径的处理就是一行return fetch(ASSET_URL + path)(cf-worker/index.js#L82),因此ASSET_URL站点必须能正常提供这些静态文件。JS_VER:当前脚本版本号(值为 10),会随响应头--ver下发给前端,用于前端判断代理端是否为最新版本(/http端点的"请更新 cfworker"提示即服务于这个探测机制)。MAX_RETRY:内容长度校验失败时的最大重试次数(后文详述)。
需要注意的是,v0.1.0 起后端代理与 cfworker 的接口做过调整以修复缓存失效问题(详见 changelogs/v0.1.0.md),服务端与 cfworker 的版本需配套,不要混用旧接口。
五、源码解析:一次 /http/ 请求的完整链路
cf-worker/index.js全文约 290 行,从源码结构看可分为入口分发、请求头还原、代理转发、响应头改写四层。
5.1 入口与全局错误处理
addEventListener('fetch', e => { const ret = fetchHandler(e) .catch(err => makeRes('cfworker error:\n' + err.stack, 502)) e.respondWith(ret) })所有请求统一进入 fetchHandler,任何未捕获异常都以 502 返回错误栈,便于排查。makeRes辅助函数(cf-worker/index.js#L26-L30)会给每个响应补上--ver版本头与access-control-allow-origin: *。
5.2 防循环依赖与 CORS preflight
httpHandler 开头有两道检查:
if (reqHdrRaw.has('x-jsproxy')) { return Response.error() }请求头中一旦带有x-jsproxy,说明流量来自 jsproxy 节点自身(nginx 版在转发前会注入proxy_set_header x-jsproxy 1,见 api.conf#L51),此时直接返回错误,防止 A→B 两个 jsproxy 节点互相代理形成环路。这与 nginx 版/http/location 中CIRCULAR_DEPENDENCY的检查逻辑一一对应(api.conf#L48-L50)。
其次是 OPTIONS 预检请求的处理:当方法为OPTIONS且携带access-control-request-headers时,直接用 PREFLIGHT_INIT 返回 204,允许全部常用方法与max-age: 1728000(20 天)的预检缓存,与 api.conf 中 /preflight 端点 的行为一致。
5.3 请求头还原:为什么参数藏在 Referer 里
这是整个代理协议最核心的设计。浏览器 fetch 对"非简单请求头"会触发 CORS preflight,而 preflight 的max-age是按 URL 记忆缓存的,jsproxy 每次请求的 URL(/http/目标URL)几乎都不同,预检缓存形同虚设。因此 v0.1.0 之后的方案是:把绝大部分请求头字段打包进Referer的 query 部分,因为Referer属于 CORS 安全头,不会触发 preflight。
Worker 侧的解析代码(cf-worker/index.js#L114-L143):
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') }规则可以概括为:
- query 中
--前缀的键是系统参数:--aceh标记前端浏览器不支持access-control-expose-headers: *通配符、需要回退到逐字段列表模式;--raw-info携带原始地址|长度|etag三段信息,用于节点切换时的内容校验; - 其余键是待还原的原始请求头:有值则
set,空值则delete(前端借此表达"原始请求没有该头"); - 若参数里没有
referer键,说明原始请求不带 referer,把代理请求自带的 referer 删掉。
这段逻辑与 nginx 版的 lua/http-dec-req-hdr.lua 大致相同(cf-worker/index.js#L112 的注释即指向它),只是 Worker 版只实现了--aceh与--raw-info两个系统参数,nginx 版还多解析--ver、--type、--mode、--level(节点切换等级)。
URL 还原处有一个针对 CloudFlare Worker 平台行为的修正(cf-worker/index.js#L145-L150):cfworker 会把路径中的//合并成/,导致https://变成https:/,所以用replace(/^(https?):\/+/, '$1://')把协议后的斜杠补回来,解析失败则返回invalid proxy url(403)。
5.4 转发与响应头改写
proxy 以redirect: 'manual'发起真实请求,不跟随重定向,并只对POST透传请求体。响应处理包含三块逻辑:
1)敏感头转义。四个对浏览器 CORS 语义有"特殊意义"的响应头会被改名为--前缀的版本再下发,防止被浏览器直接执行:
if (k === 'access-control-allow-origin' || k === 'access-control-expose-headers' || k === 'location' || k === 'set-cookie' ) { resHdrNew.set('--' + k, v) ... resHdrNew.delete(k) }nginx 版由 lua/http-enc-res-hdr.lua#L116-L148 完成同样的转义,且额外处理了重复头(多个Set-Cookie会被编码为1-set-cookie、2-set-cookie……)。前端 ServiceWorker 读到--前缀的头后还原为真实值,从而在浏览器内"重建"目标站点的响应。
2)--aceh回退模式。当请求带--aceh时,脚本会把所有非简单响应头(cache-control、content-type等六个浏览器默认可读的除外)逐一追加进access-control-expose-headers列表,并额外设置--t: 1作为"浏览器是否支持*通配"的探测标记——前端能否读到这个不属于 expose 列表的--t头,即判定是否支持通配。这与 lua/http-enc-res-hdr.lua#L21-L28 的注释逻辑完全对应。
3)内容长度校验与 YouTube 视频修复。若请求携带--raw-info中的期望长度rawLen,脚本会比对实际content-length:
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}`, ... }) }长度不一致时,parseYtVideoRedir 会尝试一个特定修复:仅当 URL 是 YouTube 视频流(host 以.googlevideo.com结尾且路径以/videoplayback开头,见 isYtUrl)且响应体小于 2KB 时,把响应体当作一个新的跳转 URL 解析并重试一次(MAX_RETRY = 1)。这可以推断是针对 YouTubevideoplayback接口偶发二次重定向、导致缓存长度不匹配的问题做的兜底。校验失败则返回 400,错误信息通过--error头下发(配合 api.conf 中 error 端点的 expose 配置 思路一致)。
4)状态码转义。与 nginx 版 lua/http-enc-res-hdr.lua#L42-L52 相同的处理:真实状态码写入--s头(非 200 时),而对外状态码保留原始值用于控制台调试(例如 404 会显示红色);但 301/302/303/307/308 会统一+10变成 311/312/313/317/318——按 CORS 标准 fetch 不允许跟随这类重定向,转义后前端可以自行处理(cf-worker/index.js#L242-L249)。最后脚本还会删除content-security-policy、content-security-policy-report-only、clear-site-data三个头,避免目标站点的 CSP 干扰代理页面。
六、与 nginx 服务端链路的对照
同一套协议在仓库中有两份实现,对照关系如下:
| 职责 | cf-worker 实现 | nginx 实现 |
|---|---|---|
| 代理入口 | /http/前缀分支(cf-worker/index.js#L69-L71) | location /http/(api.conf#L43-L74) |
| 防循环依赖 | 检测x-jsproxy头返回Response.error() | 重写为/error?msg=CIRCULAR_DEPENDENCY(api.conf#L48-L50) |
| 请求头还原 | httpHandler | lua/http-dec-req-hdr.lua(多解析--ver/--type/--mode/--level) |
| 响应头编码 | proxy | lua/http-enc-res-hdr.lua + lua/http-body-hash.lua |
| 缓存 | 无(依赖 CloudFlare 边缘缓存) | proxy_cache my_cache(api.conf#L61) |
| WebSocket | 未实现(/ws返回 400) | 已实现(api.conf#L77-L83 + lua/ws-dec-req-hdr.lua) |
| 外链白名单 | 未实现 | allowed-sites.conf限制可调用站点(api.conf#L44-L47) |
从源码结构看,Worker 版可以视为 nginx 版的"协议子集":它保留了 v0.1.0 新接口的全部关键要素(/http/目标URL路径式 URL、Referer 打包参数、--前缀转义头、--s状态码、30X 转义),但去掉了节点切换(nodeSwitched在 lua/http-enc-res-hdr.lua#L110-L113 中也仅处于注释测试状态)、缓存与白名单能力。
七、当前局限
cf-worker/README.md 明确列出了三个已知问题,与源码相互印证:
- WebSocket 代理尚未实现:
/ws路径直接返回not support(cf-worker/index.js#L76-L77)。CloudFlare Worker 的 fetch 事件模型本身也不支持长连接透传,这类站点需要回退到 nginx 节点(api.conf#L77-L83)。 - 外链限制尚未实现:nginx 版可通过 allowed-sites.conf 限定可调用代理的
github.io站点,Worker 版对所有来源一视同仁,access-control-allow-origin恒为*,因此公开部署的 Worker 节点应视为不对外提供服务的私有加速节点,流量风险由节点主自担。 - 未充分测试:作者自述以后再完善,生产环境使用前建议先用
/works端点和真实站点做充分验证。
八、小结
cf-worker 方案把 jsproxy 的节点侧压缩成了单个可托管文件:约百行的部署流程换来一个每天 10 万请求免费额度的 HTTPS 代理节点;ASSET_URL一个常量即可切换前端配置来源,实现自定义conf.js与多线路负载均衡。其核心工程价值在于展示了如何用"Referer 参数打包 +--前缀头转义 + 状态码转义"这一套协议,在不触发 CORS preflight、不执行远端敏感响应头的前提下,让浏览器里的 ServiceWorker 完整接管目标站点的请求与响应语义。由于 WebSocket 与外链白名单暂未实现,对完整功能有要求的场景仍应选择 nginx 服务端部署(参考 docs/setup.md)。
- 后端
- 网络
- 通信
【免费下载链接】jsproxy
An online proxy based on ServiceWorker
相关推荐
jsproxy 部署与运维实战:用 nginx + Service Worker 构建低开销在线代理
jsproxy 部署与运维实战:用 nginx + Service Worker 构建低开销在线代理 本文基于 jsproxy 仓库根目录 README.md
后端网络通信tg-ws-proxy 免费免域名代理指南:Cloudflare Worker 部署与 `--cfproxy-worker-domain` 接入
tg ws proxy 免费免域名代理指南:Cloudflare Worker 部署与 cfproxy worker domain 接入 导读 本文讲解 tg
tg-ws-proxy 部署 Cloudflare Worker 免费代理:从零搭建 WebSocket 中转端
tg ws proxy 部署 Cloudflare Worker 免费代理:从零搭建 WebSocket 中转端 本文档基于 tg ws proxy 官方文档
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考