Update ZapretManager_LuCI.sh

This commit is contained in:
StressOzz authored and GitHub committed 2026-09-18 18:23:19 +05:00
1 parent e0c7284bc4
commit c1e60816ea
1 file changed
+704 -16
+704 -16
View File
@@ -1,5 +1,6 @@
#!/bin/sh #!/bin/sh
# Zapret Manager by StressOzz for LuCI installer # Zapret Manager by StressOzz for LuCI installer
# Version: 1.06
set -e set -e
GREEN="\033[1;32m"; CYAN="\033[1;36m"; YELLOW="\033[1;33m"; MAGENTA="\033[1;35m"; BLUE="\033[0;34m"; NC="\033[0m"; DGRAY="\033[38;5;244m" GREEN="\033[1;32m"; CYAN="\033[1;36m"; YELLOW="\033[1;33m"; MAGENTA="\033[1;35m"; BLUE="\033[0;34m"; NC="\033[0m"; DGRAY="\033[38;5;244m"
@@ -25,6 +26,8 @@ mkdir -p /usr/lib/zapret-manager
cat > '/usr/lib/zapret-manager/backend.sh' << 'ZM_INSTALLER_EOF' cat > '/usr/lib/zapret-manager/backend.sh' << 'ZM_INSTALLER_EOF'
CONF="/etc/config/zapret" CONF="/etc/config/zapret"
ZM_VERSION="1.06"
ZM_SCRIPT_URL="https://raw.githubusercontent.com/StressOzz/Zapret-Manager/refs/heads/main/ZapretManager_LuCI.sh"
GH_RAW="https://raw.githubusercontent.com" GH_RAW="https://raw.githubusercontent.com"
GH_MAIN="https://github.com" GH_MAIN="https://github.com"
EXCLUDE_URL="${GH_RAW}/StressOzz/Zapret-Manager/refs/heads/main/zapret-hosts-user-exclude.txt" EXCLUDE_URL="${GH_RAW}/StressOzz/Zapret-Manager/refs/heads/main/zapret-hosts-user-exclude.txt"
@@ -412,14 +415,18 @@ strategy_v10() { printf '%s\n' "#v10" "--filter-tcp=443" "--hostlist-exclude=/op
_add_gp_domains() { _add_gp_domains() {
local f="/opt/zapret/ipset/zapret-hosts-google.txt" local f="/opt/zapret/ipset/zapret-hosts-google.txt" tmp
mkdir -p "$(dirname "$f")"; touch "$f" mkdir -p "$(dirname "$f")"
printf '%s\n' "gvt1.com" "googleplay.com" "play.google.com" "beacons.gvt2.com" \ tmp="$f.tmp"
"play.googleapis.com" "play-fe.googleapis.com" "lh3.googleusercontent.com" \ {
"android.clients.google.com" "connectivitycheck.gstatic.com" \ [ -f "$f" ] && cat "$f"
"play-lh.googleusercontent.com" "play-games.googleusercontent.com" \ printf '%s\n' "gvt1.com" "googleplay.com" "play.google.com" "beacons.gvt2.com" \
"prod-lt-playstoregatewayadapter-pa.googleapis.com" "youtubei.youtube.com" \ "play.googleapis.com" "play-fe.googleapis.com" "lh3.googleusercontent.com" \
| grep -Fxv -f "$f" 2>/dev/null >> "$f" "android.clients.google.com" "connectivitycheck.gstatic.com" \
"play-lh.googleusercontent.com" "play-games.googleusercontent.com" \
"prod-lt-playstoregatewayadapter-pa.googleapis.com" "youtubei.youtube.com"
} | sort -u > "$tmp"
mv "$tmp" "$f"
} }
_refresh_exclude_file() { _refresh_exclude_file() {
@@ -1455,7 +1462,8 @@ exclusions_clear() {
TG_MTPROTO_VER="0.10" TG_MTPROTO_VER="0.10"
TGWS_INSTALL_URL="https://gitlab.com/xyzmean/brb/-/raw/main/install-tgws.sh" TGWS_VERSION="0.2.5"
TGWS_BASE_URL="https://gitlab.com/xyzmean/brb/-/raw/main/dist"
TGWS_VERSION_URL="https://gitlab.com/xyzmean/brb/-/raw/main/VERSION" TGWS_VERSION_URL="https://gitlab.com/xyzmean/brb/-/raw/main/VERSION"
TG_GO_VER="1.4.1" TG_GO_VER="1.4.1"
TG_RS_VER="2.3.3" TG_RS_VER="2.3.3"
@@ -1666,11 +1674,51 @@ tgws_status() {
} }
do_tgws_install() { do_tgws_install() {
_ensure_deps
echo "==> Устанавливаем sTGWS" echo "==> Устанавливаем sTGWS"
rm -f /tmp/tgws_install.sh local ver arch file tmp
wget -q -U "Mozilla/5.0" -O /tmp/tgws_install.sh "$TGWS_INSTALL_URL" || { echo "ОШИБКА: не удалось скачать установщик sTGWS"; rm -f /tmp/tgws_install.sh; return 1; } ver=$(curl -fsSL --connect-timeout 4 --max-time 6 "$TGWS_VERSION_URL" 2>/dev/null | tr -d '[:space:]')
sh /tmp/tgws_install.sh >/dev/null 2>&1 [ -n "$ver" ] || ver="$TGWS_VERSION"
rm -f /tmp/tgws_install.sh arch="$(awk -F\' '/DISTRIB_ARCH/ {print $2}' /etc/openwrt_release)"
[ -n "$arch" ] || arch="$(opkg print-architecture 2>/dev/null | awk '$2!="all"&&$2!="noarch"{print $2}' | tail -1)"
if [ -z "$arch" ]; then
echo "ОШИБКА: не удалось определить архитектуру роутера"
return 1
fi
file="tgws-${ver}-1_${arch}.${RAZ}"
tmp="$JOBS_DIR/tgws_install.$RAZ"
echo "==> Роутер: $arch, формат пакетов: $PKG"
echo "==> Скачиваем $file"
local attempt=1 max_attempts=5
while [ "$attempt" -le "$max_attempts" ]; do
rm -f "$tmp"
wget -q -O "$tmp" "$TGWS_BASE_URL/$file" >/dev/null 2>&1
if [ -s "$tmp" ]; then
if [ "$RAZ" = "ipk" ]; then
tar -tzf "$tmp" 2>/dev/null | grep -q 'debian-binary' && break
else
head -c 512 "$tmp" | grep -qiE '<html|<!doctype' || break
fi
fi
attempt=$((attempt + 1))
done
if [ "$attempt" -gt "$max_attempts" ]; then
echo "ОШИБКА: не удалось скачать пакет sTGWS ($TGWS_BASE_URL/$file)"
rm -f "$tmp"
return 1
fi
echo "==> Обновляем список пакетов"
$UPDATE >/dev/null 2>&1
if [ "$PKG" = "apk" ]; then
if apk info -e kmod-nft-queue >/dev/null 2>&1; then
apk add kmod-nft-queue >/dev/null 2>&1
fi
elif opkg list-installed kmod-nft-queue 2>/dev/null | grep -q .; then
opkg flag user kmod-nft-queue >/dev/null 2>&1
fi
echo "==> Устанавливаем пакет"
$INSTALL "$tmp" || { echo "ОШИБКА: менеджер пакетов отказался ставить sTGWS"; rm -f "$tmp"; return 1; }
rm -f "$tmp"
if [ ! -x /etc/init.d/tgws ]; then if [ ! -x /etc/init.d/tgws ]; then
echo "ОШИБКА: установка sTGWS не удалась" echo "ОШИБКА: установка sTGWS не удалась"
return 1 return 1
@@ -1760,6 +1808,321 @@ tgws_action() {
} }
TEST_DIR="$JOBS_DIR/strategy_test"
_test_results_file() { echo "$TEST_DIR/results_$1.txt"; }
TEST_BACKUP="$TEST_DIR/backup.conf"
TEST_STOP_FLAG="$TEST_DIR/stop"
TEST_MODE_FILE="$TEST_DIR/mode"
TEST_DOMAINS_JSON="${GH_RAW}/hyperion-cs/dpi-checkers/refs/heads/main/ru/tcp-16-20/suite.v2.json"
TEST_YT_DOMAINS="youtu.be youtube.com i.ytimg.com i9.ytimg.com yt3.ggpht.com yt4.ggpht.com googleapis.com jnn-pa.googleapis.com googleusercontent.com signaler-pa.youtube.com youtubei.googleapis.com manifest.googlevideo.com yt3.googleusercontent.com rr4---sn-4g5e6nze.googlevideo.com rr4---sn-5go7yner.googlevideo.com rr4---sn-q4flrnsl.googlevideo.com rr5---sn-n8v7knez.googlevideo.com rr2---sn-q4fl6ndl.googlevideo.com rr1---sn-q4fl6n6y.googlevideo.com rr1---sn-aj5go5-53.googlevideo.com rr1---sn-4axm-n8vs.googlevideo.com rr14---sn-n8v7kn7r.googlevideo.com rr16---sn-axq7sn76.googlevideo.com rr4---sn-jvhnu5g-c35d.googlevideo.com rr1---sn-8ph2xajvh-5xge.googlevideo.com rr1---sn-xguxaxjvh-gufl.googlevideo.com rr1---sn-gvnuxaxjvh-jx3z.googlevideo.com rr1---sn-gvnuxaxjvh-jx3l.googlevideo.com rr1---sn-gvnuxaxjvh-o8ge.googlevideo.com rr5---sn-gvnuxaxjvh-n8vk.googlevideo.com rr10---sn-gvnuxaxjvh-304z.googlevideo.com rr12---sn-gvnuxaxjvh-bvwz.googlevideo.com rr3---sn-ug5onuxaxjvh-n8v6.googlevideo.com rr1---sn-ug5onuxaxjvh-p5ge.googlevideo.com rr1---sn-ug5onuxaxjvh-p3ul.googlevideo.com rr1---sn-ug5onuxaxjvh-n8v6.googlevideo.com rr1---sn-u5uuxaxjvhg0-ocje.googlevideo.com"
TEST_PARALLEL=8
_test_sort_results() {
local f="$1" tmp
tmp="$TEST_DIR/sort.$$"
awk -F'[/ ]' '{ for (i = 1; i <= NF; i++) { if ($i ~ /^[0-9]+$/) { print $i, $0; break } } }' "$f" | sort -k1,1 -nr | cut -d' ' -f2- > "$tmp"
mv "$tmp" "$f"
}
_test_prepare_urls() {
local out="$1"
: > "$out"
printf '%s\n' \
"gosuslugi.ru|https://www.gosuslugi.ru" \
"esia.gosuslugi.ru|https://esia.gosuslugi.ru" \
"nalog.ru|https://nalog.ru" \
"lkfl2.nalog.ru|https://lkfl2.nalog.ru" \
"rutube.ru|https://rutube.ru" \
"ntc.party|https://ntc.party/" \
"instagram.com|https://instagram.com" \
"facebook.com|https://facebook.com" \
"rutracker.org|https://rutracker.org" \
"nnmclub.to|https://nnmclub.to" \
"openwrt.org|https://openwrt.org" \
"discord.com|https://discord.com" \
"x.com|https://x.com" \
"forum.ru-board.com|https://forum.ru-board.com" \
"play.google.com|https://play.google.com" \
"downloads.openwrt.org|https://downloads.openwrt.org" \
"githubusercontent.com|https://raw.githubusercontent.com/StressOzz/Zapret-Manager/refs/heads/main/Zapret-Manager.sh" \
>> "$out"
curl -fsSL --connect-timeout 8 --max-time 15 "$TEST_DOMAINS_JSON" 2>/dev/null \
| sed -n 's/.*"id":[[:space:]]*"\([^"]*\)".*"host":[[:space:]]*"\([^"]*\)".*/\1|\2/p' >> "$out"
}
_test_yt_urls() {
local d
for d in $TEST_YT_DOMAINS; do
printf '%s|https://%s/\n' "$d" "$d"
done
}
_test_kill_pid_tree() {
local pid="$1" ch c
[ -n "$pid" ] || return 0
ch=$(cat "/proc/$pid/task/$pid/children" 2>/dev/null)
for c in $ch; do _test_kill_pid_tree "$c"; done
kill -9 "$pid" 2>/dev/null
}
_test_kill_bg_jobs() {
local pf="$1" pid
[ -s "$pf" ] || return 0
while IFS= read -r pid; do
[ -n "$pid" ] && _test_kill_pid_tree "$pid"
done < "$pf"
wait 2>/dev/null
}
_test_check_url() {
local entry="$1" okfile="$2" logfile="$3" text link
text=$(echo "$entry" | cut -d'|' -f1)
link=$(echo "$entry" | cut -d'|' -f2)
if curl -sL --connect-timeout 4 --max-time 6 --speed-time 3 --speed-limit 1 --range 0-65535 -A "Mozilla/5.0 (Windows NT 10.0; Win64; x64) curl/8.0" -o /dev/null "$link" >/dev/null 2>&1; then
echo 1 >> "$okfile"
echo "[ OK ] $text" >> "$logfile"
else
echo "[FAIL] $text" >> "$logfile"
fi
}
_test_check_all_urls() {
local urls="$1" logfile="$2" okfile pidfile total run=0 ok entry
okfile="$TEST_DIR/ok.$$"
pidfile="$TEST_DIR/pids.$$"
: > "$okfile"
: > "$pidfile"
total=$(printf '%s\n' "$urls" | grep -c '|')
while IFS= read -r entry; do
[ -z "$entry" ] && continue
[ -f "$TEST_STOP_FLAG" ] && break
_test_check_url "$entry" "$okfile" "$logfile" &
echo $! >> "$pidfile"
run=$((run + 1))
if [ "$run" -ge "$TEST_PARALLEL" ]; then
wait
run=0
[ -f "$TEST_STOP_FLAG" ] && break
fi
done <<-TEST_URLS_EOF
$urls
TEST_URLS_EOF
if [ -f "$TEST_STOP_FLAG" ]; then
_test_kill_bg_jobs "$pidfile"
else
wait
fi
ok=$(wc -l < "$okfile" | tr -d ' ')
rm -f "$okfile" "$pidfile"
printf '%s %s\n' "$ok" "$total"
}
_test_build_candidates() {
local mode="$1" out="$2" n f
: > "$out"
case "$mode" in
v)
for n in 1 2 3 4 5 6 7 8 9 10; do strategy_v"$n" >> "$out"; done
;;
flowseal)
f="$(_flowseal_file)"
[ -s "$f" ] || do_flowseal_download >/dev/null 2>&1
[ -s "$f" ] && cat "$f" >> "$out"
;;
v_flowseal)
f="$(_flowseal_file)"
[ -s "$f" ] || do_flowseal_download >/dev/null 2>&1
[ -s "$f" ] && cat "$f" >> "$out"
for n in 1 2 3 4 5 6 7 8 9 10; do strategy_v"$n" >> "$out"; done
;;
youtube)
f="$(_yv_file)"
[ -s "$f" ] || do_yv_download >/dev/null 2>&1
[ -s "$f" ] && cat "$f" >> "$out"
;;
esac
[ "$mode" = "youtube" ] || sed -i '/^#Y/d' "$out"
}
_test_apply_block() {
local block="$1"
sed -i "/^[[:space:]]*option NFQWS_OPT '/,\$d" "$CONF"
{ echo " option NFQWS_OPT '"; echo "$block"; echo "'"; } >> "$CONF"
}
do_test_run() {
local mode="$1" results
mkdir -p "$TEST_DIR"
rm -f "$TEST_STOP_FLAG"
results="$(_test_results_file "$mode")"
: > "$results"
echo "$mode" > "$TEST_MODE_FILE"
[ -f "$CONF" ] || { echo "ОШИБКА: Zapret не установлен"; return 1; }
cp "$CONF" "$TEST_BACKUP"
_add_gp_domains
_refresh_exclude_file
local cand="$TEST_DIR/candidates.txt"
echo "==> Собираем стратегии для теста"
_test_build_candidates "$mode" "$cand"
if [ ! -s "$cand" ]; then
echo "ОШИБКА: не удалось собрать ни одной стратегии для теста"
rm -f "$TEST_BACKUP"
return 1
fi
local urls total_domains
if [ "$mode" = "youtube" ]; then
urls="$(_test_yt_urls)"
else
local urls_file="$TEST_DIR/urls.txt"
echo "==> Собираем список доменов для теста"
_test_prepare_urls "$urls_file"
urls="$(cat "$urls_file")"
fi
total_domains=$(printf '%s\n' "$urls" | grep -c '|')
local total_str
total_str=$(grep -c '^#' "$cand")
echo "==> Найдено стратегий: $total_str"
echo "==> Доменов для теста: $total_domains"
echo "==> Контрольный тест: Zapret выключен"
/etc/init.d/zapret stop >/dev/null 2>&1
local ctrl_log="$TEST_DIR/log_control.txt" ctrl_res ctrl_ok ctrl_total
: > "$ctrl_log"
ctrl_res=$(_test_check_all_urls "$urls" "$ctrl_log")
ctrl_ok=$(echo "$ctrl_res" | cut -d' ' -f1)
ctrl_total=$(echo "$ctrl_res" | cut -d' ' -f2)
echo "Контрольный тест (Zapret выключен) → ${ctrl_ok}/${ctrl_total}" >> "$results"
echo "==> Результат: ${ctrl_ok}/${ctrl_total}"
/etc/init.d/zapret start >/dev/null 2>&1
local lines cur=0
lines=$(grep -n '^#' "$cand" | cut -d: -f1)
echo "$lines" | while read -r start; do
cur=$((cur + 1))
if [ -f "$TEST_STOP_FLAG" ]; then
echo "==> Получен сигнал остановки, прерываем тестирование"
break
fi
local next name block res ok tot log
next=$(echo "$lines" | awk -v s="$start" '$1>s{print;exit}')
if [ -z "$next" ]; then
sed -n "${start},\$p" "$cand" > "$TEST_DIR/block.txt"
else
sed -n "${start},$((next-1))p" "$cand" > "$TEST_DIR/block.txt"
fi
name=$(head -n1 "$TEST_DIR/block.txt")
name="${name#\#}"
block=$(cat "$TEST_DIR/block.txt")
echo "==> [$cur/$total_str] Тестируем: $name"
_test_apply_block "$block"
zapret_restart
log="$TEST_DIR/log_$cur.txt"
: > "$log"
res=$(_test_check_all_urls "$urls" "$log")
ok=$(echo "$res" | cut -d' ' -f1)
tot=$(echo "$res" | cut -d' ' -f2)
echo "==> Результат: ${ok}/${tot}"
echo "${name} → ${ok}/${tot}" >> "$results"
done
if [ -f "$TEST_STOP_FLAG" ]; then
echo "==> Тестирование остановлено пользователем, восстанавливаем конфигурацию"
rm -f "$TEST_STOP_FLAG"
else
echo "==> Тестирование завершено, восстанавливаем конфигурацию"
fi
echo "==> Результаты теста"
_test_sort_results "$results"
cat "$results"
local best_line
best_line=$(grep -v '^Контрольный тест' "$results" | head -n1)
[ -n "$best_line" ] && echo "==> Лучшая стратегия по результатам теста: $best_line"
cp "$TEST_BACKUP" "$CONF"
zapret_restart
rm -f "$TEST_BACKUP"
echo "==> Готово, конфигурация восстановлена"
}
test_action() {
local action="$1" mode="$2"
case "$action" in
start)
case "$mode" in
v|flowseal|v_flowseal|youtube) ;;
*) echo '{"error":"неизвестный режим теста"}'; return 1 ;;
esac
job_start strategy_test do_test_run "$mode"
;;
stop)
if [ ! -f "$JOBS_DIR/strategy_test.pid" ] || ! kill -0 "$(cat "$JOBS_DIR/strategy_test.pid" 2>/dev/null)" 2>/dev/null; then
echo '{"error":"тест не запущен"}'
return 1
fi
mkdir -p "$TEST_DIR"
touch "$TEST_STOP_FLAG"
printf '{"ok":true}\n'
;;
clear)
case "$mode" in
v|flowseal|v_flowseal|youtube) rm -f "$(_test_results_file "$mode")" ;;
*) rm -f "$TEST_DIR"/results_*.txt ;;
esac
printf '{"ok":true}\n'
;;
*) echo '{"error":"неизвестное действие"}' ;;
esac
}
test_status() {
local running="false" mode=""
if [ -f "$JOBS_DIR/strategy_test.pid" ] && kill -0 "$(cat "$JOBS_DIR/strategy_test.pid" 2>/dev/null)" 2>/dev/null; then
running="true"
fi
[ -f "$TEST_MODE_FILE" ] && mode=$(cat "$TEST_MODE_FILE")
printf '{"running":%s,"mode":"%s","has_results_v":%s,"has_results_flowseal":%s,"has_results_v_flowseal":%s,"has_results_youtube":%s}\n' \
"$running" "$(esc "$mode")" \
"$([ -s "$(_test_results_file v)" ] && echo true || echo false)" \
"$([ -s "$(_test_results_file flowseal)" ] && echo true || echo false)" \
"$([ -s "$(_test_results_file v_flowseal)" ] && echo true || echo false)" \
"$([ -s "$(_test_results_file youtube)" ] && echo true || echo false)"
}
test_results() {
local mode="$1" f
f="$(_test_results_file "$mode")"
[ -s "$f" ] || { echo '{"lines":""}'; return; }
printf '{"lines":"%s"}\n' "$(esc_ml "$(cat "$f")")"
}
zm_update_status() {
local latest_line latest=""
latest_line=$(curl -fsSL --connect-timeout 5 --max-time 8 -r 0-400 "$ZM_SCRIPT_URL" 2>/dev/null | grep -m1 '^# Version:')
latest=$(echo "$latest_line" | sed 's/^# Version:[[:space:]]*//')
printf '{"current":"%s","latest":"%s"}\n' "$(esc "$ZM_VERSION")" "$(esc "$latest")"
}
do_zm_update() {
echo "==> Скачиваем новую версию установщика"
local tmp="/tmp/zm_update_install.sh"
rm -f "$tmp"
wget -q -U "Mozilla/5.0" -O "$tmp" "$ZM_SCRIPT_URL" || { echo "ОШИБКА: не удалось скачать установщик"; rm -f "$tmp"; return 1; }
[ -s "$tmp" ] || { echo "ОШИБКА: скачался пустой файл"; rm -f "$tmp"; return 1; }
head -c 200 "$tmp" | grep -q '^#!/bin/sh' || { echo "ОШИБКА: скачанный файл не похож на установщик"; rm -f "$tmp"; return 1; }
chmod +x "$tmp"
echo "==> Запускаем установку новой версии в фоне"
( sleep 1; sh "$tmp" >/tmp/zm_update_install.log 2>&1; rm -f "$tmp" ) &
echo "==> Готово, обновление запущено — панель станет недоступна на несколько секунд, затем обновите страницу"
}
zm_update_action() {
job_start zm_update do_zm_update
}
_doh_file="/etc/config/https-dns-proxy" _doh_file="/etc/config/https-dns-proxy"
do_doh_install() { do_doh_install() {
@@ -1906,6 +2269,11 @@ case "$cmd" in
doh_remove) doh_remove ;; doh_remove) doh_remove ;;
doh_status) doh_status ;; doh_status) doh_status ;;
doh_set) doh_set "$1" ;; doh_set) doh_set "$1" ;;
test_status) test_status ;;
test_action) test_action "$1" "$2" ;;
test_results) test_results "$1" ;;
zm_update_status) zm_update_status ;;
zm_update_action) zm_update_action ;;
*) echo '{"error":"неизвестная команда"}'; exit 1 ;; *) echo '{"error":"неизвестная команда"}'; exit 1 ;;
esac esac
ZM_INSTALLER_EOF ZM_INSTALLER_EOF
@@ -1966,6 +2334,11 @@ list_methods() {
json_add_object "doh_install"; json_close_object json_add_object "doh_install"; json_close_object
json_add_object "doh_remove"; json_close_object json_add_object "doh_remove"; json_close_object
json_add_object "doh_set"; json_add_string "provider" "string"; json_close_object json_add_object "doh_set"; json_add_string "provider" "string"; json_close_object
json_add_object "test_status"; json_close_object
json_add_object "test_action"; json_add_string "action" "string"; json_add_string "mode" "string"; json_close_object
json_add_object "test_results"; json_add_string "mode" "string"; json_close_object
json_add_object "zm_update_status"; json_close_object
json_add_object "zm_update_action"; json_close_object
json_dump json_dump
} }
@@ -2021,6 +2394,11 @@ call_method() {
doh_install) "$BACKEND" doh_install ;; doh_install) "$BACKEND" doh_install ;;
doh_remove) "$BACKEND" doh_remove ;; doh_remove) "$BACKEND" doh_remove ;;
doh_set) json_get_var provider provider; "$BACKEND" doh_set "$provider" ;; doh_set) json_get_var provider provider; "$BACKEND" doh_set "$provider" ;;
test_status) "$BACKEND" test_status ;;
test_action) json_get_var action action; json_get_var mode mode; "$BACKEND" test_action "$action" "$mode" ;;
test_results) json_get_var mode mode; "$BACKEND" test_results "$mode" ;;
zm_update_status) "$BACKEND" zm_update_status ;;
zm_update_action) "$BACKEND" zm_update_action ;;
*) echo '{"error":"unknown method"}'; return 1 ;; *) echo '{"error":"unknown method"}'; return 1 ;;
esac esac
} }
@@ -2045,6 +2423,7 @@ cat > '/usr/share/rpcd/acl.d/luci-app-zapret-manager.json' << 'ZM_INSTALLER_EOF'
"strategy_list_v", "strategy_list_flowseal", "strategy_list_youtube", "strategy_list_v", "strategy_list_flowseal", "strategy_list_youtube",
"discord_status", "hosts_status", "doh_status", "game_status", "discord_status", "hosts_status", "doh_status", "game_status",
"system_status", "mirror_status", "exclusions_status", "tg_status", "tgws_status", "system_status", "mirror_status", "exclusions_status", "tg_status", "tgws_status",
"test_status", "test_results", "zm_update_status",
"zapret_latest_version" "zapret_latest_version"
] ]
} }
@@ -2060,7 +2439,7 @@ cat > '/usr/share/rpcd/acl.d/luci-app-zapret-manager.json' << 'ZM_INSTALLER_EOF'
"system_check_connectivity", "system_toggle_quic", "system_toggle_ipv6", "system_check_connectivity", "system_toggle_quic", "system_toggle_ipv6",
"system_toggle_flow_offloading_fix", "system_toggle_expert_mode", "system_uninstall_panel", "system_toggle_flow_offloading_fix", "system_toggle_expert_mode", "system_uninstall_panel",
"mirror_set", "exclusions_toggle", "exclusions_clear", "mirror_set", "exclusions_toggle", "exclusions_clear",
"tg_action", "tg_restart_all", "tgws_action" "tg_action", "tg_restart_all", "tgws_action", "test_action", "zm_update_action"
] ]
} }
} }
@@ -2089,6 +2468,11 @@ cat > '/usr/share/luci/menu.d/luci-app-zapret-manager.json' << 'ZM_INSTALLER_EOF
"order": 20, "order": 20,
"action": { "type": "view", "path": "zapret-manager/strategy" } "action": { "type": "view", "path": "zapret-manager/strategy" }
}, },
"admin/services/zapret-manager/test": {
"title": "Тест стратегий",
"order": 22,
"action": { "type": "view", "path": "zapret-manager/test" }
},
"admin/services/zapret-manager/youtube": { "admin/services/zapret-manager/youtube": {
"title": "YouTube", "title": "YouTube",
"order": 25, "order": 25,
@@ -2184,6 +2568,11 @@ var callDohStatus = rpc.declare({ object: 'zapret-manager', method: 'doh_status'
var callDohInstall = rpc.declare({ object: 'zapret-manager', method: 'doh_install', expect: {} }); var callDohInstall = rpc.declare({ object: 'zapret-manager', method: 'doh_install', expect: {} });
var callDohRemove = rpc.declare({ object: 'zapret-manager', method: 'doh_remove', expect: {} }); var callDohRemove = rpc.declare({ object: 'zapret-manager', method: 'doh_remove', expect: {} });
var callDohSet = rpc.declare({ object: 'zapret-manager', method: 'doh_set', params: ['provider'], expect: {} }); var callDohSet = rpc.declare({ object: 'zapret-manager', method: 'doh_set', params: ['provider'], expect: {} });
var callTestStatus = rpc.declare({ object: 'zapret-manager', method: 'test_status', expect: {} });
var callTestAction = rpc.declare({ object: 'zapret-manager', method: 'test_action', params: ['action', 'mode'], expect: {} });
var callTestResults = rpc.declare({ object: 'zapret-manager', method: 'test_results', params: ['mode'], expect: {} });
var callZmUpdateStatus = rpc.declare({ object: 'zapret-manager', method: 'zm_update_status', expect: {} });
var callZmUpdateAction = rpc.declare({ object: 'zapret-manager', method: 'zm_update_action', expect: {} });
function detectMissingThemeVar() { function detectMissingThemeVar() {
if (document.documentElement.hasAttribute('data-zm-theme-checked')) return; if (document.documentElement.hasAttribute('data-zm-theme-checked')) return;
@@ -2255,27 +2644,42 @@ function renderLog(logEl, text) {
logEl.scrollTop = logEl.scrollHeight; logEl.scrollTop = logEl.scrollHeight;
} }
var _activePolls = {};
function pollJob(job, logEl, onDone, onTick) { function pollJob(job, logEl, onDone, onTick) {
if (_activePolls[job]) {
clearInterval(_activePolls[job]);
delete _activePolls[job];
}
logEl.classList.add('zm-show'); logEl.classList.add('zm-show');
var failCount = 0; var failCount = 0;
var finished = false;
var timer = setInterval(function() { var timer = setInterval(function() {
if (finished) return;
Promise.all([ callJobStatus(job), callLogTail(job) ]).then(function(res) { Promise.all([ callJobStatus(job), callLogTail(job) ]).then(function(res) {
if (finished) return;
failCount = 0; failCount = 0;
var st = res[0], lg = res[1]; var st = res[0], lg = res[1];
renderLog(logEl, (lg && lg.lines) || ''); renderLog(logEl, (lg && lg.lines) || '');
if (typeof onTick === 'function') onTick(); if (typeof onTick === 'function') onTick();
if (st && st.done === true) { if (st && st.done === true) {
finished = true;
clearInterval(timer); clearInterval(timer);
delete _activePolls[job];
onDone(st.rc === '0'); onDone(st.rc === '0');
} }
}).catch(function() { }).catch(function() {
if (finished) return;
failCount++; failCount++;
if (failCount >= 8) { if (failCount >= 8) {
finished = true;
clearInterval(timer); clearInterval(timer);
delete _activePolls[job];
toast('Роутер не отвечает — операция может ещё выполняться в фоне. Обновите страницу через полминуты, чтобы проверить результат.', 'warning', 25000); toast('Роутер не отвечает — операция может ещё выполняться в фоне. Обновите страницу через полминуты, чтобы проверить результат.', 'warning', 25000);
} }
}); });
}, 1200); }, 1200);
_activePolls[job] = timer;
} }
function refreshBanner(message) { function refreshBanner(message) {
@@ -2380,7 +2784,12 @@ return baseclass.extend({
dohStatus: callDohStatus, dohStatus: callDohStatus,
dohInstall: callDohInstall, dohInstall: callDohInstall,
dohRemove: callDohRemove, dohRemove: callDohRemove,
dohSet: callDohSet dohSet: callDohSet,
testStatus: callTestStatus,
testAction: callTestAction,
testResults: callTestResults,
zmUpdateStatus: callZmUpdateStatus,
zmUpdateAction: callZmUpdateAction
}); });
ZM_INSTALLER_EOF ZM_INSTALLER_EOF
@@ -2400,7 +2809,8 @@ return view.extend({
zm.hostsStatus().catch(function() { return { items: [] }; }), zm.hostsStatus().catch(function() { return { items: [] }; }),
zm.systemStatus().catch(function() { return {}; }), zm.systemStatus().catch(function() { return {}; }),
zm.zapretLatestVersion().catch(function() { return {}; }), zm.zapretLatestVersion().catch(function() { return {}; }),
zm.systemInfo().catch(function() { return {}; }) zm.systemInfo().catch(function() { return {}; }),
zm.zmUpdateStatus().catch(function() { return {}; })
]); ]);
}, },
@@ -2409,6 +2819,7 @@ return view.extend({
var data = all[0], dohData = all[1], hostsData = all[2], sysData = all[3]; var data = all[0], dohData = all[1], hostsData = all[2], sysData = all[3];
var latestVersion = (all[4] && all[4].version) || ''; var latestVersion = (all[4] && all[4].version) || '';
var sysInfo = all[5] || {}; var sysInfo = all[5] || {};
var zmUpdate = all[6] || {};
var wrap = E('div', { 'class': 'zm-wrap' }); var wrap = E('div', { 'class': 'zm-wrap' });
var overviewEl = E('div', {}); var overviewEl = E('div', {});
var cards = E('div', { 'class': 'zm-cards' }); var cards = E('div', { 'class': 'zm-cards' });
@@ -2427,6 +2838,17 @@ return view.extend({
return E('div', { 'class': 'zm-card', 'style': 'margin-bottom:4px' }, [ return E('div', { 'class': 'zm-card', 'style': 'margin-bottom:4px' }, [
E('h3', {}, 'Обзор'), E('h3', {}, 'Обзор'),
E('div', { 'class': 'zm-row' }, [
E('span', { 'class': 'zm-label' }, 'Zapret Manager LuCI'),
E('span', {}, [
E('span', {}, 'v' + (zmUpdate.current || '?') + ' '),
E('button', {
'class': 'cbi-button',
'style': 'padding:2px 10px; font-size:12px',
'click': function() { checkForUpdates(true); }
}, 'Проверить обновления')
])
]),
E('div', { 'class': 'zm-row' }, [ E('div', { 'class': 'zm-row' }, [
E('span', { 'class': 'zm-label' }, 'Zapret'), E('span', { 'class': 'zm-label' }, 'Zapret'),
d.zapret === 'installed' d.zapret === 'installed'
@@ -2556,6 +2978,8 @@ return view.extend({
cards.appendChild(sysCard); cards.appendChild(sysCard);
} }
var lastD = data, lastDoh = dohData, lastHosts = hostsData, lastSys = sysData;
function refreshOverview() { function refreshOverview() {
Promise.all([ Promise.all([
zm.status(), zm.status(),
@@ -2563,6 +2987,7 @@ return view.extend({
zm.hostsStatus().catch(function() { return { items: [] }; }), zm.hostsStatus().catch(function() { return { items: [] }; }),
zm.systemStatus().catch(function() { return {}; }) zm.systemStatus().catch(function() { return {}; })
]).then(function(res) { ]).then(function(res) {
lastD = res[0]; lastDoh = res[1]; lastHosts = res[2]; lastSys = res[3];
overviewEl.innerHTML = ''; overviewEl.innerHTML = '';
overviewEl.appendChild(renderOverview(res[0], res[1], res[2], res[3])); overviewEl.appendChild(renderOverview(res[0], res[1], res[2], res[3]));
}); });
@@ -2589,6 +3014,66 @@ return view.extend({
this.renderCards = renderCards; this.renderCards = renderCards;
this.refreshOverview = refreshOverview; this.refreshOverview = refreshOverview;
var updateEl = E('div', {});
wrap.appendChild(updateEl);
var zmUpdateBusy = false;
function renderZmUpdate() {
updateEl.innerHTML = '';
if (!zmUpdate.latest || zmUpdate.latest === zmUpdate.current) return;
var log = E('pre', { 'class': 'zm-log' });
updateEl.appendChild(E('div', { 'class': 'zm-refresh-banner zm-show' }, [
E('span', {}, 'Доступна новая версия панели Zapret Manager: ' + zmUpdate.latest + ' (у вас установлена ' + zmUpdate.current + ').'),
E('button', {
'class': 'cbi-button cbi-button-positive',
'click': function() {
if (zmUpdateBusy) { zm.toast('Дождитесь завершения текущей операции', 'warning'); return; }
zmUpdateBusy = true;
log.classList.add('zm-show');
zm.toast('Скачиваем и запускаем обновление панели', 'warning');
zm.zmUpdateAction().then(function(res) {
if (res.error) { zmUpdateBusy = false; zm.toast(res.error, 'error'); return; }
zm.pollJob('zm_update', log, function(ok) {
zmUpdateBusy = false;
if (ok) {
zm.toast('Обновление запущено — подождите около 20 секунд, затем обновите страницу', 'info', 20000);
setTimeout(function() { location.reload(); }, 20000);
} else {
zm.toast('Не удалось скачать обновление', 'error');
}
});
}).catch(function() { zmUpdateBusy = false; });
}
}, 'Обновить панель')
]));
updateEl.appendChild(log);
}
renderZmUpdate();
var checkBusy = false;
function checkForUpdates(manual) {
if (checkBusy) { zm.toast('Дождитесь завершения проверки', 'warning'); return; }
checkBusy = true;
if (manual) zm.toast('Проверяем обновления', 'warning');
zm.zmUpdateStatus().then(function(res) {
checkBusy = false;
zmUpdate = res || {};
overviewEl.innerHTML = '';
overviewEl.appendChild(renderOverview(lastD, lastDoh, lastHosts, lastSys));
renderZmUpdate();
if (manual) {
if (zmUpdate.latest && zmUpdate.latest !== zmUpdate.current) {
zm.toast('Доступна новая версия: ' + zmUpdate.latest, 'info');
} else {
zm.toast('У вас установлена последняя версия', 'info');
}
}
}).catch(function() {
checkBusy = false;
if (manual) zm.toast('Не удалось проверить обновления', 'error');
});
}
return wrap; return wrap;
}, },
@@ -3766,6 +4251,209 @@ return view.extend({
}); });
ZM_INSTALLER_EOF ZM_INSTALLER_EOF
mkdir -p /www/luci-static/resources/view/zapret-manager
cat > '/www/luci-static/resources/view/zapret-manager/test.js' << 'ZM_INSTALLER_EOF'
'use strict';
'require view';
'require zapret-manager.common as zm';
var MODES = [
{ id: 'v', label: 'Тестировать v' },
{ id: 'flowseal', label: 'Тестировать Flowseal' },
{ id: 'v_flowseal', label: 'Тестировать v + Flowseal' }
];
var MODE_LABELS = {
v: 'v', flowseal: 'Flowseal', v_flowseal: 'v + Flowseal', youtube: 'YouTube'
};
var RESULT_MODES = ['v', 'flowseal', 'v_flowseal', 'youtube'];
return view.extend({
load: function() {
zm.injectCss();
return zm.testStatus();
},
render: function(data) {
var view = this;
var wrap = E('div', { 'class': 'zm-wrap' });
var logEl = E('pre', { 'class': 'zm-log' });
var resultsEl = E('pre', { 'class': 'zm-log' });
var busy = data.running === true;
var curMode = data.mode || '';
var status = data;
var mainCard = E('div', { 'class': 'zm-card' });
var ytCard = E('div', { 'class': 'zm-card' });
var resultsButtonsEl = E('div', {});
function stopButton() {
return E('button', { 'class': 'cbi-button cbi-button-remove', 'click': doStop }, 'Остановить тестирование стратегий');
}
function renderMain() {
mainCard.innerHTML = '';
mainCard.appendChild(E('h3', {}, 'Тест стратегий v / Flowseal'));
mainCard.appendChild(E('p', { 'class': 'zm-hint' }, 'Тест идёт в фоне — переключение вкладок или закрытие LuCI его не прервёт. По окончании конфигурация всегда возвращается к тому, что было до начала теста — стратегии только тестируются, лучшую нужно применить вручную на странице «Стратегии».'));
if (busy && curMode !== 'youtube') {
mainCard.appendChild(E('div', { 'class': 'zm-row' }, [
E('span', { 'class': 'zm-label' }, 'Статус'),
zm.badge(true, 'тест выполняется (' + (MODE_LABELS[curMode] || curMode) + ')', '')
]));
mainCard.appendChild(E('div', { 'class': 'zm-actions' }, [ stopButton() ]));
} else if (busy) {
mainCard.appendChild(E('p', { 'class': 'zm-hint' }, 'Сейчас выполняется тест YouTube — дождитесь его завершения.'));
} else {
mainCard.appendChild(E('div', { 'class': 'zm-actions' }, MODES.map(function(m) {
return E('button', {
'class': 'cbi-button cbi-button-positive',
'click': function() { doStart(m.id); }
}, m.label);
})));
}
}
function renderYt() {
ytCard.innerHTML = '';
ytCard.appendChild(E('h3', {}, 'Тест стратегий YouTube (Yv)'));
ytCard.appendChild(E('p', { 'class': 'zm-hint' }, 'Всегда тестируется отдельно от v/Flowseal — своим набором стратегий и доменов YouTube.'));
if (busy && curMode === 'youtube') {
ytCard.appendChild(E('div', { 'class': 'zm-row' }, [
E('span', { 'class': 'zm-label' }, 'Статус'),
zm.badge(true, 'тест выполняется', '')
]));
ytCard.appendChild(E('div', { 'class': 'zm-actions' }, [ stopButton() ]));
} else if (busy) {
ytCard.appendChild(E('p', { 'class': 'zm-hint' }, 'Сейчас выполняется тест v/Flowseal — дождитесь его завершения.'));
} else {
ytCard.appendChild(E('div', { 'class': 'zm-actions' }, [
E('button', {
'class': 'cbi-button cbi-button-positive',
'click': function() { doStart('youtube'); }
}, 'Тестировать YouTube')
]));
}
}
function startPolling() {
logEl.classList.add('zm-show');
zm.pollJob('strategy_test', logEl, function(ok) {
busy = false;
curMode = '';
zm.toast(ok ? 'Тест завершён' : 'Тест завершился с ошибкой', ok ? 'info' : 'error');
renderMain();
renderYt();
zm.testStatus().then(function(res) {
status = res;
renderResultsButtons();
});
});
}
function doStart(mode) {
if (busy) { zm.toast('Дождитесь завершения текущего теста', 'warning'); return; }
busy = true;
curMode = mode;
renderMain();
renderYt();
zm.toast('Запускаем тест стратегий', 'warning');
zm.testAction('start', mode).then(function(res) {
if (res.error) { busy = false; curMode = ''; zm.toast(res.error, 'error'); renderMain(); renderYt(); return; }
startPolling();
}).catch(function() { busy = false; curMode = ''; renderMain(); renderYt(); });
}
function doStop() {
zm.toast('Останавливаем тест', 'warning');
zm.testAction('stop').then(function(res) {
if (res.error) { zm.toast(res.error, 'error'); return; }
zm.toast('Тест остановлен, конфигурация восстанавливается', 'info');
});
}
function renderResultsColored(el, text) {
el.innerHTML = '';
var lines = (text || '').split('\n').filter(function(l) { return l; });
var controlOk = null;
lines.forEach(function(line) {
var m = line.match(/^(.*?)\s*→\s*(\d+)\/(\d+)\s*$/);
if (m && /^Контрольный тест/.test(m[1])) controlOk = +m[2];
});
var first = true;
lines.forEach(function(line) {
var div = document.createElement('div');
var m = line.match(/^(.*?)\s*→\s*(\d+)\/(\d+)\s*$/);
if (m) {
var name = m[1], ok = +m[2], total = +m[3];
var isControl = /^Контрольный тест/.test(name);
var nameSpan = document.createElement('span');
nameSpan.textContent = name + ' → ';
var scoreSpan = document.createElement('span');
scoreSpan.textContent = ok + '/' + total;
if (isControl) {
div.className = 'zm-log-code';
} else {
if (ok === total) scoreSpan.className = 'zm-log-msg-ok';
else if (controlOk !== null && ok < controlOk) scoreSpan.className = 'zm-log-msg-error';
else scoreSpan.className = 'zm-log-msg-warn';
if (first) { nameSpan.style.fontWeight = '700'; first = false; }
}
div.appendChild(nameSpan);
div.appendChild(scoreSpan);
} else {
div.className = 'zm-log-code';
div.textContent = line;
}
el.appendChild(div);
});
}
function showResultsFor(mode) {
zm.testResults(mode).then(function(res) {
resultsEl.classList.add('zm-show');
renderResultsColored(resultsEl, res.lines || '');
});
}
function renderResultsButtons() {
resultsButtonsEl.innerHTML = '';
var actions = [];
RESULT_MODES.forEach(function(m) {
if (!status['has_results_' + m]) return;
actions.push(E('button', {
'class': 'cbi-button',
'click': function() { showResultsFor(m); }
}, 'Показать результаты: ' + MODE_LABELS[m]));
});
if (actions.length) {
resultsButtonsEl.appendChild(E('div', { 'class': 'zm-actions' }, actions));
} else {
resultsButtonsEl.appendChild(E('p', { 'class': 'zm-hint' }, 'Пока нет сохранённых результатов — запустите тест.'));
}
}
var resultsCard = E('div', { 'class': 'zm-card' }, [
E('h3', {}, 'Результаты тестирования'),
resultsButtonsEl,
resultsEl
]);
renderMain();
renderYt();
renderResultsButtons();
if (busy) { startPolling(); }
wrap.appendChild(mainCard);
wrap.appendChild(ytCard);
wrap.appendChild(logEl);
wrap.appendChild(resultsCard);
return wrap;
}
});
ZM_INSTALLER_EOF
mkdir -p /www/luci-static/resources/view/zapret-manager mkdir -p /www/luci-static/resources/view/zapret-manager
cat > '/www/luci-static/resources/view/zapret-manager/tgproxy.js' << 'ZM_INSTALLER_EOF' cat > '/www/luci-static/resources/view/zapret-manager/tgproxy.js' << 'ZM_INSTALLER_EOF'
'use strict'; 'use strict';