使用 Cloudflare Workers 反代 gist

我经常用 GitHub 的 gist 服务来保存一些比较优秀的代码片段、配置等等,但是苦于 gist 在国内遭受了 DNS 污染,访问太不便利,所以一直在寻求一个类似 jsdelivr 加速 GitHub Repo 的方式,能够避免修改 host 直接访问 gist

可行的方式

  1. raw.githack.com
  2. 反代加速

首先说说第一种,raw.githack.com 确实可以加速 gist,而且可以加速 repo。
使用方式很简单

但是这种方式有一点弊端,就是不方便发表永链,当首次访问时,githack 会将内容缓存在 cloudflare 长达一年。当链接内容变化时,不会及时刷新,只适合发布永久内容,或者分版本发布。

第二种方式就是我经过一段时间摸索并决定采用的方式

Cloudflare Workers

Workers 的工作原理就是最近几年火热的 serverless。网站管理员不再一定需要一个服务器,只需要将对应的 Function 托管在 Workers 上,当用户访问网站时就会执行对应的 Function,值得一提的是 Cloudflare Workers 本质上只支持 JS(其他语言通过编译成 js 来执行)。

前期准备

  1. cloudflare 的帐号
  2. 域名,并且托管到 cloudflare

创建 worker

  1. 登录帐号到下图界面

    20201227143715

  2. 然后进入 Workers

    20201227143837

  3. 点击创建 worker

    20201227144011

  4. 这时候会进入如下界面

    20201227144908

反代加速 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 了,理论上是可以反代所有网站的,如果有需求的话,各位请自行修改代码~

注:

  1. project 是创建的 worker 的名称
  2. subdomain 是注册 workers 是输入的名字

自定义域名(可选)

经过上面的步骤,我们已经可以使用类似于 test.baidu.workers.dev 这样的域名使用触发 worker 了。但是如果需要使用自定义域名代替上述域名的话,还需要额外设置

ps:刚开始我以为直接在 DNS 中 CNAME 到 workers.dev 就行,但是实际操作之后发现是不可以的。

例如我们需要配置一个 test.jiz4oh.com 域名来使用 worker

  1. 配置 DNS 解析

    20201227180432

  2. 设置 worker 路由

    20201227180558

    20201227180837

  3. 等待数分钟,此时就可以访问 test.jiz4oh.com 来使用 worker 了