Markchai
杏花疏影里,吹笛到天明。
Markchai 的博客

使用 Cloudflare Worker 搭建临时 Docker 代理

由于众所周知的原因,一夜间各大 Docker 镜像源网站宣布关闭。这对于计算机从业者来说注定是一个不好的消息。

前天,SJTUG(上海交通大学 Linux 用户组)发布公告称已下架 Docker Hub 镜像。随后其他大学、商业的 Docker Hub 镜像也相继关闭。

背景

Docker Hub 是目前全球最大的容器镜像社区,但由于XXX,国内开发者从 Docker Hub 上获取容器镜像时很不方便。所以国内一些公司和学校就搭建了许多镜像源来加速下载。很早之前,Docker 官方镜像源就因为一些不可抗因素被封禁了。国内拉取 Docker 镜像都需要依赖国内镜像源:比如阿里云、腾讯云、网易云、中科大、讯飞、百度等。但是最近很多开发者都发现,这些国内镜像也都不能用了。另外,消息称后续包括 Github CDN 镜像,NPM,Python PIP,OpenWrt OPKG 等未受内容审查的镜像服务器同样也会被下架(npm和pip真的要命) ,以后国内开发者想拉取镜像服务大概率只能挂代理了。

下面提供一种临时的解决方案,由 v2ex 大神 muzihuaner 提供,jeblove 改进的 Cloudflare Worker 方案。虽然 Cloudflare Worker 的速度也极慢,但起码能跑。

解决方案

Step 1: 打开 Workers 和 Pages

登录 Cloudflare 之后,直接点击菜单 Workers 和 Pages ,进入到概述页面。

Step 2: 创建 Worker

点击创建应用程序按钮,进入创建页面,切换到 Workers 页签,接着点击页面中的创建 Worker 按钮,进入创建页面。

在创建页面中填写名称,比如就叫 docker-worker ,代码先保持默认不用管,然后点击部署按钮,完成创建。

Step 3: 编写 Worker

重点来了。

首先回到概述页面,可以看到我们已经创建好的 Worker,直接点击名称进入 Worker 的主页。

接着点击右上角的快速编辑按钮,进入编辑页面,将下列代码粘贴到编辑框中,然后点击右上角的保存并部署按钮,完成!

'use strict'

const hub_host = 'registry-1.docker.io'

const auth_url = 'https://auth.docker.io'

const workers_url = 'https://dh.jeblove.com'

/**

* static files (404.html, sw.js, conf.js)

*/

/** @type {RequestInit} */

const PREFLIGHT_INIT = {

    // 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',

    }),

}

/**

* @param {any} body

* @param {number} status

* @param {Object} headers

*/

function makeRes(body, status = 200, headers = {}) {

    headers['access-control-allow-origin'] = '*'

    return new Response(body, {
        status,
        headers
    })

}

/**

* @param {string} urlStr

*/

function newUrl(urlStr) {

    try {

        return new URL(urlStr)

    } catch (err) {

        return null

    }

}

addEventListener('fetch', e => {

    const ret = fetchHandler(e)

        .catch(err => makeRes('cfworker error:\n' + err.stack, 502))

    e.respondWith(ret)

})

/**

* @param {FetchEvent} e

*/

async function fetchHandler(e) {

    const getReqHeader = (key) => e.request.headers.get(key);

    let url = new URL(e.request.url);

    // 修改 pre head get 请求

    // 是否含有 %2F ,用于判断是否具有用户名与仓库名之间的连接符

    // 同时检查 %3A 的存在

    if (!/%2F/.test(url.search) && /%3A/.test(url.toString())) {

        let modifiedUrl = url.toString().replace(/%3A(?=.*?&)/, '%3Alibrary%2F');

        url = new URL(modifiedUrl);

        console.log(`handle_url: ${url}`)

    }

    if (url.pathname === '/token') {

        let token_parameter = {

            headers: {

                'Host': 'auth.docker.io',

                'User-Agent': getReqHeader("User-Agent"),

                'Accept': getReqHeader("Accept"),

                'Accept-Language': getReqHeader("Accept-Language"),

                'Accept-Encoding': getReqHeader("Accept-Encoding"),

                'Connection': 'keep-alive',

                'Cache-Control': 'max-age=0'

            }

        };

        let token_url = auth_url + url.pathname + url.search

        return fetch(new Request(token_url, e.request), token_parameter)

    }

    // 修改 head 请求

    if (/^\/v2\/[^/]+\/[^/]+\/[^/]+$/.test(url.pathname) && !/^\/v2\/library/.test(url.pathname)) {

        url.pathname = url.pathname.replace(/\/v2\//, '/v2/library/');

        console.log(`modified_url: ${url.pathname}`)

    }

    url.hostname = hub_host;

    let parameter = {

        headers: {

            'Host': hub_host,

            'User-Agent': getReqHeader("User-Agent"),

            'Accept': getReqHeader("Accept"),

            'Accept-Language': getReqHeader("Accept-Language"),

            'Accept-Encoding': getReqHeader("Accept-Encoding"),

            'Connection': 'keep-alive',

            'Cache-Control': 'max-age=0'

        },

        cacheTtl: 3600

    };

    if (e.request.headers.has("Authorization")) {

        parameter.headers.Authorization = getReqHeader("Authorization");

    }

    let original_response = await fetch(new Request(url, e.request), parameter)

    let original_response_clone = original_response.clone();

    let original_text = original_response_clone.body;

    let response_headers = original_response.headers;

    let new_response_headers = new Headers(response_headers);

    let status = original_response.status;

    if (new_response_headers.get("Www-Authenticate")) {

        let auth = new_response_headers.get("Www-Authenticate");

        let re = new RegExp(auth_url, 'g');

        new_response_headers.set("Www-Authenticate", response_headers.get("Www-Authenticate").replace(re, workers_url));

    }

    if (new_response_headers.get("Location")) {

        return httpHandler(e.request, new_response_headers.get("Location"))

    }

    let response = new Response(original_text, {

        status,

        headers: new_response_headers

    })

    return response;

}

/**

* @param {Request} req

* @param {string} pathname

*/

function httpHandler(req, pathname) {

    const reqHdrRaw = req.headers

    // preflight

    if (req.method === 'OPTIONS' &&

        reqHdrRaw.has('access-control-request-headers')

    ) {

        return new Response(null, PREFLIGHT_INIT)

    }

    let rawLen = ''

    const reqHdrNew = new Headers(reqHdrRaw)

    const refer = reqHdrNew.get('referer')

    let urlStr = pathname

    const urlObj = newUrl(urlStr)

    /** @type {RequestInit} */

    const reqInit = {

        method: req.method,

        headers: reqHdrNew,

        redirect: 'follow',

        body: req.body

    }

    return proxy(urlObj, reqInit, rawLen)

}

/**

*

* @param {URL} urlObj

* @param {RequestInit} reqInit

*/

async function proxy(urlObj, reqInit, rawLen) {

    const res = await fetch(urlObj.href, reqInit)

    const resHdrOld = res.headers

    const resHdrNew = new Headers(resHdrOld)

    // verify

    if (rawLen) {

        const newLen = resHdrOld.get('content-length') || ''

        const badLen = (rawLen !== newLen)

        if (badLen) {

            return makeRes(res.body, 400, {

                '--error': `bad len: ${newLen}, except: ${rawLen}`,

                'access-control-expose-headers': '--error',

            })

        }

    }

    const status = res.status

    resHdrNew.set('access-control-expose-headers', '*')

    resHdrNew.set('access-control-allow-origin', '*')

    resHdrNew.set('Cache-Control', 'max-age=1500')

    resHdrNew.delete('content-security-policy')

    resHdrNew.delete('content-security-policy-report-only')

    resHdrNew.delete('clear-site-data')

    return new Response(res.body, {

        status,

        headers: resHdrNew

    })

}

使用方法示例:

# docker pull xxx.com/node

Using default tag: latest
latest: Pulling from node
c6cf28de8a06: Downloading [> ] 506.8kB/49.58MB
891494355808: Downloading [========> ] 4.184MB/24.05MB

注意!

搭建这个服务是使用了Cloudflare的Worker和Pages服务每天可以有10万个免费请求次数这个福利,对于个人使用来说完全不用担心。所以为了你个人安全和不超次数,请不要分享给别人使用!请不要分享给别人使用!请不要分享给别人使用!

以上其实就已经完成了 Docker 代理源的搭建,但是由于 Cloudflare 自带的 Worker 域名在国内属于无法正常访问的(众所周知),所以你的域名现在开始出场了,我们需要给这个 Worker 设置一个自定义域名。

Step 4: 善后

首先还是回到到上面创建的 Worker 详情页面,点击触发器页签。继续点击添加自定义域按钮,开始添加一个自定义域名。

输入你的域名,例如 docker.yourdomain.com,并点击添加自定义域按钮,完成添加。请注意,输入的域名一定是已经将 DNS 解析添加到 Cloudflare 中的才可以。

尾声

以上就是在当前局势下临时部署的方案,但治标不治本的方案往往并不能稳定维持。以后当 pip、npm 等的镜像源都封了之后,你我又该走向何方?中国互联网又该走向何方?这个问题目前尚不能回答,也没人敢回答。毕竟生逢盛世,根本没有解决不了的问题(有问题先解决踢出问题的人)。但愿有缘,我们能在更加开放的平行世界再次相遇。

发表回复

textsms
account_circle
email

Markchai 的博客

使用 Cloudflare Worker 搭建临时 Docker 代理
由于众所周知的原因,一夜间各大 Docker 镜像源网站宣布关闭。这对于计算机从业者来说注定是一个不好的消息。 > 前天,SJTUG(上海交通大学 Linux 用户组)发布公告称已下架 Docker Hu…
扫描二维码继续阅读
2024-06-09

Optimized by WPJAM Basic