banner
宇外御风的区块链博客

宇外御风的区块链博客

我是哔哩哔哩up主宇外御风,分享有趣网络项目,这是我区块链技术搭建的博客
bilibili
telegram

Set up a Docker image proxy using Cloudflare Workers

1. Preparation Work#

  1. Register and log in to your Cloudflare account

  2. Purchase and configure a domain name

    • Purchase a domain name you like from a domain registrar.
    • Point your domain's DNS to Cloudflare.
  3. Set up Cloudflare Workers

    • Find the Workers tab in the Cloudflare dashboard and create a new worker.

2. Deploy Docker Image Proxy Service#

1. Get the Proxy Source Code#

// _worker.js

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

let blockCrawlerUA = ['netcraft'];

// Select the corresponding upstream address based on the hostname
function routeByHosts(host) {
		// Define the 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",
		"nvcr": "nvcr.io",
		
		// Testing 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', // Preflight request cache time
	}),
}

/**
 * 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 newly constructed response
}

/**
 * Construct a new URL object
 * @param {string} urlStr URL string
 */
function newUrl(urlStr) {
	try {
		return new URL(urlStr) // Attempt to construct a new URL object
	} catch (err) {
		return null // Return null if construction 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;
	
	// Test UUID string using the regular expression
	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); // Get request headers

		let url = new URL(request.url); // Parse request URL
		const userAgentHeader = request.headers.get('User-Agent');
		const userAgent = userAgentHeader ? userAgentHeader.toLowerCase() : "null";
		if (env.UA) blockCrawlerUA = blockCrawlerUA.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 upstream address
		const fakePage = checkHost[1];
		console.log(`Domain header: ${hostTop}\nProxy address: ${hub_host}\nFake homepage: ${fakePage}`);
		const isUuid = isUUID(pathname.split('/')[1].split('/')[0]);
		
		if (blockCrawlerUA.some(fxxk => userAgent.includes(fxxk)) && blockCrawlerUA.length > 0){
			// Change homepage to an nginx fake page
			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'){
					// Change homepage to an nginx fake page
					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);

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

			// Ensure 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 requests
		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 paths
		if (/^\/v2\/[^/]+\/[^/]+\/[^/]+$/.test(url.pathname) && !/^\/v2\/library/.test(url.pathname)) {
			url.pathname = url.pathname.replace(/\/v2\//, '/v2/library/');
			console.log(`modified_url: ${url.pathname}`)
		}

		// Change the hostname of the request
		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 time
		};

		// Add Authorization header
		if (request.headers.has("Authorization")) {
			parameter.headers.Authorization = getReqHeader("Authorization");
		}

		// Initiate request and handle 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;

		// Modify Www-Authenticate header
		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 redirects
		if (new_response_headers.get("Location")) {
			return httpHandler(request, new_response_headers.get("Location"))
		}

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

/**
 * Handle HTTP requests
 * @param {Request} req Request object
 * @param {string} pathname Request path
 */
function httpHandler(req, pathname) {
	const reqHdrRaw = req.headers

	// Handle preflight requests
	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)
}

/**
 * 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)

	// Validate 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 newlines 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 ;
}

worker.js

  • You can use existing open-source projects like cloudflare-docker-proxy, or write your own proxy logic.
  • Here, we take cloudflare-docker-proxy as an example and clone its source code from GitHub.

2. Modify Configuration#

S40717-21092022_com.github.android.png

  • Find the configuration file in the source code (such as config.js), and modify the relevant configurations to suit your Docker repository and your domain name.

3. Deploy Source Code to Cloudflare Workers#

  • In the Cloudflare Workers interface, create a new worker and paste or upload your source code.
  • Configure your domain name and routing rules.

4. Test and Validate#

  • Use your domain name along with the Docker image path, such as <your-domain>/image-name:tag, to pull the image.
  • Ensure everything is smooth and check the logs to diagnose any issues.

3. Notes#

  • Ensure your Cloudflare Workers quota is sufficient, as this will consume request counts.
  • Do not expose your domain name online to prevent being spammed.

This is the basic process of setting up a Docker image proxy using Cloudflare Workers. I hope this tutorial helps you! If you have any questions or need further guidance, please feel free to ask.

Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.