From 9a26ee30522893a6278462d3069e89733c3c97c0 Mon Sep 17 00:00:00 2001 From: Debadutta Panda Date: Sun, 9 Feb 2025 13:26:05 +0530 Subject: [PATCH] Refactor raw body capture middleware and improve route key generation for HTTP nodes --- .../@node-red/nodes/core/network/21-httpin.js | 180 ++++++++++-------- 1 file changed, 96 insertions(+), 84 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 e8f99d2dc..81d1f4e9b 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 @@ -27,9 +27,20 @@ module.exports = function(RED) { var isUtf8 = require('is-utf8'); var hashSum = require("hash-sum"); var PassThrough = require('stream').PassThrough - + var rootApp = getRootApp(RED.httpNode) + + // Check if middleware already exists + var isMiddlewareExists = rootApp._router.stack.some(layer => layer.name === 'setupRawBodyCapture') + + /** + * This middleware parses the raw body if the user enables it. + * @param {import('express').Request} req + * @param {import('express').Response} _res + * @param {import('express').NextFunction} next + * @returns + */ function rawBodyParser(req, _res, next) { - if(!req._nodeRedReqStream) { + if (!req._nodeRedReqStream) { return next(); } var isText = true, checkUTF = false; @@ -57,8 +68,8 @@ module.exports = function(RED) { length: req.headers['content-length'], encoding: isText ? "utf8" : null }, function (err, buf) { - if (err) { - return next(err); + if (err) { + return next(err) } if (!isText && checkUTF && isUtf8(buf)) { buf = buf.toString() @@ -71,6 +82,11 @@ module.exports = function(RED) { }); } + /** + * This method retrieves the root app by traversing the parent hierarchy. + * @param {import('express').Application} app + * @returns {import('express').Application} + */ function getRootApp(app) { if (typeof app.parent === 'undefined') { return app; @@ -78,87 +94,87 @@ module.exports = function(RED) { return getRootApp(app.parent) } - function getRouteId(req) { - var method = req.method.toLowerCase(), url = req.url; + /** + * It provide the unique route key + * @param {{method: string, url: string}} obj + * @returns + */ + function getRouteKey(obj) { + var method = obj.method.toUpperCase() + // Normalize the URL by replacing double slashes with a single slash and removing the trailing slash + var url = obj.url.replace(/\/{2,}/g, '/').replace(/\/$/, '') return `${method}:${url}`; } - var rootApp = getRootApp(RED.httpNode) + /** + * This middleware is for clone the request stream + * @param {import('express').Request} req + * @param {import('express').Response} _res + * @param {import('express').NextFunction} next + * @returns + */ + function setupRawBodyCapture(req, _res, next) { + var routeKey = getRouteKey({method: req.method, url: req._parsedUrl.pathname}) + var httpInNodes = rootApp.get('httpInNodes') + // Check if settings for this ID exist + if (httpInNodes.has(routeKey)) { + // Get the httpInNode by routeId + var httpInNode = httpInNodes.get(routeKey); - // 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 - * @param {import('express').Request} req - * @param {import('express').Response} _res - * @param {import('express').NextFunction} next - * @returns - */ - function setupRawBodyCapture(req, _res, next) { - var httpInNodeId = getRouteId(req) - - // Check if settings for this ID exist - if (httpInNodes.has(httpInNodeId)) { - // Get the httpInNode by routeId - 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 cloneStream = new PassThrough(); - - // Function to handle 'data' event - function onData(chunk) { - // Safely call clone stream write - if (cloneStream.writable) { - cloneStream.write(chunk); - } - } - - // Function to handle 'end' or 'error' events - function onEnd(err) { - if (err) { - // Safely call clone stream destroy method - if (!cloneStream.destroyed) { - cloneStream.destroy(err); - } - } else { - // Safely call clone stream end method - if(cloneStream.writable) { - cloneStream.end(); - } - } - } - - // Attach event listeners to the request stream - req.on('data', onData) - .once('end', onEnd) - .once('error', onEnd) - // Remove listeners once the request is closed - .once('close', () => { - req.removeListener('data', onData); - }) - - // Attach the clone stream to the request - Object.defineProperty(req, "_nodeRedReqStream", { - value: cloneStream - }); - // Proceed to the next middleware if no settings found + // If raw body inclusion is disabled, skip the processing + if (!httpInNode.includeRawBody) { return next(); } + + // Create a PassThrough stream to capture the request body + var cloneStream = new PassThrough(); + + // Function to handle 'data' event + function onData(chunk) { + // Safely call clone stream write + if (cloneStream.writable) { + cloneStream.write(chunk); + } + } + + // Function to handle 'end' or 'error' events + function onEnd(err) { + if (err) { + // Safely call clone stream destroy method + if (!cloneStream.destroyed) { + cloneStream.destroy(err); + } + } else { + // Safely call clone stream end method + if (cloneStream.writable) { + cloneStream.end(); + } + } + } + + // Attach event listeners to the request stream + req.on('data', onData) + .once('end', onEnd) + .once('error', onEnd) + // Remove listeners once the request is closed + .once('close', () => { + req.removeListener('data', onData); + }) + + // Attach the clone stream to the request + Object.defineProperty(req, "_nodeRedReqStream", { + value: cloneStream + }); // Proceed to the next middleware if no settings found return next(); } + // Proceed to the next middleware if no settings found + return next(); + } + + if (!isMiddlewareExists) { + // Initialize HTTP node storage + rootApp.set('httpInNodes', new Map()); // Add middleware to the stack rootApp.use(setupRawBodyCapture) // Move the router to top of the stack @@ -366,19 +382,15 @@ 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 httpInNode based on method name and url path - var httpInNodeId = getRouteId({url: this.url, method: this.method}) - // get httpInNode from RED.httpNode + var routeKey = getRouteKey({method: this.method, url: RED.httpNode.path() + this.url}) + var httpInNodes = rootApp.get('httpInNodes') - // Add httpInNode to RED.httpNode - httpInNodes.set(httpInNodeId, this); + httpInNodes.set(routeKey, this); this.on("close",function() { - // remove httpInNodeSettings from RED.httpNode - httpInNodes.delete(httpInNodeId) + httpInNodes.delete(routeKey) var node = this; RED.httpNode._router.stack.forEach(function(route,i,routes) { if (route.route && route.route.path === node.url && route.route.methods[node.method]) {