Add include raw data option in request body.

This commit is contained in:
Debadutta Panda 2025-02-03 14:11:29 +05:30 committed by Debadutta Panda
parent 92a5a28072
commit f5fa05d184
13 changed files with 134 additions and 36 deletions

View File

@ -25,11 +25,22 @@
<option value="patch">PATCH</option> <option value="patch">PATCH</option>
</select> </select>
</div> </div>
<div class="form-row form-row-http-in-upload hide">
<div id="form-reqBody-http-in-controller" class="form-row hide" style="display: flex;">
<label>&nbsp;</label> <label>&nbsp;</label>
<input type="checkbox" id="node-input-upload" style="display: inline-block; width: auto; vertical-align: top;"> <div style="display: flex; margin-left: 5px; flex-direction: column-reverse; gap:35%;">
<label for="node-input-upload" style="width: 70%;" data-i18n="httpin.label.upload"></label> <div id="form-row-http-in-upload" class="hide" style="display: flex;">
<input type="checkbox" id="node-input-upload" style="margin-right: 5%; margin-bottom: 5%;">
<label for="node-input-upload" style="text-wrap: nowrap;" data-i18n="httpin.label.upload"></label>
</div> </div>
<div id="form-row-http-in-rawdata" class="hide" style="display: flex;">
<input type="checkbox" id="node-input-includeRawBody" style="margin-right: 5%; margin-bottom: 5%;">
<label for="node-input-includeRawBody" style="text-wrap: nowrap;" data-i18n="httpin.label.rawBody"></label>
</div>
</div>
</div>
<div class="form-row"> <div class="form-row">
<label for="node-input-url"><i class="fa fa-globe"></i> <span data-i18n="httpin.label.url"></span></label> <label for="node-input-url"><i class="fa fa-globe"></i> <span data-i18n="httpin.label.url"></span></label>
<input id="node-input-url" type="text" placeholder="/url"> <input id="node-input-url" type="text" placeholder="/url">
@ -74,6 +85,7 @@
label:RED._("node-red:httpin.label.url")}, label:RED._("node-red:httpin.label.url")},
method: {value:"get",required:true}, method: {value:"get",required:true},
upload: {value:false}, upload: {value:false},
includeRawBody: {value:false},
swaggerDoc: {type:"swagger-doc", required:false} swaggerDoc: {type:"swagger-doc", required:false}
}, },
inputs:0, inputs:0,
@ -115,16 +127,22 @@
$('.row-swagger-doc').hide(); $('.row-swagger-doc').hide();
} }
$("#node-input-method").on("change", function() { $("#node-input-method").on("change", function() {
if ($(this).val() === "post") { var method = $(this).val();
$(".form-row-http-in-upload").show(); if(["post", "put", "patch","delete"].includes(method)){
$("#form-reqBody-http-in-controller").show();
$("#form-row-http-in-rawdata").show();
if (method === "post") {
$("#form-row-http-in-upload").show();
} else { } else {
$(".form-row-http-in-upload").hide(); $("#form-row-http-in-upload").hide();
}
} else {
$("#form-row-http-in-rawdata").hide();
$("#form-row-http-in-upload").hide();
$("#form-reqBody-http-in-controller").hide();
} }
}).change(); }).change();
} }
}); });
var headerTypes = [ var headerTypes = [
{value:"content-type",label:"Content-Type",hasValue: false}, {value:"content-type",label:"Content-Type",hasValue: false},

View File

@ -27,15 +27,11 @@ module.exports = function(RED) {
var isUtf8 = require('is-utf8'); var isUtf8 = require('is-utf8');
var hashSum = require("hash-sum"); var hashSum = require("hash-sum");
function rawBodyParser(req, res, next) { function rawBodyParser(req, _res, next) {
if (req.skipRawBodyParser) { next(); } // don't parse this if told to skip if(!req._nodeRedReqStream) {
if (req._body) { return next(); } return next();
req.body = ""; }
req._body = true; var isText = true, checkUTF = false;
var isText = true;
var checkUTF = false;
if (req.headers['content-type']) { if (req.headers['content-type']) {
var contentType = typer.parse(req.headers['content-type']) var contentType = typer.parse(req.headers['content-type'])
if (contentType.type) { if (contentType.type) {
@ -51,28 +47,29 @@ module.exports = function(RED) {
&& (parsedType.subtype !== "x-protobuf")) { && (parsedType.subtype !== "x-protobuf")) {
checkUTF = true; checkUTF = true;
} else { } else {
// application/octet-stream or application/cbor
isText = false; isText = false;
} }
} }
} }
getBody(req, { getBody(req._nodeRedReqStream, {
length: req.headers['content-length'], length: req.headers['content-length'],
encoding: isText ? "utf8" : null encoding: isText ? "utf8" : null
}, function (err, buf) { }, function (err, buf) {
if (err) { return next(err); } if (err) {
return next(err);
}
if (!isText && checkUTF && isUtf8(buf)) { if (!isText && checkUTF && isUtf8(buf)) {
buf = buf.toString() buf = buf.toString()
} }
req.body = buf; Object.defineProperty(req, "rawRequestBody", {
next(); value: buf,
enumerable: true
})
return next();
}); });
} }
var corsSetup = false;
function createRequestWrapper(node,req) { function createRequestWrapper(node,req) {
// This misses a bunch of properties (eg headers). Before we use this function // 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. // need to ensure it captures everything documented by Express and HTTP modules.
@ -188,6 +185,7 @@ module.exports = function(RED) {
} }
this.method = n.method; this.method = n.method;
this.upload = n.upload; this.upload = n.upload;
this.includeRawBody = n.includeRawBody;
this.swaggerDoc = n.swaggerDoc; this.swaggerDoc = n.swaggerDoc;
var node = this; var node = this;
@ -227,7 +225,9 @@ module.exports = function(RED) {
} }
var maxApiRequestSize = RED.settings.apiMaxLength || '5mb'; var maxApiRequestSize = RED.settings.apiMaxLength || '5mb';
var jsonParser = bodyParser.json({limit:maxApiRequestSize}); var jsonParser = bodyParser.json({limit:maxApiRequestSize});
var urlencParser = bodyParser.urlencoded({limit:maxApiRequestSize,extended:true}); var urlencParser = bodyParser.urlencoded({limit:maxApiRequestSize,extended:true});
var metricsHandler = function(req,res,next) { next(); } var metricsHandler = function(req,res,next) { next(); }
@ -254,25 +254,35 @@ module.exports = function(RED) {
var mp = multer({ storage: multer.memoryStorage() }).any(); var mp = multer({ storage: multer.memoryStorage() }).any();
multipartParser = function(req,res,next) { multipartParser = function(req,res,next) {
mp(req,res,function(err) { mp(req,res,function(err) {
req._body = true;
next(err); next(err);
}) })
}; };
} }
if (this.method == "get") { if (this.method == "get") {
RED.httpNode.get(this.url,cookieParser(),httpMiddleware,corsHandler,metricsHandler,this.callback,this.errorHandler); RED.httpNode.get(this.url, cookieParser(), httpMiddleware, corsHandler, metricsHandler, this.callback, this.errorHandler);
} else if (this.method == "post") { } else if (this.method == "post") {
RED.httpNode.post(this.url,cookieParser(),httpMiddleware,corsHandler,metricsHandler,jsonParser,urlencParser,multipartParser,rawBodyParser,this.callback,this.errorHandler); RED.httpNode.post(this.url,cookieParser(), httpMiddleware, corsHandler, metricsHandler, jsonParser, urlencParser, multipartParser, rawBodyParser, this.callback, this.errorHandler);
} else if (this.method == "put") { } else if (this.method == "put") {
RED.httpNode.put(this.url,cookieParser(),httpMiddleware,corsHandler,metricsHandler,jsonParser,urlencParser,rawBodyParser,this.callback,this.errorHandler); RED.httpNode.put(this.url, cookieParser(), httpMiddleware, corsHandler, metricsHandler, jsonParser, urlencParser, rawBodyParser, this.callback, this.errorHandler);
} else if (this.method == "patch") { } else if (this.method == "patch") {
RED.httpNode.patch(this.url,cookieParser(),httpMiddleware,corsHandler,metricsHandler,jsonParser,urlencParser,rawBodyParser,this.callback,this.errorHandler); RED.httpNode.patch(this.url, cookieParser(), httpMiddleware, corsHandler, metricsHandler, jsonParser, urlencParser, rawBodyParser, this.callback, this.errorHandler);
} else if (this.method == "delete") { } else if (this.method == "delete") {
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 httpInNodeSettings based on method name and url path
var httpInNodeId = `${this.method.toLowerCase()}:${this.url}`
// get httpInNodeSettings from RED.httpNode
var httpInNodes = RED.httpNode.get('httpInNodes')
// Add httpInNode to RED.httpNode
httpInNodes.set(httpInNodeId, this);
this.on("close",function() { this.on("close",function() {
// remove httpInNodeSettings from RED.httpNode
httpInNodes.remove(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]) {
@ -284,8 +294,8 @@ module.exports = function(RED) {
this.warn(RED._("httpin.errors.not-created")); this.warn(RED._("httpin.errors.not-created"));
} }
} }
RED.nodes.registerType("http in",HTTPIn);
RED.nodes.registerType("http in",HTTPIn);
function HTTPOut(n) { function HTTPOut(n) {
RED.nodes.createNode(this,n); RED.nodes.createNode(this,n);
@ -361,5 +371,6 @@ module.exports = function(RED) {
done(); done();
}); });
} }
RED.nodes.registerType("http response",HTTPOut); RED.nodes.registerType("http response",HTTPOut);
} }

View File

@ -451,6 +451,7 @@
"upload": "Dateiuploads akzeptieren", "upload": "Dateiuploads akzeptieren",
"status": "Statuscode", "status": "Statuscode",
"headers": "Kopfzeilen", "headers": "Kopfzeilen",
"rawBody": "Rohdaten einbeziehen?",
"other": "andere", "other": "andere",
"paytoqs": { "paytoqs": {
"ignore": "Ignorieren", "ignore": "Ignorieren",

View File

@ -515,6 +515,7 @@
"doc": "Docs", "doc": "Docs",
"return": "Return", "return": "Return",
"upload": "Accept file uploads?", "upload": "Accept file uploads?",
"rawBody": "Include Raw Data?",
"status": "Status code", "status": "Status code",
"headers": "Headers", "headers": "Headers",
"other": "other", "other": "other",

View File

@ -518,6 +518,7 @@
"status": "Status code", "status": "Status code",
"headers": "Headers", "headers": "Headers",
"other": "otro", "other": "otro",
"rawBody": "¿Incluir datos sin procesar?",
"paytoqs": { "paytoqs": {
"ignore": "Ignore", "ignore": "Ignore",
"query": "Append to query-string parameters", "query": "Append to query-string parameters",

View File

@ -518,6 +518,7 @@
"status": "Code d'état", "status": "Code d'état",
"headers": "En-têtes", "headers": "En-têtes",
"other": "Autre", "other": "Autre",
"rawBody": "Inclure les données brutes ?",
"paytoqs": { "paytoqs": {
"ignore": "Ignorer", "ignore": "Ignorer",
"query": "Joindre aux paramètres de chaîne de requête", "query": "Joindre aux paramètres de chaîne de requête",

View File

@ -518,6 +518,7 @@
"status": "ステータスコード", "status": "ステータスコード",
"headers": "ヘッダ", "headers": "ヘッダ",
"other": "その他", "other": "その他",
"rawBody": "生データを含める?",
"paytoqs": { "paytoqs": {
"ignore": "無視", "ignore": "無視",
"query": "クエリパラメータに追加", "query": "クエリパラメータに追加",

View File

@ -397,7 +397,8 @@
"binaryBuffer": "바이너리 버퍼", "binaryBuffer": "바이너리 버퍼",
"jsonObject": "JSON오브젝트", "jsonObject": "JSON오브젝트",
"authType": "종류별", "authType": "종류별",
"bearerToken": "토큰" "bearerToken": "토큰",
"rawBody": "원시 데이터를 포함할까요?"
}, },
"setby": "- msg.method에 정의 -", "setby": "- msg.method에 정의 -",
"basicauth": "인증을 사용", "basicauth": "인증을 사용",

View File

@ -506,6 +506,7 @@
"status": "Código de estado", "status": "Código de estado",
"headers": "Cabeçalhos", "headers": "Cabeçalhos",
"other": "outro", "other": "outro",
"rawBody": "Incluir dados brutos?",
"paytoqs" : { "paytoqs" : {
"ignore": "Ignorar", "ignore": "Ignorar",
"query": "Anexar aos parâmetros da cadeia de caracteres de consulta", "query": "Anexar aos parâmetros da cadeia de caracteres de consulta",

View File

@ -411,6 +411,7 @@
"status": "Код состояния", "status": "Код состояния",
"headers": "Заголовки", "headers": "Заголовки",
"other": "другое", "other": "другое",
"rawBody": "Включить необработанные данные?",
"paytoqs": { "paytoqs": {
"ignore": "Игнорировать", "ignore": "Игнорировать",
"query": "Добавлять к параметрам строки запроса", "query": "Добавлять к параметрам строки запроса",

View File

@ -508,6 +508,7 @@
"status": "状态码", "status": "状态码",
"headers": "头", "headers": "头",
"other": "其他", "other": "其他",
"rawBody": "包含原始数据?",
"paytoqs": { "paytoqs": {
"ignore": "忽略", "ignore": "忽略",
"query": "附加到查询字符串参数", "query": "附加到查询字符串参数",

View File

@ -416,7 +416,8 @@
"binaryBuffer": "二進制buffer", "binaryBuffer": "二進制buffer",
"jsonObject": "解析的JSON對象", "jsonObject": "解析的JSON對象",
"authType": "類型", "authType": "類型",
"bearerToken": "Token" "bearerToken": "Token",
"rawBody": "包含原始數據?"
}, },
"setby": "- 用 msg.method 設定 -", "setby": "- 用 msg.method 設定 -",
"basicauth": "基本認證", "basicauth": "基本認證",

View File

@ -47,6 +47,7 @@ var fs = require("fs-extra");
const cors = require('cors'); const cors = require('cors');
var RED = require("./lib/red.js"); var RED = require("./lib/red.js");
var PassThrough = require('stream').PassThrough;
var server; var server;
var app = express(); var app = express();
@ -65,6 +66,7 @@ var knownOpts = {
"version": Boolean, "version": Boolean,
"define": [String, Array] "define": [String, Array]
}; };
var shortHands = { var shortHands = {
"?":["--help"], "?":["--help"],
"p":["--port"], "p":["--port"],
@ -287,6 +289,63 @@ httpsPromise.then(function(startupHttps) {
} }
server.setMaxListeners(0); 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) { function formatRoot(root) {
if (root[0] != "/") { if (root[0] != "/") {
root = "/" + root; root = "/" + root;