@alwaysai/device-agent 1.3.1 → 1.4.0

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.
Files changed (91) hide show
  1. package/lib/application-control/environment-variables.d.ts.map +1 -1
  2. package/lib/application-control/environment-variables.js +9 -4
  3. package/lib/application-control/environment-variables.js.map +1 -1
  4. package/lib/application-control/environment-variables.test.js +1 -1
  5. package/lib/application-control/environment-variables.test.js.map +1 -1
  6. package/lib/application-control/install.d.ts.map +1 -1
  7. package/lib/application-control/install.js +6 -2
  8. package/lib/application-control/install.js.map +1 -1
  9. package/lib/application-control/models.d.ts.map +1 -1
  10. package/lib/application-control/models.js +4 -2
  11. package/lib/application-control/models.js.map +1 -1
  12. package/lib/application-control/status.js +4 -5
  13. package/lib/application-control/status.js.map +1 -1
  14. package/lib/cloud-connection/device-agent-cloud-connection.d.ts +3 -3
  15. package/lib/cloud-connection/device-agent-cloud-connection.d.ts.map +1 -1
  16. package/lib/cloud-connection/device-agent-cloud-connection.js +114 -99
  17. package/lib/cloud-connection/device-agent-cloud-connection.js.map +1 -1
  18. package/lib/cloud-connection/live-updates-handler.d.ts +1 -0
  19. package/lib/cloud-connection/live-updates-handler.d.ts.map +1 -1
  20. package/lib/cloud-connection/live-updates-handler.js +22 -4
  21. package/lib/cloud-connection/live-updates-handler.js.map +1 -1
  22. package/lib/cloud-connection/messages.d.ts.map +1 -1
  23. package/lib/cloud-connection/messages.js +3 -4
  24. package/lib/cloud-connection/messages.js.map +1 -1
  25. package/lib/cloud-connection/shadow-handler.d.ts +14 -21
  26. package/lib/cloud-connection/shadow-handler.d.ts.map +1 -1
  27. package/lib/cloud-connection/shadow-handler.js +162 -108
  28. package/lib/cloud-connection/shadow-handler.js.map +1 -1
  29. package/lib/cloud-connection/shadow-handler.test.js +100 -83
  30. package/lib/cloud-connection/shadow-handler.test.js.map +1 -1
  31. package/lib/device-control/device-control.d.ts +7 -14
  32. package/lib/device-control/device-control.d.ts.map +1 -1
  33. package/lib/device-control/device-control.js +37 -14
  34. package/lib/device-control/device-control.js.map +1 -1
  35. package/lib/secure-tunneling/secure-tunneling.d.ts +105 -0
  36. package/lib/secure-tunneling/secure-tunneling.d.ts.map +1 -0
  37. package/lib/secure-tunneling/secure-tunneling.js +435 -0
  38. package/lib/secure-tunneling/secure-tunneling.js.map +1 -0
  39. package/lib/secure-tunneling/secure-tunneling.test.d.ts +2 -0
  40. package/lib/secure-tunneling/secure-tunneling.test.d.ts.map +1 -0
  41. package/lib/secure-tunneling/secure-tunneling.test.js +1070 -0
  42. package/lib/secure-tunneling/secure-tunneling.test.js.map +1 -0
  43. package/lib/secure-tunneling/spawner-detached.d.ts +6 -0
  44. package/lib/secure-tunneling/spawner-detached.d.ts.map +1 -0
  45. package/lib/secure-tunneling/spawner-detached.js +107 -0
  46. package/lib/secure-tunneling/spawner-detached.js.map +1 -0
  47. package/lib/subcommands/app/analytics.d.ts.map +1 -1
  48. package/lib/subcommands/app/analytics.js +9 -13
  49. package/lib/subcommands/app/analytics.js.map +1 -1
  50. package/lib/subcommands/app/env-vars.d.ts.map +1 -1
  51. package/lib/subcommands/app/env-vars.js +11 -16
  52. package/lib/subcommands/app/env-vars.js.map +1 -1
  53. package/lib/subcommands/app/models.d.ts.map +1 -1
  54. package/lib/subcommands/app/models.js +12 -16
  55. package/lib/subcommands/app/models.js.map +1 -1
  56. package/lib/subcommands/device/clean.d.ts.map +1 -1
  57. package/lib/subcommands/device/clean.js +3 -1
  58. package/lib/subcommands/device/clean.js.map +1 -1
  59. package/lib/subcommands/device/device.d.ts.map +1 -1
  60. package/lib/subcommands/device/device.js +14 -6
  61. package/lib/subcommands/device/device.js.map +1 -1
  62. package/lib/util/cloud-mode-ready.d.ts +1 -0
  63. package/lib/util/cloud-mode-ready.d.ts.map +1 -1
  64. package/lib/util/cloud-mode-ready.js +36 -1
  65. package/lib/util/cloud-mode-ready.js.map +1 -1
  66. package/package.json +2 -2
  67. package/src/application-control/environment-variables.test.ts +1 -1
  68. package/src/application-control/environment-variables.ts +9 -6
  69. package/src/application-control/install.ts +7 -3
  70. package/src/application-control/models.ts +11 -6
  71. package/src/application-control/status.ts +8 -8
  72. package/src/cloud-connection/device-agent-cloud-connection.ts +161 -131
  73. package/src/cloud-connection/live-updates-handler.ts +34 -6
  74. package/src/cloud-connection/messages.ts +3 -4
  75. package/src/cloud-connection/shadow-handler.test.ts +101 -84
  76. package/src/cloud-connection/shadow-handler.ts +275 -133
  77. package/src/device-control/device-control.ts +46 -19
  78. package/src/secure-tunneling/secure-tunneling.test.ts +1239 -0
  79. package/src/secure-tunneling/secure-tunneling.ts +606 -0
  80. package/src/secure-tunneling/spawner-detached.ts +123 -0
  81. package/src/subcommands/app/analytics.ts +16 -13
  82. package/src/subcommands/app/env-vars.ts +18 -16
  83. package/src/subcommands/app/models.ts +20 -16
  84. package/src/subcommands/device/clean.ts +4 -1
  85. package/src/subcommands/device/device.ts +26 -10
  86. package/src/util/cloud-mode-ready.ts +36 -0
  87. package/lib/secure-tunneling/index.d.ts +0 -5
  88. package/lib/secure-tunneling/index.d.ts.map +0 -1
  89. package/lib/secure-tunneling/index.js +0 -64
  90. package/lib/secure-tunneling/index.js.map +0 -1
  91. package/src/secure-tunneling/index.ts +0 -74
@@ -0,0 +1,1239 @@
1
+ import { AAI_DIR } from 'alwaysai/lib/paths';
2
+ import { join } from 'path';
3
+ import { aaiArtifactsBucketUrl } from '../urls';
4
+ import {
5
+ AWS_ROOT_CERTIFICATE_FILE_PATH,
6
+ SECURE_TUNNEL_BIN_DIR,
7
+ SECURE_TUNNEL_BIN_NAME,
8
+ SECURE_TUNNEL_BIN_PATH
9
+ } from '../util/directories';
10
+ import { downloadFile } from '../util/download-file';
11
+ import { getArch, getDistribution, getOsVersion } from '../util/system-info';
12
+ import {
13
+ SecureTunnelHandlerSingleton,
14
+ SecureTunnelPortInfo,
15
+ SecureTunnelShadowDesRep,
16
+ SecureTunnelShadowUpdateDelta
17
+ } from './secure-tunneling';
18
+ // import { JsSpawner } from 'alwaysai/lib/util/spawner';
19
+ import { ChildProcess } from 'child_process';
20
+ import { killDetachedProcess, runDetachedProcess } from './spawner-detached';
21
+
22
+ //-----------------------------------------------------------------------------
23
+ // mocks
24
+ //-----------------------------------------------------------------------------
25
+ jest.mock('alwaysai/lib/util/spawner');
26
+ const mockJsSpawner = {
27
+ exiresolvePathsts: jest.fn(),
28
+ readdir: jest.fn(),
29
+ readFile: jest.fn(),
30
+ writeFile: jest.fn(),
31
+ mkdirp: jest.fn(),
32
+ rimraf: jest.fn(),
33
+ tar: jest.fn(),
34
+ rename: jest.fn(),
35
+ untar: jest.fn(),
36
+ exists: jest.fn(),
37
+ run: jest.fn()
38
+ };
39
+ jest.mock('alwaysai/lib/util/spawner', () => {
40
+ return {
41
+ ...jest.requireActual('alwaysai/lib/util/spawner'),
42
+ JsSpawner: jest.fn(() => mockJsSpawner)
43
+ };
44
+ });
45
+
46
+ jest.mock('../util/system-info', () => ({
47
+ getArch: jest.fn(),
48
+ getOsVersion: jest.fn(),
49
+ getDistribution: jest.fn()
50
+ }));
51
+
52
+ jest.mock('../util/download-file', () => ({
53
+ downloadFile: jest.fn()
54
+ }));
55
+
56
+ jest.mock('./spawner-detached', () => ({
57
+ runDetachedProcess: jest.fn(),
58
+ killDetachedProcess: jest.fn()
59
+ }));
60
+
61
+ //-----------------------------------------------------------------------------
62
+ // constants
63
+ //-----------------------------------------------------------------------------
64
+ const ST_START_PORT_NUMBER = 5010;
65
+ type ReadOnlyPortInfo = {
66
+ readonly enabled: boolean;
67
+ readonly type: string;
68
+ readonly ip: string;
69
+ readonly port: number;
70
+ };
71
+ type DeepReadonly<T> = {
72
+ readonly [K in keyof T]: DeepReadonly<T[K]>;
73
+ };
74
+
75
+ const disabledSshPortInfo: DeepReadonly<ReadOnlyPortInfo> = {
76
+ enabled: false,
77
+ type: 'SSH',
78
+ ip: '0.0.0.0',
79
+ port: 22
80
+ };
81
+ const enabledSshPortInfo: DeepReadonly<ReadOnlyPortInfo> = {
82
+ enabled: true,
83
+ type: 'SSH',
84
+ ip: '0.0.0.0',
85
+ port: 22
86
+ };
87
+ const invalidSshPortInfo: DeepReadonly<ReadOnlyPortInfo> = {
88
+ enabled: true,
89
+ type: 'SSH',
90
+ ip: '192.168.0.255',
91
+ port: 80
92
+ };
93
+ const disabledHttpPortInfo_1: DeepReadonly<ReadOnlyPortInfo> = {
94
+ enabled: false,
95
+ type: 'HTTP',
96
+ ip: '192.168.0.10',
97
+ port: 1
98
+ };
99
+ const enabledHttpPortInfo_1: DeepReadonly<ReadOnlyPortInfo> = {
100
+ enabled: true,
101
+ type: 'HTTP',
102
+ ip: '192.168.0.10',
103
+ port: 1
104
+ };
105
+ const disabledHttpPortInfo_2: DeepReadonly<ReadOnlyPortInfo> = {
106
+ enabled: false,
107
+ type: 'HTTP',
108
+ ip: '192.168.0.20',
109
+ port: 2
110
+ };
111
+ const enabledHttpPortInfo_2: DeepReadonly<ReadOnlyPortInfo> = {
112
+ enabled: true,
113
+ type: 'HTTP',
114
+ ip: '192.168.0.20',
115
+ port: 2
116
+ };
117
+ const disabledHttpPortInfo_3: DeepReadonly<ReadOnlyPortInfo> = {
118
+ enabled: false,
119
+ type: 'HTTP',
120
+ ip: '192.168.0.30',
121
+ port: 3
122
+ };
123
+ const enabledHttpPortInfo_3: DeepReadonly<ReadOnlyPortInfo> = {
124
+ enabled: true,
125
+ type: 'HTTP',
126
+ ip: '192.168.0.30',
127
+ port: 3
128
+ };
129
+
130
+ type SecureTunnelNotifyMsg = {
131
+ readonly clientAccessToken: string;
132
+ readonly region: string;
133
+ readonly services: string[];
134
+ };
135
+
136
+ describe('SecureTunnelHandlerSingleton', () => {
137
+ //---------------------------------------------------------------------------
138
+ // test variables
139
+ //---------------------------------------------------------------------------
140
+ let testStHandlerSingleton: SecureTunnelHandlerSingleton;
141
+ let shadowVersion = 0;
142
+ let shadowTimestamp = 1708726929;
143
+ const testDefaultShadow = {
144
+ st_ports: [disabledSshPortInfo]
145
+ } as const;
146
+
147
+ beforeEach(() => {
148
+ jest.clearAllMocks();
149
+ jest.resetModules();
150
+ testStHandlerSingleton = SecureTunnelHandlerSingleton.getInstance();
151
+ shadowVersion++;
152
+ shadowTimestamp++;
153
+ });
154
+
155
+ afterEach(async () => {
156
+ await testStHandlerSingleton.destroy();
157
+ });
158
+
159
+ //---------------------------------------------------------------------------
160
+ // help functions
161
+ //---------------------------------------------------------------------------
162
+ function createDeltaShadowMsg(
163
+ stPorts: SecureTunnelPortInfo[]
164
+ ): SecureTunnelShadowUpdateDelta {
165
+ const stPortsCopy = JSON.parse(JSON.stringify(stPorts));
166
+ const deltaShadowMsg: SecureTunnelShadowUpdateDelta = {
167
+ version: shadowVersion,
168
+ timestamp: shadowTimestamp,
169
+ state: { st_ports: stPortsCopy }
170
+ };
171
+ return deltaShadowMsg;
172
+ }
173
+
174
+ function transformDeltaToUpdateReported(
175
+ deltaMsg: SecureTunnelShadowUpdateDelta
176
+ ): SecureTunnelShadowDesRep {
177
+ const { version, state } = deltaMsg;
178
+ const reportedStateReported: SecureTunnelShadowDesRep = JSON.parse(
179
+ JSON.stringify(state)
180
+ );
181
+ return reportedStateReported;
182
+ }
183
+
184
+ //---------------------------------------------------------------------------
185
+ // test class methods
186
+ //---------------------------------------------------------------------------
187
+ it('should be a singleton', () => {
188
+ const handler = SecureTunnelHandlerSingleton.getInstance();
189
+ expect(testStHandlerSingleton).toBe(handler);
190
+ });
191
+
192
+ it('should have a getSecureTunnelShadow method', () => {
193
+ expect(testStHandlerSingleton.getSecureTunnelShadow).toBeDefined();
194
+ });
195
+
196
+ it('should have a syncShadowToDeviceState method', () => {
197
+ expect(testStHandlerSingleton.syncShadowToDeviceState).toBeDefined();
198
+ });
199
+
200
+ it('should have a secureTunnelNotifyHandler method', () => {
201
+ expect(testStHandlerSingleton.secureTunnelNotifyHandler).toBeDefined();
202
+ });
203
+
204
+ it('should have a destroy method', () => {
205
+ expect(testStHandlerSingleton.destroy).toBeDefined();
206
+ });
207
+
208
+ it('should initialize reportedShadowState to default', () => {
209
+ const actualReportedShadow = testStHandlerSingleton.getSecureTunnelShadow();
210
+ expect(actualReportedShadow).toEqual(testDefaultShadow);
211
+ });
212
+
213
+ //---------------------------------------------------------------------------
214
+ // test syncShadowToDeviceState function
215
+ //---------------------------------------------------------------------------
216
+ it('should update reportedShadowSate to 1 SSH and 1 HTTP port config', async () => {
217
+ // Arrange
218
+ // --------------------------------------------------------------------
219
+ const deltaShadowMsg = createDeltaShadowMsg([
220
+ disabledSshPortInfo,
221
+ disabledHttpPortInfo_1
222
+ ]);
223
+ const expUpdateReported = transformDeltaToUpdateReported(deltaShadowMsg);
224
+
225
+ // Act
226
+ // --------------------------------------------------------------------
227
+ const actualReportedShadow =
228
+ await testStHandlerSingleton.syncShadowToDeviceState(deltaShadowMsg);
229
+
230
+ // Assert
231
+ // --------------------------------------------------------------------
232
+ expect(actualReportedShadow).toEqual(expUpdateReported);
233
+ expect(runDetachedProcess).toHaveBeenCalledTimes(0);
234
+ expect(killDetachedProcess).toHaveBeenCalledTimes(0);
235
+ });
236
+
237
+ it('should update reportedShadowState to 1 SSH and 2 HTTP port config', async () => {
238
+ // Arrange
239
+ // --------------------------------------------------------------------
240
+ const deltaShadowMsg = createDeltaShadowMsg([
241
+ disabledSshPortInfo,
242
+ disabledHttpPortInfo_1,
243
+ disabledHttpPortInfo_2
244
+ ]);
245
+ const expUpdateReported = transformDeltaToUpdateReported(deltaShadowMsg);
246
+
247
+ // Act
248
+ // --------------------------------------------------------------------
249
+ const actualReportedShadow =
250
+ await testStHandlerSingleton.syncShadowToDeviceState(deltaShadowMsg);
251
+
252
+ // Assert
253
+ // --------------------------------------------------------------------
254
+ expect(actualReportedShadow).toEqual(expUpdateReported);
255
+ expect(runDetachedProcess).toHaveBeenCalledTimes(0);
256
+ expect(killDetachedProcess).toHaveBeenCalledTimes(0);
257
+ });
258
+
259
+ it('should update reportedShadowState to 1 SSH and 2 HTTP port config, order is important', async () => {
260
+ // Arrange
261
+ // --------------------------------------------------------------------
262
+ const deltaShadowMsg = createDeltaShadowMsg([
263
+ disabledHttpPortInfo_1,
264
+ disabledSshPortInfo,
265
+ disabledHttpPortInfo_2
266
+ ]);
267
+ const expUpdateReported = transformDeltaToUpdateReported(deltaShadowMsg);
268
+
269
+ // Act
270
+ // --------------------------------------------------------------------
271
+ const actualReportedShadow =
272
+ await testStHandlerSingleton.syncShadowToDeviceState(deltaShadowMsg);
273
+
274
+ // Assert
275
+ // --------------------------------------------------------------------
276
+ expect(actualReportedShadow).toEqual(expUpdateReported);
277
+ expect(runDetachedProcess).toHaveBeenCalledTimes(0);
278
+ expect(killDetachedProcess).toHaveBeenCalledTimes(0);
279
+ });
280
+
281
+ it('should update reportedShadowState to only 1 HTTP port config', async () => {
282
+ // Arrange
283
+ // --------------------------------------------------------------------
284
+ const deltaShadowMsg = createDeltaShadowMsg([disabledHttpPortInfo_1]);
285
+ const expUpdateReported = transformDeltaToUpdateReported(deltaShadowMsg);
286
+
287
+ // Act
288
+ // --------------------------------------------------------------------
289
+ const actualReportedShadow =
290
+ await testStHandlerSingleton.syncShadowToDeviceState(deltaShadowMsg);
291
+
292
+ // Assert
293
+ // --------------------------------------------------------------------
294
+ expect(actualReportedShadow).toEqual(expUpdateReported);
295
+ expect(runDetachedProcess).toHaveBeenCalledTimes(0);
296
+ expect(killDetachedProcess).toHaveBeenCalledTimes(0);
297
+ });
298
+
299
+ it('should update reportedShadowState from 1 SSH port to 3 HTTP ports config', async () => {
300
+ // Arrange
301
+ // --------------------------------------------------------------------
302
+ const deltaShadowMsg = createDeltaShadowMsg([
303
+ disabledHttpPortInfo_1,
304
+ disabledHttpPortInfo_2,
305
+ disabledHttpPortInfo_3
306
+ ]);
307
+ const expUpdateReported = transformDeltaToUpdateReported(deltaShadowMsg);
308
+
309
+ // Act
310
+ // --------------------------------------------------------------------
311
+ const actualReportedShadow =
312
+ await testStHandlerSingleton.syncShadowToDeviceState(deltaShadowMsg);
313
+
314
+ // Assert
315
+ // --------------------------------------------------------------------
316
+ expect(actualReportedShadow).toEqual(expUpdateReported);
317
+ expect(runDetachedProcess).toHaveBeenCalledTimes(0);
318
+ expect(killDetachedProcess).toHaveBeenCalledTimes(0);
319
+ });
320
+
321
+ it('should update reportedShadowState from 3 HTTP ports to only 1 SSH port config', async () => {
322
+ // Arrange
323
+ // --------------------------------------------------------------------
324
+ const orgDeltaShadowMsg = createDeltaShadowMsg([
325
+ disabledHttpPortInfo_1,
326
+ disabledHttpPortInfo_2,
327
+ disabledHttpPortInfo_3
328
+ ]);
329
+ const orgUpdateReported = transformDeltaToUpdateReported(orgDeltaShadowMsg);
330
+ let actualReportedShadow =
331
+ await testStHandlerSingleton.syncShadowToDeviceState(orgDeltaShadowMsg);
332
+ expect(actualReportedShadow).toEqual(orgUpdateReported);
333
+ const expDeltaShadowMsg = createDeltaShadowMsg([disabledSshPortInfo]);
334
+ const expUpdateReported = transformDeltaToUpdateReported(expDeltaShadowMsg);
335
+
336
+ // Act
337
+ // --------------------------------------------------------------------
338
+ actualReportedShadow = await testStHandlerSingleton.syncShadowToDeviceState(
339
+ expDeltaShadowMsg
340
+ );
341
+
342
+ // Assert
343
+ // --------------------------------------------------------------------
344
+ expect(actualReportedShadow).toEqual(expUpdateReported);
345
+ expect(runDetachedProcess).toHaveBeenCalledTimes(0);
346
+ expect(killDetachedProcess).toHaveBeenCalledTimes(0);
347
+ });
348
+
349
+ it('should update reportedShadowState, when the 1st HTTP shadow changes from disabled to enabled', async () => {
350
+ // Arrange
351
+ // --------------------------------------------------------------------
352
+ const orgDeltaShadowMsg = createDeltaShadowMsg([
353
+ disabledSshPortInfo,
354
+ disabledHttpPortInfo_1,
355
+ disabledHttpPortInfo_2
356
+ ]);
357
+ const orgUpdateReported = transformDeltaToUpdateReported(orgDeltaShadowMsg);
358
+ let actualReportedShadow =
359
+ await testStHandlerSingleton.syncShadowToDeviceState(orgDeltaShadowMsg);
360
+ expect(actualReportedShadow).toEqual(orgUpdateReported);
361
+ const expDeltaShadowMsg = createDeltaShadowMsg([
362
+ disabledSshPortInfo,
363
+ enabledHttpPortInfo_1,
364
+ disabledHttpPortInfo_2
365
+ ]);
366
+ const expUpdateReported = transformDeltaToUpdateReported(expDeltaShadowMsg);
367
+ jest.mocked(runDetachedProcess).mockResolvedValueOnce({} as ChildProcess);
368
+
369
+ // Act
370
+ // --------------------------------------------------------------------
371
+ actualReportedShadow = await testStHandlerSingleton.syncShadowToDeviceState(
372
+ expDeltaShadowMsg
373
+ );
374
+
375
+ // Assert
376
+ // --------------------------------------------------------------------
377
+ expect(actualReportedShadow).toEqual(expUpdateReported);
378
+ expect(runDetachedProcess).toHaveBeenCalledTimes(1);
379
+ expect(runDetachedProcess).toBeCalledWith('socat', [
380
+ `tcp4-listen:${ST_START_PORT_NUMBER + 1},fork`,
381
+ `tcp4:${enabledHttpPortInfo_1.ip}:${enabledHttpPortInfo_1.port}`
382
+ ]);
383
+ expect(killDetachedProcess).not.toBeCalled();
384
+ });
385
+
386
+ it('should update reportedShadowState, when the 1st HTTP shadow changes from enabled to disabled', async () => {
387
+ // Arrange
388
+ // --------------------------------------------------------------------
389
+ const orgDeltaShadowMsg = createDeltaShadowMsg([
390
+ disabledSshPortInfo,
391
+ enabledHttpPortInfo_1,
392
+ disabledHttpPortInfo_2
393
+ ]);
394
+ const orgUpdateReported = transformDeltaToUpdateReported(orgDeltaShadowMsg);
395
+ jest.mocked(runDetachedProcess).mockResolvedValueOnce({} as ChildProcess);
396
+ let actualReportedShadow =
397
+ await testStHandlerSingleton.syncShadowToDeviceState(orgDeltaShadowMsg);
398
+ expect(actualReportedShadow).toEqual(orgUpdateReported);
399
+ expect(runDetachedProcess).toHaveBeenCalledTimes(1);
400
+ const socatArgs = [
401
+ `tcp4-listen:${ST_START_PORT_NUMBER + 1},fork`,
402
+ `tcp4:${enabledHttpPortInfo_1.ip}:${enabledHttpPortInfo_1.port}`
403
+ ];
404
+ expect(runDetachedProcess).toHaveBeenCalledWith('socat', socatArgs);
405
+ const expDeltaShadowMsg = createDeltaShadowMsg([
406
+ disabledSshPortInfo,
407
+ disabledHttpPortInfo_1,
408
+ disabledHttpPortInfo_2
409
+ ]);
410
+ const expUpdateReported = transformDeltaToUpdateReported(expDeltaShadowMsg);
411
+ jest.clearAllMocks();
412
+ jest.resetModules();
413
+ jest.mocked(killDetachedProcess).mockResolvedValueOnce(undefined);
414
+
415
+ // Act
416
+ // --------------------------------------------------------------------
417
+ actualReportedShadow = await testStHandlerSingleton.syncShadowToDeviceState(
418
+ expDeltaShadowMsg
419
+ );
420
+
421
+ // Assert
422
+ // --------------------------------------------------------------------
423
+ expect(actualReportedShadow).toEqual(expUpdateReported);
424
+ expect(runDetachedProcess).not.toBeCalled();
425
+ expect(killDetachedProcess).toHaveBeenCalledTimes(1);
426
+ expect(killDetachedProcess).toHaveBeenCalledWith({}, [
427
+ ['socat', ...socatArgs].join(' ')
428
+ ]);
429
+ });
430
+
431
+ it('should update reportedShadowState, all 3 HTTP services enabled, with SSH disabled', async () => {
432
+ // Arrange
433
+ // --------------------------------------------------------------------
434
+ const expDeltaShadowMsg = createDeltaShadowMsg([
435
+ disabledSshPortInfo,
436
+ enabledHttpPortInfo_1,
437
+ enabledHttpPortInfo_2,
438
+ enabledHttpPortInfo_3
439
+ ]);
440
+ const expUpdateReported = transformDeltaToUpdateReported(expDeltaShadowMsg);
441
+ jest.mocked(runDetachedProcess).mockResolvedValueOnce({} as ChildProcess);
442
+ jest.mocked(runDetachedProcess).mockResolvedValueOnce({} as ChildProcess);
443
+ jest.mocked(runDetachedProcess).mockResolvedValueOnce({} as ChildProcess);
444
+
445
+ // Act
446
+ // --------------------------------------------------------------------
447
+ const actualReportedShadow =
448
+ await testStHandlerSingleton.syncShadowToDeviceState(expDeltaShadowMsg);
449
+
450
+ // Assert
451
+ // --------------------------------------------------------------------
452
+ expect(actualReportedShadow).toEqual(expUpdateReported);
453
+ expect(runDetachedProcess).toHaveBeenCalledTimes(3);
454
+ expect(runDetachedProcess).toHaveBeenNthCalledWith(1, 'socat', [
455
+ `tcp4-listen:${ST_START_PORT_NUMBER + 1},fork`,
456
+ `tcp4:${enabledHttpPortInfo_1.ip}:${enabledHttpPortInfo_1.port}`
457
+ ]);
458
+ expect(runDetachedProcess).toHaveBeenNthCalledWith(2, 'socat', [
459
+ `tcp4-listen:${ST_START_PORT_NUMBER + 2},fork`,
460
+ `tcp4:${enabledHttpPortInfo_2.ip}:${enabledHttpPortInfo_2.port}`
461
+ ]);
462
+ expect(runDetachedProcess).toHaveBeenNthCalledWith(3, 'socat', [
463
+ `tcp4-listen:${ST_START_PORT_NUMBER + 3},fork`,
464
+ `tcp4:${enabledHttpPortInfo_3.ip}:${enabledHttpPortInfo_3.port}`
465
+ ]);
466
+ expect(killDetachedProcess).toHaveBeenCalledTimes(0);
467
+ });
468
+
469
+ it('should update reportedShadowState, all 1 SSH and 2 HTTP services enabled', async () => {
470
+ // Arrange
471
+ // --------------------------------------------------------------------
472
+ const expDeltaShadowMsg = createDeltaShadowMsg([
473
+ enabledSshPortInfo,
474
+ enabledHttpPortInfo_1,
475
+ enabledHttpPortInfo_2
476
+ ]);
477
+ const expUpdateReported = transformDeltaToUpdateReported(expDeltaShadowMsg);
478
+ jest.mocked(runDetachedProcess).mockResolvedValueOnce({} as ChildProcess);
479
+ jest.mocked(runDetachedProcess).mockResolvedValueOnce({} as ChildProcess);
480
+
481
+ // Act
482
+ // --------------------------------------------------------------------
483
+ const actualReportedShadow =
484
+ await testStHandlerSingleton.syncShadowToDeviceState(expDeltaShadowMsg);
485
+
486
+ // Assert
487
+ // --------------------------------------------------------------------
488
+ expect(actualReportedShadow).toEqual(expUpdateReported);
489
+ expect(runDetachedProcess).toHaveBeenCalledTimes(2);
490
+ expect(runDetachedProcess).toHaveBeenNthCalledWith(1, 'socat', [
491
+ `tcp4-listen:${ST_START_PORT_NUMBER + 1},fork`,
492
+ `tcp4:${enabledHttpPortInfo_1.ip}:${enabledHttpPortInfo_1.port}`
493
+ ]);
494
+ expect(runDetachedProcess).toHaveBeenNthCalledWith(2, 'socat', [
495
+ `tcp4-listen:${ST_START_PORT_NUMBER + 2},fork`,
496
+ `tcp4:${enabledHttpPortInfo_2.ip}:${enabledHttpPortInfo_2.port}`
497
+ ]);
498
+ expect(killDetachedProcess).toHaveBeenCalledTimes(0);
499
+ });
500
+
501
+ it('should update reportedShadowState, SSH1, HTTP1 and HTTP3 services enabled', async () => {
502
+ // Arrange
503
+ // --------------------------------------------------------------------
504
+ const expDeltaShadowMsg = createDeltaShadowMsg([
505
+ enabledSshPortInfo,
506
+ enabledHttpPortInfo_1,
507
+ disabledHttpPortInfo_2,
508
+ enabledHttpPortInfo_3
509
+ ]);
510
+ const expUpdateReported = transformDeltaToUpdateReported(expDeltaShadowMsg);
511
+ jest.mocked(runDetachedProcess).mockResolvedValueOnce({} as ChildProcess);
512
+ jest.mocked(runDetachedProcess).mockResolvedValueOnce({} as ChildProcess);
513
+
514
+ // Act
515
+ // --------------------------------------------------------------------
516
+ const actualReportedShadow =
517
+ await testStHandlerSingleton.syncShadowToDeviceState(expDeltaShadowMsg);
518
+
519
+ // Assert
520
+ // --------------------------------------------------------------------
521
+ expect(actualReportedShadow).toEqual(expUpdateReported);
522
+ expect(runDetachedProcess).toHaveBeenCalledTimes(2);
523
+ expect(runDetachedProcess).toHaveBeenNthCalledWith(1, 'socat', [
524
+ `tcp4-listen:${ST_START_PORT_NUMBER + 1},fork`,
525
+ `tcp4:${enabledHttpPortInfo_1.ip}:${enabledHttpPortInfo_1.port}`
526
+ ]);
527
+ expect(runDetachedProcess).not.toHaveBeenCalledWith('socat', [
528
+ `tcp4-listen:${ST_START_PORT_NUMBER + 2},fork`,
529
+ `tcp4:${enabledHttpPortInfo_2.ip}:${enabledHttpPortInfo_2.port}`
530
+ ]);
531
+ expect(runDetachedProcess).toHaveBeenNthCalledWith(2, 'socat', [
532
+ `tcp4-listen:${ST_START_PORT_NUMBER + 2},fork`,
533
+ `tcp4:${enabledHttpPortInfo_3.ip}:${enabledHttpPortInfo_3.port}`
534
+ ]);
535
+ expect(killDetachedProcess).toHaveBeenCalledTimes(0);
536
+ });
537
+
538
+ it('should update last HTTP reportedShadowState, all 1 SSH and 3 HTTP services enabled', async () => {
539
+ // Arrange
540
+ // --------------------------------------------------------------------
541
+ const expectedShadow = createDeltaShadowMsg([
542
+ enabledSshPortInfo,
543
+ enabledHttpPortInfo_1,
544
+ enabledHttpPortInfo_2,
545
+ enabledHttpPortInfo_3
546
+ ]);
547
+ const expUpdateReported = transformDeltaToUpdateReported(expectedShadow);
548
+ const desiredDeltaMsg = createDeltaShadowMsg([
549
+ enabledSshPortInfo,
550
+ enabledHttpPortInfo_1,
551
+ enabledHttpPortInfo_2,
552
+ enabledHttpPortInfo_3
553
+ ]);
554
+ jest.mocked(runDetachedProcess).mockResolvedValueOnce({} as ChildProcess);
555
+ jest.mocked(runDetachedProcess).mockResolvedValueOnce({} as ChildProcess);
556
+
557
+ // Act
558
+ // --------------------------------------------------------------------
559
+ const actualReportedShadow =
560
+ await testStHandlerSingleton.syncShadowToDeviceState(desiredDeltaMsg);
561
+
562
+ // Assert
563
+ // --------------------------------------------------------------------
564
+ expect(actualReportedShadow).toEqual(expUpdateReported);
565
+ expect(runDetachedProcess).toHaveBeenCalledTimes(3);
566
+ expect(runDetachedProcess).toHaveBeenNthCalledWith(1, 'socat', [
567
+ `tcp4-listen:${ST_START_PORT_NUMBER + 1},fork`,
568
+ `tcp4:${enabledHttpPortInfo_1.ip}:${enabledHttpPortInfo_1.port}`
569
+ ]);
570
+ expect(runDetachedProcess).toHaveBeenNthCalledWith(2, 'socat', [
571
+ `tcp4-listen:${ST_START_PORT_NUMBER + 2},fork`,
572
+ `tcp4:${enabledHttpPortInfo_2.ip}:${enabledHttpPortInfo_2.port}`
573
+ ]);
574
+ expect(runDetachedProcess).toHaveBeenCalledWith('socat', [
575
+ `tcp4-listen:${ST_START_PORT_NUMBER + 3},fork`,
576
+ `tcp4:${enabledHttpPortInfo_3.ip}:${enabledHttpPortInfo_3.port}`
577
+ ]);
578
+ expect(killDetachedProcess).toHaveBeenCalledTimes(0);
579
+ });
580
+
581
+ it('should update last HTTP reportedShadowState, when ports changing the state from disabled to enabled', async () => {
582
+ // Arrange
583
+ // --------------------------------------------------------------------
584
+ const disableDeltaMsg = createDeltaShadowMsg([
585
+ disabledSshPortInfo,
586
+ disabledHttpPortInfo_1,
587
+ disabledHttpPortInfo_2,
588
+ disabledHttpPortInfo_3
589
+ ]);
590
+ const disableUpdateReported =
591
+ transformDeltaToUpdateReported(disableDeltaMsg);
592
+ const disabledReportedShadow =
593
+ await testStHandlerSingleton.syncShadowToDeviceState(disableDeltaMsg);
594
+ expect(disabledReportedShadow).toEqual(disableUpdateReported);
595
+ const enableDeltaMsg = createDeltaShadowMsg([
596
+ enabledSshPortInfo,
597
+ enabledHttpPortInfo_1,
598
+ enabledHttpPortInfo_2,
599
+ enabledHttpPortInfo_3
600
+ ]);
601
+ const expectedDeltaShadow = createDeltaShadowMsg([
602
+ enabledSshPortInfo,
603
+ enabledHttpPortInfo_1,
604
+ enabledHttpPortInfo_2,
605
+ enabledHttpPortInfo_3
606
+ ]);
607
+ const expectedUpdateReported =
608
+ transformDeltaToUpdateReported(expectedDeltaShadow);
609
+ jest.mocked(runDetachedProcess).mockResolvedValueOnce({} as ChildProcess);
610
+ jest.mocked(runDetachedProcess).mockResolvedValueOnce({} as ChildProcess);
611
+
612
+ // Act
613
+ // --------------------------------------------------------------------
614
+ const actualReportedShadow =
615
+ await testStHandlerSingleton.syncShadowToDeviceState(enableDeltaMsg);
616
+
617
+ // Assert
618
+ // --------------------------------------------------------------------
619
+ expect(actualReportedShadow).toEqual(expectedUpdateReported);
620
+ expect(runDetachedProcess).toHaveBeenCalledTimes(3);
621
+ expect(runDetachedProcess).toHaveBeenNthCalledWith(1, 'socat', [
622
+ `tcp4-listen:${ST_START_PORT_NUMBER + 1},fork`,
623
+ `tcp4:${enabledHttpPortInfo_1.ip}:${enabledHttpPortInfo_1.port}`
624
+ ]);
625
+ expect(runDetachedProcess).toHaveBeenNthCalledWith(2, 'socat', [
626
+ `tcp4-listen:${ST_START_PORT_NUMBER + 2},fork`,
627
+ `tcp4:${enabledHttpPortInfo_2.ip}:${enabledHttpPortInfo_2.port}`
628
+ ]);
629
+ expect(runDetachedProcess).toHaveBeenCalledWith('socat', [
630
+ `tcp4-listen:${ST_START_PORT_NUMBER + 3},fork`,
631
+ `tcp4:${enabledHttpPortInfo_3.ip}:${enabledHttpPortInfo_3.port}`
632
+ ]);
633
+ expect(killDetachedProcess).toHaveBeenCalledTimes(0);
634
+ });
635
+
636
+ //---------------------------------------------------------------------------
637
+ // test destroy function
638
+ //---------------------------------------------------------------------------
639
+ it('should destroy all socat processes', async () => {
640
+ // Arrange
641
+ // --------------------------------------------------------------------
642
+ const stConfig = [enabledSshPortInfo, enabledHttpPortInfo_1];
643
+ const deltaShadowMsg = createDeltaShadowMsg(stConfig);
644
+ const updateReported = transformDeltaToUpdateReported(deltaShadowMsg);
645
+ stConfig
646
+ .filter((portInfo) => portInfo.type === 'HTTP')
647
+ .forEach((portInfo) => {
648
+ jest
649
+ .mocked(runDetachedProcess)
650
+ .mockResolvedValueOnce({} as ChildProcess);
651
+ });
652
+ const newReportedShadow =
653
+ await testStHandlerSingleton.syncShadowToDeviceState(deltaShadowMsg);
654
+ expect(newReportedShadow).toEqual(updateReported);
655
+ jest.clearAllMocks(); // after setting up the shadow, we need to reset mocks again
656
+ jest.resetModules();
657
+ const expStConfig = [disabledSshPortInfo];
658
+ const expDeltaShadowMsg = createDeltaShadowMsg(expStConfig);
659
+ const expUpdateReported = transformDeltaToUpdateReported(expDeltaShadowMsg);
660
+
661
+ // Act
662
+ // --------------------------------------------------------------------
663
+ await testStHandlerSingleton.destroy();
664
+ const actualReportedShadow = testStHandlerSingleton.getSecureTunnelShadow();
665
+
666
+ // Assert
667
+ // --------------------------------------------------------------------
668
+ expect(actualReportedShadow).toEqual(expUpdateReported);
669
+ });
670
+
671
+ //---------------------------------------------------------------------------
672
+ // test secureTunnelNotifyHandler function
673
+ //---------------------------------------------------------------------------
674
+ it('should start Secure Tunnel, given localproxy already downloaded and default shadow is not enabled, that is default SSH connection', async () => {
675
+ // Arrange
676
+ // --------------------------------------------------------------------
677
+ mockJsSpawner.exists.mockResolvedValueOnce(true); // mock that localproxy already exists on file system
678
+ const message: SecureTunnelNotifyMsg = {
679
+ clientAccessToken: 'DefaultSSHConnection_001',
680
+ region: 'us-west-2',
681
+ services: ['SSH']
682
+ };
683
+ const expectedLocalproxyArgs = [
684
+ '--destination-app',
685
+ '22',
686
+ '--region',
687
+ message.region,
688
+ '--capath',
689
+ AWS_ROOT_CERTIFICATE_FILE_PATH,
690
+ '--local-bind-address',
691
+ '0.0.0.0',
692
+ '-t',
693
+ message.clientAccessToken
694
+ ];
695
+
696
+ // Act
697
+ // --------------------------------------------------------------------
698
+ await testStHandlerSingleton.secureTunnelNotifyHandler(message);
699
+
700
+ // Assert
701
+ // --------------------------------------------------------------------
702
+ expect(mockJsSpawner.exists).toHaveBeenCalledTimes(1);
703
+ expect(getArch).not.toHaveBeenCalled();
704
+ expect(getOsVersion).not.toHaveBeenCalled();
705
+ expect(getDistribution).not.toHaveBeenCalled();
706
+ expect(mockJsSpawner.mkdirp).not.toHaveBeenCalled();
707
+ expect(mockJsSpawner.run).not.toHaveBeenCalled();
708
+ expect(downloadFile).not.toHaveBeenCalled();
709
+ expect(runDetachedProcess).toHaveBeenCalledTimes(1);
710
+ expect(runDetachedProcess).toHaveBeenCalledWith(
711
+ SECURE_TUNNEL_BIN_PATH,
712
+ expectedLocalproxyArgs
713
+ );
714
+ expect(killDetachedProcess).not.toHaveBeenCalled();
715
+ });
716
+
717
+ const testCasesForDIfferentLocalproxyEnv: [string, string, string][] = [
718
+ // linuxDistro, osVersion, arch
719
+ ['debian', '12', 'aarch64'],
720
+ ['debian', '12', 'arm64'],
721
+ ['macos', '13.4', 'arm64v8'],
722
+ ['raspbian', '9.13', 'armhf'],
723
+ ['raspbian', '11', 'armhf'],
724
+ ['ubuntu', '18.04', 'amd64'],
725
+ ['ubuntu', '18.04', 'arm64'],
726
+ ['ubuntu', '18.04', 'armhf'],
727
+ ['ubuntu', '20.04', 'amd64'],
728
+ ['ubuntu', '20.04', 'arm64'],
729
+ ['ubuntu', '20.04', 'armhf'],
730
+ ['ubuntu', '22.04', 'amd64'],
731
+ ['ubuntu', '22.04', 'arm64'],
732
+ ['ubuntu', '23.04', 'amd64'],
733
+ ['ubuntu', '23.04', 'arm64'],
734
+ ['ubuntu', '23.10', 'amd64'],
735
+ ['ubuntu', '23.10', 'arm64']
736
+ ];
737
+ test.each(testCasesForDIfferentLocalproxyEnv)(
738
+ 'should start Secure Tunnel, given localproxy downloaded needed and default shadow is not enabled, that is default SSH connection',
739
+ async (linuxDistro: string, osVersion: string, arch: string) => {
740
+ // Arrange
741
+ // --------------------------------------------------------------------
742
+ mockJsSpawner.exists.mockResolvedValueOnce(false); // mock that localproxy does not exist on file system
743
+ jest.mocked(getArch).mockResolvedValueOnce(arch);
744
+ jest.mocked(getOsVersion).mockResolvedValueOnce(osVersion);
745
+ jest.mocked(getDistribution).mockResolvedValueOnce(linuxDistro);
746
+ const expectedUrl = `${aaiArtifactsBucketUrl}/securetunnel/${linuxDistro}/${osVersion}/${arch}/${SECURE_TUNNEL_BIN_NAME}`;
747
+ const message: SecureTunnelNotifyMsg = {
748
+ clientAccessToken: 'DefaultSSHConnection_001',
749
+ region: 'us-west-2',
750
+ services: ['SSH']
751
+ };
752
+ const expectedLocalproxyArgs = [
753
+ '--destination-app',
754
+ '22',
755
+ '--region',
756
+ message.region,
757
+ '--capath',
758
+ AWS_ROOT_CERTIFICATE_FILE_PATH,
759
+ '--local-bind-address',
760
+ '0.0.0.0',
761
+ '-t',
762
+ message.clientAccessToken
763
+ ];
764
+
765
+ // Act
766
+ // --------------------------------------------------------------------
767
+ await testStHandlerSingleton.secureTunnelNotifyHandler(message);
768
+
769
+ // Assert
770
+ // --------------------------------------------------------------------
771
+ expect(mockJsSpawner.exists).toHaveBeenCalledTimes(1);
772
+ expect(mockJsSpawner.exists).toHaveBeenCalledWith(SECURE_TUNNEL_BIN_PATH);
773
+ expect(getArch).toHaveBeenCalledTimes(1);
774
+ expect(getOsVersion).toHaveBeenCalledTimes(1);
775
+ expect(getDistribution).toHaveBeenCalledTimes(1);
776
+ expect(mockJsSpawner.mkdirp).toHaveBeenCalledTimes(1);
777
+ expect(mockJsSpawner.mkdirp).toHaveBeenCalledWith(
778
+ join(AAI_DIR, SECURE_TUNNEL_BIN_DIR)
779
+ );
780
+ expect(mockJsSpawner.run).toHaveBeenCalledTimes(1);
781
+ expect(mockJsSpawner.run).toHaveBeenCalledWith({
782
+ exe: 'chmod',
783
+ args: ['+x', SECURE_TUNNEL_BIN_PATH]
784
+ });
785
+ expect(downloadFile).toHaveBeenCalledTimes(1);
786
+ expect(downloadFile).toHaveBeenCalledWith({
787
+ url: expectedUrl,
788
+ path: SECURE_TUNNEL_BIN_PATH,
789
+ errorMessage: `Secure Tunnel bin for ${linuxDistro} ${osVersion} ${arch} not found}`
790
+ });
791
+ expect(runDetachedProcess).toHaveBeenCalledTimes(1);
792
+ expect(runDetachedProcess).toHaveBeenCalledWith(
793
+ SECURE_TUNNEL_BIN_PATH,
794
+ expectedLocalproxyArgs
795
+ );
796
+ expect(killDetachedProcess).not.toHaveBeenCalled();
797
+ }
798
+ );
799
+
800
+ const testCasesForInvalidSecureTunnelNotificationParameters: [
801
+ SecureTunnelNotifyMsg
802
+ ][] = [
803
+ [{ clientAccessToken: '', region: '', services: [] }], // invalid everything
804
+ [
805
+ {
806
+ clientAccessToken: '', // invalid token
807
+ region: 'us-west-2',
808
+ services: ['SSH']
809
+ }
810
+ ],
811
+ [
812
+ {
813
+ clientAccessToken: '001',
814
+ region: 'us-west-1', // not supported region
815
+ services: ['SSH']
816
+ }
817
+ ],
818
+ [
819
+ {
820
+ clientAccessToken: '002',
821
+ region: 'us-east-2', // not valid region
822
+ services: ['SSH']
823
+ }
824
+ ],
825
+ [
826
+ {
827
+ clientAccessToken: '003',
828
+ region: 'eu-central-1', // not valid region
829
+ services: ['SSH']
830
+ }
831
+ ],
832
+ [
833
+ {
834
+ clientAccessToken: '004',
835
+ region: 'invalid region', // invalid region
836
+ services: ['SSH']
837
+ }
838
+ ],
839
+ [
840
+ {
841
+ clientAccessToken: '005',
842
+ region: 'us-west-2',
843
+ services: [] // no service field
844
+ }
845
+ ],
846
+ [
847
+ {
848
+ clientAccessToken: '006',
849
+ region: 'us-west-2',
850
+ services: [''] // empty field in service
851
+ }
852
+ ],
853
+ [
854
+ {
855
+ clientAccessToken: '007',
856
+ region: 'us-west-2',
857
+ services: ['SSH', ''] // one of the fields is empty in service
858
+ }
859
+ ],
860
+ [
861
+ {
862
+ clientAccessToken: '008',
863
+ region: 'us-west-2',
864
+ services: ['SSH1', '', 'HTTP1'] // one of the fields is empty in service
865
+ }
866
+ ],
867
+ [
868
+ {
869
+ clientAccessToken: '009',
870
+ region: 'us-west-2',
871
+ services: ['SSH1', 'NOT_VALID'] // one of the fields is invalid
872
+ }
873
+ ],
874
+ [
875
+ {
876
+ clientAccessToken: '010',
877
+ region: 'us-west-2',
878
+ services: ['FTP', 'HTTP1'] // one of the fields is invalid
879
+ }
880
+ ],
881
+ [
882
+ {
883
+ clientAccessToken: '011',
884
+ region: 'us-west-2',
885
+ services: ['SSH1', 'FTP1', 'HTTP1'] // one of the fields is invalid
886
+ }
887
+ ]
888
+ ];
889
+ test.each(testCasesForInvalidSecureTunnelNotificationParameters)(
890
+ 'should NOT start Secure Tunnel, given one of the fields of message is invalid',
891
+ async (message) => {
892
+ // Arrange
893
+ // --------------------------------------------------------------------
894
+ mockJsSpawner.exists.mockResolvedValueOnce(true); // mock that localproxy already exists on file system
895
+ const expectedLocalproxyArgs = [
896
+ '--destination-app',
897
+ '22',
898
+ '--region',
899
+ message.region,
900
+ '--capath',
901
+ AWS_ROOT_CERTIFICATE_FILE_PATH,
902
+ '--local-bind-address',
903
+ '0.0.0.0',
904
+ '-t',
905
+ message.clientAccessToken
906
+ ];
907
+
908
+ // Act
909
+ // --------------------------------------------------------------------
910
+ await testStHandlerSingleton.secureTunnelNotifyHandler(message);
911
+
912
+ // Assert
913
+ // --------------------------------------------------------------------
914
+ expect(mockJsSpawner.exists).toHaveBeenCalledTimes(0);
915
+ expect(getArch).not.toHaveBeenCalled();
916
+ expect(getOsVersion).not.toHaveBeenCalled();
917
+ expect(getDistribution).not.toHaveBeenCalled();
918
+ expect(mockJsSpawner.mkdirp).not.toHaveBeenCalled();
919
+ expect(downloadFile).not.toHaveBeenCalled();
920
+ expect(mockJsSpawner.run).not.toHaveBeenCalled();
921
+ expect(runDetachedProcess).not.toHaveBeenCalled();
922
+ expect(killDetachedProcess).not.toHaveBeenCalled();
923
+ }
924
+ );
925
+
926
+ const testCasesMismatchBetweenServicesAndConfig: [
927
+ SecureTunnelNotifyMsg,
928
+ ReadOnlyPortInfo[]
929
+ ][] = [
930
+ [
931
+ {
932
+ clientAccessToken: '001',
933
+ region: 'us-west-2',
934
+ services: ['SSH']
935
+ },
936
+ [enabledHttpPortInfo_1]
937
+ ],
938
+ [
939
+ {
940
+ clientAccessToken: '002',
941
+ region: 'us-west-2',
942
+ services: ['SSH1', 'HTTP1', 'HTTP2']
943
+ },
944
+ [enabledHttpPortInfo_1, enabledHttpPortInfo_2, enabledHttpPortInfo_3]
945
+ ],
946
+ [
947
+ {
948
+ clientAccessToken: '003',
949
+ region: 'us-west-2',
950
+ services: ['HTTP1', 'HTTP2']
951
+ },
952
+ [enabledHttpPortInfo_1, enabledHttpPortInfo_2, enabledHttpPortInfo_3]
953
+ ],
954
+ [
955
+ {
956
+ clientAccessToken: '004',
957
+ region: 'us-west-2',
958
+ services: ['HTTP1', 'HTTP2', 'HTTP3']
959
+ },
960
+ [enabledHttpPortInfo_1, enabledHttpPortInfo_2]
961
+ ],
962
+ [
963
+ {
964
+ clientAccessToken: '005',
965
+ region: 'us-west-2',
966
+ services: ['HTTP1', 'HTTP2', 'HTTP3', 'HTTP4']
967
+ },
968
+ [enabledHttpPortInfo_1, enabledHttpPortInfo_2, enabledHttpPortInfo_3]
969
+ ],
970
+ [
971
+ {
972
+ clientAccessToken: '006',
973
+ region: 'us-west-2',
974
+ services: ['SSH', 'HTTP1']
975
+ },
976
+ [enabledSshPortInfo]
977
+ ],
978
+ [
979
+ {
980
+ clientAccessToken: '006',
981
+ region: 'us-west-2',
982
+ services: ['SSH1', 'SSH2']
983
+ },
984
+ [enabledSshPortInfo, invalidSshPortInfo]
985
+ ]
986
+ ];
987
+ test.each(testCasesMismatchBetweenServicesAndConfig)(
988
+ 'should NOT start Secure Tunnel, given services mismatch received config',
989
+ async (message, stConfig) => {
990
+ // Arrange
991
+ // --------------------------------------------------------------------
992
+ const deltaShadowMsg = createDeltaShadowMsg(stConfig);
993
+ const updateReported = transformDeltaToUpdateReported(deltaShadowMsg);
994
+ stConfig
995
+ .filter((portInfo) => portInfo.type === 'HTTP')
996
+ .forEach((portInfo) => {
997
+ jest
998
+ .mocked(runDetachedProcess)
999
+ .mockResolvedValueOnce({} as ChildProcess);
1000
+ });
1001
+ const actualReportedShadow =
1002
+ await testStHandlerSingleton.syncShadowToDeviceState(deltaShadowMsg);
1003
+ expect(actualReportedShadow).toEqual(updateReported);
1004
+ jest.clearAllMocks(); // after setting up the shadow, we need to reset mocks again
1005
+ jest.resetModules();
1006
+ mockJsSpawner.exists.mockResolvedValueOnce(true); // mock that localproxy already exists on file system
1007
+ const expectedLocalproxyArgs = [
1008
+ '--destination-app',
1009
+ '22',
1010
+ '--region',
1011
+ message.region,
1012
+ '--capath',
1013
+ AWS_ROOT_CERTIFICATE_FILE_PATH,
1014
+ '--local-bind-address',
1015
+ '0.0.0.0',
1016
+ '-t',
1017
+ message.clientAccessToken
1018
+ ];
1019
+
1020
+ // Act
1021
+ // --------------------------------------------------------------------
1022
+ await testStHandlerSingleton.secureTunnelNotifyHandler(message);
1023
+
1024
+ // Assert
1025
+ // --------------------------------------------------------------------
1026
+ expect(mockJsSpawner.exists).toHaveBeenCalledTimes(0);
1027
+ expect(getArch).not.toHaveBeenCalled();
1028
+ expect(getOsVersion).not.toHaveBeenCalled();
1029
+ expect(getDistribution).not.toHaveBeenCalled();
1030
+ expect(mockJsSpawner.mkdirp).not.toHaveBeenCalled();
1031
+ expect(downloadFile).not.toHaveBeenCalled();
1032
+ expect(runDetachedProcess).not.toHaveBeenCalled();
1033
+ expect(killDetachedProcess).not.toHaveBeenCalled();
1034
+ }
1035
+ );
1036
+
1037
+ const testCasesMatchBetweenServicesAndConfig: [
1038
+ SecureTunnelNotifyMsg,
1039
+ ReadOnlyPortInfo[],
1040
+ string
1041
+ ][] = [
1042
+ [
1043
+ {
1044
+ clientAccessToken: '001',
1045
+ region: 'us-west-2',
1046
+ services: ['SSH']
1047
+ },
1048
+ [enabledSshPortInfo],
1049
+ '22'
1050
+ ],
1051
+ [
1052
+ {
1053
+ clientAccessToken: '002',
1054
+ region: 'us-west-2',
1055
+ services: ['SSH1', 'HTTP1', 'HTTP2']
1056
+ },
1057
+ [
1058
+ enabledSshPortInfo,
1059
+ enabledHttpPortInfo_1,
1060
+ enabledHttpPortInfo_2,
1061
+ disabledHttpPortInfo_3
1062
+ ],
1063
+ 'SSH1=22,HTTP1=5011,HTTP2=5012'
1064
+ ],
1065
+ [
1066
+ {
1067
+ clientAccessToken: '003',
1068
+ region: 'us-west-2',
1069
+ services: ['HTTP1', 'HTTP2']
1070
+ },
1071
+ [enabledHttpPortInfo_1, enabledHttpPortInfo_2],
1072
+ 'HTTP1=5011,HTTP2=5012'
1073
+ ],
1074
+ [
1075
+ {
1076
+ clientAccessToken: '004',
1077
+ region: 'us-west-2',
1078
+ services: ['HTTP1', 'HTTP2', 'HTTP3']
1079
+ },
1080
+ [enabledHttpPortInfo_1, enabledHttpPortInfo_2, enabledHttpPortInfo_3],
1081
+ 'HTTP1=5011,HTTP2=5012,HTTP3=5013'
1082
+ ],
1083
+ [
1084
+ {
1085
+ clientAccessToken: '005',
1086
+ region: 'us-west-2',
1087
+ services: ['HTTP1', 'HTTP2', 'HTTP3']
1088
+ },
1089
+ [
1090
+ disabledSshPortInfo,
1091
+ enabledHttpPortInfo_1,
1092
+ enabledHttpPortInfo_2,
1093
+ enabledHttpPortInfo_3
1094
+ ],
1095
+ 'HTTP1=5011,HTTP2=5012,HTTP3=5013'
1096
+ ],
1097
+ [
1098
+ {
1099
+ clientAccessToken: '006',
1100
+ region: 'us-west-2',
1101
+ services: ['SSH1', 'HTTP1']
1102
+ },
1103
+ [
1104
+ enabledSshPortInfo,
1105
+ disabledHttpPortInfo_1,
1106
+ enabledHttpPortInfo_2,
1107
+ disabledHttpPortInfo_3
1108
+ ],
1109
+ 'SSH1=22,HTTP1=5011'
1110
+ ]
1111
+ ];
1112
+ test.each(testCasesMatchBetweenServicesAndConfig)(
1113
+ 'should start Secure Tunnel, given services and port config match',
1114
+ async (message, stConfig, expectedPortMapping) => {
1115
+ // Arrange
1116
+ // --------------------------------------------------------------------
1117
+ const deltaShadowMsg = createDeltaShadowMsg(stConfig);
1118
+ const updateReported = transformDeltaToUpdateReported(deltaShadowMsg);
1119
+ stConfig
1120
+ .filter((portInfo) => portInfo.type === 'HTTP')
1121
+ .forEach((portInfo) => {
1122
+ jest
1123
+ .mocked(runDetachedProcess)
1124
+ .mockResolvedValueOnce({} as ChildProcess);
1125
+ });
1126
+ const actualReportedShadow =
1127
+ await testStHandlerSingleton.syncShadowToDeviceState(deltaShadowMsg);
1128
+ expect(actualReportedShadow).toEqual(updateReported);
1129
+ jest.clearAllMocks(); // after setting up the shadow, we need to reset mocks again
1130
+ jest.resetModules();
1131
+ mockJsSpawner.exists.mockResolvedValueOnce(true); // mock that localproxy already exists on file system
1132
+ const expectedLocalproxyArgs = [
1133
+ '--destination-app',
1134
+ expectedPortMapping,
1135
+ '--region',
1136
+ message.region,
1137
+ '--capath',
1138
+ AWS_ROOT_CERTIFICATE_FILE_PATH,
1139
+ '--local-bind-address',
1140
+ '0.0.0.0',
1141
+ '-t',
1142
+ message.clientAccessToken
1143
+ ];
1144
+
1145
+ // Act
1146
+ // --------------------------------------------------------------------
1147
+ await testStHandlerSingleton.secureTunnelNotifyHandler(message);
1148
+
1149
+ // Assert
1150
+ // --------------------------------------------------------------------
1151
+ expect(mockJsSpawner.exists).toHaveBeenCalledTimes(1);
1152
+ expect(getArch).not.toHaveBeenCalled();
1153
+ expect(getOsVersion).not.toHaveBeenCalled();
1154
+ expect(getDistribution).not.toHaveBeenCalled();
1155
+ expect(mockJsSpawner.mkdirp).not.toHaveBeenCalled();
1156
+ expect(downloadFile).not.toHaveBeenCalled();
1157
+ expect(mockJsSpawner.run).not.toHaveBeenCalled();
1158
+ expect(runDetachedProcess).toHaveBeenCalledTimes(1);
1159
+ expect(runDetachedProcess).toHaveBeenCalledWith(
1160
+ SECURE_TUNNEL_BIN_PATH,
1161
+ expectedLocalproxyArgs
1162
+ );
1163
+ expect(killDetachedProcess).toHaveBeenCalledTimes(0);
1164
+ }
1165
+ );
1166
+
1167
+ it('should start Secure Tunnel, given 1 SSH and 1 HTTP ports enabled', async () => {
1168
+ // Arrange
1169
+ // --------------------------------------------------------------------
1170
+ const message = {
1171
+ clientAccessToken: '089',
1172
+ region: 'us-west-2',
1173
+ services: ['SSH1', 'HTTP1']
1174
+ };
1175
+ let stConfig = [enabledSshPortInfo, enabledHttpPortInfo_1];
1176
+ const expectedPortMapping = 'SSH1=22,HTTP1=5011';
1177
+ let deltaShadowMsg = createDeltaShadowMsg(stConfig);
1178
+ let updateReported = transformDeltaToUpdateReported(deltaShadowMsg);
1179
+ stConfig
1180
+ .filter((portInfo) => portInfo.type === 'HTTP')
1181
+ .forEach((portInfo) => {
1182
+ jest
1183
+ .mocked(runDetachedProcess)
1184
+ .mockResolvedValueOnce({} as ChildProcess);
1185
+ });
1186
+ const socatArgs = [
1187
+ `tcp4-listen:${ST_START_PORT_NUMBER + 1},fork`,
1188
+ `tcp4:${enabledHttpPortInfo_1.ip}:${enabledHttpPortInfo_1.port}`
1189
+ ];
1190
+ const beforeReportedShadow =
1191
+ await testStHandlerSingleton.syncShadowToDeviceState(deltaShadowMsg);
1192
+ expect(beforeReportedShadow).toEqual(updateReported);
1193
+ jest.clearAllMocks(); // after setting up the shadow, we need to reset mocks again
1194
+ jest.resetModules();
1195
+ mockJsSpawner.exists.mockResolvedValueOnce(true); // mock that localproxy already exists on file system
1196
+ const expectedLocalproxyArgs = [
1197
+ '--destination-app',
1198
+ expectedPortMapping,
1199
+ '--region',
1200
+ message.region,
1201
+ '--capath',
1202
+ AWS_ROOT_CERTIFICATE_FILE_PATH,
1203
+ '--local-bind-address',
1204
+ '0.0.0.0',
1205
+ '-t',
1206
+ message.clientAccessToken
1207
+ ];
1208
+ await testStHandlerSingleton.secureTunnelNotifyHandler(message);
1209
+ jest.clearAllMocks(); // after setting up the shadow, we need to reset mocks again
1210
+ jest.resetModules();
1211
+ stConfig = [disabledSshPortInfo, disabledHttpPortInfo_1];
1212
+ deltaShadowMsg = createDeltaShadowMsg(stConfig);
1213
+ updateReported = transformDeltaToUpdateReported(deltaShadowMsg);
1214
+ stConfig.forEach((portInfo) => {
1215
+ jest.mocked(runDetachedProcess).mockResolvedValueOnce({} as ChildProcess);
1216
+ });
1217
+
1218
+ // Act
1219
+ // --------------------------------------------------------------------
1220
+ const actualReportedShadow =
1221
+ await testStHandlerSingleton.syncShadowToDeviceState(deltaShadowMsg);
1222
+
1223
+ // Assert
1224
+ // --------------------------------------------------------------------
1225
+ expect(actualReportedShadow).toEqual(updateReported);
1226
+ expect(mockJsSpawner.exists).not.toHaveBeenCalled();
1227
+ expect(getArch).not.toHaveBeenCalled();
1228
+ expect(getOsVersion).not.toHaveBeenCalled();
1229
+ expect(getDistribution).not.toHaveBeenCalled();
1230
+ expect(mockJsSpawner.mkdirp).not.toHaveBeenCalled();
1231
+ expect(downloadFile).not.toHaveBeenCalled();
1232
+ expect(mockJsSpawner.run).not.toHaveBeenCalled();
1233
+ expect(runDetachedProcess).toHaveBeenCalledTimes(0);
1234
+ expect(killDetachedProcess).toHaveBeenCalledTimes(2);
1235
+ expect(killDetachedProcess).toHaveBeenCalledWith({}, [
1236
+ ['socat', ...socatArgs].join(' ')
1237
+ ]);
1238
+ });
1239
+ });