使用 Cloudflare 搭建 Docker 代理 freecrazy 2024-09-19T21:34:13+08:00 2163

docker 镜像代理搭建

一、前因

docker拉取镜像失败,经常爆红,公共的用得不放心,关键也没几个还健在了,于是扒拉几篇教程,自己搭建一个临时使用。

二、安装

前提:有 cloudflare 账号,系统安装了 docker。 登录 cloudflare,新建worker

部署之后编辑代码,贴入以下代码

// _worker.js

  

// Docker镜像仓库主机地址

let hub_host = 'registry-1.docker.io';

// Docker认证服务器地址

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

// 自定义的工作服务器地址

let workers_url = 'https://xxxxxxxxxxxxxxxxx/';

  

let 屏蔽爬虫UA = ['netcraft'];

  

// 根据主机名选择对应的上游地址

function routeByHosts(host) {

    // 定义路由表

    const routes = {

        // 生产环境

        "quay": "quay.io",

        "gcr": "gcr.io",

        "k8s-gcr": "k8s.gcr.io",

        "k8s": "registry.k8s.io",

        "ghcr": "ghcr.io",

        "cloudsmith": "docker.cloudsmith.io",

        "nvcr": "nvcr.io",

        // 测试环境

        "test": "registry-1.docker.io",

    };

  

    if (host in routes) return [ routes[host], false ];

    else return [ hub_host, true ];

}

  

/** @type {RequestInit} */

const PREFLIGHT_INIT = {

    // 预检请求配置

    headers: new Headers({

        'access-control-allow-origin': '*', // 允许所有来源

        'access-control-allow-methods': 'GET,POST,PUT,PATCH,TRACE,DELETE,HEAD,OPTIONS', // 允许的HTTP方法

        'access-control-max-age': '1728000', // 预检请求的缓存时间

    }),

}

  

/**

 * 构造响应

 * @param {any} body 响应体

 * @param {number} status 响应状态码

 * @param {Object<string, string>} headers 响应头

 */

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

    headers['access-control-allow-origin'] = '*' // 允许所有来源

    return new Response(body, { status, headers }) // 返回新构造的响应

}

  

/**

 * 构造新的URL对象

 * @param {string} urlStr URL字符串

 */

function newUrl(urlStr) {

    try {

        return new URL(urlStr) // 尝试构造新的URL对象

    } catch (err) {

        return null // 构造失败返回null

    }

}

  

function isUUID(uuid) {

    // 定义一个正则表达式来匹配 UUID 格式

    const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[4][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;

    // 使用正则表达式测试 UUID 字符串

    return uuidRegex.test(uuid);

}

  

async function nginx() {

    const text = `

    <!DOCTYPE html>

    <html>

    <head>

    <title>Welcome to nginx!</title>

    <style>

        body {

            width: 35em;

            margin: 0 auto;

            font-family: Tahoma, Verdana, Arial, sans-serif;

        }

    </style>

    </head>

    <body>

    <h1>Welcome to nginx!</h1>

    <p>If you see this page, the nginx web server is successfully installed and

    working. Further configuration is required.</p>

    <p>For online documentation and support please refer to

    <a href="http://nginx.org/">nginx.org</a>.<br/>

    Commercial support is available at

    <a href="http://nginx.com/">nginx.com</a>.</p>

    <p><em>Thank you for using nginx.</em></p>

    </body>

    </html>

    `

    return text;

}

  

async function searchInterface() {

    const text = `

    <!DOCTYPE html>

    <html>

    <head>

        <title>Docker Hub Search</title>

        <style>

        body {

            font-family: Arial, sans-serif;

            display: flex;

            flex-direction: column;

            align-items: center;

            justify-content: center;

            height: 100vh;

            margin: 0;

            background: linear-gradient(to right, rgb(28, 143, 237), rgb(29, 99, 237));

        }

        .logo {

            margin-bottom: 20px;

        }

        .search-container {

            display: flex;

            align-items: center;

        }

        #search-input {

            padding: 10px;

            font-size: 16px;

            border: 1px solid #ddd;

            border-radius: 4px;

            width: 300px;

            margin-right: 10px;

        }

        #search-button {

            padding: 10px;

            background-color: rgba(255, 255, 255, 0.2); /* 设置白色,透明度为10% */

            border: none;

            border-radius: 4px;

            cursor: pointer;

            width: 44px;

            height: 44px;

            display: flex;

            align-items: center;

            justify-content: center;

        }          

        #search-button svg {

            width: 24px;

            height: 24px;

        }

        </style>

    </head>

    <body>

        <div class="logo">

        <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 18" fill="#ffffff" width="100" height="75">

            <path d="M23.763 6.886c-.065-.053-.673-.512-1.954-.512-.32 0-.659.03-1.01.087-.248-1.703-1.651-2.533-1.716-2.57l-.345-.2-.227.328a4.596 4.596 0 0 0-.611 1.433c-.23.972-.09 1.884.403 2.666-.596.331-1.546.418-1.744.42H.752a.753.753 0 0 0-.75.749c-.007 1.456.233 2.864.692 4.07.545 1.43 1.355 2.483 2.409 3.13 1.181.725 3.104 1.14 5.276 1.14 1.016 0 2.03-.092 2.93-.266 1.417-.273 2.705-.742 3.826-1.391a10.497 10.497 0 0 0 2.61-2.14c1.252-1.42 1.998-3.005 2.553-4.408.075.003.148.005.221.005 1.371 0 2.215-.55 2.68-1.01.505-.5.685-.998.704-1.053L24 7.076l-.237-.19Z"></path>

            <path d="M2.216 8.075h2.119a.186.186 0 0 0 .185-.186V6a.186.186 0 0 0-.185-.186H2.216A.186.186 0 0 0 2.031 6v1.89c0 .103.083.186.185.186Zm2.92 0h2.118a.185.185 0 0 0 .185-.186V6a.185.185 0 0 0-.185-.186H5.136A.185.185 0 0 0 4.95 6v1.89c0 .103.083.186.186.186Zm2.964 0h2.118a.186.186 0 0 0 .185-.186V6a.186.186 0 0 0-.185-.186H8.1A.185.185 0 0 0 7.914 6v1.89c0 .103.083.186.186.186Zm2.928 0h2.119a.185.185 0 0 0 .185-.186V6a.185.185 0 0 0-.185-.186h-2.119a.186.186 0 0 0-.185.186v1.89c0 .103.083.186.185.186Zm-5.892-2.72h2.118a.185.185 0 0 0 .185-.186V3.28a.186.186 0 0 0-.185-.186H5.136a.186.186 0 0 0-.186.186v1.89c0 .103.083.186.186.186Zm2.964 0h2.118a.186.186 0 0 0 .185-.186V3.28a.186.186 0 0 0-.185-.186H8.1a.186.186 0 0 0-.186.186v1.89c0 .103.083.186.186.186Zm2.928 0h2.119a.185.185 0 0 0 .185-.186V3.28a.186.186 0 0 0-.185-.186h-2.119a.186.186 0 0 0-.185.186v1.89c0 .103.083.186.185.186Zm0-2.72h2.119a.186.186 0 0 0 .185-.186V.56a.185.185 0 0 0-.185-.186h-2.119a.186.186 0 0 0-.185.186v1.89c0 .103.083.186.185.186Zm2.955 5.44h2.118a.185.185 0 0 0 .186-.186V6a.185.185 0 0 0-.186-.186h-2.118a.185.185 0 0 0-.185.186v1.89c0 .103.083.186.185.186Z"></path>

        </svg>

        </div>

        <div class="search-container">

        <input type="text" id="search-input" placeholder="Search Docker Hub">

        <button id="search-button">

            <svg focusable="false" aria-hidden="true" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">

            <path d="M21 21L16.65 16.65M19 11C19 15.4183 15.4183 19 11 19C6.58172 19 3 15.4183 3 11C3 6.58172 6.58172 3 11 3C15.4183 3 19 6.58172 19 11Z" stroke="white" fill="none" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path>

            </svg>

        </button>

        </div>

        <script>

        function performSearch() {

            const query = document.getElementById('search-input').value;

            if (query) {

            window.location.href = '/search?q=' + encodeURIComponent(query);

            }

        }

        document.getElementById('search-button').addEventListener('click', performSearch);

        document.getElementById('search-input').addEventListener('keypress', function(event) {

            if (event.key === 'Enter') {

            performSearch();

            }

        });

        </script>

    </body>

    </html>

    `;

    return text;

}

  

export default {

    async fetch(request, env, ctx)  {

        const getReqHeader = (key) => request.headers.get(key); // 获取请求头

  

        let url = new URL(request.url); // 解析请求URL

        const userAgentHeader = request.headers.get('User-Agent');

        const userAgent = userAgentHeader ? userAgentHeader.toLowerCase() : "null";

        if (env.UA) 屏蔽爬虫UA = 屏蔽爬虫UA.concat(await ADD(env.UA));

        workers_url = `https://${url.hostname}`;

        const pathname = url.pathname;

  

        // 获取请求参数中的 ns

        const ns = url.searchParams.get('ns');

        const hostname = url.searchParams.get('hubhost') || url.hostname;

        const hostTop = hostname.split('.')[0]; // 获取主机名的第一部分

  

        let checkHost; // 在这里定义 checkHost 变量

        // 如果存在 ns 参数,优先使用它来确定 hub_host

        if (ns) {

            if (ns === 'docker.io') {

                hub_host = 'registry-1.docker.io'; // 设置上游地址为 registry-1.docker.io

            } else {

                hub_host = ns; // 直接使用 ns 作为 hub_host

            }

        } else {

            checkHost = routeByHosts(hostTop);

            hub_host = checkHost[0]; // 获取上游地址

        }

  

        const fakePage = checkHost ? checkHost[1] : false; // 确保 fakePage 不为 undefined

        console.log(`域名头部: ${hostTop}\n反代地址: ${hub_host}\n伪装首页: ${fakePage}`);

        const isUuid = isUUID(pathname.split('/')[1].split('/')[0]);

  

        if (屏蔽爬虫UA.some(fxxk => userAgent.includes(fxxk)) && 屏蔽爬虫UA.length > 0) {

            // 首页改成一个nginx伪装页

            return new Response(await nginx(), {

                headers: {

                    'Content-Type': 'text/html; charset=UTF-8',

                },

            });

        }

  

        const conditions = [

            isUuid,

            pathname.includes('/_'),

            pathname.includes('/r/'),

            pathname.includes('/v2/user'),

            pathname.includes('/v2/orgs'),

            pathname.includes('/v2/_catalog'),

            pathname.includes('/v2/categories'),

            pathname.includes('/v2/feature-flags'),

            pathname.includes('search'),

            pathname.includes('source'),

            pathname === '/',

            pathname === '/favicon.ico',

            pathname === '/auth/profile',

        ];

  

        if (conditions.some(condition => condition) && (fakePage === true || hostTop == 'docker')) {

            if (env.URL302) {

                return Response.redirect(env.URL302, 302);

            } else if (env.URL) {

                if (env.URL.toLowerCase() == 'nginx') {

                    //首页改成一个nginx伪装页

                    return new Response(await nginx(), {

                        headers: {

                            'Content-Type': 'text/html; charset=UTF-8',

                        },

                    });

                } else return fetch(new Request(env.URL, request));

            } else if (url.pathname == '/'){

                return new Response(await searchInterface(), {

                    headers: {

                      'Content-Type': 'text/html; charset=UTF-8',

                    },

                });

            }

            const newUrl = new URL("https://registry.hub.docker.com" + pathname + url.search);

  

            // 复制原始请求的标头

            const headers = new Headers(request.headers);

  

            // 确保 Host 头部被替换为 hub.docker.com

            headers.set('Host', 'registry.hub.docker.com');

  

            const newRequest = new Request(newUrl, {

                    method: request.method,

                    headers: headers,

                    body: request.method !== 'GET' && request.method !== 'HEAD' ? await request.blob() : null,

                    redirect: 'follow'

            });

  

            return fetch(newRequest);

        }

  

        // 修改包含 %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}`);

        }

  

        // 处理token请求

        if (url.pathname.includes('/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, request), token_parameter);

        }

  

        // 修改 /v2/ 请求路径

        if ( hub_host == 'registry-1.docker.io' && /^\/v2\/[^/]+\/[^/]+\/[^/]+$/.test(url.pathname) && !/^\/v2\/library/.test(url.pathname)) {

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

            url.pathname = '/v2/library/' + url.pathname.split('/v2/')[1];

            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 // 缓存时间

        };

  

        // 添加Authorization头

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

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

        }

  

        // 发起请求并处理响应

        let original_response = await fetch(new Request(url, 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;

  

        // 修改 Www-Authenticate 头

        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(request, new_response_headers.get("Location"));

        }

  

        // 返回修改后的响应

        let response = new Response(original_text, {

            status,

            headers: new_response_headers

        });

        return response;

    }

};

  

/**

 * 处理HTTP请求

 * @param {Request} req 请求对象

 * @param {string} pathname 请求路径

 */

function httpHandler(req, pathname) {

    const reqHdrRaw = req.headers;

  

    // 处理预检请求

    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 URL对象

 * @param {RequestInit} reqInit 请求初始化对象

 * @param {string} rawLen 原始长度

 */

async function proxy(urlObj, reqInit, rawLen) {

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

    const resHdrOld = res.headers;

    const resHdrNew = new Headers(resHdrOld);

  

    // 验证长度

    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

    });

}

  

async function ADD(envadd) {

    var addtext = envadd.replace(/[  |"'\r\n]+/g, ',').replace(/,+/g, ','); // 将空格、双引号、单引号和换行符替换为逗号

    if (addtext.charAt(0) == ',') addtext = addtext.slice(1);

    if (addtext.charAt(addtext.length - 1) == ',') addtext = addtext.slice(0, addtext.length - 1);

    const add = addtext.split(',');

    return add;

}

自定义服务器地址改成你自己要用的域名

部署成功后绑定自定义域名,域名需要在 cloudflare 中接管。

830212c4-9e8d-471f-8c2b-9f25a06566a2.webpwebp

三、使用

编辑 docker 配置文件,填入自定义域名即可拉去镜像。

vi /etc/docker/daemon.json

加入以下代码

{
  "registry-mirrors": ["https://xxxxxxxxxxxxx"] 
}

换成自己域名

重启 docker

systemctl daemon-reload
systemctl restart docker

四、总结

网络不好的无奈之举罢了,用来中转不错。

参考项目:cmliu/CF-Workers-docker.io

© 2021 - 2024 闲余悟道

载入运行时间...

avatar
关于我

癫狂乐天,狂放不羁,叛世逆俗,天纯不可一世。

跑酷

游戏玩腻,自个寻的,只喜纯跑,不喜炫技

滑板

身处异乡,未找到跑酷伙伴,转投滑板,oli练到死

骑行

通勤会的,通着通着成了爱好,不攀里数,不懂车构,菜腿一枚

爬山

老妈生错了生肖,喜欢在台阶或石头上蹦跶

设计

大学学的,PS效率实用流

代码

培训得的,已弃坑,得益于此,软件玩得起飞,如本博客内容

剪辑

无聊学的,人生苦短,想学的太多,学会罢了,我应该会了了吧……

写作

读书多了自然而然就……he,tui,人人都会,正在精进中

独自环过岛

海南岛,9天骑行,一人一包一帐篷,事故故事一堆堆

只身入过藏

心之所向,21点的太阳妙不可言

万里路

不少,如……

万卷书

微多,如……


人生苦短,繁华三千,可纵情声色,亦可摇桨泛舟。

碎年流光,若都平淡如水,岂非虚度一遭。

波澜不惊的一生虽不可恨,但数载光阴就历尽沧桑的生活应该更畅快。

别试图在文章末尾评论处留言,那只是摆设,不过若是打赏,我定会收到心意。联系方式本页社交链接中有猫腻。

清心决

心若冰清,天塌不惊。

万变犹定,神怡气静。

尘垢不沾,俗相不染。

虚空甯宓,浑然无物。

无有相生,难易相成。

份与物忘,同乎混涅。

天地无涯,万物齐一。

飞花落叶,虚怀若谷。

千般烦忧,才下心头。

即展眉头,灵台清幽。

心无挂碍,意无所执。

解心释神,莫然无魂。

水流心不惊,云在意俱迟。

一心不赘物,古今自逍遥。

自由转载-非商用-转载注明出处即可。