Передача файлов с телефона на ПК без приложения: HTTPS-сервер на aiohttp, самоподписанный сертификат и QR с PIN

Задача простая: снять с телефона фото и видео и получить их на ПК с Windows без кабеля, облака и установки чего-либо на телефон. Готовые решения либо требуют приложение на обе стороны, либо гоняют файлы через чужой сервер, либо жмут видео в мессенджере. Я сделал «Тропку»: ПК поднимает у себя маленький HTTPS-сайт, телефон открывает его в обычном браузере по адресу из QR-кода.

Схема
ПК запускает веб-сервер на порту 47321. В окне программы показывается QR-код с адресом вида https://192.168.1.5:47321/?k=PIN. Телефон переходит по ссылке, сервер проверяет PIN, ставит cookie и перенаправляет на /, где живёт страница с лентой. Файлы идут напрямую по локальной сети, через интернет ничего не проходит.
Стек: Python 3.12, aiohttp (сервер), cryptography (сертификат), qrcode, PySide6 (окно), pystray (трей), SQLite (лента и список телефонов), PyInstaller (один .exe). Страница телефона — чистые HTML/JS/CSS без внешних библиотек.
Сертификат: HTTPS без домена
Некоторые возможности браузера (например, Wake Lock, который не даёт экрану телефона погаснуть во время передачи) работают только в безопасном контексте, поэтому голый HTTP не подходит. Домена у локального IP нет, значит, остаётся самоподписанный сертификат. Программа создаёт его при первом запуске: ключ EC P-256, в SAN попадает IP ПК. Если у компьютера сменился адрес, сертификат пересоздаётся.
Платить за это приходится одним предупреждением: «Подключение не защищено» на Android и «Это соединение не является частным» на iPhone. Соединение при этом шифруется. Chrome помнит исключение примерно неделю, потом спрашивает снова. Убрать предупреждение можно только установкой собственного корневого сертификата на телефон, что для сценария «отсканировал и пользуйся» слишком тяжело.

Доступ: QR с PIN и cookie
Сервер слушает 0.0.0.0, поэтому открыть его мог бы любой в той же сети. Защита такая: PIN зашит в QR (?k=PIN), сервер проверяет его, ставит cookie сессии и редиректит на /. Пять неверных PIN подряд блокируют IP на минуту. Кнопка «Новый PIN» выдаёт другой.
Есть тонкость с cookie: SameSite=Strict здесь не работает. Вход по QR — это переход из приложения камеры, а Strict-cookie теряется на таком редиректе, поэтому стоит SameSite=Lax.
Ещё одно ограничение — CSP default-src 'self': на странице телефона нет ни inline-скриптов, ни внешних CDN, только свои файлы.
Приём файла без порчи
Файл с телефона отправляется PUT-запросом на /api/upload. Сервер пишет его во временный .most-<id>.part и только после завершения переименовывает в свободное имя: если а.txt уже есть, получится а (1).txt. Так оборванная передача не оставит в «Загрузках» битый файл. Деталь Windows: os.rename там не перезаписывает существующий файл, поэтому свободное имя подбирается заранее.
События (телефон подключился, файл принят, текст получен) идут через внутреннюю шину: на страницу телефона — по SSE (/api/events), в окно — через Qt-сигнал. Шина вызывается в потоке сервера, а виджеты PySide6 трогать можно только из GUI-потока, поэтому между ними стоит ретранслятор.

Брандмауэр Windows
Самое неприятное место для пользователя: при первом запуске Windows показывает свой запрос брандмауэра, и если нажать «Отмена», создаётся запрещающее правило, после чего телефон не может открыть страницу. Поэтому программа сама проверяет, пустит ли Windows телефон: есть ли правило для exe (netsh advfirewall firewall show rule, права администратора для проверки не нужны) и не «общедоступная» ли сеть (Get-NetConnectionProfile через PowerShell). Если не пустит, в окне QR появляется красное предупреждение и кнопка «Разрешить доступ телефону». Она одним запросом UAC удаляет старые правила для этого exe, добавляет разрешающее для частной сети и переключает сеть в «Частную».
Сборка в один exe
PyInstaller собирает один Tropka.exe примерно на 41 МБ. Значок и свойства файла (версия берётся из version) генерируются отдельным скриптом перед сборкой. Антивирусы иногда подозрительно относятся к программам, собранным PyInstaller, а Windows SmartScreen встречает любую неподписанную программу синим окном «Система Windows защитила ваш компьютер» — в README этому посвящён отдельный раздел.
pip install -r requirements.txt
powershell -ExecutionPolicy Bypass -File build.ps1Запуск из исходников и режим отладки без HTTPS и брандмауэра:
python -m most
python -m most --host 127.0.0.1 --no-tls --port 8080Что не получилось (пока)
Только одна сеть: ПК и телефон должны быть в одной Wi-Fi сети. Через мобильный интернет без сервера-посредника не выйдет, а делать такой сервер я не хотел.
Страница держит экран телефона включённым через Wake Lock, но если телефон всё же заблокировали или браузер надолго свернули, передача обрывается.
Нет фоновых уведомлений: новые файлы видно, только когда страница открыта.
Пакет по историческим причинам называется
most: проект переименовали из «Мост» в «Тропку».
Телефон → телефон
Если ПК не нужен, есть соседний проект «Тропка Онлайн»: страница на GitHub Pages, где телефоны находят друг друга через публичные серверы сообщений и дальше передают файлы по WebRTC напрямую. Служебные сообщения шифруются ключом, который лежит в части ссылки после #, поэтому серверы видят только шифртекст.
Ссылки
Программа: https://github.com/OlegChe77/tropka/releases/latest/download/Tropka.exe
Исходный код: https://github.com/OlegChe77/tropka
Тропка Онлайн: https://olegche77.github.io/tropka-online/
Windows 10/11, бесплатно, без рекламы и сбора данных. Критика архитектуры и найденные ошибки приветствуются в комментариях и в Issues на GitHub.
KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.