mirror of
				https://github.com/node-red/node-red.git
				synced 2025-03-01 10:36:34 +00:00 
			
		
		
		
	Refactor raw body capture middleware and improve route key generation for HTTP nodes
This commit is contained in:
		| @@ -27,9 +27,20 @@ module.exports = function(RED) { | |||||||
|     var isUtf8 = require('is-utf8'); |     var isUtf8 = require('is-utf8'); | ||||||
|     var hashSum = require("hash-sum"); |     var hashSum = require("hash-sum"); | ||||||
|     var PassThrough = require('stream').PassThrough |     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) { |     function rawBodyParser(req, _res, next) { | ||||||
|         if(!req._nodeRedReqStream) { |         if (!req._nodeRedReqStream) { | ||||||
|             return next(); |             return next(); | ||||||
|         } |         } | ||||||
|         var isText = true, checkUTF = false; |         var isText = true, checkUTF = false; | ||||||
| @@ -58,7 +69,7 @@ module.exports = function(RED) { | |||||||
|             encoding: isText ? "utf8" : null |             encoding: isText ? "utf8" : null | ||||||
|         }, function (err, buf) { |         }, function (err, buf) { | ||||||
|             if (err) { |             if (err) { | ||||||
|                 return next(err);  |                 return next(err) | ||||||
|             } |             } | ||||||
|             if (!isText && checkUTF && isUtf8(buf)) { |             if (!isText && checkUTF && isUtf8(buf)) { | ||||||
|                 buf = buf.toString() |                 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) { |     function getRootApp(app) { | ||||||
|         if (typeof app.parent === 'undefined') { |         if (typeof app.parent === 'undefined') { | ||||||
|             return app; |             return app; | ||||||
| @@ -78,87 +94,87 @@ module.exports = function(RED) { | |||||||
|         return getRootApp(app.parent) |         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}`; |         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 |             // If raw body inclusion is disabled, skip the processing | ||||||
|     var isMiddlewarePresent = rootApp._router.stack.some(layer => layer.name === 'setupRawBodyCapture') |             if (!httpInNode.includeRawBody) { | ||||||
|  |  | ||||||
|     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 |  | ||||||
|                 return next(); |                 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 |             // Proceed to the next middleware if no settings found | ||||||
|             return next(); |             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 |         // Add middleware to the stack | ||||||
|         rootApp.use(setupRawBodyCapture) |         rootApp.use(setupRawBodyCapture) | ||||||
|         // Move the router to top of the stack  |         // Move the router to top of the stack  | ||||||
| @@ -367,18 +383,14 @@ module.exports = function(RED) { | |||||||
|                 RED.httpNode.delete(this.url, cookieParser(), httpMiddleware, corsHandler, metricsHandler, jsonParser, urlencParser, rawBodyParser, this.callback, this.errorHandler); |                 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 routeKey = getRouteKey({method: this.method, url: RED.httpNode.path() + this.url}) | ||||||
|             var httpInNodeId = getRouteId({url: this.url, method: this.method}) |  | ||||||
|              |              | ||||||
|             // get httpInNode from RED.httpNode |  | ||||||
|             var httpInNodes = rootApp.get('httpInNodes') |             var httpInNodes = rootApp.get('httpInNodes') | ||||||
|  |  | ||||||
|             // Add httpInNode to RED.httpNode |             httpInNodes.set(routeKey, this); | ||||||
|             httpInNodes.set(httpInNodeId, this); |  | ||||||
|  |  | ||||||
|             this.on("close",function() { |             this.on("close",function() { | ||||||
|                 // remove httpInNodeSettings from RED.httpNode |                 httpInNodes.delete(routeKey) | ||||||
|                 httpInNodes.delete(httpInNodeId) |  | ||||||
|                 var node = this; |                 var node = this; | ||||||
|                 RED.httpNode._router.stack.forEach(function(route,i,routes) { |                 RED.httpNode._router.stack.forEach(function(route,i,routes) { | ||||||
|                     if (route.route && route.route.path === node.url && route.route.methods[node.method]) { |                     if (route.route && route.route.path === node.url && route.route.methods[node.method]) { | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user