| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| import * as http from "node:http"; |
| import * as https from "node:https"; |
| import * as crypto from "node:crypto"; |
| import * as zlib from "node:zlib"; |
| import { URL } from "node:url"; |
|
|
| |
| const CONFIG = { |
| UPSTREAM: process.env.UPSTREAM || "https://huggingface.co", |
| PORT: parseInt(process.env.PORT) || 7860, |
| CACHE_TTL: parseInt(process.env.CACHE_TTL) || 0, |
| CACHE_MAX_SIZE: parseInt(process.env.CACHE_MAX_SIZE) || 5000, |
| CACHE_MAX_BYTES: parseInt(process.env.CACHE_MAX_BYTES) || 100 * 1024 * 1024, |
| MAX_RETRIES: parseInt(process.env.MAX_RETRIES) || 3, |
| PUBLIC_HOST: process.env.PUBLIC_HOST || "", |
| UPLOAD_TOKEN_TTL: parseInt(process.env.UPLOAD_TOKEN_TTL) || 3600_000, |
| UPLOAD_TOKEN_MAX: parseInt(process.env.UPLOAD_TOKEN_MAX) || 2000, |
| RATE_LIMIT_WINDOW: parseInt(process.env.RATE_LIMIT_WINDOW) || 60_000, |
| RATE_LIMIT_MAX: parseInt(process.env.RATE_LIMIT_MAX) || 9999999, |
| REQUEST_TIMEOUT: parseInt(process.env.REQUEST_TIMEOUT) || 300_000, |
| LOG_LEVEL: process.env.LOG_LEVEL || "info", |
| }; |
|
|
| |
| |
| const HF_HOSTS = new Set([ |
| "huggingface.co", |
| "www.huggingface.co", |
| "hf.co", |
| "www.hf.co", |
| ]); |
|
|
| const HF_SUFFIXES = [".huggingface.co", ".hf.co"]; |
|
|
| |
| |
| const EXTERNAL_CDN_HOSTS = new Set([ |
| "cas-bridge.xethub.hf.co", |
| "xethub.hf.co", |
| "cdn.hf.co", |
| ]); |
|
|
| |
| function isExternalCDN(hostname) { |
| if (EXTERNAL_CDN_HOSTS.has(hostname)) return true; |
| |
| if (hostname.includes("xethub") || hostname.includes("cas-bridge")) return true; |
| return false; |
| } |
|
|
| |
| function isHuggingFaceHost(hostname) { |
| |
| if (isExternalCDN(hostname)) return false; |
|
|
| if (HF_HOSTS.has(hostname)) return true; |
| return HF_SUFFIXES.some((suffix) => hostname.endsWith(suffix)); |
| } |
|
|
| |
| const LOG_COLORS = { |
| debug: "\x1b[36m", |
| info: "\x1b[32m", |
| warn: "\x1b[33m", |
| error: "\x1b[31m", |
| reset: "\x1b[0m", |
| }; |
|
|
| const LOG_LEVELS = { debug: 0, info: 1, warn: 2, error: 3 }; |
|
|
| function log(level, message, meta = {}) { |
| if (LOG_LEVELS[level] < LOG_LEVELS[CONFIG.LOG_LEVEL]) return; |
| const timestamp = new Date().toISOString(); |
| const color = LOG_COLORS[level] || ""; |
| const reset = LOG_COLORS.reset; |
| const metaStr = Object.keys(meta).length ? " " + JSON.stringify(meta) : ""; |
| console.log(`${color}[${timestamp}] [${level.toUpperCase()}]${reset} ${message}${metaStr}`); |
| } |
|
|
| |
| class LRUCache { |
| constructor(options) { |
| this.max = options.max || 1000; |
| this.maxBytes = options.maxBytes || Infinity; |
| this.ttl = options.ttl || 0; |
| this.map = new Map(); |
| this.currentBytes = 0; |
| } |
|
|
| _makeKey(method, url, host) { |
| return `${host}:${method}:${url}`; |
| } |
|
|
| get(method, url, host) { |
| const key = this._makeKey(method, url, host); |
| const entry = this.map.get(key); |
| if (!entry) return null; |
|
|
| |
| if (this.ttl > 0 && Date.now() - entry.time > entry.ttl) { |
| this._deleteEntry(key, entry); |
| return null; |
| } |
|
|
| |
| this.map.delete(key); |
| this.map.set(key, entry); |
| return entry; |
| } |
|
|
| set(method, url, host, status, headers, body, customTtl) { |
| const ttl = customTtl || this.ttl; |
| if (ttl <= 0) return false; |
|
|
| const key = this._makeKey(method, url, host); |
| const entry = { |
| time: Date.now(), |
| ttl, |
| status, |
| headers: { ...headers }, |
| body, |
| size: body?.length || 0, |
| }; |
|
|
| |
| if (this.map.has(key)) { |
| this._deleteEntry(key, this.map.get(key)); |
| } |
|
|
| |
| while ( |
| this.map.size >= this.max || |
| (this.maxBytes !== Infinity && this.currentBytes + entry.size > this.maxBytes) |
| ) { |
| const firstKey = this.map.keys().next().value; |
| if (!firstKey) break; |
| this._deleteEntry(firstKey, this.map.get(firstKey)); |
| } |
|
|
| this.map.set(key, entry); |
| this.currentBytes += entry.size; |
| return true; |
| } |
|
|
| _deleteEntry(key, entry) { |
| this.currentBytes -= entry?.size || 0; |
| this.map.delete(key); |
| } |
|
|
| clear() { |
| this.map.clear(); |
| this.currentBytes = 0; |
| } |
|
|
| get stats() { |
| return { |
| size: this.map.size, |
| bytes: this.currentBytes, |
| max: this.max, |
| maxBytes: this.maxBytes, |
| }; |
| } |
| } |
|
|
| |
| class RateLimiter { |
| constructor(windowMs, maxRequests) { |
| this.windowMs = windowMs; |
| this.maxRequests = maxRequests; |
| this.clients = new Map(); |
| } |
|
|
| isAllowed(clientId) { |
| const now = Date.now(); |
| const record = this.clients.get(clientId); |
|
|
| if (!record) { |
| this.clients.set(clientId, { count: 1, resetTime: now + this.windowMs }); |
| return { allowed: true, remaining: this.maxRequests - 1 }; |
| } |
|
|
| if (now > record.resetTime) { |
| record.count = 1; |
| record.resetTime = now + this.windowMs; |
| return { allowed: true, remaining: this.maxRequests - 1 }; |
| } |
|
|
| if (record.count >= this.maxRequests) { |
| return { |
| allowed: false, |
| remaining: 0, |
| retryAfter: Math.ceil((record.resetTime - now) / 1000), |
| }; |
| } |
|
|
| record.count++; |
| return { allowed: true, remaining: this.maxRequests - record.count }; |
| } |
|
|
| |
| cleanup() { |
| const now = Date.now(); |
| for (const [id, record] of this.clients) { |
| if (now > record.resetTime + this.windowMs) { |
| this.clients.delete(id); |
| } |
| } |
| } |
|
|
| get stats() { |
| return { clients: this.clients.size }; |
| } |
| } |
|
|
| |
| class UploadTokenManager { |
| constructor(maxSize, ttl) { |
| this.max = maxSize; |
| this.ttl = ttl; |
| this.map = new Map(); |
| } |
|
|
| create(originalUrl, host) { |
| |
| while (this.map.size >= this.max) { |
| const firstKey = this.map.keys().next().value; |
| this.map.delete(firstKey); |
| } |
|
|
| const token = crypto.randomBytes(16).toString("hex"); |
| this.map.set(token, { url: originalUrl, time: Date.now() }); |
| return `https://${host}/proxy-upload/${token}`; |
| } |
|
|
| get(token) { |
| const entry = this.map.get(token); |
| if (!entry) return null; |
|
|
| if (Date.now() - entry.time > this.ttl) { |
| this.map.delete(token); |
| return null; |
| } |
|
|
| |
| this.map.delete(token); |
| this.map.set(token, entry); |
| return entry; |
| } |
|
|
| clear() { |
| this.map.clear(); |
| } |
|
|
| get stats() { |
| return { size: this.map.size, max: this.max }; |
| } |
| } |
|
|
| |
| const cache = new LRUCache({ |
| max: CONFIG.CACHE_MAX_SIZE, |
| maxBytes: CONFIG.CACHE_MAX_BYTES, |
| ttl: CONFIG.CACHE_TTL, |
| }); |
|
|
| const urlMap = new UploadTokenManager(CONFIG.UPLOAD_TOKEN_MAX, CONFIG.UPLOAD_TOKEN_TTL); |
|
|
| const rateLimiter = new RateLimiter(CONFIG.RATE_LIMIT_WINDOW, CONFIG.RATE_LIMIT_MAX); |
|
|
| |
| setInterval(() => rateLimiter.cleanup(), 60_000); |
|
|
| |
| function proxyRequest(clientReq, clientRes) { |
| const method = clientReq.method; |
| const url = clientReq.url; |
| const clientIp = getClientIP(clientReq); |
|
|
| |
| const rateCheck = rateLimiter.isAllowed(clientIp); |
| if (!rateCheck.allowed) { |
| log("warn", "Rate limit exceeded", { ip: clientIp, url }); |
| clientRes.writeHead(429, { |
| "content-type": "application/json", |
| "access-control-allow-origin": "*", |
| "x-ratelimit-limit": String(CONFIG.RATE_LIMIT_MAX), |
| "x-ratelimit-remaining": "0", |
| "retry-after": String(rateCheck.retryAfter), |
| }); |
| clientRes.end( |
| JSON.stringify({ |
| error: "rate_limited", |
| message: `Too many requests. Retry after ${rateCheck.retryAfter}s`, |
| }) |
| ); |
| return; |
| } |
|
|
| |
| const uploadMatch = url.match(/^\/proxy-upload\/([a-f0-9]{32})(\/.*)?$/); |
| if (uploadMatch) { |
| handleUploadProxy(clientReq, clientRes, uploadMatch[1], uploadMatch[2] || ""); |
| return; |
| } |
|
|
| const bodyChunks = []; |
| clientReq.on("data", (chunk) => bodyChunks.push(chunk)); |
| clientReq.on("end", () => { |
| const body = Buffer.concat(bodyChunks); |
| const host = getHost(clientReq); |
|
|
| |
| if (method === "GET" && !isFileDownloadUrl(url)) { |
| const cached = cache.get(method, url, host); |
| if (cached) { |
| log("debug", "Cache HIT", { url, host }); |
| clientRes.writeHead(cached.status, { |
| ...cached.headers, |
| "x-cache": "HIT", |
| "access-control-allow-origin": "*", |
| "x-ratelimit-remaining": String(rateCheck.remaining), |
| }); |
| clientRes.end(cached.body); |
| return; |
| } |
| } |
|
|
| doProxy(method, url, clientReq.headers, body, 0, clientRes, host); |
| }); |
| } |
|
|
| function doProxy(method, url, reqHeaders, body, attempt, clientRes, host) { |
| const upstreamUrl = new URL(url, CONFIG.UPSTREAM); |
|
|
| const options = { |
| method, |
| headers: filterHeaders(reqHeaders), |
| timeout: CONFIG.REQUEST_TIMEOUT, |
| }; |
|
|
| const startTime = Date.now(); |
|
|
| const proxyReq = https.request(upstreamUrl, options, (proxyRes) => { |
| const statusCode = proxyRes.statusCode; |
| const respHeaders = filterProxyHeaders(proxyRes.headers); |
| const contentType = respHeaders["content-type"] || ""; |
|
|
| log("debug", "Upstream response", { |
| method, |
| url, |
| status: statusCode, |
| attempt: attempt + 1, |
| }); |
|
|
| |
| if ([301, 302, 307, 308].includes(statusCode) && respHeaders["location"]) { |
| handleRedirect(respHeaders["location"], upstreamUrl, host, clientRes, proxyRes, statusCode); |
| return; |
| } |
|
|
| |
| if (isFileDownloadUrl(url) || statusCode === 206) { |
| clientRes.writeHead(statusCode, { |
| ...respHeaders, |
| "access-control-allow-origin": "*", |
| "x-cache": "MISS", |
| }); |
| proxyRes.pipe(clientRes); |
| return; |
| } |
|
|
| |
| const needsBodyRewrite = shouldRewriteBody(contentType, url); |
|
|
| if (!needsBodyRewrite) { |
| clientRes.writeHead(statusCode, { |
| ...respHeaders, |
| "access-control-allow-origin": "*", |
| "x-cache": "MISS", |
| }); |
| proxyRes.pipe(clientRes); |
| return; |
| } |
|
|
| |
| const chunks = []; |
| proxyRes.on("data", (chunk) => chunks.push(chunk)); |
| proxyRes.on("end", () => { |
| let fullBody = Buffer.concat(chunks); |
| const enc = proxyRes.headers["content-encoding"]; |
|
|
| try { |
| if (enc === "gzip") fullBody = zlib.gunzipSync(fullBody); |
| else if (enc === "deflate") fullBody = zlib.inflateSync(fullBody); |
| else if (enc === "br") { |
| |
| fullBody = zlib.brotliDecompressSync(fullBody); |
| } |
| } catch (decompressErr) { |
| log("warn", "Decompression failed", { url, error: decompressErr.message }); |
| |
| } |
|
|
| let newBody; |
| const outHeaders = { ...respHeaders }; |
| delete outHeaders["content-encoding"]; |
| delete outHeaders["content-length"]; |
|
|
| try { |
| if (contentType.includes("application/json") || contentType.includes("application/vnd.git-lfs+json")) { |
| const json = JSON.parse(fullBody.toString("utf-8")); |
| const rewritten = rewriteBody(json, host); |
| newBody = Buffer.from(JSON.stringify(rewritten), "utf-8"); |
| } else if (contentType.includes("text/html")) { |
| let html = fullBody.toString("utf-8"); |
| html = rewriteHtml(html, host); |
| newBody = Buffer.from(html, "utf-8"); |
| } else { |
| newBody = fullBody; |
| } |
|
|
| outHeaders["content-length"] = newBody.length; |
| outHeaders["access-control-allow-origin"] = "*"; |
| outHeaders["x-cache"] = "MISS"; |
|
|
| |
| if (method === "GET" && statusCode === 200) { |
| const customTtl = url.includes("/tree?") ? 60_000 : CONFIG.CACHE_TTL; |
| cache.set(method, url, host, statusCode, outHeaders, newBody, customTtl); |
| } |
|
|
| clientRes.writeHead(statusCode, outHeaders); |
| clientRes.end(newBody); |
| } catch (err) { |
| log("error", "Body rewrite failed", { url, error: err.message }); |
| clientRes.writeHead(statusCode, { |
| ...respHeaders, |
| "access-control-allow-origin": "*", |
| "x-cache": "MISS", |
| }); |
| clientRes.end(fullBody); |
| } |
| }); |
| }); |
|
|
| |
| proxyReq.on("error", (err) => { |
| const elapsed = Date.now() - startTime; |
| if (attempt < CONFIG.MAX_RETRIES - 1) { |
| const wait = Math.min(1000 * 2 ** attempt, 10_000); |
| log("warn", "Retrying request", { |
| method, |
| url, |
| attempt: attempt + 1, |
| maxRetries: CONFIG.MAX_RETRIES, |
| error: err.message, |
| elapsedMs: elapsed, |
| }); |
| setTimeout(() => doProxy(method, url, reqHeaders, body, attempt + 1, clientRes, host), wait); |
| } else { |
| log("error", "Upstream unreachable", { |
| method, |
| url, |
| error: err.message, |
| elapsedMs: elapsed, |
| }); |
| if (!clientRes.headersSent) { |
| clientRes.writeHead(502, { |
| "content-type": "application/json", |
| "access-control-allow-origin": "*", |
| }); |
| clientRes.end( |
| JSON.stringify({ |
| error: "upstream_unreachable", |
| message: err.message, |
| }) |
| ); |
| } |
| } |
| }); |
|
|
| proxyReq.on("timeout", () => { |
| proxyReq.destroy(); |
| const elapsed = Date.now() - startTime; |
| if (attempt < CONFIG.MAX_RETRIES - 1) { |
| log("warn", "Request timeout, retrying", { method, url, attempt: attempt + 1, elapsedMs: elapsed }); |
| setTimeout(() => doProxy(method, url, reqHeaders, body, attempt + 1, clientRes, host), 1000); |
| } else { |
| log("error", "Upstream timeout", { method, url, elapsedMs: elapsed }); |
| if (!clientRes.headersSent) { |
| clientRes.writeHead(504, { |
| "content-type": "application/json", |
| "access-control-allow-origin": "*", |
| }); |
| clientRes.end(JSON.stringify({ error: "upstream_timeout" })); |
| } |
| } |
| }); |
|
|
| if (body.length > 0) proxyReq.write(body); |
| proxyReq.end(); |
| } |
|
|
| |
| function handleRedirect(location, upstreamUrl, host, clientRes, proxyRes, statusCode) { |
| try { |
| const loc = new URL(location, upstreamUrl); |
| const locHostname = loc.hostname; |
|
|
| log("debug", "Processing redirect", { |
| original: location, |
| hostname: locHostname, |
| isHF: isHuggingFaceHost(locHostname), |
| isExternalCDN: isExternalCDN(locHostname), |
| }); |
|
|
| if (isHuggingFaceHost(locHostname)) { |
| |
| loc.hostname = host; |
| loc.protocol = "https:"; |
| log("info", "HF redirect rewritten", { |
| status: statusCode, |
| original: location, |
| rewritten: loc.href, |
| }); |
| } else { |
| |
| loc.href = urlMap.create(loc.href, host); |
| log("info", "External redirect tokenized", { |
| status: statusCode, |
| original: location, |
| rewritten: loc.href, |
| cdn: locHostname, |
| }); |
| } |
|
|
| const respHeaders = filterProxyHeaders(proxyRes.headers); |
| respHeaders["location"] = loc.href; |
|
|
| clientRes.writeHead(statusCode, { |
| ...respHeaders, |
| "access-control-allow-origin": "*", |
| }); |
| proxyRes.pipe(clientRes); |
| } catch (err) { |
| log("error", "Redirect handling failed", { location, error: err.message }); |
| clientRes.writeHead(502, { |
| "content-type": "application/json", |
| "access-control-allow-origin": "*", |
| }); |
| clientRes.end(JSON.stringify({ error: "redirect_handling_failed", message: err.message })); |
| } |
| } |
|
|
| |
| function rewriteBody(obj, host) { |
| if (!obj || typeof obj !== "object") return obj; |
|
|
| if (Array.isArray(obj)) { |
| return obj.map((item) => rewriteBody(item, host)); |
| } |
|
|
| const result = {}; |
| for (const [key, value] of Object.entries(obj)) { |
| if (typeof value === "string") { |
| let v = value; |
| v = rewriteUrl(v, host); |
|
|
| if (isExternalUploadField(key, v, host)) { |
| v = urlMap.create(v, host); |
| } |
| result[key] = v; |
| } else if (typeof value === "object" && value !== null) { |
| result[key] = rewriteBody(value, host); |
| } else { |
| result[key] = value; |
| } |
| } |
| return result; |
| } |
|
|
| function rewriteUrl(value, host) { |
| if (typeof value !== "string") return value; |
|
|
| |
| value = value.replace(/https?:\/\/huggingface\.co/g, `https://${host}`); |
|
|
| |
| value = value.replace(/https?:\/\/cdn-lfs\.hf\.co/g, `https://${host}`); |
| value = value.replace(/https?:\/\/cdn-lfs-us-1\.hf\.co/g, `https://${host}`); |
| value = value.replace(/https?:\/\/cdn-lfs-eu-1\.hf\.co/g, `https://${host}`); |
| value = value.replace(/https?:\/\/cdn-lfs-ap-1\.hf\.co/g, `https://${host}`); |
|
|
| |
| |
|
|
| return value; |
| } |
|
|
| function rewriteHtml(html, host) { |
| if (typeof html !== "string") return html; |
| return rewriteUrl(html, host); |
| } |
|
|
| function isExternalUploadField(key, value, host) { |
| if (!value.startsWith("https://")) return false; |
| if (value.startsWith(`https://${host}`)) return false; |
|
|
| |
| try { |
| const url = new URL(value); |
| if (isExternalCDN(url.hostname)) return true; |
| } catch { |
| |
| } |
|
|
| const extFields = [ |
| "href", |
| "uploadUrl", |
| "casUrl", |
| "refreshWriteTokenUrl", |
| "reconstructionUrl", |
| "refreshUrl", |
| "downloadUrl", |
| "lfs", |
| ]; |
| return extFields.includes(key); |
| } |
|
|
| function shouldRewriteBody(contentType, url) { |
| if (contentType.includes("application/json")) return true; |
| if (contentType.includes("application/vnd.git-lfs+json")) return true; |
| if (contentType.includes("text/html")) return true; |
| if (url.includes("/preupload/")) return true; |
| if (url.includes("/xet-write-token")) return true; |
| return false; |
| } |
|
|
| function isFileDownloadUrl(url) { |
| return url.includes("/resolve/") || url.includes("/raw/"); |
| } |
|
|
| |
| function handleUploadProxy(clientReq, clientRes, token, extraPath) { |
| const entry = urlMap.get(token); |
| if (!entry) { |
| log("warn", "Unknown upload token", { token: token.slice(0, 8) + "..." }); |
| clientRes.writeHead(404, { |
| "content-type": "application/json", |
| "access-control-allow-origin": "*", |
| }); |
| clientRes.end(JSON.stringify({ error: "unknown_upload_token" })); |
| return; |
| } |
|
|
| const targetUrl = new URL(entry.url + extraPath); |
| const bodyChunks = []; |
| clientReq.on("data", (chunk) => bodyChunks.push(chunk)); |
| clientReq.on("end", () => { |
| const body = Buffer.concat(bodyChunks); |
| const options = { |
| method: clientReq.method, |
| headers: filterHeaders(clientReq.headers), |
| timeout: CONFIG.REQUEST_TIMEOUT, |
| }; |
|
|
| const req2 = https.request(targetUrl, options, (res2) => { |
| const respHeaders = filterProxyHeaders(res2.headers); |
|
|
| if ([301, 302, 307, 308].includes(res2.statusCode) && respHeaders["location"]) { |
| try { |
| const loc = new URL(respHeaders["location"], targetUrl); |
| if (isHuggingFaceHost(loc.hostname)) { |
| const host = getHost(clientReq); |
| loc.hostname = host; |
| loc.protocol = "https:"; |
| respHeaders["location"] = loc.href; |
| } |
| } catch (err) { |
| log("warn", "Upload redirect rewrite failed", { error: err.message }); |
| } |
| } |
|
|
| clientRes.writeHead(res2.statusCode, { |
| ...respHeaders, |
| "access-control-allow-origin": "*", |
| }); |
| res2.pipe(clientRes); |
| }); |
|
|
| req2.on("error", (err) => { |
| log("error", "Upload forward failed", { |
| target: targetUrl.href, |
| error: err.message, |
| }); |
| if (!clientRes.headersSent) { |
| clientRes.writeHead(502, { |
| "content-type": "application/json", |
| "access-control-allow-origin": "*", |
| }); |
| clientRes.end( |
| JSON.stringify({ |
| error: "upload_forward_failed", |
| message: err.message, |
| }) |
| ); |
| } |
| }); |
|
|
| req2.on("timeout", () => { |
| req2.destroy(); |
| log("error", "Upload timeout", { target: targetUrl.href }); |
| if (!clientRes.headersSent) { |
| clientRes.writeHead(504, { "access-control-allow-origin": "*" }); |
| clientRes.end(); |
| } |
| }); |
|
|
| if (body.length > 0) req2.write(body); |
| req2.end(); |
| }); |
| } |
|
|
| |
| function getHost(req) { |
| return ( |
| CONFIG.PUBLIC_HOST || |
| req.headers["x-forwarded-host"] || |
| req.headers["host"] || |
| `localhost:${CONFIG.PORT}` |
| ); |
| } |
|
|
| function getClientIP(req) { |
| return ( |
| req.headers["x-forwarded-for"]?.split(",")[0]?.trim() || |
| req.headers["x-real-ip"] || |
| req.socket?.remoteAddress || |
| "unknown" |
| ); |
| } |
|
|
| function filterHeaders(headers) { |
| const h = {}; |
| const skip = new Set(["host", "connection", "x-cache", "x-ratelimit-limit", "x-ratelimit-remaining"]); |
| for (const [k, v] of Object.entries(headers)) { |
| if (skip.has(k.toLowerCase())) continue; |
| h[k] = v; |
| } |
| if (!h["accept-encoding"]) h["accept-encoding"] = "gzip, deflate, br"; |
| return h; |
| } |
|
|
| function filterProxyHeaders(headers) { |
| const h = {}; |
| const skip = new Set(["transfer-encoding", "connection", "keep-alive"]); |
| for (const [k, v] of Object.entries(headers)) { |
| if (skip.has(k.toLowerCase())) continue; |
| h[k] = v; |
| } |
| return h; |
| } |
|
|
| |
| const server = http.createServer((req, res) => { |
| const startTime = Date.now(); |
|
|
| |
| res.on("finish", () => { |
| const duration = Date.now() - startTime; |
| log("info", "Request completed", { |
| method: req.method, |
| url: req.url, |
| status: res.statusCode, |
| durationMs: duration, |
| ip: getClientIP(req), |
| }); |
| }); |
|
|
| if (req.method === "OPTIONS") { |
| res.writeHead(204, { |
| "access-control-allow-origin": "*", |
| "access-control-allow-methods": "GET,POST,PUT,DELETE,PATCH,OPTIONS", |
| "access-control-allow-headers": "*", |
| "access-control-max-age": "86400", |
| }); |
| res.end(); |
| return; |
| } |
|
|
| if (req.url === "/" || req.url === "") { |
| res.writeHead(200, { "content-type": "text/html; charset=utf-8" }); |
| res.end(`<!DOCTYPE html> |
| <html> |
| <head><title>HF Mirror</title> |
| <style> |
| body { font-family: system-ui, -apple-system, sans-serif; padding: 40px; max-width: 800px; margin: 0 auto; line-height: 1.6; } |
| h1 { color: #333; } |
| .stat { background: #f5f5f5; padding: 12px 16px; border-radius: 8px; margin: 8px 0; } |
| .stat-label { font-weight: 600; color: #666; } |
| .stat-value { color: #111; font-family: monospace; } |
| a { color: #2563eb; text-decoration: none; } |
| a:hover { text-decoration: underline; } |
| .links { margin-top: 24px; } |
| .links a { margin-right: 16px; } |
| .cdn-list { font-size: 12px; color: #666; margin-top: 8px; } |
| </style> |
| </head> |
| <body> |
| <h1>🤗 HF Mirror Proxy</h1> |
| <div class="stat"><span class="stat-label">上游:</span> <span class="stat-value">${CONFIG.UPSTREAM}</span></div> |
| <div class="stat"><span class="stat-label">缓存:</span> <span class="stat-value">${cache.stats.size} 条 / ${(cache.stats.bytes / 1024 / 1024).toFixed(2)} MB</span></div> |
| <div class="stat"><span class="stat-label">URL映射:</span> <span class="stat-value">${urlMap.stats.size} 条</span></div> |
| <div class="stat"><span class="stat-label">限流客户端:</span> <span class="stat-value">${rateLimiter.stats.clients} 个</span></div> |
| <div class="cdn-list"> |
| 外部CDN代理: ${Array.from(EXTERNAL_CDN_HOSTS).join(", ")} (及含 xethub/cas-bridge 的域名) |
| </div> |
| <div class="links"> |
| <a href="/health">健康检查</a> |
| <a href="/cache/clear">清缓存</a> |
| <a href="/metrics">指标</a> |
| </div> |
| </body> |
| </html>`); |
| return; |
| } |
|
|
| if (req.url === "/health") { |
| res.writeHead(200, { |
| "content-type": "application/json", |
| "access-control-allow-origin": "*", |
| }); |
| res.end( |
| JSON.stringify({ |
| status: "ok", |
| upstream: CONFIG.UPSTREAM, |
| cache: cache.stats, |
| urlMap: urlMap.stats, |
| rateLimiter: rateLimiter.stats, |
| uptime: process.uptime(), |
| memory: process.memoryUsage(), |
| }) |
| ); |
| return; |
| } |
|
|
| if (req.url === "/metrics") { |
| res.writeHead(200, { |
| "content-type": "application/json", |
| "access-control-allow-origin": "*", |
| }); |
| res.end( |
| JSON.stringify({ |
| cache: cache.stats, |
| urlMap: urlMap.stats, |
| rateLimiter: rateLimiter.stats, |
| uptime: process.uptime(), |
| memory: { |
| heapUsed: `${(process.memoryUsage().heapUsed / 1024 / 1024).toFixed(2)} MB`, |
| heapTotal: `${(process.memoryUsage().heapTotal / 1024 / 1024).toFixed(2)} MB`, |
| rss: `${(process.memoryUsage().rss / 1024 / 1024).toFixed(2)} MB`, |
| external: `${(process.memoryUsage().external / 1024 / 1024).toFixed(2)} MB`, |
| }, |
| }) |
| ); |
| return; |
| } |
|
|
| if (req.url === "/cache/clear") { |
| cache.clear(); |
| urlMap.clear(); |
| log("info", "Cache cleared manually"); |
| res.writeHead(200, { |
| "content-type": "application/json", |
| "access-control-allow-origin": "*", |
| }); |
| res.end(JSON.stringify({ cleared: true, timestamp: new Date().toISOString() })); |
| return; |
| } |
|
|
| proxyRequest(req, res); |
| }); |
|
|
| server.listen(CONFIG.PORT, "0.0.0.0", () => { |
| console.log(` |
| 🤗 HF Mirror Proxy 已启动 |
| ───────────────────────────── |
| → 上游: ${CONFIG.UPSTREAM} |
| → 端口: ${CONFIG.PORT} |
| → 公网域名: ${CONFIG.PUBLIC_HOST || "(自动检测)"} |
| → 日志级别: ${CONFIG.LOG_LEVEL} |
| → 缓存上限: ${CONFIG.CACHE_MAX_SIZE} 条 / ${(CONFIG.CACHE_MAX_BYTES / 1024 / 1024).toFixed(0)} MB |
| → 限流: ${CONFIG.RATE_LIMIT_MAX} 请求/${CONFIG.RATE_LIMIT_WINDOW / 1000}s |
| → 外部CDN: ${Array.from(EXTERNAL_CDN_HOSTS).join(", ")} |
| → 健康检查: http://localhost:${CONFIG.PORT}/health |
| → 指标: http://localhost:${CONFIG.PORT}/metrics |
| → 清缓存: http://localhost:${CONFIG.PORT}/cache/clear |
| `); |
| }); |
|
|
| |
| process.on("SIGTERM", () => { |
| log("info", "Received SIGTERM, shutting down gracefully"); |
| server.close(() => { |
| log("info", "Server closed"); |
| process.exit(0); |
| }); |
| }); |
|
|
| process.on("SIGINT", () => { |
| log("info", "Received SIGINT, shutting down gracefully"); |
| server.close(() => { |
| log("info", "Server closed"); |
| process.exit(0); |
| }); |
| }); |
|
|