Refactor VS Code routes to match others

This commit is contained in:
Asher 2024-04-16 11:00:33 -08:00
parent 323a1f3234
commit 3d8d544f89
No known key found for this signature in database
GPG Key ID: D63C1EF81242354A
2 changed files with 173 additions and 192 deletions

View File

@ -25,7 +25,7 @@ import * as login from "./login"
import * as logout from "./logout" import * as logout from "./logout"
import * as pathProxy from "./pathProxy" import * as pathProxy from "./pathProxy"
import * as update from "./update" import * as update from "./update"
import { CodeServerRouteWrapper } from "./vscode" import * as vscode from "./vscode"
/** /**
* Register all routes and middleware. * Register all routes and middleware.
@ -170,12 +170,10 @@ export const register = async (app: App, args: DefaultedArgs): Promise<Disposabl
app.router.use("/update", update.router) app.router.use("/update", update.router)
const vsServerRouteHandler = new CodeServerRouteWrapper()
// Note that the root route is replaced in Coder Enterprise by the plugin API. // Note that the root route is replaced in Coder Enterprise by the plugin API.
for (const routePrefix of ["/vscode", "/"]) { for (const routePrefix of ["/vscode", "/"]) {
app.router.use(routePrefix, vsServerRouteHandler.router) app.router.use(routePrefix, vscode.router)
app.wsRouter.use(routePrefix, vsServerRouteHandler.wsRouter) app.wsRouter.use(routePrefix, vscode.wsRouter.router)
} }
app.router.use(() => { app.router.use(() => {
@ -188,6 +186,6 @@ export const register = async (app: App, args: DefaultedArgs): Promise<Disposabl
return () => { return () => {
heart.dispose() heart.dispose()
pluginApi?.dispose() pluginApi?.dispose()
vsServerRouteHandler.dispose() vscode.dispose()
} }
} }

View File

@ -14,203 +14,186 @@ import { SocketProxyProvider } from "../socket"
import { isFile, loadAMDModule } from "../util" import { isFile, loadAMDModule } from "../util"
import { Router as WsRouter } from "../wsRouter" 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 * The API of VS Code's web client server. code-server delegates requests to VS
* to Code here. * 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<void> handleRequest(req: http.IncomingMessage, res: http.ServerResponse): Promise<void>
handleUpgrade(req: http.IncomingMessage, socket: net.Socket): void handleUpgrade(req: http.IncomingMessage, socket: net.Socket): void
handleServerError(err: Error): void handleServerError(err: Error): void
dispose(): void dispose(): void
} }
// Types for ../../../lib/vscode/src/vs/server/node/server.main.ts:72. // See ../../../lib/vscode/src/vs/server/node/server.main.ts:72.
export type CreateServer = (address: string | net.AddressInfo | null, args: CodeArgs) => Promise<IServerAPI> export type CreateServer = (address: string | net.AddressInfo | null, args: CodeArgs) => Promise<IVSCodeServerAPI>
export class CodeServerRouteWrapper { // The VS Code server is dynamically loaded in when a request is made to this
/** Assigned in `ensureCodeServerLoaded` */ // router by `ensureCodeServerLoaded`.
private _codeServerMain!: IServerAPI let vscodeServer: IVSCodeServerAPI | undefined
private _wsRouterWrapper = WsRouter()
private _socketProxyProvider = new SocketProxyProvider()
public router = express.Router()
private mintKeyPromise: Promise<Buffer> | 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<CreateServer>("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)
}
/**
* Ensure the VS Code server is loaded.
*/
export const ensureVSCodeLoaded = async (
req: express.Request,
_: express.Response,
next: express.NextFunction,
): Promise<void> => {
if (vscodeServer) {
return next() return next()
} }
// See ../../../lib/vscode/src/vs/server/node/server.main.ts:72.
constructor() { const createVSServer = await loadAMDModule<CreateServer>("vs/server/node/server.main", "createServer")
this.router.get("/", this.ensureCodeServerLoaded, this.$root) try {
this.router.get("/manifest.json", this.manifest) vscodeServer = await createVSServer(null, {
this.router.post("/mint-key", this.mintKey) ...(await toCodeArgs(req.args)),
this.router.all(/.*/, ensureAuthenticated, this.ensureCodeServerLoaded, this.$proxyRequest) "without-connection-token": true,
this._wsRouterWrapper.ws(/.*/, ensureOrigin, ensureAuthenticated, this.ensureCodeServerLoaded, this.$proxyWebsocket) })
} } catch (error) {
logError(logger, "CodeServerRouteWrapper", error)
dispose() { if (isDevMode) {
this._codeServerMain?.dispose() return next(new Error((error instanceof Error ? error.message : error) + " (VS Code may still be compiling)"))
this._socketProxyProvider.stop() }
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<Buffer> | 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()
} }