UNPKG

straightforward

Version:
293 lines (286 loc) 8.64 kB
var __defProp = Object.defineProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; // src/MiddlewareDispatcher.ts var MiddlewareDispatcher = class { constructor() { this.middlewares = []; } use(...mw) { this.middlewares.push(...mw); } dispatch(context) { return invokeMiddlewares(context, this.middlewares); } }; async function invokeMiddlewares(context, middlewares) { if (!middlewares.length) return; const mw = middlewares[0]; return mw(context, async () => { await invokeMiddlewares(context, middlewares.slice(1)); }); } // src/Straightforward.ts import http from "http"; import net from "net"; import cluster from "cluster"; import { EventEmitter } from "events"; import os from "os"; import Debug from "debug"; var numCPUs = os.cpus().length; var debug = Debug("straightforward"); function isRequest(ctx) { return ctx.res !== void 0; } function isConnect(ctx) { return ctx.clientSocket !== void 0; } var Straightforward = class extends EventEmitter { constructor(opts = {}) { super(); this.server = http.createServer(); this.instanceId = Math.random(); this.onRequest = new MiddlewareDispatcher(); this.onResponse = new MiddlewareDispatcher(); this.onConnect = new MiddlewareDispatcher(); this.stats = { onRequest: 0, onConnect: 0 }; this.opts = { requestTimeout: opts.requestTimeout || 60 * 1e3 }; debug("constructor: %o", { instanceId: this.instanceId, ...opts, pid: process.pid }); } async cluster(port, count = numCPUs) { if (cluster.isWorker) { return this.listen(port); } for (let i = 0; i < count; i++) { cluster.fork(); } } async listen(port = 9191) { this.server.on("request", this._onRequest.bind(this)); this.server.on("connect", this._onConnect.bind(this)); this.server.on("error", this._onServerError.bind(this)); this.server.on("clientError", this._onRequestError.bind(this)); this.server.on("upgrade", this._onUpgrade.bind(this)); process.on("uncaughtException", this._onUncaughtException.bind(this)); return new Promise( (resolve) => this.server.listen(port, () => { debug("listen: %o", { port, pid: process.pid }); this.emit("listen", port, process.pid, this.server); resolve(this); }) ); } close() { debug("close"); try { this.server.close(); } catch (err) { debug("close err", err); } this.emit("close"); } async _onRequest(req, res) { debug("onRequest: %s %s", req.method, req.url); this._populateUrlParts(req); this.stats.onRequest++; await this.onRequest.dispatch({ req, res }); if (!req.destroyed && !res.writableEnded) { this._proxyRequest(req, res); } else { debug("onRequest - ended: %s %s", req.method, req.url); } } _proxyRequest(req, res) { const proxyReq = http.request({ method: req.method, headers: req.headers, ...req.locals.urlParts }); proxyReq.removeHeader("proxy-connection"); req.on("destroyed", () => { debug("proxyReq - destroyed: %s %s", req.method, req.url); proxyReq.destroy(); }); proxyReq.on("error", (err) => { debug("proxyReq - error: %s %s", req.method, req.url, err); req.destroy(err); }); proxyReq.on("response", (proxyRes) => this._onResponse(req, res, proxyRes)); proxyReq.on("socket", (socket) => { socket.setTimeout(this.opts.requestTimeout, () => { debug("proxyReq: onTimeout", this.opts.requestTimeout); proxyReq.destroy(); }); if (req.destroyed) { return proxyReq.destroy(); } req.pipe(proxyReq).on("error", (e) => { debug("req.pipe(proxyReq) has error: " + e.message); }); }); } async _onResponse(req, res, proxyRes) { debug("onResponse: %s %s", req.method, req.url); proxyRes.on("error", (err) => debug("proxyRes: onError: %o", err)); await this.onResponse.dispatch({ req, res, proxyRes }); if (!res.headersSent) { res.writeHead(proxyRes.statusCode || 200, proxyRes.headers); } if (!res.writableEnded) { proxyRes.pipe(res).on("error", (e) => { debug("proxyRes.pipe(res) has error: " + e.message); }); } } async _onConnect(req, clientSocket, head) { debug("onConnect: %s %s", req.method, req.url); this._populateUrlParts(req); this.stats.onConnect++; await this.onConnect.dispatch({ req, clientSocket, head }); if (!req.destroyed && clientSocket.writable) { this._proxyConnect(req, clientSocket, head); } } _proxyConnect(req, clientSocket, head) { const serverSocket = net.connect( req.locals.urlParts.port, req.locals.urlParts.host, () => { clientSocket.write( "HTTP/1.1 200 Connection Established\r\nProxy-agent: straightforward\r\n\r\n" ); serverSocket.write(head); if (!req.destroyed && clientSocket.writable) { serverSocket.pipe(clientSocket).on("error", (e) => { debug("serverSocket.pipe(clientSocket) has error: " + e.message); }); clientSocket.pipe(serverSocket).on("error", (e) => { debug("clientSocket.pipe(serverSocket) has error: " + e.message); }); } } ); clientSocket.on("destroyed", () => { debug("clientSocket - destroyed: %s %s", req.method, req.url); serverSocket.destroy(); }); serverSocket.on("error", (err) => { debug("serverSocket error", err); clientSocket.destroy(); }); } _onUpgrade(req, clientSocket, head) { debug("onUpgrade: %s %s", req.headers.upgrade, req.url); debug("Unencrypted websockets are not supported."); this.emit("upgrade", req, clientSocket, head); clientSocket.end(); } _onRequestError(err, socket) { debug("onRequestError: %o", err); this.emit("requestError", err, socket); socket.end("HTTP/1.1 400 Bad Request\r\n\r\n"); } _onServerError(err) { debug("onServerError: %o", err); this.emit("serverError", err); } _onUncaughtException(err) { debug("onUncaughtException: %o", err); this.emit("uncaughtException", err); } _populateUrlParts(req) { if (!req.method || !req.url) { throw new Error("Invalid request"); } ; req.locals = {}; req.locals.isConnect = req.method.toLowerCase() === "connect"; if (req.locals.isConnect) { const [hostname, port] = req.url.split(":", 2); req.locals.urlParts = { host: hostname, port: parseInt(port), path: "" }; } else { const urlParts = new URL(req.url); req.locals.urlParts = { host: urlParts.host, port: parseInt(urlParts.port || "80"), path: urlParts.pathname + urlParts.search }; } } }; // src/middleware/index.ts var middleware_exports = {}; __export(middleware_exports, { auth: () => auth, echo: () => echo }); // src/middleware/auth.ts import Debug2 from "debug"; var debug2 = Debug2("straightforward:middleware"); var auth = ({ user, pass, dynamic }) => async (ctx, next) => { debug2("authenticating incoming request"); const sendAuthRequired = () => { if (isRequest(ctx)) { ctx.res.writeHead(407, { "Proxy-Authenticate": "Basic" }); ctx.res.end(); } else if (isConnect(ctx)) { ctx.clientSocket.end( "HTTP/1.1 407\r\nProxy-Authenticate: basic\r\n\r\n" ); } }; const proxyAuth = ctx.req.headers["proxy-authorization"]; if (!proxyAuth) { return sendAuthRequired(); } const [proxyUser, proxyPass] = Buffer.from( proxyAuth.replace("Basic ", ""), "base64" ).toString().split(":"); if (!dynamic && !!(!!user && !!pass)) { if (user !== proxyUser || pass !== proxyPass) { return sendAuthRequired(); } } ctx.req.locals.proxyUser = proxyUser; ctx.req.locals.proxyPass = proxyPass; return next(); }; // src/middleware/echo.ts import Debug3 from "debug"; var debug3 = Debug3("straightforward:middleware"); var echo = async ({ req, res }, next) => { debug3("echoing incoming request"); const data = { url: req.url || "", locals: req.locals }; res.writeHead(200, { "Content-Type": "application/json; charset=utf-8" }); res.end(JSON.stringify(data, null, 2)); }; // src/index.ts var src_default = Straightforward; export { MiddlewareDispatcher, Straightforward, src_default as default, isConnect, isRequest, middleware_exports as middleware };