@theia/remote 1.43.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 (142) hide show
  1. package/README.md +61 -0
  2. package/lib/electron-browser/remote-electron-file-dialog-service.d.ts +14 -0
  3. package/lib/electron-browser/remote-electron-file-dialog-service.d.ts.map +1 -0
  4. package/lib/electron-browser/remote-electron-file-dialog-service.js +58 -0
  5. package/lib/electron-browser/remote-electron-file-dialog-service.js.map +1 -0
  6. package/lib/electron-browser/remote-frontend-contribution.d.ts +26 -0
  7. package/lib/electron-browser/remote-frontend-contribution.d.ts.map +1 -0
  8. package/lib/electron-browser/remote-frontend-contribution.js +165 -0
  9. package/lib/electron-browser/remote-frontend-contribution.js.map +1 -0
  10. package/lib/electron-browser/remote-frontend-module.d.ts +4 -0
  11. package/lib/electron-browser/remote-frontend-module.d.ts.map +1 -0
  12. package/lib/electron-browser/remote-frontend-module.js +43 -0
  13. package/lib/electron-browser/remote-frontend-module.js.map +1 -0
  14. package/lib/electron-browser/remote-preferences.d.ts +11 -0
  15. package/lib/electron-browser/remote-preferences.d.ts.map +1 -0
  16. package/lib/electron-browser/remote-preferences.js +49 -0
  17. package/lib/electron-browser/remote-preferences.js.map +1 -0
  18. package/lib/electron-browser/remote-registry-contribution.d.ts +19 -0
  19. package/lib/electron-browser/remote-registry-contribution.d.ts.map +1 -0
  20. package/lib/electron-browser/remote-registry-contribution.js +75 -0
  21. package/lib/electron-browser/remote-registry-contribution.js.map +1 -0
  22. package/lib/electron-browser/remote-service.d.ts +6 -0
  23. package/lib/electron-browser/remote-service.d.ts.map +1 -0
  24. package/lib/electron-browser/remote-service.js +38 -0
  25. package/lib/electron-browser/remote-service.js.map +1 -0
  26. package/lib/electron-browser/remote-ssh-contribution.d.ts +18 -0
  27. package/lib/electron-browser/remote-ssh-contribution.d.ts.map +1 -0
  28. package/lib/electron-browser/remote-ssh-contribution.js +118 -0
  29. package/lib/electron-browser/remote-ssh-contribution.js.map +1 -0
  30. package/lib/electron-common/remote-ssh-connection-provider.d.ts +11 -0
  31. package/lib/electron-common/remote-ssh-connection-provider.d.ts.map +1 -0
  32. package/lib/electron-common/remote-ssh-connection-provider.js +21 -0
  33. package/lib/electron-common/remote-ssh-connection-provider.js.map +1 -0
  34. package/lib/electron-common/remote-status-service.d.ts +15 -0
  35. package/lib/electron-common/remote-status-service.d.ts.map +1 -0
  36. package/lib/electron-common/remote-status-service.js +21 -0
  37. package/lib/electron-common/remote-status-service.js.map +1 -0
  38. package/lib/electron-node/backend-remote-service-impl.d.ts +12 -0
  39. package/lib/electron-node/backend-remote-service-impl.d.ts.map +1 -0
  40. package/lib/electron-node/backend-remote-service-impl.js +51 -0
  41. package/lib/electron-node/backend-remote-service-impl.js.map +1 -0
  42. package/lib/electron-node/remote-backend-module.d.ts +5 -0
  43. package/lib/electron-node/remote-backend-module.d.ts.map +1 -0
  44. package/lib/electron-node/remote-backend-module.js +75 -0
  45. package/lib/electron-node/remote-backend-module.js.map +1 -0
  46. package/lib/electron-node/remote-connection-service.d.ts +15 -0
  47. package/lib/electron-node/remote-connection-service.d.ts.map +1 -0
  48. package/lib/electron-node/remote-connection-service.js +66 -0
  49. package/lib/electron-node/remote-connection-service.js.map +1 -0
  50. package/lib/electron-node/remote-connection-socket-provider.d.ts +9 -0
  51. package/lib/electron-node/remote-connection-socket-provider.d.ts.map +1 -0
  52. package/lib/electron-node/remote-connection-socket-provider.js +38 -0
  53. package/lib/electron-node/remote-connection-socket-provider.js.map +1 -0
  54. package/lib/electron-node/remote-proxy-server-provider.d.ts +6 -0
  55. package/lib/electron-node/remote-proxy-server-provider.d.ts.map +1 -0
  56. package/lib/electron-node/remote-proxy-server-provider.js +44 -0
  57. package/lib/electron-node/remote-proxy-server-provider.js.map +1 -0
  58. package/lib/electron-node/remote-status-service.d.ts +7 -0
  59. package/lib/electron-node/remote-status-service.d.ts.map +1 -0
  60. package/lib/electron-node/remote-status-service.js +55 -0
  61. package/lib/electron-node/remote-status-service.js.map +1 -0
  62. package/lib/electron-node/remote-types.d.ts +35 -0
  63. package/lib/electron-node/remote-types.d.ts.map +1 -0
  64. package/lib/electron-node/remote-types.js +18 -0
  65. package/lib/electron-node/remote-types.js.map +1 -0
  66. package/lib/electron-node/setup/app-native-dependency-contribution.d.ts +8 -0
  67. package/lib/electron-node/setup/app-native-dependency-contribution.d.ts.map +1 -0
  68. package/lib/electron-node/setup/app-native-dependency-contribution.js +58 -0
  69. package/lib/electron-node/setup/app-native-dependency-contribution.js.map +1 -0
  70. package/lib/electron-node/setup/main-copy-contribution.d.ts +5 -0
  71. package/lib/electron-node/setup/main-copy-contribution.d.ts.map +1 -0
  72. package/lib/electron-node/setup/main-copy-contribution.js +38 -0
  73. package/lib/electron-node/setup/main-copy-contribution.js.map +1 -0
  74. package/lib/electron-node/setup/remote-copy-contribution.d.ts +29 -0
  75. package/lib/electron-node/setup/remote-copy-contribution.d.ts.map +1 -0
  76. package/lib/electron-node/setup/remote-copy-contribution.js +79 -0
  77. package/lib/electron-node/setup/remote-copy-contribution.js.map +1 -0
  78. package/lib/electron-node/setup/remote-copy-service.d.ts +19 -0
  79. package/lib/electron-node/setup/remote-copy-service.d.ts.map +1 -0
  80. package/lib/electron-node/setup/remote-copy-service.js +127 -0
  81. package/lib/electron-node/setup/remote-copy-service.js.map +1 -0
  82. package/lib/electron-node/setup/remote-native-dependency-contribution.d.ts +35 -0
  83. package/lib/electron-node/setup/remote-native-dependency-contribution.d.ts.map +1 -0
  84. package/lib/electron-node/setup/remote-native-dependency-contribution.js +35 -0
  85. package/lib/electron-node/setup/remote-native-dependency-contribution.js.map +1 -0
  86. package/lib/electron-node/setup/remote-native-dependency-service.d.ts +24 -0
  87. package/lib/electron-node/setup/remote-native-dependency-service.d.ts.map +1 -0
  88. package/lib/electron-node/setup/remote-native-dependency-service.js +119 -0
  89. package/lib/electron-node/setup/remote-native-dependency-service.js.map +1 -0
  90. package/lib/electron-node/setup/remote-node-setup-service.d.ts +23 -0
  91. package/lib/electron-node/setup/remote-node-setup-service.d.ts.map +1 -0
  92. package/lib/electron-node/setup/remote-node-setup-service.js +133 -0
  93. package/lib/electron-node/setup/remote-node-setup-service.js.map +1 -0
  94. package/lib/electron-node/setup/remote-setup-script-service.d.ts +41 -0
  95. package/lib/electron-node/setup/remote-setup-script-service.d.ts.map +1 -0
  96. package/lib/electron-node/setup/remote-setup-script-service.js +133 -0
  97. package/lib/electron-node/setup/remote-setup-script-service.js.map +1 -0
  98. package/lib/electron-node/setup/remote-setup-service.d.ts +29 -0
  99. package/lib/electron-node/setup/remote-setup-service.d.ts.map +1 -0
  100. package/lib/electron-node/setup/remote-setup-service.js +199 -0
  101. package/lib/electron-node/setup/remote-setup-service.js.map +1 -0
  102. package/lib/electron-node/ssh/remote-ssh-connection-provider.d.ts +56 -0
  103. package/lib/electron-node/ssh/remote-ssh-connection-provider.d.ts.map +1 -0
  104. package/lib/electron-node/ssh/remote-ssh-connection-provider.js +343 -0
  105. package/lib/electron-node/ssh/remote-ssh-connection-provider.js.map +1 -0
  106. package/lib/electron-node/ssh/ssh-identity-file-collector.d.ts +13 -0
  107. package/lib/electron-node/ssh/ssh-identity-file-collector.d.ts.map +1 -0
  108. package/lib/electron-node/ssh/ssh-identity-file-collector.js +132 -0
  109. package/lib/electron-node/ssh/ssh-identity-file-collector.js.map +1 -0
  110. package/lib/package.spec.d.ts +1 -0
  111. package/lib/package.spec.d.ts.map +1 -0
  112. package/lib/package.spec.js +26 -0
  113. package/lib/package.spec.js.map +1 -0
  114. package/package.json +67 -0
  115. package/src/electron-browser/remote-electron-file-dialog-service.ts +47 -0
  116. package/src/electron-browser/remote-frontend-contribution.ts +145 -0
  117. package/src/electron-browser/remote-frontend-module.ts +49 -0
  118. package/src/electron-browser/remote-preferences.ts +62 -0
  119. package/src/electron-browser/remote-registry-contribution.ts +70 -0
  120. package/src/electron-browser/remote-service.ts +31 -0
  121. package/src/electron-browser/remote-ssh-contribution.ts +102 -0
  122. package/src/electron-common/remote-ssh-connection-provider.ts +29 -0
  123. package/src/electron-common/remote-status-service.ts +35 -0
  124. package/src/electron-node/backend-remote-service-impl.ts +45 -0
  125. package/src/electron-node/remote-backend-module.ts +80 -0
  126. package/src/electron-node/remote-connection-service.ts +55 -0
  127. package/src/electron-node/remote-connection-socket-provider.ts +34 -0
  128. package/src/electron-node/remote-proxy-server-provider.ts +37 -0
  129. package/src/electron-node/remote-status-service.ts +41 -0
  130. package/src/electron-node/remote-types.ts +56 -0
  131. package/src/electron-node/setup/app-native-dependency-contribution.ts +48 -0
  132. package/src/electron-node/setup/main-copy-contribution.ts +28 -0
  133. package/src/electron-node/setup/remote-copy-contribution.ts +90 -0
  134. package/src/electron-node/setup/remote-copy-service.ts +114 -0
  135. package/src/electron-node/setup/remote-native-dependency-contribution.ts +63 -0
  136. package/src/electron-node/setup/remote-native-dependency-service.ts +111 -0
  137. package/src/electron-node/setup/remote-node-setup-service.ts +123 -0
  138. package/src/electron-node/setup/remote-setup-script-service.ts +146 -0
  139. package/src/electron-node/setup/remote-setup-service.ts +197 -0
  140. package/src/electron-node/ssh/remote-ssh-connection-provider.ts +356 -0
  141. package/src/electron-node/ssh/ssh-identity-file-collector.ts +137 -0
  142. package/src/package.spec.ts +29 -0
@@ -0,0 +1,197 @@
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 { inject, injectable } from '@theia/core/shared/inversify';
18
+ import { RemoteConnection, RemoteExecResult, RemotePlatform, RemoteStatusReport } from '../remote-types';
19
+ import { ApplicationPackage } from '@theia/core/shared/@theia/application-package';
20
+ import { RemoteCopyService } from './remote-copy-service';
21
+ import { RemoteNativeDependencyService } from './remote-native-dependency-service';
22
+ import { OS, THEIA_VERSION } from '@theia/core';
23
+ import { RemoteNodeSetupService } from './remote-node-setup-service';
24
+ import { RemoteSetupScriptService } from './remote-setup-script-service';
25
+
26
+ export interface RemoteSetupOptions {
27
+ connection: RemoteConnection;
28
+ report: RemoteStatusReport;
29
+ nodeDownloadTemplate?: string;
30
+ }
31
+
32
+ @injectable()
33
+ export class RemoteSetupService {
34
+
35
+ @inject(RemoteCopyService)
36
+ protected readonly copyService: RemoteCopyService;
37
+
38
+ @inject(RemoteNativeDependencyService)
39
+ protected readonly nativeDependencyService: RemoteNativeDependencyService;
40
+
41
+ @inject(RemoteNodeSetupService)
42
+ protected readonly nodeSetupService: RemoteNodeSetupService;
43
+
44
+ @inject(RemoteSetupScriptService)
45
+ protected readonly scriptService: RemoteSetupScriptService;
46
+
47
+ @inject(ApplicationPackage)
48
+ protected readonly applicationPackage: ApplicationPackage;
49
+
50
+ async setup(options: RemoteSetupOptions): Promise<void> {
51
+ const {
52
+ connection,
53
+ report,
54
+ nodeDownloadTemplate
55
+ } = options;
56
+ report('Identifying remote system...');
57
+ // 1. Identify remote platform
58
+ const platform = await this.detectRemotePlatform(connection);
59
+ // 2. Setup home directory
60
+ const remoteHome = await this.getRemoteHomeDirectory(connection, platform);
61
+ const applicationDirectory = this.scriptService.joinPath(platform, remoteHome, `.${this.getRemoteAppName()}`);
62
+ await this.mkdirRemote(connection, platform, applicationDirectory);
63
+ // 3. Download+copy node for that platform
64
+ const nodeFileName = this.nodeSetupService.getNodeFileName(platform);
65
+ const nodeDirName = this.nodeSetupService.getNodeDirectoryName(platform);
66
+ const remoteNodeDirectory = this.scriptService.joinPath(platform, applicationDirectory, nodeDirName);
67
+ const nodeDirExists = await this.dirExistsRemote(connection, remoteNodeDirectory);
68
+ if (!nodeDirExists) {
69
+ report('Downloading and installing Node.js on remote...');
70
+ // Download the binaries locally and move it via SSH
71
+ const nodeArchive = await this.nodeSetupService.downloadNode(platform, nodeDownloadTemplate);
72
+ const remoteNodeZip = this.scriptService.joinPath(platform, applicationDirectory, nodeFileName);
73
+ await connection.copy(nodeArchive, remoteNodeZip);
74
+ await this.unzipRemote(connection, platform, remoteNodeZip, applicationDirectory);
75
+ }
76
+ // 4. Copy backend to remote system
77
+ const libDir = this.scriptService.joinPath(platform, applicationDirectory, 'lib');
78
+ const libDirExists = await this.dirExistsRemote(connection, libDir);
79
+ if (!libDirExists) {
80
+ report('Installing application on remote...');
81
+ const applicationZipFile = this.scriptService.joinPath(platform, applicationDirectory, `${this.getRemoteAppName()}.tar`);
82
+ await this.copyService.copyToRemote(connection, platform, applicationZipFile);
83
+ await this.unzipRemote(connection, platform, applicationZipFile, applicationDirectory);
84
+ }
85
+ // 5. start remote backend
86
+ report('Starting application on remote...');
87
+ const port = await this.startApplication(connection, platform, applicationDirectory, remoteNodeDirectory);
88
+ connection.remotePort = port;
89
+ }
90
+
91
+ protected async startApplication(connection: RemoteConnection, platform: RemotePlatform, remotePath: string, nodeDir: string): Promise<number> {
92
+ const nodeExecutable = this.scriptService.joinPath(platform, nodeDir, ...(platform.os === OS.Type.Windows ? ['node.exe'] : ['bin', 'node']));
93
+ const mainJsFile = this.scriptService.joinPath(platform, remotePath, 'lib', 'backend', 'main.js');
94
+ const localAddressRegex = /listening on http:\/\/127.0.0.1:(\d+)/;
95
+ let prefix = '';
96
+ if (platform.os === OS.Type.Windows) {
97
+ // We might to switch to PowerShell beforehand on Windows
98
+ prefix = this.scriptService.exec(platform) + ' ';
99
+ }
100
+ // Change to the remote application path and start a node process with the copied main.js file
101
+ // This way, our current working directory is set as expected
102
+ const result = await connection.execPartial(`${prefix}cd "${remotePath}";${nodeExecutable}`,
103
+ stdout => localAddressRegex.test(stdout),
104
+ [mainJsFile, '--hostname=127.0.0.1', '--port=0', '--remote']);
105
+
106
+ const match = localAddressRegex.exec(result.stdout);
107
+ if (!match) {
108
+ throw new Error('Could not start remote system: ' + result.stdout);
109
+ } else {
110
+ return Number(match[1]);
111
+ }
112
+ }
113
+
114
+ protected async detectRemotePlatform(connection: RemoteConnection): Promise<RemotePlatform> {
115
+ const osResult = await connection.exec('uname -s');
116
+
117
+ let os: OS.Type | undefined;
118
+ if (osResult.stderr) {
119
+ // Only Windows systems return an error stream here
120
+ os = OS.Type.Windows;
121
+ } else if (osResult.stdout) {
122
+ if (osResult.stdout.includes('windows32') || osResult.stdout.includes('MINGW64')) {
123
+ os = OS.Type.Windows;
124
+ } else if (osResult.stdout.includes('Linux')) {
125
+ os = OS.Type.Linux;
126
+ } else if (osResult.stdout.includes('Darwin')) {
127
+ os = OS.Type.OSX;
128
+ }
129
+ }
130
+ if (!os) {
131
+ throw new Error('Failed to identify remote system: ' + osResult.stdout + '\n' + osResult.stderr);
132
+ }
133
+ let arch: string | undefined;
134
+ if (os === OS.Type.Windows) {
135
+ const wmicResult = await connection.exec('wmic OS get OSArchitecture');
136
+ if (wmicResult.stdout.includes('64-bit')) {
137
+ arch = 'x64';
138
+ } else if (wmicResult.stdout.includes('32-bit')) {
139
+ arch = 'x86';
140
+ }
141
+ } else {
142
+ const archResult = (await connection.exec('uname -m')).stdout;
143
+ if (archResult.includes('x86_64')) {
144
+ arch = 'x64';
145
+ } else if (archResult.match(/i\d83/)) { // i386, i483, i683
146
+ arch = 'x86';
147
+ } else {
148
+ arch = archResult.trim();
149
+ }
150
+ }
151
+ if (!arch) {
152
+ throw new Error('Could not identify remote system architecture');
153
+ }
154
+ return {
155
+ os,
156
+ arch
157
+ };
158
+ }
159
+
160
+ protected async getRemoteHomeDirectory(connection: RemoteConnection, platform: RemotePlatform): Promise<string> {
161
+ const result = await connection.exec(this.scriptService.home(platform));
162
+ return result.stdout.trim();
163
+ }
164
+
165
+ protected getRemoteAppName(): string {
166
+ const appName = this.applicationPackage.pck.name || 'theia';
167
+ const appVersion = this.applicationPackage.pck.version || THEIA_VERSION;
168
+ return `${this.cleanupDirectoryName(`${appName}-${appVersion}`)}-remote`;
169
+ }
170
+
171
+ protected cleanupDirectoryName(name: string): string {
172
+ return name.replace(/[@<>:"\\|?*]/g, '').replace(/\//g, '-');
173
+ }
174
+
175
+ protected async mkdirRemote(connection: RemoteConnection, platform: RemotePlatform, remotePath: string): Promise<void> {
176
+ const result = await connection.exec(this.scriptService.mkdir(platform, remotePath));
177
+ if (result.stderr) {
178
+ throw new Error('Failed to create directory: ' + result.stderr);
179
+ }
180
+ }
181
+
182
+ protected async dirExistsRemote(connection: RemoteConnection, remotePath: string): Promise<boolean> {
183
+ const cdResult = await connection.exec(`cd "${remotePath}"`);
184
+ return !Boolean(cdResult.stderr);
185
+ }
186
+
187
+ protected async unzipRemote(connection: RemoteConnection, platform: RemotePlatform, remoteFile: string, remoteDirectory: string): Promise<void> {
188
+ const result = await connection.exec(this.scriptService.unzip(platform, remoteFile, remoteDirectory));
189
+ if (result.stderr) {
190
+ throw new Error('Failed to unzip: ' + result.stderr);
191
+ }
192
+ }
193
+
194
+ protected async executeScriptRemote(connection: RemoteConnection, platform: RemotePlatform, script: string): Promise<RemoteExecResult> {
195
+ return connection.exec(this.scriptService.exec(platform), [script]);
196
+ }
197
+ }
@@ -0,0 +1,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 { 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
+ }
@@ -0,0 +1,137 @@
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 fs from '@theia/core/shared/fs-extra';
18
+ import * as os from 'os';
19
+ import * as path from 'path';
20
+ import * as crypto from 'crypto';
21
+ import { ParsedKey } from 'ssh2';
22
+ import * as ssh2 from 'ssh2';
23
+ import { injectable } from '@theia/core/shared/inversify';
24
+
25
+ export interface SSHKey {
26
+ filename: string;
27
+ parsedKey: ParsedKey;
28
+ fingerprint: string;
29
+ agentSupport?: boolean;
30
+ isPrivate?: boolean;
31
+ }
32
+
33
+ @injectable()
34
+ export class SSHIdentityFileCollector {
35
+
36
+ protected getDefaultIdentityFiles(): string[] {
37
+ const homeDir = os.homedir();
38
+ const PATH_SSH_CLIENT_ID_DSA = path.join(homeDir, '.ssh', '/id_dsa');
39
+ const PATH_SSH_CLIENT_ID_ECDSA = path.join(homeDir, '.ssh', '/id_ecdsa');
40
+ const PATH_SSH_CLIENT_ID_RSA = path.join(homeDir, '.ssh', '/id_rsa');
41
+ const PATH_SSH_CLIENT_ID_ED25519 = path.join(homeDir, '.ssh', '/id_ed25519');
42
+ const PATH_SSH_CLIENT_ID_XMSS = path.join(homeDir, '.ssh', '/id_xmss');
43
+ const PATH_SSH_CLIENT_ID_ECDSA_SK = path.join(homeDir, '.ssh', '/id_ecdsa_sk');
44
+ const PATH_SSH_CLIENT_ID_ED25519_SK = path.join(homeDir, '.ssh', '/id_ed25519_sk');
45
+
46
+ return [
47
+ PATH_SSH_CLIENT_ID_DSA,
48
+ PATH_SSH_CLIENT_ID_ECDSA,
49
+ PATH_SSH_CLIENT_ID_ECDSA_SK,
50
+ PATH_SSH_CLIENT_ID_ED25519,
51
+ PATH_SSH_CLIENT_ID_ED25519_SK,
52
+ PATH_SSH_CLIENT_ID_RSA,
53
+ PATH_SSH_CLIENT_ID_XMSS
54
+ ];
55
+ }
56
+
57
+ async gatherIdentityFiles(sshAgentSock?: string): Promise<SSHKey[]> {
58
+ const identityFiles = this.getDefaultIdentityFiles();
59
+
60
+ const identityFileContentsResult = await Promise.allSettled(identityFiles.map(async keyPath => {
61
+ keyPath = await fs.pathExists(keyPath + '.pub') ? keyPath + '.pub' : keyPath;
62
+ return fs.promises.readFile(keyPath);
63
+ }));
64
+ const fileKeys: SSHKey[] = identityFileContentsResult.map((result, i) => {
65
+ if (result.status === 'rejected') {
66
+ return undefined;
67
+ }
68
+
69
+ const parsedResult = ssh2.utils.parseKey(result.value);
70
+ if (parsedResult instanceof Error || !parsedResult) {
71
+ console.log(`Error while parsing SSH public key ${identityFiles[i]}:`, parsedResult);
72
+ return undefined;
73
+ }
74
+
75
+ const parsedKey = Array.isArray(parsedResult) ? parsedResult[0] : parsedResult;
76
+ const fingerprint = crypto.createHash('sha256').update(parsedKey.getPublicSSH()).digest('base64');
77
+
78
+ return {
79
+ filename: identityFiles[i],
80
+ parsedKey,
81
+ fingerprint
82
+ };
83
+ }).filter(<T>(v: T | undefined): v is T => !!v);
84
+
85
+ let sshAgentParsedKeys: ParsedKey[] = [];
86
+ if (sshAgentSock) {
87
+ sshAgentParsedKeys = await new Promise<ParsedKey[]>((resolve, reject) => {
88
+ const sshAgent = new ssh2.OpenSSHAgent(sshAgentSock);
89
+ sshAgent.getIdentities((err, publicKeys) => {
90
+ if (err) {
91
+ reject(err);
92
+ } else if (publicKeys) {
93
+ resolve(publicKeys.map(key => {
94
+ if ('pubKey' in key) {
95
+ const pubKey = key.pubKey;
96
+ if ('pubKey' in pubKey) {
97
+ return pubKey.pubKey as ParsedKey;
98
+ }
99
+ return pubKey;
100
+ } else {
101
+ return key;
102
+ }
103
+ }));
104
+ } else {
105
+ resolve([]);
106
+ }
107
+ });
108
+ });
109
+ }
110
+
111
+ const sshAgentKeys: SSHKey[] = sshAgentParsedKeys.map(parsedKey => {
112
+ const fingerprint = crypto.createHash('sha256').update(parsedKey.getPublicSSH()).digest('base64');
113
+ return {
114
+ filename: parsedKey.comment,
115
+ parsedKey,
116
+ fingerprint,
117
+ agentSupport: true
118
+ };
119
+ });
120
+
121
+ const agentKeys: SSHKey[] = [];
122
+ const preferredIdentityKeys: SSHKey[] = [];
123
+ for (const agentKey of sshAgentKeys) {
124
+ const foundIdx = fileKeys.findIndex(k => agentKey.parsedKey.type === k.parsedKey.type && agentKey.fingerprint === k.fingerprint);
125
+ if (foundIdx >= 0) {
126
+ preferredIdentityKeys.push({ ...fileKeys[foundIdx], agentSupport: true });
127
+ fileKeys.splice(foundIdx, 1);
128
+ } else {
129
+ agentKeys.push(agentKey);
130
+ }
131
+ }
132
+ preferredIdentityKeys.push(...agentKeys);
133
+ preferredIdentityKeys.push(...fileKeys);
134
+
135
+ return preferredIdentityKeys;
136
+ }
137
+ }