@alwaysai/device-agent 1.3.1 → 1.5.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 (183) hide show
  1. package/lib/application-control/config.js +2 -2
  2. package/lib/application-control/config.js.map +1 -1
  3. package/lib/application-control/environment-variables.d.ts.map +1 -1
  4. package/lib/application-control/environment-variables.js +9 -4
  5. package/lib/application-control/environment-variables.js.map +1 -1
  6. package/lib/application-control/environment-variables.test.js +1 -1
  7. package/lib/application-control/environment-variables.test.js.map +1 -1
  8. package/lib/application-control/install.d.ts.map +1 -1
  9. package/lib/application-control/install.js +7 -2
  10. package/lib/application-control/install.js.map +1 -1
  11. package/lib/application-control/models.d.ts +5 -0
  12. package/lib/application-control/models.d.ts.map +1 -1
  13. package/lib/application-control/models.js +28 -14
  14. package/lib/application-control/models.js.map +1 -1
  15. package/lib/application-control/status.d.ts.map +1 -1
  16. package/lib/application-control/status.js +14 -17
  17. package/lib/application-control/status.js.map +1 -1
  18. package/lib/application-control/utils.js +2 -2
  19. package/lib/application-control/utils.js.map +1 -1
  20. package/lib/cloud-connection/device-agent-cloud-connection.d.ts +5 -5
  21. package/lib/cloud-connection/device-agent-cloud-connection.d.ts.map +1 -1
  22. package/lib/cloud-connection/device-agent-cloud-connection.js +140 -105
  23. package/lib/cloud-connection/device-agent-cloud-connection.js.map +1 -1
  24. package/lib/cloud-connection/live-updates-handler.d.ts +4 -2
  25. package/lib/cloud-connection/live-updates-handler.d.ts.map +1 -1
  26. package/lib/cloud-connection/live-updates-handler.js +46 -25
  27. package/lib/cloud-connection/live-updates-handler.js.map +1 -1
  28. package/lib/cloud-connection/live-updates-handler.test.js +132 -16
  29. package/lib/cloud-connection/live-updates-handler.test.js.map +1 -1
  30. package/lib/cloud-connection/messages.d.ts.map +1 -1
  31. package/lib/cloud-connection/messages.js +3 -4
  32. package/lib/cloud-connection/messages.js.map +1 -1
  33. package/lib/cloud-connection/passthrough-handler.d.ts +5 -3
  34. package/lib/cloud-connection/passthrough-handler.d.ts.map +1 -1
  35. package/lib/cloud-connection/passthrough-handler.js +76 -62
  36. package/lib/cloud-connection/passthrough-handler.js.map +1 -1
  37. package/lib/cloud-connection/shadow-handler.d.ts +16 -21
  38. package/lib/cloud-connection/shadow-handler.d.ts.map +1 -1
  39. package/lib/cloud-connection/shadow-handler.js +162 -108
  40. package/lib/cloud-connection/shadow-handler.js.map +1 -1
  41. package/lib/cloud-connection/shadow-handler.test.js +100 -83
  42. package/lib/cloud-connection/shadow-handler.test.js.map +1 -1
  43. package/lib/cloud-connection/transaction-manager.d.ts +3 -0
  44. package/lib/cloud-connection/transaction-manager.d.ts.map +1 -1
  45. package/lib/cloud-connection/transaction-manager.js +11 -0
  46. package/lib/cloud-connection/transaction-manager.js.map +1 -1
  47. package/lib/cloud-connection/transaction-manager.test.js +102 -0
  48. package/lib/cloud-connection/transaction-manager.test.js.map +1 -1
  49. package/lib/device-control/device-control.d.ts +16 -15
  50. package/lib/device-control/device-control.d.ts.map +1 -1
  51. package/lib/device-control/device-control.js +117 -18
  52. package/lib/device-control/device-control.js.map +1 -1
  53. package/lib/docker/docker-compose.d.ts +14 -0
  54. package/lib/docker/docker-compose.d.ts.map +1 -0
  55. package/lib/docker/docker-compose.js +56 -0
  56. package/lib/docker/docker-compose.js.map +1 -0
  57. package/lib/index.js +2 -5
  58. package/lib/index.js.map +1 -1
  59. package/lib/infrastructure/agent-config.d.ts +45 -14
  60. package/lib/infrastructure/agent-config.d.ts.map +1 -1
  61. package/lib/infrastructure/agent-config.js +30 -15
  62. package/lib/infrastructure/agent-config.js.map +1 -1
  63. package/lib/infrastructure/agent-config.test.js +3 -0
  64. package/lib/infrastructure/agent-config.test.js.map +1 -1
  65. package/lib/local-connection/rabbitmq-connection.js +11 -11
  66. package/lib/local-connection/rabbitmq-connection.js.map +1 -1
  67. package/lib/secure-tunneling/secure-tunneling.d.ts +97 -0
  68. package/lib/secure-tunneling/secure-tunneling.d.ts.map +1 -0
  69. package/lib/secure-tunneling/secure-tunneling.js +435 -0
  70. package/lib/secure-tunneling/secure-tunneling.js.map +1 -0
  71. package/lib/secure-tunneling/secure-tunneling.test.d.ts +2 -0
  72. package/lib/secure-tunneling/secure-tunneling.test.d.ts.map +1 -0
  73. package/lib/secure-tunneling/secure-tunneling.test.js +1070 -0
  74. package/lib/secure-tunneling/secure-tunneling.test.js.map +1 -0
  75. package/lib/secure-tunneling/spawner-detached.d.ts +6 -0
  76. package/lib/secure-tunneling/spawner-detached.d.ts.map +1 -0
  77. package/lib/secure-tunneling/spawner-detached.js +107 -0
  78. package/lib/secure-tunneling/spawner-detached.js.map +1 -0
  79. package/lib/subcommands/app/analytics.d.ts.map +1 -1
  80. package/lib/subcommands/app/analytics.js +9 -13
  81. package/lib/subcommands/app/analytics.js.map +1 -1
  82. package/lib/subcommands/app/env-vars.d.ts.map +1 -1
  83. package/lib/subcommands/app/env-vars.js +11 -16
  84. package/lib/subcommands/app/env-vars.js.map +1 -1
  85. package/lib/subcommands/app/models.d.ts.map +1 -1
  86. package/lib/subcommands/app/models.js +12 -16
  87. package/lib/subcommands/app/models.js.map +1 -1
  88. package/lib/subcommands/device/clean.d.ts.map +1 -1
  89. package/lib/subcommands/device/clean.js +8 -6
  90. package/lib/subcommands/device/clean.js.map +1 -1
  91. package/lib/subcommands/device/get-info.d.ts +2 -0
  92. package/lib/subcommands/device/get-info.d.ts.map +1 -0
  93. package/lib/subcommands/device/get-info.js +36 -0
  94. package/lib/subcommands/device/get-info.js.map +1 -0
  95. package/lib/subcommands/device/index.d.ts.map +1 -1
  96. package/lib/subcommands/device/index.js +11 -2
  97. package/lib/subcommands/device/index.js.map +1 -1
  98. package/lib/subcommands/device/init.d.ts +5 -0
  99. package/lib/subcommands/device/init.d.ts.map +1 -0
  100. package/lib/subcommands/device/{device.js → init.js} +5 -36
  101. package/lib/subcommands/device/init.js.map +1 -0
  102. package/lib/subcommands/device/refresh.d.ts +2 -0
  103. package/lib/subcommands/device/refresh.d.ts.map +1 -0
  104. package/lib/subcommands/device/refresh.js +24 -0
  105. package/lib/subcommands/device/refresh.js.map +1 -0
  106. package/lib/subcommands/device/restart.d.ts +2 -0
  107. package/lib/subcommands/device/restart.d.ts.map +1 -0
  108. package/lib/subcommands/device/restart.js +14 -0
  109. package/lib/subcommands/device/restart.js.map +1 -0
  110. package/lib/util/check-for-updates.d.ts +3 -0
  111. package/lib/util/check-for-updates.d.ts.map +1 -0
  112. package/lib/util/check-for-updates.js +69 -0
  113. package/lib/util/check-for-updates.js.map +1 -0
  114. package/lib/util/cloud-mode-ready.d.ts +1 -0
  115. package/lib/util/cloud-mode-ready.d.ts.map +1 -1
  116. package/lib/util/cloud-mode-ready.js +36 -1
  117. package/lib/util/cloud-mode-ready.js.map +1 -1
  118. package/lib/util/file.d.ts +7 -0
  119. package/lib/util/file.d.ts.map +1 -0
  120. package/lib/util/file.js +66 -0
  121. package/lib/util/file.js.map +1 -0
  122. package/lib/util/file.test.d.ts +2 -0
  123. package/lib/util/file.test.d.ts.map +1 -0
  124. package/lib/util/file.test.js +87 -0
  125. package/lib/util/file.test.js.map +1 -0
  126. package/package.json +8 -7
  127. package/readme.md +3 -3
  128. package/src/application-control/config.ts +1 -1
  129. package/src/application-control/environment-variables.test.ts +1 -1
  130. package/src/application-control/environment-variables.ts +9 -6
  131. package/src/application-control/install.ts +8 -3
  132. package/src/application-control/models.ts +47 -19
  133. package/src/application-control/status.ts +16 -14
  134. package/src/application-control/utils.ts +1 -1
  135. package/src/cloud-connection/device-agent-cloud-connection.ts +202 -148
  136. package/src/cloud-connection/live-updates-handler.test.ts +161 -20
  137. package/src/cloud-connection/live-updates-handler.ts +63 -31
  138. package/src/cloud-connection/messages.ts +3 -4
  139. package/src/cloud-connection/passthrough-handler.ts +98 -76
  140. package/src/cloud-connection/shadow-handler.test.ts +101 -84
  141. package/src/cloud-connection/shadow-handler.ts +287 -133
  142. package/src/cloud-connection/transaction-manager.test.ts +124 -0
  143. package/src/cloud-connection/transaction-manager.ts +15 -0
  144. package/src/device-control/device-control.ts +125 -23
  145. package/src/docker/docker-compose.ts +60 -0
  146. package/src/index.ts +2 -6
  147. package/src/infrastructure/agent-config.test.ts +3 -0
  148. package/src/infrastructure/agent-config.ts +38 -40
  149. package/src/local-connection/rabbitmq-connection.ts +8 -8
  150. package/src/secure-tunneling/secure-tunneling.test.ts +1239 -0
  151. package/src/secure-tunneling/secure-tunneling.ts +599 -0
  152. package/src/secure-tunneling/spawner-detached.ts +123 -0
  153. package/src/subcommands/app/analytics.ts +16 -13
  154. package/src/subcommands/app/env-vars.ts +18 -16
  155. package/src/subcommands/app/models.ts +20 -16
  156. package/src/subcommands/device/clean.ts +5 -2
  157. package/src/subcommands/device/get-info.ts +49 -0
  158. package/src/subcommands/device/index.ts +11 -2
  159. package/src/subcommands/device/{device.ts → init.ts} +5 -47
  160. package/src/subcommands/device/refresh.ts +22 -0
  161. package/src/subcommands/device/restart.ts +11 -0
  162. package/src/util/check-for-updates.ts +69 -0
  163. package/src/util/cloud-mode-ready.ts +36 -0
  164. package/src/util/file.test.ts +90 -0
  165. package/src/util/file.ts +76 -0
  166. package/lib/docker/docker-compose-cmd.d.ts +0 -5
  167. package/lib/docker/docker-compose-cmd.d.ts.map +0 -1
  168. package/lib/docker/docker-compose-cmd.js +0 -16
  169. package/lib/docker/docker-compose-cmd.js.map +0 -1
  170. package/lib/secure-tunneling/index.d.ts +0 -5
  171. package/lib/secure-tunneling/index.d.ts.map +0 -1
  172. package/lib/secure-tunneling/index.js +0 -64
  173. package/lib/secure-tunneling/index.js.map +0 -1
  174. package/lib/subcommands/device/device.d.ts +0 -7
  175. package/lib/subcommands/device/device.d.ts.map +0 -1
  176. package/lib/subcommands/device/device.js.map +0 -1
  177. package/lib/util/safe-rimraf.d.ts +0 -2
  178. package/lib/util/safe-rimraf.d.ts.map +0 -1
  179. package/lib/util/safe-rimraf.js +0 -16
  180. package/lib/util/safe-rimraf.js.map +0 -1
  181. package/src/docker/docker-compose-cmd.ts +0 -15
  182. package/src/secure-tunneling/index.ts +0 -74
  183. package/src/util/safe-rimraf.ts +0 -14
@@ -0,0 +1,599 @@
1
+ import { AAI_DIR } from 'alwaysai/lib/paths';
2
+ import { JsSpawner } from 'alwaysai/lib/util';
3
+ import { ChildProcess } from 'child_process';
4
+ import { join } from 'path';
5
+ import { aaiArtifactsBucketUrl } from '../urls';
6
+ import { isValidAwsRegion } from '../util/cloud-mode-ready';
7
+ import {
8
+ AWS_ROOT_CERTIFICATE_FILE_PATH,
9
+ SECURE_TUNNEL_BIN_DIR,
10
+ SECURE_TUNNEL_BIN_NAME,
11
+ SECURE_TUNNEL_BIN_PATH
12
+ } from '../util/directories';
13
+ import { downloadFile } from '../util/download-file';
14
+ import { logger } from '../util/logger';
15
+ import { getArch, getDistribution, getOsVersion } from '../util/system-info';
16
+ import { killDetachedProcess, runDetachedProcess } from './spawner-detached';
17
+ import {
18
+ SecureTunnelPortInfo,
19
+ SecureTunnelShadowDescriptionReported
20
+ } from '@alwaysai/device-agent-schemas';
21
+
22
+ enum SecureTunnelServiceType {
23
+ SSH = 'SSH',
24
+ HTTP = 'HTTP'
25
+ }
26
+
27
+ export type SecureTunnelShadowState = {
28
+ reported?: SecureTunnelShadowDescriptionReported;
29
+ desired?: SecureTunnelShadowDescriptionReported;
30
+ };
31
+
32
+ export type SecureTunnelShadowUpdateDelta = {
33
+ version: number;
34
+ timestamp: number;
35
+ state: SecureTunnelShadowDescriptionReported;
36
+ metadata?: any;
37
+ };
38
+
39
+ export type SecureTunnelShadowUpdate = {
40
+ version: number;
41
+ state: SecureTunnelShadowState;
42
+ };
43
+
44
+ type SecureTunnelProxyMap = {
45
+ st_port: SecureTunnelPortInfo;
46
+ localhostPort: number;
47
+ mapProcess: ChildProcess;
48
+ };
49
+
50
+ type SecureTunnelLocalProxyInfo = {
51
+ lpDstAccessKey: string;
52
+ lpRegion: string;
53
+ lpServices: string;
54
+ lpProcess: ChildProcess | null;
55
+ };
56
+
57
+ const defaultSecureTunnelPortInfo: SecureTunnelPortInfo = {
58
+ enabled: false,
59
+ type: SecureTunnelServiceType.SSH,
60
+ ip: '0.0.0.0',
61
+ port: 22
62
+ };
63
+
64
+ type SecureTunnelNotificationType = {
65
+ clientAccessToken: string;
66
+ region: string;
67
+ services: string[];
68
+ };
69
+
70
+ // socat tcp4-listen:5001,fork tcp4:100.70.31.118:80
71
+ const ST_MAPPING_TOOL = 'socat';
72
+ const ST_START_PORT_NUMBER = 5010;
73
+
74
+ /**
75
+ * Handles Secure Tunnel Shadow State
76
+ */
77
+ export class SecureTunnelHandlerSingleton {
78
+ private static instance: SecureTunnelHandlerSingleton;
79
+ private reported: SecureTunnelShadowDescriptionReported;
80
+ private httpProxyMap: SecureTunnelProxyMap[];
81
+ private localProxyInfo: SecureTunnelLocalProxyInfo;
82
+
83
+ /**
84
+ * Initializes private variables of SecureTunnel handler.
85
+ * private constructor to prevent instantiation from outside
86
+ */
87
+ private constructor() {
88
+ logger.debug('-> SecureTunnelHandlerSingleton.constructor');
89
+ this.reported = {
90
+ st_ports: [JSON.parse(JSON.stringify(defaultSecureTunnelPortInfo))]
91
+ };
92
+ this.httpProxyMap = [];
93
+ this.localProxyInfo = {
94
+ lpDstAccessKey: '',
95
+ lpRegion: '',
96
+ lpServices: '',
97
+ lpProcess: null
98
+ };
99
+ // TODO: create a recovery process if we just restarted
100
+ // for that to work we need to store somewhere the dst access key,
101
+ // recover it to restart local proxy to reconnect to secure tunnel
102
+ logger.debug('<- SecureTunnelHandlerSingleton.constructor');
103
+ }
104
+
105
+ //---------------------------------------------------------------------------
106
+ // public functions
107
+ //---------------------------------------------------------------------------
108
+ /**
109
+ * Implements the Singleton of the SecureTunnel
110
+ */
111
+ public static getInstance(): SecureTunnelHandlerSingleton {
112
+ logger.debug('-> SecureTunnelHandlerSingleton.getInstance');
113
+ if (!SecureTunnelHandlerSingleton.instance) {
114
+ SecureTunnelHandlerSingleton.instance =
115
+ new SecureTunnelHandlerSingleton();
116
+ }
117
+ logger.debug('<- SecureTunnelHandlerSingleton.getInstance');
118
+ return SecureTunnelHandlerSingleton.instance;
119
+ }
120
+
121
+ /**
122
+ * Stops all proxies started before
123
+ */
124
+ public async destroy(): Promise<void> {
125
+ logger.debug('-> SecureTunnelHandlerSingleton.destroy');
126
+ logger.debug(`httpProxyMap before : ${JSON.stringify(this.httpProxyMap)}`);
127
+ logger.debug(`reported before : ${JSON.stringify(this.reported)}`);
128
+
129
+ // We need operate on a copy instead of the original this.httpProxyMap
130
+ // because stopProxyMapping removes entries and there for manipulates this loop
131
+ const httpProxyMapCopy: SecureTunnelProxyMap[] = JSON.parse(
132
+ JSON.stringify(this.httpProxyMap)
133
+ );
134
+ for (const item of httpProxyMapCopy) {
135
+ await this.stopProxyMapping(item.st_port);
136
+ }
137
+
138
+ this.reported = {
139
+ st_ports: [JSON.parse(JSON.stringify(defaultSecureTunnelPortInfo))]
140
+ };
141
+ logger.debug(`httpProxyMap after : ${JSON.stringify(this.httpProxyMap)}`);
142
+ logger.debug(`reported after : ${JSON.stringify(this.reported)}`);
143
+ await this.stopLocalProxy();
144
+ logger.debug('<- SecureTunnelHandlerSingleton.destroy');
145
+ }
146
+
147
+ /**
148
+ * Returns current state of SecureTunnel shadow
149
+ * @returns {SecureTunnelShadowDescriptionReported} - reported state of the SecureTunnel shadow
150
+ */
151
+ public getSecureTunnelShadow(): SecureTunnelShadowDescriptionReported {
152
+ logger.debug('-> SecureTunnelHandlerSingleton.getSecureTunnelShadow');
153
+ logger.debug(`reported: ${JSON.stringify(this.reported)}`);
154
+ logger.debug('<- SecureTunnelHandlerSingleton.getSecureTunnelShadow');
155
+ return this.reported;
156
+ }
157
+
158
+ /**
159
+ * Updates current state of SecureTunnel shadow
160
+ * @param {SecureTunnelShadowUpdateDelta} deltaMsg - delta message, which includes desired state of the SecureTunnel shadow
161
+ * @return {SecureTunnelShadowDescriptionReported} update reported message to send back to AWS IoT device shadow
162
+ */
163
+ public async syncShadowToDeviceState(
164
+ deltaMsg: SecureTunnelShadowUpdateDelta
165
+ ): Promise<SecureTunnelShadowDescriptionReported> {
166
+ logger.debug('-> SecureTunnelHandlerSingleton.syncShadowToDeviceState');
167
+ const { version, state } = deltaMsg;
168
+ if (!state || typeof state.st_ports === 'undefined') {
169
+ return this.reported;
170
+ }
171
+ logger.debug(`version: ${version}`);
172
+ logger.debug(`state.st_ports: ${JSON.stringify(state.st_ports)}`);
173
+ await this.CleanupReportedPorts(state.st_ports);
174
+
175
+ for (const item of state.st_ports) {
176
+ logger.debug(`desiredItem: ${JSON.stringify(item)}`);
177
+ const existingPort = this.reported.st_ports.find(
178
+ (portInfo) =>
179
+ portInfo.ip === item.ip &&
180
+ portInfo.port === item.port &&
181
+ portInfo.type === item.type
182
+ );
183
+ logger.debug(`existingPort: ${JSON.stringify(existingPort)}`);
184
+
185
+ const numberOfEnabledPorts: number = this.reported.st_ports.reduce(
186
+ (acc, port) => {
187
+ return port.enabled ? acc + 1 : acc;
188
+ },
189
+ 0
190
+ );
191
+
192
+ // check that port already exist in the reported shadow
193
+ if (!existingPort) {
194
+ if (item.enabled && item.type === SecureTunnelServiceType.HTTP) {
195
+ await this.startProxyMapping(item);
196
+ }
197
+ this.reported.st_ports.push(item);
198
+ } else if (existingPort.enabled !== item.enabled) {
199
+ if (item.type === SecureTunnelServiceType.HTTP) {
200
+ if (item.enabled) {
201
+ await this.startProxyMapping(item);
202
+ } else {
203
+ await this.stopProxyMapping(item);
204
+ }
205
+ }
206
+ existingPort.enabled = item.enabled;
207
+ }
208
+ }
209
+
210
+ // if all entries are disabled, we need also to kill local proxy
211
+ if (this.reported.st_ports.every((portInfo) => !portInfo.enabled)) {
212
+ await this.stopLocalProxy();
213
+ }
214
+
215
+ // need to order list in the same order as desired before sending back
216
+ const sortedPorts = this.sortPorts(state);
217
+
218
+ logger.debug(`sortedPorts: ${JSON.stringify(sortedPorts)}`);
219
+ logger.debug('<- SecureTunnelHandlerSingleton.syncShadowToDeviceState');
220
+ return sortedPorts;
221
+ }
222
+
223
+ /**
224
+ * Starts SecureTunnel
225
+ * @param {SecureTunnelNotificationType} message - AWS notification received
226
+ */
227
+ public async secureTunnelNotifyHandler(
228
+ message: SecureTunnelNotificationType
229
+ ): Promise<void> {
230
+ logger.debug('-> SecureTunnelHandlerSingleton.secureTunnelNotifyHandler');
231
+
232
+ try {
233
+ await this.stopLocalProxy();
234
+ this.processNotifyMessage(message);
235
+ await this.downloadSecureTunnel();
236
+ await this.startLocalProxy();
237
+ } catch (error) {
238
+ logger.error(error);
239
+ }
240
+
241
+ logger.info(`Local Proxy Started: ${JSON.stringify(this.localProxyInfo)}`);
242
+ logger.debug('<- SecureTunnelHandlerSingleton.secureTunnelNotifyHandler');
243
+ }
244
+
245
+ //---------------------------------------------------------------------------
246
+ // private functions
247
+ //---------------------------------------------------------------------------
248
+ /**
249
+ * Removes reported ports which are do not exist in desired
250
+ * @param {SecureTunnelPortInfo[]} desiredPorts - desired port config
251
+ */
252
+ private async CleanupReportedPorts(desiredPorts: SecureTunnelPortInfo[]) {
253
+ logger.debug('-> SecureTunnelHandlerSingleton.CleanupReportedPorts');
254
+ const itemsToRemove: SecureTunnelPortInfo[] = [];
255
+
256
+ for (const item of this.reported.st_ports) {
257
+ logger.debug(`Checking item: ${JSON.stringify(item)}`);
258
+ if (
259
+ !desiredPorts.some(
260
+ (port) =>
261
+ port.ip === item.ip &&
262
+ port.port === item.port &&
263
+ port.type === item.type
264
+ )
265
+ ) {
266
+ logger.debug(`Marking item for removal: ${JSON.stringify(item)}`);
267
+ itemsToRemove.push(item);
268
+ }
269
+ }
270
+
271
+ for (const item of itemsToRemove) {
272
+ const index = this.reported.st_ports.indexOf(item);
273
+ if (index !== -1) {
274
+ const removedItem = this.reported.st_ports.splice(index, 1)[0];
275
+ logger.debug(`Removing item: ${JSON.stringify(removedItem)}`);
276
+ if (removedItem.type === SecureTunnelServiceType.HTTP) {
277
+ await this.stopProxyMapping(removedItem);
278
+ }
279
+ }
280
+ }
281
+
282
+ logger.debug(`reportedPorts: ${JSON.stringify(this.reported.st_ports)}`);
283
+ logger.debug('<- SecureTunnelHandlerSingleton.CleanupReportedPorts');
284
+ }
285
+
286
+ /**
287
+ * Starts port proxy mapping process
288
+ * @param {SecureTunnelPortInfo} portInfo - port info to start the port mapping process for
289
+ */
290
+ private async startProxyMapping(
291
+ portInfo: SecureTunnelPortInfo
292
+ ): Promise<void> {
293
+ logger.debug('-> SecureTunnelHandlerSingleton.startProxyMapping');
294
+ logger.debug(`portInfo: ${JSON.stringify(portInfo)}`);
295
+
296
+ // if there is already a process running for the ip:port, don't start another one
297
+ const itemIndex = this.httpProxyMap.findIndex(
298
+ (item) =>
299
+ item.st_port.ip === portInfo.ip && item.st_port.port === portInfo.port
300
+ );
301
+ if (itemIndex !== -1) {
302
+ logger.debug(`socat already active for: ${portInfo.ip}:${portInfo.port}`);
303
+ logger.debug('<- SecureTunnelHandlerSingleton.startProxyMapping');
304
+ return;
305
+ }
306
+
307
+ const localPort = this.getNextAvailablePort();
308
+ try {
309
+ logger.info(`Starting Port Proxy Mapping on ${localPort}`);
310
+ const args = [
311
+ `tcp4-listen:${localPort},fork`,
312
+ `tcp4:${portInfo.ip}:${portInfo.port}`
313
+ ];
314
+ const childProcess = await runDetachedProcess(ST_MAPPING_TOOL, args);
315
+ // TODO: if there is a device restart, to restore port mapping,
316
+ // this info needs to be saved on drive to be able to recover
317
+ this.httpProxyMap.push({
318
+ st_port: portInfo,
319
+ localhostPort: localPort,
320
+ mapProcess: childProcess
321
+ });
322
+ logger.info(
323
+ `Started Port Proxy Mapping for: ${JSON.stringify(
324
+ this.httpProxyMap[this.httpProxyMap.length - 1]
325
+ )}`
326
+ );
327
+ } catch (error) {
328
+ logger.error(
329
+ `ERROR: starting socat for: ${portInfo.ip}:${portInfo.port} on localhost port: ${localPort}, error: ${error}`
330
+ );
331
+ const lastHttpProxyMap = this.httpProxyMap.pop();
332
+ logger.info(`removed last proxyMap: ${JSON.stringify(lastHttpProxyMap)}`);
333
+ }
334
+ logger.debug('<- SecureTunnelHandlerSingleton.startProxyMapping');
335
+ }
336
+
337
+ /**
338
+ * Stops port proxy mapping process
339
+ * @param {SecureTunnelPortInfo} portInfo - port info to stop the port mapping process for
340
+ */
341
+ private async stopProxyMapping(
342
+ portInfo: SecureTunnelPortInfo
343
+ ): Promise<void> {
344
+ logger.debug('-> SecureTunnelHandlerSingleton.stopProxyMapping');
345
+ logger.debug(`portInfo: ${JSON.stringify(portInfo)}`);
346
+
347
+ const itemIndex = this.httpProxyMap.findIndex(
348
+ (item) =>
349
+ item.st_port.ip === portInfo.ip && item.st_port.port === portInfo.port
350
+ );
351
+
352
+ if (itemIndex !== -1) {
353
+ logger.info(
354
+ `Stopping Port Proxy Mapping for: ${JSON.stringify(
355
+ this.httpProxyMap[itemIndex]
356
+ )}`
357
+ );
358
+ try {
359
+ const processName = [
360
+ ST_MAPPING_TOOL,
361
+ `tcp4-listen:${this.httpProxyMap[itemIndex].localhostPort},fork`,
362
+ `tcp4:${portInfo.ip}:${portInfo.port}`
363
+ ];
364
+ await killDetachedProcess(this.httpProxyMap[itemIndex].mapProcess, [
365
+ processName.join(' ')
366
+ ]);
367
+ logger.debug(
368
+ `SUCCESS: killing socat process: ${JSON.stringify(
369
+ this.httpProxyMap[itemIndex]
370
+ )}`
371
+ );
372
+ logger.debug(`Remaining map: ${JSON.stringify(this.httpProxyMap)}`);
373
+ } catch (e) {
374
+ logger.error('ERROR: killing socat process:', e);
375
+ }
376
+ logger.debug(
377
+ `Removing map: ${JSON.stringify(this.httpProxyMap[itemIndex])}`
378
+ );
379
+ this.httpProxyMap.splice(itemIndex, 1);
380
+ }
381
+ logger.debug('<- SecureTunnelHandlerSingleton.stopProxyMapping');
382
+ }
383
+
384
+ /**
385
+ * Starts SecureTunnel localProxy process
386
+ */
387
+ private async startLocalProxy(): Promise<void> {
388
+ logger.debug('-> SecureTunnelHandlerSingleton.startLocalProxy');
389
+
390
+ const args = [
391
+ '--destination-app',
392
+ this.localProxyInfo.lpServices,
393
+ '--region',
394
+ this.localProxyInfo.lpRegion,
395
+ '--capath',
396
+ AWS_ROOT_CERTIFICATE_FILE_PATH,
397
+ '--local-bind-address',
398
+ '0.0.0.0',
399
+ '-t',
400
+ this.localProxyInfo.lpDstAccessKey
401
+ ];
402
+
403
+ this.localProxyInfo.lpProcess = await runDetachedProcess(
404
+ SECURE_TUNNEL_BIN_PATH,
405
+ args
406
+ );
407
+ logger.debug('<- SecureTunnelHandlerSingleton.startLocalProxy');
408
+ }
409
+
410
+ /**
411
+ * Stops SecureTunnel local proxy process
412
+ */
413
+ private async stopLocalProxy(): Promise<void> {
414
+ logger.debug('-> SecureTunnelHandlerSingleton.stopLocalProxy');
415
+ if (!this.localProxyInfo.lpProcess) {
416
+ logger.debug('No local proxy process running');
417
+ logger.debug('<- SecureTunnelHandlerSingleton.stopLocalProxy');
418
+ return;
419
+ }
420
+
421
+ try {
422
+ logger.debug(
423
+ `About to kill local proxy: ${this.localProxyInfo.lpDstAccessKey}`
424
+ );
425
+ await killDetachedProcess(this.localProxyInfo.lpProcess, [
426
+ SECURE_TUNNEL_BIN_PATH
427
+ ]);
428
+ logger.debug('SUCCESS: killing local proxy process');
429
+ } catch (e) {
430
+ logger.error('ERROR: killing local proxy process:', e);
431
+ }
432
+ this.localProxyInfo = {
433
+ lpDstAccessKey: '',
434
+ lpRegion: '',
435
+ lpServices: '',
436
+ lpProcess: null
437
+ };
438
+ logger.debug('<- SecureTunnelHandlerSingleton.stopLocalProxy');
439
+ }
440
+
441
+ /**
442
+ * processes and validate notify message
443
+ * @param {string[]} message - message, which contains: clientAccessToken, region, services
444
+ */
445
+ private processNotifyMessage(message: SecureTunnelNotificationType): void {
446
+ logger.debug('-> SecureTunnelHandlerSingleton.processNotifyMessage');
447
+ const { clientAccessToken, region, services } = message;
448
+ const portMappingList: string[] = [];
449
+
450
+ if (clientAccessToken === '') {
451
+ throw new Error('ERROR: invalid destination access token');
452
+ }
453
+ if (!isValidAwsRegion(region)) {
454
+ throw new Error(`ERROR: invalid/unsupported region: ${region}`);
455
+ }
456
+ if (services.length === 0) {
457
+ throw new Error(`ERROR: services field is empty: ${region}`);
458
+ }
459
+ if (services.some((field) => field === '')) {
460
+ throw new Error(`ERROR: one service fields is empty: ${region}`);
461
+ }
462
+ if (
463
+ services.some(
464
+ (service) =>
465
+ !service.startsWith(SecureTunnelServiceType.SSH) &&
466
+ !service.startsWith(SecureTunnelServiceType.HTTP)
467
+ )
468
+ ) {
469
+ throw new Error(
470
+ `ERROR: one service fields is invalid: ${JSON.stringify(services)}`
471
+ );
472
+ }
473
+ const sshEnabledPorts = this.reported.st_ports.filter(
474
+ (port) => port.type === SecureTunnelServiceType.SSH && port.enabled
475
+ );
476
+ const sshServicePorts = services.filter((service) =>
477
+ service.startsWith(SecureTunnelServiceType.SSH)
478
+ );
479
+ const httpServicePorts = services.filter((service) =>
480
+ service.startsWith(SecureTunnelServiceType.HTTP)
481
+ );
482
+ if (sshServicePorts.length > 1) {
483
+ throw new Error(
484
+ `ERROR: None or only 1 SSH port is allowed! sshEnabledPortsCount: ${sshEnabledPorts.length}`
485
+ );
486
+ }
487
+ // the new SSH mismatch, but NOT the SSH, because that device without shadow
488
+ if (
489
+ sshEnabledPorts.length !== sshServicePorts.length &&
490
+ sshServicePorts[0] !== SecureTunnelServiceType.SSH
491
+ ) {
492
+ throw new Error(
493
+ `ERROR: SSH ports mismatch! sshEnabledPortsCount: ${sshEnabledPorts.length}, sshServicePortsCount: ${sshServicePorts.length}`
494
+ );
495
+ }
496
+ if (this.httpProxyMap.length !== httpServicePorts.length) {
497
+ throw new Error(
498
+ `ERROR: HTTP ports mismatch! httpMappedPortsCount: ${this.httpProxyMap.length}, httpServicePortsCount: ${httpServicePorts.length}`
499
+ );
500
+ }
501
+
502
+ // this is the default case: just 1 SSH without any HTTP service
503
+ if (
504
+ services.length === 1 &&
505
+ services[0] === defaultSecureTunnelPortInfo.type
506
+ ) {
507
+ portMappingList.push(defaultSecureTunnelPortInfo.port.toString());
508
+ } else {
509
+ // this is the multi port case, need port mapping to running services
510
+ sshServicePorts.forEach((port, index) => {
511
+ portMappingList.push(`${port}=${sshEnabledPorts[index].port}`);
512
+ });
513
+ httpServicePorts.forEach((port, index) => {
514
+ portMappingList.push(
515
+ `${port}=${this.httpProxyMap[index].localhostPort}`
516
+ );
517
+ });
518
+ }
519
+
520
+ this.localProxyInfo.lpDstAccessKey = clientAccessToken;
521
+ this.localProxyInfo.lpRegion = region;
522
+ this.localProxyInfo.lpServices = portMappingList.join(',');
523
+ logger.debug(`reported = ${JSON.stringify(this.reported.st_ports)}`);
524
+ logger.debug(`localProxyInfo = ${JSON.stringify(this.localProxyInfo)}`);
525
+ logger.debug('<- SecureTunnelHandlerSingleton.processNotifyMessage');
526
+ }
527
+
528
+ /**
529
+ * Downloads SecureTunnel local proxy, if it was not downloaded before
530
+ */
531
+ private async downloadSecureTunnel(): Promise<void> {
532
+ logger.debug('-> SecureTunnelHandlerSingleton.downloadSecureTunnel');
533
+
534
+ if (await JsSpawner().exists(SECURE_TUNNEL_BIN_PATH)) {
535
+ logger.debug('<- SecureTunnelHandlerSingleton.downloadSecureTunnel');
536
+ return;
537
+ }
538
+
539
+ const [arch, linuxDistro, osVersion] = await Promise.all([
540
+ getArch(),
541
+ getDistribution(),
542
+ getOsVersion()
543
+ ]);
544
+
545
+ logger.info('Downloading SecureTunnel local proxy ...');
546
+ const url = `${aaiArtifactsBucketUrl}/securetunnel/${linuxDistro}/${osVersion}/${arch}/${SECURE_TUNNEL_BIN_NAME}`;
547
+ await JsSpawner().mkdirp(join(AAI_DIR, SECURE_TUNNEL_BIN_DIR));
548
+ await downloadFile({
549
+ url,
550
+ path: SECURE_TUNNEL_BIN_PATH,
551
+ errorMessage: `Secure Tunnel bin for ${linuxDistro} ${osVersion} ${arch} not found}`
552
+ });
553
+
554
+ await JsSpawner().run({
555
+ exe: 'chmod',
556
+ args: ['+x', SECURE_TUNNEL_BIN_PATH]
557
+ });
558
+ logger.debug('<- SecureTunnelHandlerSingleton.downloadSecureTunnel');
559
+ }
560
+
561
+ /**
562
+ * Gets next available localhost port
563
+ */
564
+ private getNextAvailablePort(): number {
565
+ logger.debug('-> SecureTunnelHandlerSingleton.getNextAvailablePort');
566
+ let lastLocalhostPort: number = ST_START_PORT_NUMBER;
567
+ if (this.httpProxyMap.length > 0) {
568
+ lastLocalhostPort = this.httpProxyMap.reduce((maxPort, proxy) => {
569
+ return proxy.localhostPort > maxPort ? proxy.localhostPort : maxPort;
570
+ }, ST_START_PORT_NUMBER);
571
+ }
572
+ // for now just check whether we are using specific port number
573
+ // TODO: in the future we need to check whether some other services are using a specific port
574
+ // possible way to check taken ports: "sudo netstat -tuln | grep <port_number>""
575
+ logger.debug(`lastLocalhostPort: ${(lastLocalhostPort + 1).toString()}`);
576
+ logger.debug('<- SecureTunnelHandlerSingleton.getNextAvailablePort');
577
+ return lastLocalhostPort + 1;
578
+ }
579
+
580
+ private sortPorts(
581
+ desired: SecureTunnelShadowDescriptionReported
582
+ ): SecureTunnelShadowDescriptionReported {
583
+ logger.debug('-> SecureTunnelHandlerSingleton.sortPorts');
584
+ const sortedPorts = JSON.parse(JSON.stringify(this.reported));
585
+ sortedPorts.st_ports.sort((a, b) => {
586
+ const aOriginalIndex = desired.st_ports.findIndex(
587
+ (item) =>
588
+ item.type === a.type && item.ip === a.ip && item.port === a.port
589
+ );
590
+ const bOriginalIndex = desired.st_ports.findIndex(
591
+ (item) =>
592
+ item.type === b.type && item.ip === b.ip && item.port === b.port
593
+ );
594
+ return aOriginalIndex - bOriginalIndex;
595
+ });
596
+ logger.debug('<- SecureTunnelHandlerSingleton.sortPorts');
597
+ return sortedPorts;
598
+ }
599
+ }