diff --git a/README.md b/README.md index 1c99099..09bfd7c 100644 --- a/README.md +++ b/README.md @@ -78,8 +78,9 @@ curl -Ls https://raw.githubusercontent.com/grendervilll/mitactl/main/bootstrap.s - [x] Генерация клиентских конфигов (mieru JSON и sing-box JSON) - [x] Скачивание конфигов и отображение QR-кодов - [x] Управление WARP на пользователя (toggle + правила: домены, IP, geosite, geoip, URL источники) -- [x] SSL-сертификаты: самоподписной (10 лет) создаётся автоматически при установке, Let's Encrypt через UI -- [x] Кнопка перезапуска панели на вкладке SSL +- [x] SSL-сертификаты: самоподписной (10 лет) авто при установке, LE через UI, кнопка перезапуска +- [x] Переключение режима доступа: IP / SSH-туннель через вкладку SSL в веб-панели +- [x] Автоопределение Telegram ID через API бота при установке (веб-панель, CLI, install.sh) - [x] Интеграция fail2ban: rate-limiting панели + fail2ban jail (пресеты) - [x] Управление файрволом (UFW/iptables) через CLI и панель - [x] Режим SSH-туннеля: панель только на 127.0.0.1 diff --git a/app.py b/app.py index e997bc4..3d777ac 100644 --- a/app.py +++ b/app.py @@ -677,6 +677,99 @@ def api_panel_restart(): return jsonify({"ok": False, "error": r.stderr.strip()}), 500 return jsonify({"ok": True}) +@app.route(f"{BASE}/api/panel/access") +@login_required +def api_panel_access_get(): + pc = load_panel_config() + panel_port = os.environ.get("PANEL_PORT", "8080") + secret = os.environ.get("SECRET_PATH", "") + mode = pc.get("access_mode", "ip") + ssh_port = pc.get("ssh_port", 22) + return jsonify({ + "mode": mode, + "panel_port": panel_port, + "secret": secret, + "ssh_port": ssh_port, + "server_ip": get_server_ip(), + }) + +@app.route(f"{BASE}/api/panel/access", methods=["POST"]) +@login_required +def api_panel_access_set(): + data = request.get_json(silent=True) or {} + mode = data.get("mode", "").strip() + + if mode not in ("ip", "ssh"): + return jsonify({"ok": False, "error": "mode должен быть ip или ssh"}), 400 + + pc = load_panel_config() + panel_port = os.environ.get("PANEL_PORT", "8080") + secret = os.environ.get("SECRET_PATH", "") + + if mode == "ssh": + bind = "127.0.0.1" + # Закрыть порт в firewall + _fw_close_port(panel_port) + else: + bind = "0.0.0.0" + _fw_open_port(panel_port) + + # Обновить start.sh с новым bind + start_sh = "/opt/mita-panel/start.sh" + try: + Path(start_sh).write_text(f"""#!/bin/bash +set -a; source /etc/mita/panel.env; set +a +SSL_ARGS="" +if [[ -n "$SSL_CERT" && -n "$SSL_KEY" && -f "$SSL_CERT" && -f "$SSL_KEY" ]]; then + SSL_ARGS="--certfile=$SSL_CERT --keyfile=$SSL_KEY" +fi +exec /opt/mita-panel/venv/bin/gunicorn \\ + --bind {bind}:$PANEL_PORT \\ + --workers 2 --timeout 120 \\ + --access-logfile /var/log/mita-panel-access.log \\ + --error-logfile /var/log/mita-panel.log \\ + $SSL_ARGS app:app +""") + except Exception: + pass + + # Сохранить режим в panel.json + pc["access_mode"] = mode + Path(PANEL_CONFIG).write_text(json.dumps(pc, indent=2)) + + subprocess.run(["systemctl","restart","mita-panel"], capture_output=True, timeout=10) + + ssh_port = pc.get("ssh_port", 22) + return jsonify({ + "ok": True, + "mode": mode, + "panel_port": panel_port, + "secret": secret, + "server_ip": get_server_ip(), + "ssh_port": ssh_port, + }) + +def _fw_close_port(port): + try: + if subprocess.run(["which","ufw"], capture_output=True).returncode == 0: + subprocess.run(["ufw","delete","allow",f"{port}/tcp"], capture_output=True) + elif subprocess.run(["which","iptables"], capture_output=True).returncode == 0: + subprocess.run(["iptables","-D","INPUT","-p","tcp","--dport",str(port),"-j","ACCEPT"], capture_output=True) + except Exception: + pass + +def _fw_open_port(port): + try: + if subprocess.run(["which","ufw"], capture_output=True).returncode == 0 \ + and subprocess.run(["ufw","status"], capture_output=True, text=True).stdout.find("active") != -1: + subprocess.run(["ufw","allow",f"{port}/tcp","comment","mita-panel"], capture_output=True) + elif subprocess.run(["which","iptables"], capture_output=True).returncode == 0: + r = subprocess.run(["iptables","-C","INPUT","-p","tcp","--dport",str(port),"-j","ACCEPT"], capture_output=True) + if r.returncode != 0: + subprocess.run(["iptables","-I","INPUT","1","-p","tcp","--dport",str(port),"-j","ACCEPT"], capture_output=True) + except Exception: + pass + def _update_env(key, value): env_file = "/etc/mita/panel.env" lines = [] @@ -1132,6 +1225,43 @@ def api_bot_config(): "admin_ids": cfg.get("admin_ids", []), }) +@app.route(f"{BASE}/api/bot/detect-id", methods=["POST"]) +@login_required +def api_bot_detect_id(): + data = request.get_json(silent=True) or {} + token = data.get("token","").strip() + if not token: + return jsonify({"ok": False, "error": "Токен обязателен"}), 400 + + import urllib.request + url = f"https://api.telegram.org/bot{token}/getUpdates" + # Очищаем pending updates перед опросом + try: + req = urllib.request.Request(url + "?offset=-1", headers={"User-Agent": "mita-panel"}) + urllib.request.urlopen(req, timeout=5).read() + except Exception: + pass + + admin_id = "" + for _ in range(20): + time.sleep(3) + try: + req = urllib.request.Request(url, headers={"User-Agent": "mita-panel"}) + with urllib.request.urlopen(req, timeout=5) as resp: + data_raw = json.loads(resp.read()) + ids = [r["message"]["from"]["id"] + for r in data_raw.get("result", []) + if "message" in r and "from" in r["message"]] + if ids: + admin_id = str(ids[-1]) + break + except Exception: + continue + + if admin_id: + return jsonify({"ok": True, "admin_id": admin_id}) + return jsonify({"ok": False, "error": "Не получено ни одного сообщения. Отправьте любое сообщение боту и попробуйте снова."}), 404 + # ── API: скачать конфиг как файл ───────────────────────────────────────────── @app.route(f"{BASE}/api/users/config/download") @login_required diff --git a/install-panel.sh b/install-panel.sh index 7fd1f3e..0a0c315 100755 --- a/install-panel.sh +++ b/install-panel.sh @@ -84,6 +84,9 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" cp "$SCRIPT_DIR/app.py" "$PANEL_DIR/" cp -r "$SCRIPT_DIR/templates" "$PANEL_DIR/" [[ -d "$SCRIPT_DIR/static" ]] && cp -r "$SCRIPT_DIR/static" "$PANEL_DIR/" +# Копируем установщик бота рядом — чтобы работала установка через веб-панель и mita-ctl +[[ -f "$SCRIPT_DIR/install-bot.sh" ]] && cp "$SCRIPT_DIR/install-bot.sh" "$PANEL_DIR/" +[[ -f "$SCRIPT_DIR/bot.py" ]] && cp "$SCRIPT_DIR/bot.py" "$PANEL_DIR/" ok "Файлы скопированы в $PANEL_DIR" # ── генерация секретов ───────────────────────────────────────────── diff --git a/install.sh b/install.sh index 09b397a..026752a 100755 --- a/install.sh +++ b/install.sh @@ -10,7 +10,7 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # ========================= ЦВЕТА ДЛЯ ВЫВОДА ========================= RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m' -BLUE='\033[0;34m'; CYAN='\033[0;36m'; NC='\033[0m' +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} $*"; } @@ -782,10 +782,10 @@ else 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 -e " ${BOLD}2.${NC} Откройте своего бота в Telegram и отправьте любое сообщение" + echo -e " (например «привет»). ID определится автоматически." echo "" - echo -e " ${BOLD}3.${NC} Когда токен и ID готовы — нажмите Enter для продолжения" + echo -e " ${BOLD}3.${NC} Когда токен готов — нажмите Enter для продолжения" while true; do echo "" @@ -797,16 +797,40 @@ else 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 + echo "" + info "Определение вашего Telegram ID..." + echo -e " Отправьте ${YELLOW}любое сообщение${NC} своему боту в Telegram прямо сейчас." + echo -e " Ожидание сообщения (макс. 60 сек)..." + + BOT_ADMIN="" + for i in $(seq 1 30); do + BOT_ADMIN=$(curl -s --max-time 5 "https://api.telegram.org/bot${BOT_TOKEN}/getUpdates" 2>/dev/null | \ + python3 -c " +import sys,json +try: + d=json.load(sys.stdin) + ids=[r['message']['from']['id'] for r in d.get('result',[]) if 'message' in r and 'from' in r['message']] + print(ids[-1] if ids else '') +except: pass +" 2>/dev/null) + [[ -n "$BOT_ADMIN" ]] && break + sleep 2 done + if [[ -z "$BOT_ADMIN" || ! "$BOT_ADMIN" =~ ^[0-9]+$ ]]; then + warn "Не удалось определить ID автоматически." + while true; do + echo "" + read -r -p " Впишите ваш Telegram ID вручную (число): " BOT_ADMIN + if [[ -n "$BOT_ADMIN" && "$BOT_ADMIN" =~ ^[0-9]+$ ]]; then + break + fi + warn "Telegram ID должен быть числом" + done + else + ok "Telegram ID определён: ${GREEN}${BOT_ADMIN}${NC}" + fi + echo "" info "Запуск установки Telegram-бота..." BOT_TOKEN_NONINTERACTIVE="$BOT_TOKEN" BOT_ADMIN_NONINTERACTIVE="$BOT_ADMIN" bash "$BOT_INSTALLER" diff --git a/mita-ctl.sh b/mita-ctl.sh index f367c59..bba3a08 100755 --- a/mita-ctl.sh +++ b/mita-ctl.sh @@ -1151,7 +1151,7 @@ _bot_install() { section "Установка Telegram-бота" local installer="" - for candidate in /opt/mita-panel/install-bot.sh "$SCRIPT_DIR/install-bot.sh"; do + for candidate in /opt/mita-panel/install-bot.sh; do [[ -f "$candidate" ]] && { installer="$candidate"; break; } done @@ -1164,7 +1164,32 @@ _bot_install() { read -r -p "Токен бота (от @BotFather): " token [[ -z "$token" ]] && { error "Токен обязателен"; return; } - read -r -p "Ваш Telegram ID (число, @userinfobot): " admin_id + echo "" + info "Определение вашего Telegram ID..." + echo -e " Отправьте ${YELLOW}любое сообщение${NC} своему боту в Telegram прямо сейчас." + echo -e " Ожидание сообщения (макс. 60 сек)..." + + admin_id="" + for i in $(seq 1 30); do + admin_id=$(curl -s --max-time 5 "https://api.telegram.org/bot${token}/getUpdates" 2>/dev/null | \ + python3 -c " +import sys,json +try: + d=json.load(sys.stdin) + ids=[r['message']['from']['id'] for r in d.get('result',[]) if 'message' in r and 'from' in r['message']] + print(ids[-1] if ids else '') +except: pass +" 2>/dev/null) + [[ -n "$admin_id" ]] && break + sleep 2 + done + + if [[ -z "$admin_id" || ! "$admin_id" =~ ^[0-9]+$ ]]; then + warn "Не удалось определить ID автоматически." + read -r -p "Впишите ваш Telegram ID вручную (число): " admin_id + else + ok "Telegram ID определён: ${GREEN}${admin_id}${NC}" + fi [[ -z "$admin_id" || ! "$admin_id" =~ ^[0-9]+$ ]] && { error "ID должен быть числом"; return; } BOT_TOKEN_NONINTERACTIVE="$token" BOT_ADMIN_NONINTERACTIVE="$admin_id" bash "$installer" diff --git a/templates/index.html b/templates/index.html index d98de6c..6a7be30 100644 --- a/templates/index.html +++ b/templates/index.html @@ -52,9 +52,8 @@
- Генерируется мгновенно, действует 10 лет. - Браузер покажет предупреждение — его можно принять. - Подходит для личного использования. + Панель будет слушать только 127.0.0.1 — недоступна снаружи. + Подключение через зашифрованный SSH-туннель с вашего компьютера.
-${escH(d.server_ip)}${d.panel_port}