@im-sampm/act-js
Version:
nodejs wrapper for nektos/act
182 lines (181 loc) • 7.18 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.ForwardProxy = void 0;
const net_1 = __importDefault(require("net"));
const express_1 = __importDefault(require("express"));
const follow_redirects_1 = require("follow-redirects");
const os_1 = require("os");
const proxy_types_1 = require("../proxy/proxy.types");
class ForwardProxy {
constructor(apis, verbose = false, shouldHandleConnect = true) {
this.apis = apis;
this.app = (0, express_1.default)();
this.logger = verbose ? console.log.bind(undefined, "[act-js API Mocker]") : _msg => undefined;
this.server = follow_redirects_1.http.createServer(this.app);
this.currentConnections = {};
this.currentSocketId = 0;
this.shouldHandleConnect = shouldHandleConnect;
}
/**
* Start the server and return the ip address and port in the form of a string - ip:port
* If it is already running then throw an error
* @returns
*/
start() {
this.initServer();
return new Promise((resolve, reject) => {
if (this.server.listening) {
reject(new Error("Server has already started"));
}
else {
this.server.listen(0, () => {
const address = this.server.address();
const port = typeof address === "string" ? address : address.port.toString();
const ip = this.getIp();
this.logger(`forward proxy started at ${ip}:${port}`);
resolve(`${ip}:${port}`);
});
}
});
}
/**
* Gracefully stop the server.
* If it wasn't running then throw an error
* @returns
*/
stop() {
return new Promise((resolve, reject) => {
if (!this.server.listening) {
reject(new Error("Server has not been started"));
}
else {
this.server.close(err => {
if (err) {
reject(err);
}
});
Object.values(this.currentConnections).forEach(socket => socket.destroy());
if (this.secondaryProxy) {
this.secondaryProxy.proxy.stop().then(resolve);
}
else {
resolve();
}
}
});
}
/**
* Return the ip address. Traverses the available network interfaces and returns the first
* public ipv4 address
* @returns
*/
getIp() {
const nics = (0, os_1.networkInterfaces)();
const publicIps = Object.values(nics).reduce((prev, current) => {
prev?.push(...(current?.filter(c => !c.internal && c.family === "IPv4") ?? []));
return prev;
}, []);
if (!publicIps || publicIps.length === 0) {
throw new Error("Could not detect IP address");
}
return publicIps[0].address;
}
initServer() {
// keep track of connected sockets for clean up
this.server.on("connection", socket => {
const socketId = this.currentSocketId;
socket.on("close", () => {
delete this.currentConnections[socketId];
});
this.currentSocketId += 1;
});
if (this.shouldHandleConnect) {
this.handleCONNECT();
}
else {
// reject connect if received and the proxy was initialized not to accept connect requests
this.server.on("connect", (_, socket) => {
socket.write("HTTP/1.1 400 Bad request\r\n\r\n");
});
}
this.handleAPIInterception();
}
/**
* forward any http(s) connections intiated via CONNECT as is
*/
handleCONNECT() {
this.server.on("connect", async (req, socket) => {
socket.on("error", this.logger);
const host = req.url?.split(":")[0];
const port = req.url?.split(":")[1];
this.logger(`received connect request for ${host}:${port}`);
if (!host || !port) {
socket.write("HTTP/1.1 400 Bad request\r\n\r\n");
return;
}
let targetHost = host, targetPort = port;
// if connect request was issued for http target, then we handle it by accepting the connect,
// spinning up a secondary proxy and piping the request to the secondary proxy
if (port === "80") {
if (!this.secondaryProxy) {
// NOTE: make sure to disable CONNECT handling in the secondary proxy to avoid spinning up infinite proxies
// this can happen if the client is malicious and keeps on sending CONNECT requests
const proxy = new ForwardProxy(this.apis, false, false);
const proxyAddress = await proxy.start();
this.secondaryProxy = { proxy, proxyAddress };
}
const [_, secondaryProxyPort] = this.secondaryProxy.proxyAddress.split(":");
targetHost = proxy_types_1.LOCALHOST;
targetPort = secondaryProxyPort;
}
const target = net_1.default.createConnection({ host: targetHost, port: parseInt(targetPort) });
target.on("error", this.logger);
target.on("connect", () => {
this.logger(`connected to ${targetHost}:${targetPort}`);
socket.write("HTTP/1.1 200 OK\r\n\r\n");
socket.pipe(target);
target.pipe(socket);
});
});
}
handleAPIInterception() {
// mock all apis
this.apis.forEach(api => api.reply());
// forward the intercepted api call
this.app.all("/*", (req, res) => {
let path = req.path;
// for http connections initiated via CONNECT, req.url resolves to be the path and not the entire url
try {
path += new URL(req.url).search;
}
catch (err) {
path += new URL(`http://${req.hostname}${req.url}`).search;
}
const opts = {
host: req.hostname,
path,
method: req.method,
headers: req.headers,
agent: false,
};
this.logger(JSON.stringify(opts));
const request = follow_redirects_1.http.request(opts);
request.on("response", response => {
// set status code
if (response.statusCode) {
res.status(response.statusCode);
}
// set headers
if (response.headers) {
res.set(response.headers);
}
response.pipe(res);
});
req.pipe(request);
});
}
}
exports.ForwardProxy = ForwardProxy;