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 maxIterations = settings.GetInt("MaxIterations", 100);
|
||||||
|
|
||||||
var hasRemote = remoteMgr.GetBySessionId(sessionId) != null;
|
var remoteConn = remoteMgr.GetBySessionId(sessionId);
|
||||||
var ctx = await chatService.PrepareContext(sessionId, userId, maxIterations, planMode: body.PlanMode, hasRemoteConnection: hasRemote);
|
var hasRemote = remoteConn != null;
|
||||||
|
var ctx = await chatService.PrepareContext(sessionId, userId, maxIterations, planMode: body.PlanMode, hasRemoteConnection: hasRemote, remoteOs: remoteConn?.OsVersion);
|
||||||
if (ctx == null)
|
if (ctx == null)
|
||||||
{
|
{
|
||||||
res.StatusCode = 400;
|
res.StatusCode = 400;
|
||||||
|
|||||||
@@ -29,9 +29,9 @@ public static class RemoteEndpoint
|
|||||||
app.MapPost("/api/remote/assign", AssignConnection);
|
app.MapPost("/api/remote/assign", AssignConnection);
|
||||||
app.MapPost("/api/remote/unassign", UnassignConnection);
|
app.MapPost("/api/remote/unassign", UnassignConnection);
|
||||||
|
|
||||||
// PowerShell-Script ausliefern
|
// Scripts ausliefern
|
||||||
app.MapGet("/remote.ps1", (Delegate)ServeScript);
|
app.MapGet("/remote.ps1", (Delegate)((HttpContext ctx) => ServeScript(ctx, "remote.ps1")));
|
||||||
app.MapGet("/remote", (Delegate)ServeScript);
|
app.MapGet("/remote.sh", (Delegate)((HttpContext ctx) => ServeScript(ctx, "remote.sh")));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task HandleWebSocket(HttpContext ctx, RemoteConnectionManager mgr)
|
private static async Task HandleWebSocket(HttpContext ctx, RemoteConnectionManager mgr)
|
||||||
@@ -239,9 +239,9 @@ public static class RemoteEndpoint
|
|||||||
return Results.Ok(new { ok = true });
|
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))
|
if (!File.Exists(scriptPath))
|
||||||
return Results.NotFound("Script nicht gefunden");
|
return Results.NotFound("Script nicht gefunden");
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ public class AuthMiddleware
|
|||||||
var isAutologin = path == "/api/auth/autologin" && ctx.Request.Method == "GET";
|
var isAutologin = path == "/api/auth/autologin" && ctx.Request.Method == "GET";
|
||||||
var isImageFile = path.StartsWith("/api/images/file/") && ctx.Request.Method == "GET";
|
var isImageFile = path.StartsWith("/api/images/file/") && ctx.Request.Method == "GET";
|
||||||
var isRemoteWs = path == "/api/remote";
|
var isRemoteWs = path == "/api/remote";
|
||||||
var isRemoteScript = path == "/remote.ps1";
|
var isRemoteScript = path == "/remote.ps1" || path == "/remote.sh";
|
||||||
var isInternal = path.StartsWith("/api/internal/");
|
var isInternal = path.StartsWith("/api/internal/");
|
||||||
if (!path.StartsWith("/api/") || path == "/api/auth/login" || isTelegramWebhook || isSharedGet || isAutologin || isImageFile || isRemoteWs || isRemoteScript || isInternal)
|
if (!path.StartsWith("/api/") || path == "/api/auth/login" || isTelegramWebhook || isSharedGet || isAutologin || isImageFile || isRemoteWs || isRemoteScript || isInternal)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -64,8 +64,10 @@ public partial class AnthropicService
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
// MCP-Config generieren (immer – für ask_user_choice, optional Memory-Tools + remote_execute)
|
// 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;
|
var remoteConn = remoteMgr != null && !string.IsNullOrEmpty(sessionId) ? remoteMgr.GetBySessionId(sessionId) : null;
|
||||||
mcpConfigPath = GenerateMcpConfig(sessionId, agentId, hasRemote, unityMcpUrl, unityMcpToken);
|
var hasRemote = remoteConn != null;
|
||||||
|
var remoteOs = remoteConn?.OsVersion ?? "";
|
||||||
|
mcpConfigPath = GenerateMcpConfig(sessionId, agentId, hasRemote, unityMcpUrl, unityMcpToken, remoteOs);
|
||||||
|
|
||||||
// Python-Verfügbarkeit prüfen und ggf. warnen
|
// Python-Verfügbarkeit prüfen und ggf. warnen
|
||||||
if (FindPythonCommand() == null)
|
if (FindPythonCommand() == null)
|
||||||
@@ -567,7 +569,7 @@ public partial class AnthropicService
|
|||||||
/// Generiert eine temporäre MCP-Config-Datei für die Claude CLI.
|
/// Generiert eine temporäre MCP-Config-Datei für die Claude CLI.
|
||||||
/// Enthält den MCP-Server für remote_execute.
|
/// Enthält den MCP-Server für remote_execute.
|
||||||
/// </summary>
|
/// </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
|
// Port aus Konfiguration oder Default
|
||||||
var port = 19000;
|
var port = 19000;
|
||||||
@@ -587,7 +589,8 @@ public partial class AnthropicService
|
|||||||
["GOTT_SESSION_ID"] = sessionId ?? "",
|
["GOTT_SESSION_ID"] = sessionId ?? "",
|
||||||
["GOTT_AUTH_TOKEN"] = Endpoints.InternalEndpoint.InternalToken,
|
["GOTT_AUTH_TOKEN"] = Endpoints.InternalEndpoint.InternalToken,
|
||||||
["GOTT_AGENT_ID"] = agentId ?? "",
|
["GOTT_AGENT_ID"] = agentId ?? "",
|
||||||
["GOTT_HAS_REMOTE"] = hasRemote ? "true" : "false"
|
["GOTT_HAS_REMOTE"] = hasRemote ? "true" : "false",
|
||||||
|
["GOTT_REMOTE_OS"] = remoteOs ?? ""
|
||||||
};
|
};
|
||||||
|
|
||||||
var pythonCmd = FindPythonCommand();
|
var pythonCmd = FindPythonCommand();
|
||||||
|
|||||||
@@ -101,7 +101,8 @@ public partial class AnthropicService
|
|||||||
if (systemPrompt != null && systemPrompt.Count > 0)
|
if (systemPrompt != null && systemPrompt.Count > 0)
|
||||||
payload["system"] = systemPrompt;
|
payload["system"] = systemPrompt;
|
||||||
|
|
||||||
var hasRemote = remoteMgr?.GetBySessionId(sessionId) != null;
|
var remoteConn = remoteMgr?.GetBySessionId(sessionId);
|
||||||
|
var hasRemote = remoteConn != null;
|
||||||
AddToolsToPayload(payload, new ToolPayloadOptions
|
AddToolsToPayload(payload, new ToolPayloadOptions
|
||||||
{
|
{
|
||||||
Workspace = workspace,
|
Workspace = workspace,
|
||||||
@@ -113,6 +114,7 @@ public partial class AnthropicService
|
|||||||
AllowUserChoice = allowUserChoice,
|
AllowUserChoice = allowUserChoice,
|
||||||
PlanMode = planMode,
|
PlanMode = planMode,
|
||||||
IncludeRemote = hasRemote,
|
IncludeRemote = hasRemote,
|
||||||
|
RemoteOs = remoteConn?.OsVersion,
|
||||||
IncludeGitHubSearch = useGitHubSearch && _toolService.HasGitHubToken,
|
IncludeGitHubSearch = useGitHubSearch && _toolService.HasGitHubToken,
|
||||||
IncludeSerperSearch = useSerperSearch && _toolService.HasSerperKey
|
IncludeSerperSearch = useSerperSearch && _toolService.HasSerperKey
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ public partial class AnthropicService
|
|||||||
public bool AllowUserChoice { get; init; }
|
public bool AllowUserChoice { get; init; }
|
||||||
public bool PlanMode { get; init; }
|
public bool PlanMode { get; init; }
|
||||||
public bool IncludeRemote { get; init; }
|
public bool IncludeRemote { get; init; }
|
||||||
|
public string RemoteOs { get; init; }
|
||||||
public bool IncludeGitHubSearch { get; init; }
|
public bool IncludeGitHubSearch { get; init; }
|
||||||
public bool IncludeSerperSearch { get; init; }
|
public bool IncludeSerperSearch { get; init; }
|
||||||
}
|
}
|
||||||
@@ -337,7 +338,7 @@ public partial class AnthropicService
|
|||||||
var allTools = _toolService.GetToolDefinitions(new ToolDefinitionOptions
|
var allTools = _toolService.GetToolDefinitions(new ToolDefinitionOptions
|
||||||
{
|
{
|
||||||
IncludeFileTools = hasWorkspace, IncludeMemory = includeMemory,
|
IncludeFileTools = hasWorkspace, IncludeMemory = includeMemory,
|
||||||
IncludeUserChoice = opts.AllowUserChoice, IncludeRemote = opts.IncludeRemote,
|
IncludeUserChoice = opts.AllowUserChoice, IncludeRemote = opts.IncludeRemote, RemoteOs = opts.RemoteOs,
|
||||||
IncludeMemorySearch = includeMemorySearch, IncludeSendToSession = includeSendToSession,
|
IncludeMemorySearch = includeMemorySearch, IncludeSendToSession = includeSendToSession,
|
||||||
IncludeGitHubSearch = opts.IncludeGitHubSearch, IncludeSerperSearch = opts.IncludeSerperSearch
|
IncludeGitHubSearch = opts.IncludeGitHubSearch, IncludeSerperSearch = opts.IncludeSerperSearch
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ public class ChatService
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Baut den vollständigen ChatContext auf: Agent, Model, SystemPrompt, Chain, Workspace etc.
|
/// Baut den vollständigen ChatContext auf: Agent, Model, SystemPrompt, Chain, Workspace etc.
|
||||||
/// </summary>
|
/// </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);
|
var session = await _db.GetSession(sessionId);
|
||||||
if (session == null || (userId != null && session.UserId != userId) || string.IsNullOrEmpty(session.AgentId))
|
if (session == null || (userId != null && session.UserId != userId) || string.IsNullOrEmpty(session.AgentId))
|
||||||
@@ -89,7 +89,7 @@ public class ChatService
|
|||||||
? _toolService.GetToolDefinitions(new ToolDefinitionOptions
|
? _toolService.GetToolDefinitions(new ToolDefinitionOptions
|
||||||
{
|
{
|
||||||
IncludeFileTools = hasWorkspace, IncludeMemory = includeMemory,
|
IncludeFileTools = hasWorkspace, IncludeMemory = includeMemory,
|
||||||
IncludeUserChoice = agent.AllowUserChoice, IncludeRemote = hasRemoteConnection,
|
IncludeUserChoice = agent.AllowUserChoice, IncludeRemote = hasRemoteConnection, RemoteOs = remoteOs,
|
||||||
IncludeGitHubSearch = includeGitHubSearch, IncludeSerperSearch = includeSerperSearch
|
IncludeGitHubSearch = includeGitHubSearch, IncludeSerperSearch = includeSerperSearch
|
||||||
})
|
})
|
||||||
: Array.Empty<object>();
|
: Array.Empty<object>();
|
||||||
|
|||||||
+13
-4
@@ -21,6 +21,7 @@ public class ToolDefinitionOptions
|
|||||||
public bool IncludeMemory { get; set; }
|
public bool IncludeMemory { get; set; }
|
||||||
public bool IncludeUserChoice { get; set; } = true;
|
public bool IncludeUserChoice { get; set; } = true;
|
||||||
public bool IncludeRemote { get; set; }
|
public bool IncludeRemote { get; set; }
|
||||||
|
public string RemoteOs { get; set; }
|
||||||
public bool IncludeMemorySearch { get; set; }
|
public bool IncludeMemorySearch { get; set; }
|
||||||
public bool IncludeSendToSession { get; set; }
|
public bool IncludeSendToSession { get; set; }
|
||||||
public bool IncludeGitHubSearch { get; set; }
|
public bool IncludeGitHubSearch { get; set; }
|
||||||
@@ -106,7 +107,7 @@ public class ToolService
|
|||||||
if (opts.IncludeMemorySearch)
|
if (opts.IncludeMemorySearch)
|
||||||
allTools.Add(GetMemorySearchTool());
|
allTools.Add(GetMemorySearchTool());
|
||||||
if (opts.IncludeRemote)
|
if (opts.IncludeRemote)
|
||||||
allTools.Add(GetRemoteExecuteTool());
|
allTools.Add(GetRemoteExecuteTool(opts.RemoteOs));
|
||||||
if (opts.IncludeSendToSession)
|
if (opts.IncludeSendToSession)
|
||||||
allTools.Add(GetSendToSessionTool());
|
allTools.Add(GetSendToSessionTool());
|
||||||
if (opts.IncludeGitHubSearch)
|
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
|
return new
|
||||||
{
|
{
|
||||||
name = "remote_execute",
|
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
|
input_schema = new
|
||||||
{
|
{
|
||||||
type = "object",
|
type = "object",
|
||||||
properties = new Dictionary<string, 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" }
|
required = new[] { "command" }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,22 +21,34 @@ SESSION_ID = os.environ.get("GOTT_SESSION_ID", "")
|
|||||||
AUTH_TOKEN = os.environ.get("GOTT_AUTH_TOKEN", "")
|
AUTH_TOKEN = os.environ.get("GOTT_AUTH_TOKEN", "")
|
||||||
AGENT_ID = os.environ.get("GOTT_AGENT_ID", "")
|
AGENT_ID = os.environ.get("GOTT_AGENT_ID", "")
|
||||||
HAS_REMOTE = os.environ.get("GOTT_HAS_REMOTE", "false") == "true"
|
HAS_REMOTE = os.environ.get("GOTT_HAS_REMOTE", "false") == "true"
|
||||||
|
REMOTE_OS = os.environ.get("GOTT_REMOTE_OS", "").lower() # z.B. "windows", "linux"
|
||||||
|
|
||||||
TOOLS = []
|
TOOLS = []
|
||||||
|
|
||||||
# Remote-Execute Tool (nur wenn Remote-Verbindung vorhanden)
|
# Remote-Execute Tool (nur wenn Remote-Verbindung vorhanden)
|
||||||
if HAS_REMOTE:
|
if HAS_REMOTE:
|
||||||
TOOLS.append({
|
if "linux" in REMOTE_OS:
|
||||||
"name": "remote_execute",
|
_desc = (
|
||||||
"description": (
|
"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. "
|
"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 "
|
"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."
|
"(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": {
|
"inputSchema": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"command": {"type": "string", "description": "PowerShell-Befehl zum Ausführen auf dem Remote-PC"}
|
"command": {"type": "string", "description": _cmd_desc}
|
||||||
},
|
},
|
||||||
"required": ["command"]
|
"required": ["command"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -324,8 +324,10 @@ async function showRemoteModal(sessionId) {
|
|||||||
|
|
||||||
var empty = document.createElement('div');
|
var empty = document.createElement('div');
|
||||||
empty.className = 'sp-loading';
|
empty.className = 'sp-loading';
|
||||||
empty.innerHTML = '<div>Powershell-Befehl unter Windows ausführen</div>' +
|
empty.innerHTML = '<div>Windows (PowerShell)</div>' +
|
||||||
'<code>irm ' + esc(location.origin) + '/remote | iex</code>';
|
'<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);
|
body.appendChild(empty);
|
||||||
|
|
||||||
if (!connections || connections.length === 0) {
|
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