/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { window, InputBoxOptions, Uri, Disposable, workspace, QuickPickOptions, l10n } 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'; 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(); constructor(private ipc?: IIPCServer) { 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: (process.versions['electron'] && process.versions['microsoft-build']) ? '--ms-enable-electron-run-as-node' : '', 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'; 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'); if (!enabled) { 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); 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(request: string, host?: string, file?: string, fingerprint?: string): Promise { // passphrase if (/passphrase/i.test(request)) { const options: InputBoxOptions = { password: true, placeHolder: l10n.t('Passphrase'), prompt: `SSH Key: ${file}`, ignoreFocusOut: true }; return await window.showInputBox(options) || ''; } // authenticity 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(); } }