Github的正确打开方式 分币不掏搭建镜像站

55次阅读
没有评论

引言

“ 问:打开 GitHub,一共分几步?”
“ 答:看命。”
幸运的时候,你能勉强打开主页,克隆下载项目像挤牙膏一样,永远卡在几十 kb,进度条和蜗牛有的一比;不幸的时候,你连主页都打不开,直接给你来个“无法访问此页面”
网上的方法也试过,什么修改 host、换 DNS 等等,无一列外几乎都是徒劳而返。直到我发现镜像站这玩意,可以完美的满足使用 Github 需求,不过多数都是公益项目,稳定性又成了问题。事已至此,何不自己搭建一个镜像站?鼠标带你一步步,从零开始、全程白嫖,一分钱不花搭建个 Github 镜像站!

前期工作

必要条件

  • 免费域名一个
  • Cloudflare 账号

免费域名申请

访问这个免费域名的 网站主页 ,点击右上角的Sign Up 注册账号,根据提示填入用户名邮箱等信息,最后勾选协议点击 注册账号

账号注册好后登录账号,控制台页面左侧导航点击 免费域名 ;免费域名管理平台页面,点击 注册新域名 ,弹出页面,根域名下拉建议选择 cc.cd,因为有的域名不支持托管到 Cloudflare;域名前缀根据自己喜好填写,前提是不能与已有的冲突,最后点击 确认注册,至此免费域名就申请注册好了,这个页面先不要关,后面还用的到!

注册 Cloudflare

访问 Cloudflare 官网主页,依次点击 Login 注册 打开注册页,如果页面不是中文,可以点击语言选项设置为中文。

注册页面,你可以直接使 Google、Apple 以及 Github 账号直接登录,也可以输入邮箱和密码进行注册,注册成功后会返回登录页,输入设置的密码直接登录,后台默认是英文的,我们可以将鼠标放在用户上,依次点击 Language 简体中文,设置后台为中文,至此,我们的 Cloudflare 也注册好了。

托管域名

为了方便我们接下来的操作,需要我们将申请的免费域名,托管到 Cloudflare。

Cloudflare 管理页面,左侧导航依次点击 域名 概览 ,域名概览页面依次点击 添加域名 连接域名 ;域名填入我们之前申请的免费域名,其他保持默认点击 继续 ;选择计划这里选择免费计划,接着点击 继续前往激活 ,弹出提示点击 确认,到了更新名称服务器页面,可以看见有两个名称服务器,保持页面不要动。

回到之前注册域名的页面,点击我们申请的域名后 管理域名 ,上方导航选中DNS 服务器,再点击 修改 DNS 服务器,我们复制 Cloudflare 的两条服务器信息,填入到DNS 服务器列表,最后点击保存设置。

再次回到 Cloudflare,网页下滑到最底部,点击 我已更新名称服务器 ,解析生效需要一会,当我们回到域名概览页面,看见我们的域名状态为 活动 的时候,说明免费域名成功托管到 Cloudflare 了,这也意味着前期工作已经完成!

搭建镜像站

部署 workers

现在到了本期教程的核心内容,就是利用 Cloudflare 的 workers,搭建 Gtihub 镜像站。原理很简单,Cloudflare 扮演反代角色,借其线路间接访问 Github。

Cloudflare 管理页面,左侧导航依次点击 计算 Workers 和 Pages 创建应用程序 从 Hello World! 开始 ,部署页面,可以根据自己喜好填入一个项目名,最后点击 部署

部署成功后会返回项目概述页面,点击右上角的 编辑代码 ,左侧代码编辑窗口,清空已有的所有代码,并粘贴如下代码,部分情况鼠标右键无法粘贴,我们可以使用快捷键Ctrl+V 进行粘贴操作。

/**
 * Gh-Mirror v4.0(稳定版)*
 * 支持:*   https:// 你的域名 /owner/repo                    → 仓库页
 *   https:// 你的域名 /owner/repo/blob/main/x.py     → 代码页
 *   https:// 你的域名 /owner/repo/raw/main/x.py      → 原始文件(A 形式直连)*   https:// 你的域名 /raw/owner/repo/main/x.py      → 原始文件(B 形式直连)*   https:// 你的域名 /proxy//<路径>      → 静态资源 /API
 *   git clone https:// 你的域名 /owner/repo.git       → git smart HTTP 代理
 */

const SCHEME = 'https';
const MAIN_HOST = 'github.com';
const RAW_HOST = 'raw.githubusercontent.com';

const USER_AGENT =
  'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0 Safari/537.36';

const PROXY_HOSTS = new Set([
  'github.com',
  'github.githubassets.com',
  'avatars.githubusercontent.com',
  'user-images.githubusercontent.com',
  'private-user-images.githubusercontent.com',
  'media.githubusercontent.com',
  'cloud.githubusercontent.com',
  'camo.githubusercontent.com',
  'objects.githubusercontent.com',
  'github-releases.githubusercontent.com',
  'raw.githubusercontent.com',
  'codeload.github.com',
  'gist.github.com',
  'api.github.com',
  'github-cloud.s3.amazonaws.com',
  'github-production-release-asset-2e65be.s3.amazonaws.com',
  'github-production-user-asset-6210df.s3.amazonaws.com',
  'github-production-repository-file-5c1aeb.s3.amazonaws.com',
]);

const STATIC_CACHE_HOSTS = new Set([
  'github.githubassets.com',
  'avatars.githubusercontent.com',
  'user-images.githubusercontent.com',
  'private-user-images.githubusercontent.com',
  'media.githubusercontent.com',
  'cloud.githubusercontent.com',
  'camo.githubusercontent.com',
]);

const GITHUB_S3_RE = /^github-[a-z0-9][a-z0-9-]*\.s3\.amazonaws\.com$/i;

const REWRITE_RE = /(https?:)?\/\/([a-z0-9.-]+)(\/[^\s"'<>()]*)?/gi;
const PROXY_PATH_RE = /^\/proxy\/([^/]+)(\/[\s\S]*)?$/;
const RAW_PATH_RE = /^\/raw\/([\s\S]+)$/;
const GITHUB_RAW_RE = /^\/([^/]+)\/([^/]+)\/raw\/([\s\S]+)$/;
const GITHUB_BLOB_RE = /^\/([^/]+)\/([^/]+)\/blob\/([\s\S]+)$/;

// git smart HTTP
const GIT_INFO_RE = /\/info\/refs$/;
const GIT_UPLOAD_RE = /\/git-upload-pack$/;
const GIT_RECEIVE_RE = /\/git-receive-pack$/;

const TTL_STATIC = 86400; // 24h
const TTL_RAW = 600;      // 10min

const ALLOW_METHODS = new Set(['GET', 'HEAD', 'POST']);
const REDIRECT_STATUSES = new Set([301, 302, 303, 307, 308]);
const RETRY_STATUSES = new Set([408, 429, 500, 502, 503, 504]);

const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

const CLIENT_SCRIPT = `
(function () {
  'use strict';

  function mirror(u) {if (typeof u !== 'string' || !u) return u;
    var s = u.trim();
    if (!s || s.charAt(0) === '#' ||
        s.slice(0, 7) === 'mailto:' ||
        s.slice(0, 11) === 'javascript:' ||
        s.slice(0, 5) === 'data:') return u;

    var url;
    try {url = new URL(s, document.baseURI || location.href); } catch (e) {return u;}

    var h = url.hostname.toLowerCase();
    var p = url.pathname + url.search + url.hash;

    if (h === 'github.com') return p || '/';
    if (h === 'raw.githubusercontent.com') return '/raw' + p;
    if (/[.](github|githubusercontent)[.]com$/.test(h) ||
        /^github-[a-z0-9-]+[.]s3[.]amazonaws[.]com$/i.test(h)) {return '/proxy/' + h + p;}
    return u;
  }

  function fixHref(a) {
    try {var oldHref = a.getAttribute('href');
      if (!oldHref) return;
      var n = mirror(oldHref);
      if (n !== oldHref) a.setAttribute('href', n);
    } catch (e) {}}

  function scan(root) {if (!root || !root.querySelectorAll) return;
    try {root.querySelectorAll('a[href], area[href]').forEach(fixHref); } catch (e) {}}

  var nativeFetch = window.fetch;
  if (typeof nativeFetch === 'function') {window.fetch = function (input, init) {
      try {if (typeof input === 'string') {input = mirror(input);
        } else if (input && typeof input === 'object' && typeof input.url === 'string') {var n = mirror(input.url);
          if (n !== input.url) {
            input = new Request(n, {
              method: input.method,
              headers: input.headers,
              body: input.body,
              mode: input.mode,
              credentials: input.credentials,
              cache: input.cache,
              redirect: input.redirect,
              referrer: input.referrer,
              referrerPolicy: input.referrerPolicy,
              integrity: input.integrity,
              keepalive: input.keepalive
            });
          }
        }
      } catch (e) {}
      return nativeFetch.call(this, input, init);
    };
  }

  if (typeof XMLHttpRequest !== 'undefined') {
    var nativeOpen = XMLHttpRequest.prototype.open;
    XMLHttpRequest.prototype.open = function (method, url) {try { arguments[1] = mirror(url); } catch (e) {}
      return nativeOpen.apply(this, arguments);
    };
  }

  if (typeof window.open === 'function') {
    var nativeOpenWindow = window.open;
    window.open = function (u) {try { arguments[0] = mirror(u); } catch (e) {}
      return nativeOpenWindow.apply(this, arguments);
    };
  }

  document.addEventListener('click', function (e) {
    var el = e.target;
    while (el && el !== document && !(el.tagName && String(el.tagName).toLowerCase() === 'a')) {el = el.parentNode;}
    if (el && el.tagName) fixHref(el);
  }, true);

  if (document.readyState === 'loading') {document.addEventListener('DOMContentLoaded', function () {scan(document); });
  } else {scan(document);
  }
})();
`;

function isProxiable(host) {if (!host) return false;
  const h = host.toLowerCase();
  if (PROXY_HOSTS.has(h) || h === MAIN_HOST) return true;
  if (h.endsWith('.github.com') || h.endsWith('.githubusercontent.com')) return true;
  if (GITHUB_S3_RE.test(h)) return true;
  return false;
}

function rewriteText(text, self) {if (!text) return text;
  return text.replace(REWRITE_RE, (m, _scheme, host, path) => {const h = (host || '').toLowerCase();
    const rest = path || '/';
    if (h === MAIN_HOST) return self + rest;
    if (h === RAW_HOST) return self + '/raw' + rest;
    if (isProxiable(h)) return self + '/proxy/' + h + rest;
    return m;
  });
}

function injectScript(html, script) {const lower = html.toLowerCase();
  const headIdx = lower.indexOf('', headIdx);
  if (endIdx === -1) return html;

  return (html.slice(0, endIdx + 1) +
    '\n\n' +
    html.slice(endIdx + 1)
  );
}

function errorResponse(status, message) {return new Response(GitHub Mirror Error (${status})\n${message}, {
    status,
    headers: {
      'content-type': 'text/plain; charset=utf-8',
      'cache-control': 'no-store',
    },
  });
}

function cleanRequestHeaders(request) {const headers = new Headers(request.headers);

  [
    'host',
    'accept-encoding',
    'cookie',
    'origin',
    'expect',
    'transfer-encoding',
    'connection',
    'proxy-connection',
    'cf-connecting-ip',
    'cf-ray',
    'cf-visitor',
    'cf-worker',
    'cdn-loop',
    'x-forwarded-for',
    'x-forwarded-proto',
    'x-real-ip',
  ].forEach((h) => headers.delete(h));

  // 注意:这里保留 authorization,以便私有仓库带 token 也能 clone。// 如果你不打算支持私有仓库,可以取消下一行的删除:// headers.delete('authorization');

  const ua = headers.get('user-agent');
  if (!ua || /^cloudflare/i.test(ua)) {headers.set('user-agent', USER_AGENT);
  }

  if (!headers.has('accept')) {headers.set('accept', '*/*');
  }

  return headers;
}

/**
 * GET/HEAD 以及已缓冲的 upload-pack POST 可重试一次。*/
async function fetchWithRetry(target, init, canRetry = false) {
  const attempts = canRetry ? 2 : 1;

  for (let attempt = 0; attempt < attempts; attempt++) {
    let resp;
    try {resp = await fetch(target, init);
    } catch (err) {if (attempt === 0 && attempts> 1) {await sleep(200);
        continue;
      }
      throw err;
    }

    if (attempt === 0 && attempts> 1 && RETRY_STATUSES.has(resp.status)) {
      try {await resp.body?.cancel();
      } catch (_) {}
      await sleep(200);
      continue;
    }

    return resp;
  }

  throw new Error('upstream fetch failed after retries');
}

async function upstream(request, targetUrl, opts) {
  const {
    self,
    isPage = false,
    cache = false,
    cacheTtl = TTL_STATIC,
    retryPost = false,
  } = opts;

  const method = request.method.toUpperCase();
  const headers = cleanRequestHeaders(request);

  const init = {
    method,
    headers,
    redirect: 'manual',
  };

  if (cache && (method === 'GET' || method === 'HEAD') && !headers.has('range')) {
    init.cf = {
      cacheEverything: true,
      cacheTtl,
    };
  }

  if (method !== 'GET' && method !== 'HEAD') {if (retryPost) {
      // upload-pack 的请求体通常很小,缓冲后可安全重试
      init.body = await request.arrayBuffer();
      headers.delete('content-length'); // 交给 fetch 重新计算
    } else {
      // push 等大请求体保持流式,不缓冲
      init.body = request.body;
    }
  }

  let resp;
  try {
    resp = await fetchWithRetry(
      targetUrl,
      init,
      method === 'GET' || method === 'HEAD' || retryPost
    );
  } catch (err) {return errorResponse(502, '上游请求失败:' + (err && err.message ? err.message : err));
  }

  const outHeaders = new Headers(resp.headers);

  [
    'set-cookie',
    'set-cookie2',
    'content-security-policy',
    'content-security-policy-report-only',
    'x-frame-options',
  ].forEach((h) => outHeaders.delete(h));

  if (REDIRECT_STATUSES.has(resp.status)) {const loc = outHeaders.get('location');
    if (loc) outHeaders.set('location', rewriteText(loc, self));
    outHeaders.delete('content-length');
    return new Response(resp.body, { status: resp.status, headers: outHeaders});
  }

  if (method === 'HEAD') {return new Response(null, { status: resp.status, headers: outHeaders});
  }

  const ctype = outHeaders.get('content-type') || '';

  if (isPage && ctype.includes('text/html')) {
    try {const html = await resp.text();
      outHeaders.delete('content-length');
      outHeaders.delete('content-encoding');
      outHeaders.set('content-type', 'text/html; charset=utf-8');

      const rewritten = rewriteText(html, self);
      return new Response(injectScript(rewritten, CLIENT_SCRIPT), {
        status: resp.status,
        headers: outHeaders,
      });
    } catch (err) {return errorResponse(502, 'HTML 处理失败:' + err.message);
    }
  }

  return new Response(resp.body, { status: resp.status, headers: outHeaders});
}

export default {async fetch(request) {const url = new URL(request.url);
    const self = SCHEME + '://' + url.host;
    const path = url.pathname;
    const q = url.search;

    if (path === '/healthz' || path === '/_health') {
      return new Response('ok', {
        status: 200,
        headers: {
          'content-type': 'text/plain; charset=utf-8',
          'cache-control': 'no-store',
        },
      });
    }

    const method = request.method.toUpperCase();
    if (!ALLOW_METHODS.has(method)) {
      return new Response('Method Not Allowed', {
        status: 405,
        headers: {
          allow: 'GET, HEAD, POST',
          'cache-control': 'no-store',
        },
      });
    }

    // a) /proxy/<真实域名>/<路径>
    const pm = path.match(PROXY_PATH_RE);
    if (pm) {const host = pm[1].toLowerCase();
      if (!isProxiable(host)) {return errorResponse(403, '不允许代理的主机:' + host);
      }
      const rest = pm[2] ? pm[2].slice(1) : '';
      const isMain = host === MAIN_HOST;
      const isRaw = host === RAW_HOST;
      return upstream(request, ${SCHEME}://${host}/${rest}${q}, {
        self,
        isPage: isMain,
        cache: !isMain && (STATIC_CACHE_HOSTS.has(host) || isRaw),
        cacheTtl: isRaw ? TTL_RAW : TTL_STATIC,
      });
    }

    // b) /raw////
    const rm = path.match(RAW_PATH_RE);
    if (rm) {return upstream(request, ${SCHEME}://${RAW_HOST}/${rm[1]}${q}, {
        self,
        isPage: false,
        cache: true,
        cacheTtl: TTL_RAW,
      });
    }

    // c) /owner/repo/raw/ref/file
    const rawShortcut = path.match(GITHUB_RAW_RE);
    if (rawShortcut) {
      return upstream(
        request,
        ${SCHEME}://${RAW_HOST}/${rawShortcut[1]}/${rawShortcut[2]}/${rawShortcut[3]}${q},
        {self, isPage: false, cache: true, cacheTtl: TTL_RAW}
      );
    }

    // d) /owner/repo/blob/ref/file?raw=1
    const wantRaw =
      url.searchParams.get('raw') === '1' || url.searchParams.get('raw') === 'true';
    if (wantRaw) {const blob = path.match(GITHUB_BLOB_RE);
      if (blob) {
        return upstream(
          request,
          ${SCHEME}://${RAW_HOST}/${blob[1]}/${blob[2]}/${blob[3]},
          {self, isPage: false, cache: true, cacheTtl: TTL_RAW}
        );
      }
    }

    // e) git smart HTTP:/owner/repo(.git)/info/refs|git-upload-pack|git-receive-pack
    const isGitInfo = GIT_INFO_RE.test(path);
    const isGitUpload = GIT_UPLOAD_RE.test(path);
    const isGitReceive = GIT_RECEIVE_RE.test(path);

    if (isGitInfo || isGitUpload || isGitReceive) {return upstream(request, ${SCHEME}://${MAIN_HOST}${path}${q}, {
        self,
        isPage: false,
        cache: false,
        // upload-pack 是只读请求,缓冲后可安全重试;receive-pack(push)不重试
        retryPost: isGitUpload,
      });
    }

    // f) 其余按 github.com 页面处理
    return upstream(request, ${SCHEME}://${MAIN_HOST}${path}${q}, {
      self,
      isPage: true,
      cache: false,
    });
  },
};

完事后点击右上角 部署 ,项目成功部署后返回项目概述页面,理论上我们点击右上角的 访问,就可以直接访问 Github 主页,但是由于国内拦截了 workers 的默认域名,所以我们需要绑定自定义域名。

绑定域名

在我们刚刚部署的 workers 项目概述页面,顶部导航选中 添加域名 ,弹出页面会列出我们托管在 Cloudflare 的域名,选中域名后,输入子域名,没有具体要求,根据自己喜好,建议还是简短一点好,比较容易记嘛!输入子域名后点击 添加域名 ,解析生效后,我们就可以点击右上角的 访问,不出意外,我们应该就打开了 Github 主页,不借助任何特殊网络环境!

实际应用

既然 Gtihub 镜像站部署好了,实际应用中我们该如何配置使用呢?且在实际应用中,效果到底如何呢?

浏览项目

当我们需要查看 github 项目时,也就是浏览器访问 github 时,我们只需要将 github 主域名,替换为我们绑定的域名,譬如:

https://github.com/anywhere-labs/dsh-desktop
    =>
https://gh.shubiao.cc.cd/anywhere-labs/dsh-desktop

这样我们就可以正常打开项目,查看代码、下载源码包,或者 Release 里下载程序包等,而且速度非常快。

命令行

命令行场景,是我们最长用 Github 的地方,或是部署服务器,或是克隆项目等!镜像站的使用方法,还是一样,无论你是运行 git 命令,还是语言包管理器等!我们只需替换 GitHub 主域名,譬如:

git clone https://github.com/anywhere-labs/dsh-desktop.git
    =>
git clone https://gh.shubiao.cc.cd/anywhere-labs/dsh-desktop.git

还有一种特殊情况,就是需要运行一些脚本什么的,会涉及 raw 域名,我们也可以直接替换,具体方法:

https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh
    =>
https://gh.shubiao.cc.cd/raw/nvm-sh/nvm/v0.40.3/install.sh

当然我只是演示,这么做只能通过镜像站下载这个脚本,脚本里还有 github 资源,我们就需要通过全局 git 重写,使得我们这次安装,只要是 Github 的资源,都通过镜像站下载:

git config --global url."https://gh.shubiao.cc.cd/".insteadOf "https://github.com/"
curl -o- https://gh.shubiao.cc.cd/raw/nvm-sh/nvm/v0.40.3/install.sh | bash

不夸张的说,原来需要数十分钟跑完的操作,现在不到一分钟就完事,其他就不用我多说了吧!

上述只是鼠标用到的场景,但是绝对不仅限于这些场景。其次,Cloudflare 每天免费的请求次数有限,所以建议部署的镜像站自用就好,避免不必要的麻烦和风控等!

总结

以上就是本期视频全部内容,讲了利用 Cloudflare,给 Github 搭建镜像站,以方便我们学习使用。全程分币不掏,极致白嫖,从此使用 Github 不再看命!本期视频就到这里,感觉教程有用,收藏备用,点赞关注不迷路!我是鼠标谢谢您的观看!

正文完
 0