/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { window, InputBoxOptions, Uri, Disposable, workspace, QuickPickOptions, l10n, LogOutputChannel } from 'vscode'; import { IDisposable, EmptyDisposable, toDisposable, extractFilePathFromArgs } from './util'; import * as path from 'path'; import { IIPCHandler, IIPCServer } from './ipc/ipcServer'; import { CredentialsProvider, Credentials } from './api/git'; import { ITerminalEnvironmentProvider } from './terminal'; export class Askpass implements IIPCHandler, ITerminalEnvironmentProvider { private env: { [key: string]: string }; private sshEnv: { [key: string]: string }; private disposable: IDisposable = EmptyDisposable; private cache = new Map(); private credentialsProviders = new Set(); readonly featureDescription = 'git auth provider'; constructor(private ipc: IIPCServer | undefined, private readonly logger: LogOutputChannel) { if (ipc) { this.disposable = ipc.registerHandler('askpass', this); } this.env = { // GIT_ASKPASS GIT_ASKPASS: path.join(__dirname, this.ipc ? 'askpass.sh' : 'askpass-empty.sh'), // VSCODE_GIT_ASKPASS VSCODE_GIT_ASKPASS_NODE: process.execPath, VSCODE_GIT_ASKPASS_EXTRA_ARGS: '', VSCODE_GIT_ASKPASS_MAIN: path.join(__dirname, 'askpass-main.js') }; this.sshEnv = { // SSH_ASKPASS SSH_ASKPASS: path.join(__dirname, this.ipc ? 'ssh-askpass.sh' : 'ssh-askpass-empty.sh'), SSH_ASKPASS_REQUIRE: 'force' }; } async handle(payload: { askpassType: 'https' | 'ssh'; argv: string[] }): Promise { this.logger.trace(`[Askpass][handle] ${JSON.stringify(payload)}`); const config = workspace.getConfiguration('git', null); const enabled = config.get('enabled'); if (!enabled) { this.logger.trace(`[Askpass][handle] Git is disabled`); return ''; } return payload.askpassType === 'https' ? await this.handleAskpass(payload.argv) : await this.handleSSHAskpass(payload.argv); } async handleAskpass(argv: string[]): Promise { // HTTPS (username | password) // Username for 'https://github.com': // Password for 'https://github.com': const request = argv[2]; const host = argv[4].replace(/^["']+|["':]+$/g, ''); this.logger.trace(`[Askpass][handleAskpass] request: ${request}, host: ${host}`); const uri = Uri.parse(host); const authority = uri.authority.replace(/^.*@/, ''); const password = /password/i.test(request); const cached = this.cache.get(authority); if (cached && password) { this.cache.delete(authority); return cached.password; } if (!password) { for (const credentialsProvider of this.credentialsProviders) { try { const credentials = await credentialsProvider.getCredentials(uri); if (credentials) { this.cache.set(authority, credentials); setTimeout(() => this.cache.delete(authority), 60_000); return credentials.username; } } catch { } } } const options: InputBoxOptions = { password, placeHolder: request, prompt: `Git: ${host}`, ignoreFocusOut: true }; return await window.showInputBox(options) || ''; } async handleSSHAskpass(argv: string[]): Promise { // SSH (passphrase | authenticity) const request = argv[3]; // passphrase if (/passphrase/i.test(request)) { // Commit signing - Enter passphrase: // Commit signing - Enter passphrase for '/c/Users//.ssh/id_ed25519': // Git operation - Enter passphrase for key '/c/Users//.ssh/id_ed25519': let file: string | undefined = undefined; if (argv[5] && !/key/i.test(argv[5])) { file = extractFilePathFromArgs(argv, 5); } else if (argv[6]) { file = extractFilePathFromArgs(argv, 6); } this.logger.trace(`[Askpass][handleSSHAskpass] request: ${request}, file: ${file}`); const options: InputBoxOptions = { password: true, placeHolder: l10n.t('Passphrase'), prompt: file ? `SSH Key: ${file}` : undefined, ignoreFocusOut: true }; return await window.showInputBox(options) || ''; } // authenticity const host = argv[6].replace(/^["']+|["':]+$/g, ''); const fingerprint = argv[15]; this.logger.trace(`[Askpass][handleSSHAskpass] request: ${request}, host: ${host}, fingerprint: ${fingerprint}`); const options: QuickPickOptions = { canPickMany: false, ignoreFocusOut: true, placeHolder: l10n.t('Are you sure you want to continue connecting?'), title: l10n.t('"{0}" has fingerprint "{1}"', host ?? '', fingerprint ?? '') }; const items = [l10n.t('yes'), l10n.t('no')]; return await window.showQuickPick(items, options) ?? ''; } getEnv(): { [key: string]: string } { const config = workspace.getConfiguration('git'); return config.get('useIntegratedAskPass') ? { ...this.env, ...this.sshEnv } : {}; } getTerminalEnv(): { [key: string]: string } { const config = workspace.getConfiguration('git'); return config.get('useIntegratedAskPass') && config.get('terminalAuthentication') ? this.env : {}; } registerCredentialsProvider(provider: CredentialsProvider): Disposable { this.credentialsProviders.add(provider); return toDisposable(() => this.credentialsProviders.delete(provider)); } dispose(): void { this.disposable.dispose(); } }