diff --git a/extensions/git/src/askpass-main.ts b/extensions/git/src/askpass-main.ts index f17dc63463f..405122a4a0b 100644 --- a/extensions/git/src/askpass-main.ts +++ b/extensions/git/src/askpass-main.ts @@ -16,24 +16,49 @@ function fatal(err: any): void { } function main(argv: string[]): void { - if (argv.length !== 5) { - return fatal('Wrong number of arguments'); - } - if (!process.env['VSCODE_GIT_ASKPASS_PIPE']) { return fatal('Missing pipe'); } + if (!process.env['VSCODE_GIT_ASKPASS_TYPE']) { + return fatal('Missing type'); + } + + if (process.env['VSCODE_GIT_ASKPASS_TYPE'] !== 'https' && process.env['VSCODE_GIT_ASKPASS_TYPE'] !== 'ssh') { + return fatal(`Invalid type: ${process.env['VSCODE_GIT_ASKPASS_TYPE']}`); + } + if (process.env['VSCODE_GIT_COMMAND'] === 'fetch' && !!process.env['VSCODE_GIT_FETCH_SILENT']) { return fatal('Skip silent fetch commands'); } const output = process.env['VSCODE_GIT_ASKPASS_PIPE'] as string; - const request = argv[2]; - const host = argv[4].replace(/^["']+|["':]+$/g, ''); - const ipcClient = new IPCClient('askpass'); + const askpassType = process.env['VSCODE_GIT_ASKPASS_TYPE'] as 'https' | 'ssh'; - ipcClient.call({ request, host }).then(res => { + // HTTPS (username | password), SSH (passphrase | authenticity) + const request = askpassType === 'https' ? argv[2] : argv[3]; + + let host: string | undefined, + file: string | undefined, + fingerprint: string | undefined; + + if (askpassType === 'https') { + host = argv[4].replace(/^["']+|["':]+$/g, ''); + } + + if (askpassType === 'ssh') { + if (/passphrase/i.test(request)) { + // passphrase + file = argv[6].replace(/^["']+|["':]+$/g, ''); + } else { + // authenticity + host = argv[6].replace(/^["']+|["':]+$/g, ''); + fingerprint = argv[15]; + } + } + + const ipcClient = new IPCClient('askpass'); + ipcClient.call({ askpassType, request, host, file, fingerprint }).then(res => { fs.writeFileSync(output, res + '\n'); setTimeout(() => process.exit(0), 0); }).catch(err => fatal(err)); diff --git a/extensions/git/src/askpass.sh b/extensions/git/src/askpass.sh index 4536224764d..93a08c3896f 100755 --- a/extensions/git/src/askpass.sh +++ b/extensions/git/src/askpass.sh @@ -1,5 +1,5 @@ #!/bin/sh VSCODE_GIT_ASKPASS_PIPE=`mktemp` -ELECTRON_RUN_AS_NODE="1" VSCODE_GIT_ASKPASS_PIPE="$VSCODE_GIT_ASKPASS_PIPE" "$VSCODE_GIT_ASKPASS_NODE" "$VSCODE_GIT_ASKPASS_MAIN" $VSCODE_GIT_ASKPASS_EXTRA_ARGS $* +ELECTRON_RUN_AS_NODE="1" VSCODE_GIT_ASKPASS_PIPE="$VSCODE_GIT_ASKPASS_PIPE" VSCODE_GIT_ASKPASS_TYPE="https" "$VSCODE_GIT_ASKPASS_NODE" "$VSCODE_GIT_ASKPASS_MAIN" $VSCODE_GIT_ASKPASS_EXTRA_ARGS $* cat $VSCODE_GIT_ASKPASS_PIPE rm $VSCODE_GIT_ASKPASS_PIPE diff --git a/extensions/git/src/askpass.ts b/extensions/git/src/askpass.ts index cbf981eea19..6b0be6807dc 100644 --- a/extensions/git/src/askpass.ts +++ b/extensions/git/src/askpass.ts @@ -3,13 +3,16 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { window, InputBoxOptions, Uri, Disposable, workspace } from 'vscode'; +import * as nls from 'vscode-nls'; +import { window, InputBoxOptions, Uri, Disposable, workspace, QuickPickOptions } from 'vscode'; import { IDisposable, EmptyDisposable, toDisposable } from './util'; import * as path from 'path'; import { IIPCHandler, IIPCServer } from './ipc/ipcServer'; import { CredentialsProvider, Credentials } from './api/git'; import { ITerminalEnvironmentProvider } from './terminal'; +const localize = nls.loadMessageBundle(); + export class Askpass implements IIPCHandler, ITerminalEnvironmentProvider { private env: { [key: string]: string }; @@ -23,14 +26,22 @@ export class Askpass implements IIPCHandler, ITerminalEnvironmentProvider { } this.env = { + // GIT_ASKPASS GIT_ASKPASS: path.join(__dirname, this.ipc ? 'askpass.sh' : 'askpass-empty.sh'), + // SSH_ASKPASS + SSH_ASKPASS: path.join(__dirname, this.ipc ? 'ssh-askpass.sh' : 'ssh-askpass-empty.sh'), + SSH_ASKPASS_REQUIRE: 'force', + // VSCODE_GIT_ASKPASS VSCODE_GIT_ASKPASS_NODE: process.execPath, VSCODE_GIT_ASKPASS_EXTRA_ARGS: (process.versions['electron'] && process.versions['microsoft-build']) ? '--ms-enable-electron-run-as-node' : '', VSCODE_GIT_ASKPASS_MAIN: path.join(__dirname, 'askpass-main.js'), }; } - async handle({ request, host }: { request: string; host: string }): Promise { + async handle(payload: + { askpassType: 'https'; request: string; host: string } | + { askpassType: 'ssh'; request: string; host?: string; file?: string; fingerprint?: string } + ): Promise { const config = workspace.getConfiguration('git', null); const enabled = config.get('enabled'); @@ -38,6 +49,16 @@ export class Askpass implements IIPCHandler, ITerminalEnvironmentProvider { return ''; } + // https + if (payload.askpassType === 'https') { + return await this.handleAskpass(payload.request, payload.host); + } + + // ssh + return await this.handleSSHAskpass(payload.request, payload.host, payload.file, payload.fingerprint); + } + + async handleAskpass(request: string, host: string): Promise { const uri = Uri.parse(host); const authority = uri.authority.replace(/^.*@/, ''); const password = /password/i.test(request); @@ -72,6 +93,33 @@ export class Askpass implements IIPCHandler, ITerminalEnvironmentProvider { return await window.showInputBox(options) || ''; } + async handleSSHAskpass(request: string, host?: string, file?: string, fingerprint?: string): Promise { + // passphrase + if (/passphrase/i.test(request)) { + const options: InputBoxOptions = { + password: true, + placeHolder: localize('ssh passphrase', "Passphrase"), + prompt: `SSH Key: ${file}`, + ignoreFocusOut: true + }; + + return await window.showInputBox(options) || ''; + } + + // authenticity + const options: QuickPickOptions = { + canPickMany: false, + ignoreFocusOut: true, + placeHolder: localize('ssh authenticity prompt', "Are you sure you want to continue connecting?"), + title: localize('ssh authenticity title', "\"{0}\" has fingerprint \"{1}\"", host, fingerprint) + }; + const items = [ + localize('ssh authenticity prompt yes', "yes"), + localize('ssh authenticity prompt no', "no") + ]; + return await window.showQuickPick(items, options) ?? ''; + } + getEnv(): { [key: string]: string } { const config = workspace.getConfiguration('git'); return config.get('useIntegratedAskPass') ? this.env : {}; diff --git a/extensions/git/src/ssh-askpass-empty.sh b/extensions/git/src/ssh-askpass-empty.sh new file mode 100755 index 00000000000..8fb014e5cc9 --- /dev/null +++ b/extensions/git/src/ssh-askpass-empty.sh @@ -0,0 +1,2 @@ +#!/bin/sh +echo '' \ No newline at end of file diff --git a/extensions/git/src/ssh-askpass.sh b/extensions/git/src/ssh-askpass.sh new file mode 100755 index 00000000000..dca45bc8404 --- /dev/null +++ b/extensions/git/src/ssh-askpass.sh @@ -0,0 +1,5 @@ +#!/bin/sh +VSCODE_GIT_ASKPASS_PIPE=`mktemp` +ELECTRON_RUN_AS_NODE="1" VSCODE_GIT_ASKPASS_PIPE="$VSCODE_GIT_ASKPASS_PIPE" VSCODE_GIT_ASKPASS_TYPE="ssh" "$VSCODE_GIT_ASKPASS_NODE" "$VSCODE_GIT_ASKPASS_MAIN" $VSCODE_GIT_ASKPASS_EXTRA_ARGS $* +cat $VSCODE_GIT_ASKPASS_PIPE +rm $VSCODE_GIT_ASKPASS_PIPE