@theia/remote 1.52.0 → 1.53.0-next.55

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 (37) hide show
  1. package/README.md +61 -61
  2. package/lib/electron-node/setup/remote-setup-script-service.js +11 -11
  3. package/package.json +5 -5
  4. package/src/electron-browser/port-forwarding/port-forwading-contribution.ts +33 -33
  5. package/src/electron-browser/port-forwarding/port-forwarding-service.ts +92 -92
  6. package/src/electron-browser/port-forwarding/port-forwarding-widget.tsx +140 -140
  7. package/src/electron-browser/remote-electron-file-dialog-service.ts +47 -47
  8. package/src/electron-browser/remote-frontend-contribution.ts +143 -143
  9. package/src/electron-browser/remote-frontend-module.ts +68 -68
  10. package/src/electron-browser/remote-preferences.ts +62 -62
  11. package/src/electron-browser/remote-registry-contribution.ts +73 -73
  12. package/src/electron-browser/remote-service.ts +31 -31
  13. package/src/electron-browser/remote-ssh-contribution.ts +102 -102
  14. package/src/electron-browser/style/port-forwarding-widget.css +44 -44
  15. package/src/electron-common/remote-port-forwarding-provider.ts +30 -30
  16. package/src/electron-common/remote-ssh-connection-provider.ts +29 -29
  17. package/src/electron-common/remote-status-service.ts +35 -35
  18. package/src/electron-node/backend-remote-service-impl.ts +45 -45
  19. package/src/electron-node/remote-backend-module.ts +87 -87
  20. package/src/electron-node/remote-connection-service.ts +56 -56
  21. package/src/electron-node/remote-connection-socket-provider.ts +34 -34
  22. package/src/electron-node/remote-port-forwarding-provider.ts +66 -66
  23. package/src/electron-node/remote-proxy-server-provider.ts +37 -37
  24. package/src/electron-node/remote-status-service.ts +41 -41
  25. package/src/electron-node/remote-types.ts +64 -64
  26. package/src/electron-node/setup/app-native-dependency-contribution.ts +48 -48
  27. package/src/electron-node/setup/main-copy-contribution.ts +28 -28
  28. package/src/electron-node/setup/remote-copy-contribution.ts +74 -74
  29. package/src/electron-node/setup/remote-copy-service.ts +116 -116
  30. package/src/electron-node/setup/remote-native-dependency-contribution.ts +63 -63
  31. package/src/electron-node/setup/remote-native-dependency-service.ts +111 -111
  32. package/src/electron-node/setup/remote-node-setup-service.ts +123 -123
  33. package/src/electron-node/setup/remote-setup-script-service.ts +146 -146
  34. package/src/electron-node/setup/remote-setup-service.ts +220 -220
  35. package/src/electron-node/ssh/remote-ssh-connection-provider.ts +358 -358
  36. package/src/electron-node/ssh/ssh-identity-file-collector.ts +137 -137
  37. package/src/package.spec.ts +29 -29
@@ -1,358 +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 { 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, port?: number): void {
282
- this.client.forwardOut(socket.localAddress!, socket.localPort!, '127.0.0.1', port ?? 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
- }
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, port?: number): void {
282
+ this.client.forwardOut(socket.localAddress!, socket.localPort!, '127.0.0.1', port ?? 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
+ }