From c5179c2a06b6e7f47ac1244b2f75db0a0a6f4e0e Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Sun, 10 May 2020 01:19:32 -0400 Subject: [PATCH 01/11] Add support for a YAML config file --- package.json | 5 ++++- src/node/cli.ts | 50 +++++++++++++++++++++++++++++++++++++++++++++++ src/node/entry.ts | 4 +++- yarn.lock | 10 ++++++++++ 4 files changed, 67 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index d27eff822..f1813e899 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "@types/adm-zip": "^0.4.32", "@types/fs-extra": "^8.0.1", "@types/http-proxy": "^1.17.4", + "@types/js-yaml": "^3.12.3", "@types/mocha": "^5.2.7", "@types/node": "^12.12.7", "@types/parcel-bundler": "^1.12.1", @@ -69,13 +70,15 @@ "fs-extra": "^8.1.0", "http-proxy": "^1.18.0", "httpolyglot": "^0.1.2", + "js-yaml": "^3.13.1", "limiter": "^1.1.5", "pem": "^1.14.2", "safe-compare": "^1.1.4", "semver": "^7.1.3", "tar": "^6.0.1", "tar-fs": "^2.0.0", - "ws": "^7.2.0" + "ws": "^7.2.0", + "xdg-basedir": "^4.0.0" }, "bin": { "code-server": "out/node/entry.js" diff --git a/src/node/cli.ts b/src/node/cli.ts index 5b2075518..f135d32a6 100644 --- a/src/node/cli.ts +++ b/src/node/cli.ts @@ -1,8 +1,11 @@ +import * as fs from "fs-extra" +import yaml from "js-yaml" import * as path from "path" import { field, logger, Level } from "@coder/logger" import { Args as VsArgs } from "../../lib/vscode/src/vs/server/ipc" import { AuthType } from "./http" import { xdgLocalDir } from "./util" +import xdgBasedir from "xdg-basedir" export class Optional { public constructor(public readonly value?: T) {} @@ -19,6 +22,7 @@ export enum LogLevel { export class OptionalString extends Optional {} export interface Args extends VsArgs { + readonly config?: string readonly auth?: AuthType readonly cert?: OptionalString readonly "cert-key"?: string @@ -95,6 +99,8 @@ const options: Options> = { "bind-addr": { type: "string", description: "Address to bind to in host:port." }, + config: { type: "string", description: "Path to yaml config file." }, + // These two have been deprecated by bindAddr. host: { type: "string", description: "" }, port: { type: "number", description: "" }, @@ -275,3 +281,47 @@ export const parse = (argv: string[]): Args => { return args } + +// readConfigFile reads the config file specified in the config flag +// and loads it's configuration. +// +// Flags set on the CLI take priority. +// +// The config file can also be passed via $CODE_SERVER_CONFIG and defaults +// to ~/.config/code-server/config.yaml. +export async function readConfigFile(args: Args): Promise { + const configPath = getConfigPath(args) + if (configPath === undefined) { + return args + } + + if (!(await fs.pathExists(configPath))) { + await fs.outputFile(configPath, `default: hello`) + } + + const configFile = await fs.readFile(configPath) + const config = yaml.safeLoad(configFile.toString(), { + filename: args.config, + }) + + // We convert the config file into a set of flags. + // This is a temporary measure until we add a proper CLI library. + const configFileArgv = Object.entries(config).map(([optName, opt]) => `--${optName}=${opt}`) + const configFileArgs = parse(configFileArgv) + + // This prioritizes the flags set in args over the ones in the config file. + return Object.assign(configFileArgs, args) +} + +function getConfigPath(args: Args): string | undefined { + if (args.config !== undefined) { + return args.config + } + if (process.env.CODE_SERVER_CONFIG !== undefined) { + return process.env.CODE_SERVER_CONFIG + } + if (xdgBasedir.config !== undefined) { + return `${xdgBasedir.config}/code-server/config.yaml` + } + return undefined +} diff --git a/src/node/entry.ts b/src/node/entry.ts index c4b0cbc88..493d26895 100644 --- a/src/node/entry.ts +++ b/src/node/entry.ts @@ -9,7 +9,7 @@ import { ProxyHttpProvider } from "./app/proxy" import { StaticHttpProvider } from "./app/static" import { UpdateHttpProvider } from "./app/update" import { VscodeHttpProvider } from "./app/vscode" -import { Args, optionDescriptions, parse } from "./cli" +import { Args, optionDescriptions, parse, readConfigFile } from "./cli" import { AuthType, HttpServer, HttpServerOptions } from "./http" import { generateCertificate, generatePassword, hash, open } from "./util" import { ipcMain, wrap } from "./wrapper" @@ -32,6 +32,8 @@ const version = pkg.version || "development" const commit = pkg.commit || "development" const main = async (args: Args): Promise => { + args = await readConfigFile(args) + const auth = args.auth || AuthType.Password const originalPassword = auth === AuthType.Password && (process.env.PASSWORD || (await generatePassword())) diff --git a/yarn.lock b/yarn.lock index 473107bb4..470f299fd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -931,6 +931,11 @@ dependencies: "@types/node" "*" +"@types/js-yaml@^3.12.3": + version "3.12.3" + resolved "https://registry.yarnpkg.com/@types/js-yaml/-/js-yaml-3.12.3.tgz#abf383c5b639d0aa8b8c4a420d6a85f703357d6c" + integrity sha512-otRe77JNNWzoVGLKw8TCspKswRoQToys4tuL6XYVBFxjgeM0RUrx7m3jkaTdxILxeGry3zM8mGYkGXMeQ02guA== + "@types/json-schema@^7.0.3": version "7.0.4" resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.4.tgz#38fd73ddfd9b55abb1e1b2ed578cb55bd7b7d339" @@ -7373,6 +7378,11 @@ ws@^7.2.0: resolved "https://registry.yarnpkg.com/ws/-/ws-7.2.5.tgz#abb1370d4626a5a9cd79d8de404aa18b3465d10d" integrity sha512-C34cIU4+DB2vMyAbmEKossWq2ZQDr6QEyuuCzWrM9zfw1sGc0mYiJ0UnG9zzNykt49C2Fi34hvr2vssFQRS6EA== +xdg-basedir@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-4.0.0.tgz#4bc8d9984403696225ef83a1573cbbcb4e79db13" + integrity sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q== + xml-name-validator@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-3.0.0.tgz#6ae73e06de4d8c6e47f9fb181f78d648ad457c6a" From 00d164b67fa5597fea53e9e982eb5fd1a4d80dbd Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Sun, 10 May 2020 01:35:42 -0400 Subject: [PATCH 02/11] Add default config file and improve config/data directory detection --- doc/FAQ.md | 6 ++---- package.json | 1 + src/node/cli.ts | 32 ++++++++++++++++------------ src/node/entry.ts | 7 ++++++- src/node/http.ts | 4 ++-- src/node/settings.ts | 4 ++-- src/node/util.ts | 50 ++++++++++++++++++++++++++++++++------------ test/cli.test.ts | 6 +++--- yarn.lock | 5 +++++ 9 files changed, 77 insertions(+), 38 deletions(-) diff --git a/doc/FAQ.md b/doc/FAQ.md index ec0acf934..cc6197302 100644 --- a/doc/FAQ.md +++ b/doc/FAQ.md @@ -168,10 +168,8 @@ code-server crashes can be helpful. ### Where is the data directory? If the `XDG_DATA_HOME` environment variable is set the data directory will be -`$XDG_DATA_HOME/code-server`. Otherwise the default is: - -1. Linux: `~/.local/share/code-server`. -2. Mac: `~/Library/Application\ Support/code-server`. +`$XDG_DATA_HOME/code-server`. Otherwise the default is `~/.local/share/code-server`. +On Windows, it will be `%APPDATA%\Local\code-server\Data`. ## Enterprise diff --git a/package.json b/package.json index f1813e899..c86e0174f 100644 --- a/package.json +++ b/package.json @@ -67,6 +67,7 @@ "dependencies": { "@coder/logger": "1.1.11", "adm-zip": "^0.4.14", + "env-paths": "^2.2.0", "fs-extra": "^8.1.0", "http-proxy": "^1.18.0", "httpolyglot": "^0.1.2", diff --git a/src/node/cli.ts b/src/node/cli.ts index f135d32a6..53561d258 100644 --- a/src/node/cli.ts +++ b/src/node/cli.ts @@ -4,8 +4,7 @@ import * as path from "path" import { field, logger, Level } from "@coder/logger" import { Args as VsArgs } from "../../lib/vscode/src/vs/server/ipc" import { AuthType } from "./http" -import { xdgLocalDir } from "./util" -import xdgBasedir from "xdg-basedir" +import { paths, uxPath } from "./util" export class Optional { public constructor(public readonly value?: T) {} @@ -272,7 +271,7 @@ export const parse = (argv: string[]): Args => { } if (!args["user-data-dir"]) { - args["user-data-dir"] = xdgLocalDir + args["user-data-dir"] = paths.data } if (!args["extensions-dir"]) { @@ -282,6 +281,11 @@ export const parse = (argv: string[]): Args => { return args } +const defaultConfigFile = ` +auth: password +bind-addr: 127.0.0.1:8080 +`.trimLeft() + // readConfigFile reads the config file specified in the config flag // and loads it's configuration. // @@ -291,14 +295,14 @@ export const parse = (argv: string[]): Args => { // to ~/.config/code-server/config.yaml. export async function readConfigFile(args: Args): Promise { const configPath = getConfigPath(args) - if (configPath === undefined) { - return args - } if (!(await fs.pathExists(configPath))) { - await fs.outputFile(configPath, `default: hello`) + await fs.outputFile(configPath, defaultConfigFile) + logger.info(`Wrote default config file to ${uxPath(configPath)}`) } + logger.info(`Using config file from ${uxPath(configPath)}`) + const configFile = await fs.readFile(configPath) const config = yaml.safeLoad(configFile.toString(), { filename: args.config, @@ -306,22 +310,24 @@ export async function readConfigFile(args: Args): Promise { // We convert the config file into a set of flags. // This is a temporary measure until we add a proper CLI library. - const configFileArgv = Object.entries(config).map(([optName, opt]) => `--${optName}=${opt}`) + const configFileArgv = Object.entries(config).map(([optName, opt]) => { + if (opt === null) { + return `--${optName}` + } + return `--${optName}=${opt}` + }) const configFileArgs = parse(configFileArgv) // This prioritizes the flags set in args over the ones in the config file. return Object.assign(configFileArgs, args) } -function getConfigPath(args: Args): string | undefined { +function getConfigPath(args: Args): string { if (args.config !== undefined) { return args.config } if (process.env.CODE_SERVER_CONFIG !== undefined) { return process.env.CODE_SERVER_CONFIG } - if (xdgBasedir.config !== undefined) { - return `${xdgBasedir.config}/code-server/config.yaml` - } - return undefined + return path.join(paths.config, "config.yaml") } diff --git a/src/node/entry.ts b/src/node/entry.ts index 493d26895..f048ec9a8 100644 --- a/src/node/entry.ts +++ b/src/node/entry.ts @@ -11,7 +11,7 @@ import { UpdateHttpProvider } from "./app/update" import { VscodeHttpProvider } from "./app/vscode" import { Args, optionDescriptions, parse, readConfigFile } from "./cli" import { AuthType, HttpServer, HttpServerOptions } from "./http" -import { generateCertificate, generatePassword, hash, open } from "./util" +import { generateCertificate, generatePassword, hash, open, uxPath } from "./util" import { ipcMain, wrap } from "./wrapper" process.on("uncaughtException", (error) => { @@ -34,6 +34,11 @@ const commit = pkg.commit || "development" const main = async (args: Args): Promise => { args = await readConfigFile(args) + if (args.verbose === true) { + logger.info(`Using extensions-dir at ${uxPath(args["extensions-dir"]!)}`) + logger.info(`Using user-data-dir at ${uxPath(args["user-data-dir"]!)}`) + } + const auth = args.auth || AuthType.Password const originalPassword = auth === AuthType.Password && (process.env.PASSWORD || (await generatePassword())) diff --git a/src/node/http.ts b/src/node/http.ts index 5c0653747..96f074a6c 100644 --- a/src/node/http.ts +++ b/src/node/http.ts @@ -14,7 +14,7 @@ import * as url from "url" import { HttpCode, HttpError } from "../common/http" import { normalize, Options, plural, split } from "../common/util" import { SocketProxyProvider } from "./socket" -import { getMediaMime, xdgLocalDir } from "./util" +import { getMediaMime, paths } from "./util" export type Cookies = { [key: string]: string[] | undefined } export type PostData = { [key: string]: string | string[] | undefined } @@ -473,7 +473,7 @@ export class HttpServer { public constructor(private readonly options: HttpServerOptions) { this.proxyDomains = new Set((options.proxyDomains || []).map((d) => d.replace(/^\*\./, ""))) - this.heart = new Heart(path.join(xdgLocalDir, "heartbeat"), async () => { + this.heart = new Heart(path.join(paths.data, "heartbeat"), async () => { const connections = await this.getConnections() logger.trace(`${connections} active connection${plural(connections)}`) return connections !== 0 diff --git a/src/node/settings.ts b/src/node/settings.ts index 0d6152b13..32166ddb7 100644 --- a/src/node/settings.ts +++ b/src/node/settings.ts @@ -1,6 +1,6 @@ import * as fs from "fs-extra" import * as path from "path" -import { extend, xdgLocalDir } from "./util" +import { extend, paths } from "./util" import { logger } from "@coder/logger" export type Settings = { [key: string]: Settings | string | boolean | number } @@ -60,4 +60,4 @@ export interface CoderSettings extends UpdateSettings { /** * Global code-server settings file. */ -export const settings = new SettingsProvider(path.join(xdgLocalDir, "coder.json")) +export const settings = new SettingsProvider(path.join(paths.data, "coder.json")) diff --git a/src/node/util.ts b/src/node/util.ts index 44e27be05..72d3332fe 100644 --- a/src/node/util.ts +++ b/src/node/util.ts @@ -4,24 +4,48 @@ import * as fs from "fs-extra" import * as os from "os" import * as path from "path" import * as util from "util" +import envPaths from "env-paths" +import xdgBasedir from "xdg-basedir" export const tmpdir = path.join(os.tmpdir(), "code-server") -const getXdgDataDir = (): string => { - switch (process.platform) { - case "win32": - return path.join(process.env.XDG_DATA_HOME || path.join(os.homedir(), "AppData/Local"), "code-server/Data") - case "darwin": - return path.join( - process.env.XDG_DATA_HOME || path.join(os.homedir(), "Library/Application Support"), - "code-server", - ) - default: - return path.join(process.env.XDG_DATA_HOME || path.join(os.homedir(), ".local/share"), "code-server") - } +interface Paths { + data: string + config: string } -export const xdgLocalDir = getXdgDataDir() +export const paths = getEnvPaths() + +// getEnvPaths gets the config and data paths for the current platform/configuration. +// +// On MacOS this function gets the standard XDG directories instead of using the native macOS +// ones. Most CLIs do this as in practice only GUI apps use the standard macOS directories. +function getEnvPaths(): Paths { + let paths: Paths + if (process.platform === "win32") { + paths = envPaths("code-server", { + suffix: "", + }) + } else { + if (xdgBasedir.data === undefined) { + throw new Error("Missing data directory?") + } + if (xdgBasedir.config === undefined) { + throw new Error("Missing config directory?") + } + paths = { + data: path.join(xdgBasedir.data, "code-server"), + config: path.join(xdgBasedir.config, "code-server"), + } + } + + return paths +} + +// uxPath replaces the home directory in p with ~. +export function uxPath(p: string): string { + return p.replace(os.homedir(), "~") +} export const generateCertificate = async (): Promise<{ cert: string; certKey: string }> => { const paths = { diff --git a/test/cli.test.ts b/test/cli.test.ts index 4f1c2bfb0..52c9fb61f 100644 --- a/test/cli.test.ts +++ b/test/cli.test.ts @@ -2,7 +2,7 @@ import { logger, Level } from "@coder/logger" import * as assert from "assert" import * as path from "path" import { parse } from "../src/node/cli" -import { xdgLocalDir } from "../src/node/util" +import { paths } from "../src/node/util" describe("cli", () => { beforeEach(() => { @@ -12,8 +12,8 @@ describe("cli", () => { // The parser will always fill these out. const defaults = { _: [], - "extensions-dir": path.join(xdgLocalDir, "extensions"), - "user-data-dir": xdgLocalDir, + "extensions-dir": path.join(paths.data, "extensions"), + "user-data-dir": paths.data, } it("should set defaults", () => { diff --git a/yarn.lock b/yarn.lock index 470f299fd..782882fbd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2562,6 +2562,11 @@ entities@^2.0.0: resolved "https://registry.yarnpkg.com/entities/-/entities-2.0.0.tgz#68d6084cab1b079767540d80e56a39b423e4abf4" integrity sha512-D9f7V0JSRwIxlRI2mjMqufDrRDnx8p+eEOz7aUM9SuvF8gsBzra0/6tbjl1m8eQHrZlYj6PxqE00hZ1SAIKPLw== +env-paths@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/env-paths/-/env-paths-2.2.0.tgz#cdca557dc009152917d6166e2febe1f039685e43" + integrity sha512-6u0VYSCo/OW6IoD5WCLLy9JUGARbamfSavcNXry/eu8aHVFei6CD3Sw+VGX5alea1i9pgPHW0mbu6Xj0uBh7gA== + envinfo@^7.3.1: version "7.5.1" resolved "https://registry.yarnpkg.com/envinfo/-/envinfo-7.5.1.tgz#93c26897225a00457c75e734d354ea9106a72236" From 4f67f4e096ecda9d39da35616b677c7c8fd5eb2d Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Sun, 10 May 2020 02:23:41 -0400 Subject: [PATCH 03/11] Disable automatic updates --- src/node/cli.ts | 2 -- src/node/entry.ts | 4 ++-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/node/cli.ts b/src/node/cli.ts index 53561d258..34eaf2e52 100644 --- a/src/node/cli.ts +++ b/src/node/cli.ts @@ -25,7 +25,6 @@ export interface Args extends VsArgs { readonly auth?: AuthType readonly cert?: OptionalString readonly "cert-key"?: string - readonly "disable-updates"?: boolean readonly "disable-telemetry"?: boolean readonly help?: boolean readonly host?: string @@ -90,7 +89,6 @@ const options: Options> = { description: "Path to certificate. Generated if no path is provided.", }, "cert-key": { type: "string", path: true, description: "Path to certificate key when using non-generated cert." }, - "disable-updates": { type: "boolean", description: "Disable automatic updates." }, "disable-telemetry": { type: "boolean", description: "Disable telemetry." }, help: { type: "boolean", short: "h", description: "Show this output." }, json: { type: "boolean" }, diff --git a/src/node/entry.ts b/src/node/entry.ts index f048ec9a8..fce92dce2 100644 --- a/src/node/entry.ts +++ b/src/node/entry.ts @@ -74,7 +74,7 @@ const main = async (args: Args): Promise => { const httpServer = new HttpServer(options) const vscode = httpServer.registerHttpProvider("/", VscodeHttpProvider, args) const api = httpServer.registerHttpProvider("/api", ApiHttpProvider, httpServer, vscode, args["user-data-dir"]) - const update = httpServer.registerHttpProvider("/update", UpdateHttpProvider, !args["disable-updates"]) + const update = httpServer.registerHttpProvider("/update", UpdateHttpProvider, true) httpServer.registerHttpProvider("/proxy", ProxyHttpProvider) httpServer.registerHttpProvider("/login", LoginHttpProvider) httpServer.registerHttpProvider("/static", StaticHttpProvider) @@ -114,7 +114,7 @@ const main = async (args: Args): Promise => { httpServer.proxyDomains.forEach((domain) => logger.info(` - *.${domain}`)) } - logger.info(`Automatic updates are ${update.enabled ? "enabled" : "disabled"}`) + // logger.info(`Automatic updates are ${update.enabled ? "enabled" : "disabled"}`) if (serverAddress && !options.socket && args.open) { // The web socket doesn't seem to work if browsing with 0.0.0.0. From e02d94ad2f668c3a13efda64f6457b2be1cf6237 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Sun, 10 May 2020 02:35:15 -0400 Subject: [PATCH 04/11] Allow password authentication in the config file --- src/node/cli.ts | 7 ++++++- src/node/entry.ts | 11 ++++++----- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/node/cli.ts b/src/node/cli.ts index 34eaf2e52..f2b6de1fa 100644 --- a/src/node/cli.ts +++ b/src/node/cli.ts @@ -23,6 +23,7 @@ export class OptionalString extends Optional {} export interface Args extends VsArgs { readonly config?: string readonly auth?: AuthType + readonly password?: string readonly cert?: OptionalString readonly "cert-key"?: string readonly "disable-telemetry"?: boolean @@ -83,6 +84,7 @@ type Options = { const options: Options> = { auth: { type: AuthType, description: "The type of authentication to use." }, + password: { type: "string", description: "The password for password authentication." }, cert: { type: OptionalString, path: true, @@ -96,7 +98,10 @@ const options: Options> = { "bind-addr": { type: "string", description: "Address to bind to in host:port." }, - config: { type: "string", description: "Path to yaml config file." }, + config: { + type: "string", + description: "Path to yaml config file. Every flag maps directory to a key in the config file.", + }, // These two have been deprecated by bindAddr. host: { type: "string", description: "" }, diff --git a/src/node/entry.ts b/src/node/entry.ts index fce92dce2..8ff9463a8 100644 --- a/src/node/entry.ts +++ b/src/node/entry.ts @@ -40,7 +40,8 @@ const main = async (args: Args): Promise => { } const auth = args.auth || AuthType.Password - const originalPassword = auth === AuthType.Password && (process.env.PASSWORD || (await generatePassword())) + const generatedPassword = (args.password || process.env.PASSWORD) !== "" + const password = auth === AuthType.Password && (args.password || process.env.PASSWORD || (await generatePassword())) let host = args.host let port = args.port @@ -55,7 +56,7 @@ const main = async (args: Args): Promise => { auth, commit, host: host || (args.auth === AuthType.Password && args.cert !== undefined ? "0.0.0.0" : "localhost"), - password: originalPassword ? hash(originalPassword) : undefined, + password: password ? hash(password) : undefined, port: port !== undefined ? port : process.env.PORT ? parseInt(process.env.PORT, 10) : 8080, proxyDomains: args["proxy-domain"], socket: args.socket, @@ -86,9 +87,9 @@ const main = async (args: Args): Promise => { const serverAddress = await httpServer.listen() logger.info(`HTTP server listening on ${serverAddress}`) - if (auth === AuthType.Password && !process.env.PASSWORD) { - logger.info(` - Password is ${originalPassword}`) - logger.info(" - To use your own password set the PASSWORD environment variable") + if (auth === AuthType.Password && generatedPassword) { + logger.info(` - Password is ${password}`) + logger.info(" - To use your own password set it in the config file with the password key or use $PASSWORD") if (!args.auth) { logger.info(" - To disable use `--auth none`") } From d288131a33330c8f2720bed4c1e15ceb43292a9d Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Sun, 10 May 2020 02:42:34 -0400 Subject: [PATCH 05/11] Fix lint errors --- .eslintrc.yaml | 6 ++++++ src/node/entry.ts | 2 ++ 2 files changed, 8 insertions(+) diff --git a/.eslintrc.yaml b/.eslintrc.yaml index 06b284bde..e68c237e7 100644 --- a/.eslintrc.yaml +++ b/.eslintrc.yaml @@ -22,3 +22,9 @@ rules: # For overloads. no-dupe-class-members: off "@typescript-eslint/no-use-before-define": off + +settings: + # Does not work with CommonJS unfortunately. + import/ignore: + - env-paths + - xdg-basedir diff --git a/src/node/entry.ts b/src/node/entry.ts index 8ff9463a8..857104628 100644 --- a/src/node/entry.ts +++ b/src/node/entry.ts @@ -35,7 +35,9 @@ const main = async (args: Args): Promise => { args = await readConfigFile(args) if (args.verbose === true) { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion logger.info(`Using extensions-dir at ${uxPath(args["extensions-dir"]!)}`) + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion logger.info(`Using user-data-dir at ${uxPath(args["user-data-dir"]!)}`) } From 28edf4af2ede33bfbd9222028c27ba82cc0a9396 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Sun, 10 May 2020 02:58:34 -0400 Subject: [PATCH 06/11] Add systemd user service to .deb and .rpm --- ci/README.md | 2 ++ ci/build/code-server.service | 11 +++++++++++ ci/build/nfpm.yaml | 1 + 3 files changed, 14 insertions(+) create mode 100644 ci/build/code-server.service diff --git a/ci/README.md b/ci/README.md index a20c00bdc..b09545246 100644 --- a/ci/README.md +++ b/ci/README.md @@ -78,6 +78,8 @@ You can disable minification by setting `MINIFY=`. - Used to configure [nfpm](https://github.com/goreleaser/nfpm) to generate .deb and .rpm - [./build/code-server-nfpm.sh](./build/code-server-nfpm.sh) - Entrypoint script for code-server for .deb and .rpm +- [./build/code-server.service](./build/code-server.service) + - systemd user service packaged into the debs and rpms - [./build/release-github-draft.sh](./build/release-github-draft.sh) (`yarn release:github-draft`) - Uses [hub](https://github.com/github/hub) to create a draft release with a template description - [./build/release-github-assets.sh](./build/release-github-assets.sh) (`yarn release:github-assets`) diff --git a/ci/build/code-server.service b/ci/build/code-server.service new file mode 100644 index 000000000..414f54515 --- /dev/null +++ b/ci/build/code-server.service @@ -0,0 +1,11 @@ +[Unit] +Description=code-server +After=network.target + +[Service] +Type=exec +ExecStart=/usr/bin/code-server +Restart=always + +[Install] +WantedBy=multi-user.target diff --git a/ci/build/nfpm.yaml b/ci/build/nfpm.yaml index efea4ece0..c290eb18b 100644 --- a/ci/build/nfpm.yaml +++ b/ci/build/nfpm.yaml @@ -13,4 +13,5 @@ license: "MIT" bindir: "/usr/bin" files: ./ci/build/code-server-nfpm.sh: /usr/bin/code-server + ./ci/build/code-server.service: /usr/lib/systemd/user/code-server.service ./release-static/**/*: "/usr/lib/code-server/" From d6ea9d78f6e5ae21a7c2fdc0c8f2b1998d01f2c8 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Sun, 10 May 2020 04:15:29 -0400 Subject: [PATCH 07/11] Configuration file bug fixes based on @code-asher's review --- doc/FAQ.md | 6 +- src/node/cli.ts | 148 +++++++++++++++++++++++++++++++++------------- src/node/entry.ts | 61 +++++++++---------- src/node/util.ts | 28 +++++---- 4 files changed, 155 insertions(+), 88 deletions(-) diff --git a/doc/FAQ.md b/doc/FAQ.md index cc6197302..a43575608 100644 --- a/doc/FAQ.md +++ b/doc/FAQ.md @@ -168,8 +168,10 @@ code-server crashes can be helpful. ### Where is the data directory? If the `XDG_DATA_HOME` environment variable is set the data directory will be -`$XDG_DATA_HOME/code-server`. Otherwise the default is `~/.local/share/code-server`. -On Windows, it will be `%APPDATA%\Local\code-server\Data`. +`$XDG_DATA_HOME/code-server`. Otherwise: + +1. Unix: `~/.local/share/code-server` +1. Windows: `%APPDATA%\Local\code-server\Data` ## Enterprise diff --git a/src/node/cli.ts b/src/node/cli.ts index f2b6de1fa..6e6fb5de0 100644 --- a/src/node/cli.ts +++ b/src/node/cli.ts @@ -1,10 +1,10 @@ +import { field, Level, logger } from "@coder/logger" import * as fs from "fs-extra" import yaml from "js-yaml" import * as path from "path" -import { field, logger, Level } from "@coder/logger" import { Args as VsArgs } from "../../lib/vscode/src/vs/server/ipc" import { AuthType } from "./http" -import { paths, uxPath } from "./util" +import { generatePassword, humanPath, paths } from "./util" export class Optional { public constructor(public readonly value?: T) {} @@ -84,7 +84,10 @@ type Options = { const options: Options> = { auth: { type: AuthType, description: "The type of authentication to use." }, - password: { type: "string", description: "The password for password authentication." }, + password: { + type: "string", + description: "The password for password authentication (can only be passed in via $PASSWORD or the config file).", + }, cert: { type: OptionalString, path: true, @@ -96,11 +99,14 @@ const options: Options> = { json: { type: "boolean" }, open: { type: "boolean", description: "Open in browser on startup. Does not work remotely." }, - "bind-addr": { type: "string", description: "Address to bind to in host:port." }, + "bind-addr": { + type: "string", + description: "Address to bind to in host:port. You can also use $PORT to override the port.", + }, config: { type: "string", - description: "Path to yaml config file. Every flag maps directory to a key in the config file.", + description: "Path to yaml config file. Every flag maps directly to a key in the config file.", }, // These two have been deprecated by bindAddr. @@ -145,7 +151,19 @@ export const optionDescriptions = (): string[] => { ) } -export const parse = (argv: string[]): Args => { +export const parse = ( + argv: string[], + opts?: { + configFile: string + }, +): Args => { + const error = (msg: string): Error => { + if (opts?.configFile) { + msg = `error reading ${opts.configFile}: ${msg}` + } + return new Error(msg) + } + const args: Args = { _: [] } let ended = false @@ -175,7 +193,11 @@ export const parse = (argv: string[]): Args => { } if (!key || !options[key]) { - throw new Error(`Unknown option ${arg}`) + throw error(`Unknown option ${arg}`) + } + + if (key === "password" && !opts?.configFile) { + throw new Error("--password can only be set in the config file or passed in via $PASSWORD") } const option = options[key] @@ -194,7 +216,11 @@ export const parse = (argv: string[]): Args => { ;(args[key] as OptionalString) = new OptionalString(value) continue } else if (!value) { - throw new Error(`--${key} requires a value`) + throw error(`--${key} requires a value`) + } + + if (option.type == OptionalString && value == "false") { + continue } if (option.path) { @@ -214,7 +240,7 @@ export const parse = (argv: string[]): Args => { case "number": ;(args[key] as number) = parseInt(value, 10) if (isNaN(args[key] as number)) { - throw new Error(`--${key} must be a number`) + throw error(`--${key} must be a number`) } break case OptionalString: @@ -222,7 +248,7 @@ export const parse = (argv: string[]): Args => { break default: { if (!Object.values(option.type).includes(value)) { - throw new Error(`--${key} valid values: [${Object.values(option.type).join(", ")}]`) + throw error(`--${key} valid values: [${Object.values(option.type).join(", ")}]`) } ;(args[key] as string) = value break @@ -284,53 +310,93 @@ export const parse = (argv: string[]): Args => { return args } -const defaultConfigFile = ` +async function defaultConfigFile(): Promise { + return `bind-addr: 127.0.0.1:8080 auth: password -bind-addr: 127.0.0.1:8080 -`.trimLeft() +password: ${await generatePassword()} +cert: false +` +} -// readConfigFile reads the config file specified in the config flag -// and loads it's configuration. -// -// Flags set on the CLI take priority. -// -// The config file can also be passed via $CODE_SERVER_CONFIG and defaults -// to ~/.config/code-server/config.yaml. -export async function readConfigFile(args: Args): Promise { - const configPath = getConfigPath(args) - - if (!(await fs.pathExists(configPath))) { - await fs.outputFile(configPath, defaultConfigFile) - logger.info(`Wrote default config file to ${uxPath(configPath)}`) +/** + * Reads the code-server yaml config file and returns it as Args. + * + * @param configPath Read the config from configPath instead of $CODE_SERVER_CONFIG or the default. + */ +export async function readConfigFile(configPath?: string): Promise { + if (!configPath) { + configPath = process.env.CODE_SERVER_CONFIG + if (!configPath) { + configPath = path.join(paths.config, "config.yaml") + } } - logger.info(`Using config file from ${uxPath(configPath)}`) + if (!(await fs.pathExists(configPath))) { + await fs.outputFile(configPath, await defaultConfigFile()) + logger.info(`Wrote default config file to ${humanPath(configPath)}`) + } + + logger.info(`Using config file from ${humanPath(configPath)}`) const configFile = await fs.readFile(configPath) const config = yaml.safeLoad(configFile.toString(), { - filename: args.config, + filename: configPath, }) // We convert the config file into a set of flags. // This is a temporary measure until we add a proper CLI library. const configFileArgv = Object.entries(config).map(([optName, opt]) => { - if (opt === null) { + if (opt === true) { return `--${optName}` } return `--${optName}=${opt}` }) - const configFileArgs = parse(configFileArgv) - - // This prioritizes the flags set in args over the ones in the config file. - return Object.assign(configFileArgs, args) + const args = parse(configFileArgv, { + configFile: configPath, + }) + return { + ...args, + config: configPath, + } } -function getConfigPath(args: Args): string { - if (args.config !== undefined) { - return args.config - } - if (process.env.CODE_SERVER_CONFIG !== undefined) { - return process.env.CODE_SERVER_CONFIG - } - return path.join(paths.config, "config.yaml") +function parseBindAddr(bindAddr: string): [string, number] { + const u = new URL(`http://${bindAddr}`) + return [u.hostname, parseInt(u.port, 10)] +} + +interface Addr { + host: string + port: number +} + +function bindAddrFromArgs(addr: Addr, args: Args): Addr { + addr = { ...addr } + if (args["bind-addr"]) { + ;[addr.host, addr.port] = parseBindAddr(args["bind-addr"]) + } + if (args.host) { + addr.host = args.host + } + if (args.port !== undefined) { + addr.port = args.port + } + return addr +} + +export function bindAddrFromAllSources(cliArgs: Args, configArgs: Args): [string, number] { + let addr: Addr = { + host: "localhost", + port: 8080, + } + + addr = bindAddrFromArgs(addr, configArgs) + + if (process.env.PORT) { + addr.port = parseInt(process.env.PORT, 10) + } + + addr = bindAddrFromArgs(addr, cliArgs) + + return [addr.host, addr.port] } diff --git a/src/node/entry.ts b/src/node/entry.ts index 857104628..b5fea7b42 100644 --- a/src/node/entry.ts +++ b/src/node/entry.ts @@ -9,9 +9,9 @@ import { ProxyHttpProvider } from "./app/proxy" import { StaticHttpProvider } from "./app/static" import { UpdateHttpProvider } from "./app/update" import { VscodeHttpProvider } from "./app/vscode" -import { Args, optionDescriptions, parse, readConfigFile } from "./cli" +import { Args, bindAddrFromAllSources, optionDescriptions, parse, readConfigFile } from "./cli" import { AuthType, HttpServer, HttpServerOptions } from "./http" -import { generateCertificate, generatePassword, hash, open, uxPath } from "./util" +import { generateCertificate, hash, open, humanPath } from "./util" import { ipcMain, wrap } from "./wrapper" process.on("uncaughtException", (error) => { @@ -31,35 +31,24 @@ try { const version = pkg.version || "development" const commit = pkg.commit || "development" -const main = async (args: Args): Promise => { - args = await readConfigFile(args) +const main = async (cliArgs: Args): Promise => { + const configArgs = await readConfigFile(cliArgs.config) + // This prioritizes the flags set in args over the ones in the config file. + let args = Object.assign(configArgs, cliArgs) - if (args.verbose === true) { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - logger.info(`Using extensions-dir at ${uxPath(args["extensions-dir"]!)}`) - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - logger.info(`Using user-data-dir at ${uxPath(args["user-data-dir"]!)}`) - } + logger.trace(`Using extensions-dir at ${humanPath(args["extensions-dir"])}`) + logger.trace(`Using user-data-dir at ${humanPath(args["user-data-dir"])}`) - const auth = args.auth || AuthType.Password - const generatedPassword = (args.password || process.env.PASSWORD) !== "" - const password = auth === AuthType.Password && (args.password || process.env.PASSWORD || (await generatePassword())) - - let host = args.host - let port = args.port - if (args["bind-addr"] !== undefined) { - const u = new URL(`http://${args["bind-addr"]}`) - host = u.hostname - port = parseInt(u.port, 10) - } + const password = args.auth === AuthType.Password && (process.env.PASSWORD || args.password) + const [host, port] = bindAddrFromAllSources(cliArgs, configArgs) // Spawn the main HTTP server. const options: HttpServerOptions = { - auth, + auth: args.auth, commit, - host: host || (args.auth === AuthType.Password && args.cert !== undefined ? "0.0.0.0" : "localhost"), + host: host, password: password ? hash(password) : undefined, - port: port !== undefined ? port : process.env.PORT ? parseInt(process.env.PORT, 10) : 8080, + port: port, proxyDomains: args["proxy-domain"], socket: args.socket, ...(args.cert && !args.cert.value @@ -77,7 +66,7 @@ const main = async (args: Args): Promise => { const httpServer = new HttpServer(options) const vscode = httpServer.registerHttpProvider("/", VscodeHttpProvider, args) const api = httpServer.registerHttpProvider("/api", ApiHttpProvider, httpServer, vscode, args["user-data-dir"]) - const update = httpServer.registerHttpProvider("/update", UpdateHttpProvider, true) + const update = httpServer.registerHttpProvider("/update", UpdateHttpProvider, false) httpServer.registerHttpProvider("/proxy", ProxyHttpProvider) httpServer.registerHttpProvider("/login", LoginHttpProvider) httpServer.registerHttpProvider("/static", StaticHttpProvider) @@ -89,14 +78,20 @@ const main = async (args: Args): Promise => { const serverAddress = await httpServer.listen() logger.info(`HTTP server listening on ${serverAddress}`) - if (auth === AuthType.Password && generatedPassword) { - logger.info(` - Password is ${password}`) - logger.info(" - To use your own password set it in the config file with the password key or use $PASSWORD") - if (!args.auth) { - logger.info(" - To disable use `--auth none`") + if (!args.auth) { + args = { + ...args, + auth: AuthType.Password, } - } else if (auth === AuthType.Password) { - logger.info(" - Using custom password for authentication") + } + + if (args.auth === AuthType.Password) { + if (process.env.PASSWORD) { + logger.info(" - Using password from $PASSWORD") + } else { + logger.info(` - Using password from ${humanPath(args.config)}`) + } + logger.info(" - To disable use `--auth none`") } else { logger.info(" - No authentication") } @@ -117,8 +112,6 @@ const main = async (args: Args): Promise => { httpServer.proxyDomains.forEach((domain) => logger.info(` - *.${domain}`)) } - // logger.info(`Automatic updates are ${update.enabled ? "enabled" : "disabled"}`) - if (serverAddress && !options.socket && args.open) { // The web socket doesn't seem to work if browsing with 0.0.0.0. const openAddress = serverAddress.replace(/:\/\/0.0.0.0/, "://localhost") diff --git a/src/node/util.ts b/src/node/util.ts index 72d3332fe..33e6de12f 100644 --- a/src/node/util.ts +++ b/src/node/util.ts @@ -16,10 +16,11 @@ interface Paths { export const paths = getEnvPaths() -// getEnvPaths gets the config and data paths for the current platform/configuration. -// -// On MacOS this function gets the standard XDG directories instead of using the native macOS -// ones. Most CLIs do this as in practice only GUI apps use the standard macOS directories. +/** + * Gets the config and data paths for the current platform/configuration. + * On MacOS this function gets the standard XDG directories instead of using the native macOS + * ones. Most CLIs do this as in practice only GUI apps use the standard macOS directories. + */ function getEnvPaths(): Paths { let paths: Paths if (process.platform === "win32") { @@ -27,11 +28,8 @@ function getEnvPaths(): Paths { suffix: "", }) } else { - if (xdgBasedir.data === undefined) { - throw new Error("Missing data directory?") - } - if (xdgBasedir.config === undefined) { - throw new Error("Missing config directory?") + if (xdgBasedir.data === undefined || xdgBasedir.config === undefined) { + throw new Error("No home folder?") } paths = { data: path.join(xdgBasedir.data, "code-server"), @@ -42,8 +40,16 @@ function getEnvPaths(): Paths { return paths } -// uxPath replaces the home directory in p with ~. -export function uxPath(p: string): string { +/** + * humanPath replaces the home directory in p with ~. + * Makes it more readable. + * + * @param p + */ +export function humanPath(p?: string): string { + if (!p) { + return "" + } return p.replace(os.homedir(), "~") } From 1e432b25eac8c1a1c49ece51ae0b3639078ed6e2 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Mon, 11 May 2020 21:23:50 -0400 Subject: [PATCH 08/11] Comment on hash(password) --- src/node/entry.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/node/entry.ts b/src/node/entry.ts index b5fea7b42..b46b1a914 100644 --- a/src/node/entry.ts +++ b/src/node/entry.ts @@ -47,6 +47,7 @@ const main = async (cliArgs: Args): Promise => { auth: args.auth, commit, host: host, + // The hash does not add any actual security but we do it for obfuscation purposes. password: password ? hash(password) : undefined, port: port, proxyDomains: args["proxy-domain"], From 524b0205e99b302cc5e334774433be576e26074d Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Tue, 12 May 2020 02:59:03 -0400 Subject: [PATCH 09/11] Workaround for GH Actions ruining file permissions --- ci/steps/publish-npm.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ci/steps/publish-npm.sh b/ci/steps/publish-npm.sh index cbad679d5..10865a613 100755 --- a/ci/steps/publish-npm.sh +++ b/ci/steps/publish-npm.sh @@ -10,6 +10,8 @@ main() { fi download_artifact npm-package ./release + # https://github.com/actions/upload-artifact/issues/38 + chmod +x $(grep -rl '^#!/.\+' release) yarn publish --non-interactive release } From c0d6eb4664107ad58b690dff71f248461f9511ad Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Tue, 12 May 2020 19:19:37 -0400 Subject: [PATCH 10/11] Improve password handling - Error out if auth is enabled but no password is passed in - Indicate password location on login page --- .eslintrc.yaml | 1 + package.json | 6 +++--- src/browser/pages/login.html | 2 +- src/node/app/login.ts | 17 +++++++++++++++-- src/node/entry.ts | 22 +++++++++++++--------- 5 files changed, 33 insertions(+), 15 deletions(-) diff --git a/.eslintrc.yaml b/.eslintrc.yaml index e68c237e7..a06f1d766 100644 --- a/.eslintrc.yaml +++ b/.eslintrc.yaml @@ -22,6 +22,7 @@ rules: # For overloads. no-dupe-class-members: off "@typescript-eslint/no-use-before-define": off + "@typescript-eslint/no-non-null-assertion": off settings: # Does not work with CommonJS unfortunately. diff --git a/package.json b/package.json index c86e0174f..87696289b 100644 --- a/package.json +++ b/package.json @@ -56,8 +56,7 @@ "stylelint": "^13.0.0", "stylelint-config-recommended": "^3.0.0", "ts-node": "^8.4.1", - "typescript": "3.7.2", - "yarn": "^1.22.4" + "typescript": "3.7.2" }, "resolutions": { "@types/node": "^12.12.7", @@ -79,7 +78,8 @@ "tar": "^6.0.1", "tar-fs": "^2.0.0", "ws": "^7.2.0", - "xdg-basedir": "^4.0.0" + "xdg-basedir": "^4.0.0", + "yarn": "^1.22.4" }, "bin": { "code-server": "out/node/entry.js" diff --git a/src/browser/pages/login.html b/src/browser/pages/login.html index 6f8ce250e..37d51f203 100644 --- a/src/browser/pages/login.html +++ b/src/browser/pages/login.html @@ -26,7 +26,7 @@

Welcome to code-server

-
Please log in below. Check code-server's logs for the generated password.
+
Please log in below. {{PASSWORD_MSG}}