Skip to content

Commit 6e46440

Browse files
committed
fix: further error handling enhancements
1 parent 9e96502 commit 6e46440

File tree

3 files changed

+124
-102
lines changed

3 files changed

+124
-102
lines changed

client/src/connection/ssh/auth.ts

Lines changed: 34 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,15 @@
33
import { l10n, window } from "vscode";
44

55
import { readFileSync } from "fs";
6-
import { NextAuthHandler, ParsedKey, Prompt, utils } from "ssh2";
6+
import {
7+
AgentAuthMethod,
8+
KeyboardInteractiveAuthMethod,
9+
ParsedKey,
10+
PasswordAuthMethod,
11+
Prompt,
12+
PublicKeyAuthMethod,
13+
utils,
14+
} from "ssh2";
715

816
/**
917
* Abstraction for presenting authentication prompts to the user.
@@ -118,13 +126,13 @@ export class AuthHandler {
118126
* @param resolve a function that resolves the promise that is waiting for the password
119127
* @param username the user name to use for the connection
120128
*/
121-
passwordAuth = async (cb: NextAuthHandler, username: string) => {
129+
passwordAuth = async (username: string): Promise<PasswordAuthMethod> => {
122130
const pw = await this._authPresenter.presentPasswordPrompt(username);
123-
cb({
131+
return {
124132
type: "password",
125133
password: pw,
126134
username: username,
127-
});
135+
};
128136
};
129137

130138
/**
@@ -133,8 +141,10 @@ export class AuthHandler {
133141
* @param resolve a function that resolves the promise that is waiting for authentication
134142
* @param username the user name to use for the connection
135143
*/
136-
keyboardInteractiveAuth = async (cb: NextAuthHandler, username: string) => {
137-
cb({
144+
keyboardInteractiveAuth = async (
145+
username: string,
146+
): Promise<KeyboardInteractiveAuthMethod> => {
147+
return {
138148
type: "keyboard-interactive",
139149
username: username,
140150
prompt: (_name, _instructions, _instructionsLang, prompts, finish) => {
@@ -146,20 +156,20 @@ export class AuthHandler {
146156
finish(answers);
147157
});
148158
},
149-
});
159+
};
150160
};
151161

152162
/**
153163
* Authenticate to the server using the ssh-agent. See the extension Docs for more information on how to set up the ssh-agent.
154164
* @param cb ssh2 NextHandler callback instance. This is used to pass the authentication information to the ssh server.
155165
* @param username the user name to use for the connection
156166
*/
157-
sshAgentAuth = (cb: NextAuthHandler, username: string) => {
158-
cb({
167+
sshAgentAuth = (username: string): AgentAuthMethod => {
168+
return {
159169
type: "agent",
160170
agent: process.env.SSH_AUTH_SOCK,
161171
username: username,
162-
});
172+
};
163173
};
164174

165175
/**
@@ -172,10 +182,9 @@ export class AuthHandler {
172182
* @param username the user name to use for the connection
173183
*/
174184
privateKeyAuth = async (
175-
cb: NextAuthHandler,
176185
privateKeyFilePath: string,
177186
username: string,
178-
) => {
187+
): Promise<PublicKeyAuthMethod> => {
179188
// first, try to parse the key file without a passphrase
180189
const parsedKeyResult = this._keyParser.parseKey(privateKeyFilePath);
181190
const hasParseError = parsedKeyResult instanceof Error;
@@ -186,7 +195,6 @@ export class AuthHandler {
186195
// key is encrypted, prompt for passphrase
187196
if (passphraseRequired) {
188197
const passphrase = await this._authPresenter.presentPassphrasePrompt();
189-
190198
//parse the keyfile using the passphrase
191199
const passphrasedKeyContentsResult = this._keyParser.parseKey(
192200
privateKeyFilePath,
@@ -195,23 +203,24 @@ export class AuthHandler {
195203

196204
if (passphrasedKeyContentsResult instanceof Error) {
197205
throw passphrasedKeyContentsResult;
206+
} else {
207+
return {
208+
type: "publickey",
209+
key: passphrasedKeyContentsResult,
210+
passphrase: passphrase,
211+
username: username,
212+
};
198213
}
199-
200-
cb({
201-
type: "publickey",
202-
key: passphrasedKeyContentsResult,
203-
passphrase: passphrase,
204-
username: username,
205-
});
206214
} else {
207215
if (hasParseError) {
208216
throw parsedKeyResult;
217+
} else {
218+
return {
219+
type: "publickey",
220+
key: parsedKeyResult,
221+
username: username,
222+
};
209223
}
210-
cb({
211-
type: "publickey",
212-
key: parsedKeyResult,
213-
username: username,
214-
});
215224
}
216225
};
217226
}

client/src/connection/ssh/index.ts

Lines changed: 71 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,16 @@
33
import { l10n } from "vscode";
44

55
import {
6+
AgentAuthMethod,
67
AuthHandlerMiddleware,
78
AuthenticationType,
89
Client,
910
ClientChannel,
1011
ConnectConfig,
12+
KeyboardInteractiveAuthMethod,
1113
NextAuthHandler,
14+
PasswordAuthMethod,
15+
PublicKeyAuthMethod,
1216
} from "ssh2";
1317

1418
import { BaseConfig, RunResult } from "..";
@@ -87,14 +91,17 @@ export class SSHSession extends Session {
8791
return;
8892
}
8993

94+
const authHandlerFn = this.handleSSHAuthentication();
9095
const cfg: ConnectConfig = {
9196
host: this._config.host,
9297
port: this._config.port,
9398
username: this._config.username,
9499
readyTimeout: CONNECT_READY_TIMEOUT,
95100
keepaliveInterval: KEEPALIVE_INTERVAL,
96101
keepaliveCountMax: KEEPALIVE_UNANSWERED_THRESHOLD,
97-
authHandler: this.handleSSHAuthentication,
102+
authHandler: (methodsLeft, partialSuccess, callback) => (
103+
authHandlerFn(methodsLeft, partialSuccess, callback), undefined
104+
),
98105
};
99106

100107
if (!this._conn) {
@@ -305,71 +312,74 @@ export class SSHSession extends Session {
305312
*/
306313
private clearAuthState = (): void => {
307314
this._sessionReady = false;
315+
this._authsLeft = [];
308316
};
309317

310-
private handleSSHAuthentication: AuthHandlerMiddleware = (
311-
authsLeft: AuthenticationType[],
312-
partialSuccess: boolean, //used in scenarios which require multiple auth methods to denote partial success
313-
nextAuth: NextAuthHandler,
314-
) => {
315-
if (!authsLeft) {
316-
nextAuth("none"); //sending none will prompt the server to send supported auth methods
317-
return;
318-
}
319-
320-
if (authsLeft.length === 0) {
321-
this._reject?.(
322-
new Error(l10n.t("Could not authenticate to the SSH server.")),
323-
);
324-
this.clearAuthState();
325-
return false; //returning false will stop the authentication process
326-
}
318+
private handleSSHAuthentication = (): AuthHandlerMiddleware => {
319+
//The ssh2 library supports sending false to stop the authentication process
320+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
321+
const END_AUTH = false as unknown as AuthenticationType;
322+
return async (
323+
authsLeft: AuthenticationType[],
324+
partialSuccess: boolean, //used in scenarios which require multiple auth methods to denote partial success
325+
nextAuth: NextAuthHandler,
326+
) => {
327+
if (!authsLeft) {
328+
return nextAuth({ type: "none", username: this._config.username }); //sending none will prompt the server to send supported auth methods
329+
} else {
330+
if (authsLeft.length === 0) {
331+
return nextAuth(END_AUTH);
332+
}
327333

328-
if (this._authsLeft.length === 0 || partialSuccess) {
329-
this._authsLeft = authsLeft;
330-
}
334+
if (this._authsLeft.length === 0 || partialSuccess) {
335+
this._authsLeft = authsLeft;
336+
}
331337

332-
const authMethod = this._authsLeft.shift();
333-
334-
switch (authMethod) {
335-
case "publickey": {
336-
//user set a keyfile path in profile config
337-
if (this._config.privateKeyFilePath) {
338-
this._authHandler
339-
.privateKeyAuth(
340-
nextAuth,
341-
this._config.privateKeyFilePath,
342-
this._config.username,
343-
)
344-
.catch((e) => {
345-
this._reject?.(e);
346-
return false;
347-
});
348-
} else if (process.env.SSH_AUTH_SOCK) {
349-
this._authHandler.sshAgentAuth(nextAuth, this._config.username);
338+
const authMethod = this._authsLeft.shift();
339+
340+
try {
341+
let authPayload:
342+
| PublicKeyAuthMethod
343+
| AgentAuthMethod
344+
| PasswordAuthMethod
345+
| KeyboardInteractiveAuthMethod;
346+
347+
switch (authMethod) {
348+
case "publickey": {
349+
//user set a keyfile path in profile config
350+
if (this._config.privateKeyFilePath) {
351+
authPayload = await this._authHandler.privateKeyAuth(
352+
this._config.privateKeyFilePath,
353+
this._config.username,
354+
);
355+
} else if (process.env.SSH_AUTH_SOCK) {
356+
authPayload = this._authHandler.sshAgentAuth(
357+
this._config.username,
358+
);
359+
}
360+
break;
361+
}
362+
case "password": {
363+
authPayload = await this._authHandler.passwordAuth(
364+
this._config.username,
365+
);
366+
break;
367+
}
368+
case "keyboard-interactive": {
369+
authPayload = await this._authHandler.keyboardInteractiveAuth(
370+
this._config.username,
371+
);
372+
break;
373+
}
374+
default:
375+
nextAuth(authMethod);
376+
}
377+
return nextAuth(authPayload);
378+
} catch (e) {
379+
this._reject?.(e);
380+
return nextAuth(END_AUTH);
350381
}
351-
break;
352382
}
353-
case "password": {
354-
this._authHandler
355-
.passwordAuth(nextAuth, this._config.username)
356-
.catch((e) => {
357-
this._reject?.(e);
358-
return false;
359-
});
360-
break;
361-
}
362-
case "keyboard-interactive": {
363-
this._authHandler
364-
.keyboardInteractiveAuth(nextAuth, this._config.username)
365-
.catch((e) => {
366-
this._reject?.(e);
367-
return false;
368-
});
369-
break;
370-
}
371-
default:
372-
nextAuth(authMethod);
373-
}
383+
};
374384
};
375385
}

0 commit comments

Comments
 (0)