straightforward
Version:
A straightforward forward-proxy.
293 lines (286 loc) • 8.64 kB
JavaScript
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
};