371 lines
12 KiB
JavaScript
371 lines
12 KiB
JavaScript
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 {
|
|
getScopeFromUrl,
|
|
handleDockerAuth,
|
|
readRegistryTokenResponse
|
|
} 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('derives scoped pull access from /cr-prefixed registry requests', () => {
|
|
const url = new URL('https://example.com/cr/docker/v2/nginx/manifests/latest');
|
|
|
|
expect(getScopeFromUrl(url, url.pathname, 'cr-docker')).toBe('repository:library/nginx:pull');
|
|
});
|
|
|
|
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/');
|
|
});
|
|
|
|
it('routes registry manifests without duplicating /v2', async () => {
|
|
const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValue(
|
|
new Response('', {
|
|
status: 200,
|
|
headers: { 'Content-Length': '0' }
|
|
})
|
|
);
|
|
|
|
const request = new Request(
|
|
'https://example.com/cr/ghcr/v2/nginxinc/nginx-unprivileged/manifests/latest',
|
|
{
|
|
method: 'HEAD'
|
|
}
|
|
);
|
|
const response = await worker.fetch(request, {}, executionContext);
|
|
|
|
expect(response.status).toBe(200);
|
|
expect(String(fetchSpy.mock.calls[0][0])).toBe(
|
|
'https://ghcr.io/v2/nginxinc/nginx-unprivileged/manifests/latest'
|
|
);
|
|
});
|
|
|
|
it('routes host-style registry manifests through the upstream v2 API', async () => {
|
|
const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValue(
|
|
new Response('', {
|
|
status: 200,
|
|
headers: { 'Content-Length': '0' }
|
|
})
|
|
);
|
|
|
|
const request = new Request('https://example.com/v2/cr/ghcr/xixu-me/xget/manifests/latest', {
|
|
method: 'HEAD'
|
|
});
|
|
const response = await worker.fetch(request, {}, executionContext);
|
|
|
|
expect(response.status).toBe(200);
|
|
expect(String(fetchSpy.mock.calls[0][0])).toBe(
|
|
'https://ghcr.io/v2/xixu-me/xget/manifests/latest'
|
|
);
|
|
});
|
|
|
|
it('normalizes Docker Hub official image paths during proxying', async () => {
|
|
const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValue(
|
|
new Response('', {
|
|
status: 200,
|
|
headers: { 'Content-Length': '0' }
|
|
})
|
|
);
|
|
|
|
const request = new Request('https://example.com/cr/docker/v2/nginx/manifests/latest', {
|
|
headers: { Accept: 'application/vnd.docker.distribution.manifest.v2+json' }
|
|
});
|
|
const response = await worker.fetch(request, {}, executionContext);
|
|
|
|
expect(response.status).toBe(200);
|
|
expect(String(fetchSpy.mock.calls[0][0])).toBe(
|
|
'https://registry-1.docker.io/v2/library/nginx/manifests/latest'
|
|
);
|
|
});
|
|
|
|
it('preserves platform-specific Docker auth challenges', async () => {
|
|
let callCount = 0;
|
|
vi.spyOn(globalThis, 'fetch').mockImplementation(async () => {
|
|
callCount++;
|
|
|
|
if (callCount === 1) {
|
|
return new Response('', {
|
|
status: 401,
|
|
headers: {
|
|
'WWW-Authenticate': 'Bearer realm="https://ghcr.io/token",service="ghcr.io"'
|
|
}
|
|
});
|
|
}
|
|
|
|
return new Response('denied', { status: 401 });
|
|
});
|
|
|
|
const request = new Request('https://example.com/cr/ghcr/v2/private/repo/manifests/latest', {
|
|
headers: { Accept: 'application/vnd.docker.distribution.manifest.v2+json' }
|
|
});
|
|
const response = await worker.fetch(request, {}, executionContext);
|
|
|
|
expect(response.status).toBe(401);
|
|
expect(response.headers.get('WWW-Authenticate')).toBe(
|
|
'Bearer realm="https://example.com/cr/ghcr/v2/auth",service="Xget"'
|
|
);
|
|
expect(await response.text()).toContain('UNAUTHORIZED');
|
|
});
|
|
|
|
it('follows 303 redirects for Docker registry responses without forwarding auth headers', async () => {
|
|
const fetchSpy = vi.spyOn(globalThis, 'fetch').mockImplementation(async (input, init) => {
|
|
const headers = new Headers(init?.headers);
|
|
const url = String(input);
|
|
|
|
if (url === 'https://ghcr.io/v2/xixu-me/xget/manifests/latest') {
|
|
expect(headers.get('Authorization')).toBe('Bearer token123');
|
|
return new Response(null, {
|
|
status: 303,
|
|
headers: {
|
|
Location: 'https://pkg-containers.githubusercontent.com/manifest'
|
|
}
|
|
});
|
|
}
|
|
|
|
if (url === 'https://pkg-containers.githubusercontent.com/manifest') {
|
|
expect(headers.get('Authorization')).toBeNull();
|
|
return new Response('', {
|
|
status: 200,
|
|
headers: { 'Content-Length': '0' }
|
|
});
|
|
}
|
|
|
|
throw new Error(`Unexpected fetch URL: ${url}`);
|
|
});
|
|
|
|
const request = new Request('https://example.com/v2/cr/ghcr/xixu-me/xget/manifests/latest', {
|
|
headers: { Authorization: 'Bearer token123' }
|
|
});
|
|
const response = await worker.fetch(request, {}, executionContext);
|
|
|
|
expect(response.status).toBe(200);
|
|
expect(fetchSpy).toHaveBeenCalledTimes(2);
|
|
});
|
|
|
|
it('accepts standard repository scopes on platform-prefixed auth endpoints', async () => {
|
|
const fetchSpy = vi.spyOn(globalThis, 'fetch').mockImplementation(async input => {
|
|
const url = String(input);
|
|
|
|
if (url === '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?scope=repository:private/repo:pull&service=Xget'
|
|
);
|
|
const response = await worker.fetch(request, {}, executionContext);
|
|
|
|
expect(response.status).toBe(200);
|
|
expect(String(fetchSpy.mock.calls[1][0])).toContain('scope=repository%3Aprivate%2Frepo%3Apull');
|
|
});
|
|
|
|
it('treats empty JSON token responses as unusable instead of throwing', async () => {
|
|
const token = await readRegistryTokenResponse(
|
|
new Response('', {
|
|
status: 200,
|
|
headers: { 'Content-Type': 'application/json' }
|
|
})
|
|
);
|
|
|
|
expect(token).toBeNull();
|
|
});
|
|
|
|
it('falls back cleanly when the token service returns an empty success body', async () => {
|
|
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
let callCount = 0;
|
|
|
|
vi.spyOn(globalThis, 'fetch').mockImplementation(async () => {
|
|
callCount++;
|
|
|
|
if (callCount === 1) {
|
|
return new Response('', {
|
|
status: 401,
|
|
headers: {
|
|
'WWW-Authenticate': 'Bearer realm="https://ghcr.io/token",service="ghcr.io"'
|
|
}
|
|
});
|
|
}
|
|
|
|
return new Response('', { status: 200 });
|
|
});
|
|
|
|
const request = new Request('https://example.com/cr/ghcr/v2/private/repo/manifests/latest', {
|
|
headers: { Accept: 'application/vnd.docker.distribution.manifest.v2+json' }
|
|
});
|
|
const response = await worker.fetch(request, {}, executionContext);
|
|
|
|
expect(response.status).toBe(401);
|
|
expect(response.headers.get('WWW-Authenticate')).toBe(
|
|
'Bearer realm="https://example.com/cr/ghcr/v2/auth",service="Xget"'
|
|
);
|
|
expect(await response.text()).toContain('UNAUTHORIZED');
|
|
expect(warnSpy).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
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'
|
|
});
|
|
});
|
|
|
|
it('updates Content-Length after rewriting npm metadata', async () => {
|
|
const upstreamBody = JSON.stringify({
|
|
dist: {
|
|
tarball: 'https://registry.npmjs.org/pkg/-/pkg-1.0.0.tgz'
|
|
}
|
|
});
|
|
|
|
vi.spyOn(globalThis, 'fetch').mockResolvedValue(
|
|
new Response(upstreamBody, {
|
|
status: 200,
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Content-Length': String(upstreamBody.length)
|
|
}
|
|
})
|
|
);
|
|
|
|
const response = await worker.fetch(
|
|
new Request('https://example.com/npm/pkg'),
|
|
{},
|
|
executionContext
|
|
);
|
|
const body = await response.text();
|
|
|
|
expect(body).toContain('https://example.com/npm/pkg/-/pkg-1.0.0.tgz');
|
|
expect(response.headers.get('Content-Length')).toBe(
|
|
String(new TextEncoder().encode(body).byteLength)
|
|
);
|
|
});
|
|
});
|