Language: English | Deutsch
vsTaskViewer ist eine Go-Anwendung, die vordefinierte Commands als Hintergrund-Tasks startet und deren Ausgabe (stdout/stderr) live über einen Web-Interface anzeigt.
Hinweis: Dieser Code wurde mit Unterstützung von LLM/AI-Tools erstellt.
- Task-Management: Startet vordefinierte Tasks als Hintergrundprozesse
- Parametrisierte Tasks: Tasks können mit typisierten Parametern (int/string) konfiguriert werden
- Web-Interface: Minimalistisches HTML-Interface zur Live-Anzeige der Task-Ausgabe
- WebSocket-Support: Live-Streaming von stdout und stderr über WebSocket
- JWT-Authentifizierung: Alle Requests müssen mit einem gültigen JWT-Token authentifiziert werden
- Max Execution Time: Automatische Beendigung von Tasks nach konfigurierbarer Zeit (SIGTERM → SIGKILL)
- Rate Limiting: Schutz vor Brute-Force und DoS-Angriffen
- Request Size Limits: Schutz vor zu großen Requests
- Optional TLS/HTTPS: Unterstützung für verschlüsselte Verbindungen
- Health Check:
/healthEndpunkt für Monitoring - Single Binary: Erstellt ein einzelnes Linux amd64 Binary
make buildOder manuell:
GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -o vsTaskViewerDie Konfigurationsdatei wird in folgender Reihenfolge gesucht:
- Pfad angegeben mit
-cFlag vsTaskViewer.tomlim gleichen Verzeichnis wie die Binary/etc/vsTaskViewer/vsTaskViewer.toml
Das Templates-Verzeichnis (HTML-Dateien) wird in folgender Reihenfolge gesucht:
- Pfad angegeben mit
-tFlag html/im gleichen Verzeichnis wie die Binary/etc/vsTaskViewer/html/
Das Task-Ausgabe-Verzeichnis wird in folgender Reihenfolge gesucht:
- Pfad angegeben mit
-dFlag task_diraus der Konfigurationsdatei/var/vsTaskViewer(Standard)
Der Ausführungsbenutzer (exec user) wird in folgender Reihenfolge gesucht:
- Benutzer angegeben mit
-uFlag exec_useraus der Konfigurationsdateiwww-data(Standard, UID 33)
Beispiel-Installation:
# Systemweite Installation
sudo mkdir -p /etc/vsTaskViewer/html
sudo cp example-config.toml /etc/vsTaskViewer/vsTaskViewer.toml
sudo cp -r html/* /etc/vsTaskViewer/html/
sudo nano /etc/vsTaskViewer/vsTaskViewer.toml
# Binary installieren
sudo cp vsTaskViewer /usr/local/bin/
sudo chmod +x /usr/local/bin/vsTaskViewer
# Task-Verzeichnis erstellen
sudo mkdir -p /var/vsTaskViewer
sudo chown www-data:www-data /var/vsTaskViewer
sudo chmod 700 /var/vsTaskViewerWichtig: Ändern Sie das auth.secret in der Konfiguration!
Ein systemd Service-File ist im Repository enthalten (vsTaskViewer.service):
# Service-File installieren
sudo cp vsTaskViewer.service /etc/systemd/system/
# Service aktivieren und starten
sudo systemctl daemon-reload
sudo systemctl enable vsTaskViewer
sudo systemctl start vsTaskViewer
# Status prüfen
sudo systemctl status vsTaskViewer
# Logs anzeigen
sudo journalctl -u vsTaskViewer -fSicherheitshinweise für den systemd Service:
- Der Service startet als
rootund reduziert automatisch die Rechte aufwww-data - Strikte Sicherheitseinstellungen sind aktiviert (ProtectSystem, NoNewPrivileges, etc.)
- Der Service benötigt
CAP_NET_BIND_SERVICEfür Ports < 1024 undCAP_CHOWNfür Verzeichnis-Erstellung - Nur
/var/vsTaskViewerist schreibbar, alle anderen Pfade sind read-only - PrivateTmp verhindert Zugriff auf temporäre Dateien anderer Prozesse
Die Konfigurationsdatei /etc/vsTaskViewer.toml hat folgende Struktur:
[server]
port = 8080
# Pfad zum HTML-Verzeichnis (muss existieren)
html_dir = "./html"
# Pfad zum Task-Ausgabe-Verzeichnis (Standard: /var/vsTaskViewer)
# Muss im Besitz des ausführenden Benutzers sein und Berechtigungen 700 haben
# task_dir = "/var/vsTaskViewer"
# Benutzer zum Ausführen (Standard: www-data)
# Muss existieren und wird nach dem Laden der TLS-Dateien gesetzt
# exec_user = "www-data"
# Rate Limiting: Requests pro Minute pro IP (0 = deaktiviert)
rate_limit_rpm = 60
# Maximale Request-Größe in Bytes (0 = Standard 10MB)
max_request_size = 10485760
# TLS-Konfiguration (optional, leer lassen um HTTPS zu deaktivieren)
# tls_key_file = "/etc/ssl/private/key.pem"
# tls_cert_file = "/etc/ssl/certs/fullchain.pem"
# Erlaubte Origins für WebSocket (leer = alle erlauben)
# allowed_origins = ["http://localhost:8080"]
[auth]
secret = "your-secret-key"
[[tasks]]
name = "task-name"
description = "Task description"
command = "command to execute"
# Maximum execution time in seconds (0 = no limit)
# If exceeded, SIGTERM is sent, then SIGKILL after 30 seconds
max_execution_time = 300
# Tasks können parametrisiert werden
# Parameter werden im Command mit {{param_name}} substituiert
[[tasks]]
name = "parameterized-task"
description = "Task mit Parametern"
command = "echo 'Processing {{filename}} with timeout {{timeout}}'"
max_execution_time = 300
# Parameter-Definitionen
[[tasks.parameters]]
name = "filename"
type = "string" # "int" oder "string"
optional = false # true = optional, false = erforderlich
[[tasks.parameters]]
name = "timeout"
type = "int"
optional = trueDas html_dir Verzeichnis muss folgende Dateien enthalten:
viewer.html- Haupt-Viewer-Seite (mit Template-Platzhaltern{{.TaskID}}und{{.WebSocketURL}})400.html- Bad Request Fehlerseite401.html- Unauthorized Fehlerseite404.html- Not Found Fehlerseite405.html- Method Not Allowed Fehlerseite500.html- Internal Server Error Fehlerseite
Alle HTML-Dateien enthalten inline CSS und JavaScript.
./vsTaskViewerVerfügbare Optionen:
# Hilfe anzeigen
./vsTaskViewer -h
# Mit spezifischer Config-Datei
./vsTaskViewer -c /path/to/config.toml
# Mit spezifischem Templates-Verzeichnis
./vsTaskViewer -t /path/to/html
# Mit spezifischem Task-Ausgabe-Verzeichnis
./vsTaskViewer -d /var/vsTaskViewer
# Mit spezifischem Ausführungsbenutzer
./vsTaskViewer -u www-data
# Mit spezifischem Port
./vsTaskViewer -p 9090
# Kombiniert
./vsTaskViewer -c /path/to/config.toml -t /path/to/html -d /var/vsTaskViewer -u www-data -p 90901. JWT-Token generieren
Erstellen Sie ein JWT-Token mit HS256. Wichtig: API-Tokens müssen einen body_sha1 Claim enthalten, der dem SHA1-Hash des normalisierten JSON-Request-Bodies entspricht.
// Beispiel in Go
import (
"crypto/sha1"
"encoding/hex"
"encoding/json"
"github.com/golang-jwt/jwt/v5"
"time"
)
// Request-Body für den Task-Start
requestBody := map[string]interface{}{
"task_name": "example-task",
// optional: "parameters": map[string]interface{}{...}
}
// JSON normalisieren (entfernt Whitespace-Unterschiede)
bodyJSON, _ := json.Marshal(requestBody)
// SHA1-Hash des normalisierten Bodies berechnen
hash := sha1.New()
hash.Write(bodyJSON)
bodySHA1 := hex.EncodeToString(hash.Sum(nil))
// JWT-Token mit body_sha1 Claim erstellen
claims := jwt.MapClaims{
"body_sha1": bodySHA1, // Erforderlich für API-Tokens
"exp": time.Now().Add(24 * time.Hour).Unix(),
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
tokenString, _ := token.SignedString([]byte("your-secret-key"))Hinweis: Der body_sha1 Claim bindet das Token an den spezifischen Request-Body und verhindert Manipulationen. Der JSON-Body wird vor dem Hashing normalisiert, sodass Formatierungsunterschiede (Whitespace, Zeilenumbrüche) den Hash nicht beeinflussen.
2. Task über API starten
Wichtig: Das JWT-Token muss einen body_sha1 Claim enthalten, der dem SHA1-Hash des normalisierten JSON-Request-Bodies entspricht. Siehe oben für ein Beispiel zur Token-Generierung.
Task ohne Parameter:
# Zuerst: Token mit body_sha1 für '{"task_name": "example-task"}' generieren
# Dann:
curl -X POST http://localhost:8080/api/start?token=YOUR_JWT_TOKEN \
-H "Content-Type: application/json" \
-d '{"task_name": "example-task"}'Task mit Parametern:
# Zuerst: Token mit body_sha1 für den Request-Body generieren
# Dann:
curl -X POST http://localhost:8080/api/start?token=YOUR_JWT_TOKEN \
-H "Content-Type: application/json" \
-d '{
"task_name": "parameterized-task",
"parameters": {
"filename": "data.txt",
"timeout": 30
}
}'Antwort:
{
"task_id": "uuid-here",
"viewer_url": "http://localhost:8080/viewer?task_id=uuid-here&token=viewer-token"
}3. Viewer öffnen
Öffnen Sie die viewer_url aus der Antwort im Browser. Die Seite zeigt live die stdout und stderr Ausgabe des Tasks.
Startet einen Task.
Query Parameter:
token: JWT-Token (HS256) mitbody_sha1Claim
Request Body:
{
"task_name": "task-name",
"parameters": {
"param1": "value1",
"param2": 42
}
}Request Body Felder:
task_name(erforderlich): Name des Tasks aus der Konfigurationparameters(optional): Map von Parameternamen zu Werten- String-Parameter:
"param": "value" - Integer-Parameter:
"param": 42oder"param": "42"
Token-Anforderungen:
Das JWT-Token muss einen body_sha1 Claim enthalten, der dem SHA1-Hash des normalisierten JSON-Request-Bodies entspricht. Der Server validiert, dass der Hash im Token mit dem tatsächlich gesendeten Request-Body übereinstimmt.
Response:
{
"task_id": "uuid",
"viewer_url": "http://..."
}Fehler:
400 Bad Request: Ungültige Parameter, fehlende erforderliche Parameter, ungültige Zeichen, ungültiges JSON-Format401 Unauthorized: Ungültiges oder fehlendes JWT-Token, Token-Audience-Mismatch, Request-Body-Hash stimmt nicht mit Token überein500 Internal Server Error: Task konnte nicht gestartet werden
Zeigt die HTML-Viewer-Seite.
Query Parameter:
task_id: Task-ID (UUID)token: JWT-Token für Viewer-Zugriff
WebSocket-Endpunkt für Live-Output.
Query Parameter:
task_id: Task-ID (UUID)token: JWT-Token
Nachrichten:
{
"type": "stdout",
"data": "output line\n"
}oder
{
"type": "stderr",
"data": "error line\n"
}Health-Check-Endpunkt für Monitoring (keine Authentifizierung erforderlich).
Response:
OK
Status Code: 200 OK
Alle Requests müssen ein JWT-Token im URL-Query-Parameter token enthalten.
Claims:
task_id(optional): Task-Kennungbody_sha1(erforderlich für API-Tokens): SHA1-Hash des normalisierten JSON-Request-Bodies (hex-kodiert)exp: Ablaufzeit (Unix Timestamp)aud(Audience): Token-Typ zur Verhinderung von Token-Reuse- API-Tokens: Kein
audClaim oder leereraudClaim - Viewer-Tokens:
aud="viewer"- können nur für Viewer/WebSocket-Endpunkte verwendet werden
- API-Tokens: Kein
Signatur:
- Algorithmus: HS256
- Secret: Aus der Konfiguration (
auth.secret)
Body-Hashing für API-Tokens:
API-Tokens müssen einen body_sha1 Claim enthalten, der dem SHA1-Hash des normalisierten JSON-Request-Bodies entspricht. Dies bietet folgende Sicherheitsvorteile:
- Integritätsschutz: Verhindert Manipulation des Request-Bodies nach Token-Generierung
- Token-Bindung: Bindet das Token an einen spezifischen Request
- Normalisierung: Der JSON-Body wird vor dem Hashing normalisiert (Whitespace, Zeilenumbrüche und Schlüsselreihenfolge werden ignoriert), sodass Formatierungsunterschiede den Hash nicht beeinflussen
Beispiel für Body-Hash-Berechnung:
- Erstellen Sie den JSON-Request-Body (z.B.
{"task_name": "example-task"}) - Normalisieren Sie den JSON (parsen und neu encodieren in kompakter Form)
- Berechnen Sie den SHA1-Hash des normalisierten JSON
- Kodieren Sie den Hash als Hex-String
- Fügen Sie den Hash als
body_sha1Claim zum JWT-Token hinzu
Sicherheit:
- Viewer-Tokens haben
aud="viewer"und können nicht für API-Requests verwendet werden - API-Tokens haben kein
audClaim und können nicht für Viewer/WebSocket-Endpunkte verwendet werden - API-Tokens müssen einen
body_sha1Claim enthalten, der dem Request-Body entspricht - Dies verhindert, dass Viewer-Tokens für neue API-Requests missbraucht werden und schützt vor Request-Body-Manipulation
Tasks werden so ausgeführt, dass ihre Ausgabe in einem konfigurierbaren Verzeichnis gespeichert wird (Standard: /var/vsTaskViewer/[task-id]/):
[task-dir]/[task-id]/stdout: Standard-Ausgabe[task-dir]/[task-id]/stderr: Fehler-Ausgabe[task-dir]/[task-id]/pid: Prozess-ID des laufenden Tasks[task-dir]/[task-id]/exitcode: Exit-Code nach Beendigung[task-dir]/[task-id]/run.sh: Wrapper-Script (wird automatisch erstellt)
Der WebSocket-Endpunkt liest diese Dateien kontinuierlich und sendet neue Zeilen an den Client.
Sicherheit:
- Die Verzeichnisse haben Berechtigungen
0700(nur Owner-Zugriff) für zusätzliche Sicherheit - Beim Start wird das Task-Ausgabe-Verzeichnis validiert:
- Das Verzeichnis muss existieren oder mit den Rechten des ausführenden Benutzers erstellt werden können
- Das Verzeichnis muss im Besitz des ausführenden Benutzers sein (UID/GID)
- Das Verzeichnis muss Berechtigungen
700haben - Bei Fehlern wird die Anwendung mit einer Fehlermeldung beendet
Jeder Task kann eine maximale Ausführungszeit (max_execution_time) in Sekunden definieren:
0= Kein Timeout (Task läuft unbegrenzt)> 0= Maximale Ausführungszeit in Sekunden
Timeout-Verhalten:
-
Wenn die maximale Ausführungszeit überschritten wird:
- Es wird
SIGTERMan den Prozess gesendet (graceful shutdown) - Eine Systemnachricht wird über WebSocket gesendet
- Es wird
-
Nach 30 Sekunden:
- Wenn der Prozess noch läuft, wird
SIGKILLgesendet (force kill) - Eine weitere Systemnachricht wird über WebSocket gesendet
- Wenn der Prozess noch läuft, wird
Beispiel:
[[tasks]]
name = "limited-task"
command = "long-running-script.sh"
max_execution_time = 300 # 5 MinutenSystemnachrichten im WebSocket:
{
"type": "timeout",
"data": "Process exceeded maximum execution time. Sending SIGTERM (graceful shutdown)...",
"pid": 12345
}Tasks können mit typisierten Parametern konfiguriert werden, die im Command substituiert werden.
Parameter werden in der Task-Konfiguration definiert:
[[tasks.parameters]]
name = "param_name"
type = "int" # oder "string"
optional = false # true = optional, false = erforderlich- int: Nur Ziffern 0-9 erlaubt
- string: Nur folgende Zeichen erlaubt:
-a-zA-Z0-9_:,.(Bindestrich, Buchstaben, Ziffern, Unterstrich, Doppelpunkt, Komma, Punkt)
Parameter werden im Command mit der Syntax {{param_name}} substituiert:
command = "echo 'Processing {{filename}} with timeout {{timeout}}'"- Erforderliche Parameter: Fehlen erforderliche Parameter, wird der Request mit
400 Bad Requestabgelehnt - Typ-Validierung: Parameter müssen dem definierten Typ entsprechen
- Zeichen-Validierung: Ungültige Zeichen führen zu
400 Bad Requestmit entsprechender Fehlermeldung - Unbekannte Parameter: Nicht definierte Parameter werden abgelehnt
- Sicherheit: Die strikte Validierung verhindert Command-Injection durch Parameter
Task mit erforderlichem String-Parameter:
[[tasks]]
name = "process-file"
command = "cat {{filepath}}"
[[tasks.parameters]]
name = "filepath"
type = "string"
optional = falseTask mit optionalen Parametern:
[[tasks]]
name = "custom-task"
command = "echo '{{message}}' && sleep {{duration}}"
[[tasks.parameters]]
name = "message"
type = "string"
optional = true
[[tasks.parameters]]
name = "duration"
type = "int"
optional = trueAPI-Aufruf:
curl -X POST http://localhost:8080/api/start?token=TOKEN \
-H "Content-Type: application/json" \
-d '{
"task_name": "process-file",
"parameters": {
"filepath": "/path/to/file.txt"
}
}'Bei fehlerhaften Parametern wird ein 400 Bad Request mit einer beschreibenden Fehlermeldung zurückgegeben:
Fehlende erforderliche Parameter:
{
"error": "parameter validation failed: required parameter 'filename' (type string) is missing"
}Ungültige Zeichen in int-Parameter:
{
"error": "parameter validation failed: parameter 'timeout' (type int) contains invalid characters. Only digits 0-9 are allowed, got: 30abc"
}Ungültige Zeichen in string-Parameter:
{
"error": "parameter validation failed: parameter 'filename' (type string) contains invalid characters. Only [-a-zA-Z0-9_:,.] are allowed, got: /path/to/file"
}Unbekannte Parameter:
{
"error": "parameter validation failed: unknown parameter 'unknown_param' provided (not defined in task configuration)"
}Falscher Typ:
{
"error": "parameter validation failed: parameter 'timeout' must be of type 'int', got string"
}- JWT-Authentifizierung: Alle Endpunkte (außer
/health) erfordern gültige JWT-Tokens - Body-Hashing: API-Tokens müssen einen
body_sha1Claim enthalten, der dem Request-Body entspricht - verhindert Request-Body-Manipulation - Vordefinierte Tasks: Nur in der Konfiguration definierte Tasks können gestartet werden
- Token-Validierung: Expiration, Signatur und Audience werden geprüft
- Parameter-Validierung: Strikte Typ- und Zeichen-Validierung verhindert Command-Injection
- Rate Limiting: Schutz vor Brute-Force und DoS-Angriffen
- Request Size Limits: Schutz vor zu großen Requests (Standard: 10MB)
- Command Escaping: Commands werden sicher escaped, um Injection zu verhindern
- Privilege Dropping: Die Anwendung läuft standardmäßig als
www-data(UID 33) nach dem Start - TLS-Dateien: TLS-Schlüssel und Zertifikate werden vor dem Dropping der Rechte geladen
Die Anwendung folgt einer spezifischen Startup-Reihenfolge für maximale Sicherheit:
- Konfiguration laden: Konfigurationsdatei wird geladen und Pfade werden aufgelöst
- TLS-Dateien laden: Wenn TLS konfiguriert ist, werden die Schlüssel- und Zertifikatsdateien vor dem Dropping der Rechte in den Speicher geladen (benötigt möglicherweise erhöhte Rechte)
- Rechte reduzieren: Die Anwendung wechselt zum konfigurierten Ausführungsbenutzer (
exec_user, Standard:www-data) - Validierung: Verzeichnisse werden als Ausführungsbenutzer validiert
- Server starten: HTTP/HTTPS-Server wird gestartet
Wichtig für Produktion:
- Starten Sie die Anwendung als
root, wenn TLS verwendet wird und die TLS-Dateien erhöhte Rechte benötigen - Die Anwendung reduziert automatisch die Rechte nach dem Laden der TLS-Dateien
- Wenn die Anwendung bereits als Zielbenutzer läuft (nicht root), wird kein Privilege Dropping durchgeführt
Wichtig für Produktion:
- Verwenden Sie ein starkes, zufälliges Secret in der Konfiguration
- Verwenden Sie HTTPS in Produktion
- Beschränken Sie den Zugriff auf die API (Firewall, Reverse Proxy)
- Überprüfen Sie alle Task-Definitionen und Parameter-Validierungen
- Verwenden Sie optionale Parameter nur wenn nötig
- Konfigurieren Sie eine Firewall, um den Zugriff auf den Port zu beschränken
- Verwenden Sie einen Reverse Proxy (z.B. nginx) für zusätzliche Sicherheit
- Überwachen Sie die Logs regelmäßig auf verdächtige Aktivitäten
- Begrenzen Sie die Anzahl der gleichzeitigen Tasks in der Konfiguration
- Verwenden Sie Rate Limiting (in der Konfiguration aktiviert)
github.com/BurntSushi/toml- TOML-Konfigurationgithub.com/golang-jwt/jwt/v5- JWT-Tokengithub.com/google/uuid- UUID-Generierunggithub.com/gorilla/websocket- WebSocket-Support
Siehe LICENSE-Datei.