@tiledesk/tiledesk-server 2.13.25 → 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 CHANGED
@@ -5,8 +5,12 @@
5
5
  🚀 IN PRODUCTION 🚀
6
6
  (https://www.npmjs.com/package/@tiledesk/tiledesk-server/v/2.3.77)
7
7
 
8
- # 2.13.25
9
- - Fixed bug: LLM preview doesn't works
8
+ # 2.13.27
9
+ - Added rate manager for webhook call
10
+ - Increased json body limit for /webhook endpoint
11
+
12
+ # 2.13.26
13
+ - Fixed bug: LLM preview not working
10
14
 
11
15
  # 2.13.24
12
16
  - Code improvements
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:: ", err);
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.25",
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.78",
55
- "@tiledesk/tiledesk-whatsapp-connector": "1.0.10",
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/kb.js CHANGED
@@ -291,6 +291,10 @@ router.post('/qa', async (req, res) => {
291
291
 
292
292
  winston.debug("/qa data: ", data);
293
293
 
294
+ if (!data.llm) {
295
+ data.llm = "openai";
296
+ }
297
+
294
298
  if (data.llm === 'ollama') {
295
299
  data.gptkey = process.env.GPTKEY;
296
300
  try {
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;
@@ -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
  });