利用CFWorkers自建docker镜像代理加速


由于国内已经把官方https://hub.docker.com给墙了,并且国内加速镜像也陆陆续续关闭了,为了保证可用性,故自己折腾了一个代理,由大善人CF提供支持(:

准备

准备一个域名,并且域名解析托管到CF,这里不做赘述,请自行搜索教程

CFWorkers设置

  • 打开CF,登录后在左侧菜单找到Workers & Pages
  • 打开页面后找到创建按钮,在Workerstab页点击创建 Worker,取一个喜欢的名字,然后点部署
  • 部署完成后,点编辑代码,在左边编辑器输入以下内容覆盖,然后把代理里的你的域名替换成自己的(如dockerproxy.example.com)然后点右上角的部署即可
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    194
    195
    196
    197
    198
    199
    200
    201
    202
    203
    204
    205
    206
    207
    208
    209
    210
    211
    212
    213
    214
    215
    216
    217
    218
    219
    220
    221
    222
    223
    224
    225
    226
    227
    228
    229
    230
    231
    232
    233
    234
    235
    236
    237
    238
    239
    240
    241
    242
    243
    244
    245
    246
    247
    248
    249
    250
    251
    252
    253
    254
    255
    256
    257
    258
    259
    260
    261
    262
    263
    264
    265
    266
    267
    268
    269
    270
    271
    272
    273
    274
    275
    276
    277
    278
    279
    280
    281
    282
    283
    284
    285
    286
    287
    288
    289
    290
    291
    292
    293
    294
    295
    296
    297
    298
    299
    300
    301
    302
    303
    304
    305
    306
    307
    308
    309
    310
    311
    312
    313
    314
    315
    316
    317
    318
    // _worker.js

    // Docker镜像仓库主机地址
    let hub_host = 'registry-1.docker.io'
    // Docker认证服务器地址
    const auth_url = 'https://auth.docker.io'
    // 自定义的工作服务器地址
    let workers_url = 'https://你的域名'

    // 根据主机名选择对应的上游地址
    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",

    // 测试环境
    "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 ;
    }

    export default {
    async fetch(request, env, ctx) {
    const getReqHeader = (key) => request.headers.get(key); // 获取请求头

    let url = new URL(request.url); // 解析请求URL
    workers_url = `https://${url.hostname}`;
    const pathname = url.pathname;
    const hostname = url.searchParams.get('hubhost') || url.hostname;
    const hostTop = hostname.split('.')[0];// 获取主机名的第一部分
    const checkHost = routeByHosts(hostTop);
    hub_host = checkHost[0]; // 获取上游地址
    const fakePage = checkHost[1];
    console.log(`域名头部: ${hostTop}\n反代地址: ${hub_host}\n伪装首页: ${fakePage}`);
    const isUuid = isUUID(pathname.split('/')[1].split('/')[0]);

    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));
    }

    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 (/^\/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 // 缓存时间
    };

    // 添加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
    })
    }
  • 如果需要自定义域名则继续往下看,不需要则用cf提供的类似https://自定义名称.自有域名.workers.dev即可
  • 点击返回,然后依次点击设置-触发器会看到个自定义域,点击添加然后填写兹定于域名即可
  • 还可以通过环境变量功能伪装首页,点击变量-添加变量,变量名称可填URL302URL,值就填写伪装站地址,如https://baidu.com/即可

使用

1
2
3
4
5
6
7
官方镜像路径前面加域名:
docker pull dockerproxy.example.com.com/xxx
一键设置镜像加速:
vim /etc/docker/daemon.json 添加以下代码:
{
"registry-mirrors": ["https://dockerproxy.example.com"] # 请替换为您自己的Worker自定义域名
}

参考文章