diff --git a/extensions/microsoft-authentication/src/node/authServer.ts b/extensions/microsoft-authentication/src/node/authServer.ts deleted file mode 100644 index 2d6a8d03861..00000000000 --- a/extensions/microsoft-authentication/src/node/authServer.ts +++ /dev/null @@ -1,207 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. 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; - /** - * Stops the server. - * @throws If the server is not started. - * @throws If the server fails to stop. - */ - stop(): Promise; - /** - * Returns a promise that resolves to the result of the OAuth flow. - */ - waitForOAuthResponse(): Promise; -} - -export class LoopbackAuthServer implements ILoopbackServer { - private readonly _server: http.Server; - private readonly _resultPromise: Promise; - 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((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, '+'); - const error = reqUrl.searchParams.get('error') ?? undefined; - if (error) { - res.writeHead(302, { location: `/?error=${reqUrl.searchParams.get('error_description')}` }); - res.end(); - deferred.reject(new Error(error)); - break; - } - if (!code || !state || !nonce) { - res.writeHead(400); - res.end(); - break; - } - if (this.state !== state) { - res.writeHead(302, { location: `/?error=${encodeURIComponent('State does not match.')}` }); - res.end(); - deferred.reject(new Error('State does not match.')); - break; - } - if (this.nonce !== nonce) { - res.writeHead(302, { location: `/?error=${encodeURIComponent('Nonce does not match.')}` }); - res.end(); - deferred.reject(new Error('Nonce does not match.')); - break; - } - 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 { - return new Promise((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 { - return new Promise((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 { - return this._resultPromise; - } -}