@aikidosec/broker-client 1.0.3 → 1.0.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/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 = {
@@ -36,9 +37,12 @@ const DNS_SERVERS = process.env.DNS_SERVERS
36
37
  : null;
37
38
 
38
39
  // Configure axios defaults
40
+ const MAX_RESPONSE_SIZE = 100 * 1024 * 1024; // 100 MB
39
41
  const axiosConfig = {
40
42
  timeout: 30000,
41
- maxRedirects: 5
43
+ maxRedirects: 5,
44
+ maxContentLength: MAX_RESPONSE_SIZE,
45
+ maxBodyLength: MAX_RESPONSE_SIZE
42
46
  };
43
47
 
44
48
  // Create axios instance for internal requests
@@ -213,36 +217,9 @@ function isInternalUrl(url) {
213
217
  }
214
218
  }
215
219
 
216
- /**
217
- * Check if a resource is registered with the broker.
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;
220
+ let agent;
221
+ if (process.env.HTTPS_PROXY || process.env.ALL_PROXY) {
222
+ agent = new HttpsProxyAgent(process.env.HTTPS_PROXY || process.env.ALL_PROXY);
246
223
  }
247
224
 
248
225
  // Create Socket.IO client
@@ -250,13 +227,16 @@ const socket = io(SERVER_URL, {
250
227
  auth: {
251
228
  client_secret: CLIENT_SECRET
252
229
  },
230
+ agent,
253
231
  transports: ['websocket', 'polling'],
254
232
  reconnection: true,
255
233
  reconnectionAttempts: Infinity,
256
234
  reconnectionDelay: 1000,
257
235
  reconnectionDelayMax: 30000,
258
236
  randomizationFactor: 0.5,
259
- autoConnect: false // Don't connect until after registration
237
+ tryAllTransports: true, // if we don't, it won't try to fallback from websocket to polling
238
+ autoConnect: false, // Don't connect until after registration
239
+ withCredentials: true // make sure cookies work for sticky sessions
260
240
  });
261
241
 
262
242
  // Socket.IO event handlers
@@ -324,17 +304,17 @@ socket.on('forward_request', async (data, callback) => {
324
304
  request_id: requestId,
325
305
  status_code: 403,
326
306
  headers: {},
327
- body: 'Target URL is not an allowed internal resource'
307
+ body: formatMessageBody('Target URL is not an allowed internal resource')
328
308
  });
329
309
  return;
330
310
  }
331
311
 
332
- if (!isResourceAllowed(targetUrl)) {
312
+ if (!(await resourceManager.isResourceAllowedWithSync(targetUrl))) {
333
313
  callback({
334
314
  request_id: requestId,
335
315
  status_code: 403,
336
316
  headers: {},
337
- body: 'Target URL is not in the allowed resources list'
317
+ body: formatMessageBody('Target URL is not in the allowed resources list')
338
318
  });
339
319
  return;
340
320
  }
@@ -361,30 +341,41 @@ socket.on('forward_request', async (data, callback) => {
361
341
  url: resolvedUrl,
362
342
  headers,
363
343
  data: body,
364
- validateStatus: () => true // Accept any status code
344
+ validateStatus: () => true, // Accept any status code
345
+ responseType: 'arraybuffer', // Get raw bytes, don't parse JSON
365
346
  });
366
347
 
367
348
  log.info(`Successfully forwarded request ${requestId} to ${targetUrl}, status: ${response.status}`);
368
349
 
369
350
  // Return response via acknowledgement
351
+ // Send body as base64 to preserve binary data byte-for-byte (critical for Docker registry digests)
352
+ const responseBody = response.data ? Buffer.from(response.data).toString('base64') : null;
353
+
370
354
  callback({
371
355
  request_id: requestId,
372
356
  status_code: response.status,
373
357
  headers: response.headers,
374
- body: typeof response.data === 'string' ? response.data : JSON.stringify(response.data)
358
+ body: responseBody,
359
+ version: 2
375
360
  });
376
361
 
377
362
  } catch (error) {
378
- log.error(`Error forwarding request ${requestId} to ${targetUrl}: ${error.message}`);
363
+ log.error(`Error forwarding request ${requestId} to ${targetUrl}: ${error?.response?.status || error.message}`);
364
+ const errorMessage = `Error reaching internal resource: ${error?.response?.status || error.message}`;
379
365
  callback({
380
366
  request_id: requestId,
381
367
  status_code: 502,
382
368
  headers: {},
383
- body: `Error reaching internal resource: ${error.message}`
369
+ body: formatMessageBody(errorMessage),
370
+ version: 2
384
371
  });
385
372
  }
386
373
  });
387
374
 
375
+ function formatMessageBody(message) {
376
+ return Buffer.from(message, 'utf-8').toString('base64');
377
+ }
378
+
388
379
  /**
389
380
  * Register this client with the broker server
390
381
  */
@@ -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",
3
+ "version": "1.0.5",
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"