version 1. Работают все функции на веб-панели, исправлено поведение при создании пользователей, теперь всё создаётся правильно
This commit is contained in:
@@ -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", [])],
|
||||
})
|
||||
|
||||
|
||||
+44
-5
@@ -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
|
||||
|
||||
+62
-4
@@ -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
|
||||
|
||||
|
||||
+13
-1
@@ -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');
|
||||
|
||||
@@ -150,7 +150,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
<!-- SECURITY / FAIL2BAN -->
|
||||
<div class="page" id="page-security">
|
||||
<h1>Безопасность</h1>
|
||||
@@ -182,7 +181,7 @@
|
||||
</div>
|
||||
<div class="form-group" style="max-width:220px">
|
||||
<label>Время блокировки</label>
|
||||
<select id="f2b-bantime" onchange="toggleCustomBantime()">
|
||||
<select id="f2b-bantime" onchange="toggleCustomBantime(this.value)">
|
||||
<option value="300">5 минут</option>
|
||||
<option value="900">15 минут</option>
|
||||
<option value="1800">30 минут</option>
|
||||
@@ -210,7 +209,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
@@ -697,10 +696,10 @@ function fmtSeconds(s) {
|
||||
return s + ' сек.';
|
||||
}
|
||||
|
||||
document.getElementById('f2b-bantime').addEventListener('change', function() {
|
||||
function toggleCustomBantime(val) {
|
||||
document.getElementById('custom-bantime-group').style.display =
|
||||
this.value === '0' ? '' : 'none';
|
||||
});
|
||||
val === '0' ? '' : 'none';
|
||||
}
|
||||
|
||||
function applyPreset(maxRetry, banTime) {
|
||||
document.getElementById('f2b-maxretry').value = maxRetry;
|
||||
@@ -744,7 +743,7 @@ function codeBlock(json){
|
||||
const txt=JSON.stringify(json,null,2);
|
||||
const id='cb'+Math.random().toString(36).slice(2);
|
||||
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>
|
||||
{% endblock %}
|
||||
|
||||
Reference in New Issue
Block a user