soajs.controller 4.2.4 → 4.2.5
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/Gruntfile.js +68 -12
- package/mw/traffic/index.js +104 -65
- package/mw/traffic/model/memory.js +73 -0
- package/mw/traffic/model/mongo.js +115 -0
- package/package.json +9 -5
- package/server/controller.js +36 -33
- package/test/unit/mw/traffic/index.js +3 -0
package/Gruntfile.js
CHANGED
|
@@ -56,7 +56,7 @@ let lib = {
|
|
|
56
56
|
module.exports = function (grunt) {
|
|
57
57
|
//Loading the needed plugins to run the grunt tasks
|
|
58
58
|
let pluginsRootPath = lib.findRoot();
|
|
59
|
-
lib.loadTasks(grunt, pluginsRootPath, ['grunt-contrib-jshint', 'grunt-jsdoc', 'grunt-contrib-clean', 'grunt-mocha-test', 'grunt-env', 'grunt-istanbul', 'grunt-coveralls', 'grunt-contrib-copy']);
|
|
59
|
+
lib.loadTasks(grunt, pluginsRootPath, ['grunt-contrib-jshint', 'grunt-jsdoc', 'grunt-contrib-clean', 'grunt-mocha-test', 'grunt-env', 'grunt-istanbul', 'grunt-coveralls', 'grunt-babel', 'grunt-contrib-copy']);
|
|
60
60
|
grunt.initConfig({
|
|
61
61
|
//Defining jshint tasks
|
|
62
62
|
jshint: {
|
|
@@ -65,7 +65,7 @@ module.exports = function (grunt) {
|
|
|
65
65
|
"curly": true,
|
|
66
66
|
"eqeqeq": true,
|
|
67
67
|
"eqnull": true,
|
|
68
|
-
"esversion":
|
|
68
|
+
"esversion": 8,
|
|
69
69
|
"forin": true,
|
|
70
70
|
"latedef": "nofunc",
|
|
71
71
|
"leanswitch": true,
|
|
@@ -89,7 +89,7 @@ module.exports = function (grunt) {
|
|
|
89
89
|
}
|
|
90
90
|
},
|
|
91
91
|
files: {
|
|
92
|
-
src: ['index.js', '_index.js', 'config.js', 'Gruntfile.js', 'lib/**/*.js', 'modules
|
|
92
|
+
src: ['index.js', '_index.js', 'config.js', 'Gruntfile.js', 'lib/**/*.js', 'modules/**/*.js', 'mw/**/*.js', 'server/**/*.js', 'utilities/**/*.js']
|
|
93
93
|
},
|
|
94
94
|
gruntfile: {
|
|
95
95
|
src: 'Gruntfile.js'
|
|
@@ -109,7 +109,7 @@ module.exports = function (grunt) {
|
|
|
109
109
|
SOAJS_ENV: "dev",
|
|
110
110
|
SOAJS_TEST: true,
|
|
111
111
|
SOAJS_PROFILE: __dirname + "/profiles/single.js",
|
|
112
|
-
APP_DIR_FOR_CODE_COVERAGE: '../test/coverage/instrument/'
|
|
112
|
+
APP_DIR_FOR_CODE_COVERAGE: '../test/coverage/instrument/test/dist/'
|
|
113
113
|
}
|
|
114
114
|
},
|
|
115
115
|
|
|
@@ -119,23 +119,78 @@ module.exports = function (grunt) {
|
|
|
119
119
|
},
|
|
120
120
|
coverage: {
|
|
121
121
|
src: ['test/coverage/']
|
|
122
|
+
},
|
|
123
|
+
dist: {
|
|
124
|
+
src: ['test/dist/']
|
|
122
125
|
}
|
|
123
126
|
},
|
|
124
127
|
|
|
128
|
+
babel: {
|
|
129
|
+
options: {
|
|
130
|
+
sourceMap: true,
|
|
131
|
+
presets: ['@babel/preset-env'],
|
|
132
|
+
plugins: ["@babel/plugin-transform-runtime"],
|
|
133
|
+
},
|
|
134
|
+
dist: {
|
|
135
|
+
files: [
|
|
136
|
+
|
|
137
|
+
{
|
|
138
|
+
expand: true,
|
|
139
|
+
cwd: 'lib/',
|
|
140
|
+
src: ['*.js'],
|
|
141
|
+
dest: 'test/dist/lib/'
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
expand: true,
|
|
145
|
+
cwd: 'modules/',
|
|
146
|
+
src: ['**/*.js'],
|
|
147
|
+
dest: 'test/dist/modules/'
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
expand: true,
|
|
151
|
+
cwd: 'mw/',
|
|
152
|
+
src: ['**/*.js'],
|
|
153
|
+
dest: 'test/dist/mw/'
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
expand: true,
|
|
157
|
+
cwd: 'profiles/',
|
|
158
|
+
src: ['*.js'],
|
|
159
|
+
dest: 'test/dist/profiles/'
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
expand: true,
|
|
163
|
+
cwd: 'server/',
|
|
164
|
+
src: ['*.js'],
|
|
165
|
+
dest: 'test/dist/server/'
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
expand: true,
|
|
169
|
+
cwd: 'utilities/',
|
|
170
|
+
src: ['*.js'],
|
|
171
|
+
dest: 'test/dist/utilities/'
|
|
172
|
+
},
|
|
173
|
+
{ 'test/dist/index.js': 'index.js' },
|
|
174
|
+
{ 'test/dist/_index.js': '_index.js' },
|
|
175
|
+
{ 'test/dist/config.js': 'config.js' },
|
|
176
|
+
]
|
|
177
|
+
}
|
|
178
|
+
},
|
|
179
|
+
|
|
125
180
|
copy: {
|
|
126
181
|
main: {
|
|
127
182
|
files: [
|
|
128
|
-
{expand: true, src: ['package.json'], dest: 'test/coverage/instrument/', filter: 'isFile'},
|
|
183
|
+
{expand: true, src: ['package.json'], dest: 'test/coverage/instrument/test/dist/', filter: 'isFile'},
|
|
129
184
|
{
|
|
130
185
|
cwd: 'modules/driver/kubernetes/model/swagger/',
|
|
131
186
|
src: '**/*',
|
|
132
|
-
dest: 'test/coverage/instrument/modules/driver/kubernetes/model/swagger/',
|
|
187
|
+
dest: 'test/coverage/instrument/test/dist/modules/driver/kubernetes/model/swagger/',
|
|
133
188
|
expand: true
|
|
134
189
|
},
|
|
135
190
|
{
|
|
136
191
|
cwd: 'modules/driver/kubernetes/model/clients/', // set working folder / root to copy
|
|
137
192
|
src: '**/*',
|
|
138
|
-
dest: 'test/coverage/instrument/modules/driver/kubernetes/model/clients/', // destination folder
|
|
193
|
+
dest: 'test/coverage/instrument/test/dist/modules/driver/kubernetes/model/clients/', // destination folder
|
|
139
194
|
expand: true // required when using cwd
|
|
140
195
|
}
|
|
141
196
|
]
|
|
@@ -143,7 +198,8 @@ module.exports = function (grunt) {
|
|
|
143
198
|
},
|
|
144
199
|
|
|
145
200
|
instrument: {
|
|
146
|
-
files: ['index.js', '_index.js', 'config.js', 'lib/**/*.js', 'modules/driver/*.js', 'modules/driver/kubernetes/*.js', 'modules/driver/kubernetes/bl/*.js', 'modules/driver/kubernetes/model/*.js', 'modules/registry/*.js', 'mw/**/*.js', 'server/**/*.js', 'utilities/**/*.js'],
|
|
201
|
+
// files: ['index.js', '_index.js', 'config.js', 'lib/**/*.js', 'modules/driver/*.js', 'modules/driver/kubernetes/*.js', 'modules/driver/kubernetes/bl/*.js', 'modules/driver/kubernetes/model/*.js', 'modules/registry/*.js', 'mw/**/*.js', 'server/**/*.js', 'utilities/**/*.js'],
|
|
202
|
+
files: ['test/dist/index.js', 'test/dist/_index.js', 'test/dist/config.js', 'test/dist/lib/**/*.js', 'test/dist/modules/**/*.js', 'test/dist/mw/**/*.js', 'test/dist/server/**/*.js', 'test/dist/utilities/**/*.js'],
|
|
147
203
|
options: {
|
|
148
204
|
lazy: false,
|
|
149
205
|
basePath: 'test/coverage/instrument/'
|
|
@@ -202,11 +258,11 @@ module.exports = function (grunt) {
|
|
|
202
258
|
process.env.SHOW_LOGS = grunt.option('showLogs');
|
|
203
259
|
grunt.registerTask("default", ['jshint']);
|
|
204
260
|
grunt.registerTask("integration", ['env:mochaTest', 'mochaTest:integration']);
|
|
205
|
-
grunt.registerTask("integration-coverage", ['clean', 'copy', 'env:coverage', 'instrument', 'mochaTest:integration', 'storeCoverage', 'makeReport']);
|
|
261
|
+
grunt.registerTask("integration-coverage", ['clean', 'babel', 'copy', 'env:coverage', 'instrument', 'mochaTest:integration', 'storeCoverage', 'makeReport']);
|
|
206
262
|
grunt.registerTask("unit", ['env:mochaTest', 'mochaTest:unit']);
|
|
207
|
-
grunt.registerTask("unit-coverage", ['clean', 'copy', 'env:coverage', 'instrument', 'mochaTest:unit', 'storeCoverage', 'makeReport']);
|
|
208
|
-
grunt.registerTask("test", ['clean', 'copy', 'env:coverage', 'instrument', 'mochaTest:unit', 'mochaTest:integration', 'storeCoverage', 'makeReport']);
|
|
209
|
-
grunt.registerTask("coverage", ['clean', 'copy', 'env:coverage', 'instrument', 'mochaTest:unit', 'mochaTest:integration', 'storeCoverage', 'makeReport', 'coveralls']);
|
|
263
|
+
grunt.registerTask("unit-coverage", ['clean', 'babel', 'copy', 'env:coverage', 'instrument', 'mochaTest:unit', 'storeCoverage', 'makeReport']);
|
|
264
|
+
grunt.registerTask("test", ['clean', 'babel', 'copy', 'env:coverage', 'instrument', 'mochaTest:unit', 'mochaTest:integration', 'storeCoverage', 'makeReport']);
|
|
265
|
+
grunt.registerTask("coverage", ['clean', 'babel', 'copy', 'env:coverage', 'instrument', 'mochaTest:unit', 'mochaTest:integration', 'storeCoverage', 'makeReport', 'coveralls']);
|
|
210
266
|
|
|
211
267
|
};
|
|
212
268
|
|
package/mw/traffic/index.js
CHANGED
|
@@ -8,17 +8,25 @@
|
|
|
8
8
|
* found in the LICENSE file at the root of this repository
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
+
|
|
12
|
+
const get = (p, o) => p.reduce((xs, x) => (xs && xs[x]) ? xs[x] : null, o);
|
|
13
|
+
const memory = require('./model/memory.js');
|
|
14
|
+
const mongo = require('./model/mongo.js');
|
|
15
|
+
|
|
16
|
+
let model = {
|
|
17
|
+
memory, mongo
|
|
18
|
+
};
|
|
11
19
|
/**
|
|
12
20
|
*
|
|
13
21
|
* @param configuration
|
|
14
22
|
* @returns {Function}
|
|
15
23
|
*/
|
|
16
|
-
module.exports = function () {
|
|
17
|
-
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
24
|
+
module.exports = function (configuration) {
|
|
25
|
+
|
|
26
|
+
// let registry.throttling = {
|
|
27
|
+
// 'publicAPIStrategy' : 'default', // can be null means throttling is off
|
|
28
|
+
// 'privateAPIStrategy': 'heavy', // can be null means throttling is off
|
|
29
|
+
// 'default': {
|
|
22
30
|
// 'status': 1, // 0=Off, 1=On
|
|
23
31
|
// 'type': 1, // 0= tenant, 1= tenant -> ip
|
|
24
32
|
// 'window': 60000,
|
|
@@ -26,7 +34,7 @@ module.exports = function () {
|
|
|
26
34
|
// 'retries': 2,
|
|
27
35
|
// 'delay': 1000
|
|
28
36
|
// },
|
|
29
|
-
//
|
|
37
|
+
// 'off': {
|
|
30
38
|
// 'status': 0, // 0=Off, 1=On
|
|
31
39
|
// 'type': 1, // 0= tenant, 1= tenant -> ip
|
|
32
40
|
// 'window': 60000,
|
|
@@ -34,7 +42,7 @@ module.exports = function () {
|
|
|
34
42
|
// 'retries': 2,
|
|
35
43
|
// 'delay': 1000
|
|
36
44
|
// },
|
|
37
|
-
//
|
|
45
|
+
// 'heavy': {
|
|
38
46
|
// 'status': 1, // 0=Off, 1=On
|
|
39
47
|
// 'type': 1, // 0= tenant, 1= tenant -> ip
|
|
40
48
|
// 'window': 60000,
|
|
@@ -43,64 +51,65 @@ module.exports = function () {
|
|
|
43
51
|
// 'delay': 1000
|
|
44
52
|
// }
|
|
45
53
|
// };
|
|
46
|
-
//
|
|
47
|
-
//
|
|
48
|
-
//
|
|
54
|
+
// let provision.gateway.throttling = {
|
|
55
|
+
// 'publicAPIStrategy' : 'default', // can be null means throttling is off, if not set means inherit from registry
|
|
56
|
+
// 'privateAPIStrategy': null, // can be null means throttling is off, if not set means inherit from registry
|
|
57
|
+
// 'oauth' : {
|
|
58
|
+
// 'publicAPIStrategy' : 'default'
|
|
59
|
+
// }
|
|
49
60
|
// };
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
61
|
+
// let customRegistry.gateway = {
|
|
62
|
+
// "traffic": {
|
|
63
|
+
// "ip2ban": [
|
|
64
|
+
// "::1"
|
|
65
|
+
// ],
|
|
66
|
+
// "model": "mongo",
|
|
67
|
+
// "throttling": {
|
|
68
|
+
// "oauth": {
|
|
69
|
+
// "publicAPIStrategy": "test",
|
|
70
|
+
// "apis": [
|
|
71
|
+
// "/token"
|
|
72
|
+
// ]
|
|
73
|
+
// }
|
|
74
|
+
// }
|
|
75
|
+
// },
|
|
76
|
+
// };
|
|
77
|
+
|
|
78
|
+
model.memory.initThrottling(configuration.log);
|
|
79
|
+
model.mongo.initThrottling(configuration.log, configuration.gatewayDB);
|
|
80
|
+
|
|
81
|
+
let checkThrottling = async (obj, cb) => {
|
|
53
82
|
let trafficKey = obj.trafficKey;
|
|
54
83
|
let throttling = obj.throttling;
|
|
55
|
-
|
|
56
|
-
let
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
if (throttling.type === 0) {
|
|
61
|
-
dataHolder[trafficKey.l1] = {
|
|
62
|
-
'firstReqTime': new Date(Date.now()),
|
|
63
|
-
'count': 0
|
|
64
|
-
};
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
if (throttling.type === 1) {
|
|
69
|
-
if (!dataHolder[trafficKey.l1][trafficKey.l2]) {
|
|
70
|
-
dataHolder[trafficKey.l1][trafficKey.l2] = {
|
|
71
|
-
'firstReqTime': new Date(Date.now()),
|
|
72
|
-
'count': 0
|
|
73
|
-
};
|
|
74
|
-
}
|
|
75
|
-
throttlingObj = dataHolder[trafficKey.l1][trafficKey.l2];
|
|
76
|
-
} else {
|
|
77
|
-
throttlingObj = dataHolder[trafficKey.l1];
|
|
78
|
-
}
|
|
79
|
-
|
|
84
|
+
let strategy = obj.strategy;
|
|
85
|
+
let modelType = obj.throttling.model || 'memory';
|
|
86
|
+
|
|
87
|
+
let throttlingObj = await model[modelType].getThrottling(throttling, trafficKey);
|
|
88
|
+
|
|
80
89
|
let remainingLifetime = throttling.window - Math.floor(Date.now() - throttlingObj.firstReqTime.getTime());
|
|
81
|
-
|
|
90
|
+
|
|
82
91
|
if (remainingLifetime < 1) {
|
|
83
|
-
throttlingObj
|
|
84
|
-
throttlingObj.count = 0;
|
|
92
|
+
throttlingObj = await model[modelType].resetThrottling(throttling, trafficKey);
|
|
85
93
|
}
|
|
86
|
-
|
|
94
|
+
let throttlingCount = throttlingObj.count;
|
|
87
95
|
if (obj.retry === 0) {
|
|
88
|
-
throttlingObj.count
|
|
89
|
-
|
|
96
|
+
throttlingCount = throttlingObj.count + 1;
|
|
97
|
+
await model[modelType].incrementThrottling(throttling, trafficKey);
|
|
90
98
|
}
|
|
91
|
-
|
|
92
|
-
if (
|
|
99
|
+
|
|
100
|
+
if (throttlingCount > throttling.limit) {
|
|
93
101
|
obj.retry++;
|
|
94
102
|
if (obj.retry <= throttling.retries) {
|
|
95
103
|
setTimeout(function () {
|
|
96
|
-
checkThrottling({
|
|
104
|
+
checkThrottling({ trafficKey, throttling, 'retry': obj.retry, strategy }, cb);
|
|
97
105
|
}, throttling.delay);
|
|
98
106
|
} else {
|
|
107
|
+
await model[modelType].logTooManyRequests(trafficKey, strategy);
|
|
99
108
|
return cb({
|
|
100
109
|
'result': false, 'headObj': {
|
|
101
110
|
'Retry-After': remainingLifetime / 1000,
|
|
102
111
|
'X-RateLimit-Limit': throttling.limit,
|
|
103
|
-
'X-RateLimit-Remaining': throttling.limit -
|
|
112
|
+
'X-RateLimit-Remaining': throttling.limit - throttlingCount
|
|
104
113
|
}
|
|
105
114
|
});
|
|
106
115
|
}
|
|
@@ -109,43 +118,73 @@ module.exports = function () {
|
|
|
109
118
|
'result': true, 'headObj': {
|
|
110
119
|
'Retry-After': remainingLifetime / 1000,
|
|
111
120
|
'X-RateLimit-Limit': throttling.limit,
|
|
112
|
-
'X-RateLimit-Remaining': throttling.limit -
|
|
121
|
+
'X-RateLimit-Remaining': throttling.limit - throttlingCount
|
|
113
122
|
}
|
|
114
123
|
});
|
|
115
124
|
}
|
|
116
125
|
};
|
|
117
|
-
|
|
126
|
+
|
|
118
127
|
return (req, res, next) => {
|
|
128
|
+
|
|
129
|
+
let ip2ban = get(["soajs", "registry", "custom", "gateway", "value", "traffic", "ip2ban"], req);
|
|
130
|
+
let clientIP = req.getClientIP();
|
|
131
|
+
|
|
132
|
+
if (ip2ban && Array.isArray(ip2ban) && ip2ban.includes(clientIP)) {
|
|
133
|
+
req.soajs.controllerResponse({
|
|
134
|
+
'status': 403,
|
|
135
|
+
'msg': 'banned'
|
|
136
|
+
});
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
119
140
|
if (req && req.soajs && req.soajs.registry && req.soajs.registry.serviceConfig && req.soajs.registry.serviceConfig.throttling && req.soajs.tenant && req.soajs.controller) {
|
|
141
|
+
|
|
120
142
|
let serviceName = req.soajs.controller.serviceParams.name;
|
|
121
|
-
let
|
|
143
|
+
let api = req.soajs.controller.serviceParams.path;
|
|
144
|
+
let strategy = (req.soajs.controller.serviceParams.isAPIPublic ? 'publicAPIStrategy' : 'privateAPIStrategy');
|
|
122
145
|
let throttlingStrategy = req.soajs.registry.serviceConfig.throttling[strategy];
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
146
|
+
|
|
147
|
+
let throttlingConfiguration = get(["soajs", "servicesConfig", "gateway", "throttling"], req);
|
|
148
|
+
if (!throttlingConfiguration) {
|
|
149
|
+
throttlingConfiguration = get(["soajs", "registry", "custom", "gateway", "value", "traffic", "throttling"], req);
|
|
150
|
+
}
|
|
151
|
+
if (throttlingConfiguration) {
|
|
152
|
+
if (throttlingConfiguration[serviceName]) {
|
|
153
|
+
if (throttlingConfiguration[serviceName].hasOwnProperty(strategy)) {
|
|
154
|
+
if (throttlingConfiguration[serviceName].apis && Array.isArray(throttlingConfiguration[serviceName].apis)) {
|
|
155
|
+
if (throttlingConfiguration[serviceName].apis.includes(api)) {
|
|
156
|
+
throttlingStrategy = throttlingConfiguration[serviceName][strategy];
|
|
157
|
+
}
|
|
158
|
+
} else {
|
|
159
|
+
throttlingStrategy = throttlingConfiguration[serviceName][strategy];
|
|
160
|
+
}
|
|
131
161
|
}
|
|
132
162
|
}
|
|
133
163
|
}
|
|
164
|
+
|
|
134
165
|
if (!throttlingStrategy) {
|
|
135
166
|
return next();
|
|
136
167
|
}
|
|
137
168
|
let throttling = req.soajs.registry.serviceConfig.throttling[throttlingStrategy];
|
|
138
|
-
|
|
169
|
+
|
|
139
170
|
if (throttling) {
|
|
140
|
-
let
|
|
141
|
-
|
|
142
|
-
|
|
171
|
+
let trafficModel = get(["soajs", "registry", "custom", "gateway", "value", "traffic", "model"], req);
|
|
172
|
+
if (trafficModel) {
|
|
173
|
+
throttling.model = trafficModel;
|
|
174
|
+
if (!model[throttling.model]) {
|
|
175
|
+
req.soajs.log.warn('Throttling:', 'Unkown model [' + throttling.model + ']. It can only be [memory || mongo]');
|
|
176
|
+
return next();
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
let trafficKey = { 'l1': req.soajs.tenant.id, 'l2': clientIP };
|
|
180
|
+
|
|
181
|
+
checkThrottling({ trafficKey, throttling, 'retry': 0, 'strategy': throttlingStrategy }, function (response) {
|
|
143
182
|
if (response.result) {
|
|
144
183
|
return next();
|
|
145
184
|
} else {
|
|
146
185
|
req.soajs.controllerResponse({
|
|
147
186
|
'status': 429,
|
|
148
|
-
'msg':
|
|
187
|
+
'msg': 'too many requests',
|
|
149
188
|
'headObj': response.headObj
|
|
150
189
|
});
|
|
151
190
|
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @license
|
|
5
|
+
* Copyright SOAJS All Rights Reserved.
|
|
6
|
+
*
|
|
7
|
+
* Use of this source code is governed by an Apache license that can be
|
|
8
|
+
* found in the LICENSE file at the root of this repository
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
let dataHolder = {};
|
|
12
|
+
|
|
13
|
+
let model = {
|
|
14
|
+
'log': null,
|
|
15
|
+
'initThrottling': (log) => {
|
|
16
|
+
model.log = log;
|
|
17
|
+
dataHolder = {};
|
|
18
|
+
},
|
|
19
|
+
|
|
20
|
+
'getThrottling': (throttling, trafficKey) => {
|
|
21
|
+
|
|
22
|
+
let throttlingObj = {};
|
|
23
|
+
if (!dataHolder[trafficKey.l1]) {
|
|
24
|
+
dataHolder[trafficKey.l1] = {
|
|
25
|
+
'firstReqTime': new Date(Date.now()),
|
|
26
|
+
'count': 0
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
if (throttling.type === 1) {
|
|
30
|
+
if (!dataHolder[trafficKey.l1][trafficKey.l2]) {
|
|
31
|
+
dataHolder[trafficKey.l1][trafficKey.l2] = {
|
|
32
|
+
'firstReqTime': new Date(Date.now()),
|
|
33
|
+
'count': 0
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
throttlingObj = dataHolder[trafficKey.l1][trafficKey.l2];
|
|
37
|
+
} else {
|
|
38
|
+
throttlingObj = dataHolder[trafficKey.l1];
|
|
39
|
+
}
|
|
40
|
+
return throttlingObj;
|
|
41
|
+
},
|
|
42
|
+
|
|
43
|
+
'resetThrottling': (throttling, trafficKey) => {
|
|
44
|
+
if (throttling.type === 1) {
|
|
45
|
+
dataHolder[trafficKey.l1][trafficKey.l2] = {
|
|
46
|
+
'firstReqTime': new Date(Date.now()),
|
|
47
|
+
'count': 0
|
|
48
|
+
};
|
|
49
|
+
return (dataHolder[trafficKey.l1][trafficKey.l2]);
|
|
50
|
+
} else {
|
|
51
|
+
dataHolder[trafficKey.l1] = {
|
|
52
|
+
'firstReqTime': new Date(Date.now()),
|
|
53
|
+
'count': 0
|
|
54
|
+
};
|
|
55
|
+
return (dataHolder[trafficKey.l1]);
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
|
|
59
|
+
'incrementThrottling': (throttling, trafficKey) => {
|
|
60
|
+
if (throttling.type === 1) {
|
|
61
|
+
dataHolder[trafficKey.l1][trafficKey.l2].count++;
|
|
62
|
+
} else {
|
|
63
|
+
dataHolder[trafficKey.l1].count++;
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
|
|
67
|
+
'logTooManyRequests': (trafficKey, throttlingStrategy) => {
|
|
68
|
+
model.log.error('Throttling [' + throttlingStrategy + ']rejected:', 'using tenant id [' + trafficKey.l1 + '] from ip [' + trafficKey.l2 + ']');
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
module.exports = model;
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @license
|
|
5
|
+
* Copyright SOAJS All Rights Reserved.
|
|
6
|
+
*
|
|
7
|
+
* Use of this source code is governed by an Apache license that can be
|
|
8
|
+
* found in the LICENSE file at the root of this repository
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const core = require('soajs.core.modules');
|
|
12
|
+
const Mongo = core.mongo;
|
|
13
|
+
|
|
14
|
+
const throttling_monitor = 'throttling_monitor';
|
|
15
|
+
const throttling_error = 'throttling_error';
|
|
16
|
+
|
|
17
|
+
let model = {
|
|
18
|
+
'mongo': null,
|
|
19
|
+
'log': null,
|
|
20
|
+
|
|
21
|
+
'initThrottling': (log, dbConfiguration) => {
|
|
22
|
+
model.log = log;
|
|
23
|
+
if (!model.mongo) {
|
|
24
|
+
model.mongo = new Mongo(dbConfiguration);
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
|
|
28
|
+
'getThrottling': async (throttling, trafficKey) => {
|
|
29
|
+
|
|
30
|
+
let condition = {
|
|
31
|
+
'l1': trafficKey.l1
|
|
32
|
+
};
|
|
33
|
+
if (throttling.type === 1) {
|
|
34
|
+
condition.l2 = trafficKey.l2;
|
|
35
|
+
} else {
|
|
36
|
+
condition.l2 = '_ALL_';
|
|
37
|
+
}
|
|
38
|
+
let throttlingObj = null;
|
|
39
|
+
try {
|
|
40
|
+
throttlingObj = await model.mongo.findOne(throttling_monitor, condition);
|
|
41
|
+
if (!throttlingObj) {
|
|
42
|
+
throttlingObj = await model.resetThrottling(throttling, trafficKey);
|
|
43
|
+
}
|
|
44
|
+
} catch (e) {
|
|
45
|
+
model.log.error('Throttling:', e.message);
|
|
46
|
+
throttlingObj = {
|
|
47
|
+
'firstReqTime': new Date(Date.now()),
|
|
48
|
+
'count': 0
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
return throttlingObj;
|
|
52
|
+
},
|
|
53
|
+
|
|
54
|
+
'resetThrottling': async (throttling, trafficKey) => {
|
|
55
|
+
let options = {
|
|
56
|
+
'upsert': true
|
|
57
|
+
};
|
|
58
|
+
let condition = {
|
|
59
|
+
'l1': trafficKey.l1
|
|
60
|
+
};
|
|
61
|
+
if (throttling.type === 1) {
|
|
62
|
+
condition.l2 = trafficKey.l2;
|
|
63
|
+
} else {
|
|
64
|
+
condition.l2 = '_ALL_';
|
|
65
|
+
}
|
|
66
|
+
let throttlingObj = {
|
|
67
|
+
'firstReqTime': new Date(Date.now()),
|
|
68
|
+
'count': 0
|
|
69
|
+
};
|
|
70
|
+
let s = {
|
|
71
|
+
'$set': throttlingObj
|
|
72
|
+
};
|
|
73
|
+
try {
|
|
74
|
+
await model.mongo.updateOne(throttling_monitor, condition, s, options);
|
|
75
|
+
} catch (e) {
|
|
76
|
+
model.log.error('Throttling:', e.message);
|
|
77
|
+
}
|
|
78
|
+
return throttlingObj;
|
|
79
|
+
},
|
|
80
|
+
|
|
81
|
+
'incrementThrottling': async (throttling, trafficKey) => {
|
|
82
|
+
let options = {
|
|
83
|
+
};
|
|
84
|
+
let condition = {
|
|
85
|
+
'l1': trafficKey.l1
|
|
86
|
+
};
|
|
87
|
+
if (throttling.type === 1) {
|
|
88
|
+
condition.l2 = trafficKey.l2;
|
|
89
|
+
} else {
|
|
90
|
+
condition.l2 = '_ALL_';
|
|
91
|
+
}
|
|
92
|
+
let s = {
|
|
93
|
+
'$inc': { 'count': 1 }
|
|
94
|
+
};
|
|
95
|
+
try {
|
|
96
|
+
await model.mongo.updateOne(throttling_monitor, condition, s, options);
|
|
97
|
+
} catch (e) {
|
|
98
|
+
model.log.error('Throttling:', e.message);
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
|
|
102
|
+
'logTooManyRequests': (trafficKey, strategy) => {
|
|
103
|
+
let s = {
|
|
104
|
+
"staregy": strategy,
|
|
105
|
+
"l1": trafficKey.l1,
|
|
106
|
+
"l2": trafficKey.l2,
|
|
107
|
+
"ts": new Date(Date.now())
|
|
108
|
+
};
|
|
109
|
+
model.mongo.insertOne(throttling_error, s, {}, () => {
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
module.exports = model;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "soajs.controller",
|
|
3
3
|
"description": "soajs multi tenant API gateway",
|
|
4
|
-
"version": "4.2.
|
|
4
|
+
"version": "4.2.5",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "soajs team",
|
|
7
7
|
"email": "team@soajs.org"
|
|
@@ -35,17 +35,21 @@
|
|
|
35
35
|
"useragent": "2.3.0"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
|
+
"@babel/core": "^7.22.5",
|
|
39
|
+
"@babel/plugin-transform-runtime": "7.19.6",
|
|
40
|
+
"@babel/preset-env": "7.20.2",
|
|
38
41
|
"express": "4.18.2",
|
|
39
|
-
"grunt": "1.
|
|
40
|
-
"grunt-
|
|
42
|
+
"grunt": "1.5.3",
|
|
43
|
+
"grunt-babel": "8.0.0",
|
|
44
|
+
"grunt-contrib-clean": "2.0.1",
|
|
41
45
|
"grunt-contrib-copy": "1.0.0",
|
|
42
|
-
"grunt-contrib-jshint": "2.
|
|
46
|
+
"grunt-contrib-jshint": "3.2.0",
|
|
43
47
|
"grunt-coveralls": "2.0.0",
|
|
44
48
|
"grunt-env": "1.0.1",
|
|
45
49
|
"grunt-istanbul": "0.8.0",
|
|
46
50
|
"grunt-jsdoc": "2.4.0",
|
|
47
51
|
"grunt-mocha-test": "0.13.3",
|
|
48
|
-
"mocha": "
|
|
52
|
+
"mocha": "10.2.0",
|
|
49
53
|
"sinon": "7.5.0"
|
|
50
54
|
}
|
|
51
55
|
}
|
package/server/controller.js
CHANGED
|
@@ -41,7 +41,7 @@ const traffic_mw = require("../mw/traffic/index");
|
|
|
41
41
|
const utils = require("../utilities/utils");
|
|
42
42
|
|
|
43
43
|
let autoRegHost = process.env.SOAJS_SRV_AUTOREGISTERHOST || true;
|
|
44
|
-
if (autoRegHost && typeof(autoRegHost) !== 'boolean') {
|
|
44
|
+
if (autoRegHost && typeof (autoRegHost) !== 'boolean') {
|
|
45
45
|
autoRegHost = (autoRegHost === 'true');
|
|
46
46
|
}
|
|
47
47
|
let regEnvironment = (process.env.SOAJS_ENV || "dev");
|
|
@@ -49,10 +49,10 @@ regEnvironment = regEnvironment.toLowerCase();
|
|
|
49
49
|
|
|
50
50
|
function Controller(config) {
|
|
51
51
|
let _self = this;
|
|
52
|
-
|
|
52
|
+
|
|
53
53
|
config = config || {};
|
|
54
54
|
let param = lib.param(config);
|
|
55
|
-
_self.soajs = {"param": param};
|
|
55
|
+
_self.soajs = { "param": param };
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
Controller.prototype.init = function (callback) {
|
|
@@ -77,7 +77,7 @@ Controller.prototype.init = function (callback) {
|
|
|
77
77
|
log.info("The IP registered for service [" + _self.soajs.param.init.serviceName + "] awareness : ", service.fetched.ip);
|
|
78
78
|
}
|
|
79
79
|
}
|
|
80
|
-
|
|
80
|
+
|
|
81
81
|
let app = connect();
|
|
82
82
|
app.use(favicon_mw());
|
|
83
83
|
app.use(soajs_mw({
|
|
@@ -85,9 +85,9 @@ Controller.prototype.init = function (callback) {
|
|
|
85
85
|
"core": core
|
|
86
86
|
}));
|
|
87
87
|
app.use(cors_mw());
|
|
88
|
-
app.use(response_mw({"controllerResponse": true}));
|
|
88
|
+
app.use(response_mw({ "controllerResponse": true }));
|
|
89
89
|
app.use(maintenanceMode_mw()); //NOTE: this is for global maintenance mode
|
|
90
|
-
|
|
90
|
+
|
|
91
91
|
log.info("Loading Provision ...");
|
|
92
92
|
let dbConfig = registry.coreDB.provision;
|
|
93
93
|
if (registry.coreDB.oauth) {
|
|
@@ -101,11 +101,11 @@ Controller.prototype.init = function (callback) {
|
|
|
101
101
|
if (loaded) {
|
|
102
102
|
log.info("Service provision loaded.");
|
|
103
103
|
let server = http.createServer(app);
|
|
104
|
-
|
|
104
|
+
|
|
105
105
|
// Create maintenance routes
|
|
106
106
|
let maintenance = require("./maintenance.js");
|
|
107
107
|
let serverMaintenance = maintenance(core, log, _self.soajs.param.init, service.ip, regEnvironment, awareness_mw, soajsLib, provision);
|
|
108
|
-
|
|
108
|
+
|
|
109
109
|
// Add gateway middleware
|
|
110
110
|
app.use(awareness_mw.getMw({
|
|
111
111
|
"awareness": _self.soajs.param.init.awareness,
|
|
@@ -116,7 +116,7 @@ Controller.prototype.init = function (callback) {
|
|
|
116
116
|
"serviceIp": service.ip
|
|
117
117
|
}));
|
|
118
118
|
log.info("Awareness middleware initialization done.");
|
|
119
|
-
|
|
119
|
+
|
|
120
120
|
//added mw awarenessEnv so that proxy can use req.soajs.awarenessEnv.getHost('dev', cb)
|
|
121
121
|
app.use(awarenessEnv_mw.getMw({
|
|
122
122
|
"awarenessEnv": true,
|
|
@@ -126,17 +126,17 @@ Controller.prototype.init = function (callback) {
|
|
|
126
126
|
"log": log
|
|
127
127
|
}));
|
|
128
128
|
log.info("AwarenessEnv middleware initialization done.");
|
|
129
|
-
|
|
129
|
+
|
|
130
130
|
app.use(enhancer_mw());
|
|
131
|
-
|
|
132
|
-
app.use(url_mw({"core": core}));
|
|
133
|
-
|
|
134
|
-
app.use(key_mw({"provision": provision, "regEnvironment": regEnvironment}));
|
|
135
|
-
|
|
131
|
+
|
|
132
|
+
app.use(url_mw({ "core": core }));
|
|
133
|
+
|
|
134
|
+
app.use(key_mw({ "provision": provision, "regEnvironment": regEnvironment }));
|
|
135
|
+
|
|
136
136
|
//TODO: add maintenance by tenant here req.soajs.controller.serviceParams.keyObj.config
|
|
137
137
|
|
|
138
138
|
app.use(keyACL_mw());
|
|
139
|
-
|
|
139
|
+
|
|
140
140
|
app.use(gotoService_mw({
|
|
141
141
|
"provision": provision,
|
|
142
142
|
"serviceName": _self.soajs.param.init.serviceName,
|
|
@@ -144,7 +144,7 @@ Controller.prototype.init = function (callback) {
|
|
|
144
144
|
"serviceGroup": _self.soajs.param.init.serviceGroup,
|
|
145
145
|
"core": core
|
|
146
146
|
}));
|
|
147
|
-
|
|
147
|
+
|
|
148
148
|
if (registry.serviceConfig.oauth) {
|
|
149
149
|
let oauth_mw = require("../mw/oauth/index");
|
|
150
150
|
// NOTE: oauth_mw is set on soajs.oauth and is triggered from inside mt_mw
|
|
@@ -153,10 +153,10 @@ Controller.prototype.init = function (callback) {
|
|
|
153
153
|
"serviceConfig": registry.serviceConfig,
|
|
154
154
|
"model": provision.oauthModel
|
|
155
155
|
});
|
|
156
|
-
|
|
156
|
+
|
|
157
157
|
log.info("oAuth middleware initialization done.");
|
|
158
158
|
}
|
|
159
|
-
|
|
159
|
+
|
|
160
160
|
app.use(mt_mw({
|
|
161
161
|
"soajs": _self.soajs,
|
|
162
162
|
"app": app,
|
|
@@ -166,14 +166,17 @@ Controller.prototype.init = function (callback) {
|
|
|
166
166
|
"provision": provision
|
|
167
167
|
}));
|
|
168
168
|
log.info("SOAJS MT middleware initialization done.");
|
|
169
|
-
|
|
170
|
-
app.use(traffic_mw({
|
|
171
|
-
|
|
169
|
+
|
|
170
|
+
app.use(traffic_mw({
|
|
171
|
+
"log": log,
|
|
172
|
+
"gatewayDB": registry.coreDB.gateway || registry.coreDB.provision
|
|
173
|
+
}));
|
|
174
|
+
|
|
172
175
|
app.use((req, res, next) => {
|
|
173
176
|
setImmediate(() => {
|
|
174
177
|
req.soajs.controller.gotoservice(req, res, next);
|
|
175
178
|
});
|
|
176
|
-
|
|
179
|
+
|
|
177
180
|
req.on("error", (error) => {
|
|
178
181
|
req.soajs.log.error("Error @ controller:", error);
|
|
179
182
|
if (req.soajs.controller.redirectedRequest) {
|
|
@@ -181,7 +184,7 @@ Controller.prototype.init = function (callback) {
|
|
|
181
184
|
req.soajs.controller.redirectedRequest = null;
|
|
182
185
|
}
|
|
183
186
|
});
|
|
184
|
-
|
|
187
|
+
|
|
185
188
|
req.on("close", () => {
|
|
186
189
|
if (req.soajs.controller.redirectedRequest) {
|
|
187
190
|
req.soajs.log.info("Request closed:", req.url);
|
|
@@ -190,11 +193,11 @@ Controller.prototype.init = function (callback) {
|
|
|
190
193
|
}
|
|
191
194
|
});
|
|
192
195
|
});
|
|
193
|
-
|
|
196
|
+
|
|
194
197
|
app.use(utils.logErrors);
|
|
195
198
|
app.use(utils.controllerClientErrorHandler);
|
|
196
199
|
app.use(utils.controllerErrorHandler);
|
|
197
|
-
|
|
200
|
+
|
|
198
201
|
callback(registry, log, service, server, serverMaintenance);
|
|
199
202
|
} else {
|
|
200
203
|
log.error('Unable to load provision. controller will not start :(');
|
|
@@ -217,13 +220,13 @@ Controller.prototype.start = function (registry, log, service, server, serverMai
|
|
|
217
220
|
log.error(err.message);
|
|
218
221
|
}
|
|
219
222
|
});
|
|
220
|
-
|
|
223
|
+
|
|
221
224
|
let getAwarenessInfo = (terminate, cb) => {
|
|
222
225
|
let tmp = registryModule.get();
|
|
223
226
|
if (tmp && (tmp.services || tmp.daemons)) {
|
|
224
227
|
let awarenessStatData = {
|
|
225
228
|
"ts": Date.now(),
|
|
226
|
-
"data": soajsUtils.cloneObj({"services": tmp.services, "daemons": tmp.daemons})
|
|
229
|
+
"data": soajsUtils.cloneObj({ "services": tmp.services, "daemons": tmp.daemons })
|
|
227
230
|
};
|
|
228
231
|
if (terminate) {
|
|
229
232
|
for (let serviceName in awarenessStatData.data.services) {
|
|
@@ -252,7 +255,7 @@ Controller.prototype.start = function (registry, log, service, server, serverMai
|
|
|
252
255
|
}, cb);
|
|
253
256
|
}
|
|
254
257
|
};
|
|
255
|
-
|
|
258
|
+
|
|
256
259
|
server.listen(registry.services.controller.port, (err) => {
|
|
257
260
|
if (err) {
|
|
258
261
|
log.error(err.message);
|
|
@@ -268,7 +271,7 @@ Controller.prototype.start = function (registry, log, service, server, serverMai
|
|
|
268
271
|
}, registry, (registered) => {
|
|
269
272
|
if (registered) {
|
|
270
273
|
log.info("Host IP [" + service.ip + "] for service [" + _self.soajs.param.init.serviceName + "@" + _self.soajs.param.init.serviceVersion + "] successfully registered.");
|
|
271
|
-
|
|
274
|
+
|
|
272
275
|
//update the database with the awareness Response generated.
|
|
273
276
|
setTimeout(() => {
|
|
274
277
|
getAwarenessInfo(false, (error) => {
|
|
@@ -277,7 +280,7 @@ Controller.prototype.start = function (registry, log, service, server, serverMai
|
|
|
277
280
|
}
|
|
278
281
|
});
|
|
279
282
|
}, registry.serviceConfig.awareness.healthCheckInterval);
|
|
280
|
-
|
|
283
|
+
|
|
281
284
|
//update the database with the awareness Response generated.
|
|
282
285
|
//controller has been terminated.
|
|
283
286
|
process.on('SIGINT', () => {
|
|
@@ -313,7 +316,7 @@ Controller.prototype.start = function (registry, log, service, server, serverMai
|
|
|
313
316
|
log.info(_self.soajs.param.init.serviceName + " service maintenance is listening on port: " + maintenancePort);
|
|
314
317
|
}
|
|
315
318
|
});
|
|
316
|
-
|
|
319
|
+
|
|
317
320
|
}
|
|
318
321
|
};
|
|
319
322
|
|
|
@@ -321,7 +324,7 @@ Controller.prototype.stop = function (registry, log, service, server, serverMain
|
|
|
321
324
|
let maintenancePort = registry.services.controller.port + registry.serviceConfig.ports.maintenanceInc;
|
|
322
325
|
log.info('stopping Server on port:', registry.services.controller.port);
|
|
323
326
|
log.info('stopping MaintenanceServer on port:', maintenancePort);
|
|
324
|
-
|
|
327
|
+
|
|
325
328
|
server.close((err) => {
|
|
326
329
|
serverMaintenance.close(() => {
|
|
327
330
|
if (callback) {
|