Switch to Express

It doesn't do anything yet.
This commit is contained in:
Asher 2020-10-16 12:43:49 -05:00
parent 8e93e28162
commit 71dc5c7542
No known key found for this signature in database
GPG Key ID: D63C1EF81242354A
3 changed files with 84 additions and 58 deletions

57
src/node/app.ts Normal file
View File

@ -0,0 +1,57 @@
import { logger } from "@coder/logger"
import express, { Express } from "express"
import { promises as fs } from "fs"
import http from "http"
import * as httpolyglot from "httpolyglot"
import { DefaultedArgs } from "./cli"
/**
* Create an Express app and an HTTP/S server to serve it.
*/
export const createApp = async (args: DefaultedArgs): Promise<[Express, http.Server]> => {
const app = express()
const server = args.cert
? httpolyglot.createServer(
{
cert: args.cert && (await fs.readFile(args.cert.value)),
key: args["cert-key"] && (await fs.readFile(args["cert-key"])),
},
app,
)
: http.createServer(app)
await new Promise<http.Server>(async (resolve, reject) => {
server.on("error", reject)
if (args.socket) {
try {
await fs.unlink(args.socket)
} catch (error) {
if (error.code !== "ENOENT") {
logger.error(error.message)
}
}
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)
}
})
return [app, server]
}
/**
* Get the address of a server as a string (protocol not included) while
* ensuring there is one (will throw if there isn't).
*/
export const ensureAddress = (server: http.Server): string => {
const addr = server.address()
if (!addr) {
throw new Error("server has no address")
}
if (typeof addr !== "string") {
return `${addr.address}:${addr.port}`
}
return addr
}

View File

@ -5,13 +5,9 @@ import http from "http"
import * as path from "path" import * as path from "path"
import { CliMessage, OpenCommandPipeArgs } from "../../lib/vscode/src/vs/server/ipc" import { CliMessage, OpenCommandPipeArgs } from "../../lib/vscode/src/vs/server/ipc"
import { plural } from "../common/util" import { plural } from "../common/util"
import { HealthHttpProvider } from "./routes/health" import { createApp, ensureAddress } from "./app"
import { LoginHttpProvider } from "./routes/login"
import { ProxyHttpProvider } from "./routes/proxy"
import { StaticHttpProvider } from "./routes/static"
import { UpdateHttpProvider } from "./routes/update"
import { VscodeHttpProvider } from "./routes/vscode"
import { import {
AuthType,
DefaultedArgs, DefaultedArgs,
optionDescriptions, optionDescriptions,
parse, parse,
@ -21,9 +17,8 @@ import {
shouldRunVsCodeCli, shouldRunVsCodeCli,
} from "./cli" } from "./cli"
import { coderCloudBind } from "./coder-cloud" import { coderCloudBind } from "./coder-cloud"
import { AuthType, HttpServer, HttpServerOptions } from "./http"
import { loadPlugins } from "./plugin" import { loadPlugins } from "./plugin"
import { hash, humanPath, open } from "./util" import { humanPath, open } from "./util"
import { ipcMain, WrapperProcess } from "./wrapper" import { ipcMain, WrapperProcess } from "./wrapper"
let pkg: { version?: string; commit?: string } = {} let pkg: { version?: string; commit?: string } = {}
@ -117,65 +112,39 @@ export const openInExistingInstance = async (args: DefaultedArgs, socketPath: st
} }
const main = async (args: DefaultedArgs): Promise<void> => { const main = async (args: DefaultedArgs): Promise<void> => {
logger.info(`code-server ${version} ${commit}`)
logger.info(`Using user-data-dir ${humanPath(args["user-data-dir"])}`) logger.info(`Using user-data-dir ${humanPath(args["user-data-dir"])}`)
logger.trace(`Using extensions-dir ${humanPath(args["extensions-dir"])}`) logger.trace(`Using extensions-dir ${humanPath(args["extensions-dir"])}`)
if (args.auth === AuthType.Password && !args.password) { if (args.auth === AuthType.Password && !args.password) {
throw new Error("Please pass in a password via the config file or $PASSWORD") throw new Error("Please pass in a password via the config file or $PASSWORD")
} }
// Spawn the main HTTP server.
const options: HttpServerOptions = {
auth: args.auth,
commit,
host: args.host,
// The hash does not add any actual security but we do it for obfuscation purposes.
password: args.password ? hash(args.password) : undefined,
port: args.port,
proxyDomains: args["proxy-domain"],
socket: args.socket,
cert: args.cert && args.cert.value,
certKey: args["cert-key"],
}
if (options.cert && !options.certKey) {
throw new Error("--cert-key is missing")
}
const httpServer = new HttpServer(options)
httpServer.registerHttpProvider(["/", "/vscode"], VscodeHttpProvider, args)
httpServer.registerHttpProvider("/update", UpdateHttpProvider, false)
httpServer.registerHttpProvider("/proxy", ProxyHttpProvider)
httpServer.registerHttpProvider("/login", LoginHttpProvider, args.config!, args.usingEnvPassword)
httpServer.registerHttpProvider("/static", StaticHttpProvider)
httpServer.registerHttpProvider("/healthz", HealthHttpProvider, httpServer.heart)
await loadPlugins(httpServer, args)
ipcMain.onDispose(() => { ipcMain.onDispose(() => {
httpServer.dispose().then((errors) => { // TODO: register disposables
errors.forEach((error) => logger.error(error.message))
})
}) })
logger.info(`code-server ${version} ${commit}`) const [app, server] = await createApp(args)
const serverAddress = ensureAddress(server)
// TODO: register routes
await loadPlugins(app, args)
logger.info(`Using config file ${humanPath(args.config)}`) logger.info(`Using config file ${humanPath(args.config)}`)
const serverAddress = await httpServer.listen()
logger.info(`HTTP server listening on ${serverAddress} ${args.link ? "(randomized by --link)" : ""}`) logger.info(`HTTP server listening on ${serverAddress} ${args.link ? "(randomized by --link)" : ""}`)
if (args.auth === AuthType.Password) { if (args.auth === AuthType.Password) {
logger.info(" - Authentication is enabled")
if (args.usingEnvPassword) { if (args.usingEnvPassword) {
logger.info(" - Using password from $PASSWORD") logger.info(" - Using password from $PASSWORD")
} else { } else {
logger.info(` - Using password from ${humanPath(args.config)}`) logger.info(` - Using password from ${humanPath(args.config)}`)
} }
logger.info(" - To disable use `--auth none`")
} else { } else {
logger.info(` - No authentication ${args.link ? "(disabled by --link)" : ""}`) logger.info(` - Authentication is disabled ${args.link ? "(disabled by --link)" : ""}`)
} }
if (httpServer.protocol === "https") { if (args.cert) {
logger.info( logger.info(
args.cert && args.cert.value args.cert && args.cert.value
? ` - Using provided certificate and key for HTTPS` ? ` - Using provided certificate and key for HTTPS`
@ -192,7 +161,7 @@ const main = async (args: DefaultedArgs): Promise<void> => {
if (args.link) { if (args.link) {
try { try {
await coderCloudBind(serverAddress!, args.link.value) await coderCloudBind(serverAddress, args.link.value)
logger.info(" - Connected to cloud agent") logger.info(" - Connected to cloud agent")
} catch (err) { } catch (err) {
logger.error(err.message) logger.error(err.message)
@ -200,7 +169,7 @@ const main = async (args: DefaultedArgs): Promise<void> => {
} }
} }
if (serverAddress && !options.socket && args.open) { if (serverAddress && !args.socket && args.open) {
// The web socket doesn't seem to work if browsing with 0.0.0.0. // The web socket doesn't seem to work if browsing with 0.0.0.0.
const openAddress = serverAddress.replace(/:\/\/0.0.0.0/, "://localhost") const openAddress = serverAddress.replace(/:\/\/0.0.0.0/, "://localhost")
await open(openAddress).catch((error: Error) => { await open(openAddress).catch((error: Error) => {

View File

@ -1,14 +1,14 @@
import { field, logger } from "@coder/logger" import { field, logger } from "@coder/logger"
import { Express } from "express"
import * as fs from "fs" import * as fs from "fs"
import * as path from "path" import * as path from "path"
import * as util from "util" import * as util from "util"
import { Args } from "./cli" import { Args } from "./cli"
import { HttpServer } from "./http"
import { paths } from "./util" import { paths } from "./util"
/* eslint-disable @typescript-eslint/no-var-requires */ /* eslint-disable @typescript-eslint/no-var-requires */
export type Activate = (httpServer: HttpServer, args: Args) => void export type Activate = (app: Express, args: Args) => void
/** /**
* Plugins must implement this interface. * Plugins must implement this interface.
@ -30,10 +30,10 @@ require("module")._load = function (request: string, parent: object, isMain: boo
/** /**
* Load a plugin and run its activation function. * Load a plugin and run its activation function.
*/ */
const loadPlugin = async (pluginPath: string, httpServer: HttpServer, args: Args): Promise<void> => { const loadPlugin = async (pluginPath: string, app: Express, args: Args): Promise<void> => {
try { try {
const plugin: Plugin = require(pluginPath) const plugin: Plugin = require(pluginPath)
plugin.activate(httpServer, args) plugin.activate(app, args)
const packageJson = require(path.join(pluginPath, "package.json")) const packageJson = require(path.join(pluginPath, "package.json"))
logger.debug( logger.debug(
@ -50,12 +50,12 @@ const loadPlugin = async (pluginPath: string, httpServer: HttpServer, args: Args
/** /**
* Load all plugins in the specified directory. * Load all plugins in the specified directory.
*/ */
const _loadPlugins = async (pluginDir: string, httpServer: HttpServer, args: Args): Promise<void> => { const _loadPlugins = async (pluginDir: string, app: Express, args: Args): Promise<void> => {
try { try {
const files = await util.promisify(fs.readdir)(pluginDir, { const files = await util.promisify(fs.readdir)(pluginDir, {
withFileTypes: true, withFileTypes: true,
}) })
await Promise.all(files.map((file) => loadPlugin(path.join(pluginDir, file.name), httpServer, args))) await Promise.all(files.map((file) => loadPlugin(path.join(pluginDir, file.name), app, args)))
} catch (error) { } catch (error) {
if (error.code !== "ENOENT") { if (error.code !== "ENOENT") {
logger.warn(error.message) logger.warn(error.message)
@ -68,17 +68,17 @@ const _loadPlugins = async (pluginDir: string, httpServer: HttpServer, args: Arg
* `CS_PLUGIN_PATH` (colon-separated), and individual plugins specified by * `CS_PLUGIN_PATH` (colon-separated), and individual plugins specified by
* `CS_PLUGIN` (also colon-separated). * `CS_PLUGIN` (also colon-separated).
*/ */
export const loadPlugins = async (httpServer: HttpServer, args: Args): Promise<void> => { export const loadPlugins = async (app: Express, args: Args): Promise<void> => {
const pluginPath = process.env.CS_PLUGIN_PATH || `${path.join(paths.data, "plugins")}:/usr/share/code-server/plugins` const pluginPath = process.env.CS_PLUGIN_PATH || `${path.join(paths.data, "plugins")}:/usr/share/code-server/plugins`
const plugin = process.env.CS_PLUGIN || "" const plugin = process.env.CS_PLUGIN || ""
await Promise.all([ await Promise.all([
// Built-in plugins. // Built-in plugins.
_loadPlugins(path.resolve(__dirname, "../../plugins"), httpServer, args), _loadPlugins(path.resolve(__dirname, "../../plugins"), app, args),
// User-added plugins. // User-added plugins.
...pluginPath ...pluginPath
.split(":") .split(":")
.filter((p) => !!p) .filter((p) => !!p)
.map((dir) => _loadPlugins(path.resolve(dir), httpServer, args)), .map((dir) => _loadPlugins(path.resolve(dir), app, args)),
// Individual plugins so you don't have to symlink or move them into a // Individual plugins so you don't have to symlink or move them into a
// directory specifically for plugins. This lets you load plugins that are // directory specifically for plugins. This lets you load plugins that are
// on the same level as other directories that are not plugins (if you tried // on the same level as other directories that are not plugins (if you tried
@ -87,6 +87,6 @@ export const loadPlugins = async (httpServer: HttpServer, args: Args): Promise<v
...plugin ...plugin
.split(":") .split(":")
.filter((p) => !!p) .filter((p) => !!p)
.map((dir) => loadPlugin(path.resolve(dir), httpServer, args)), .map((dir) => loadPlugin(path.resolve(dir), app, args)),
]) ])
} }