Move providers from app
to routes
This commit is contained in:
22
src/node/routes/health.ts
Normal file
22
src/node/routes/health.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import { Heart } from "../heart"
|
||||
import { HttpProvider, HttpProviderOptions, HttpResponse } from "../http"
|
||||
|
||||
/**
|
||||
* Check the heartbeat.
|
||||
*/
|
||||
export class HealthHttpProvider extends HttpProvider {
|
||||
public constructor(options: HttpProviderOptions, private readonly heart: Heart) {
|
||||
super(options)
|
||||
}
|
||||
|
||||
public async handleRequest(): Promise<HttpResponse> {
|
||||
return {
|
||||
cache: false,
|
||||
mime: "application/json",
|
||||
content: {
|
||||
status: this.heart.alive() ? "alive" : "expired",
|
||||
lastHeartbeat: this.heart.lastHeartbeat,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
145
src/node/routes/login.ts
Normal file
145
src/node/routes/login.ts
Normal file
@ -0,0 +1,145 @@
|
||||
import * as http from "http"
|
||||
import * as limiter from "limiter"
|
||||
import * as querystring from "querystring"
|
||||
import { HttpCode, HttpError } from "../../common/http"
|
||||
import { AuthType } from "../cli"
|
||||
import { HttpProvider, HttpProviderOptions, HttpResponse, Route } from "../http"
|
||||
import { hash, humanPath } from "../util"
|
||||
|
||||
interface LoginPayload {
|
||||
password?: string
|
||||
/**
|
||||
* Since we must set a cookie with an absolute path, we need to know the full
|
||||
* base path.
|
||||
*/
|
||||
base?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Login HTTP provider.
|
||||
*/
|
||||
export class LoginHttpProvider extends HttpProvider {
|
||||
public constructor(
|
||||
options: HttpProviderOptions,
|
||||
private readonly configFile: string,
|
||||
private readonly envPassword: boolean,
|
||||
) {
|
||||
super(options)
|
||||
}
|
||||
|
||||
public async handleRequest(route: Route, request: http.IncomingMessage): Promise<HttpResponse> {
|
||||
if (this.options.auth !== AuthType.Password || !this.isRoot(route)) {
|
||||
throw new HttpError("Not found", HttpCode.NotFound)
|
||||
}
|
||||
switch (route.base) {
|
||||
case "/":
|
||||
switch (request.method) {
|
||||
case "POST":
|
||||
this.ensureMethod(request, ["GET", "POST"])
|
||||
return this.tryLogin(route, request)
|
||||
default:
|
||||
this.ensureMethod(request)
|
||||
if (this.authenticated(request)) {
|
||||
return {
|
||||
redirect: (Array.isArray(route.query.to) ? route.query.to[0] : route.query.to) || "/",
|
||||
query: { to: undefined },
|
||||
}
|
||||
}
|
||||
return this.getRoot(route)
|
||||
}
|
||||
}
|
||||
|
||||
throw new HttpError("Not found", HttpCode.NotFound)
|
||||
}
|
||||
|
||||
public async getRoot(route: Route, error?: Error): Promise<HttpResponse> {
|
||||
const response = await this.getUtf8Resource(this.rootPath, "src/browser/pages/login.html")
|
||||
response.content = response.content.replace(/{{ERROR}}/, error ? `<div class="error">${error.message}</div>` : "")
|
||||
let passwordMsg = `Check the config file at ${humanPath(this.configFile)} for the password.`
|
||||
if (this.envPassword) {
|
||||
passwordMsg = "Password was set from $PASSWORD."
|
||||
}
|
||||
response.content = response.content.replace(/{{PASSWORD_MSG}}/g, passwordMsg)
|
||||
return this.replaceTemplates(route, response)
|
||||
}
|
||||
|
||||
private readonly limiter = new RateLimiter()
|
||||
|
||||
/**
|
||||
* Try logging in. On failure, show the login page with an error.
|
||||
*/
|
||||
private async tryLogin(route: Route, request: http.IncomingMessage): Promise<HttpResponse> {
|
||||
// Already authenticated via cookies?
|
||||
const providedPassword = this.authenticated(request)
|
||||
if (providedPassword) {
|
||||
return { code: HttpCode.Ok }
|
||||
}
|
||||
|
||||
try {
|
||||
if (!this.limiter.try()) {
|
||||
throw new Error("Login rate limited!")
|
||||
}
|
||||
|
||||
const data = await this.getData(request)
|
||||
const payload = data ? querystring.parse(data) : {}
|
||||
return await this.login(payload, route, request)
|
||||
} catch (error) {
|
||||
return this.getRoot(route, error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a cookie if the user is authenticated otherwise throw an error.
|
||||
*/
|
||||
private async login(payload: LoginPayload, route: Route, request: http.IncomingMessage): Promise<HttpResponse> {
|
||||
const password = this.authenticated(request, {
|
||||
key: typeof payload.password === "string" ? [hash(payload.password)] : undefined,
|
||||
})
|
||||
|
||||
if (password) {
|
||||
return {
|
||||
redirect: (Array.isArray(route.query.to) ? route.query.to[0] : route.query.to) || "/",
|
||||
query: { to: undefined },
|
||||
cookie:
|
||||
typeof password === "string"
|
||||
? {
|
||||
key: "key",
|
||||
value: password,
|
||||
path: payload.base,
|
||||
}
|
||||
: undefined,
|
||||
}
|
||||
}
|
||||
|
||||
// Only log if it was an actual login attempt.
|
||||
if (payload && payload.password) {
|
||||
console.error(
|
||||
"Failed login attempt",
|
||||
JSON.stringify({
|
||||
xForwardedFor: request.headers["x-forwarded-for"],
|
||||
remoteAddress: request.connection.remoteAddress,
|
||||
userAgent: request.headers["user-agent"],
|
||||
timestamp: Math.floor(new Date().getTime() / 1000),
|
||||
}),
|
||||
)
|
||||
|
||||
throw new Error("Incorrect password")
|
||||
}
|
||||
|
||||
throw new Error("Missing password")
|
||||
}
|
||||
}
|
||||
|
||||
// RateLimiter wraps around the limiter library for logins.
|
||||
// It allows 2 logins every minute and 12 logins every hour.
|
||||
class RateLimiter {
|
||||
private readonly minuteLimiter = new limiter.RateLimiter(2, "minute")
|
||||
private readonly hourLimiter = new limiter.RateLimiter(12, "hour")
|
||||
|
||||
public try(): boolean {
|
||||
if (this.minuteLimiter.tryRemoveTokens(1)) {
|
||||
return true
|
||||
}
|
||||
return this.hourLimiter.tryRemoveTokens(1)
|
||||
}
|
||||
}
|
43
src/node/routes/proxy.ts
Normal file
43
src/node/routes/proxy.ts
Normal file
@ -0,0 +1,43 @@
|
||||
import * as http from "http"
|
||||
import { HttpCode, HttpError } from "../../common/http"
|
||||
import { HttpProvider, HttpResponse, Route, WsResponse } from "../http"
|
||||
|
||||
/**
|
||||
* Proxy HTTP provider.
|
||||
*/
|
||||
export class ProxyHttpProvider extends HttpProvider {
|
||||
public async handleRequest(route: Route, request: http.IncomingMessage): Promise<HttpResponse> {
|
||||
if (!this.authenticated(request)) {
|
||||
if (this.isRoot(route)) {
|
||||
return { redirect: "/login", query: { to: route.fullPath } }
|
||||
}
|
||||
throw new HttpError("Unauthorized", HttpCode.Unauthorized)
|
||||
}
|
||||
|
||||
// Ensure there is a trailing slash so relative paths work correctly.
|
||||
if (this.isRoot(route) && !route.fullPath.endsWith("/")) {
|
||||
return {
|
||||
redirect: `${route.fullPath}/`,
|
||||
}
|
||||
}
|
||||
|
||||
const port = route.base.replace(/^\//, "")
|
||||
return {
|
||||
proxy: {
|
||||
strip: `${route.providerBase}/${port}`,
|
||||
port,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
public async handleWebSocket(route: Route, request: http.IncomingMessage): Promise<WsResponse> {
|
||||
this.ensureAuthenticated(request)
|
||||
const port = route.base.replace(/^\//, "")
|
||||
return {
|
||||
proxy: {
|
||||
strip: `${route.providerBase}/${port}`,
|
||||
port,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
73
src/node/routes/static.ts
Normal file
73
src/node/routes/static.ts
Normal file
@ -0,0 +1,73 @@
|
||||
import { field, logger } from "@coder/logger"
|
||||
import * as http from "http"
|
||||
import * as path from "path"
|
||||
import { Readable } from "stream"
|
||||
import * as tarFs from "tar-fs"
|
||||
import * as zlib from "zlib"
|
||||
import { HttpProvider, HttpResponse, Route } from "../http"
|
||||
import { pathToFsPath } from "../util"
|
||||
|
||||
/**
|
||||
* Static file HTTP provider. Static requests do not require authentication if
|
||||
* the resource is in the application's directory except requests to serve a
|
||||
* directory as a tar which always requires authentication.
|
||||
*/
|
||||
export class StaticHttpProvider extends HttpProvider {
|
||||
public async handleRequest(route: Route, request: http.IncomingMessage): Promise<HttpResponse> {
|
||||
this.ensureMethod(request)
|
||||
|
||||
if (typeof route.query.tar === "string") {
|
||||
this.ensureAuthenticated(request)
|
||||
return this.getTarredResource(request, pathToFsPath(route.query.tar))
|
||||
}
|
||||
|
||||
const response = await this.getReplacedResource(request, route)
|
||||
if (!this.isDev) {
|
||||
response.cache = true
|
||||
}
|
||||
return response
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a resource with variables replaced where necessary.
|
||||
*/
|
||||
protected async getReplacedResource(request: http.IncomingMessage, route: Route): Promise<HttpResponse> {
|
||||
// The first part is always the commit (for caching purposes).
|
||||
const split = route.requestPath.split("/").slice(1)
|
||||
|
||||
const resourcePath = path.resolve("/", ...split)
|
||||
|
||||
// Make sure it's in code-server or a plugin.
|
||||
const validPaths = [this.rootPath, process.env.PLUGIN_DIR]
|
||||
if (!validPaths.find((p) => p && resourcePath.startsWith(p))) {
|
||||
this.ensureAuthenticated(request)
|
||||
}
|
||||
|
||||
switch (split[split.length - 1]) {
|
||||
case "manifest.json": {
|
||||
const response = await this.getUtf8Resource(resourcePath)
|
||||
return this.replaceTemplates(route, response)
|
||||
}
|
||||
}
|
||||
return this.getResource(resourcePath)
|
||||
}
|
||||
|
||||
/**
|
||||
* Tar up and stream a directory.
|
||||
*/
|
||||
private async getTarredResource(request: http.IncomingMessage, ...parts: string[]): Promise<HttpResponse> {
|
||||
const filePath = path.join(...parts)
|
||||
let stream: Readable = tarFs.pack(filePath)
|
||||
const headers: http.OutgoingHttpHeaders = {}
|
||||
if (request.headers["accept-encoding"] && request.headers["accept-encoding"].includes("gzip")) {
|
||||
logger.debug("gzipping tar", field("filePath", filePath))
|
||||
const compress = zlib.createGzip()
|
||||
stream.pipe(compress)
|
||||
stream.on("error", (error) => compress.destroy(error))
|
||||
stream.on("close", () => compress.end())
|
||||
stream = compress
|
||||
headers["content-encoding"] = "gzip"
|
||||
}
|
||||
return { stream, filePath, mime: "application/x-tar", cache: true, headers }
|
||||
}
|
||||
}
|
172
src/node/routes/update.ts
Normal file
172
src/node/routes/update.ts
Normal file
@ -0,0 +1,172 @@
|
||||
import { field, logger } from "@coder/logger"
|
||||
import * as http from "http"
|
||||
import * as https from "https"
|
||||
import * as path from "path"
|
||||
import * as semver from "semver"
|
||||
import * as url from "url"
|
||||
import { HttpCode, HttpError } from "../../common/http"
|
||||
import { HttpProvider, HttpProviderOptions, HttpResponse, Route } from "../http"
|
||||
import { settings as globalSettings, SettingsProvider, UpdateSettings } from "../settings"
|
||||
|
||||
export interface Update {
|
||||
checked: number
|
||||
version: string
|
||||
}
|
||||
|
||||
export interface LatestResponse {
|
||||
name: string
|
||||
}
|
||||
|
||||
/**
|
||||
* HTTP provider for checking updates (does not download/install them).
|
||||
*/
|
||||
export class UpdateHttpProvider extends HttpProvider {
|
||||
private update?: Promise<Update>
|
||||
private updateInterval = 1000 * 60 * 60 * 24 // Milliseconds between update checks.
|
||||
|
||||
public constructor(
|
||||
options: HttpProviderOptions,
|
||||
public readonly enabled: boolean,
|
||||
/**
|
||||
* The URL for getting the latest version of code-server. Should return JSON
|
||||
* that fulfills `LatestResponse`.
|
||||
*/
|
||||
private readonly latestUrl = "https://api.github.com/repos/cdr/code-server/releases/latest",
|
||||
/**
|
||||
* Update information will be stored here. If not provided, the global
|
||||
* settings will be used.
|
||||
*/
|
||||
private readonly settings: SettingsProvider<UpdateSettings> = globalSettings,
|
||||
) {
|
||||
super(options)
|
||||
}
|
||||
|
||||
public async handleRequest(route: Route, request: http.IncomingMessage): Promise<HttpResponse> {
|
||||
this.ensureAuthenticated(request)
|
||||
this.ensureMethod(request)
|
||||
|
||||
if (!this.isRoot(route)) {
|
||||
throw new HttpError("Not found", HttpCode.NotFound)
|
||||
}
|
||||
|
||||
if (!this.enabled) {
|
||||
throw new Error("update checks are disabled")
|
||||
}
|
||||
|
||||
switch (route.base) {
|
||||
case "/check":
|
||||
case "/": {
|
||||
const update = await this.getUpdate(route.base === "/check")
|
||||
return {
|
||||
content: {
|
||||
...update,
|
||||
isLatest: this.isLatestVersion(update),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw new HttpError("Not found", HttpCode.NotFound)
|
||||
}
|
||||
|
||||
/**
|
||||
* Query for and return the latest update.
|
||||
*/
|
||||
public async getUpdate(force?: boolean): Promise<Update> {
|
||||
// Don't run multiple requests at a time.
|
||||
if (!this.update) {
|
||||
this.update = this._getUpdate(force)
|
||||
this.update.then(() => (this.update = undefined))
|
||||
}
|
||||
|
||||
return this.update
|
||||
}
|
||||
|
||||
private async _getUpdate(force?: boolean): Promise<Update> {
|
||||
const now = Date.now()
|
||||
try {
|
||||
let { update } = !force ? await this.settings.read() : { update: undefined }
|
||||
if (!update || update.checked + this.updateInterval < now) {
|
||||
const buffer = await this.request(this.latestUrl)
|
||||
const data = JSON.parse(buffer.toString()) as LatestResponse
|
||||
update = { checked: now, version: data.name }
|
||||
await this.settings.write({ update })
|
||||
}
|
||||
logger.debug("got latest version", field("latest", update.version))
|
||||
return update
|
||||
} catch (error) {
|
||||
logger.error("Failed to get latest version", field("error", error.message))
|
||||
return {
|
||||
checked: now,
|
||||
version: "unknown",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public get currentVersion(): string {
|
||||
return require(path.resolve(__dirname, "../../../package.json")).version
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if the currently installed version is the latest.
|
||||
*/
|
||||
public isLatestVersion(latest: Update): boolean {
|
||||
const version = this.currentVersion
|
||||
logger.debug("comparing versions", field("current", version), field("latest", latest.version))
|
||||
try {
|
||||
return latest.version === version || semver.lt(latest.version, version)
|
||||
} catch (error) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
private async request(uri: string): Promise<Buffer> {
|
||||
const response = await this.requestResponse(uri)
|
||||
return new Promise((resolve, reject) => {
|
||||
const chunks: Buffer[] = []
|
||||
let bufferLength = 0
|
||||
response.on("data", (chunk) => {
|
||||
bufferLength += chunk.length
|
||||
chunks.push(chunk)
|
||||
})
|
||||
response.on("error", reject)
|
||||
response.on("end", () => {
|
||||
resolve(Buffer.concat(chunks, bufferLength))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
private async requestResponse(uri: string): Promise<http.IncomingMessage> {
|
||||
let redirects = 0
|
||||
const maxRedirects = 10
|
||||
return new Promise((resolve, reject) => {
|
||||
const request = (uri: string): void => {
|
||||
logger.debug("Making request", field("uri", uri))
|
||||
const httpx = uri.startsWith("https") ? https : http
|
||||
const client = httpx.get(uri, { headers: { "User-Agent": "code-server" } }, (response) => {
|
||||
if (
|
||||
response.statusCode &&
|
||||
response.statusCode >= 300 &&
|
||||
response.statusCode < 400 &&
|
||||
response.headers.location
|
||||
) {
|
||||
++redirects
|
||||
if (redirects > maxRedirects) {
|
||||
return reject(new Error("reached max redirects"))
|
||||
}
|
||||
response.destroy()
|
||||
return request(url.resolve(uri, response.headers.location))
|
||||
}
|
||||
|
||||
if (!response.statusCode || response.statusCode < 200 || response.statusCode >= 400) {
|
||||
return reject(new Error(`${uri}: ${response.statusCode || "500"}`))
|
||||
}
|
||||
|
||||
resolve(response)
|
||||
})
|
||||
client.on("error", reject)
|
||||
}
|
||||
request(uri)
|
||||
})
|
||||
}
|
||||
}
|
237
src/node/routes/vscode.ts
Normal file
237
src/node/routes/vscode.ts
Normal file
@ -0,0 +1,237 @@
|
||||
import { field, logger } from "@coder/logger"
|
||||
import * as cp from "child_process"
|
||||
import * as crypto from "crypto"
|
||||
import * as fs from "fs-extra"
|
||||
import * as http from "http"
|
||||
import * as net from "net"
|
||||
import * as path from "path"
|
||||
import {
|
||||
CodeServerMessage,
|
||||
Options,
|
||||
StartPath,
|
||||
VscodeMessage,
|
||||
VscodeOptions,
|
||||
WorkbenchOptions,
|
||||
} from "../../../lib/vscode/src/vs/server/ipc"
|
||||
import { HttpCode, HttpError } from "../../common/http"
|
||||
import { arrayify, generateUuid } from "../../common/util"
|
||||
import { Args } from "../cli"
|
||||
import { HttpProvider, HttpProviderOptions, HttpResponse, Route } from "../http"
|
||||
import { settings } from "../settings"
|
||||
import { pathToFsPath } from "../util"
|
||||
|
||||
export class VscodeHttpProvider extends HttpProvider {
|
||||
private readonly serverRootPath: string
|
||||
private readonly vsRootPath: string
|
||||
private _vscode?: Promise<cp.ChildProcess>
|
||||
|
||||
public constructor(options: HttpProviderOptions, private readonly args: Args) {
|
||||
super(options)
|
||||
this.vsRootPath = path.resolve(this.rootPath, "lib/vscode")
|
||||
this.serverRootPath = path.join(this.vsRootPath, "out/vs/server")
|
||||
}
|
||||
|
||||
public get running(): boolean {
|
||||
return !!this._vscode
|
||||
}
|
||||
|
||||
public async dispose(): Promise<void> {
|
||||
if (this._vscode) {
|
||||
const vscode = await this._vscode
|
||||
vscode.removeAllListeners()
|
||||
this._vscode = undefined
|
||||
vscode.kill()
|
||||
}
|
||||
}
|
||||
|
||||
private async initialize(options: VscodeOptions): Promise<WorkbenchOptions> {
|
||||
const id = generateUuid()
|
||||
const vscode = await this.fork()
|
||||
|
||||
logger.debug("setting up vs code...")
|
||||
return new Promise<WorkbenchOptions>((resolve, reject) => {
|
||||
vscode.once("message", (message: VscodeMessage) => {
|
||||
logger.debug("got message from vs code", field("message", message))
|
||||
return message.type === "options" && message.id === id
|
||||
? resolve(message.options)
|
||||
: reject(new Error("Unexpected response during initialization"))
|
||||
})
|
||||
vscode.once("error", reject)
|
||||
vscode.once("exit", (code) => reject(new Error(`VS Code exited unexpectedly with code ${code}`)))
|
||||
this.send({ type: "init", id, options }, vscode)
|
||||
})
|
||||
}
|
||||
|
||||
private fork(): Promise<cp.ChildProcess> {
|
||||
if (!this._vscode) {
|
||||
logger.debug("forking vs code...")
|
||||
const vscode = cp.fork(path.join(this.serverRootPath, "fork"))
|
||||
vscode.on("error", (error) => {
|
||||
logger.error(error.message)
|
||||
this._vscode = undefined
|
||||
})
|
||||
vscode.on("exit", (code) => {
|
||||
logger.error(`VS Code exited unexpectedly with code ${code}`)
|
||||
this._vscode = undefined
|
||||
})
|
||||
|
||||
this._vscode = new Promise((resolve, reject) => {
|
||||
vscode.once("message", (message: VscodeMessage) => {
|
||||
logger.debug("got message from vs code", field("message", message))
|
||||
return message.type === "ready"
|
||||
? resolve(vscode)
|
||||
: reject(new Error("Unexpected response waiting for ready response"))
|
||||
})
|
||||
vscode.once("error", reject)
|
||||
vscode.once("exit", (code) => reject(new Error(`VS Code exited unexpectedly with code ${code}`)))
|
||||
})
|
||||
}
|
||||
|
||||
return this._vscode
|
||||
}
|
||||
|
||||
public async handleWebSocket(route: Route, request: http.IncomingMessage, socket: net.Socket): Promise<void> {
|
||||
if (!this.authenticated(request)) {
|
||||
throw new Error("not authenticated")
|
||||
}
|
||||
|
||||
// VS Code expects a raw socket. It will handle all the web socket frames.
|
||||
// We just need to handle the initial upgrade.
|
||||
// This magic value is specified by the websocket spec.
|
||||
const magic = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
|
||||
const reply = crypto
|
||||
.createHash("sha1")
|
||||
.update(request.headers["sec-websocket-key"] + magic)
|
||||
.digest("base64")
|
||||
socket.write(
|
||||
[
|
||||
"HTTP/1.1 101 Switching Protocols",
|
||||
"Upgrade: websocket",
|
||||
"Connection: Upgrade",
|
||||
`Sec-WebSocket-Accept: ${reply}`,
|
||||
].join("\r\n") + "\r\n\r\n",
|
||||
)
|
||||
|
||||
const vscode = await this._vscode
|
||||
this.send({ type: "socket", query: route.query }, vscode, socket)
|
||||
}
|
||||
|
||||
private send(message: CodeServerMessage, vscode?: cp.ChildProcess, socket?: net.Socket): void {
|
||||
if (!vscode || vscode.killed) {
|
||||
throw new Error("vscode is not running")
|
||||
}
|
||||
vscode.send(message, socket)
|
||||
}
|
||||
|
||||
public async handleRequest(route: Route, request: http.IncomingMessage): Promise<HttpResponse> {
|
||||
this.ensureMethod(request)
|
||||
|
||||
switch (route.base) {
|
||||
case "/":
|
||||
if (!this.isRoot(route)) {
|
||||
throw new HttpError("Not found", HttpCode.NotFound)
|
||||
} else if (!this.authenticated(request)) {
|
||||
return { redirect: "/login", query: { to: route.providerBase } }
|
||||
}
|
||||
try {
|
||||
return await this.getRoot(request, route)
|
||||
} catch (error) {
|
||||
const message = `<div>VS Code failed to load.</div> ${
|
||||
this.isDev
|
||||
? `<div>It might not have finished compiling.</div>` +
|
||||
`Check for <code>Finished <span class="success">compilation</span></code> in the output.`
|
||||
: ""
|
||||
} <br><br>${error}`
|
||||
return this.getErrorRoot(route, "VS Code failed to load", "500", message)
|
||||
}
|
||||
}
|
||||
|
||||
this.ensureAuthenticated(request)
|
||||
|
||||
switch (route.base) {
|
||||
case "/resource":
|
||||
case "/vscode-remote-resource":
|
||||
if (typeof route.query.path === "string") {
|
||||
return this.getResource(pathToFsPath(route.query.path))
|
||||
}
|
||||
break
|
||||
case "/webview":
|
||||
if (/^\/vscode-resource/.test(route.requestPath)) {
|
||||
return this.getResource(route.requestPath.replace(/^\/vscode-resource(\/file)?/, ""))
|
||||
}
|
||||
return this.getResource(this.vsRootPath, "out/vs/workbench/contrib/webview/browser/pre", route.requestPath)
|
||||
}
|
||||
|
||||
throw new HttpError("Not found", HttpCode.NotFound)
|
||||
}
|
||||
|
||||
private async getRoot(request: http.IncomingMessage, route: Route): Promise<HttpResponse> {
|
||||
const remoteAuthority = request.headers.host as string
|
||||
const { lastVisited } = await settings.read()
|
||||
const startPath = await this.getFirstPath([
|
||||
{ url: route.query.workspace, workspace: true },
|
||||
{ url: route.query.folder, workspace: false },
|
||||
this.args._ && this.args._.length > 0 ? { url: path.resolve(this.args._[this.args._.length - 1]) } : undefined,
|
||||
lastVisited,
|
||||
])
|
||||
const [response, options] = await Promise.all([
|
||||
await this.getUtf8Resource(this.rootPath, "src/browser/pages/vscode.html"),
|
||||
this.initialize({
|
||||
args: this.args,
|
||||
remoteAuthority,
|
||||
startPath,
|
||||
}),
|
||||
])
|
||||
|
||||
settings.write({
|
||||
lastVisited: startPath || lastVisited, // If startpath is undefined, then fallback to lastVisited
|
||||
query: route.query,
|
||||
})
|
||||
|
||||
if (!this.isDev) {
|
||||
response.content = response.content.replace(/<!-- PROD_ONLY/g, "").replace(/END_PROD_ONLY -->/g, "")
|
||||
}
|
||||
|
||||
options.productConfiguration.codeServerVersion = require("../../../package.json").version
|
||||
|
||||
response.content = response.content
|
||||
.replace(`"{{REMOTE_USER_DATA_URI}}"`, `'${JSON.stringify(options.remoteUserDataUri)}'`)
|
||||
.replace(`"{{PRODUCT_CONFIGURATION}}"`, `'${JSON.stringify(options.productConfiguration)}'`)
|
||||
.replace(`"{{WORKBENCH_WEB_CONFIGURATION}}"`, `'${JSON.stringify(options.workbenchWebConfiguration)}'`)
|
||||
.replace(`"{{NLS_CONFIGURATION}}"`, `'${JSON.stringify(options.nlsConfiguration)}'`)
|
||||
return this.replaceTemplates<Options>(route, response, {
|
||||
disableTelemetry: !!this.args["disable-telemetry"],
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Choose the first non-empty path.
|
||||
*/
|
||||
private async getFirstPath(
|
||||
startPaths: Array<{ url?: string | string[]; workspace?: boolean } | undefined>,
|
||||
): Promise<StartPath | undefined> {
|
||||
const isFile = async (path: string): Promise<boolean> => {
|
||||
try {
|
||||
const stat = await fs.stat(path)
|
||||
return stat.isFile()
|
||||
} catch (error) {
|
||||
logger.warn(error.message)
|
||||
return false
|
||||
}
|
||||
}
|
||||
for (let i = 0; i < startPaths.length; ++i) {
|
||||
const startPath = startPaths[i]
|
||||
const url = arrayify(startPath && startPath.url).find((p) => !!p)
|
||||
if (startPath && url) {
|
||||
return {
|
||||
url,
|
||||
// The only time `workspace` is undefined is for the command-line
|
||||
// argument, in which case it's a path (not a URL) so we can stat it
|
||||
// without having to parse it.
|
||||
workspace: typeof startPath.workspace !== "undefined" ? startPath.workspace : await isFile(url),
|
||||
}
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user