diff --git a/deployment/Caddyfile b/deployment/Caddyfile index c13008a..b928718 100644 --- a/deployment/Caddyfile +++ b/deployment/Caddyfile @@ -1,6 +1,7 @@ { - # Disable automatic HTTPS for local development + # No TLS is terminated here, so disable HTTPS redirects/certs and the admin API auto_https off + admin off } :8080 { @@ -13,6 +14,10 @@ # HTTP API endpoints (preserve /api/* path) handle /api/* { + # Reject oversized uploads before they reach the backend + request_body { + max_size {$MAX_UPLOAD_SIZE:100MB} + } reverse_proxy 127.0.0.1:8082 } diff --git a/deployment/Dockerfile b/deployment/Dockerfile index db83d95..fd79693 100644 --- a/deployment/Dockerfile +++ b/deployment/Dockerfile @@ -112,12 +112,12 @@ RUN groupadd -r webssh && useradd -r -g webssh webssh WORKDIR /app/src -# Copy application and static files -COPY src/ /app/src/ +# Copy application and static files (owned by webssh in one layer, no separate chown pass) +COPY --chown=webssh:webssh src/ /app/src/ COPY deployment/docker-entrypoint.sh /usr/local/bin/ # Create .ssh directory for host keys -RUN mkdir -p /app/src/.ssh && chown -R webssh:webssh /app/src +RUN mkdir -p /app/src/.ssh && chown webssh:webssh /app/src/.ssh # Copy entrypoint RUN chmod +x /usr/local/bin/docker-entrypoint.sh @@ -137,7 +137,7 @@ EXPOSE 8080 8081 8082 8083 # Health check via Caddy (serving root) using Python (no curl required) HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ - CMD python -c "import sys,urllib.request as u;\ntry:\n r=u.urlopen('http://127.0.0.1:8080/', timeout=5);\n code=getattr(r,'status',None) or r.getcode();\n sys.exit(0 if code==200 else 1)\nexcept Exception:\n sys.exit(1)" + CMD python -c "import sys, urllib.request; sys.exit(0 if urllib.request.urlopen('http://127.0.0.1:8080/', timeout=5).getcode() == 200 else 1)" # Use shell form to see entrypoint errors ENTRYPOINT /usr/local/bin/docker-entrypoint.sh \ No newline at end of file diff --git a/deployment/docker-compose.yml b/deployment/docker-compose.yml index 7daa10d..a5c47a4 100644 --- a/deployment/docker-compose.yml +++ b/deployment/docker-compose.yml @@ -14,8 +14,8 @@ services: - LOG_LEVEL=INFO # Configure users: format is username:password,username2:password2 - WEBSSH_USERS=admin:admin - # Max upload size (supports format like "100M" for 100MB or "50M" for 50MB) - - MAX_UPLOAD_SIZE=30M + # Max upload size, enforced by both Caddy and the backend (e.g. "30MB", "512KB") + - MAX_UPLOAD_SIZE=30MB # Session Recording Configuration - RECORDING_ENABLED=true - RECORDING_RETENTION_DAYS=30 diff --git a/deployment/docker-entrypoint.sh b/deployment/docker-entrypoint.sh index bdc292c..5cfa28d 100644 --- a/deployment/docker-entrypoint.sh +++ b/deployment/docker-entrypoint.sh @@ -1,47 +1,88 @@ -#!/bin/sh +#!/bin/bash +set -e set -x -# Check if running as root -if [ "$(id -u)" != "0" ]; then - echo "Not running as root, starting application directly..." - exec python /app/src/run.py +CADDY_PID="" +APP_PID="" + +# Tear down both processes together so a crash of one doesn't leave the +# container running half-broken (and eventually silently unhealthy). +cleanup() { + trap - TERM INT + [ -n "$CADDY_PID" ] && kill -TERM "$CADDY_PID" 2>/dev/null || true + [ -n "$APP_PID" ] && kill -TERM "$APP_PID" 2>/dev/null || true + wait 2>/dev/null || true +} +trap cleanup TERM INT + +RUN_AS_ROOT=0 +if [ "$(id -u)" = "0" ]; then + RUN_AS_ROOT=1 fi -# Create data directory with correct permissions for the WebSSH user -if [ ! -d "/data" ]; then - echo "Creating /data directory..." - mkdir -p /data +if [ "$RUN_AS_ROOT" = "1" ]; then + # Create data directory with correct permissions for the WebSSH user + if [ ! -d "/data" ]; then + echo "Creating /data directory..." + mkdir -p /data + fi + + echo "Current /data permissions:" + ls -la /data || true + + # Ensure /data is writable and owned by the WebSSH user + echo "Setting permissions on /data..." + chmod 755 /data + chown webssh:webssh /data + + if [ "$(stat -c %U /data)" != "webssh" ]; then + echo "WARNING: Could not change ownership of /data to webssh" + echo "Current owner: $(stat -c %U /data)" + echo "This may happen with bind mounts. Ensure host directory is writable." + fi + + echo "Final /data permissions:" + ls -la /data || true +else + echo "Not running as root (uid=$(id -u)); skipping /data permission setup." fi -echo "Current /data permissions:" -ls -la /data || true -ls -la / | grep data || true - -# Ensure /data is writable and owned by the WebSSH user (UID 999) -echo "Setting permissions on /data..." -chmod 755 /data -chown webssh:webssh /data - -# Check if chown worked -if [ "$(stat -c %U /data)" != "webssh" ]; then - echo "WARNING: Could not change ownership of /data to webssh" - echo "Current owner: $(stat -c %U /data)" - echo "This may happen with bind mounts. Ensure host directory is writable." +# Caddy (run as a non-root user below) needs a writable home for its autosave +# state and, if TLS is ever enabled, ACME certificates/keys. +export XDG_DATA_HOME="${XDG_DATA_HOME:-/tmp/caddy-data}" +export XDG_CONFIG_HOME="${XDG_CONFIG_HOME:-/tmp/caddy-config}" +mkdir -p "$XDG_DATA_HOME" "$XDG_CONFIG_HOME" +if [ "$RUN_AS_ROOT" = "1" ]; then + chown -R webssh:webssh "$XDG_DATA_HOME" "$XDG_CONFIG_HOME" fi -echo "Final /data permissions:" -ls -la /data || true - -# Configure nginx max upload size from environment -MAX_UPLOAD_SIZE="${MAX_UPLOAD_SIZE:-100M}" - -# Start Caddy in background (serves static files and reverse-proxies to local services) +# Start Caddy (serves static files and reverse-proxies to local services) as +# the unprivileged webssh user; it handles untrusted network input and must +# not run as root. if command -v caddy >/dev/null 2>&1; then echo "Starting Caddy..." - caddy run --config /etc/caddy/Caddyfile & + if [ "$RUN_AS_ROOT" = "1" ]; then + gosu webssh caddy run --config /etc/caddy/Caddyfile & + else + caddy run --config /etc/caddy/Caddyfile & + fi + CADDY_PID=$! else echo "Caddy binary not found; continuing without reverse proxy" fi -# Start the application (run as the WebSSH user) -exec gosu webssh python /app/src/run.py +# Start the application, also dropping privileges when possible +if [ "$RUN_AS_ROOT" = "1" ]; then + gosu webssh python /app/src/run.py & +else + python /app/src/run.py & +fi +APP_PID=$! + +# If either supervised process exits, shut the container down instead of +# silently continuing in a half-working (but "healthy") state. +wait -n +EXIT_CODE=$? +echo "A supervised process exited with code $EXIT_CODE; shutting down" +cleanup +exit "$EXIT_CODE" diff --git a/src/static/webssh.css b/src/static/webssh.css index 231a2d8..d8ac2ed 100644 --- a/src/static/webssh.css +++ b/src/static/webssh.css @@ -111,8 +111,9 @@ body { .connection-heading { display: flex; - align-items: baseline; - gap: 8px; + flex-direction: column; /* Добавено: подрежда ги вертикално */ + align-items: flex-start; /* Променено: подравнява ги вляво */ + gap: 2px; /* Намалено, за да са по-близо едно до друго вертикално */ min-width: 0; margin-bottom: 4px; }