@tiledesk/tiledesk-server 2.13.26 → 2.13.27
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/CHANGELOG.md +4 -0
- package/app.js +29 -5
- package/package.json +3 -3
- package/routes/webhook.js +7 -0
- package/services/RateManager.js +116 -0
- package/test/webhookRoute.js +81 -0
package/CHANGELOG.md
CHANGED
package/app.js
CHANGED
|
@@ -217,10 +217,14 @@ var BanUserNotifier = require('./services/banUserNotifier');
|
|
|
217
217
|
BanUserNotifier.listen();
|
|
218
218
|
const { ChatbotService } = require('./services/chatbotService');
|
|
219
219
|
const { QuoteManager } = require('./services/QuoteManager');
|
|
220
|
+
const RateManager = require('./services/RateManager');
|
|
220
221
|
|
|
221
222
|
let qm = new QuoteManager({ tdCache: tdCache });
|
|
222
223
|
qm.start();
|
|
223
224
|
|
|
225
|
+
let rm = new RateManager({ tdCache: tdCache });
|
|
226
|
+
|
|
227
|
+
|
|
224
228
|
var modulesManager = undefined;
|
|
225
229
|
try {
|
|
226
230
|
modulesManager = require('./services/modulesManager');
|
|
@@ -255,7 +259,7 @@ app.set('view engine', 'jade');
|
|
|
255
259
|
app.set('chatbot_service', new ChatbotService())
|
|
256
260
|
app.set('redis_client', tdCache);
|
|
257
261
|
app.set('quote_manager', qm);
|
|
258
|
-
|
|
262
|
+
app.set('rate_manager', rm);
|
|
259
263
|
|
|
260
264
|
// TODO DELETE IT IN THE NEXT RELEASE
|
|
261
265
|
if (process.env.ENABLE_ALTERNATIVE_CORS_MIDDLEWARE === "true") {
|
|
@@ -287,6 +291,13 @@ if (process.env.ENABLE_ALTERNATIVE_CORS_MIDDLEWARE === "true") {
|
|
|
287
291
|
const JSON_BODY_LIMIT = process.env.JSON_BODY_LIMIT || '500KB';
|
|
288
292
|
winston.debug("JSON_BODY_LIMIT : " + JSON_BODY_LIMIT);
|
|
289
293
|
|
|
294
|
+
const WEBHOOK_BODY_LIMIT = process.env.WEBHOOK_BODY_LIMIT || '5mb';
|
|
295
|
+
winston.debug("WEBHOOK_BODY_LIMIT : " + WEBHOOK_BODY_LIMIT);
|
|
296
|
+
|
|
297
|
+
const webhookParser = bodyParser.json({ limit: WEBHOOK_BODY_LIMIT });
|
|
298
|
+
|
|
299
|
+
app.use('/webhook', webhookParser, webhook);
|
|
300
|
+
|
|
290
301
|
app.use(bodyParser.json({limit: JSON_BODY_LIMIT,
|
|
291
302
|
verify: function (req, res, buf) {
|
|
292
303
|
// var url = req.originalUrl;
|
|
@@ -388,7 +399,7 @@ app.options('*', cors());
|
|
|
388
399
|
|
|
389
400
|
|
|
390
401
|
// });
|
|
391
|
-
|
|
402
|
+
console.log("MAX_UPLOAD_FILE_SIZE: ", process.env.MAX_UPLOAD_FILE_SIZE);
|
|
392
403
|
|
|
393
404
|
|
|
394
405
|
if (process.env.ROUTELOGGER_ENABLED==="true") {
|
|
@@ -517,7 +528,7 @@ app.use('/users_util', usersUtil);
|
|
|
517
528
|
app.use('/logs', [passport.authenticate(['basic', 'jwt'], { session: false }), validtoken], logs);
|
|
518
529
|
app.use('/requests_util', [passport.authenticate(['basic', 'jwt'], { session: false }), validtoken], requestUtilRoot);
|
|
519
530
|
|
|
520
|
-
app.use('/webhook', webhook);
|
|
531
|
+
//app.use('/webhook', webhook); // moved on top before body parser middleware
|
|
521
532
|
|
|
522
533
|
// TODO security issues
|
|
523
534
|
if (process.env.DISABLE_TRANSCRIPT_VIEW_PAGE ) {
|
|
@@ -674,13 +685,26 @@ app.use((err, req, res, next) => {
|
|
|
674
685
|
return res.status(401).json({ err: "error ip filter" });
|
|
675
686
|
}
|
|
676
687
|
|
|
688
|
+
const realIp = req.headers['x-forwarded-for']?.split(',')[0] || req.headers['x-real-ip'] || req.ip;
|
|
689
|
+
|
|
677
690
|
//emitted by multer when the file is too big
|
|
678
691
|
if (err.code === "LIMIT_FILE_SIZE") {
|
|
679
692
|
winston.debug("LIMIT_FILE_SIZE");
|
|
693
|
+
winston.warn(`LIMIT_FILE_SIZE on ${req.originalUrl}`, {
|
|
694
|
+
limit: process.env.MAX_UPLOAD_FILE_SIZE,
|
|
695
|
+
ip: req.ip,
|
|
696
|
+
realIp: realIp
|
|
697
|
+
});
|
|
680
698
|
return res.status(413).json({ err: "Content Too Large", limit_file_size: process.env.MAX_UPLOAD_FILE_SIZE });
|
|
681
|
-
}
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
if (err.type === "entity.too.large" || err.name === "PayloadTooLargeError") {
|
|
702
|
+
winston.warn("Payload too large", { expected: err.expected, limit: err.limit, length: err.length });
|
|
703
|
+
return res.status(413).json({ err: "Request entity too large", limit: err.limit});
|
|
704
|
+
}
|
|
705
|
+
|
|
682
706
|
|
|
683
|
-
winston.error("General error
|
|
707
|
+
winston.error("General error: ", err);
|
|
684
708
|
return res.status(500).json({ err: "error" });
|
|
685
709
|
});
|
|
686
710
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tiledesk/tiledesk-server",
|
|
3
3
|
"description": "The Tiledesk server module",
|
|
4
|
-
"version": "2.13.
|
|
4
|
+
"version": "2.13.27",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"start": "node ./bin/www",
|
|
7
7
|
"pretest": "mongodb-runner start",
|
|
@@ -51,8 +51,8 @@
|
|
|
51
51
|
"@tiledesk/tiledesk-telegram-connector": "^0.1.14",
|
|
52
52
|
"@tiledesk/tiledesk-tybot-connector": "^2.0.31",
|
|
53
53
|
"@tiledesk/tiledesk-voice-twilio-connector": "^0.1.22",
|
|
54
|
-
"@tiledesk/tiledesk-vxml-connector": "^0.1.
|
|
55
|
-
"@tiledesk/tiledesk-whatsapp-connector": "1.0.
|
|
54
|
+
"@tiledesk/tiledesk-vxml-connector": "^0.1.81",
|
|
55
|
+
"@tiledesk/tiledesk-whatsapp-connector": "1.0.11",
|
|
56
56
|
"@tiledesk/tiledesk-whatsapp-jobworker": "^0.0.13",
|
|
57
57
|
"amqplib": "^0.5.5",
|
|
58
58
|
"app-root-path": "^3.0.0",
|
package/routes/webhook.js
CHANGED
|
@@ -209,6 +209,13 @@ router.all('/:webhook_id', async (req, res) => {
|
|
|
209
209
|
return res.status(422).send({ success: false, error: "Webhook " + webhook_id + " is currently turned off"})
|
|
210
210
|
}
|
|
211
211
|
|
|
212
|
+
const rate_manager = req.app.get("rate_manager");
|
|
213
|
+
const allowed = await rate_manager.canExecute(webhook.id_project, null, 'webhook');
|
|
214
|
+
if (!allowed) {
|
|
215
|
+
winston.warn("Webhook rate limit exceeded for project " + webhook.id_project)
|
|
216
|
+
return res.status(429).send({ message: "Rate limit exceeded"});
|
|
217
|
+
}
|
|
218
|
+
|
|
212
219
|
payload.request_id = "automation-request-" + webhook.id_project + "-" + new ObjectId() + "-" + webhook_id;
|
|
213
220
|
|
|
214
221
|
// To delete - Start
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
const project = require("../models/project");
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class RateManager {
|
|
5
|
+
|
|
6
|
+
constructor(config) {
|
|
7
|
+
|
|
8
|
+
if (!config) {
|
|
9
|
+
throw new Error('config is mandatory')
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
if (!config.tdCache) {
|
|
13
|
+
throw new Error('config.tdCache is mandatory')
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
this.tdCache = config.tdCache;
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
// Default rates
|
|
20
|
+
this.defaultRates = {
|
|
21
|
+
webhook: {
|
|
22
|
+
capacity: parseInt(process.env.BUCKET_WH_CAPACITY) || 10,
|
|
23
|
+
refill_rate: (parseInt(process.env.BUCKET_WH_REFILL_RATE_PER_MIN) || 10) / 60
|
|
24
|
+
},
|
|
25
|
+
message: {
|
|
26
|
+
capacity: parseInt(process.env.BUCKET_MSG_CAPACITY) || 100,
|
|
27
|
+
refill_rate: (parseInt(process.env.BUCKET_MSG_REFILL_RATE_PER_MIN) || 100) / 60
|
|
28
|
+
},
|
|
29
|
+
block: {
|
|
30
|
+
capacity: parseInt(process.env.BUCKET_BLK_CAPACITY) || 100,
|
|
31
|
+
refill_rate: (parseInt(process.env.BUCKET_BLK_REFILL_RATE_PER_MIN) || 100) / 60
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async canExecute(projectOrId, id, type) {
|
|
37
|
+
|
|
38
|
+
let project = projectOrId;
|
|
39
|
+
if (typeof projectOrId === 'string') {
|
|
40
|
+
project = await this.loadProject(projectOrId);
|
|
41
|
+
id = projectOrId;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const config = await this.getRateConfig(project, type);
|
|
45
|
+
const key = `bucket:${type}:${id}`
|
|
46
|
+
|
|
47
|
+
let bucket = await this.getBucket(key, type, project);
|
|
48
|
+
let current_tokens = bucket.tokens;
|
|
49
|
+
let elapsed = (new Date() - new Date(bucket.timestamp)) / 1000;
|
|
50
|
+
let tokens = Math.min(config.capacity, current_tokens + (elapsed * config.refill_rate));
|
|
51
|
+
|
|
52
|
+
if (tokens > 0) {
|
|
53
|
+
tokens -= 1;
|
|
54
|
+
bucket.tokens = tokens;
|
|
55
|
+
bucket.timestamp = new Date();
|
|
56
|
+
this.setBucket(key, bucket)
|
|
57
|
+
return true;
|
|
58
|
+
} else {
|
|
59
|
+
bucket.timestamp = new Date();
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async getRateConfig(project, type) {
|
|
65
|
+
|
|
66
|
+
const baseConfig = this.defaultRates[type];
|
|
67
|
+
if (!project) {
|
|
68
|
+
return baseConfig;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const custom = project?.profile?.customization?.rates?.[type];
|
|
72
|
+
if (!custom) {
|
|
73
|
+
return baseConfig;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
...baseConfig,
|
|
78
|
+
...custom
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async setBucket(key, bucket) {
|
|
83
|
+
const bucket_string = JSON.stringify(bucket);
|
|
84
|
+
await this.tdCache.set(key, bucket_string, { EX: 600 });
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async getBucket(key, type, project) {
|
|
88
|
+
let bucket = await this.tdCache.get(key);
|
|
89
|
+
if (bucket) {
|
|
90
|
+
return JSON.parse(bucket);
|
|
91
|
+
}
|
|
92
|
+
bucket = await this.createBucket(type, project);
|
|
93
|
+
return bucket;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async createBucket(type, project) {
|
|
97
|
+
const config = await this.getRateConfig(project, type)
|
|
98
|
+
return {
|
|
99
|
+
tokens: config.capacity,
|
|
100
|
+
timestamp: new Date()
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async loadProject(id_project) {
|
|
105
|
+
// Hint: implement redis cache also for project
|
|
106
|
+
try {
|
|
107
|
+
return project.findById(id_project);
|
|
108
|
+
} catch (err) {
|
|
109
|
+
winston.error("(RateManager) Error getting project ", err);
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
module.exports = RateManager;
|
package/test/webhookRoute.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
//During the test the env variable is set to test
|
|
2
2
|
process.env.NODE_ENV = 'test';
|
|
3
3
|
process.env.LOG_LEVEL = 'error';
|
|
4
|
+
process.env.BUCKET_WH_CAPACITY = 2
|
|
5
|
+
process.env.BUCKET_WH_REFILL_RATE = 2 / 60;
|
|
4
6
|
|
|
5
7
|
var projectService = require('../services/projectService');
|
|
6
8
|
var userService = require('../services/userService');
|
|
@@ -617,4 +619,83 @@ describe('WebhookRoute', () => {
|
|
|
617
619
|
});
|
|
618
620
|
});
|
|
619
621
|
})
|
|
622
|
+
|
|
623
|
+
it('webhook-rate-limit', (done) => {
|
|
624
|
+
|
|
625
|
+
var email = "test-signup-" + Date.now() + "@email.com";
|
|
626
|
+
var pwd = "pwd";
|
|
627
|
+
|
|
628
|
+
userService.signup(email, pwd, "Test Firstname", "Test lastname").then(function (savedUser) {
|
|
629
|
+
projectService.create("test-webhook-create", savedUser._id).then(function (savedProject) {
|
|
630
|
+
|
|
631
|
+
chai.request(server)
|
|
632
|
+
.post('/' + savedProject._id + '/faq_kb')
|
|
633
|
+
.auth(email, pwd)
|
|
634
|
+
.send({ name: "testbot", type: "tilebot", subtype: "webhook", language: "en", template: "blank" })
|
|
635
|
+
.end((err, res) => {
|
|
636
|
+
|
|
637
|
+
if (err) { console.error("err: ", err); }
|
|
638
|
+
if (log) { console.log("res.body", res.body); }
|
|
639
|
+
|
|
640
|
+
res.should.have.status(200);
|
|
641
|
+
res.body.should.be.a('object');
|
|
642
|
+
|
|
643
|
+
let chatbot_id = res.body._id;
|
|
644
|
+
let webhook_intent_id = "3bfda939-ff76-4762-bbe0-fc0f0dc4c777"
|
|
645
|
+
|
|
646
|
+
chai.request(server)
|
|
647
|
+
.post('/' + savedProject._id + '/webhooks/')
|
|
648
|
+
.auth(email, pwd)
|
|
649
|
+
.send({ chatbot_id: chatbot_id, block_id: webhook_intent_id })
|
|
650
|
+
.end((err, res) => {
|
|
651
|
+
|
|
652
|
+
if (err) { console.error("err: ", err); }
|
|
653
|
+
if (log) { console.log("res.body", res.body); }
|
|
654
|
+
|
|
655
|
+
res.should.have.status(200);
|
|
656
|
+
res.body.should.be.a('object');
|
|
657
|
+
|
|
658
|
+
let webhook_id = res.body.webhook_id;
|
|
659
|
+
|
|
660
|
+
let iterations = 4;
|
|
661
|
+
let interval = 1000;
|
|
662
|
+
async function send(i) {
|
|
663
|
+
chai.request(server)
|
|
664
|
+
.post('/webhook/' + webhook_id)
|
|
665
|
+
.auth(email, pwd)
|
|
666
|
+
.end((err, res) => {
|
|
667
|
+
|
|
668
|
+
if (err) { console.error("err: ", err); }
|
|
669
|
+
if (log) { console.log("res.body", res.body); }
|
|
670
|
+
|
|
671
|
+
if (i !== 3) {
|
|
672
|
+
res.should.have.status(200);
|
|
673
|
+
res.body.should.be.a('object');
|
|
674
|
+
expect(res.body.success).to.equal(true);
|
|
675
|
+
expect(res.body.message).to.equal("Webhook disabled in test mode");
|
|
676
|
+
} else {
|
|
677
|
+
res.should.have.status(429);
|
|
678
|
+
res.body.should.be.a('object');
|
|
679
|
+
expect(res.body.message).to.equal("Rate limit exceeded");
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
i += 1;
|
|
683
|
+
if (i < iterations) {
|
|
684
|
+
setTimeout(() => {
|
|
685
|
+
send(i)
|
|
686
|
+
}, interval);
|
|
687
|
+
} else {
|
|
688
|
+
done();
|
|
689
|
+
}
|
|
690
|
+
});
|
|
691
|
+
}
|
|
692
|
+
send(0);
|
|
693
|
+
|
|
694
|
+
|
|
695
|
+
});
|
|
696
|
+
|
|
697
|
+
});
|
|
698
|
+
});
|
|
699
|
+
});
|
|
700
|
+
})
|
|
620
701
|
});
|