2020-02-04 20:27:46 +01:00
|
|
|
import * as crypto from "crypto"
|
2020-12-10 22:59:24 +01:00
|
|
|
import { Request, Router } from "express"
|
2020-10-21 01:05:58 +02:00
|
|
|
import { promises as fs } from "fs"
|
2020-02-04 20:27:46 +01:00
|
|
|
import * as path from "path"
|
2020-12-10 22:59:24 +01:00
|
|
|
import qs from "qs"
|
2021-05-03 22:00:54 +02:00
|
|
|
import * as ipc from "../../../typings/ipc"
|
2020-12-10 22:59:24 +01:00
|
|
|
import { Emitter } from "../../common/emitter"
|
|
|
|
import { HttpCode, HttpError } from "../../common/http"
|
|
|
|
import { getFirstString } from "../../common/util"
|
2021-05-04 23:46:08 +02:00
|
|
|
import { Feature } from "../cli"
|
2021-05-05 17:16:01 +02:00
|
|
|
import { isDevMode, rootPath, version } from "../constants"
|
2020-10-21 01:05:58 +02:00
|
|
|
import { authenticated, ensureAuthenticated, redirect, replaceTemplates } from "../http"
|
|
|
|
import { getMediaMime, pathToFsPath } from "../util"
|
|
|
|
import { VscodeProvider } from "../vscode"
|
2020-11-05 19:58:37 +01:00
|
|
|
import { Router as WsRouter } from "../wsRouter"
|
2020-02-06 19:29:19 +01:00
|
|
|
|
2020-10-21 01:05:58 +02:00
|
|
|
export const router = Router()
|
2020-02-04 20:27:46 +01:00
|
|
|
|
2020-10-21 01:05:58 +02:00
|
|
|
const vscode = new VscodeProvider()
|
2020-02-04 20:27:46 +01:00
|
|
|
|
2020-10-21 01:05:58 +02:00
|
|
|
router.get("/", async (req, res) => {
|
2021-06-02 22:21:59 +02:00
|
|
|
const isAuthenticated = await authenticated(req)
|
|
|
|
if (!isAuthenticated) {
|
2020-10-21 01:05:58 +02:00
|
|
|
return redirect(req, res, "login", {
|
2020-11-11 01:14:18 +01:00
|
|
|
// req.baseUrl can be blank if already at the root.
|
|
|
|
to: req.baseUrl && req.baseUrl !== "/" ? req.baseUrl : undefined,
|
2020-02-04 20:27:46 +01:00
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2020-10-21 01:05:58 +02:00
|
|
|
const [content, options] = await Promise.all([
|
|
|
|
await fs.readFile(path.join(rootPath, "src/browser/pages/vscode.html"), "utf8"),
|
2020-11-11 01:20:44 +01:00
|
|
|
(async () => {
|
|
|
|
try {
|
|
|
|
return await vscode.initialize({ args: req.args, remoteAuthority: req.headers.host || "" }, req.query)
|
|
|
|
} catch (error) {
|
2021-05-05 17:16:01 +02:00
|
|
|
const devMessage = isDevMode ? "It might not have finished compiling." : ""
|
2020-10-21 01:05:58 +02:00
|
|
|
throw new Error(`VS Code failed to load. ${devMessage} ${error.message}`)
|
2020-11-11 01:20:44 +01:00
|
|
|
}
|
|
|
|
})(),
|
2020-10-21 01:05:58 +02:00
|
|
|
])
|
|
|
|
|
|
|
|
options.productConfiguration.codeServerVersion = version
|
|
|
|
|
|
|
|
res.send(
|
2021-05-03 22:00:54 +02:00
|
|
|
replaceTemplates<ipc.Options>(
|
2020-10-21 01:05:58 +02:00
|
|
|
req,
|
|
|
|
// Uncomment prod blocks if not in development. TODO: Would this be
|
|
|
|
// better as a build step? Or maintain two HTML files again?
|
2021-05-05 17:16:01 +02:00
|
|
|
!isDevMode ? content.replace(/<!-- PROD_ONLY/g, "").replace(/END_PROD_ONLY -->/g, "") : content,
|
2020-10-21 01:05:58 +02:00
|
|
|
{
|
2021-05-03 22:00:54 +02:00
|
|
|
authed: req.args.auth !== "none",
|
2020-11-25 17:37:40 +01:00
|
|
|
disableUpdateCheck: !!req.args["disable-update-check"],
|
2020-10-21 01:05:58 +02:00
|
|
|
},
|
|
|
|
)
|
2020-02-27 21:56:14 +01:00
|
|
|
.replace(`"{{REMOTE_USER_DATA_URI}}"`, `'${JSON.stringify(options.remoteUserDataUri)}'`)
|
|
|
|
.replace(`"{{PRODUCT_CONFIGURATION}}"`, `'${JSON.stringify(options.productConfiguration)}'`)
|
|
|
|
.replace(`"{{WORKBENCH_WEB_CONFIGURATION}}"`, `'${JSON.stringify(options.workbenchWebConfiguration)}'`)
|
2020-10-21 01:05:58 +02:00
|
|
|
.replace(`"{{NLS_CONFIGURATION}}"`, `'${JSON.stringify(options.nlsConfiguration)}'`),
|
|
|
|
)
|
|
|
|
})
|
|
|
|
|
2020-11-03 23:30:45 +01:00
|
|
|
/**
|
|
|
|
* TODO: Might currently be unused.
|
|
|
|
*/
|
2020-11-03 23:45:03 +01:00
|
|
|
router.get("/resource(/*)?", ensureAuthenticated, async (req, res) => {
|
2021-07-19 23:22:42 +02:00
|
|
|
const path = getFirstString(req.query.path)
|
|
|
|
if (path) {
|
|
|
|
res.set("Content-Type", getMediaMime(path))
|
|
|
|
res.send(await fs.readFile(pathToFsPath(path)))
|
2020-10-21 01:05:58 +02:00
|
|
|
}
|
|
|
|
})
|
|
|
|
|
2020-11-03 23:30:45 +01:00
|
|
|
/**
|
|
|
|
* Used by VS Code to load files.
|
|
|
|
*/
|
2020-11-03 23:45:03 +01:00
|
|
|
router.get("/vscode-remote-resource(/*)?", ensureAuthenticated, async (req, res) => {
|
2021-07-19 23:22:42 +02:00
|
|
|
const path = getFirstString(req.query.path)
|
|
|
|
if (path) {
|
|
|
|
res.set("Content-Type", getMediaMime(path))
|
|
|
|
res.send(await fs.readFile(pathToFsPath(path)))
|
2020-02-04 20:27:46 +01:00
|
|
|
}
|
2020-10-21 01:05:58 +02:00
|
|
|
})
|
2020-02-05 21:21:59 +01:00
|
|
|
|
2020-11-03 23:30:45 +01:00
|
|
|
/**
|
|
|
|
* VS Code webviews use these paths to load files and to load webview assets
|
|
|
|
* like HTML and JavaScript.
|
|
|
|
*/
|
2020-11-03 23:45:03 +01:00
|
|
|
router.get("/webview/*", ensureAuthenticated, async (req, res) => {
|
2020-10-21 01:05:58 +02:00
|
|
|
res.set("Content-Type", getMediaMime(req.path))
|
2020-10-27 23:17:05 +01:00
|
|
|
if (/^vscode-resource/.test(req.params[0])) {
|
|
|
|
return res.send(await fs.readFile(req.params[0].replace(/^vscode-resource(\/file)?/, "")))
|
2020-02-06 19:29:19 +01:00
|
|
|
}
|
2020-10-21 01:05:58 +02:00
|
|
|
return res.send(
|
|
|
|
await fs.readFile(path.join(vscode.vsRootPath, "out/vs/workbench/contrib/webview/browser/pre", req.params[0])),
|
|
|
|
)
|
|
|
|
})
|
2020-11-05 19:58:37 +01:00
|
|
|
|
2020-12-10 22:59:24 +01:00
|
|
|
interface Callback {
|
|
|
|
uri: {
|
|
|
|
scheme: string
|
|
|
|
authority?: string
|
|
|
|
path?: string
|
|
|
|
query?: string
|
|
|
|
fragment?: string
|
|
|
|
}
|
|
|
|
timeout: NodeJS.Timeout
|
|
|
|
}
|
|
|
|
|
|
|
|
const callbacks = new Map<string, Callback>()
|
|
|
|
const callbackEmitter = new Emitter<{ id: string; callback: Callback }>()
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get vscode-requestId from the query and throw if it's missing or invalid.
|
|
|
|
*/
|
|
|
|
const getRequestId = (req: Request): string => {
|
|
|
|
if (!req.query["vscode-requestId"]) {
|
|
|
|
throw new HttpError("vscode-requestId is missing", HttpCode.BadRequest)
|
|
|
|
}
|
|
|
|
|
|
|
|
if (typeof req.query["vscode-requestId"] !== "string") {
|
|
|
|
throw new HttpError("vscode-requestId is not a string", HttpCode.BadRequest)
|
|
|
|
}
|
|
|
|
|
|
|
|
return req.query["vscode-requestId"]
|
|
|
|
}
|
|
|
|
|
|
|
|
// Matches VS Code's fetch timeout.
|
|
|
|
const fetchTimeout = 5 * 60 * 1000
|
|
|
|
|
|
|
|
// The callback endpoints are used during authentication. A URI is stored on
|
|
|
|
// /callback and then fetched later on /fetch-callback.
|
|
|
|
// See ../../../lib/vscode/resources/web/code-web.js
|
|
|
|
router.get("/callback", ensureAuthenticated, async (req, res) => {
|
|
|
|
const uriKeys = [
|
|
|
|
"vscode-requestId",
|
|
|
|
"vscode-scheme",
|
|
|
|
"vscode-authority",
|
|
|
|
"vscode-path",
|
|
|
|
"vscode-query",
|
|
|
|
"vscode-fragment",
|
|
|
|
]
|
|
|
|
|
|
|
|
const id = getRequestId(req)
|
|
|
|
|
|
|
|
// Move any query variables that aren't URI keys into the URI's query
|
|
|
|
// (importantly, this will include the code for oauth).
|
|
|
|
const query: qs.ParsedQs = {}
|
|
|
|
for (const key in req.query) {
|
|
|
|
if (!uriKeys.includes(key)) {
|
|
|
|
query[key] = req.query[key]
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
const callback = {
|
|
|
|
uri: {
|
|
|
|
scheme: getFirstString(req.query["vscode-scheme"]) || "code-oss",
|
|
|
|
authority: getFirstString(req.query["vscode-authority"]),
|
|
|
|
path: getFirstString(req.query["vscode-path"]),
|
2020-12-18 18:21:32 +01:00
|
|
|
query: (getFirstString(req.query.query) || "") + "&" + qs.stringify(query),
|
2020-12-10 22:59:24 +01:00
|
|
|
fragment: getFirstString(req.query["vscode-fragment"]),
|
|
|
|
},
|
|
|
|
// Make sure the map doesn't leak if nothing fetches this URI.
|
|
|
|
timeout: setTimeout(() => callbacks.delete(id), fetchTimeout),
|
|
|
|
}
|
|
|
|
|
|
|
|
callbacks.set(id, callback)
|
|
|
|
callbackEmitter.emit({ id, callback })
|
|
|
|
|
|
|
|
res.sendFile(path.join(rootPath, "lib/vscode/resources/web/callback.html"))
|
|
|
|
})
|
|
|
|
|
|
|
|
router.get("/fetch-callback", ensureAuthenticated, async (req, res) => {
|
|
|
|
const id = getRequestId(req)
|
|
|
|
|
|
|
|
const send = (callback: Callback) => {
|
|
|
|
clearTimeout(callback.timeout)
|
|
|
|
callbacks.delete(id)
|
|
|
|
res.json(callback.uri)
|
|
|
|
}
|
|
|
|
|
|
|
|
const callback = callbacks.get(id)
|
|
|
|
if (callback) {
|
|
|
|
return send(callback)
|
|
|
|
}
|
|
|
|
|
|
|
|
// VS Code will try again if the route returns no content but it seems more
|
|
|
|
// efficient to just wait on this request for as long as possible?
|
|
|
|
const handler = callbackEmitter.event(({ id: emitId, callback }) => {
|
|
|
|
if (id === emitId) {
|
|
|
|
handler.dispose()
|
|
|
|
send(callback)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
// If the client closes the connection.
|
|
|
|
req.on("close", () => handler.dispose())
|
|
|
|
})
|
|
|
|
|
2020-11-05 19:58:37 +01:00
|
|
|
export const wsRouter = WsRouter()
|
|
|
|
|
|
|
|
wsRouter.ws("/", ensureAuthenticated, async (req) => {
|
|
|
|
const magic = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
|
|
|
|
const reply = crypto
|
|
|
|
.createHash("sha1")
|
|
|
|
.update(req.headers["sec-websocket-key"] + magic)
|
|
|
|
.digest("base64")
|
2021-03-10 20:14:24 +01:00
|
|
|
|
|
|
|
const responseHeaders = [
|
|
|
|
"HTTP/1.1 101 Switching Protocols",
|
|
|
|
"Upgrade: websocket",
|
|
|
|
"Connection: Upgrade",
|
|
|
|
`Sec-WebSocket-Accept: ${reply}`,
|
|
|
|
]
|
|
|
|
|
2021-05-04 23:46:08 +02:00
|
|
|
// See if the browser reports it supports web socket compression.
|
2021-03-10 20:14:24 +01:00
|
|
|
// TODO: Parse this header properly.
|
2021-03-02 23:42:25 +01:00
|
|
|
const extensions = req.headers["sec-websocket-extensions"]
|
2021-05-04 23:46:08 +02:00
|
|
|
const isCompressionSupported = extensions ? extensions.includes("permessage-deflate") : false
|
|
|
|
|
|
|
|
// TODO: For now we only use compression if the user enables it.
|
|
|
|
const isCompressionEnabled = !!req.args.enable?.includes(Feature.PermessageDeflate)
|
|
|
|
|
|
|
|
const useCompression = isCompressionEnabled && isCompressionSupported
|
|
|
|
if (useCompression) {
|
|
|
|
// This response header tells the browser the server supports compression.
|
2021-03-29 19:59:36 +02:00
|
|
|
responseHeaders.push("Sec-WebSocket-Extensions: permessage-deflate; server_max_window_bits=15")
|
|
|
|
}
|
2021-03-10 20:14:24 +01:00
|
|
|
|
|
|
|
req.ws.write(responseHeaders.join("\r\n") + "\r\n\r\n")
|
|
|
|
|
2021-05-04 23:46:08 +02:00
|
|
|
await vscode.sendWebsocket(req.ws, req.query, useCompression)
|
2020-11-05 19:58:37 +01:00
|
|
|
})
|