Build a GitHub Reverse Proxy with Cloudflare Worker
Preface
When I saw this DD system project https://github.com/bin456789/reinstall
I was drawn to its installation command.
curl -O https://raw.githubusercontent.com/bin456789/reinstall/main/reinstall.sh || wget -O ${_##*/} $_
I asked GPT to analyze the principle. It's to consider that some Linux systems have wget by default, and some have curl by default.
I'm going to change the installation method of my minimalist one-click scripts to this style.
curl -LO https://github.com/crazypeace/xray-vless-reality/raw/main/install.sh || wget -O ${_##*/}$_ && bash install.sh 4 8443
Then at the very beginning of the script, install curl and wget.
apt-get -y install curl wget -qq
I tend to display error content, so that when beginners report issues, they can just send a screenshot or log directly. So I didn't add silent parameters to curl and wget.
When complex script commands are combined with my ghproxy, to implement nested GitHub script calls, existing solutions are more difficult to handle.
I'm going to switch to the WJQSERVER-ghproxy approach.
During the ghproxy's own processing, for .sh resources, do a full search-and-replace, wrapping all links that access GitHub resources again with my own ghproxy. Then return.
It won't affect users of the original gh-proxy.
worker.js
I thought about it,
Based on Cloudflare's worker, develop a tool specifically for reverse-proxying GitHub
1. The path part this proxy receives should be a http:// or https://
2. If the path part doesn't start with http:// or https://
then add http:// or https://
3. Determine whether the link this proxy receives is GitHub
The judgment method is:
The domain part of the link should be a main domain starting with git
such as
github.com
raw.githubusercontent.com
api.github.com
gist.github.com
codeload.github.com
avatars.githubusercontent.com
assets-cdn.github.com
The main domains of these all start with git
4. After fetching the content that needs reverse-proxying
check whether the path ends with .sh to determine if it's a script file
5. For script files ending with .sh
do a search-and-replace on the text content
Prepend the proxy's own domain to all GitHub links,
so as to solve the scenario of nested script usage
The method for determining whether something is a GitHub link refers to step 3
I sent the above requirements to GPT.
Went through the code, made some minor modification requests.
And got our worker.js
addEventListener('fetch', event => {
event.respondWith(handleRequest(event.request));
});
/**
* Handle all incoming requests
* @param {Request} request
*/
async function handleRequest(request) {
const url = new URL(request.url);
const workerUrl = url.origin; // Get the worker's own domain, e.g., https://my-worker.example.com
// 1. The path part this proxy receives should be a http:// or https://
// We extract the target URL from the path
let path = url.pathname.substring(1); // Remove the leading '/'
if (!path || path === 'favicon.ico') {
return new Response('Usage: ' + workerUrl + '/', { status: 400 });
}
// 2. If the path part doesn't start with http:// or https://, then add https://
if (!path.startsWith('http://') && !path.startsWith('https://')) {
path = 'https://' + path;
}
let targetUrl;
try {
targetUrl = new URL(path);
} catch (e) {
return new Response('The path contains an invalid URL', { status: 400 });
}
// 3. Determine whether the link this proxy receives is GitHub
if (!isGitHubDomain(targetUrl.hostname)) {
return new Response('Access denied: this proxy only supports GitHub-related domains.', { status: 403 });
}
// Prepare to forward the request
// GET or HEAD methods cannot have a body
const hasBody = request.method === 'POST' || request.method === 'PUT' || request.method === 'PATCH';
const response = await fetch(targetUrl.toString(), {
method: request.method,
headers: request.headers,
body: hasBody ? request.body : null,
redirect: 'follow',
});
// Copy response headers and set CORS
const newHeaders = new Headers(response.headers);
newHeaders.set('access-control-allow-origin', '*');
newHeaders.set('access-control-allow-headers', '*');
newHeaders.set('access-control-allow-methods', '*');
// 4. Check whether the path ends with .sh
const finalUrl = new URL(response.url);
const isScript = finalUrl.pathname.endsWith('.sh');
// 5. For script files ending with .sh (and request succeeded)
if (isScript && response.status === 200) {
let bodyText = await response.text();
// ********** git.io short links ************
// Fix git.io links: [space]git.io replaced with [space]https://git.io
bodyText = bodyText.replace(/(\s)(git\.io)/g, '$1https://$2');
// **********************************
// Search and replace all GitHub links (nested proxy)
// Match all https?://... links
const urlRegex = /(https?:\/\/[^\s"'`()<>]+)/g;
bodyText = bodyText.replace(urlRegex, (match) => {
try {
// 'match' is a complete URL, e.g., "https://github.com/foo"
const linkUrl = new URL(match);
// Use the isGitHubDomain function to determine
if (isGitHubDomain(linkUrl.hostname)) {
// If it's a GitHub link, add the proxy prefix
return `${workerUrl}/${match}`;
} else {
// If not, keep as-is
return match;
}
} catch (e) {
// If URL parsing fails (for example, it might be text that just looks like a URL), keep as-is
return match;
}
});
// Since the content has been modified, the content-length header is invalid; delete it
newHeaders.delete('content-length');
return new Response(bodyText, {
status: response.status,
statusText: response.statusText,
headers: newHeaders,
});
}
// For non-.sh files or non-200 status codes, return directly (with modified CORS and Location headers)
return new Response(response.body, {
status: response.status,
statusText: response.statusText,
headers: newHeaders,
});
}
/**
* Helper function: determine whether the hostname is a target GitHub domain
* Whether the domain starts with git
* @param {string} hostname
* @returns {boolean}
*/
function isGitHubDomain(hostname) {
// This regex checks:
// 1. (^|\.) : whether the string starts with... (^) or (|) starts with a dot (.)
// 2. git : followed immediately by 'git'
//
// Examples:
// - "github.com" -> matches (^git)
// - "api.github.com" -> matches (.git)hub.com
// - "gitlab.com" -> matches (^git)
// - "my.gitee.com" -> matches (.git)ee.com
// - "my-git.com" -> doesn't match (because the character before 'g' is '-' rather than '.' or start)
return /(^|\.)git/.test(hostname);
}
index.html
The tool page index.html itself is relatively independent from the GitHub proxy.
The HTML content is very little, and very simple.
Reference the previous gh-proxy's index.html, remove some interface elements, adjust some text descriptions.
main.js
When clicking the button on the tool page index.html, the work of adding the ghproxy to the link is done in main.js.
Since worker.js is also JS, the implementation can be borrowed and the code can be directly copy-pasted.
Display index.html when accessing the worker
When the path is empty, when the path is styles.css, when the path is main.js, it should access the GitHub page
// If path is empty, return the main page
if (!path) {
return fetch(ASSET_URL)
}
if (path === 'styles.css') {
return fetch(ASSET_URL + path)
}
if (path === 'main.js') {
return fetch(ASSET_URL + path)
}
Handle nested proxy calls
Prevent occurrences like https://ghproxy.icdyct.nyc.mn/https://ghproxy.icdyct.nyc.mn/https://api.github.com/repos/XTLS/Xray-core/releases/latest
const selfPrefixFull = workerUrl + '/';
while (true) {
if (path.startsWith(selfPrefixFull)) {
path = path.substring(selfPrefixFull.length);
} else {
// When the path no longer starts with any prefix, break out of the loop
break;
}
}
Upload to GitHub
Demo site
Demo video
Deployment method
========
Postscript
In this (2025-11-6) GPT-assisted development, the most useful GPT was
========
