#!/bin/bash set -e echo "[1/5] Creating agent directory..." mkdir -p /opt/xiaoxia-cmd-agent echo "[2/5] Writing server.py..." cat > /opt/xiaoxia-cmd-agent/server.py << 'PYEOF' #!/usr/bin/env python3 import http.server, subprocess, json, os, sys, secrets from urllib.parse import urlparse, parse_qs LISTEN_PORT = 18888 AUTH_TOKEN = "xsa-" + secrets.token_hex(16) LOG_FILE = "/var/log/xiaoxia-cmd-agent.log" class Handler(http.server.BaseHTTPRequestHandler): def log_message(self, fmt, *args): try: with open(LOG_FILE, "a") as f: f.write("%s - %s\n" % (self.log_date_time_string(), fmt % args)) except: pass def check_auth(self): t = self.headers.get("Authorization", "") if t != AUTH_TOKEN: self._j({"error": "unauthorized"}, 401); return False return True def _j(self, data, code=200): body = json.dumps(data, ensure_ascii=False).encode() self.send_response(code) self.send_header("Content-Type", "application/json; charset=utf-8") self.send_header("Content-Length", str(len(body))) self.end_headers() self.wfile.write(body) def do_GET(self): if not self.check_auth(): return if self.path.startswith("/download"): self._download(); return if self.path == "/status": self._j({"status":"ok","hostname":os.uname().nodename,"port":LISTEN_PORT}) else: self._j({"error":"use /status or /download?path=xxx"}, 404) def do_POST(self): if not self.check_auth(): return try: length = int(self.headers.get("Content-Length", 0)) except: self._j({"error":"bad content-length"},400); return if self.path == "/exec": self._exec(length) elif self.path == "/list": self._list(length) elif self.path == "/upload": self._upload(length) elif self.path == "/mkdir": self._mkdir(length) else: self._j({"error":"unknown"}, 404) def do_DELETE(self): if not self.check_auth(): return try: length = int(self.headers.get("Content-Length", 0)) except: self._j({"error":"bad"},400); return body = json.loads(self.rfile.read(length)) if length > 0 else {} fp = body.get("path","") if not fp: self._j({"error":"path required"},400); return try: import shutil if os.path.isdir(fp): shutil.rmtree(fp) else: os.remove(fp) self._j({"ok":True,"deleted":fp}) except Exception as e: self._j({"error":str(e)},500) def _exec(self, length): try: data = json.loads(self.rfile.read(length)) if length>0 else {} except: self._j({"error":"invalid json"},400); return cmd = data.get("command","") timeout = min(data.get("timeout",30), 120) if not cmd: self._j({"error":"command required"},400); return try: r = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=timeout) self._j({"exit_code":r.returncode,"stdout":r.stdout[-100000:] if r.stdout else "","stderr":r.stderr[-10000:] if r.stderr else ""}) except subprocess.TimeoutExpired: self._j({"error":f"timeout {timeout}s"},408) except Exception as e: self._j({"error":str(e)},500) def _list(self, length): try: data = json.loads(self.rfile.read(length)) if length>0 else {} except: self._j({"error":"invalid json"},400); return path = data.get("path",".") try: entries = [] for name in sorted(os.listdir(path)): fp2 = os.path.join(path, name) try: st = os.stat(fp2) entries.append({"name":name,"is_dir":os.path.isdir(fp2),"size":st.st_size,"mtime":st.st_mtime}) except: entries.append({"name":name,"is_dir":False,"size":0,"mtime":0}) self._j({"path":path,"entries":entries}) except Exception as e: self._j({"error":str(e)},500) def _upload(self, length): ct = self.headers.get("Content-Type","") if "multipart/form-data" not in ct: self._j({"error":"multipart required"},400); return try: boundary = ct.split("boundary=")[1].strip() body = self.rfile.read(length) parts = body.split(b"--" + boundary.encode()) fname = fdata = None for part in parts: if b"filename=" in part: h, _, content = part.partition(b"\r\n\r\n") for line in h.decode(errors="replace").split("\r\n"): if 'filename="' in line: fname = line.split('filename="')[1].split('"')[0] fdata = content.rstrip(b"\r\n") if fname is None or fdata is None: self._j({"error":"no file"},400); return dest_dir = "/tmp" dest = os.path.join(dest_dir, fname) os.makedirs(dest_dir, exist_ok=True) with open(dest,"wb") as f: f.write(fdata) self._j({"ok":True,"path":dest,"size":len(fdata)}) except Exception as e: self._j({"error":str(e)},500) def _mkdir(self, length): try: data = json.loads(self.rfile.read(length)) if length>0 else {} except: self._j({"error":"invalid json"},400); return path = data.get("path","") if not path: self._j({"error":"path required"},400); return try: os.makedirs(path, exist_ok=True); self._j({"ok":True,"created":path}) except Exception as e: self._j({"error":str(e)},500) def _download(self): if not self.check_auth(): return qs = parse_qs(urlparse(self.path).query) fp = qs.get("path",[""])[0] if not fp or not os.path.isfile(fp): self._j({"error":"not found"},404); return try: with open(fp,"rb") as f: data = f.read() self.send_response(200) self.send_header("Content-Type","application/octet-stream") self.send_header("Content-Length",str(len(data))) self.end_headers() self.wfile.write(data) except Exception as e: self._j({"error":str(e)},500) def main(): server = http.server.HTTPServer(("127.0.0.1", LISTEN_PORT), Handler) with open("/etc/xiaoxia-cmd-agent.token","w") as f: f.write(AUTH_TOKEN) os.chmod("/etc/xiaoxia-cmd-agent.token", 0o600) svc = """[Unit] Description=Xiaoxia Command Agent After=network.target [Service] Type=simple ExecStart=/usr/bin/python3 /opt/xiaoxia-cmd-agent/server.py Restart=always RestartSec=5 [Install] WantedBy=multi-user.target """ with open("/etc/systemd/system/xiaoxia-cmd-agent.service","w") as f: f.write(svc) os.system("systemctl daemon-reload && systemctl enable xiaoxia-cmd-agent && systemctl restart xiaoxia-cmd-agent") print(f"TOKEN: {AUTH_TOKEN}") print(f"Listening: 127.0.0.1:{LISTEN_PORT}") print(f"Service started") if __name__ == "__main__": main() PYEOF echo "[3/5] Starting agent service..." cd /opt/xiaoxia-cmd-agent python3 server.py & sleep 2 TOKEN=$(cat /etc/xiaoxia-cmd-agent.token) echo "Agent running. Token: $TOKEN" echo "[4/5] Configuring Nginx reverse proxy..." mkdir -p /etc/nginx/ssl openssl req -x509 -nodes -days 3650 -newkey rsa:2048 \ -keyout /etc/nginx/ssl/cmd-agent.key \ -out /etc/nginx/ssl/cmd-agent.crt \ -subj "/CN=cmd-agent" 2>/dev/null # Check nginx sites-available structure if [ -d /etc/nginx/sites-available ]; then CONF=/etc/nginx/sites-available/cmd-agent LINK=/etc/nginx/sites-enabled/cmd-agent else CONF=/etc/nginx/conf.d/cmd-agent.conf LINK="" fi cat > "$CONF" << NGINXEOF server { listen 8443 ssl; server_name _; ssl_certificate /etc/nginx/ssl/cmd-agent.crt; ssl_certificate_key /etc/nginx/ssl/cmd-agent.key; location / { proxy_pass http://127.0.0.1:18888; proxy_set_header Host \$host; proxy_read_timeout 120s; client_max_body_size 50M; } } NGINXEOF [ -n "$LINK" ] && ln -sf "$CONF" "$LINK" nginx -t 2>&1 && systemctl reload nginx echo "[5/5] Testing..." curl -s -H "Authorization: $TOKEN" http://127.0.0.1:18888/status echo "" echo "=== DEPLOY COMPLETE ===" echo "Token: $TOKEN" echo "Local: http://127.0.0.1:18888" echo "Nginx: https://127.0.0.1:8443"