Skip to content

feat: add path-based sharing for kasm #115

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 15 commits into from
May 30, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion registry/coder/modules/kasmvnc/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,10 @@ Automatically install [KasmVNC](https://kasmweb.com/kasmvnc) in a workspace, and
module "kasmvnc" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/kasmvnc/coder"
version = "1.0.23"
version = "1.1.0"
agent_id = coder_agent.example.id
desktop_environment = "xfce"
subdomain = true
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
subdomain = true

We can drop as it's the default value.

Add a new example below with subdomain = false to share how they can use it without a subdomain.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looked like we were using that to show all of the values. I can do a follow-up PR if that is "minimum config to start" and make a section for "Using the module with path-based sharing"

}
```

Expand Down
22 changes: 16 additions & 6 deletions registry/coder/modules/kasmvnc/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -29,32 +29,42 @@ variable "kasm_version" {
variable "desktop_environment" {
type = string
description = "Specifies the desktop environment of the workspace. This should be pre-installed on the workspace."

validation {
condition = contains(["xfce", "kde", "gnome", "lxde", "lxqt"], var.desktop_environment)
error_message = "Invalid desktop environment. Please specify a valid desktop environment."
}
}

variable "subdomain" {
type = bool
default = true
description = "Is subdomain sharing enabled in your cluster?"
}

resource "coder_script" "kasm_vnc" {
agent_id = var.agent_id
display_name = "KasmVNC"
icon = "/icon/kasmvnc.svg"
run_on_start = true
script = templatefile("${path.module}/run.sh", {
PORT : var.port,
DESKTOP_ENVIRONMENT : var.desktop_environment,
KASM_VERSION : var.kasm_version
PORT = var.port,
DESKTOP_ENVIRONMENT = var.desktop_environment,
KASM_VERSION = var.kasm_version
SUBDOMAIN = tostring(var.subdomain)
PATH_VNC_HTML = var.subdomain ? "" : file("${path.module}/path_vnc.html")
})
run_on_start = true
}

resource "coder_app" "kasm_vnc" {
agent_id = var.agent_id
slug = "kasm-vnc"
display_name = "kasmVNC"
display_name = "KasmVNC"
url = "http://localhost:${var.port}"
icon = "/icon/kasmvnc.svg"
subdomain = true
subdomain = var.subdomain
share = "owner"

healthcheck {
url = "http://localhost:${var.port}/app"
interval = 5
Expand Down
81 changes: 81 additions & 0 deletions registry/coder/modules/kasmvnc/path_vnc.html
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't understand, but probably it's needed for the path-based service to work.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Exactly, this leverages javascript as a fast path rewrite to provide the configuration in a way that the vnc.html page can parse as a valid configuration, thus setting the websocket address to the proper path.

Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
<!DOCTYPE html>
<html>
<head>
<title>Path-Sharing Bounce Page</title>
<style type="text/css">
:root {
color-scheme: light dark;
--dark: #121212;
--header-bg: rgba(127,127,127,0.2);
--light: white;
--rule-color: light-dark(rgba(0,0,0,0.8), rgba(255,255,255,0.8));
background-color: light-dark(var(--light), var(--dark));
color: light-dark(var(--dark), var(--light));
}
body, h1, p {
box-sizing: border-box;
margin:0; padding:0;
}
body{
font-family:Inter, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
}
h1{
width: 100%;
padding: 1rem;
letter-spacing: -1.5pt;
padding-bottom:10px;
border-bottom: 1px solid var(--rule-color);
background-color: var(--header-bg);
}
p {
padding: 1rem; letter-spacing: -0.5pt;}
a.indent { display:inline-block; padding-top:0.5rem; padding-left: 2rem; font-size:0.8rem }
</style>
<meta charset="UTF-8" />
</head>
<body>
<h1>Path-Sharing Bounce Page</h1>
<p>
This application is being served via path sharing.
If you are not redirected, <span id="help">check the
Javascript console in your browser's developer tools
for more information.</span>
</p>
</body>
<script language="javascript">
// This page exists to satisfy the querystring driven client API
// specified here - https://raw.githubusercontent.com/kasmtech/noVNC/bce2d6a7048025c6e6c05df9d98b206c23f6dbab/docs/EMBEDDING.md
// tl;dr:
// * `host` - The WebSocket host to connect to.
// This is just the hostname component of the original URL
// * `port` - The WebSocket port to connect to.
// It doesn't look like we need to set this unless it's different
// than the incoming http request.
// * `encrypt` - If TLS should be used for the WebSocket connection.
// we base this on whether or not the protocol is `https`, seems
// reasonable for now.
// * `path` - The WebSocket path to use.
// This apparently doesn't tolerate a leading `/` so we use a
// function to tidy that up.
function trimFirstCharIf(str, char) {
return str.charAt(0) === char ? str.slice(1) : str;
}
function trimLastCharIf(str, char) {
return str.endsWith("/") ? str.slice(0,str.length-1) : str;
}
const newloc = new URL(window.location);
const h = document.getElementById("help")

// Building the websockify path must happen before we append the filename to newloc.pathname
newloc.searchParams.append("path",
trimLastCharIf(trimFirstCharIf(newloc.pathname,"/"),"/")+"/websockify");
newloc.searchParams.append("encrypted", newloc.protocol==="https:"? true : false);

newloc.pathname += "vnc.html"
console.log(newloc);

h.innerHTML = `click <a id="link" href="${newloc.toString()}">here</a> to go to the application.
<br/><br/>The rewritten URL is:<br/><a id="link" class="indent" href="${newloc.toString()}">${newloc.toString()}</a>`
window.location = newloc.href;
</script>
</html>
87 changes: 78 additions & 9 deletions registry/coder/modules/kasmvnc/run.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
# Exit on error, undefined variables, and pipe failures
set -euo pipefail

error() { printf "💀 ERROR: %s\n" "$@"; exit 1; }

# Function to check if vncserver is already installed
check_installed() {
if command -v vncserver &> /dev/null; then
Expand Down Expand Up @@ -188,7 +190,7 @@ if command -v sudo &> /dev/null && sudo -n true 2> /dev/null; then
SUDO=sudo
else
kasm_config_file="$HOME/.vnc/kasmvnc.yaml"
SUDO=
SUDO=""

echo "WARNING: Sudo access not available, using user config dir!"

Expand All @@ -206,6 +208,7 @@ echo "Writing KasmVNC config to $kasm_config_file"
$SUDO tee "$kasm_config_file" > /dev/null << EOF
network:
protocol: http
interface: 127.0.0.1
websocket_port: ${PORT}
ssl:
require_ssl: false
Expand All @@ -220,16 +223,82 @@ EOF
# and does not listen publicly
echo -e "password\npassword\n" | vncpasswd -wo -u "$USER"

get_http_dir() {
# determine the served file path
# Start with the default
httpd_directory="/usr/share/kasmvnc/www"

# Check the system configuration path
if [[ -e /etc/kasmvnc/kasmvnc.yaml ]]; then
d=($(grep -E "^\s*httpd_directory:.*$" /etc/kasmvnc/kasmvnc.yaml))
# If this grep is successful, it will return:
# httpd_directory: /usr/share/kasmvnc/www
if [[ $${#d[@]} -eq 2 && -d "$${d[1]}" ]]; then
httpd_directory="$${d[1]}"
fi
fi

# Check the home directory for overriding values
if [[ -e "$HOME/.vnc/kasmvnc.yaml" ]]; then
d=($(grep -E "^\s*httpd_directory:.*$" /etc/kasmvnc/kasmvnc.yaml))
if [[ $${#d[@]} -eq 2 && -d "$${d[1]}" ]]; then
httpd_directory="$${d[1]}"
fi
fi
echo $httpd_directory
}

fix_server_index_file(){
local fname=$${FUNCNAME[0]} # gets current function name
if [[ $# -ne 1 ]]; then
error "$fname requires exactly 1 parameter:\n\tpath to KasmVNC httpd_directory"
fi
local httpdir="$1"
if [[ ! -d "$httpdir" ]]; then
error "$fname: $httpdir is not a directory"
fi
pushd "$httpdir" > /dev/null

cat <<'EOH' > /tmp/path_vnc.html
${PATH_VNC_HTML}
EOH
$SUDO mv /tmp/path_vnc.html .
# check for the switcheroo
if [[ -f "index.html" && -L "vnc.html" ]]; then
$SUDO mv $httpdir/index.html $httpdir/vnc.html
fi
$SUDO ln -s -f path_vnc.html index.html
popd > /dev/null
}

patch_kasm_http_files(){
homedir=$(get_http_dir)
fix_server_index_file "$homedir"
}

if [[ "${SUBDOMAIN}" == "false" ]]; then
echo "🩹 Patching up webserver files to support path-sharing..."
patch_kasm_http_files
fi

VNC_LOG="/tmp/kasmvncserver.log"
# Start the server
printf "🚀 Starting KasmVNC server...\n"
vncserver -select-de "${DESKTOP_ENVIRONMENT}" -disableBasicAuth > /tmp/kasmvncserver.log 2>&1 &
pid=$!

# Wait for server to start
sleep 5
grep -v '^[[:space:]]*$' /tmp/kasmvncserver.log | tail -n 10
if ps -p $pid | grep -q "^$pid"; then
echo "ERROR: Failed to start KasmVNC server. Check full logs at /tmp/kasmvncserver.log"

set +e
vncserver -select-de "${DESKTOP_ENVIRONMENT}" -disableBasicAuth > "$VNC_LOG" 2>&1
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is dropping & at the end intentional?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, vncserver forks all by itself if it successfully launches.

RETVAL=$?
set -e

if [[ $RETVAL -ne 0 ]]; then
echo "ERROR: Failed to start KasmVNC server. Return code: $RETVAL"
if [[ -f "$VNC_LOG" ]]; then
echo "Full logs:"
cat "$VNC_LOG"
else
echo "ERROR: Log file not found: $VNC_LOG"
fi
exit 1
fi

printf "🚀 KasmVNC server started successfully!\n"