feat: update dependencies and improve request handling
This commit is contained in:
1 parent
3957f78d9d
commit
929337c494
16 files changed
+185
-563
No files matched your search
@@ -104,7 +104,7 @@ describe('Security Features', () => {
|
||||
expect(response.status).not.toBe(500);
|
||||
}
|
||||
}
|
||||
}, 45000);
|
||||
}, 30000);
|
||||
|
||||
it('should reject extremely long paths', async () => {
|
||||
const longPath = `/gh/${'a'.repeat(3000)}`;
|
||||
|
||||
@@ -92,13 +92,6 @@ describe('Xget Core Functionality', () => {
|
||||
// Should attempt to proxy to conda
|
||||
expect(response.status).not.toBe(400);
|
||||
});
|
||||
|
||||
it('should not treat nested /v2/ path segments as container registry requests', async () => {
|
||||
const testUrl = 'https://example.com/gh/microsoft/vscode/releases/download/v2/file.tar.gz';
|
||||
const response = await SELF.fetch(testUrl, { method: 'HEAD' });
|
||||
|
||||
expect(response.status).not.toBe(400);
|
||||
});
|
||||
});
|
||||
|
||||
describe('HTTP Method Validation', () => {
|
||||
@@ -133,18 +126,6 @@ describe('Xget Core Functionality', () => {
|
||||
|
||||
expect(response.status).toBe(405);
|
||||
});
|
||||
|
||||
it('should reject AI-like POST requests outside /ip providers', async () => {
|
||||
const response = await SELF.fetch('https://example.com/gh/test/repo/v1/chat/completions', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ message: 'test' })
|
||||
});
|
||||
|
||||
expect(response.status).toBe(405);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Git Protocol Support', () => {
|
||||
|
||||
@@ -26,7 +26,7 @@ describe('Integration Tests', () => {
|
||||
const testUrl = 'https://example.com/gh/microsoft/vscode/archive/refs/heads/main.zip';
|
||||
const response = await SELF.fetch(testUrl, { method: 'HEAD' });
|
||||
|
||||
expect([200, 301, 302, 404, 408]).toContain(response.status);
|
||||
expect([200, 301, 302, 404]).toContain(response.status);
|
||||
}, 60000);
|
||||
|
||||
it('should proxy GitLab file requests correctly', async () => {
|
||||
@@ -152,16 +152,18 @@ describe('Integration Tests', () => {
|
||||
it('should include performance metrics in all responses', async () => {
|
||||
const testUrls = [
|
||||
'https://example.com/gh/test/repo/file.txt',
|
||||
'https://example.com/gl/test/repo/file.txt',
|
||||
'https://example.com/hf/test/model/config.json',
|
||||
'https://example.com/npm/test-package',
|
||||
'https://example.com/pypi/simple/test/'
|
||||
'https://example.com/pypi/simple/test/',
|
||||
'https://example.com/conda/pkgs/main/test.json'
|
||||
];
|
||||
|
||||
const responses = await Promise.all(testUrls.map(url => SELF.fetch(url, { method: 'HEAD' })));
|
||||
|
||||
for (const response of responses) {
|
||||
for (const url of testUrls) {
|
||||
const response = await SELF.fetch(url, { method: 'HEAD' });
|
||||
expect(response.headers.get('X-Performance-Metrics')).toBeTruthy();
|
||||
}
|
||||
}, 20000);
|
||||
}, 10000);
|
||||
});
|
||||
|
||||
describe('Content Type Handling', () => {
|
||||
|
||||
@@ -235,6 +235,42 @@ describe('Container Registry Support', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('Container Registry Platform Support', () => {
|
||||
const containerRegistries = [
|
||||
{ name: 'Docker Hub', prefix: 'cr/docker', expectedStatus: [200, 301, 302, 401, 404, 429] },
|
||||
{ name: 'Quay.io', prefix: 'cr/quay', expectedStatus: [200, 301, 302, 401, 404, 429] },
|
||||
{
|
||||
name: 'Google Container Registry',
|
||||
prefix: 'cr/gcr',
|
||||
expectedStatus: [200, 301, 302, 401, 404, 429]
|
||||
},
|
||||
{
|
||||
name: 'Microsoft Container Registry',
|
||||
prefix: 'cr/mcr',
|
||||
expectedStatus: [200, 301, 302, 401, 404, 429]
|
||||
},
|
||||
{
|
||||
name: 'GitHub Container Registry',
|
||||
prefix: 'cr/ghcr',
|
||||
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 }) => {
|
||||
it(`should support ${name} registry`, async () => {
|
||||
const testUrl = `https://example.com/${prefix}/v2/test/image/manifests/latest`;
|
||||
const response = await SELF.fetch(testUrl, { method: 'HEAD' });
|
||||
|
||||
expect(expectedStatus).toContain(response.status);
|
||||
}, 10000);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Docker Hub Specific Tests', () => {
|
||||
it('should handle Docker Hub official images (single-name images)', async () => {
|
||||
// Official images like nginx, redis are stored as library/nginx in Docker Hub
|
||||
|
||||
@@ -1,138 +0,0 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import worker from '../../src/index.js';
|
||||
|
||||
/** @type {ExecutionContext} */
|
||||
const executionContext = {
|
||||
waitUntil() {},
|
||||
passThroughOnException() {}
|
||||
};
|
||||
|
||||
describe('CORS and Proxy Request Options', () => {
|
||||
beforeEach(() => {
|
||||
vi.stubGlobal('caches', {
|
||||
default: {
|
||||
match: vi.fn(async () => null),
|
||||
put: vi.fn(async () => undefined)
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('does not send a synthetic Origin header upstream', async () => {
|
||||
const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValue(
|
||||
new Response('ok', {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'text/plain' }
|
||||
})
|
||||
);
|
||||
|
||||
const response = await worker.fetch(
|
||||
new Request('https://example.com/gh/test/repo/index.html'),
|
||||
{},
|
||||
executionContext
|
||||
);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
const upstreamHeaders = new Headers(fetchSpy.mock.calls[0][1]?.headers);
|
||||
expect(upstreamHeaders.has('Origin')).toBe(false);
|
||||
});
|
||||
|
||||
it('does not enable Cloudflare minification for proxied responses', async () => {
|
||||
const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValue(
|
||||
new Response('<html>ok</html>', {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'text/html' }
|
||||
})
|
||||
);
|
||||
|
||||
await worker.fetch(
|
||||
new Request('https://example.com/gh/test/repo/index.html'),
|
||||
{},
|
||||
executionContext
|
||||
);
|
||||
|
||||
const fetchOptions = /** @type {RequestInit & { cf?: Record<string, unknown> }} */ (
|
||||
fetchSpy.mock.calls[0][1] || {}
|
||||
);
|
||||
|
||||
expect(fetchOptions.cf).toEqual(
|
||||
expect.objectContaining({
|
||||
http3: true,
|
||||
cacheEverything: true,
|
||||
preconnect: true
|
||||
})
|
||||
);
|
||||
expect(fetchOptions.cf).not.toHaveProperty('minify');
|
||||
});
|
||||
|
||||
it('responds to preflight requests for allowed origins', async () => {
|
||||
const response = await worker.fetch(
|
||||
new Request('https://example.com/gh/test/repo', {
|
||||
method: 'OPTIONS',
|
||||
headers: {
|
||||
Origin: 'https://app.example.com',
|
||||
'Access-Control-Request-Method': 'GET',
|
||||
'Access-Control-Request-Headers': 'X-Custom-Header'
|
||||
}
|
||||
}),
|
||||
{
|
||||
ALLOWED_ORIGINS: 'https://app.example.com'
|
||||
},
|
||||
executionContext
|
||||
);
|
||||
|
||||
expect(response.status).toBe(204);
|
||||
expect(response.headers.get('Access-Control-Allow-Origin')).toBe('https://app.example.com');
|
||||
expect(response.headers.get('Access-Control-Allow-Methods')).toContain('GET');
|
||||
expect(response.headers.get('Access-Control-Allow-Headers')).toBe('X-Custom-Header');
|
||||
});
|
||||
|
||||
it('rejects preflight requests for disallowed origins', async () => {
|
||||
const response = await worker.fetch(
|
||||
new Request('https://example.com/gh/test/repo', {
|
||||
method: 'OPTIONS',
|
||||
headers: {
|
||||
Origin: 'https://evil.example.com',
|
||||
'Access-Control-Request-Method': 'GET'
|
||||
}
|
||||
}),
|
||||
{
|
||||
ALLOWED_ORIGINS: 'https://app.example.com'
|
||||
},
|
||||
executionContext
|
||||
);
|
||||
|
||||
expect(response.status).toBe(403);
|
||||
expect(response.headers.get('Access-Control-Allow-Origin')).toBeNull();
|
||||
});
|
||||
|
||||
it('adds CORS headers to normal responses for allowed origins', async () => {
|
||||
vi.spyOn(globalThis, 'fetch').mockResolvedValue(
|
||||
new Response('ok', {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'text/plain' }
|
||||
})
|
||||
);
|
||||
|
||||
const response = await worker.fetch(
|
||||
new Request('https://example.com/gh/test/repo/file.txt', {
|
||||
headers: {
|
||||
Origin: 'https://app.example.com'
|
||||
}
|
||||
}),
|
||||
{
|
||||
ALLOWED_ORIGINS: 'https://app.example.com'
|
||||
},
|
||||
executionContext
|
||||
);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.headers.get('Access-Control-Allow-Origin')).toBe('https://app.example.com');
|
||||
expect(response.headers.get('Vary')).toContain('Origin');
|
||||
});
|
||||
});
|
||||
@@ -1,15 +0,0 @@
|
||||
import { createRequire } from 'node:module';
|
||||
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
describe('Package manifest', () => {
|
||||
it('does not depend on itself', () => {
|
||||
const require = createRequire(import.meta.url);
|
||||
const packageJson = require('../../package.json');
|
||||
const { dependencies } = packageJson;
|
||||
const typedDependencies = /** @type {Record<string, string> | undefined} */ (dependencies);
|
||||
|
||||
expect(packageJson.name).toBe('xget');
|
||||
expect(typedDependencies?.xget).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -330,10 +330,6 @@ describe('Platform Configuration', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should use the correct Amazon ECR Public base URL', () => {
|
||||
expect(PLATFORMS['cr-ecr']).toBe('https://public.ecr.aws');
|
||||
});
|
||||
|
||||
it('should transform all container registry paths correctly', () => {
|
||||
const containerRegistries = [
|
||||
'cr-quay',
|
||||
|
||||
@@ -1,135 +0,0 @@
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
import worker from '../../src/index.js';
|
||||
import { CONFIG } from '../../src/config/index.js';
|
||||
import { isAIInferenceRequest } from '../../src/protocols/ai.js';
|
||||
import { handleDockerAuth } from '../../src/protocols/docker.js';
|
||||
import { isDockerRequest } from '../../src/utils/validation.js';
|
||||
|
||||
/** @type {ExecutionContext} */
|
||||
const executionContext = {
|
||||
waitUntil() {},
|
||||
passThroughOnException() {}
|
||||
};
|
||||
|
||||
describe('Protocol Detection', () => {
|
||||
it('only treats /ip-prefixed paths as AI inference requests', () => {
|
||||
const request = new Request('https://example.com/gh/user/repo/v1/chat/completions', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: '{}'
|
||||
});
|
||||
const url = new URL(request.url);
|
||||
|
||||
expect(isAIInferenceRequest(request, url)).toBe(false);
|
||||
});
|
||||
|
||||
it('does not treat nested /v2/ segments in regular paths as Docker requests', () => {
|
||||
const request = new Request(
|
||||
'https://example.com/gh/user/repo/releases/download/v2/file.tar.gz'
|
||||
);
|
||||
const url = new URL(request.url);
|
||||
|
||||
expect(isDockerRequest(request, url)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Docker Authentication', () => {
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('normalizes Docker Hub official image scopes during auth proxying', async () => {
|
||||
const fetchSpy = vi.spyOn(globalThis, 'fetch').mockImplementation(async input => {
|
||||
const url = String(input);
|
||||
|
||||
if (url === 'https://registry-1.docker.io/v2/') {
|
||||
return new Response('', {
|
||||
status: 401,
|
||||
headers: {
|
||||
'WWW-Authenticate':
|
||||
'Bearer realm="https://auth.docker.io/token",service="registry.docker.io"'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return new Response(JSON.stringify({ token: 'token' }), {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
});
|
||||
|
||||
const request = new Request(
|
||||
'https://example.com/cr/docker/v2/auth?scope=repository:cr/docker/nginx:pull&service=Xget'
|
||||
);
|
||||
const response = await handleDockerAuth(request, new URL(request.url), CONFIG);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(String(fetchSpy.mock.calls[1][0])).toContain(
|
||||
'scope=repository%3Alibrary%2Fnginx%3Apull'
|
||||
);
|
||||
});
|
||||
|
||||
it('routes platform-prefixed auth endpoints without duplicating /v2', async () => {
|
||||
/** @type {string[]} */
|
||||
const upstreamCalls = [];
|
||||
vi.spyOn(globalThis, 'fetch').mockImplementation(async input => {
|
||||
upstreamCalls.push(String(input));
|
||||
|
||||
if (String(input) === 'https://ghcr.io/v2/') {
|
||||
return new Response('', {
|
||||
status: 401,
|
||||
headers: {
|
||||
'WWW-Authenticate': 'Bearer realm="https://ghcr.io/token",service="ghcr.io"'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return new Response(JSON.stringify({ token: 'token' }), {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
});
|
||||
|
||||
const request = new Request('https://example.com/cr/ghcr/v2/auth?service=Xget');
|
||||
const response = await worker.fetch(request, {}, executionContext);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(upstreamCalls[0]).toBe('https://ghcr.io/v2/');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Protocol Header Configuration', () => {
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('does not send Git user-agent for AI inference requests', async () => {
|
||||
/** @type {{ url: string, userAgent: string | null }[]} */
|
||||
const observed = [];
|
||||
vi.spyOn(globalThis, 'fetch').mockImplementation(async (input, init) => {
|
||||
const headers = new Headers(init?.headers);
|
||||
observed.push({
|
||||
url: String(input),
|
||||
userAgent: headers.get('User-Agent')
|
||||
});
|
||||
|
||||
return new Response('{}', {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
});
|
||||
|
||||
const request = new Request('https://example.com/ip/openai/v1/chat/completions', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: '{}'
|
||||
});
|
||||
const response = await worker.fetch(request, {}, executionContext);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(observed[0]).toEqual({
|
||||
url: 'https://api.openai.com/v1/chat/completions',
|
||||
userAgent: 'Xget-AI-Proxy/1.0'
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user