diff --git a/packages/node_modules/@node-red/nodes/core/network/21-httpin.html b/packages/node_modules/@node-red/nodes/core/network/21-httpin.html index f828077a1..c7724d9b4 100644 --- a/packages/node_modules/@node-red/nodes/core/network/21-httpin.html +++ b/packages/node_modules/@node-red/nodes/core/network/21-httpin.html @@ -25,11 +25,22 @@ -
+ +
- - +
+
+ + +
+
+ + +
+
+ +
@@ -74,6 +85,7 @@ label:RED._("node-red:httpin.label.url")}, method: {value:"get",required:true}, upload: {value:false}, + includeRawBody: {value:false}, swaggerDoc: {type:"swagger-doc", required:false} }, inputs:0, @@ -115,16 +127,22 @@ $('.row-swagger-doc').hide(); } $("#node-input-method").on("change", function() { - if ($(this).val() === "post") { - $(".form-row-http-in-upload").show(); + var method = $(this).val(); + 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 { + $("#form-row-http-in-upload").hide(); + } } else { - $(".form-row-http-in-upload").hide(); + $("#form-row-http-in-rawdata").hide(); + $("#form-row-http-in-upload").hide(); + $("#form-reqBody-http-in-controller").hide(); } }).change(); - - } - }); var headerTypes = [ {value:"content-type",label:"Content-Type",hasValue: false}, 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 22e83b411..1d46a3cb2 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,16 +26,12 @@ module.exports = function(RED) { var mediaTyper = require('media-typer'); var isUtf8 = require('is-utf8'); var hashSum = require("hash-sum"); - - function rawBodyParser(req, res, next) { - if (req.skipRawBodyParser) { next(); } // don't parse this if told to skip - if (req._body) { return next(); } - req.body = ""; - req._body = true; - - var isText = true; - var checkUTF = false; - + + function rawBodyParser(req, _res, next) { + if(!req._nodeRedReqStream) { + return next(); + } + var isText = true, checkUTF = false; if (req.headers['content-type']) { var contentType = typer.parse(req.headers['content-type']) if (contentType.type) { @@ -51,28 +47,29 @@ module.exports = function(RED) { && (parsedType.subtype !== "x-protobuf")) { checkUTF = true; } else { - // application/octet-stream or application/cbor isText = false; } - } } - getBody(req, { + getBody(req._nodeRedReqStream, { 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() } - req.body = buf; - next(); + Object.defineProperty(req, "rawRequestBody", { + value: buf, + enumerable: true + }) + return next(); }); } - var corsSetup = false; - 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. @@ -188,6 +185,7 @@ module.exports = function(RED) { } this.method = n.method; this.upload = n.upload; + this.includeRawBody = n.includeRawBody; this.swaggerDoc = n.swaggerDoc; var node = this; @@ -227,7 +225,9 @@ module.exports = function(RED) { } var maxApiRequestSize = RED.settings.apiMaxLength || '5mb'; + var jsonParser = bodyParser.json({limit:maxApiRequestSize}); + var urlencParser = bodyParser.urlencoded({limit:maxApiRequestSize,extended:true}); var metricsHandler = function(req,res,next) { next(); } @@ -254,25 +254,35 @@ module.exports = function(RED) { var mp = multer({ storage: multer.memoryStorage() }).any(); multipartParser = function(req,res,next) { mp(req,res,function(err) { - req._body = true; next(err); }) }; } 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") { - 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") { - 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") { - 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") { - 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() { + // remove httpInNodeSettings from RED.httpNode + httpInNodes.remove(httpInNodeId) 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]) { @@ -284,9 +294,9 @@ module.exports = function(RED) { this.warn(RED._("httpin.errors.not-created")); } } + RED.nodes.registerType("http in",HTTPIn); - function HTTPOut(n) { RED.nodes.createNode(this,n); var node = this; @@ -361,5 +371,6 @@ module.exports = function(RED) { done(); }); } + RED.nodes.registerType("http response",HTTPOut); } diff --git a/packages/node_modules/@node-red/nodes/locales/de/messages.json b/packages/node_modules/@node-red/nodes/locales/de/messages.json index a51e504cf..332712902 100644 --- a/packages/node_modules/@node-red/nodes/locales/de/messages.json +++ b/packages/node_modules/@node-red/nodes/locales/de/messages.json @@ -451,6 +451,7 @@ "upload": "Dateiuploads akzeptieren", "status": "Statuscode", "headers": "Kopfzeilen", + "rawBody": "Rohdaten einbeziehen?", "other": "andere", "paytoqs": { "ignore": "Ignorieren", diff --git a/packages/node_modules/@node-red/nodes/locales/en-US/messages.json b/packages/node_modules/@node-red/nodes/locales/en-US/messages.json index bc89992e2..76ff2394b 100644 --- a/packages/node_modules/@node-red/nodes/locales/en-US/messages.json +++ b/packages/node_modules/@node-red/nodes/locales/en-US/messages.json @@ -515,6 +515,7 @@ "doc": "Docs", "return": "Return", "upload": "Accept file uploads?", + "rawBody": "Include Raw Data?", "status": "Status code", "headers": "Headers", "other": "other", diff --git a/packages/node_modules/@node-red/nodes/locales/es-ES/messages.json b/packages/node_modules/@node-red/nodes/locales/es-ES/messages.json index b8ac84f1c..56465e451 100644 --- a/packages/node_modules/@node-red/nodes/locales/es-ES/messages.json +++ b/packages/node_modules/@node-red/nodes/locales/es-ES/messages.json @@ -518,6 +518,7 @@ "status": "Status code", "headers": "Headers", "other": "otro", + "rawBody": "¿Incluir datos sin procesar?", "paytoqs": { "ignore": "Ignore", "query": "Append to query-string parameters", diff --git a/packages/node_modules/@node-red/nodes/locales/fr/messages.json b/packages/node_modules/@node-red/nodes/locales/fr/messages.json index 99002f48f..b675efcb5 100644 --- a/packages/node_modules/@node-red/nodes/locales/fr/messages.json +++ b/packages/node_modules/@node-red/nodes/locales/fr/messages.json @@ -518,6 +518,7 @@ "status": "Code d'état", "headers": "En-têtes", "other": "Autre", + "rawBody": "Inclure les données brutes ?", "paytoqs": { "ignore": "Ignorer", "query": "Joindre aux paramètres de chaîne de requête", diff --git a/packages/node_modules/@node-red/nodes/locales/ja/messages.json b/packages/node_modules/@node-red/nodes/locales/ja/messages.json index 8d38ac077..eefdf5762 100644 --- a/packages/node_modules/@node-red/nodes/locales/ja/messages.json +++ b/packages/node_modules/@node-red/nodes/locales/ja/messages.json @@ -518,6 +518,7 @@ "status": "ステータスコード", "headers": "ヘッダ", "other": "その他", + "rawBody": "生データを含める?", "paytoqs": { "ignore": "無視", "query": "クエリパラメータに追加", diff --git a/packages/node_modules/@node-red/nodes/locales/ko/messages.json b/packages/node_modules/@node-red/nodes/locales/ko/messages.json index c82e0f51b..abef1c89c 100644 --- a/packages/node_modules/@node-red/nodes/locales/ko/messages.json +++ b/packages/node_modules/@node-red/nodes/locales/ko/messages.json @@ -397,7 +397,8 @@ "binaryBuffer": "바이너리 버퍼", "jsonObject": "JSON오브젝트", "authType": "종류별", - "bearerToken": "토큰" + "bearerToken": "토큰", + "rawBody": "원시 데이터를 포함할까요?" }, "setby": "- msg.method에 정의 -", "basicauth": "인증을 사용", diff --git a/packages/node_modules/@node-red/nodes/locales/pt-BR/messages.json b/packages/node_modules/@node-red/nodes/locales/pt-BR/messages.json index 51e1fd897..003c484af 100644 --- a/packages/node_modules/@node-red/nodes/locales/pt-BR/messages.json +++ b/packages/node_modules/@node-red/nodes/locales/pt-BR/messages.json @@ -506,6 +506,7 @@ "status": "Código de estado", "headers": "Cabeçalhos", "other": "outro", + "rawBody": "Incluir dados brutos?", "paytoqs" : { "ignore": "Ignorar", "query": "Anexar aos parâmetros da cadeia de caracteres de consulta", diff --git a/packages/node_modules/@node-red/nodes/locales/ru/messages.json b/packages/node_modules/@node-red/nodes/locales/ru/messages.json index 2694ac6a5..49ed92239 100644 --- a/packages/node_modules/@node-red/nodes/locales/ru/messages.json +++ b/packages/node_modules/@node-red/nodes/locales/ru/messages.json @@ -411,6 +411,7 @@ "status": "Код состояния", "headers": "Заголовки", "other": "другое", + "rawBody": "Включить необработанные данные?", "paytoqs": { "ignore": "Игнорировать", "query": "Добавлять к параметрам строки запроса", diff --git a/packages/node_modules/@node-red/nodes/locales/zh-CN/messages.json b/packages/node_modules/@node-red/nodes/locales/zh-CN/messages.json index 7d5616a8f..23b6b07ee 100644 --- a/packages/node_modules/@node-red/nodes/locales/zh-CN/messages.json +++ b/packages/node_modules/@node-red/nodes/locales/zh-CN/messages.json @@ -508,6 +508,7 @@ "status": "状态码", "headers": "头", "other": "其他", + "rawBody": "包含原始数据?", "paytoqs": { "ignore": "忽略", "query": "附加到查询字符串参数", diff --git a/packages/node_modules/@node-red/nodes/locales/zh-TW/messages.json b/packages/node_modules/@node-red/nodes/locales/zh-TW/messages.json index 7d16c5817..2f9f3b560 100644 --- a/packages/node_modules/@node-red/nodes/locales/zh-TW/messages.json +++ b/packages/node_modules/@node-red/nodes/locales/zh-TW/messages.json @@ -416,7 +416,8 @@ "binaryBuffer": "二進制buffer", "jsonObject": "解析的JSON對象", "authType": "類型", - "bearerToken": "Token" + "bearerToken": "Token", + "rawBody": "包含原始數據?" }, "setby": "- 用 msg.method 設定 -", "basicauth": "基本認證", diff --git a/packages/node_modules/node-red/red.js b/packages/node_modules/node-red/red.js index 5f3c9da25..273ef71e2 100755 --- a/packages/node_modules/node-red/red.js +++ b/packages/node_modules/node-red/red.js @@ -47,6 +47,7 @@ var fs = require("fs-extra"); const cors = require('cors'); var RED = require("./lib/red.js"); +var PassThrough = require('stream').PassThrough; var server; var app = express(); @@ -65,6 +66,7 @@ var knownOpts = { "version": Boolean, "define": [String, Array] }; + var shortHands = { "?":["--help"], "p":["--port"], @@ -287,6 +289,63 @@ httpsPromise.then(function(startupHttps) { } 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] != "/") { root = "/" + root;