1. 程式人生 > 實用技巧 >利用cloudflare-works邊緣計算搭建線上網頁代理

利用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賬號,forkjsproxy專案到你的倉庫中
  2. 進入你fork的jsproxy專案的setting中,啟用下方的Github pages,其中專案分支選擇gh-pages branch分支即可,配置見下圖:
  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
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。
  4. 修改example.yourwebstie.com的DNS記錄為cname記錄,這條cname記錄指向cloudflare分配給你的workers.dev下剛剛部署好的workers站點。

Then, 在你使用沒有翻牆軟體的電腦時,也能利用這個網頁代理隨心上谷歌看油管咯~enjoy it!