利用cloudflare works边缘计算搭建在线网页代理

今天看到阮一峰老师的Twitter发的“关于Cloudflare 正式发布 workers 功能”,搜索了一下关于 workers 功能使用教程,找了一篇文章(Xiaomage’s Blog 利用cloudflare works边缘计算搭建在线网页代理)还不错,先码后续继续研究。

工具

  1. 开源项目jsproxy
  2. 一个cloudflare账号
  3. 一个Github账号,或者一台服务器+域名

一点说明:

要利用cloudflare works边缘计算搭建在线网页代理,需要用到大神EtherDream的开源项目jsproxy

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

你可以使用Github pages服务,快速搭建起页面前端,从而做到真正的serverless。当然,如果你有一台服务器+域名,你也可以把服务器放在自己的服务器上。这一步只是给cloudflare一个回源服务器,用户访问的一切流量都要经过cloudflaer服务器,而不是Github或者你的服务器。所以服务器位置并不会影响网页代理的速度,而是用户到所连接到的cloudflare服务器的速度。建议使用Github pages的服务即可,下面的教程也将演示利用Github pages搭建此代理的过程。

操作步骤

GitHub方面

  1. 登录你的Github账号,fork jsproxy项目到你的仓库中
  2. 进入你fork的jsproxy项目的setting中,启用下方的Github pages,其中项目分支选择gh-pages branch分支即可,配置见下图:
    图1
  3. 进入你fork的jsproxy项目的source(源代码)中,切换到gh-pages branch分支,新建一个index.html,内容为空即可。
  4. 访问你Github pages服务生成的网址,如果为白屏,没有报404错误的话,回到刚才的源代码,将index.html删除即可。3、4两部可以在Github里直接操作,也可以用git命令拉取到本地进行修改,这里不再赘述。
  5. 如果你想自定义页面的样式,可以修改gh-pages branch分支中的index_v3.html

Cloudflare方面

  1. https://dash.cloudflare.com/登录你的cloudflare账号,点击右侧大大的workers进入workers控制面板。
  2. 第一次使用workers功能,需要完成一个新手引导教程。第一步,需要选择一个cloudflare提供的*.workers.dev的二级域名,根据自己的喜好填写,按照提示next就可以了。
  3. 新手教程第二步会让你选择plan,我们白嫖党当然要选择Free Plan啦,每天有100000个请求配额,个人使用绰绰有余。
  4. 下一步可能要验证邮箱,到注册cloudflare的邮箱里点击链接激活一下就可以。
  5. 完成新手引导后,回到workers面板,点击蓝色的Create a Worker按钮,新建一个worker。
  6. 这时会打开一个带有代码编辑器的新标签页,在左侧选择Script标签,粘贴以下内容:注意在第六行里填写好你Github pages的网址,即https://xxx.github.io/jsproxy
    图2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
'use strict'

/**
* static files (404.html, sw.js, conf.js)
*/
const ASSET_URL = 'https://xxx.github.io/jsproxy'//这里填写你Github pages的网址!

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
}

之后点击下方的save and deploy部署就生效啦!记下cloudflare分配给你的workers.dev的三级域名,这就是你部署好的在线代理网址。

如果你正巧有托管在Cloudflare或它旗下的Partner的话,你可以就可以自定义网页代理的域名,不必记下冗长的三级域名,方法如下:

  1. 进入你的域名控制台,点击控制台顶部的workers标签,进入对应域名的workers设置。
  2. 点击右侧的Add route按钮,部署一条新规则。
  3. 在弹出的对话框中:Route中填写 example.yourwebstie.com/* ,其中example是网页代理的二级域名,可以自定义,Worker选择你刚刚部署的worker。
    图3
  4. 修改example.yourwebstie.com的DNS记录为cname记录,这条cname记录指向cloudflare分配给你的workers.dev下刚刚部署好的workers站点。

Then, 在你使用没有翻墙软件的电脑时,也能利用这个网页代理随心上谷歌看油管咯~enjoy it!

坚持原创技术分享,您的支持将鼓励我继续创作!