使用 Cloudflare Workers 反代 gist
我经常用 GitHub 的 gist 服务来保存一些比较优秀的代码片段、配置等等,但是苦于 gist 在国内遭受了 DNS 污染,访问太不便利,所以一直在寻求一个类似 jsdelivr 加速 GitHub Repo 的方式,能够避免修改 host 直接访问 gist
可行的方式
- raw.githack.com
- 反代加速
首先说说第一种,raw.githack.com 确实可以加速 gist,而且可以加速 repo。
使用方式很简单
- 将 gist.githubusercontent.com 替换为 gistcdn.githack.com
例:http://gistcdn.githack.com/jiz4oh/189ed96ca1607b3ddf07bd64bcb459cd/raw/trojan.json - 将 raw.githubusercontent.com 替换为 rawcdn.githack.com
例:https://rawcdn.githack.com/jiz4oh/vim/master/vimrc
但是这种方式有一点弊端,就是不方便发表永链,当首次访问时,githack 会将内容缓存在 cloudflare 长达一年。当链接内容变化时,不会及时刷新,只适合发布永久内容,或者分版本发布。
第二种方式就是我经过一段时间摸索并决定采用的方式
Cloudflare Workers
Workers 的工作原理就是最近几年火热的 serverless。网站管理员不再一定需要一个服务器,只需要将对应的 Function 托管在 Workers 上,当用户访问网站时就会执行对应的 Function,值得一提的是 Cloudflare Workers 本质上只支持 JS(其他语言通过编译成 js 来执行)。
前期准备
- cloudflare 的帐号
- 域名,并且托管到 cloudflare
创建 worker
登录帐号到下图界面
然后进入 Workers
点击创建 worker
这时候会进入如下界面
反代加速 gist 代码如下:
// 需要反代的地址
const upstream = 'gist.github.com'
// 反代地址的子路径
const upstreamPath = '/'
// 反代网站的移动端域名
const upstreamMobile = 'gist.github.com'
// 是否使用 https
const useHttps = true
// 禁止使用该 worker 的国家代码
const blockedRegion = ['KP', 'SY', 'PK', 'CU']
// 禁止使用该 worker 的 ip 地址
const blockedIp = ['0.0.0.0', '127.0.0.1']
// 是否关闭缓存
const disableCache = false
// 替换条件
const contentTypes = [
'text/plain',
'text/html'
]
// 反代网站中其他需要被替换的地址
const replaceDict = {
'$upstream': '$workerDomain',
}
addEventListener('fetch', event => {
event.respondWith(handleRequest(event.request))
})
/**
* Respond to the request
* @param {Request} request
*/
async function handleRequest(request) {
const region = request.headers.get('cf-ipcountry') || '';
const ip = request.headers.get('cf-connecting-ip');
if (blockedRegion.includes(region.toUpperCase())) {
return new Response('Access denied: WorkersProxy is not available in your region yet.', {
status: 403
});
}
if (blockedIp.includes(ip)) {
return new Response('Access denied: Your IP address is blocked by WorkersProxy.', {
status: 403
});
}
const upstreamDomain = isMobile(request.headers.get('user-agent')) ? upstreamMobile : upstream;
// 构建上游请求地址
let url = new URL(request.url);
const workerDomain = url.host;
url.protocol = useHttps ? 'https:' : 'http';
url.pathname = url.pathname === '/' ? upstreamPath : upstreamPath + url.pathname;
url.host = upstreamDomain;
// 构建上游请求头
const newRequestHeaders = new Headers(request.headers);
newRequestHeaders.set('Host', upstreamDomain);
newRequestHeaders.set('Referer', url.protocol + '//' + workerDomain);
// 获取上游响应
const originalResponse = await fetch(url.href, {
method: request.method,
headers: newRequestHeaders
})
const connectionUpgrade = newRequestHeaders.get("Upgrade");
if (connectionUpgrade && connectionUpgrade.toLowerCase() === "websocket") {
return originalResponse;
}
let originalResponseClone = originalResponse.clone();
// 构建响应头
let responseHeaders = originalResponseClone.headers;
let newResponseHeaders = buildResponseHeaders(responseHeaders);
if (newResponseHeaders.get("x-pjax-url")) {
newResponseHeaders.set("x-pjax-url", responseHeaders.get("x-pjax-url").replace("//" + upstreamDomain, "//" + workerDomain));
}
// 构建响应体
let originalText;
const contentType = newResponseHeaders.get('content-type');
if (contentType != null) {
const types = contentType.replace(' ','').split(';')
if (types.includes('charset=utf-8')){
for (let i of contentTypes) {
if (types.includes(i)){
originalText = await replaceResponseText(originalResponseClone, upstreamDomain, workerDomain);
break
}
}
}
} else {
originalText = originalResponseClone.body
}
return new Response(originalText, {
status: originalResponseClone.status,
headers: newResponseHeaders
})
}
function isMobile(userAgent) {
userAgent = userAgent || ''
let agents = ["Android", "iPhone", "SymbianOS", "Windows Phone", "iPad", "iPod"];
for (let v = 0; v < agents.length; v++) {
if (userAgent.indexOf(agents[v]) > 0) {
return true;
}
}
}
function buildResponseHeaders(originalHeaders) {
const result = new Headers(originalHeaders);
if (disableCache) {
result.set('Cache-Control', 'no-store');
}
result.set('access-control-allow-origin', '*');
result.set('access-control-allow-credentials', true);
result.delete('content-security-policy');
result.delete('content-security-policy-report-only');
result.delete('clear-site-data');
return result
}
async function replaceResponseText(response, upstreamDomain, workerDomain) {
let text = await response.text()
const placeholders = {
"$upstream": upstreamDomain,
"$workerDomain": workerDomain
}
for (let origin in replaceDict) {
let target = replaceDict[origin]
origin = placeholders[origin] || origin
target = placeholders[target] || target
const re = new RegExp(origin, 'g')
text = text.replace(re, target);
}
return text;
}
然后点击部署,就可以通过 [project].[subdomain].workers.dev
绕墙访问 gist 了,理论上是可以反代所有网站的,如果有需求的话,各位请自行修改代码~
注:
- project 是创建的 worker 的名称
- subdomain 是注册 workers 是输入的名字
自定义域名(可选)
经过上面的步骤,我们已经可以使用类似于 test.baidu.workers.dev
这样的域名使用触发 worker 了。但是如果需要使用自定义域名代替上述域名的话,还需要额外设置
ps:刚开始我以为直接在 DNS 中 CNAME 到 workers.dev 就行,但是实际操作之后发现是不可以的。
例如我们需要配置一个 test.jiz4oh.com 域名来使用 worker
配置 DNS 解析
设置 worker 路由
等待数分钟,此时就可以访问
test.jiz4oh.com
来使用 worker 了