From e8d7a0ef6b4bb1024f0bd46d3273db21d7c5b01a Mon Sep 17 00:00:00 2001 From: Xi Xu Date: Fri, 12 Dec 2025 13:16:21 +0800 Subject: [PATCH] Refactor Docker scope extraction and 401 response Moved Docker registry scope extraction logic into a new getScopeFromUrl function in docker.js for reuse and clarity. Updated responseUnauthorized to accept an optional scope and improved its error response format to be more Docker/OCI-compliant. Refactored index.js to use the new scope extraction and 401 response logic. --- src/index.js | 42 +++++++------------------ src/protocols/docker.js | 70 +++++++++++++++++++++++++++++++++++++++-- 2 files changed, 78 insertions(+), 34 deletions(-) diff --git a/src/index.js b/src/index.js index bdbe9a1..e662006 100644 --- a/src/index.js +++ b/src/index.js @@ -12,10 +12,11 @@ import { CONFIG, createConfig } from './config/index.js'; import { SORTED_PLATFORMS, transformPath } from './config/platforms.js'; import { configureAIHeaders, isAIInferenceRequest } from './protocols/ai.js'; import { - fetchToken, - handleDockerAuth, - parseAuthenticate, - responseUnauthorized + fetchToken, + getScopeFromUrl, + handleDockerAuth, + parseAuthenticate, + responseUnauthorized } from './protocols/docker.js'; import { configureGitHeaders, isGitLFSRequest, isGitRequest } from './protocols/git.js'; import { PerformanceMonitor, addPerformanceHeaders } from './utils/performance.js'; @@ -336,36 +337,15 @@ async function handleRequest(request, env, ctx) { monitor.mark('docker_auth_challenge'); const authenticateStr = response.headers.get('WWW-Authenticate'); + let scope = ''; + + // Calculate scope first so we can use it for both token fetch and unauthorized response + scope = getScopeFromUrl(url, effectivePath, platform); + if (authenticateStr) { try { const wwwAuthenticate = parseAuthenticate(authenticateStr); - // Infer scope from the request path for container registry requests - let scope = ''; - const pathParts = url.pathname.split('/'); - if (pathParts.length >= 4 && pathParts[1] === 'v2') { - const platformPrefix = `/${platform.replace(/-/g, '/')}/`; - if (effectivePath.startsWith(platformPrefix)) { - const repoPathFull = effectivePath.slice(platformPrefix.length); - const repoParts = repoPathFull.split('/'); - if (repoParts.length >= 1) { - let repoName = repoParts.slice(0, -2).join('/'); // Remove /manifests/tag or /blobs/sha - - if ( - platform === 'cr-docker' && - repoName && - !repoName.includes('/') - ) { - repoName = `library/${repoName}`; - } - - if (repoName) { - scope = `repository:${repoName}:pull`; - } - } - } - } - // Try to get a token for public access (without authorization) const tokenResponse = await fetchToken( wwwAuthenticate, @@ -396,7 +376,7 @@ async function handleRequest(request, env, ctx) { } } - response = responseUnauthorized(url); + response = responseUnauthorized(url, scope); break; } diff --git a/src/protocols/docker.js b/src/protocols/docker.js index e114bca..f4631b1 100644 --- a/src/protocols/docker.js +++ b/src/protocols/docker.js @@ -72,18 +72,82 @@ export async function fetchToken(wwwAuthenticate, scope, authorization) { return await fetch(url, { method: 'GET', headers }); } +/** + * Parses the request URL to determine the appropriate Docker registry scope. + * + * Analyzes the path to extract the repository name and constructs a standard + * Docker scope string (repository:name:pull). Handles platform-specific + * path conventions and defaults. + * @param {URL} url - The request URL + * @param {string} effectivePath - The effective path after stripping prefixes + * @param {string} platform - The platform identifier (e.g., 'cr-docker') + * @returns {string} One of: + * - "repository:name:pull" for repository access + * - "registry:catalog:*" for catalog access + * - "" (empty string) if scope cannot be determined + */ +export function getScopeFromUrl(url, effectivePath, platform) { + // Infer scope from the request path for container registry requests + let scope = ''; + const pathParts = url.pathname.split('/'); + + // Check for catalog endpoint + if (pathParts.includes('_catalog')) { + return 'registry:catalog:*'; + } + + if (pathParts.length >= 4 && pathParts[1] === 'v2') { + const platformPrefix = `/${platform.replace(/-/g, '/')}/`; + if (effectivePath.startsWith(platformPrefix)) { + const repoPathFull = effectivePath.slice(platformPrefix.length); + const repoParts = repoPathFull.split('/'); + if (repoParts.length >= 1) { + // Remove /manifests/tag or /blobs/sha suffix to get repo name + // Common suffixes in v2 API: /manifests/, /blobs/, /tags/ + const suffixIndex = repoParts.findIndex(p => + ['manifests', 'blobs', 'tags', 'referrers'].includes(p) + ); + + let repoName = suffixIndex !== -1 + ? repoParts.slice(0, suffixIndex).join('/') + : repoParts.join('/'); + + if (platform === 'cr-docker' && repoName && !repoName.includes('/')) { + repoName = `library/${repoName}`; + } + + if (repoName) { + scope = `repository:${repoName}:pull`; + } + } + } + } + return scope; +} + /** * Creates an unauthorized (401) response for container registry authentication. * * Generates a Docker/OCI registry-compliant 401 response with a WWW-Authenticate * header that directs clients to the token authentication endpoint. * @param {URL} url - Request URL used to construct authentication realm + * @param {string} [scope] - Optional scope to include in the challenge * @returns {Response} Unauthorized response with WWW-Authenticate header */ -export function responseUnauthorized(url) { +export function responseUnauthorized(url, scope) { const headers = new Headers(); - headers.set('WWW-Authenticate', `Bearer realm="https://${url.hostname}/v2/auth",service="Xget"`); - return new Response(JSON.stringify({ message: 'UNAUTHORIZED' }), { + let authHeader = `Bearer realm="https://${url.hostname}/v2/auth",service="Xget"`; + if (scope) { + authHeader += `,scope="${scope}"`; + } + headers.set('WWW-Authenticate', authHeader); + return new Response(JSON.stringify({ + errors: [{ + code: 'UNAUTHORIZED', + message: 'authentication required', + detail: null + }] + }), { status: 401, headers });