Add middleware to capture raw request body for HTTP nodes

This commit is contained in:
Debadutta Panda 2025-02-05 00:50:35 +05:30
parent 57ccd1e15f
commit aaa0467ba0
2 changed files with 87 additions and 62 deletions

View File

@ -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)

View File

@ -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] != "/") {