From 3d8d544f89f9ee1254d3374d8e9267f4ee4d75d8 Mon Sep 17 00:00:00 2001 From: Asher Date: Tue, 16 Apr 2024 11:00:33 -0800 Subject: [PATCH] Refactor VS Code routes to match others --- src/node/routes/index.ts | 10 +- src/node/routes/vscode.ts | 355 ++++++++++++++++++-------------------- 2 files changed, 173 insertions(+), 192 deletions(-) diff --git a/src/node/routes/index.ts b/src/node/routes/index.ts index 9c494a546..1be3988ca 100644 --- a/src/node/routes/index.ts +++ b/src/node/routes/index.ts @@ -25,7 +25,7 @@ import * as login from "./login" import * as logout from "./logout" import * as pathProxy from "./pathProxy" import * as update from "./update" -import { CodeServerRouteWrapper } from "./vscode" +import * as vscode from "./vscode" /** * Register all routes and middleware. @@ -170,12 +170,10 @@ export const register = async (app: App, args: DefaultedArgs): Promise { @@ -188,6 +186,6 @@ export const register = async (app: App, args: DefaultedArgs): Promise { heart.dispose() pluginApi?.dispose() - vsServerRouteHandler.dispose() + vscode.dispose() } } diff --git a/src/node/routes/vscode.ts b/src/node/routes/vscode.ts index c4f2808a5..b8f88a0fc 100644 --- a/src/node/routes/vscode.ts +++ b/src/node/routes/vscode.ts @@ -14,203 +14,186 @@ import { SocketProxyProvider } from "../socket" import { isFile, loadAMDModule } from "../util" import { Router as WsRouter } from "../wsRouter" +export const router = express.Router() + +export const wsRouter = WsRouter() + /** - * This is the API of Code's web client server. code-server delegates requests - * to Code here. + * The API of VS Code's web client server. code-server delegates requests to VS + * Code here. + * + * @see ../../../lib/vscode/src/vs/server/node/server.main.ts:72 */ -export interface IServerAPI { +export interface IVSCodeServerAPI { handleRequest(req: http.IncomingMessage, res: http.ServerResponse): Promise handleUpgrade(req: http.IncomingMessage, socket: net.Socket): void handleServerError(err: Error): void dispose(): void } -// Types for ../../../lib/vscode/src/vs/server/node/server.main.ts:72. -export type CreateServer = (address: string | net.AddressInfo | null, args: CodeArgs) => Promise +// See ../../../lib/vscode/src/vs/server/node/server.main.ts:72. +export type CreateServer = (address: string | net.AddressInfo | null, args: CodeArgs) => Promise -export class CodeServerRouteWrapper { - /** Assigned in `ensureCodeServerLoaded` */ - private _codeServerMain!: IServerAPI - private _wsRouterWrapper = WsRouter() - private _socketProxyProvider = new SocketProxyProvider() - public router = express.Router() - private mintKeyPromise: Promise | undefined - - public get wsRouter() { - return this._wsRouterWrapper.router - } - - //#region Route Handlers - - private manifest: express.Handler = async (req, res, next) => { - const appName = req.args["app-name"] || "code-server" - res.writeHead(200, { "Content-Type": "application/manifest+json" }) - - return res.end( - replaceTemplates( - req, - JSON.stringify( - { - name: appName, - short_name: appName, - start_url: ".", - display: "fullscreen", - display_override: ["window-controls-overlay"], - description: "Run Code on a remote server.", - icons: [192, 512].map((size) => ({ - src: `{{BASE}}/_static/src/browser/media/pwa-icon-${size}.png`, - type: "image/png", - sizes: `${size}x${size}`, - })), - }, - null, - 2, - ), - ), - ) - } - - private mintKey: express.Handler = async (req, res, next) => { - if (!this.mintKeyPromise) { - this.mintKeyPromise = new Promise(async (resolve) => { - const keyPath = path.join(req.args["user-data-dir"], "serve-web-key-half") - logger.debug(`Reading server web key half from ${keyPath}`) - try { - resolve(await fs.readFile(keyPath)) - return - } catch (error: any) { - if (error.code !== "ENOENT") { - logError(logger, `read ${keyPath}`, error) - } - } - // VS Code wants 256 bits. - const key = crypto.randomBytes(32) - try { - await fs.writeFile(keyPath, key) - } catch (error: any) { - logError(logger, `write ${keyPath}`, error) - } - resolve(key) - }) - } - const key = await this.mintKeyPromise - res.end(key) - } - - private $root: express.Handler = async (req, res, next) => { - const isAuthenticated = await authenticated(req) - const NO_FOLDER_OR_WORKSPACE_QUERY = !req.query.folder && !req.query.workspace - // Ew means the workspace was closed so clear the last folder/workspace. - const FOLDER_OR_WORKSPACE_WAS_CLOSED = req.query.ew - - if (!isAuthenticated) { - const to = self(req) - return redirect(req, res, "login", { - to: to !== "/" ? to : undefined, - }) - } - - if (NO_FOLDER_OR_WORKSPACE_QUERY && !FOLDER_OR_WORKSPACE_WAS_CLOSED) { - const settings = await req.settings.read() - const lastOpened = settings.query || {} - // This flag disables the last opened behavior - const IGNORE_LAST_OPENED = req.args["ignore-last-opened"] - const HAS_LAST_OPENED_FOLDER_OR_WORKSPACE = lastOpened.folder || lastOpened.workspace - const HAS_FOLDER_OR_WORKSPACE_FROM_CLI = req.args._.length > 0 - const to = self(req) - - let folder = undefined - let workspace = undefined - - // Redirect to the last folder/workspace if nothing else is opened. - if (HAS_LAST_OPENED_FOLDER_OR_WORKSPACE && !IGNORE_LAST_OPENED) { - folder = lastOpened.folder - workspace = lastOpened.workspace - } else if (HAS_FOLDER_OR_WORKSPACE_FROM_CLI) { - const lastEntry = path.resolve(req.args._[req.args._.length - 1]) - const entryIsFile = await isFile(lastEntry) - const IS_WORKSPACE_FILE = entryIsFile && path.extname(lastEntry) === ".code-workspace" - - if (IS_WORKSPACE_FILE) { - workspace = lastEntry - } else if (!entryIsFile) { - folder = lastEntry - } - } - - if (folder || workspace) { - return redirect(req, res, to, { - folder, - workspace, - }) - } - } - - // Store the query parameters so we can use them on the next load. This - // also allows users to create functionality around query parameters. - await req.settings.write({ query: req.query }) - - next() - } - - private $proxyRequest: express.Handler = async (req, res, next) => { - this._codeServerMain.handleRequest(req, res) - } - - private $proxyWebsocket = async (req: WebsocketRequest) => { - const wrappedSocket = await this._socketProxyProvider.createProxy(req.ws) - // This should actually accept a duplex stream but it seems Code has not - // been updated to match the Node 16 types so cast for now. There does not - // appear to be any code specific to sockets so this should be fine. - this._codeServerMain.handleUpgrade(req, wrappedSocket as net.Socket) - - req.ws.resume() - } - - //#endregion - - /** - * Fetches a code server instance asynchronously to avoid an initial memory overhead. - */ - private ensureCodeServerLoaded: express.Handler = async (req, _res, next) => { - if (this._codeServerMain) { - // Already loaded... - return next() - } - - // Create the server... - - const { args } = req - - // See ../../../lib/vscode/src/vs/server/node/server.main.ts:72. - const createVSServer = await loadAMDModule("vs/server/node/server.main", "createServer") - - try { - this._codeServerMain = await createVSServer(null, { - ...(await toCodeArgs(args)), - "without-connection-token": true, - }) - } catch (error) { - logError(logger, "CodeServerRouteWrapper", error) - if (isDevMode) { - return next(new Error((error instanceof Error ? error.message : error) + " (VS Code may still be compiling)")) - } - return next(error) - } +// The VS Code server is dynamically loaded in when a request is made to this +// router by `ensureCodeServerLoaded`. +let vscodeServer: IVSCodeServerAPI | undefined +/** + * Ensure the VS Code server is loaded. + */ +export const ensureVSCodeLoaded = async ( + req: express.Request, + _: express.Response, + next: express.NextFunction, +): Promise => { + if (vscodeServer) { return next() } - - constructor() { - this.router.get("/", this.ensureCodeServerLoaded, this.$root) - this.router.get("/manifest.json", this.manifest) - this.router.post("/mint-key", this.mintKey) - this.router.all(/.*/, ensureAuthenticated, this.ensureCodeServerLoaded, this.$proxyRequest) - this._wsRouterWrapper.ws(/.*/, ensureOrigin, ensureAuthenticated, this.ensureCodeServerLoaded, this.$proxyWebsocket) - } - - dispose() { - this._codeServerMain?.dispose() - this._socketProxyProvider.stop() + // See ../../../lib/vscode/src/vs/server/node/server.main.ts:72. + const createVSServer = await loadAMDModule("vs/server/node/server.main", "createServer") + try { + vscodeServer = await createVSServer(null, { + ...(await toCodeArgs(req.args)), + "without-connection-token": true, + }) + } catch (error) { + logError(logger, "CodeServerRouteWrapper", error) + if (isDevMode) { + return next(new Error((error instanceof Error ? error.message : error) + " (VS Code may still be compiling)")) + } + return next(error) } + return next() +} + +router.get("/", ensureVSCodeLoaded, async (req, res, next) => { + const isAuthenticated = await authenticated(req) + const NO_FOLDER_OR_WORKSPACE_QUERY = !req.query.folder && !req.query.workspace + // Ew means the workspace was closed so clear the last folder/workspace. + const FOLDER_OR_WORKSPACE_WAS_CLOSED = req.query.ew + + if (!isAuthenticated) { + const to = self(req) + return redirect(req, res, "login", { + to: to !== "/" ? to : undefined, + }) + } + + if (NO_FOLDER_OR_WORKSPACE_QUERY && !FOLDER_OR_WORKSPACE_WAS_CLOSED) { + const settings = await req.settings.read() + const lastOpened = settings.query || {} + // This flag disables the last opened behavior + const IGNORE_LAST_OPENED = req.args["ignore-last-opened"] + const HAS_LAST_OPENED_FOLDER_OR_WORKSPACE = lastOpened.folder || lastOpened.workspace + const HAS_FOLDER_OR_WORKSPACE_FROM_CLI = req.args._.length > 0 + const to = self(req) + + let folder = undefined + let workspace = undefined + + // Redirect to the last folder/workspace if nothing else is opened. + if (HAS_LAST_OPENED_FOLDER_OR_WORKSPACE && !IGNORE_LAST_OPENED) { + folder = lastOpened.folder + workspace = lastOpened.workspace + } else if (HAS_FOLDER_OR_WORKSPACE_FROM_CLI) { + const lastEntry = path.resolve(req.args._[req.args._.length - 1]) + const entryIsFile = await isFile(lastEntry) + const IS_WORKSPACE_FILE = entryIsFile && path.extname(lastEntry) === ".code-workspace" + + if (IS_WORKSPACE_FILE) { + workspace = lastEntry + } else if (!entryIsFile) { + folder = lastEntry + } + } + + if (folder || workspace) { + return redirect(req, res, to, { + folder, + workspace, + }) + } + } + + // Store the query parameters so we can use them on the next load. This + // also allows users to create functionality around query parameters. + await req.settings.write({ query: req.query }) + + next() +}) + +router.get("/manifest.json", async (req, res) => { + const appName = req.args["app-name"] || "code-server" + res.writeHead(200, { "Content-Type": "application/manifest+json" }) + + return res.end( + replaceTemplates( + req, + JSON.stringify( + { + name: appName, + short_name: appName, + start_url: ".", + display: "fullscreen", + display_override: ["window-controls-overlay"], + description: "Run Code on a remote server.", + icons: [192, 512].map((size) => ({ + src: `{{BASE}}/_static/src/browser/media/pwa-icon-${size}.png`, + type: "image/png", + sizes: `${size}x${size}`, + })), + }, + null, + 2, + ), + ), + ) +}) + +let mintKeyPromise: Promise | undefined +router.post("/mint-key", async (req, res) => { + if (!mintKeyPromise) { + mintKeyPromise = new Promise(async (resolve) => { + const keyPath = path.join(req.args["user-data-dir"], "serve-web-key-half") + logger.debug(`Reading server web key half from ${keyPath}`) + try { + resolve(await fs.readFile(keyPath)) + return + } catch (error: any) { + if (error.code !== "ENOENT") { + logError(logger, `read ${keyPath}`, error) + } + } + // VS Code wants 256 bits. + const key = crypto.randomBytes(32) + try { + await fs.writeFile(keyPath, key) + } catch (error: any) { + logError(logger, `write ${keyPath}`, error) + } + resolve(key) + }) + } + const key = await mintKeyPromise + res.end(key) +}) + +router.all(/.*/, ensureAuthenticated, ensureVSCodeLoaded, async (req, res) => { + vscodeServer!.handleRequest(req, res) +}) + +const socketProxyProvider = new SocketProxyProvider() +wsRouter.ws(/.*/, ensureOrigin, ensureAuthenticated, ensureVSCodeLoaded, async (req: WebsocketRequest) => { + const wrappedSocket = await socketProxyProvider.createProxy(req.ws) + // This should actually accept a duplex stream but it seems Code has not + // been updated to match the Node 16 types so cast for now. There does not + // appear to be any code specific to sockets so this should be fine. + vscodeServer!.handleUpgrade(req, wrappedSocket as net.Socket) + + req.ws.resume() +}) + +export function dispose() { + vscodeServer?.dispose() + socketProxyProvider.stop() }