diff --git a/src/config/platforms.js b/src/config/platforms.js index 8558240..7f71eb1 100644 --- a/src/config/platforms.js +++ b/src/config/platforms.js @@ -13,7 +13,6 @@ export const PLATFORMS = { 'conda-community': 'https://conda.anaconda.org', // Container Registries - 'cr-docker': 'https://registry-1.docker.io', 'cr-quay': 'https://quay.io', 'cr-gcr': 'https://gcr.io', 'cr-mcr': 'https://mcr.microsoft.com', diff --git a/src/index.js b/src/index.js index 5fae7de..3117263 100644 --- a/src/index.js +++ b/src/index.js @@ -34,13 +34,13 @@ class PerformanceMonitor { } /** - * Detects if a request is a Docker registry operation + * Detects if a request is a container registry operation * @param {Request} request - The incoming request object * @param {URL} url - Parsed URL object - * @returns {boolean} True if this is a Docker registry operation + * @returns {boolean} True if this is a container registry operation */ function isDockerRequest(request, url) { - // Check for Docker registry API endpoints + // Check for container registry API endpoints if (url.pathname.startsWith('/v2/')) { return true; } @@ -160,7 +160,7 @@ function parseAuthenticate(authenticateStr) { } /** - * Fetches authentication token from Docker registry + * Fetches authentication token from container registry * @param {{realm: string, service: string}} wwwAuthenticate - Authentication info * @param {string} scope - The scope for the token * @param {string} authorization - Authorization header value @@ -182,16 +182,13 @@ async function fetchToken(wwwAuthenticate, scope, authorization) { } /** - * Creates unauthorized response for Docker registry + * Creates unauthorized response for container registry * @param {URL} url - Request URL * @returns {Response} Unauthorized response */ function responseUnauthorized(url) { const headers = new Headers(); - headers.set( - 'WWW-Authenticate', - `Bearer realm="https://${url.hostname}/v2/auth",service="Xget"` - ); + headers.set('WWW-Authenticate', `Bearer realm="https://${url.hostname}/v2/auth",service="Xget"`); return new Response(JSON.stringify({ message: 'UNAUTHORIZED' }), { status: 401, headers: headers @@ -240,17 +237,17 @@ async function handleRequest(request, env, ctx) { let platform; let effectivePath = url.pathname; - // Handle Docker registry paths specially + // Handle container registry paths specially if (isDocker) { // 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', { + return new Response('container registry requests must use /cr/ prefix', { status: 400, headers: addSecurityHeaders(new Headers()) }); } - // Remove /v2 from the path for Docker registry API consistency if present + // Remove /v2 from the path for container registry API consistency if present effectivePath = url.pathname.replace(/^\/v2/, ''); } @@ -277,27 +274,15 @@ async function handleRequest(request, env, ctx) { // Transform URL based on platform using unified logic const targetPath = transformPath(effectivePath, platform); - // For Docker registries, ensure we add the /v2 prefix for the Docker API + // For container registries, ensure we add the /v2 prefix for the Docker API let finalTargetPath; if (platform.startsWith('cr-')) { finalTargetPath = `/v2${targetPath}`; - - // 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) - if (pathParts.length === 5 && (pathParts[3] === 'manifests' || pathParts[3] === 'blobs')) { - pathParts.splice(2, 0, 'library'); - finalTargetPath = pathParts.join('/'); - } - } } else { finalTargetPath = targetPath; } const targetUrl = `${CONFIG.PLATFORMS[platform]}${finalTargetPath}${url.search}`; - const isDockerHub = platform === 'cr-docker'; const authorization = request.headers.get('Authorization'); // Handle Docker authentication @@ -316,29 +301,9 @@ async function handleRequest(request, env, ctx) { } 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('/'); - if (pathParts.length == 5) { - pathParts.splice(2, 0, 'library'); - const redirectUrl = new URL(url); - redirectUrl.pathname = pathParts.join('/'); - return Response.redirect(redirectUrl, 301); - } - } - // Check if this is a Git operation const isGit = isGitRequest(request, url); @@ -360,8 +325,7 @@ async function handleRequest(request, env, ctx) { const fetchOptions = { method: request.method, headers: new Headers(), - // For Docker Hub, use manual redirect to handle blob redirects properly - redirect: isDockerHub ? 'manual' : 'follow' + redirect: 'follow' }; // Add body for POST/PUT/PATCH requests (Git/Docker operations) @@ -449,41 +413,29 @@ async function handleRequest(request, env, ctx) { clearTimeout(timeoutId); - // Handle Docker Hub blob redirects manually - if (isDocker && isDockerHub && response.status === 307) { - const location = response.headers.get('Location'); - if (location) { - const redirectResponse = await fetch(location, { - method: 'GET', - redirect: 'follow' - }); - response = redirectResponse; - } - } - if (response.ok || response.status === 206) { monitor.mark('success'); break; } - // For Docker registry, handle authentication challenges more intelligently + // For container registry, handle authentication challenges more intelligently if (isDocker && response.status === 401) { monitor.mark('docker_auth_challenge'); - // For Docker registries, first check if we can get a token without credentials + // For container registries, first check if we can get a token without credentials // This allows access to public repositories const authenticateStr = response.headers.get('WWW-Authenticate'); if (authenticateStr) { try { const wwwAuthenticate = parseAuthenticate(authenticateStr); - // Infer scope from the request path for Docker registry requests + // Infer scope from the request path for container registry requests let scope = ''; const pathParts = url.pathname.split('/'); if (pathParts.length >= 4 && pathParts[1] === 'v2') { - // Extract repository name from path like /v2/cr/docker/library/nginx/manifests/latest + // Extract repository name from path like /v2/cr/ghcr/library/nginx/manifests/latest // Remove /v2 and platform prefix to get the repo path - const repoPath = pathParts.slice(4).join('/'); // Skip /v2/cr/docker + const repoPath = pathParts.slice(4).join('/'); // Skip /v2/cr/[registry] const repoParts = repoPath.split('/'); if (repoParts.length >= 1) { const repoName = repoParts.slice(0, -2).join('/'); // Remove /manifests/tag or /blobs/sha @@ -493,15 +445,6 @@ 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) { @@ -578,7 +521,7 @@ async function handleRequest(request, env, ctx) { if (isDocker && response.status === 401) { const errorText = await response.text().catch(() => ''); return new Response( - `Authentication required for this Docker registry resource. This may be a private repository. Original error: ${errorText}`, + `Authentication required for this container registry resource. This may be a private repository. Original error: ${errorText}`, { status: 401, headers: addSecurityHeaders(new Headers()) diff --git a/test/docker-registry.test.js b/test/container-registry.test.js similarity index 63% rename from test/docker-registry.test.js rename to test/container-registry.test.js index 30b25f9..cb113f1 100644 --- a/test/docker-registry.test.js +++ b/test/container-registry.test.js @@ -1,7 +1,7 @@ import { SELF } from 'cloudflare:test'; import { describe, expect, it } from 'vitest'; -describe('Docker Registry Support', () => { +describe('Container Registry Support', () => { describe('Docker API Version Check', () => { it('should handle /v2/ endpoint correctly', async () => { const response = await SELF.fetch('https://example.com/v2/'); @@ -22,23 +22,7 @@ describe('Docker Registry Support', () => { }); }); - describe('Docker Registry URL Transformation', () => { - it('should handle Docker Hub manifest requests', async () => { - const testUrl = 'https://example.com/cr/docker/v2/library/nginx/manifests/latest'; - const response = await SELF.fetch(testUrl, { method: 'HEAD' }); - - // Should attempt to proxy to Docker Hub - expect(response.status).not.toBe(400); - }); - - it('should handle Docker Hub without library prefix', async () => { - const testUrl = 'https://example.com/cr/docker/v2/nginx/manifests/latest'; - const response = await SELF.fetch(testUrl, { method: 'HEAD' }); - - // Should attempt to proxy to Docker Hub with library prefix added - expect(response.status).not.toBe(400); - }); - + describe('Container Registry URL Transformation', () => { it('should handle Quay.io registry requests', async () => { const testUrl = 'https://example.com/cr/quay/v2/bitnami/nginx/manifests/latest'; const response = await SELF.fetch(testUrl, { method: 'HEAD' }); @@ -67,7 +51,7 @@ describe('Docker Registry Support', () => { describe('Docker Authentication', () => { it('should pass through 401 authentication challenges', async () => { // This test simulates an upstream 401 response which should be passed through - const testUrl = 'https://example.com/cr/docker/v2/private/repo/manifests/latest'; + const testUrl = 'https://example.com/cr/ghcr/v2/private/repo/manifests/latest'; const response = await SELF.fetch(testUrl, { headers: { Accept: 'application/vnd.docker.distribution.manifest.v2+json' @@ -81,8 +65,8 @@ describe('Docker Registry Support', () => { } }); - it('should handle Docker registry token requests', async () => { - const testUrl = 'https://example.com/cr/docker/v2/auth'; + it('should handle container registry token requests', async () => { + const testUrl = 'https://example.com/cr/ghcr/v2/auth'; const response = await SELF.fetch(testUrl, { headers: { Authorization: 'Basic dGVzdDp0ZXN0' @@ -97,7 +81,7 @@ describe('Docker Registry Support', () => { describe('Docker Request Detection', () => { it('should detect Docker requests by path', async () => { const response = await SELF.fetch( - 'https://example.com/cr/docker/v2/library/nginx/manifests/latest', + 'https://example.com/cr/ghcr/v2/library/nginx/manifests/latest', { method: 'GET' } @@ -108,28 +92,22 @@ describe('Docker Registry Support', () => { }); it('should detect Docker requests by Accept header', async () => { - const response = await SELF.fetch( - 'https://example.com/cr/docker/v2/test/repo/manifests/tag', - { - headers: { - Accept: 'application/vnd.docker.distribution.manifest.v2+json' - } + const response = await SELF.fetch('https://example.com/cr/ghcr/v2/test/repo/manifests/tag', { + headers: { + Accept: 'application/vnd.docker.distribution.manifest.v2+json' } - ); + }); // Should not reject with 405 (method not allowed) expect(response.status).not.toBe(405); }); it('should detect Docker requests by User-Agent', async () => { - const response = await SELF.fetch( - 'https://example.com/cr/docker/v2/test/repo/manifests/tag', - { - headers: { - 'User-Agent': 'docker/20.10.7' - } + const response = await SELF.fetch('https://example.com/cr/ghcr/v2/test/repo/manifests/tag', { + headers: { + 'User-Agent': 'docker/20.10.7' } - ); + }); // Should not reject with 405 (method not allowed) expect(response.status).not.toBe(405); @@ -139,7 +117,7 @@ describe('Docker Registry Support', () => { describe('Docker HTTP Methods', () => { it('should allow GET for manifest requests', async () => { const response = await SELF.fetch( - 'https://example.com/cr/docker/v2/library/nginx/manifests/latest', + 'https://example.com/cr/ghcr/v2/library/nginx/manifests/latest', { method: 'GET' } @@ -150,7 +128,7 @@ describe('Docker Registry Support', () => { it('should allow HEAD for manifest requests', async () => { const response = await SELF.fetch( - 'https://example.com/cr/docker/v2/library/nginx/manifests/latest', + 'https://example.com/cr/ghcr/v2/library/nginx/manifests/latest', { method: 'HEAD' } @@ -160,63 +138,48 @@ describe('Docker Registry Support', () => { }); it('should allow PUT for manifest uploads', async () => { - const response = await SELF.fetch( - 'https://example.com/cr/docker/v2/test/repo/manifests/tag', - { - method: 'PUT', - headers: { - 'Content-Type': 'application/vnd.docker.distribution.manifest.v2+json' - }, - body: JSON.stringify({ - schemaVersion: 2, - mediaType: 'application/vnd.docker.distribution.manifest.v2+json' - }) - } - ); + const response = await SELF.fetch('https://example.com/cr/ghcr/v2/test/repo/manifests/tag', { + method: 'PUT', + headers: { + 'Content-Type': 'application/vnd.docker.distribution.manifest.v2+json' + }, + body: JSON.stringify({ + schemaVersion: 2, + mediaType: 'application/vnd.docker.distribution.manifest.v2+json' + }) + }); expect(response.status).not.toBe(405); }); it('should allow POST for blob uploads', async () => { - const response = await SELF.fetch( - 'https://example.com/cr/docker/v2/test/repo/blobs/uploads/', - { - method: 'POST', - headers: { - 'Content-Type': 'application/octet-stream' - } + const response = await SELF.fetch('https://example.com/cr/ghcr/v2/test/repo/blobs/uploads/', { + method: 'POST', + headers: { + 'Content-Type': 'application/octet-stream' } - ); + }); expect(response.status).not.toBe(405); }); }); - describe('Docker Registry Error Handling', () => { + describe('Container Registry Error Handling', () => { it('should reject non-cr prefixed Docker requests', async () => { const response = await SELF.fetch('https://example.com/v2/library/nginx/manifests/latest'); expect(response.status).toBe(400); - expect(await response.text()).toContain('Docker registry requests must use /cr/ prefix'); - }); - - it('should handle Docker Hub blob redirects', async () => { - // This test would verify that 307 redirects from Docker Hub are handled - const testUrl = 'https://example.com/cr/docker/v2/library/nginx/blobs/sha256:abc123'; - const response = await SELF.fetch(testUrl); - - // The request should be processed (not fail with a redirect error) - expect([200, 301, 302, 307, 404]).toContain(response.status); + expect(await response.text()).toContain('container registry requests must use /cr/ prefix'); }); }); - describe('Docker Registry Headers', () => { - it('should preserve Docker-specific headers', async () => { + describe('Container Registry Headers', () => { + it('should preserve container-specific headers', async () => { const response = await SELF.fetch( - 'https://example.com/cr/docker/v2/library/nginx/manifests/latest', + 'https://example.com/cr/ghcr/v2/library/nginx/manifests/latest', { headers: { - 'Docker-Content-Digest': 'sha256:abc123', + 'Container-Content-Digest': 'sha256:abc123', Accept: 'application/vnd.docker.distribution.manifest.v2+json', Authorization: 'Bearer token123' } @@ -227,12 +190,12 @@ describe('Docker Registry Support', () => { expect(response.status).not.toBe(400); }); - it('should not cache Docker registry responses', async () => { - const testUrl = 'https://example.com/cr/docker/v2/library/nginx/manifests/latest'; + it('should not cache container registry responses', async () => { + const testUrl = 'https://example.com/cr/ghcr/v2/library/nginx/manifests/latest'; const response = await SELF.fetch(testUrl); - // Docker registry responses should not be cached + // Container registry responses should not be cached const cacheControl = response.headers.get('Cache-Control'); if (cacheControl) { expect(cacheControl).not.toContain('max-age=1800'); @@ -242,7 +205,6 @@ describe('Docker Registry Support', () => { describe('Container Registry Platform Support', () => { const containerRegistries = [ - { name: 'Docker Hub', prefix: 'cr/docker', expectedStatus: [200, 301, 302, 401, 404] }, { name: 'Quay.io', prefix: 'cr/quay', expectedStatus: [200, 301, 302, 401, 404] }, { name: 'Google Container Registry', diff --git a/test/platforms.test.js b/test/platforms.test.js index 0a48d6e..1e04b72 100644 --- a/test/platforms.test.js +++ b/test/platforms.test.js @@ -84,10 +84,6 @@ describe('Platform Configuration', () => { }); it('should transform container registry paths correctly', () => { - expect(transformPath('/cr/docker/v2/library/nginx/manifests/latest', 'cr-docker')).toBe( - '/v2/library/nginx/manifests/latest' - ); - expect(transformPath('/cr/ghcr/v2/microsoft/vscode/manifests/latest', 'cr-ghcr')).toBe( '/v2/microsoft/vscode/manifests/latest' ); @@ -129,7 +125,6 @@ describe('Platform Configuration', () => { }); it('should have correct container registry base URLs', () => { - expect(PLATFORMS['cr-docker']).toBe('https://registry-1.docker.io'); expect(PLATFORMS['cr-ghcr']).toBe('https://ghcr.io'); expect(PLATFORMS['cr-gcr']).toBe('https://gcr.io'); expect(PLATFORMS['cr-mcr']).toBe('https://mcr.microsoft.com'); @@ -181,19 +176,18 @@ describe('Platform Configuration', () => { }); it('should handle container registry URL construction', () => { - const testPath = '/cr/docker/v2/library/nginx/manifests/latest'; - const transformedPath = transformPath(testPath, 'cr-docker'); - const fullUrl = PLATFORMS['cr-docker'] + transformedPath; + const testPath = '/cr/ghcr/v2/microsoft/vscode/manifests/latest'; + const transformedPath = transformPath(testPath, 'cr-ghcr'); + const fullUrl = PLATFORMS['cr-ghcr'] + transformedPath; expect(() => new URL(fullUrl)).not.toThrow(); - expect(fullUrl).toBe('https://registry-1.docker.io/v2/library/nginx/manifests/latest'); + expect(fullUrl).toBe('https://ghcr.io/v2/microsoft/vscode/manifests/latest'); }); }); describe('Container Registry Support', () => { it('should have all major container registries defined', () => { const containerRegistries = [ - 'cr-docker', 'cr-quay', 'cr-gcr', 'cr-mcr', @@ -222,7 +216,6 @@ describe('Platform Configuration', () => { it('should transform all container registry paths correctly', () => { const containerRegistries = [ - 'cr-docker', 'cr-quay', 'cr-gcr', 'cr-mcr',