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.
This commit is contained in:
1 parent
af22287f2f
commit
46288a92da
1 file changed
+62
-219
+62
-219
@@ -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
|
||||
|
||||
Reference in new issue
Block a user