Refactor vscode endpoints to use fork directly.
This commit is contained in:
@ -1,20 +0,0 @@
|
||||
{
|
||||
"name": "code-server",
|
||||
"short_name": "code-server",
|
||||
"start_url": "{{BASE}}",
|
||||
"display": "fullscreen",
|
||||
"background-color": "#fff",
|
||||
"description": "Run editors on a remote server.",
|
||||
"icons": [
|
||||
{
|
||||
"src": "{{CS_STATIC_BASE}}/src/browser/media/pwa-icon-192.png",
|
||||
"type": "image/png",
|
||||
"sizes": "192x192"
|
||||
},
|
||||
{
|
||||
"src": "{{CS_STATIC_BASE}}/src/browser/media/pwa-icon-512.png",
|
||||
"type": "image/png",
|
||||
"sizes": "512x512"
|
||||
}
|
||||
]
|
||||
}
|
@ -10,10 +10,11 @@
|
||||
http-equiv="Content-Security-Policy"
|
||||
content="style-src 'self'; manifest-src 'self'; img-src 'self' data:; font-src 'self' data:;"
|
||||
/>
|
||||
|
||||
<title>{{ERROR_TITLE}} - code-server</title>
|
||||
<link rel="icon" href="{{CS_STATIC_BASE}}/src/browser/media/favicon-dark-support.svg" />
|
||||
<link rel="alternate icon" href="{{CS_STATIC_BASE}}/src/browser/media/favicon.ico" />
|
||||
<link rel="manifest" href="{{CS_STATIC_BASE}}/src/browser/media/manifest.json" crossorigin="use-credentials" />
|
||||
<link rel="manifest" href="/manifest.json" crossorigin="use-credentials" />
|
||||
<link rel="apple-touch-icon" sizes="192x192" href="{{CS_STATIC_BASE}}/src/browser/media/pwa-icon-192.png" />
|
||||
<link rel="apple-touch-icon" sizes="512x512" href="{{CS_STATIC_BASE}}/src/browser/media/pwa-icon-512.png" />
|
||||
<link href="{{CS_STATIC_BASE}}/src/browser/pages/global.css" rel="stylesheet" />
|
||||
@ -30,6 +31,5 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script data-cfasync="false" src="{{CS_STATIC_BASE}}/out/browser/register.browserified.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
@ -13,7 +13,7 @@
|
||||
<title>code-server login</title>
|
||||
<link rel="icon" href="{{CS_STATIC_BASE}}/src/browser/media/favicon-dark-support.svg" />
|
||||
<link rel="alternate icon" href="{{CS_STATIC_BASE}}/src/browser/media/favicon.ico" />
|
||||
<link rel="manifest" href="{{CS_STATIC_BASE}}/src/browser/media/manifest.json" crossorigin="use-credentials" />
|
||||
<link rel="manifest" href="{{BASE}}/manifest.json" crossorigin="use-credentials" />
|
||||
<link rel="apple-touch-icon" sizes="192x192" href="{{CS_STATIC_BASE}}/src/browser/media/pwa-icon-192.png" />
|
||||
<link rel="apple-touch-icon" sizes="512x512" href="{{CS_STATIC_BASE}}/src/browser/media/pwa-icon-512.png" />
|
||||
<link href="{{CS_STATIC_BASE}}/src/browser/pages/global.css" rel="stylesheet" />
|
||||
@ -30,7 +30,6 @@
|
||||
<div class="content">
|
||||
<form class="login-form" method="post">
|
||||
<input class="user" type="text" autocomplete="username" />
|
||||
<input id="base" type="hidden" name="base" value="/" />
|
||||
<div class="field">
|
||||
<input
|
||||
required
|
||||
@ -49,5 +48,4 @@
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
<script data-cfasync="false" src="{{CS_STATIC_BASE}}/out/browser/pages/login.browserified.js"></script>
|
||||
</html>
|
||||
|
@ -1,8 +0,0 @@
|
||||
import { getOptions } from "../../common/util"
|
||||
import "../register"
|
||||
|
||||
const options = getOptions()
|
||||
const el = document.getElementById("base") as HTMLInputElement
|
||||
if (el) {
|
||||
el.value = options.base
|
||||
}
|
@ -1,54 +0,0 @@
|
||||
<!-- Copyright (C) Microsoft Corporation. All rights reserved. -->
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<script>
|
||||
performance.mark("code/didStartRenderer")
|
||||
</script>
|
||||
|
||||
<meta charset="utf-8" />
|
||||
|
||||
<!-- Disable pinch zooming -->
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no"
|
||||
/>
|
||||
|
||||
<!-- Workbench Configuration -->
|
||||
<meta id="vscode-workbench-web-configuration" data-settings="{{WORKBENCH_WEB_CONFIGURATION}}" />
|
||||
|
||||
<!-- Workarounds/Hacks (remote user data uri) -->
|
||||
<meta id="vscode-remote-user-data-uri" data-settings="{{REMOTE_USER_DATA_URI}}" />
|
||||
<meta id="vscode-remote-product-configuration" data-settings="{{PRODUCT_CONFIGURATION}}" />
|
||||
<meta id="vscode-remote-nls-configuration" data-settings="{{NLS_CONFIGURATION}}" />
|
||||
|
||||
<!-- Workbench Icon/Manifest/CSS -->
|
||||
<link rel="icon" href="{{CS_STATIC_BASE}}/src/browser/media/favicon-dark-support.svg" />
|
||||
<link rel="alternate icon" href="{{CS_STATIC_BASE}}/src/browser/media/favicon.ico" />
|
||||
<link rel="manifest" href="{{CS_STATIC_BASE}}/src/browser/media/manifest.json" crossorigin="use-credentials" />
|
||||
<!-- PROD_ONLY
|
||||
<link data-name="vs/workbench/workbench.web.api" rel="stylesheet" href="{{CS_STATIC_BASE}}/vendor/modules/code-oss-dev/out/vs/workbench/workbench.web.api.css">
|
||||
END_PROD_ONLY -->
|
||||
<link rel="apple-touch-icon" sizes="192x192" href="{{CS_STATIC_BASE}}/src/browser/media/pwa-icon-192.png" />
|
||||
<link rel="apple-touch-icon" sizes="512x512" href="{{CS_STATIC_BASE}}/src/browser/media/pwa-icon-512.png" />
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
|
||||
<meta id="coder-options" data-settings="{{OPTIONS}}" />
|
||||
</head>
|
||||
|
||||
<body aria-label=""></body>
|
||||
|
||||
<!-- Startup (do not modify order of script tags!) -->
|
||||
<script data-cfasync="false" src="{{CS_STATIC_BASE}}/out/browser/pages/vscode.browserified.js"></script>
|
||||
<script data-cfasync="false" src="{{CS_STATIC_BASE}}/vendor/modules/code-oss-dev/out/vs/loader.js"></script>
|
||||
<script>
|
||||
performance.mark("code/willLoadWorkbenchMain")
|
||||
</script>
|
||||
<!-- PROD_ONLY
|
||||
<script data-cfasync="false" src="{{CS_STATIC_BASE}}/vendor/modules/code-oss-dev/out/vs/workbench/workbench.web.api.nls.js"></script>
|
||||
<script data-cfasync="false" src="{{CS_STATIC_BASE}}/vendor/modules/code-oss-dev/out/vs/workbench/workbench.web.api.js"></script>
|
||||
END_PROD_ONLY -->
|
||||
<script>
|
||||
require(["vs/code/browser/workbench/workbench"], function () {})
|
||||
</script>
|
||||
</html>
|
@ -1,253 +0,0 @@
|
||||
import { getOptions, Options } from "../../common/util"
|
||||
import "../register"
|
||||
|
||||
// TODO@jsjoeio: Add proper types.
|
||||
type FixMeLater = any
|
||||
|
||||
// NOTE@jsjoeio
|
||||
// This lives here ../../../lib/vscode/src/vs/base/common/platform.ts#L106
|
||||
export const nlsConfigElementId = "vscode-remote-nls-configuration"
|
||||
|
||||
type NlsConfiguration = {
|
||||
locale: string
|
||||
availableLanguages: { [key: string]: string } | {}
|
||||
_languagePackId?: string
|
||||
_translationsConfigFile?: string
|
||||
_cacheRoot?: string
|
||||
_resolvedLanguagePackCoreLocation?: string
|
||||
_corruptedFile?: string
|
||||
_languagePackSupport?: boolean
|
||||
loadBundle?: FixMeLater
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to create the path to the bundle
|
||||
* for getNlsConfiguration.
|
||||
*/
|
||||
export function createBundlePath(_resolvedLanguagePackCoreLocation: string | undefined, bundle: string) {
|
||||
// NOTE@jsjoeio - this comment was here before me
|
||||
// Refers to operating systems that use a different path separator.
|
||||
// Probably just Windows but we're not sure if "/" breaks on Windows
|
||||
// so we'll leave it alone for now.
|
||||
// FIXME: Only works if path separators are /.
|
||||
return (_resolvedLanguagePackCoreLocation || "") + "/" + bundle.replace(/\//g, "!") + ".nls.json"
|
||||
}
|
||||
|
||||
/**
|
||||
* A helper function to get the NLS Configuration settings.
|
||||
*
|
||||
* This is used by VSCode for localizations (i.e. changing
|
||||
* the display language).
|
||||
*
|
||||
* Make sure to wrap this in a try/catch block when you call it.
|
||||
**/
|
||||
export function getNlsConfiguration(_document: Document, base: string) {
|
||||
const errorMsgPrefix = "[vscode]"
|
||||
const nlsConfigElement = _document?.getElementById(nlsConfigElementId)
|
||||
const dataSettings = nlsConfigElement?.getAttribute("data-settings")
|
||||
|
||||
if (!nlsConfigElement) {
|
||||
throw new Error(
|
||||
`${errorMsgPrefix} Could not parse NLS configuration. Could not find nlsConfigElement with id: ${nlsConfigElementId}`,
|
||||
)
|
||||
}
|
||||
|
||||
if (!dataSettings) {
|
||||
throw new Error(
|
||||
`${errorMsgPrefix} Could not parse NLS configuration. Found nlsConfigElement but missing data-settings attribute.`,
|
||||
)
|
||||
}
|
||||
|
||||
const nlsConfig = JSON.parse(dataSettings) as NlsConfiguration
|
||||
|
||||
if (nlsConfig._resolvedLanguagePackCoreLocation) {
|
||||
// NOTE@jsjoeio
|
||||
// Not sure why we use Object.create(null) instead of {}
|
||||
// They are not the same
|
||||
// See: https://stackoverflow.com/a/15518712/3015595
|
||||
// We copied this from ../../../lib/vscode/src/bootstrap.js#L143
|
||||
const bundles: {
|
||||
[key: string]: string
|
||||
} = Object.create(null)
|
||||
|
||||
type LoadBundleCallback = (_: undefined, result?: string) => void
|
||||
|
||||
nlsConfig.loadBundle = async (bundle: string, _language: string, cb: LoadBundleCallback): Promise<void> => {
|
||||
const result = bundles[bundle]
|
||||
|
||||
if (result) {
|
||||
return cb(undefined, result)
|
||||
}
|
||||
|
||||
try {
|
||||
const path = createBundlePath(nlsConfig._resolvedLanguagePackCoreLocation, bundle)
|
||||
const response = await fetch(`${base}/vscode/resource/?path=${encodeURIComponent(path)}`)
|
||||
const json = await response.json()
|
||||
bundles[bundle] = json
|
||||
return cb(undefined, json)
|
||||
} catch (error) {
|
||||
return cb(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nlsConfig
|
||||
}
|
||||
|
||||
type GetLoaderParams = {
|
||||
nlsConfig: NlsConfiguration
|
||||
options: Options
|
||||
_window: Window
|
||||
}
|
||||
|
||||
/**
|
||||
* Link to types in the loader source repo
|
||||
* https://github.com/microsoft/vscode-loader/blob/main/src/loader.d.ts#L280
|
||||
*/
|
||||
type Loader = {
|
||||
baseUrl: string
|
||||
recordStats: boolean
|
||||
// TODO@jsjoeio: There don't appear to be any types for trustedTypes yet.
|
||||
trustedTypesPolicy: FixMeLater
|
||||
paths: {
|
||||
[key: string]: string
|
||||
}
|
||||
"vs/nls": NlsConfiguration
|
||||
}
|
||||
|
||||
/**
|
||||
* A helper function which creates a script url if the value
|
||||
* is valid.
|
||||
*
|
||||
* Extracted into a function to make it easier to test
|
||||
*/
|
||||
export function _createScriptURL(value: string, origin: string): string {
|
||||
if (value.startsWith(origin)) {
|
||||
return value
|
||||
}
|
||||
throw new Error(`Invalid script url: ${value}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* A helper function to get the require loader
|
||||
*
|
||||
* This used by VSCode/code-server
|
||||
* to load files.
|
||||
*
|
||||
* We extracted the logic into a function so that
|
||||
* it's easier to test.
|
||||
**/
|
||||
export function getConfigurationForLoader({ nlsConfig, options, _window }: GetLoaderParams) {
|
||||
const loader: Loader = {
|
||||
// Without the full URL VS Code will try to load file://.
|
||||
baseUrl: `${window.location.origin}${options.csStaticBase}/vendor/modules/code-oss-dev/out`,
|
||||
recordStats: true,
|
||||
trustedTypesPolicy: (_window as FixMeLater).trustedTypes?.createPolicy("amdLoader", {
|
||||
createScriptURL(value: string): string {
|
||||
return _createScriptURL(value, window.location.origin)
|
||||
},
|
||||
}),
|
||||
paths: {
|
||||
"vscode-textmate": `../node_modules/vscode-textmate/release/main`,
|
||||
"vscode-oniguruma": `../node_modules/vscode-oniguruma/release/main`,
|
||||
xterm: `../node_modules/xterm/lib/xterm.js`,
|
||||
"xterm-addon-search": `../node_modules/xterm-addon-search/lib/xterm-addon-search.js`,
|
||||
"xterm-addon-unicode11": `../node_modules/xterm-addon-unicode11/lib/xterm-addon-unicode11.js`,
|
||||
"xterm-addon-webgl": `../node_modules/xterm-addon-webgl/lib/xterm-addon-webgl.js`,
|
||||
"tas-client-umd": `../node_modules/tas-client-umd/lib/tas-client-umd.js`,
|
||||
"iconv-lite-umd": `../node_modules/iconv-lite-umd/lib/iconv-lite-umd.js`,
|
||||
jschardet: `../node_modules/jschardet/dist/jschardet.min.js`,
|
||||
},
|
||||
"vs/nls": nlsConfig,
|
||||
}
|
||||
|
||||
return loader
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the body background color to match the theme.
|
||||
*/
|
||||
export function setBodyBackgroundToThemeBackgroundColor(_document: Document, _localStorage: Storage) {
|
||||
const errorMsgPrefix = "[vscode]"
|
||||
const colorThemeData = _localStorage.getItem("colorThemeData")
|
||||
|
||||
if (!colorThemeData) {
|
||||
throw new Error(
|
||||
`${errorMsgPrefix} Could not set body background to theme background color. Could not find colorThemeData in localStorage.`,
|
||||
)
|
||||
}
|
||||
|
||||
let _colorThemeData
|
||||
try {
|
||||
// We wrap this JSON.parse logic in a try/catch
|
||||
// because it can throw if the JSON is invalid.
|
||||
// and instead of throwing a random error
|
||||
// we can throw our own error, which will be more helpful
|
||||
// to the end user.
|
||||
_colorThemeData = JSON.parse(colorThemeData)
|
||||
} catch {
|
||||
throw new Error(
|
||||
`${errorMsgPrefix} Could not set body background to theme background color. Could not parse colorThemeData from localStorage.`,
|
||||
)
|
||||
}
|
||||
|
||||
const hasColorMapProperty = Object.prototype.hasOwnProperty.call(_colorThemeData, "colorMap")
|
||||
if (!hasColorMapProperty) {
|
||||
throw new Error(
|
||||
`${errorMsgPrefix} Could not set body background to theme background color. colorThemeData is missing colorMap.`,
|
||||
)
|
||||
}
|
||||
|
||||
const editorBgColor = _colorThemeData.colorMap["editor.background"]
|
||||
|
||||
if (!editorBgColor) {
|
||||
throw new Error(
|
||||
`${errorMsgPrefix} Could not set body background to theme background color. colorThemeData.colorMap["editor.background"] is undefined.`,
|
||||
)
|
||||
}
|
||||
|
||||
_document.body.style.background = editorBgColor
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* A helper function to encapsulate all the
|
||||
* logic used in this file.
|
||||
*
|
||||
* We purposely include all of this in a single function
|
||||
* so that it's easier to test.
|
||||
*/
|
||||
export function main(_document: Document | undefined, _window: Window | undefined, _localStorage: Storage | undefined) {
|
||||
if (!_document) {
|
||||
throw new Error(`document is undefined.`)
|
||||
}
|
||||
|
||||
if (!_window) {
|
||||
throw new Error(`window is undefined.`)
|
||||
}
|
||||
|
||||
if (!_localStorage) {
|
||||
throw new Error(`localStorage is undefined.`)
|
||||
}
|
||||
|
||||
const options = getOptions()
|
||||
const nlsConfig = getNlsConfiguration(_document, options.base)
|
||||
|
||||
const loader = getConfigurationForLoader({
|
||||
nlsConfig,
|
||||
options,
|
||||
_window,
|
||||
})
|
||||
|
||||
;(self.require as unknown as Loader) = loader
|
||||
|
||||
setBodyBackgroundToThemeBackgroundColor(_document, _localStorage)
|
||||
}
|
||||
|
||||
try {
|
||||
main(document, window, localStorage)
|
||||
} catch (error) {
|
||||
console.error("[vscode] failed to initialize VS Code")
|
||||
console.error(error)
|
||||
}
|
@ -1,23 +0,0 @@
|
||||
import { logger } from "@coder/logger"
|
||||
import { getOptions, normalize, logError } from "../common/util"
|
||||
|
||||
export async function registerServiceWorker(): Promise<void> {
|
||||
const options = getOptions()
|
||||
logger.level = options.logLevel
|
||||
|
||||
const path = normalize(`${options.csStaticBase}/out/browser/serviceWorker.js`)
|
||||
try {
|
||||
await navigator.serviceWorker.register(path, {
|
||||
scope: options.base + "/",
|
||||
})
|
||||
logger.info(`[Service Worker] registered`)
|
||||
} catch (error) {
|
||||
logError(logger, `[Service Worker] registration`, error)
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof navigator !== "undefined" && "serviceWorker" in navigator) {
|
||||
registerServiceWorker()
|
||||
} else {
|
||||
logger.error(`[Service Worker] navigator is undefined`)
|
||||
}
|
@ -1,14 +0,0 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
self.addEventListener("install", () => {
|
||||
console.log("[Service Worker] installed")
|
||||
})
|
||||
|
||||
self.addEventListener("activate", (event: any) => {
|
||||
event.waitUntil((self as any).clients.claim())
|
||||
console.log("[Service Worker] activated")
|
||||
})
|
||||
|
||||
self.addEventListener("fetch", () => {
|
||||
// Without this event handler we won't be recognized as a PWA.
|
||||
})
|
@ -46,7 +46,7 @@ export class Emitter<T> {
|
||||
this.listeners.map(async (cb) => {
|
||||
try {
|
||||
await cb(value, promise)
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
logger.error(error.message)
|
||||
}
|
||||
}),
|
||||
|
@ -1,19 +1,3 @@
|
||||
/*
|
||||
* This file exists in two locations:
|
||||
* - src/common/util.ts
|
||||
* - lib/vscode/src/vs/server/common/util.ts
|
||||
* The second is a symlink to the first.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Base options included on every page.
|
||||
*/
|
||||
export interface Options {
|
||||
base: string
|
||||
csStaticBase: string
|
||||
logLevel: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Split a string up to the delimiter. If the delimiter doesn't exist the first
|
||||
* item will have all the text and the second item will be an empty string.
|
||||
@ -67,14 +51,14 @@ export const resolveBase = (base?: string): string => {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get options embedded in the HTML or query params.
|
||||
* Get client-side configuration embedded in the HTML or query params.
|
||||
*/
|
||||
export const getOptions = <T extends Options>(): T => {
|
||||
let options: T
|
||||
export const getClientConfiguration = <T extends CodeServerLib.ClientConfiguration>(): T => {
|
||||
let config: T
|
||||
try {
|
||||
options = JSON.parse(document.getElementById("coder-options")!.getAttribute("data-settings")!)
|
||||
config = JSON.parse(document.getElementById("coder-options")!.getAttribute("data-settings")!)
|
||||
} catch (error) {
|
||||
options = {} as T
|
||||
config = {} as T
|
||||
}
|
||||
|
||||
// You can also pass options in stringified form to the options query
|
||||
@ -83,16 +67,16 @@ export const getOptions = <T extends Options>(): T => {
|
||||
const params = new URLSearchParams(location.search)
|
||||
const queryOpts = params.get("options")
|
||||
if (queryOpts) {
|
||||
options = {
|
||||
...options,
|
||||
config = {
|
||||
...config,
|
||||
...JSON.parse(queryOpts),
|
||||
}
|
||||
}
|
||||
|
||||
options.base = resolveBase(options.base)
|
||||
options.csStaticBase = resolveBase(options.csStaticBase)
|
||||
config.base = resolveBase(config.base)
|
||||
config.csStaticBase = resolveBase(config.csStaticBase)
|
||||
|
||||
return options
|
||||
return config
|
||||
}
|
||||
|
||||
/**
|
||||
@ -109,17 +93,6 @@ export const arrayify = <T>(value?: T | T[]): T[] => {
|
||||
return [value]
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the first string. If there's no string return undefined.
|
||||
*/
|
||||
export const getFirstString = (value: string | string[] | object | undefined): string | undefined => {
|
||||
if (Array.isArray(value)) {
|
||||
return value[0]
|
||||
}
|
||||
|
||||
return typeof value === "string" ? value : undefined
|
||||
}
|
||||
|
||||
// TODO: Might make sense to add Error handling to the logger itself.
|
||||
export function logError(logger: { error: (msg: string) => void }, prefix: string, err: Error | string): void {
|
||||
if (err instanceof Error) {
|
||||
|
@ -9,6 +9,35 @@ import { DefaultedArgs } from "./cli"
|
||||
import { isNodeJSErrnoException } from "./util"
|
||||
import { handleUpgrade } from "./wsRouter"
|
||||
|
||||
type ListenOptions = Pick<DefaultedArgs, "socket" | "port" | "host">
|
||||
|
||||
const listen = (server: http.Server, { host, port, socket }: ListenOptions) => {
|
||||
return new Promise<void>(async (resolve, reject) => {
|
||||
server.on("error", reject)
|
||||
|
||||
const onListen = () => {
|
||||
// Promise resolved earlier so this is an unrelated error.
|
||||
server.off("error", reject)
|
||||
server.on("error", (err) => util.logError(logger, "http server error", err))
|
||||
|
||||
resolve()
|
||||
}
|
||||
|
||||
if (socket) {
|
||||
try {
|
||||
await fs.unlink(socket)
|
||||
} catch (error: any) {
|
||||
handleArgsSocketCatchError(error)
|
||||
}
|
||||
|
||||
server.listen(socket, onListen)
|
||||
} else {
|
||||
// [] is the correct format when using :: but Node errors with them.
|
||||
server.listen(port, host.replace(/^\[|\]$/g, ""), onListen)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an Express app and an HTTP/S server to serve it.
|
||||
*/
|
||||
@ -27,28 +56,7 @@ export const createApp = async (args: DefaultedArgs): Promise<[Express, Express,
|
||||
)
|
||||
: http.createServer(app)
|
||||
|
||||
let resolved = false
|
||||
await new Promise<void>(async (resolve2, reject) => {
|
||||
const resolve = () => {
|
||||
resolved = true
|
||||
resolve2()
|
||||
}
|
||||
server.on("error", (err) => {
|
||||
handleServerError(resolved, err, reject)
|
||||
})
|
||||
|
||||
if (args.socket) {
|
||||
try {
|
||||
await fs.unlink(args.socket)
|
||||
} catch (error: any) {
|
||||
handleArgsSocketCatchError(error)
|
||||
}
|
||||
server.listen(args.socket, resolve)
|
||||
} else {
|
||||
// [] is the correct format when using :: but Node errors with them.
|
||||
server.listen(args.port, args.host.replace(/^\[|\]$/g, ""), resolve)
|
||||
}
|
||||
})
|
||||
await listen(server, args)
|
||||
|
||||
const wsApp = express()
|
||||
handleUpgrade(wsApp, server)
|
||||
|
@ -3,12 +3,11 @@ import { promises as fs } from "fs"
|
||||
import yaml from "js-yaml"
|
||||
import * as os from "os"
|
||||
import * as path from "path"
|
||||
import { Args as VsArgs } from "../../typings/ipc"
|
||||
import { canConnect, generateCertificate, generatePassword, humanPath, paths } from "./util"
|
||||
|
||||
export enum Feature {
|
||||
/** Web socket compression. */
|
||||
PermessageDeflate = "permessage-deflate",
|
||||
// No current experimental features!
|
||||
Placeholder = "placeholder",
|
||||
}
|
||||
|
||||
export enum AuthType {
|
||||
@ -30,7 +29,21 @@ export enum LogLevel {
|
||||
|
||||
export class OptionalString extends Optional<string> {}
|
||||
|
||||
export interface Args extends VsArgs {
|
||||
export interface Args
|
||||
extends Pick<
|
||||
CodeServerLib.NativeParsedArgs,
|
||||
| "user-data-dir"
|
||||
| "enable-proposed-api"
|
||||
| "extensions-dir"
|
||||
| "builtin-extensions-dir"
|
||||
| "extra-extensions-dir"
|
||||
| "extra-builtin-extensions-dir"
|
||||
| "ignore-last-opened"
|
||||
| "locale"
|
||||
| "log"
|
||||
| "verbose"
|
||||
| "_"
|
||||
> {
|
||||
config?: string
|
||||
auth?: AuthType
|
||||
password?: string
|
||||
@ -56,8 +69,6 @@ export interface Args extends VsArgs {
|
||||
"show-versions"?: boolean
|
||||
"uninstall-extension"?: string[]
|
||||
"proxy-domain"?: string[]
|
||||
locale?: string
|
||||
_: string[]
|
||||
"reuse-window"?: boolean
|
||||
"new-window"?: boolean
|
||||
|
||||
@ -546,7 +557,7 @@ export async function readConfigFile(configPath?: string): Promise<ConfigArgs> {
|
||||
flag: "wx", // wx means to fail if the path exists.
|
||||
})
|
||||
logger.info(`Wrote default config file to ${humanPath(configPath)}`)
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
// EEXIST is fine; we don't want to overwrite existing configurations.
|
||||
if (error.code !== "EEXIST") {
|
||||
throw error
|
||||
@ -670,7 +681,7 @@ export const shouldOpenInExistingInstance = async (args: Args): Promise<string |
|
||||
const readSocketPath = async (): Promise<string | undefined> => {
|
||||
try {
|
||||
return await fs.readFile(path.join(os.tmpdir(), "vscode-ipc"), "utf8")
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
if (error.code !== "ENOENT") {
|
||||
throw error
|
||||
}
|
||||
|
@ -3,11 +3,13 @@ import { JSONSchemaForNPMPackageJsonFiles } from "@schemastore/package"
|
||||
import * as os from "os"
|
||||
import * as path from "path"
|
||||
|
||||
export const WORKBENCH_WEB_CONFIG_ID = "vscode-workbench-web-configuration"
|
||||
|
||||
export function getPackageJson(relativePath: string): JSONSchemaForNPMPackageJsonFiles {
|
||||
let pkg = {}
|
||||
try {
|
||||
pkg = require(relativePath)
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
logger.warn(error.message)
|
||||
}
|
||||
|
||||
@ -19,5 +21,6 @@ const pkg = getPackageJson("../../package.json")
|
||||
export const version = pkg.version || "development"
|
||||
export const commit = pkg.commit || "development"
|
||||
export const rootPath = path.resolve(__dirname, "../..")
|
||||
export const vsRootPath = path.join(rootPath, "vendor/modules/code-oss-dev")
|
||||
export const tmpdir = path.join(os.tmpdir(), "code-server")
|
||||
export const isDevMode = commit === "development"
|
||||
|
@ -9,11 +9,11 @@ import {
|
||||
} from "./cli"
|
||||
import { commit, version } from "./constants"
|
||||
import { openInExistingInstance, runCodeServer, runVsCodeCli } from "./main"
|
||||
import * as proxyAgent from "./proxy_agent"
|
||||
import { monkeyPatchProxyProtocols } from "./proxy_agent"
|
||||
import { isChild, wrapper } from "./wrapper"
|
||||
|
||||
async function entry(): Promise<void> {
|
||||
proxyAgent.monkeyPatch(false)
|
||||
monkeyPatchProxyProtocols()
|
||||
|
||||
// There's no need to check flags like --help or to spawn in an existing
|
||||
// instance for the child process because these would have already happened in
|
||||
@ -46,11 +46,13 @@ async function entry(): Promise<void> {
|
||||
|
||||
if (args.version) {
|
||||
if (args.json) {
|
||||
console.log({
|
||||
codeServer: version,
|
||||
commit,
|
||||
vscode: require("../../vendor/modules/code-oss-dev/package.json").version,
|
||||
})
|
||||
console.log(
|
||||
JSON.stringify({
|
||||
codeServer: version,
|
||||
commit,
|
||||
vscode: require("../../vendor/modules/code-oss-dev/package.json").version,
|
||||
}),
|
||||
)
|
||||
} else {
|
||||
console.log(version, commit)
|
||||
}
|
||||
|
@ -1,13 +1,14 @@
|
||||
import { field, logger } from "@coder/logger"
|
||||
import * as express from "express"
|
||||
import * as expressCore from "express-serve-static-core"
|
||||
import path from "path"
|
||||
import qs from "qs"
|
||||
import { HttpCode, HttpError } from "../common/http"
|
||||
import { normalize, Options } from "../common/util"
|
||||
import { normalize } from "../common/util"
|
||||
import { AuthType, DefaultedArgs } from "./cli"
|
||||
import { commit, rootPath } from "./constants"
|
||||
import { version as codeServerVersion } from "./constants"
|
||||
import { Heart } from "./heart"
|
||||
import { getPasswordMethod, IsCookieValidArgs, isCookieValid, sanitizeString, escapeHtml } from "./util"
|
||||
import { getPasswordMethod, IsCookieValidArgs, isCookieValid, sanitizeString, escapeHtml, escapeJSON } from "./util"
|
||||
|
||||
declare global {
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
@ -19,6 +20,16 @@ declare global {
|
||||
}
|
||||
}
|
||||
|
||||
export const createClientConfiguration = (req: express.Request): CodeServerLib.ClientConfiguration => {
|
||||
const base = relativeRoot(req)
|
||||
|
||||
return {
|
||||
base,
|
||||
csStaticBase: normalize(path.join(base, "_static/")),
|
||||
codeServerVersion,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace common variable strings in HTML templates.
|
||||
*/
|
||||
@ -27,18 +38,16 @@ export const replaceTemplates = <T extends object>(
|
||||
content: string,
|
||||
extraOpts?: Omit<T, "base" | "csStaticBase" | "logLevel">,
|
||||
): string => {
|
||||
const base = relativeRoot(req)
|
||||
const options: Options = {
|
||||
base,
|
||||
csStaticBase: base + "/static/" + commit + rootPath,
|
||||
logLevel: logger.level,
|
||||
const serverOptions: CodeServerLib.ClientConfiguration = {
|
||||
...createClientConfiguration(req),
|
||||
...extraOpts,
|
||||
}
|
||||
|
||||
return content
|
||||
.replace(/{{TO}}/g, (typeof req.query.to === "string" && escapeHtml(req.query.to)) || "/")
|
||||
.replace(/{{BASE}}/g, options.base)
|
||||
.replace(/{{CS_STATIC_BASE}}/g, options.csStaticBase)
|
||||
.replace(/"{{OPTIONS}}"/, `'${JSON.stringify(options)}'`)
|
||||
.replace(/{{BASE}}/g, serverOptions.base)
|
||||
.replace(/{{CS_STATIC_BASE}}/g, serverOptions.csStaticBase)
|
||||
.replace("{{OPTIONS}}", () => escapeJSON(serverOptions))
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1,8 +1,6 @@
|
||||
import { field, logger } from "@coder/logger"
|
||||
import * as cp from "child_process"
|
||||
import http from "http"
|
||||
import * as path from "path"
|
||||
import { CliMessage, OpenCommandPipeArgs } from "../../typings/ipc"
|
||||
import path from "path"
|
||||
import { plural } from "../common/util"
|
||||
import { createApp, ensureAddress } from "./app"
|
||||
import { AuthType, DefaultedArgs, Feature } from "./cli"
|
||||
@ -10,41 +8,35 @@ import { coderCloudBind } from "./coder_cloud"
|
||||
import { commit, version } from "./constants"
|
||||
import { startLink } from "./link"
|
||||
import { register } from "./routes"
|
||||
import { humanPath, isFile, open } from "./util"
|
||||
import { humanPath, isFile, loadAMDModule, open } from "./util"
|
||||
|
||||
export const runVsCodeCli = (args: DefaultedArgs): void => {
|
||||
logger.debug("forking vs code cli...")
|
||||
const vscode = cp.fork(path.resolve(__dirname, "../../vendor/modules/code-oss-dev/out/vs/server/fork"), [], {
|
||||
env: {
|
||||
...process.env,
|
||||
CODE_SERVER_PARENT_PID: process.pid.toString(),
|
||||
},
|
||||
})
|
||||
vscode.once("message", (message: any) => {
|
||||
logger.debug("got message from VS Code", field("message", message))
|
||||
if (message.type !== "ready") {
|
||||
logger.error("Unexpected response waiting for ready response", field("type", message.type))
|
||||
process.exit(1)
|
||||
}
|
||||
const send: CliMessage = { type: "cli", args }
|
||||
vscode.send(send)
|
||||
})
|
||||
vscode.once("error", (error) => {
|
||||
/**
|
||||
* This is useful when an CLI arg should be passed to VS Code directly,
|
||||
* such as when managing extensions.
|
||||
* @deprecated This should be removed when code-server merges with lib/vscode.
|
||||
*/
|
||||
export const runVsCodeCli = async (args: DefaultedArgs): Promise<void> => {
|
||||
logger.debug("Running VS Code CLI")
|
||||
|
||||
const cliProcessMain = await loadAMDModule<CodeServerLib.IMainCli["main"]>("vs/code/node/cliProcessMain", "main")
|
||||
|
||||
try {
|
||||
await cliProcessMain(args)
|
||||
} catch (error) {
|
||||
logger.error("Got error from VS Code", field("error", error))
|
||||
process.exit(1)
|
||||
})
|
||||
vscode.on("exit", (code) => process.exit(code || 0))
|
||||
}
|
||||
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
export const openInExistingInstance = async (args: DefaultedArgs, socketPath: string): Promise<void> => {
|
||||
const pipeArgs: OpenCommandPipeArgs & { fileURIs: string[] } = {
|
||||
const pipeArgs: CodeServerLib.OpenCommandPipeArgs & { fileURIs: string[] } = {
|
||||
type: "open",
|
||||
folderURIs: [],
|
||||
fileURIs: [],
|
||||
forceReuseWindow: args["reuse-window"],
|
||||
forceNewWindow: args["new-window"],
|
||||
}
|
||||
|
||||
for (let i = 0; i < args._.length; i++) {
|
||||
const fp = path.resolve(args._[i])
|
||||
if (await isFile(fp)) {
|
||||
@ -53,17 +45,14 @@ export const openInExistingInstance = async (args: DefaultedArgs, socketPath: st
|
||||
pipeArgs.folderURIs.push(fp)
|
||||
}
|
||||
}
|
||||
|
||||
if (pipeArgs.forceNewWindow && pipeArgs.fileURIs.length > 0) {
|
||||
logger.error("--new-window can only be used with folder paths")
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
if (pipeArgs.folderURIs.length === 0 && pipeArgs.fileURIs.length === 0) {
|
||||
logger.error("Please specify at least one file or folder")
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const vscode = http.request(
|
||||
{
|
||||
path: "/",
|
||||
@ -135,8 +124,8 @@ export const runCodeServer = async (args: DefaultedArgs): Promise<http.Server> =
|
||||
startLink(port).catch((ex) => {
|
||||
logger.debug("Link daemon exited!", field("error", ex))
|
||||
})
|
||||
} catch (ex) {
|
||||
logger.debug("Failed to start link daemon!", ex)
|
||||
} catch (error) {
|
||||
logger.debug("Failed to start link daemon!", error as any)
|
||||
}
|
||||
|
||||
if (args.enable && args.enable.length > 0) {
|
||||
|
@ -172,9 +172,9 @@ export class PluginAPI {
|
||||
}
|
||||
await this.loadPlugin(path.join(dir, ent.name))
|
||||
}
|
||||
} catch (err) {
|
||||
if (err.code !== "ENOENT") {
|
||||
this.logger.warn(`failed to load plugins from ${q(dir)}: ${err.message}`)
|
||||
} catch (error: any) {
|
||||
if (error.code !== "ENOENT") {
|
||||
this.logger.warn(`failed to load plugins from ${q(dir)}: ${error.message}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -195,9 +195,9 @@ export class PluginAPI {
|
||||
}
|
||||
const p = this._loadPlugin(dir, packageJSON)
|
||||
this.plugins.set(p.name, p)
|
||||
} catch (err) {
|
||||
if (err.code !== "ENOENT") {
|
||||
this.logger.warn(`failed to load plugin: ${err.stack}`)
|
||||
} catch (error: any) {
|
||||
if (error.code !== "ENOENT") {
|
||||
this.logger.warn(`failed to load plugin: ${error.stack}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -278,7 +278,7 @@ export class PluginAPI {
|
||||
}
|
||||
try {
|
||||
await p.deinit()
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
this.logger.error("plugin failed to deinit", field("name", p.name), field("error", error.message))
|
||||
}
|
||||
}),
|
||||
|
@ -1,7 +1,10 @@
|
||||
import { logger } from "@coder/logger"
|
||||
import * as http from "http"
|
||||
import * as proxyAgent from "proxy-agent"
|
||||
import * as proxyFromEnv from "proxy-from-env"
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Coder Technologies. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import ProxyAgent from "proxy-agent"
|
||||
import { getProxyForUrl } from "proxy-from-env"
|
||||
|
||||
/**
|
||||
* This file has nothing to do with the code-server proxy.
|
||||
@ -11,10 +14,6 @@ import * as proxyFromEnv from "proxy-from-env"
|
||||
* - https://www.npmjs.com/package/proxy-agent
|
||||
* - https://www.npmjs.com/package/proxy-from-env
|
||||
*
|
||||
* This file exists in two locations:
|
||||
* - src/node/proxy_agent.ts
|
||||
* - lib/vscode/src/vs/base/node/proxy_agent.ts
|
||||
* The second is a symlink to the first.
|
||||
*/
|
||||
|
||||
/**
|
||||
@ -31,50 +30,41 @@ import * as proxyFromEnv from "proxy-from-env"
|
||||
* Even if they do, it's probably the same proxy so we should be fine! And those knobs
|
||||
* are deprecated anyway.
|
||||
*/
|
||||
export function monkeyPatch(inVSCode: boolean): void {
|
||||
if (shouldEnableProxy()) {
|
||||
const http = require("http")
|
||||
const https = require("https")
|
||||
|
||||
// If we do not pass in a proxy URL, proxy-agent will get the URL from the environment.
|
||||
// See https://www.npmjs.com/package/proxy-from-env.
|
||||
// Also see shouldEnableProxy.
|
||||
const pa = newProxyAgent(inVSCode)
|
||||
http.globalAgent = pa
|
||||
https.globalAgent = pa
|
||||
export function monkeyPatchProxyProtocols(): void {
|
||||
if (!shouldEnableProxy()) {
|
||||
return
|
||||
}
|
||||
|
||||
const http = require("http")
|
||||
const https = require("https")
|
||||
|
||||
// If we do not pass in a proxy URL, proxy-agent will get the URL from the environment.
|
||||
// See https://www.npmjs.com/package/proxy-from-env.
|
||||
// Also see shouldEnableProxy.
|
||||
const pa = new ProxyAgent()
|
||||
http.globalAgent = pa
|
||||
https.globalAgent = pa
|
||||
}
|
||||
|
||||
function newProxyAgent(inVSCode: boolean): http.Agent {
|
||||
// The reasoning for this split is that VS Code's build process does not have
|
||||
// esModuleInterop enabled but the code-server one does. As a result depending on where
|
||||
// we execute, we either have a default attribute or we don't.
|
||||
//
|
||||
// I can't enable esModuleInterop in VS Code's build process as it breaks and spits out
|
||||
// a huge number of errors. And we can't use require as otherwise the modules won't be
|
||||
// included in the final product.
|
||||
if (inVSCode) {
|
||||
return new (proxyAgent as any)()
|
||||
} else {
|
||||
return new (proxyAgent as any).default()
|
||||
}
|
||||
}
|
||||
const sampleUrls = [new URL("http://example.com"), new URL("https://example.com")]
|
||||
|
||||
// If they have $NO_PROXY set to example.com then this check won't work!
|
||||
// But that's drastically unlikely.
|
||||
export function shouldEnableProxy(): boolean {
|
||||
const testedProxyEndpoints = sampleUrls.map((url) => {
|
||||
return {
|
||||
url,
|
||||
proxyUrl: getProxyForUrl(url.toString()),
|
||||
}
|
||||
})
|
||||
|
||||
let shouldEnable = false
|
||||
|
||||
const httpProxy = proxyFromEnv.getProxyForUrl(`http://example.com`)
|
||||
if (httpProxy) {
|
||||
shouldEnable = true
|
||||
logger.debug(`using $HTTP_PROXY ${httpProxy}`)
|
||||
}
|
||||
|
||||
const httpsProxy = proxyFromEnv.getProxyForUrl(`https://example.com`)
|
||||
if (httpsProxy) {
|
||||
shouldEnable = true
|
||||
logger.debug(`using $HTTPS_PROXY ${httpsProxy}`)
|
||||
for (const { url, proxyUrl } of testedProxyEndpoints) {
|
||||
if (proxyUrl) {
|
||||
console.debug(`${url.protocol} -- Using "${proxyUrl}"`)
|
||||
shouldEnable = true
|
||||
}
|
||||
}
|
||||
|
||||
return shouldEnable
|
||||
|
45
src/node/routes/errors.ts
Normal file
45
src/node/routes/errors.ts
Normal file
@ -0,0 +1,45 @@
|
||||
import { logger } from "@coder/logger"
|
||||
import express from "express"
|
||||
import { promises as fs } from "fs"
|
||||
import path from "path"
|
||||
import { WebsocketRequest } from "../../../typings/pluginapi"
|
||||
import { HttpCode } from "../../common/http"
|
||||
import { rootPath } from "../constants"
|
||||
import { replaceTemplates } from "../http"
|
||||
import { getMediaMime } from "../util"
|
||||
|
||||
const notFoundCodes = ["ENOENT", "EISDIR", "FileNotFound"]
|
||||
export const errorHandler: express.ErrorRequestHandler = async (err, req, res, next) => {
|
||||
if (notFoundCodes.includes(err.code)) {
|
||||
err.status = HttpCode.NotFound
|
||||
}
|
||||
|
||||
const status = err.status ?? err.statusCode ?? 500
|
||||
res.status(status)
|
||||
|
||||
// Assume anything that explicitly accepts text/html is a user browsing a
|
||||
// page (as opposed to an xhr request). Don't use `req.accepts()` since
|
||||
// *every* request that I've seen (in Firefox and Chromium at least)
|
||||
// includes `*/*` making it always truthy. Even for css/javascript.
|
||||
if (req.headers.accept && req.headers.accept.includes("text/html")) {
|
||||
const resourcePath = path.resolve(rootPath, "src/browser/pages/error.html")
|
||||
res.set("Content-Type", getMediaMime(resourcePath))
|
||||
const content = await fs.readFile(resourcePath, "utf8")
|
||||
res.send(
|
||||
replaceTemplates(req, content)
|
||||
.replace(/{{ERROR_TITLE}}/g, status)
|
||||
.replace(/{{ERROR_HEADER}}/g, status)
|
||||
.replace(/{{ERROR_BODY}}/g, err.message),
|
||||
)
|
||||
} else {
|
||||
res.json({
|
||||
error: err.message,
|
||||
...(err.details || {}),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export const wsErrorHandler: express.ErrorRequestHandler = async (err, req, res, next) => {
|
||||
logger.error(`${err.message} ${err.stack}`)
|
||||
;(req as WebsocketRequest).ws.end()
|
||||
}
|
@ -1,5 +1,4 @@
|
||||
import { logger } from "@coder/logger"
|
||||
import bodyParser from "body-parser"
|
||||
import cookieParser from "cookie-parser"
|
||||
import * as express from "express"
|
||||
import { promises as fs } from "fs"
|
||||
@ -10,22 +9,21 @@ import * as pluginapi from "../../../typings/pluginapi"
|
||||
import { HttpCode, HttpError } from "../../common/http"
|
||||
import { plural } from "../../common/util"
|
||||
import { AuthType, DefaultedArgs } from "../cli"
|
||||
import { rootPath } from "../constants"
|
||||
import { commit, isDevMode, rootPath } from "../constants"
|
||||
import { Heart } from "../heart"
|
||||
import { ensureAuthenticated, redirect, replaceTemplates } from "../http"
|
||||
import { ensureAuthenticated, redirect } from "../http"
|
||||
import { PluginAPI } from "../plugin"
|
||||
import { getMediaMime, paths } from "../util"
|
||||
import { wrapper } from "../wrapper"
|
||||
import * as apps from "./apps"
|
||||
import * as domainProxy from "./domainProxy"
|
||||
import { errorHandler, wsErrorHandler } from "./errors"
|
||||
import * as health from "./health"
|
||||
import * as login from "./login"
|
||||
import * as logout from "./logout"
|
||||
import * as pathProxy from "./pathProxy"
|
||||
// static is a reserved keyword.
|
||||
import * as _static from "./static"
|
||||
import * as update from "./update"
|
||||
import * as vscode from "./vscode"
|
||||
import { createVSServerRouter, VSServerResult } from "./vscode"
|
||||
|
||||
/**
|
||||
* Register all routes and middleware.
|
||||
@ -124,13 +122,15 @@ export const register = async (
|
||||
wrapper.onDispose(() => pluginApi.dispose())
|
||||
}
|
||||
|
||||
app.use(bodyParser.json())
|
||||
app.use(bodyParser.urlencoded({ extended: true }))
|
||||
app.use(express.json())
|
||||
app.use(express.urlencoded({ extended: true }))
|
||||
|
||||
app.use("/", vscode.router)
|
||||
wsApp.use("/", vscode.wsRouter.router)
|
||||
app.use("/vscode", vscode.router)
|
||||
wsApp.use("/vscode", vscode.wsRouter.router)
|
||||
app.use(
|
||||
"/_static",
|
||||
express.static(rootPath, {
|
||||
cacheControl: commit !== "development",
|
||||
}),
|
||||
)
|
||||
|
||||
app.use("/healthz", health.router)
|
||||
wsApp.use("/healthz", health.wsRouter.router)
|
||||
@ -143,49 +143,32 @@ export const register = async (
|
||||
app.all("/logout", (req, res) => redirect(req, res, "/", {}))
|
||||
}
|
||||
|
||||
app.use("/static", _static.router)
|
||||
app.use("/update", update.router)
|
||||
|
||||
let vscode: VSServerResult
|
||||
try {
|
||||
vscode = await createVSServerRouter(args)
|
||||
app.use("/", vscode.router)
|
||||
wsApp.use("/", vscode.wsRouter.router)
|
||||
app.use("/vscode", vscode.router)
|
||||
wsApp.use("/vscode", vscode.wsRouter.router)
|
||||
} catch (error: any) {
|
||||
if (isDevMode) {
|
||||
logger.warn(error)
|
||||
logger.warn("VS Server router may still be compiling.")
|
||||
} else {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
server.on("close", () => {
|
||||
vscode.vscodeServer.close()
|
||||
})
|
||||
|
||||
app.use(() => {
|
||||
throw new HttpError("Not Found", HttpCode.NotFound)
|
||||
})
|
||||
|
||||
const errorHandler: express.ErrorRequestHandler = async (err, req, res, next) => {
|
||||
if (err.code === "ENOENT" || err.code === "EISDIR") {
|
||||
err.status = HttpCode.NotFound
|
||||
}
|
||||
|
||||
const status = err.status ?? err.statusCode ?? 500
|
||||
res.status(status)
|
||||
|
||||
// Assume anything that explicitly accepts text/html is a user browsing a
|
||||
// page (as opposed to an xhr request). Don't use `req.accepts()` since
|
||||
// *every* request that I've seen (in Firefox and Chromium at least)
|
||||
// includes `*/*` making it always truthy. Even for css/javascript.
|
||||
if (req.headers.accept && req.headers.accept.includes("text/html")) {
|
||||
const resourcePath = path.resolve(rootPath, "src/browser/pages/error.html")
|
||||
res.set("Content-Type", getMediaMime(resourcePath))
|
||||
const content = await fs.readFile(resourcePath, "utf8")
|
||||
res.send(
|
||||
replaceTemplates(req, content)
|
||||
.replace(/{{ERROR_TITLE}}/g, status)
|
||||
.replace(/{{ERROR_HEADER}}/g, status)
|
||||
.replace(/{{ERROR_BODY}}/g, err.message),
|
||||
)
|
||||
} else {
|
||||
res.json({
|
||||
error: err.message,
|
||||
...(err.details || {}),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
app.use(errorHandler)
|
||||
|
||||
const wsErrorHandler: express.ErrorRequestHandler = async (err, req, res, next) => {
|
||||
logger.error(`${err.message} ${err.stack}`)
|
||||
;(req as pluginapi.WebsocketRequest).ws.end()
|
||||
}
|
||||
|
||||
wsApp.use(wsErrorHandler)
|
||||
}
|
||||
|
@ -111,7 +111,7 @@ router.post("/", async (req, res) => {
|
||||
)
|
||||
|
||||
throw new Error("Incorrect password")
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
const renderedHtml = await getRoot(req, error)
|
||||
res.send(renderedHtml)
|
||||
}
|
||||
|
@ -1,71 +0,0 @@
|
||||
import { field, logger } from "@coder/logger"
|
||||
import { Router } from "express"
|
||||
import { promises as fs } from "fs"
|
||||
import * as path from "path"
|
||||
import { Readable } from "stream"
|
||||
import * as tarFs from "tar-fs"
|
||||
import * as zlib from "zlib"
|
||||
import { HttpCode, HttpError } from "../../common/http"
|
||||
import { getFirstString } from "../../common/util"
|
||||
import { rootPath } from "../constants"
|
||||
import { authenticated, ensureAuthenticated, replaceTemplates } from "../http"
|
||||
import { getMediaMime, pathToFsPath } from "../util"
|
||||
|
||||
export const router = Router()
|
||||
|
||||
// The commit is for caching.
|
||||
router.get("/(:commit)(/*)?", async (req, res) => {
|
||||
// Used by VS Code to load extensions into the web worker.
|
||||
const tar = getFirstString(req.query.tar)
|
||||
if (tar) {
|
||||
await ensureAuthenticated(req)
|
||||
let stream: Readable = tarFs.pack(pathToFsPath(tar))
|
||||
if (req.headers["accept-encoding"] && req.headers["accept-encoding"].includes("gzip")) {
|
||||
logger.debug("gzipping tar", field("path", tar))
|
||||
const compress = zlib.createGzip()
|
||||
stream.pipe(compress)
|
||||
stream.on("error", (error) => compress.destroy(error))
|
||||
stream.on("close", () => compress.end())
|
||||
stream = compress
|
||||
res.header("content-encoding", "gzip")
|
||||
}
|
||||
res.set("Content-Type", "application/x-tar")
|
||||
stream.on("close", () => res.end())
|
||||
return stream.pipe(res)
|
||||
}
|
||||
|
||||
// If not a tar use the remainder of the path to load the resource.
|
||||
if (!req.params[0]) {
|
||||
throw new HttpError("Not Found", HttpCode.NotFound)
|
||||
}
|
||||
|
||||
const resourcePath = path.resolve(req.params[0])
|
||||
|
||||
// Make sure it's in code-server if you aren't authenticated. This lets
|
||||
// unauthenticated users load the login assets.
|
||||
const isAuthenticated = await authenticated(req)
|
||||
if (!resourcePath.startsWith(rootPath) && !isAuthenticated) {
|
||||
throw new HttpError("Unauthorized", HttpCode.Unauthorized)
|
||||
}
|
||||
|
||||
// Don't cache during development. - can also be used if you want to make a
|
||||
// static request without caching.
|
||||
if (req.params.commit !== "development" && req.params.commit !== "-") {
|
||||
res.header("Cache-Control", "public, max-age=31536000")
|
||||
}
|
||||
|
||||
// Without this the default is to use the directory the script loaded from.
|
||||
if (req.headers["service-worker"]) {
|
||||
res.header("service-worker-allowed", "/")
|
||||
}
|
||||
|
||||
res.set("Content-Type", getMediaMime(resourcePath))
|
||||
|
||||
if (resourcePath.endsWith("manifest.json")) {
|
||||
const content = await fs.readFile(resourcePath, "utf8")
|
||||
return res.send(replaceTemplates(req, content))
|
||||
}
|
||||
|
||||
const content = await fs.readFile(resourcePath)
|
||||
return res.send(content)
|
||||
})
|
@ -1,232 +1,73 @@
|
||||
import * as crypto from "crypto"
|
||||
import { Request, Router } from "express"
|
||||
import { promises as fs } from "fs"
|
||||
import * as path from "path"
|
||||
import qs from "qs"
|
||||
import * as ipc from "../../../typings/ipc"
|
||||
import { Emitter } from "../../common/emitter"
|
||||
import { HttpCode, HttpError } from "../../common/http"
|
||||
import { getFirstString } from "../../common/util"
|
||||
import { Feature } from "../cli"
|
||||
import { isDevMode, rootPath, version } from "../constants"
|
||||
import { authenticated, ensureAuthenticated, redirect, replaceTemplates } from "../http"
|
||||
import { getMediaMime, pathToFsPath } from "../util"
|
||||
import { VscodeProvider } from "../vscode"
|
||||
import { Router as WsRouter } from "../wsRouter"
|
||||
import * as express from "express"
|
||||
import { Server } from "http"
|
||||
import path from "path"
|
||||
import { AuthType, DefaultedArgs } from "../cli"
|
||||
import { version as codeServerVersion, vsRootPath } from "../constants"
|
||||
import { ensureAuthenticated } from "../http"
|
||||
import { loadAMDModule } from "../util"
|
||||
import { Router as WsRouter, WebsocketRouter } from "../wsRouter"
|
||||
import { errorHandler } from "./errors"
|
||||
|
||||
export const router = Router()
|
||||
|
||||
const vscode = new VscodeProvider()
|
||||
|
||||
router.get("/", async (req, res) => {
|
||||
const isAuthenticated = await authenticated(req)
|
||||
if (!isAuthenticated) {
|
||||
return redirect(req, res, "login", {
|
||||
// req.baseUrl can be blank if already at the root.
|
||||
to: req.baseUrl && req.baseUrl !== "/" ? req.baseUrl : undefined,
|
||||
})
|
||||
}
|
||||
|
||||
const [content, options] = await Promise.all([
|
||||
await fs.readFile(path.join(rootPath, "src/browser/pages/vscode.html"), "utf8"),
|
||||
(async () => {
|
||||
try {
|
||||
return await vscode.initialize({ args: req.args, remoteAuthority: req.headers.host || "" }, req.query)
|
||||
} catch (error) {
|
||||
const devMessage = isDevMode ? "It might not have finished compiling." : ""
|
||||
throw new Error(`VS Code failed to load. ${devMessage} ${error.message}`)
|
||||
}
|
||||
})(),
|
||||
])
|
||||
|
||||
options.productConfiguration.codeServerVersion = version
|
||||
|
||||
res.send(
|
||||
replaceTemplates<ipc.Options>(
|
||||
req,
|
||||
// Uncomment prod blocks if not in development. TODO: Would this be
|
||||
// better as a build step? Or maintain two HTML files again?
|
||||
!isDevMode ? content.replace(/<!-- PROD_ONLY/g, "").replace(/END_PROD_ONLY -->/g, "") : content,
|
||||
{
|
||||
authed: req.args.auth !== "none",
|
||||
disableUpdateCheck: !!req.args["disable-update-check"],
|
||||
},
|
||||
)
|
||||
.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)}'`),
|
||||
)
|
||||
})
|
||||
|
||||
/**
|
||||
* TODO: Might currently be unused.
|
||||
*/
|
||||
router.get("/resource(/*)?", ensureAuthenticated, async (req, res) => {
|
||||
const path = getFirstString(req.query.path)
|
||||
if (path) {
|
||||
res.set("Content-Type", getMediaMime(path))
|
||||
res.send(await fs.readFile(pathToFsPath(path)))
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* Used by VS Code to load files.
|
||||
*/
|
||||
router.get("/vscode-remote-resource(/*)?", ensureAuthenticated, async (req, res) => {
|
||||
const path = getFirstString(req.query.path)
|
||||
if (path) {
|
||||
res.set("Content-Type", getMediaMime(path))
|
||||
res.send(await fs.readFile(pathToFsPath(path)))
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* VS Code webviews use these paths to load files and to load webview assets
|
||||
* like HTML and JavaScript.
|
||||
*/
|
||||
router.get("/webview/*", ensureAuthenticated, async (req, res) => {
|
||||
res.set("Content-Type", getMediaMime(req.path))
|
||||
if (/^vscode-resource/.test(req.params[0])) {
|
||||
return res.send(await fs.readFile(req.params[0].replace(/^vscode-resource(\/file)?/, "")))
|
||||
}
|
||||
return res.send(
|
||||
await fs.readFile(path.join(vscode.vsRootPath, "out/vs/workbench/contrib/webview/browser/pre", req.params[0])),
|
||||
)
|
||||
})
|
||||
|
||||
interface Callback {
|
||||
uri: {
|
||||
scheme: string
|
||||
authority?: string
|
||||
path?: string
|
||||
query?: string
|
||||
fragment?: string
|
||||
}
|
||||
timeout: NodeJS.Timeout
|
||||
export interface VSServerResult {
|
||||
router: express.Router
|
||||
wsRouter: WebsocketRouter
|
||||
vscodeServer: Server
|
||||
}
|
||||
|
||||
const callbacks = new Map<string, Callback>()
|
||||
const callbackEmitter = new Emitter<{ id: string; callback: Callback }>()
|
||||
export const createVSServerRouter = async (args: DefaultedArgs): Promise<VSServerResult> => {
|
||||
// Delete `VSCODE_CWD` very early even before
|
||||
// importing bootstrap files. We have seen
|
||||
// reports where `code .` would use the wrong
|
||||
// current working directory due to our variable
|
||||
// somehow escaping to the parent shell
|
||||
// (https://github.com/microsoft/vscode/issues/126399)
|
||||
delete process.env["VSCODE_CWD"]
|
||||
|
||||
/**
|
||||
* 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)
|
||||
}
|
||||
const bootstrap = require(path.join(vsRootPath, "out", "bootstrap"))
|
||||
const bootstrapNode = require(path.join(vsRootPath, "out", "bootstrap-node"))
|
||||
const product = require(path.join(vsRootPath, "product.json"))
|
||||
|
||||
if (typeof req.query["vscode-requestId"] !== "string") {
|
||||
throw new HttpError("vscode-requestId is not a string", HttpCode.BadRequest)
|
||||
}
|
||||
// Avoid Monkey Patches from Application Insights
|
||||
bootstrap.avoidMonkeyPatchFromAppInsights()
|
||||
|
||||
return req.query["vscode-requestId"]
|
||||
}
|
||||
// Enable portable support
|
||||
bootstrapNode.configurePortable(product)
|
||||
|
||||
// Matches VS Code's fetch timeout.
|
||||
const fetchTimeout = 5 * 60 * 1000
|
||||
// Enable ASAR support
|
||||
bootstrap.enableASARSupport()
|
||||
|
||||
// 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",
|
||||
]
|
||||
// Signal processes that we got launched as CLI
|
||||
process.env["VSCODE_CLI"] = "1"
|
||||
|
||||
const id = getRequestId(req)
|
||||
const vscodeServerMain = await loadAMDModule<CodeServerLib.CreateVSServer>("vs/server/entry", "createVSServer")
|
||||
|
||||
// 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"]),
|
||||
query: (getFirstString(req.query.query) || "") + "&" + qs.stringify(query),
|
||||
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, "vendor/modules/code-oss-dev/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)
|
||||
}
|
||||
const serverUrl = new URL(`${args.cert ? "https" : "http"}://${args.host}:${args.port}`)
|
||||
const vscodeServer = await vscodeServerMain({
|
||||
codeServerVersion,
|
||||
serverUrl,
|
||||
args,
|
||||
authed: args.auth !== AuthType.None,
|
||||
disableUpdateCheck: !!args["disable-update-check"],
|
||||
})
|
||||
|
||||
// If the client closes the connection.
|
||||
req.on("close", () => handler.dispose())
|
||||
})
|
||||
const router = express.Router()
|
||||
const wsRouter = WsRouter()
|
||||
|
||||
export const wsRouter = WsRouter()
|
||||
router.all("*", ensureAuthenticated, (req, res, next) => {
|
||||
req.on("error", (error) => errorHandler(error, req, res, next))
|
||||
|
||||
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")
|
||||
vscodeServer.emit("request", req, res)
|
||||
})
|
||||
|
||||
const responseHeaders = [
|
||||
"HTTP/1.1 101 Switching Protocols",
|
||||
"Upgrade: websocket",
|
||||
"Connection: Upgrade",
|
||||
`Sec-WebSocket-Accept: ${reply}`,
|
||||
]
|
||||
wsRouter.ws("/", ensureAuthenticated, (req) => {
|
||||
vscodeServer.emit("upgrade", req, req.socket, req.head)
|
||||
|
||||
// See if the browser reports it supports web socket compression.
|
||||
// TODO: Parse this header properly.
|
||||
const extensions = req.headers["sec-websocket-extensions"]
|
||||
const isCompressionSupported = extensions ? extensions.includes("permessage-deflate") : false
|
||||
req.socket.resume()
|
||||
})
|
||||
|
||||
// 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.
|
||||
responseHeaders.push("Sec-WebSocket-Extensions: permessage-deflate; server_max_window_bits=15")
|
||||
return {
|
||||
router,
|
||||
wsRouter,
|
||||
vscodeServer,
|
||||
}
|
||||
|
||||
req.ws.write(responseHeaders.join("\r\n") + "\r\n\r\n")
|
||||
|
||||
await vscode.sendWebsocket(req.ws, req.query, useCompression)
|
||||
})
|
||||
}
|
||||
|
@ -20,7 +20,7 @@ export class SettingsProvider<T> {
|
||||
try {
|
||||
const raw = (await fs.readFile(this.settingsPath, "utf8")).trim()
|
||||
return raw ? JSON.parse(raw) : {}
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
if (error.code !== "ENOENT") {
|
||||
logger.warn(error.message)
|
||||
}
|
||||
@ -37,7 +37,7 @@ export class SettingsProvider<T> {
|
||||
const oldSettings = await this.read()
|
||||
const nextSettings = { ...oldSettings, ...settings }
|
||||
await fs.writeFile(this.settingsPath, JSON.stringify(nextSettings, null, 2))
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
logger.warn(error.message)
|
||||
}
|
||||
}
|
||||
|
@ -60,7 +60,7 @@ export class UpdateProvider {
|
||||
}
|
||||
logger.debug("got latest version", field("latest", update.version))
|
||||
return update
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
logger.error("Failed to get latest version", field("error", error.message))
|
||||
return {
|
||||
checked: now,
|
||||
|
@ -1,66 +0,0 @@
|
||||
// In a bit of a hack, this file is stored in two places
|
||||
// - src/node/uri_transformer.ts
|
||||
// - lib/vscode/src/vs/server/uriTransformer.ts
|
||||
|
||||
// The reason for this is that we need a CommonJS-compiled
|
||||
// version of this file to supply as a command line argument
|
||||
// to extensionHostProcessSetup.ts; but we also need to include
|
||||
// it ourselves cleanly in `lib/vscode/src/vs/server`.
|
||||
|
||||
// @oxy: Could not figure out how to compile as a CommonJS module
|
||||
// in the same tree as VSCode, which is why I came up with the solution
|
||||
// of storing it in two places.
|
||||
|
||||
// NOTE: copied over from lib/vscode/src/vs/common/uriIpc.ts
|
||||
// remember to update this for proper type checks!
|
||||
|
||||
interface UriParts {
|
||||
scheme: string
|
||||
authority?: string
|
||||
path?: string
|
||||
}
|
||||
|
||||
interface IRawURITransformer {
|
||||
transformIncoming(uri: UriParts): UriParts
|
||||
transformOutgoing(uri: UriParts): UriParts
|
||||
transformOutgoingScheme(scheme: string): string
|
||||
}
|
||||
|
||||
// Using `export =` is deliberate.
|
||||
// See lib/vscode/src/vs/workbench/services/extensions/node/extensionHostProcessSetup.ts;
|
||||
// they include the file directly with a node require and expect a function as `module.exports`.
|
||||
// `export =` in TypeScript is equivalent to `module.exports =` in vanilla JS.
|
||||
export = function rawURITransformerFactory(authority: string) {
|
||||
return new RawURITransformer(authority)
|
||||
}
|
||||
|
||||
class RawURITransformer implements IRawURITransformer {
|
||||
constructor(private readonly authority: string) {}
|
||||
|
||||
transformIncoming(uri: UriParts): UriParts {
|
||||
switch (uri.scheme) {
|
||||
case "vscode-remote":
|
||||
return { scheme: "file", path: uri.path }
|
||||
default:
|
||||
return uri
|
||||
}
|
||||
}
|
||||
|
||||
transformOutgoing(uri: UriParts): UriParts {
|
||||
switch (uri.scheme) {
|
||||
case "file":
|
||||
return { scheme: "vscode-remote", authority: this.authority, path: uri.path }
|
||||
default:
|
||||
return uri
|
||||
}
|
||||
}
|
||||
|
||||
transformOutgoingScheme(scheme: string): string {
|
||||
switch (scheme) {
|
||||
case "file":
|
||||
return "vscode-remote"
|
||||
default:
|
||||
return scheme
|
||||
}
|
||||
}
|
||||
}
|
@ -10,7 +10,7 @@ import * as path from "path"
|
||||
import safeCompare from "safe-compare"
|
||||
import * as util from "util"
|
||||
import xdgBasedir from "xdg-basedir"
|
||||
import { getFirstString } from "../common/util"
|
||||
import { vsRootPath } from "./constants"
|
||||
|
||||
export interface Paths {
|
||||
data: string
|
||||
@ -157,7 +157,7 @@ export const generatePassword = async (length = 24): Promise<string> => {
|
||||
export const hash = async (password: string): Promise<string> => {
|
||||
try {
|
||||
return await argon2.hash(password)
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
logger.error(error)
|
||||
return ""
|
||||
}
|
||||
@ -172,7 +172,7 @@ export const isHashMatch = async (password: string, hash: string) => {
|
||||
}
|
||||
try {
|
||||
return await argon2.verify(hash, password)
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
throw new Error(error)
|
||||
}
|
||||
}
|
||||
@ -439,55 +439,6 @@ export const isObject = <T extends object>(obj: T): obj is T => {
|
||||
return !Array.isArray(obj) && typeof obj === "object" && obj !== null
|
||||
}
|
||||
|
||||
/**
|
||||
* Taken from vs/base/common/charCode.ts. Copied for now instead of importing so
|
||||
* we don't have to set up a `vs` alias to be able to import with types (since
|
||||
* the alternative is to directly import from `out`).
|
||||
*/
|
||||
enum CharCode {
|
||||
Slash = 47,
|
||||
A = 65,
|
||||
Z = 90,
|
||||
a = 97,
|
||||
z = 122,
|
||||
Colon = 58,
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute `fsPath` for the given uri.
|
||||
* Taken from vs/base/common/uri.ts. It's not imported to avoid also importing
|
||||
* everything that file imports.
|
||||
*/
|
||||
export function pathToFsPath(path: string, keepDriveLetterCasing = false): string {
|
||||
const isWindows = process.platform === "win32"
|
||||
const uri = { authority: undefined, path: getFirstString(path) || "", scheme: "file" }
|
||||
let value: string
|
||||
|
||||
if (uri.authority && uri.path.length > 1 && uri.scheme === "file") {
|
||||
// unc path: file://shares/c$/far/boo
|
||||
value = `//${uri.authority}${uri.path}`
|
||||
} else if (
|
||||
uri.path.charCodeAt(0) === CharCode.Slash &&
|
||||
((uri.path.charCodeAt(1) >= CharCode.A && uri.path.charCodeAt(1) <= CharCode.Z) ||
|
||||
(uri.path.charCodeAt(1) >= CharCode.a && uri.path.charCodeAt(1) <= CharCode.z)) &&
|
||||
uri.path.charCodeAt(2) === CharCode.Colon
|
||||
) {
|
||||
if (!keepDriveLetterCasing) {
|
||||
// windows drive letter: file:///c:/far/boo
|
||||
value = uri.path[1].toLowerCase() + uri.path.substr(2)
|
||||
} else {
|
||||
value = uri.path.substr(1)
|
||||
}
|
||||
} else {
|
||||
// other path
|
||||
value = uri.path
|
||||
}
|
||||
if (isWindows) {
|
||||
value = value.replace(/\//g, "\\")
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a promise that resolves with whether the socket path is active.
|
||||
*/
|
||||
@ -533,3 +484,23 @@ export function escapeHtml(unsafe: string): string {
|
||||
export function isNodeJSErrnoException(error: unknown): error is NodeJS.ErrnoException {
|
||||
return error instanceof Error && (error as NodeJS.ErrnoException).code !== undefined
|
||||
}
|
||||
|
||||
// TODO: Replace with proper templating system.
|
||||
export const escapeJSON = (value: cp.Serializable) => JSON.stringify(value).replace(/"/g, """)
|
||||
|
||||
type AMDModule<T> = { [exportName: string]: T }
|
||||
|
||||
/**
|
||||
* Loads AMD module, typically from a compiled VSCode bundle.
|
||||
*
|
||||
* @deprecated This should be gradually phased out as code-server migrates to lib/vscode
|
||||
* @param amdPath Path to module relative to lib/vscode
|
||||
* @param exportName Given name of export in the file
|
||||
*/
|
||||
export const loadAMDModule = async <T>(amdPath: string, exportName: string): Promise<T> => {
|
||||
const module = await new Promise<AMDModule<T>>((resolve, reject) => {
|
||||
require(path.join(vsRootPath, "out/bootstrap-amd")).load(amdPath, resolve, reject)
|
||||
})
|
||||
|
||||
return module[exportName] as T
|
||||
}
|
||||
|
@ -1,168 +0,0 @@
|
||||
import { logger } from "@coder/logger"
|
||||
import * as cp from "child_process"
|
||||
import * as net from "net"
|
||||
import * as path from "path"
|
||||
import * as ipc from "../../typings/ipc"
|
||||
import { arrayify, generateUuid } from "../common/util"
|
||||
import { rootPath } from "./constants"
|
||||
import { settings } from "./settings"
|
||||
import { SocketProxyProvider } from "./socket"
|
||||
import { isFile } from "./util"
|
||||
import { onMessage, wrapper } from "./wrapper"
|
||||
|
||||
export class VscodeProvider {
|
||||
public readonly serverRootPath: string
|
||||
public readonly vsRootPath: string
|
||||
private _vscode?: Promise<cp.ChildProcess>
|
||||
private readonly socketProvider = new SocketProxyProvider()
|
||||
|
||||
public constructor() {
|
||||
this.vsRootPath = path.resolve(rootPath, "vendor/modules/code-oss-dev")
|
||||
this.serverRootPath = path.join(this.vsRootPath, "out/vs/server")
|
||||
wrapper.onDispose(() => this.dispose())
|
||||
}
|
||||
|
||||
public async dispose(): Promise<void> {
|
||||
this.socketProvider.stop()
|
||||
if (this._vscode) {
|
||||
const vscode = await this._vscode
|
||||
vscode.removeAllListeners()
|
||||
vscode.kill()
|
||||
this._vscode = undefined
|
||||
}
|
||||
}
|
||||
|
||||
public async initialize(
|
||||
options: Omit<ipc.VscodeOptions, "startPath">,
|
||||
query: ipc.Query,
|
||||
): Promise<ipc.WorkbenchOptions> {
|
||||
const { lastVisited } = await settings.read()
|
||||
let startPath = await this.getFirstPath([
|
||||
{ url: query.workspace, workspace: true },
|
||||
{ url: query.folder, workspace: false },
|
||||
options.args._ && options.args._.length > 0
|
||||
? { url: path.resolve(options.args._[options.args._.length - 1]) }
|
||||
: undefined,
|
||||
!options.args["ignore-last-opened"] ? lastVisited : undefined,
|
||||
])
|
||||
|
||||
if (query.ew) {
|
||||
startPath = undefined
|
||||
}
|
||||
|
||||
settings.write({
|
||||
lastVisited: startPath,
|
||||
query,
|
||||
})
|
||||
|
||||
const id = generateUuid()
|
||||
const vscode = await this.fork()
|
||||
|
||||
logger.debug("setting up vs code...")
|
||||
|
||||
this.send(
|
||||
{
|
||||
type: "init",
|
||||
id,
|
||||
options: {
|
||||
...options,
|
||||
startPath,
|
||||
},
|
||||
},
|
||||
vscode,
|
||||
)
|
||||
|
||||
const message = await onMessage<ipc.VscodeMessage, ipc.OptionsMessage>(
|
||||
vscode,
|
||||
(message): message is ipc.OptionsMessage => {
|
||||
// There can be parallel initializations so wait for the right ID.
|
||||
return message.type === "options" && message.id === id
|
||||
},
|
||||
)
|
||||
|
||||
return message.options
|
||||
}
|
||||
|
||||
private fork(): Promise<cp.ChildProcess> {
|
||||
if (this._vscode) {
|
||||
return this._vscode
|
||||
}
|
||||
|
||||
logger.debug("forking vs code...")
|
||||
const vscode = cp.fork(path.join(this.serverRootPath, "fork"))
|
||||
|
||||
const dispose = () => {
|
||||
vscode.removeAllListeners()
|
||||
vscode.kill()
|
||||
this._vscode = undefined
|
||||
}
|
||||
|
||||
vscode.on("error", (error: Error) => {
|
||||
logger.error(error.message)
|
||||
if (error.stack) {
|
||||
logger.debug(error.stack)
|
||||
}
|
||||
dispose()
|
||||
})
|
||||
|
||||
vscode.on("exit", (code) => {
|
||||
logger.error(`VS Code exited unexpectedly with code ${code}`)
|
||||
dispose()
|
||||
})
|
||||
|
||||
this._vscode = onMessage<ipc.VscodeMessage, ipc.ReadyMessage>(vscode, (message): message is ipc.ReadyMessage => {
|
||||
return message.type === "ready"
|
||||
}).then(() => vscode)
|
||||
|
||||
return this._vscode
|
||||
}
|
||||
|
||||
/**
|
||||
* VS Code expects a raw socket. It will handle all the web socket frames.
|
||||
*/
|
||||
public async sendWebsocket(socket: net.Socket, query: ipc.Query, permessageDeflate: boolean): Promise<void> {
|
||||
const vscode = await this._vscode
|
||||
// TLS sockets cannot be transferred to child processes so we need an
|
||||
// in-between. Non-TLS sockets will be returned as-is.
|
||||
const socketProxy = await this.socketProvider.createProxy(socket)
|
||||
this.send({ type: "socket", query, permessageDeflate }, vscode, socketProxy)
|
||||
}
|
||||
|
||||
private send(message: ipc.CodeServerMessage, vscode?: cp.ChildProcess, socket?: net.Socket): void {
|
||||
if (!vscode || vscode.killed) {
|
||||
throw new Error("vscode is not running")
|
||||
}
|
||||
vscode.send(message, socket)
|
||||
}
|
||||
|
||||
/**
|
||||
* Choose the first non-empty path from the provided array.
|
||||
*
|
||||
* Each array item consists of `url` and an optional `workspace` boolean that
|
||||
* indicates whether that url is for a workspace.
|
||||
*
|
||||
* `url` can be a fully qualified URL or just the path portion.
|
||||
*
|
||||
* `url` can also be a query object to make it easier to pass in query
|
||||
* variables directly but anything that isn't a string or string array is not
|
||||
* valid and will be ignored.
|
||||
*/
|
||||
private async getFirstPath(
|
||||
startPaths: Array<{ url?: string | string[] | ipc.Query | ipc.Query[]; workspace?: boolean } | undefined>,
|
||||
): Promise<ipc.StartPath | undefined> {
|
||||
for (let i = 0; i < startPaths.length; ++i) {
|
||||
const startPath = startPaths[i]
|
||||
const url = arrayify(startPath && startPath.url).find((p) => !!p)
|
||||
if (startPath && url && typeof url === "string") {
|
||||
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
|
||||
}
|
||||
}
|
@ -267,7 +267,7 @@ export class ParentProcess extends Process {
|
||||
try {
|
||||
this.started = this._start()
|
||||
await this.started
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
this.logger.error(error.message)
|
||||
this.exit(typeof error.code === "number" ? error.code : 1)
|
||||
}
|
||||
|
Reference in New Issue
Block a user