From aaa0467ba0282ee79291f90308ff0eeaf52d652b Mon Sep 17 00:00:00 2001 From: Debadutta Panda Date: Wed, 5 Feb 2025 00:50:35 +0530 Subject: [PATCH] Add middleware to capture raw request body for HTTP nodes --- .../@node-red/nodes/core/network/21-httpin.js | 91 ++++++++++++++++++- packages/node_modules/node-red/red.js | 58 +----------- 2 files changed, 87 insertions(+), 62 deletions(-) diff --git a/packages/node_modules/@node-red/nodes/core/network/21-httpin.js b/packages/node_modules/@node-red/nodes/core/network/21-httpin.js index 1d46a3cb2..1c67b3bc7 100644 --- a/packages/node_modules/@node-red/nodes/core/network/21-httpin.js +++ b/packages/node_modules/@node-red/nodes/core/network/21-httpin.js @@ -26,6 +26,7 @@ module.exports = function(RED) { var mediaTyper = require('media-typer'); var isUtf8 = require('is-utf8'); var hashSum = require("hash-sum"); + var PassThrough = require('stream').PassThrough function rawBodyParser(req, _res, next) { if(!req._nodeRedReqStream) { @@ -70,6 +71,85 @@ module.exports = function(RED) { }); } + function getRootRouter(app) { + if (typeof app.parent === 'undefined') { + return app; + } + return getRootRouter(app.parent) + } + + function createRouteId(req) { + var method = req.method.toLowerCase(), url = req.url; + return `${method}:${url}`; + } + + var rootApp = getRootRouter(RED.httpNode) + + // Check if middleware already exists + var isMiddlewarePresent = rootApp._router.stack.some(layer => layer.name === 'setupRawBodyCapture') + + if (!isMiddlewarePresent) { + // Initialize HTTP node settings storage + var httpInNodes = new Map() + rootApp.set('httpInNodes', httpInNodes); + + // This middleware must always be at the root to handle the raw request body correctly + function setupRawBodyCapture(req, _res, next) { + var httpInNodeId = createRouteId(req) + + // Check if settings for this ID exist + if (httpInNodes.has(httpInNodeId)) { + var httpInNode = httpInNodes.get(httpInNodeId); + // If raw body inclusion is disabled, skip the processing + if (!httpInNode.includeRawBody) { + return next(); + } + // Create a PassThrough stream to capture the request body + var passThrough = new PassThrough(); + + // Function to handle 'data' event + function onData(chunk) { + passThrough.write(chunk); + } + + // Function to handle 'end' or 'error' events + function onEnd(err) { + if (err) { + passThrough.destroy(err); + } else { + passThrough.end(); + } + } + + // Attach event listeners to the request stream + req.on('data', onData); + req.on('end', onEnd); + req.on('error', onEnd); + + // Remove listeners once the request is closed + req.once('close', function () { + req.removeListener('data', onData); + req.removeListener('end', onEnd); + req.removeListener('error', onEnd); + }); + + // Attach the passThrough stream to the request + Object.defineProperty(req, "_nodeRedReqStream", { + value: passThrough + }); + + return next(); + } + + // Proceed to the next middleware if no settings found + return next(); + } + // Add middleware to the stack + rootApp.use(setupRawBodyCapture) + // Move the router to top of the stack + rootApp._router.stack.unshift(rootApp._router.stack.pop()) + } + function createRequestWrapper(node,req) { // This misses a bunch of properties (eg headers). Before we use this function // need to ensure it captures everything documented by Express and HTTP modules. @@ -122,6 +202,7 @@ module.exports = function(RED) { return wrapper; } + function createResponseWrapper(node,res) { var wrapper = { _res: res @@ -270,16 +351,16 @@ module.exports = function(RED) { } else if (this.method == "delete") { RED.httpNode.delete(this.url, cookieParser(), httpMiddleware, corsHandler, metricsHandler, jsonParser, urlencParser, rawBodyParser, this.callback, this.errorHandler); } - - // unique id for httpInNodeSettings based on method name and url path - var httpInNodeId = `${this.method.toLowerCase()}:${this.url}` + // unique id for httpInNodeSettings based on method name and url path + var httpInNodeId = createRouteId({url: this.url, method: this.method}) + // get httpInNodeSettings from RED.httpNode - var httpInNodes = RED.httpNode.get('httpInNodes') + var httpInNodes = rootApp.get('httpInNodes') // Add httpInNode to RED.httpNode httpInNodes.set(httpInNodeId, this); - + this.on("close",function() { // remove httpInNodeSettings from RED.httpNode httpInNodes.remove(httpInNodeId) diff --git a/packages/node_modules/node-red/red.js b/packages/node_modules/node-red/red.js index 273ef71e2..8e9c2639b 100755 --- a/packages/node_modules/node-red/red.js +++ b/packages/node_modules/node-red/red.js @@ -287,64 +287,8 @@ httpsPromise.then(function(startupHttps) { } else { server = http.createServer(function(req,res) {app(req,res);}); } + server.setMaxListeners(0); - - var httpInNodes = new Map() - - app.set('httpInNodes', httpInNodes); - - // This middleware must always be at the root to handle the raw request body correctly. - app.use(function(req, _res, next) { - var method = req.method.toLowerCase(), url = req.url; - var httpInNodeId = `${method}:${url}` - - // Check if settings for this ID exist - if (httpInNodes.has(httpInNodeId)) { - var httpInNode = httpInNodes.get(httpInNodeId); - // If raw body inclusion is disabled, skip the processing - if (!httpInNode.includeRawBody) { - return next(); - } - // Create a PassThrough stream to capture the request body - const passThrough = new PassThrough(); - - // Function to handle 'data' event - function onData(chunk) { - passThrough.write(chunk); - } - - // Function to handle 'end' or 'error' events - function onEnd(err) { - if (err) { - passThrough.destroy(err); - } else { - passThrough.end(); - } - } - - // Attach event listeners to the request stream - req.on('data', onData); - req.on('end', onEnd); - req.on('error', onEnd); - - // Remove listeners once the request is closed - req.once('close', function() { - req.removeListener('data', onData); - req.removeListener('end', onEnd); - req.removeListener('error', onEnd); - }); - - // Attach the passThrough stream to the request - Object.defineProperty(req, "_nodeRedReqStream", { - value: passThrough - }); - - return next(); - } - - // Proceed to the next middleware if no settings found - return next(); - }); function formatRoot(root) { if (root[0] != "/") {