@aikidosec/broker-client 1.0.4 → 1.0.6
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 +1 -0
- package/app/client.js +29 -57
- package/app/config.js +65 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -60,6 +60,7 @@ Resource IDs are displayed in the Aikido UI when you register them.
|
|
|
60
60
|
- `HTTP_PROXY` - Proxy server for HTTP requests (e.g., `http://proxy.company.local:8080`)
|
|
61
61
|
- `HTTPS_PROXY` - Proxy server for HTTPS requests (e.g., `http://proxy.company.local:8080`)
|
|
62
62
|
- `ALL_PROXY` - Universal proxy fallback for all protocols if protocol-specific proxy is not set
|
|
63
|
+
- `BROKER_TARGET_URL` - Override the broker server URL (defaults to `https://broker.aikidobroker.com`)
|
|
63
64
|
|
|
64
65
|
## How It Works
|
|
65
66
|
|
package/app/client.js
CHANGED
|
@@ -10,9 +10,9 @@ import axios from 'axios';
|
|
|
10
10
|
import { URL } from 'url';
|
|
11
11
|
import { Address4, Address6 } from 'ip-address';
|
|
12
12
|
import dns from 'native-dns';
|
|
13
|
-
import fs from 'fs';
|
|
14
13
|
import { ResourceManager } from './resourceManager.js';
|
|
15
14
|
import { HttpsProxyAgent } from 'https-proxy-agent';
|
|
15
|
+
import { getClientId, setClientIdCache, getServerUrl, getClientSecret } from './config.js';
|
|
16
16
|
|
|
17
17
|
// Configure logging
|
|
18
18
|
const log = {
|
|
@@ -23,10 +23,9 @@ const log = {
|
|
|
23
23
|
};
|
|
24
24
|
|
|
25
25
|
// Broker Server Configuration
|
|
26
|
-
const
|
|
26
|
+
const CLIENT_SECRET = getClientSecret();
|
|
27
|
+
const SERVER_URL = getServerUrl();
|
|
27
28
|
|
|
28
|
-
// Client Configuration (from environment)
|
|
29
|
-
const CLIENT_SECRET = process.env.CLIENT_SECRET;
|
|
30
29
|
const ALLOWED_SUBNETS = process.env.ALLOWED_INTERNAL_SUBNETS
|
|
31
30
|
? process.env.ALLOWED_INTERNAL_SUBNETS.split(',').map(s => s.trim()).filter(s => s)
|
|
32
31
|
: [];
|
|
@@ -37,9 +36,12 @@ const DNS_SERVERS = process.env.DNS_SERVERS
|
|
|
37
36
|
: null;
|
|
38
37
|
|
|
39
38
|
// Configure axios defaults
|
|
39
|
+
const MAX_RESPONSE_SIZE = 100 * 1024 * 1024; // 100 MB
|
|
40
40
|
const axiosConfig = {
|
|
41
|
-
timeout:
|
|
42
|
-
maxRedirects: 5
|
|
41
|
+
timeout: 300000,
|
|
42
|
+
maxRedirects: 5,
|
|
43
|
+
maxContentLength: MAX_RESPONSE_SIZE,
|
|
44
|
+
maxBodyLength: MAX_RESPONSE_SIZE
|
|
43
45
|
};
|
|
44
46
|
|
|
45
47
|
// Create axios instance for internal requests
|
|
@@ -48,31 +50,6 @@ const internalHttpClient = axios.create(axiosConfig);
|
|
|
48
50
|
// Initialize ResourceManager
|
|
49
51
|
const resourceManager = new ResourceManager();
|
|
50
52
|
|
|
51
|
-
// Cache for client_id
|
|
52
|
-
let _clientIdCache = null;
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Get the client ID from cache or read from file.
|
|
56
|
-
* Returns null if not registered yet.
|
|
57
|
-
*/
|
|
58
|
-
function getClientId() {
|
|
59
|
-
if (_clientIdCache !== null) {
|
|
60
|
-
return _clientIdCache;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
const clientIdPath = '/config/client_id';
|
|
64
|
-
if (fs.existsSync(clientIdPath)) {
|
|
65
|
-
try {
|
|
66
|
-
_clientIdCache = fs.readFileSync(clientIdPath, 'utf8').trim();
|
|
67
|
-
return _clientIdCache;
|
|
68
|
-
} catch (e) {
|
|
69
|
-
log.error(`Failed to read client_id: ${e.message}`);
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
return null;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
53
|
/**
|
|
77
54
|
* Resolve hostname using custom DNS servers if configured.
|
|
78
55
|
* Falls back to system DNS if DNS_SERVERS not set.
|
|
@@ -232,7 +209,8 @@ const socket = io(SERVER_URL, {
|
|
|
232
209
|
reconnectionDelayMax: 30000,
|
|
233
210
|
randomizationFactor: 0.5,
|
|
234
211
|
tryAllTransports: true, // if we don't, it won't try to fallback from websocket to polling
|
|
235
|
-
autoConnect: false // Don't connect until after registration
|
|
212
|
+
autoConnect: false, // Don't connect until after registration
|
|
213
|
+
withCredentials: true // make sure cookies work for sticky sessions
|
|
236
214
|
});
|
|
237
215
|
|
|
238
216
|
// Socket.IO event handlers
|
|
@@ -300,7 +278,7 @@ socket.on('forward_request', async (data, callback) => {
|
|
|
300
278
|
request_id: requestId,
|
|
301
279
|
status_code: 403,
|
|
302
280
|
headers: {},
|
|
303
|
-
body: 'Target URL is not an allowed internal resource'
|
|
281
|
+
body: formatMessageBody('Target URL is not an allowed internal resource')
|
|
304
282
|
});
|
|
305
283
|
return;
|
|
306
284
|
}
|
|
@@ -310,7 +288,7 @@ socket.on('forward_request', async (data, callback) => {
|
|
|
310
288
|
request_id: requestId,
|
|
311
289
|
status_code: 403,
|
|
312
290
|
headers: {},
|
|
313
|
-
body: 'Target URL is not in the allowed resources list'
|
|
291
|
+
body: formatMessageBody('Target URL is not in the allowed resources list')
|
|
314
292
|
});
|
|
315
293
|
return;
|
|
316
294
|
}
|
|
@@ -337,30 +315,41 @@ socket.on('forward_request', async (data, callback) => {
|
|
|
337
315
|
url: resolvedUrl,
|
|
338
316
|
headers,
|
|
339
317
|
data: body,
|
|
340
|
-
validateStatus: () => true // Accept any status code
|
|
318
|
+
validateStatus: () => true, // Accept any status code
|
|
319
|
+
responseType: 'arraybuffer', // Get raw bytes, don't parse JSON
|
|
341
320
|
});
|
|
342
321
|
|
|
343
322
|
log.info(`Successfully forwarded request ${requestId} to ${targetUrl}, status: ${response.status}`);
|
|
344
323
|
|
|
345
324
|
// Return response via acknowledgement
|
|
325
|
+
// Send body as base64 to preserve binary data byte-for-byte (critical for Docker registry digests)
|
|
326
|
+
const responseBody = response.data ? Buffer.from(response.data).toString('base64') : null;
|
|
327
|
+
|
|
346
328
|
callback({
|
|
347
329
|
request_id: requestId,
|
|
348
330
|
status_code: response.status,
|
|
349
331
|
headers: response.headers,
|
|
350
|
-
body:
|
|
332
|
+
body: responseBody,
|
|
333
|
+
version: 2
|
|
351
334
|
});
|
|
352
335
|
|
|
353
336
|
} catch (error) {
|
|
354
|
-
log.error(`Error forwarding request ${requestId} to ${targetUrl}: ${error.message}`);
|
|
337
|
+
log.error(`Error forwarding request ${requestId} to ${targetUrl}: ${error?.response?.status || error.message}`);
|
|
338
|
+
const errorMessage = `Error reaching internal resource: ${error?.response?.status || error.message}`;
|
|
355
339
|
callback({
|
|
356
340
|
request_id: requestId,
|
|
357
341
|
status_code: 502,
|
|
358
342
|
headers: {},
|
|
359
|
-
body:
|
|
343
|
+
body: formatMessageBody(errorMessage),
|
|
344
|
+
version: 2
|
|
360
345
|
});
|
|
361
346
|
}
|
|
362
347
|
});
|
|
363
348
|
|
|
349
|
+
function formatMessageBody(message) {
|
|
350
|
+
return Buffer.from(message, 'utf-8').toString('base64');
|
|
351
|
+
}
|
|
352
|
+
|
|
364
353
|
/**
|
|
365
354
|
* Register this client with the broker server
|
|
366
355
|
*/
|
|
@@ -387,14 +376,7 @@ async function registerWithServer() {
|
|
|
387
376
|
log.info(`✓ Successfully registered with broker server as ${clientId}`);
|
|
388
377
|
|
|
389
378
|
// Save client_id to file and cache
|
|
390
|
-
|
|
391
|
-
try {
|
|
392
|
-
fs.mkdirSync('/config', { recursive: true });
|
|
393
|
-
fs.writeFileSync('/config/client_id', clientId);
|
|
394
|
-
log.info("💾 Saved client_id to /config/client_id");
|
|
395
|
-
} catch (e) {
|
|
396
|
-
log.warn(`Could not save client_id file: ${e.message}`);
|
|
397
|
-
}
|
|
379
|
+
setClientIdCache(clientId);
|
|
398
380
|
|
|
399
381
|
log.info("Waiting for server to propagate configuration...");
|
|
400
382
|
await new Promise(resolve => setTimeout(resolve, 5000));
|
|
@@ -402,17 +384,7 @@ async function registerWithServer() {
|
|
|
402
384
|
} else if (response.status === 409) {
|
|
403
385
|
log.info("✓ Client already registered with server (this is OK)");
|
|
404
386
|
// Try to extract client_id from response if available
|
|
405
|
-
|
|
406
|
-
const clientId = response.data.client_id;
|
|
407
|
-
if (clientId) {
|
|
408
|
-
_clientIdCache = clientId;
|
|
409
|
-
fs.mkdirSync('/config', { recursive: true });
|
|
410
|
-
fs.writeFileSync('/config/client_id', clientId);
|
|
411
|
-
log.info("💾 Saved client_id to /config/client_id");
|
|
412
|
-
}
|
|
413
|
-
} catch (e) {
|
|
414
|
-
log.warn(`Could not save client_id file: ${e.message}`);
|
|
415
|
-
}
|
|
387
|
+
setClientIdCache(response.data.client_id);
|
|
416
388
|
return;
|
|
417
389
|
} else {
|
|
418
390
|
log.warn(`Registration attempt ${attempt + 1} failed: ${response.status} - ${response.data}`);
|
package/app/config.js
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
|
|
3
|
+
// Client ID file path
|
|
4
|
+
const CLIENT_ID_PATH = '/config/client_id';
|
|
5
|
+
|
|
6
|
+
// Cache for client_id
|
|
7
|
+
let _clientIdCache = null;
|
|
8
|
+
|
|
9
|
+
// Client secret from environment
|
|
10
|
+
const CLIENT_SECRET = process.env.CLIENT_SECRET;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Get the server URL.
|
|
14
|
+
* Uses BROKER_TARGET_URL env var, defaults to https://broker.aikidobroker.com
|
|
15
|
+
*/
|
|
16
|
+
export function getServerUrl() {
|
|
17
|
+
return process.env.BROKER_TARGET_URL || 'https://broker.aikidobroker.com';
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Get the client secret from environment.
|
|
22
|
+
*/
|
|
23
|
+
export function getClientSecret() {
|
|
24
|
+
return CLIENT_SECRET;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Get the client ID from cache or read from file.
|
|
29
|
+
* Returns null if not registered yet.
|
|
30
|
+
*/
|
|
31
|
+
export function getClientId() {
|
|
32
|
+
if (_clientIdCache !== null) {
|
|
33
|
+
return _clientIdCache;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (fs.existsSync(CLIENT_ID_PATH)) {
|
|
37
|
+
try {
|
|
38
|
+
_clientIdCache = fs.readFileSync(CLIENT_ID_PATH, 'utf8').trim();
|
|
39
|
+
return _clientIdCache;
|
|
40
|
+
} catch (e) {
|
|
41
|
+
console.error(`[ERROR] ${new Date().toISOString()} - Failed to read client_id: ${e.message}`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Set the client ID cache and persist to file.
|
|
50
|
+
* Does nothing if clientId is null/undefined.
|
|
51
|
+
*/
|
|
52
|
+
export function setClientIdCache(clientId) {
|
|
53
|
+
if (!clientId) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
_clientIdCache = clientId;
|
|
57
|
+
try {
|
|
58
|
+
fs.mkdirSync('/config', { recursive: true });
|
|
59
|
+
fs.writeFileSync(CLIENT_ID_PATH, clientId);
|
|
60
|
+
console.log(`[INFO] ${new Date().toISOString()} - 💾 Saved client_id to ${CLIENT_ID_PATH}`);
|
|
61
|
+
} catch (e) {
|
|
62
|
+
console.error(`[ERROR] ${new Date().toISOString()} - Failed to write client_id: ${e.message}`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
package/package.json
CHANGED