feat: Remote-Execute für Linux/Bash – OS-basierte Tool-Descriptions und remote.sh Script
Remote-Verbindungen erkennen jetzt das OS des Clients und passen die Tool-Description dynamisch an (PowerShell für Windows, Bash für Linux). Neues remote.sh Script für Linux/macOS-Clients mit embedded Python WebSocket-Client. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
1035066a39
commit
53cee67bef
11 files changed
+381
-27
No files matched your search
@@ -53,8 +53,9 @@ public static class ChatEndpoint
|
||||
|
||||
var maxIterations = settings.GetInt("MaxIterations", 100);
|
||||
|
||||
var hasRemote = remoteMgr.GetBySessionId(sessionId) != null;
|
||||
var ctx = await chatService.PrepareContext(sessionId, userId, maxIterations, planMode: body.PlanMode, hasRemoteConnection: hasRemote);
|
||||
var remoteConn = remoteMgr.GetBySessionId(sessionId);
|
||||
var hasRemote = remoteConn != null;
|
||||
var ctx = await chatService.PrepareContext(sessionId, userId, maxIterations, planMode: body.PlanMode, hasRemoteConnection: hasRemote, remoteOs: remoteConn?.OsVersion);
|
||||
if (ctx == null)
|
||||
{
|
||||
res.StatusCode = 400;
|
||||
|
||||
@@ -29,9 +29,9 @@ public static class RemoteEndpoint
|
||||
app.MapPost("/api/remote/assign", AssignConnection);
|
||||
app.MapPost("/api/remote/unassign", UnassignConnection);
|
||||
|
||||
// PowerShell-Script ausliefern
|
||||
app.MapGet("/remote.ps1", (Delegate)ServeScript);
|
||||
app.MapGet("/remote", (Delegate)ServeScript);
|
||||
// Scripts ausliefern
|
||||
app.MapGet("/remote.ps1", (Delegate)((HttpContext ctx) => ServeScript(ctx, "remote.ps1")));
|
||||
app.MapGet("/remote.sh", (Delegate)((HttpContext ctx) => ServeScript(ctx, "remote.sh")));
|
||||
}
|
||||
|
||||
private static async Task HandleWebSocket(HttpContext ctx, RemoteConnectionManager mgr)
|
||||
@@ -239,9 +239,9 @@ public static class RemoteEndpoint
|
||||
return Results.Ok(new { ok = true });
|
||||
}
|
||||
|
||||
private static async Task<IResult> ServeScript(HttpContext ctx)
|
||||
private static async Task<IResult> ServeScript(HttpContext ctx, string filename)
|
||||
{
|
||||
var scriptPath = Path.Combine(AppPaths.WwwRoot, "remote.ps1");
|
||||
var scriptPath = Path.Combine(AppPaths.WwwRoot, filename);
|
||||
if (!File.Exists(scriptPath))
|
||||
return Results.NotFound("Script nicht gefunden");
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ public class AuthMiddleware
|
||||
var isAutologin = path == "/api/auth/autologin" && ctx.Request.Method == "GET";
|
||||
var isImageFile = path.StartsWith("/api/images/file/") && ctx.Request.Method == "GET";
|
||||
var isRemoteWs = path == "/api/remote";
|
||||
var isRemoteScript = path == "/remote.ps1";
|
||||
var isRemoteScript = path == "/remote.ps1" || path == "/remote.sh";
|
||||
var isInternal = path.StartsWith("/api/internal/");
|
||||
if (!path.StartsWith("/api/") || path == "/api/auth/login" || isTelegramWebhook || isSharedGet || isAutologin || isImageFile || isRemoteWs || isRemoteScript || isInternal)
|
||||
{
|
||||
|
||||
@@ -64,8 +64,10 @@ public partial class AnthropicService
|
||||
try
|
||||
{
|
||||
// MCP-Config generieren (immer – für ask_user_choice, optional Memory-Tools + remote_execute)
|
||||
var hasRemote = remoteMgr != null && !string.IsNullOrEmpty(sessionId) && remoteMgr.GetBySessionId(sessionId) != null;
|
||||
mcpConfigPath = GenerateMcpConfig(sessionId, agentId, hasRemote, unityMcpUrl, unityMcpToken);
|
||||
var remoteConn = remoteMgr != null && !string.IsNullOrEmpty(sessionId) ? remoteMgr.GetBySessionId(sessionId) : null;
|
||||
var hasRemote = remoteConn != null;
|
||||
var remoteOs = remoteConn?.OsVersion ?? "";
|
||||
mcpConfigPath = GenerateMcpConfig(sessionId, agentId, hasRemote, unityMcpUrl, unityMcpToken, remoteOs);
|
||||
|
||||
// Python-Verfügbarkeit prüfen und ggf. warnen
|
||||
if (FindPythonCommand() == null)
|
||||
@@ -567,7 +569,7 @@ public partial class AnthropicService
|
||||
/// Generiert eine temporäre MCP-Config-Datei für die Claude CLI.
|
||||
/// Enthält den MCP-Server für remote_execute.
|
||||
/// </summary>
|
||||
private string GenerateMcpConfig(string sessionId, string agentId = null, bool hasRemote = false, string unityMcpUrl = null, string unityMcpToken = null)
|
||||
private string GenerateMcpConfig(string sessionId, string agentId = null, bool hasRemote = false, string unityMcpUrl = null, string unityMcpToken = null, string remoteOs = null)
|
||||
{
|
||||
// Port aus Konfiguration oder Default
|
||||
var port = 19000;
|
||||
@@ -587,7 +589,8 @@ public partial class AnthropicService
|
||||
["GOTT_SESSION_ID"] = sessionId ?? "",
|
||||
["GOTT_AUTH_TOKEN"] = Endpoints.InternalEndpoint.InternalToken,
|
||||
["GOTT_AGENT_ID"] = agentId ?? "",
|
||||
["GOTT_HAS_REMOTE"] = hasRemote ? "true" : "false"
|
||||
["GOTT_HAS_REMOTE"] = hasRemote ? "true" : "false",
|
||||
["GOTT_REMOTE_OS"] = remoteOs ?? ""
|
||||
};
|
||||
|
||||
var pythonCmd = FindPythonCommand();
|
||||
|
||||
@@ -101,7 +101,8 @@ public partial class AnthropicService
|
||||
if (systemPrompt != null && systemPrompt.Count > 0)
|
||||
payload["system"] = systemPrompt;
|
||||
|
||||
var hasRemote = remoteMgr?.GetBySessionId(sessionId) != null;
|
||||
var remoteConn = remoteMgr?.GetBySessionId(sessionId);
|
||||
var hasRemote = remoteConn != null;
|
||||
AddToolsToPayload(payload, new ToolPayloadOptions
|
||||
{
|
||||
Workspace = workspace,
|
||||
@@ -113,6 +114,7 @@ public partial class AnthropicService
|
||||
AllowUserChoice = allowUserChoice,
|
||||
PlanMode = planMode,
|
||||
IncludeRemote = hasRemote,
|
||||
RemoteOs = remoteConn?.OsVersion,
|
||||
IncludeGitHubSearch = useGitHubSearch && _toolService.HasGitHubToken,
|
||||
IncludeSerperSearch = useSerperSearch && _toolService.HasSerperKey
|
||||
});
|
||||
|
||||
@@ -31,6 +31,7 @@ public partial class AnthropicService
|
||||
public bool AllowUserChoice { get; init; }
|
||||
public bool PlanMode { get; init; }
|
||||
public bool IncludeRemote { get; init; }
|
||||
public string RemoteOs { get; init; }
|
||||
public bool IncludeGitHubSearch { get; init; }
|
||||
public bool IncludeSerperSearch { get; init; }
|
||||
}
|
||||
@@ -337,7 +338,7 @@ public partial class AnthropicService
|
||||
var allTools = _toolService.GetToolDefinitions(new ToolDefinitionOptions
|
||||
{
|
||||
IncludeFileTools = hasWorkspace, IncludeMemory = includeMemory,
|
||||
IncludeUserChoice = opts.AllowUserChoice, IncludeRemote = opts.IncludeRemote,
|
||||
IncludeUserChoice = opts.AllowUserChoice, IncludeRemote = opts.IncludeRemote, RemoteOs = opts.RemoteOs,
|
||||
IncludeMemorySearch = includeMemorySearch, IncludeSendToSession = includeSendToSession,
|
||||
IncludeGitHubSearch = opts.IncludeGitHubSearch, IncludeSerperSearch = opts.IncludeSerperSearch
|
||||
});
|
||||
|
||||
@@ -51,7 +51,7 @@ public class ChatService
|
||||
/// <summary>
|
||||
/// Baut den vollständigen ChatContext auf: Agent, Model, SystemPrompt, Chain, Workspace etc.
|
||||
/// </summary>
|
||||
public async Task<ChatContext> PrepareContext(string sessionId, string userId, int maxIterations = 100, bool planMode = false, bool hasRemoteConnection = false)
|
||||
public async Task<ChatContext> PrepareContext(string sessionId, string userId, int maxIterations = 100, bool planMode = false, bool hasRemoteConnection = false, string remoteOs = null)
|
||||
{
|
||||
var session = await _db.GetSession(sessionId);
|
||||
if (session == null || (userId != null && session.UserId != userId) || string.IsNullOrEmpty(session.AgentId))
|
||||
@@ -89,7 +89,7 @@ public class ChatService
|
||||
? _toolService.GetToolDefinitions(new ToolDefinitionOptions
|
||||
{
|
||||
IncludeFileTools = hasWorkspace, IncludeMemory = includeMemory,
|
||||
IncludeUserChoice = agent.AllowUserChoice, IncludeRemote = hasRemoteConnection,
|
||||
IncludeUserChoice = agent.AllowUserChoice, IncludeRemote = hasRemoteConnection, RemoteOs = remoteOs,
|
||||
IncludeGitHubSearch = includeGitHubSearch, IncludeSerperSearch = includeSerperSearch
|
||||
})
|
||||
: Array.Empty<object>();
|
||||
|
||||
+13
-4
@@ -21,6 +21,7 @@ public class ToolDefinitionOptions
|
||||
public bool IncludeMemory { get; set; }
|
||||
public bool IncludeUserChoice { get; set; } = true;
|
||||
public bool IncludeRemote { get; set; }
|
||||
public string RemoteOs { get; set; }
|
||||
public bool IncludeMemorySearch { get; set; }
|
||||
public bool IncludeSendToSession { get; set; }
|
||||
public bool IncludeGitHubSearch { get; set; }
|
||||
@@ -106,7 +107,7 @@ public class ToolService
|
||||
if (opts.IncludeMemorySearch)
|
||||
allTools.Add(GetMemorySearchTool());
|
||||
if (opts.IncludeRemote)
|
||||
allTools.Add(GetRemoteExecuteTool());
|
||||
allTools.Add(GetRemoteExecuteTool(opts.RemoteOs));
|
||||
if (opts.IncludeSendToSession)
|
||||
allTools.Add(GetSendToSessionTool());
|
||||
if (opts.IncludeGitHubSearch)
|
||||
@@ -352,18 +353,26 @@ public class ToolService
|
||||
};
|
||||
}
|
||||
|
||||
private object GetRemoteExecuteTool()
|
||||
private object GetRemoteExecuteTool(string remoteOs = null)
|
||||
{
|
||||
var isLinux = (remoteOs ?? "").Contains("Linux", StringComparison.OrdinalIgnoreCase);
|
||||
var desc = isLinux
|
||||
? "Führt einen Bash-Befehl auf der verbundenen Remote-Linux-Workstation aus. Gibt stdout und stderr zurück. Nutze dies für Diagnose und Administration des Remote-PCs (z.B. Netzwerk, Services, Logs, Pakete). Timeout: 180 Sekunden."
|
||||
: "Führt einen PowerShell-Befehl auf der verbundenen Remote-Windows-Workstation aus. Gibt stdout und stderr zurück. Nutze dies für Diagnose und Administration des Remote-PCs (z.B. Druckerprobleme, Netzwerk, Services, Registry). Timeout: 180 Sekunden.";
|
||||
var cmdDesc = isLinux
|
||||
? "Bash-Befehl zum Ausführen auf dem Remote-PC"
|
||||
: "PowerShell-Befehl zum Ausführen auf dem Remote-PC";
|
||||
|
||||
return new
|
||||
{
|
||||
name = "remote_execute",
|
||||
description = "Führt einen PowerShell-Befehl auf der verbundenen Remote-Windows-Workstation aus. Gibt stdout und stderr zurück. Nutze dies für Diagnose und Administration des Remote-PCs (z.B. Druckerprobleme, Netzwerk, Services, Registry). Timeout: 180 Sekunden.",
|
||||
description = desc,
|
||||
input_schema = new
|
||||
{
|
||||
type = "object",
|
||||
properties = new Dictionary<string, object>
|
||||
{
|
||||
["command"] = new { type = "string", description = "PowerShell-Befehl zum Ausführen auf dem Remote-PC" }
|
||||
["command"] = new { type = "string", description = cmdDesc }
|
||||
},
|
||||
required = new[] { "command" }
|
||||
}
|
||||
|
||||
@@ -21,22 +21,34 @@ SESSION_ID = os.environ.get("GOTT_SESSION_ID", "")
|
||||
AUTH_TOKEN = os.environ.get("GOTT_AUTH_TOKEN", "")
|
||||
AGENT_ID = os.environ.get("GOTT_AGENT_ID", "")
|
||||
HAS_REMOTE = os.environ.get("GOTT_HAS_REMOTE", "false") == "true"
|
||||
REMOTE_OS = os.environ.get("GOTT_REMOTE_OS", "").lower() # z.B. "windows", "linux"
|
||||
|
||||
TOOLS = []
|
||||
|
||||
# Remote-Execute Tool (nur wenn Remote-Verbindung vorhanden)
|
||||
if HAS_REMOTE:
|
||||
TOOLS.append({
|
||||
"name": "remote_execute",
|
||||
"description": (
|
||||
if "linux" in REMOTE_OS:
|
||||
_desc = (
|
||||
"Führt einen Bash-Befehl auf der verbundenen Remote-Linux-Workstation aus. "
|
||||
"Gibt stdout und stderr zurück. Nutze dies für Diagnose und Administration des Remote-PCs "
|
||||
"(z.B. Netzwerk, Services, Logs, Pakete). Timeout: 180 Sekunden."
|
||||
)
|
||||
_cmd_desc = "Bash-Befehl zum Ausführen auf dem Remote-PC"
|
||||
else:
|
||||
_desc = (
|
||||
"Führt einen PowerShell-Befehl auf der verbundenen Remote-Windows-Workstation aus. "
|
||||
"Gibt stdout und stderr zurück. Nutze dies für Diagnose und Administration des Remote-PCs "
|
||||
"(z.B. Druckerprobleme, Netzwerk, Services, Registry). Timeout: 180 Sekunden."
|
||||
),
|
||||
)
|
||||
_cmd_desc = "PowerShell-Befehl zum Ausführen auf dem Remote-PC"
|
||||
|
||||
TOOLS.append({
|
||||
"name": "remote_execute",
|
||||
"description": _desc,
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"command": {"type": "string", "description": "PowerShell-Befehl zum Ausführen auf dem Remote-PC"}
|
||||
"command": {"type": "string", "description": _cmd_desc}
|
||||
},
|
||||
"required": ["command"]
|
||||
}
|
||||
|
||||
@@ -324,8 +324,10 @@ async function showRemoteModal(sessionId) {
|
||||
|
||||
var empty = document.createElement('div');
|
||||
empty.className = 'sp-loading';
|
||||
empty.innerHTML = '<div>Powershell-Befehl unter Windows ausführen</div>' +
|
||||
'<code>irm ' + esc(location.origin) + '/remote | iex</code>';
|
||||
empty.innerHTML = '<div>Windows (PowerShell)</div>' +
|
||||
'<code>irm ' + esc(location.origin) + '/remote.ps1 | iex</code>' +
|
||||
'<div style="margin-top:8px">Linux (Bash)</div>' +
|
||||
'<code>curl -s ' + esc(location.origin) + '/remote.sh | bash</code>';
|
||||
body.appendChild(empty);
|
||||
|
||||
if (!connections || connections.length === 0) {
|
||||
|
||||
@@ -0,0 +1,324 @@
|
||||
#!/usr/bin/env bash
|
||||
# GOTT Remote Agent - Linux Bash Client
|
||||
# Verbindet sich per WebSocket zum GOTT-Server und fuehrt Befehle aus.
|
||||
#
|
||||
# Verwendung:
|
||||
# curl -s https://gott.example.com/remote.sh | bash
|
||||
# oder:
|
||||
# chmod +x remote.sh && ./remote.sh
|
||||
# oder mit eigener URL:
|
||||
# WS_URL="wss://example.com/api/remote" ./remote.sh
|
||||
|
||||
WS_URL="${WS_URL:-{{WS_URL}}}"
|
||||
|
||||
# Python finden
|
||||
PYTHON=""
|
||||
for cmd in python3 python py; do
|
||||
if command -v "$cmd" &>/dev/null; then
|
||||
PYTHON="$cmd"
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -z "$PYTHON" ]; then
|
||||
echo ""
|
||||
echo " [FEHLER] Python nicht gefunden!"
|
||||
echo " Bitte installiere Python 3: sudo apt install python3"
|
||||
echo ""
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Python-Script inline ausfuehren
|
||||
exec "$PYTHON" - "$WS_URL" << 'PYTHON_SCRIPT'
|
||||
import sys
|
||||
import os
|
||||
import json
|
||||
import subprocess
|
||||
import signal
|
||||
import socket
|
||||
import ssl
|
||||
import hashlib
|
||||
import base64
|
||||
import struct
|
||||
import time
|
||||
from urllib.parse import urlparse
|
||||
|
||||
WS_URL = sys.argv[1] if len(sys.argv) > 1 else "ws://localhost/api/remote"
|
||||
|
||||
CYAN = "\033[96m"
|
||||
GREEN = "\033[92m"
|
||||
YELLOW = "\033[93m"
|
||||
RED = "\033[91m"
|
||||
GRAY = "\033[90m"
|
||||
WHITE = "\033[97m"
|
||||
RESET = "\033[0m"
|
||||
|
||||
def cprint(msg, color=""):
|
||||
print(f"{color}{msg}{RESET}", flush=True)
|
||||
|
||||
class WebSocketClient:
|
||||
"""Minimaler WebSocket-Client (RFC 6455) ohne externe Abhaengigkeiten."""
|
||||
|
||||
def __init__(self, url):
|
||||
self.url = url
|
||||
self.sock = None
|
||||
self.closed = False
|
||||
|
||||
def connect(self):
|
||||
parsed = urlparse(self.url)
|
||||
use_ssl = parsed.scheme in ("wss", "https")
|
||||
host = parsed.hostname
|
||||
port = parsed.port or (443 if use_ssl else 80)
|
||||
path = parsed.path or "/"
|
||||
if parsed.query:
|
||||
path += "?" + parsed.query
|
||||
|
||||
self.sock = socket.create_connection((host, port), timeout=10)
|
||||
if use_ssl:
|
||||
ctx = ssl.create_default_context()
|
||||
self.sock = ctx.wrap_socket(self.sock, server_hostname=host)
|
||||
|
||||
# WebSocket-Handshake
|
||||
key = base64.b64encode(os.urandom(16)).decode()
|
||||
handshake = (
|
||||
f"GET {path} HTTP/1.1\r\n"
|
||||
f"Host: {host}\r\n"
|
||||
f"Upgrade: websocket\r\n"
|
||||
f"Connection: Upgrade\r\n"
|
||||
f"Sec-WebSocket-Key: {key}\r\n"
|
||||
f"Sec-WebSocket-Version: 13\r\n"
|
||||
f"\r\n"
|
||||
)
|
||||
self.sock.sendall(handshake.encode())
|
||||
|
||||
# Response lesen
|
||||
response = b""
|
||||
while b"\r\n\r\n" not in response:
|
||||
chunk = self.sock.recv(4096)
|
||||
if not chunk:
|
||||
raise ConnectionError("Handshake fehlgeschlagen")
|
||||
response += chunk
|
||||
|
||||
status_line = response.split(b"\r\n")[0].decode()
|
||||
if "101" not in status_line:
|
||||
raise ConnectionError(f"WebSocket-Handshake fehlgeschlagen: {status_line}")
|
||||
|
||||
self.sock.settimeout(None)
|
||||
|
||||
def send(self, data):
|
||||
if self.closed:
|
||||
return
|
||||
payload = data.encode("utf-8")
|
||||
frame = bytearray()
|
||||
frame.append(0x81) # FIN + Text
|
||||
|
||||
length = len(payload)
|
||||
if length < 126:
|
||||
frame.append(0x80 | length) # Mask-Bit gesetzt
|
||||
elif length < 65536:
|
||||
frame.append(0x80 | 126)
|
||||
frame.extend(struct.pack(">H", length))
|
||||
else:
|
||||
frame.append(0x80 | 127)
|
||||
frame.extend(struct.pack(">Q", length))
|
||||
|
||||
# Masking Key (Client muss maskieren)
|
||||
mask = os.urandom(4)
|
||||
frame.extend(mask)
|
||||
masked = bytearray(b ^ mask[i % 4] for i, b in enumerate(payload))
|
||||
frame.extend(masked)
|
||||
|
||||
self.sock.sendall(frame)
|
||||
|
||||
def recv(self):
|
||||
if self.closed:
|
||||
return None
|
||||
|
||||
def read_exact(n):
|
||||
data = b""
|
||||
while len(data) < n:
|
||||
chunk = self.sock.recv(n - len(data))
|
||||
if not chunk:
|
||||
raise ConnectionError("Verbindung geschlossen")
|
||||
data += chunk
|
||||
return data
|
||||
|
||||
header = read_exact(2)
|
||||
opcode = header[0] & 0x0F
|
||||
is_masked = header[1] & 0x80
|
||||
length = header[1] & 0x7F
|
||||
|
||||
if opcode == 0x8: # Close
|
||||
self.closed = True
|
||||
return None
|
||||
if opcode == 0x9: # Ping
|
||||
self.send_pong()
|
||||
return self.recv()
|
||||
|
||||
if length == 126:
|
||||
length = struct.unpack(">H", read_exact(2))[0]
|
||||
elif length == 127:
|
||||
length = struct.unpack(">Q", read_exact(8))[0]
|
||||
|
||||
if is_masked:
|
||||
mask = read_exact(4)
|
||||
data = read_exact(length)
|
||||
data = bytearray(b ^ mask[i % 4] for i, b in enumerate(data))
|
||||
else:
|
||||
data = read_exact(length)
|
||||
|
||||
return data.decode("utf-8")
|
||||
|
||||
def send_pong(self):
|
||||
if self.closed:
|
||||
return
|
||||
frame = bytearray([0x8A, 0x80]) # FIN + Pong, Mask-Bit, Length 0
|
||||
frame.extend(os.urandom(4)) # Mask key
|
||||
self.sock.sendall(frame)
|
||||
|
||||
def close(self):
|
||||
if self.closed:
|
||||
return
|
||||
self.closed = True
|
||||
try:
|
||||
# Close-Frame senden
|
||||
frame = bytearray([0x88, 0x82]) # FIN + Close, Mask-Bit, Length 2
|
||||
mask = os.urandom(4)
|
||||
frame.extend(mask)
|
||||
# Status 1000 (Normal Closure) maskiert
|
||||
code = struct.pack(">H", 1000)
|
||||
frame.extend(bytearray(b ^ mask[i % 4] for i, b in enumerate(code)))
|
||||
self.sock.sendall(frame)
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
self.sock.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def main():
|
||||
print()
|
||||
cprint(" ============================", CYAN)
|
||||
cprint(" GOTT Remote Agent", CYAN)
|
||||
cprint(" ============================", CYAN)
|
||||
print()
|
||||
cprint(f" Verbinde zu: {WS_URL}", GRAY)
|
||||
print()
|
||||
|
||||
ws = WebSocketClient(WS_URL)
|
||||
running = True
|
||||
|
||||
def on_signal(sig, frame):
|
||||
nonlocal running
|
||||
running = False
|
||||
|
||||
signal.signal(signal.SIGINT, on_signal)
|
||||
signal.signal(signal.SIGTERM, on_signal)
|
||||
|
||||
try:
|
||||
ws.connect()
|
||||
except Exception as e:
|
||||
cprint(f" [FEHLER] Verbindung fehlgeschlagen: {e}", RED)
|
||||
cprint(f" URL: {WS_URL}", GRAY)
|
||||
return
|
||||
|
||||
cprint(" [OK] Verbunden!", GREEN)
|
||||
|
||||
# Hello-Message senden
|
||||
hostname = os.uname().nodename if hasattr(os, "uname") else socket.gethostname()
|
||||
username = os.environ.get("USER", os.environ.get("LOGNAME", "unknown"))
|
||||
import platform
|
||||
os_version = f"{platform.system()} {platform.release()}"
|
||||
|
||||
hello = json.dumps({
|
||||
"type": "hello",
|
||||
"hostname": hostname,
|
||||
"username": username,
|
||||
"os": os_version
|
||||
})
|
||||
ws.send(hello)
|
||||
|
||||
cprint(f" Host: {username}@{hostname} ({os_version})", GRAY)
|
||||
print()
|
||||
cprint(" Warte auf Befehle... (Strg+C zum Beenden)", YELLOW)
|
||||
print()
|
||||
|
||||
while running and not ws.closed:
|
||||
try:
|
||||
raw = ws.recv()
|
||||
except (ConnectionError, OSError):
|
||||
cprint(" Verbindung verloren.", RED)
|
||||
break
|
||||
|
||||
if raw is None:
|
||||
cprint(" Server hat Verbindung geschlossen.", YELLOW)
|
||||
break
|
||||
|
||||
try:
|
||||
msg = json.loads(raw)
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
|
||||
msg_type = msg.get("type", "")
|
||||
|
||||
if msg_type == "execute":
|
||||
command = msg.get("command", "")
|
||||
timestamp = time.strftime("%H:%M:%S")
|
||||
print(f" {CYAN}[{timestamp}] Ausfuehren: {WHITE}{command}{RESET}", flush=True)
|
||||
|
||||
exit_code = 0
|
||||
stdout = ""
|
||||
stderr = ""
|
||||
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["bash", "-c", command],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=300
|
||||
)
|
||||
stdout = result.stdout
|
||||
stderr = result.stderr
|
||||
exit_code = result.returncode
|
||||
except subprocess.TimeoutExpired:
|
||||
stderr = "Timeout nach 300 Sekunden"
|
||||
exit_code = 124
|
||||
except Exception as e:
|
||||
stderr = str(e)
|
||||
exit_code = 1
|
||||
|
||||
response = json.dumps({
|
||||
"type": "result",
|
||||
"exitCode": exit_code,
|
||||
"output": stdout or "",
|
||||
"error": stderr or ""
|
||||
})
|
||||
ws.send(response)
|
||||
|
||||
# Kurzausgabe lokal
|
||||
if stdout:
|
||||
preview = stdout[:200] + "..." if len(stdout) > 200 else stdout.rstrip()
|
||||
cprint(f" -> {preview}", GRAY)
|
||||
if stderr:
|
||||
cprint(f" [stderr] {stderr.rstrip()}", RED)
|
||||
color = GREEN if exit_code == 0 else RED
|
||||
cprint(f" [Exit: {exit_code}]", color)
|
||||
print()
|
||||
|
||||
elif msg_type == "ping":
|
||||
ws.send('{"type":"pong"}')
|
||||
|
||||
elif msg_type == "pong":
|
||||
pass
|
||||
|
||||
else:
|
||||
cprint(f" Unbekannte Nachricht: {msg_type}", YELLOW)
|
||||
|
||||
ws.close()
|
||||
print()
|
||||
cprint(" Verbindung beendet.", YELLOW)
|
||||
|
||||
|
||||
main()
|
||||
PYTHON_SCRIPT
|
||||
Reference in new issue
Block a user