version 1. Работают все функции на веб-панели, исправлено поведение при создании пользователей, теперь всё создаётся правильно

This commit is contained in:
2026-06-19 22:46:43 +03:00
parent 5469a967a2
commit 39c1fcf438
5 changed files with 215 additions and 19 deletions
+90 -2
View File
@@ -2,11 +2,14 @@
""" """
mita Web Panel — Flask backend 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 datetime import datetime, timedelta
from functools import wraps from functools import wraps
from pathlib import Path 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, from flask import (Flask, render_template, request, jsonify,
session, redirect, url_for, abort) session, redirect, url_for, abort)
from collections import defaultdict from collections import defaultdict
@@ -81,7 +84,57 @@ def load_mita_config():
def save_mita_config(cfg): def save_mita_config(cfg):
Path(MITA_CONFIG).write_text(json.dumps(cfg, indent=2, ensure_ascii=False)) 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): def mita_cmd(*args):
r = subprocess.run(["mita", *args], capture_output=True, text=True) 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}"'} 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", [])],
})
+44 -5
View File
@@ -31,9 +31,10 @@ MITA_PROTOCOL="TCP" # TCP или UDP
# ---- Пользователи mita ---- # ---- Пользователи mita ----
# Формат: "имя:пароль" # Формат: "имя:пароль"
# Добавляйте сколько угодно строк # Добавляйте сколько угодно строк
# ВНИМАНИЕ: перед использованием ОБЯЗАТЕЛЬНО замените пароли ниже на свои (минимум 8 символов)!
MITA_USERS=( MITA_USERS=(
"alice:ЗАМЕНИТЕ_НА_СИЛЬНЫЙ_ПАРОЛЬ_1" "alice:qwerty1234"
"bob:ЗАМЕНИТЕ_НА_СИЛЬНЫЙ_ПАРОЛЬ_2" "bob:qwerty1234"
) )
# ---- WARP ---- # ---- WARP ----
@@ -76,21 +77,59 @@ for entry in "${MITA_USERS[@]}"; do
done done
ok "Пользователи настроены корректно (${#MITA_USERS[@]} шт.)" 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 "Установка зависимостей" section "Установка зависимостей"
apt-get update -qq apt-get update -qq
apt-get install -y -qq \ DEBIAN_FRONTEND=noninteractive apt-get install -y -qq \
curl wget jq cron \ curl wget jq cron \
ca-certificates gnupg lsb-release \ ca-certificates gnupg lsb-release \
iptables ipset ufw \ iptables ipset ufw iptables-persistent \
fail2ban openssl \ fail2ban openssl \
net-tools \ net-tools \
python3 python3-yaml \ python3 python3-yaml \
2>/dev/null 2>/dev/null
ok "Базовые пакеты установлены (включая fail2ban, ufw, ipset, python3-yaml)" ok "Базовые пакеты установлены (включая fail2ban, ufw, ipset, iptables-persistent, python3-yaml)"
# ============================================================================= # =============================================================================
# УСТАНОВКА MITA # УСТАНОВКА MITA
+62 -4
View File
@@ -1088,6 +1088,31 @@ menu_uninstall() {
echo "" echo ""
section "Удаление в процессе..." 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. Остановить и удалить веб-панель ───────────────────────── # ── 1. Остановить и удалить веб-панель ─────────────────────────
info "Остановка mita-panel..." info "Остановка mita-panel..."
systemctl stop mita-panel 2>/dev/null || true systemctl stop mita-panel 2>/dev/null || true
@@ -1164,14 +1189,47 @@ menu_uninstall() {
rm -f /usr/local/bin/mita-update-lists rm -f /usr/local/bin/mita-update-lists
ok "Cron-задачи удалены" ok "Cron-задачи удалены"
# ── 8. Закрыть порты в firewall (best-effort) ────────────────── # ── 8. Закрыть порты в firewall ────────────────────────────────
info "Откат правил firewall (порты mita/панели)..." info "Закрытие портов mita ($_MITA_RANGE) и панели ($_PANEL_PORT) в firewall..."
if command -v ufw &>/dev/null; then 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 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 [[ -n "$num" ]] && yes | ufw delete "$num" 2>/dev/null || true
done done
ok "UFW: правила удалены"
fi 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 systemctl daemon-reload 2>/dev/null || true
+13 -1
View File
@@ -171,7 +171,19 @@ function showToast(msg,isErr=false){
t.textContent=msg;t.style.color=isErr?'var(--danger)':'var(--ok)'; t.textContent=msg;t.style.color=isErr?'var(--danger)':'var(--ok)';
t.style.display='block';setTimeout(()=>t.style.display='none',2500); 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){ function toggleAccordion(hdr){
hdr.classList.toggle('aopen'); hdr.classList.toggle('aopen');
hdr.nextElementSibling.classList.toggle('open'); hdr.nextElementSibling.classList.toggle('open');
+6 -7
View File
@@ -150,7 +150,6 @@
</div> </div>
</div> </div>
{% endblock %}
<!-- SECURITY / FAIL2BAN --> <!-- SECURITY / FAIL2BAN -->
<div class="page" id="page-security"> <div class="page" id="page-security">
<h1>Безопасность</h1> <h1>Безопасность</h1>
@@ -182,7 +181,7 @@
</div> </div>
<div class="form-group" style="max-width:220px"> <div class="form-group" style="max-width:220px">
<label>Время блокировки</label> <label>Время блокировки</label>
<select id="f2b-bantime" onchange="toggleCustomBantime()"> <select id="f2b-bantime" onchange="toggleCustomBantime(this.value)">
<option value="300">5 минут</option> <option value="300">5 минут</option>
<option value="900">15 минут</option> <option value="900">15 минут</option>
<option value="1800">30 минут</option> <option value="1800">30 минут</option>
@@ -210,7 +209,7 @@
</div> </div>
</div> </div>
{% endblock %}
{% block scripts %} {% block scripts %}
<script> <script>
@@ -697,10 +696,10 @@ function fmtSeconds(s) {
return s + ' сек.'; return s + ' сек.';
} }
document.getElementById('f2b-bantime').addEventListener('change', function() { function toggleCustomBantime(val) {
document.getElementById('custom-bantime-group').style.display = document.getElementById('custom-bantime-group').style.display =
this.value === '0' ? '' : 'none'; val === '0' ? '' : 'none';
}); }
function applyPreset(maxRetry, banTime) { function applyPreset(maxRetry, banTime) {
document.getElementById('f2b-maxretry').value = maxRetry; document.getElementById('f2b-maxretry').value = maxRetry;
@@ -744,7 +743,7 @@ function codeBlock(json){
const txt=JSON.stringify(json,null,2); const txt=JSON.stringify(json,null,2);
const id='cb'+Math.random().toString(36).slice(2); const id='cb'+Math.random().toString(36).slice(2);
return `<div class="code-block" id="${id}">${escH(txt)}</div> return `<div class="code-block" id="${id}">${escH(txt)}</div>
<button class="btn btn-secondary btn-sm" style="margin-top:8px" onclick="navigator.clipboard.writeText(document.getElementById('${id}').textContent).then(()=>showToast('Скопировано ✓'))">Копировать</button>`; <button class="btn btn-secondary btn-sm" style="margin-top:8px" onclick="copyText(document.getElementById('${id}').textContent)">Копировать</button>`;
} }
</script> </script>
{% endblock %} {% endblock %}