@theia/remote 1.45.1 → 1.46.0-next.72

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 (87) hide show
  1. package/README.md +61 -61
  2. package/lib/electron-browser/remote-electron-file-dialog-service.d.ts +13 -13
  3. package/lib/electron-browser/remote-electron-file-dialog-service.js +57 -57
  4. package/lib/electron-browser/remote-frontend-contribution.d.ts +25 -25
  5. package/lib/electron-browser/remote-frontend-contribution.js +164 -164
  6. package/lib/electron-browser/remote-frontend-module.d.ts +3 -3
  7. package/lib/electron-browser/remote-frontend-module.js +42 -42
  8. package/lib/electron-browser/remote-preferences.d.ts +10 -10
  9. package/lib/electron-browser/remote-preferences.js +48 -48
  10. package/lib/electron-browser/remote-registry-contribution.d.ts +18 -18
  11. package/lib/electron-browser/remote-registry-contribution.js +74 -74
  12. package/lib/electron-browser/remote-service.d.ts +5 -5
  13. package/lib/electron-browser/remote-service.js +37 -37
  14. package/lib/electron-browser/remote-ssh-contribution.d.ts +17 -17
  15. package/lib/electron-browser/remote-ssh-contribution.js +117 -117
  16. package/lib/electron-common/remote-ssh-connection-provider.d.ts +10 -10
  17. package/lib/electron-common/remote-ssh-connection-provider.js +20 -20
  18. package/lib/electron-common/remote-status-service.d.ts +14 -14
  19. package/lib/electron-common/remote-status-service.js +20 -20
  20. package/lib/electron-node/backend-remote-service-impl.d.ts +11 -11
  21. package/lib/electron-node/backend-remote-service-impl.js +50 -50
  22. package/lib/electron-node/remote-backend-module.d.ts +4 -4
  23. package/lib/electron-node/remote-backend-module.js +74 -74
  24. package/lib/electron-node/remote-connection-service.d.ts +14 -14
  25. package/lib/electron-node/remote-connection-service.js +65 -65
  26. package/lib/electron-node/remote-connection-socket-provider.d.ts +8 -8
  27. package/lib/electron-node/remote-connection-socket-provider.js +37 -37
  28. package/lib/electron-node/remote-proxy-server-provider.d.ts +5 -5
  29. package/lib/electron-node/remote-proxy-server-provider.js +43 -43
  30. package/lib/electron-node/remote-status-service.d.ts +6 -6
  31. package/lib/electron-node/remote-status-service.js +54 -54
  32. package/lib/electron-node/remote-types.d.ts +34 -34
  33. package/lib/electron-node/remote-types.js +17 -17
  34. package/lib/electron-node/setup/app-native-dependency-contribution.d.ts +7 -7
  35. package/lib/electron-node/setup/app-native-dependency-contribution.js +57 -57
  36. package/lib/electron-node/setup/main-copy-contribution.d.ts +4 -4
  37. package/lib/electron-node/setup/main-copy-contribution.js +37 -37
  38. package/lib/electron-node/setup/remote-copy-contribution.d.ts +28 -28
  39. package/lib/electron-node/setup/remote-copy-contribution.js +78 -78
  40. package/lib/electron-node/setup/remote-copy-service.d.ts +18 -18
  41. package/lib/electron-node/setup/remote-copy-service.js +126 -126
  42. package/lib/electron-node/setup/remote-native-dependency-contribution.d.ts +34 -34
  43. package/lib/electron-node/setup/remote-native-dependency-contribution.js +34 -34
  44. package/lib/electron-node/setup/remote-native-dependency-service.d.ts +23 -23
  45. package/lib/electron-node/setup/remote-native-dependency-service.js +118 -118
  46. package/lib/electron-node/setup/remote-node-setup-service.d.ts +22 -22
  47. package/lib/electron-node/setup/remote-node-setup-service.js +132 -132
  48. package/lib/electron-node/setup/remote-setup-script-service.d.ts +40 -40
  49. package/lib/electron-node/setup/remote-setup-script-service.js +132 -132
  50. package/lib/electron-node/setup/remote-setup-service.d.ts +28 -28
  51. package/lib/electron-node/setup/remote-setup-service.js +198 -198
  52. package/lib/electron-node/ssh/remote-ssh-connection-provider.d.ts +55 -55
  53. package/lib/electron-node/ssh/remote-ssh-connection-provider.d.ts.map +1 -1
  54. package/lib/electron-node/ssh/remote-ssh-connection-provider.js +344 -342
  55. package/lib/electron-node/ssh/remote-ssh-connection-provider.js.map +1 -1
  56. package/lib/electron-node/ssh/ssh-identity-file-collector.d.ts +12 -12
  57. package/lib/electron-node/ssh/ssh-identity-file-collector.js +131 -131
  58. package/lib/package.spec.js +25 -25
  59. package/package.json +6 -7
  60. package/src/electron-browser/remote-electron-file-dialog-service.ts +47 -47
  61. package/src/electron-browser/remote-frontend-contribution.ts +145 -145
  62. package/src/electron-browser/remote-frontend-module.ts +49 -49
  63. package/src/electron-browser/remote-preferences.ts +62 -62
  64. package/src/electron-browser/remote-registry-contribution.ts +70 -70
  65. package/src/electron-browser/remote-service.ts +31 -31
  66. package/src/electron-browser/remote-ssh-contribution.ts +102 -102
  67. package/src/electron-common/remote-ssh-connection-provider.ts +29 -29
  68. package/src/electron-common/remote-status-service.ts +35 -35
  69. package/src/electron-node/backend-remote-service-impl.ts +45 -45
  70. package/src/electron-node/remote-backend-module.ts +80 -80
  71. package/src/electron-node/remote-connection-service.ts +55 -55
  72. package/src/electron-node/remote-connection-socket-provider.ts +34 -34
  73. package/src/electron-node/remote-proxy-server-provider.ts +37 -37
  74. package/src/electron-node/remote-status-service.ts +41 -41
  75. package/src/electron-node/remote-types.ts +56 -56
  76. package/src/electron-node/setup/app-native-dependency-contribution.ts +48 -48
  77. package/src/electron-node/setup/main-copy-contribution.ts +28 -28
  78. package/src/electron-node/setup/remote-copy-contribution.ts +90 -90
  79. package/src/electron-node/setup/remote-copy-service.ts +114 -114
  80. package/src/electron-node/setup/remote-native-dependency-contribution.ts +63 -63
  81. package/src/electron-node/setup/remote-native-dependency-service.ts +111 -111
  82. package/src/electron-node/setup/remote-node-setup-service.ts +123 -123
  83. package/src/electron-node/setup/remote-setup-script-service.ts +146 -146
  84. package/src/electron-node/setup/remote-setup-service.ts +197 -197
  85. package/src/electron-node/ssh/remote-ssh-connection-provider.ts +358 -356
  86. package/src/electron-node/ssh/ssh-identity-file-collector.ts +137 -137
  87. package/src/package.spec.ts +29 -29
@@ -1,356 +1,358 @@
1
- // *****************************************************************************
2
- // Copyright (C) 2023 TypeFox and others.
3
- //
4
- // This program and the accompanying materials are made available under the
5
- // terms of the Eclipse Public License v. 2.0 which is available at
6
- // http://www.eclipse.org/legal/epl-2.0.
7
- //
8
- // This Source Code may also be made available under the following Secondary
9
- // Licenses when the conditions for such availability set forth in the Eclipse
10
- // Public License v. 2.0 are satisfied: GNU General Public License, version 2
11
- // with the GNU Classpath Exception which is available at
12
- // https://www.gnu.org/software/classpath/license.html.
13
- //
14
- // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
15
- // *****************************************************************************
16
-
17
- import * as ssh2 from 'ssh2';
18
- import * as net from 'net';
19
- import * as fs from '@theia/core/shared/fs-extra';
20
- import SftpClient = require('ssh2-sftp-client');
21
- import { Emitter, Event, MessageService, QuickInputService } from '@theia/core';
22
- import { inject, injectable } from '@theia/core/shared/inversify';
23
- import { RemoteSSHConnectionProvider, RemoteSSHConnectionProviderOptions } from '../../electron-common/remote-ssh-connection-provider';
24
- import { RemoteConnectionService } from '../remote-connection-service';
25
- import { RemoteProxyServerProvider } from '../remote-proxy-server-provider';
26
- import { RemoteConnection, RemoteExecOptions, RemoteExecResult, RemoteExecTester, RemoteStatusReport } from '../remote-types';
27
- import { Deferred, timeout } from '@theia/core/lib/common/promise-util';
28
- import { SSHIdentityFileCollector, SSHKey } from './ssh-identity-file-collector';
29
- import { RemoteSetupService } from '../setup/remote-setup-service';
30
- import { v4 } from 'uuid';
31
-
32
- @injectable()
33
- export class RemoteSSHConnectionProviderImpl implements RemoteSSHConnectionProvider {
34
-
35
- @inject(RemoteConnectionService)
36
- protected readonly remoteConnectionService: RemoteConnectionService;
37
-
38
- @inject(RemoteProxyServerProvider)
39
- protected readonly serverProvider: RemoteProxyServerProvider;
40
-
41
- @inject(SSHIdentityFileCollector)
42
- protected readonly identityFileCollector: SSHIdentityFileCollector;
43
-
44
- @inject(RemoteSetupService)
45
- protected readonly remoteSetup: RemoteSetupService;
46
-
47
- @inject(QuickInputService)
48
- protected readonly quickInputService: QuickInputService;
49
-
50
- @inject(MessageService)
51
- protected readonly messageService: MessageService;
52
-
53
- protected passwordRetryCount = 3;
54
- protected passphraseRetryCount = 3;
55
-
56
- async establishConnection(options: RemoteSSHConnectionProviderOptions): Promise<string> {
57
- const progress = await this.messageService.showProgress({
58
- text: 'Remote SSH'
59
- });
60
- const report: RemoteStatusReport = message => progress.report({ message });
61
- report('Connecting to remote system...');
62
- try {
63
- const remote = await this.establishSSHConnection(options.host, options.user);
64
- await this.remoteSetup.setup({
65
- connection: remote,
66
- report,
67
- nodeDownloadTemplate: options.nodeDownloadTemplate
68
- });
69
- const registration = this.remoteConnectionService.register(remote);
70
- const server = await this.serverProvider.getProxyServer(socket => {
71
- remote.forwardOut(socket);
72
- });
73
- remote.onDidDisconnect(() => {
74
- server.close();
75
- registration.dispose();
76
- });
77
- const localPort = (server.address() as net.AddressInfo).port;
78
- remote.localPort = localPort;
79
- return localPort.toString();
80
- } finally {
81
- progress.cancel();
82
- }
83
- }
84
-
85
- async establishSSHConnection(host: string, user: string): Promise<RemoteSSHConnection> {
86
- const deferred = new Deferred<RemoteSSHConnection>();
87
- const sshClient = new ssh2.Client();
88
- const identityFiles = await this.identityFileCollector.gatherIdentityFiles();
89
- const sshAuthHandler = this.getAuthHandler(user, host, identityFiles);
90
- sshClient
91
- .on('ready', async () => {
92
- const connection = new RemoteSSHConnection({
93
- client: sshClient,
94
- id: v4(),
95
- name: host,
96
- type: 'SSH'
97
- });
98
- try {
99
- await this.testConnection(connection);
100
- deferred.resolve(connection);
101
- } catch (err) {
102
- deferred.reject(err);
103
- }
104
- }).on('end', () => {
105
- console.log(`Ended remote connection to host '${user}@${host}'`);
106
- }).on('error', err => {
107
- deferred.reject(err);
108
- }).connect({
109
- host: host,
110
- username: user,
111
- authHandler: (methodsLeft, successes, callback) => (sshAuthHandler(methodsLeft, successes, callback), undefined)
112
- });
113
- return deferred.promise;
114
- }
115
-
116
- /**
117
- * Sometimes, ssh2.exec will not execute and retrieve any data right after the `ready` event fired.
118
- * In this method, we just perform `echo hello` in a loop to ensure that the connection is really ready.
119
- * See also https://github.com/mscdex/ssh2/issues/48
120
- */
121
- protected async testConnection(connection: RemoteSSHConnection): Promise<void> {
122
- for (let i = 0; i < 100; i++) {
123
- const result = await connection.exec('echo hello');
124
- if (result.stdout.includes('hello')) {
125
- return;
126
- }
127
- await timeout(50);
128
- }
129
- throw new Error('SSH connection failed testing. Could not execute "echo"');
130
- }
131
-
132
- protected getAuthHandler(user: string, host: string, identityKeys: SSHKey[]): ssh2.AuthHandlerMiddleware {
133
- let passwordRetryCount = this.passwordRetryCount;
134
- let keyboardRetryCount = this.passphraseRetryCount;
135
- // `false` is a valid return value, indicating that the authentication has failed
136
- const END_AUTH = false as unknown as ssh2.AuthenticationType;
137
- // `null` indicates that we just want to continue with the next auth type
138
- // eslint-disable-next-line no-null/no-null
139
- const NEXT_AUTH = null as unknown as ssh2.AuthenticationType;
140
- return async (methodsLeft: string[] | null, _partialSuccess: boolean | null, callback: ssh2.NextAuthHandler) => {
141
- if (!methodsLeft) {
142
- return callback({
143
- type: 'none',
144
- username: user,
145
- });
146
- }
147
- if (methodsLeft && methodsLeft.includes('publickey') && identityKeys.length) {
148
- const identityKey = identityKeys.shift()!;
149
- if (identityKey.isPrivate) {
150
- return callback({
151
- type: 'publickey',
152
- username: user,
153
- key: identityKey.parsedKey
154
- });
155
- }
156
- if (!await fs.pathExists(identityKey.filename)) {
157
- // Try next identity file
158
- return callback(NEXT_AUTH);
159
- }
160
-
161
- const keyBuffer = await fs.promises.readFile(identityKey.filename);
162
- let result = ssh2.utils.parseKey(keyBuffer); // First try without passphrase
163
- if (result instanceof Error && result.message.match(/no passphrase given/)) {
164
- let passphraseRetryCount = this.passphraseRetryCount;
165
- while (result instanceof Error && passphraseRetryCount > 0) {
166
- const passphrase = await this.quickInputService.input({
167
- title: `Enter passphrase for ${identityKey.filename}`,
168
- password: true
169
- });
170
- if (!passphrase) {
171
- break;
172
- }
173
- result = ssh2.utils.parseKey(keyBuffer, passphrase);
174
- passphraseRetryCount--;
175
- }
176
- }
177
- if (!result || result instanceof Error) {
178
- // Try next identity file
179
- return callback(NEXT_AUTH);
180
- }
181
-
182
- const key = Array.isArray(result) ? result[0] : result;
183
- return callback({
184
- type: 'publickey',
185
- username: user,
186
- key
187
- });
188
- }
189
- if (methodsLeft && methodsLeft.includes('password') && passwordRetryCount > 0) {
190
- const password = await this.quickInputService.input({
191
- title: `Enter password for ${user}@${host}`,
192
- password: true
193
- });
194
- passwordRetryCount--;
195
-
196
- return callback(password
197
- ? {
198
- type: 'password',
199
- username: user,
200
- password
201
- }
202
- : END_AUTH);
203
- }
204
- if (methodsLeft && methodsLeft.includes('keyboard-interactive') && keyboardRetryCount > 0) {
205
- return callback({
206
- type: 'keyboard-interactive',
207
- username: user,
208
- prompt: async (_name, _instructions, _instructionsLang, prompts, finish) => {
209
- const responses: string[] = [];
210
- for (const prompt of prompts) {
211
- const response = await this.quickInputService.input({
212
- title: `(${user}@${host}) ${prompt.prompt}`,
213
- password: !prompt.echo
214
- });
215
- if (response === undefined) {
216
- keyboardRetryCount = 0;
217
- break;
218
- }
219
- responses.push(response);
220
- }
221
- keyboardRetryCount--;
222
- finish(responses);
223
- }
224
- });
225
- }
226
-
227
- callback(END_AUTH);
228
- };
229
- }
230
- }
231
-
232
- export interface RemoteSSHConnectionOptions {
233
- id: string;
234
- name: string;
235
- type: string;
236
- client: ssh2.Client;
237
- }
238
-
239
- export class RemoteSSHConnection implements RemoteConnection {
240
-
241
- id: string;
242
- name: string;
243
- type: string;
244
- client: ssh2.Client;
245
- localPort = 0;
246
- remotePort = 0;
247
-
248
- private sftpClientPromise: Promise<SftpClient>;
249
-
250
- private readonly onDidDisconnectEmitter = new Emitter<void>();
251
-
252
- get onDidDisconnect(): Event<void> {
253
- return this.onDidDisconnectEmitter.event;
254
- }
255
-
256
- constructor(options: RemoteSSHConnectionOptions) {
257
- this.id = options.id;
258
- this.type = options.type;
259
- this.name = options.name;
260
- this.client = options.client;
261
- this.onDidDisconnect(() => this.dispose());
262
- this.client.on('end', () => {
263
- this.onDidDisconnectEmitter.fire();
264
- });
265
- this.sftpClientPromise = this.setupSftpClient();
266
- }
267
-
268
- protected async setupSftpClient(): Promise<SftpClient> {
269
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
270
- const sftpClient = new SftpClient() as any;
271
- // A hack to set the internal ssh2 client of the sftp client
272
- // That way, we don't have to create a second connection
273
- sftpClient.client = this.client;
274
- // Calling this function establishes the sftp connection on the ssh client
275
- await sftpClient.getSftpChannel();
276
- return sftpClient;
277
- }
278
-
279
- forwardOut(socket: net.Socket): void {
280
- this.client.forwardOut(socket.localAddress!, socket.localPort!, '127.0.0.1', this.remotePort, (err, stream) => {
281
- if (err) {
282
- console.debug('Proxy message rejected', err);
283
- } else {
284
- stream.pipe(socket).pipe(stream);
285
- }
286
- });
287
- }
288
-
289
- async copy(localPath: string, remotePath: string): Promise<void> {
290
- const sftpClient = await this.sftpClientPromise;
291
- await sftpClient.put(localPath, remotePath);
292
- }
293
-
294
- exec(cmd: string, args?: string[], options: RemoteExecOptions = {}): Promise<RemoteExecResult> {
295
- const deferred = new Deferred<RemoteExecResult>();
296
- cmd = this.buildCmd(cmd, args);
297
- this.client.exec(cmd, options, (err, stream) => {
298
- if (err) {
299
- return deferred.reject(err);
300
- }
301
- let stdout = '';
302
- let stderr = '';
303
- stream.on('close', () => {
304
- deferred.resolve({ stdout, stderr });
305
- }).on('data', (data: Buffer | string) => {
306
- stdout += data.toString();
307
- }).stderr.on('data', (data: Buffer | string) => {
308
- stderr += data.toString();
309
- });
310
- });
311
- return deferred.promise;
312
- }
313
-
314
- execPartial(cmd: string, tester: RemoteExecTester, args?: string[], options: RemoteExecOptions = {}): Promise<RemoteExecResult> {
315
- const deferred = new Deferred<RemoteExecResult>();
316
- cmd = this.buildCmd(cmd, args);
317
- this.client.exec(cmd, {
318
- ...options,
319
- // Ensure that the process on the remote ends when the connection is closed
320
- pty: true
321
- }, (err, stream) => {
322
- if (err) {
323
- return deferred.reject(err);
324
- }
325
- // in pty mode we only have an stdout stream
326
- // return stdout as stderr as well
327
- let stdout = '';
328
- stream.on('close', () => {
329
- if (deferred.state === 'unresolved') {
330
- deferred.resolve({ stdout, stderr: stdout });
331
- }
332
- }).on('data', (data: Buffer | string) => {
333
- if (deferred.state === 'unresolved') {
334
- stdout += data.toString();
335
-
336
- if (tester(stdout, stdout)) {
337
- deferred.resolve({ stdout, stderr: stdout });
338
- }
339
- }
340
- });
341
- });
342
- return deferred.promise;
343
- }
344
-
345
- protected buildCmd(cmd: string, args?: string[]): string {
346
- const escapedArgs = args?.map(arg => `"${arg.replace(/"/g, '\\"')}"`) || [];
347
- const fullCmd = cmd + (escapedArgs.length > 0 ? (' ' + escapedArgs.join(' ')) : '');
348
- return fullCmd;
349
- }
350
-
351
- dispose(): void {
352
- this.client.end();
353
- this.client.destroy();
354
- }
355
-
356
- }
1
+ // *****************************************************************************
2
+ // Copyright (C) 2023 TypeFox and others.
3
+ //
4
+ // This program and the accompanying materials are made available under the
5
+ // terms of the Eclipse Public License v. 2.0 which is available at
6
+ // http://www.eclipse.org/legal/epl-2.0.
7
+ //
8
+ // This Source Code may also be made available under the following Secondary
9
+ // Licenses when the conditions for such availability set forth in the Eclipse
10
+ // Public License v. 2.0 are satisfied: GNU General Public License, version 2
11
+ // with the GNU Classpath Exception which is available at
12
+ // https://www.gnu.org/software/classpath/license.html.
13
+ //
14
+ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
15
+ // *****************************************************************************
16
+
17
+ import * as ssh2 from 'ssh2';
18
+ import * as net from 'net';
19
+ import * as fs from '@theia/core/shared/fs-extra';
20
+ import SftpClient = require('ssh2-sftp-client');
21
+ import { Emitter, Event, MessageService, QuickInputService } from '@theia/core';
22
+ import { inject, injectable } from '@theia/core/shared/inversify';
23
+ import { RemoteSSHConnectionProvider, RemoteSSHConnectionProviderOptions } from '../../electron-common/remote-ssh-connection-provider';
24
+ import { RemoteConnectionService } from '../remote-connection-service';
25
+ import { RemoteProxyServerProvider } from '../remote-proxy-server-provider';
26
+ import { RemoteConnection, RemoteExecOptions, RemoteExecResult, RemoteExecTester, RemoteStatusReport } from '../remote-types';
27
+ import { Deferred, timeout } from '@theia/core/lib/common/promise-util';
28
+ import { SSHIdentityFileCollector, SSHKey } from './ssh-identity-file-collector';
29
+ import { RemoteSetupService } from '../setup/remote-setup-service';
30
+ import { generateUuid } from '@theia/core/lib/common/uuid';
31
+
32
+ @injectable()
33
+ export class RemoteSSHConnectionProviderImpl implements RemoteSSHConnectionProvider {
34
+
35
+ @inject(RemoteConnectionService)
36
+ protected readonly remoteConnectionService: RemoteConnectionService;
37
+
38
+ @inject(RemoteProxyServerProvider)
39
+ protected readonly serverProvider: RemoteProxyServerProvider;
40
+
41
+ @inject(SSHIdentityFileCollector)
42
+ protected readonly identityFileCollector: SSHIdentityFileCollector;
43
+
44
+ @inject(RemoteSetupService)
45
+ protected readonly remoteSetup: RemoteSetupService;
46
+
47
+ @inject(QuickInputService)
48
+ protected readonly quickInputService: QuickInputService;
49
+
50
+ @inject(MessageService)
51
+ protected readonly messageService: MessageService;
52
+
53
+ protected passwordRetryCount = 3;
54
+ protected passphraseRetryCount = 3;
55
+
56
+ async establishConnection(options: RemoteSSHConnectionProviderOptions): Promise<string> {
57
+ const progress = await this.messageService.showProgress({
58
+ text: 'Remote SSH'
59
+ });
60
+ const report: RemoteStatusReport = message => progress.report({ message });
61
+ report('Connecting to remote system...');
62
+ try {
63
+ const remote = await this.establishSSHConnection(options.host, options.user);
64
+ await this.remoteSetup.setup({
65
+ connection: remote,
66
+ report,
67
+ nodeDownloadTemplate: options.nodeDownloadTemplate
68
+ });
69
+ const registration = this.remoteConnectionService.register(remote);
70
+ const server = await this.serverProvider.getProxyServer(socket => {
71
+ remote.forwardOut(socket);
72
+ });
73
+ remote.onDidDisconnect(() => {
74
+ server.close();
75
+ registration.dispose();
76
+ });
77
+ const localPort = (server.address() as net.AddressInfo).port;
78
+ remote.localPort = localPort;
79
+ return localPort.toString();
80
+ } finally {
81
+ progress.cancel();
82
+ }
83
+ }
84
+
85
+ async establishSSHConnection(host: string, user: string): Promise<RemoteSSHConnection> {
86
+ const deferred = new Deferred<RemoteSSHConnection>();
87
+ const sshClient = new ssh2.Client();
88
+ const identityFiles = await this.identityFileCollector.gatherIdentityFiles();
89
+ const hostUrl = new URL(`ssh://${host}`);
90
+ const sshAuthHandler = this.getAuthHandler(user, hostUrl.hostname, identityFiles);
91
+ sshClient
92
+ .on('ready', async () => {
93
+ const connection = new RemoteSSHConnection({
94
+ client: sshClient,
95
+ id: generateUuid(),
96
+ name: hostUrl.hostname,
97
+ type: 'SSH'
98
+ });
99
+ try {
100
+ await this.testConnection(connection);
101
+ deferred.resolve(connection);
102
+ } catch (err) {
103
+ deferred.reject(err);
104
+ }
105
+ }).on('end', () => {
106
+ console.log(`Ended remote connection to host '${user}@${hostUrl.hostname}'`);
107
+ }).on('error', err => {
108
+ deferred.reject(err);
109
+ }).connect({
110
+ host: hostUrl.hostname,
111
+ port: hostUrl.port ? parseInt(hostUrl.port, 10) : undefined,
112
+ username: user,
113
+ authHandler: (methodsLeft, successes, callback) => (sshAuthHandler(methodsLeft, successes, callback), undefined)
114
+ });
115
+ return deferred.promise;
116
+ }
117
+
118
+ /**
119
+ * Sometimes, ssh2.exec will not execute and retrieve any data right after the `ready` event fired.
120
+ * In this method, we just perform `echo hello` in a loop to ensure that the connection is really ready.
121
+ * See also https://github.com/mscdex/ssh2/issues/48
122
+ */
123
+ protected async testConnection(connection: RemoteSSHConnection): Promise<void> {
124
+ for (let i = 0; i < 100; i++) {
125
+ const result = await connection.exec('echo hello');
126
+ if (result.stdout.includes('hello')) {
127
+ return;
128
+ }
129
+ await timeout(50);
130
+ }
131
+ throw new Error('SSH connection failed testing. Could not execute "echo"');
132
+ }
133
+
134
+ protected getAuthHandler(user: string, host: string, identityKeys: SSHKey[]): ssh2.AuthHandlerMiddleware {
135
+ let passwordRetryCount = this.passwordRetryCount;
136
+ let keyboardRetryCount = this.passphraseRetryCount;
137
+ // `false` is a valid return value, indicating that the authentication has failed
138
+ const END_AUTH = false as unknown as ssh2.AuthenticationType;
139
+ // `null` indicates that we just want to continue with the next auth type
140
+ // eslint-disable-next-line no-null/no-null
141
+ const NEXT_AUTH = null as unknown as ssh2.AuthenticationType;
142
+ return async (methodsLeft: string[] | null, _partialSuccess: boolean | null, callback: ssh2.NextAuthHandler) => {
143
+ if (!methodsLeft) {
144
+ return callback({
145
+ type: 'none',
146
+ username: user,
147
+ });
148
+ }
149
+ if (methodsLeft && methodsLeft.includes('publickey') && identityKeys.length) {
150
+ const identityKey = identityKeys.shift()!;
151
+ if (identityKey.isPrivate) {
152
+ return callback({
153
+ type: 'publickey',
154
+ username: user,
155
+ key: identityKey.parsedKey
156
+ });
157
+ }
158
+ if (!await fs.pathExists(identityKey.filename)) {
159
+ // Try next identity file
160
+ return callback(NEXT_AUTH);
161
+ }
162
+
163
+ const keyBuffer = await fs.promises.readFile(identityKey.filename);
164
+ let result = ssh2.utils.parseKey(keyBuffer); // First try without passphrase
165
+ if (result instanceof Error && result.message.match(/no passphrase given/)) {
166
+ let passphraseRetryCount = this.passphraseRetryCount;
167
+ while (result instanceof Error && passphraseRetryCount > 0) {
168
+ const passphrase = await this.quickInputService.input({
169
+ title: `Enter passphrase for ${identityKey.filename}`,
170
+ password: true
171
+ });
172
+ if (!passphrase) {
173
+ break;
174
+ }
175
+ result = ssh2.utils.parseKey(keyBuffer, passphrase);
176
+ passphraseRetryCount--;
177
+ }
178
+ }
179
+ if (!result || result instanceof Error) {
180
+ // Try next identity file
181
+ return callback(NEXT_AUTH);
182
+ }
183
+
184
+ const key = Array.isArray(result) ? result[0] : result;
185
+ return callback({
186
+ type: 'publickey',
187
+ username: user,
188
+ key
189
+ });
190
+ }
191
+ if (methodsLeft && methodsLeft.includes('password') && passwordRetryCount > 0) {
192
+ const password = await this.quickInputService.input({
193
+ title: `Enter password for ${user}@${host}`,
194
+ password: true
195
+ });
196
+ passwordRetryCount--;
197
+
198
+ return callback(password
199
+ ? {
200
+ type: 'password',
201
+ username: user,
202
+ password
203
+ }
204
+ : END_AUTH);
205
+ }
206
+ if (methodsLeft && methodsLeft.includes('keyboard-interactive') && keyboardRetryCount > 0) {
207
+ return callback({
208
+ type: 'keyboard-interactive',
209
+ username: user,
210
+ prompt: async (_name, _instructions, _instructionsLang, prompts, finish) => {
211
+ const responses: string[] = [];
212
+ for (const prompt of prompts) {
213
+ const response = await this.quickInputService.input({
214
+ title: `(${user}@${host}) ${prompt.prompt}`,
215
+ password: !prompt.echo
216
+ });
217
+ if (response === undefined) {
218
+ keyboardRetryCount = 0;
219
+ break;
220
+ }
221
+ responses.push(response);
222
+ }
223
+ keyboardRetryCount--;
224
+ finish(responses);
225
+ }
226
+ });
227
+ }
228
+
229
+ callback(END_AUTH);
230
+ };
231
+ }
232
+ }
233
+
234
+ export interface RemoteSSHConnectionOptions {
235
+ id: string;
236
+ name: string;
237
+ type: string;
238
+ client: ssh2.Client;
239
+ }
240
+
241
+ export class RemoteSSHConnection implements RemoteConnection {
242
+
243
+ id: string;
244
+ name: string;
245
+ type: string;
246
+ client: ssh2.Client;
247
+ localPort = 0;
248
+ remotePort = 0;
249
+
250
+ private sftpClientPromise: Promise<SftpClient>;
251
+
252
+ private readonly onDidDisconnectEmitter = new Emitter<void>();
253
+
254
+ get onDidDisconnect(): Event<void> {
255
+ return this.onDidDisconnectEmitter.event;
256
+ }
257
+
258
+ constructor(options: RemoteSSHConnectionOptions) {
259
+ this.id = options.id;
260
+ this.type = options.type;
261
+ this.name = options.name;
262
+ this.client = options.client;
263
+ this.onDidDisconnect(() => this.dispose());
264
+ this.client.on('end', () => {
265
+ this.onDidDisconnectEmitter.fire();
266
+ });
267
+ this.sftpClientPromise = this.setupSftpClient();
268
+ }
269
+
270
+ protected async setupSftpClient(): Promise<SftpClient> {
271
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
272
+ const sftpClient = new SftpClient() as any;
273
+ // A hack to set the internal ssh2 client of the sftp client
274
+ // That way, we don't have to create a second connection
275
+ sftpClient.client = this.client;
276
+ // Calling this function establishes the sftp connection on the ssh client
277
+ await sftpClient.getSftpChannel();
278
+ return sftpClient;
279
+ }
280
+
281
+ forwardOut(socket: net.Socket): void {
282
+ this.client.forwardOut(socket.localAddress!, socket.localPort!, '127.0.0.1', this.remotePort, (err, stream) => {
283
+ if (err) {
284
+ console.debug('Proxy message rejected', err);
285
+ } else {
286
+ stream.pipe(socket).pipe(stream);
287
+ }
288
+ });
289
+ }
290
+
291
+ async copy(localPath: string, remotePath: string): Promise<void> {
292
+ const sftpClient = await this.sftpClientPromise;
293
+ await sftpClient.put(localPath, remotePath);
294
+ }
295
+
296
+ exec(cmd: string, args?: string[], options: RemoteExecOptions = {}): Promise<RemoteExecResult> {
297
+ const deferred = new Deferred<RemoteExecResult>();
298
+ cmd = this.buildCmd(cmd, args);
299
+ this.client.exec(cmd, options, (err, stream) => {
300
+ if (err) {
301
+ return deferred.reject(err);
302
+ }
303
+ let stdout = '';
304
+ let stderr = '';
305
+ stream.on('close', () => {
306
+ deferred.resolve({ stdout, stderr });
307
+ }).on('data', (data: Buffer | string) => {
308
+ stdout += data.toString();
309
+ }).stderr.on('data', (data: Buffer | string) => {
310
+ stderr += data.toString();
311
+ });
312
+ });
313
+ return deferred.promise;
314
+ }
315
+
316
+ execPartial(cmd: string, tester: RemoteExecTester, args?: string[], options: RemoteExecOptions = {}): Promise<RemoteExecResult> {
317
+ const deferred = new Deferred<RemoteExecResult>();
318
+ cmd = this.buildCmd(cmd, args);
319
+ this.client.exec(cmd, {
320
+ ...options,
321
+ // Ensure that the process on the remote ends when the connection is closed
322
+ pty: true
323
+ }, (err, stream) => {
324
+ if (err) {
325
+ return deferred.reject(err);
326
+ }
327
+ // in pty mode we only have an stdout stream
328
+ // return stdout as stderr as well
329
+ let stdout = '';
330
+ stream.on('close', () => {
331
+ if (deferred.state === 'unresolved') {
332
+ deferred.resolve({ stdout, stderr: stdout });
333
+ }
334
+ }).on('data', (data: Buffer | string) => {
335
+ if (deferred.state === 'unresolved') {
336
+ stdout += data.toString();
337
+
338
+ if (tester(stdout, stdout)) {
339
+ deferred.resolve({ stdout, stderr: stdout });
340
+ }
341
+ }
342
+ });
343
+ });
344
+ return deferred.promise;
345
+ }
346
+
347
+ protected buildCmd(cmd: string, args?: string[]): string {
348
+ const escapedArgs = args?.map(arg => `"${arg.replace(/"/g, '\\"')}"`) || [];
349
+ const fullCmd = cmd + (escapedArgs.length > 0 ? (' ' + escapedArgs.join(' ')) : '');
350
+ return fullCmd;
351
+ }
352
+
353
+ dispose(): void {
354
+ this.client.end();
355
+ this.client.destroy();
356
+ }
357
+
358
+ }