title: Router Service | abcdesktop.io description: Developer reference for the abcdesktop.io router service: WebSocket proxying, JWT token validation, nginx configuration, and session routing. keywords: router, WebSocket, nginx, JWT, proxy, abcdesktop, contribute, architecture tags: - contribute - router - WebSocket
Route - OpenResty HTTP Router
Project Specification
Purpose
route is an OpenResty-based HTTP reverse proxy designed for the abcdesktop platform. It provides secure, JWT-authenticated routing to user desktop containers running in Kubernetes.
Architecture Overview
┌─────────────────┐
│ Client │
│ (Browser) │
└────────┬────────┘
│ HTTP Request + JWT
▼
┌─────────────────────────────────────────────────────────┐
│ route (OpenResty) │
│ ┌───────────────────────────────────────────────────┐ │
│ │ get.targetmap.lua │ │
│ │ - Extract JWT from request │ │
│ │ - Verify JWT signature │ │
│ │ - RSA Decrypt payload to get target hostname │ │
│ │ - Cache result in shared dict (10 min) │ │
│ └───────────────────────────────────────────────────┘ │
└────────┬────────────────────────────────────────────────┘
│ $target variable resolved
▼
┌─────────────────────────────────────────────────────────┐
│ Pods (Kubernetes) │
│ ┌──────────────────────┐ │
│ │ Desktop Pod │ │
│ │ service TCP port │ │
│ └──────────────────────┘ │
└─────────────────────────────────────────────────────────┘
Detailed Analysis: get.targetmap.lua
Role
This Lua script is the core authentication and routing engine. It executes during the rewrite phase for protected endpoints and resolves the $target nginx variable used in proxy_pass directives.
Execution Flow
1. Extract JWT token from request
│
▼
2. Check targetmap cache
┌──────────────┐
│ Cache Hit? │──Yes──→ Set $target → Continue request
└──────────────┘
│ No
▼
3. Read public key from env or cache
│
▼
4. Verify JWT signature
│
▼
5. Decrypt payload.hash using dedicated RSA private key
│
▼
6. Cache result in targetmap (TTL: min(exp-now, 600s))
│
▼
7. Set $target = decrypted hostname
Code Walkthrough
1. Token Extraction
local jwt_token = ngx.var.jwt_token
The token is sourced from:
- Query parameter: ?jwt_token=xxx
- Header: Authorization: Bearer <token>
- Header: AbcAuthorization: <token>
The no_bearer() function strips the "Bearer " prefix if present.
2. Response Helper
function ngxexitresponse(status, msg)
ngx.status = status
ngx.log(ngx.ERR, msg)
ngx.say(msg)
ngx.exit(ngx.HTTP_OK)
end
Returns HTTP error responses with logging. Uses ngx.exit(ngx.HTTP_OK) after setting ngx.status to properly interrupt execution and flush the response.
3. RSA Decryption
local function decrypt(msg, private_key)
local rsa = require "resty.rsa"
local priv, err = rsa:new({ private_key = private_key })
local crypto = ngx.decode_base64(msg)
local decrypted, err = priv:decrypt(crypto)
return decrypted
end
The JWT payload contains a hash field that is:
1. Base64-encoded
2. RSA-encrypted with the server's public key
3. Contains the target pod hostname
Decrypting the hash field yields the DNS hostname of the user's desktop pod, which nginx uses as the proxy_pass target.
4. Cache Lookup
local target = ngx.shared.targetmap:get(jwt_token)
The targetmap shared dictionary (8MB) caches resolved targets to avoid repeated JWT processing.
5. JWT Verification
local jwt = require "resty.jwt"
local jwt_secret = ngx.shared.rsakeymap:get('jwt_desktop_signing_public_key')
or readfile(os.getenv("JWT_DESKTOP_SIGNING_PUBLIC_KEY"))
local jwt_obj = jwt:verify(jwt_secret, jwt_token)
Verifies the JWT signature using the configured RSA public key.
6. Payload Decryption & Caching
local private_key = ngx.shared.rsakeymap:get('jwt_desktop_payload_private_key')
or readfile(os.getenv("JWT_DESKTOP_PAYLOAD_PRIVATE_KEY"))
target = decrypt(payload.hash, private_key)
-- Cache with TTL
local expire_value = payload.exp - ngx.time()
if expire_value > 600 then expire_value = 600 end
if expire_value > 1 then
ngx.shared.targetmap:set(jwt_token, target, expire_value)
end
Cache TTL Logic:
- Uses JWT
expclaim for expiration - Capped at 600 seconds (10 minutes) maximum
- Minimum 1 second required to cache
- Prevents caching of expired/invalid tokens
7. Target Assignment
ngx.var.target = target
Sets the nginx variable used by proxy_pass http://$target:$port/
Endpoint Routing Table
Service Endpoints
| Endpoint | Service Variable | Default Port | Protocol |
|---|---|---|---|
/spawner |
$spawner_service_tcp_port |
29786 | HTTP |
/terminals |
$xterm_tcp_port |
29781 | WebSocket |
/terminals/{id}/size |
$xterm_tcp_port |
29781 | HTTP |
/filer |
$file_service_tcp_port |
29783 | HTTP (8GB max) |
/printerfiler |
$printerfile_service_tcp_port |
29782 | HTTP |
/websockify |
$ws_tcp_bridge_tcp_port |
6081 | WebSocket |
/signalling |
$signalling_service_tcp_port |
29787 | WebSocket |
/broadcast |
$broadcast_tcp_port |
29784 | WebSocket |
/sound |
$sound_service_tcp_port |
29788 | WebSocket |
/microphone |
$microphone_service_tcp_port |
29789 | WebSocket |
/gamepad |
$gamepad_service_tcp_port |
29790 | WebSocket |
/snapshot |
$snapshot_service_tcp_port |
29785 | HTTP |
/console |
console upstream |
- | HTTP |
pyos Endpoints
Pattern proxy_pass backend: http://$pyos_fqdn:$pyos_service_port:
/API/manager/imagewith client_max_body_size 16m;/API/composer/launchdesktopwith proxy_read_timeout 600s;/API
Utility Endpoints
| Endpoint | Description |
|---|---|
/healthz |
Health check, returns "OK" |
/node |
Returns NODE_NAME environment variable |
/.well-known/* |
Let's Encrypt ACME challenges |
/speedtest |
Raw data throughput testing (no gzip) |
/ |
Default website content |
Configuration Files
nginx.conf - Main Configuration
Key directives:
worker_processes auto— Scales worker processes to the number of available CPUsdaemon off— Required for container foreground execution-
envdirectives — Exposes environment variables to Lua scripts running inside nginx -
Lua shared dictionaries:
rsakeymap(1MB) - RSA key content cachersafilenamekeymap(1MB) - Key filename mappingstargetmap(8MB) - JWT → target resolution cache
Security settings:
server_tokens off- Hides nginx versionmore_clear_headers Server- Removes Server headerTLS 1.2+with strong cipher suite
sites-enabled/routehttp.conf - Server Block
Initialization:
init_by_lua_block {
pyos_fqdn = os.getenv("PYOS_FQDN") or "pyos"
pyos_service_port = os.getenv("PYOS_SERVICE_PORT") or "8000"
}
Service port variables:
set $ws_tcp_bridge_tcp_port 6081;
set $xterm_tcp_port 29781;
set $file_service_tcp_port 29783;
# ... etc
Listening:
- Port 80 (HTTP)
- Port 443 (HTTPS, commented out by default)
proxy.conf - Standard Proxy Headers
proxy_set_header User-Agent $http_user_agent;
proxy_set_header Origin $http_origin;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Host $server_name;
proxy_set_header Accept-Language $http_accept_language;
proxy_set_header Host $host;
ws.conf - WebSocket Configuration
sendfile off;
tcp_nopush off;
proxy_buffering off;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
Docker Images
Default Image (Ubuntu Jammy)
FROM openresty/openresty:jammy
Packages installed:
- lua-resty-jwt
- lua-resty-string
- lua-cjson
- lua-resty-rsa
- lua-resty-dns
Alpine Image
FROM openresty/openresty:alpine
Additional tools:
- certbot (Let's Encrypt)
Build approach:
- Installs build dependencies, compiles LuaRocks packages, removes build tools
Environment Variables
| Variable | Required | Default | Description |
|---|---|---|---|
NODE_NAME |
No | - | Node identifier for /node endpoint |
JWT_DESKTOP_PAYLOAD_PRIVATE_KEY |
Yes | - | Path to RSA private key for payload decryption |
JWT_DESKTOP_SIGNING_PUBLIC_KEY |
Yes | - | Path to RSA public key for signature verification |
PYOS_SERVICE_PORT |
No | 8000 | PyOS backend port |
PYOS_FQDN |
No | pyos |
PyOS backend hostname |
NAMESERVER_RESOLVER |
No | From /etc/resolv.conf |
DNS resolver for nginx |
Security Considerations
- JWT Validation Chain
- Signature verification with RSA public key
- Payload decryption with RSA private key
-
Expiration validation via cache TTL
-
Information Disclosure Prevention
- Server version hidden
-
Error messages logged but minimal in response
-
Resource Limits
- Upload limits: 8MB (spawner), 8GB (filer)
- Cache TTL capped at 10 minutes
-
Shared dictionary sizes bounded
-
WebSocket Security
- Proper Upgrade/Connection headers
- Buffering disabled for real-time communication
Logging
- Access log:
/var/log/nginx/access.log - Error log:
/var/log/nginx/error.log - JWT errors: Logged at
ngx.ERRlevel - Cache hits: Logged at
ngx.NOTICElevel with TTL and target
Rebuild the container images
- from
openresty/openrestyalpine
git clone -b 4.4 https://github.com/abcdesktopio/route.git
cd route
REPO=abcdesdesktop
docker build -t $REPO/route:4.4 -f Dockerfile.alpine --build-arg BASE_IMAGE_RELEASE=alpine --build-arg BASE_IMAGE=openresty/openresty .