soajs.controller 4.2.43 → 4.3.1
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/.github/workflows/ci.yml +2 -2
- package/.github/workflows/release.yml +5 -3
- package/docs/middleware/mt.md +382 -2
- package/mw/lastSeen/index.js +133 -42
- package/package.json +9 -9
- package/test/unit/mw/lastSeen/index.js +946 -2
package/.github/workflows/ci.yml
CHANGED
|
@@ -12,7 +12,7 @@ jobs:
|
|
|
12
12
|
|
|
13
13
|
services:
|
|
14
14
|
mongodb:
|
|
15
|
-
image: mongo:
|
|
15
|
+
image: mongo:8
|
|
16
16
|
ports:
|
|
17
17
|
- 27017:27017
|
|
18
18
|
|
|
@@ -23,7 +23,7 @@ jobs:
|
|
|
23
23
|
- name: Setup Node.js
|
|
24
24
|
uses: actions/setup-node@v4
|
|
25
25
|
with:
|
|
26
|
-
node-version: '
|
|
26
|
+
node-version: '24'
|
|
27
27
|
|
|
28
28
|
- name: Install dependencies
|
|
29
29
|
run: npm install
|
|
@@ -2,7 +2,9 @@ name: Release
|
|
|
2
2
|
|
|
3
3
|
on:
|
|
4
4
|
push:
|
|
5
|
-
branches:
|
|
5
|
+
branches:
|
|
6
|
+
- release
|
|
7
|
+
- 'release/**'
|
|
6
8
|
|
|
7
9
|
jobs:
|
|
8
10
|
test:
|
|
@@ -10,7 +12,7 @@ jobs:
|
|
|
10
12
|
|
|
11
13
|
services:
|
|
12
14
|
mongodb:
|
|
13
|
-
image: mongo:
|
|
15
|
+
image: mongo:8
|
|
14
16
|
ports:
|
|
15
17
|
- 27017:27017
|
|
16
18
|
|
|
@@ -21,7 +23,7 @@ jobs:
|
|
|
21
23
|
- name: Setup Node.js
|
|
22
24
|
uses: actions/setup-node@v4
|
|
23
25
|
with:
|
|
24
|
-
node-version: '
|
|
26
|
+
node-version: '24'
|
|
25
27
|
|
|
26
28
|
- name: Install dependencies
|
|
27
29
|
run: npm install
|
package/docs/middleware/mt.md
CHANGED
|
@@ -82,13 +82,393 @@ The MT middleware handles multi-tenant security checks, URAC integration, and pr
|
|
|
82
82
|
| 1 | `ipWhitelist` | Validates IP against whitelist |
|
|
83
83
|
| 2 | `securityGeoCheck` | Validates geographic location |
|
|
84
84
|
| 3 | `securityDeviceCheck` | Validates device fingerprint |
|
|
85
|
-
| 4 | `aclCheck` |
|
|
85
|
+
| 4 | `aclCheck` | Loads service-level ACL |
|
|
86
86
|
| 5 | `oauthCheck` | Validates OAuth token (if required) |
|
|
87
87
|
| 6 | `uracCheck` | Loads user profile from URAC |
|
|
88
|
-
| 7 | `aclUrackCheck` |
|
|
88
|
+
| 7 | `aclUrackCheck` | Overrides ACL with user-specific ACL |
|
|
89
89
|
| 8 | `serviceCheck` | Validates service access |
|
|
90
90
|
| 9 | `apiCheck` | Validates API access |
|
|
91
91
|
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
## Waterfall Check Details
|
|
95
|
+
|
|
96
|
+
### 1. ipWhitelist
|
|
97
|
+
|
|
98
|
+
**Purpose:** Check if the client IP is in a trusted whitelist, optionally bypassing ACL and OAuth checks.
|
|
99
|
+
|
|
100
|
+
**Location:** `mw/mt/utils.js:166`
|
|
101
|
+
|
|
102
|
+
**Configuration:** `registry.custom.gateway.value.mt.whitelist`
|
|
103
|
+
|
|
104
|
+
```javascript
|
|
105
|
+
{
|
|
106
|
+
"mt": {
|
|
107
|
+
"whitelist": {
|
|
108
|
+
"ips": ["10.0.0.0/8", "192.168.1.0/24", "127.0.0.1"],
|
|
109
|
+
"acl": true, // Skip ACL checks for whitelisted IPs
|
|
110
|
+
"oauth": true // Skip OAuth checks for whitelisted IPs
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
**Behavior:**
|
|
117
|
+
- Uses CIDR notation (Netmask) for IP range matching
|
|
118
|
+
- Sets `obj.skipACL = true` if `whitelist.acl` is enabled and IP matches
|
|
119
|
+
- Sets `obj.skipOAUTH = true` if `whitelist.oauth` is enabled and IP matches
|
|
120
|
+
- Netmask objects are cached for performance
|
|
121
|
+
|
|
122
|
+
**Use Cases:**
|
|
123
|
+
- Trusted internal services calling the gateway
|
|
124
|
+
- Development/testing environments
|
|
125
|
+
- Load balancer health checks
|
|
126
|
+
|
|
127
|
+
---
|
|
128
|
+
|
|
129
|
+
### 2. securityGeoCheck
|
|
130
|
+
|
|
131
|
+
**Purpose:** Validates the client IP against geographic allow/deny lists configured on the tenant key.
|
|
132
|
+
|
|
133
|
+
**Location:** `mw/mt/utils.js:309`
|
|
134
|
+
|
|
135
|
+
**Configuration:** `keyObj.geo`
|
|
136
|
+
|
|
137
|
+
```javascript
|
|
138
|
+
{
|
|
139
|
+
"geo": {
|
|
140
|
+
"allow": ["10.0.0.0/8", "192.168.0.0/16"], // Only these IPs allowed
|
|
141
|
+
"deny": ["10.0.1.0/24"] // Block these IPs
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
**Behavior:**
|
|
147
|
+
- **Deny list checked first:** If client IP matches any deny entry → Error 155
|
|
148
|
+
- **Allow list checked second:** If allow list exists and IP doesn't match → Error 155
|
|
149
|
+
- Uses CIDR notation for IP range matching
|
|
150
|
+
- Skipped if `obj.skipACL = true` (from ipWhitelist)
|
|
151
|
+
|
|
152
|
+
**Error Codes:**
|
|
153
|
+
| Code | Description |
|
|
154
|
+
|------|-------------|
|
|
155
|
+
| 155 | Geographic access denied |
|
|
156
|
+
|
|
157
|
+
---
|
|
158
|
+
|
|
159
|
+
### 3. securityDeviceCheck
|
|
160
|
+
|
|
161
|
+
**Purpose:** Validates the client device/browser against allow/deny lists configured on the tenant key.
|
|
162
|
+
|
|
163
|
+
**Location:** `mw/mt/utils.js:355`
|
|
164
|
+
|
|
165
|
+
**Configuration:** `keyObj.device`
|
|
166
|
+
|
|
167
|
+
```javascript
|
|
168
|
+
{
|
|
169
|
+
"device": {
|
|
170
|
+
"allow": [
|
|
171
|
+
{ "family": "Chrome" },
|
|
172
|
+
{ "family": "Firefox", "major": { "min": "90" } },
|
|
173
|
+
{ "family": "Safari", "os": { "family": "Mac OS X" } }
|
|
174
|
+
],
|
|
175
|
+
"deny": [
|
|
176
|
+
{ "family": "IE" },
|
|
177
|
+
{ "family": "Chrome", "major": { "max": "80" } }
|
|
178
|
+
]
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
**Validation Fields:**
|
|
184
|
+
| Field | Type | Description |
|
|
185
|
+
|-------|------|-------------|
|
|
186
|
+
| `family` | string | Browser family (Chrome, Firefox, Safari, IE, etc.) |
|
|
187
|
+
| `major` | string/object | Major version. Object: `{ "min": "90", "max": "100" }` |
|
|
188
|
+
| `minor` | string/object | Minor version |
|
|
189
|
+
| `patch` | string/object | Patch version |
|
|
190
|
+
| `os.family` | string | OS family (Windows, Mac OS X, Linux, etc.) |
|
|
191
|
+
| `os.major` | string/object | OS major version |
|
|
192
|
+
|
|
193
|
+
**Behavior:**
|
|
194
|
+
- Uses `useragent` library to parse User-Agent header
|
|
195
|
+
- **Deny list checked first:** If device matches any deny entry → Error 156
|
|
196
|
+
- **Allow list checked second:** If allow list exists and device doesn't match → Error 156
|
|
197
|
+
- Wildcard `"*"` matches any value
|
|
198
|
+
- Skipped if `obj.skipACL = true` (from ipWhitelist)
|
|
199
|
+
|
|
200
|
+
**Error Codes:**
|
|
201
|
+
| Code | Description |
|
|
202
|
+
|------|-------------|
|
|
203
|
+
| 156 | Device access denied |
|
|
204
|
+
|
|
205
|
+
---
|
|
206
|
+
|
|
207
|
+
### 4. aclCheck
|
|
208
|
+
|
|
209
|
+
**Purpose:** Loads the pre-computed ACL from `serviceParams.finalAcl` into the waterfall object.
|
|
210
|
+
|
|
211
|
+
**Location:** `mw/mt/utils.js:274`
|
|
212
|
+
|
|
213
|
+
**Behavior:**
|
|
214
|
+
- Simple passthrough that loads `obj.req.soajs.controller.serviceParams.finalAcl` into `obj.finalAcl`
|
|
215
|
+
- The ACL was computed earlier in the [keyACL middleware](./keyACL.md)
|
|
216
|
+
- This ACL will be used by subsequent checks (serviceCheck, apiCheck)
|
|
217
|
+
|
|
218
|
+
---
|
|
219
|
+
|
|
220
|
+
### 5. oauthCheck
|
|
221
|
+
|
|
222
|
+
**Purpose:** Validates OAuth access tokens for private APIs and handles PIN-locked sessions.
|
|
223
|
+
|
|
224
|
+
**Location:** `mw/mt/utils.js:463`
|
|
225
|
+
|
|
226
|
+
**Prerequisites:**
|
|
227
|
+
- `serviceParam.oauth = true` must be set on the service
|
|
228
|
+
- Skipped if `obj.skipOAUTH = true` (from ipWhitelist)
|
|
229
|
+
|
|
230
|
+
**Public vs Private API Detection:**
|
|
231
|
+
| Scenario | ACL Configuration | Result |
|
|
232
|
+
|----------|-------------------|--------|
|
|
233
|
+
| No access restriction | `access: false` or missing | Public |
|
|
234
|
+
| Service restricted, API public | `access: true`, `apis[path].access: false` | Public |
|
|
235
|
+
| Restricted permission, API public | `apisPermission: "restricted"`, `apis[path].access: false` | Public |
|
|
236
|
+
| Any access restriction | `access: true` or `apis[path].access: true` | Private |
|
|
237
|
+
|
|
238
|
+
**PIN Login Support:**
|
|
239
|
+
|
|
240
|
+
PIN login is a two-factor authentication mechanism where users must enter a PIN after OAuth login.
|
|
241
|
+
|
|
242
|
+
**Configuration:** `registry.custom.oauth.value`
|
|
243
|
+
|
|
244
|
+
```javascript
|
|
245
|
+
{
|
|
246
|
+
"oauth": {
|
|
247
|
+
"value": {
|
|
248
|
+
"pinWrapper": {
|
|
249
|
+
"servicename": "custom-service",
|
|
250
|
+
"apiname": "/verify-pin"
|
|
251
|
+
},
|
|
252
|
+
"pinWhitelist": {
|
|
253
|
+
"servicename": {
|
|
254
|
+
"get": {
|
|
255
|
+
"apis": ["/public-api"],
|
|
256
|
+
"regex": ["/user/:id/profile"]
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
**PIN Flow:**
|
|
266
|
+
1. User logs in with OAuth → receives token with `pinLocked: true`
|
|
267
|
+
2. Most APIs blocked (error 145) until PIN verified
|
|
268
|
+
3. PIN whitelist allows specific APIs before verification
|
|
269
|
+
4. After PIN verification, `pinLogin: true` set, full access granted
|
|
270
|
+
|
|
271
|
+
**Error Codes:**
|
|
272
|
+
| Code | Description |
|
|
273
|
+
|------|-------------|
|
|
274
|
+
| 145 | PIN verification required |
|
|
275
|
+
|
|
276
|
+
---
|
|
277
|
+
|
|
278
|
+
### 6. uracCheck
|
|
279
|
+
|
|
280
|
+
**Purpose:** Initializes the URAC driver to load user profile, groups, ACL, and configuration.
|
|
281
|
+
|
|
282
|
+
**Location:** `mw/mt/utils.js:624`
|
|
283
|
+
|
|
284
|
+
**Behavior:**
|
|
285
|
+
- Skipped if `obj.skipOAUTH = true` or no bearer token present
|
|
286
|
+
- Creates `req.soajs.uracDriver` instance
|
|
287
|
+
- Handles **roaming** across environments (e.g., dashboard token used in dev environment)
|
|
288
|
+
|
|
289
|
+
**Roaming Support:**
|
|
290
|
+
|
|
291
|
+
Roaming allows users authenticated in a "sensitive" environment (default: `dashboard`) to access other environments.
|
|
292
|
+
|
|
293
|
+
**Configuration:** Environment variable `SOAJS_SENSITIVE_ENVS`
|
|
294
|
+
```bash
|
|
295
|
+
SOAJS_SENSITIVE_ENVS='["dashboard", "production"]'
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
**Roaming Flow:**
|
|
299
|
+
1. Check if token's environment is in sensitive list AND differs from current
|
|
300
|
+
2. Load tenant data for the token's client ID
|
|
301
|
+
3. Load registry for the token's environment
|
|
302
|
+
4. Set `req.soajs.tenant.roaming` with cross-environment context
|
|
303
|
+
|
|
304
|
+
**Token Account Validation:**
|
|
305
|
+
|
|
306
|
+
Validates that user account hasn't changed since token was issued.
|
|
307
|
+
|
|
308
|
+
**Configuration:** `keyObj.config.gateway.validateTokenAccount`
|
|
309
|
+
|
|
310
|
+
```javascript
|
|
311
|
+
{
|
|
312
|
+
"gateway": {
|
|
313
|
+
"validateTokenAccount": {
|
|
314
|
+
"email": true, // Invalidate if email changed
|
|
315
|
+
"phone": true // Invalidate if phone changed
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
**Error Codes:**
|
|
322
|
+
| Code | Description |
|
|
323
|
+
|------|-------------|
|
|
324
|
+
| 146 | User profile not found or account changed |
|
|
325
|
+
| 170 | Roaming context failed to load |
|
|
326
|
+
|
|
327
|
+
---
|
|
328
|
+
|
|
329
|
+
### 7. aclUrackCheck
|
|
330
|
+
|
|
331
|
+
**Purpose:** Overrides the tenant/package ACL with user-specific ACL from URAC profile.
|
|
332
|
+
|
|
333
|
+
**Location:** `mw/mt/utils.js:217`
|
|
334
|
+
|
|
335
|
+
**Behavior:**
|
|
336
|
+
- Skipped if `obj.skipOAUTH = true` or no URAC driver
|
|
337
|
+
- Retrieves user's personal ACL via `uracDriver.getAcl()`
|
|
338
|
+
- If user ACL exists, it **replaces** the tenant/package ACL
|
|
339
|
+
- Handles version resolution when no version requested
|
|
340
|
+
- Processes method-specific ACL (GET, POST, etc.)
|
|
341
|
+
|
|
342
|
+
**ACL Structure:**
|
|
343
|
+
```javascript
|
|
344
|
+
// User ACL from URAC
|
|
345
|
+
{
|
|
346
|
+
"servicename": {
|
|
347
|
+
"1": { // Version
|
|
348
|
+
"access": true,
|
|
349
|
+
"get": {
|
|
350
|
+
"apis": { "/endpoint": { "access": true } },
|
|
351
|
+
"apisRegExp": [...],
|
|
352
|
+
"apisPermission": "restricted"
|
|
353
|
+
},
|
|
354
|
+
"post": { ... }
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
**Version Resolution:**
|
|
361
|
+
- If request doesn't specify version, finds latest version in user's ACL
|
|
362
|
+
- Uses `coreLibs.version.getLatest()` for semantic version comparison
|
|
363
|
+
|
|
364
|
+
---
|
|
365
|
+
|
|
366
|
+
### 8. serviceCheck
|
|
367
|
+
|
|
368
|
+
**Purpose:** Validates that the tenant/user has access to the requested service.
|
|
369
|
+
|
|
370
|
+
**Location:** `mw/mt/utils.js:289`
|
|
371
|
+
|
|
372
|
+
**Behavior:**
|
|
373
|
+
- Skipped if `obj.skipACL = true` (from ipWhitelist)
|
|
374
|
+
- Checks if `obj.finalAcl` exists for the service
|
|
375
|
+
- If no ACL found → Error 154 (service not accessible)
|
|
376
|
+
|
|
377
|
+
**Error Codes:**
|
|
378
|
+
| Code | Description |
|
|
379
|
+
|------|-------------|
|
|
380
|
+
| 154 | Service not accessible |
|
|
381
|
+
|
|
382
|
+
---
|
|
383
|
+
|
|
384
|
+
### 9. apiCheck
|
|
385
|
+
|
|
386
|
+
**Purpose:** Validates access to the specific API endpoint based on ACL configuration.
|
|
387
|
+
|
|
388
|
+
**Location:** `mw/mt/utils.js:748`
|
|
389
|
+
|
|
390
|
+
**Behavior:**
|
|
391
|
+
- Skipped if `obj.skipACL = true` (from ipWhitelist)
|
|
392
|
+
- Matches API path against `apis` (exact match) or `apisRegExp` (pattern match)
|
|
393
|
+
- Sets `isAPIPublic = true` for traffic middleware throttling strategy
|
|
394
|
+
|
|
395
|
+
**API Matching Order:**
|
|
396
|
+
1. Exact match in `system.apis[path]`
|
|
397
|
+
2. RegExp match in `system.apisRegExp[]`
|
|
398
|
+
|
|
399
|
+
**Access Control Logic:**
|
|
400
|
+
|
|
401
|
+
| ACL Configuration | User State | Result |
|
|
402
|
+
|-------------------|------------|--------|
|
|
403
|
+
| `access: false` | Any | Public API, allowed |
|
|
404
|
+
| `access: true`, no user | - | Error 158 |
|
|
405
|
+
| `access: true`, user logged in | - | Check group permissions |
|
|
406
|
+
| `access: ["group1", "group2"]` | User in group | Allowed |
|
|
407
|
+
| `access: ["group1"]` | User not in group | Error 157/160 |
|
|
408
|
+
| `apisPermission: "restricted"` | API not in list | Error 159 |
|
|
409
|
+
|
|
410
|
+
**Error Codes:**
|
|
411
|
+
| Code | Description |
|
|
412
|
+
|------|-------------|
|
|
413
|
+
| 157 | User not in required access group |
|
|
414
|
+
| 158 | Login required |
|
|
415
|
+
| 159 | API not found (restricted mode) |
|
|
416
|
+
| 160 | User lacks required group membership |
|
|
417
|
+
| 161 | API requires login but user not authenticated |
|
|
418
|
+
|
|
419
|
+
---
|
|
420
|
+
|
|
421
|
+
## Waterfall Execution Flow
|
|
422
|
+
|
|
423
|
+
```
|
|
424
|
+
┌─────────────────┐
|
|
425
|
+
│ ipWhitelist │──▶ Sets skipACL, skipOAUTH flags
|
|
426
|
+
└────────┬────────┘
|
|
427
|
+
│
|
|
428
|
+
▼
|
|
429
|
+
┌─────────────────┐
|
|
430
|
+
│ securityGeoCheck│──▶ Error 155 if geo-blocked
|
|
431
|
+
└────────┬────────┘ (skipped if skipACL)
|
|
432
|
+
│
|
|
433
|
+
▼
|
|
434
|
+
┌─────────────────┐
|
|
435
|
+
│securityDeviceChk│──▶ Error 156 if device blocked
|
|
436
|
+
└────────┬────────┘ (skipped if skipACL)
|
|
437
|
+
│
|
|
438
|
+
▼
|
|
439
|
+
┌─────────────────┐
|
|
440
|
+
│ aclCheck │──▶ Loads finalAcl from keyACL middleware
|
|
441
|
+
└────────┬────────┘
|
|
442
|
+
│
|
|
443
|
+
▼
|
|
444
|
+
┌─────────────────┐
|
|
445
|
+
│ oauthCheck │──▶ Error 145 if PIN required
|
|
446
|
+
└────────┬────────┘ (skipped if skipOAUTH or public API)
|
|
447
|
+
│
|
|
448
|
+
▼
|
|
449
|
+
┌─────────────────┐
|
|
450
|
+
│ uracCheck │──▶ Error 146/170 if user load fails
|
|
451
|
+
└────────┬────────┘ (skipped if skipOAUTH)
|
|
452
|
+
│
|
|
453
|
+
▼
|
|
454
|
+
┌─────────────────┐
|
|
455
|
+
│ aclUrackCheck │──▶ Overrides ACL with user ACL
|
|
456
|
+
└────────┬────────┘ (skipped if skipOAUTH)
|
|
457
|
+
│
|
|
458
|
+
▼
|
|
459
|
+
┌─────────────────┐
|
|
460
|
+
│ serviceCheck │──▶ Error 154 if no service ACL
|
|
461
|
+
└────────┬────────┘ (skipped if skipACL)
|
|
462
|
+
│
|
|
463
|
+
▼
|
|
464
|
+
┌─────────────────┐
|
|
465
|
+
│ apiCheck │──▶ Error 157-161 if access denied
|
|
466
|
+
└────────┬────────┘ (skipped if skipACL)
|
|
467
|
+
│
|
|
468
|
+
▼
|
|
469
|
+
Success
|
|
470
|
+
```
|
|
471
|
+
|
|
92
472
|
## Injection Object Structure
|
|
93
473
|
|
|
94
474
|
The `soajsinjectobj` header contains:
|
package/mw/lastSeen/index.js
CHANGED
|
@@ -9,8 +9,131 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
const { httpRequest } = require("../../lib/request.js");
|
|
12
|
+
const { pathToRegexp } = require("path-to-regexp");
|
|
12
13
|
const get = (p, o) => p.reduce((xs, x) => (xs && xs[x]) ? xs[x] : null, o);
|
|
13
14
|
|
|
15
|
+
// Cache compiled RegExp objects for route pattern matching
|
|
16
|
+
const regexpCache = new Map();
|
|
17
|
+
const MAX_REGEXP_CACHE_SIZE = 100;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Check if a route pattern contains path parameters
|
|
21
|
+
* @param {string} route - The route pattern
|
|
22
|
+
* @returns {boolean}
|
|
23
|
+
*/
|
|
24
|
+
function hasPathParams(route) {
|
|
25
|
+
return route.includes("/:");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Get or create a cached RegExp for a route pattern
|
|
30
|
+
* @param {string} route - The route pattern
|
|
31
|
+
* @returns {RegExp}
|
|
32
|
+
*/
|
|
33
|
+
function getRouteRegExp(route) {
|
|
34
|
+
if (regexpCache.has(route)) {
|
|
35
|
+
return regexpCache.get(route);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
let out = pathToRegexp(route, { sensitive: true });
|
|
39
|
+
let regexp = out.regexp || out;
|
|
40
|
+
|
|
41
|
+
// Simple LRU: clear oldest entries when cache is full
|
|
42
|
+
if (regexpCache.size >= MAX_REGEXP_CACHE_SIZE) {
|
|
43
|
+
const keysToDelete = Array.from(regexpCache.keys()).slice(0, Math.floor(MAX_REGEXP_CACHE_SIZE * 0.25));
|
|
44
|
+
keysToDelete.forEach(key => regexpCache.delete(key));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
regexpCache.set(route, regexp);
|
|
48
|
+
return regexp;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Check if the current request should trigger lastSeen
|
|
53
|
+
* @param {Object} include - The include whitelist configuration
|
|
54
|
+
* @param {string} serviceName - The current service name
|
|
55
|
+
* @param {string} apiPath - The current API path
|
|
56
|
+
* @param {string} method - The HTTP method (GET, POST, etc.)
|
|
57
|
+
* @returns {boolean} - true if lastSeen should be triggered
|
|
58
|
+
*/
|
|
59
|
+
function shouldTriggerLastSeen(include, serviceName, apiPath, method) {
|
|
60
|
+
// No include filter = trigger for all (backward compatible)
|
|
61
|
+
if (!include) {
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Service not in whitelist
|
|
66
|
+
if (!include[serviceName]) {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const serviceConfig = include[serviceName];
|
|
71
|
+
|
|
72
|
+
// Service-level match (all APIs, all methods)
|
|
73
|
+
if (serviceConfig === true) {
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// API-level match
|
|
78
|
+
if (typeof serviceConfig === 'object' && serviceConfig.apis) {
|
|
79
|
+
return isApiMethodMatch(serviceConfig.apis, apiPath, method);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Check if API path and method match the apis configuration
|
|
87
|
+
* @param {Object} apis - The apis configuration object
|
|
88
|
+
* @param {string} apiPath - The current API path
|
|
89
|
+
* @param {string} method - The HTTP method
|
|
90
|
+
* @returns {boolean}
|
|
91
|
+
*/
|
|
92
|
+
function isApiMethodMatch(apis, apiPath, method) {
|
|
93
|
+
// Check for wildcard first
|
|
94
|
+
if (apis["*"]) {
|
|
95
|
+
return isMethodMatch(apis["*"], method);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Check exact match first
|
|
99
|
+
if (apis[apiPath]) {
|
|
100
|
+
return isMethodMatch(apis[apiPath], method);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Check pattern matching for routes with path params
|
|
104
|
+
for (const routePattern of Object.keys(apis)) {
|
|
105
|
+
if (hasPathParams(routePattern)) {
|
|
106
|
+
const regexp = getRouteRegExp(routePattern);
|
|
107
|
+
if (regexp.test(apiPath)) {
|
|
108
|
+
return isMethodMatch(apis[routePattern], method);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Check if method matches the method configuration
|
|
118
|
+
* @param {boolean|Array} methodConfig - true for all methods, or array of methods
|
|
119
|
+
* @param {string} method - The HTTP method
|
|
120
|
+
* @returns {boolean}
|
|
121
|
+
*/
|
|
122
|
+
function isMethodMatch(methodConfig, method) {
|
|
123
|
+
// true = all methods
|
|
124
|
+
if (methodConfig === true) {
|
|
125
|
+
return true;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Array of methods (case-insensitive)
|
|
129
|
+
if (Array.isArray(methodConfig)) {
|
|
130
|
+
const lowerMethod = method.toLowerCase();
|
|
131
|
+
return methodConfig.some(m => m.toLowerCase() === lowerMethod);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
|
|
14
137
|
/**
|
|
15
138
|
*
|
|
16
139
|
* @param configuration
|
|
@@ -25,6 +148,16 @@ module.exports = () => {
|
|
|
25
148
|
let lastSeen = get(["soajs", "registry", "custom", "gateway", "value", "lastSeen"], req);
|
|
26
149
|
if (lastSeen && lastSeen.active) {
|
|
27
150
|
|
|
151
|
+
// Check include filter
|
|
152
|
+
let currentService = get(["soajs", "controller", "serviceParams", "name"], req);
|
|
153
|
+
let currentApi = get(["soajs", "controller", "serviceParams", "parsedUrl", "pathname"], req);
|
|
154
|
+
let currentMethod = req.method;
|
|
155
|
+
|
|
156
|
+
if (!shouldTriggerLastSeen(lastSeen.include, currentService, currentApi, currentMethod)) {
|
|
157
|
+
req.soajs.log.debug("lastSeen skipped for [" + currentService + " " + currentMethod + " " + currentApi + "]");
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
|
|
28
161
|
const item = {
|
|
29
162
|
"name": lastSeen.serviceName || "urac",
|
|
30
163
|
"version": lastSeen.serviceVersion || "3",
|
|
@@ -62,49 +195,7 @@ module.exports = () => {
|
|
|
62
195
|
} else {
|
|
63
196
|
req.soajs.log.debug("lastSeen ", body);
|
|
64
197
|
}
|
|
65
|
-
// else if (!body.result) {
|
|
66
|
-
// let errorMsg = "";
|
|
67
|
-
// if (body.errors && body.errors && body.errors.details && body.errors.details.length > 0) {
|
|
68
|
-
// body.errors.details.forEach((detail) => {
|
|
69
|
-
// if (errorMsg === "") {
|
|
70
|
-
// errorMsg += detail.message;
|
|
71
|
-
// }
|
|
72
|
-
// else {
|
|
73
|
-
// errorMsg += " - " + detail.message;
|
|
74
|
-
// }
|
|
75
|
-
// });
|
|
76
|
-
// }
|
|
77
|
-
// req.soajs.log.debug("lastSeen", errorMsg);
|
|
78
|
-
// }
|
|
79
198
|
});
|
|
80
|
-
|
|
81
|
-
// let options = {
|
|
82
|
-
// method: "post",
|
|
83
|
-
// uri: "http://" + item.host + ":" + item.port + item.api,
|
|
84
|
-
// json: true
|
|
85
|
-
// };
|
|
86
|
-
// if (req.headers && req.headers.soajsinjectobj) {
|
|
87
|
-
// options.headers = { "soajsinjectobj": req.headers.soajsinjectobj };
|
|
88
|
-
// }
|
|
89
|
-
// request(options, (error, response, body) => {
|
|
90
|
-
// if (error) {
|
|
91
|
-
// req.soajs.log.debug("lastSeen failed for [" + item.name + "@" + item.version + " " + item.api + "]");
|
|
92
|
-
// req.soajs.log.debug("lastSeen " + error.message);
|
|
93
|
-
// } else if (!body.result) {
|
|
94
|
-
// let errorMsg = "";
|
|
95
|
-
// if (body.errors && body.errors && body.errors.details && body.errors.details.length > 0) {
|
|
96
|
-
// body.errors.details.forEach((detail) => {
|
|
97
|
-
// if (errorMsg === "") {
|
|
98
|
-
// errorMsg += detail.message;
|
|
99
|
-
// }
|
|
100
|
-
// else {
|
|
101
|
-
// errorMsg += " - " + detail.message;
|
|
102
|
-
// }
|
|
103
|
-
// });
|
|
104
|
-
// }
|
|
105
|
-
// req.soajs.log.debug("lastSeen", errorMsg);
|
|
106
|
-
// }
|
|
107
|
-
// });
|
|
108
199
|
} else {
|
|
109
200
|
req.soajs.log.debug("lastSeen failed with no host for [" + item.name + "@" + item.version + " " + item.api + "]");
|
|
110
201
|
}
|
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.
|
|
4
|
+
"version": "4.3.1",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "soajs team",
|
|
7
7
|
"email": "team@soajs.org"
|
|
@@ -23,20 +23,20 @@
|
|
|
23
23
|
"http-proxy": "1.18.1",
|
|
24
24
|
"jsonwebtoken": "9.0.3",
|
|
25
25
|
"kubernetes-client": "9.0.0",
|
|
26
|
-
"lodash": "4.17.
|
|
26
|
+
"lodash": "4.17.23",
|
|
27
27
|
"merge": "2.1.1",
|
|
28
28
|
"netmask": "2.0.2",
|
|
29
29
|
"oauth2-server": "2.4.1",
|
|
30
30
|
"path-to-regexp": "8.3.0",
|
|
31
|
-
"soajs.core.libs": "1.2.
|
|
32
|
-
"soajs.core.modules": "5.2.
|
|
33
|
-
"soajs.urac.driver": "3.2.
|
|
31
|
+
"soajs.core.libs": "1.2.2",
|
|
32
|
+
"soajs.core.modules": "5.2.18",
|
|
33
|
+
"soajs.urac.driver": "3.2.15",
|
|
34
34
|
"useragent": "2.3.0"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
|
-
"@babel/core": "7.
|
|
38
|
-
"@babel/plugin-transform-runtime": "7.
|
|
39
|
-
"@babel/preset-env": "7.
|
|
37
|
+
"@babel/core": "7.29.0",
|
|
38
|
+
"@babel/plugin-transform-runtime": "7.29.0",
|
|
39
|
+
"@babel/preset-env": "7.29.0",
|
|
40
40
|
"@babel/runtime": "7.28.6",
|
|
41
41
|
"express": "5.2.1",
|
|
42
42
|
"grunt": "1.6.1",
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
"grunt-jsdoc": "2.4.1",
|
|
51
51
|
"grunt-mocha-test": "0.13.3",
|
|
52
52
|
"mocha": "11.7.5",
|
|
53
|
-
"nock": "14.0.
|
|
53
|
+
"nock": "14.0.11",
|
|
54
54
|
"request": "2.88.2",
|
|
55
55
|
"sinon": "21.0.1"
|
|
56
56
|
}
|