UNPKG

@codelook/proxy-server

Version:

HTTP/HTTPS proxy server with Docker deployment and Cloudflare Workers support

314 lines (310 loc) 9.24 kB
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, { get: (a, b) => (typeof require !== "undefined" ? require : a)[b] }) : x)(function(x) { if (typeof require !== "undefined") return require.apply(this, arguments); throw Error('Dynamic require of "' + x + '" is not supported'); }); // src/core/proxy-server.ts import http from "http"; import httpProxy from "http-proxy"; import { URL } from "url"; import { pino } from "pino"; // src/core/auth.ts var AuthMiddleware = class { enabled; token; constructor(config) { this.enabled = config?.enabled ?? false; this.token = config?.token || null; if (this.enabled && !this.token) { this.token = process.env.PROXY_AUTH_TOKEN || null; if (!this.token) { throw new Error("Authentication is enabled but no token is provided"); } } } authenticate(req) { if (!this.enabled) { return true; } const authHeader = req.headers.authorization; if (!authHeader) { return false; } const match = authHeader.match(/^Bearer (.+)$/i); if (!match) { return false; } const providedToken = match[1]; return providedToken === this.token; } /** * 生成新的认证令牌 */ static generateToken() { const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; let token = "pst_"; for (let i = 0; i < 32; i++) { token += chars.charAt(Math.floor(Math.random() * chars.length)); } return token; } }; // src/core/rate-limiter.ts var RateLimiter = class { windowMs; maxRequests; clients = /* @__PURE__ */ new Map(); constructor(config) { this.windowMs = config?.windowMs || 6e4; this.maxRequests = config?.maxRequests || 100; setInterval(() => this.cleanup(), this.windowMs); } allow(clientIp) { const now = Date.now(); const client = this.clients.get(clientIp); if (!client || now > client.resetTime) { this.clients.set(clientIp, { requests: 1, resetTime: now + this.windowMs }); return true; } if (client.requests >= this.maxRequests) { return false; } client.requests++; return true; } cleanup() { const now = Date.now(); for (const [ip, record] of this.clients.entries()) { if (now > record.resetTime) { this.clients.delete(ip); } } } reset(clientIp) { if (clientIp) { this.clients.delete(clientIp); } else { this.clients.clear(); } } getStatus(clientIp) { const client = this.clients.get(clientIp); if (!client) { return null; } return { requests: client.requests, remaining: Math.max(0, this.maxRequests - client.requests), resetTime: client.resetTime }; } }; // src/core/proxy-server.ts var ProxyServer = class { constructor(config) { this.config = config; this.logger = pino({ level: config.logging?.level || "info", transport: config.logging?.pretty ? { target: "pino-pretty", options: { colorize: true, translateTime: "HH:MM:ss Z", ignore: "pid,hostname" } } : void 0 }); this.auth = new AuthMiddleware(config.auth); this.rateLimiter = new RateLimiter(config.rateLimit); this.proxy = httpProxy.createProxyServer({ changeOrigin: config.target?.changeOrigin ?? true, secure: config.target?.secure ?? false, ws: true, followRedirects: true }); this.setupProxyHandlers(); } proxy; server = null; logger; auth; rateLimiter; requestCount = 0; setupProxyHandlers() { this.proxy.on("proxyReq", (proxyReq, req, res) => { const requestId = this.generateRequestId(); req.requestId = requestId; const clientIp = this.getClientIp(req); proxyReq.setHeader("X-Forwarded-For", clientIp); proxyReq.setHeader("X-Real-IP", clientIp); proxyReq.setHeader("X-Request-ID", requestId); this.logger.info({ requestId, method: req.method, url: req.url, headers: req.headers }, "Proxying request"); }); this.proxy.on("proxyRes", (proxyRes, req, res) => { const requestId = req.requestId; proxyRes.headers["X-Proxy-By"] = "CodeLook Proxy Server"; proxyRes.headers["X-Request-ID"] = requestId; this.logger.info({ requestId, statusCode: proxyRes.statusCode, headers: proxyRes.headers }, "Proxy response"); }); this.proxy.on("error", (err, req, res) => { const requestId = req.requestId; this.logger.error({ requestId, error: err.message, stack: err.stack }, "Proxy error"); if (res instanceof http.ServerResponse && !res.headersSent) { res.writeHead(502, { "Content-Type": "application/json" }); res.end(JSON.stringify({ error: "Bad Gateway", message: "The proxy server received an invalid response", requestId })); } }); } start(port) { return new Promise((resolve, reject) => { const serverPort = port || this.config.port || 8080; const serverHost = this.config.host || "0.0.0.0"; this.server = http.createServer(async (req, res) => { if (req.url === "/health") { res.writeHead(200, { "Content-Type": "application/json" }); res.end(JSON.stringify({ status: "ok", uptime: process.uptime(), requests: this.requestCount })); return; } if (!this.auth.authenticate(req)) { res.writeHead(401, { "Content-Type": "application/json" }); res.end(JSON.stringify({ error: "Unauthorized" })); return; } const clientIp = this.getClientIp(req); if (!this.rateLimiter.allow(clientIp)) { res.writeHead(429, { "Content-Type": "application/json" }); res.end(JSON.stringify({ error: "Too Many Requests" })); return; } const targetUrl = this.extractTargetUrl(req); if (!targetUrl) { res.writeHead(400, { "Content-Type": "application/json" }); res.end(JSON.stringify({ error: "Missing target URL" })); return; } this.requestCount++; this.proxy.web(req, res, { target: targetUrl, headers: { host: new URL(targetUrl).host } }); }); this.server.on("connect", (req, clientSocket, head) => { if (!this.auth.authenticate(req)) { clientSocket.write("HTTP/1.1 401 Unauthorized\r\n\r\n"); clientSocket.end(); return; } const { hostname, port: port2 } = this.parseConnectUrl(req.url); const serverPort2 = parseInt(port2) || 443; this.logger.info({ method: "CONNECT", hostname, port: serverPort2 }, "Handling CONNECT request"); const serverSocket = __require("net").connect(serverPort2, hostname, () => { clientSocket.write("HTTP/1.1 200 Connection Established\r\nProxy-agent: CodeLook Proxy\r\n\r\n"); serverSocket.write(head); serverSocket.pipe(clientSocket); clientSocket.pipe(serverSocket); }); serverSocket.on("error", (err) => { this.logger.error({ error: err.message, hostname, port: serverPort2 }, "CONNECT error"); clientSocket.end(); }); clientSocket.on("error", () => { serverSocket.end(); }); }); this.server.listen(serverPort, serverHost, () => { this.logger.info({ port: serverPort, host: serverHost }, "Proxy server started"); resolve(); }); this.server.on("error", (err) => { this.logger.error({ error: err }, "Server error"); reject(err); }); }); } stop() { return new Promise((resolve) => { if (this.server) { this.server.close(() => { this.logger.info("Proxy server stopped"); resolve(); }); } else { resolve(); } }); } extractTargetUrl(req) { const targetHeader = req.headers["x-target-url"]; if (targetHeader) { return targetHeader; } const url = new URL(req.url, `http://${req.headers.host}`); const targetParam = url.searchParams.get("target"); if (targetParam) { return targetParam; } const pathMatch = req.url?.match(/^\/proxy\/(https?:\/\/.+)/); if (pathMatch) { return pathMatch[1]; } return null; } parseConnectUrl(url) { const [hostname, port = "443"] = url.split(":"); return { hostname, port }; } getClientIp(req) { const forwarded = req.headers["x-forwarded-for"]; if (forwarded) { return forwarded.split(",")[0].trim(); } return req.socket.remoteAddress || "unknown"; } generateRequestId() { return `req_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; } }; export { AuthMiddleware, ProxyServer, RateLimiter }; //# sourceMappingURL=index.js.map