From 46288a92daa0cd6fabde18eea4e0ba5653720cee Mon Sep 17 00:00:00 2001 From: Xi Xu Date: Fri, 25 Jul 2025 18:07:00 +0800 Subject: [PATCH] Refactor Docker registry request handling Simplifies Docker authentication and registry path handling by removing redundant logic and enforcing /cr/ prefix for Docker registry requests. Updates allowed HTTP methods for Git and Docker, streamlines DockerHub library image path transformation, and improves unauthorized response handling. --- src/index.js | 281 ++++++++++++--------------------------------------- 1 file changed, 62 insertions(+), 219 deletions(-) diff --git a/src/index.js b/src/index.js index 1914199..5fae7de 100644 --- a/src/index.js +++ b/src/index.js @@ -113,9 +113,7 @@ function validateRequest(request, url) { const isDocker = isDockerRequest(request, url); const allowedMethods = - isGit || isDocker - ? ['GET', 'HEAD', 'POST', 'PUT', 'PATCH', 'DELETE'] - : CONFIG.SECURITY.ALLOWED_METHODS; + isGit || isDocker ? ['GET', 'HEAD', 'POST', 'PUT', 'PATCH'] : CONFIG.SECURITY.ALLOWED_METHODS; if (!allowedMethods.includes(request.method)) { return { valid: false, error: 'Method not allowed', status: 405 }; @@ -192,10 +190,8 @@ function responseUnauthorized(url) { const headers = new Headers(); headers.set( 'WWW-Authenticate', - `Bearer realm="https://${url.hostname}/v2/auth",service="cloudflare-docker-proxy"` + `Bearer realm="https://${url.hostname}/v2/auth",service="Xget"` ); - headers.set('Content-Type', 'application/json'); - headers.set('Docker-Distribution-Api-Version', 'registry/2.0'); return new Response(JSON.stringify({ message: 'UNAUTHORIZED' }), { status: 401, headers: headers @@ -216,139 +212,8 @@ async function handleRequest(request, env, ctx) { const monitor = new PerformanceMonitor(); - // Handle Docker authentication FIRST - before any other processing - if (isDocker && url.pathname === '/v2/auth') { - console.log('Handling Docker auth request:', url.toString()); - - // Extract the platform from the URL for auth requests - let authPlatform = 'cr-docker'; // default to Docker Hub - - // Check if the request came from a specific registry based on the referrer or service - const service = url.searchParams.get('service'); - if (service && service !== 'cloudflare-docker-proxy') { - // If service is set to a real service name, try to infer the platform - if (service === 'registry.docker.io') authPlatform = 'cr-docker'; - else if (service.includes('quay.io')) authPlatform = 'cr-quay'; - else if (service.includes('gcr.io')) authPlatform = 'cr-gcr'; - // Add more platform detection as needed - } - - try { - const newUrl = new URL(CONFIG.PLATFORMS[authPlatform] + '/v2/'); - const resp = await fetch(newUrl.toString(), { - method: 'GET', - redirect: 'follow' - }); - - if (resp.status !== 401) { - // If no auth required, just return success response with empty token - return new Response(JSON.stringify({ token: '', access_token: '' }), { - status: 200, - headers: { 'Content-Type': 'application/json' } - }); - } - - const authenticateStr = resp.headers.get('WWW-Authenticate'); - if (authenticateStr === null) { - return new Response(JSON.stringify({ error: 'No authentication challenge found' }), { - status: 500, - headers: { 'Content-Type': 'application/json' } - }); - } - - const wwwAuthenticate = parseAuthenticate(authenticateStr); - let scope = url.searchParams.get('scope'); - - // Autocomplete repo part into scope for DockerHub library images - // Example: repository:busybox:pull => repository:library/busybox:pull - if (scope && authPlatform === 'cr-docker') { - let scopeParts = scope.split(':'); - if (scopeParts.length == 3 && !scopeParts[1].includes('/')) { - scopeParts[1] = 'library/' + scopeParts[1]; - scope = scopeParts.join(':'); - } - } - - const tokenResponse = await fetchToken( - wwwAuthenticate, - scope || '', - request.headers.get('Authorization') || '' - ); - - // Check if token request was successful - if (!tokenResponse.ok) { - const errorText = await tokenResponse.text().catch(() => 'Unknown error'); - console.error('Token fetch failed:', errorText); - return new Response( - JSON.stringify({ - error: 'Authentication failed', - details: errorText - }), - { - status: tokenResponse.status, - headers: { 'Content-Type': 'application/json' } - } - ); - } - - // Ensure the response has correct content type - const tokenData = await tokenResponse.text(); - console.log('Token response:', tokenData); - return new Response(tokenData, { - status: tokenResponse.status, - headers: { 'Content-Type': 'application/json' } - }); - } catch (error) { - console.error('Error in Docker auth:', error); - const message = error instanceof Error ? error.message : String(error); - return new Response( - JSON.stringify({ - error: 'Authentication error', - message: message - }), - { - status: 500, - headers: { 'Content-Type': 'application/json' } - } - ); - } - } - // Handle Docker API version check if (isDocker && (url.pathname === '/v2/' || url.pathname === '/v2')) { - // For Docker v2 API endpoint, we need to check if authentication is required - try { - // Default to Docker Hub if no specific platform in path - let platform = 'cr-docker'; - - // Extract platform from path if present (e.g., /v2/ -> check default, /cr/docker/v2/ -> docker) - if (url.pathname.startsWith('/cr/')) { - const pathParts = url.pathname.split('/'); - if (pathParts.length >= 3) { - platform = `cr-${pathParts[2]}`; - } - } - - // Check if the upstream registry requires authentication - if (CONFIG.PLATFORMS[platform]) { - const upstreamUrl = `${CONFIG.PLATFORMS[platform]}/v2/`; - const upstreamResponse = await fetch(upstreamUrl, { - method: 'GET', - redirect: 'follow' - }); - - // If upstream returns 401, we should return 401 to trigger proper auth flow - if (upstreamResponse.status === 401) { - return responseUnauthorized(url); - } - } - } catch (error) { - console.log('Error checking upstream /v2/:', error); - // If we can't check upstream, assume auth is required for safety - return responseUnauthorized(url); - } - - // If no auth required or we can't determine, return success const headers = new Headers({ 'Docker-Distribution-Api-Version': 'registry/2.0', 'Content-Type': 'application/json' @@ -377,30 +242,16 @@ async function handleRequest(request, env, ctx) { // Handle Docker registry paths specially if (isDocker) { - // Skip /cr/ prefix requirement for auth endpoint - if (url.pathname === '/v2/auth') { - // Auth endpoint doesn't need /cr/ prefix, keep as is - effectivePath = url.pathname; - } else { - // For Docker requests, if they don't start with /cr/ and aren't /v2/ endpoint, - // we need to determine the default registry - if ( - !url.pathname.startsWith('/cr/') && - !url.pathname.startsWith('/v2/') && - url.pathname !== '/v2' - ) { - // Default to Docker Hub for paths like /nginx/manifests/latest - effectivePath = `/cr/docker${url.pathname}`; - } else if (url.pathname.startsWith('/v2/') && !url.pathname.startsWith('/v2/cr/')) { - // For paths like /v2/nginx/manifests/latest, default to Docker Hub - effectivePath = url.pathname.replace(/^\/v2/, '/cr/docker'); - } else if (!url.pathname.startsWith('/cr/')) { - // For other cases, default to Docker Hub - effectivePath = `/cr/docker${url.pathname}`; - } - // Remove /v2 from the beginning if present for processing - effectivePath = effectivePath.replace(/^\/v2/, ''); + // For Docker requests (excluding version check which is handled above), + // check if they have /cr/ prefix + if (!url.pathname.startsWith('/cr/') && !url.pathname.startsWith('/v2/cr/')) { + return new Response('Docker registry requests must use /cr/ prefix', { + status: 400, + headers: addSecurityHeaders(new Headers()) + }); } + // Remove /v2 from the path for Docker registry API consistency if present + effectivePath = url.pathname.replace(/^\/v2/, ''); } // Platform detection using transform patterns @@ -418,19 +269,9 @@ async function handleRequest(request, env, ctx) { return effectivePath.startsWith(expectedPrefix); }) || effectivePath.split('/')[1]; - // Special handling for Docker auth endpoint - if (isDocker && url.pathname === '/v2/auth') { - platform = 'cr-docker'; // Default to Docker Hub for auth - } - if (!platform || !CONFIG.PLATFORMS[platform]) { - // Special case: if this is Docker auth endpoint, don't redirect - if (isDocker && url.pathname === '/v2/auth') { - platform = 'cr-docker'; - } else { - const HOME_PAGE_URL = 'https://github.com/xixu-me/Xget'; - return Response.redirect(HOME_PAGE_URL, 302); - } + const HOME_PAGE_URL = 'https://github.com/xixu-me/Xget'; + return Response.redirect(HOME_PAGE_URL, 302); } // Transform URL based on platform using unified logic @@ -441,12 +282,11 @@ async function handleRequest(request, env, ctx) { if (platform.startsWith('cr-')) { finalTargetPath = `/v2${targetPath}`; - // Handle Docker Hub library image path transformation for the target URL + // Handle Docker Hub library image path transformation // Example: /v2/nginx/manifests/latest => /v2/library/nginx/manifests/latest if (platform === 'cr-docker') { const pathParts = finalTargetPath.split('/'); - // Check if this is a library image path (no namespace) that needs library/ prefix - // Format: /v2/imagename/manifests/tag or /v2/imagename/blobs/sha256:... + // Check if this is a library image path (no namespace) if (pathParts.length === 5 && (pathParts[3] === 'manifests' || pathParts[3] === 'blobs')) { pathParts.splice(2, 0, 'library'); finalTargetPath = pathParts.join('/'); @@ -460,41 +300,39 @@ async function handleRequest(request, env, ctx) { const isDockerHub = platform === 'cr-docker'; const authorization = request.headers.get('Authorization'); + // Handle Docker authentication + if (isDocker && url.pathname === '/v2/auth') { + const newUrl = new URL(CONFIG.PLATFORMS[platform] + '/v2/'); + const resp = await fetch(newUrl.toString(), { + method: 'GET', + redirect: 'follow' + }); + if (resp.status !== 401) { + return resp; + } + const authenticateStr = resp.headers.get('WWW-Authenticate'); + if (authenticateStr === null) { + return resp; + } + const wwwAuthenticate = parseAuthenticate(authenticateStr); + let scope = url.searchParams.get('scope'); + // autocomplete repo part into scope for DockerHub library images + // Example: repository:busybox:pull => repository:library/busybox:pull + if (scope && isDockerHub) { + let scopeParts = scope.split(':'); + if (scopeParts.length == 3 && !scopeParts[1].includes('/')) { + scopeParts[1] = 'library/' + scopeParts[1]; + scope = scopeParts.join(':'); + } + } + return await fetchToken(wwwAuthenticate, scope || '', authorization || ''); + } + // Handle DockerHub library image redirects before making the request if (isDocker && isDockerHub) { const pathParts = url.pathname.split('/'); - // For Docker Hub, check if this is a library image path that needs redirection - // Handle different path formats: - // 1. /cr/docker/nginx/manifests/tag -> /cr/docker/library/nginx/manifests/tag - // 2. /v2/nginx/manifests/tag -> /v2/library/nginx/manifests/tag - // 3. /nginx/manifests/tag -> /library/nginx/manifests/tag (after transformation) - - let needsRedirect = false; - let redirectPath = ''; - - if (pathParts.length === 5 && (pathParts[3] === 'manifests' || pathParts[3] === 'blobs')) { - // Format: /cr/docker/nginx/manifests/tag - pathParts.splice(3, 0, 'library'); - needsRedirect = true; - } else if ( - pathParts.length === 4 && - (pathParts[2] === 'manifests' || pathParts[2] === 'blobs') - ) { - // Format: /v2/nginx/manifests/tag + if (pathParts.length == 5) { pathParts.splice(2, 0, 'library'); - needsRedirect = true; - } else if ( - pathParts.length === 6 && - pathParts[1] === 'cr' && - pathParts[2] === 'docker' && - (pathParts[4] === 'manifests' || pathParts[4] === 'blobs') - ) { - // Format: /cr/docker/nginx/manifests/tag (6 parts, needs library) - pathParts.splice(3, 0, 'library'); - needsRedirect = true; - } - - if (needsRedirect) { const redirectUrl = new URL(url); redirectUrl.pathname = pathParts.join('/'); return Response.redirect(redirectUrl, 301); @@ -527,7 +365,7 @@ async function handleRequest(request, env, ctx) { }; // Add body for POST/PUT/PATCH requests (Git/Docker operations) - if (['POST', 'PUT', 'PATCH', 'DELETE'].includes(request.method) && (isGit || isDocker)) { + if (['POST', 'PUT', 'PATCH'].includes(request.method) && (isGit || isDocker)) { fetchOptions.body = request.body; } @@ -611,7 +449,7 @@ async function handleRequest(request, env, ctx) { clearTimeout(timeoutId); - // Handle Docker Hub blob redirects manually (similar to cloudflare-docker-proxy) + // Handle Docker Hub blob redirects manually if (isDocker && isDockerHub && response.status === 307) { const location = response.headers.get('Location'); if (location) { @@ -647,8 +485,7 @@ async function handleRequest(request, env, ctx) { // Remove /v2 and platform prefix to get the repo path const repoPath = pathParts.slice(4).join('/'); // Skip /v2/cr/docker const repoParts = repoPath.split('/'); - if (repoParts.length >= 3) { - // For paths like library/nginx/manifests/latest, repo is library/nginx + if (repoParts.length >= 1) { const repoName = repoParts.slice(0, -2).join('/'); // Remove /manifests/tag or /blobs/sha if (repoName) { scope = `repository:${repoName}:pull`; @@ -656,24 +493,30 @@ async function handleRequest(request, env, ctx) { } } + // For Docker Hub library images, adjust the scope + if (isDockerHub && scope) { + let scopeParts = scope.split(':'); + if (scopeParts.length === 3 && !scopeParts[1].includes('/')) { + scopeParts[1] = 'library/' + scopeParts[1]; + scope = scopeParts.join(':'); + } + } + // Try to get a token for public access (without authorization) const tokenResponse = await fetchToken(wwwAuthenticate, scope || '', ''); if (tokenResponse.ok) { const tokenData = await tokenResponse.json(); - if (tokenData.token || tokenData.access_token) { + if (tokenData.token) { // Retry the original request with the obtained token const retryHeaders = new Headers(requestHeaders); - retryHeaders.set( - 'Authorization', - `Bearer ${tokenData.token || tokenData.access_token}` - ); + retryHeaders.set('Authorization', `Bearer ${tokenData.token}`); const retryResponse = await fetch(targetUrl, { ...finalFetchOptions, headers: retryHeaders }); - if (retryResponse.ok || retryResponse.status === 206) { + if (retryResponse.ok) { response = retryResponse; monitor.mark('success'); break; @@ -685,9 +528,9 @@ async function handleRequest(request, env, ctx) { } } - // If token fetch failed or didn't work, return the original 401 response - // This will trigger proper Docker authentication flow in the client - return response; + // If token fetch failed or didn't work, return the unauthorized response + // Only return this if we truly can't access the resource + return responseUnauthorized(url); } // Don't retry on client errors (4xx) - these won't improve with retries