diff --git a/src/index.js b/src/index.js index 007d73e..f47f7e6 100644 --- a/src/index.js +++ b/src/index.js @@ -17,17 +17,10 @@ import { parseAuthenticate, responseUnauthorized } from './protocols/docker.js'; -import { - configureGitHeaders, - isGitLFSRequest, - isGitRequest -} from './protocols/git.js'; +import { configureGitHeaders, isGitLFSRequest, isGitRequest } from './protocols/git.js'; import { PerformanceMonitor, addPerformanceHeaders } from './utils/performance.js'; import { addSecurityHeaders, createErrorResponse } from './utils/security.js'; -import { - isDockerRequest, - validateRequest -} from './utils/validation.js'; +import { isDockerRequest, validateRequest } from './utils/validation.js'; /** * Main request handler with comprehensive caching, retry logic, and security measures. @@ -80,10 +73,7 @@ async function handleRequest(request, env, ctx) { !url.pathname.startsWith('/v2/cr/') && url.pathname !== '/v2/auth' ) { - response = createErrorResponse( - 'container registry requests must use /cr/ prefix', - 400 - ); + response = createErrorResponse('container registry requests must use /cr/ prefix', 400); } else { // Remove /v2 from the path for container registry API consistency if present effectivePath = url.pathname.replace(/^\/v2/, ''); @@ -140,7 +130,10 @@ async function handleRequest(request, env, ctx) { /** @type {Cache | null} */ /** @type {Cache | null} */ // @ts-ignore - Cloudflare Workers cache API - const cache = typeof caches !== 'undefined' && /** @type {any} */ (caches).default ? /** @type {any} */ (caches).default : null; + const cache = + typeof caches !== 'undefined' && /** @type {any} */ (caches).default + ? /** @type {any} */ (caches).default + : null; if (cache && !isGit && !isGitLFS && !isDocker && !isAI) { try { @@ -301,8 +294,7 @@ async function handleRequest(request, env, ctx) { contentLength = rangeResponse.headers.get('Content-Length'); if (!contentLength) { const sizeLimit = 50 * 1024 * 1024; - const contentLengthHint = - rangeResponse.headers.get('Content-Length'); + const contentLengthHint = rangeResponse.headers.get('Content-Length'); if ( !contentLengthHint || parseInt(contentLengthHint, 10) < sizeLimit @@ -503,20 +495,20 @@ async function handleRequest(request, env, ctx) { (await response.clone().text()).includes('UNAUTHORIZED'); if (!isCustomError) { - const errorText = await response.text().catch(() => ''); - response = createErrorResponse( + const errorText = await response.text().catch(() => ''); + response = createErrorResponse( `Authentication required for this container registry resource. This may be a private repository. Original error: ${errorText}`, 401, true ); } } else { - const errorText = await response.text().catch(() => 'Unknown error'); - response = createErrorResponse( - `Upstream server error (${response.status}): ${errorText}`, - response.status, - true - ); + const errorText = await response.text().catch(() => 'Unknown error'); + response = createErrorResponse( + `Upstream server error (${response.status}): ${errorText}`, + response.status, + true + ); } } else { // Success case processing (rewriting URLs etc) @@ -584,50 +576,52 @@ async function handleRequest(request, env, ctx) { // Cache success logic if ( - cache && - !isGit && - !isGitLFS && - !isDocker && - !isAI && - request.method === 'GET' && - response.ok && - response.status === 200 - ) { - const rangeHeader = request.headers.get('Range'); - const cacheKey = rangeHeader - ? new Request(targetUrl, { - method: 'GET', - headers: new Headers( - [...request.headers.entries()].filter(([k]) => k.toLowerCase() !== 'range') + cache && + !isGit && + !isGitLFS && + !isDocker && + !isAI && + request.method === 'GET' && + response.ok && + response.status === 200 + ) { + const rangeHeader = request.headers.get('Range'); + const cacheKey = rangeHeader + ? new Request(targetUrl, { + method: 'GET', + headers: new Headers( + [...request.headers.entries()].filter( + ([k]) => k.toLowerCase() !== 'range' ) - }) - : new Request(targetUrl, { method: 'GET' }); + ) + }) + : new Request(targetUrl, { method: 'GET' }); - try { - if (ctx && typeof ctx.waitUntil === 'function') { - ctx.waitUntil(cache.put(cacheKey, response.clone())); - } else { - cache.put(cacheKey, response.clone()).catch(error => { - console.warn('Cache put failed:', error); - }); - } - - if (rangeHeader && response.status === 200) { - const rangedResponse = await cache.match( - new Request(targetUrl, { - method: 'GET', - headers: request.headers - }) - ); - if (rangedResponse) { - monitor.mark('range_cache_hit_after_full_cache'); - response = rangedResponse; - } - } - } catch (cacheError) { - console.warn('Cache put/match failed:', cacheError); + try { + if (ctx && typeof ctx.waitUntil === 'function') { + ctx.waitUntil(cache.put(cacheKey, response.clone())); + } else { + cache.put(cacheKey, response.clone()).catch(error => { + console.warn('Cache put failed:', error); + }); } + + if (rangeHeader && response.status === 200) { + const rangedResponse = await cache.match( + new Request(targetUrl, { + method: 'GET', + headers: request.headers + }) + ); + if (rangedResponse) { + monitor.mark('range_cache_hit_after_full_cache'); + response = rangedResponse; + } + } + } catch (cacheError) { + console.warn('Cache put/match failed:', cacheError); } + } } } } diff --git a/test/features/auth.test.js b/test/features/auth.test.js new file mode 100644 index 0000000..eab0961 --- /dev/null +++ b/test/features/auth.test.js @@ -0,0 +1,70 @@ +import { SELF } from 'cloudflare:test'; +import { describe, expect, it } from 'vitest'; + +describe('Authentication Header Forwarding', () => { + it('should forward Authorization header for Hugging Face requests', async () => { + // Test with a Hugging Face dataset file that would require authentication + const testUrl = 'https://example.com/hf/datasets/test/private-dataset/resolve/main/data.csv'; + const authToken = 'Bearer hf_test_token_12345'; + + const response = await SELF.fetch(testUrl, { + method: 'HEAD', + headers: { + Authorization: authToken + } + }); + + // Should accept the request (not 400 bad request) + expect(response.status).not.toBe(400); + // Should attempt to proxy to HF with auth (status depends on actual HF response) + expect([200, 401, 403, 404]).toContain(response.status); + }); + + it('should forward Authorization header for GitHub API requests', async () => { + const testUrl = 'https://example.com/gh/test/private-repo/README.md'; + const authToken = 'Bearer ghp_test_token_12345'; + + const response = await SELF.fetch(testUrl, { + method: 'HEAD', + headers: { + Authorization: authToken + } + }); + + // Should accept the request and forward the auth header + expect(response.status).not.toBe(400); + expect([200, 401, 403, 404]).toContain(response.status); + }); + + it('should forward Authorization header for PyPI authenticated requests', async () => { + const testUrl = 'https://example.com/pypi/simple/private-package/'; + const authToken = 'Basic dGVzdDp0ZXN0MTIzNDU='; + + const response = await SELF.fetch(testUrl, { + method: 'HEAD', + headers: { + Authorization: authToken + } + }); + + // Should accept the request + expect(response.status).not.toBe(400); + expect([200, 401, 403, 404]).toContain(response.status); + }); + + it('should work with gated Hugging Face models', async () => { + // Simulate a request to a gated model that requires authentication + const testUrl = 'https://example.com/hf/meta-llama/Llama-2-7b/resolve/main/config.json'; + const authToken = 'Bearer hf_authenticated_token'; + + const response = await SELF.fetch(testUrl, { + headers: { + Authorization: authToken + } + }); + + // Should attempt to proxy with authentication + // The actual status depends on whether the token is valid and the model exists + expect(response.status).not.toBe(400); + }); +}); diff --git a/test/features/git-lfs.test.js b/test/features/git-lfs.test.js new file mode 100644 index 0000000..b16f066 --- /dev/null +++ b/test/features/git-lfs.test.js @@ -0,0 +1,95 @@ +import { SELF } from 'cloudflare:test'; +import { describe, expect, it } from 'vitest'; + +describe('Git LFS Protocol Integration', () => { + it('should handle LFS info/lfs requests', async () => { + const testUrl = 'https://example.com/gh/microsoft/vscode.git/info/lfs'; + const response = await SELF.fetch(testUrl, { + headers: { + 'User-Agent': 'git-lfs/3.0.0 (GitHub; darwin amd64; go 1.17.2)' + } + }); + + expect([200, 301, 302, 404]).toContain(response.status); + }); + + it('should handle LFS batch API requests', async () => { + const testUrl = 'https://example.com/gh/microsoft/vscode.git/objects/batch'; + const response = await SELF.fetch(testUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/vnd.git-lfs+json', + Accept: 'application/vnd.git-lfs+json', + 'User-Agent': 'git-lfs/3.0.0' + }, + body: JSON.stringify({ + operation: 'download', + objects: [ + { + oid: 'a1b2c3d4e5f6789012345678901234567890123456789012345678901234abcd', + size: 1024 + } + ] + }) + }); + + expect([200, 301, 302, 400, 403, 404]).toContain(response.status); + }); + + it('should handle LFS object download requests', async () => { + const testUrl = + 'https://example.com/gh/microsoft/vscode.git/objects/a1b2c3d4e5f6789012345678901234567890123456789012345678901234abcd'; + const response = await SELF.fetch(testUrl, { + headers: { + 'User-Agent': 'git-lfs/3.0.0', + Accept: 'application/octet-stream' + } + }); + + expect([200, 301, 302, 403, 404]).toContain(response.status); + }); + + it('should preserve LFS-specific headers', async () => { + const testUrl = 'https://example.com/gh/test/repo.git/objects/batch'; + const response = await SELF.fetch(testUrl, { + method: 'POST', + headers: { + 'User-Agent': 'git-lfs/3.0.0', + Accept: 'application/vnd.git-lfs+json', + 'Content-Type': 'application/vnd.git-lfs+json' + }, + body: '{}' + }); + + // Should not reject LFS-specific headers + expect(response.status).not.toBe(400); + }); + + it('should skip caching for LFS requests', async () => { + const testUrl = 'https://example.com/gh/test/repo.git/info/lfs'; + + // First request + const response1 = await SELF.fetch(testUrl, { + headers: { + 'User-Agent': 'git-lfs/3.0.0' + } + }); + + // Second request - should not be cached + const response2 = await SELF.fetch(testUrl, { + headers: { + 'User-Agent': 'git-lfs/3.0.0' + } + }); + + // Both requests should go to origin (no cache hit) + const metrics1 = response1.headers.get('X-Performance-Metrics'); + const metrics2 = response2.headers.get('X-Performance-Metrics'); + + // Verify that neither indicates a cache hit + if (metrics1 && metrics2) { + expect(metrics1).not.toContain('cache_hit'); + expect(metrics2).not.toContain('cache_hit'); + } + }); +}); diff --git a/test/features/git.test.js b/test/features/git.test.js new file mode 100644 index 0000000..5300812 --- /dev/null +++ b/test/features/git.test.js @@ -0,0 +1,42 @@ +import { SELF } from 'cloudflare:test'; +import { describe, expect, it } from 'vitest'; + +describe('Git Protocol Integration', () => { + it('should handle Git info/refs requests', async () => { + const testUrl = 'https://example.com/gh/microsoft/vscode.git/info/refs?service=git-upload-pack'; + const response = await SELF.fetch(testUrl, { + headers: { + 'User-Agent': 'git/2.34.1' + } + }); + + expect([200, 301, 302, 404]).toContain(response.status); + }); + + it('should handle Git upload-pack requests', async () => { + const testUrl = 'https://example.com/gh/microsoft/vscode.git/git-upload-pack'; + const response = await SELF.fetch(testUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-git-upload-pack-request', + 'User-Agent': 'git/2.34.1' + }, + body: '0000' // Minimal Git protocol data + }); + + expect([200, 301, 302, 400, 404]).toContain(response.status); + }); + + it('should preserve Git-specific headers', async () => { + const testUrl = 'https://example.com/gh/test/repo.git/info/refs'; + const response = await SELF.fetch(testUrl, { + headers: { + 'User-Agent': 'git/2.34.1', + 'Git-Protocol': 'version=2' + } + }); + + // Should not reject Git-specific headers + expect(response.status).not.toBe(400); + }); +}); diff --git a/test/features/range-cache.test.js b/test/features/range-cache.test.js index 54e503a..1e9076f 100644 --- a/test/features/range-cache.test.js +++ b/test/features/range-cache.test.js @@ -1,4 +1,5 @@ -import { beforeAll, describe, expect, it } from 'vitest'; +import { SELF } from 'cloudflare:test'; +import { describe, expect, it } from 'vitest'; /** * Tests for Range Request Caching Strategy @@ -11,17 +12,6 @@ import { beforeAll, describe, expect, it } from 'vitest'; */ describe('Range Request Caching Strategy', () => { - /** @type {any} */ - let SELF; - - beforeAll(async () => { - const { unstable_dev: unstableDev } = await import('wrangler'); - const worker = await unstableDev('src/index.js', { - experimental: { disableExperimentalWarning: true } - }); - SELF = worker; - }); - describe('Cache Behavior for Range Requests', () => { it('should not attempt to cache 206 responses', async () => { const testUrl = 'https://example.com/gh/test/repo/sample.pdf'; @@ -44,7 +34,7 @@ describe('Range Request Caching Strategy', () => { // Should not contain any cache put errors const errorKeys = Object.keys(parsedMetrics).filter( - key => key.includes('error') || key.includes('fail') + key => (key.includes('error') || key.includes('fail')) && key !== 'client_error' ); expect(errorKeys).toHaveLength(0); } diff --git a/test/features/security.test.js b/test/features/security.test.js index 21540a0..2f4b1d6 100644 --- a/test/features/security.test.js +++ b/test/features/security.test.js @@ -183,7 +183,7 @@ describe('Security Features', () => { // If it doesn't throw, it should not be a server error } catch (error) { // Expected to throw TypeError for invalid header value - expect((/** @type {Error} */ (error)).message).toMatch(/[Ii]nvalid|[Hh]eader/); + expect(/** @type {Error} */ (error).message).toMatch(/[Ii]nvalid|[Hh]eader/); } }); }); @@ -200,7 +200,7 @@ describe('Security Features', () => { responses.forEach((/** @type {Response} */ response) => { expect(response.status).not.toBe(500); }); - }); + }, 30000); it('should timeout long-running requests', async () => { // This test would need to be implemented based on actual timeout behavior diff --git a/test/integration.test.js b/test/integration.test.js index 419731e..863bb50 100644 --- a/test/integration.test.js +++ b/test/integration.test.js @@ -66,140 +66,6 @@ describe('Integration Tests', () => { }); }); - describe('Git Protocol Integration', () => { - it('should handle Git info/refs requests', async () => { - const testUrl = - 'https://example.com/gh/microsoft/vscode.git/info/refs?service=git-upload-pack'; - const response = await SELF.fetch(testUrl, { - headers: { - 'User-Agent': 'git/2.34.1' - } - }); - - expect([200, 301, 302, 404]).toContain(response.status); - }); - - it('should handle Git upload-pack requests', async () => { - const testUrl = 'https://example.com/gh/microsoft/vscode.git/git-upload-pack'; - const response = await SELF.fetch(testUrl, { - method: 'POST', - headers: { - 'Content-Type': 'application/x-git-upload-pack-request', - 'User-Agent': 'git/2.34.1' - }, - body: '0000' // Minimal Git protocol data - }); - - expect([200, 301, 302, 400, 404]).toContain(response.status); - }); - - it('should preserve Git-specific headers', async () => { - const testUrl = 'https://example.com/gh/test/repo.git/info/refs'; - const response = await SELF.fetch(testUrl, { - headers: { - 'User-Agent': 'git/2.34.1', - 'Git-Protocol': 'version=2' - } - }); - - // Should not reject Git-specific headers - expect(response.status).not.toBe(400); - }); - }); - - describe('Git LFS Protocol Integration', () => { - it('should handle LFS info/lfs requests', async () => { - const testUrl = 'https://example.com/gh/microsoft/vscode.git/info/lfs'; - const response = await SELF.fetch(testUrl, { - headers: { - 'User-Agent': 'git-lfs/3.0.0 (GitHub; darwin amd64; go 1.17.2)' - } - }); - - expect([200, 301, 302, 404]).toContain(response.status); - }); - - it('should handle LFS batch API requests', async () => { - const testUrl = 'https://example.com/gh/microsoft/vscode.git/objects/batch'; - const response = await SELF.fetch(testUrl, { - method: 'POST', - headers: { - 'Content-Type': 'application/vnd.git-lfs+json', - Accept: 'application/vnd.git-lfs+json', - 'User-Agent': 'git-lfs/3.0.0' - }, - body: JSON.stringify({ - operation: 'download', - objects: [ - { - oid: 'a1b2c3d4e5f6789012345678901234567890123456789012345678901234abcd', - size: 1024 - } - ] - }) - }); - - expect([200, 301, 302, 400, 403, 404]).toContain(response.status); - }); - - it('should handle LFS object download requests', async () => { - const testUrl = - 'https://example.com/gh/microsoft/vscode.git/objects/a1b2c3d4e5f6789012345678901234567890123456789012345678901234abcd'; - const response = await SELF.fetch(testUrl, { - headers: { - 'User-Agent': 'git-lfs/3.0.0', - Accept: 'application/octet-stream' - } - }); - - expect([200, 301, 302, 403, 404]).toContain(response.status); - }); - - it('should preserve LFS-specific headers', async () => { - const testUrl = 'https://example.com/gh/test/repo.git/objects/batch'; - const response = await SELF.fetch(testUrl, { - method: 'POST', - headers: { - 'User-Agent': 'git-lfs/3.0.0', - Accept: 'application/vnd.git-lfs+json', - 'Content-Type': 'application/vnd.git-lfs+json' - }, - body: '{}' - }); - - // Should not reject LFS-specific headers - expect(response.status).not.toBe(400); - }); - - it('should skip caching for LFS requests', async () => { - const testUrl = 'https://example.com/gh/test/repo.git/info/lfs'; - - // First request - const response1 = await SELF.fetch(testUrl, { - headers: { - 'User-Agent': 'git-lfs/3.0.0' - } - }); - - // Second request - should not be cached - const response2 = await SELF.fetch(testUrl, { - headers: { - 'User-Agent': 'git-lfs/3.0.0' - } - }); - - // Both requests should go to origin (no cache hit) - const metrics1 = response1.headers.get('X-Performance-Metrics'); - const metrics2 = response2.headers.get('X-Performance-Metrics'); - - // Verify that neither indicates a cache hit - if (metrics1 && metrics2) { - expect(metrics1).not.toContain('cache_hit'); - expect(metrics2).not.toContain('cache_hit'); - } - }); - }); - describe('Caching Integration', () => { it('should cache responses appropriately', async () => { const testUrl = 'https://example.com/gh/test/repo/static-file.txt'; @@ -439,72 +305,4 @@ describe('Integration Tests', () => { }); }); }); - - describe('Authentication Header Forwarding', () => { - it('should forward Authorization header for Hugging Face requests', async () => { - // Test with a Hugging Face dataset file that would require authentication - const testUrl = 'https://example.com/hf/datasets/test/private-dataset/resolve/main/data.csv'; - const authToken = 'Bearer hf_test_token_12345'; - - const response = await SELF.fetch(testUrl, { - method: 'HEAD', - headers: { - Authorization: authToken - } - }); - - // Should accept the request (not 400 bad request) - expect(response.status).not.toBe(400); - // Should attempt to proxy to HF with auth (status depends on actual HF response) - expect([200, 401, 403, 404]).toContain(response.status); - }); - - it('should forward Authorization header for GitHub API requests', async () => { - const testUrl = 'https://example.com/gh/test/private-repo/README.md'; - const authToken = 'Bearer ghp_test_token_12345'; - - const response = await SELF.fetch(testUrl, { - method: 'HEAD', - headers: { - Authorization: authToken - } - }); - - // Should accept the request and forward the auth header - expect(response.status).not.toBe(400); - expect([200, 401, 403, 404]).toContain(response.status); - }); - - it('should forward Authorization header for PyPI authenticated requests', async () => { - const testUrl = 'https://example.com/pypi/simple/private-package/'; - const authToken = 'Basic dGVzdDp0ZXN0MTIzNDU='; - - const response = await SELF.fetch(testUrl, { - method: 'HEAD', - headers: { - Authorization: authToken - } - }); - - // Should accept the request - expect(response.status).not.toBe(400); - expect([200, 401, 403, 404]).toContain(response.status); - }); - - it('should work with gated Hugging Face models', async () => { - // Simulate a request to a gated model that requires authentication - const testUrl = 'https://example.com/hf/meta-llama/Llama-2-7b/resolve/main/config.json'; - const authToken = 'Bearer hf_authenticated_token'; - - const response = await SELF.fetch(testUrl, { - headers: { - Authorization: authToken - } - }); - - // Should attempt to proxy with authentication - // The actual status depends on whether the token is valid and the model exists - expect(response.status).not.toBe(400); - }); - }); }); diff --git a/test/platforms/container-registry.test.js b/test/platforms/container-registry.test.js index 8c35b8f..1b9cb0a 100644 --- a/test/platforms/container-registry.test.js +++ b/test/platforms/container-registry.test.js @@ -254,7 +254,11 @@ describe('Container Registry Support', () => { prefix: 'cr/ghcr', expectedStatus: [200, 301, 302, 401, 404, 429] }, - { name: 'Amazon ECR Public', prefix: 'cr/ecr', expectedStatus: [200, 301, 302, 401, 404, 429] } + { + name: 'Amazon ECR Public', + prefix: 'cr/ecr', + expectedStatus: [200, 301, 302, 401, 404, 429] + } ]; containerRegistries.forEach(({ name, prefix, expectedStatus }) => {