返回文章列表文章索引阅读全文
STARBLOG / 阅读手记
docker

Using Cloudflare Worker to accelerate Docker images

The request was rejected because it was considered high risk

曦远 (DealiAxy)约 37 分钟
目录7 个章节

Preface

Developers have it harder now 🥱. Nowadays, Docker mirrors (Docker镜像) within China are also all gone 🈚️. Without mirrors, using Docker 🦈 is extremely difficult, and proxies 🌍 are very slow.

The only remaining option now is to build your own mirror, which is a pain 😭.

There are open-source projects on GitHub that can help you quickly set up your own mirror registry, but it’s still somewhat troublesome. Fortunately, Cloudflare is still alive for now 😮‍💨.

This article documents the method of using Cloudflare Worker to set up a Docker mirror (Docker镜像) 📒.


About Workers

Cloudflare Workers is a serverless computing platform provided by Cloudflare that allows you to run code written in languages like JavaScript, Rust, C, C++, and COBOL on Cloudflare's global edge network. It is primarily used to build high-performance, highly scalable web applications and APIs. 👍

Cloudflare Workers is ideal for handling various network requests, such as API gateways, content customization, and dynamic rendering, and can also be used to modify or determine how network requests are responded to. It empowers developers to rapidly deploy applications and services globally.

Some key features:

  1. Edge Computing: Since code is executed directly on Cloudflare's edge nodes, it can significantly reduce data transmission time and improve response speed.
  2. Serverless Architecture: You don't need to manage any servers or instances; just focus on writing and deploying code. Cloudflare takes care of running and scaling the code.
  3. Pay-as-you-go Billing: Cloudflare Workers' billing model is based on the number of requests and execution time, rather than pre-allocated resources, meaning you pay only for what you use.
  4. Highly Integrated: It can be tightly integrated with other Cloudflare services (like KV storage, Durable Objects, etc.), facilitating data storage and state management.
  5. Security: Since Workers run in an isolated environment, they provide a higher level of security compared to traditional servers or containers.

Create a worker

Open the Cloudflare Worker and Pages page, create a worker

Give it a descriptive name, such as docker-proxy or something similar

Then click the【Deploy】button

Edit Code

Once deployment is complete✅, click the [Edit Code] button next to it.

Input the following JavaScript code 👨‍🏫

You need to replace workers_url with your own domain, for example https://docker.example.com.

// Docker image registry host address
let hub_host = 'registry-1.docker.io'
// Docker authentication server URL
const auth_url = 'https://auth.docker.io'
// Custom worker server URL
let workers_url = 'https://your-domain'

let blockCrawlerUA = ['netcraft'];

// Select upstream address based on host name function routeByHosts(host) { // Define routing table const routes = { // Production environment "quay": "quay.io", "gcr": "gcr.io", "k8s-gcr": "k8s.gcr.io", "k8s": "registry.k8s.io", "ghcr": "ghcr.io", "cloudsmith": "docker.cloudsmith.io",

    // Test environment
    "test": "registry-1.docker.io",
  };
  if (host in routes) return [ routes[host], false ];
  else return [ hub_host, true ];
}

/** @type {RequestInit} / const PREFLIGHT_INIT = { // Preflight request configuration headers: new Headers({ 'access-control-allow-origin': '', // Allow all origins 'access-control-allow-methods': 'GET,POST,PUT,PATCH,TRACE,DELETE,HEAD,OPTIONS', // Allowed HTTP methods 'access-control-max-age': '1728000', // Cache duration for preflight requests }), }

/**

  • Construct response
  • @param {any} body Response body
  • @param {number} status Response status code
  • @param {Object<string, string>} headers Response headers / function makeRes(body, status = 200, headers = {}) { headers['access-control-allow-origin'] = '' // Allow all origins return new Response(body, { status, headers }) // Return a newly constructed response }
/**
 * Construct a new URL object
 * @param {string} urlStr URL string
 */
function newUrl(urlStr) {
  try {
    return new URL(urlStr) // Attempt to create a new URL object
  } catch (err) {
    return null // Return null if creation fails
  }
}
function isUUID(uuid) {
  // Define a regular expression to match UUID format
  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;

// Use a regular expression to test the UUID string 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 ;


```markdown
export default {
  async fetch(request, env, ctx) {
    const getReqHeader = (key) => request.headers.get(key); // Get request header
let url = new URL(request.url); // Parse the request URL
const userAgentHeader = request.headers.get('User-Agent');
const userAgent = userAgentHeader ? userAgentHeader.toLowerCase() : "null";
if (env.UA) blockBotUA = blockBotUA.concat(await ADD(env.UA));
workers_url = `https://${url.hostname}`;
const pathname = url.pathname;
const hostname = url.searchParams.get('hubhost') || url.hostname; 
const hostTop = hostname.split('.')[0]; // Get the first part of the hostname
const checkHost = routeByHosts(hostTop);
hub_host = checkHost[0]; // Get the upstream address
const fakePage = checkHost[1];
console.log(`Domain head: ${hostTop}\nReverse proxy address: ${hub_host}\nFake homepage: ${fakePage}`);
const isUuid = isUUID(pathname.split('/')[1].split('/')[0]);
    if (blockCrawlerUA.some(fxxk => userAgent.includes(fxxk)) && blockCrawlerUA.length > 0){
      //Redirect homepage to an nginx decoy page
      return new Response(await nginx(), {
        headers: {
          'Content-Type': 'text/html; charset=UTF-8',
        },
      });
    }

Translation notes:

  • 屏蔽爬虫UA → blockCrawlerUA (a variable storing user-agent strings to block)
  • 首页改成一个nginx伪装页 → "Redirect homepage to an nginx decoy page"

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); // Get the complete registry URL

// Copy the original request headers const headers = new Headers(request.headers);

// Ensure the Host header is replaced with 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);
    }

// Modify requests containing %2F and %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}) }

// Handle token request 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) }

// Modify /v2/ request path if (/^/v2/[^/]+/[^/]+/[^/]+$/.test(url.pathname) && !/^/v2/library/.test(url.pathname)) { url.pathname = url.pathname.replace(//v2//, '/v2/library/'); console.log(modified_url: ${url.pathname}) }

// Modify the request's hostname url.hostname = hub_host;

    // Construct request parameters
    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 // Cache duration
    };
    // 添加Authorization头
    if (request.headers.has("Authorization")) {
      parameter.headers.Authorization = getReqHeader("Authorization");
    }
// Initiate request and process response
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));
}

// Handle redirect if (new_response_headers.get("Location")) { return httpHandler(request, new_response_headers.get("Location")) }

  // Return the modified response
    let response = new Response(original_text, {
      status,
      headers: new_response_headers
    })
    return response;
  }
};

/**

  • Handle HTTP Requests (Process)
  • @param {Request} req The request object
  • @param {string} pathname The request path */ function httpHandler(req, pathname) { const reqHdrRaw = req.headers
  // Handle preflight request
  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 = new URL(urlStr)
/** @type {RequestInit} */
const reqInit = {
  method: req.method,
  headers: reqHdrNew,
  redirect: 'follow',
  body: req.body
}
return proxy(urlObj, reqInit, rawLen)

/**

  • Proxy request
  • @param {URL} urlObj URL object
  • @param {RequestInit} reqInit Request initialization object
  • @param {string} rawLen Original length */ async function proxy(urlObj, reqInit, rawLen) { const res = await fetch(urlObj.href, reqInit) const resHdrOld = res.headers const resHdrNew = new Headers(resHdrOld)

// Verify length 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')

// Remove unnecessary headers 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, ',');  // Replace spaces, double quotes, single quotes and newline characters with commas
  //console.log(addtext);
  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(',');
  //console.log(add);
  return add ;
}

OK👌, after entering the code, click [Deploy] again. Once the deployment is successful, you can open the docker🦈hub page.

Setting up the Domain

Didn't we just configure the domain in the code earlier❓

We need to continue with the configuration now.

Go back to the【Workers and Pages / docker-proxy】configuration page.

Click【Settings】-【Triggers】🥬

Under【Custom domains】, click to add, and enter the domain name, for example 🌰 docker.example.com

All done🤝. Cloudflare will automatically apply for an SSL certificate and handle everything else✔️

Configure Registry

Edit the /etc/docker/daemon.json file

{
  "registry-mirrors": ["https://docker.example.com"],
  "insecure-registries": ["docker.example.com"]
}

Restart Docker

sudo systemctl daemon-reload
sudo systemctl restart docker

Then, when you pull images, the speed will be fast like taking off 🛫✈️.

References

_EOF_

Using Cloudflare Worker to accelerate Docker images

本文作者:
程序设计实验室
版权声明:
本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!

相关文章

讨论

共 0 条

良言一句三冬暖,恶语伤人六月寒。

加载评论中…

Using Cloudflare Worker to accelerate Docker images · StarBlog