2021-11-15 02:03:20 +01:00
|
|
|
import { logger } from "@coder/logger"
|
2023-09-26 18:35:41 +02:00
|
|
|
import * as crypto from "crypto"
|
2021-09-30 05:14:56 +02:00
|
|
|
import * as express from "express"
|
2023-09-26 18:35:41 +02:00
|
|
|
import { promises as fs } from "fs"
|
2022-03-22 21:07:14 +01:00
|
|
|
import * as http from "http"
|
|
|
|
import * as net from "net"
|
2022-03-02 23:36:38 +01:00
|
|
|
import * as path from "path"
|
2021-11-15 02:03:20 +01:00
|
|
|
import { WebsocketRequest } from "../../../typings/pluginapi"
|
|
|
|
import { logError } from "../../common/util"
|
2022-03-22 21:07:14 +01:00
|
|
|
import { CodeArgs, toCodeArgs } from "../cli"
|
2021-12-17 20:06:52 +01:00
|
|
|
import { isDevMode } from "../constants"
|
2023-03-03 10:12:34 +01:00
|
|
|
import { authenticated, ensureAuthenticated, ensureOrigin, redirect, replaceTemplates, self } from "../http"
|
2022-02-15 22:51:42 +01:00
|
|
|
import { SocketProxyProvider } from "../socket"
|
2022-03-02 23:36:38 +01:00
|
|
|
import { isFile, loadAMDModule } from "../util"
|
2021-11-15 02:03:20 +01:00
|
|
|
import { Router as WsRouter } from "../wsRouter"
|
2022-03-22 21:07:14 +01:00
|
|
|
|
|
|
|
/**
|
|
|
|
* This is the API of Code's web client server. code-server delegates requests
|
|
|
|
* to Code here.
|
|
|
|
*/
|
|
|
|
export interface IServerAPI {
|
|
|
|
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>
|
2021-09-30 05:14:56 +02:00
|
|
|
|
2021-11-15 02:03:20 +01:00
|
|
|
export class CodeServerRouteWrapper {
|
|
|
|
/** Assigned in `ensureCodeServerLoaded` */
|
2022-03-22 21:07:14 +01:00
|
|
|
private _codeServerMain!: IServerAPI
|
2021-11-15 02:03:20 +01:00
|
|
|
private _wsRouterWrapper = WsRouter()
|
2022-02-15 22:51:42 +01:00
|
|
|
private _socketProxyProvider = new SocketProxyProvider()
|
2021-11-15 02:03:20 +01:00
|
|
|
public router = express.Router()
|
2023-09-26 18:35:41 +02:00
|
|
|
private mintKeyPromise: Promise<Buffer> | undefined
|
2021-11-10 06:28:31 +01:00
|
|
|
|
2021-11-15 02:03:20 +01:00
|
|
|
public get wsRouter() {
|
|
|
|
return this._wsRouterWrapper.router
|
|
|
|
}
|
2020-12-10 22:59:24 +01:00
|
|
|
|
2021-11-15 02:03:20 +01:00
|
|
|
//#region Route Handlers
|
2020-12-10 22:59:24 +01:00
|
|
|
|
2022-03-22 21:07:14 +01:00
|
|
|
private manifest: express.Handler = async (req, res, next) => {
|
2023-03-22 02:02:31 +01:00
|
|
|
const appName = req.args["app-name"] || "code-server"
|
2022-03-22 21:07:14 +01:00
|
|
|
res.writeHead(200, { "Content-Type": "application/manifest+json" })
|
|
|
|
|
|
|
|
return res.end(
|
|
|
|
replaceTemplates(
|
|
|
|
req,
|
|
|
|
JSON.stringify(
|
|
|
|
{
|
2023-03-22 02:02:31 +01:00
|
|
|
name: appName,
|
|
|
|
short_name: appName,
|
2022-03-22 21:07:14 +01:00
|
|
|
start_url: ".",
|
|
|
|
display: "fullscreen",
|
2023-10-02 20:03:37 +02:00
|
|
|
display_override: ["window-controls-overlay"],
|
2022-03-22 21:07:14 +01:00
|
|
|
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,
|
|
|
|
),
|
|
|
|
),
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2023-09-26 18:35:41 +02:00
|
|
|
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)
|
|
|
|
}
|
|
|
|
|
2021-11-15 02:03:20 +01:00
|
|
|
private $root: express.Handler = async (req, res, next) => {
|
2021-10-28 22:27:17 +02:00
|
|
|
const isAuthenticated = await authenticated(req)
|
2022-03-02 23:36:38 +01:00
|
|
|
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
|
2021-11-15 02:03:20 +01:00
|
|
|
|
2021-10-28 22:27:17 +02:00
|
|
|
if (!isAuthenticated) {
|
2021-12-17 20:06:52 +01:00
|
|
|
const to = self(req)
|
2021-12-08 22:52:15 +01:00
|
|
|
return redirect(req, res, "login", {
|
2021-12-17 20:06:52 +01:00
|
|
|
to: to !== "/" ? to : undefined,
|
2021-10-28 22:27:17 +02:00
|
|
|
})
|
|
|
|
}
|
2021-11-15 02:03:20 +01:00
|
|
|
|
2022-03-02 23:36:38 +01:00
|
|
|
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
|
2021-12-17 20:06:52 +01:00
|
|
|
|
|
|
|
// Redirect to the last folder/workspace if nothing else is opened.
|
2022-03-02 23:36:38 +01:00
|
|
|
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) {
|
2021-12-17 20:06:52 +01:00
|
|
|
return redirect(req, res, to, {
|
2022-03-02 23:36:38 +01:00
|
|
|
folder,
|
|
|
|
workspace,
|
2021-12-17 20:06:52 +01:00
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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 })
|
|
|
|
|
2021-10-28 22:27:17 +02:00
|
|
|
next()
|
2021-11-15 02:03:20 +01:00
|
|
|
}
|
2021-10-28 22:27:17 +02:00
|
|
|
|
2021-11-15 02:03:20 +01:00
|
|
|
private $proxyRequest: express.Handler = async (req, res, next) => {
|
|
|
|
this._codeServerMain.handleRequest(req, res)
|
|
|
|
}
|
2021-03-10 20:14:24 +01:00
|
|
|
|
2021-11-15 02:03:20 +01:00
|
|
|
private $proxyWebsocket = async (req: WebsocketRequest) => {
|
2022-02-15 22:51:42 +01:00
|
|
|
const wrappedSocket = await this._socketProxyProvider.createProxy(req.ws)
|
2022-08-10 23:15:52 +02:00
|
|
|
// 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)
|
2021-05-04 23:46:08 +02:00
|
|
|
|
2022-02-15 22:51:42 +01:00
|
|
|
req.ws.resume()
|
2021-11-15 02:03:20 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
//#endregion
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Fetches a code server instance asynchronously to avoid an initial memory overhead.
|
|
|
|
*/
|
|
|
|
private ensureCodeServerLoaded: express.Handler = async (req, _res, next) => {
|
|
|
|
if (this._codeServerMain) {
|
2021-11-19 22:03:40 +01:00
|
|
|
// Already loaded...
|
2021-11-15 02:03:20 +01:00
|
|
|
return next()
|
|
|
|
}
|
|
|
|
|
2021-11-19 22:03:40 +01:00
|
|
|
// Create the server...
|
|
|
|
|
2021-11-15 02:03:20 +01:00
|
|
|
const { args } = req
|
|
|
|
|
2022-03-22 21:07:14 +01:00
|
|
|
// See ../../../lib/vscode/src/vs/server/node/server.main.ts:72.
|
|
|
|
const createVSServer = await loadAMDModule<CreateServer>("vs/server/node/server.main", "createServer")
|
2021-11-15 02:03:20 +01:00
|
|
|
|
|
|
|
try {
|
2022-01-28 02:00:06 +01:00
|
|
|
this._codeServerMain = await createVSServer(null, {
|
2022-03-22 21:07:14 +01:00
|
|
|
...(await toCodeArgs(args)),
|
|
|
|
"without-connection-token": true,
|
2022-01-28 02:00:06 +01:00
|
|
|
})
|
2021-12-17 20:06:52 +01:00
|
|
|
} 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)
|
2021-11-15 02:03:20 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
return next()
|
|
|
|
}
|
|
|
|
|
|
|
|
constructor() {
|
|
|
|
this.router.get("/", this.ensureCodeServerLoaded, this.$root)
|
2022-08-09 19:54:00 +02:00
|
|
|
this.router.get("/manifest.json", this.manifest)
|
2023-09-26 18:35:41 +02:00
|
|
|
this.router.post("/mint-key", this.mintKey)
|
2021-11-15 02:03:20 +01:00
|
|
|
this.router.all("*", ensureAuthenticated, this.ensureCodeServerLoaded, this.$proxyRequest)
|
2023-03-03 10:12:34 +01:00
|
|
|
this._wsRouterWrapper.ws("*", ensureOrigin, ensureAuthenticated, this.ensureCodeServerLoaded, this.$proxyWebsocket)
|
2021-11-15 02:03:20 +01:00
|
|
|
}
|
2021-05-04 23:46:08 +02:00
|
|
|
|
2021-11-15 02:03:20 +01:00
|
|
|
dispose() {
|
|
|
|
this._codeServerMain?.dispose()
|
2022-02-15 22:51:42 +01:00
|
|
|
this._socketProxyProvider.stop()
|
2021-03-29 19:59:36 +02:00
|
|
|
}
|
2021-09-30 05:14:56 +02:00
|
|
|
}
|