提交 ce1e0612 编写于 作者: B Benjamin Pasero

proxy auth - implement a way to remember credentials

上级 4d064bc7
......@@ -34,6 +34,7 @@ import { resolveCommonProperties } from 'vs/platform/telemetry/node/commonProper
import { getDelayedChannel, StaticRouter, createChannelReceiver, createChannelSender } from 'vs/base/parts/ipc/common/ipc';
import product from 'vs/platform/product/common/product';
import { ProxyAuthHandler } from 'vs/code/electron-main/auth';
import { ProxyAuthHandler2 } from 'vs/code/electron-main/auth2';
import { Disposable } from 'vs/base/common/lifecycle';
import { IWindowsMainService, ICodeWindow } from 'vs/platform/windows/electron-main/windows';
import { URI } from 'vs/base/common/uri';
......@@ -111,56 +112,6 @@ export class CodeApplication extends Disposable {
// Dispose on shutdown
this.lifecycleMainService.onWillShutdown(() => this.dispose());
// Login handler for proxy support
if (this.configurationService.getValue('window.enableExperimentalProxyLoginDialog') === true) {
let handledProxyLogins = 0;
app.on('login', (event, webContents, req, authInfo, cb) => {
if (!authInfo.isProxy) {
return; // only for proxy
}
this.logService.trace('app#login (proxy) - enter');
if (!this.windowsMainService) {
this.logService.trace('app#login (proxy) - exit - too early to handle');
return; // too early to handle
}
// Find suitable window to show dialog
const window = this.windowsMainService.getWindowByWebContents(webContents) || this.windowsMainService.getLastActiveWindow();
if (!window) {
this.logService.trace('app#login (proxy) - exit - no opened window found');
return; // unexpected
}
// Limit logins to 1 per session to avoid duplicate login prompts
handledProxyLogins++;
if (handledProxyLogins > 1) {
return; // only once
}
// Signal we handle this on our own
event.preventDefault();
// Open proxy dialog
this.logService.trace(`app#login (proxy) - asking window ${window.id} to handle proxy login`);
const payload = { authInfo, replyChannel: `vscode:proxyAuthResponse:${generateUuid()}` };
window.sendWhenReady('vscode:openProxyAuthDialog', payload);
// Handle reply
const proxyAuthResponseHandler = (event: Event, channel: string, credentials: { username: string, password: string }) => {
if (channel === payload.replyChannel) {
this.logService.trace(`app#login (proxy) - exit - received credentials from window ${window.id}`);
webContents.off('ipc-message', proxyAuthResponseHandler);
cb(credentials.username, credentials.password);
}
};
webContents.on('ipc-message', proxyAuthResponseHandler);
});
}
// Contextmenu via IPC support
registerContextMenuListener();
......@@ -440,6 +391,8 @@ export class CodeApplication extends Disposable {
// Setup Auth Handler
if (this.configurationService.getValue('window.enableExperimentalProxyLoginDialog') !== true) {
this._register(new ProxyAuthHandler());
} else {
this._register(appInstantiationService.createInstance(ProxyAuthHandler2));
}
// Open Windows
......
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Disposable } from 'vs/base/common/lifecycle';
import { Event } from 'vs/base/common/event';
import { app, AuthInfo, WebContents, Event as ElectronEvent } from 'electron';
import { ILogService } from 'vs/platform/log/common/log';
import { IWindowsMainService } from 'vs/platform/windows/electron-main/windows';
import { INativeHostMainService } from 'vs/platform/native/electron-main/nativeHostMainService';
import { IEncryptionMainService } from 'vs/platform/encryption/electron-main/encryptionMainService';
import { generateUuid } from 'vs/base/common/uuid';
type LoginEvent = {
event: ElectronEvent;
webContents: WebContents;
authInfo: AuthInfo;
cb: (username: string, password: string) => void;
};
type Credentials = {
username: string;
password: string;
};
export class ProxyAuthHandler2 extends Disposable {
private static PROXY_CREDENTIALS_SERVICE_KEY = 'vscode.proxy-credentials';
private pendingProxyHandler = false;
private proxyDialogShown = false;
private storedProxyCredentialsUsed = false;
constructor(
@ILogService private readonly logService: ILogService,
@IWindowsMainService private readonly windowsMainService: IWindowsMainService,
@INativeHostMainService private readonly nativeHostMainService: INativeHostMainService,
@IEncryptionMainService private readonly encryptionMainService: IEncryptionMainService
) {
super();
this.registerListeners();
}
private registerListeners(): void {
const onLogin = Event.fromNodeEventEmitter<LoginEvent>(app, 'login', (event, webContents, req, authInfo, cb) => ({ event, webContents, req, authInfo, cb }));
this._register(onLogin(this.onLogin, this));
}
private onLogin(event: LoginEvent): void {
if (!event.authInfo.isProxy) {
return; // only for proxy
}
if (this.pendingProxyHandler) {
this.logService.trace('auth#onLogin (proxy) - exit - pending proxy handling found');
return; // never more than once at the same time
}
if (this.proxyDialogShown) {
this.logService.trace('auth#onLogin (proxy) - exit - proxy dialog already shown');
return; // only one dialog per session at max
}
this.handleOnLogin(event);
}
private async handleOnLogin({ event, webContents, authInfo, cb }: LoginEvent): Promise<void> {
this.logService.trace('auth#handleOnLogin (proxy) - enter');
this.pendingProxyHandler = true;
try {
const credentials = await this.resolveProxyCredentials(event, webContents, authInfo);
if (credentials) {
this.logService.trace('auth#handleOnLogin (proxy) - got credentials');
cb(credentials.username, credentials.password);
} else {
this.logService.trace('auth#handleOnLogin (proxy) - did not get credentials');
}
} finally {
this.logService.trace('auth#handleOnLogin (proxy) - exit');
this.pendingProxyHandler = false;
}
}
private async resolveProxyCredentials(event: ElectronEvent, webContents: WebContents, authInfo: AuthInfo): Promise<Credentials | undefined> {
this.logService.trace('auth#resolveProxyCredentials - enter');
// Signal we handle this on our own
event.preventDefault();
// Find any previously stored credentials
let username: string | undefined = undefined;
let password: string | undefined = undefined;
try {
const encryptedSerializedProxyCredentials = await this.nativeHostMainService.getPassword(undefined, ProxyAuthHandler2.PROXY_CREDENTIALS_SERVICE_KEY, 'account');
if (encryptedSerializedProxyCredentials) {
const credentials: Credentials = JSON.parse(await this.encryptionMainService.decrypt(encryptedSerializedProxyCredentials));
if (credentials.username && credentials.password) {
username = credentials.username;
password = credentials.password;
}
}
} catch (error) {
this.logService.error(error); // handle errors by asking user for login via dialog
}
// Reply with stored credentials unless we used them already.
// In that case we need to show a login dialog again because
// they seem invalid.
if (!this.storedProxyCredentialsUsed && username && password) {
this.logService.trace('auth#resolveProxyCredentials (proxy) - exit - found stored credentials to use');
this.storedProxyCredentialsUsed = true;
return { username, password };
}
// Find suitable window to show dialog
const window = this.windowsMainService.getWindowByWebContents(webContents) || this.windowsMainService.getLastActiveWindow();
if (!window) {
this.logService.trace('auth#resolveProxyCredentials (proxy) - exit - no opened window found to show dialog in');
return undefined; // unexpected
}
this.logService.trace(`auth#resolveProxyCredentials (proxy) - asking window ${window.id} to handle proxy login`);
// Open proxy dialog
const payload = { authInfo, username, password, replyChannel: `vscode:proxyAuthResponse:${generateUuid()}` };
window.sendWhenReady('vscode:openProxyAuthenticationDialog', payload);
this.proxyDialogShown = true;
// Handle reply
return new Promise(resolve => {
const proxyAuthResponseHandler = async (event: ElectronEvent, channel: string, reply: Credentials & { remember: boolean }) => {
if (channel === payload.replyChannel) {
this.logService.trace(`auth#resolveProxyCredentials - exit - received credentials from window ${window.id}`);
webContents.off('ipc-message', proxyAuthResponseHandler);
const credentials: Credentials = { username: reply.username, password: reply.password };
// Update stored credentials based on `remember` flag
try {
if (reply.remember) {
const encryptedSerializedCredentials = await this.encryptionMainService.encrypt(JSON.stringify(credentials));
await this.nativeHostMainService.setPassword(undefined, ProxyAuthHandler2.PROXY_CREDENTIALS_SERVICE_KEY, 'account', encryptedSerializedCredentials);
} else {
await this.nativeHostMainService.deletePassword(undefined, ProxyAuthHandler2.PROXY_CREDENTIALS_SERVICE_KEY, 'account');
}
} catch (error) {
this.logService.error(error); // handle gracefully
}
resolve({ username: credentials.username, password: credentials.password });
}
};
webContents.on('ipc-message', proxyAuthResponseHandler);
});
}
}
......@@ -43,7 +43,7 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti
import { MenubarControl } from '../browser/parts/titlebar/menubarControl';
import { ILabelService } from 'vs/platform/label/common/label';
import { IUpdateService } from 'vs/platform/update/common/update';
import { IStorageService } from 'vs/platform/storage/common/storage';
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
import { IPreferencesService } from '../services/preferences/common/preferences';
import { IMenubarData, IMenubarMenu, IMenubarKeybinding, IMenubarMenuItemSubmenu, IMenubarMenuItemAction, MenubarMenuItem } from 'vs/platform/menubar/common/menubar';
import { IMenubarService } from 'vs/platform/menubar/electron-sandbox/menubar';
......@@ -68,6 +68,8 @@ import { AuthInfo } from 'vs/base/parts/sandbox/electron-sandbox/electronTypes';
export class NativeWindow extends Disposable {
private static REMEMBER_PROXY_CREDENTIALS_KEY = 'window.rememberProxyCredentials';
private touchBarMenu: IMenu | undefined;
private readonly touchBarDisposables = this._register(new DisposableStore());
private lastInstalledTouchedBar: ICommandAction[][] | undefined;
......@@ -110,7 +112,8 @@ export class NativeWindow extends Disposable {
@IFilesConfigurationService private readonly filesConfigurationService: IFilesConfigurationService,
@IProductService private readonly productService: IProductService,
@IRemoteAuthorityResolverService private readonly remoteAuthorityResolverService: IRemoteAuthorityResolverService,
@IDialogService private readonly dialogService: IDialogService
@IDialogService private readonly dialogService: IDialogService,
@IStorageService private readonly storageService: IStorageService
) {
super();
......@@ -203,24 +206,40 @@ export class NativeWindow extends Disposable {
});
// Proxy Login Dialog
ipcRenderer.on('vscode:openProxyAuthDialog', async (event: unknown, payload: { authInfo: AuthInfo, replyChannel: string }) => {
ipcRenderer.on('vscode:openProxyAuthenticationDialog', async (event: unknown, payload: { authInfo: AuthInfo, username?: string, password?: string, replyChannel: string }) => {
const rememberCredentials = this.storageService.getBoolean(NativeWindow.REMEMBER_PROXY_CREDENTIALS_KEY, StorageScope.GLOBAL);
const result = await this.dialogService.input('question', nls.localize('proxyAuthRequired', "Proxy Authentication Required"),
[
nls.localize({ key: 'loginButton', comment: ['&& denotes a mnemonic'] }, "&&Log In"),
nls.localize({ key: 'cancelButton', comment: ['&& denotes a mnemonic'] }, "&&Cancel")
],
[
{ placeholder: nls.localize('username', "Username") },
{ placeholder: nls.localize('password', "Password"), type: 'password' }
{ placeholder: nls.localize('username', "Username"), value: payload.username },
{ placeholder: nls.localize('password', "Password"), type: 'password', value: payload.password }
],
{
cancelId: 1,
detail: nls.localize('proxyDetail', "The proxy {0} requires a userame and password.", `${payload.authInfo.host}:${payload.authInfo.port}`)
detail: nls.localize('proxyDetail', "The proxy {0} requires a userame and password.", `${payload.authInfo.host}:${payload.authInfo.port}`),
checkbox: {
label: nls.localize('rememberCredentials', "Remember my credentials"),
checked: rememberCredentials
}
});
if (result.choice !== 1 && result.values) {
ipcRenderer.send(payload.replyChannel, { username: result.values[0], password: result.values[1] });
if (result.choice !== 0 || !result.values) {
return; // dialog canceled
}
// Update state based on checkbox
if (result.checkboxChecked) {
this.storageService.store(NativeWindow.REMEMBER_PROXY_CREDENTIALS_KEY, true, StorageScope.GLOBAL);
} else {
this.storageService.remove(NativeWindow.REMEMBER_PROXY_CREDENTIALS_KEY, StorageScope.GLOBAL);
}
// Reply back to main side with credentials
const [username, password] = result.values;
ipcRenderer.send(payload.replyChannel, { username, password, remember: !!result.checkboxChecked });
});
// Accessibility support changed event
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册