| |
| |
| |
| |
| |
|
|
| import assert from 'assert'; |
| import * as fs from 'fs'; |
| import * as http from 'http'; |
| import * as path from 'path'; |
| import { OAuth2Client } from 'google-auth-library'; |
| import vscode from 'vscode'; |
| import { CONFIG } from '../../colab-config'; |
| import { log } from '../../common/logging'; |
| import { LoopbackHandler, LoopbackServer } from '../../common/loopback-server'; |
| import { CodeManager } from '../code-manager'; |
| import { |
| DEFAULT_AUTH_URL_OPTS, |
| OAuth2Flow, |
| OAuth2TriggerOptions, |
| FlowResult, |
| } from './flows'; |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| export class LocalServerFlow implements OAuth2Flow, vscode.Disposable { |
| private readonly codeManager = new CodeManager(); |
| private readonly handler: Handler; |
| private readonly activeServers = new Set<vscode.Disposable>(); |
|
|
| constructor( |
| private readonly vs: typeof vscode, |
| private readonly serveRoot: string, |
| private readonly oAuth2Client: OAuth2Client, |
| extensionUri: string, |
| ) { |
| this.handler = new Handler( |
| vs, |
| this.serveRoot, |
| this.codeManager, |
| extensionUri, |
| ); |
| } |
|
|
| dispose() { |
| this.codeManager.dispose(); |
| for (const disposable of this.activeServers) { |
| disposable.dispose(); |
| } |
| this.activeServers.clear(); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| async trigger(options: OAuth2TriggerOptions): Promise<FlowResult> { |
| const server = new LoopbackServer(this.handler); |
| this.activeServers.add(server); |
| try { |
| const code = this.codeManager.waitForCode(options.nonce, options.cancel); |
| options.cancel.onCancellationRequested(server.dispose.bind(server)); |
| const port = await server.start(); |
| const address = `http://127.0.0.1:${port.toString()}`; |
| const authUrl = this.oAuth2Client.generateAuthUrl({ |
| ...DEFAULT_AUTH_URL_OPTS, |
| redirect_uri: address, |
| state: `nonce=${options.nonce}`, |
| scope: options.scopes, |
| code_challenge: options.pkceChallenge, |
| }); |
|
|
| await this.vs.env.openExternal(this.vs.Uri.parse(authUrl)); |
| |
| |
| |
| |
| return { |
| code: await code, |
| redirectUri: address, |
| }; |
| } catch (err: unknown) { |
| server.dispose(); |
| this.activeServers.delete(server); |
| throw err; |
| } |
| } |
| } |
|
|
| class Handler implements LoopbackHandler { |
| constructor( |
| private readonly vs: typeof vscode, |
| private readonly serveRoot: string, |
| private readonly codeProvider: CodeManager, |
| private readonly extensionUri: string, |
| ) {} |
|
|
| handleRequest(req: http.IncomingMessage, res: http.ServerResponse): void { |
| |
| assert(req.url); |
| assert(req.headers.host); |
| const url = new URL(req.url, `http://${req.headers.host}`); |
| if (req.method !== 'GET') { |
| res.writeHead(405, { Allow: 'GET' }); |
| res.end('Method Not Allowed'); |
| return; |
| } |
| switch (url.pathname) { |
| case '/': { |
| const state = url.searchParams.get('state'); |
| if (!state) { |
| throw new Error('Missing state in redirect URL'); |
| } |
| const parsedState = new URLSearchParams(state); |
| const nonce = parsedState.get('nonce'); |
| const code = url.searchParams.get('code'); |
| if (!nonce || !code) { |
| throw new Error('Missing nonce or code in redirect URI'); |
| } |
| this.codeProvider.resolveCode(nonce, code); |
|
|
| void this.redirectSuccessfulAuth(res).catch((err: unknown) => { |
| log.error('Unable to redirect the successful auth request', err); |
| }); |
| break; |
| } |
| case '/favicon.ico': { |
| const assetPath = url.pathname.substring(1); |
| sendFile(res, path.join(this.serveRoot, assetPath)); |
| break; |
| } |
| default: { |
| log.warn('Received unhandled request', req); |
| res.writeHead(404); |
| res.end('Not Found'); |
| break; |
| } |
| } |
| } |
|
|
| async redirectSuccessfulAuth(res: http.ServerResponse): Promise<void> { |
| const authSuccessUri = await this.vs.env.asExternalUri( |
| this.vs.Uri.parse(`${this.extensionUri}/auth-success`), |
| ); |
| const successState = encodeURIComponent(authSuccessUri.toString()); |
| const redirectUri = `${CONFIG.ColabApiDomain}/vscode/auth-success?state=${successState}`; |
| |
| |
| |
| |
| if (res.headersSent) { |
| return; |
| } |
| res.writeHead(302, { Location: redirectUri }); |
| res.end(); |
| } |
| } |
|
|
| function sendFile(res: http.ServerResponse, filepath: string): void { |
| fs.readFile(filepath, (err, body) => { |
| if (err) { |
| log.error(`Unable to read file: ${filepath}`, err); |
| res.writeHead(500); |
| res.end('Internal Server Error'); |
| } else { |
| res.setHeader('content-length', body.length); |
| res.writeHead(200); |
| res.end(body); |
| } |
| }); |
| } |
|
|