@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.
- package/README.md +61 -0
- package/lib/electron-browser/remote-electron-file-dialog-service.d.ts +14 -0
- package/lib/electron-browser/remote-electron-file-dialog-service.d.ts.map +1 -0
- package/lib/electron-browser/remote-electron-file-dialog-service.js +58 -0
- package/lib/electron-browser/remote-electron-file-dialog-service.js.map +1 -0
- package/lib/electron-browser/remote-frontend-contribution.d.ts +26 -0
- package/lib/electron-browser/remote-frontend-contribution.d.ts.map +1 -0
- package/lib/electron-browser/remote-frontend-contribution.js +165 -0
- package/lib/electron-browser/remote-frontend-contribution.js.map +1 -0
- package/lib/electron-browser/remote-frontend-module.d.ts +4 -0
- package/lib/electron-browser/remote-frontend-module.d.ts.map +1 -0
- package/lib/electron-browser/remote-frontend-module.js +43 -0
- package/lib/electron-browser/remote-frontend-module.js.map +1 -0
- package/lib/electron-browser/remote-preferences.d.ts +11 -0
- package/lib/electron-browser/remote-preferences.d.ts.map +1 -0
- package/lib/electron-browser/remote-preferences.js +49 -0
- package/lib/electron-browser/remote-preferences.js.map +1 -0
- package/lib/electron-browser/remote-registry-contribution.d.ts +19 -0
- package/lib/electron-browser/remote-registry-contribution.d.ts.map +1 -0
- package/lib/electron-browser/remote-registry-contribution.js +75 -0
- package/lib/electron-browser/remote-registry-contribution.js.map +1 -0
- package/lib/electron-browser/remote-service.d.ts +6 -0
- package/lib/electron-browser/remote-service.d.ts.map +1 -0
- package/lib/electron-browser/remote-service.js +38 -0
- package/lib/electron-browser/remote-service.js.map +1 -0
- package/lib/electron-browser/remote-ssh-contribution.d.ts +18 -0
- package/lib/electron-browser/remote-ssh-contribution.d.ts.map +1 -0
- package/lib/electron-browser/remote-ssh-contribution.js +118 -0
- package/lib/electron-browser/remote-ssh-contribution.js.map +1 -0
- package/lib/electron-common/remote-ssh-connection-provider.d.ts +11 -0
- package/lib/electron-common/remote-ssh-connection-provider.d.ts.map +1 -0
- package/lib/electron-common/remote-ssh-connection-provider.js +21 -0
- package/lib/electron-common/remote-ssh-connection-provider.js.map +1 -0
- package/lib/electron-common/remote-status-service.d.ts +15 -0
- package/lib/electron-common/remote-status-service.d.ts.map +1 -0
- package/lib/electron-common/remote-status-service.js +21 -0
- package/lib/electron-common/remote-status-service.js.map +1 -0
- package/lib/electron-node/backend-remote-service-impl.d.ts +12 -0
- package/lib/electron-node/backend-remote-service-impl.d.ts.map +1 -0
- package/lib/electron-node/backend-remote-service-impl.js +51 -0
- package/lib/electron-node/backend-remote-service-impl.js.map +1 -0
- package/lib/electron-node/remote-backend-module.d.ts +5 -0
- package/lib/electron-node/remote-backend-module.d.ts.map +1 -0
- package/lib/electron-node/remote-backend-module.js +75 -0
- package/lib/electron-node/remote-backend-module.js.map +1 -0
- package/lib/electron-node/remote-connection-service.d.ts +15 -0
- package/lib/electron-node/remote-connection-service.d.ts.map +1 -0
- package/lib/electron-node/remote-connection-service.js +66 -0
- package/lib/electron-node/remote-connection-service.js.map +1 -0
- package/lib/electron-node/remote-connection-socket-provider.d.ts +9 -0
- package/lib/electron-node/remote-connection-socket-provider.d.ts.map +1 -0
- package/lib/electron-node/remote-connection-socket-provider.js +38 -0
- package/lib/electron-node/remote-connection-socket-provider.js.map +1 -0
- package/lib/electron-node/remote-proxy-server-provider.d.ts +6 -0
- package/lib/electron-node/remote-proxy-server-provider.d.ts.map +1 -0
- package/lib/electron-node/remote-proxy-server-provider.js +44 -0
- package/lib/electron-node/remote-proxy-server-provider.js.map +1 -0
- package/lib/electron-node/remote-status-service.d.ts +7 -0
- package/lib/electron-node/remote-status-service.d.ts.map +1 -0
- package/lib/electron-node/remote-status-service.js +55 -0
- package/lib/electron-node/remote-status-service.js.map +1 -0
- package/lib/electron-node/remote-types.d.ts +35 -0
- package/lib/electron-node/remote-types.d.ts.map +1 -0
- package/lib/electron-node/remote-types.js +18 -0
- package/lib/electron-node/remote-types.js.map +1 -0
- package/lib/electron-node/setup/app-native-dependency-contribution.d.ts +8 -0
- package/lib/electron-node/setup/app-native-dependency-contribution.d.ts.map +1 -0
- package/lib/electron-node/setup/app-native-dependency-contribution.js +58 -0
- package/lib/electron-node/setup/app-native-dependency-contribution.js.map +1 -0
- package/lib/electron-node/setup/main-copy-contribution.d.ts +5 -0
- package/lib/electron-node/setup/main-copy-contribution.d.ts.map +1 -0
- package/lib/electron-node/setup/main-copy-contribution.js +38 -0
- package/lib/electron-node/setup/main-copy-contribution.js.map +1 -0
- package/lib/electron-node/setup/remote-copy-contribution.d.ts +29 -0
- package/lib/electron-node/setup/remote-copy-contribution.d.ts.map +1 -0
- package/lib/electron-node/setup/remote-copy-contribution.js +79 -0
- package/lib/electron-node/setup/remote-copy-contribution.js.map +1 -0
- package/lib/electron-node/setup/remote-copy-service.d.ts +19 -0
- package/lib/electron-node/setup/remote-copy-service.d.ts.map +1 -0
- package/lib/electron-node/setup/remote-copy-service.js +127 -0
- package/lib/electron-node/setup/remote-copy-service.js.map +1 -0
- package/lib/electron-node/setup/remote-native-dependency-contribution.d.ts +35 -0
- package/lib/electron-node/setup/remote-native-dependency-contribution.d.ts.map +1 -0
- package/lib/electron-node/setup/remote-native-dependency-contribution.js +35 -0
- package/lib/electron-node/setup/remote-native-dependency-contribution.js.map +1 -0
- package/lib/electron-node/setup/remote-native-dependency-service.d.ts +24 -0
- package/lib/electron-node/setup/remote-native-dependency-service.d.ts.map +1 -0
- package/lib/electron-node/setup/remote-native-dependency-service.js +119 -0
- package/lib/electron-node/setup/remote-native-dependency-service.js.map +1 -0
- package/lib/electron-node/setup/remote-node-setup-service.d.ts +23 -0
- package/lib/electron-node/setup/remote-node-setup-service.d.ts.map +1 -0
- package/lib/electron-node/setup/remote-node-setup-service.js +133 -0
- package/lib/electron-node/setup/remote-node-setup-service.js.map +1 -0
- package/lib/electron-node/setup/remote-setup-script-service.d.ts +41 -0
- package/lib/electron-node/setup/remote-setup-script-service.d.ts.map +1 -0
- package/lib/electron-node/setup/remote-setup-script-service.js +133 -0
- package/lib/electron-node/setup/remote-setup-script-service.js.map +1 -0
- package/lib/electron-node/setup/remote-setup-service.d.ts +29 -0
- package/lib/electron-node/setup/remote-setup-service.d.ts.map +1 -0
- package/lib/electron-node/setup/remote-setup-service.js +199 -0
- package/lib/electron-node/setup/remote-setup-service.js.map +1 -0
- package/lib/electron-node/ssh/remote-ssh-connection-provider.d.ts +56 -0
- package/lib/electron-node/ssh/remote-ssh-connection-provider.d.ts.map +1 -0
- package/lib/electron-node/ssh/remote-ssh-connection-provider.js +343 -0
- package/lib/electron-node/ssh/remote-ssh-connection-provider.js.map +1 -0
- package/lib/electron-node/ssh/ssh-identity-file-collector.d.ts +13 -0
- package/lib/electron-node/ssh/ssh-identity-file-collector.d.ts.map +1 -0
- package/lib/electron-node/ssh/ssh-identity-file-collector.js +132 -0
- package/lib/electron-node/ssh/ssh-identity-file-collector.js.map +1 -0
- package/lib/package.spec.d.ts +1 -0
- package/lib/package.spec.d.ts.map +1 -0
- package/lib/package.spec.js +26 -0
- package/lib/package.spec.js.map +1 -0
- package/package.json +67 -0
- package/src/electron-browser/remote-electron-file-dialog-service.ts +47 -0
- package/src/electron-browser/remote-frontend-contribution.ts +145 -0
- package/src/electron-browser/remote-frontend-module.ts +49 -0
- package/src/electron-browser/remote-preferences.ts +62 -0
- package/src/electron-browser/remote-registry-contribution.ts +70 -0
- package/src/electron-browser/remote-service.ts +31 -0
- package/src/electron-browser/remote-ssh-contribution.ts +102 -0
- package/src/electron-common/remote-ssh-connection-provider.ts +29 -0
- package/src/electron-common/remote-status-service.ts +35 -0
- package/src/electron-node/backend-remote-service-impl.ts +45 -0
- package/src/electron-node/remote-backend-module.ts +80 -0
- package/src/electron-node/remote-connection-service.ts +55 -0
- package/src/electron-node/remote-connection-socket-provider.ts +34 -0
- package/src/electron-node/remote-proxy-server-provider.ts +37 -0
- package/src/electron-node/remote-status-service.ts +41 -0
- package/src/electron-node/remote-types.ts +56 -0
- package/src/electron-node/setup/app-native-dependency-contribution.ts +48 -0
- package/src/electron-node/setup/main-copy-contribution.ts +28 -0
- package/src/electron-node/setup/remote-copy-contribution.ts +90 -0
- package/src/electron-node/setup/remote-copy-service.ts +114 -0
- package/src/electron-node/setup/remote-native-dependency-contribution.ts +63 -0
- package/src/electron-node/setup/remote-native-dependency-service.ts +111 -0
- package/src/electron-node/setup/remote-node-setup-service.ts +123 -0
- package/src/electron-node/setup/remote-setup-script-service.ts +146 -0
- package/src/electron-node/setup/remote-setup-service.ts +197 -0
- package/src/electron-node/ssh/remote-ssh-connection-provider.ts +356 -0
- package/src/electron-node/ssh/ssh-identity-file-collector.ts +137 -0
- 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
|
+
}
|