http-proxy-3
Version:
Modern rewrite of http-proxy
266 lines (263 loc) • 9.95 kB
JavaScript
;
/*
Websockets Passes: Array of passes.
A `pass` is just a function that is executed on `req, socket, options`
so that you can easily add new checks while still keeping the base
flexible.
The names of passes are exported as WS_PASSES from this module.
*/
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.WS_PASSES = void 0;
exports.numOpenSockets = numOpenSockets;
exports.checkMethodAndHeader = checkMethodAndHeader;
exports.XHeaders = XHeaders;
exports.stream = stream;
const http = __importStar(require("node:http"));
const https = __importStar(require("node:https"));
const common = __importStar(require("../common"));
const web_outgoing_1 = require("./web-outgoing");
const debug_1 = __importDefault(require("debug"));
const log = (0, debug_1.default)("http-proxy-3:ws-incoming");
const web_o = Object.values(web_outgoing_1.OUTGOING_PASSES);
function createSocketCounter(name) {
let sockets = new Set();
return ({ add, rm, } = {}) => {
if (add) {
if (!add.id) {
add.id = Math.random();
}
if (!sockets.has(add.id)) {
sockets.add(add.id);
}
}
if (rm) {
if (!rm.id) {
rm.id = Math.random();
}
if (sockets.has(rm.id)) {
sockets.delete(rm.id);
}
}
log("socket counter:", { [name]: sockets.size }, add ? "add" : rm ? "rm" : "");
return sockets.size;
};
}
const socketCounter = createSocketCounter("socket");
const proxySocketCounter = createSocketCounter("proxySocket");
/* MockResponse
when a websocket gets a regular HTTP Response,
apply proxied headers
*/
class MockResponse {
constructor() {
this.headers = {};
this.statusCode = 200;
this.statusMessage = "";
}
setHeader(key, value) {
this.headers[key] = value;
return this;
}
;
}
function numOpenSockets() {
return socketCounter() + proxySocketCounter();
}
// WebSocket requests must have the `GET` method and
// the `upgrade:websocket` header
function checkMethodAndHeader(req, socket) {
log("websocket: checkMethodAndHeader");
if (req.method !== "GET" || !req.headers.upgrade) {
socket.destroy();
return true;
}
if (req.headers.upgrade.toLowerCase() !== "websocket") {
socket.destroy();
return true;
}
}
// Sets `x-forwarded-*` headers if specified in config.
function XHeaders(req, _socket, options) {
if (!options.xfwd)
return;
log("websocket: XHeaders");
const values = {
for: req.connection.remoteAddress || req.socket.remoteAddress,
port: common.getPort(req),
proto: common.hasEncryptedConnection(req) ? "wss" : "ws",
};
for (const header of ["for", "port", "proto"]) {
req.headers["x-forwarded-" + header] =
(req.headers["x-forwarded-" + header] || "") +
(req.headers["x-forwarded-" + header] ? "," : "") +
values[header];
}
}
// Do the actual proxying. Make the request and upgrade it.
// Send the Switching Protocols request and pipe the sockets.
function stream(req, socket, options, head, server, cb) {
log("websocket: new stream");
const proxySockets = [];
socketCounter({ add: socket });
const cleanUpProxySockets = () => {
for (const p of proxySockets) {
p.end();
}
};
socket.on("close", () => {
socketCounter({ rm: socket });
cleanUpProxySockets();
});
// The pipe below will end proxySocket if socket closes cleanly, but not
// if it errors (eg, vanishes from the net and starts returning
// EHOSTUNREACH). We need to do that explicitly.
socket.on("error", cleanUpProxySockets);
const createHttpHeader = (line, headers) => {
return (Object.keys(headers)
.reduce((head, key) => {
const value = headers[key];
if (!Array.isArray(value)) {
head.push(key + ": " + value);
return head;
}
for (let i = 0; i < value.length; i++) {
head.push(key + ": " + value[i]);
}
return head;
}, [line])
.join("\r\n") + "\r\n\r\n");
};
common.setupSocket(socket);
if (head && head.length) {
socket.unshift(head);
}
// @ts-expect-error FIXME: options.target may be undefined
const proto = common.isSSL.test(options.target.protocol) ? https : http;
const outgoingOptions = common.setupOutgoing(options.ssl || {}, options, req);
const proxyReq = proto.request(outgoingOptions);
// Enable developers to modify the proxyReq before headers are sent
if (server) {
server.emit("proxyReqWs", proxyReq, req, socket, options, head);
}
// Error Handler
proxyReq.on("error", onOutgoingError);
proxyReq.on("upgrade", (proxyRes, proxySocket, proxyHead) => {
log("upgrade");
proxySocketCounter({ add: proxySocket });
proxySockets.push(proxySocket);
proxySocket.on("close", () => {
proxySocketCounter({ rm: proxySocket });
});
proxySocket.on("error", onOutgoingError);
// Allow us to listen for when the websocket has completed.
proxySocket.on("end", () => {
server.emit("close", proxyRes, proxySocket, proxyHead);
});
proxySocket.on("close", () => {
socket.end();
});
common.setupSocket(proxySocket);
if (proxyHead && proxyHead.length) {
proxySocket.unshift(proxyHead);
}
// Remark: Handle writing the headers to the socket when switching protocols
// Also handles when a header is an array.
socket.write(createHttpHeader("HTTP/1.1 101 Switching Protocols", proxyRes.headers));
proxySocket.pipe(socket).pipe(proxySocket);
server.emit("open", proxySocket);
});
function onOutgoingError(err) {
if (cb) {
cb(err, req, socket);
}
else {
server.emit("error", err, req, socket);
}
// I changed this from "socket.end()" which is what node-http-proxy does to destroySoon() due to getting
// the unit test "should close client socket if upstream is closed before upgrade" from lib/http-proxy.test.ts
// to work. Just doing socket.end() leaves things half open for a while if proxySocket errors out,
// which may be another leak type situation and definitely doesn't work for unit testing.
socket.destroySoon();
}
// if we get a response, backend is not a websocket endpoint,
// relay HTTP response and close the socket
proxyReq.on("response", (proxyRes) => {
log("got non-ws HTTP response", {
statusCode: proxyRes.statusCode,
statusMessage: proxyRes.statusMessage,
});
const res = new MockResponse();
for (const pass of web_o) {
// note: none of these return anything
pass(req, res, proxyRes, options);
}
// implement HTTP/1.1 chunked transfer unless content-length is defined
// matches proxyRes.pipe(res) behavior,
// but we are piping directly to the socket instead, so it's our job.
let writeChunk = (chunk) => {
socket.write(chunk);
};
if (req.httpVersion === "1.1" && proxyRes.headers["content-length"] === undefined) {
res.headers["transfer-encoding"] = "chunked";
writeChunk = (chunk) => {
socket.write(chunk.length.toString(16));
socket.write("\r\n");
socket.write(chunk);
socket.write("\r\n");
};
}
const proxyHead = createHttpHeader(`HTTP/${req.httpVersion} ${proxyRes.statusCode} ${proxyRes.statusMessage}`, res.headers);
if (!socket.destroyed) {
socket.write(proxyHead);
proxyRes.on("data", (chunk) => {
writeChunk(chunk);
});
proxyRes.on("end", () => {
writeChunk("");
socket.destroySoon();
});
}
else {
// make sure response is consumed
proxyRes.resume();
}
});
proxyReq.end();
}
exports.WS_PASSES = { checkMethodAndHeader, XHeaders, stream };