From d911eac5f26130158ef1e3e1acf2003c23e8e13f Mon Sep 17 00:00:00 2001 From: Gabe Rudy Date: Sun, 22 Dec 2024 13:04:07 -0700 Subject: [PATCH 1/5] Support HTTP BasicAuth for authentication if $AUTH_USER is set --- src/node/cli.ts | 10 ++++++++++ src/node/http.ts | 22 ++++++++++++++++++++++ src/node/main.ts | 4 ++++ src/node/routes/domainProxy.ts | 5 +++++ src/node/routes/pathProxy.ts | 3 ++- src/node/routes/vscode.ts | 8 +++++++- 6 files changed, 50 insertions(+), 2 deletions(-) diff --git a/src/node/cli.ts b/src/node/cli.ts index 9eb6e5163e8a..aace0b59a0eb 100644 --- a/src/node/cli.ts +++ b/src/node/cli.ts @@ -12,6 +12,7 @@ export enum Feature { export enum AuthType { Password = "password", + HttpBasic = "http-basic", None = "none", } @@ -65,6 +66,7 @@ export interface UserProvidedCodeArgs { export interface UserProvidedArgs extends UserProvidedCodeArgs { config?: string auth?: AuthType + "auth-user"?: string password?: string "hashed-password"?: string cert?: OptionalString @@ -137,6 +139,10 @@ export type Options = { export const options: Options> = { auth: { type: AuthType, description: "The type of authentication to use." }, + "auth-user": { + type: "string", + description: "The username for http-basic authentication." + }, password: { type: "string", description: "The password for password authentication (can only be passed in via $PASSWORD or the config file).", @@ -569,6 +575,10 @@ export async function setDefaults(cliArgs: UserProvidedArgs, configArgs?: Config if (process.env.PASSWORD) { args.password = process.env.PASSWORD } + if (process.env.AUTH_USER) { + args["auth"] = AuthType.HttpBasic + args["auth-user"] = process.env.AUTH_USER + } if (process.env.CS_DISABLE_FILE_DOWNLOADS?.match(/^(1|true)$/)) { args["disable-file-downloads"] = true diff --git a/src/node/http.ts b/src/node/http.ts index e0fb3a4caf6b..88dad9c255fd 100644 --- a/src/node/http.ts +++ b/src/node/http.ts @@ -111,6 +111,25 @@ export const ensureAuthenticated = async ( } } +/** + * Validate basic auth credentials. + */ +const validateBasicAuth = (authHeader: string | undefined, authUser: string | undefined, authPassword: string | undefined): boolean => { + if (!authHeader?.startsWith('Basic ')) { + return false; + } + + try { + const base64Credentials = authHeader.split(' ')[1]; + const credentials = Buffer.from(base64Credentials, 'base64').toString('utf-8'); + const [username, password] = credentials.split(':'); + return username === authUser && password === authPassword; + } catch (error) { + logger.error('Error validating basic auth:' + error); + return false; + } +}; + /** * Return true if authenticated via cookies. */ @@ -132,6 +151,9 @@ export const authenticated = async (req: express.Request): Promise => { return await isCookieValid(isCookieValidArgs) } + case AuthType.HttpBasic: { + return validateBasicAuth(req.headers.authorization, req.args["auth-user"], req.args.password); + } default: { throw new Error(`Unsupported auth type ${req.args.auth}`) } diff --git a/src/node/main.ts b/src/node/main.ts index b3c4e4c14500..5c02bf0eb653 100644 --- a/src/node/main.ts +++ b/src/node/main.ts @@ -142,6 +142,10 @@ export const runCodeServer = async ( } else { logger.info(` - Using password from ${args.config}`) } + } else if (args.auth === AuthType.HttpBasic) { + logger.info(" - HTTP basic authentication is enabled") + logger.info(" - Using user from $AUTH_USER") + logger.info(" - Using password from $PASSWORD") } else { logger.info(" - Authentication is disabled") } diff --git a/src/node/routes/domainProxy.ts b/src/node/routes/domainProxy.ts index 0a9bb4a324f7..05624a9f7972 100644 --- a/src/node/routes/domainProxy.ts +++ b/src/node/routes/domainProxy.ts @@ -3,6 +3,7 @@ import { HttpCode, HttpError } from "../../common/http" import { getHost, ensureProxyEnabled, authenticated, ensureAuthenticated, ensureOrigin, redirect, self } from "../http" import { proxy } from "../proxy" import { Router as WsRouter } from "../wsRouter" +import { AuthType } from "../cli" export const router = Router() @@ -78,6 +79,10 @@ router.all(/.*/, async (req, res, next) => { if (/\/login\/?/.test(req.path)) { return next() } + // If auth is HttpBasic, return a 401. + if (req.args.auth === AuthType.HttpBasic) { + throw new HttpError("Unauthorized", HttpCode.Unauthorized) + } // Redirect all other pages to the login. const to = self(req) return redirect(req, res, "login", { diff --git a/src/node/routes/pathProxy.ts b/src/node/routes/pathProxy.ts index ccfb0cc824a0..848a514f6243 100644 --- a/src/node/routes/pathProxy.ts +++ b/src/node/routes/pathProxy.ts @@ -4,6 +4,7 @@ import * as pluginapi from "../../../typings/pluginapi" import { HttpCode, HttpError } from "../../common/http" import { ensureProxyEnabled, authenticated, ensureAuthenticated, ensureOrigin, redirect, self } from "../http" import { proxy as _proxy } from "../proxy" +import { AuthType } from "../cli" const getProxyTarget = ( req: Request, @@ -28,7 +29,7 @@ export async function proxy( if (!(await authenticated(req))) { // If visiting the root (/:port only) redirect to the login page. - if (!req.params.path || req.params.path === "/") { + if ((!req.params.path || req.params.path === "/") && req.args.auth !== AuthType.HttpBasic) { const to = self(req) return redirect(req, res, "login", { to: to !== "/" ? to : undefined, diff --git a/src/node/routes/vscode.ts b/src/node/routes/vscode.ts index 7e8f0f3ff4e5..d2bd8e120aad 100644 --- a/src/node/routes/vscode.ts +++ b/src/node/routes/vscode.ts @@ -7,12 +7,13 @@ import * as net from "net" import * as path from "path" import { WebsocketRequest } from "../../../typings/pluginapi" import { logError } from "../../common/util" -import { CodeArgs, toCodeArgs } from "../cli" +import { AuthType, CodeArgs, toCodeArgs } from "../cli" import { isDevMode, vsRootPath } from "../constants" import { authenticated, ensureAuthenticated, ensureOrigin, redirect, replaceTemplates, self } from "../http" import { SocketProxyProvider } from "../socket" import { isFile } from "../util" import { Router as WsRouter } from "../wsRouter" +import { HttpCode, HttpError } from "../../common/http" export const router = express.Router() @@ -118,6 +119,11 @@ router.get("/", ensureVSCodeLoaded, async (req, res, next) => { const FOLDER_OR_WORKSPACE_WAS_CLOSED = req.query.ew if (!isAuthenticated) { + // If auth is HttpBasic, return a 401. + if (req.args.auth === AuthType.HttpBasic) { + res.setHeader('WWW-Authenticate', 'Basic realm="Access to the site"') + throw new HttpError("Unauthorized", HttpCode.Unauthorized) + }; const to = self(req) return redirect(req, res, "login", { to: to !== "/" ? to : undefined, From 6448408fc47bd4e38c9c1239774d5afb1773e396 Mon Sep 17 00:00:00 2001 From: Gabe Rudy Date: Sun, 19 Jan 2025 16:51:16 -0700 Subject: [PATCH 2/5] Support hashed password for basic auth and match style --- src/node/cli.ts | 10 +++++++-- src/node/http.ts | 39 ++++++++++++++++++++++++---------- src/node/main.ts | 13 +++++++----- src/node/routes/domainProxy.ts | 2 +- src/node/routes/vscode.ts | 6 +++--- 5 files changed, 48 insertions(+), 22 deletions(-) diff --git a/src/node/cli.ts b/src/node/cli.ts index aace0b59a0eb..60136913258c 100644 --- a/src/node/cli.ts +++ b/src/node/cli.ts @@ -140,8 +140,8 @@ export type Options = { export const options: Options> = { auth: { type: AuthType, description: "The type of authentication to use." }, "auth-user": { - type: "string", - description: "The username for http-basic authentication." + type: "string", + description: "The username for http-basic authentication.", }, password: { type: "string", @@ -486,6 +486,7 @@ export interface DefaultedArgs extends ConfigArgs { "proxy-domain": string[] verbose: boolean usingEnvPassword: boolean + usingEnvAuthUser: boolean usingEnvHashedPassword: boolean "extensions-dir": string "user-data-dir": string @@ -575,9 +576,13 @@ export async function setDefaults(cliArgs: UserProvidedArgs, configArgs?: Config if (process.env.PASSWORD) { args.password = process.env.PASSWORD } + + const usingEnvAuthUser = !!process.env.AUTH_USER if (process.env.AUTH_USER) { args["auth"] = AuthType.HttpBasic args["auth-user"] = process.env.AUTH_USER + } else if (args["auth-user"]) { + args["auth"] = AuthType.HttpBasic } if (process.env.CS_DISABLE_FILE_DOWNLOADS?.match(/^(1|true)$/)) { @@ -631,6 +636,7 @@ export async function setDefaults(cliArgs: UserProvidedArgs, configArgs?: Config return { ...args, usingEnvPassword, + usingEnvAuthUser, usingEnvHashedPassword, } as DefaultedArgs // TODO: Technically no guarantee this is fulfilled. } diff --git a/src/node/http.ts b/src/node/http.ts index 88dad9c255fd..28419c6d6886 100644 --- a/src/node/http.ts +++ b/src/node/http.ts @@ -4,6 +4,7 @@ import * as expressCore from "express-serve-static-core" import * as http from "http" import * as net from "net" import * as qs from "qs" +import safeCompare from "safe-compare" import { Disposable } from "../common/emitter" import { CookieKeys, HttpCode, HttpError } from "../common/http" import { normalize } from "../common/util" @@ -20,6 +21,7 @@ import { escapeHtml, escapeJSON, splitOnFirstEquals, + isHashMatch, } from "./util" /** @@ -114,21 +116,31 @@ export const ensureAuthenticated = async ( /** * Validate basic auth credentials. */ -const validateBasicAuth = (authHeader: string | undefined, authUser: string | undefined, authPassword: string | undefined): boolean => { - if (!authHeader?.startsWith('Basic ')) { - return false; +const validateBasicAuth = async ( + authHeader: string | undefined, + authUser: string | undefined, + authPassword: string | undefined, + hashedPassword: string | undefined, +): Promise => { + if (!authHeader?.startsWith("Basic ")) { + return false } try { - const base64Credentials = authHeader.split(' ')[1]; - const credentials = Buffer.from(base64Credentials, 'base64').toString('utf-8'); - const [username, password] = credentials.split(':'); - return username === authUser && password === authPassword; + const base64Credentials = authHeader.split(" ")[1] + const credentials = Buffer.from(base64Credentials, "base64").toString("utf-8") + const [username, password] = credentials.split(":") + if (username !== authUser) return false + if (hashedPassword) { + return await isHashMatch(password, hashedPassword) + } else { + return safeCompare(password, authPassword || "") + } } catch (error) { - logger.error('Error validating basic auth:' + error); - return false; + logger.error("Error validating basic auth:" + error) + return false } -}; +} /** * Return true if authenticated via cookies. @@ -152,7 +164,12 @@ export const authenticated = async (req: express.Request): Promise => { return await isCookieValid(isCookieValidArgs) } case AuthType.HttpBasic: { - return validateBasicAuth(req.headers.authorization, req.args["auth-user"], req.args.password); + return await validateBasicAuth( + req.headers.authorization, + req.args["auth-user"], + req.args.password, + req.args["hashed-password"], + ) } default: { throw new Error(`Unsupported auth type ${req.args.auth}`) diff --git a/src/node/main.ts b/src/node/main.ts index 5c02bf0eb653..a8c8560e18cc 100644 --- a/src/node/main.ts +++ b/src/node/main.ts @@ -133,7 +133,7 @@ export const runCodeServer = async ( logger.info(`Using config file ${args.config}`) logger.info(`${protocol.toUpperCase()} server listening on ${serverAddress.toString()}`) - if (args.auth === AuthType.Password) { + if (args.auth === AuthType.Password || args.auth === AuthType.HttpBasic) { logger.info(" - Authentication is enabled") if (args.usingEnvPassword) { logger.info(" - Using password from $PASSWORD") @@ -142,10 +142,13 @@ export const runCodeServer = async ( } else { logger.info(` - Using password from ${args.config}`) } - } else if (args.auth === AuthType.HttpBasic) { - logger.info(" - HTTP basic authentication is enabled") - logger.info(" - Using user from $AUTH_USER") - logger.info(" - Using password from $PASSWORD") + if (args.auth === AuthType.HttpBasic) { + if (args.usingEnvAuthUser) { + logger.info(" - Using user from $AUTH_USER") + } else { + logger.info(` - With user ${args["auth-user"]}`) + } + } } else { logger.info(" - Authentication is disabled") } diff --git a/src/node/routes/domainProxy.ts b/src/node/routes/domainProxy.ts index 05624a9f7972..e2af5cc4dfac 100644 --- a/src/node/routes/domainProxy.ts +++ b/src/node/routes/domainProxy.ts @@ -1,9 +1,9 @@ import { Request, Router } from "express" import { HttpCode, HttpError } from "../../common/http" +import { AuthType } from "../cli" import { getHost, ensureProxyEnabled, authenticated, ensureAuthenticated, ensureOrigin, redirect, self } from "../http" import { proxy } from "../proxy" import { Router as WsRouter } from "../wsRouter" -import { AuthType } from "../cli" export const router = Router() diff --git a/src/node/routes/vscode.ts b/src/node/routes/vscode.ts index d2bd8e120aad..7e04d5dad49d 100644 --- a/src/node/routes/vscode.ts +++ b/src/node/routes/vscode.ts @@ -6,6 +6,7 @@ import * as http from "http" import * as net from "net" import * as path from "path" import { WebsocketRequest } from "../../../typings/pluginapi" +import { HttpCode, HttpError } from "../../common/http" import { logError } from "../../common/util" import { AuthType, CodeArgs, toCodeArgs } from "../cli" import { isDevMode, vsRootPath } from "../constants" @@ -13,7 +14,6 @@ import { authenticated, ensureAuthenticated, ensureOrigin, redirect, replaceTemp import { SocketProxyProvider } from "../socket" import { isFile } from "../util" import { Router as WsRouter } from "../wsRouter" -import { HttpCode, HttpError } from "../../common/http" export const router = express.Router() @@ -121,9 +121,9 @@ router.get("/", ensureVSCodeLoaded, async (req, res, next) => { if (!isAuthenticated) { // If auth is HttpBasic, return a 401. if (req.args.auth === AuthType.HttpBasic) { - res.setHeader('WWW-Authenticate', 'Basic realm="Access to the site"') + res.setHeader("WWW-Authenticate", `Basic realm="${req.args["app-name"] || "code-server"}"`) throw new HttpError("Unauthorized", HttpCode.Unauthorized) - }; + } const to = self(req) return redirect(req, res, "login", { to: to !== "/" ? to : undefined, From 2d79605848a1aa2b8cbeabb9618934df84734bdf Mon Sep 17 00:00:00 2001 From: Gabe Rudy Date: Sun, 19 Jan 2025 19:40:58 -0700 Subject: [PATCH 3/5] Add file shutdown action and endpoint with allow-shutdown --- patches/series | 1 + patches/shutdown.diff | 114 +++++++++++++++++++++++++++++++++++++++ src/node/cli.ts | 5 ++ src/node/routes/index.ts | 9 ++++ 4 files changed, 129 insertions(+) create mode 100644 patches/shutdown.diff diff --git a/patches/series b/patches/series index 61c801ae9357..53e7ba4e6c48 100644 --- a/patches/series +++ b/patches/series @@ -20,3 +20,4 @@ getting-started.diff keepalive.diff clipboard.diff display-language.diff +shutdown.diff diff --git a/patches/shutdown.diff b/patches/shutdown.diff new file mode 100644 index 000000000000..ca845f788514 --- /dev/null +++ b/patches/shutdown.diff @@ -0,0 +1,114 @@ +Add a File > Exit menu item and a command to shutdown the server. + +Index: code-server/lib/vscode/src/vs/server/node/webClientServer.ts +=================================================================== +--- code-server.orig/lib/vscode/src/vs/server/node/webClientServer.ts ++++ code-server/lib/vscode/src/vs/server/node/webClientServer.ts +@@ -316,7 +316,8 @@ export class WebClientServer { + codeServerVersion: this._productService.codeServerVersion, + rootEndpoint: base, + updateEndpoint: !this._environmentService.args['disable-update-check'] ? base + '/update/check' : undefined, +- logoutEndpoint: this._environmentService.args['auth'] && this._environmentService.args['auth'] !== "none" ? base + '/logout' : undefined, ++ logoutEndpoint: this._environmentService.args['auth'] && this._environmentService.args['auth'] !== "none" && this._environmentService.args['auth'] !== "http-basic" ? base + '/logout' : undefined, ++ shutdownEndpoint: this._environmentService.args['allow-shutdown'] ? base + '/shutdown' : undefined, + proxyEndpointTemplate: process.env.VSCODE_PROXY_URI ?? base + '/proxy/{{port}}/', + serviceWorker: { + scope: vscodeBase + '/', +Index: code-server/lib/vscode/src/vs/base/common/product.ts +=================================================================== +--- code-server.orig/lib/vscode/src/vs/base/common/product.ts ++++ code-server/lib/vscode/src/vs/base/common/product.ts +@@ -59,6 +59,7 @@ export interface IProductConfiguration { + readonly rootEndpoint?: string + readonly updateEndpoint?: string + readonly logoutEndpoint?: string ++ readonly shutdownEndpoint?: string + readonly proxyEndpointTemplate?: string + readonly serviceWorker?: { + readonly path: string; +Index: code-server/lib/vscode/src/vs/workbench/browser/client.ts +=================================================================== +--- code-server.orig/lib/vscode/src/vs/workbench/browser/client.ts ++++ code-server/lib/vscode/src/vs/workbench/browser/client.ts +@@ -9,6 +9,7 @@ import { IStorageService, StorageScope, + + export class CodeServerClient extends Disposable { + static LOGOUT_COMMAND_ID = 'code-server.logout'; ++ static SHUTDOWN_COMMAND_ID = 'code-server.shutdown'; + + constructor ( + @ILogService private logService: ILogService, +@@ -90,6 +91,10 @@ export class CodeServerClient extends Di + this.addLogoutCommand(this.productService.logoutEndpoint); + } + ++ if (this.productService.shutdownEndpoint) { ++ this.addShutdownCommand(this.productService.shutdownEndpoint); ++ } ++ + if (this.productService.serviceWorker) { + await this.registerServiceWorker(this.productService.serviceWorker); + } +@@ -164,6 +169,22 @@ export class CodeServerClient extends Di + }, + }); + } ++ } ++ ++ private addShutdownCommand(shutdownEndpoint: string) { ++ CommandsRegistry.registerCommand(CodeServerClient.SHUTDOWN_COMMAND_ID, () => { ++ const shutdownUrl = new URL(shutdownEndpoint, window.location.href); ++ window.location.assign(shutdownUrl); ++ }); ++ ++ for (const menuId of [MenuId.CommandPalette, MenuId.MenubarHomeMenu]) { ++ MenuRegistry.appendMenuItem(menuId, { ++ command: { ++ id: CodeServerClient.SHUTDOWN_COMMAND_ID, ++ title: localize('exit', "Exit"), ++ }, ++ }); ++ } + } + + private async registerServiceWorker(serviceWorker: { path: string; scope: string }) { +Index: code-server/src/node/routes/index.ts +=================================================================== +--- code-server.orig/src/node/routes/index.ts ++++ code-server/src/node/routes/index.ts +@@ -170,6 +170,15 @@ export const register = async (app: App, + app.router.all("/logout", (req, res) => redirect(req, res, "/", {})) + } + ++ if (args["allow-shutdown"] ) { ++ app.router.use("/shutdown", async (req, res) => { ++ res.send("Shutting down...") ++ process.exit(0) ++ }) ++ } else { ++ app.router.use("/shutdown", (req, res) => redirect(req, res, "/", {})) ++ } ++ + app.router.use("/update", update.router) + + // Note that the root route is replaced in Coder Enterprise by the plugin API. +Index: code-server/lib/vscode/src/vs/server/node/serverEnvironmentService.ts +=================================================================== +--- code-server.orig/lib/vscode/src/vs/server/node/serverEnvironmentService.ts ++++ code-server/lib/vscode/src/vs/server/node/serverEnvironmentService.ts +@@ -16,6 +16,8 @@ export const serverOptions: OptionDescri + /* ----- code-server ----- */ + 'disable-update-check': { type: 'boolean' }, + 'auth': { type: 'string' }, ++ 'allow-shutdown': { type: 'boolean' }, + 'disable-file-downloads': { type: 'boolean' }, + 'disable-file-uploads': { type: 'boolean' }, + 'disable-getting-started-override': { type: 'boolean' }, +@@ -103,6 +105,8 @@ export interface ServerParsedArgs { + /* ----- code-server ----- */ + 'disable-update-check'?: boolean; + 'auth'?: string; ++ 'allow-shutdown'?: boolean; + 'disable-file-downloads'?: boolean; + 'disable-file-uploads'?: boolean; + 'disable-getting-started-override'?: boolean, diff --git a/src/node/cli.ts b/src/node/cli.ts index 60136913258c..8f990b15e43b 100644 --- a/src/node/cli.ts +++ b/src/node/cli.ts @@ -35,6 +35,7 @@ export class OptionalString extends Optional {} */ export interface UserProvidedCodeArgs { "disable-telemetry"?: boolean + "allow-shutdown"?: boolean force?: boolean "user-data-dir"?: string "enable-proposed-api"?: string[] @@ -164,6 +165,10 @@ export const options: Options> = { }, "cert-key": { type: "string", path: true, description: "Path to certificate key when using non-generated cert." }, "disable-telemetry": { type: "boolean", description: "Disable telemetry." }, + "allow-shutdown": { + type: "boolean", + description: "Allow the server to be shut down remotely.", + }, "disable-update-check": { type: "boolean", description: diff --git a/src/node/routes/index.ts b/src/node/routes/index.ts index e61cbd65795c..42b19f9c0c44 100644 --- a/src/node/routes/index.ts +++ b/src/node/routes/index.ts @@ -170,6 +170,15 @@ export const register = async (app: App, args: DefaultedArgs): Promise redirect(req, res, "/", {})) } + if (args["allow-shutdown"] ) { + app.router.use("/shutdown", async (req, res) => { + res.send("Shutting down...") + process.exit(0) + }) + } else { + app.router.use("/shutdown", (req, res) => redirect(req, res, "/", {})) + } + app.router.use("/update", update.router) // Note that the root route is replaced in Coder Enterprise by the plugin API. From 03c5fbe09f77866157689f4e1f2397de618854e6 Mon Sep 17 00:00:00 2001 From: Gabe Rudy Date: Mon, 20 Jan 2025 08:43:17 -0700 Subject: [PATCH 4/5] Use process.kill SIGTERM for file shutdown behavior --- src/node/routes/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/node/routes/index.ts b/src/node/routes/index.ts index 42b19f9c0c44..56bbfd9d248b 100644 --- a/src/node/routes/index.ts +++ b/src/node/routes/index.ts @@ -172,8 +172,8 @@ export const register = async (app: App, args: DefaultedArgs): Promise { - res.send("Shutting down...") - process.exit(0) + res.send(`Shutting down...`) + process.kill(process.pid, "SIGTERM") }) } else { app.router.use("/shutdown", (req, res) => redirect(req, res, "/", {})) From 27ac8c8f9544f9589288aa2f8c7d32aa2003465a Mon Sep 17 00:00:00 2001 From: Gabe Rudy Date: Mon, 20 Jan 2025 16:45:51 -0700 Subject: [PATCH 5/5] Use wrapper.exit to perform /shutdown --- src/node/routes/index.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/node/routes/index.ts b/src/node/routes/index.ts index 56bbfd9d248b..3acba071153d 100644 --- a/src/node/routes/index.ts +++ b/src/node/routes/index.ts @@ -17,6 +17,7 @@ import { PluginAPI } from "../plugin" import { CoderSettings, SettingsProvider } from "../settings" import { UpdateProvider } from "../update" import { getMediaMime, paths } from "../util" +import { wrapper } from "../wrapper" import * as apps from "./apps" import * as domainProxy from "./domainProxy" import { errorHandler, wsErrorHandler } from "./errors" @@ -172,8 +173,9 @@ export const register = async (app: App, args: DefaultedArgs): Promise { - res.send(`Shutting down...`) - process.kill(process.pid, "SIGTERM") + redirect(req, res, "/", {}) + logger.warn("Shutting down due to /shutdown") + setTimeout(() => wrapper.exit(0), 10) }) } else { app.router.use("/shutdown", (req, res) => redirect(req, res, "/", {}))