Refactor VS Code routes to match others
This commit is contained in:
parent
323a1f3234
commit
3d8d544f89
@ -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<Disposabl
|
||||
|
||||
app.router.use("/update", update.router)
|
||||
|
||||
const vsServerRouteHandler = new CodeServerRouteWrapper()
|
||||
|
||||
// Note that the root route is replaced in Coder Enterprise by the plugin API.
|
||||
for (const routePrefix of ["/vscode", "/"]) {
|
||||
app.router.use(routePrefix, vsServerRouteHandler.router)
|
||||
app.wsRouter.use(routePrefix, vsServerRouteHandler.wsRouter)
|
||||
app.router.use(routePrefix, vscode.router)
|
||||
app.wsRouter.use(routePrefix, vscode.wsRouter.router)
|
||||
}
|
||||
|
||||
app.router.use(() => {
|
||||
@ -188,6 +186,6 @@ export const register = async (app: App, args: DefaultedArgs): Promise<Disposabl
|
||||
return () => {
|
||||
heart.dispose()
|
||||
pluginApi?.dispose()
|
||||
vsServerRouteHandler.dispose()
|
||||
vscode.dispose()
|
||||
}
|
||||
}
|
||||
|
@ -14,90 +14,59 @@ 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<void>
|
||||
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<IServerAPI>
|
||||
// See ../../../lib/vscode/src/vs/server/node/server.main.ts:72.
|
||||
export type CreateServer = (address: string | net.AddressInfo | null, args: CodeArgs) => Promise<IVSCodeServerAPI>
|
||||
|
||||
export class CodeServerRouteWrapper {
|
||||
/** Assigned in `ensureCodeServerLoaded` */
|
||||
private _codeServerMain!: IServerAPI
|
||||
private _wsRouterWrapper = WsRouter()
|
||||
private _socketProxyProvider = new SocketProxyProvider()
|
||||
public router = express.Router()
|
||||
private mintKeyPromise: Promise<Buffer> | undefined
|
||||
// The VS Code server is dynamically loaded in when a request is made to this
|
||||
// router by `ensureCodeServerLoaded`.
|
||||
let vscodeServer: IVSCodeServerAPI | undefined
|
||||
|
||||
public get wsRouter() {
|
||||
return this._wsRouterWrapper.router
|
||||
/**
|
||||
* 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()
|
||||
}
|
||||
|
||||
//#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}`)
|
||||
// See ../../../lib/vscode/src/vs/server/node/server.main.ts:72.
|
||||
const createVSServer = await loadAMDModule<CreateServer>("vs/server/node/server.main", "createServer")
|
||||
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)
|
||||
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)"))
|
||||
}
|
||||
const key = await this.mintKeyPromise
|
||||
res.end(key)
|
||||
return next(error)
|
||||
}
|
||||
return next()
|
||||
}
|
||||
|
||||
private $root: express.Handler = async (req, res, 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.
|
||||
@ -151,66 +120,80 @@ export class CodeServerRouteWrapper {
|
||||
await req.settings.write({ query: req.query })
|
||||
|
||||
next()
|
||||
}
|
||||
})
|
||||
|
||||
private $proxyRequest: express.Handler = async (req, res, next) => {
|
||||
this._codeServerMain.handleRequest(req, res)
|
||||
}
|
||||
router.get("/manifest.json", async (req, res) => {
|
||||
const appName = req.args["app-name"] || "code-server"
|
||||
res.writeHead(200, { "Content-Type": "application/manifest+json" })
|
||||
|
||||
private $proxyWebsocket = async (req: WebsocketRequest) => {
|
||||
const wrappedSocket = await this._socketProxyProvider.createProxy(req.ws)
|
||||
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.
|
||||
this._codeServerMain.handleUpgrade(req, wrappedSocket as net.Socket)
|
||||
vscodeServer!.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)
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
export function dispose() {
|
||||
vscodeServer?.dispose()
|
||||
socketProxyProvider.stop()
|
||||
}
|
||||
|
Reference in New Issue
Block a user