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.
This commit is contained in:
xixu-me committed 2025-12-12 13:16:21 +08:00
1 parent 07f5f35972
commit e8d7a0ef6b
2 files changed
+78 -34

No files matched your search

+11 -31
View File
@@ -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;
}
+67 -3
View File
@@ -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
});