http-proxy-3
Version:
Modern rewrite of http-proxy
364 lines (361 loc) • 14.4 kB
JavaScript
;
/*
A `pass` is just a function that is executed on `req, res, options`
so that you can easily add new checks while still keeping the base
flexible.
The names of passes are exported as WEB_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;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.WEB_PASSES = void 0;
exports.deleteLength = deleteLength;
exports.timeout = timeout;
exports.XHeaders = XHeaders;
exports.stream = stream;
const http = __importStar(require("node:http"));
const https = __importStar(require("node:https"));
const followRedirects = __importStar(require("follow-redirects"));
const common = __importStar(require("../common"));
const web_outgoing_1 = require("./web-outgoing");
const node_stream_1 = require("node:stream");
const web_o = Object.values(web_outgoing_1.OUTGOING_PASSES);
const nativeAgents = { http, https };
// Sets `content-length` to '0' if request is of DELETE type.
function deleteLength(req) {
if ((req.method === "DELETE" || req.method === "OPTIONS") && !req.headers["content-length"]) {
req.headers["content-length"] = "0";
delete req.headers["transfer-encoding"];
}
}
// Sets timeout in request socket if it was specified in options.
function timeout(req, _res, options) {
if (options.timeout) {
req.socket.setTimeout(options.timeout);
}
}
// Sets `x-forwarded-*` headers if specified in config.
function XHeaders(req, _res, options) {
if (!options.xfwd) {
return;
}
const encrypted = common.hasEncryptedConnection(req);
const values = {
for: req.connection.remoteAddress || req.socket.remoteAddress,
port: common.getPort(req),
proto: encrypted ? "https" : "http",
};
for (const header of ["for", "port", "proto"]) {
req.headers["x-forwarded-" + header] =
(req.headers["x-forwarded-" + header] || "") + (req.headers["x-forwarded-" + header] ? "," : "") + values[header];
}
req.headers["x-forwarded-host"] = req.headers["x-forwarded-host"] || req.headers[":authority"] || req.headers["host"] || "";
}
// Does the actual proxying. If `forward` is enabled fires up
// a ForwardStream (there is NO RESPONSE), same happens for ProxyStream. The request
// just dies otherwise.
function stream(req, res, options, _, server, cb) {
// And we begin!
server.emit("start", req, res, options.target || options.forward);
if (options.fetch || options.fetchOptions || process.env.FORCE_FETCH_PATH === "true") {
return stream2(req, res, options, _, server, cb);
}
const agents = options.followRedirects ? followRedirects : nativeAgents;
const http = agents.http;
const https = agents.https;
if (options.forward) {
// forward enabled, so just pipe the request
const proto = options.forward.protocol === "https:" ? https : http;
const outgoingOptions = common.setupOutgoing(options.ssl || {}, options, req, "forward");
const forwardReq = proto.request(outgoingOptions);
// error handler (e.g. ECONNRESET, ECONNREFUSED)
// Handle errors on incoming request as well as it makes sense to
const forwardError = createErrorHandler(forwardReq, options.forward);
req.on("error", forwardError);
forwardReq.on("error", forwardError);
(options.buffer || req).pipe(forwardReq);
if (!options.target) {
// no target, so we do not send anything back to the client.
// If target is set, we do a separate proxy below, which might be to a
// completely different server.
return res.end();
}
}
// Request initalization
const proto = options.target.protocol === "https:" ? 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
proxyReq.on("socket", (socket) => {
if (server && !proxyReq.getHeader("expect")) {
server.emit("proxyReq", proxyReq, req, res, options, socket);
}
});
// allow outgoing socket to timeout so that we could
// show an error page at the initial request
if (options.proxyTimeout) {
proxyReq.setTimeout(options.proxyTimeout, () => {
proxyReq.destroy();
});
}
// Ensure we abort proxy if request is aborted
res.on("close", () => {
const aborted = !res.writableFinished;
if (aborted) {
proxyReq.destroy();
}
});
// handle errors in proxy and incoming request, just like for forward proxy
const proxyError = createErrorHandler(proxyReq, options.target);
req.on("error", proxyError);
proxyReq.on("error", proxyError);
function createErrorHandler(proxyReq, url) {
return (err) => {
if (req.socket.destroyed && err.code === "ECONNRESET") {
server.emit("econnreset", err, req, res, url);
proxyReq.destroy();
return;
}
if (cb) {
cb(err, req, res, url);
}
else {
server.emit("error", err, req, res, url);
}
};
}
(options.buffer || req).pipe(proxyReq);
proxyReq.on("response", (proxyRes) => {
server?.emit("proxyRes", proxyRes, req, res);
if (!res.headersSent && !options.selfHandleResponse) {
for (const pass of web_o) {
// note: none of these return anything
pass(req, res, proxyRes, options);
}
}
if (!res.finished) {
// Allow us to listen for when the proxy has completed
proxyRes.on("end", () => {
server?.emit("end", req, res, proxyRes);
});
// We pipe to the response unless its expected to be handled by the user
if (!options.selfHandleResponse) {
proxyRes.pipe(res);
}
}
else {
server?.emit("end", req, res, proxyRes);
}
});
}
async function stream2(req, res, options, _, server, cb) {
// Helper function to handle errors consistently throughout the fetch path
const handleError = (err, target) => {
const e = err;
// Copy code from cause if available and missing on err
if (e.code === undefined && e.cause?.code) {
e.code = e.cause.code;
}
if (cb) {
cb(err, req, res, target);
}
else {
server.emit("error", err, req, res, target);
}
};
req.on("error", (err) => {
if (req.socket.destroyed && err.code === "ECONNRESET") {
const target = options.target || options.forward;
if (target) {
server.emit("econnreset", err, req, res, target);
}
return;
}
handleError(err);
});
const customFetch = options.fetch || fetch;
const fetchOptions = options.fetchOptions ?? {};
const prepareRequest = (outgoing) => {
const requestOptions = {
method: outgoing.method,
...fetchOptions.requestOptions,
};
const headers = new Headers(fetchOptions.requestOptions?.headers);
if (!fetchOptions.requestOptions?.headers && outgoing.headers) {
for (const [key, value] of Object.entries(outgoing.headers)) {
if (typeof key === "string") {
if (Array.isArray(value)) {
for (const v of value) {
headers.append(key, v);
}
}
else if (value != null) {
headers.append(key, value);
}
}
}
}
if (options.auth) {
headers.set("authorization", `Basic ${Buffer.from(options.auth).toString("base64")}`);
}
if (options.proxyTimeout) {
requestOptions.signal = AbortSignal.timeout(options.proxyTimeout);
}
requestOptions.headers = headers;
if (options.buffer) {
requestOptions.body = options.buffer;
}
else if (req.method !== "GET" && req.method !== "HEAD") {
requestOptions.body = req;
requestOptions.duplex = "half";
}
return requestOptions;
};
if (options.forward) {
const outgoingOptions = common.setupOutgoing(options.ssl || {}, options, req, "forward");
const requestOptions = prepareRequest(outgoingOptions);
let targetUrl = new URL(outgoingOptions.url).origin + outgoingOptions.path;
if (targetUrl.startsWith("ws")) {
targetUrl = targetUrl.replace("ws", "http");
}
// Call onBeforeRequest callback before making the forward request
if (fetchOptions.onBeforeRequest) {
try {
await fetchOptions.onBeforeRequest(requestOptions, req, res, options);
}
catch (err) {
handleError(err, options.forward);
return;
}
}
try {
const result = await customFetch(targetUrl, requestOptions);
// Call onAfterResponse callback for forward requests (though they typically don't expect responses)
if (fetchOptions.onAfterResponse) {
try {
await fetchOptions.onAfterResponse(result, req, res, options);
}
catch (err) {
handleError(err, options.forward);
return;
}
}
}
catch (err) {
handleError(err, options.forward);
}
if (!options.target) {
return res.end();
}
}
const outgoingOptions = common.setupOutgoing(options.ssl || {}, options, req);
const requestOptions = prepareRequest(outgoingOptions);
let targetUrl = new URL(outgoingOptions.url).origin + outgoingOptions.path;
if (targetUrl.startsWith("ws")) {
targetUrl = targetUrl.replace("ws", "http");
}
// Call onBeforeRequest callback before making the request
if (fetchOptions.onBeforeRequest) {
try {
await fetchOptions.onBeforeRequest(requestOptions, req, res, options);
}
catch (err) {
handleError(err, options.target);
return;
}
}
try {
const response = await customFetch(targetUrl, requestOptions);
// Call onAfterResponse callback after receiving the response
if (fetchOptions.onAfterResponse) {
try {
await fetchOptions.onAfterResponse(response, req, res, options);
}
catch (err) {
handleError(err, options.target);
return;
}
}
// ProxyRes is used in the outgoing passes
// But since only certain properties are used, we can fake it here
// to avoid having to refactor everything.
const fakeProxyRes = {
statusCode: response.status,
statusMessage: response.statusText,
headers: Object.fromEntries(response.headers.entries()),
rawHeaders: Object.entries(response.headers).flatMap(([key, value]) => {
if (Array.isArray(value)) {
return value.flatMap((v) => (v != null ? [key, v] : []));
}
return value != null ? [key, value] : [];
}),
};
server?.emit("proxyRes", fakeProxyRes, req, res);
if (!res.headersSent && !options.selfHandleResponse) {
for (const pass of web_o) {
// note: none of these return anything
pass(req, res, fakeProxyRes, options);
}
}
if (!res.writableEnded) {
// Allow us to listen for when the proxy has completed
const nodeStream = response.body ? node_stream_1.Readable.from(response.body) : null;
if (nodeStream) {
nodeStream.on("error", (err) => {
handleError(err, options.target);
});
nodeStream.on("end", () => {
server?.emit("end", req, res, fakeProxyRes);
});
// We pipe to the response unless its expected to be handled by the user
if (!options.selfHandleResponse) {
nodeStream.pipe(res, { end: true });
}
else {
nodeStream.resume();
}
}
else {
server?.emit("end", req, res, fakeProxyRes);
}
}
else {
server?.emit("end", req, res, fakeProxyRes);
}
}
catch (err) {
handleError(err, options.target);
}
}
exports.WEB_PASSES = { deleteLength, timeout, XHeaders, stream };