Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/git/src/askpass.ts
3314 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import { window, InputBoxOptions, Uri, Disposable, workspace, QuickPickOptions, l10n, LogOutputChannel } from 'vscode';
7
import { IDisposable, EmptyDisposable, toDisposable, extractFilePathFromArgs } from './util';
8
import * as path from 'path';
9
import { IIPCHandler, IIPCServer } from './ipc/ipcServer';
10
import { CredentialsProvider, Credentials } from './api/git';
11
import { ITerminalEnvironmentProvider } from './terminal';
12
13
export class Askpass implements IIPCHandler, ITerminalEnvironmentProvider {
14
15
private env: { [key: string]: string };
16
private sshEnv: { [key: string]: string };
17
private disposable: IDisposable = EmptyDisposable;
18
private cache = new Map<string, Credentials>();
19
private credentialsProviders = new Set<CredentialsProvider>();
20
21
readonly featureDescription = 'git auth provider';
22
23
constructor(private ipc: IIPCServer | undefined, private readonly logger: LogOutputChannel) {
24
if (ipc) {
25
this.disposable = ipc.registerHandler('askpass', this);
26
}
27
28
this.env = {
29
// GIT_ASKPASS
30
GIT_ASKPASS: path.join(__dirname, this.ipc ? 'askpass.sh' : 'askpass-empty.sh'),
31
// VSCODE_GIT_ASKPASS
32
VSCODE_GIT_ASKPASS_NODE: process.execPath,
33
VSCODE_GIT_ASKPASS_EXTRA_ARGS: '',
34
VSCODE_GIT_ASKPASS_MAIN: path.join(__dirname, 'askpass-main.js')
35
};
36
37
this.sshEnv = {
38
// SSH_ASKPASS
39
SSH_ASKPASS: path.join(__dirname, this.ipc ? 'ssh-askpass.sh' : 'ssh-askpass-empty.sh'),
40
SSH_ASKPASS_REQUIRE: 'force'
41
};
42
}
43
44
async handle(payload: { askpassType: 'https' | 'ssh'; argv: string[] }): Promise<string> {
45
this.logger.trace(`[Askpass][handle] ${JSON.stringify(payload)}`);
46
47
const config = workspace.getConfiguration('git', null);
48
const enabled = config.get<boolean>('enabled');
49
50
if (!enabled) {
51
this.logger.trace(`[Askpass][handle] Git is disabled`);
52
return '';
53
}
54
55
return payload.askpassType === 'https'
56
? await this.handleAskpass(payload.argv)
57
: await this.handleSSHAskpass(payload.argv);
58
}
59
60
async handleAskpass(argv: string[]): Promise<string> {
61
// HTTPS (username | password)
62
// Username for 'https://github.com':
63
// Password for 'https://github.com':
64
const request = argv[2];
65
const host = argv[4].replace(/^["']+|["':]+$/g, '');
66
67
this.logger.trace(`[Askpass][handleAskpass] request: ${request}, host: ${host}`);
68
69
const uri = Uri.parse(host);
70
const authority = uri.authority.replace(/^.*@/, '');
71
const password = /password/i.test(request);
72
const cached = this.cache.get(authority);
73
74
if (cached && password) {
75
this.cache.delete(authority);
76
return cached.password;
77
}
78
79
if (!password) {
80
for (const credentialsProvider of this.credentialsProviders) {
81
try {
82
const credentials = await credentialsProvider.getCredentials(uri);
83
84
if (credentials) {
85
this.cache.set(authority, credentials);
86
setTimeout(() => this.cache.delete(authority), 60_000);
87
return credentials.username;
88
}
89
} catch { }
90
}
91
}
92
93
const options: InputBoxOptions = {
94
password,
95
placeHolder: request,
96
prompt: `Git: ${host}`,
97
ignoreFocusOut: true
98
};
99
100
return await window.showInputBox(options) || '';
101
}
102
103
async handleSSHAskpass(argv: string[]): Promise<string> {
104
// SSH (passphrase | authenticity)
105
const request = argv[3];
106
107
// passphrase
108
if (/passphrase/i.test(request)) {
109
// Commit signing - Enter passphrase:
110
// Commit signing - Enter passphrase for '/c/Users/<username>/.ssh/id_ed25519':
111
// Git operation - Enter passphrase for key '/c/Users/<username>/.ssh/id_ed25519':
112
let file: string | undefined = undefined;
113
if (argv[5] && !/key/i.test(argv[5])) {
114
file = extractFilePathFromArgs(argv, 5);
115
} else if (argv[6]) {
116
file = extractFilePathFromArgs(argv, 6);
117
}
118
119
this.logger.trace(`[Askpass][handleSSHAskpass] request: ${request}, file: ${file}`);
120
121
const options: InputBoxOptions = {
122
password: true,
123
placeHolder: l10n.t('Passphrase'),
124
prompt: file ? `SSH Key: ${file}` : undefined,
125
ignoreFocusOut: true
126
};
127
128
return await window.showInputBox(options) || '';
129
}
130
131
// authenticity
132
const host = argv[6].replace(/^["']+|["':]+$/g, '');
133
const fingerprint = argv[15];
134
135
this.logger.trace(`[Askpass][handleSSHAskpass] request: ${request}, host: ${host}, fingerprint: ${fingerprint}`);
136
137
const options: QuickPickOptions = {
138
canPickMany: false,
139
ignoreFocusOut: true,
140
placeHolder: l10n.t('Are you sure you want to continue connecting?'),
141
title: l10n.t('"{0}" has fingerprint "{1}"', host ?? '', fingerprint ?? '')
142
};
143
const items = [l10n.t('yes'), l10n.t('no')];
144
return await window.showQuickPick(items, options) ?? '';
145
}
146
147
getEnv(): { [key: string]: string } {
148
const config = workspace.getConfiguration('git');
149
return config.get<boolean>('useIntegratedAskPass') ? { ...this.env, ...this.sshEnv } : {};
150
}
151
152
getTerminalEnv(): { [key: string]: string } {
153
const config = workspace.getConfiguration('git');
154
return config.get<boolean>('useIntegratedAskPass') && config.get<boolean>('terminalAuthentication') ? this.env : {};
155
}
156
157
registerCredentialsProvider(provider: CredentialsProvider): Disposable {
158
this.credentialsProviders.add(provider);
159
return toDisposable(() => this.credentialsProviders.delete(provider));
160
}
161
162
dispose(): void {
163
this.disposable.dispose();
164
}
165
}
166
167