引言
“ 问:打开 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 不再看命!本期视频就到这里,感觉教程有用,收藏备用,点赞关注不迷路!我是鼠标谢谢您的观看!





Ttzip天天绿软
感谢分享,谢谢站长!!已收藏