@aikidosec/broker-client 1.0.2 → 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 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 = {
@@ -83,7 +84,6 @@ async function resolveInternalHostname(hostname) {
83
84
  const dnsPromises = await import('dns/promises');
84
85
  const result = await dnsPromises.resolve4(hostname);
85
86
  const ip = result[0];
86
- log.debug(`Resolved ${hostname} to ${ip} (system DNS)`);
87
87
  return ip;
88
88
  } catch (e) {
89
89
  log.error(`Failed to resolve ${hostname}: ${e.message}`);
@@ -162,13 +162,11 @@ function isInternalUrl(url) {
162
162
  // Not an IP, allow it through if we have custom DNS configured
163
163
  // The actual resolution will happen in the forward_request handler
164
164
  if (DNS_SERVERS !== null && DNS_SERVERS.length > 0) {
165
- log.debug(`Hostname ${hostname} will be resolved with custom DNS during forward`);
166
165
  return true;
167
166
  }
168
167
 
169
168
  // For system DNS, we can't do synchronous lookup here
170
169
  // Allow all private hostnames through and validate during forward
171
- log.debug(`Hostname ${hostname} will be validated during forward`);
172
170
  return true;
173
171
  }
174
172
  }
@@ -216,36 +214,9 @@ function isInternalUrl(url) {
216
214
  }
217
215
  }
218
216
 
219
- /**
220
- * Check if a resource is registered with the broker.
221
- */
222
- function isResourceAllowed(resource) {
223
- const registeredResources = resourceManager.getResources();
224
-
225
- // If no resources registered yet, deny (customer must register resources first)
226
- if (Object.keys(registeredResources).length === 0) {
227
- log.warn("No resources registered - customer must register resources via setup mode");
228
- return false;
229
- }
230
-
231
- // Normalize the target URL for comparison
232
- const resourceParsed = new URL(resource);
233
- const resourceNormalized = `${resourceParsed.protocol}//${resourceParsed.host}${resourceParsed.pathname || '/'}`;
234
-
235
- // Check if the exact base_url is registered
236
- for (const baseUrl of Object.keys(registeredResources)) {
237
- const baseParsed = new URL(baseUrl);
238
- const baseNormalized = `${baseParsed.protocol}//${baseParsed.host}${baseParsed.pathname || '/'}`;
239
-
240
- // Allow if target URL starts with registered base URL
241
- if (resourceNormalized.startsWith(baseNormalized)) {
242
- log.info(`Resource ${resource} matches registered base_url ${baseUrl}`);
243
- return true;
244
- }
245
- }
246
-
247
- log.warn(`Resource ${resource} not found in registered resources: ${Object.keys(registeredResources)}`);
248
- 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);
249
220
  }
250
221
 
251
222
  // Create Socket.IO client
@@ -253,12 +224,14 @@ const socket = io(SERVER_URL, {
253
224
  auth: {
254
225
  client_secret: CLIENT_SECRET
255
226
  },
227
+ agent,
256
228
  transports: ['websocket', 'polling'],
257
229
  reconnection: true,
258
230
  reconnectionAttempts: Infinity,
259
231
  reconnectionDelay: 1000,
260
232
  reconnectionDelayMax: 30000,
261
233
  randomizationFactor: 0.5,
234
+ tryAllTransports: true, // if we don't, it won't try to fallback from websocket to polling
262
235
  autoConnect: false // Don't connect until after registration
263
236
  });
264
237
 
@@ -283,6 +256,30 @@ socket.on('disconnect', () => {
283
256
  log.warn("Disconnected from broker server");
284
257
  });
285
258
 
259
+ socket.on('connect_error', (error) => {
260
+ log.error(`Socket.IO connection error: ${error.message}`);
261
+ if (error.description) {
262
+ log.error(` Description: ${JSON.stringify(error.description)}`);
263
+ }
264
+ if (error.context) {
265
+ log.error(` Context: ${JSON.stringify(error.context)}`);
266
+ }
267
+ log.error(` Type: ${error.type || 'unknown'}`);
268
+ });
269
+
270
+ // Manager events (on socket.io)
271
+ socket.io.on('error', (error) => {
272
+ log.error(`Socket.IO Manager error: ${error.message || JSON.stringify(error)}`);
273
+ });
274
+
275
+ socket.io.on('reconnect_error', (error) => {
276
+ log.error(`Socket.IO reconnection error: ${error.message}`);
277
+ });
278
+
279
+ socket.io.on('reconnect_failed', () => {
280
+ log.error(`Socket.IO reconnection failed after all attempts`);
281
+ });
282
+
286
283
  socket.on('forward_request', async (data, callback) => {
287
284
  /**
288
285
  * Receive request from broker server and forward to internal resource
@@ -308,7 +305,7 @@ socket.on('forward_request', async (data, callback) => {
308
305
  return;
309
306
  }
310
307
 
311
- if (!isResourceAllowed(targetUrl)) {
308
+ if (!(await resourceManager.isResourceAllowedWithSync(targetUrl))) {
312
309
  callback({
313
310
  request_id: requestId,
314
311
  status_code: 403,
@@ -421,7 +418,14 @@ async function registerWithServer() {
421
418
  log.warn(`Registration attempt ${attempt + 1} failed: ${response.status} - ${response.data}`);
422
419
  }
423
420
  } catch (error) {
424
- log.warn(`Registration attempt ${attempt + 1} failed: ${error.name}: ${error.message}`);
421
+ if (error.response) {
422
+ // HTTP error response
423
+ log.warn(`Registration attempt ${attempt + 1} failed: ${error.response.status} - ${JSON.stringify(error.response.data)}`);
424
+ } else {
425
+ // Error setting up request
426
+ log.warn(`Registration attempt ${attempt + 1} failed: ${error.message}`);
427
+ }
428
+
425
429
  if (getClientId() !== null) {
426
430
  // If we have a client_id already, don't retry
427
431
  // we should try once to deal with secret rotation
@@ -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
 
@@ -102,7 +104,7 @@ export class ResourceManager {
102
104
  this._resources = { ...resourcesDict };
103
105
  this._saveToFile();
104
106
  } else {
105
- log.debug("No resource changes detected");
107
+ log.info("No resource changes detected");
106
108
  }
107
109
  }
108
110
 
@@ -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,13 +243,20 @@ 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}`);
183
250
  }
184
251
 
185
252
  } catch (error) {
186
- log.error(`Error fetching resources from broker: ${error.message}`);
253
+ if (error.response) {
254
+ // HTTP error response
255
+ log.error(`Error fetching resources from broker: ${error.response.status} - ${JSON.stringify(error.response.data)}`);
256
+ } else {
257
+ // Error setting up request
258
+ log.error(`Error fetching resources from broker: ${error.message}`);
259
+ }
187
260
  }
188
261
  }
189
262
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aikidosec/broker-client",
3
- "version": "1.0.2",
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"