diff --git a/README.md b/README.md index 358b734..90f23c8 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Telegram Shop Bot -Телеграм-бот для организации онлайн-продаж через Telegram с поддержкой криптовалют и WireGuard VPN. +Телеграм-бот для организации онлайн-продаж через Telegram с поддержкой криптовалют, WireGuard VPN и Tor-прокси для доступа к админ-панели через onion-адрес. ## Возможности @@ -10,6 +10,7 @@ - История транзакций и покупок - SaaS-система с автоматическим расчётом комиссий - Админ-панель на порту 3001 +- Tor-прокси с двумя onion-сервисами (SSH + админка) - WireGuard VPN для безопасных транзакций ## Быстрый старт (одна команда) @@ -61,8 +62,13 @@ curl http://localhost:3001/health | `BOT_TOKEN` | ✅ | Токен Telegram бота (@BotFather) | | `ADMIN_IDS` | ✅ | ID администраторов через запятую | | `ENCRYPTION_KEY` | ✅ | Ключ шифрования (32 байта hex) | +| `ADMIN_SECRET` | ✅ | Секрет для админ-панели | +| `ADMIN_PORT` | — | Порт админ-панели (по умолчанию 3001) | +| `ADMIN_URL` | — | Полный URL админ-панели (для фото товаров) | | `SUPER_ADMIN_IDS` | — | ID супер-админов | | `SUPPORT_LINK` | — | Ссылка на поддержку | +| `SSH_HOST_IP` | — | Куда Tor перенаправляет SSH (по умолчанию host.docker.internal) | +| `SHOP_CONTAINER` | — | Имя контейнера магазина (по умолчанию telegram_shop_prod) | | `WG_ENABLED` | — | `true` / `false` (по умолчанию `false`) | | `WG_PRIVATE_KEY` | — | Приватный ключ WireGuard | | `WG_PUBLIC_KEY` | — | Публичный ключ WireGuard | @@ -76,6 +82,58 @@ curl http://localhost:3001/health node -e "console.log(require('crypto').randomBytes(32).toString('hex'))" ``` +## Tor Proxy + +Проект включает Tor-прокси для доступа к SSH и админ-панели через onion-адреса. + +### Архитектура + +``` +Internet → Tor Network → tor-proxy контейнер + ├── Onion #1 :22 → хост SSH + └── Onion #2 :80 → telegram_shop_prod:3001 + (через Docker сеть tor_proxy_net) +``` + +### Файлы Tor-прокси + +| Файл | Назначение | +|---|---| +| `tor-proxy/Dockerfile` | Alpine + Tor образ | +| `tor-proxy/entrypoint.sh` | Генерация torrc из env vars, валидация, запись onion-адресов | +| `tor-proxy/get-onions.sh` | Скрипт чтения onion-адресов и обновления .env | +| `tor-proxy/hosts/` | Директория для onion-hosts.txt (bind mount) | + +### После запуска + +Onion-адреса автоматически сохраняются в `tor-proxy/hosts/onion-hosts.txt`. Обновить `.env`: + +```bash +./tor-proxy/get-onions.sh +``` + +Вывод: +``` +============================================================ + Onion services +============================================================ + SSH : xxxxx.onion (port 22 -> host SSH) + Admin : yyyyy.onion (port 80 -> telegram_shop_prod:3001) +============================================================ + +Usage: + SSH : torify ssh user@xxxxx.onion + Admin : open http://yyyyy.onion in Tor Browser +``` + +### Переменные Tor + +| Переменная | По умолчанию | Описание | +|---|---|---| +| `SSH_HOST_IP` | `host.docker.internal` | Куда Tor перенаправляет SSH | +| `SHOP_CONTAINER` | `telegram_shop_prod` | Контейнер магазина | +| `ADMIN_PORT` | `3001` | Порт админки | + ## Поддерживаемые устройства | Устройство | Архитектура | RAM | Статус | @@ -91,30 +149,31 @@ Docker автоматически собирает нативные модули ## Архитектура Docker ``` -┌─────────────────────────────────────────────┐ -│ telegram_shop_prod (node:22-alpine) │ -│ ┌──────────────┐ ┌─────────────────────┐ │ -│ │ Builder │ │ Runtime │ │ -│ │ python3, g++ │→ │ bash, curl, │ │ -│ │ make, gcc │ │ wireguard-tools, │ │ -│ │ npm install │ │ iptables, bind-tools │ │ -│ └──────────────┘ └─────────────────────┘ │ -│ Port 3001 (localhost) │ -│ Limit: 384MB RAM │ -└─────────────────────────────────────────────┘ - ↑ Volumes: db/, uploads/, .env +┌──────────────────────────────────────────────────────────┐ +│ docker-compose │ +│ │ +│ ┌────────────────────────┐ ┌─────────────────────────┐ │ +│ │ telegram_shop_prod │ │ tor-proxy │ │ +│ │ (node:22-alpine) │ │ (alpine:3.18 + tor) │ │ +│ │ │ │ │ │ +│ │ Port 3001 │◄─┤ HiddenService :80 │ │ +│ │ Bot + Admin Panel │ │ HiddenService :22 → SSH│ │ +│ │ │ │ │ │ +│ │ Net: default │ │ Net: default + proxy_net │ │ +│ │ + tor_proxy_net │ │ │ │ +│ └────────────────────────┘ └─────────────────────────┘ │ +│ │ │ │ +│ Volumes: Volumes: │ +│ db/, uploads/, .env tor_data, hosts/ │ +└──────────────────────────────────────────────────────────┘ ``` -### Файлы +### Сети Docker -| Файл | Назначение | +| Сеть | Назначение | |---|---| -| `Dockerfile` | Multi-stage сборка (builder + runtime) | -| `docker-compose.yml` | Конфигурация контейнера | -| `install.sh` | Автоматический установщик | -| `.dockerignore` | Исключения из Docker-образа | -| `.env.example` | Шаблон переменных окружения | -| `wg/start.sh` | Скрипт запуска (WireGuard + Node.js) | +| `default` | Внутренняя связь между контейнерами | +| `tor_proxy_net` | Связь tor-proxy ↔ telegram_shop_prod | ## Команды управления @@ -128,6 +187,10 @@ docker compose up -d --build # Логи docker compose logs -f +# Логи конкретного сервиса +docker compose logs -f tor-proxy +docker compose logs -f telegram_shop_prod + # Стоп docker compose down @@ -139,6 +202,13 @@ docker compose ps # Health-check curl http://localhost:3001/health + +# Onion-адреса +docker exec tor-proxy cat /var/lib/tor/ssh/hostname +docker exec tor-proxy cat /var/lib/tor/admin/hostname + +# Обновить .env с onion-адресами +./tor-proxy/get-onions.sh ``` ## WireGuard @@ -154,36 +224,56 @@ WireGuard по умолчанию отключен (`WG_ENABLED=false`). Для ## Безопасность - `.env` монтируется только для чтения (`:ro`) -- Порт 3001 привязан к `127.0.0.1` (доступен только с хоста) -- Нативные модули компилируются в builder-стейдже (чистый runtime) +- Порт 3001 доступен из LAN и через Tor onion +- Onion-адреса сохраняются в volume (персистентность при перезапуске) +- Tor hidden services с валидацией env vars +- Нативные модули компилируются в builder-стейдже - `devDependencies` не попадают в production-образ - Тестовые файлы исключены из Docker-образа (`.dockerignore`) - `node_modules` хоста не попадают в образ (`.dockerignore`) +## Устойчивость к ошибкам + +- Бот не крашит контейнер при невалидном `BOT_TOKEN`: 5 попыток с задержкой 5с, затем бот отключается, админка продолжает работать +- Комиссионные кошельки не обязательны для старта: при отсутствии логируется предупреждение +- При потере связи с Telegram API: polling ошибки логируются, процесс продолжает работать + ## Структура проекта ``` ├── src/ -│ ├── admin/ # Админ-панель (Express) -│ ├── config/ # Конфигурация (БД, крипто) -│ ├── context/ # Контекст и состояния бота -│ ├── handlers/ # Обработчики команд -│ ├── middleware/ # Промежуточные обработчики -│ ├── migrations/ # Миграции БД -│ ├── models/ # Модели данных (SQLite) -│ ├── router/ # Роутинг Express -│ ├── services/ # Бизнес-логика -│ ├── utils/ # Утилиты -│ └── index.js # Точка входа -├── wg/ # WireGuard конфигурация -│ └── start.sh # Скрипт запуска контейнера -├── db/ # SQLite база данных (volume) -├── uploads/ # Загруженные фото (volume) -├── Dockerfile # Multi-stage сборка -├── docker-compose.yml # Конфигурация контейнера -├── install.sh # Установщик (POSIX sh) -├── .dockerignore # Исключения из образа -├── .env.example # Шаблон переменных +│ ├── admin/ # Админ-панель (Express) +│ │ ├── routes/ # Роуты админ-панели +│ │ ├── views/ # Шаблоны HTML +│ │ ├── public/ # Статические файлы (CSS) +│ │ ├── auth.js # Авторизация +│ │ └── server.js # Express-сервер +│ ├── config/ # Конфигурация (БД, крипто) +│ ├── context/ # Контекст и состояния бота +│ ├── handlers/ # Обработчики команд +│ │ ├── adminHandlers/ # Обработчики админа +│ │ └── userHandlers/ # Обработчики пользователя +│ ├── middleware/ # Промежуточные обработчики +│ ├── migrations/ # Миграции БД +│ ├── models/ # Модели данных +│ ├── router/ # Роутинг Express +│ ├── services/ # Бизнес-логика +│ ├── utils/ # Утилиты (логирование, валидация, ошибки) +│ └── index.js # Точка входа +├── tor-proxy/ # Tor прокси для SSH и админки +│ ├── Dockerfile # Alpine + Tor образ +│ ├── entrypoint.sh # Генерация torrc, валидация env vars +│ ├── get-onions.sh # Скрипт обновления .env с onion-адресами +│ └── hosts/ # Директория для onion-hosts.txt +├── wg/ # WireGuard конфигурация +│ └── start.sh # Скрипт запуска контейнера +├── db/ # SQLite база данных (volume) +├── uploads/ # Загруженные фото (volume) +├── Dockerfile # Multi-stage сборка магазина +├── docker-compose.yml # Конфигурация обоих контейнеров +├── install.sh # Установщик (POSIX sh) +├── .dockerignore # Исключения из образа +├── .env.example # Шаблон переменных └── package.json ``` diff --git a/install.sh b/install.sh index 4c8e599..0374aaa 100755 --- a/install.sh +++ b/install.sh @@ -256,6 +256,45 @@ else printf " %s logs -f\n" "$COMPOSE_CMD" fi +# ============================================================ +# 9. Tor Proxy — onion-адреса +# ============================================================ +printf "\n" +print_step "Проверка Tor-прокси" + +TOR_RUNNING=0 +for ATTEMPT in 1 2 3 4 5 6; do + sleep 5 + if docker exec tor-proxy test -s /var/lib/tor/ssh/hostname 2>/dev/null && \ + docker exec tor-proxy test -s /var/lib/tor/admin/hostname 2>/dev/null; then + TOR_RUNNING=1 + break + fi + print_info "Попытка $ATTEMPT/6 — Tor генерирует onion-адреса..." +done + +if [ "$TOR_RUNNING" -eq 1 ]; then + SSH_ONION=$(docker exec tor-proxy cat /var/lib/tor/ssh/hostname 2>/dev/null || echo "") + ADMIN_ONION=$(docker exec tor-proxy cat /var/lib/tor/admin/hostname 2>/dev/null || echo "") + if [ -n "$SSH_ONION" ] && [ -n "$ADMIN_ONION" ]; then + print_ok "Tor-прокси работает!" + printf "\n ${CYAN}${BOLD}SSH onion:${NC} %s\n" "$SSH_ONION" + printf " ${CYAN}${BOLD}Admin onion:${NC} %s\n" "$ADMIN_ONION" + printf "\n ${BOLD}Подключение:${NC}\n" + printf " SSH: torify ssh root@%s\n" "$SSH_ONION" + printf " Admin: откройте http://%s в Tor Browser\n" "$ADMIN_ONION" + + if [ -f "./tor-proxy/get-onions.sh" ]; then + sh ./tor-proxy/get-onions.sh 2>/dev/null || true + fi + else + print_warn "Tor запущен, но onion-адреса не найдены" + fi +else + print_warn "Tor-прокси не стартовал за 30 секунд. Проверьте логи:" + printf " docker logs tor-proxy\n" +fi + # ============================================================ # Готово # ============================================================ diff --git a/src/handlers/adminHandlers/product/viewHandler.js b/src/handlers/adminHandlers/product/viewHandler.js index 620c4ab..01874c3 100644 --- a/src/handlers/adminHandlers/product/viewHandler.js +++ b/src/handlers/adminHandlers/product/viewHandler.js @@ -62,15 +62,17 @@ export default class ViewHandler { let hiddenPhotoMessage; if (product.photo_url) { + const photoUrl = product.photo_url.startsWith('http') ? product.photo_url : `${process.env.ADMIN_URL}${product.photo_url}`; try { - photoMessage = await bot.sendPhoto(chatId, product.photo_url, {caption: 'Public photo'}); + photoMessage = await bot.sendPhoto(chatId, photoUrl, {caption: 'Public photo'}); } catch (e) { photoMessage = await bot.sendPhoto(chatId, "./corrupt-photo.jpg", {caption: 'Public photo'}) } } if (product.hidden_photo_url) { + const hiddenPhotoUrl = product.hidden_photo_url.startsWith('http') ? product.hidden_photo_url : `${process.env.ADMIN_URL}${product.hidden_photo_url}`; try { - hiddenPhotoMessage = await bot.sendPhoto(chatId, product.hidden_photo_url, {caption: 'Hidden photo'}); + hiddenPhotoMessage = await bot.sendPhoto(chatId, hiddenPhotoUrl, {caption: 'Hidden photo'}); } catch (e) { hiddenPhotoMessage = await bot.sendPhoto(chatId, "./corrupt-photo.jpg", {caption: 'Hidden photo'}) } diff --git a/src/handlers/userHandlers/userProductHandler.js b/src/handlers/userHandlers/userProductHandler.js index fd2790d..b1ff606 100644 --- a/src/handlers/userHandlers/userProductHandler.js +++ b/src/handlers/userHandlers/userProductHandler.js @@ -356,9 +356,11 @@ export default class UserProductHandler { // Отправляем фото, если оно существует let photoMessage; if (product.photo_url) { + const photoUrl = product.photo_url.startsWith('http') ? product.photo_url : `${process.env.ADMIN_URL}${product.photo_url}`; try { - photoMessage = await bot.sendPhoto(chatId, product.photo_url, { caption: 'Public photo' }); + photoMessage = await bot.sendPhoto(chatId, photoUrl, { caption: 'Public photo' }); } catch (e) { + logger.warn({ err: e }, 'Failed to send product photo'); photoMessage = await bot.sendPhoto(chatId, "./corrupt-photo.jpg", { caption: 'Public photo' }); } } @@ -706,9 +708,11 @@ export default class UserProductHandler { // Отправляем Hidden Photo let hiddenPhotoMessage; if (product.hidden_photo_url) { + const hiddenPhotoUrl = product.hidden_photo_url.startsWith('http') ? product.hidden_photo_url : `${process.env.ADMIN_URL}${product.hidden_photo_url}`; try { - hiddenPhotoMessage = await bot.sendPhoto(chatId, product.hidden_photo_url, { caption: 'Hidden photo' }); + hiddenPhotoMessage = await bot.sendPhoto(chatId, hiddenPhotoUrl, { caption: 'Hidden photo' }); } catch (e) { + logger.warn({ err: e }, 'Failed to send hidden photo'); hiddenPhotoMessage = await bot.sendPhoto(chatId, "./corrupt-photo.jpg", { caption: 'Hidden photo' }); } } diff --git a/src/handlers/userHandlers/userPurchaseHandler.js b/src/handlers/userHandlers/userPurchaseHandler.js index 0fe96ee..a4e5881 100644 --- a/src/handlers/userHandlers/userPurchaseHandler.js +++ b/src/handlers/userHandlers/userPurchaseHandler.js @@ -180,8 +180,9 @@ export default class UserPurchaseHandler { // Отправляем Hidden Photo let hiddenPhotoMessage; if (product.hidden_photo_url) { + const hiddenPhotoUrl = product.hidden_photo_url.startsWith('http') ? product.hidden_photo_url : `${process.env.ADMIN_URL}${product.hidden_photo_url}`; try { - hiddenPhotoMessage = await bot.sendPhoto(chatId, product.hidden_photo_url, { caption: 'Hidden photo' }); + hiddenPhotoMessage = await bot.sendPhoto(chatId, hiddenPhotoUrl, { caption: 'Hidden photo' }); } catch (e) { hiddenPhotoMessage = await bot.sendPhoto(chatId, "./corrupt-photo.jpg", { caption: 'Hidden photo' }); }