@inteli.city/node-red-contrib-http-plus 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/README.md +516 -0
- package/http-auth-config+.html +167 -0
- package/http-auth-config+.js +29 -0
- package/httpin+.html +494 -0
- package/httpin+.js +626 -0
- package/httpproxy+.html +140 -0
- package/httpproxy+.js +138 -0
- package/httprequest+.html +233 -0
- package/httprequest+.js +311 -0
- package/libs/swagger.js +213 -0
- package/package.json +37 -0
package/httpin+.js
ADDED
|
@@ -0,0 +1,626 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright JS Foundation and other contributors, http://js.foundation
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
**/
|
|
16
|
+
|
|
17
|
+
var swagger = require("./libs/swagger");
|
|
18
|
+
var swaggerInitialized = false;
|
|
19
|
+
|
|
20
|
+
module.exports = function(RED) {
|
|
21
|
+
"use strict";
|
|
22
|
+
|
|
23
|
+
if (!swaggerInitialized) {
|
|
24
|
+
swagger.registerRoutes(RED);
|
|
25
|
+
swaggerInitialized = true;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
var bodyParser = require("body-parser");
|
|
29
|
+
var multer = require("multer");
|
|
30
|
+
var cookieParser = require("cookie-parser");
|
|
31
|
+
var getBody = require('raw-body');
|
|
32
|
+
var cors = require('cors');
|
|
33
|
+
var onHeaders = require('on-headers');
|
|
34
|
+
var typer = require('content-type');
|
|
35
|
+
var mediaTyper = require('media-typer');
|
|
36
|
+
var isUtf8 = require('is-utf8');
|
|
37
|
+
var hashSum = require("hash-sum");
|
|
38
|
+
var jwt = require('jsonwebtoken');
|
|
39
|
+
var jwksRsa = require('jwks-rsa');
|
|
40
|
+
var { z } = require('zod');
|
|
41
|
+
var os = require('os');
|
|
42
|
+
|
|
43
|
+
function rawBodyParser(req, res, next) {
|
|
44
|
+
if (req.skipRawBodyParser) { next(); } // don't parse this if told to skip
|
|
45
|
+
if (req._body) { return next(); }
|
|
46
|
+
req.body = "";
|
|
47
|
+
req._body = true;
|
|
48
|
+
|
|
49
|
+
var isText = true;
|
|
50
|
+
var checkUTF = false;
|
|
51
|
+
|
|
52
|
+
if (req.headers['content-type']) {
|
|
53
|
+
var contentType = typer.parse(req.headers['content-type'])
|
|
54
|
+
if (contentType.type) {
|
|
55
|
+
var parsedType = mediaTyper.parse(contentType.type);
|
|
56
|
+
if (parsedType.type === "text") {
|
|
57
|
+
isText = true;
|
|
58
|
+
} else if (parsedType.subtype === "xml" || parsedType.suffix === "xml") {
|
|
59
|
+
isText = true;
|
|
60
|
+
} else if (parsedType.type !== "application") {
|
|
61
|
+
isText = false;
|
|
62
|
+
} else if ((parsedType.subtype !== "octet-stream")
|
|
63
|
+
&& (parsedType.subtype !== "cbor")
|
|
64
|
+
&& (parsedType.subtype !== "x-protobuf")) {
|
|
65
|
+
checkUTF = true;
|
|
66
|
+
} else {
|
|
67
|
+
// application/octet-stream or application/cbor
|
|
68
|
+
isText = false;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
getBody(req, {
|
|
75
|
+
length: req.headers['content-length'],
|
|
76
|
+
encoding: isText ? "utf8" : null
|
|
77
|
+
}, function (err, buf) {
|
|
78
|
+
if (err) { return next(err); }
|
|
79
|
+
if (!isText && checkUTF && isUtf8(buf)) {
|
|
80
|
+
buf = buf.toString()
|
|
81
|
+
}
|
|
82
|
+
req.body = buf;
|
|
83
|
+
next();
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
var corsSetup = false;
|
|
88
|
+
|
|
89
|
+
function createRequestWrapper(node,req) {
|
|
90
|
+
// This misses a bunch of properties (eg headers). Before we use this function
|
|
91
|
+
// need to ensure it captures everything documented by Express and HTTP modules.
|
|
92
|
+
var wrapper = {
|
|
93
|
+
_req: req
|
|
94
|
+
};
|
|
95
|
+
var toWrap = [
|
|
96
|
+
"param",
|
|
97
|
+
"get",
|
|
98
|
+
"is",
|
|
99
|
+
"acceptsCharset",
|
|
100
|
+
"acceptsLanguage",
|
|
101
|
+
"app",
|
|
102
|
+
"baseUrl",
|
|
103
|
+
"body",
|
|
104
|
+
"cookies",
|
|
105
|
+
"fresh",
|
|
106
|
+
"hostname",
|
|
107
|
+
"ip",
|
|
108
|
+
"ips",
|
|
109
|
+
"originalUrl",
|
|
110
|
+
"params",
|
|
111
|
+
"path",
|
|
112
|
+
"protocol",
|
|
113
|
+
"query",
|
|
114
|
+
"route",
|
|
115
|
+
"secure",
|
|
116
|
+
"signedCookies",
|
|
117
|
+
"stale",
|
|
118
|
+
"subdomains",
|
|
119
|
+
"xhr",
|
|
120
|
+
"socket" // TODO: tidy this up
|
|
121
|
+
];
|
|
122
|
+
toWrap.forEach(function(f) {
|
|
123
|
+
if (typeof req[f] === "function") {
|
|
124
|
+
wrapper[f] = function() {
|
|
125
|
+
node.warn(RED._("httpin.errors.deprecated-call",{method:"msg.req."+f}));
|
|
126
|
+
var result = req[f].apply(req,arguments);
|
|
127
|
+
if (result === req) {
|
|
128
|
+
return wrapper;
|
|
129
|
+
} else {
|
|
130
|
+
return result;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
} else {
|
|
134
|
+
wrapper[f] = req[f];
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
return wrapper;
|
|
140
|
+
}
|
|
141
|
+
function createResponseWrapper(node,res) {
|
|
142
|
+
var wrapper = {
|
|
143
|
+
_res: res
|
|
144
|
+
};
|
|
145
|
+
var toWrap = [
|
|
146
|
+
"append",
|
|
147
|
+
"attachment",
|
|
148
|
+
"cookie",
|
|
149
|
+
"clearCookie",
|
|
150
|
+
"download",
|
|
151
|
+
"end",
|
|
152
|
+
"format",
|
|
153
|
+
"get",
|
|
154
|
+
"json",
|
|
155
|
+
"jsonp",
|
|
156
|
+
"links",
|
|
157
|
+
"location",
|
|
158
|
+
"redirect",
|
|
159
|
+
"render",
|
|
160
|
+
"send",
|
|
161
|
+
"sendfile",
|
|
162
|
+
"sendFile",
|
|
163
|
+
"sendStatus",
|
|
164
|
+
"set",
|
|
165
|
+
"status",
|
|
166
|
+
"type",
|
|
167
|
+
"vary"
|
|
168
|
+
];
|
|
169
|
+
toWrap.forEach(function(f) {
|
|
170
|
+
wrapper[f] = function() {
|
|
171
|
+
node.warn(RED._("httpin.errors.deprecated-call",{method:"msg.res."+f}));
|
|
172
|
+
var result = res[f].apply(res,arguments);
|
|
173
|
+
if (result === res) {
|
|
174
|
+
return wrapper;
|
|
175
|
+
} else {
|
|
176
|
+
return result;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
return wrapper;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
var corsHandler = function(req,res,next) { next(); }
|
|
184
|
+
|
|
185
|
+
if (RED.settings.httpNodeCors) {
|
|
186
|
+
corsHandler = cors(RED.settings.httpNodeCors);
|
|
187
|
+
RED.httpNode.options("*",corsHandler);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function HTTPIn(n) {
|
|
191
|
+
RED.nodes.createNode(this,n);
|
|
192
|
+
|
|
193
|
+
// Zod schema compilation (once at deploy time)
|
|
194
|
+
var compiledSchema = null;
|
|
195
|
+
var schemaText = (n.zodSchema || "").trim();
|
|
196
|
+
if (n.enableZod === true && schemaText) {
|
|
197
|
+
try {
|
|
198
|
+
compiledSchema = eval(schemaText); // z is in scope
|
|
199
|
+
} catch(err) {
|
|
200
|
+
this.error("http.in+: invalid Zod schema — " + err.message);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (compiledSchema && n.method === "get") {
|
|
205
|
+
try {
|
|
206
|
+
var _def = compiledSchema._def;
|
|
207
|
+
if (_def && _def.typeName === "ZodObject") {
|
|
208
|
+
var _shape = typeof _def.shape === "function" ? _def.shape() : _def.shape;
|
|
209
|
+
if (_shape.body) {
|
|
210
|
+
this.warn(
|
|
211
|
+
"http.in+: GET endpoint defines a body schema. " +
|
|
212
|
+
"Use 'query' instead. Body will be ignored."
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
} catch(e) {
|
|
217
|
+
// ignore
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (compiledSchema) {
|
|
222
|
+
swagger.registerEndpoint({
|
|
223
|
+
nodeId: this.id,
|
|
224
|
+
method: n.method,
|
|
225
|
+
path: n.url && n.url[0] !== '/' ? '/' + n.url : n.url,
|
|
226
|
+
schema: compiledSchema,
|
|
227
|
+
rawSchemaText: schemaText
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Load auth config node (may be null if not configured)
|
|
232
|
+
var authConfig = RED.nodes.getNode(n.authConfig);
|
|
233
|
+
|
|
234
|
+
// Create JWKS client once per node instance (built-in key caching)
|
|
235
|
+
var jwksClient = null;
|
|
236
|
+
if (authConfig && authConfig.authType === 'cognito' && authConfig.jwksUrl) {
|
|
237
|
+
jwksClient = jwksRsa({
|
|
238
|
+
jwksUri: authConfig.jwksUrl,
|
|
239
|
+
cache: true,
|
|
240
|
+
cacheMaxEntries: 5,
|
|
241
|
+
cacheMaxAge: 3600000 // 1 hour
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (RED.settings.httpNodeRoot !== false) {
|
|
246
|
+
|
|
247
|
+
if (!n.url) {
|
|
248
|
+
this.warn(RED._("httpin.errors.missing-path"));
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
this.url = n.url;
|
|
252
|
+
if (this.url[0] !== '/') {
|
|
253
|
+
this.url = '/'+this.url;
|
|
254
|
+
}
|
|
255
|
+
this.method = n.method;
|
|
256
|
+
this.swaggerDoc = n.swaggerDoc;
|
|
257
|
+
this.enableUpload = n.enableUpload;
|
|
258
|
+
this.uploadStorage = n.uploadStorage || 'memory';
|
|
259
|
+
this.maxFileSize = n.maxFileSize || 5;
|
|
260
|
+
this.uploadDir = n.uploadDir || '';
|
|
261
|
+
|
|
262
|
+
var node = this;
|
|
263
|
+
|
|
264
|
+
this.errorHandler = function(err,req,res,next) {
|
|
265
|
+
node.warn(err);
|
|
266
|
+
res.sendStatus(500);
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
this.callback = function(req,res) {
|
|
270
|
+
// Dispatch msg downstream after successful auth
|
|
271
|
+
function dispatchMsg(user) {
|
|
272
|
+
var msgid = RED.util.generateId();
|
|
273
|
+
res._msgid = msgid;
|
|
274
|
+
// Since Node 15, req.headers are lazily computed and the property
|
|
275
|
+
// marked as non-enumerable.
|
|
276
|
+
// That means it doesn't show up in the Debug sidebar.
|
|
277
|
+
// This redefines the property causing it to be evaluated *and*
|
|
278
|
+
// marked as enumerable again.
|
|
279
|
+
Object.defineProperty(req, 'headers', {
|
|
280
|
+
value: req.headers,
|
|
281
|
+
enumerable: true
|
|
282
|
+
});
|
|
283
|
+
var msg = {_msgid:msgid, req:req, res:createResponseWrapper(node,res)};
|
|
284
|
+
if (user !== undefined) { msg.user = user; }
|
|
285
|
+
if (node.method.match(/^(post|delete|put|options|patch)$/)) {
|
|
286
|
+
msg.payload = req.body;
|
|
287
|
+
} else if (node.method == "get") {
|
|
288
|
+
msg.payload = req.query;
|
|
289
|
+
}
|
|
290
|
+
if (req._uploadedFiles) { msg.files = req._uploadedFiles; }
|
|
291
|
+
|
|
292
|
+
// ── Zod validation ─────────────────────────────────────
|
|
293
|
+
if (compiledSchema) {
|
|
294
|
+
var validationInput = {
|
|
295
|
+
body: msg.payload,
|
|
296
|
+
query: req.query,
|
|
297
|
+
params: req.params,
|
|
298
|
+
files: msg.files
|
|
299
|
+
};
|
|
300
|
+
try {
|
|
301
|
+
var parsed = compiledSchema.parse(validationInput);
|
|
302
|
+
msg.validated = parsed;
|
|
303
|
+
} catch(validationErr) {
|
|
304
|
+
res.status(400).json({
|
|
305
|
+
error: "invalid_request",
|
|
306
|
+
details: validationErr.errors
|
|
307
|
+
});
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
node.send(msg);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// ── No auth config or authType "none" ──────────────────────
|
|
316
|
+
if (!authConfig || authConfig.authType === 'none') {
|
|
317
|
+
return dispatchMsg();
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// ── Basic Auth ─────────────────────────────────────────────
|
|
321
|
+
if (authConfig.authType === 'basic') {
|
|
322
|
+
var basicHeader = req.headers['authorization'] || '';
|
|
323
|
+
if (!basicHeader.startsWith('Basic ')) {
|
|
324
|
+
res.set('WWW-Authenticate', 'Basic realm="Restricted"');
|
|
325
|
+
return res.status(401).json({error: "unauthorized"});
|
|
326
|
+
}
|
|
327
|
+
var decoded = Buffer.from(basicHeader.slice(6), 'base64').toString('utf8');
|
|
328
|
+
var colonIdx = decoded.indexOf(':');
|
|
329
|
+
var reqUser = colonIdx >= 0 ? decoded.slice(0, colonIdx) : decoded;
|
|
330
|
+
var reqPass = colonIdx >= 0 ? decoded.slice(colonIdx + 1) : '';
|
|
331
|
+
var authed = false;
|
|
332
|
+
if (authConfig.useJsonUsers) {
|
|
333
|
+
try {
|
|
334
|
+
var users = JSON.parse(authConfig.usersJson || '{}');
|
|
335
|
+
authed = Object.prototype.hasOwnProperty.call(users, reqUser) &&
|
|
336
|
+
users[reqUser] === reqPass;
|
|
337
|
+
} catch(e) {
|
|
338
|
+
node.error('http.in+: invalid usersJson — ' + e.message);
|
|
339
|
+
}
|
|
340
|
+
} else {
|
|
341
|
+
var creds = authConfig.credentials || {};
|
|
342
|
+
authed = reqUser === creds.username && reqPass === creds.password;
|
|
343
|
+
}
|
|
344
|
+
if (authed) {
|
|
345
|
+
if (req.headers && req.headers.authorization) {
|
|
346
|
+
delete req.headers.authorization;
|
|
347
|
+
}
|
|
348
|
+
return dispatchMsg(reqUser);
|
|
349
|
+
}
|
|
350
|
+
res.set('WWW-Authenticate', 'Basic realm="Restricted"');
|
|
351
|
+
return res.status(401).json({error: "unauthorized"});
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// ── Cognito JWT ────────────────────────────────────────────
|
|
355
|
+
if (authConfig.authType === 'cognito') {
|
|
356
|
+
if (!jwksClient) {
|
|
357
|
+
node.warn('http.in+: cognito auth is configured but jwksUrl is missing');
|
|
358
|
+
return res.status(401).json({error: "unauthorized"});
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// Extract token: Bearer header first, then ?token= query param
|
|
362
|
+
var bearerHeader = req.headers['authorization'] || '';
|
|
363
|
+
var token = null;
|
|
364
|
+
if (bearerHeader.startsWith('Bearer ')) {
|
|
365
|
+
token = bearerHeader.slice(7);
|
|
366
|
+
} else if (req.query && req.query.token) {
|
|
367
|
+
token = req.query.token;
|
|
368
|
+
}
|
|
369
|
+
if (!token) {
|
|
370
|
+
return res.status(401).json({error: "unauthorized"});
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
var verifyOptions = {};
|
|
374
|
+
if (authConfig.audience) { verifyOptions.audience = authConfig.audience; }
|
|
375
|
+
if (authConfig.issuer) { verifyOptions.issuer = authConfig.issuer; }
|
|
376
|
+
|
|
377
|
+
function getKey(header, callback) {
|
|
378
|
+
jwksClient.getSigningKey(header.kid, function(err, key) {
|
|
379
|
+
if (err) { return callback(err); }
|
|
380
|
+
var signingKey = key.getPublicKey
|
|
381
|
+
? key.getPublicKey()
|
|
382
|
+
: (key.publicKey || key.rsaPublicKey);
|
|
383
|
+
callback(null, signingKey);
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
jwt.verify(token, getKey, verifyOptions, function(err, jwtPayload) {
|
|
388
|
+
if (err) {
|
|
389
|
+
return res.status(401).json({error: "unauthorized"});
|
|
390
|
+
}
|
|
391
|
+
if (req.headers && req.headers.authorization) {
|
|
392
|
+
delete req.headers.authorization;
|
|
393
|
+
}
|
|
394
|
+
if (req.query && req.query.token) {
|
|
395
|
+
delete req.query.token;
|
|
396
|
+
}
|
|
397
|
+
if (!authConfig.exposeUser) {
|
|
398
|
+
return dispatchMsg();
|
|
399
|
+
}
|
|
400
|
+
var mappingStr = (authConfig.userMapping || '').trim();
|
|
401
|
+
var mapping = null;
|
|
402
|
+
try {
|
|
403
|
+
mapping = JSON.parse(mappingStr);
|
|
404
|
+
} catch(e) {
|
|
405
|
+
node.warn('http.in+: invalid userMapping JSON — ' + e.message + '. msg.user will not be set.');
|
|
406
|
+
return dispatchMsg();
|
|
407
|
+
}
|
|
408
|
+
var mappedUser = {};
|
|
409
|
+
for (var key in mapping) {
|
|
410
|
+
if (Object.prototype.hasOwnProperty.call(mapping, key)) {
|
|
411
|
+
mappedUser[key] = jwtPayload[mapping[key]];
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
dispatchMsg(mappedUser);
|
|
415
|
+
});
|
|
416
|
+
return; // async path — dispatchMsg called in jwt.verify callback
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// ── Unknown authType — allow through ───────────────────────
|
|
420
|
+
dispatchMsg();
|
|
421
|
+
};
|
|
422
|
+
|
|
423
|
+
var httpMiddleware = function(req,res,next) { next(); }
|
|
424
|
+
|
|
425
|
+
if (RED.settings.httpNodeMiddleware) {
|
|
426
|
+
if (typeof RED.settings.httpNodeMiddleware === "function" || Array.isArray(RED.settings.httpNodeMiddleware)) {
|
|
427
|
+
httpMiddleware = RED.settings.httpNodeMiddleware;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
var maxApiRequestSize = RED.settings.apiMaxLength || '5mb';
|
|
432
|
+
var jsonParser = bodyParser.json({limit:maxApiRequestSize});
|
|
433
|
+
var urlencParser = bodyParser.urlencoded({limit:maxApiRequestSize,extended:true});
|
|
434
|
+
|
|
435
|
+
var metricsHandler = function(req,res,next) { next(); }
|
|
436
|
+
if (this.metric()) {
|
|
437
|
+
metricsHandler = function(req, res, next) {
|
|
438
|
+
var startAt = process.hrtime();
|
|
439
|
+
onHeaders(res, function() {
|
|
440
|
+
if (res._msgid) {
|
|
441
|
+
var diff = process.hrtime(startAt);
|
|
442
|
+
var ms = diff[0] * 1e3 + diff[1] * 1e-6;
|
|
443
|
+
var metricResponseTime = ms.toFixed(3);
|
|
444
|
+
var metricContentLength = res.getHeader("content-length");
|
|
445
|
+
//assuming that _id has been set for res._metrics in HttpOut node!
|
|
446
|
+
node.metric("response.time.millis", {_msgid:res._msgid} , metricResponseTime);
|
|
447
|
+
node.metric("response.content-length.bytes", {_msgid:res._msgid} , metricContentLength);
|
|
448
|
+
}
|
|
449
|
+
});
|
|
450
|
+
next();
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
var multipartParser = function(req,res,next) { next(); }
|
|
455
|
+
if (this.enableUpload) {
|
|
456
|
+
var storageEngine;
|
|
457
|
+
if (this.uploadStorage === 'disk') {
|
|
458
|
+
var uploadDest = this.uploadDir || os.tmpdir();
|
|
459
|
+
storageEngine = multer.diskStorage({
|
|
460
|
+
destination: uploadDest,
|
|
461
|
+
filename: function(req, file, cb) {
|
|
462
|
+
var name = Date.now() + '-' + Math.round(Math.random() * 1e9) + '-' + file.originalname;
|
|
463
|
+
cb(null, name);
|
|
464
|
+
}
|
|
465
|
+
});
|
|
466
|
+
} else {
|
|
467
|
+
storageEngine = multer.memoryStorage();
|
|
468
|
+
}
|
|
469
|
+
var uploadInstance = multer({
|
|
470
|
+
storage: storageEngine,
|
|
471
|
+
limits: { fileSize: (node.maxFileSize || 5) * 1024 * 1024 }
|
|
472
|
+
});
|
|
473
|
+
var uploadMiddleware = uploadInstance.any();
|
|
474
|
+
var uploadStorageLabel = node.uploadStorage || 'memory';
|
|
475
|
+
multipartParser = function(req, res, next) {
|
|
476
|
+
uploadMiddleware(req, res, function(err) {
|
|
477
|
+
if (err) {
|
|
478
|
+
if (err.code === 'LIMIT_FILE_SIZE') {
|
|
479
|
+
return res.status(413).json({ error: 'file_too_large' });
|
|
480
|
+
}
|
|
481
|
+
return next(err);
|
|
482
|
+
}
|
|
483
|
+
req._body = true;
|
|
484
|
+
if (req.files && req.files.length > 0) {
|
|
485
|
+
req._uploadedFiles = req.files.map(function(file) {
|
|
486
|
+
return {
|
|
487
|
+
fieldname: file.fieldname,
|
|
488
|
+
originalname: file.originalname,
|
|
489
|
+
mimetype: file.mimetype,
|
|
490
|
+
size: file.size,
|
|
491
|
+
storage: uploadStorageLabel,
|
|
492
|
+
buffer: uploadStorageLabel === 'memory' ? file.buffer : undefined,
|
|
493
|
+
path: uploadStorageLabel === 'disk' ? file.path : undefined
|
|
494
|
+
};
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
next();
|
|
498
|
+
});
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
if (this.method == "get") {
|
|
503
|
+
RED.httpNode.get(this.url,cookieParser(),httpMiddleware,corsHandler,metricsHandler,this.callback,this.errorHandler);
|
|
504
|
+
} else if (this.method == "post") {
|
|
505
|
+
RED.httpNode.post(this.url,cookieParser(),httpMiddleware,corsHandler,metricsHandler,jsonParser,urlencParser,multipartParser,rawBodyParser,this.callback,this.errorHandler);
|
|
506
|
+
} else if (this.method == "put") {
|
|
507
|
+
RED.httpNode.put(this.url,cookieParser(),httpMiddleware,corsHandler,metricsHandler,jsonParser,urlencParser,rawBodyParser,this.callback,this.errorHandler);
|
|
508
|
+
} else if (this.method == "patch") {
|
|
509
|
+
RED.httpNode.patch(this.url,cookieParser(),httpMiddleware,corsHandler,metricsHandler,jsonParser,urlencParser,rawBodyParser,this.callback,this.errorHandler);
|
|
510
|
+
} else if (this.method == "delete") {
|
|
511
|
+
RED.httpNode.delete(this.url,cookieParser(),httpMiddleware,corsHandler,metricsHandler,jsonParser,urlencParser,rawBodyParser,this.callback,this.errorHandler);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
this.on("close",function() {
|
|
515
|
+
swagger.unregisterEndpoint(node.id);
|
|
516
|
+
RED.httpNode._router.stack.forEach(function(route,i,routes) {
|
|
517
|
+
if (route.route && route.route.path === node.url && route.route.methods[node.method]) {
|
|
518
|
+
routes.splice(i,1);
|
|
519
|
+
}
|
|
520
|
+
});
|
|
521
|
+
});
|
|
522
|
+
} else {
|
|
523
|
+
this.warn(RED._("httpin.errors.not-created"));
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
RED.nodes.registerType("http.in+",HTTPIn);
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
function HTTPOut(n) {
|
|
530
|
+
RED.nodes.createNode(this,n);
|
|
531
|
+
var node = this;
|
|
532
|
+
this.headers = n.headers||{};
|
|
533
|
+
this.statusCode = parseInt(n.statusCode);
|
|
534
|
+
this.on("input",function(msg,_send,done) {
|
|
535
|
+
if (msg.res) {
|
|
536
|
+
var headers = RED.util.cloneMessage(node.headers);
|
|
537
|
+
if (msg.headers) {
|
|
538
|
+
if (msg.headers.hasOwnProperty('x-node-red-request-node')) {
|
|
539
|
+
var headerHash = msg.headers['x-node-red-request-node'];
|
|
540
|
+
delete msg.headers['x-node-red-request-node'];
|
|
541
|
+
var hash = hashSum(msg.headers);
|
|
542
|
+
if (hash === headerHash) {
|
|
543
|
+
delete msg.headers;
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
if (msg.headers) {
|
|
547
|
+
for (var h in msg.headers) {
|
|
548
|
+
if (msg.headers.hasOwnProperty(h) && !headers.hasOwnProperty(h)) {
|
|
549
|
+
headers[h] = msg.headers[h];
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
// ── Streaming mode ─────────────────────────────────────────
|
|
555
|
+
var stream = msg.stream || (msg.payload && typeof msg.payload.pipe === 'function' && !Buffer.isBuffer(msg.payload) ? msg.payload : null);
|
|
556
|
+
if (stream) {
|
|
557
|
+
var rawRes = msg.res._res;
|
|
558
|
+
if (rawRes.headersSent) { return done(); }
|
|
559
|
+
rawRes.statusCode = msg.statusCode || 200;
|
|
560
|
+
for (var sh in headers) {
|
|
561
|
+
if (headers.hasOwnProperty(sh)) { rawRes.setHeader(sh, headers[sh]); }
|
|
562
|
+
}
|
|
563
|
+
stream.on('error', function(err) {
|
|
564
|
+
if (!rawRes.headersSent) {
|
|
565
|
+
rawRes.statusCode = 500;
|
|
566
|
+
}
|
|
567
|
+
rawRes.end('Stream error');
|
|
568
|
+
done();
|
|
569
|
+
});
|
|
570
|
+
stream.on('end', function() { done(); });
|
|
571
|
+
stream.pipe(rawRes);
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
if (Object.keys(headers).length > 0) {
|
|
576
|
+
msg.res._res.set(headers);
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
if (msg.cookies) {
|
|
580
|
+
for (var name in msg.cookies) {
|
|
581
|
+
if (msg.cookies.hasOwnProperty(name)) {
|
|
582
|
+
if (msg.cookies[name] === null || msg.cookies[name].value === null) {
|
|
583
|
+
if (msg.cookies[name]!==null) {
|
|
584
|
+
msg.res._res.clearCookie(name,msg.cookies[name]);
|
|
585
|
+
} else {
|
|
586
|
+
msg.res._res.clearCookie(name);
|
|
587
|
+
}
|
|
588
|
+
} else if (typeof msg.cookies[name] === 'object') {
|
|
589
|
+
msg.res._res.cookie(name,msg.cookies[name].value,msg.cookies[name]);
|
|
590
|
+
} else {
|
|
591
|
+
msg.res._res.cookie(name,msg.cookies[name]);
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
var statusCode = node.statusCode || parseInt(msg.statusCode) || 200;
|
|
597
|
+
if (typeof msg.payload == "object" && !Buffer.isBuffer(msg.payload)) {
|
|
598
|
+
msg.res._res.status(statusCode).jsonp(msg.payload);
|
|
599
|
+
} else {
|
|
600
|
+
if (msg.res._res.get('content-length') == null) {
|
|
601
|
+
var len;
|
|
602
|
+
if (msg.payload == null) {
|
|
603
|
+
len = 0;
|
|
604
|
+
} else if (Buffer.isBuffer(msg.payload)) {
|
|
605
|
+
len = msg.payload.length;
|
|
606
|
+
} else if (typeof msg.payload == "number") {
|
|
607
|
+
len = Buffer.byteLength(""+msg.payload);
|
|
608
|
+
} else {
|
|
609
|
+
len = Buffer.byteLength(msg.payload);
|
|
610
|
+
}
|
|
611
|
+
msg.res._res.set('content-length', len);
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
if (typeof msg.payload === "number") {
|
|
615
|
+
msg.payload = ""+msg.payload;
|
|
616
|
+
}
|
|
617
|
+
msg.res._res.status(statusCode).send(msg.payload);
|
|
618
|
+
}
|
|
619
|
+
} else {
|
|
620
|
+
node.warn(RED._("httpin.errors.no-response"));
|
|
621
|
+
}
|
|
622
|
+
done();
|
|
623
|
+
});
|
|
624
|
+
}
|
|
625
|
+
RED.nodes.registerType("http.out+",HTTPOut);
|
|
626
|
+
}
|