soajs.controller 4.2.40 → 4.2.41
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/README.md +85 -0
- package/lib/request.js +14 -8
- package/modules/registry/index.js +14 -4
- package/mw/gotoService/proxyRequest.js +8 -1
- package/mw/gotoService/redirectToService.js +18 -5
- package/mw/mt/index.js +19 -4
- package/mw/mt/utils.js +64 -29
- package/mw/response/response.js +1 -1
- package/mw/traffic/model/memory.js +64 -0
- package/package.json +7 -7
package/README.md
CHANGED
|
@@ -35,6 +35,91 @@ Enabling CORS allows different domains to communicate with SOAJS via its gateway
|
|
|
35
35
|
|
|
36
36
|
---
|
|
37
37
|
|
|
38
|
+
## Environment Variables
|
|
39
|
+
|
|
40
|
+
The SOAJS Controller can be configured using the following environment variables:
|
|
41
|
+
|
|
42
|
+
### Core Configuration
|
|
43
|
+
|
|
44
|
+
| Variable | Type | Default | Description |
|
|
45
|
+
|----------|------|---------|-------------|
|
|
46
|
+
| `SOAJS_ENV` | string | `"dev"` | Environment code (e.g., dev, staging, production) |
|
|
47
|
+
| `SOAJS_PROFILE` | string | `./profiles/single.js` (mongo mode)<br>`./profiles/solo.js` (solo mode) | Path to the SOAJS profile configuration file |
|
|
48
|
+
| `SOAJS_SOLO` | boolean | `false` | Enable solo mode (uses local registry instead of MongoDB) |
|
|
49
|
+
| `SOAJS_SRVIP` | string | auto-detected | Service IP address for registration |
|
|
50
|
+
| `SOAJS_SRV_AUTOREGISTERHOST` | boolean | `true` | Automatically register host with the service |
|
|
51
|
+
|
|
52
|
+
### Deployment & High Availability
|
|
53
|
+
|
|
54
|
+
| Variable | Type | Default | Description |
|
|
55
|
+
|----------|------|---------|-------------|
|
|
56
|
+
| `SOAJS_DEPLOY_HA` | string | - | High availability deployment type (`kubernetes`, `swarm`, or empty for non-HA) |
|
|
57
|
+
| `SOAJS_DEPLOY_MANUAL` | number | - | Manual deployment mode flag (1 = enabled) |
|
|
58
|
+
| `SOAJS_MANUAL` | boolean | - | Skip automatic deployment operations |
|
|
59
|
+
| `SOAJS_SWARM_UNIX_PORT` | string | `/var/run/docker.sock` | Docker Swarm Unix socket path |
|
|
60
|
+
|
|
61
|
+
### Security & Tenant Configuration
|
|
62
|
+
|
|
63
|
+
| Variable | Type | Default | Description |
|
|
64
|
+
|----------|------|---------|-------------|
|
|
65
|
+
| `SOAJS_SENSITIVE_ENVS` | JSON array | `["dashboard"]` | List of sensitive environment codes requiring additional security checks |
|
|
66
|
+
|
|
67
|
+
### Performance & Resource Management
|
|
68
|
+
|
|
69
|
+
| Variable | Type | Default | Description |
|
|
70
|
+
|----------|------|---------|-------------|
|
|
71
|
+
| `SOAJS_MAX_BODY_SIZE` | number | `10485760` (10MB) | Maximum request body size in bytes for monitoring. Prevents memory exhaustion from large request bodies |
|
|
72
|
+
| `SOAJS_THROTTLE_CLEANUP_INTERVAL` | number | `60000` (1 min) | Interval in milliseconds for cleaning up expired throttling cache entries |
|
|
73
|
+
|
|
74
|
+
### Database & Connection
|
|
75
|
+
|
|
76
|
+
| Variable | Type | Default | Description |
|
|
77
|
+
|----------|------|---------|-------------|
|
|
78
|
+
| `SOAJS_MONGO_CON_KEEPALIVE` | boolean | `false` | Enable MongoDB connection keepalive |
|
|
79
|
+
|
|
80
|
+
### Testing & Development
|
|
81
|
+
|
|
82
|
+
| Variable | Type | Default | Description |
|
|
83
|
+
|----------|------|---------|-------------|
|
|
84
|
+
| `SOAJS_TEST` | boolean | - | Enable test mode (affects registry loading and service queries) |
|
|
85
|
+
| `SOAJS_IMPORTER_DROPDB` | boolean | `false` | Drop database before importing (test data only) |
|
|
86
|
+
| `NODE_ENV` | string | - | Node.js environment mode (development, production) |
|
|
87
|
+
| `APP_DIR_FOR_CODE_COVERAGE` | string | `../` | Directory path for code coverage testing |
|
|
88
|
+
| `SHOW_LOGS` | boolean | - | Enable/disable log output for testing |
|
|
89
|
+
|
|
90
|
+
### Example Configurations
|
|
91
|
+
|
|
92
|
+
#### Production Kubernetes Deployment
|
|
93
|
+
```bash
|
|
94
|
+
export SOAJS_ENV=production
|
|
95
|
+
export SOAJS_DEPLOY_HA=kubernetes
|
|
96
|
+
export SOAJS_SRVIP=10.0.1.50
|
|
97
|
+
export SOAJS_PROFILE=/etc/soajs/profile.js
|
|
98
|
+
export SOAJS_SENSITIVE_ENVS='["dashboard","production"]'
|
|
99
|
+
export SOAJS_MAX_BODY_SIZE=20971520 # 20MB
|
|
100
|
+
export SOAJS_THROTTLE_CLEANUP_INTERVAL=120000 # 2 minutes
|
|
101
|
+
export SOAJS_MONGO_CON_KEEPALIVE=true
|
|
102
|
+
export NODE_ENV=production
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
#### Solo Development Mode
|
|
106
|
+
```bash
|
|
107
|
+
export SOAJS_ENV=dev
|
|
108
|
+
export SOAJS_SOLO=true
|
|
109
|
+
export SOAJS_SRVIP=127.0.0.1
|
|
110
|
+
export SOAJS_PROFILE=/opt/soajs/profiles/dev.js
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
#### Docker Swarm Deployment
|
|
114
|
+
```bash
|
|
115
|
+
export SOAJS_ENV=staging
|
|
116
|
+
export SOAJS_DEPLOY_HA=swarm
|
|
117
|
+
export SOAJS_SWARM_UNIX_PORT=/var/run/docker.sock
|
|
118
|
+
export SOAJS_PROFILE=/config/soajs_profile.js
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
38
123
|
More information on the Controller is available on the website section [Controller](https://soajsorg.atlassian.net/wiki/x/loAwb)
|
|
39
124
|
|
|
40
125
|
|
package/lib/request.js
CHANGED
|
@@ -16,10 +16,12 @@ function checkStatus(uri) {
|
|
|
16
16
|
let onResponse = false;
|
|
17
17
|
|
|
18
18
|
const req = http.get(uri, (res) => {
|
|
19
|
+
onResponse = true;
|
|
20
|
+
res.resume();
|
|
19
21
|
if (res.statusCode === 200) {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
22
|
+
resolve(true);
|
|
23
|
+
} else {
|
|
24
|
+
reject(new Error(`Status Code: ${res.statusCode}`));
|
|
23
25
|
}
|
|
24
26
|
// res.on('data', () => { }); // Consume data to avoid issues (important!)
|
|
25
27
|
|
|
@@ -109,19 +111,21 @@ function httpRequestLight({ uri, data = null, method = 'GET', headers = null, js
|
|
|
109
111
|
return reject(new Error(`Status Code: ${res.statusCode}`));
|
|
110
112
|
}
|
|
111
113
|
|
|
112
|
-
|
|
114
|
+
const chunks = [];
|
|
113
115
|
|
|
114
116
|
res.on('data', (chunk) => {
|
|
115
|
-
|
|
117
|
+
chunks.push(chunk);
|
|
116
118
|
});
|
|
117
119
|
|
|
118
120
|
res.on('end', () => {
|
|
119
121
|
if (!onResponse) {
|
|
120
122
|
onResponse = true;
|
|
121
123
|
try {
|
|
124
|
+
const responseData = Buffer.concat(chunks).toString();
|
|
122
125
|
const parsedData = json ? JSON.parse(responseData) : responseData;
|
|
123
126
|
return resolve(parsedData);
|
|
124
127
|
} catch (error) {
|
|
128
|
+
const responseData = Buffer.concat(chunks).toString();
|
|
125
129
|
return resolve(responseData);
|
|
126
130
|
}
|
|
127
131
|
}
|
|
@@ -235,15 +239,17 @@ function httpRequest({ uri, data = null, body = null, qs = null, method = 'GET',
|
|
|
235
239
|
}
|
|
236
240
|
|
|
237
241
|
req.on('response', (res) => { // Listen for the 'response' event
|
|
238
|
-
|
|
242
|
+
const chunks = [];
|
|
239
243
|
|
|
240
244
|
res.on('data', (chunk) => {
|
|
241
|
-
|
|
245
|
+
chunks.push(chunk);
|
|
242
246
|
});
|
|
243
247
|
|
|
244
248
|
res.on('end', () => {
|
|
245
249
|
if (!onResponse) {
|
|
246
250
|
onResponse = true;
|
|
251
|
+
const responseData = Buffer.concat(chunks).toString();
|
|
252
|
+
|
|
247
253
|
if (res.statusCode < 200 || res.statusCode >= 300) {
|
|
248
254
|
const error = new Error(`Status Code: ${res.statusCode}`);
|
|
249
255
|
try {
|
|
@@ -295,7 +301,7 @@ function httpRequest({ uri, data = null, body = null, qs = null, method = 'GET',
|
|
|
295
301
|
req.destroy(); // IMPORTANT: Forcefully end the request
|
|
296
302
|
if (!onResponse) {
|
|
297
303
|
onResponse = true;
|
|
298
|
-
return reject(new Error('Request timed out'));
|
|
304
|
+
return reject({ error: new Error('Request timed out'), body: null });
|
|
299
305
|
}
|
|
300
306
|
});
|
|
301
307
|
// Set the timeout on the request
|
|
@@ -34,7 +34,7 @@ if (process.env.SOAJS_SENSITIVE_ENVS) {
|
|
|
34
34
|
} catch (e) {
|
|
35
35
|
temp_sensitiveEnvCodes = null;
|
|
36
36
|
}
|
|
37
|
-
if (Array.isArray(temp_sensitiveEnvCodes) && temp_sensitiveEnvCodes > 0) {
|
|
37
|
+
if (Array.isArray(temp_sensitiveEnvCodes) && temp_sensitiveEnvCodes.length > 0) {
|
|
38
38
|
sensitiveEnvCodes = temp_sensitiveEnvCodes;
|
|
39
39
|
}
|
|
40
40
|
}
|
|
@@ -496,15 +496,25 @@ function getRegistry(param, options, cb) {
|
|
|
496
496
|
registry_struct[options.envCode] = registry;
|
|
497
497
|
}
|
|
498
498
|
if (registry && registry.serviceConfig.awareness.autoRelaodRegistry) {
|
|
499
|
+
if (!autoReloadTimeout[options.envCode]) {
|
|
500
|
+
autoReloadTimeout[options.envCode] = {};
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// Performance: Prevent overlapping reloads with status tracking
|
|
504
|
+
if (autoReloadTimeout[options.envCode].reloading) {
|
|
505
|
+
// Already reloading, skip scheduling another
|
|
506
|
+
return cb(null, registry_struct[options.envCode]);
|
|
507
|
+
}
|
|
508
|
+
|
|
499
509
|
let autoReload = () => {
|
|
510
|
+
autoReloadTimeout[options.envCode].reloading = true;
|
|
500
511
|
options.setBy = "autoReload";
|
|
501
512
|
getRegistry(param, options, () => {
|
|
513
|
+
autoReloadTimeout[options.envCode].reloading = false;
|
|
502
514
|
// cb(err, reg);
|
|
503
515
|
});
|
|
504
516
|
};
|
|
505
|
-
|
|
506
|
-
autoReloadTimeout[options.envCode] = {};
|
|
507
|
-
}
|
|
517
|
+
|
|
508
518
|
if (autoReloadTimeout[options.envCode].timeout) {
|
|
509
519
|
clearTimeout(autoReloadTimeout[options.envCode].timeout);
|
|
510
520
|
}
|
|
@@ -76,10 +76,17 @@ module.exports = (configuration) => {
|
|
|
76
76
|
};
|
|
77
77
|
|
|
78
78
|
proxyRequest(req, res, requestConfig, extraOptions)
|
|
79
|
-
.then(() => {
|
|
79
|
+
.then(() => {
|
|
80
|
+
// Performance: Clean up proxy request reference after success
|
|
81
|
+
if (req.soajs.controller.redirectedRequest) {
|
|
82
|
+
req.soajs.controller.redirectedRequest.removeAllListeners();
|
|
83
|
+
req.soajs.controller.redirectedRequest = null;
|
|
84
|
+
}
|
|
85
|
+
})
|
|
80
86
|
.catch((error) => {
|
|
81
87
|
req.soajs.log.error(error.message);
|
|
82
88
|
if (req.soajs.controller.redirectedRequest) {
|
|
89
|
+
req.soajs.controller.redirectedRequest.removeAllListeners();
|
|
83
90
|
req.soajs.controller.redirectedRequest.destroy();
|
|
84
91
|
req.soajs.controller.redirectedRequest = null;
|
|
85
92
|
}
|
|
@@ -116,6 +116,11 @@ module.exports = (configuration) => {
|
|
|
116
116
|
(req.method === 'POST' || req.method === 'PUT' || req.method === 'PATCH' || req.method === 'DELETE')) {
|
|
117
117
|
let allowedContentType = false;
|
|
118
118
|
let isStream = false;
|
|
119
|
+
// Performance: Prevent memory exhaustion from large request bodies
|
|
120
|
+
const MAX_BODY_SIZE = process.env.SOAJS_MAX_BODY_SIZE || 10 * 1024 * 1024; // 10MB default
|
|
121
|
+
let bodySize = 0;
|
|
122
|
+
let bodyExceeded = false;
|
|
123
|
+
|
|
119
124
|
req.on("data", (chunk) => {
|
|
120
125
|
let resContentType = req.headers['content-type'];
|
|
121
126
|
if (resContentType) {
|
|
@@ -127,11 +132,19 @@ module.exports = (configuration) => {
|
|
|
127
132
|
}
|
|
128
133
|
}
|
|
129
134
|
if (!isStream && allowedContentType) {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
+
bodySize += chunk.length;
|
|
136
|
+
if (bodySize > MAX_BODY_SIZE) {
|
|
137
|
+
if (!bodyExceeded) {
|
|
138
|
+
bodyExceeded = true;
|
|
139
|
+
monitoObj.body = "{\"error\": \"Request body exceeds maximum size limit\"}";
|
|
140
|
+
}
|
|
141
|
+
} else if (!bodyExceeded) {
|
|
142
|
+
if (!monitoObj.body) {
|
|
143
|
+
monitoObj.time.req_body_start = new Date().getTime();
|
|
144
|
+
monitoObj.body = chunk;
|
|
145
|
+
} else {
|
|
146
|
+
monitoObj.body += chunk;
|
|
147
|
+
}
|
|
135
148
|
}
|
|
136
149
|
} else {
|
|
137
150
|
monitoObj.body = "{\"contentType\": \"is stream or not allowed\", \"value': \"" + resContentType + "\"}";
|
package/mw/mt/index.js
CHANGED
|
@@ -339,15 +339,30 @@ module.exports = (configuration) => {
|
|
|
339
339
|
if (err) {
|
|
340
340
|
req.soajs.log.error(err.message);
|
|
341
341
|
}
|
|
342
|
+
|
|
343
|
+
// Security: Validate and limit soajsinjectobj header size
|
|
342
344
|
let _h = JSON.stringify(injectObj);
|
|
343
|
-
|
|
344
|
-
|
|
345
|
+
const MAX_HEADER_SIZE = process.env.SOAJS_MAX_INJECT_HEADER_SIZE || 65536; // 64KB default
|
|
346
|
+
|
|
347
|
+
if (_h.length > MAX_HEADER_SIZE) {
|
|
348
|
+
req.soajs.log.error('soajsinjectobj header exceeds maximum size:', _h.length);
|
|
349
|
+
return next(135);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
req.headers.soajsinjectobj = _h;
|
|
345
353
|
return next();
|
|
346
354
|
});
|
|
347
355
|
} else {
|
|
356
|
+
// Security: Validate and limit soajsinjectobj header size
|
|
348
357
|
let _h = JSON.stringify(injectObj);
|
|
349
|
-
|
|
350
|
-
|
|
358
|
+
const MAX_HEADER_SIZE = process.env.SOAJS_MAX_INJECT_HEADER_SIZE || 65536; // 64KB default
|
|
359
|
+
|
|
360
|
+
if (_h.length > MAX_HEADER_SIZE) {
|
|
361
|
+
req.soajs.log.error('soajsinjectobj header exceeds maximum size:', _h.length);
|
|
362
|
+
return next(135);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
req.headers.soajsinjectobj = _h;
|
|
351
366
|
return next();
|
|
352
367
|
}
|
|
353
368
|
}
|
package/mw/mt/utils.js
CHANGED
|
@@ -20,6 +20,35 @@ const merge = require('merge');
|
|
|
20
20
|
|
|
21
21
|
const UracDriver = require("./urac.js");
|
|
22
22
|
|
|
23
|
+
// Performance: Configuration accessor utility for deep property access with defaults
|
|
24
|
+
function getConfig(obj, path, defaultValue = undefined) {
|
|
25
|
+
const keys = path.split('.');
|
|
26
|
+
let result = obj;
|
|
27
|
+
for (const key of keys) {
|
|
28
|
+
if (result && typeof result === 'object' && key in result) {
|
|
29
|
+
result = result[key];
|
|
30
|
+
} else {
|
|
31
|
+
return defaultValue;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return result;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Performance: Cache Netmask objects to avoid repeated parsing
|
|
38
|
+
const netmaskCache = new Map();
|
|
39
|
+
function getCachedNetmask(addr) {
|
|
40
|
+
if (!netmaskCache.has(addr)) {
|
|
41
|
+
try {
|
|
42
|
+
netmaskCache.set(addr, new Netmask(addr));
|
|
43
|
+
} catch (err) {
|
|
44
|
+
// Store null for invalid addresses to avoid repeated parsing attempts
|
|
45
|
+
netmaskCache.set(addr, null);
|
|
46
|
+
throw err;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return netmaskCache.get(addr);
|
|
50
|
+
}
|
|
51
|
+
|
|
23
52
|
let sensitiveEnvCodes = ["dashboard"];
|
|
24
53
|
if (process.env.SOAJS_SENSITIVE_ENVS) {
|
|
25
54
|
let temp_sensitiveEnvCodes = null;
|
|
@@ -28,24 +57,36 @@ if (process.env.SOAJS_SENSITIVE_ENVS) {
|
|
|
28
57
|
} catch (e) {
|
|
29
58
|
temp_sensitiveEnvCodes = null;
|
|
30
59
|
}
|
|
31
|
-
if (Array.isArray(temp_sensitiveEnvCodes) && temp_sensitiveEnvCodes > 0) {
|
|
60
|
+
if (Array.isArray(temp_sensitiveEnvCodes) && temp_sensitiveEnvCodes.length > 0) {
|
|
32
61
|
sensitiveEnvCodes = temp_sensitiveEnvCodes;
|
|
33
62
|
}
|
|
34
63
|
}
|
|
35
64
|
|
|
36
65
|
const { pathToRegexp } = require("path-to-regexp");
|
|
37
66
|
|
|
67
|
+
// Performance: Cache compiled RegExp objects for route matching
|
|
68
|
+
const regexpCache = new Map();
|
|
69
|
+
const MAX_REGEXP_CACHE_SIZE = 500; // Prevent unbounded growth
|
|
70
|
+
|
|
38
71
|
function constructRegExp(route) {
|
|
39
|
-
//
|
|
40
|
-
|
|
72
|
+
// Check cache first
|
|
73
|
+
if (regexpCache.has(route)) {
|
|
74
|
+
return regexpCache.get(route);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Compile the route pattern
|
|
41
78
|
let out = pathToRegexp(route, { sensitive: true });
|
|
42
|
-
|
|
43
|
-
|
|
79
|
+
let regexp = out.regexp || out;
|
|
80
|
+
|
|
81
|
+
// Implement simple LRU by clearing cache when it gets too large
|
|
82
|
+
if (regexpCache.size >= MAX_REGEXP_CACHE_SIZE) {
|
|
83
|
+
// Clear oldest 25% of entries
|
|
84
|
+
const keysToDelete = Array.from(regexpCache.keys()).slice(0, Math.floor(MAX_REGEXP_CACHE_SIZE * 0.25));
|
|
85
|
+
keysToDelete.forEach(key => regexpCache.delete(key));
|
|
44
86
|
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
return out;
|
|
87
|
+
|
|
88
|
+
regexpCache.set(route, regexp);
|
|
89
|
+
return regexp;
|
|
49
90
|
}
|
|
50
91
|
|
|
51
92
|
/**
|
|
@@ -139,20 +180,15 @@ let utils = {
|
|
|
139
180
|
}
|
|
140
181
|
*
|
|
141
182
|
*/
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
obj.req.soajs.registry.custom.gateway.value &&
|
|
146
|
-
obj.req.soajs.registry.custom.gateway.value.mt &&
|
|
147
|
-
obj.req.soajs.registry.custom.gateway.value.mt.whitelist &&
|
|
148
|
-
obj.req.soajs.registry.custom.gateway.value.mt.whitelist.ips) {
|
|
183
|
+
// Performance: Use config accessor to simplify deeply nested property access
|
|
184
|
+
const whitelistIps = getConfig(obj.req, 'soajs.registry.custom.gateway.value.mt.whitelist.ips');
|
|
185
|
+
if (whitelistIps && Array.isArray(whitelistIps)) {
|
|
149
186
|
let clientIp = obj.req.getClientIP();
|
|
150
|
-
let geoAccess = obj.req.soajs.registry.custom.gateway.value.mt.whitelist.ips; //["127.0.0.0/8"];
|
|
151
187
|
let checkAccess = (geoAccessArr, ip) => {
|
|
152
188
|
return (geoAccessArr.some(function (addr) {
|
|
153
189
|
try {
|
|
154
|
-
let block =
|
|
155
|
-
return block.contains(ip);
|
|
190
|
+
let block = getCachedNetmask(addr);
|
|
191
|
+
return block && block.contains(ip);
|
|
156
192
|
} catch (err) {
|
|
157
193
|
obj.req.soajs.log.error('IP whitelist security configuration failed: ', addr);
|
|
158
194
|
obj.req.soajs.log.error(err);
|
|
@@ -160,23 +196,22 @@ let utils = {
|
|
|
160
196
|
return false;
|
|
161
197
|
}));
|
|
162
198
|
};
|
|
163
|
-
if (clientIp
|
|
164
|
-
let matched = checkAccess(
|
|
199
|
+
if (clientIp) {
|
|
200
|
+
let matched = checkAccess(whitelistIps, clientIp);
|
|
165
201
|
if (matched) {
|
|
166
202
|
obj.req.soajs.log.debug("ACL skip detected for ip: " + clientIp);
|
|
167
|
-
|
|
203
|
+
const whitelistConfig = getConfig(obj.req, 'soajs.registry.custom.gateway.value.mt.whitelist', {});
|
|
204
|
+
if (whitelistConfig.acl) {
|
|
168
205
|
obj.skipACL = true;
|
|
169
206
|
}
|
|
170
|
-
if (
|
|
207
|
+
if (whitelistConfig.oauth) {
|
|
171
208
|
obj.skipOAUTH = true;
|
|
172
209
|
}
|
|
173
210
|
}
|
|
174
211
|
}
|
|
175
|
-
|
|
176
|
-
return cb(null, obj);
|
|
177
|
-
} else {
|
|
178
|
-
return cb(null, obj);
|
|
179
212
|
}
|
|
213
|
+
|
|
214
|
+
return cb(null, obj);
|
|
180
215
|
},
|
|
181
216
|
|
|
182
217
|
"aclUrackCheck": (obj, cb) => {
|
|
@@ -282,8 +317,8 @@ let utils = {
|
|
|
282
317
|
let checkAccess = (geoAccessArr, ip) => {
|
|
283
318
|
return (geoAccessArr.some(function (addr) {
|
|
284
319
|
try {
|
|
285
|
-
let block =
|
|
286
|
-
return block.contains(ip);
|
|
320
|
+
let block = getCachedNetmask(addr);
|
|
321
|
+
return block && block.contains(ip);
|
|
287
322
|
} catch (err) {
|
|
288
323
|
obj.req.soajs.log.error('Geographic security configuration failed: ', addr);
|
|
289
324
|
obj.req.soajs.log.error(err.message);
|
package/mw/response/response.js
CHANGED
|
@@ -9,12 +9,56 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
let dataHolder = {};
|
|
12
|
+
let cleanupInterval = null;
|
|
12
13
|
|
|
13
14
|
let model = {
|
|
14
15
|
'log': null,
|
|
15
16
|
'initThrottling': (log) => {
|
|
16
17
|
model.log = log;
|
|
17
18
|
dataHolder = {};
|
|
19
|
+
|
|
20
|
+
// Performance: Implement periodic cleanup of expired throttling entries
|
|
21
|
+
if (cleanupInterval) {
|
|
22
|
+
clearInterval(cleanupInterval);
|
|
23
|
+
}
|
|
24
|
+
const CLEANUP_INTERVAL = process.env.SOAJS_THROTTLE_CLEANUP_INTERVAL || 60000; // 1 minute default
|
|
25
|
+
cleanupInterval = setInterval(() => {
|
|
26
|
+
model.cleanupExpiredEntries();
|
|
27
|
+
}, CLEANUP_INTERVAL);
|
|
28
|
+
},
|
|
29
|
+
|
|
30
|
+
'cleanupExpiredEntries': () => {
|
|
31
|
+
const now = Date.now();
|
|
32
|
+
const MAX_AGE = 3600000; // 1 hour - entries older than this are removed
|
|
33
|
+
let removedCount = 0;
|
|
34
|
+
|
|
35
|
+
for (const l1Key in dataHolder) {
|
|
36
|
+
if (dataHolder.hasOwnProperty(l1Key)) {
|
|
37
|
+
const entry = dataHolder[l1Key];
|
|
38
|
+
|
|
39
|
+
// Check if l1 level entry is expired
|
|
40
|
+
if (entry.firstReqTime && (now - entry.firstReqTime.getTime()) > MAX_AGE) {
|
|
41
|
+
delete dataHolder[l1Key];
|
|
42
|
+
removedCount++;
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Check l2 level entries (if any)
|
|
47
|
+
for (const l2Key in entry) {
|
|
48
|
+
if (entry.hasOwnProperty(l2Key) && l2Key !== 'firstReqTime' && l2Key !== 'count') {
|
|
49
|
+
const l2Entry = entry[l2Key];
|
|
50
|
+
if (l2Entry.firstReqTime && (now - l2Entry.firstReqTime.getTime()) > MAX_AGE) {
|
|
51
|
+
delete entry[l2Key];
|
|
52
|
+
removedCount++;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (removedCount > 0 && model.log) {
|
|
60
|
+
model.log.debug(`Throttling cache cleanup: removed ${removedCount} expired entries`);
|
|
61
|
+
}
|
|
18
62
|
},
|
|
19
63
|
|
|
20
64
|
'getThrottling': (throttling, trafficKey) => {
|
|
@@ -66,8 +110,28 @@ let model = {
|
|
|
66
110
|
|
|
67
111
|
'logTooManyRequests': (trafficKey, throttlingStrategy) => {
|
|
68
112
|
model.log.error('Throttling [' + throttlingStrategy + ']rejected:', 'using tenant id [' + trafficKey.l1 + '] from ip [' + trafficKey.l2 + ']');
|
|
113
|
+
},
|
|
114
|
+
|
|
115
|
+
'shutdown': () => {
|
|
116
|
+
// Performance: Clean up interval on shutdown to prevent memory leaks
|
|
117
|
+
if (cleanupInterval) {
|
|
118
|
+
clearInterval(cleanupInterval);
|
|
119
|
+
cleanupInterval = null;
|
|
120
|
+
}
|
|
121
|
+
dataHolder = {};
|
|
122
|
+
if (model.log) {
|
|
123
|
+
model.log.info('Traffic throttling model shutdown complete');
|
|
124
|
+
}
|
|
69
125
|
}
|
|
70
126
|
};
|
|
71
127
|
|
|
128
|
+
// Performance: Register shutdown handlers
|
|
129
|
+
if (typeof process !== 'undefined') {
|
|
130
|
+
const shutdownHandler = () => {
|
|
131
|
+
model.shutdown();
|
|
132
|
+
};
|
|
133
|
+
process.once('SIGTERM', shutdownHandler);
|
|
134
|
+
process.once('SIGINT', shutdownHandler);
|
|
135
|
+
}
|
|
72
136
|
|
|
73
137
|
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.41",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "soajs team",
|
|
7
7
|
"email": "team@soajs.org"
|
|
@@ -29,14 +29,14 @@
|
|
|
29
29
|
"oauth2-server": "2.4.1",
|
|
30
30
|
"path-to-regexp": "8.3.0",
|
|
31
31
|
"soajs.core.libs": "1.2.1",
|
|
32
|
-
"soajs.core.modules": "5.2.
|
|
33
|
-
"soajs.urac.driver": "3.2.
|
|
32
|
+
"soajs.core.modules": "5.2.17",
|
|
33
|
+
"soajs.urac.driver": "3.2.14",
|
|
34
34
|
"useragent": "2.3.0"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
|
-
"@babel/core": "7.28.
|
|
38
|
-
"@babel/plugin-transform-runtime": "7.28.
|
|
39
|
-
"@babel/preset-env": "7.28.
|
|
37
|
+
"@babel/core": "7.28.5",
|
|
38
|
+
"@babel/plugin-transform-runtime": "7.28.5",
|
|
39
|
+
"@babel/preset-env": "7.28.5",
|
|
40
40
|
"@babel/runtime": "7.28.4",
|
|
41
41
|
"express": "4.21.1",
|
|
42
42
|
"grunt": "1.6.1",
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
"grunt-istanbul": "0.8.0",
|
|
50
50
|
"grunt-jsdoc": "2.4.1",
|
|
51
51
|
"grunt-mocha-test": "0.13.3",
|
|
52
|
-
"mocha": "11.7.
|
|
52
|
+
"mocha": "11.7.5",
|
|
53
53
|
"nock": "14.0.10",
|
|
54
54
|
"request": "2.88.2",
|
|
55
55
|
"sinon": "21.0.0"
|