Files
xiaoxia-saas/scripts/deploy_cmd_agent.sh
T
2026-06-26 15:04:46 +08:00

218 lines
8.1 KiB
Bash

#!/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"