mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-01-21 09:35:38 -05:00
* 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
199 lines
5.6 KiB
TypeScript
199 lines
5.6 KiB
TypeScript
/*---------------------------------------------------------------------------------------------
|
|
* 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;
|
|
}
|
|
}
|