Skip to content

Commit e0e26dd

Browse files
committed
fix: bug fixes found during mfa testing
1 parent debafaf commit e0e26dd

File tree

3 files changed

+78
-68
lines changed

3 files changed

+78
-68
lines changed

client/src/connection/ssh/auth.ts

Lines changed: 33 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -27,29 +27,46 @@ export interface AuthPresenter {
2727
* @param prompts an array of prompts to present to the user
2828
* @returns array of answers to the prompts
2929
*/
30-
presentMultiplePrompts: (prompts: Prompt[]) => Promise<string[]>;
30+
presentMultiplePrompts: (
31+
username: string,
32+
prompts: Prompt[],
33+
) => Promise<string[]>;
3134
}
3235

3336
class AuthPresenterImpl implements AuthPresenter {
3437
presentPasswordPrompt = async (username: string): Promise<string> => {
35-
return this.presentSecurePrompt(
38+
return this.presentPrompt(
3639
l10n.t("Enter the password for user: {username}", { username }),
3740
l10n.t("Password Required"),
41+
true,
3842
);
3943
};
4044

4145
presentPassphrasePrompt = async (): Promise<string> => {
42-
return this.presentSecurePrompt(
46+
return this.presentPrompt(
4347
l10n.t("Enter the passphrase for the private key"),
4448
l10n.t("Passphrase Required"),
49+
true,
4550
);
4651
};
4752

48-
presentMultiplePrompts = async (prompts: Prompt[]): Promise<string[]> => {
53+
presentMultiplePrompts = async (
54+
username: string,
55+
prompts: Prompt[],
56+
): Promise<string[]> => {
4957
const answers: string[] = [];
5058
for (const prompt of prompts) {
51-
const answer = await this.presentSecurePrompt(prompt.prompt);
52-
answers.push(answer);
59+
const answer = await this.presentPrompt(
60+
undefined,
61+
l10n.t("User {username} {prompt}", {
62+
username,
63+
prompt: prompt.prompt,
64+
}),
65+
!prompt.echo,
66+
);
67+
if (answer) {
68+
answers.push(answer);
69+
}
5370
}
5471
return answers;
5572
};
@@ -58,17 +75,19 @@ class AuthPresenterImpl implements AuthPresenter {
5875
* Present a secure prompt to the user.
5976
* @param prompt the prompt to display to the user
6077
* @param title optional title for the prompt
78+
* @param isSecureInput whether the input should be hidden
6179
* @returns the user's response to the prompt
6280
*/
63-
private presentSecurePrompt = async (
81+
private presentPrompt = async (
6482
prompt: string,
6583
title?: string,
84+
isSecureInput?: boolean,
6685
): Promise<string> => {
6786
return window.showInputBox({
6887
ignoreFocusOut: true,
6988
prompt: prompt,
7089
title: title,
71-
password: true,
90+
password: isSecureInput,
7291
});
7392
};
7493
}
@@ -119,18 +138,20 @@ export class AuthHandler {
119138
cb({
120139
type: "keyboard-interactive",
121140
username: username,
122-
prompt: (_name, _instructions, _instructionsLang, prompts, promptCb) => {
141+
prompt: (_name, _instructions, _instructionsLang, prompts, finish) => {
123142
// often, the server will only send a single prompt for the password.
124143
// however, PAM can send multiple prompts, so we need to handle that case
125144
this._authPresenter
126-
.presentMultiplePrompts(prompts)
127-
.then((answers) => promptCb(answers));
145+
.presentMultiplePrompts(username, prompts)
146+
.then((answers) => {
147+
finish(answers);
148+
});
128149
},
129150
});
130151
};
131152

132153
/**
133-
* Authenticate to the server using the ssh-agent. See the extension README for more information on how to set up the ssh-agent.
154+
* Authenticate to the server using the ssh-agent. See the extension Docs for more information on how to set up the ssh-agent.
134155
* @param cb ssh2 NextHandler callback instance. This is used to pass the authentication information to the ssh server.
135156
* @param username the user name to use for the connection
136157
*/

client/src/connection/ssh/const.ts

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,10 @@
22
// SPDX-License-Identifier: Apache-2.0
33

44
const SECOND = 1000;
5+
const MINUTE = 60 * SECOND;
6+
const HOUR = 60 * MINUTE;
57
export const KEEPALIVE_INTERVAL = 60 * SECOND; //How often (in milliseconds) to send SSH-level keepalive packets to the server. Set to 0 to disable.
6-
// 720 * 60 seconds = 43200 seconds = 12 hours
7-
export const KEEPALIVE_UNANSWERED_THRESHOLD = 720; //How many consecutive, unanswered SSH-level keepalive packets that can be sent to the server before disconnection.
8+
export const KEEPALIVE_UNANSWERED_THRESHOLD = 12 * HOUR; //How many consecutive, unanswered SSH-level keepalive packets that can be sent to the server before disconnection.
89
export const WORK_DIR_START_TAG = "<WorkDirectory>";
910
export const WORK_DIR_END_TAG = "</WorkDirectory>";
10-
export const SAS_LAUNCH_TIMEOUT = 60000;
11-
export const SUPPORTED_AUTH_METHODS = [
12-
"publickey",
13-
"password",
14-
"keyboard-interactive",
15-
];
11+
export const CONNECT_READY_TIMEOUT = 5 * MINUTE; //allow extra time due to possible prompting

client/src/connection/ssh/index.ts

Lines changed: 41 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,9 @@ import { Session } from "../session";
1818
import { extractOutputHtmlFileName } from "../util";
1919
import { AuthHandler } from "./auth";
2020
import {
21+
CONNECT_READY_TIMEOUT,
2122
KEEPALIVE_INTERVAL,
2223
KEEPALIVE_UNANSWERED_THRESHOLD,
23-
SAS_LAUNCH_TIMEOUT,
24-
SUPPORTED_AUTH_METHODS,
2524
WORK_DIR_END_TAG,
2625
WORK_DIR_START_TAG,
2726
} from "./const";
@@ -51,7 +50,6 @@ export class SSHSession extends Session {
5150
private _reject: ((reason?) => void) | undefined;
5251
private _html5FileName = "";
5352
private _sessionReady: boolean;
54-
private _authMethods: AuthenticationType[]; //auth methods that this session can support
5553
private _authHandler: AuthHandler;
5654
private _workDirectory: string;
5755
private _workDirectoryParser: LineParser;
@@ -60,7 +58,6 @@ export class SSHSession extends Session {
6058
super();
6159
this._config = c;
6260
this._conn = client;
63-
this._authMethods = ["publickey", "password", "keyboard-interactive"];
6461
this._sessionReady = false;
6562
this._authHandler = new AuthHandler();
6663
this._workDirectoryParser = new LineParser(
@@ -92,7 +89,7 @@ export class SSHSession extends Session {
9289
host: this._config.host,
9390
port: this._config.port,
9491
username: this._config.username,
95-
readyTimeout: SAS_LAUNCH_TIMEOUT,
92+
readyTimeout: CONNECT_READY_TIMEOUT,
9693
keepaliveInterval: KEEPALIVE_INTERVAL,
9794
keepaliveCountMax: KEEPALIVE_UNANSWERED_THRESHOLD,
9895

@@ -107,6 +104,7 @@ export class SSHSession extends Session {
107104
}
108105

109106
this._conn
107+
.on("close", this.onConnectionClose)
110108
.on("ready", () => {
111109
this._conn.shell(this.onShell);
112110
})
@@ -136,6 +134,12 @@ export class SSHSession extends Session {
136134
this._stream.close();
137135
};
138136

137+
private onConnectionClose = () => {
138+
if (!this._sessionReady) {
139+
this._reject?.(new Error(l10n.t("Could not connect to the SAS server.")));
140+
}
141+
};
142+
139143
private onConnectionError = (err: Error) => {
140144
this.clearAuthState();
141145
this._reject?.(err);
@@ -206,6 +210,7 @@ export class SSHSession extends Session {
206210

207211
return foundWorkDirectory || "";
208212
};
213+
209214
private resolveSystemVars = (): void => {
210215
const code = `%let workDir = %sysfunc(pathname(work));
211216
%put ${WORK_DIR_START_TAG};
@@ -216,6 +221,7 @@ export class SSHSession extends Session {
216221
`;
217222
this._stream.write(code);
218223
};
224+
219225
private onStreamData = (data: Buffer): void => {
220226
const output = data.toString().trimEnd();
221227

@@ -293,68 +299,55 @@ export class SSHSession extends Session {
293299
* Resets the SSH auth state.
294300
*/
295301
private clearAuthState = (): void => {
296-
this._authMethods = undefined;
297302
this._sessionReady = false;
298303
};
299304

300305
private handleSSHAuthentication: AuthHandlerMiddleware = (
301306
authsLeft: AuthenticationType[],
302307
_partialSuccess: boolean,
303-
cb: NextAuthHandler,
308+
nextAuth: NextAuthHandler,
304309
) => {
305310
if (!authsLeft) {
306-
cb("none"); //sending none will prompt the server to send supported auth methods
311+
nextAuth("none"); //sending none will prompt the server to send supported auth methods
307312
return;
308313
}
309314

310-
if (!this._authMethods) {
311-
this._authMethods = authsLeft;
312-
}
313-
314-
if (this._authMethods.length === 0) {
315-
//if we're out of auth methods to try, then reject with an error
315+
if (authsLeft.length === 0) {
316316
this._reject?.(
317317
new Error(l10n.t("Could not authenticate to the SSH server.")),
318318
);
319-
320319
this.clearAuthState();
321-
//returning false will stop the auth process
322-
return false;
320+
return false; //returning false will stop the authentication process
323321
}
324322

325-
//otherwise, fetch the next auth method to try
326-
const authMethod = this._authMethods.shift();
327-
328-
//make sure the auth method is supported by the server
329-
if (SUPPORTED_AUTH_METHODS.includes(authMethod)) {
330-
switch (authMethod) {
331-
case "publickey": {
332-
//user set a keyfile path in profile config
333-
if (this._config.privateKeyFilePath) {
334-
this._authHandler.privateKeyAuth(
335-
cb,
336-
this._config.privateKeyFilePath,
337-
this._config.username,
338-
);
339-
} else if (process.env.SSH_AUTH_SOCK) {
340-
this._authHandler.sshAgentAuth(cb, this._config.username);
341-
}
342-
break;
343-
}
344-
case "password": {
345-
this._authHandler.passwordAuth(cb, this._config.username);
346-
break;
323+
const authMethod = authsLeft.shift();
324+
switch (authMethod) {
325+
case "publickey": {
326+
//user set a keyfile path in profile config
327+
if (this._config.privateKeyFilePath) {
328+
this._authHandler.privateKeyAuth(
329+
nextAuth,
330+
this._config.privateKeyFilePath,
331+
this._config.username,
332+
);
333+
} else if (process.env.SSH_AUTH_SOCK) {
334+
this._authHandler.sshAgentAuth(nextAuth, this._config.username);
347335
}
348-
case "keyboard-interactive": {
349-
this._authHandler.keyboardInteractiveAuth(cb, this._config.username);
350-
break;
351-
}
352-
default:
353-
cb("none");
336+
break;
354337
}
355-
} else {
356-
console.warn(`Server does not support ${authMethod} auth method.`);
357-
cb("none");
338+
case "password": {
339+
this._authHandler.passwordAuth(nextAuth, this._config.username);
340+
break;
341+
}
342+
case "keyboard-interactive": {
343+
this._authHandler.keyboardInteractiveAuth(
344+
nextAuth,
345+
this._config.username,
346+
);
347+
break;
348+
}
349+
default:
350+
nextAuth(authMethod);
358351
}
359352
};
360353
}

0 commit comments

Comments
 (0)