@aikidosec/broker-client 1.0.3 → 1.0.4
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/app/client.js +7 -31
- package/app/resourceManager.js +67 -0
- package/package.json +2 -1
package/app/client.js
CHANGED
|
@@ -12,6 +12,7 @@ import { Address4, Address6 } from 'ip-address';
|
|
|
12
12
|
import dns from 'native-dns';
|
|
13
13
|
import fs from 'fs';
|
|
14
14
|
import { ResourceManager } from './resourceManager.js';
|
|
15
|
+
import { HttpsProxyAgent } from 'https-proxy-agent';
|
|
15
16
|
|
|
16
17
|
// Configure logging
|
|
17
18
|
const log = {
|
|
@@ -213,36 +214,9 @@ function isInternalUrl(url) {
|
|
|
213
214
|
}
|
|
214
215
|
}
|
|
215
216
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
function isResourceAllowed(resource) {
|
|
220
|
-
const registeredResources = resourceManager.getResources();
|
|
221
|
-
|
|
222
|
-
// If no resources registered yet, deny (customer must register resources first)
|
|
223
|
-
if (Object.keys(registeredResources).length === 0) {
|
|
224
|
-
log.warn("No resources registered - customer must register resources via setup mode");
|
|
225
|
-
return false;
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
// Normalize the target URL for comparison
|
|
229
|
-
const resourceParsed = new URL(resource);
|
|
230
|
-
const resourceNormalized = `${resourceParsed.protocol}//${resourceParsed.host}${resourceParsed.pathname || '/'}`;
|
|
231
|
-
|
|
232
|
-
// Check if the exact base_url is registered
|
|
233
|
-
for (const baseUrl of Object.keys(registeredResources)) {
|
|
234
|
-
const baseParsed = new URL(baseUrl);
|
|
235
|
-
const baseNormalized = `${baseParsed.protocol}//${baseParsed.host}${baseParsed.pathname || '/'}`;
|
|
236
|
-
|
|
237
|
-
// Allow if target URL starts with registered base URL
|
|
238
|
-
if (resourceNormalized.startsWith(baseNormalized)) {
|
|
239
|
-
log.info(`Resource ${resource} matches registered base_url ${baseUrl}`);
|
|
240
|
-
return true;
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
log.warn(`Resource ${resource} not found in registered resources: ${Object.keys(registeredResources)}`);
|
|
245
|
-
return false;
|
|
217
|
+
let agent;
|
|
218
|
+
if (process.env.HTTPS_PROXY || process.env.ALL_PROXY) {
|
|
219
|
+
agent = new HttpsProxyAgent(process.env.HTTPS_PROXY || process.env.ALL_PROXY);
|
|
246
220
|
}
|
|
247
221
|
|
|
248
222
|
// Create Socket.IO client
|
|
@@ -250,12 +224,14 @@ const socket = io(SERVER_URL, {
|
|
|
250
224
|
auth: {
|
|
251
225
|
client_secret: CLIENT_SECRET
|
|
252
226
|
},
|
|
227
|
+
agent,
|
|
253
228
|
transports: ['websocket', 'polling'],
|
|
254
229
|
reconnection: true,
|
|
255
230
|
reconnectionAttempts: Infinity,
|
|
256
231
|
reconnectionDelay: 1000,
|
|
257
232
|
reconnectionDelayMax: 30000,
|
|
258
233
|
randomizationFactor: 0.5,
|
|
234
|
+
tryAllTransports: true, // if we don't, it won't try to fallback from websocket to polling
|
|
259
235
|
autoConnect: false // Don't connect until after registration
|
|
260
236
|
});
|
|
261
237
|
|
|
@@ -329,7 +305,7 @@ socket.on('forward_request', async (data, callback) => {
|
|
|
329
305
|
return;
|
|
330
306
|
}
|
|
331
307
|
|
|
332
|
-
if (!
|
|
308
|
+
if (!(await resourceManager.isResourceAllowedWithSync(targetUrl))) {
|
|
333
309
|
callback({
|
|
334
310
|
request_id: requestId,
|
|
335
311
|
status_code: 403,
|
package/app/resourceManager.js
CHANGED
|
@@ -29,6 +29,8 @@ export class ResourceManager {
|
|
|
29
29
|
this._serverUrl = null;
|
|
30
30
|
this._clientId = null;
|
|
31
31
|
this._clientSecret = null;
|
|
32
|
+
this._lastResourceSync = 0;
|
|
33
|
+
this._syncThrottleMs = 30000; // 30 seconds
|
|
32
34
|
this._loadFromFile();
|
|
33
35
|
}
|
|
34
36
|
|
|
@@ -114,6 +116,70 @@ export class ResourceManager {
|
|
|
114
116
|
return { ...this._resources };
|
|
115
117
|
}
|
|
116
118
|
|
|
119
|
+
/**
|
|
120
|
+
* Check if a resource URL is allowed (registered).
|
|
121
|
+
* @param {string} resource - The full URL to check
|
|
122
|
+
* @returns {boolean} True if allowed, false otherwise
|
|
123
|
+
*/
|
|
124
|
+
isResourceAllowed(resource) {
|
|
125
|
+
// If no resources registered yet, deny
|
|
126
|
+
if (Object.keys(this._resources).length === 0) {
|
|
127
|
+
log.warn("No resources registered - customer must register resources via setup mode");
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Normalize the target URL for comparison
|
|
132
|
+
const resourceParsed = new URL(resource);
|
|
133
|
+
const resourceNormalized = `${resourceParsed.protocol}//${resourceParsed.host}${resourceParsed.pathname || '/'}`;
|
|
134
|
+
|
|
135
|
+
// Check if the exact base_url is registered
|
|
136
|
+
for (const baseUrl of Object.keys(this._resources)) {
|
|
137
|
+
const baseParsed = new URL(baseUrl);
|
|
138
|
+
const baseNormalized = `${baseParsed.protocol}//${baseParsed.host}${baseParsed.pathname || '/'}`;
|
|
139
|
+
|
|
140
|
+
// Allow if target URL starts with registered base URL
|
|
141
|
+
if (resourceNormalized.startsWith(baseNormalized)) {
|
|
142
|
+
log.info(`Resource ${resource} matches registered base_url ${baseUrl}`);
|
|
143
|
+
return true;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
log.warn(`Resource ${resource} not found in registered resources: ${Object.keys(this._resources)}`);
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Check if a resource is allowed, with automatic sync retry if not found.
|
|
153
|
+
* Syncs from broker if resource not found and throttle period has passed.
|
|
154
|
+
* @param {string} resource - The full URL to check
|
|
155
|
+
* @returns {Promise<boolean>} True if allowed, false otherwise
|
|
156
|
+
*/
|
|
157
|
+
async isResourceAllowedWithSync(resource) {
|
|
158
|
+
// First check without syncing
|
|
159
|
+
if (this.isResourceAllowed(resource)) {
|
|
160
|
+
return true;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Resource not found - try syncing from broker if enough time has passed
|
|
164
|
+
const now = Date.now();
|
|
165
|
+
if (now - this._lastResourceSync > this._syncThrottleMs) {
|
|
166
|
+
log.info(`Resource ${resource} not found, syncing from broker...`);
|
|
167
|
+
this._lastResourceSync = now;
|
|
168
|
+
try {
|
|
169
|
+
await this.syncFromBroker();
|
|
170
|
+
// Check again after sync
|
|
171
|
+
if (this.isResourceAllowed(resource)) {
|
|
172
|
+
log.info(`Resource ${resource} found after sync`);
|
|
173
|
+
return true;
|
|
174
|
+
}
|
|
175
|
+
} catch (error) {
|
|
176
|
+
log.error(`Failed to sync resources: ${error.message}`);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
|
|
117
183
|
/**
|
|
118
184
|
* Get resource_id for a base_url.
|
|
119
185
|
* @param {string} baseUrl - The base URL to look up
|
|
@@ -177,6 +243,7 @@ export class ResourceManager {
|
|
|
177
243
|
|
|
178
244
|
// Sync from broker response
|
|
179
245
|
this.syncFromCore(resources);
|
|
246
|
+
this._lastResourceSync = Date.now();
|
|
180
247
|
log.info(`✓ Synced ${Object.keys(resources).length} resources from broker`);
|
|
181
248
|
} else {
|
|
182
249
|
log.warn(`Failed to fetch resources: ${response.status} - ${response.data}`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aikidosec/broker-client",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.4",
|
|
4
4
|
"description": "Aikido Broker Client - Runs in customer network to forward requests to internal resources",
|
|
5
5
|
"main": "app/client.js",
|
|
6
6
|
"type": "module",
|
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
35
|
"axios": "1.13.2",
|
|
36
|
+
"https-proxy-agent": "^7.0.6",
|
|
36
37
|
"ip-address": "10.0.1",
|
|
37
38
|
"native-dns": "0.7.0",
|
|
38
39
|
"socket.io-client": "4.8.1"
|