mirror of
https://github.com/coder/code-server.git
synced 2026-05-07 04:51:59 +02:00
Merge commit 'be3e8236086165e5e45a5a10783823874b3f3ebd' as 'lib/vscode'
This commit is contained in:
241
lib/vscode/src/vs/code/electron-main/auth2.ts
Normal file
241
lib/vscode/src/vs/code/electron-main/auth2.ts
Normal file
@@ -0,0 +1,241 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 { hash } from 'vs/base/common/hash';
|
||||
import { app, AuthInfo, WebContents, Event as ElectronEvent, AuthenticationResponseDetails } 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';
|
||||
import product from 'vs/platform/product/common/product';
|
||||
|
||||
interface ElectronAuthenticationResponseDetails extends AuthenticationResponseDetails {
|
||||
firstAuthAttempt?: boolean; // https://github.com/electron/electron/blob/84a42a050e7d45225e69df5bd2d2bf9f1037ea41/shell/browser/login_handler.cc#L70
|
||||
}
|
||||
|
||||
type LoginEvent = {
|
||||
event: ElectronEvent;
|
||||
authInfo: AuthInfo;
|
||||
req: ElectronAuthenticationResponseDetails;
|
||||
|
||||
callback: (username?: string, password?: string) => void;
|
||||
};
|
||||
|
||||
type Credentials = {
|
||||
username: string;
|
||||
password: string;
|
||||
};
|
||||
|
||||
enum ProxyAuthState {
|
||||
|
||||
/**
|
||||
* Initial state: we will try to use stored credentials
|
||||
* first to reply to the auth challenge.
|
||||
*/
|
||||
Initial = 1,
|
||||
|
||||
/**
|
||||
* We used stored credentials and are still challenged,
|
||||
* so we will show a login dialog next.
|
||||
*/
|
||||
StoredCredentialsUsed,
|
||||
|
||||
/**
|
||||
* Finally, if we showed a login dialog already, we will
|
||||
* not show any more login dialogs until restart to reduce
|
||||
* the UI noise.
|
||||
*/
|
||||
LoginDialogShown
|
||||
}
|
||||
|
||||
export class ProxyAuthHandler2 extends Disposable {
|
||||
|
||||
private static PROXY_CREDENTIALS_SERVICE_KEY = `${product.urlProtocol}.proxy-credentials`;
|
||||
|
||||
private pendingProxyResolve: Promise<Credentials | undefined> | undefined = undefined;
|
||||
|
||||
private state = ProxyAuthState.Initial;
|
||||
|
||||
private sessionCredentials: Credentials | undefined = undefined;
|
||||
|
||||
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: ElectronEvent, webContents: WebContents, req: ElectronAuthenticationResponseDetails, authInfo: AuthInfo, callback) => ({ event, webContents, req, authInfo, callback }));
|
||||
this._register(onLogin(this.onLogin, this));
|
||||
}
|
||||
|
||||
private async onLogin({ event, authInfo, req, callback }: LoginEvent): Promise<void> {
|
||||
if (!authInfo.isProxy) {
|
||||
return; // only for proxy
|
||||
}
|
||||
|
||||
if (!this.pendingProxyResolve && this.state === ProxyAuthState.LoginDialogShown && req.firstAuthAttempt) {
|
||||
this.logService.trace('auth#onLogin (proxy) - exit - proxy dialog already shown');
|
||||
|
||||
return; // only one dialog per session at max (except when firstAuthAttempt: false which indicates a login problem)
|
||||
}
|
||||
|
||||
// Signal we handle this event on our own, otherwise
|
||||
// Electron will ignore our provided credentials.
|
||||
event.preventDefault();
|
||||
|
||||
let credentials: Credentials | undefined = undefined;
|
||||
if (!this.pendingProxyResolve) {
|
||||
this.logService.trace('auth#onLogin (proxy) - no pending proxy handling found, starting new');
|
||||
|
||||
this.pendingProxyResolve = this.resolveProxyCredentials(authInfo);
|
||||
try {
|
||||
credentials = await this.pendingProxyResolve;
|
||||
} finally {
|
||||
this.pendingProxyResolve = undefined;
|
||||
}
|
||||
} else {
|
||||
this.logService.trace('auth#onLogin (proxy) - pending proxy handling found');
|
||||
|
||||
credentials = await this.pendingProxyResolve;
|
||||
}
|
||||
|
||||
// According to Electron docs, it is fine to call back without
|
||||
// username or password to signal that the authentication was handled
|
||||
// by us, even though without having credentials received:
|
||||
//
|
||||
// > If `callback` is called without a username or password, the authentication
|
||||
// > request will be cancelled and the authentication error will be returned to the
|
||||
// > page.
|
||||
callback(credentials?.username, credentials?.password);
|
||||
}
|
||||
|
||||
private async resolveProxyCredentials(authInfo: AuthInfo): Promise<Credentials | undefined> {
|
||||
this.logService.trace('auth#resolveProxyCredentials (proxy) - enter');
|
||||
|
||||
try {
|
||||
const credentials = await this.doResolveProxyCredentials(authInfo);
|
||||
if (credentials) {
|
||||
this.logService.trace('auth#resolveProxyCredentials (proxy) - got credentials');
|
||||
|
||||
return credentials;
|
||||
} else {
|
||||
this.logService.trace('auth#resolveProxyCredentials (proxy) - did not get credentials');
|
||||
}
|
||||
} finally {
|
||||
this.logService.trace('auth#resolveProxyCredentials (proxy) - exit');
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private async doResolveProxyCredentials(authInfo: AuthInfo): Promise<Credentials | undefined> {
|
||||
this.logService.trace('auth#doResolveProxyCredentials - enter', authInfo);
|
||||
|
||||
// Compute a hash over the authentication info to be used
|
||||
// with the credentials store to return the right credentials
|
||||
// given the properties of the auth request
|
||||
// (see https://github.com/microsoft/vscode/issues/109497)
|
||||
const authInfoHash = String(hash({ scheme: authInfo.scheme, host: authInfo.host, port: authInfo.port }));
|
||||
|
||||
// Find any previously stored credentials
|
||||
let storedUsername: string | undefined = undefined;
|
||||
let storedPassword: string | undefined = undefined;
|
||||
try {
|
||||
const encryptedSerializedProxyCredentials = await this.nativeHostMainService.getPassword(undefined, ProxyAuthHandler2.PROXY_CREDENTIALS_SERVICE_KEY, authInfoHash);
|
||||
if (encryptedSerializedProxyCredentials) {
|
||||
const credentials: Credentials = JSON.parse(await this.encryptionMainService.decrypt(encryptedSerializedProxyCredentials));
|
||||
|
||||
storedUsername = credentials.username;
|
||||
storedPassword = 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.state !== ProxyAuthState.StoredCredentialsUsed && typeof storedUsername === 'string' && typeof storedPassword === 'string') {
|
||||
this.logService.trace('auth#doResolveProxyCredentials (proxy) - exit - found stored credentials to use');
|
||||
this.state = ProxyAuthState.StoredCredentialsUsed;
|
||||
|
||||
return { username: storedUsername, password: storedPassword };
|
||||
}
|
||||
|
||||
// Find suitable window to show dialog: prefer to show it in the
|
||||
// active window because any other network request will wait on
|
||||
// the credentials and we want the user to present the dialog.
|
||||
const window = this.windowsMainService.getFocusedWindow() || this.windowsMainService.getLastActiveWindow();
|
||||
if (!window) {
|
||||
this.logService.trace('auth#doResolveProxyCredentials (proxy) - exit - no opened window found to show dialog in');
|
||||
|
||||
return undefined; // unexpected
|
||||
}
|
||||
|
||||
this.logService.trace(`auth#doResolveProxyCredentials (proxy) - asking window ${window.id} to handle proxy login`);
|
||||
|
||||
// Open proxy dialog
|
||||
const payload = {
|
||||
authInfo,
|
||||
username: this.sessionCredentials?.username ?? storedUsername, // prefer to show already used username (if any) over stored
|
||||
password: this.sessionCredentials?.password ?? storedPassword, // prefer to show already used password (if any) over stored
|
||||
replyChannel: `vscode:proxyAuthResponse:${generateUuid()}`
|
||||
};
|
||||
window.sendWhenReady('vscode:openProxyAuthenticationDialog', payload);
|
||||
this.state = ProxyAuthState.LoginDialogShown;
|
||||
|
||||
// Handle reply
|
||||
const loginDialogCredentials = await new Promise<Credentials | undefined>(resolve => {
|
||||
const proxyAuthResponseHandler = async (event: ElectronEvent, channel: string, reply: Credentials & { remember: boolean } | undefined /* canceled */) => {
|
||||
if (channel === payload.replyChannel) {
|
||||
this.logService.trace(`auth#doResolveProxyCredentials - exit - received credentials from window ${window.id}`);
|
||||
window.win.webContents.off('ipc-message', proxyAuthResponseHandler);
|
||||
|
||||
// We got credentials from the window
|
||||
if (reply) {
|
||||
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, authInfoHash, encryptedSerializedCredentials);
|
||||
} else {
|
||||
await this.nativeHostMainService.deletePassword(undefined, ProxyAuthHandler2.PROXY_CREDENTIALS_SERVICE_KEY, authInfoHash);
|
||||
}
|
||||
} catch (error) {
|
||||
this.logService.error(error); // handle gracefully
|
||||
}
|
||||
|
||||
resolve({ username: credentials.username, password: credentials.password });
|
||||
}
|
||||
|
||||
// We did not get any credentials from the window (e.g. cancelled)
|
||||
else {
|
||||
resolve(undefined);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
window.win.webContents.on('ipc-message', proxyAuthResponseHandler);
|
||||
});
|
||||
|
||||
// Remember credentials for the session in case
|
||||
// the credentials are wrong and we show the dialog
|
||||
// again
|
||||
this.sessionCredentials = loginDialogCredentials;
|
||||
|
||||
return loginDialogCredentials;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user