import { field, logger } from "@coder/logger"; import { ServerMessage, SharedProcessActive } from "@coder/protocol/src/proto"; import { withEnv } from "@coder/protocol"; import { ChildProcess, fork, ForkOptions } from "child_process"; import { randomFillSync } from "crypto"; import * as fs from "fs"; import * as fse from "fs-extra"; import * as os from "os"; import * as path from "path"; import * as WebSocket from "ws"; import { buildDir, cacheHome, dataHome, isCli, serveStatic } from "./constants"; import { createApp } from "./server"; import { forkModule, requireModule } from "./vscode/bootstrapFork"; import { SharedProcess, SharedProcessState } from "./vscode/sharedProcess"; import opn = require("opn"); import * as commander from "commander"; const collect = (value: T, previous: T[]): T[] => { return previous.concat(value); }; commander.version(process.env.VERSION || "development") .name("code-server") .description("Run VS Code on a remote server.") .option("--cert ") .option("--cert-key ") .option("-e, --extensions-dir ", "Override the main default path for user extensions.") .option("--extra-extensions-dir [dir]", "Path to an extra user extension directory (repeatable).", collect, []) .option("--extra-builtin-extensions-dir [dir]", "Path to an extra built-in extension directory (repeatable).", collect, []) .option("-d, --user-data-dir ", "Specifies the directory that user data is kept in, useful when running as root.") .option("--data-dir ", "DEPRECATED: Use '--user-data-dir' instead. Customize where user-data is stored.") .option("-h, --host ", "Customize the hostname.", "0.0.0.0") .option("-o, --open", "Open in the browser on startup.", false) .option("-p, --port ", "Port to bind on.", parseInt(process.env.PORT!, 10) || 8443) .option("-N, --no-auth", "Start without requiring authentication.", false) .option("-H, --allow-http", "Allow http connections.", false) .option("-P, --password ", "DEPRECATED: Use the PASSWORD environment variable instead. Specify a password for authentication.") .option("--disable-telemetry", "Disables ALL telemetry.", false) .option("--socket ", "Listen on a UNIX socket. Host and port will be ignored when set.") .option("--trust-proxy", "Trust the X-Forwarded-For header, useful when using a reverse proxy.", false) .option("--install-extension ", "Install an extension by its ID.") .option("--bootstrap-fork ", "Used for development. Never set.") .option("--extra-args ", "Used for development. Never set.") .arguments("Specify working directory.") .parse(process.argv); Error.stackTraceLimit = Infinity; if (isCli) { require("nbin").shimNativeFs(buildDir); require("nbin").shimNativeFs("/node_modules"); } // Makes strings or numbers bold in stdout const bold = (text: string | number): string | number => { return `\u001B[1m${text}\u001B[0m`; }; (async (): Promise => { const args = commander.args; const options = commander.opts() as { noAuth: boolean; readonly allowHttp: boolean; readonly host: string; readonly port: number; readonly disableTelemetry: boolean; readonly userDataDir?: string; readonly extensionsDir?: string; readonly extraExtensionsDir?: string[]; readonly extraBuiltinExtensionsDir?: string[]; readonly dataDir?: string; readonly password?: string; readonly open?: boolean; readonly cert?: string; readonly certKey?: string; readonly socket?: string; readonly trustProxy?: boolean; readonly installExtension?: string; readonly bootstrapFork?: string; readonly extraArgs?: string; }; if (options.disableTelemetry) { process.env.DISABLE_TELEMETRY = "true"; } // Commander has an exception for `--no` prefixes. Here we'll adjust that. // tslint:disable-next-line:no-any const noAuthValue = (commander as any).auth; options.noAuth = !noAuthValue; const dataDir = path.resolve(options.userDataDir || options.dataDir || path.join(dataHome, "code-server")); const extensionsDir = options.extensionsDir ? path.resolve(options.extensionsDir) : path.resolve(dataDir, "extensions"); const builtInExtensionsDir = path.resolve(buildDir || path.join(__dirname, ".."), "build/extensions"); const extraExtensionDirs = options.extraExtensionsDir ? options.extraExtensionsDir.map((p) => path.resolve(p)) : []; const extraBuiltinExtensionDirs = options.extraBuiltinExtensionsDir ? options.extraBuiltinExtensionsDir.map((p) => path.resolve(p)) : []; const workingDir = path.resolve(args[0] || process.cwd()); const dependenciesDir = path.join(os.tmpdir(), "code-server/dependencies"); if (!fs.existsSync(dataDir)) { const oldDataDir = path.resolve(path.join(os.homedir(), ".code-server")); if (fs.existsSync(oldDataDir)) { await fse.move(oldDataDir, dataDir); logger.info(`Moved data directory from ${oldDataDir} to ${dataDir}`); } } await Promise.all([ fse.mkdirp(cacheHome), fse.mkdirp(dataDir), fse.mkdirp(extensionsDir), fse.mkdirp(workingDir), fse.mkdirp(dependenciesDir), ...extraExtensionDirs.map((p) => fse.mkdirp(p)), ...extraBuiltinExtensionDirs.map((p) => fse.mkdirp(p)), ]); const unpackExecutable = (binaryName: string): void => { const memFile = path.join(isCli ? buildDir! : path.join(__dirname, ".."), "build/dependencies", binaryName); const diskFile = path.join(dependenciesDir, binaryName); if (!fse.existsSync(diskFile)) { fse.writeFileSync(diskFile, fse.readFileSync(memFile)); } fse.chmodSync(diskFile, "755"); }; unpackExecutable("rg"); // tslint:disable-next-line no-any (global).RIPGREP_LOCATION = path.join(dependenciesDir, "rg"); if (options.bootstrapFork) { const modulePath = options.bootstrapFork; if (!modulePath) { logger.error("No module path specified to fork!"); process.exit(1); } process.argv = [ process.argv[0], process.argv[1], ...(options.extraArgs ? JSON.parse(options.extraArgs) : []), ]; return requireModule(modulePath, builtInExtensionsDir); } const logDir = path.join(cacheHome, "code-server/logs", new Date().toISOString().replace(/[-:.TZ]/g, "")); process.env.VSCODE_LOGS = logDir; const certPath = options.cert ? path.resolve(options.cert) : undefined; const certKeyPath = options.certKey ? path.resolve(options.certKey) : undefined; if (certPath && !certKeyPath) { logger.error("'--cert-key' flag is required when specifying a certificate!"); process.exit(1); } if (!certPath && certKeyPath) { logger.error("'--cert' flag is required when specifying certificate key!"); process.exit(1); } let certData: Buffer | undefined; let certKeyData: Buffer | undefined; if (typeof certPath !== "undefined" && typeof certKeyPath !== "undefined") { try { certData = fs.readFileSync(certPath); } catch (ex) { logger.error(`Failed to read certificate: ${ex.message}`); process.exit(1); } try { certKeyData = fs.readFileSync(certKeyPath); } catch (ex) { logger.error(`Failed to read certificate key: ${ex.message}`); process.exit(1); } } logger.info(`\u001B[1mcode-server ${process.env.VERSION ? `v${process.env.VERSION}` : "development"}`); if (options.dataDir) { logger.warn('"--data-dir" is deprecated. Use "--user-data-dir" instead.'); } if (options.installExtension) { const fork = forkModule("vs/code/node/cli", [ "--user-data-dir", dataDir, "--builtin-extensions-dir", builtInExtensionsDir, "--extensions-dir", extensionsDir, "--install-extension", options.installExtension, ], withEnv({ env: { VSCODE_ALLOW_IO: "true" } }), dataDir); fork.stdout.on("data", (d: Buffer) => d.toString().split("\n").forEach((l) => logger.info(l))); fork.stderr.on("data", (d: Buffer) => d.toString().split("\n").forEach((l) => logger.error(l))); fork.on("exit", () => process.exit()); return; } // TODO: fill in appropriate doc url logger.info("Additional documentation: http://github.com/cdr/code-server"); logger.info("Initializing", field("data-dir", dataDir), field("extensions-dir", extensionsDir), field("working-dir", workingDir), field("log-dir", logDir)); const sharedProcess = new SharedProcess(dataDir, extensionsDir, builtInExtensionsDir, extraExtensionDirs, extraBuiltinExtensionDirs); const sendSharedProcessReady = (socket: WebSocket): void => { const active = new SharedProcessActive(); active.setSocketPath(sharedProcess.socketPath); active.setLogPath(logDir); const serverMessage = new ServerMessage(); serverMessage.setSharedProcessActive(active); socket.send(serverMessage.serializeBinary()); }; sharedProcess.onState((event) => { if (event.state === SharedProcessState.Ready) { app.wss.clients.forEach((c) => sendSharedProcessReady(c)); } }); if (options.password) { logger.warn('"--password" is deprecated. Use the PASSWORD environment variable instead.'); } let password = options.password || process.env.PASSWORD; const usingCustomPassword = !!password; if (!password) { // Generate a random password with a length of 24. const buffer = Buffer.alloc(12); randomFillSync(buffer); password = buffer.toString("hex"); } const hasCustomHttps = certData && certKeyData; const app = await createApp({ allowHttp: options.allowHttp, bypassAuth: options.noAuth, registerMiddleware: (app): void => { // If we're not running from the binary and we aren't serving the static // pre-built version, use webpack to serve the web files. if (!isCli && !serveStatic) { const webpackConfig = require(path.resolve(__dirname, "..", "..", "web", "webpack.config.js")); const compiler = require("webpack")(webpackConfig); app.use(require("webpack-dev-middleware")(compiler, { logger: { trace: (m: string): void => logger.trace("webpack", field("message", m)), debug: (m: string): void => logger.debug("webpack", field("message", m)), info: (m: string): void => logger.info("webpack", field("message", m)), warn: (m: string): void => logger.warn("webpack", field("message", m)), error: (m: string): void => logger.error("webpack", field("message", m)), }, publicPath: webpackConfig.output.publicPath, stats: webpackConfig.stats, })); app.use(require("webpack-hot-middleware")(compiler)); } }, serverOptions: { extensionsDirectory: extensionsDir, builtInExtensionsDirectory: builtInExtensionsDir, extraExtensionDirectories: extraExtensionDirs, extraBuiltinExtensionDirectories: extraBuiltinExtensionDirs, dataDirectory: dataDir, workingDirectory: workingDir, cacheDirectory: cacheHome, fork: (modulePath: string, args?: string[], options?: ForkOptions): ChildProcess => { if (options && options.env && options.env.AMD_ENTRYPOINT) { return forkModule(options.env.AMD_ENTRYPOINT, args, options, dataDir); } return fork(modulePath, args, options); }, }, password, trustProxy: options.trustProxy, httpsOptions: hasCustomHttps ? { key: certKeyData, cert: certData, } : undefined, }); if (options.socket) { logger.info("Starting webserver via socket...", field("socket", options.socket)); app.server.listen(options.socket, () => { logger.info(" "); logger.info("Started on socket address:"); logger.info(options.socket!); logger.info(" "); }); } else { logger.info("Starting webserver...", field("host", options.host), field("port", options.port)); app.server.listen(options.port, options.host, async () => { const protocol = options.allowHttp ? "http" : "https"; const address = app.server.address(); const port = typeof address === "string" ? options.port : address.port; const url = `${protocol}://localhost:${port}/`; logger.info(" "); logger.info("Started (click the link below to open):"); logger.info(url); logger.info(" "); if (options.open) { try { await opn(url); } catch (e) { logger.warn("Url couldn't be opened automatically.", field("url", url), field("error", e.message)); } } }); } let clientId = 1; app.wss.on("connection", (ws, req) => { const id = clientId++; if (sharedProcess.state === SharedProcessState.Ready) { sendSharedProcessReady(ws); } logger.info(`WebSocket opened \u001B[0m${req.url}`, field("client", id), field("ip", req.socket.remoteAddress)); ws.on("close", (code) => { logger.info(`WebSocket closed \u001B[0m${req.url}`, field("client", id), field("code", code)); }); }); app.wss.on("error", (err: NodeJS.ErrnoException) => { if (err.code === "EADDRINUSE") { if (options.socket) { logger.error(`Socket ${bold(options.socket)} is in use. Please specify a different socket.`); } else { logger.error(`Port ${bold(options.port)} is in use. Please free up port ${options.port} or specify a different port with the -p flag`); } process.exit(1); } }); if (!options.certKey && !options.cert) { logger.warn("No certificate specified. \u001B[1mThis could be insecure."); // TODO: fill in appropriate doc url logger.warn("Documentation on securing your setup: https://github.com/cdr/code-server/blob/master/doc/security/ssl.md"); } if (!options.noAuth) { logger.info(" "); logger.info(usingCustomPassword ? "Using custom password." : `Password:\u001B[1m ${password}`); } else { logger.warn(" "); logger.warn("Launched without authentication."); } if (options.disableTelemetry) { logger.info(" "); logger.info("Telemetry is disabled."); } })().catch((ex) => { logger.error(ex); });