@codelook/proxy-server
Version:
HTTP/HTTPS proxy server with Docker deployment and Cloudflare Workers support
314 lines (310 loc) • 9.24 kB
JavaScript
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