From ae1a57fc88d1729a7866a6fe74dec3e401d14654 Mon Sep 17 00:00:00 2001 From: JK Date: Wed, 9 Dec 2020 03:28:37 +0000 Subject: [PATCH 01/11] Add "get-request" argument to cli --- src/node/cli.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/node/cli.ts b/src/node/cli.ts index b3017b666175..97b9edac9f3c 100644 --- a/src/node/cli.ts +++ b/src/node/cli.ts @@ -43,6 +43,7 @@ export interface Args extends VsArgs { port?: number "bind-addr"?: string socket?: string + "enable-get-requests"?: boolean version?: boolean force?: boolean "list-extensions"?: boolean @@ -147,6 +148,10 @@ const options: Options> = { port: { type: "number", description: "" }, socket: { type: "string", path: true, description: "Path to a socket (bind-addr will be ignored)." }, + "enable-get-requests": { + type: "boolean", + description: `Enable authentication via the url with queries. (Usage: ?pass=[password] after the rest of the url)` + }, version: { type: "boolean", short: "v", description: "Display version information." }, _: { type: "string[]" }, From 5069722deccf9491dfcbe290ecc31522c9426f0b Mon Sep 17 00:00:00 2001 From: JK Date: Wed, 9 Dec 2020 03:33:32 +0000 Subject: [PATCH 02/11] Add authentication to GET route --- src/node/routes/login.ts | 61 +++++++++++++++++++++++++++------------- 1 file changed, 41 insertions(+), 20 deletions(-) diff --git a/src/node/routes/login.ts b/src/node/routes/login.ts index 4db7fd825d13..4e21e456d30a 100644 --- a/src/node/routes/login.ts +++ b/src/node/routes/login.ts @@ -43,45 +43,35 @@ const getRoot = async (req: Request, error?: Error): Promise => { const limiter = new RateLimiter() -export const router = Router() - -router.use((req, res, next) => { - const to = (typeof req.query.to === "string" && req.query.to) || "/" - if (authenticated(req)) { - return redirect(req, res, to, { to: undefined }) - } - next() -}) - -router.get("/", async (req, res) => { - res.send(await getRoot(req)) -}) - -router.post("/", async (req, res) => { +const login = async (req : Request, res : any, password : string) => { try { if (!limiter.try()) { throw new Error("Login rate limited!") } - if (!req.body.password) { + if (!password) { throw new Error("Missing password") } if ( req.args.hashedPassword - ? safeCompare(hash(req.body.password), req.args.hashedPassword) - : req.args.password && safeCompare(req.body.password, req.args.password) + ? safeCompare(hash(password), req.args.hashedPassword) + : req.args.password && safeCompare(password, req.args.password) ) { // The hash does not add any actual security but we do it for // obfuscation purposes (and as a side effect it handles escaping). - res.cookie(Cookie.Key, hash(req.body.password), { + res.cookie(Cookie.Key, hash(password), { domain: getCookieDomain(req.headers.host || "", req.args["proxy-domain"]), path: req.body.base || "/", sameSite: "lax", }) const to = (typeof req.query.to === "string" && req.query.to) || "/" - return redirect(req, res, to, { to: undefined }) + return redirect(req, res, to, { + to: undefined, + password: undefined, + pass: undefined, + }) } console.error( @@ -98,4 +88,35 @@ router.post("/", async (req, res) => { } catch (error) { res.send(await getRoot(req, error)) } +} + +export const router = Router() + +router.use((req : any, res : any, next : any) => { + const to = (typeof req.query.to === "string" && req.query.to) || "/" + if (authenticated(req)) { + return redirect(req, res, to, { + to: undefined, + password: undefined, + pass: undefined, + }) + } + next() }) + +router.get("/", async (req : any, res : any) => { + if (req.args["enable-get-requests"]) { + // `?password` overrides `?pass` + if (req.query.password) { + return await login(req, res, req.query.password as string) + } + else if (req.query.pass) { + return await login(req, res, req.query.pass as string) + } + } + res.send(await getRoot(req)) +}) + +router.post("/", async (req : any, res : any) => { + await login(req, res, req.body.password) +}) \ No newline at end of file From c7571710ed55f777646aec9ade42d527851bd526 Mon Sep 17 00:00:00 2001 From: JK Date: Wed, 9 Dec 2020 04:02:53 +0000 Subject: [PATCH 03/11] Log if GET reqs are enabled --- src/node/entry.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/node/entry.ts b/src/node/entry.ts index ac615da68352..41e40c4746fd 100644 --- a/src/node/entry.ts +++ b/src/node/entry.ts @@ -125,6 +125,10 @@ const main = async (args: DefaultedArgs): Promise => { logger.info(` - Authentication is disabled ${args.link ? "(disabled by --link)" : ""}`) } + if (args["enable-get-requests"]) { + logger.info(` - Login via GET is enabled ${args.auth === AuthType.None ? "(however auth is disabled)" : ""}`) + } + if (args.cert) { logger.info(` - Using certificate for HTTPS: ${humanPath(args.cert.value)}`) } else { From c26b8bf0434efb0106c992aee6479d40881aac56 Mon Sep 17 00:00:00 2001 From: JK Date: Fri, 11 Dec 2020 00:21:10 +0000 Subject: [PATCH 04/11] Use strict types --- src/node/routes/login.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/node/routes/login.ts b/src/node/routes/login.ts index 4e21e456d30a..3d416f15bb41 100644 --- a/src/node/routes/login.ts +++ b/src/node/routes/login.ts @@ -1,4 +1,4 @@ -import { Router, Request } from "express" +import { Router, Request, Response, NextFunction } from "express" import { promises as fs } from "fs" import { RateLimiter as Limiter } from "limiter" import * as path from "path" @@ -43,7 +43,7 @@ const getRoot = async (req: Request, error?: Error): Promise => { const limiter = new RateLimiter() -const login = async (req : Request, res : any, password : string) => { +const login = async (req : Request, res : Response, password : string) => { try { if (!limiter.try()) { throw new Error("Login rate limited!") @@ -92,7 +92,7 @@ const login = async (req : Request, res : any, password : string) => { export const router = Router() -router.use((req : any, res : any, next : any) => { +router.use((req : Request, res : Response, next : NextFunction) => { const to = (typeof req.query.to === "string" && req.query.to) || "/" if (authenticated(req)) { return redirect(req, res, to, { @@ -104,7 +104,7 @@ router.use((req : any, res : any, next : any) => { next() }) -router.get("/", async (req : any, res : any) => { +router.get("/", async (req : Request, res : Response) => { if (req.args["enable-get-requests"]) { // `?password` overrides `?pass` if (req.query.password) { @@ -117,6 +117,6 @@ router.get("/", async (req : any, res : any) => { res.send(await getRoot(req)) }) -router.post("/", async (req : any, res : any) => { +router.post("/", async (req : Request, res : Response) => { await login(req, res, req.body.password) }) \ No newline at end of file From bf03423008ee7dc9d3f77b7ec9082e4b3721e4e1 Mon Sep 17 00:00:00 2001 From: JammSpread <61063879+JammSpread@users.noreply.github.com> Date: Thu, 10 Dec 2020 19:39:43 -0500 Subject: [PATCH 05/11] Type check password query Co-authored-by: Asher --- src/node/routes/login.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/node/routes/login.ts b/src/node/routes/login.ts index 3d416f15bb41..a0d4e3c58f0e 100644 --- a/src/node/routes/login.ts +++ b/src/node/routes/login.ts @@ -107,8 +107,8 @@ router.use((req : Request, res : Response, next : NextFunction) => { router.get("/", async (req : Request, res : Response) => { if (req.args["enable-get-requests"]) { // `?password` overrides `?pass` - if (req.query.password) { - return await login(req, res, req.query.password as string) + if (req.query.password && typeof req.query.password === "string") { + return await login(req, res, req.query.password) } else if (req.query.pass) { return await login(req, res, req.query.pass as string) @@ -119,4 +119,4 @@ router.get("/", async (req : Request, res : Response) => { router.post("/", async (req : Request, res : Response) => { await login(req, res, req.body.password) -}) \ No newline at end of file +}) From 6f473cd2b36f2be7aa483176ddd0f046603bdb6b Mon Sep 17 00:00:00 2001 From: JammSpread <61063879+JammSpread@users.noreply.github.com> Date: Thu, 10 Dec 2020 19:40:27 -0500 Subject: [PATCH 06/11] Type check pass query & fix linting error Co-authored-by: Asher --- src/node/routes/login.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/node/routes/login.ts b/src/node/routes/login.ts index a0d4e3c58f0e..201a7eb602de 100644 --- a/src/node/routes/login.ts +++ b/src/node/routes/login.ts @@ -109,9 +109,8 @@ router.get("/", async (req : Request, res : Response) => { // `?password` overrides `?pass` if (req.query.password && typeof req.query.password === "string") { return await login(req, res, req.query.password) - } - else if (req.query.pass) { - return await login(req, res, req.query.pass as string) +} else if (req.query.pass && typeof req.query.pass === "string") { + return await login(req, res, req.query.pass) } } res.send(await getRoot(req)) From 3961ce9a178aedf7d4785c3c424fc7de1b107876 Mon Sep 17 00:00:00 2001 From: JK Date: Fri, 11 Dec 2020 00:59:49 +0000 Subject: [PATCH 07/11] Modify 'enable-get-requests' desc Co-authored-by: Asher --- src/node/cli.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/node/cli.ts b/src/node/cli.ts index 97b9edac9f3c..71f8a553f329 100644 --- a/src/node/cli.ts +++ b/src/node/cli.ts @@ -150,7 +150,7 @@ const options: Options> = { socket: { type: "string", path: true, description: "Path to a socket (bind-addr will be ignored)." }, "enable-get-requests": { type: "boolean", - description: `Enable authentication via the url with queries. (Usage: ?pass=[password] after the rest of the url)` + description: `Enable authentication via the url with a query parameter. (Usage: ?pass=[password] after the rest of the url.)` }, version: { type: "boolean", short: "v", description: "Display version information." }, _: { type: "string[]" }, From fe110da229eabc2a7d61253c54f99791d564a419 Mon Sep 17 00:00:00 2001 From: JK Date: Fri, 11 Dec 2020 20:27:03 +0000 Subject: [PATCH 08/11] Add token options --- src/node/cli.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/node/cli.ts b/src/node/cli.ts index 71f8a553f329..6f7e3eb1b222 100644 --- a/src/node/cli.ts +++ b/src/node/cli.ts @@ -44,6 +44,9 @@ export interface Args extends VsArgs { "bind-addr"?: string socket?: string "enable-get-requests"?: boolean + tokens?: string[] + "generate-token"?: boolean + "list-tokens"?: boolean version?: boolean force?: boolean "list-extensions"?: boolean @@ -152,6 +155,19 @@ const options: Options> = { type: "boolean", description: `Enable authentication via the url with a query parameter. (Usage: ?pass=[password] after the rest of the url.)` }, + "tokens": { + type: "string[]", + description: "" + }, + "list-tokens": { + type: "boolean", + short: "t", + description: "List currently active tokens." + }, + "generate-token": { + type: "boolean", + description: "Generate a new token for quick access." + }, version: { type: "boolean", short: "v", description: "Display version information." }, _: { type: "string[]" }, From b83c22a758e510353075515ead80d8ee0b7c0ba3 Mon Sep 17 00:00:00 2001 From: JK Date: Fri, 11 Dec 2020 20:28:04 +0000 Subject: [PATCH 09/11] cli.ts: Add 'writeConfigFile' method --- src/node/cli.ts | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/src/node/cli.ts b/src/node/cli.ts index 6f7e3eb1b222..c6235d679b7b 100644 --- a/src/node/cli.ts +++ b/src/node/cli.ts @@ -562,6 +562,31 @@ export async function readConfigFile(configPath?: string): Promise { } } +export async function writeConfigFile(configPath?: string, data? : object) { + if (!configPath) { + configPath = process.env.CODE_SERVER_CONFIG + if (!configPath) { + configPath = path.join(paths.config, "config.yaml") + } + } + + if (!(await fs.pathExists(configPath))) { + await fs.outputFile(configPath, await defaultConfigFile()) + logger.info(`Wrote default config file to ${humanPath(configPath)}`) + } + + const configFile = await fs.readFile(configPath) + const config = yaml.safeLoad(configFile.toString(), { + filename: configPath, + }) + if (!config || typeof config === "string") { + throw new Error(`invalid config: ${config}`) + } + + const dumpedData = yaml.safeDump({...config, ...data}); + await fs.outputFile(configPath, dumpedData) +} + function parseBindAddr(bindAddr: string): Addr { const u = new URL(`http://${bindAddr}`) return { From f4ac75e2f82a27c778b010ca4273db6f47b9945b Mon Sep 17 00:00:00 2001 From: JK Date: Fri, 11 Dec 2020 21:02:07 +0000 Subject: [PATCH 10/11] cli.ts: Add 'revoke-token' option --- src/node/cli.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/node/cli.ts b/src/node/cli.ts index c6235d679b7b..d21f6464fd40 100644 --- a/src/node/cli.ts +++ b/src/node/cli.ts @@ -47,6 +47,7 @@ export interface Args extends VsArgs { tokens?: string[] "generate-token"?: boolean "list-tokens"?: boolean + "revoke-token"?: string version?: boolean force?: boolean "list-extensions"?: boolean @@ -168,6 +169,10 @@ const options: Options> = { type: "boolean", description: "Generate a new token for quick access." }, + "revoke-token": { + type: "string", + description: "Remove and disable a specific token from use." + }, version: { type: "boolean", short: "v", description: "Display version information." }, _: { type: "string[]" }, From 716f6bddab00e1b403b4cfd93f60ec2d9d8f7a5f Mon Sep 17 00:00:00 2001 From: JK Date: Fri, 11 Dec 2020 21:19:34 +0000 Subject: [PATCH 11/11] Add token option functionality --- src/node/entry.ts | 52 ++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 51 insertions(+), 1 deletion(-) diff --git a/src/node/entry.ts b/src/node/entry.ts index 41e40c4746fd..50e5345d58a2 100644 --- a/src/node/entry.ts +++ b/src/node/entry.ts @@ -11,6 +11,7 @@ import { optionDescriptions, parse, readConfigFile, + writeConfigFile, setDefaults, shouldOpenInExistingInstance, shouldRunVsCodeCli, @@ -19,7 +20,7 @@ import { coderCloudBind } from "./coder_cloud" import { commit, version } from "./constants" import * as proxyAgent from "./proxy_agent" import { register } from "./routes" -import { humanPath, isFile, open } from "./util" +import { humanPath, isFile, open, generatePassword } from "./util" import { isChild, wrapper } from "./wrapper" export const runVsCodeCli = (args: DefaultedArgs): void => { @@ -206,6 +207,55 @@ async function entry(): Promise { return } + if (args.tokens) { + args.tokens = args.tokens[0].split(",") + } + + if (args["list-tokens"]) { + console.log("code-server", version, commit) + console.log("") + if (!args.tokens) { + return console.log("No tokens currently exist") + } + console.log("Tokens") + args.tokens.forEach(token => { + console.log(" -", token) + }) + return + } + + if (args["generate-token"]) { + console.log("code-server", version, commit) + console.log("") + + if (!args.tokens) { + args.tokens = [] + } + + const token = await generatePassword() + args.tokens.push(token) + writeConfigFile(cliArgs.config, { tokens: args.tokens }) + console.log("Generated token:", token) + return + } + + if (args["revoke-token"]) { + console.log("code-server", version, commit) + console.log("") + + if (args.tokens?.includes(args["revoke-token"])) { + args.tokens = args.tokens.filter(token => { + return token != args["revoke-token"] + }) + writeConfigFile(cliArgs.config, { tokens: args.tokens }) + console.log("The token has successfully been revoked") + } + else { + console.log("The token specified does not exist") + } + return + } + if (shouldRunVsCodeCli(args)) { return runVsCodeCli(args) }