搭建镜像加速站(GitHub,Docker)

@zgcwkj  2024年07月13日

分类:

代码 其它 

使用 Cloudflare 的 Workers 搭建镜像加速服务(GitHub,Docker)

代码

//by zgcwkj 20251215
const HTML = `<html lang="zh-Hans">
<head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width,initial-scale=1.0">
    <title>加速服务</title>
    <style>html,body{width:100%;margin:0}html{height:100%}body{min-height:100%;padding:20px;box-sizing:border-box}p{word-break:break-all}@media (max-width:500px){h1{margin-top:80px}}.block{display:block;position:relative}.url{font-size:18px;padding:10px 10px 10px 5px;position:relative;width:300px;border:none;border-bottom:1px solid #bfbfbf}input:focus{outline:none}.bar{content:"";height:2px;width:100%;bottom:0;position:absolute;background:#00bfb3;transition:0.2s ease transform;-moz-transition:0.2s ease transform;-webkit-transition:0.2s ease transform;transform:scaleX(0)}.url:focus~.bar{transform:scaleX(1)}.btn{line-height:38px;background-color:#00bfb3;color:#fff;white-space:nowrap;text-align:center;font-size:14px;border:none;border-radius:2px;cursor:pointer;padding:5px;width:160px;margin:5px 0}.tabs{display:flex;flex-direction:row}.tab{cursor:pointer;padding:10px;border:1px solid #00bfb3;display:inline-block}.tab:not(:last-child){border-right:none}.tab-content{display:none}.active{display:block}.tab.active{color:red}</style>
</head>
<body>
    <div class="tabs">
        <div class="tab active" data-target="github-tab">GitHub 文件加速</div>
        <div class="tab" data-target="docker-tab">Docker 镜像加速</div>
    </div>
    <hr />
    <div id="github-tab" class="tab-content active">
        <form action="./" method="get" style="padding-bottom: 40px;margin: 30px 0" target="_blank" onsubmit="toSubmit(event)">
            <label class="block" style="width: fit-content">
                <input class="block url" name="q" type="text" placeholder="键入Github文件链接"
                    pattern="^((https|http):\/\/)?(github\.com\/.+?\/.+?\/(?:releases|archive|blob|raw|suites)|((?:raw|gist)\.(?:githubusercontent|github)\.com))\/.+$"
                    required>
                <div class="bar"></div>
            </label>
            <input class="block btn" type="submit" value="下载">
        </form>
        <div>
            <p>GitHub文件链接带不带协议头都可以,支持release、archive以及文件,右键复制出来的链接都是符合标准的,更多用法、clone加速请参考<a href="https://hunsh.net/archives/23/">这篇文章</a>。</p>
            <p>release、archive使用cf加速,文件会跳转至JsDelivr</p>
            <p>注意,不支持项目文件夹</p>
        </div>
        <div>
            <p>分支源码:https://github.com/hunshcn/project/archive/master.zip</p>
            <p>release源码:https://github.com/hunshcn/project/archive/v0.1.0.tar.gz</p>
            <p>release文件:https://github.com/hunshcn/project/releases/download/v0.1.0/example.zip</p>
            <p>分支文件:https://github.com/hunshcn/project/blob/master/filename</p>
        </div>
    </div>
    <div id="docker-tab" class="tab-content">
        <div class="tips">
            <p>为了加速镜像拉取,你可以使用以下命令设置registery mirror:</p>
            <pre>
sudo tee /etc/docker/daemon.json &lt;&lt;EOF
{
    "registry-mirrors": ["https://{{host}}"]
}
EOF</pre>
        </div>
        <div class="example">
            <p>为了避免 Worker 用量耗尽,你可以手动 pull 镜像然后 re-tag 之后 push 至本地镜像仓库:</p>
            <pre>
docker pull {{host}}/library/alpine:latest # 拉取 library 镜像
docker pull {{host}}/coredns/coredns:latest # 拉取 library 镜像</pre>
        </div>
    </div>
    <hr />
    <div>
      <p style="position: sticky">Blog: <a style="color: #3294ea" href="http://blog.zgcwkj.cn/">zgcwkj</a></p>
      <p style="position: sticky">GitHub: <a style="color: #3294ea" href="https://github.com/hunshcn/gh-proxy">hunshcn/gh-proxy</a></p>
      <p style="position: sticky">Docker: <a style="color: #3294ea" href="https://github.com/Doublemine/container-registry-worker">Doublemine/container-registry-worker</a></p>
    </div>
    <script>
        function toSubmit(e) {
            e.preventDefault()
            window.open(location.href.substr(0, location.href.lastIndexOf("/") + 1) + document.getElementsByName("q")[0].value);
            return false
        }
        document.querySelectorAll('.tab').forEach(tab => {
            tab.onclick = function () {
                document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
                document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));

                this.classList.add('active');
                const content = document.getElementById(this.getAttribute('data-target'));
                content.classList.add('active');
            };
        });
    </script>
</body>
</html>`
const PREFIX = '/';
const Config = { jsdelivr: 0 };
const whiteList = [];

const exp1 = /^(?:https?:\/\/)?github\.com\/.+?\/.+?\/(?:releases|archive)\/.*$/i;
const exp2 = /^(?:https?:\/\/)?github\.com\/.+?\/.+?\/(?:blob|raw)\/.*$/i;
const exp3 = /^(?:https?:\/\/)?github\.com\/.+?\/.+?\/(?:info|git-).*$/i;
const exp4 = /^(?:https?:\/\/)?raw\.(?:githubusercontent|github)\.com\/.+?\/.+?\/.+?\/.+$/i;
const exp5 = /^(?:https?:\/\/)?gist\.(?:githubusercontent|github)\.com\/.+?\/.+?\/.+$/i;
const exp6 = /^(?:https?:\/\/)?github\.com\/.+?\/.+?\/tags.*$/i;

/** @type {RequestInit} */
const PREFLIGHT_INIT = {
    // @ts-ignore
    status: 204,
    headers: new Headers({
        'access-control-allow-origin': '*',
        'access-control-allow-methods': 'GET, POST, PUT, PATCH, TRACE, DELETE, HEAD, OPTIONS',
        'access-control-max-age': '1728000',
    }),
};

/**
 * Create a new response.
 * @param {any} body
 * @param {number} [status=200]
 * @param {Object<string, string>} headers
 * @returns {Response}
 */
function makeResponse(body, status = 200, headers = {}) {
    headers['access-control-allow-origin'] = '*';
    return new Response(body, { status, headers });
}

/**
 * Create a new URL object.
 * @param {string} urlStr
 * @returns {URL|null}
 */
function createURL(urlStr) {
    try {
        return new URL(urlStr);
    } catch (err) {
        return null;
    }
}

addEventListener('fetch', (event) => {
    event.respondWith(handleFetchEvent(event).catch(err => makeResponse(`cfworker error:\n${err.stack}`, 502)));
});

/**
 * Handle the fetch event.
 * @param {FetchEvent} event
 * @returns {Promise<Response>}
 */
async function handleFetchEvent(event) {
    const req = event.request;
    const url = new URL(req.url);

    if (url.pathname.startsWith('/v2/') || url.pathname.startsWith('/token')) {
        return handleDockerProxy(req, url);
    }

    if (url.pathname.startsWith(PREFIX)) {
        return handleGitHubProxy(req, url);
    }

    return makeResponse('Not Found', 404);
}

/**
 * Handle token requests and Docker proxy.
 * @param {Request} req
 * @param {URL} url
 * @returns {Promise<Response>}
 */
async function handleDockerProxy(req, url) {
    const path = url.pathname;
    
    const DOCKER_HUB = "registry-1.docker.io";
    const DOCKER_AUTH = "auth.docker.io";

    let targetHost = DOCKER_HUB;
    let targetUrl = "";

    // 路由分发
    if (path.startsWith("/token")) {
        // 认证请求 -> auth.docker.io
        targetHost = DOCKER_AUTH;
        targetUrl = `https://${DOCKER_AUTH}${path}${url.search}`;
    } else {
        // 镜像请求 -> registry-1.docker.io
        targetHost = DOCKER_HUB;
        targetUrl = `https://${DOCKER_HUB}${path}`;
    }

    const headers = new Headers(req.headers);
    headers.set("Host", targetHost);

    const proxyRequest = new Request(targetUrl, {
        method: req.method,
        headers: headers,
        body: req.body,
        redirect: "follow", 
    });

    try {
        const response = await fetch(proxyRequest);
        const responseHeaders = new Headers(response.headers);

        // 重写 Www-Authenticate
        // 将 Docker 官方要求的认证地址,替换为 Worker 的地址
        const wwwAuth = responseHeaders.get("Www-Authenticate");
        if (wwwAuth) {
            const newAuth = wwwAuth.replace('realm="https://auth.docker.io/token"', `realm="https://${url.hostname}/token"`);
            responseHeaders.set("Www-Authenticate", newAuth);
        }

        // 处理跨域
        responseHeaders.set("access-control-allow-origin", "*");
        responseHeaders.set("access-control-allow-headers", "Authorization");
        
        return new Response(response.body, {
            status: response.status,
            statusText: response.statusText,
            headers: responseHeaders,
        });
    } catch (err) {
        return makeResponse(`Docker Proxy Error: ${err.message}`, 500);
    }
}

/**
 * Handle GitHub proxy requests.
 * @param {Request} req
 * @param {URL} url
 * @returns {Promise<Response>}
 */
async function handleGitHubProxy(req, url) {
    let path = url.searchParams.get('q');
    if (path) {
        return Response.redirect('https://' + url.host + PREFIX + path, 301);
    }
    path = url.href.substr(url.origin.length + PREFIX.length).replace(/^https?:\/+/, 'https://');
    if (checkUrl(path)) {
        return httpHandler(req, path);
    } else if (path.search(exp2) === 0) {
        if (Config.jsdelivr) {
            const newUrl = path.replace('/blob/', '@').replace(/^(?:https?:\/\/)?github\.com/, 'https://cdn.jsdelivr.net/gh');
            return Response.redirect(newUrl, 302);
        } else {
            path = path.replace('/blob/', '/raw/');
            return httpHandler(req, path);
        }
    } else if (path.search(exp4) === 0) {
        const newUrl = path.replace(/(?<=com\/.+?\/.+?)\/(.+?\/)/, '@$1').replace(/^(?:https?:\/\/)?raw\.(?:githubusercontent|github)\.com/, 'https://cdn.jsdelivr.net/gh');
        return Response.redirect(newUrl, 302);
    } else {
        return makeResponse(HTML.replace(/{{host}}/g, url.host), 200, {
            "content-type": "text/html"
        });
    }
}

/**
 * Check if the URL matches GitHub patterns.
 * @param {string} url
 * @returns {boolean}
 */
function checkUrl(url) {
    return [exp1, exp2, exp3, exp4, exp5, exp6].some(exp => url.search(exp) === 0);
}

/**
 * Handle HTTP requests.
 * @param {Request} req
 * @param {string} pathname
 * @returns {Promise<Response>}
 */
async function httpHandler(req, pathname) {
    if (req.method === 'OPTIONS' && req.headers.has('access-control-request-headers')) {
        return new Response(null, PREFLIGHT_INIT);
    }

    const headers = new Headers(req.headers);
    let flag = !whiteList.length;
    for (const i of whiteList) {
        if (pathname.includes(i)) {
            flag = true;
            break;
        }
    }
    if (!flag) {
        return new Response('blocked', { status: 403 });
    }

    if (pathname.search(/^https?:\/\//) !== 0) {
        pathname = 'https://' + pathname;
    }

    const url = createURL(pathname);
    return proxyRequest(url, { method: req.method, headers, body: req.body });
}

/**
 * Proxy a request.
 * @param {URL} url
 * @param {RequestInit} reqInit
 * @returns {Promise<Response>}
 */
async function proxyRequest(url, reqInit) {
    const response = await fetch(url.href, reqInit);
    const responseHeaders = new Headers(response.headers);

    if (responseHeaders.has('location')) {
        const location = responseHeaders.get('location');
        if (checkUrl(location)) {
            responseHeaders.set('location', PREFIX + location);
        } else {
            reqInit.redirect = 'follow';
            return proxyRequest(createURL(location), reqInit);
        }
    }

    responseHeaders.set('access-control-expose-headers', '*');
    responseHeaders.set('access-control-allow-origin', '*');
    responseHeaders.delete('content-security-policy');
    responseHeaders.delete('content-security-policy-report-only');
    responseHeaders.delete('clear-site-data');

    return new Response(response.body, {
        status: response.status,
        headers: responseHeaders,
    });
}

JS:Cloudflare_Workers_GithubAndDocker.txt



评论已关闭

Top