version 1. Работают все функции на веб-панели, исправлено поведение при создании пользователей, теперь всё создаётся правильно
This commit is contained in:
@@ -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
@@ -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
@@ -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
@@ -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');
|
||||||
|
|||||||
@@ -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 %}
|
||||||
|
|||||||
Reference in New Issue
Block a user