mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-01-14 01:25:37 -05:00
Merge vscode 1.67 (#20883)
* Fix initial build breaks from 1.67 merge (#2514) * Update yarn lock files * Update build scripts * Fix tsconfig * Build breaks * WIP * Update yarn lock files * Misc breaks * Updates to package.json * Breaks * Update yarn * Fix breaks * Breaks * Build breaks * Breaks * Breaks * Breaks * Breaks * Breaks * Missing file * Breaks * Breaks * Breaks * Breaks * Breaks * Fix several runtime breaks (#2515) * Missing files * Runtime breaks * Fix proxy ordering issue * Remove commented code * Fix breaks with opening query editor * Fix post merge break * Updates related to setup build and other breaks (#2516) * Fix bundle build issues * Update distro * Fix distro merge and update build JS files * Disable pipeline steps * Remove stats call * Update license name * Make new RPM dependencies a warning * Fix extension manager version checks * Update JS file * Fix a few runtime breaks * Fixes * Fix runtime issues * Fix build breaks * Update notebook tests (part 1) * Fix broken tests * Linting errors * Fix hygiene * Disable lint rules * Bump distro * Turn off smoke tests * Disable integration tests * Remove failing "activate" test * Remove failed test assertion * Disable other broken test * Disable query history tests * Disable extension unit tests * Disable failing tasks
This commit is contained in:
198
extensions/github-authentication/src/authServer.ts
Normal file
198
extensions/github-authentication/src/authServer.ts
Normal file
@@ -0,0 +1,198 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
import * as http from 'http';
|
||||
import { URL } from 'url';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { randomBytes } from 'crypto';
|
||||
|
||||
function sendFile(res: http.ServerResponse, filepath: string) {
|
||||
fs.readFile(filepath, (err, body) => {
|
||||
if (err) {
|
||||
console.error(err);
|
||||
res.writeHead(404);
|
||||
res.end();
|
||||
} else {
|
||||
res.writeHead(200, {
|
||||
'content-length': body.length,
|
||||
});
|
||||
res.end(body);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
interface IOAuthResult {
|
||||
code: string;
|
||||
state: string;
|
||||
}
|
||||
|
||||
interface ILoopbackServer {
|
||||
/**
|
||||
* If undefined, the server is not started yet.
|
||||
*/
|
||||
port: number | undefined;
|
||||
|
||||
/**
|
||||
* The nonce used
|
||||
*/
|
||||
nonce: string;
|
||||
|
||||
/**
|
||||
* The state parameter used in the OAuth flow.
|
||||
*/
|
||||
state: string | undefined;
|
||||
|
||||
/**
|
||||
* Starts the server.
|
||||
* @returns The port to listen on.
|
||||
* @throws If the server fails to start.
|
||||
* @throws If the server is already started.
|
||||
*/
|
||||
start(): Promise<number>;
|
||||
/**
|
||||
* Stops the server.
|
||||
* @throws If the server is not started.
|
||||
* @throws If the server fails to stop.
|
||||
*/
|
||||
stop(): Promise<void>;
|
||||
/**
|
||||
* Returns a promise that resolves to the result of the OAuth flow.
|
||||
*/
|
||||
waitForOAuthResponse(): Promise<IOAuthResult>;
|
||||
}
|
||||
|
||||
export class LoopbackAuthServer implements ILoopbackServer {
|
||||
private readonly _server: http.Server;
|
||||
private readonly _resultPromise: Promise<IOAuthResult>;
|
||||
private _startingRedirect: URL;
|
||||
|
||||
public nonce = randomBytes(16).toString('base64');
|
||||
public port: number | undefined;
|
||||
|
||||
public set state(state: string | undefined) {
|
||||
if (state) {
|
||||
this._startingRedirect.searchParams.set('state', state);
|
||||
} else {
|
||||
this._startingRedirect.searchParams.delete('state');
|
||||
}
|
||||
}
|
||||
public get state(): string | undefined {
|
||||
return this._startingRedirect.searchParams.get('state') ?? undefined;
|
||||
}
|
||||
|
||||
constructor(serveRoot: string, startingRedirect: string) {
|
||||
if (!serveRoot) {
|
||||
throw new Error('serveRoot must be defined');
|
||||
}
|
||||
if (!startingRedirect) {
|
||||
throw new Error('startingRedirect must be defined');
|
||||
}
|
||||
this._startingRedirect = new URL(startingRedirect);
|
||||
let deferred: { resolve: (result: IOAuthResult) => void; reject: (reason: any) => void };
|
||||
this._resultPromise = new Promise<IOAuthResult>((resolve, reject) => deferred = { resolve, reject });
|
||||
|
||||
this._server = http.createServer((req, res) => {
|
||||
const reqUrl = new URL(req.url!, `http://${req.headers.host}`);
|
||||
switch (reqUrl.pathname) {
|
||||
case '/signin': {
|
||||
const receivedNonce = (reqUrl.searchParams.get('nonce') ?? '').replace(/ /g, '+');
|
||||
if (receivedNonce !== this.nonce) {
|
||||
res.writeHead(302, { location: `/?error=${encodeURIComponent('Nonce does not match.')}` });
|
||||
res.end();
|
||||
}
|
||||
res.writeHead(302, { location: this._startingRedirect.toString() });
|
||||
res.end();
|
||||
break;
|
||||
}
|
||||
case '/callback': {
|
||||
const code = reqUrl.searchParams.get('code') ?? undefined;
|
||||
const state = reqUrl.searchParams.get('state') ?? undefined;
|
||||
const nonce = (reqUrl.searchParams.get('nonce') ?? '').replace(/ /g, '+');
|
||||
if (!code || !state || !nonce) {
|
||||
res.writeHead(400);
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
if (this.state !== state) {
|
||||
res.writeHead(302, { location: `/?error=${encodeURIComponent('State does not match.')}` });
|
||||
res.end();
|
||||
throw new Error('State does not match.');
|
||||
}
|
||||
if (this.nonce !== nonce) {
|
||||
res.writeHead(302, { location: `/?error=${encodeURIComponent('Nonce does not match.')}` });
|
||||
res.end();
|
||||
throw new Error('Nonce does not match.');
|
||||
}
|
||||
deferred.resolve({ code, state });
|
||||
res.writeHead(302, { location: '/' });
|
||||
res.end();
|
||||
break;
|
||||
}
|
||||
// Serve the static files
|
||||
case '/':
|
||||
sendFile(res, path.join(serveRoot, 'index.html'));
|
||||
break;
|
||||
default:
|
||||
// substring to get rid of leading '/'
|
||||
sendFile(res, path.join(serveRoot, reqUrl.pathname.substring(1)));
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public start(): Promise<number> {
|
||||
return new Promise<number>((resolve, reject) => {
|
||||
if (this._server.listening) {
|
||||
throw new Error('Server is already started');
|
||||
}
|
||||
const portTimeout = setTimeout(() => {
|
||||
reject(new Error('Timeout waiting for port'));
|
||||
}, 5000);
|
||||
this._server.on('listening', () => {
|
||||
const address = this._server.address();
|
||||
if (typeof address === 'string') {
|
||||
this.port = parseInt(address);
|
||||
} else if (address instanceof Object) {
|
||||
this.port = address.port;
|
||||
} else {
|
||||
throw new Error('Unable to determine port');
|
||||
}
|
||||
|
||||
clearTimeout(portTimeout);
|
||||
|
||||
// set state which will be used to redirect back to vscode
|
||||
this.state = `http://127.0.0.1:${this.port}/callback?nonce=${encodeURIComponent(this.nonce)}`;
|
||||
|
||||
resolve(this.port);
|
||||
});
|
||||
this._server.on('error', err => {
|
||||
reject(new Error(`Error listening to server: ${err}`));
|
||||
});
|
||||
this._server.on('close', () => {
|
||||
reject(new Error('Closed'));
|
||||
});
|
||||
this._server.listen(0, '127.0.0.1');
|
||||
});
|
||||
}
|
||||
|
||||
public stop(): Promise<void> {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
if (!this._server.listening) {
|
||||
throw new Error('Server is not started');
|
||||
}
|
||||
this._server.close((err) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public waitForOAuthResponse(): Promise<IOAuthResult> {
|
||||
return this._resultPromise;
|
||||
}
|
||||
}
|
||||
26
extensions/github-authentication/src/common/env.ts
Normal file
26
extensions/github-authentication/src/common/env.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
import { Uri } from 'vscode';
|
||||
|
||||
const VALID_DESKTOP_CALLBACK_SCHEMES = [
|
||||
'vscode',
|
||||
'vscode-insiders',
|
||||
// On Windows, some browsers don't seem to redirect back to OSS properly.
|
||||
// As a result, you get stuck in the auth flow. We exclude this from the
|
||||
// list until we can figure out a way to fix this behavior in browsers.
|
||||
// 'code-oss',
|
||||
'vscode-wsl',
|
||||
'vscode-exploration'
|
||||
];
|
||||
|
||||
export function isSupportedEnvironment(uri: Uri): boolean {
|
||||
return (
|
||||
VALID_DESKTOP_CALLBACK_SCHEMES.includes(uri.scheme) ||
|
||||
// vscode.dev & insiders.vscode.dev
|
||||
/(?:^|\.)vscode\.dev$/.test(uri.authority) ||
|
||||
// github.dev & codespaces
|
||||
/(?:^|\.)github\.dev$/.test(uri.authority)
|
||||
);
|
||||
}
|
||||
@@ -6,11 +6,8 @@
|
||||
// keytar depends on a native module shipped in vscode, so this is
|
||||
// how we load it
|
||||
import * as vscode from 'vscode';
|
||||
import * as nls from 'vscode-nls';
|
||||
import { Log } from './logger';
|
||||
|
||||
const localize = nls.loadMessageBundle();
|
||||
|
||||
export class Keychain {
|
||||
constructor(
|
||||
private readonly context: vscode.ExtensionContext,
|
||||
@@ -24,11 +21,6 @@ export class Keychain {
|
||||
} catch (e) {
|
||||
// Ignore
|
||||
this.Logger.error(`Setting token failed: ${e}`);
|
||||
const troubleshooting = localize('troubleshooting', "Troubleshooting Guide");
|
||||
const result = await vscode.window.showErrorMessage(localize('keychainWriteError', "Writing login information to the keychain failed with error '{0}'.", e.message), troubleshooting);
|
||||
if (result === troubleshooting) {
|
||||
vscode.env.openExternal(vscode.Uri.parse('https://code.visualstudio.com/docs/editor/settings-sync#_troubleshooting-keychain-issues'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -49,12 +49,12 @@ const passthrough = (value: any, resolve: (value?: any) => void) => resolve(valu
|
||||
*/
|
||||
export function promiseFromEvent<T, U>(
|
||||
event: Event<T>,
|
||||
adapter: PromiseAdapter<T, U> = passthrough): { promise: Promise<U>, cancel: EventEmitter<void> } {
|
||||
adapter: PromiseAdapter<T, U> = passthrough): { promise: Promise<U>; cancel: EventEmitter<void> } {
|
||||
let subscription: Disposable;
|
||||
let cancel = new EventEmitter<void>();
|
||||
return {
|
||||
promise: new Promise<U>((resolve, reject) => {
|
||||
cancel.event(_ => reject());
|
||||
cancel.event(_ => reject('Cancelled'));
|
||||
subscription = event((value: T) => {
|
||||
try {
|
||||
Promise.resolve(adapter(value, resolve, reject))
|
||||
|
||||
@@ -3,5 +3,10 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
/// <reference path='../../../../src/vs/vscode.d.ts'/>
|
||||
/// <reference path='../../../../src/vs/vscode.proposed.d.ts'/>
|
||||
export function startServer(_: any): any {
|
||||
throw new Error('Not implemented');
|
||||
}
|
||||
|
||||
export function createServer(_: any): any {
|
||||
throw new Error('Not implemented');
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import TelemetryReporter from 'vscode-extension-telemetry';
|
||||
import TelemetryReporter from '@vscode/extension-telemetry';
|
||||
import { getExperimentationService, IExperimentationService, IExperimentationTelemetry, TargetPopulation } from 'vscode-tas-client';
|
||||
|
||||
export class ExperimentationTelemetry implements IExperimentationTelemetry {
|
||||
|
||||
@@ -9,7 +9,7 @@ import { Keychain } from './common/keychain';
|
||||
import { GitHubEnterpriseServer, GitHubServer, IGitHubServer } from './githubServer';
|
||||
import { arrayEquals } from './common/utils';
|
||||
import { ExperimentationTelemetry } from './experimentationService';
|
||||
import TelemetryReporter from 'vscode-extension-telemetry';
|
||||
import TelemetryReporter from '@vscode/extension-telemetry';
|
||||
import { Log } from './common/logger';
|
||||
|
||||
interface SessionData {
|
||||
@@ -18,7 +18,7 @@ interface SessionData {
|
||||
label?: string;
|
||||
displayName?: string;
|
||||
id: string;
|
||||
}
|
||||
};
|
||||
scopes: string[];
|
||||
accessToken: string;
|
||||
}
|
||||
@@ -36,20 +36,29 @@ export class GitHubAuthenticationProvider implements vscode.AuthenticationProvid
|
||||
|
||||
private _keychain: Keychain = new Keychain(this.context, `${this.type}.auth`, this._logger);
|
||||
private _sessionsPromise: Promise<vscode.AuthenticationSession[]>;
|
||||
private _accountsSeen = new Set<string>();
|
||||
private _disposable: vscode.Disposable;
|
||||
|
||||
constructor(private readonly context: vscode.ExtensionContext, private readonly type: AuthProviderType) {
|
||||
const { name, version, aiKey } = context.extension.packageJSON as { name: string, version: string, aiKey: string };
|
||||
const { name, version, aiKey } = context.extension.packageJSON as { name: string; version: string; aiKey: string };
|
||||
this._telemetryReporter = new ExperimentationTelemetry(context, new TelemetryReporter(name, version, aiKey));
|
||||
|
||||
if (this.type === AuthProviderType.github) {
|
||||
this._githubServer = new GitHubServer(this._logger, this._telemetryReporter);
|
||||
this._githubServer = new GitHubServer(
|
||||
// We only can use the Device Code flow when we have a full node environment because of CORS.
|
||||
context.extension.extensionKind === vscode.ExtensionKind.Workspace || vscode.env.uiKind === vscode.UIKind.Desktop,
|
||||
this._logger,
|
||||
this._telemetryReporter);
|
||||
} else {
|
||||
this._githubServer = new GitHubEnterpriseServer(this._logger, this._telemetryReporter);
|
||||
}
|
||||
|
||||
// Contains the current state of the sessions we have available.
|
||||
this._sessionsPromise = this.readSessions();
|
||||
this._sessionsPromise = this.readSessions().then((sessions) => {
|
||||
// fire telemetry after a second to allow the workbench to focus on loading
|
||||
setTimeout(() => sessions.forEach(s => this.afterSessionLoad(s)), 1000);
|
||||
return sessions;
|
||||
});
|
||||
|
||||
this._disposable = vscode.Disposable.from(
|
||||
this._telemetryReporter,
|
||||
@@ -68,18 +77,24 @@ export class GitHubAuthenticationProvider implements vscode.AuthenticationProvid
|
||||
}
|
||||
|
||||
async getSessions(scopes?: string[]): Promise<vscode.AuthenticationSession[]> {
|
||||
this._logger.info(`Getting sessions for ${scopes?.join(',') || 'all scopes'}...`);
|
||||
// For GitHub scope list, order doesn't matter so we immediately sort the scopes
|
||||
const sortedScopes = scopes?.sort() || [];
|
||||
this._logger.info(`Getting sessions for ${sortedScopes.length ? sortedScopes.join(',') : 'all scopes'}...`);
|
||||
const sessions = await this._sessionsPromise;
|
||||
const finalSessions = scopes
|
||||
? sessions.filter(session => arrayEquals([...session.scopes].sort(), scopes.sort()))
|
||||
const finalSessions = sortedScopes.length
|
||||
? sessions.filter(session => arrayEquals([...session.scopes].sort(), sortedScopes))
|
||||
: sessions;
|
||||
|
||||
this._logger.info(`Got ${finalSessions.length} sessions for ${scopes?.join(',') || 'all scopes'}...`);
|
||||
this._logger.info(`Got ${finalSessions.length} sessions for ${sortedScopes?.join(',') ?? 'all scopes'}...`);
|
||||
return finalSessions;
|
||||
}
|
||||
|
||||
private async afterTokenLoad(token: string): Promise<void> {
|
||||
this._githubServer.sendAdditionalTelemetryInfo(token);
|
||||
private async afterSessionLoad(session: vscode.AuthenticationSession): Promise<void> {
|
||||
// We only want to fire a telemetry if we haven't seen this account yet in this session.
|
||||
if (!this._accountsSeen.has(session.account.id)) {
|
||||
this._accountsSeen.add(session.account.id);
|
||||
this._githubServer.sendAdditionalTelemetryInfo(session.accessToken);
|
||||
}
|
||||
}
|
||||
|
||||
private async checkForUpdates() {
|
||||
@@ -134,12 +149,20 @@ export class GitHubAuthenticationProvider implements vscode.AuthenticationProvid
|
||||
return [];
|
||||
}
|
||||
|
||||
// TODO: eventually remove this Set because we should only have one session per set of scopes.
|
||||
const scopesSeen = new Set<string>();
|
||||
const sessionPromises = sessionData.map(async (session: SessionData) => {
|
||||
let userInfo: { id: string, accountName: string } | undefined;
|
||||
// For GitHub scope list, order doesn't matter so we immediately sort the scopes
|
||||
const sortedScopes = session.scopes.sort();
|
||||
const scopesStr = sortedScopes.join(' ');
|
||||
if (scopesSeen.has(scopesStr)) {
|
||||
return undefined;
|
||||
}
|
||||
let userInfo: { id: string; accountName: string } | undefined;
|
||||
if (!session.account) {
|
||||
try {
|
||||
userInfo = await this._githubServer.getUserInfo(session.accessToken);
|
||||
this._logger.info(`Verified session with the following scopes: ${session.scopes}`);
|
||||
this._logger.info(`Verified session with the following scopes: ${scopesStr}`);
|
||||
} catch (e) {
|
||||
// Remove sessions that return unauthorized response
|
||||
if (e.message === 'Unauthorized') {
|
||||
@@ -148,9 +171,8 @@ export class GitHubAuthenticationProvider implements vscode.AuthenticationProvid
|
||||
}
|
||||
}
|
||||
|
||||
setTimeout(() => this.afterTokenLoad(session.accessToken), 1000);
|
||||
|
||||
this._logger.trace(`Read the following session from the keychain with the following scopes: ${session.scopes}`);
|
||||
this._logger.trace(`Read the following session from the keychain with the following scopes: ${scopesStr}`);
|
||||
scopesSeen.add(scopesStr);
|
||||
return {
|
||||
id: session.id,
|
||||
account: {
|
||||
@@ -159,7 +181,7 @@ export class GitHubAuthenticationProvider implements vscode.AuthenticationProvid
|
||||
: userInfo?.accountName ?? '<unknown>',
|
||||
id: session.account?.id ?? userInfo?.id ?? '<unknown>'
|
||||
},
|
||||
scopes: session.scopes,
|
||||
scopes: sortedScopes,
|
||||
accessToken: session.accessToken
|
||||
};
|
||||
});
|
||||
@@ -186,22 +208,26 @@ export class GitHubAuthenticationProvider implements vscode.AuthenticationProvid
|
||||
|
||||
public async createSession(scopes: string[]): Promise<vscode.AuthenticationSession> {
|
||||
try {
|
||||
// For GitHub scope list, order doesn't matter so we immediately sort the scopes
|
||||
const sortedScopes = scopes.sort();
|
||||
|
||||
/* __GDPR__
|
||||
"login" : {
|
||||
"scopes": { "classification": "PublicNonPersonalData", "purpose": "FeatureInsight" }
|
||||
}
|
||||
*/
|
||||
this._telemetryReporter?.sendTelemetryEvent('login', {
|
||||
scopes: JSON.stringify(scopes),
|
||||
scopes: JSON.stringify(sortedScopes),
|
||||
});
|
||||
|
||||
const scopeString = scopes.join(' ');
|
||||
|
||||
const scopeString = sortedScopes.join(' ');
|
||||
const token = await this._githubServer.login(scopeString);
|
||||
this.afterTokenLoad(token);
|
||||
const session = await this.tokenToSession(token, scopes);
|
||||
const session = await this.tokenToSession(token, sortedScopes);
|
||||
this.afterSessionLoad(session);
|
||||
|
||||
const sessions = await this._sessionsPromise;
|
||||
const sessionIndex = sessions.findIndex(s => s.id === session.id || s.scopes.join(' ') === scopeString);
|
||||
const sessionIndex = sessions.findIndex(s => s.id === session.id || arrayEquals([...s.scopes].sort(), sortedScopes));
|
||||
if (sessionIndex > -1) {
|
||||
sessions.splice(sessionIndex, 1, session);
|
||||
} else {
|
||||
@@ -216,7 +242,7 @@ export class GitHubAuthenticationProvider implements vscode.AuthenticationProvid
|
||||
return session;
|
||||
} catch (e) {
|
||||
// If login was cancelled, do not notify user.
|
||||
if (e === 'Cancelled') {
|
||||
if (e === 'Cancelled' || e.message === 'Cancelled') {
|
||||
/* __GDPR__
|
||||
"loginCancelled" : { }
|
||||
*/
|
||||
|
||||
@@ -11,12 +11,19 @@ import { PromiseAdapter, promiseFromEvent } from './common/utils';
|
||||
import { ExperimentationTelemetry } from './experimentationService';
|
||||
import { AuthProviderType } from './github';
|
||||
import { Log } from './common/logger';
|
||||
import { isSupportedEnvironment } from './common/env';
|
||||
import { LoopbackAuthServer } from './authServer';
|
||||
import path = require('path');
|
||||
|
||||
const localize = nls.loadMessageBundle();
|
||||
|
||||
const CLIENT_ID = '01ab8ac9400c4e429b23';
|
||||
const GITHUB_AUTHORIZE_URL = 'https://github.com/login/oauth/authorize';
|
||||
// TODO: change to stable when that happens
|
||||
const GITHUB_TOKEN_URL = 'https://vscode.dev/codeExchangeProxyEndpoints/github/login/oauth/access_token';
|
||||
const NETWORK_ERROR = 'network error';
|
||||
const AUTH_RELAY_SERVER = 'vscode-auth.github.com';
|
||||
// const AUTH_RELAY_STAGING_SERVER = 'client-auth-staging-14a768b.herokuapp.com';
|
||||
|
||||
const REDIRECT_URL_STABLE = 'https://vscode.dev/redirect';
|
||||
const REDIRECT_URL_INSIDERS = 'https://insiders.vscode.dev/redirect';
|
||||
|
||||
class UriEventHandler extends vscode.EventEmitter<vscode.Uri> implements vscode.UriHandler {
|
||||
constructor(private readonly Logger: Log) {
|
||||
@@ -29,22 +36,21 @@ class UriEventHandler extends vscode.EventEmitter<vscode.Uri> implements vscode.
|
||||
}
|
||||
}
|
||||
|
||||
function parseQuery(uri: vscode.Uri) {
|
||||
return uri.query.split('&').reduce((prev: any, current) => {
|
||||
const queryString = current.split('=');
|
||||
prev[queryString[0]] = queryString[1];
|
||||
return prev;
|
||||
}, {});
|
||||
}
|
||||
|
||||
export interface IGitHubServer extends vscode.Disposable {
|
||||
login(scopes: string): Promise<string>;
|
||||
getUserInfo(token: string): Promise<{ id: string, accountName: string }>;
|
||||
getUserInfo(token: string): Promise<{ id: string; accountName: string }>;
|
||||
sendAdditionalTelemetryInfo(token: string): Promise<void>;
|
||||
friendlyName: string;
|
||||
type: AuthProviderType;
|
||||
}
|
||||
|
||||
interface IGitHubDeviceCodeResponse {
|
||||
device_code: string;
|
||||
user_code: string;
|
||||
verification_uri: string;
|
||||
interval: number;
|
||||
}
|
||||
|
||||
async function getScopes(token: string, serverUri: vscode.Uri, logger: Log): Promise<string[]> {
|
||||
try {
|
||||
logger.info('Getting token scopes...');
|
||||
@@ -68,7 +74,7 @@ async function getScopes(token: string, serverUri: vscode.Uri, logger: Log): Pro
|
||||
}
|
||||
}
|
||||
|
||||
async function getUserInfo(token: string, serverUri: vscode.Uri, logger: Log): Promise<{ id: string, accountName: string }> {
|
||||
async function getUserInfo(token: string, serverUri: vscode.Uri, logger: Log): Promise<{ id: string; accountName: string }> {
|
||||
let result: Response;
|
||||
try {
|
||||
logger.info('Getting user info...');
|
||||
@@ -84,41 +90,57 @@ async function getUserInfo(token: string, serverUri: vscode.Uri, logger: Log): P
|
||||
}
|
||||
|
||||
if (result.ok) {
|
||||
const json = await result.json();
|
||||
logger.info('Got account info!');
|
||||
return { id: json.id, accountName: json.login };
|
||||
try {
|
||||
const json = await result.json();
|
||||
logger.info('Got account info!');
|
||||
return { id: json.id, accountName: json.login };
|
||||
} catch (e) {
|
||||
logger.error(`Unexpected error parsing response from GitHub: ${e.message ?? e}`);
|
||||
throw e;
|
||||
}
|
||||
} else {
|
||||
logger.error(`Getting account info failed: ${result.statusText}`);
|
||||
throw new Error(result.statusText);
|
||||
// either display the response message or the http status text
|
||||
let errorMessage = result.statusText;
|
||||
try {
|
||||
const json = await result.json();
|
||||
if (json.message) {
|
||||
errorMessage = json.message;
|
||||
}
|
||||
} catch (err) {
|
||||
// noop
|
||||
}
|
||||
logger.error(`Getting account info failed: ${errorMessage}`);
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
export class GitHubServer implements IGitHubServer {
|
||||
friendlyName = 'GitHub';
|
||||
type = AuthProviderType.github;
|
||||
private _statusBarItem: vscode.StatusBarItem | undefined;
|
||||
private _onDidManuallyProvideToken = new vscode.EventEmitter<string | undefined>();
|
||||
|
||||
private _pendingStates = new Map<string, string[]>();
|
||||
private _codeExchangePromises = new Map<string, { promise: Promise<string>, cancel: vscode.EventEmitter<void> }>();
|
||||
private _statusBarCommandId = `${this.type}.provide-manually`;
|
||||
private _pendingNonces = new Map<string, string[]>();
|
||||
private _codeExchangePromises = new Map<string, { promise: Promise<string>; cancel: vscode.EventEmitter<void> }>();
|
||||
private _disposable: vscode.Disposable;
|
||||
private _uriHandler = new UriEventHandler(this._logger);
|
||||
private readonly getRedirectEndpoint: Thenable<string>;
|
||||
|
||||
constructor(private readonly _logger: Log, private readonly _telemetryReporter: ExperimentationTelemetry) {
|
||||
this._disposable = vscode.Disposable.from(
|
||||
vscode.commands.registerCommand(this._statusBarCommandId, () => this.manuallyProvideUri()),
|
||||
vscode.window.registerUriHandler(this._uriHandler));
|
||||
constructor(private readonly _supportDeviceCodeFlow: boolean, private readonly _logger: Log, private readonly _telemetryReporter: ExperimentationTelemetry) {
|
||||
this._disposable = vscode.window.registerUriHandler(this._uriHandler);
|
||||
|
||||
this.getRedirectEndpoint = vscode.commands.executeCommand<{ [providerId: string]: string } | undefined>('workbench.getCodeExchangeProxyEndpoints').then((proxyEndpoints) => {
|
||||
// If we are running in insiders vscode.dev, then ensure we use the redirect route on that.
|
||||
let redirectUri = REDIRECT_URL_STABLE;
|
||||
if (proxyEndpoints?.github && new URL(proxyEndpoints.github).hostname === 'insiders.vscode.dev') {
|
||||
redirectUri = REDIRECT_URL_INSIDERS;
|
||||
}
|
||||
return redirectUri;
|
||||
});
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this._disposable.dispose();
|
||||
}
|
||||
|
||||
private isTestEnvironment(url: vscode.Uri): boolean {
|
||||
return /\.azurewebsites\.net$/.test(url.authority) || url.authority.startsWith('localhost:');
|
||||
}
|
||||
|
||||
// TODO@joaomoreno TODO@TylerLeonhardt
|
||||
private async isNoCorsEnvironment(): Promise<boolean> {
|
||||
const uri = await vscode.env.asExternalUri(vscode.Uri.parse(`${vscode.env.uriScheme}://vscode.github-authentication/dummy`));
|
||||
@@ -128,152 +150,325 @@ export class GitHubServer implements IGitHubServer {
|
||||
public async login(scopes: string): Promise<string> {
|
||||
this._logger.info(`Logging in for the following scopes: ${scopes}`);
|
||||
|
||||
const callbackUri = await vscode.env.asExternalUri(vscode.Uri.parse(`${vscode.env.uriScheme}://vscode.github-authentication/did-authenticate`));
|
||||
// Used for showing a friendlier message to the user when the explicitly cancel a flow.
|
||||
let userCancelled: boolean | undefined;
|
||||
const yes = localize('yes', "Yes");
|
||||
const no = localize('no', "No");
|
||||
const promptToContinue = async () => {
|
||||
if (userCancelled === undefined) {
|
||||
// We haven't had a failure yet so wait to prompt
|
||||
return;
|
||||
}
|
||||
const message = userCancelled
|
||||
? localize('userCancelledMessage', "Having trouble logging in? Would you like to try a different way?")
|
||||
: localize('otherReasonMessage', "You have not yet finished authorizing this extension to use GitHub. Would you like to keep trying?");
|
||||
const result = await vscode.window.showWarningMessage(message, yes, no);
|
||||
if (result !== yes) {
|
||||
throw new Error('Cancelled');
|
||||
}
|
||||
};
|
||||
|
||||
if (this.isTestEnvironment(callbackUri)) {
|
||||
const token = await vscode.window.showInputBox({ prompt: 'GitHub Personal Access Token', ignoreFocusOut: true });
|
||||
if (!token) { throw new Error('Sign in failed: No token provided'); }
|
||||
const nonce = uuid();
|
||||
const callbackUri = await vscode.env.asExternalUri(vscode.Uri.parse(`${vscode.env.uriScheme}://vscode.github-authentication/did-authenticate?nonce=${encodeURIComponent(nonce)}`));
|
||||
|
||||
const tokenScopes = await getScopes(token, this.getServerUri('/'), this._logger); // Example: ['repo', 'user']
|
||||
const scopesList = scopes.split(' '); // Example: 'read:user repo user:email'
|
||||
if (!scopesList.every(scope => {
|
||||
const included = tokenScopes.includes(scope);
|
||||
if (included || !scope.includes(':')) {
|
||||
return included;
|
||||
}
|
||||
const supported = isSupportedEnvironment(callbackUri);
|
||||
if (supported) {
|
||||
try {
|
||||
return await this.doLoginWithoutLocalServer(scopes, nonce, callbackUri);
|
||||
} catch (e) {
|
||||
this._logger.error(e);
|
||||
userCancelled = e.message ?? e === 'User Cancelled';
|
||||
}
|
||||
}
|
||||
|
||||
return scope.split(':').some(splitScopes => {
|
||||
return tokenScopes.includes(splitScopes);
|
||||
});
|
||||
})) {
|
||||
throw new Error(`The provided token does not match the requested scopes: ${scopes}`);
|
||||
// Starting a local server isn't supported in web
|
||||
if (vscode.env.uiKind === vscode.UIKind.Desktop) {
|
||||
try {
|
||||
await promptToContinue();
|
||||
return await this.doLoginWithLocalServer(scopes);
|
||||
} catch (e) {
|
||||
this._logger.error(e);
|
||||
userCancelled = e.message ?? e === 'User Cancelled';
|
||||
}
|
||||
}
|
||||
|
||||
if (this._supportDeviceCodeFlow) {
|
||||
try {
|
||||
await promptToContinue();
|
||||
return await this.doLoginDeviceCodeFlow(scopes);
|
||||
} catch (e) {
|
||||
this._logger.error(e);
|
||||
userCancelled = e.message ?? e === 'User Cancelled';
|
||||
}
|
||||
} else if (!supported) {
|
||||
try {
|
||||
await promptToContinue();
|
||||
return await this.doLoginWithPat(scopes);
|
||||
} catch (e) {
|
||||
this._logger.error(e);
|
||||
userCancelled = e.message ?? e === 'User Cancelled';
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(userCancelled ? 'Cancelled' : 'No auth flow succeeded.');
|
||||
}
|
||||
|
||||
private async doLoginWithoutLocalServer(scopes: string, nonce: string, callbackUri: vscode.Uri): Promise<string> {
|
||||
this._logger.info(`Trying without local server... (${scopes})`);
|
||||
return await vscode.window.withProgress<string>({
|
||||
location: vscode.ProgressLocation.Notification,
|
||||
title: localize('signingIn', "Signing in to github.com..."),
|
||||
cancellable: true
|
||||
}, async (_, token) => {
|
||||
const existingNonces = this._pendingNonces.get(scopes) || [];
|
||||
this._pendingNonces.set(scopes, [...existingNonces, nonce]);
|
||||
const redirectUri = await this.getRedirectEndpoint;
|
||||
const searchParams = new URLSearchParams([
|
||||
['client_id', CLIENT_ID],
|
||||
['redirect_uri', redirectUri],
|
||||
['scope', scopes],
|
||||
['state', encodeURIComponent(callbackUri.toString(true))]
|
||||
]);
|
||||
const uri = vscode.Uri.parse(`${GITHUB_AUTHORIZE_URL}?${searchParams.toString()}`);
|
||||
await vscode.env.openExternal(uri);
|
||||
|
||||
// Register a single listener for the URI callback, in case the user starts the login process multiple times
|
||||
// before completing it.
|
||||
let codeExchangePromise = this._codeExchangePromises.get(scopes);
|
||||
if (!codeExchangePromise) {
|
||||
codeExchangePromise = promiseFromEvent(this._uriHandler.event, this.handleUri(scopes));
|
||||
this._codeExchangePromises.set(scopes, codeExchangePromise);
|
||||
}
|
||||
|
||||
return token;
|
||||
}
|
||||
|
||||
this.updateStatusBarItem(true);
|
||||
|
||||
const state = uuid();
|
||||
const existingStates = this._pendingStates.get(scopes) || [];
|
||||
this._pendingStates.set(scopes, [...existingStates, state]);
|
||||
|
||||
const uri = vscode.Uri.parse(`https://${AUTH_RELAY_SERVER}/authorize/?callbackUri=${encodeURIComponent(callbackUri.toString())}&scope=${scopes}&state=${state}&responseType=code&authServer=https://github.com`);
|
||||
await vscode.env.openExternal(uri);
|
||||
|
||||
// Register a single listener for the URI callback, in case the user starts the login process multiple times
|
||||
// before completing it.
|
||||
let codeExchangePromise = this._codeExchangePromises.get(scopes);
|
||||
if (!codeExchangePromise) {
|
||||
codeExchangePromise = promiseFromEvent(this._uriHandler.event, this.exchangeCodeForToken(scopes));
|
||||
this._codeExchangePromises.set(scopes, codeExchangePromise);
|
||||
}
|
||||
|
||||
return Promise.race([
|
||||
codeExchangePromise.promise,
|
||||
promiseFromEvent<string | undefined, string>(this._onDidManuallyProvideToken.event, (token: string | undefined, resolve, reject): void => {
|
||||
if (!token) {
|
||||
reject('Cancelled');
|
||||
} else {
|
||||
resolve(token);
|
||||
}
|
||||
}).promise,
|
||||
new Promise<string>((_, reject) => setTimeout(() => reject('Cancelled'), 60000))
|
||||
]).finally(() => {
|
||||
this._pendingStates.delete(scopes);
|
||||
codeExchangePromise?.cancel.fire();
|
||||
this._codeExchangePromises.delete(scopes);
|
||||
this.updateStatusBarItem(false);
|
||||
try {
|
||||
return await Promise.race([
|
||||
codeExchangePromise.promise,
|
||||
new Promise<string>((_, reject) => setTimeout(() => reject('Cancelled'), 60000)),
|
||||
promiseFromEvent<any, any>(token.onCancellationRequested, (_, __, reject) => { reject('User Cancelled'); }).promise
|
||||
]);
|
||||
} finally {
|
||||
this._pendingNonces.delete(scopes);
|
||||
codeExchangePromise?.cancel.fire();
|
||||
this._codeExchangePromises.delete(scopes);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private exchangeCodeForToken: (scopes: string) => PromiseAdapter<vscode.Uri, string> =
|
||||
(scopes) => async (uri, resolve, reject) => {
|
||||
const query = parseQuery(uri);
|
||||
const code = query.code;
|
||||
private async doLoginWithLocalServer(scopes: string): Promise<string> {
|
||||
this._logger.info(`Trying with local server... (${scopes})`);
|
||||
return await vscode.window.withProgress<string>({
|
||||
location: vscode.ProgressLocation.Notification,
|
||||
title: localize('signingInAnotherWay', "Signing in to github.com..."),
|
||||
cancellable: true
|
||||
}, async (_, token) => {
|
||||
const redirectUri = await this.getRedirectEndpoint;
|
||||
const searchParams = new URLSearchParams([
|
||||
['client_id', CLIENT_ID],
|
||||
['redirect_uri', redirectUri],
|
||||
['scope', scopes],
|
||||
]);
|
||||
const loginUrl = `${GITHUB_AUTHORIZE_URL}?${searchParams.toString()}`;
|
||||
const server = new LoopbackAuthServer(path.join(__dirname, '../media'), loginUrl);
|
||||
const port = await server.start();
|
||||
|
||||
const acceptedStates = this._pendingStates.get(scopes) || [];
|
||||
if (!acceptedStates.includes(query.state)) {
|
||||
let codeToExchange;
|
||||
try {
|
||||
vscode.env.openExternal(vscode.Uri.parse(`http://127.0.0.1:${port}/signin?nonce=${encodeURIComponent(server.nonce)}`));
|
||||
const { code } = await Promise.race([
|
||||
server.waitForOAuthResponse(),
|
||||
new Promise<any>((_, reject) => setTimeout(() => reject('Cancelled'), 60000)),
|
||||
promiseFromEvent<any, any>(token.onCancellationRequested, (_, __, reject) => { reject('User Cancelled'); }).promise
|
||||
]);
|
||||
codeToExchange = code;
|
||||
} finally {
|
||||
setTimeout(() => {
|
||||
void server.stop();
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
const accessToken = await this.exchangeCodeForToken(codeToExchange);
|
||||
return accessToken;
|
||||
});
|
||||
}
|
||||
|
||||
private async doLoginDeviceCodeFlow(scopes: string): Promise<string> {
|
||||
this._logger.info(`Trying device code flow... (${scopes})`);
|
||||
|
||||
// Get initial device code
|
||||
const uri = `https://github.com/login/device/code?client_id=${CLIENT_ID}&scope=${scopes}`;
|
||||
const result = await fetch(uri, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json'
|
||||
}
|
||||
});
|
||||
if (!result.ok) {
|
||||
throw new Error(`Failed to get one-time code: ${await result.text()}`);
|
||||
}
|
||||
|
||||
const json = await result.json() as IGitHubDeviceCodeResponse;
|
||||
|
||||
|
||||
const modalResult = await vscode.window.showInformationMessage(
|
||||
localize('code.title', "Your Code: {0}", json.user_code),
|
||||
{
|
||||
modal: true,
|
||||
detail: localize('code.detail', "To finish authenticating, navigate to GitHub and paste in the above one-time code.")
|
||||
}, 'Copy & Continue to GitHub');
|
||||
|
||||
if (modalResult !== 'Copy & Continue to GitHub') {
|
||||
throw new Error('User Cancelled');
|
||||
}
|
||||
|
||||
await vscode.env.clipboard.writeText(json.user_code);
|
||||
|
||||
const uriToOpen = await vscode.env.asExternalUri(vscode.Uri.parse(json.verification_uri));
|
||||
await vscode.env.openExternal(uriToOpen);
|
||||
|
||||
return await this.waitForDeviceCodeAccessToken(json);
|
||||
}
|
||||
|
||||
private async doLoginWithPat(scopes: string): Promise<string> {
|
||||
this._logger.info(`Trying to retrieve PAT... (${scopes})`);
|
||||
const token = await vscode.window.showInputBox({ prompt: 'GitHub Personal Access Token', ignoreFocusOut: true });
|
||||
if (!token) { throw new Error('User Cancelled'); }
|
||||
|
||||
const tokenScopes = await getScopes(token, this.getServerUri('/'), this._logger); // Example: ['repo', 'user']
|
||||
const scopesList = scopes.split(' '); // Example: 'read:user repo user:email'
|
||||
if (!scopesList.every(scope => {
|
||||
const included = tokenScopes.includes(scope);
|
||||
if (included || !scope.includes(':')) {
|
||||
return included;
|
||||
}
|
||||
|
||||
return scope.split(':').some(splitScopes => {
|
||||
return tokenScopes.includes(splitScopes);
|
||||
});
|
||||
})) {
|
||||
throw new Error(`The provided token does not match the requested scopes: ${scopes}`);
|
||||
}
|
||||
|
||||
return token;
|
||||
}
|
||||
|
||||
private async waitForDeviceCodeAccessToken(
|
||||
json: IGitHubDeviceCodeResponse,
|
||||
): Promise<string> {
|
||||
return await vscode.window.withProgress<string>({
|
||||
location: vscode.ProgressLocation.Notification,
|
||||
cancellable: true,
|
||||
title: localize(
|
||||
'progress',
|
||||
"Open [{0}]({0}) in a new tab and paste your one-time code: {1}",
|
||||
json.verification_uri,
|
||||
json.user_code)
|
||||
}, async (_, token) => {
|
||||
const refreshTokenUri = `https://github.com/login/oauth/access_token?client_id=${CLIENT_ID}&device_code=${json.device_code}&grant_type=urn:ietf:params:oauth:grant-type:device_code`;
|
||||
|
||||
// Try for 2 minutes
|
||||
const attempts = 120 / json.interval;
|
||||
for (let i = 0; i < attempts; i++) {
|
||||
await new Promise(resolve => setTimeout(resolve, json.interval * 1000));
|
||||
if (token.isCancellationRequested) {
|
||||
throw new Error('User Cancelled');
|
||||
}
|
||||
let accessTokenResult;
|
||||
try {
|
||||
accessTokenResult = await fetch(refreshTokenUri, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json'
|
||||
}
|
||||
});
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!accessTokenResult.ok) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const accessTokenJson = await accessTokenResult.json();
|
||||
|
||||
if (accessTokenJson.error === 'authorization_pending') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (accessTokenJson.error) {
|
||||
throw new Error(accessTokenJson.error_description);
|
||||
}
|
||||
|
||||
return accessTokenJson.access_token;
|
||||
}
|
||||
|
||||
throw new Error('Cancelled');
|
||||
});
|
||||
}
|
||||
|
||||
private handleUri: (scopes: string) => PromiseAdapter<vscode.Uri, string> =
|
||||
(scopes) => (uri, resolve, reject) => {
|
||||
const query = new URLSearchParams(uri.query);
|
||||
const code = query.get('code');
|
||||
const nonce = query.get('nonce');
|
||||
if (!code) {
|
||||
reject(new Error('No code'));
|
||||
return;
|
||||
}
|
||||
if (!nonce) {
|
||||
reject(new Error('No nonce'));
|
||||
return;
|
||||
}
|
||||
|
||||
const acceptedNonces = this._pendingNonces.get(scopes) || [];
|
||||
if (!acceptedNonces.includes(nonce)) {
|
||||
// A common scenario of this happening is if you:
|
||||
// 1. Trigger a sign in with one set of scopes
|
||||
// 2. Before finishing 1, you trigger a sign in with a different set of scopes
|
||||
// In this scenario we should just return and wait for the next UriHandler event
|
||||
// to run as we are probably still waiting on the user to hit 'Continue'
|
||||
this._logger.info('State not found in accepted state. Skipping this execution...');
|
||||
this._logger.info('Nonce not found in accepted nonces. Skipping this execution...');
|
||||
return;
|
||||
}
|
||||
|
||||
const url = `https://${AUTH_RELAY_SERVER}/token?code=${code}&state=${query.state}`;
|
||||
this._logger.info('Exchanging code for token...');
|
||||
|
||||
try {
|
||||
const result = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
if (result.ok) {
|
||||
const json = await result.json();
|
||||
this._logger.info('Token exchange success!');
|
||||
resolve(json.access_token);
|
||||
} else {
|
||||
reject(result.statusText);
|
||||
}
|
||||
} catch (ex) {
|
||||
reject(ex);
|
||||
}
|
||||
resolve(this.exchangeCodeForToken(code));
|
||||
};
|
||||
|
||||
private async exchangeCodeForToken(code: string): Promise<string> {
|
||||
this._logger.info('Exchanging code for token...');
|
||||
|
||||
const proxyEndpoints: { [providerId: string]: string } | undefined = await vscode.commands.executeCommand('workbench.getCodeExchangeProxyEndpoints');
|
||||
const endpointUrl = proxyEndpoints?.github ? `${proxyEndpoints.github}login/oauth/access_token` : GITHUB_TOKEN_URL;
|
||||
|
||||
const body = `code=${code}`;
|
||||
const result = await fetch(endpointUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'Content-Length': body.toString()
|
||||
|
||||
},
|
||||
body
|
||||
});
|
||||
|
||||
if (result.ok) {
|
||||
const json = await result.json();
|
||||
this._logger.info('Token exchange success!');
|
||||
return json.access_token;
|
||||
} else {
|
||||
const text = await result.text();
|
||||
const error = new Error(text);
|
||||
error.name = 'GitHubTokenExchangeError';
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
private getServerUri(path: string = '') {
|
||||
const apiUri = vscode.Uri.parse('https://api.github.com');
|
||||
return vscode.Uri.parse(`${apiUri.scheme}://${apiUri.authority}${path}`);
|
||||
}
|
||||
|
||||
private updateStatusBarItem(isStart?: boolean) {
|
||||
if (isStart && !this._statusBarItem) {
|
||||
this._statusBarItem = vscode.window.createStatusBarItem('status.git.signIn', vscode.StatusBarAlignment.Left);
|
||||
this._statusBarItem.name = localize('status.git.signIn.name', "GitHub Sign-in");
|
||||
this._statusBarItem.text = localize('signingIn', "$(mark-github) Signing in to github.com...");
|
||||
this._statusBarItem.command = this._statusBarCommandId;
|
||||
this._statusBarItem.show();
|
||||
}
|
||||
|
||||
if (!isStart && this._statusBarItem) {
|
||||
this._statusBarItem.dispose();
|
||||
this._statusBarItem = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private async manuallyProvideUri() {
|
||||
const uri = await vscode.window.showInputBox({
|
||||
prompt: 'Uri',
|
||||
ignoreFocusOut: true,
|
||||
validateInput(value) {
|
||||
if (!value) {
|
||||
return undefined;
|
||||
}
|
||||
const error = localize('validUri', "Please enter a valid Uri from the GitHub login page.");
|
||||
try {
|
||||
const uri = vscode.Uri.parse(value.trim());
|
||||
if (!uri.scheme || uri.scheme === 'file') {
|
||||
return error;
|
||||
}
|
||||
} catch (e) {
|
||||
return error;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
});
|
||||
if (!uri) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._uriHandler.handleUri(vscode.Uri.parse(uri.trim()));
|
||||
}
|
||||
|
||||
public getUserInfo(token: string): Promise<{ id: string, accountName: string }> {
|
||||
public getUserInfo(token: string): Promise<{ id: string; accountName: string }> {
|
||||
return getUserInfo(token, this.getServerUri('/user'), this._logger);
|
||||
}
|
||||
|
||||
@@ -297,7 +492,7 @@ export class GitHubServer implements IGitHubServer {
|
||||
});
|
||||
|
||||
if (result.ok) {
|
||||
const json: { student: boolean, faculty: boolean } = await result.json();
|
||||
const json: { student: boolean; faculty: boolean } = await result.json();
|
||||
|
||||
/* __GDPR__
|
||||
"session" : {
|
||||
@@ -331,7 +526,7 @@ export class GitHubServer implements IGitHubServer {
|
||||
return;
|
||||
}
|
||||
|
||||
const json: { verifiable_password_authentication: boolean, installed_version: string } = await result.json();
|
||||
const json: { verifiable_password_authentication: boolean; installed_version: string } = await result.json();
|
||||
|
||||
/* __GDPR__
|
||||
"ghe-session" : {
|
||||
@@ -351,20 +546,9 @@ export class GitHubEnterpriseServer implements IGitHubServer {
|
||||
friendlyName = 'GitHub Enterprise';
|
||||
type = AuthProviderType.githubEnterprise;
|
||||
|
||||
private _onDidManuallyProvideToken = new vscode.EventEmitter<string | undefined>();
|
||||
private _statusBarCommandId = `github-enterprise.provide-manually`;
|
||||
private _disposable: vscode.Disposable;
|
||||
constructor(private readonly _logger: Log, private readonly telemetryReporter: ExperimentationTelemetry) { }
|
||||
|
||||
constructor(private readonly _logger: Log, private readonly telemetryReporter: ExperimentationTelemetry) {
|
||||
this._disposable = vscode.commands.registerCommand(this._statusBarCommandId, async () => {
|
||||
const token = await vscode.window.showInputBox({ prompt: 'Token', ignoreFocusOut: true });
|
||||
this._onDidManuallyProvideToken.fire(token);
|
||||
});
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this._disposable.dispose();
|
||||
}
|
||||
dispose() { }
|
||||
|
||||
public async login(scopes: string): Promise<string> {
|
||||
this._logger.info(`Logging in for the following scopes: ${scopes}`);
|
||||
@@ -395,7 +579,7 @@ export class GitHubEnterpriseServer implements IGitHubServer {
|
||||
return vscode.Uri.parse(`${apiUri.scheme}://${apiUri.authority}/api/v3${path}`);
|
||||
}
|
||||
|
||||
public async getUserInfo(token: string): Promise<{ id: string, accountName: string }> {
|
||||
public async getUserInfo(token: string): Promise<{ id: string; accountName: string }> {
|
||||
return getUserInfo(token, this.getServerUri('/user'), this._logger);
|
||||
}
|
||||
|
||||
@@ -413,7 +597,7 @@ export class GitHubEnterpriseServer implements IGitHubServer {
|
||||
return;
|
||||
}
|
||||
|
||||
const json: { verifiable_password_authentication: boolean, installed_version: string } = await result.json();
|
||||
const json: { verifiable_password_authentication: boolean; installed_version: string } = await result.json();
|
||||
|
||||
/* __GDPR__
|
||||
"ghe-session" : {
|
||||
|
||||
Reference in New Issue
Block a user