From d911eac5f26130158ef1e3e1acf2003c23e8e13f Mon Sep 17 00:00:00 2001 From: Gabe Rudy Date: Sun, 22 Dec 2024 13:04:07 -0700 Subject: [PATCH 1/3] 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 cee70311b54ef0a225195fcf8e092575a8e066c2 Mon Sep 17 00:00:00 2001 From: Gabe Rudy Date: Sat, 28 Dec 2024 21:52:16 -0700 Subject: [PATCH 2/3] Build scripts for custom Golden Helix build of code-server --- .dockerignore | 3 +++ Dockerfile | 51 ++++++++++++++++++++++++++++++++++++ build.sh | 45 +++++++++++++++++++++++++++++++ extensions.txt | 15 +++++++++++ install_locales.sh | 12 +++++++++ install_system_extensions.sh | 44 +++++++++++++++++++++++++++++++ package-lock.json | 8 +++--- package.json | 1 + startup.sh | 27 +++++++++++++++++++ 9 files changed, 203 insertions(+), 3 deletions(-) create mode 100644 Dockerfile create mode 100755 build.sh create mode 100644 extensions.txt create mode 100644 install_locales.sh create mode 100755 install_system_extensions.sh create mode 100644 startup.sh diff --git a/.dockerignore b/.dockerignore index 9bcce7a80897..659d188bec22 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,3 +1,6 @@ ** !release-packages !ci +!release-standalone +!startup.sh +!install_locales.sh \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 000000000000..39b7579f8538 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,51 @@ +ARG SERVER_VERSION=20241223_895aa2908981 +FROM registry.goldenhelix.com/gh/server:${SERVER_VERSION} + +USER root + +# Install various locales to support users from different regions +COPY ./install_locales.sh /tmp/install_locales.sh +RUN bash /tmp/install_locales.sh + +# Copy the release-standalone directory +COPY ./release-standalone /opt/code-server +COPY ./startup.sh /opt/code-server/startup.sh + +# Remove the existing node binary and create symlink to the system node +RUN rm -f /opt/code-server/lib/node && \ + ln -s /opt/node/bin/node /opt/code-server/lib/node && \ + chmod +x /opt/code-server/startup.sh + +# Set the environment variables +ARG LANG='en_US.UTF-8' +ARG LANGUAGE='en_US:en' +ARG LC_ALL='en_US.UTF-8' +ARG START_XFCE4=1 +ARG TZ='Etc/UTC' +ENV HOME=/home/ghuser \ + SHELL=/bin/bash \ + USERNAME=ghuser \ + LANG=$LANG \ + LANGUAGE=$LANGUAGE \ + LC_ALL=$LC_ALL \ + TZ=$TZ \ + AUTH_USER=ghuser \ + CODE_SERVER_SESSION_SOCKET=/home/ghuser/.config/code-server/code-server-ipc.sock \ + PASSWORD=ghuserpassword \ + PORT=8080 + +### Ports and user +EXPOSE $PORT +WORKDIR $HOME +USER 1000 + +RUN mkdir -p $HOME/.config/code-server && \ +echo "bind-addr: 0.0.0.0:8080" > $HOME/.config/code-server/config.yaml && \ +echo "auth: password" >> $HOME/.config/code-server/config.yaml && \ +echo "password: \${PASSWORD}" >> $HOME/.config/code-server/config.yaml && \ +echo "cert: true" >> $HOME/.config/code-server/config.yaml + + +RUN mkdir -p $HOME/Workspace/Documents + +ENTRYPOINT ["/opt/code-server/startup.sh"] diff --git a/build.sh b/build.sh new file mode 100755 index 000000000000..92100752d210 --- /dev/null +++ b/build.sh @@ -0,0 +1,45 @@ +#!/bin/bash + +# Build a Golden Helix docker image for code-server with changes to support App Streaming on VSWarehouse + +# Follow the directions under [CONTRIBUTING.md](docs/CONTRIBUTING.md) to build the image + +# git submodule update --init +# quilt push -a +# npm install +# npm run build +# VERSION=4.96.2 npm run build:vscode +# npm run release + +# VERSION=4.96.2 npm run package +# cd release +# npm install --omit=dev +# cd .. +# npm run release:standalone + +export VERSION=4.96.2 + +export SERVER_VERSION=20241223_895aa2908981 + +# Ensure we're in the correct directory +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +cd "$SCRIPT_DIR" + +# Temporarily move the bundled node out of the way +if [ -f "release-standalone/lib/node" ]; then + mv release-standalone/lib/node ./node +fi + +echo "PWD: $PWD" + +docker build --no-cache \ + --build-arg SERVER_VERSION=${SERVER_VERSION} \ + -t registry.goldenhelix.com/gh/code-server:${VERSION} . + +# Move the bundled node back +if [ -f "./node" ]; then + mv ./node release-standalone/lib/node +fi + +# Run like +# docker run -it -p 8081:8080 -e PASSWORD=your_secure_password123 -e PORT=8080 registry.goldenhelix.com/gh/code-server:20241223_895aa2908981 /home/ghuser/Workspace/ diff --git a/extensions.txt b/extensions.txt new file mode 100644 index 000000000000..bb499ddce72b --- /dev/null +++ b/extensions.txt @@ -0,0 +1,15 @@ +github.copilot +github.copilot-chat +hashicorp.terraform +ms-python.debugpy +ms-python.python +ms-python.vscode-pylance +ms-toolsai.jupyter +ms-toolsai.jupyter-keymap +ms-toolsai.jupyter-renderers +ms-toolsai.vscode-jupyter-cell-tags +ms-toolsai.vscode-jupyter-slideshow +ms-vscode-remote.remote-containers +stkb.rewrap +streetsidesoftware.code-spell-checker +redhat.vscode-yaml diff --git a/install_locales.sh b/install_locales.sh new file mode 100644 index 000000000000..320faa28a82a --- /dev/null +++ b/install_locales.sh @@ -0,0 +1,12 @@ +#!/bin/bash +# *GH* selected major languages for space and time +LOCALES="de_DE fr_FR it_IT es_ES en_GB nl_NL sv_SE pl_PL fr_BE nl_BE de_AT da_DK fi_FI el_GR en_IE pt_PT cs_CZ hu_HU ro_RO bg_BG ja_JP zh_CN ko_KR hi_IN ru_RU" + +echo "Installing languages" +apt-get update +apt-get install -y \ + locales locales-all +for LOCALE in ${LOCALES}; do + echo "Generating Locale for ${LOCALE}" + localedef -i ${LOCALE} -f UTF-8 ${LOCALE}.UTF-8 +done \ No newline at end of file diff --git a/install_system_extensions.sh b/install_system_extensions.sh new file mode 100755 index 000000000000..52a69be0dbc2 --- /dev/null +++ b/install_system_extensions.sh @@ -0,0 +1,44 @@ +#!/bin/bash + +EXTENSIONS_FILE="./extensions.txt" +CODE_SERVER="./release-standalone/bin/code-server" +EXTENSIONS_DIR="~/.local/share/code-server/extensions" +TARGET_DIR="./release-standalone/lib/vscode/extensions" + +# Check if code-server exists +if [ ! -f "$CODE_SERVER" ]; then + echo "Error: code-server not found at $CODE_SERVER" + exit 1 +fi + +# Create target directory if it doesn't exist +mkdir -p "$TARGET_DIR" + +# Read extensions file line by line +while IFS= read -r extension || [ -n "$extension" ]; do + # Skip empty lines and comments + if [[ -z "$extension" || "$extension" =~ ^# ]]; then + continue + fi + + echo "Installing extension: $extension" + + # Install the extension + $CODE_SERVER --install-extension "$extension" + + if [ $? -ne 0 ]; then + echo "Warning: Failed to install $extension" + continue + fi + + echo "Copying extension files to standalone directory" + # Use cp -R with expanded source path + if ! eval "cp -R $EXTENSIONS_DIR/${extension}* $TARGET_DIR/"; then + echo "Warning: Failed to copy $extension to standalone directory" + fi + + echo "Completed processing $extension" + echo "----------------------------------------" +done < "$EXTENSIONS_FILE" + +echo "All extensions processed" \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 3779af901516..fe5ad5c90604 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,6 +28,7 @@ "safe-buffer": "^5.2.1", "safe-compare": "^1.1.4", "semver": "^7.5.4", + "tslib": "^2.8.1", "ws": "^8.14.2", "xdg-basedir": "^4.0.0" }, @@ -5631,9 +5632,10 @@ } }, "node_modules/tslib": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", - "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==" + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" }, "node_modules/type-check": { "version": "0.4.0", diff --git a/package.json b/package.json index e91feacc035e..febd67725616 100644 --- a/package.json +++ b/package.json @@ -86,6 +86,7 @@ "safe-buffer": "^5.2.1", "safe-compare": "^1.1.4", "semver": "^7.5.4", + "tslib": "^2.8.1", "ws": "^8.14.2", "xdg-basedir": "^4.0.0" }, diff --git a/startup.sh b/startup.sh new file mode 100644 index 000000000000..cb2d2e4cb78e --- /dev/null +++ b/startup.sh @@ -0,0 +1,27 @@ +#!/bin/bash + +# Try to set up the private (per user) User Data folder +set +e +mkdir -p $HOME/Workspace/Documents/$USERNAME/code-server +export XDG_DATA_HOME=$HOME/Workspace/Documents/$USERNAME +set -e + +echo 'export PS1="$USERNAME:\w\$ "' >> $HOME/.bashrc + +# Set the default project folder +DEFAULT_PROJECT_FOLDER="$HOME/Workspace/" + +# Use the provided PROJECT_FOLDER or default to DEFAULT_PROJECT_FOLDER +STARTING_FOLDER="${PROJECT_FOLDER:-$DEFAULT_PROJECT_FOLDER}" + +# Your script logic here +echo "Starting in folder: $STARTING_FOLDER" + +/opt/code-server/bin/code-server \ + --disable-telemetry \ + --disable-update-check \ + --disable-workspace-trust \ + --locale=$LANG \ + --welcome-text="Welcome to your Golden Helix VSCode environment" \ + --ignore-last-opened \ + $STARTING_FOLDER \ No newline at end of file From 69063082a23efc8f030ce0ec7130d0c0f27f2b9a Mon Sep 17 00:00:00 2001 From: Gabe Rudy Date: Sun, 19 Jan 2025 16:01:38 -0700 Subject: [PATCH 3/3] Add code as symlink --- build.sh | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/build.sh b/build.sh index 92100752d210..f5b26a6049b8 100755 --- a/build.sh +++ b/build.sh @@ -13,7 +13,7 @@ # VERSION=4.96.2 npm run package # cd release -# npm install --omit=dev +# npm install --omit=dev # cd .. # npm run release:standalone @@ -41,5 +41,9 @@ if [ -f "./node" ]; then mv ./node release-standalone/lib/node fi +# Add "code" as symlink +cd release-standalone/lib/vscode/bin/remote-cli/ +ln -s code-linux.sh code + # Run like # docker run -it -p 8081:8080 -e PASSWORD=your_secure_password123 -e PORT=8080 registry.goldenhelix.com/gh/code-server:20241223_895aa2908981 /home/ghuser/Workspace/