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,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 |
-
|
|
|
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 |
-
|
|
|
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') {
|
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 |
-
|
154 |
-
|
155 |
-
|
156 |
-
|
157 |
-
}
|
158 |
-
|
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');
|
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 |
-
|
|
|
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 |
-
|
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) {
|
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 |
-
|
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 |
-
|
286 |
-
}
|
287 |
-
// Result will be updated by refreshOutput
|
288 |
} catch (error) {
|
289 |
console.error("Network error sending command:", error);
|
290 |
-
|
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: '
|
303 |
-
|
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,
|
330 |
let payload = { command_type: type };
|
331 |
-
if (
|
332 |
-
|
333 |
-
|
334 |
-
|
335 |
-
|
336 |
-
|
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 |
-
|
374 |
-
|
|
|
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 |
-
|
|
|
|
|
|
|
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 |
-
|
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 |
-
<
|
495 |
-
<
|
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/` или `~/`
|
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',
|
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',
|
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',
|
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="
|
545 |
-
<input type="text" id="
|
546 |
</div>
|
547 |
<div class="control-section">
|
548 |
-
<button onclick="submitMediaCommand('screenshot'
|
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'
|
606 |
-
if (currentView === 'media'
|
607 |
-
if (currentView === 'clipboard'
|
608 |
-
if (currentView === 'utils'
|
|
|
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': #
|
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 |
-
|
|
|
|
|
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':
|
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 |
-
|
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
|
741 |
command_output = "Список уведомлений обновлен."
|
|
|
|
|
|
|
|
|
|
|
742 |
|
743 |
|
744 |
if 'heartbeat' in data and data['heartbeat']:
|
745 |
-
# Only set
|
746 |
-
if ('output'
|
747 |
-
|
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 |
-
|
761 |
-
|
762 |
-
|
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 |
-
|
770 |
-
|
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 |
-
|
780 |
-
|
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 |
-
|
788 |
-
|
789 |
-
final_filename = os.path.basename(filepath_candidate)
|
790 |
|
791 |
try:
|
792 |
-
file.save(
|
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 |
-
|
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':
|
804 |
except Exception as e:
|
805 |
-
|
806 |
-
|
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 |
-
|
830 |
-
|
831 |
-
|
832 |
-
|
833 |
-
|
834 |
-
|
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 |
-
|
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 |
-
|
863 |
-
|
|
|
|
|
864 |
return jsonify({
|
865 |
-
'status': 'success',
|
866 |
-
'server_filename': server_side_filename,
|
867 |
-
'original_filename': original_filename,
|
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 |
-
|
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)
|
|
|
|
|
|
|
|
|
|