diff --git a/app.py b/app.py index 54b3d13..bb4fbc0 100644 --- a/app.py +++ b/app.py @@ -2,11 +2,14 @@ """ mita Web Panel — Flask backend """ -import os, json, subprocess, secrets, string, random, ipaddress, socket +import os, json, subprocess, secrets, string, random, ipaddress, socket, time, logging from datetime import datetime, timedelta from functools import wraps from pathlib import Path +logging.basicConfig(level=logging.INFO, + format="%(asctime)s %(levelname)s [%(name)s] %(message)s") + from flask import (Flask, render_template, request, jsonify, session, redirect, url_for, abort) from collections import defaultdict @@ -81,7 +84,57 @@ def load_mita_config(): def save_mita_config(cfg): Path(MITA_CONFIG).write_text(json.dumps(cfg, indent=2, ensure_ascii=False)) - subprocess.run(["mita", "apply", "config", MITA_CONFIG], capture_output=True) + _apply_mita_config_safe() + +_log = logging.getLogger("mita_panel") + +def _apply_mita_config_safe(): + """Apply mita config and restart the service. + Mirrors the shell _apply_mita_config logic: starts mita temporarily if stopped, + so mita apply config (which requires a running daemon) always succeeds.""" + + bg_proc = None + mita_was_stopped = False + + if not _mita_running(): + pb = Path("/etc/mita/server.conf.pb") + try: + pb.unlink(missing_ok=True) + except Exception: + pass + subprocess.run(["systemctl", "reset-failed", "mita"], capture_output=True) + bg_proc = subprocess.Popen( + ["/usr/bin/mita", "run"], + stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL + ) + # Installer uses sleep 3 — give mita enough time to open its RPC socket + time.sleep(3) + mita_was_stopped = True + + r = subprocess.run(["mita", "apply", "config", MITA_CONFIG], + capture_output=True, text=True) + if r.returncode != 0: + _log.error("mita apply config failed (rc=%d): %s %s", + r.returncode, r.stdout.strip(), r.stderr.strip()) + + if mita_was_stopped and bg_proc is not None: + bg_proc.terminate() + try: + bg_proc.wait(timeout=3) + except subprocess.TimeoutExpired: + bg_proc.kill() + time.sleep(1) + + # Fix ownership so the mita systemd service (runs as mita user) can read the .pb + pb = Path("/etc/mita/server.conf.pb") + if pb.exists(): + subprocess.run(["chown", "mita:mita", str(pb)], capture_output=True) + + subprocess.run(["systemctl", "reset-failed", "mita"], capture_output=True) + rs = subprocess.run(["systemctl", "restart", "mita"], capture_output=True, text=True) + if rs.returncode != 0: + _log.error("systemctl restart mita failed (rc=%d): %s %s", + rs.returncode, rs.stdout.strip(), rs.stderr.strip()) def mita_cmd(*args): r = subprocess.run(["mita", *args], capture_output=True, text=True) @@ -940,3 +993,38 @@ def api_user_config_download(): headers={"Content-Disposition": f'attachment; filename="{filename}"'} ) + +@app.route(f"{BASE}/api/mita/apply", methods=["POST"]) +@login_required +def api_mita_apply(): + """Диагностика: вручную применить конфиг mita и вернуть подробный результат.""" + mita_running = _mita_running() + pb = Path("/etc/mita/server.conf.pb") + pb_exists_before = pb.exists() + + r = subprocess.run(["mita", "apply", "config", MITA_CONFIG], + capture_output=True, text=True) + pb_exists_after = pb.exists() + + # Chown после apply — нужно если .pb создавался root'ом + chown_result = None + if pb_exists_after: + cr = subprocess.run(["chown", "mita:mita", str(pb)], capture_output=True, text=True) + chown_result = {"rc": cr.returncode, "err": cr.stderr.strip()} + + rs = subprocess.run(["systemctl", "restart", "mita"], capture_output=True, text=True) + + cfg = load_mita_config() + return jsonify({ + "mita_was_running": mita_running, + "apply_rc": r.returncode, + "apply_stdout": r.stdout.strip(), + "apply_stderr": r.stderr.strip(), + "pb_existed_before": pb_exists_before, + "pb_exists_after": pb_exists_after, + "chown": chown_result, + "restart_rc": rs.returncode, + "restart_stderr": rs.stderr.strip(), + "users_in_json": [u["name"] for u in cfg.get("users", [])], + }) + diff --git a/install.sh b/install.sh index aaf5f86..eefead3 100755 --- a/install.sh +++ b/install.sh @@ -31,9 +31,10 @@ MITA_PROTOCOL="TCP" # TCP или UDP # ---- Пользователи mita ---- # Формат: "имя:пароль" # Добавляйте сколько угодно строк +# ВНИМАНИЕ: перед использованием ОБЯЗАТЕЛЬНО замените пароли ниже на свои (минимум 8 символов)! MITA_USERS=( - "alice:ЗАМЕНИТЕ_НА_СИЛЬНЫЙ_ПАРОЛЬ_1" - "bob:ЗАМЕНИТЕ_НА_СИЛЬНЫЙ_ПАРОЛЬ_2" + "alice:qwerty1234" + "bob:qwerty1234" ) # ---- WARP ---- @@ -76,21 +77,59 @@ for entry in "${MITA_USERS[@]}"; do done ok "Пользователи настроены корректно (${#MITA_USERS[@]} шт.)" +# ============================================================================= +# ВЫБОР ПОРТОВ MITA +# ============================================================================= +section "Порты mita" + +echo -e " Текущее значение: ${YELLOW}${MITA_PORT_RANGE}${NC} (${MITA_PROTOCOL})" +echo -e " Допустимые форматы:" +echo -e " диапазон: ${CYAN}2100-2110${NC} — mita случайно выбирает порт из диапазона при каждом соединении" +echo -e " одиночный: ${CYAN}2100${NC} — фиксированный порт" +echo -e " Рекомендуется диапазон из 5-20 портов." +echo "" +read -r -p " Введите порт или диапазон [${MITA_PORT_RANGE}]: " _PORT_INPUT + +if [[ -n "$_PORT_INPUT" ]]; then + if [[ "$_PORT_INPUT" =~ ^([0-9]+)-([0-9]+)$ ]]; then + _P_START="${BASH_REMATCH[1]}" + _P_END="${BASH_REMATCH[2]}" + if [[ $_P_START -lt 1024 || $_P_END -gt 65535 ]]; then + error "Порты должны быть в диапазоне 1024–65535. Получено: $_P_START-$_P_END" + fi + if [[ $_P_START -ge $_P_END ]]; then + error "Начало диапазона ($_P_START) должно быть меньше конца ($_P_END)" + fi + MITA_PORT_RANGE="$_PORT_INPUT" + ok "Диапазон портов: $MITA_PORT_RANGE" + elif [[ "$_PORT_INPUT" =~ ^[0-9]+$ ]]; then + if [[ $_PORT_INPUT -lt 1024 || $_PORT_INPUT -gt 65535 ]]; then + error "Порт $_PORT_INPUT вне допустимого диапазона (1024–65535)" + fi + MITA_PORT_RANGE="$_PORT_INPUT" + ok "Порт: $MITA_PORT_RANGE" + else + error "Неверный формат: '$_PORT_INPUT'. Ожидается число (2100) или диапазон (2100-2110)" + fi +else + ok "Оставлен по умолчанию: $MITA_PORT_RANGE" +fi + # ============================================================================= # ЗАВИСИМОСТИ # ============================================================================= section "Установка зависимостей" apt-get update -qq -apt-get install -y -qq \ +DEBIAN_FRONTEND=noninteractive apt-get install -y -qq \ curl wget jq cron \ ca-certificates gnupg lsb-release \ - iptables ipset ufw \ + iptables ipset ufw iptables-persistent \ fail2ban openssl \ net-tools \ python3 python3-yaml \ 2>/dev/null -ok "Базовые пакеты установлены (включая fail2ban, ufw, ipset, python3-yaml)" +ok "Базовые пакеты установлены (включая fail2ban, ufw, ipset, iptables-persistent, python3-yaml)" # ============================================================================= # УСТАНОВКА MITA diff --git a/mita-ctl.sh b/mita-ctl.sh index 83c18c5..1df891a 100755 --- a/mita-ctl.sh +++ b/mita-ctl.sh @@ -1088,6 +1088,31 @@ menu_uninstall() { echo "" section "Удаление в процессе..." + # ── 0. Сохраняем порты до удаления файлов ────────────────────── + # Читаем параметры заранее — файлы конфигурации удалим позже + _MITA_RANGE="2100-2110" + if [[ -f "$MITA_CONFIG" ]]; then + _MITA_RANGE=$(python3 -c " +import json +try: + with open('$MITA_CONFIG') as f: d=json.load(f) + b=d.get('portBindings',[{}])[0] + print(b.get('portRange',str(b.get('port','2100-2110')))) +except: print('2100-2110') +" 2>/dev/null || echo "2100-2110") + fi + + _PANEL_PORT="8080" + [[ -f "$PANEL_ENV" ]] && _PANEL_PORT=$(grep "^PANEL_PORT=" "$PANEL_ENV" | cut -d= -f2 || echo "8080") + _PANEL_PORT=${_PANEL_PORT:-8080} + + # Автодетект текущего SSH порта из sshd_config (чтобы не трогать его при чистке) + _SSH_PORT=$(grep -E "^[[:space:]]*Port[[:space:]]+" /etc/ssh/sshd_config 2>/dev/null | awk '{print $2}' | head -1) + _SSH_PORT=${_SSH_PORT:-22} + info "Обнаружен SSH порт: $_SSH_PORT (будет сохранён)" + info "Порты mita для закрытия: $_MITA_RANGE" + info "Порт панели для закрытия: $_PANEL_PORT" + # ── 1. Остановить и удалить веб-панель ───────────────────────── info "Остановка mita-panel..." systemctl stop mita-panel 2>/dev/null || true @@ -1164,14 +1189,47 @@ menu_uninstall() { rm -f /usr/local/bin/mita-update-lists ok "Cron-задачи удалены" - # ── 8. Закрыть порты в firewall (best-effort) ────────────────── - info "Откат правил firewall (порты mita/панели)..." - if command -v ufw &>/dev/null; then + # ── 8. Закрыть порты в firewall ──────────────────────────────── + info "Закрытие портов mita ($_MITA_RANGE) и панели ($_PANEL_PORT) в firewall..." + info "SSH порт $_SSH_PORT остаётся открытым." + + # ---- UFW ---- + if command -v ufw &>/dev/null && ufw status 2>/dev/null | grep -q "Status: active"; then + # Удаляем правило mita (диапазон или одиночный порт) + if [[ "$_MITA_RANGE" == *"-"* ]]; then + _SP="${_MITA_RANGE%-*}"; _EP="${_MITA_RANGE#*-}" + ufw delete allow "${_SP}:${_EP}/tcp" 2>/dev/null || true + ufw delete allow "${_SP}:${_EP}/udp" 2>/dev/null || true + else + ufw delete allow "$_MITA_RANGE/tcp" 2>/dev/null || true + ufw delete allow "$_MITA_RANGE/udp" 2>/dev/null || true + fi + # Удаляем правило панели + ufw delete allow "$_PANEL_PORT/tcp" 2>/dev/null || true + # Дополнительно: чистим правила по комментарию (совместимость с разными версиями) ufw status numbered 2>/dev/null | grep -iE "mita|panel" | awk -F'[][]' '{print $2}' | sort -rn | while read -r num; do [[ -n "$num" ]] && yes | ufw delete "$num" 2>/dev/null || true done + ok "UFW: правила удалены" fi - ok "Правила firewall обработаны (проверьте 'ufw status' вручную при необходимости)" + + # ---- iptables ---- + if 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 + iptables -D INPUT -p udp --dport "${_SP}:${_EP}" -j ACCEPT 2>/dev/null || true + else + iptables -D INPUT -p tcp --dport "$_MITA_RANGE" -j ACCEPT 2>/dev/null || true + 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 + command -v iptables-save &>/dev/null && iptables-save > /etc/iptables/rules.v4 2>/dev/null || true + ok "iptables: правила удалены" + fi + + ok "Firewall: порты mita и панели закрыты. SSH ($_SSH_PORT) не тронут." systemctl daemon-reload 2>/dev/null || true diff --git a/templates/base.html b/templates/base.html index 10cad98..ca9831e 100644 --- a/templates/base.html +++ b/templates/base.html @@ -171,7 +171,19 @@ function showToast(msg,isErr=false){ t.textContent=msg;t.style.color=isErr?'var(--danger)':'var(--ok)'; t.style.display='block';setTimeout(()=>t.style.display='none',2500); } -function copyText(text){navigator.clipboard.writeText(text).then(()=>showToast('Скопировано ✓'));} +function copyText(text){ + if(navigator.clipboard&&navigator.clipboard.writeText){ + navigator.clipboard.writeText(text).then(()=>showToast('Скопировано ✓')).catch(()=>_copyFallback(text)); + }else{_copyFallback(text);} +} +function _copyFallback(text){ + const ta=document.createElement('textarea'); + ta.value=text;ta.style.cssText='position:fixed;opacity:0;top:0;left:0'; + document.body.appendChild(ta);ta.focus();ta.select(); + try{document.execCommand('copy');showToast('Скопировано ✓');} + catch(e){showToast('Ошибка копирования',true);} + document.body.removeChild(ta); +} function toggleAccordion(hdr){ hdr.classList.toggle('aopen'); hdr.nextElementSibling.classList.toggle('open'); diff --git a/templates/index.html b/templates/index.html index c84793f..64a0581 100644 --- a/templates/index.html +++ b/templates/index.html @@ -150,7 +150,6 @@ -{% endblock %}