version 1.2: Telegram-бот, мониторинг сервера на дашборде, README
- bot.py: Telegram-бот (/users, /create, /delete, /warp, /stats, /dashboard)
- install-bot.sh: установщик бота (systemd, конфиг /etc/mita/bot.json)
- app.py: API /api/bot/{status,install,uninstall,config}, /api/stats (CPU/RAM/диск)
- templates: раздел «Telegram бот» в UI, мониторинг CPU/RAM/диск на дашборде (2s auto-refresh)
- mita-ctl.sh: пункт 11 «Telegram-бот», удаление бота в полном uninstall
- install.sh: опциональная установка бота с инструкцией
- README.md: описание проекта и дорожная карта
This commit is contained in:
@@ -0,0 +1,114 @@
|
||||
# MSWP — mita Server Web Panel
|
||||
|
||||
Веб-панель и система развёртывания для прокси-протокола **[mieru/mita](https://github.com/enfein/mieru)**, с опциональной интеграцией **Cloudflare WARP** и CLI-утилитой `mita-ctl`.
|
||||
|
||||
> Проект рассчитан на Ubuntu/Debian VPS. Состоит из трёх компонентов: серверная часть установки, веб-панель на Flask и CLI-утилита.
|
||||
|
||||
---
|
||||
|
||||
## Архитектура
|
||||
|
||||
```
|
||||
curl -Ls ... | sudo bash (bootstrap.sh)
|
||||
│
|
||||
▼
|
||||
install.sh — установка mita, WARP, веб-панели, cron
|
||||
├── mita (systemd) — прокси-сервер
|
||||
├── WARP Docker — SOCKS5-прокси (127.0.0.1:40000)
|
||||
├── mita-panel (Flask) — веб-интерфейс управления
|
||||
├── mita-ctl — CLI-меню
|
||||
├── manage-users.sh — CLI для пользователей
|
||||
└── auto-update (cron) — ежедневное обновление списков WARP
|
||||
```
|
||||
|
||||
## Технологии
|
||||
|
||||
| Компонент | Технология |
|
||||
|---|---|
|
||||
| Бэкенд | Python 3 / Flask / Gunicorn |
|
||||
| Фронтенд | Vanilla JS, Jinja2, QRCode.js |
|
||||
| Прокси | mieru/mita |
|
||||
| WARP | seiry/cloudflare-warp-proxy (Docker) |
|
||||
| CLI | Bash |
|
||||
| Защита | fail2ban, UFW/iptables, Let's Encrypt |
|
||||
|
||||
## Быстрый старт
|
||||
|
||||
```bash
|
||||
curl -Ls https://raw.githubusercontent.com/grendervilll/mitactl/main/bootstrap.sh | sudo bash
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Структура проекта
|
||||
|
||||
```
|
||||
.
|
||||
├── app.py # Бэкенд Flask
|
||||
├── bot.py # Telegram-бот
|
||||
├── bootstrap.sh # Bootstrap-установщик
|
||||
├── install.sh # Полная установка сервера
|
||||
├── install-panel.sh # Установка только панели
|
||||
├── install-bot.sh # Установка Telegram-бота
|
||||
├── mita-ctl.sh # CLI-утилита
|
||||
├── manage-users.sh # CLI управление пользователями
|
||||
├── domains.txt # Список доменов для WARP
|
||||
├── templates/
|
||||
│ ├── base.html # Базовый шаблон: сайдбар, модалы, toast
|
||||
│ ├── index.html # SPA: Dashboard, Users, SSL, Security, Bot
|
||||
│ └── login.html # Страница входа
|
||||
└── static/
|
||||
└── qrcode.min.js # QRCode.js (minified)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Дорожная карта
|
||||
|
||||
### ✅ Версия 1.1 (реализовано)
|
||||
|
||||
- [x] Автоматическая установка mita сервера (systemd)
|
||||
- [x] Интеграция Cloudflare WARP через Docker
|
||||
- [x] Веб-панель с секретным путём (Flask + Gunicorn, path-based security)
|
||||
- [x] Тёмная тема, адаптивный UI: Dashboard, Users, SSL, Security
|
||||
- [x] CRUD пользователей (создание, удаление, список) — авто/ручное/bulk
|
||||
- [x] Статистика трафика: день, 7 дней, 30 дней
|
||||
- [x] Онлайн-статус пользователей
|
||||
- [x] Генерация клиентских конфигов (mieru JSON и sing-box JSON)
|
||||
- [x] Скачивание конфигов и отображение QR-кодов
|
||||
- [x] Управление WARP на пользователя (toggle + правила: домены, IP, geosite, geoip, URL источники)
|
||||
- [x] SSL-сертификаты: self-signed (10 лет) и Let's Encrypt через веб-интерфейс
|
||||
- [x] Интеграция fail2ban: rate-limiting панели + fail2ban jail (пресеты)
|
||||
- [x] Управление файрволом (UFW/iptables) через CLI и панель
|
||||
- [x] Режим SSH-туннеля: панель только на 127.0.0.1
|
||||
- [x] Ежедневное автообновление списков WARP (cron + dlc.dat v2fly)
|
||||
- [x] CLI `mita-ctl` — интерактивное меню (10+ функций)
|
||||
- [x] CLI `manage-users.sh` — add/remove/list/quota
|
||||
- [x] Полное удаление всех компонентов (+ self-destruct утилиты)
|
||||
- [x] Инструкция для клиента Karing (iOS/Android)
|
||||
- [x] Защита сессий: динамический Secure cookie, in-memory rate limiter
|
||||
- [x] Bootstrap-установщик (одна команда)
|
||||
- [x] Telegram-бот: создание/удаление пользователей, трафик, WARP, CPU/RAM/диск
|
||||
- [x] Установка бота через веб-панель, CLI и полный установщик
|
||||
|
||||
### ⬜ Запланировано
|
||||
|
||||
- [ ] Квоты пользователей в веб-панели (бэкенд готов, нужен UI)
|
||||
- [ ] Per-user egress routing (обход ограничения mita)
|
||||
- [ ] Поддержка нескольких протоколов (TCP + UDP) — доработка
|
||||
- [ ] Автоматический импорт конфигов в Karing (упрощение ручной инструкции)
|
||||
- [ ] Резервное копирование и восстановление конфигурации
|
||||
- [ ] Мониторинг состояния сервера (CPU, RAM, диск) в Dashboard
|
||||
- [ ] Уведомления (Telegram-бот / email) при событиях
|
||||
- [ ] Мультиязычность интерфейса (i18n: RU/EN)
|
||||
- [ ] API-ключи для внешней интеграции
|
||||
- [ ] Графики использования трафика
|
||||
- [ ] Автоматическое обновление mita бинарника
|
||||
- [ ] Развёртывание панели в Docker
|
||||
- [ ] Логирование действий администратора (audit log)
|
||||
|
||||
---
|
||||
|
||||
## Лицензия
|
||||
|
||||
Проект разработан **grendervill**. Репозиторий: `https://gt.netbird.mhspx.ru/grendervill/MSWP.git`
|
||||
@@ -2,7 +2,8 @@
|
||||
"""
|
||||
mita Web Panel — Flask backend
|
||||
"""
|
||||
import os, json, subprocess, secrets, string, random, ipaddress, socket, time, logging
|
||||
import os, json, re, subprocess, secrets, string, random, ipaddress, socket, time, logging
|
||||
import psutil
|
||||
from datetime import datetime, timedelta
|
||||
from functools import wraps
|
||||
from pathlib import Path
|
||||
@@ -174,14 +175,14 @@ def get_warp_ip():
|
||||
return "недоступен"
|
||||
|
||||
def get_traffic_stats():
|
||||
"""Возвращает трафик по всем пользователям за 7 и 30 дней."""
|
||||
"""Возвращает трафик и статус активности по всем пользователям."""
|
||||
raw = mita_cmd("get", "users")
|
||||
week_total = month_total = 0.0
|
||||
users_stats = []
|
||||
today = datetime.now().date()
|
||||
|
||||
# mita get users output format (v3.x):
|
||||
# USER LAST ACTIVE 1 DAY DOWN 1 DAY UP 30 DAYS DOWN 30 DAYS UP
|
||||
# username 2024-01-01 1.2MiB 0.5MiB 12.3GiB 4.5GiB
|
||||
# mita get users output format:
|
||||
# User LastActive 1DayDown 1DayUp 7DaysDown 7DaysUp 30DaysDown 30DaysUp
|
||||
lines = raw.splitlines()
|
||||
data_lines = []
|
||||
for line in lines:
|
||||
@@ -191,19 +192,50 @@ def get_traffic_stats():
|
||||
data_lines.append(line)
|
||||
|
||||
for line in data_lines:
|
||||
# Split on 2+ spaces to handle names with spaces
|
||||
import re as _re
|
||||
parts = _re.split(r"\s{2,}", line.strip())
|
||||
parts = re.split(r"\s{2,}", line.strip())
|
||||
if len(parts) < 3:
|
||||
# Try single-space split as fallback
|
||||
parts = line.split()
|
||||
if len(parts) < 3:
|
||||
continue
|
||||
try:
|
||||
name = parts[0]
|
||||
# Find traffic columns — they contain units like MiB, GiB, B, KiB
|
||||
traffic_parts = [p for p in parts[1:] if any(u in p for u in ["B","iB","KiB","MiB","GiB","TiB"])]
|
||||
if len(traffic_parts) >= 4:
|
||||
|
||||
# Parse LAST ACTIVE field (parts[1])
|
||||
last_active_raw = parts[1].strip() if len(parts) > 1 else ""
|
||||
online = False
|
||||
last_active_display = "никогда"
|
||||
if last_active_raw and last_active_raw.lower() not in ("never", "-", "n/a", "никогда"):
|
||||
try:
|
||||
la_date = datetime.strptime(last_active_raw[:10], "%Y-%m-%d").date()
|
||||
delta = (today - la_date).days
|
||||
if delta == 0:
|
||||
online = True
|
||||
last_active_display = "сегодня"
|
||||
elif delta == 1:
|
||||
last_active_display = "вчера"
|
||||
elif delta < 7:
|
||||
last_active_display = f"{delta} дн. назад"
|
||||
elif delta < 30:
|
||||
last_active_display = f"{delta // 7} нед. назад"
|
||||
else:
|
||||
last_active_display = f"{delta} дн. назад"
|
||||
except Exception:
|
||||
last_active_display = last_active_raw
|
||||
|
||||
# Find traffic columns — contain units like MiB, GiB, KiB, B
|
||||
# mita output: 1DayDown 1DayUp 7DaysDown 7DaysUp 30DaysDown 30DaysUp
|
||||
traffic_parts = [p for p in parts[1:] if any(u in p for u in ["TiB","GiB","MiB","KiB","iB","B"])]
|
||||
d1_down = d1_up = d7_down = d7_up = d30_down = d30_up = 0.0
|
||||
if len(traffic_parts) >= 6:
|
||||
d1_down = _parse_traffic(traffic_parts[0])
|
||||
d1_up = _parse_traffic(traffic_parts[1])
|
||||
d7_down = _parse_traffic(traffic_parts[2])
|
||||
d7_up = _parse_traffic(traffic_parts[3])
|
||||
d30_down = _parse_traffic(traffic_parts[4])
|
||||
d30_up = _parse_traffic(traffic_parts[5])
|
||||
elif len(traffic_parts) >= 4:
|
||||
d1_down = _parse_traffic(traffic_parts[0])
|
||||
d1_up = _parse_traffic(traffic_parts[1])
|
||||
d30_down = _parse_traffic(traffic_parts[2])
|
||||
d30_up = _parse_traffic(traffic_parts[3])
|
||||
elif len(traffic_parts) >= 2:
|
||||
@@ -211,12 +243,17 @@ def get_traffic_stats():
|
||||
d30_up = _parse_traffic(traffic_parts[1])
|
||||
else:
|
||||
continue
|
||||
d7_approx = (d30_down + d30_up) / 30 * 7
|
||||
|
||||
d7_bytes = d7_down + d7_up if (d7_down + d7_up) > 0 else (d30_down + d30_up) / 30 * 7
|
||||
month_total += d30_down + d30_up
|
||||
week_total += d7_approx
|
||||
week_total += d7_bytes
|
||||
users_stats.append({
|
||||
"name": name,
|
||||
"month_mb": round((d30_down + d30_up) / 1024 / 1024, 1),
|
||||
"name": name,
|
||||
"online": online,
|
||||
"last_active": last_active_display,
|
||||
"day_mb": round((d1_down + d1_up) / 1024 / 1024, 2),
|
||||
"week_mb": round(d7_bytes / 1024 / 1024, 2),
|
||||
"month_mb": round((d30_down + d30_up) / 1024 / 1024, 2),
|
||||
})
|
||||
except Exception:
|
||||
continue
|
||||
@@ -443,6 +480,19 @@ def api_dashboard():
|
||||
"mita_running": _mita_running(),
|
||||
})
|
||||
|
||||
@app.route(f"{BASE}/api/stats")
|
||||
@login_required
|
||||
def api_stats():
|
||||
return jsonify({
|
||||
"cpu": psutil.cpu_percent(interval=0.5),
|
||||
"ram_pct": psutil.virtual_memory().percent,
|
||||
"ram_used": psutil.virtual_memory().used,
|
||||
"ram_total": psutil.virtual_memory().total,
|
||||
"disk_pct": psutil.disk_usage("/").percent,
|
||||
"disk_used": psutil.disk_usage("/").used,
|
||||
"disk_total": psutil.disk_usage("/").total,
|
||||
})
|
||||
|
||||
def _mita_running():
|
||||
try:
|
||||
r = subprocess.run(["systemctl","is-active","mita"],
|
||||
@@ -451,6 +501,16 @@ def _mita_running():
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
# ── API: users stats (traffic + online status) ────────────────────────────────
|
||||
@app.route(f"{BASE}/api/users/stats")
|
||||
@login_required
|
||||
def api_users_stats():
|
||||
try:
|
||||
stats = get_traffic_stats()
|
||||
return jsonify({"users": stats["users"]})
|
||||
except Exception as e:
|
||||
return jsonify({"users": [], "error": str(e)})
|
||||
|
||||
# ── API: users ────────────────────────────────────────────────────────────────
|
||||
@app.route(f"{BASE}/api/users", methods=["GET"])
|
||||
@login_required
|
||||
@@ -940,7 +1000,7 @@ def api_fail2ban_configure():
|
||||
|
||||
# Write fail2ban filter for mita-panel
|
||||
filter_content = """[Definition]
|
||||
failregex = ^.*"POST /[^"]+/login[^"]*" 4(01|29).*$
|
||||
failregex = ^<HOST> .+ "POST /[^"]+/login[^"]*" 4(?:01|29).*$
|
||||
ignoreregex =
|
||||
"""
|
||||
jail_content = f"""[mita-panel]
|
||||
@@ -951,19 +1011,137 @@ logpath = /var/log/mita-panel-access.log
|
||||
maxretry = {max_retry}
|
||||
bantime = {ban_time}
|
||||
findtime = {ban_time}
|
||||
action = iptables-multiport[name=mita-panel, port="http,https,8080,8443", protocol=tcp]
|
||||
"""
|
||||
try:
|
||||
Path("/etc/fail2ban/filter.d/mita-panel.conf").write_text(filter_content)
|
||||
Path("/etc/fail2ban/jail.d/mita-panel.conf").write_text(jail_content)
|
||||
subprocess.run(["systemctl","enable","fail2ban","--now"], capture_output=True)
|
||||
subprocess.run(["fail2ban-client","reload"], capture_output=True)
|
||||
ok_msg = "Настройки сохранены и применены"
|
||||
r_reload = subprocess.run(["systemctl","restart","fail2ban"], capture_output=True)
|
||||
r_active = subprocess.run(["systemctl","is-active","fail2ban"], capture_output=True, text=True)
|
||||
if r_active.stdout.strip() == "active":
|
||||
ok_msg = "Настройки сохранены и применены"
|
||||
else:
|
||||
ok_msg = "Настройки сохранены, но fail2ban не запустился — проверьте journalctl -u fail2ban"
|
||||
except Exception as e:
|
||||
ok_msg = f"Настройки сохранены (fail2ban: {e})"
|
||||
|
||||
return jsonify({"ok": True, "message": ok_msg})
|
||||
|
||||
# ── API: Telegram Bot ─────────────────────────────────────────────────────────
|
||||
BOT_CONFIG_PATH = "/etc/mita/bot.json"
|
||||
BOT_DIR = "/opt/mita-bot"
|
||||
BOT_SERVICE = "mita-bot"
|
||||
BOT_INSTALLER = "/opt/mita-bot/install-bot.sh"
|
||||
|
||||
def _bot_installed():
|
||||
return os.path.exists("/etc/systemd/system/mita-bot.service")
|
||||
|
||||
def _bot_running():
|
||||
try:
|
||||
r = subprocess.run(["systemctl","is-active",BOT_SERVICE],
|
||||
capture_output=True, text=True, timeout=3)
|
||||
return r.stdout.strip() == "active"
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def _load_bot_config():
|
||||
try:
|
||||
return json.loads(Path(BOT_CONFIG_PATH).read_text())
|
||||
except Exception:
|
||||
return {}
|
||||
|
||||
@app.route(f"{BASE}/api/bot/status")
|
||||
@login_required
|
||||
def api_bot_status():
|
||||
installed = _bot_installed()
|
||||
running = _bot_running() if installed else False
|
||||
cfg = _load_bot_config() if installed else {}
|
||||
return jsonify({
|
||||
"installed": installed,
|
||||
"running": running,
|
||||
"token": (cfg.get("token","")[:8]+"…") if cfg.get("token") else "",
|
||||
"admin_ids": cfg.get("admin_ids", []),
|
||||
})
|
||||
|
||||
@app.route(f"{BASE}/api/bot/install", methods=["POST"])
|
||||
@login_required
|
||||
def api_bot_install():
|
||||
data = request.get_json(silent=True) or {}
|
||||
token = data.get("token","").strip()
|
||||
admin_id = str(data.get("admin_id","")).strip()
|
||||
|
||||
if not token:
|
||||
return jsonify({"ok": False, "error": "Токен обязателен"}), 400
|
||||
if not admin_id or not admin_id.isdigit():
|
||||
return jsonify({"ok": False, "error": "Telegram ID должен быть числом"}), 400
|
||||
|
||||
# Ищем install-bot.sh рядом с app.py или в /opt/mita-panel
|
||||
installer = None
|
||||
for candidate in [
|
||||
os.path.join(os.path.dirname(os.path.abspath(__file__)), "install-bot.sh"),
|
||||
"/opt/mita-panel/install-bot.sh",
|
||||
]:
|
||||
if os.path.exists(candidate):
|
||||
installer = candidate
|
||||
break
|
||||
|
||||
if not installer:
|
||||
return jsonify({"ok": False, "error": "install-bot.sh не найден. Поместите его в /opt/mita-panel/"}), 500
|
||||
|
||||
# Передаём в install-bot.sh через env
|
||||
env = os.environ.copy()
|
||||
env["BOT_TOKEN_NONINTERACTIVE"] = token
|
||||
env["BOT_ADMIN_NONINTERACTIVE"] = admin_id
|
||||
|
||||
r = subprocess.run(["bash", installer], capture_output=True, text=True,
|
||||
env=env, timeout=120)
|
||||
if r.returncode != 0:
|
||||
return jsonify({"ok": False, "error": r.stderr.strip() or r.stdout.strip()[-500:]}), 500
|
||||
|
||||
running = _bot_running()
|
||||
return jsonify({"ok": True, "running": running, "output": r.stdout.strip()[-500:]})
|
||||
|
||||
@app.route(f"{BASE}/api/bot/uninstall", methods=["POST"])
|
||||
@login_required
|
||||
def api_bot_uninstall():
|
||||
subprocess.run(["systemctl","stop",BOT_SERVICE], capture_output=True)
|
||||
subprocess.run(["systemctl","disable",BOT_SERVICE], capture_output=True)
|
||||
Path("/etc/systemd/system/mita-bot.service").unlink(missing_ok=True)
|
||||
subprocess.run(["systemctl","daemon-reload"], capture_output=True)
|
||||
import shutil
|
||||
if os.path.isdir(BOT_DIR):
|
||||
shutil.rmtree(BOT_DIR)
|
||||
Path(BOT_CONFIG_PATH).unlink(missing_ok=True)
|
||||
return jsonify({"ok": True})
|
||||
|
||||
@app.route(f"{BASE}/api/bot/config", methods=["GET","POST"])
|
||||
@login_required
|
||||
def api_bot_config():
|
||||
if request.method == "POST":
|
||||
data = request.get_json(silent=True) or {}
|
||||
token = data.get("token","").strip()
|
||||
admin_id = str(data.get("admin_id","")).strip()
|
||||
|
||||
if not _bot_installed():
|
||||
return jsonify({"ok": False, "error": "Бот не установлен"}), 400
|
||||
|
||||
cfg = _load_bot_config()
|
||||
if token:
|
||||
cfg["token"] = token
|
||||
if admin_id:
|
||||
if admin_id not in cfg.get("admin_ids", []):
|
||||
cfg.setdefault("admin_ids", []).append(admin_id)
|
||||
|
||||
Path(BOT_CONFIG_PATH).write_text(json.dumps(cfg, indent=2))
|
||||
subprocess.run(["systemctl","restart",BOT_SERVICE], capture_output=True)
|
||||
return jsonify({"ok": True})
|
||||
|
||||
cfg = _load_bot_config()
|
||||
return jsonify({
|
||||
"token": cfg.get("token",""),
|
||||
"admin_ids": cfg.get("admin_ids", []),
|
||||
})
|
||||
|
||||
# ── API: скачать конфиг как файл ─────────────────────────────────────────────
|
||||
@app.route(f"{BASE}/api/users/config/download")
|
||||
@login_required
|
||||
|
||||
@@ -0,0 +1,403 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
mita Telegram Bot — управление сервером через Telegram
|
||||
"""
|
||||
import os, json, re, subprocess, psutil, logging
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
logging.basicConfig(level=logging.INFO,
|
||||
format="%(asctime)s %(levelname)s [bot] %(message)s")
|
||||
_log = logging.getLogger("mita_bot")
|
||||
|
||||
BOT_CONFIG = os.environ.get("BOT_CONFIG", "/etc/mita/bot.json")
|
||||
MITA_CONFIG = "/etc/mita/server_config.json"
|
||||
PANEL_CONFIG = "/etc/mita/panel.json"
|
||||
|
||||
try:
|
||||
from telegram import Update
|
||||
from telegram.ext import Application, CommandHandler, ContextTypes
|
||||
except ImportError:
|
||||
_log.error("python-telegram-bot не установлен. pip install python-telegram-bot")
|
||||
raise SystemExit(1)
|
||||
|
||||
# ── config ────────────────────────────────────────────────────────────────────
|
||||
def load_bot_config():
|
||||
try:
|
||||
return json.loads(Path(BOT_CONFIG).read_text())
|
||||
except Exception:
|
||||
return {}
|
||||
|
||||
def load_mita_config():
|
||||
try:
|
||||
return json.loads(Path(MITA_CONFIG).read_text())
|
||||
except Exception:
|
||||
return {}
|
||||
|
||||
def load_panel_config():
|
||||
try:
|
||||
return json.loads(Path(PANEL_CONFIG).read_text())
|
||||
except Exception:
|
||||
return {}
|
||||
|
||||
# ── helpers ───────────────────────────────────────────────────────────────────
|
||||
def mita_cmd(*args):
|
||||
r = subprocess.run(["mita", *args], capture_output=True, text=True)
|
||||
return r.stdout.strip()
|
||||
|
||||
def _parse_traffic(s: str) -> float:
|
||||
s = s.strip()
|
||||
mul = {"TiB":1024**4,"GiB":1024**3,"MiB":1024**2,"KiB":1024,"B":1}
|
||||
for suffix, factor in mul.items():
|
||||
if s.endswith(suffix):
|
||||
try:
|
||||
return float(s[:-len(suffix)]) * factor
|
||||
except ValueError:
|
||||
return 0.0
|
||||
try:
|
||||
return float(s)
|
||||
except ValueError:
|
||||
return 0.0
|
||||
|
||||
def fmt_bytes(b):
|
||||
if b >= 1024**4: return f"{b/1024**4:.2f} TiB"
|
||||
if b >= 1024**3: return f"{b/1024**3:.2f} GiB"
|
||||
if b >= 1024**2: return f"{b/1024**2:.1f} MiB"
|
||||
if b >= 1024: return f"{b/1024:.0f} KiB"
|
||||
return f"{b:.0f} B"
|
||||
|
||||
def get_server_ip():
|
||||
try:
|
||||
return subprocess.run(
|
||||
["curl","-s","--max-time","5","ifconfig.me"],
|
||||
capture_output=True, text=True).stdout.strip() or "?"
|
||||
except Exception:
|
||||
return "?"
|
||||
|
||||
def _mita_running():
|
||||
try:
|
||||
r = subprocess.run(["systemctl","is-active","mita"],
|
||||
capture_output=True, text=True, timeout=3)
|
||||
return r.stdout.strip() == "active"
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
# ── auth ──────────────────────────────────────────────────────────────────────
|
||||
def admin_only(func):
|
||||
async def wrapper(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
user_id = str(update.effective_user.id)
|
||||
cfg = load_bot_config()
|
||||
admins = cfg.get("admin_ids", [])
|
||||
if user_id not in admins:
|
||||
await update.message.reply_text("⛔ Доступ запрещён.")
|
||||
return
|
||||
return await func(update, context)
|
||||
return wrapper
|
||||
|
||||
# ── commands ──────────────────────────────────────────────────────────────────
|
||||
@admin_only
|
||||
async def cmd_start(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
await update.message.reply_text(
|
||||
"🤖 *mita Bot* готов к работе\\.\n\n"
|
||||
"/users \\- список пользователей\n"
|
||||
"/create \\[имя\\] \\- создать пользователя\n"
|
||||
"/delete \\<имя\\> \\- удалить пользователя\n"
|
||||
"/warp \\<имя\\> on\\|off \\- управление WARP\n"
|
||||
"/stats \\- статистика сервера\n"
|
||||
"/dashboard \\- полный дашборд\n"
|
||||
"/help \\- все команды",
|
||||
parse_mode="MarkdownV2"
|
||||
)
|
||||
|
||||
@admin_only
|
||||
async def cmd_help(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
await update.message.reply_text(
|
||||
"📋 *Команды*\n\n"
|
||||
"/users \\- список пользователей с трафиком и статусом\n"
|
||||
"/create \\- создать пользователя \\(случайное имя\\)\n"
|
||||
"/create `имя` \\- создать с указанным именем\n"
|
||||
"/delete `имя` \\- удалить пользователя\n"
|
||||
"/warp `имя` on \\- включить WARP\n"
|
||||
"/warp `имя` off \\- выключить WARP\n"
|
||||
"/stats \\- CPU, RAM, диск\n"
|
||||
"/dashboard \\- полный обзор сервера\n"
|
||||
"/config `имя` \\- клиентский конфиг",
|
||||
parse_mode="MarkdownV2"
|
||||
)
|
||||
|
||||
@admin_only
|
||||
async def cmd_users(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
cfg = load_mita_config()
|
||||
users = cfg.get("users", [])
|
||||
if not users:
|
||||
await update.message.reply_text("👤 Нет пользователей.")
|
||||
return
|
||||
|
||||
raw = mita_cmd("get", "users")
|
||||
pc = load_panel_config()
|
||||
warp_users = set(pc.get("warp_users", []))
|
||||
|
||||
lines = []
|
||||
today = datetime.now().date()
|
||||
|
||||
data_lines = []
|
||||
for line in raw.splitlines():
|
||||
line = line.strip()
|
||||
if not line or line.upper().startswith("USER"):
|
||||
continue
|
||||
data_lines.append(line)
|
||||
|
||||
stats_map = {}
|
||||
for line in data_lines:
|
||||
parts = re.split(r"\s{2,}", line.strip())
|
||||
if len(parts) < 3:
|
||||
parts = line.split()
|
||||
if len(parts) < 3:
|
||||
continue
|
||||
try:
|
||||
name = parts[0]
|
||||
traffic_parts = [p for p in parts[1:] if any(u in p for u in ["TiB","GiB","MiB","KiB","iB","B"])]
|
||||
d1 = d30 = 0.0
|
||||
if len(traffic_parts) >= 6:
|
||||
d1 = _parse_traffic(traffic_parts[0]) + _parse_traffic(traffic_parts[1])
|
||||
d30= _parse_traffic(traffic_parts[4]) + _parse_traffic(traffic_parts[5])
|
||||
elif len(traffic_parts) >= 4:
|
||||
d1 = _parse_traffic(traffic_parts[0]) + _parse_traffic(traffic_parts[1])
|
||||
d30= _parse_traffic(traffic_parts[2]) + _parse_traffic(traffic_parts[3])
|
||||
elif len(traffic_parts) >= 2:
|
||||
d30= _parse_traffic(traffic_parts[0]) + _parse_traffic(traffic_parts[1])
|
||||
|
||||
online = False
|
||||
la_raw = parts[1].strip() if len(parts) > 1 else ""
|
||||
if la_raw and la_raw.lower() not in ("never","-","n/a","никогда"):
|
||||
try:
|
||||
la_date = datetime.strptime(la_raw[:10], "%Y-%m-%d").date()
|
||||
if (today - la_date).days == 0:
|
||||
online = True
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
stats_map[name] = {"online": online, "day_mb": round(d1/1024/1024, 2),
|
||||
"month_mb": round(d30/1024/1024, 2)}
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
for u in users:
|
||||
name = u["name"]
|
||||
s = stats_map.get(name, {})
|
||||
w = "🟢" if name in warp_users else "⚪"
|
||||
o = "🟢" if s.get("online") else "🔴"
|
||||
day = f"{s.get('day_mb', 0):.1f} МБ" if s.get("day_mb") else "—"
|
||||
mon = f"{s.get('month_mb', 0):.1f} МБ" if s.get("month_mb") else "—"
|
||||
lines.append(f"{o} `{name}` {w} \\| день: {day} \\| мес: {mon}")
|
||||
|
||||
await update.message.reply_text(
|
||||
"👥 *Пользователи*\n\n" + "\n".join(lines),
|
||||
parse_mode="MarkdownV2"
|
||||
)
|
||||
|
||||
@admin_only
|
||||
async def cmd_create(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
args = context.args
|
||||
cfg = load_mita_config()
|
||||
existing = {u["name"] for u in cfg.get("users", [])}
|
||||
|
||||
if args:
|
||||
name = args[0].strip()
|
||||
else:
|
||||
import secrets
|
||||
adj = ["swift","brave","quiet","cool","sharp","calm","bright","dark","wild","free"]
|
||||
nouns = ["fox","hawk","river","storm","ember","peak","orbit","tide","frost","spark"]
|
||||
name = f"{secrets.choice(adj)}_{secrets.choice(nouns)}_{secrets.randbelow(9000)+1000}"
|
||||
while name in existing:
|
||||
name = f"{secrets.choice(adj)}_{secrets.choice(nouns)}_{secrets.randbelow(9000)+1000}"
|
||||
|
||||
if name in existing:
|
||||
await update.message.reply_text(f"❌ Пользователь `{name}` уже существует\\.")
|
||||
return
|
||||
|
||||
import string
|
||||
chars = string.ascii_letters + string.digits + "!@#%^*_-=+?."
|
||||
password = "".join(secrets.choice(chars) for _ in range(64))
|
||||
|
||||
cfg.setdefault("users", []).append({"name": name, "password": password})
|
||||
Path(MITA_CONFIG).write_text(json.dumps(cfg, indent=2, ensure_ascii=False))
|
||||
|
||||
subprocess.run(["mita", "apply", "config", MITA_CONFIG], capture_output=True)
|
||||
subprocess.run(["systemctl", "restart", "mita"], capture_output=True)
|
||||
|
||||
ip = get_server_ip()
|
||||
bindings = cfg.get("portBindings", [{}])[0]
|
||||
port = bindings.get("portRange", str(bindings.get("port","?")))
|
||||
|
||||
await update.message.reply_text(
|
||||
f"✅ Пользователь *{name}* создан\\.\n\n"
|
||||
f"Сервер: `{ip}`\n"
|
||||
f"Порт: `{port}`\n"
|
||||
f"Пароль: `{password}`\n\n"
|
||||
f"Получить JSON\\-конфиг: /config `{name}`",
|
||||
parse_mode="MarkdownV2"
|
||||
)
|
||||
|
||||
@admin_only
|
||||
async def cmd_delete(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
if not context.args:
|
||||
await update.message.reply_text("❌ Укажите имя: `/delete имя`", parse_mode="MarkdownV2")
|
||||
return
|
||||
name = context.args[0].strip()
|
||||
cfg = load_mita_config()
|
||||
cfg["users"] = [u for u in cfg.get("users", []) if u["name"] != name]
|
||||
Path(MITA_CONFIG).write_text(json.dumps(cfg, indent=2, ensure_ascii=False))
|
||||
subprocess.run(["mita", "apply", "config", MITA_CONFIG], capture_output=True)
|
||||
subprocess.run(["systemctl", "restart", "mita"], capture_output=True)
|
||||
await update.message.reply_text(f"🗑 Пользователь `{name}` удалён\\.", parse_mode="MarkdownV2")
|
||||
|
||||
@admin_only
|
||||
async def cmd_warp(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
if len(context.args) < 2:
|
||||
await update.message.reply_text(
|
||||
"❌ Формат: `/warp имя on` или `/warp имя off`", parse_mode="MarkdownV2")
|
||||
return
|
||||
name = context.args[0].strip()
|
||||
action = context.args[1].lower()
|
||||
if action not in ("on", "off"):
|
||||
await update.message.reply_text("❌ Укажите `on` или `off`\\.", parse_mode="MarkdownV2")
|
||||
return
|
||||
|
||||
pc = load_panel_config()
|
||||
warp_users = set(pc.get("warp_users", []))
|
||||
if action == "on":
|
||||
warp_users.add(name)
|
||||
else:
|
||||
warp_users.discard(name)
|
||||
pc["warp_users"] = list(warp_users)
|
||||
Path(PANEL_CONFIG).write_text(json.dumps(pc, indent=2))
|
||||
await update.message.reply_text(
|
||||
f"🔄 WARP для `{name}`: *{action.upper()}* \\(сохранено в panel\\.json\\)",
|
||||
parse_mode="MarkdownV2"
|
||||
)
|
||||
|
||||
@admin_only
|
||||
async def cmd_config(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
if not context.args:
|
||||
await update.message.reply_text("❌ Укажите имя: `/config имя`", parse_mode="MarkdownV2")
|
||||
return
|
||||
name = context.args[0].strip()
|
||||
cfg = load_mita_config()
|
||||
user = next((u for u in cfg.get("users", []) if u["name"] == name), None)
|
||||
if not user:
|
||||
await update.message.reply_text(f"❌ Пользователь `{name}` не найден\\.", parse_mode="MarkdownV2")
|
||||
return
|
||||
ip = get_server_ip()
|
||||
bindings = cfg.get("portBindings", [{}])[0]
|
||||
port = bindings.get("portRange", str(bindings.get("port","?")))
|
||||
data = json.dumps({
|
||||
"profiles": [{"profileName": "default",
|
||||
"user": {"name": user["name"], "password": user["password"]},
|
||||
"servers": [{"ipAddress": ip,
|
||||
"portBindings": [{"portRange": port,
|
||||
"protocol": bindings.get("protocol","TCP")}]}]}],
|
||||
"activeProfile": "default",
|
||||
"rpcPort": 8964,
|
||||
"socks5Port": 1080,
|
||||
}, indent=2, ensure_ascii=False)
|
||||
|
||||
await update.message.reply_text(f"```json\n{data}\n```", parse_mode=None)
|
||||
|
||||
@admin_only
|
||||
async def cmd_stats(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
cpu = psutil.cpu_percent(interval=1)
|
||||
mem = psutil.virtual_memory()
|
||||
disk = psutil.disk_usage("/")
|
||||
boot = datetime.fromtimestamp(psutil.boot_time()).strftime("%d.%m.%Y %H:%M")
|
||||
|
||||
await update.message.reply_text(
|
||||
f"📊 *Сервер*\n\n"
|
||||
f"CPU: `{cpu:.1f}%`\n"
|
||||
f"RAM: `{mem.percent:.1f}%` \\({fmt_bytes(mem.used)} / {fmt_bytes(mem.total)}\\)\n"
|
||||
f"Диск: `{disk.percent:.1f}%` \\({fmt_bytes(disk.used)} / {fmt_bytes(disk.total)}\\)\n"
|
||||
f"Аптайм: с `{boot}`\n"
|
||||
f"mita: {'🟢 работает' if _mita_running() else '🔴 остановлен'}",
|
||||
parse_mode="MarkdownV2"
|
||||
)
|
||||
|
||||
@admin_only
|
||||
async def cmd_dashboard(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
cpu = psutil.cpu_percent(interval=1)
|
||||
mem = psutil.virtual_memory()
|
||||
disk = psutil.disk_usage("/")
|
||||
ip = get_server_ip()
|
||||
mita_ok = _mita_running()
|
||||
|
||||
warp_ip = "недоступен"
|
||||
try:
|
||||
r = subprocess.run(["curl","-s","--max-time","5","--proxy",
|
||||
"socks5h://127.0.0.1:40000","https://ifconfig.me"],
|
||||
capture_output=True, text=True)
|
||||
warp_ip = r.stdout.strip()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
cfg = load_mita_config()
|
||||
user_count = len(cfg.get("users", []))
|
||||
bindings = cfg.get("portBindings", [{}])[0]
|
||||
port = bindings.get("portRange", str(bindings.get("port","?")))
|
||||
|
||||
traffic = None
|
||||
try:
|
||||
raw = mita_cmd("get", "users")
|
||||
d_lines = [l.strip() for l in raw.splitlines() if l.strip() and not l.upper().startswith("USER")]
|
||||
week_total = month_total = 0.0
|
||||
for line in d_lines:
|
||||
parts = re.split(r"\s{2,}", line.strip())
|
||||
if len(parts) < 3:
|
||||
parts = line.split()
|
||||
if len(parts) < 3:
|
||||
continue
|
||||
tp = [p for p in parts[1:] if any(u in p for u in ["TiB","GiB","MiB","KiB","iB","B"])]
|
||||
if len(tp) >= 6:
|
||||
week_total += _parse_traffic(tp[2]) + _parse_traffic(tp[3])
|
||||
month_total += _parse_traffic(tp[4]) + _parse_traffic(tp[5])
|
||||
traffic = (round(week_total/1024**3, 2), round(month_total/1024**3, 2))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
w, m = traffic or (0, 0)
|
||||
|
||||
await update.message.reply_text(
|
||||
f"📊 *Дашборд*\n\n"
|
||||
f"IP сервера: `{ip}`\n"
|
||||
f"IP WARP: `{warp_ip}`\n"
|
||||
f"Порт mita: `{port}`\n"
|
||||
f"Пользователей: `{user_count}`\n"
|
||||
f"Трафик нед\\.: `{w} ГБ`\n"
|
||||
f"Трафик мес\\.: `{m} ГБ`\n"
|
||||
f"mita: {'🟢' if mita_ok else '🔴'}\n"
|
||||
f"CPU: `{cpu:.1f}%` RAM: `{mem.percent:.1f}%` Диск: `{disk.percent:.1f}%`",
|
||||
parse_mode="MarkdownV2"
|
||||
)
|
||||
|
||||
# ── main ──────────────────────────────────────────────────────────────────────
|
||||
def main():
|
||||
cfg = load_bot_config()
|
||||
token = os.environ.get("BOT_TOKEN", cfg.get("token", ""))
|
||||
if not token:
|
||||
_log.error("BOT_TOKEN не задан — ни в env, ни в %s", BOT_CONFIG)
|
||||
raise SystemExit(1)
|
||||
|
||||
app = Application.builder().token(token).build()
|
||||
app.add_handler(CommandHandler("start", cmd_start))
|
||||
app.add_handler(CommandHandler("help", cmd_help))
|
||||
app.add_handler(CommandHandler("users", cmd_users))
|
||||
app.add_handler(CommandHandler("create", cmd_create))
|
||||
app.add_handler(CommandHandler("delete", cmd_delete))
|
||||
app.add_handler(CommandHandler("warp", cmd_warp))
|
||||
app.add_handler(CommandHandler("config", cmd_config))
|
||||
app.add_handler(CommandHandler("stats", cmd_stats))
|
||||
app.add_handler(CommandHandler("dashboard", cmd_dashboard))
|
||||
|
||||
_log.info("Бот запущен")
|
||||
app.run_polling()
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
+170
@@ -0,0 +1,170 @@
|
||||
#!/bin/bash
|
||||
# =============================================================================
|
||||
# Установка Telegram-бота для управления mita
|
||||
# =============================================================================
|
||||
set -euo pipefail
|
||||
|
||||
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'; CYAN='\033[0;36m'; BOLD='\033[1m'; NC='\033[0m'
|
||||
info() { echo -e "${BLUE}[INFO]${NC} $*"; }
|
||||
ok() { echo -e "${GREEN}[OK]${NC} $*"; }
|
||||
warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
|
||||
error() { echo -e "${RED}[ERROR]${NC} $*"; exit 1; }
|
||||
section() { echo -e "\n${CYAN}══════ $* ══════${NC}"; }
|
||||
|
||||
[[ $EUID -ne 0 ]] && error "Запустите от root: sudo bash install-bot.sh"
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
BOT_DIR="/opt/mita-bot"
|
||||
BOT_CONFIG="/etc/mita/bot.json"
|
||||
|
||||
# ── поиск bot.py ────────────────────────────────────────────────────
|
||||
BOT_SRC=""
|
||||
for candidate in "$SCRIPT_DIR/bot.py" "$SCRIPT_DIR/mita-bot-pkg/bot.py"; do
|
||||
if [[ -f "$candidate" ]]; then
|
||||
BOT_SRC="$candidate"
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ -z "$BOT_SRC" ]]; then
|
||||
error "bot.py не найден рядом со скриптом. Убедитесь что bot.py лежит в одной папке с install-bot.sh"
|
||||
fi
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════
|
||||
section "Установка Telegram-бота mita"
|
||||
|
||||
# ── неинтерактивный режим (вызов из веб-панели или API) ────────────
|
||||
if [[ -n "${BOT_TOKEN_NONINTERACTIVE:-}" && -n "${BOT_ADMIN_NONINTERACTIVE:-}" ]]; then
|
||||
BOT_TOKEN="$BOT_TOKEN_NONINTERACTIVE"
|
||||
ADMIN_ID="$BOT_ADMIN_NONINTERACTIVE"
|
||||
info "Неинтерактивный режим"
|
||||
else
|
||||
echo ""
|
||||
echo -e " ${BOLD}Для работы бота нужны:${NC}"
|
||||
echo -e " ${YELLOW}1.${NC} Токен бота от @BotFather"
|
||||
echo -e " ${YELLOW}2.${NC} Ваш Telegram ID (узнать: @userinfobot)"
|
||||
echo ""
|
||||
|
||||
# ── токен ───────────────────────────────────────────────────────────
|
||||
read -r -p "Введите токен бота (от @BotFather): " BOT_TOKEN
|
||||
if [[ -z "$BOT_TOKEN" ]]; then
|
||||
error "Токен не может быть пустым"
|
||||
fi
|
||||
|
||||
# ── admin ID ─────────────────────────────────────────────────────────
|
||||
read -r -p "Введите ваш Telegram ID (число, узнать через @userinfobot): " ADMIN_ID
|
||||
if [[ -z "$ADMIN_ID" || ! "$ADMIN_ID" =~ ^[0-9]+$ ]]; then
|
||||
error "Telegram ID должен быть числом"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo ""
|
||||
info "Токен: ${BOT_TOKEN:0:8}..."
|
||||
info "Admin ID: $ADMIN_ID"
|
||||
|
||||
# ── зависимости ─────────────────────────────────────────────────────
|
||||
section "Установка зависимостей"
|
||||
apt-get update -qq
|
||||
apt-get install -y -qq python3 python3-pip python3-venv
|
||||
|
||||
if python3 -c "import psutil" 2>/dev/null; then
|
||||
ok "psutil уже установлен"
|
||||
else
|
||||
info "Установка psutil..."
|
||||
pip3 install psutil
|
||||
fi
|
||||
|
||||
if python3 -c "from telegram.ext import Application" 2>/dev/null; then
|
||||
ok "python-telegram-bot уже установлен"
|
||||
else
|
||||
info "Установка python-telegram-bot..."
|
||||
pip3 install python-telegram-bot
|
||||
fi
|
||||
|
||||
# ── копирование файлов ──────────────────────────────────────────────
|
||||
section "Установка бота"
|
||||
mkdir -p "$BOT_DIR"
|
||||
cp "$BOT_SRC" "$BOT_DIR/bot.py"
|
||||
chmod +x "$BOT_DIR/bot.py"
|
||||
ok "bot.py скопирован в $BOT_DIR/"
|
||||
|
||||
# ── конфиг ──────────────────────────────────────────────────────────
|
||||
mkdir -p /etc/mita
|
||||
if [[ -f "$BOT_CONFIG" ]]; then
|
||||
python3 -c "
|
||||
import json
|
||||
with open('$BOT_CONFIG') as f: d=json.load(f)
|
||||
d['token']='$BOT_TOKEN'
|
||||
admin_ids=d.get('admin_ids',[])
|
||||
if '$ADMIN_ID' not in admin_ids: admin_ids.append('$ADMIN_ID')
|
||||
d['admin_ids']=admin_ids
|
||||
with open('$BOT_CONFIG','w') as f: json.dump(d,f,indent=2)
|
||||
"
|
||||
ok "Конфиг обновлён"
|
||||
else
|
||||
cat > "$BOT_CONFIG" <<JSON
|
||||
{
|
||||
"token": "$BOT_TOKEN",
|
||||
"admin_ids": ["$ADMIN_ID"]
|
||||
}
|
||||
JSON
|
||||
ok "Конфиг создан: $BOT_CONFIG"
|
||||
fi
|
||||
|
||||
# ── env файл ────────────────────────────────────────────────────────
|
||||
cat > "$BOT_DIR/env" <<EOF
|
||||
BOT_CONFIG=$BOT_CONFIG
|
||||
BOT_TOKEN=$BOT_TOKEN
|
||||
EOF
|
||||
|
||||
# ── systemd unit ────────────────────────────────────────────────────
|
||||
cat > /etc/systemd/system/mita-bot.service <<UNIT
|
||||
[Unit]
|
||||
Description=mita Telegram Bot
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
WorkingDirectory=$BOT_DIR
|
||||
EnvironmentFile=$BOT_DIR/env
|
||||
ExecStart=/usr/bin/python3 $BOT_DIR/bot.py
|
||||
Restart=always
|
||||
RestartSec=10
|
||||
User=root
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
UNIT
|
||||
|
||||
systemctl daemon-reload
|
||||
systemctl enable mita-bot
|
||||
systemctl restart mita-bot
|
||||
|
||||
sleep 2
|
||||
if systemctl is-active --quiet mita-bot; then
|
||||
ok "Бот запущен и добавлен в автозагрузку"
|
||||
else
|
||||
warn "Бот не запустился. Проверьте: journalctl -u mita-bot -f"
|
||||
fi
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════
|
||||
section "Установка завершена"
|
||||
|
||||
echo ""
|
||||
echo -e " ${GREEN}Бот установлен!${NC}"
|
||||
echo ""
|
||||
echo -e " ${BOLD}Команды управления:${NC}"
|
||||
echo -e " ${YELLOW}systemctl status mita-bot${NC} — статус бота"
|
||||
echo -e " ${YELLOW}systemctl restart mita-bot${NC} — перезапустить"
|
||||
echo -e " ${YELLOW}journalctl -u mita-bot -f${NC} — логи"
|
||||
echo ""
|
||||
echo -e " ${BOLD}Файлы:${NC}"
|
||||
echo -e " Бот: ${YELLOW}$BOT_DIR/bot.py${NC}"
|
||||
echo -e " Конфиг: ${YELLOW}$BOT_CONFIG${NC}"
|
||||
echo -e " Unit: ${YELLOW}/etc/systemd/system/mita-bot.service${NC}"
|
||||
echo ""
|
||||
echo -e " ${BOLD}Для добавления других админов:${NC}"
|
||||
echo -e " Отредактируйте ${YELLOW}$BOT_CONFIG${NC} и добавьте ID в массив admin_ids,"
|
||||
echo -e " затем: ${YELLOW}systemctl restart mita-bot${NC}"
|
||||
echo ""
|
||||
+2
-2
@@ -74,8 +74,8 @@ PANEL_DIR="/opt/mita-panel"
|
||||
mkdir -p "$PANEL_DIR"
|
||||
|
||||
python3 -m venv "$PANEL_DIR/venv"
|
||||
"$PANEL_DIR/venv/bin/pip" install -q flask gunicorn pyyaml 2>/dev/null
|
||||
ok "Python venv и Flask установлены"
|
||||
"$PANEL_DIR/venv/bin/pip" install -q flask gunicorn pyyaml psutil 2>/dev/null
|
||||
ok "Python venv, Flask и зависимости установлены"
|
||||
|
||||
# ── копирование файлов ─────────────────────────────────────────────
|
||||
section "Копирование файлов панели"
|
||||
|
||||
+86
-1
@@ -629,12 +629,20 @@ bash "$UPDATE_SCRIPT" && ok "Списки обновлены, конфиг пр
|
||||
section "Настройка firewall"
|
||||
|
||||
if command -v ufw &>/dev/null && ufw status | grep -q "Status: active"; then
|
||||
# UFW требует двоеточие для диапазонов: 3000:3010/tcp, а не 3000-3010/tcp
|
||||
if [[ "$MITA_PORT_RANGE" == *"-"* ]]; then
|
||||
ufw allow "${MITA_PORT_RANGE}/${MITA_PROTOCOL,,}" comment "mita proxy" 2>/dev/null || true
|
||||
_UFW_START="${MITA_PORT_RANGE%-*}"; _UFW_END="${MITA_PORT_RANGE#*-}"
|
||||
ufw allow "${_UFW_START}:${_UFW_END}/${MITA_PROTOCOL,,}" comment "mita proxy" 2>/dev/null || true
|
||||
else
|
||||
ufw allow "${MITA_PORT_RANGE}/${MITA_PROTOCOL,,}" comment "mita proxy" 2>/dev/null || true
|
||||
fi
|
||||
ok "UFW: порты $MITA_PORT_RANGE/$MITA_PROTOCOL открыты"
|
||||
echo ""
|
||||
warn "ВАЖНО: UFW управляет только локальным фаерволом ОС."
|
||||
warn "Если ваш VPS у облачного провайдера (Hetzner, AWS, GCP, DigitalOcean, Vultr и др.),"
|
||||
warn "откройте порты ${MITA_PORT_RANGE} (${MITA_PROTOCOL}) ТАКЖЕ в консоли провайдера"
|
||||
warn "(Security Groups / Firewall / Network Rules) — иначе подключения не будет."
|
||||
echo ""
|
||||
elif command -v iptables &>/dev/null; then
|
||||
if [[ "$MITA_PORT_RANGE" == *"-"* ]]; then
|
||||
START_PORT="${MITA_PORT_RANGE%-*}"; END_PORT="${MITA_PORT_RANGE#*-}"
|
||||
@@ -732,6 +740,83 @@ else
|
||||
fi
|
||||
fi
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════
|
||||
# УСТАНОВКА TELEGRAM-БОТА (опционально)
|
||||
# ═══════════════════════════════════════════════════════════════════════
|
||||
section "Telegram-бот"
|
||||
|
||||
BOT_INSTALLER=""
|
||||
for candidate in \
|
||||
"$SCRIPT_DIR/install-bot.sh" \
|
||||
"$SCRIPT_DIR/mita-bot-pkg/install-bot.sh"
|
||||
do
|
||||
if [[ -f "$candidate" ]]; then
|
||||
BOT_INSTALLER="$candidate"
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ -z "$BOT_INSTALLER" ]]; then
|
||||
warn "install-bot.sh не найден рядом со скриптом."
|
||||
warn "Поместите bot.py и install-bot.sh рядом с install.sh для установки бота."
|
||||
else
|
||||
echo -e "Установить Telegram-бота для управления сервером?"
|
||||
echo -e " Бот позволит: создавать/удалять пользователей, смотреть трафик,"
|
||||
echo -e " включать WARP, мониторить CPU/RAM/диск — всё через Telegram."
|
||||
echo ""
|
||||
echo -e " ${GREEN}y${NC} — да, установить сейчас (потребуется токен и Telegram ID)"
|
||||
echo -e " ${YELLOW}n${NC} — пропустить (установить позже: bash $BOT_INSTALLER или через веб-панель)"
|
||||
echo ""
|
||||
read -r -p "Установить бота? [y/N]: " INSTALL_BOT
|
||||
echo ""
|
||||
|
||||
if [[ "${INSTALL_BOT,,}" == "y" || "${INSTALL_BOT,,}" == "yes" ]]; then
|
||||
echo ""
|
||||
echo -e "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
||||
echo -e "${CYAN} Инструкция по созданию бота${NC}"
|
||||
echo -e "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
||||
echo ""
|
||||
echo -e " ${BOLD}1.${NC} Откройте Telegram и найдите ${GREEN}@BotFather${NC}"
|
||||
echo -e " Отправьте команду ${YELLOW}/newbot${NC}"
|
||||
echo -e " Следуйте инструкциям: придумайте имя и username бота"
|
||||
echo -e " В ответ вы получите ${BOLD}токен${NC} — длинную строку вида:"
|
||||
echo -e " ${YELLOW}1234567890:AAHdqTcvCH1vGWJxfSeofSAs0K5PALDsaw${NC}"
|
||||
echo ""
|
||||
echo -e " ${BOLD}2.${NC} Найдите ${GREEN}@userinfobot${NC} и нажмите /start"
|
||||
echo -e " Он покажет ваш ${BOLD}Telegram ID${NC} — число (например 123456789)"
|
||||
echo ""
|
||||
echo -e " ${BOLD}3.${NC} Когда токен и ID готовы — нажмите Enter для продолжения"
|
||||
|
||||
while true; do
|
||||
echo ""
|
||||
read -r -p " Впишите токен от @BotFather: " BOT_TOKEN
|
||||
if [[ -z "$BOT_TOKEN" ]]; then
|
||||
warn "Токен не может быть пустым"
|
||||
continue
|
||||
fi
|
||||
break
|
||||
done
|
||||
|
||||
while true; do
|
||||
echo ""
|
||||
read -r -p " Впишите ваш Telegram ID (число): " BOT_ADMIN
|
||||
if [[ -z "$BOT_ADMIN" || ! "$BOT_ADMIN" =~ ^[0-9]+$ ]]; then
|
||||
warn "Telegram ID должен быть числом (узнайте у @userinfobot)"
|
||||
continue
|
||||
fi
|
||||
break
|
||||
done
|
||||
|
||||
echo ""
|
||||
info "Запуск установки Telegram-бота..."
|
||||
BOT_TOKEN_NONINTERACTIVE="$BOT_TOKEN" BOT_ADMIN_NONINTERACTIVE="$BOT_ADMIN" bash "$BOT_INSTALLER"
|
||||
else
|
||||
info "Пропускаем установку бота."
|
||||
echo -e " Установить позже: ${YELLOW}bash $BOT_INSTALLER${NC}"
|
||||
echo -e " Или через веб-панель: раздел «Telegram бот»"
|
||||
fi
|
||||
fi
|
||||
|
||||
# ── Установка mita-ctl как системной команды ──────────────────────
|
||||
section "Установка утилиты управления mita-ctl"
|
||||
CTL_SRC="$SCRIPT_DIR/mita-ctl.sh"
|
||||
|
||||
+194
-19
@@ -76,6 +76,7 @@ main_menu() {
|
||||
echo -e " ${BOLD}8.${NC} Рекомендации по безопасности VPS"
|
||||
echo -e " ${RED}${BOLD}9.${NC} ${RED}Полное удаление mita, панели и WARP${NC}"
|
||||
echo -e " ${BOLD}10.${NC} Показать текущую конфигурацию"
|
||||
echo -e " ${BOLD}11.${NC} Управление Telegram-ботом"
|
||||
echo -e " ${BOLD}0.${NC} Выход"
|
||||
echo ""
|
||||
read -r -p " Выберите пункт: " choice
|
||||
@@ -90,6 +91,7 @@ main_menu() {
|
||||
8) menu_security ;;
|
||||
9) menu_uninstall ;;
|
||||
10) menu_show_config ;;
|
||||
11) menu_bot ;;
|
||||
0) exit 0 ;;
|
||||
*) warn "Неверный выбор" ;;
|
||||
esac
|
||||
@@ -332,16 +334,30 @@ print(b.get('portRange',b.get('port','2100-2110')))
|
||||
|
||||
_fw_open() {
|
||||
local port="$1" proto="${2:-tcp}"
|
||||
command -v ufw &>/dev/null && ufw status | grep -q "Status: active" \
|
||||
&& ufw allow "$port/$proto" 2>/dev/null || true
|
||||
iptables -I INPUT -p "$proto" --dport "$port" -j ACCEPT 2>/dev/null || true
|
||||
if [[ "$port" == *"-"* ]]; then
|
||||
local _s="${port%-*}" _e="${port#*-}"
|
||||
command -v ufw &>/dev/null && ufw status | grep -q "Status: active" \
|
||||
&& ufw allow "${_s}:${_e}/${proto}" 2>/dev/null || true
|
||||
iptables -I INPUT -p "$proto" --dport "${_s}:${_e}" -j ACCEPT 2>/dev/null || true
|
||||
else
|
||||
command -v ufw &>/dev/null && ufw status | grep -q "Status: active" \
|
||||
&& ufw allow "${port}/${proto}" 2>/dev/null || true
|
||||
iptables -I INPUT -p "$proto" --dport "$port" -j ACCEPT 2>/dev/null || true
|
||||
fi
|
||||
}
|
||||
|
||||
_fw_close() {
|
||||
local port="$1" proto="${2:-tcp}"
|
||||
command -v ufw &>/dev/null && ufw status | grep -q "Status: active" \
|
||||
&& ufw delete allow "$port/$proto" 2>/dev/null || true
|
||||
iptables -D INPUT -p "$proto" --dport "$port" -j ACCEPT 2>/dev/null || true
|
||||
if [[ "$port" == *"-"* ]]; then
|
||||
local _s="${port%-*}" _e="${port#*-}"
|
||||
command -v ufw &>/dev/null && ufw status | grep -q "Status: active" \
|
||||
&& ufw delete allow "${_s}:${_e}/${proto}" 2>/dev/null || true
|
||||
iptables -D INPUT -p "$proto" --dport "${_s}:${_e}" -j ACCEPT 2>/dev/null || true
|
||||
else
|
||||
command -v ufw &>/dev/null && ufw status | grep -q "Status: active" \
|
||||
&& ufw delete allow "${port}/${proto}" 2>/dev/null || true
|
||||
iptables -D INPUT -p "$proto" --dport "$port" -j ACCEPT 2>/dev/null || true
|
||||
fi
|
||||
}
|
||||
|
||||
# ════════════════════════════════════════════════════════════════════════════
|
||||
@@ -1030,7 +1046,7 @@ with open('$PANEL_CONFIG','w') as f: json.dump(d,f,indent=2)
|
||||
mkdir -p /etc/fail2ban/filter.d /etc/fail2ban/jail.d
|
||||
cat > /etc/fail2ban/filter.d/mita-panel.conf << EOF
|
||||
[Definition]
|
||||
failregex = ^.*"POST /[^"]+/login[^"]*" 4(01|29).*$
|
||||
failregex = ^<HOST> .+ "POST /[^"]+/login[^"]*" 4(?:01|29).*$
|
||||
ignoreregex =
|
||||
EOF
|
||||
cat > /etc/fail2ban/jail.d/mita-panel.conf << EOF
|
||||
@@ -1042,10 +1058,12 @@ logpath = /var/log/mita-panel-access.log
|
||||
maxretry = $max_retry
|
||||
bantime = $ban_time
|
||||
findtime = $ban_time
|
||||
action = iptables-multiport[name=mita-panel, port="http,https,8080,8443", protocol=tcp]
|
||||
EOF
|
||||
systemctl restart fail2ban 2>/dev/null || true
|
||||
ok "fail2ban настроен: $max_retry попыток / ${ban_time}с"
|
||||
if systemctl restart fail2ban 2>/dev/null && systemctl is-active --quiet fail2ban; then
|
||||
ok "fail2ban настроен и запущен: $max_retry попыток / ${ban_time}с"
|
||||
else
|
||||
warn "fail2ban настроен, но не запустился. Проверьте: journalctl -u fail2ban -n 20"
|
||||
fi
|
||||
else
|
||||
warn "fail2ban не установлен — лимиты сохранены только в panel.json"
|
||||
fi
|
||||
@@ -1055,6 +1073,141 @@ EOF
|
||||
ok "Настройки применены. Попыток: $max_retry, бан: ${ban_time}с"
|
||||
}
|
||||
|
||||
# ════════════════════════════════════════════════════════════════════════════
|
||||
# 11. TELEGRAM БОТ
|
||||
# ════════════════════════════════════════════════════════════════════════════
|
||||
menu_bot() {
|
||||
section "Управление Telegram-ботом"
|
||||
|
||||
BOT_SERVICE="mita-bot"
|
||||
BOT_DIR="/opt/mita-bot"
|
||||
BOT_CONFIG="/etc/mita/bot.json"
|
||||
|
||||
local installed=false running=false
|
||||
if [[ -f "/etc/systemd/system/${BOT_SERVICE}.service" ]]; then
|
||||
installed=true
|
||||
systemctl is-active --quiet "$BOT_SERVICE" && running=true
|
||||
fi
|
||||
|
||||
echo ""
|
||||
if $installed; then
|
||||
echo -e " Статус: ${GREEN}● Установлен${NC}"
|
||||
if $running; then
|
||||
echo -e " Состояние: ${GREEN}● Активен${NC}"
|
||||
else
|
||||
echo -e " Состояние: ${RED}● Остановлен${NC}"
|
||||
fi
|
||||
if [[ -f "$BOT_CONFIG" ]]; then
|
||||
local token
|
||||
token=$(python3 -c "import json; d=json.load(open('$BOT_CONFIG')); print(d.get('token','')[:8]+'…')" 2>/dev/null || echo "—")
|
||||
echo -e " Токен: ${YELLOW}${token}${NC}"
|
||||
local admins
|
||||
admins=$(python3 -c "import json; d=json.load(open('$BOT_CONFIG')); print(', '.join(d.get('admin_ids',[])))" 2>/dev/null || echo "—")
|
||||
echo -e " Админы ID: ${YELLOW}${admins}${NC}"
|
||||
fi
|
||||
else
|
||||
echo -e " Статус: ${YELLOW}● Не установлен${NC}"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo -e " ${BOLD}1.${NC} Установить / обновить бота"
|
||||
echo -e " ${BOLD}2.${NC} Перезапустить бота"
|
||||
echo -e " ${BOLD}3.${NC} Добавить админа (Telegram ID)"
|
||||
echo -e " ${BOLD}4.${NC} Логи бота (journalctl)"
|
||||
if $installed; then
|
||||
echo -e " ${RED}${BOLD}5.${NC} ${RED}Удалить бота${NC}"
|
||||
fi
|
||||
echo -e " ${BOLD}0.${NC} Назад"
|
||||
echo ""
|
||||
read -r -p " Выбор: " choice
|
||||
|
||||
case "$choice" in
|
||||
1) _bot_install ;;
|
||||
2) _bot_restart ;;
|
||||
3) _bot_add_admin ;;
|
||||
4) journalctl -u "$BOT_SERVICE" -f --no-pager -n 50 ;;
|
||||
5)
|
||||
if $installed; then
|
||||
read -r -p " Точно удалить бота? [y/N]: " del
|
||||
if [[ "${del,,}" == "y" ]]; then
|
||||
_bot_uninstall
|
||||
fi
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
_bot_install() {
|
||||
section "Установка Telegram-бота"
|
||||
|
||||
local installer=""
|
||||
for candidate in /opt/mita-panel/install-bot.sh "$SCRIPT_DIR/install-bot.sh"; do
|
||||
[[ -f "$candidate" ]] && { installer="$candidate"; break; }
|
||||
done
|
||||
|
||||
if [[ -z "$installer" ]]; then
|
||||
error "install-bot.sh не найден. Поместите его в /opt/mita-panel/"
|
||||
pause
|
||||
return
|
||||
fi
|
||||
|
||||
read -r -p "Токен бота (от @BotFather): " token
|
||||
[[ -z "$token" ]] && { error "Токен обязателен"; return; }
|
||||
|
||||
read -r -p "Ваш Telegram ID (число, @userinfobot): " admin_id
|
||||
[[ -z "$admin_id" || ! "$admin_id" =~ ^[0-9]+$ ]] && { error "ID должен быть числом"; return; }
|
||||
|
||||
BOT_TOKEN_NONINTERACTIVE="$token" BOT_ADMIN_NONINTERACTIVE="$admin_id" bash "$installer"
|
||||
pause
|
||||
}
|
||||
|
||||
_bot_restart() {
|
||||
section "Перезапуск бота"
|
||||
BOT_SERVICE="mita-bot"
|
||||
systemctl restart "$BOT_SERVICE" 2>/dev/null || true
|
||||
sleep 1
|
||||
if systemctl is-active --quiet "$BOT_SERVICE"; then
|
||||
ok "Бот перезапущен"
|
||||
else
|
||||
warn "Бот не запустился. Проверьте: journalctl -u $BOT_SERVICE -f"
|
||||
fi
|
||||
pause
|
||||
}
|
||||
|
||||
_bot_add_admin() {
|
||||
section "Добавление админа"
|
||||
BOT_CONFIG="/etc/mita/bot.json"
|
||||
[[ ! -f "$BOT_CONFIG" ]] && { error "Бот не установлен"; return; }
|
||||
|
||||
read -r -p "Telegram ID нового админа (число): " new_id
|
||||
[[ -z "$new_id" || ! "$new_id" =~ ^[0-9]+$ ]] && { error "ID должен быть числом"; return; }
|
||||
|
||||
python3 -c "
|
||||
import json
|
||||
with open('$BOT_CONFIG') as f: d=json.load(f)
|
||||
ids=d.get('admin_ids',[])
|
||||
if '$new_id' not in ids: ids.append('$new_id')
|
||||
d['admin_ids']=ids
|
||||
with open('$BOT_CONFIG','w') as f: json.dump(d,f,indent=2)
|
||||
"
|
||||
systemctl restart mita-bot 2>/dev/null || true
|
||||
ok "Админ $new_id добавлен"
|
||||
pause
|
||||
}
|
||||
|
||||
_bot_uninstall() {
|
||||
section "Удаление Telegram-бота"
|
||||
BOT_SERVICE="mita-bot"
|
||||
systemctl stop "$BOT_SERVICE" 2>/dev/null || true
|
||||
systemctl disable "$BOT_SERVICE" 2>/dev/null || true
|
||||
rm -f "/etc/systemd/system/${BOT_SERVICE}.service"
|
||||
systemctl daemon-reload 2>/dev/null || true
|
||||
rm -rf /opt/mita-bot
|
||||
rm -f /etc/mita/bot.json
|
||||
ok "Бот удалён"
|
||||
pause
|
||||
}
|
||||
|
||||
# ════════════════════════════════════════════════════════════════════════════
|
||||
# 9. ПОЛНОЕ УДАЛЕНИЕ
|
||||
# ════════════════════════════════════════════════════════════════════════════
|
||||
@@ -1070,6 +1223,7 @@ menu_uninstall() {
|
||||
echo -e " ${YELLOW}•${NC} mita сервер (бинарник, systemd unit, конфиги)"
|
||||
echo -e " ${YELLOW}•${NC} Веб-панель (/opt/mita-panel, systemd unit)"
|
||||
echo -e " ${YELLOW}•${NC} Cloudflare WARP (Docker-контейнер)"
|
||||
echo -e " ${YELLOW}•${NC} Telegram-бот (/opt/mita-bot, systemd unit)"
|
||||
echo -e " ${YELLOW}•${NC} Все пользователи и пароли (/etc/mita/)"
|
||||
echo -e " ${YELLOW}•${NC} SSL-сертификаты, выпущенные для панели"
|
||||
echo -e " ${YELLOW}•${NC} Правила fail2ban для панели"
|
||||
@@ -1200,12 +1354,30 @@ except: print('2100-2110')
|
||||
rm -f /usr/local/bin/mita-update-lists
|
||||
ok "Cron-задачи удалены"
|
||||
|
||||
# ── 7a. Удалить Telegram-бота ────────────────────────────────────
|
||||
if [[ -f "/etc/systemd/system/mita-bot.service" ]]; then
|
||||
info "Удаление Telegram-бота..."
|
||||
systemctl stop mita-bot 2>/dev/null || true
|
||||
systemctl disable mita-bot 2>/dev/null || true
|
||||
rm -f /etc/systemd/system/mita-bot.service
|
||||
systemctl daemon-reload 2>/dev/null || true
|
||||
rm -rf /opt/mita-bot
|
||||
rm -f /etc/mita/bot.json
|
||||
ok "Telegram-бот удалён"
|
||||
fi
|
||||
|
||||
# ── 8. Закрыть порты в firewall ────────────────────────────────
|
||||
info "Закрытие портов mita ($_MITA_RANGE) и панели ($_PANEL_PORT) в firewall..."
|
||||
info "SSH порт $_SSH_PORT остаётся открытым."
|
||||
info "SSH порт $_SSH_PORT будет явно сохранён открытым."
|
||||
|
||||
_UFW_ACTIVE=false
|
||||
command -v ufw &>/dev/null && ufw status 2>/dev/null | grep -q "Status: active" && _UFW_ACTIVE=true
|
||||
|
||||
# ---- UFW ----
|
||||
if command -v ufw &>/dev/null && ufw status 2>/dev/null | grep -q "Status: active"; then
|
||||
if $_UFW_ACTIVE; then
|
||||
# Сначала явно гарантируем SSH, ПОТОМ удаляем остальные правила
|
||||
ufw allow "$_SSH_PORT/tcp" comment "SSH" 2>/dev/null || true
|
||||
|
||||
# Удаляем правило mita (диапазон или одиночный порт)
|
||||
if [[ "$_MITA_RANGE" == *"-"* ]]; then
|
||||
_SP="${_MITA_RANGE%-*}"; _EP="${_MITA_RANGE#*-}"
|
||||
@@ -1218,14 +1390,14 @@ except: print('2100-2110')
|
||||
# Удаляем правило панели
|
||||
ufw delete allow "$_PANEL_PORT/tcp" 2>/dev/null || true
|
||||
# Дополнительно: чистим правила по комментарию (совместимость с разными версиями)
|
||||
ufw status numbered 2>/dev/null | { grep -iE "mita|panel" || true; } | awk -F'[][]' '{print $2}' | sort -rn | while read -r num; do
|
||||
ufw status numbered 2>/dev/null | { grep -iE "mita|panel" || true; } | awk -F'[][]' '{gsub(/[[:space:]]/, "", $2); print $2}' | sort -rn | while read -r num; do
|
||||
[[ -n "$num" ]] && yes | ufw delete "$num" 2>/dev/null || true
|
||||
done
|
||||
ok "UFW: правила удалены"
|
||||
ok "UFW: правила удалены, SSH ($_SSH_PORT) сохранён"
|
||||
fi
|
||||
|
||||
# ---- iptables ----
|
||||
if command -v iptables &>/dev/null; then
|
||||
# ---- iptables (только когда UFW не управляет firewall) ----
|
||||
if ! $_UFW_ACTIVE && command -v iptables &>/dev/null; then
|
||||
if [[ "$_MITA_RANGE" == *"-"* ]]; then
|
||||
_SP="${_MITA_RANGE%-*}"; _EP="${_MITA_RANGE#*-}"
|
||||
iptables -D INPUT -p tcp --dport "${_SP}:${_EP}" -j ACCEPT 2>/dev/null || true
|
||||
@@ -1235,12 +1407,15 @@ except: print('2100-2110')
|
||||
iptables -D INPUT -p udp --dport "$_MITA_RANGE" -j ACCEPT 2>/dev/null || true
|
||||
fi
|
||||
iptables -D INPUT -p tcp --dport "$_PANEL_PORT" -j ACCEPT 2>/dev/null || true
|
||||
# Сохраняем правила если есть iptables-save
|
||||
# Явно обеспечиваем SSH-правило первым в цепочке
|
||||
iptables -D INPUT -p tcp --dport "$_SSH_PORT" -j ACCEPT 2>/dev/null || true
|
||||
iptables -I INPUT 1 -p tcp --dport "$_SSH_PORT" -j ACCEPT 2>/dev/null || true
|
||||
# Сохраняем правила
|
||||
command -v iptables-save &>/dev/null && iptables-save > /etc/iptables/rules.v4 2>/dev/null || true
|
||||
ok "iptables: правила удалены"
|
||||
ok "iptables: правила удалены, SSH ($_SSH_PORT) сохранён"
|
||||
fi
|
||||
|
||||
ok "Firewall: порты mita и панели закрыты. SSH ($_SSH_PORT) не тронут."
|
||||
ok "Firewall: порты mita и панели закрыты. SSH ($_SSH_PORT) открыт."
|
||||
|
||||
systemctl daemon-reload 2>/dev/null || true
|
||||
|
||||
|
||||
+7
-1
@@ -125,6 +125,10 @@ tr:hover td{background:rgba(255,255,255,.02)}
|
||||
<svg class="nav-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"/></svg>
|
||||
Безопасность
|
||||
</button>
|
||||
<button class="nav-item" onclick="showPage('bot',this)">
|
||||
<svg class="nav-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"/></svg>
|
||||
Telegram бот
|
||||
</button>
|
||||
<div class="sidebar-footer"><a href="{{ base }}/logout">Выйти</a></div>
|
||||
</nav>
|
||||
<main class="main">{% block content %}{% endblock %}</main>
|
||||
@@ -148,10 +152,12 @@ function showPage(id,btn){
|
||||
document.querySelectorAll('.nav-item').forEach(n=>n.classList.remove('active'));
|
||||
document.getElementById('page-'+id).classList.add('active');
|
||||
if(btn)btn.classList.add('active');
|
||||
if(id==='dashboard')loadDashboard();
|
||||
if(id==='dashboard'){loadDashboard();if(typeof startStatsPolling==='function')startStatsPolling();}
|
||||
else{if(typeof stopStatsPolling==='function')stopStatsPolling();}
|
||||
if(id==='users')loadUsers();
|
||||
if(id==='ssl')loadSSL();
|
||||
if(id==='security')loadSecurityPage();
|
||||
if(id==='bot')loadBotPage();
|
||||
}
|
||||
function openModal(title,html){
|
||||
document.getElementById('modal-title-text').textContent=title;
|
||||
|
||||
+258
-6
@@ -30,6 +30,32 @@
|
||||
<div id="s-mita-status" style="margin-top:6px"><span class="badge badge-off">—</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-title">Мониторинг сервера</div>
|
||||
<div class="grid-4" style="margin-bottom:0">
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">CPU</div>
|
||||
<div class="stat-value accent" id="s-cpu">—</div>
|
||||
<div class="stat-sub">%</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">RAM</div>
|
||||
<div class="stat-value" id="s-ram-pct">—</div>
|
||||
<div class="stat-sub" id="s-ram-detail" style="white-space:nowrap">—</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">Диск</div>
|
||||
<div class="stat-value" id="s-disk-pct">—</div>
|
||||
<div class="stat-sub" id="s-disk-detail" style="white-space:nowrap">—</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">Аптайм</div>
|
||||
<div class="stat-value" style="font-size:1.1rem" id="s-uptime">—</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-title">Быстрые действия</div>
|
||||
<div class="btn-group">
|
||||
@@ -88,8 +114,19 @@
|
||||
<div class="card-title">Список пользователей</div>
|
||||
<div class="table-wrap">
|
||||
<table>
|
||||
<thead><tr><th>Имя</th><th>WARP вкл</th><th>Правила WARP</th><th>Конфиг клиента</th><th>Действия</th></tr></thead>
|
||||
<tbody id="users-tbody"><tr><td colspan="4" style="color:var(--muted);text-align:center;padding:20px">Загрузка...</td></tr></tbody>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Пользователь</th>
|
||||
<th title="Трафик за последние 24 часа">День</th>
|
||||
<th title="Примерный трафик за 7 дней (на основе 30-дневных данных)">7 дней</th>
|
||||
<th title="Трафик за последние 30 дней">30 дней</th>
|
||||
<th>WARP</th>
|
||||
<th>Правила WARP</th>
|
||||
<th>Конфиг</th>
|
||||
<th>Действия</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="users-tbody"><tr><td colspan="8" style="color:var(--muted);text-align:center;padding:20px">Загрузка...</td></tr></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
@@ -209,6 +246,63 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- TELEGRAM BOT -->
|
||||
<div class="page" id="page-bot">
|
||||
<h1>Telegram бот</h1>
|
||||
<p class="subtitle">Управление сервером через Telegram</p>
|
||||
|
||||
<div class="card" id="bot-status-card">
|
||||
<div class="card-title">Статус бота</div>
|
||||
<div id="bot-status-body"><div style="color:var(--muted)">Загрузка...</div></div>
|
||||
</div>
|
||||
|
||||
<div class="card" id="bot-install-card" style="display:none">
|
||||
<div class="card-title">Установка бота</div>
|
||||
<p style="color:var(--muted);font-size:.875rem;margin-bottom:18px;line-height:1.6">
|
||||
Создайте бота через <strong style="color:var(--accent)">@BotFather</strong> в Telegram,
|
||||
скопируйте токен и узнайте ваш Telegram ID через <strong style="color:var(--accent)">@userinfobot</strong>.
|
||||
</p>
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label>Токен бота (от @BotFather)</label>
|
||||
<input type="text" id="bot-token" placeholder="123456:ABC-DEF1234gh...">
|
||||
</div>
|
||||
<div class="form-group" style="max-width:200px">
|
||||
<label>Ваш Telegram ID</label>
|
||||
<input type="text" id="bot-admin-id" placeholder="123456789">
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn btn-primary" onclick="installBot()">
|
||||
<svg width="14" height="14" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M12 4v16m8-8H4"/></svg>
|
||||
Установить / обновить
|
||||
</button>
|
||||
<div id="bot-install-result" style="margin-top:12px"></div>
|
||||
</div>
|
||||
|
||||
<div class="card" id="bot-manage-card" style="display:none">
|
||||
<div class="card-title">Управление</div>
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-secondary" onclick="restartBot()">⟳ Перезапустить</button>
|
||||
<button class="btn btn-danger" onclick="uninstallBot()">
|
||||
<svg width="14" height="14" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/></svg>
|
||||
Удалить бота
|
||||
</button>
|
||||
</div>
|
||||
<div class="divider"></div>
|
||||
<div class="card-title" style="margin-bottom:12px">Добавить админа</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group" style="max-width:220px">
|
||||
<label>Telegram ID</label>
|
||||
<input type="text" id="bot-add-admin-id" placeholder="123456789">
|
||||
</div>
|
||||
</div>
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-secondary" onclick="addBotAdmin()">Добавить</button>
|
||||
</div>
|
||||
<div id="bot-admin-result" style="margin-top:12px"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
@@ -237,6 +331,54 @@ async function loadDashboard(){
|
||||
}
|
||||
loadDashboard();
|
||||
|
||||
let _statsTimer=null;
|
||||
startStatsPolling();
|
||||
|
||||
function fmtBytes(b){
|
||||
if(b>=1024**4)return(b/1024**4).toFixed(1)+' ТБ';
|
||||
if(b>=1024**3)return(b/1024**3).toFixed(1)+' ГБ';
|
||||
if(b>=1024**2)return(b/1024**2).toFixed(0)+' МБ';
|
||||
if(b>=1024) return(b/1024).toFixed(0)+' КБ';
|
||||
return b+' Б';
|
||||
}
|
||||
|
||||
async function loadServerStats(){
|
||||
const d=await api('/api/stats');
|
||||
if(!d)return;
|
||||
|
||||
const cpuEl=document.getElementById('s-cpu');
|
||||
if(cpuEl){
|
||||
cpuEl.textContent=d.cpu.toFixed(1);
|
||||
cpuEl.style.color=d.cpu>80?'var(--danger)':d.cpu>50?'var(--warn)':'var(--accent)';
|
||||
}
|
||||
|
||||
const ramPctEl=document.getElementById('s-ram-pct');
|
||||
if(ramPctEl){
|
||||
ramPctEl.textContent=d.ram_pct.toFixed(1)+'%';
|
||||
ramPctEl.style.color=d.ram_pct>90?'var(--danger)':d.ram_pct>70?'var(--warn)':'var(--text)';
|
||||
}
|
||||
const ramDetail=document.getElementById('s-ram-detail');
|
||||
if(ramDetail)ramDetail.textContent=fmtBytes(d.ram_used)+' / '+fmtBytes(d.ram_total);
|
||||
|
||||
const diskPctEl=document.getElementById('s-disk-pct');
|
||||
if(diskPctEl){
|
||||
diskPctEl.textContent=d.disk_pct.toFixed(1)+'%';
|
||||
diskPctEl.style.color=d.disk_pct>90?'var(--danger)':d.disk_pct>75?'var(--warn)':'var(--text)';
|
||||
}
|
||||
const diskDetail=document.getElementById('s-disk-detail');
|
||||
if(diskDetail)diskDetail.textContent=fmtBytes(d.disk_used)+' / '+fmtBytes(d.disk_total);
|
||||
}
|
||||
|
||||
function startStatsPolling(){
|
||||
stopStatsPolling();
|
||||
loadServerStats();
|
||||
_statsTimer=setInterval(loadServerStats,2000);
|
||||
}
|
||||
|
||||
function stopStatsPolling(){
|
||||
if(_statsTimer){clearInterval(_statsTimer);_statsTimer=null;}
|
||||
}
|
||||
|
||||
function showClients(){
|
||||
const clients=[
|
||||
{name:'mieru (официальный CLI)',url:'https://github.com/enfein/mieru',desc:'CLI-клиент для macOS, Linux, Windows, OpenWrt'},
|
||||
@@ -271,19 +413,46 @@ function showClients(){
|
||||
// ──────────────── USERS ────────────────
|
||||
let warpUsers=new Set();
|
||||
|
||||
function fmtMB(mb){
|
||||
if(mb==null||mb===0)return'—';
|
||||
if(mb>=1024)return(mb/1024).toFixed(2)+' ГБ';
|
||||
if(mb>=1)return mb.toFixed(1)+' МБ';
|
||||
return Math.round(mb*1024)+' КБ';
|
||||
}
|
||||
|
||||
async function loadUsers(){
|
||||
const [ud,wd]=await Promise.all([api('/api/users'),api('/api/users/warp_status')]);
|
||||
const [ud,wd,sd]=await Promise.all([
|
||||
api('/api/users'),
|
||||
api('/api/users/warp_status'),
|
||||
api('/api/users/stats'),
|
||||
]);
|
||||
if(!ud)return;
|
||||
warpUsers=new Set(wd?.warp_users||[]);
|
||||
const statsMap={};
|
||||
(sd?.users||[]).forEach(s=>{statsMap[s.name]=s;});
|
||||
|
||||
const tbody=document.getElementById('users-tbody');
|
||||
if(!ud.users.length){
|
||||
tbody.innerHTML='<tr><td colspan="4" style="color:var(--muted);text-align:center;padding:20px">Нет пользователей</td></tr>';
|
||||
tbody.innerHTML='<tr><td colspan="8" style="color:var(--muted);text-align:center;padding:20px">Нет пользователей</td></tr>';
|
||||
return;
|
||||
}
|
||||
tbody.innerHTML=ud.users.map(u=>{
|
||||
const wOn=warpUsers.has(u.name);
|
||||
const s=statsMap[u.name]||{};
|
||||
const statusBadge=s.online
|
||||
?'<span class="badge badge-ok" style="margin-left:6px">● онлайн</span>'
|
||||
:s.last_active&&s.last_active!=='никогда'
|
||||
?`<span class="badge badge-off" style="margin-left:6px;white-space:nowrap">● ${escH(s.last_active)}</span>`
|
||||
:'<span class="badge badge-off" style="margin-left:6px">● никогда</span>';
|
||||
return `<tr>
|
||||
<td><strong>${escH(u.name)}</strong></td>
|
||||
<td>
|
||||
<div style="display:flex;align-items:center;flex-wrap:wrap;gap:4px">
|
||||
<strong>${escH(u.name)}</strong>${statusBadge}
|
||||
</div>
|
||||
</td>
|
||||
<td style="white-space:nowrap;font-size:.82rem;color:var(--muted)">${fmtMB(s.day_mb)}</td>
|
||||
<td style="white-space:nowrap;font-size:.82rem;color:var(--muted)">${fmtMB(s.week_mb)}</td>
|
||||
<td style="white-space:nowrap;font-size:.82rem;color:var(--muted)">${fmtMB(s.month_mb)}</td>
|
||||
<td>
|
||||
<label class="toggle" title="Весь WARP для этого пользователя">
|
||||
<input type="checkbox" ${wOn?'checked':''} onchange="setWarp('${escH(u.name)}',this.checked)">
|
||||
@@ -717,7 +886,90 @@ async function getLetsEncrypt(){
|
||||
}
|
||||
}
|
||||
|
||||
// ──────────────── SECURITY / FAIL2BAN ────────────────
|
||||
// ──────────────── TELEGRAM BOT ────────────────
|
||||
async function loadBotPage() {
|
||||
const d = await api('/api/bot/status');
|
||||
if (!d) return;
|
||||
const statusEl = document.getElementById('bot-status-body');
|
||||
const installEl = document.getElementById('bot-install-card');
|
||||
const manageEl = document.getElementById('bot-manage-card');
|
||||
|
||||
if (d.installed) {
|
||||
statusEl.innerHTML = `
|
||||
<div style="display:flex;gap:10px;flex-wrap:wrap;margin-bottom:10px">
|
||||
<span class="badge badge-ok">Установлен</span>
|
||||
${d.running
|
||||
? '<span class="badge badge-ok">● Активен</span>'
|
||||
: '<span class="badge" style="background:rgba(255,95,109,.15);color:var(--danger)">● Остановлен</span>'}
|
||||
</div>
|
||||
<div style="font-size:.85rem;color:var(--muted)">
|
||||
Токен: <code style="color:var(--accent)">${escH(d.token||'—')}</code>
|
||||
</div>
|
||||
<div style="font-size:.85rem;color:var(--muted);margin-top:4px">
|
||||
Админы (Telegram ID): <strong style="color:var(--text)">${escH((d.admin_ids||[]).join(', ')||'—')}</strong>
|
||||
</div>`;
|
||||
installEl.style.display = 'none';
|
||||
manageEl.style.display = '';
|
||||
} else {
|
||||
statusEl.innerHTML = '<div style="color:var(--warn)">Бот не установлен</div>';
|
||||
installEl.style.display = '';
|
||||
manageEl.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
async function installBot() {
|
||||
const token = document.getElementById('bot-token').value.trim();
|
||||
const adminId = document.getElementById('bot-admin-id').value.trim();
|
||||
if (!token) { showToast('Введите токен', true); return; }
|
||||
if (!adminId) { showToast('Введите Telegram ID', true); return; }
|
||||
|
||||
const btn = document.querySelector('#bot-install-card .btn-primary');
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<span class="spinner"></span> Установка...';
|
||||
const d = await api('/api/bot/install', 'POST', {token, admin_id: adminId});
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = '<svg width="14" height="14" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M12 4v16m8-8H4"/></svg> Установить / обновить';
|
||||
|
||||
const el = document.getElementById('bot-install-result');
|
||||
if (d?.ok) {
|
||||
el.innerHTML = `<div class="alert alert-ok">✓ Бот установлен и ${d.running ? 'запущен' : 'установлен (проверьте логи)'}</div>`;
|
||||
showToast('Бот установлен');
|
||||
loadBotPage();
|
||||
} else {
|
||||
el.innerHTML = `<div class="alert alert-err">Ошибка: ${escH(d?.error||d?.output||'')}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
async function uninstallBot() {
|
||||
if (!confirm('Удалить Telegram-бота? Это действие необратимо.')) return;
|
||||
await api('/api/bot/uninstall', 'POST');
|
||||
showToast('Бот удалён');
|
||||
loadBotPage();
|
||||
}
|
||||
|
||||
async function restartBot() {
|
||||
const d = await api('/api/bot/config', 'POST', {});
|
||||
if (d?.ok) {
|
||||
showToast('Бот перезапущен');
|
||||
setTimeout(loadBotPage, 1500);
|
||||
} else {
|
||||
showToast('Ошибка перезапуска', true);
|
||||
}
|
||||
}
|
||||
|
||||
async function addBotAdmin() {
|
||||
const id = document.getElementById('bot-add-admin-id').value.trim();
|
||||
if (!id) { showToast('Введите Telegram ID', true); return; }
|
||||
const d = await api('/api/bot/config', 'POST', {admin_id: id});
|
||||
const el = document.getElementById('bot-admin-result');
|
||||
if (d?.ok) {
|
||||
el.innerHTML = '<div class="alert alert-ok">✓ Админ добавлен</div>';
|
||||
document.getElementById('bot-add-admin-id').value = '';
|
||||
loadBotPage();
|
||||
} else {
|
||||
el.innerHTML = `<div class="alert alert-err">Ошибка: ${escH(d?.error||'')}</div>`;
|
||||
}
|
||||
}
|
||||
async function loadSecurityPage() {
|
||||
const d = await api('/api/fail2ban/status');
|
||||
if (!d) return;
|
||||
|
||||
Reference in New Issue
Block a user