file transfer

This commit is contained in:
2026-07-16 14:30:36 +05:30
parent 80e1bd8e3a
commit b9aeedc5ef
10 changed files with 1388 additions and 82 deletions

9
.env
View File

@@ -1,3 +1,12 @@
ROCKETCHAT_WEBHOOK_URL="https://text.seekright.com/hooks/6a0eb41ea88367bc6e101f0c/gvXfD8zSNRti43kEabqdE9tMCvNi9zAAoGfbao7cxcKbiW6R"
MONGODB_URI="mongodb://localhost:27017"
MONGODB_DB="rmm_db"
# --- Authentication (added 2026-07-16) ---
# Shared secret every agent must present in the X-Agent-Token header.
AGENT_TOKEN="170LRiZafLZThrIGQ6VlP8K9FPw7i8InwzM3dVoYnK4"
# Signing key for dashboard login tokens.
JWT_SECRET_KEY="Gme4fl7mCxTQv9b7H7iahacoEG6j5R4Av7kLAPQpzgVzHjINNScKIDWZ_tJsf93a"
# Dashboard login. CHANGE THESE, then restart the backend.
DASHBOARD_USERNAME="root"
DASHBOARD_PASSWORD="seekright159@"

1
.gitignore vendored
View File

@@ -7,3 +7,4 @@ logs.json
.DS_Store
.vscode/
.idea/
file_transfers/

View File

@@ -11,11 +11,11 @@ else:
# Fallback to load default .env if any variables are not yet defined
load_dotenv()
from fastapi import FastAPI, HTTPException, Depends, Header
from fastapi import FastAPI, HTTPException, Depends, Header, Request, BackgroundTasks
from fastapi.responses import PlainTextResponse
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel
from typing import Dict, Any, List
from typing import Dict, Any, List, Optional
import uvicorn
import base64
import hmac
@@ -104,6 +104,28 @@ db = mongo_client[DB_NAME]
clients_collection = db["clients"]
logs_collection = db["logs"]
config_collection = db["config"]
# --- Agent authentication ---
# Shared secret agents present in X-Agent-Token. Auth mode ("grace"|"strict") is
# stored in the config collection so it can be flipped live from the dashboard:
# grace = accept authenticated and legacy (tokenless) agents; used during rollout
# strict = reject any request without a valid token
AGENT_TOKEN = os.getenv("AGENT_TOKEN", "")
def get_agent_auth_mode() -> str:
doc = config_collection.find_one({"_id": "agent_auth"})
return (doc or {}).get("mode", "grace")
def is_valid_agent_token(token: Optional[str]) -> bool:
return bool(AGENT_TOKEN) and token is not None and hmac.compare_digest(token, AGENT_TOKEN)
async def require_agent_token(x_agent_token: str = Header(None)):
"""Dependency guarding every agent-facing endpoint. Enforces only in strict mode;
in grace mode it lets legacy tokenless agents through so a fleet can migrate."""
if get_agent_auth_mode() == "strict" and not is_valid_agent_token(x_agent_token):
raise HTTPException(status_code=401, detail="Invalid or missing agent token")
return is_valid_agent_token(x_agent_token)
MAX_ENTRIES_PER_CLIENT = 100 # Keep the latest 100 historical logs per client node
@@ -303,6 +325,9 @@ class TelemetryPayload(BaseModel):
memory_free_gb: float
disks: List[Dict[str, Any]]
gpus: List[Dict[str, Any]]
agent_version: Optional[str] = None
speed_upload_mbps: Optional[float] = None
speed_download_mbps: Optional[float] = None
class CommandResultPayload(BaseModel):
command: str
@@ -314,15 +339,45 @@ class LoginPayload(BaseModel):
username: str
password: str
DASHBOARD_USERNAME = os.getenv("DASHBOARD_USERNAME", "root")
DASHBOARD_PASSWORD = os.getenv("DASHBOARD_PASSWORD", "seekright159@")
@app.post("/api/login")
async def login(payload: LoginPayload):
if payload.username == "root" and payload.password == "seekright159@":
user_ok = hmac.compare_digest(payload.username, DASHBOARD_USERNAME)
pass_ok = hmac.compare_digest(payload.password, DASHBOARD_PASSWORD)
if user_ok and pass_ok:
token = generate_token("root")
return {"token": token}
raise HTTPException(status_code=401, detail="Invalid username or password")
class AuthModePayload(BaseModel):
mode: str
@app.get("/api/agent-auth-mode")
async def read_agent_auth_mode(current_user: str = Depends(get_current_user)):
return {"mode": get_agent_auth_mode()}
@app.post("/api/set-agent-auth-mode")
async def set_agent_auth_mode(payload: AuthModePayload, current_user: str = Depends(get_current_user)):
if payload.mode not in ("grace", "strict"):
raise HTTPException(status_code=400, detail="mode must be 'grace' or 'strict'")
config_collection.update_one({"_id": "agent_auth"}, {"$set": {"mode": payload.mode}}, upsert=True)
return {"status": "success", "mode": payload.mode}
@app.get("/api/agent-versions")
async def agent_versions(current_user: str = Depends(get_current_user)):
"""Migration dashboard: which agent version each node reports and whether its token is valid."""
out = {}
for doc in clients_collection.find({}, {"agent_version": 1, "agent_token_ok": 1}):
out[doc["_id"]] = {
"agent_version": doc.get("agent_version", "unknown"),
"agent_token_ok": doc.get("agent_token_ok", False),
}
return out
@app.get("/api/get-commands")
async def get_whitelisted_commands(platform: str):
async def get_whitelisted_commands(platform: str, token_valid: bool = Depends(require_agent_token)):
"""
Endpoint for remote agents to fetch their OS-specific whitelisted commands.
"""
@@ -335,7 +390,7 @@ async def get_whitelisted_commands(platform: str):
raise HTTPException(status_code=500, detail=f"Failed to load commands: {e}")
@app.get("/api/get-command")
async def get_command(client_id: str):
async def get_command(client_id: str, token_valid: bool = Depends(require_agent_token)):
"""
Endpoint for clients to poll for pending commands.
Client Agent hits this endpoint to ask: "Do I have any work to do?"
@@ -389,7 +444,7 @@ async def schedule_command(client_id: str, command: str, current_user: str = Dep
return {"status": "success", "message": f"Command '{command}' scheduled for {', '.join(target_ids)}"}
@app.post("/api/telemetry")
async def receive_telemetry(client_id: str, payload: TelemetryPayload):
async def receive_telemetry(client_id: str, payload: TelemetryPayload, token_valid: bool = Depends(require_agent_token)):
"""
Endpoint for agents to push their live hardware telemetry.
"""
@@ -548,7 +603,7 @@ async def get_logs_history(current_user: str = Depends(get_current_user)):
return {}
@app.post("/api/command-result")
async def receive_command_result(client_id: str, payload: CommandResultPayload):
async def receive_command_result(client_id: str, payload: CommandResultPayload, token_valid: bool = Depends(require_agent_token)):
"""
Endpoint for remote agents to push execution results back to the central server.
"""
@@ -636,3 +691,278 @@ if __name__ == "__main__":
print("[*] Starting FastAPI Central Server...")
# uvicorn runs the FastAPI app on port 8000
uvicorn.run("central_api_prototype:app", host="0.0.0.0", port=8000, reload=True)
# ============================================================
# Remote File Transfer (video fetch from agent TAKELEAP folders)
# ============================================================
import os
FILE_TRANSFER_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "file_transfers")
os.makedirs(FILE_TRANSFER_DIR, exist_ok=True)
class FileStatusPayload(BaseModel):
filename: str
status: str
message: Optional[str] = ""
progress_percent: Optional[float] = None
def set_file_transfer_state(client_id: str, filename: str, status: str, message: str = "", extra: Optional[Dict[str, Any]] = None):
state = {
"filename": filename,
"status": status,
"message": message,
"updated": datetime.now().isoformat()
}
if extra:
state.update(extra)
clients_collection.update_one(
{"_id": client_id},
{"$set": {"file_transfer": state}},
upsert=True
)
@app.post("/api/request-file")
async def request_file(client_id: str, filename: str, current_user: str = Depends(get_current_user)):
safe_name = os.path.basename(filename.strip())
if not client_id.strip() or not safe_name:
raise HTTPException(status_code=400, detail="client_id and filename are required")
clients_collection.update_one(
{"_id": client_id},
{"$set": {"pending_file_request": safe_name}},
upsert=True
)
set_file_transfer_state(client_id, safe_name, "requested", "Waiting for agent to poll...")
append_to_logs("file_requested", client_id, {"filename": safe_name})
return {"status": "success", "message": f"File '{safe_name}' requested from {client_id}"}
@app.get("/api/get-file-request")
async def get_file_request(client_id: str, token_valid: bool = Depends(require_agent_token)):
if not client_id or not client_id.strip():
raise HTTPException(status_code=400, detail="client_id cannot be empty")
doc = clients_collection.find_one_and_update(
{"_id": client_id},
{"$set": {"pending_file_request": "none"}},
return_document=False
)
fname = doc.get("pending_file_request", "none") if doc else "none"
return {"filename": fname}
@app.post("/api/file-transfer-status")
async def update_file_transfer_status(client_id: str, payload: FileStatusPayload, token_valid: bool = Depends(require_agent_token)):
if not client_id or not client_id.strip():
raise HTTPException(status_code=400, detail="client_id cannot be empty")
set_file_transfer_state(client_id, payload.filename, payload.status, payload.message or "", {"progress_percent": payload.progress_percent})
append_to_logs("file_transfer_status", client_id, payload.dict())
return {"status": "success"}
@app.post("/api/cancel-upload")
async def cancel_upload(client_id: str, filename: str):
safe_name = os.path.basename(filename.strip())
set_file_transfer_state(client_id, safe_name, "cancelled", "Upload cancelled by user")
client_dir = os.path.join(FILE_TRANSFER_DIR, os.path.basename(client_id.strip()))
if os.path.exists(client_dir):
for f in os.listdir(client_dir):
if f.startswith(f"{safe_name}.part"):
try:
os.remove(os.path.join(client_dir, f))
except:
pass
return {"status": "success"}
@app.get("/api/received-chunks")
async def received_chunks(client_id: str, filename: str, chunk_size: int, file_size: int, token_valid: bool = Depends(require_agent_token)):
"""Resume support: report which chunk indexes are already stored for this file.
Parts whose size doesn't match the current chunking scheme (stale from an
earlier run with a different chunk size) are deleted so they can't corrupt
the final stitch."""
safe_name = os.path.basename(filename.strip())
if not client_id.strip() or not safe_name or chunk_size <= 0 or file_size < 0:
raise HTTPException(status_code=400, detail="invalid parameters")
total_chunks = 1 if file_size == 0 else (file_size + chunk_size - 1) // chunk_size
last_expected = file_size - (total_chunks - 1) * chunk_size
client_dir = os.path.join(FILE_TRANSFER_DIR, os.path.basename(client_id.strip()))
received = []
if os.path.isdir(client_dir):
prefix = f"{safe_name}.part"
for entry in os.listdir(client_dir):
if not entry.startswith(prefix) or not entry[len(prefix):].isdigit():
continue
idx = int(entry[len(prefix):])
path = os.path.join(client_dir, entry)
expected = chunk_size if idx < total_chunks - 1 else last_expected
try:
if idx < total_chunks and os.path.getsize(path) == expected:
received.append(idx)
else:
os.remove(path)
except OSError:
pass # part is mid-write or locked; report as not received
return {"received": sorted(received)}
@app.post("/api/upload-chunk")
async def upload_chunk(client_id: str, filename: str, chunk_index: int, request: Request, token_valid: bool = Depends(require_agent_token)):
safe_name = os.path.basename(filename.strip())
if not client_id.strip() or not safe_name:
raise HTTPException(status_code=400, detail="client_id and filename are required")
doc = clients_collection.find_one({"_id": client_id})
if doc and doc.get("file_transfer", {}).get("filename") == safe_name and doc.get("file_transfer", {}).get("status") == "cancelled":
return {"status": "cancelled"}
client_dir = os.path.join(FILE_TRANSFER_DIR, os.path.basename(client_id.strip()))
os.makedirs(client_dir, exist_ok=True)
dest_path = os.path.join(client_dir, f"{safe_name}.part{chunk_index}")
try:
with open(dest_path, "wb") as f:
async for chunk in request.stream():
f.write(chunk)
except Exception as e:
raise HTTPException(status_code=500, detail=f"Failed to store chunk: {e}")
return {"status": "success", "chunk_index": chunk_index}
def enforce_storage_limit(max_bytes: int = 20 * 1024**3):
"""Scan FILE_TRANSFER_DIR recursively and delete oldest completed files if total size > max_bytes."""
all_files = []
total_size = 0
for root, dirs, files in os.walk(FILE_TRANSFER_DIR):
for name in files:
if ".part" in name:
continue
path = os.path.join(root, name)
try:
stat = os.stat(path)
all_files.append((stat.st_mtime, path, stat.st_size))
total_size += stat.st_size
except FileNotFoundError:
pass
if total_size > max_bytes:
all_files.sort(key=lambda x: x[0])
for mtime, path, size in all_files:
if total_size <= max_bytes:
break
try:
os.remove(path)
total_size -= size
print(f"Deleted old file {path} to free space.")
except Exception as e:
print(f"Error deleting file {path}: {e}")
@app.post("/api/upload-complete")
async def upload_complete(client_id: str, filename: str, total_chunks: int, background_tasks: BackgroundTasks, token_valid: bool = Depends(require_agent_token)):
safe_name = os.path.basename(filename.strip())
client_dir = os.path.join(FILE_TRANSFER_DIR, os.path.basename(client_id.strip()))
final_path = os.path.join(client_dir, safe_name)
try:
with open(final_path, "wb") as outfile:
for i in range(total_chunks):
part_path = os.path.join(client_dir, f"{safe_name}.part{i}")
if not os.path.exists(part_path):
raise HTTPException(status_code=400, detail=f"Missing chunk {i}")
with open(part_path, "rb") as infile:
outfile.write(infile.read())
os.remove(part_path)
except Exception as e:
set_file_transfer_state(client_id, safe_name, "error", f"Upload finalize failed: {e}")
raise HTTPException(status_code=500, detail=f"Failed to finalize file: {e}")
size_mb = round(os.path.getsize(final_path) / (1024 * 1024), 2)
set_file_transfer_state(client_id, safe_name, "ready", f"File received ({size_mb} MB)", {"size_mb": size_mb, "progress_percent": 100.0})
append_to_logs("file_received", client_id, {"filename": safe_name, "size_mb": size_mb})
send_rocketchat_notification(
text=f"📥 **File Received:** `{safe_name}` ({size_mb} MB) uploaded from client `{client_id}`.",
color="#2ecc71"
)
background_tasks.add_task(enforce_storage_limit)
return {"status": "success", "size_mb": size_mb}
@app.get("/api/file-transfers")
async def get_file_transfers(current_user: str = Depends(get_current_user)):
try:
cursor = clients_collection.find({}, {"file_transfer": 1})
return {doc["_id"]: doc.get("file_transfer") for doc in cursor if doc.get("file_transfer")}
except Exception as e:
print(f"[!] Error loading file transfers: {e}")
return {}
from fastapi.responses import FileResponse, Response
@app.get("/deploy_agent.sh")
async def get_agent_installer(x_agent_token: str = Header(None), token: Optional[str] = None):
# In strict mode the installer carries the live token, so downloading it must
# itself be authenticated (header for agent self-update, ?token= for humans).
if get_agent_auth_mode() == "strict" and not (is_valid_agent_token(x_agent_token) or is_valid_agent_token(token)):
raise HTTPException(status_code=401, detail="Invalid or missing agent token")
path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "deploy_agent.sh")
if not os.path.isfile(path):
raise HTTPException(status_code=404, detail="Installer script not found on server")
# Normalize CRLF -> LF (edited on Windows, executed by bash on Linux) and inject
# the live agent token so a freshly downloaded installer bakes it into the agent.
with open(path, "rb") as f:
content = f.read().replace(b"\r\n", b"\n")
content = content.replace(b"__AGENT_TOKEN__", AGENT_TOKEN.encode("utf-8"))
return Response(
content=content,
media_type="text/x-shellscript",
headers={"Content-Disposition": 'attachment; filename="deploy_agent.sh"'},
)
@app.get("/api/download-file")
async def download_file(client_id: str, filename: str, token: Optional[str] = None, inline: bool = False, authorization: str = Header(None)):
authed = False
if authorization and authorization.startswith("Bearer ") and verify_token(authorization.split(" ")[1]):
authed = True
if token and verify_token(token):
authed = True
if not authed:
raise HTTPException(status_code=401, detail="Authentication token is invalid or has expired")
safe_name = os.path.basename(filename.strip())
path = os.path.join(FILE_TRANSFER_DIR, os.path.basename(client_id.strip()), safe_name)
if not os.path.isfile(path):
raise HTTPException(status_code=404, detail="File not found on server")
media_type = "video/mp4" if safe_name.lower().endswith(".mp4") else "application/octet-stream"
if inline:
return FileResponse(path, media_type=media_type)
return FileResponse(path, media_type=media_type, filename=safe_name)
@app.post("/api/heartbeat")
async def receive_heartbeat(client_id: str, payload: TelemetryPayload,
token_valid: bool = Depends(require_agent_token)):
await receive_telemetry(client_id, payload)
# Record migration status so the dashboard can confirm the fleet is upgraded
# and every node is authenticated before auth is flipped to strict.
clients_collection.update_one(
{"_id": client_id},
{"$set": {"agent_version": payload.agent_version or "unknown",
"speed_upload_mbps": payload.speed_upload_mbps,
"speed_download_mbps": payload.speed_download_mbps,
"agent_token_ok": token_valid}},
)
doc = clients_collection.find_one_and_update(
{"_id": client_id},
{"$set": {"pending_command": "none", "pending_file_request": "none"}},
return_document=False
)
cmd = "none"
fname = "none"
if doc:
cmd = doc.get("pending_command", "none")
fname = doc.get("pending_file_request", "none")
if cmd != "none":
append_to_logs("command_polled", client_id, {"command": cmd})
return {
"status": "success",
"command": cmd,
"filename": fname
}

View File

@@ -1,42 +1,169 @@
{
"nt": {
"whoami": ["whoami"],
"ls": ["cmd.exe", "/c", "dir"],
"echo": ["cmd.exe", "/c", "echo", "The prototype executed the echo command successfully on Windows!"],
"reboot": ["cmd.exe", "/c", "echo", "SIMULATED WINDOWS REBOOT"],
"systeminfo": ["systeminfo"],
"ipconfig": ["ipconfig"],
"date": ["cmd.exe", "/c", "date", "/t"],
"check_space": ["powershell.exe", "-Command", "Get-PSDrive -PSProvider FileSystem"],
"check_takeleap": ["python", "client_agent_prototype.py", "--check-takeleap"],
"list_takeleap": ["python", "client_agent_prototype.py", "--list-takeleap"],
"list_takeleap_size": ["python", "client_agent_prototype.py", "--list-takeleap-size"],
"list_takeleap_shift_size": ["python", "client_agent_prototype.py", "--list-takeleap-sub-size", "SHIFT"],
"list_takeleap_shift_files": ["python", "client_agent_prototype.py", "--list-takeleap-sub-files", "SHIFT"],
"list_takeleap_errors_size": ["python", "client_agent_prototype.py", "--list-takeleap-sub-size", "Error_Videos"],
"list_takeleap_errors_files": ["python", "client_agent_prototype.py", "--list-takeleap-sub-files", "Error_Videos"],
"list_takeleap_upload_size": ["python", "client_agent_prototype.py", "--list-takeleap-sub-size", "UPLOAD_FOLDER"],
"list_takeleap_upload_files": ["python", "client_agent_prototype.py", "--list-takeleap-sub-files", "UPLOAD_FOLDER"]
"whoami": [
"whoami"
],
"ls": [
"cmd.exe",
"/c",
"dir"
],
"echo": [
"cmd.exe",
"/c",
"echo",
"The prototype executed the echo command successfully on Windows!"
],
"reboot": [
"cmd.exe",
"/c",
"echo",
"SIMULATED WINDOWS REBOOT"
],
"systeminfo": [
"systeminfo"
],
"ipconfig": [
"ipconfig"
],
"date": [
"cmd.exe",
"/c",
"date",
"/t"
],
"check_space": [
"powershell.exe",
"-Command",
"Get-PSDrive -PSProvider FileSystem"
],
"check_takeleap": [
"python",
"client_agent_prototype.py",
"--check-takeleap"
],
"list_takeleap": [
"python",
"client_agent_prototype.py",
"--list-takeleap"
],
"list_takeleap_size": [
"python",
"client_agent_prototype.py",
"--list-takeleap-size"
],
"list_takeleap_shift_size": [
"python",
"client_agent_prototype.py",
"--list-takeleap-sub-size",
"SHIFT"
],
"list_takeleap_shift_files": [
"python",
"client_agent_prototype.py",
"--list-takeleap-sub-files",
"SHIFT"
],
"list_takeleap_errors_size": [
"python",
"client_agent_prototype.py",
"--list-takeleap-sub-size",
"Error_Videos"
],
"list_takeleap_errors_files": [
"python",
"client_agent_prototype.py",
"--list-takeleap-sub-files",
"Error_Videos"
],
"list_takeleap_upload_size": [
"python",
"client_agent_prototype.py",
"--list-takeleap-sub-size",
"UPLOAD_FOLDER"
],
"list_takeleap_upload_files": [
"python",
"client_agent_prototype.py",
"--list-takeleap-sub-files",
"UPLOAD_FOLDER"
]
},
"posix": {
"whoami": ["whoami"],
"ls": ["ls", "-la"],
"echo": ["echo", "The prototype executed the echo command successfully on POSIX/WSL!"],
"pwd": ["pwd"],
"date": ["date"],
"time": ["date", "+%T"],
"systeminfo": ["uname", "-a"],
"ipconfig": ["ip", "addr"],
"check_space": ["df", "-h"],
"memory": ["free", "-h"],
"ps": ["ps", "aux"],
"kill_python": ["pkill", "-9", "python"],
"ping_google": ["ping", "-c", "4", "google.com"],
"env": ["env"],
"hostname": ["hostname"],
"reboot": ["sudo", "reboot"],
"restart_mysql": ["sudo", "systemctl", "restart", "mysql"],
"mysql_status": ["sudo", "systemctl", "status", "mysql"],
"whoami": [
"whoami"
],
"ls": [
"ls",
"-la"
],
"echo": [
"echo",
"The prototype executed the echo command successfully on POSIX/WSL!"
],
"pwd": [
"pwd"
],
"date": [
"date"
],
"time": [
"date",
"+%T"
],
"systeminfo": [
"uname",
"-a"
],
"ipconfig": [
"ip",
"addr"
],
"check_space": [
"df",
"-h"
],
"memory": [
"free",
"-h"
],
"ps": [
"ps",
"aux"
],
"kill_python": [
"pkill",
"-9",
"python"
],
"ping_google": [
"ping",
"-c",
"4",
"google.com"
],
"env": [
"env"
],
"hostname": [
"hostname"
],
"reboot": [
"sudo",
"reboot"
],
"restart_mysql": [
"sudo",
"systemctl",
"restart",
"mysql"
],
"mysql_status": [
"sudo",
"systemctl",
"status",
"mysql"
],
"repair_mysql": [
"sudo",
"mysqlcheck",
@@ -54,17 +181,149 @@
"--optimize",
"Test_seekright"
],
"storage_devices": ["lsblk"],
"mounted_disks": ["df", "-hT"],
"netstat": ["ss", "-tulnp"],
"check_takeleap": ["python3", "/opt/seekright-agent/client_agent_prototype.py", "--check-takeleap"],
"list_takeleap": ["python3", "/opt/seekright-agent/client_agent_prototype.py", "--list-takeleap"],
"list_takeleap_size": ["python3", "/opt/seekright-agent/client_agent_prototype.py", "--list-takeleap-size"],
"list_takeleap_shift_size": ["python3", "/opt/seekright-agent/client_agent_prototype.py", "--list-takeleap-sub-size", "SHIFT"],
"list_takeleap_shift_files": ["python3", "/opt/seekright-agent/client_agent_prototype.py", "--list-takeleap-sub-files", "SHIFT"],
"list_takeleap_errors_size": ["python3", "/opt/seekright-agent/client_agent_prototype.py", "--list-takeleap-sub-size", "Error_Videos"],
"list_takeleap_errors_files": ["python3", "/opt/seekright-agent/client_agent_prototype.py", "--list-takeleap-sub-files", "Error_Videos"],
"list_takeleap_upload_size": ["python3", "/opt/seekright-agent/client_agent_prototype.py", "--list-takeleap-sub-size", "UPLOAD_FOLDER"],
"list_takeleap_upload_files": ["python3", "/opt/seekright-agent/client_agent_prototype.py", "--list-takeleap-sub-files", "UPLOAD_FOLDER"]
"restart_rustdesk": [
"sudo",
"systemctl",
"restart",
"rustdesk"
],
"check_display": [
"bash",
"-c",
"for i in /sys/class/drm/*/status; do echo \"$i: $(cat $i)\"; done"
],
"drm_details": [
"bash",
"-c",
"ls -l /sys/class/drm && echo '---' && cat /sys/class/drm/*/status"
],
"headless_check": [
"bash",
"-c",
"connected=0; for f in /sys/class/drm/*/status; do if grep -q connected $f; then connected=1; fi; done; if [ $connected -eq 1 ]; then echo DISPLAY_CONNECTED; else echo HEADLESS; fi"
],
"framebuffer_check": [
"bash",
"-c",
"ls -l /dev/fb* && cat /proc/fb"
],
"install_temp_tools": [
"bash",
"-c",
"sudo apt update && sudo apt install -y lm-sensors"
],
"detect_sensors": [
"bash",
"-c",
"sudo sensors-detect --auto"
],
"gpu_temp": [
"bash",
"-c",
"nvidia-smi --query-gpu=temperature.gpu --format=csv,noheader"
],
"temperature": [
"bash",
"-c",
"sensors"
],
"check_edid": [
"bash",
"-c",
"for f in /sys/class/drm/*/edid; do echo $f; [ -s $f ] && echo 'EDID PRESENT' || echo 'NO EDID'; done"
],
"xrandr_display": [
"bash",
"-c",
"xrandr --query 2>&1"
],
"nvidia_status": [
"bash",
"-c",
"nvidia-smi"
],
"check_display_session": [
"bash",
"-c",
"echo DISPLAY=$DISPLAY && loginctl list-sessions"
],
"check_gpu_display": [
"bash",
"-c",
"lspci | grep -i -E 'vga|display|3d'"
],
"rustdesk_status": [
"sudo",
"systemctl",
"status",
"rustdesk"
],
"storage_devices": [
"lsblk"
],
"mounted_disks": [
"df",
"-hT"
],
"netstat": [
"ss",
"-tulnp"
],
"update_agent": [
"bash",
"-c",
"set -e; PY=/opt/seekright-agent/client_agent_prototype.py; URL=$(sed -n 's/^CENTRAL_BASE = \"\\(.*\\)\"/\\1/p' \"$PY\" | head -1); TOK=$(sed -n 's/^AGENT_TOKEN = \"\\(.*\\)\"/\\1/p' \"$PY\" | head -1); curl -fsSL -H \"X-Agent-Token: $TOK\" \"$URL/deploy_agent.sh\" -o /tmp/sr_update.sh; test -s /tmp/sr_update.sh; systemd-run --collect --description='SeekRight agent self-update' bash /tmp/sr_update.sh >/dev/null 2>&1; echo 'Update launched detached via systemd-run; agent will restart on the new version shortly.'"
],
"check_takeleap": [
"python3",
"/opt/seekright-agent/client_agent_prototype.py",
"--check-takeleap"
],
"list_takeleap": [
"python3",
"/opt/seekright-agent/client_agent_prototype.py",
"--list-takeleap"
],
"list_takeleap_size": [
"python3",
"/opt/seekright-agent/client_agent_prototype.py",
"--list-takeleap-size"
],
"list_takeleap_shift_size": [
"python3",
"/opt/seekright-agent/client_agent_prototype.py",
"--list-takeleap-sub-size",
"SHIFT"
],
"list_takeleap_shift_files": [
"python3",
"/opt/seekright-agent/client_agent_prototype.py",
"--list-takeleap-sub-files",
"SHIFT"
],
"list_takeleap_errors_size": [
"python3",
"/opt/seekright-agent/client_agent_prototype.py",
"--list-takeleap-sub-size",
"Error_Videos"
],
"list_takeleap_errors_files": [
"python3",
"/opt/seekright-agent/client_agent_prototype.py",
"--list-takeleap-sub-files",
"Error_Videos"
],
"list_takeleap_upload_size": [
"python3",
"/opt/seekright-agent/client_agent_prototype.py",
"--list-takeleap-sub-size",
"UPLOAD_FOLDER"
],
"list_takeleap_upload_files": [
"python3",
"/opt/seekright-agent/client_agent_prototype.py",
"--list-takeleap-sub-files",
"UPLOAD_FOLDER"
]
}
}
}

332
commands_new.json Normal file
View File

@@ -0,0 +1,332 @@
{
"nt": {
"whoami": [
"whoami"
],
"ls": [
"cmd.exe",
"/c",
"dir"
],
"echo": [
"cmd.exe",
"/c",
"echo",
"The prototype executed the echo command successfully on Windows!"
],
"reboot": [
"cmd.exe",
"/c",
"echo",
"SIMULATED WINDOWS REBOOT"
],
"systeminfo": [
"systeminfo"
],
"ipconfig": [
"ipconfig"
],
"date": [
"cmd.exe",
"/c",
"date",
"/t"
],
"check_space": [
"powershell.exe",
"-Command",
"Get-PSDrive -PSProvider FileSystem"
],
"check_takeleap": [
"python",
"client_agent_prototype.py",
"--check-takeleap"
],
"list_takeleap": [
"python",
"client_agent_prototype.py",
"--list-takeleap"
],
"list_takeleap_size": [
"python",
"client_agent_prototype.py",
"--list-takeleap-size"
],
"list_takeleap_shift_size": [
"python",
"client_agent_prototype.py",
"--list-takeleap-sub-size",
"SHIFT"
],
"list_takeleap_shift_files": [
"python",
"client_agent_prototype.py",
"--list-takeleap-sub-files",
"SHIFT"
],
"list_takeleap_errors_size": [
"python",
"client_agent_prototype.py",
"--list-takeleap-sub-size",
"Error_Videos"
],
"list_takeleap_errors_files": [
"python",
"client_agent_prototype.py",
"--list-takeleap-sub-files",
"Error_Videos"
],
"list_takeleap_upload_size": [
"python",
"client_agent_prototype.py",
"--list-takeleap-sub-size",
"UPLOAD_FOLDER"
],
"list_takeleap_upload_files": [
"python",
"client_agent_prototype.py",
"--list-takeleap-sub-files",
"UPLOAD_FOLDER"
]
},
"posix": {
"whoami": [
"whoami"
],
"ls": [
"ls",
"-la"
],
"echo": [
"echo",
"The prototype executed the echo command successfully on POSIX/WSL!"
],
"pwd": [
"pwd"
],
"date": [
"date"
],
"time": [
"date",
"+%T"
],
"systeminfo": [
"uname",
"-a"
],
"ipconfig": [
"ip",
"addr"
],
"check_space": [
"df",
"-h"
],
"memory": [
"free",
"-h"
],
"ps": [
"ps",
"aux"
],
"kill_python": [
"pkill",
"-9",
"python"
],
"ping_google": [
"ping",
"-c",
"4",
"google.com"
],
"env": [
"env"
],
"hostname": [
"hostname"
],
"reboot": [
"sudo",
"reboot"
],
"restart_mysql": [
"sudo",
"systemctl",
"restart",
"mysql"
],
"mysql_status": [
"sudo",
"systemctl",
"status",
"mysql"
],
"repair_mysql": [
"sudo",
"mysqlcheck",
"--defaults-file=/root/.my.cnf",
"--auto-repair",
"--optimize",
"Test_seekright",
"tbl_site_anomaly"
],
"repair_mysql_db": [
"sudo",
"mysqlcheck",
"--defaults-file=/root/.my.cnf",
"--auto-repair",
"--optimize",
"Test_seekright"
],
"restart_rustdesk": [
"sudo",
"systemctl",
"restart",
"rustdesk"
],
"check_display": [
"bash",
"-c",
"for i in /sys/class/drm/*/status; do echo \"$i: $(cat $i)\"; done"
],
"drm_details": [
"bash",
"-c",
"ls -l /sys/class/drm && echo '---' && cat /sys/class/drm/*/status"
],
"headless_check": [
"bash",
"-c",
"connected=0; for f in /sys/class/drm/*/status; do if grep -q connected $f; then connected=1; fi; done; if [ $connected -eq 1 ]; then echo DISPLAY_CONNECTED; else echo HEADLESS; fi"
],
"framebuffer_check": [
"bash",
"-c",
"ls -l /dev/fb* && cat /proc/fb"
],
"install_temp_tools": [
"bash",
"-c",
"sudo apt update && sudo apt install -y lm-sensors"
],
"detect_sensors": [
"bash",
"-c",
"sudo sensors-detect --auto"
],
"gpu_temp": [
"bash",
"-c",
"nvidia-smi --query-gpu=temperature.gpu --format=csv,noheader"
],
"temperature": [
"bash",
"-c",
"sensors"
],
"check_edid": [
"bash",
"-c",
"for f in /sys/class/drm/*/edid; do echo $f; [ -s $f ] && echo 'EDID PRESENT' || echo 'NO EDID'; done"
],
"xrandr_display": [
"bash",
"-c",
"xrandr --query 2>&1"
],
"nvidia_status": [
"bash",
"-c",
"nvidia-smi"
],
"check_display_session": [
"bash",
"-c",
"echo DISPLAY=$DISPLAY && loginctl list-sessions"
],
"check_gpu_display": [
"bash",
"-c",
"lspci | grep -i -E 'vga|display|3d'"
],
"rustdesk_status": [
"sudo",
"systemctl",
"status",
"rustdesk"
],
"storage_devices": [
"lsblk"
],
"mounted_disks": [
"df",
"-hT"
],
"netstat": [
"ss",
"-tulnp"
],
"check_takeleap": [
"python3",
"/opt/seekright-agent/client_agent_prototype.py",
"--check-takeleap"
],
"list_takeleap": [
"python3",
"/opt/seekright-agent/client_agent_prototype.py",
"--list-takeleap"
],
"list_takeleap_size": [
"python3",
"/opt/seekright-agent/client_agent_prototype.py",
"--list-takeleap-size"
],
"list_takeleap_shift_size": [
"python3",
"/opt/seekright-agent/client_agent_prototype.py",
"--list-takeleap-sub-size",
"SHIFT"
],
"list_takeleap_shift_files": [
"python3",
"/opt/seekright-agent/client_agent_prototype.py",
"--list-takeleap-sub-files",
"SHIFT"
],
"list_takeleap_errors_size": [
"python3",
"/opt/seekright-agent/client_agent_prototype.py",
"--list-takeleap-sub-size",
"Error_Videos"
],
"list_takeleap_errors_files": [
"python3",
"/opt/seekright-agent/client_agent_prototype.py",
"--list-takeleap-sub-files",
"Error_Videos"
],
"list_takeleap_upload_size": [
"python3",
"/opt/seekright-agent/client_agent_prototype.py",
"--list-takeleap-sub-size",
"UPLOAD_FOLDER"
],
"list_takeleap_upload_files": [
"python3",
"/opt/seekright-agent/client_agent_prototype.py",
"--list-takeleap-sub-files",
"UPLOAD_FOLDER"
],
"last_reboot": [
"last",
"reboot"
],
"dummy": [
"last",
"reboot"
]
}
}

View File

@@ -10,26 +10,57 @@ if [ "$EUID" -ne 0 ]; then
fi
# ====================================================
# ⚙️ CONFIGURATION: Set your Client ID & Central URL
# ⚙️ CONFIGURATION: Client ID, Central URL, Agent Token
# ====================================================
CLIENT_ID=""
# Resolution priority for each value: explicit CLI arg > existing install > default.
# Reading from the existing install makes re-running with no args a safe in-place
# upgrade — the node keeps its identity. This is what fleet self-update relies on.
# Usage: deploy_agent.sh [CLIENT_ID] [CENTRAL_URL] [AGENT_TOKEN]
EXISTING_PY="/opt/seekright-agent/client_agent_prototype.py"
# For Production deployment, use: "http://rmm-backend.seekright.com"
# For Local WSL testing on your laptop, use: "http://172.18.16.1:8000"
# Check if Client ID is set; if not, check command line arguments, otherwise default to hostname
if [ -z "$CLIENT_ID" ]; then
CLIENT_ID="${1:-$(hostname)}"
CLIENT_ID="${1:-}"
CENTRAL_URL="${2:-}"
ARG_TOKEN="${3:-}"
if [ -f "$EXISTING_PY" ]; then
if [ -z "$CLIENT_ID" ]; then
CLIENT_ID="$(sed -n 's/^CLIENT_ID = "\(.*\)"/\1/p' "$EXISTING_PY" | head -1)"
fi
if [ -z "$CENTRAL_URL" ]; then
CENTRAL_URL="$(sed -n 's/^CENTRAL_BASE = "\(.*\)"/\1/p' "$EXISTING_PY" | head -1)"
fi
fi
CENTRAL_URL="${2:-http://rmm-backend.seekright.com}"
# Fallbacks when nothing was provided and there is no prior install
if [ -z "$CLIENT_ID" ]; then CLIENT_ID="$(hostname)"; fi
if [ -z "$CENTRAL_URL" ]; then CENTRAL_URL="http://rmm-backend.seekright.com"; fi
echo "🚀 Preparing SeekRight RMM Agent installation..."
# Agent token. The server injects the live token into the line below at download
# time (replacing __AGENT_TOKEN__). Priority: CLI arg > server-injected > existing install.
AGENT_TOKEN="__AGENT_TOKEN__"
if [ -n "$ARG_TOKEN" ]; then
AGENT_TOKEN="$ARG_TOKEN"
elif [ "$AGENT_TOKEN" = "__AGENT_TOKEN__" ]; then
if [ -f "$EXISTING_PY" ]; then
AGENT_TOKEN="$(sed -n 's/^AGENT_TOKEN = "\(.*\)"/\1/p' "$EXISTING_PY" | head -1)"
else
AGENT_TOKEN=""
fi
fi
echo "🚀 Preparing SeekRight RMM Agent installation (agent v3.2-auth)..."
echo "📍 Target Client ID: $CLIENT_ID"
echo "🌐 Central Server URL: $CENTRAL_URL"
# 1. Install System Dependencies
echo "🔄 Updating system package indexes and installing python dependencies..."
apt-get update -y
# Fix potential MySQL GPG key expiration issues before updating (common issue on Ubuntu 22.04)
apt-key del B7B3B788A8D3785C || true
wget -q -O - https://repo.mysql.com/RPM-GPG-KEY-mysql-2025 | apt-key add - || true
wget -q -O - https://repo.mysql.com/RPM-GPG-KEY-mysql-2025 | gpg --dearmor --yes -o /usr/share/keyrings/mysql-apt-config.gpg || true
apt-get update -y || true
apt-get install -y python3 python3-pip python3-psutil
# 2. Create Destination Directories
@@ -41,6 +72,7 @@ chmod 755 /opt/seekright-agent
echo "📝 Generating client agent file..."
cat << 'EOF' > /opt/seekright-agent/client_agent_prototype.py
import urllib.request
import urllib.parse
import json
import time
import subprocess
@@ -50,17 +82,47 @@ import sys
import string
import threading
def get_cpu_temp():
if not hasattr(psutil, "sensors_temperatures"):
return None
try:
temps = psutil.sensors_temperatures()
if not temps:
return None
for name in ['coretemp', 'k10temp', 'acpitz', 'cpu_thermal']:
if name in temps and temps[name]:
return temps[name][0].current
for name, entries in temps.items():
if entries:
return entries[0].current
except Exception:
pass
return None
AGENT_VERSION = "3.2-auth"
CLIENT_ID = "TEMPLATE_CLIENT_ID"
CENTRAL_BASE = "TEMPLATE_CENTRAL_URL"
AGENT_TOKEN = "TEMPLATE_AGENT_TOKEN"
# Attach the shared headers (agent token + tunnel bypass) to every outbound request
# by installing a default opener, so we don't have to touch each call site.
_opener = urllib.request.build_opener()
_opener.addheaders = [('ngrok-skip-browser-warning', 'true'), ('X-Agent-Token', AGENT_TOKEN)]
urllib.request.install_opener(_opener)
CENTRAL_API_URL = f"{CENTRAL_BASE}/api/get-command?client_id={CLIENT_ID}"
CENTRAL_TELEMETRY_URL = f"{CENTRAL_BASE}/api/telemetry?client_id={CLIENT_ID}"
CENTRAL_HEARTBEAT_URL = f"{CENTRAL_BASE}/api/heartbeat?client_id={CLIENT_ID}"
platform_name = "nt" if os.name == "nt" else "posix"
CENTRAL_COMMANDS_URL = f"{CENTRAL_BASE}/api/get-commands?platform={platform_name}"
CENTRAL_FILE_REQUEST_URL = f"{CENTRAL_BASE}/api/get-file-request?client_id={CLIENT_ID}"
CENTRAL_FILE_STATUS_URL = f"{CENTRAL_BASE}/api/file-transfer-status?client_id={CLIENT_ID}"
CACHED_COMMANDS = {}
command_in_progress = False
file_transfer_in_progress = False
def run_command_async(command_key, safe_cmd_list, is_shutdown_or_restart):
global command_in_progress
@@ -87,12 +149,43 @@ def run_command_async(command_key, safe_cmd_list, is_shutdown_or_restart):
finally:
command_in_progress = False
def execute_command_async(command_key):
# Resolve a command key received via heartbeat against the whitelist and run it.
# (The heartbeat loop sets command_in_progress=True before dispatching here, so
# every exit path must clear it unless run_command_async takes over that duty.)
global command_in_progress
try:
allowed_commands = load_allowed_commands()
if command_key not in allowed_commands:
warning_msg = f"Server requested unknown/unwhitelisted command: '{command_key}'. Ignored."
print(f" -> [!] SECURITY WARNING: {warning_msg}")
send_rocket_chat_notification("⚠️ SECURITY WARNING", warning_msg, "#FFA500")
command_in_progress = False
return
print(f" -> Executing approved command: '{command_key}'")
safe_cmd_list = allowed_commands[command_key]
is_shutdown_or_restart = any(x in command_key.lower() for x in ["reboot", "restart", "kill_python"])
if is_shutdown_or_restart:
print(" -> Shutdown/restart command detected. Sending pre-execution notification...")
send_rocket_chat_notification(
f"🔄 Executing: {command_key}",
f"System/agent is performing an action: {' '.join(safe_cmd_list)}\nConnection may drop temporarily.",
"#FFA500"
)
time.sleep(3)
# run_command_async clears command_in_progress in its finally block
run_command_async(command_key, safe_cmd_list, is_shutdown_or_restart)
except Exception as e:
print(f" -> [!] Command dispatch error: {e}")
command_in_progress = False
def load_allowed_commands():
global CACHED_COMMANDS
try:
req = urllib.request.Request(CENTRAL_COMMANDS_URL)
req.add_header('ngrok-skip-browser-warning', 'true')
with urllib.request.urlopen(req, timeout=3) as response:
with urllib.request.urlopen(req, timeout=15) as response:
commands = json.loads(response.read().decode())
if commands:
CACHED_COMMANDS = commands
@@ -121,7 +214,7 @@ def send_rocket_chat_notification(title, message, color="#764FA5"):
}
data = json.dumps(payload).encode('utf-8')
req = urllib.request.Request(ROCKET_CHAT_WEBHOOK, data=data, headers={'Content-Type': 'application/json'}, method='POST')
urllib.request.urlopen(req, timeout=3)
urllib.request.urlopen(req, timeout=15)
except Exception as e:
print(f" -> [!] Failed to send webhook: {e}")
@@ -156,7 +249,7 @@ def get_windows_label(drive):
pass
return ""
def collect_and_send_telemetry():
def send_heartbeat():
try:
gpus = []
try:
@@ -240,20 +333,44 @@ def collect_and_send_telemetry():
payload = {
"cpu_percent": psutil.cpu_percent(interval=0.5),
"cpu_temp": get_cpu_temp(),
"memory_percent": psutil.virtual_memory().percent,
"memory_total_gb": round(psutil.virtual_memory().total / (1024**3), 2),
"memory_free_gb": round(psutil.virtual_memory().available / (1024**3), 2),
"disks": disks,
"gpus": gpus
"gpus": gpus,
"agent_version": AGENT_VERSION
}
data = json.dumps(payload).encode('utf-8')
req = urllib.request.Request(CENTRAL_TELEMETRY_URL, data=data, headers={'Content-Type': 'application/json'}, method='POST')
req = urllib.request.Request(CENTRAL_HEARTBEAT_URL, data=data, headers={'Content-Type': 'application/json'}, method='POST')
req.add_header('ngrok-skip-browser-warning', 'true')
urllib.request.urlopen(req, timeout=3)
print("[*] Telemetry pushed successfully.")
with urllib.request.urlopen(req, timeout=30) as response:
res = json.loads(response.read().decode())
# Handle commands
cmd = res.get("command", "none")
if cmd != "none":
global command_in_progress
if not command_in_progress:
command_in_progress = True
t = threading.Thread(target=execute_command_async, args=(cmd,))
t.daemon = True
t.start()
# Handle file requests
fname = res.get("filename", "none")
if fname != "none":
global file_transfer_in_progress
if not file_transfer_in_progress:
file_transfer_in_progress = True
t = threading.Thread(target=handle_file_request_async, args=(fname,))
t.daemon = True
t.start()
print("[*] Heartbeat successful.")
except Exception as e:
print(f"[!] Failed to push telemetry: {e}")
print(f"[!] Failed heartbeat: {e}")
def send_command_result_to_server(command: str, returncode: int, stdout: str, stderr: str):
try:
@@ -273,7 +390,7 @@ def send_command_result_to_server(command: str, returncode: int, stdout: str, st
method='POST'
)
req.add_header('ngrok-skip-browser-warning', 'true')
with urllib.request.urlopen(req, timeout=3) as response:
with urllib.request.urlopen(req, timeout=15) as response:
pass
print(" -> Sent command execution output to Central Server.")
except Exception as e:
@@ -287,7 +404,7 @@ def poll_server():
print(f"[*] Checking for commands...")
req = urllib.request.Request(CENTRAL_API_URL)
req.add_header('ngrok-skip-browser-warning', 'true')
with urllib.request.urlopen(req, timeout=3) as response:
with urllib.request.urlopen(req, timeout=15) as response:
data = json.loads(response.read().decode())
command_key = data.get("command", "none")
@@ -568,6 +685,213 @@ def list_takeleap_subfolder_files(subfolder_name):
def list_takeleap_subfolder(subfolder_name):
list_takeleap_subfolder_files(subfolder_name)
# ============================================================
# Remote File Transfer: dashboard requests a file by name,
# agent finds it under any TAKELEAP folder and uploads it.
# ============================================================
def send_file_transfer_status(filename, status, message="", progress_percent=None):
try:
payload = {"filename": filename, "status": status, "message": message}
if progress_percent is not None:
payload["progress_percent"] = progress_percent
data = json.dumps(payload).encode('utf-8')
req = urllib.request.Request(
CENTRAL_FILE_STATUS_URL,
data=data,
headers={'Content-Type': 'application/json'},
method='POST'
)
req.add_header('ngrok-skip-browser-warning', 'true')
urllib.request.urlopen(req, timeout=30)
except Exception as e:
print(f" -> [!] Failed to send file transfer status: {e}")
def find_file_in_takeleap(filename):
target = filename.lower()
# Fast-path optimization: Extract YYYYMMDD from filename
# e.g., 20260612112725_000000.MP4 -> SHIFT/2026/June/12
fast_path_rel = None
try:
if len(filename) >= 8 and filename[:8].isdigit():
import calendar
y = filename[0:4]
m = int(filename[4:6])
d = filename[6:8]
if 1 <= m <= 12:
month_name = calendar.month_name[m]
fast_path_rel = os.path.join("SHIFT", y, month_name, d)
except Exception:
pass
if os.name == "nt":
drives = [f"{letter}:\\" for letter in string.ascii_uppercase if os.path.exists(f"{letter}:\\")]
else:
drives = ["/mnt", "/media", "/home"]
if os.path.exists("/TAKELEAP"):
drives.append("/TAKELEAP")
for drive in drives:
for root, dirs, files in os.walk(drive, topdown=True):
if os.name == "nt":
dirs[:] = [d for d in dirs if d.lower() not in {"windows", "program files", "program files (x86)", "appdata", "programdata", "$recycle.bin", "system volume information", "node_modules", ".git"}]
else:
if root == "/":
dirs[:] = [d for d in dirs if d.lower() not in {"proc", "sys", "dev", "var", "lib", "run", "boot", "snap", "node_modules", ".git"}]
elif root == "/mnt":
dirs[:] = [d for d in dirs if not (len(d) == 1 and d.isalpha()) and d.lower() not in {"wslg", "wsl"}]
if os.path.basename(root).upper() == "TAKELEAP":
# FAST PATH: Check the specific date folder first
if fast_path_rel:
specific_dir = os.path.join(root, fast_path_rel)
if os.path.exists(specific_dir):
for froot, fdirs, ffiles in os.walk(specific_dir):
for f in ffiles:
if f.lower() == target:
return os.path.join(froot, f)
# Exhaustively search this TAKELEAP subtree for the requested file
for froot, fdirs, ffiles in os.walk(root):
for f in ffiles:
if f.lower() == target:
return os.path.join(froot, f)
dirs[:] = [] # subtree fully searched, don't descend again
return None
def upload_file_to_server(filename, filepath):
import time
size = os.path.getsize(filepath)
quoted = urllib.parse.quote(filename)
CHUNK_SIZE = 512 * 1024 # 512 KB: small enough to finish well under the tunnel gateway timeout
if size == 0:
total_chunks = 1
else:
total_chunks = (size + CHUNK_SIZE - 1) // CHUNK_SIZE
def fetch_received():
# Returns the set of chunk indexes the server already holds, or None if unreachable
url = f"{CENTRAL_BASE}/api/received-chunks?client_id={CLIENT_ID}&filename={quoted}&chunk_size={CHUNK_SIZE}&file_size={size}"
try:
req = urllib.request.Request(url)
req.add_header('ngrok-skip-browser-warning', 'true')
with urllib.request.urlopen(req, timeout=30) as r:
return set(json.loads(r.read().decode()).get("received", []))
except Exception as e:
print(f" -> [!] Could not fetch resume state ({e}).")
return None
def upload_one(f, chunk_index):
# Returns True on success, False if the server reports the transfer cancelled
f.seek(chunk_index * CHUNK_SIZE)
chunk_data = f.read(CHUNK_SIZE)
chunk_url = f"{CENTRAL_BASE}/api/upload-chunk?client_id={CLIENT_ID}&filename={quoted}&chunk_index={chunk_index}"
while True:
try:
req = urllib.request.Request(chunk_url, data=chunk_data, method='POST')
req.add_header('Content-Type', 'application/octet-stream')
req.add_header('ngrok-skip-browser-warning', 'true')
with urllib.request.urlopen(req, timeout=120) as response:
res = json.loads(response.read().decode())
return res.get("status") != "cancelled"
except Exception as e:
print(f" -> [!] Chunk {chunk_index} failed ({e}). Retrying in 5s...")
time.sleep(5)
received = fetch_received() or set()
if received:
print(f" -> Resuming: server already has {len(received)}/{total_chunks} chunks.")
uploaded = 0
with open(filepath, 'rb') as f:
for chunk_index in range(total_chunks):
if chunk_index in received:
continue
if not upload_one(f, chunk_index):
print(f" -> [!] Upload cancelled by server.")
send_file_transfer_status(filename, "error", "Upload cancelled.")
return
uploaded += 1
# Brief pause between chunks so heartbeat traffic can slip through
time.sleep(0.1)
if uploaded % 8 == 0 or chunk_index == total_chunks - 1:
progress = round(((len(received) + uploaded) / total_chunks) * 100, 1)
send_file_transfer_status(filename, "uploading", f"Uploading... ({progress}%)", progress_percent=progress)
# Finalize with self-healing: verify the server holds every chunk (re-uploading
# any lost to a mid-transfer cancel or cleanup), then ask it to stitch the file.
while True:
on_server = fetch_received()
if on_server is None:
time.sleep(5)
continue
missing = sorted(set(range(total_chunks)) - on_server)
if missing:
print(f" -> [!] Server is missing {len(missing)} chunk(s). Re-uploading...")
for chunk_index in missing:
if not upload_one(f, chunk_index):
print(f" -> [!] Upload cancelled by server.")
send_file_transfer_status(filename, "error", "Upload cancelled.")
return
time.sleep(0.1)
continue
complete_url = f"{CENTRAL_BASE}/api/upload-complete?client_id={CLIENT_ID}&filename={quoted}&total_chunks={total_chunks}"
try:
req = urllib.request.Request(complete_url, method='POST', data=b'')
req.add_header('ngrok-skip-browser-warning', 'true')
with urllib.request.urlopen(req, timeout=180) as response:
return json.loads(response.read().decode())
except Exception as e:
print(f" -> [!] Finalize failed ({e}). Re-verifying chunks in 5s...")
time.sleep(5)
def handle_file_request_async(filename):
global file_transfer_in_progress
try:
print(f" -> File request received: '{filename}'. Searching TAKELEAP folders...")
send_file_transfer_status(filename, "searching", "Searching TAKELEAP folders on client...")
filepath = find_file_in_takeleap(filename)
if not filepath:
print(f" -> [!] File '{filename}' not found in any TAKELEAP folder.")
send_file_transfer_status(filename, "not_found", "File was not found in any TAKELEAP folder on this client.")
send_rocket_chat_notification(f"❌ File Not Found: {filename}", f"Requested file was not found on client {CLIENT_ID}.", "#FF0000")
return
size_mb = round(os.path.getsize(filepath) / (1024 * 1024), 2)
print(f" -> Found at {filepath} ({size_mb} MB). Uploading...")
send_file_transfer_status(filename, "uploading", f"Found at {filepath} ({size_mb} MB). Uploading to server...")
upload_file_to_server(filename, filepath)
# Server marks the transfer 'ready' once the upload completes
print(f" -> Upload complete: {filename}")
send_rocket_chat_notification(f"✅ File Uploaded: {filename}", f"Uploaded {size_mb} MB from {filepath} to the central server.", "#00FF00")
except Exception as ex:
print(f" -> [!] File transfer error: {ex}")
send_file_transfer_status(filename, "error", f"Transfer failed on client: {ex}")
send_rocket_chat_notification(f"❌ File Transfer Failed: {filename}", str(ex), "#FF0000")
finally:
file_transfer_in_progress = False
def poll_file_request():
global file_transfer_in_progress
if file_transfer_in_progress:
return
try:
req = urllib.request.Request(CENTRAL_FILE_REQUEST_URL)
req.add_header('ngrok-skip-browser-warning', 'true')
with urllib.request.urlopen(req, timeout=15) as response:
data = json.loads(response.read().decode())
filename = data.get("filename", "none")
if filename and filename != "none":
file_transfer_in_progress = True
t = threading.Thread(target=handle_file_request_async, args=(filename,))
t.daemon = True
t.start()
except Exception as e:
print(f" -> [!] Failed to poll file requests: {e}")
if __name__ == "__main__":
if len(sys.argv) > 1:
if sys.argv[1] == "--check-takeleap":
@@ -592,20 +916,21 @@ if __name__ == "__main__":
print(f"Unknown argument or missing parameters: {sys.argv[1]}")
sys.exit(1)
else:
print(f"Starting Agent for {CLIENT_ID}...")
print(f"Starting Agent v{AGENT_VERSION} for {CLIENT_ID}...")
try:
while True:
collect_and_send_telemetry()
poll_server()
send_heartbeat()
time.sleep(10)
except KeyboardInterrupt:
print("\n[*] Agent stopped.")
EOF
# Inject the Client ID and Central URL dynamically
# Inject the Client ID, Central URL, and Agent Token dynamically
sed -i "s/TEMPLATE_CLIENT_ID/$CLIENT_ID/g" /opt/seekright-agent/client_agent_prototype.py
sed -i "s|TEMPLATE_CENTRAL_URL|$CENTRAL_URL|g" /opt/seekright-agent/client_agent_prototype.py
chmod 644 /opt/seekright-agent/client_agent_prototype.py
sed -i "s|TEMPLATE_AGENT_TOKEN|$AGENT_TOKEN|g" /opt/seekright-agent/client_agent_prototype.py
# Agent file holds the shared secret; restrict it to root
chmod 600 /opt/seekright-agent/client_agent_prototype.py
# 4. Create Background systemd Service
echo "⚙️ Configuring background systemd daemon service..."

6
dispatch_jagan.py Normal file
View File

@@ -0,0 +1,6 @@
from pymongo import MongoClient
import os
db = MongoClient('mongodb://localhost:27017')['rmm_db']
db.clients.update_one({'_id': 'JAGAN'}, {'$set': {'pending_command': 'update_agent'}})
print('Dispatched update_agent to JAGAN!')

6
set_strict.py Normal file
View File

@@ -0,0 +1,6 @@
import urllib.request, json
from pymongo import MongoClient
db = MongoClient('mongodb://localhost:27017')['rmm_db']
db.config.update_one({'_id': 'agent_auth'}, {'$set': {'mode': 'strict'}}, upsert=True)
print("Set to strict")

33
test_auth.py Normal file
View File

@@ -0,0 +1,33 @@
import urllib.request, json
from pymongo import MongoClient
# Flip to strict
db = MongoClient('mongodb://localhost:27017')['rmm_db']
db.config.update_one({'_id': 'agent_auth'}, {'$set': {'mode': 'strict'}}, upsert=True)
def test_heartbeat(token=None):
headers = {'Content-Type': 'application/json'}
if token is not None:
headers['X-Agent-Token'] = token
req = urllib.request.Request(
'http://localhost:8000/api/heartbeat?client_id=TEST_AUTH',
data=json.dumps({'cpu_percent':10,'cpu_temp':40,'memory_percent':50,'memory_total_gb':16,'memory_free_gb':8,'disks':[],'gpus':[],'agent_version':'test'}).encode('utf-8'),
headers=headers
)
try:
urllib.request.urlopen(req)
return 'OK'
except Exception as e:
return str(e)
print('Strict mode:')
print('No token:', test_heartbeat())
print('Wrong token:', test_heartbeat('wrong'))
print('Correct token:', test_heartbeat('170LRiZafLZThrIGQ6VlP8K9FPw7i8InwzM3dVoYnK4'))
# Dispatch update_agent to JAGAN
db.clients.update_one({'_id': 'JAGAN'}, {'$set': {'pending_command': 'update_agent'}})
print('Dispatched update_agent to JAGAN!')
# Flip back to grace so we don't break other things during the migration
db.config.update_one({'_id': 'agent_auth'}, {'$set': {'mode': 'grace'}})

5
verify_syntax.py Normal file
View File

@@ -0,0 +1,5 @@
import re, ast
src = open('c:/Users/seekr/OneDrive/Desktop/work/RMM/RMM-BE/rmm-backend/deploy_agent.sh', encoding='utf-8').read()
m = re.search(r"cat << 'EOF' > /opt/seekright-agent/client_agent_prototype.py\n(.*?)\nEOF", src, re.S)
ast.parse(m.group(1))
print("Embedded Python syntax is OK")