-
Notifications
You must be signed in to change notification settings - Fork 47
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: username and password support for SSH connection type #1126
base: main
Are you sure you want to change the base?
Changes from 11 commits
482f2f2
bda97f1
86840c7
7404025
54d8d66
36a778c
69b6664
cc667a4
38a575a
f0016cb
21284e4
8aa0c23
91a6d0f
0607d64
6ac98e8
a092524
978fdb4
debafaf
e0e26dd
51ce78a
9879746
ea17b75
3e1f267
76a271f
7a2c0b4
430f80b
04444a7
5f9ec57
d3c9cd2
6d28a43
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
// Copyright © 2024, SAS Institute Inc., Cary, NC, USA. All Rights Reserved. | ||
// SPDX-License-Identifier: Apache-2.0 | ||
|
||
export class LineParser { | ||
protected processedLines: string[] = []; | ||
protected capturingLine: boolean = false; | ||
|
||
public constructor( | ||
protected startTag: string, | ||
protected endTag: string, | ||
protected returnNonProcessedLines: boolean, | ||
) {} | ||
|
||
public processLine(line: string): string | undefined { | ||
if (line.includes(this.startTag) || this.capturingLine) { | ||
this.processedLines.push(line); | ||
this.capturingLine = true; | ||
if (line.includes(this.endTag)) { | ||
return this.processedLine(); | ||
} | ||
return; | ||
} | ||
|
||
return this.returnNonProcessedLines ? line : undefined; | ||
} | ||
|
||
protected processedLine(): string { | ||
this.capturingLine = false; | ||
const fullError = this.processedLines | ||
.join("") | ||
.replace(this.startTag, "") | ||
.replace(this.endTag, ""); | ||
this.processedLines = []; | ||
return fullError; | ||
} | ||
|
||
public isCapturingLine(): boolean { | ||
return this.capturingLine; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,171 @@ | ||
import { CancellationTokenSource, l10n, window } from "vscode"; | ||
|
||
import { readFileSync } from "fs"; | ||
import { NextAuthHandler, utils } from "ssh2"; | ||
|
||
let _cancellationSource: CancellationTokenSource | undefined; | ||
|
||
/** | ||
* Authenticate to the server using the password method. | ||
* @param cb ssh2 NextHandler callback instance. This is used to pass the authentication information to the ssh server. | ||
* @param resolve a function that resolves the promise that is waiting for the password | ||
* @param username the user name to use for the connection | ||
*/ | ||
export const passwordAuth = ( | ||
cb: NextAuthHandler, | ||
resolve: ((value?) => void) | undefined, | ||
username: string, | ||
) => { | ||
promptForPassword(resolve).then((pw) => { | ||
cb({ | ||
type: "password", | ||
password: pw, | ||
username: username, | ||
}); | ||
}); | ||
}; | ||
|
||
/** | ||
* Authenticate to the server using the keyboard-interactive method. | ||
* @param cb ssh2 NextHandler callback instance. This is used to pass the authentication information to the ssh server. | ||
* @param resolve a function that resolves the promise that is waiting for authentication | ||
* @param username the user name to use for the connection | ||
*/ | ||
export const keyboardInteractiveAuth = ( | ||
cb: NextAuthHandler, | ||
resolve: ((value?) => void) | undefined, | ||
username: string, | ||
) => { | ||
cb({ | ||
type: "keyboard-interactive", | ||
username: username, | ||
prompt: (_name, _instructions, _instructionsLang, prompts, cb) => { | ||
if (prompts.length === 1 && prompts[0].prompt === "Password:") { | ||
promptForPassword(resolve).then((pw) => { | ||
cb([pw]); | ||
}); | ||
} else { | ||
cb([]); | ||
} | ||
}, | ||
}); | ||
}; | ||
|
||
/** | ||
* Authenticate to the server using the ssh-agent. See the extension README for more information on how to set up the ssh-agent. | ||
* @param cb ssh2 NextHandler callback instance. This is used to pass the authentication information to the ssh server. | ||
* @param username the user name to use for the connection | ||
*/ | ||
export const sshAgentAuth = (cb: NextAuthHandler, username: string) => { | ||
//attempt to auth using ssh-agent | ||
cb({ | ||
type: "agent", | ||
agent: process.env.SSH_AUTH_SOCK, | ||
username: username, | ||
}); | ||
}; | ||
|
||
/** | ||
* Authenticate to the server using a private key file. | ||
* If a private key file is defined in the connection profile, this function will read the file and use it to authenticate to the server. | ||
* If the key is encrypted, the user will be prompted for the passphrase. | ||
* @param cb ssh2 NextHandler callback instance. This is used to pass the authentication information to the ssh server. | ||
* @param resolve a function that resolves the promise that is waiting for authentication | ||
* @param privateKeyFilePath the path to the private key file defined in the connection profile | ||
* @param username the user name to use for the connection | ||
*/ | ||
export const privateKeyAuth = ( | ||
cb: NextAuthHandler, | ||
resolve: ((value?) => void) | undefined, | ||
privateKeyFilePath: string, | ||
username: string, | ||
) => { | ||
let keyContents: Buffer; | ||
try { | ||
keyContents = readFileSync(privateKeyFilePath); | ||
} catch (e) { | ||
l10n.t("Error reading private key file: {filePath}, error: {message}", { | ||
filePath: privateKeyFilePath, | ||
message: e.message, | ||
}); | ||
} | ||
//check for passphrase, prompt if necessary | ||
//and then attempt to auth | ||
const parsedKeyResult = utils.parseKey(keyContents); | ||
const hasParseError = parsedKeyResult instanceof Error; | ||
const passphraseRequired = | ||
hasParseError && | ||
parsedKeyResult.message === | ||
"Encrypted OpenSSH private key detected, but no passphrase given"; | ||
// key is encrypted, prompt for passphrase | ||
if (passphraseRequired) { | ||
promptForPassphrase(resolve).then((passphrase) => { | ||
//parse the keyfile using the passphrase | ||
const reparsedKeyContentsResult = utils.parseKey(keyContents, passphrase); | ||
|
||
if (!(reparsedKeyContentsResult instanceof Error)) { | ||
cb({ | ||
type: "publickey", | ||
key: reparsedKeyContentsResult, | ||
passphrase: passphrase, | ||
username: username, | ||
}); | ||
} | ||
}); | ||
} else { | ||
if (!hasParseError) { | ||
cb({ | ||
type: "publickey", | ||
key: parsedKeyResult, | ||
username: username, | ||
}); | ||
} | ||
} | ||
}; | ||
|
||
/** | ||
* Prompt the user for a passphrase. | ||
* @param resolve a function that resolves the promise that is waiting for authentication | ||
* @returns the passphrase entered by the user | ||
*/ | ||
const promptForPassphrase = async (resolve): Promise<string> => { | ||
const passphrase = await window.showInputBox({ | ||
prompt: l10n.t("Enter the passphrase for the private key."), | ||
password: true, | ||
}); | ||
|
||
// user cancelled password dialog | ||
if (!passphrase) { | ||
resolve?.({}); | ||
} | ||
|
||
return passphrase; | ||
}; | ||
|
||
/** | ||
* Prompt the user for a password. | ||
* @param resolve a function that resolves the promise that is waiting for authentication | ||
* @returns the password entered by the user | ||
*/ | ||
const promptForPassword = async ( | ||
resolve: ((value?) => void) | undefined, | ||
): Promise<string> => { | ||
const source = new CancellationTokenSource(); | ||
_cancellationSource = source; | ||
const pw = await window.showInputBox( | ||
{ | ||
ignoreFocusOut: true, | ||
password: true, | ||
prompt: l10n.t("Enter your password for this connection."), | ||
title: l10n.t("Password Required"), | ||
}, | ||
_cancellationSource.token, | ||
); | ||
|
||
// user cancelled password dialog | ||
if (!pw) { | ||
resolve?.({}); | ||
} | ||
|
||
return pw; | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
// Copyright © 2024, SAS Institute Inc., Cary, NC, USA. All Rights Reserved. | ||
// SPDX-License-Identifier: Apache-2.0 | ||
|
||
const SECOND = 1000; | ||
export const KEEPALIVE_INTERVAL = 60 * SECOND; //How often (in milliseconds) to send SSH-level keepalive packets to the server. Set to 0 to disable. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @chunky any thoughts on going with these values for keepalive and max unanswered? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @smorrisj 60 seconds is a reasonable figure - I think I usually set it to 30, but I have no strong feelings as long as it's well inside 90. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
// 720 * 60 seconds = 43200 seconds = 12 hours | ||
export const KEEPALIVE_UNANSWERED_THRESHOLD = 720; //How many consecutive, unanswered SSH-level keepalive packets that can be sent to the server before disconnection. | ||
export const WORK_DIR_START_TAG = "<WorkDirectory>"; | ||
export const WORK_DIR_END_TAG = "</WorkDirectory>"; | ||
export const SAS_LAUNCH_TIMEOUT = 600000; | ||
export const SUPPORTED_AUTH_METHODS = [ | ||
"publickey", | ||
"password", | ||
"keyboard-interactive", | ||
]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
do we want to remove the
itc/LineParser.ts
in favor of this?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for catching. I had originally promoted LineParser up higher. A merge must have errantly added it back. Fixed in 76a271f.