682 lines
54 KiB
Python
682 lines
54 KiB
Python
#!/usr/bin/env python3
|
||
"""3X-UI_KIT (форк hacker07): накатывает наши «швы» на чистый код автора.
|
||
|
||
Это источник правды для форка: единственное место, где описано, ЧТО именно мы правим в
|
||
файлах автора. Всё, что мы дописываем по существу, живёт в scripts/kit-local.sh — файле,
|
||
который автор не трогает, поэтому обновления автора его не ломают.
|
||
|
||
Правила:
|
||
* каждый якорь обязан найтись РОВНО один раз, иначе — стоп (автор изменил код, правку
|
||
надо пересмотреть руками);
|
||
* при любой ошибке ничего не пишется наполовину: сначала прогоняй --check;
|
||
* открытый ключ подписи берётся из tools/fork-signer.pub (закрытая часть — только на
|
||
рабочей станции владельца, в репозиторий не попадает).
|
||
|
||
Использование:
|
||
python3 tools/fork_apply.py --check [корень-репозитория]
|
||
python3 tools/fork_apply.py [корень-репозитория]
|
||
"""
|
||
|
||
import os
|
||
import pathlib
|
||
import re
|
||
import sys
|
||
|
||
GITEA = os.environ.get("FORK_GITEA_BASE", "https://git.rukavishnikoff.ru/hacker07/3X-UI_autoVPS")
|
||
SIGNER_FILE = os.environ.get("FORK_SIGNER_PUB", "tools/fork-signer.pub")
|
||
|
||
# README.md форка — НАШ файл целиком: авторское описание из него убрано, чтобы никто не
|
||
# ставил сервер авторской командой и не оказался на авторском канале обновлений. Шва здесь нет:
|
||
# при обновлении из авторского проекта README конфликтует и решается в нашу пользу (см. FORK.md).
|
||
HOOK = """
|
||
# --- 3X-UI_KIT (форк hacker07): локальные команды и хуки ---
|
||
# Подключаем scripts/kit-local.sh, если он установлен. Наш файл: правки автора его не касаются.
|
||
# Все команды форка — под «kit local …» (см. kit local help).
|
||
KIT_LOCAL_LIB="${KIT_LOCAL_LIB:-/usr/local/lib/kit/kit-local.sh}"
|
||
if [[ -r $KIT_LOCAL_LIB ]]; then . "$KIT_LOCAL_LIB"; kit_local_dispatch "$@"; fi
|
||
"""
|
||
|
||
|
||
def die(msg):
|
||
print("! " + msg, file=sys.stderr)
|
||
sys.exit(1)
|
||
|
||
|
||
def ok(msg):
|
||
print("==> " + msg)
|
||
|
||
|
||
def count(text, anchor, regex):
|
||
if regex:
|
||
return len(re.findall(anchor, text, re.MULTILINE))
|
||
return text.count(anchor)
|
||
|
||
|
||
def main():
|
||
args = [a for a in sys.argv[1:]]
|
||
check = False
|
||
if "--check" in args:
|
||
check = True
|
||
args.remove("--check")
|
||
root = pathlib.Path(args[0] if args else ".").resolve()
|
||
if not (root / "VERSION").exists():
|
||
die("%s не похож на репозиторий 3X-UI_autoVPS (нет VERSION)" % root)
|
||
os.chdir(root)
|
||
|
||
version = (root / "VERSION").read_text(encoding="utf-8").strip()
|
||
if not re.fullmatch(r"[0-9]+(\.[0-9]+)+", version):
|
||
die("VERSION=%r: нужен числовой номер вида 1.2.1" % version)
|
||
|
||
signer_path = pathlib.Path(SIGNER_FILE)
|
||
if not signer_path.exists():
|
||
die(
|
||
"нет %s — сначала создай ключ подписи:\n"
|
||
" ssh-keygen -t ed25519 -C \"3X-UI KIT fork releases\" -f ~/.ssh/3x-ui-kit-fork/release_key\n"
|
||
" mkdir -p tools && ssh-keygen -y -f ~/.ssh/3x-ui-kit-fork/release_key > tools/fork-signer.pub"
|
||
% SIGNER_FILE
|
||
)
|
||
pub = ""
|
||
for line in signer_path.read_text(encoding="utf-8").splitlines():
|
||
line = line.strip()
|
||
if line.startswith(("ssh-ed25519 ", "ssh-rsa ")):
|
||
pub = line
|
||
break
|
||
if not pub:
|
||
die("%s: не нашёл строку открытого ключа (ssh-ed25519 …)" % signer_path)
|
||
|
||
version_re = r'^KIT_VERSION="[0-9.]+"$'
|
||
edits = [
|
||
(
|
||
'scripts/kit.sh',
|
||
'# kit – управление сервером 3X-UI KIT: пользователи, обновление, резервная копия.\n',
|
||
False,
|
||
'# kit – управление сервером 3X-UI_autoVPS: пользователи, обновление, резервная копия.\n',
|
||
'авто-шов: replace, строк 1',
|
||
),
|
||
(
|
||
'scripts/kit.sh',
|
||
'kit_ref_raw() { echo "https://raw.githubusercontent.com/itsnotkubrick/3X-UI_KIT/v$1"; }\n',
|
||
False,
|
||
'kit_ref_raw() { echo "https://git.rukavishnikoff.ru/hacker07/3X-UI_autoVPS/raw/tag/v$1"; }\n\n# --- 3X-UI_KIT (форк hacker07): локальные команды и хуки ---\n# Подключаем scripts/kit-local.sh, если он установлен. Наш файл: правки автора его не касаются.\n# Все команды форка — под «kit local …» (см. kit local help).\nKIT_LOCAL_LIB="${KIT_LOCAL_LIB:-/usr/local/lib/kit/kit-local.sh}"\nif [[ -r $KIT_LOCAL_LIB ]]; then . "$KIT_LOCAL_LIB"; kit_local_dispatch "$@"; fi\n\n',
|
||
'авто-шов: replace, строк 1',
|
||
),
|
||
(
|
||
'scripts/kit.sh',
|
||
'Description=3X-UI KIT: общий лимит трафика пользователей (сумма по всем записям)\n',
|
||
False,
|
||
'Description=3X-UI_autoVPS: общий лимит трафика пользователей (сумма по всем записям)\n',
|
||
'авто-шов: replace, строк 1',
|
||
),
|
||
(
|
||
'scripts/kit.sh',
|
||
'Description=3X-UI KIT: проверка общего лимита трафика раз в 5 минут\n',
|
||
False,
|
||
'Description=3X-UI_autoVPS: проверка общего лимита трафика раз в 5 минут\n',
|
||
'авто-шов: replace, строк 1',
|
||
),
|
||
(
|
||
'scripts/kit.sh',
|
||
' # SHA256:bNjI10eGVGjdFeDJnAHvUFi73rlrXVy6vKFf8lmNyxU (itsnotkubrick, 2026-10-14; прежний ключ SHA256:VDuGgJ8d… утерян, снят)\n "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEWDFGq2Wno/ADRS2E6UKsgFbUSLvaiPIfRBvSEGJr81 3X-UI KIT releases"\n',
|
||
False,
|
||
' # Наш шов: здесь только ключ форка (hacker07). Авторские ключи сняты намеренно — сервер обязан\n # принимать обновления лишь из нашего канала (git.rukavishnikoff.ru/hacker07/3X-UI_autoVPS).\n "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKM0Sfn/UQeqjvUvY9wVgg7QtG1RR2pEUkqrUD5Yy6EF 3X-UI_autoVPS fork releases (hacker07)"\n',
|
||
'авто-шов: replace, строк 2',
|
||
),
|
||
(
|
||
'scripts/kit.sh',
|
||
'update_hint() {\n',
|
||
False,
|
||
'update_hint() { # подсказка про новую версию\n [[ -d /etc/kit ]] || return 0 # kit удалён — подсказывать нечего (раньше тут сыпались ошибки про latest-version)\n',
|
||
'авто-шов: replace, строк 1',
|
||
),
|
||
(
|
||
'scripts/kit.sh',
|
||
' echo "3X-UI KIT $KIT_VERSION"\n',
|
||
False,
|
||
' echo "3X-UI_autoVPS $KIT_VERSION"\n',
|
||
'авто-шов: replace, строк 1',
|
||
),
|
||
(
|
||
'scripts/kit.sh',
|
||
' curl -fsS -m 5 "$KIT_RAW/CHANGELOG.md" 2>/dev/null | awk -v v="## v$1" \'\n',
|
||
False,
|
||
' { curl -fsS -m 5 "$KIT_RAW/CHANGELOG.md" 2>/dev/null; curl -fsS -m 5 "$KIT_RAW/FORK-CHANGELOG.md" 2>/dev/null; } | awk -v v="## v$1" \'\n',
|
||
'авто-шов: replace, строк 1',
|
||
),
|
||
(
|
||
'scripts/kit.sh',
|
||
' grep -qx "# 3X-UI KIT $v" "$d/SHA256SUMS" || { warn "Подписанный релиз не той версии – ничего не ставлю."; return 1; }\n for f in scripts/kit.sh scripts/kit-sub.py; do\n',
|
||
False,
|
||
' grep -qxE "# (3X-UI KIT|3X-UI_autoVPS) $v" "$d/SHA256SUMS" || { warn "Подписанный релиз не той версии – ничего не ставлю."; return 1; }\n for f in scripts/kit.sh scripts/kit-sub.py scripts/kit-local.sh; do\n',
|
||
'авто-шов: replace, строк 2',
|
||
),
|
||
(
|
||
'scripts/kit.sh',
|
||
'Description=kit-sub: подписка с учётом приложения (3X-UI KIT)\n',
|
||
False,
|
||
'Description=kit-sub: подписка с учётом приложения (3X-UI_autoVPS)\n',
|
||
'авто-шов: replace, строк 1',
|
||
),
|
||
(
|
||
'scripts/kit.sh',
|
||
'Description=3X-UI KIT: автообновление kit и kit-sub (только подписанные релизы)\n',
|
||
False,
|
||
'Description=3X-UI_autoVPS: автообновление kit и kit-sub (только подписанные релизы)\n',
|
||
'авто-шов: replace, строк 1',
|
||
),
|
||
(
|
||
'scripts/kit.sh',
|
||
'Description=3X-UI KIT: проверка обновлений раз в сутки\n',
|
||
False,
|
||
'Description=3X-UI_autoVPS: проверка обновлений раз в сутки\n',
|
||
'авто-шов: replace, строк 1',
|
||
),
|
||
(
|
||
'scripts/kit.sh',
|
||
' if [[ $latest == "$KIT_VERSION" ]]; then say "Применяю исправления версии $latest"; else say "3X-UI KIT $KIT_VERSION → $latest"; fi\n',
|
||
False,
|
||
' if [[ $latest == "$KIT_VERSION" ]]; then say "Применяю исправления версии $latest"; else say "3X-UI_autoVPS $KIT_VERSION → $latest"; fi\n',
|
||
'авто-шов: replace, строк 1',
|
||
),
|
||
(
|
||
'scripts/kit.sh',
|
||
' install -m 755 "$tmp/kit.sh" /usr/local/bin/kit.new && mv -f /usr/local/bin/kit.new /usr/local/bin/kit\n',
|
||
False,
|
||
' install -m 755 "$tmp/kit.sh" /usr/local/bin/kit.new && mv -f /usr/local/bin/kit.new /usr/local/bin/kit\n # Форк: ставим наши локальные дополнения из того же подписанного релиза.\n install -d -m 755 /usr/local/lib/kit\n install -m 644 "$tmp/kit-local.sh" /usr/local/lib/kit/kit-local.sh\n',
|
||
'авто-шов: insert, строк 1',
|
||
),
|
||
(
|
||
'scripts/kit.sh',
|
||
' echo "${G}✓ Готово: 3X-UI KIT $latest.${N} Пользователи, ссылки и подписки не менялись."\n',
|
||
False,
|
||
' echo "${G}✓ Готово: 3X-UI_autoVPS $latest.${N} Пользователи, ссылки и подписки не менялись."\n',
|
||
'авто-шов: replace, строк 1',
|
||
),
|
||
(
|
||
'scripts/kit.sh',
|
||
'split_panel_ours() { # наш профиль: JSON или ссылка happ://routing/onadd/<base64> с Name "3X-UI KIT"\n',
|
||
False,
|
||
'split_panel_ours() { # наш профиль: JSON или ссылка happ://routing/onadd/<base64> с Name "3X-UI_autoVPS"\n',
|
||
'авто-шов: replace, строк 1',
|
||
),
|
||
(
|
||
'scripts/kit.sh',
|
||
' grep -q \'"Name":"3X-UI KIT"\' <<<"$v"\n',
|
||
False,
|
||
' grep -q \'"Name":"3X-UI_autoVPS"\' <<<"$v"\n',
|
||
'авто-шов: replace, строк 1',
|
||
),
|
||
(
|
||
'scripts/kit.sh',
|
||
' box "3X-UI KIT $KIT_VERSION – управление сервером" "1. Пользователи" "2. Протоколы, порты и маскировка" "3. Проверка сервера" "" \\\n',
|
||
False,
|
||
' box "3X-UI_autoVPS $KIT_VERSION – управление сервером" "1. Пользователи" "2. Протоколы, порты и маскировка" "3. Проверка сервера" "" \\\n',
|
||
'авто-шов: replace, строк 1',
|
||
),
|
||
(
|
||
'scripts/kit.sh',
|
||
' echo "${B}Полное удаление 3X-UI KIT${N}"\n',
|
||
False,
|
||
' echo "${B}Полное удаление 3X-UI_autoVPS${N}"\n',
|
||
'авто-шов: replace, строк 1',
|
||
),
|
||
(
|
||
'scripts/kit.sh',
|
||
'${B}3X-UI KIT $KIT_VERSION${N} – команды (то же самое есть в меню: просто ${B}kit${N})\n',
|
||
False,
|
||
'${B}3X-UI_autoVPS $KIT_VERSION${N} – команды (то же самое есть в меню: просто ${B}kit${N})\n',
|
||
'авто-шов: replace, строк 1',
|
||
),
|
||
(
|
||
'scripts/3x-ui.sh',
|
||
'KIT_RAW="https://raw.githubusercontent.com/itsnotkubrick/3X-UI_KIT/v$KIT_VERSION"\n',
|
||
False,
|
||
'KIT_RAW="https://git.rukavishnikoff.ru/hacker07/3X-UI_autoVPS/raw/tag/v$KIT_VERSION"\n',
|
||
'авто-шов: replace, строк 1',
|
||
),
|
||
(
|
||
'scripts/3x-ui.sh',
|
||
'SNI_CANDIDATES=(dl.google.com www.amazon.com www.samsung.com www.yahoo.com)\n',
|
||
False,
|
||
'# Наш шов: добавлены известные сайты (Microsoft, AMD, Intel, NVIDIA) — их можно выбрать вручную.\nSNI_CANDIDATES=(dl.google.com www.amazon.com www.samsung.com www.yahoo.com www.microsoft.com www.amd.com www.intel.com www.nvidia.com)\n',
|
||
'авто-шов: replace, строк 1',
|
||
),
|
||
(
|
||
'scripts/3x-ui.sh',
|
||
" seq 1 254 | xargs -P 8 -I{} bash -c '\n",
|
||
False,
|
||
" # Наш шов: 32 потока вместо 8 и таймаут 3 с — скан подсети занимает секунды, а не минуту.\n seq 1 254 | xargs -P 32 -I{} bash -c '\n",
|
||
'авто-шов: replace, строк 1',
|
||
),
|
||
(
|
||
'scripts/3x-ui.sh',
|
||
' echo | timeout 4 openssl s_client -connect "$ip:443" -tls1_3 -alpn h2 2>/dev/null | openssl x509 -noout -ext subjectAltName 2>/dev/null \\\n | tr "," "\\n" | sed -n "s/^ *DNS://p"\' _ "$1.{}" "$me" 2>/dev/null | sort -u | grep -E "$SNI_RE" | head -40 || true\n',
|
||
False,
|
||
' echo | timeout 3 openssl s_client -connect "$ip:443" -tls1_3 -alpn h2 2>/dev/null | openssl x509 -noout -ext subjectAltName 2>/dev/null \\\n | tr "," "\\n" | sed -n "s/^ *DNS://p"\' _ "$1.{}" "$me" 2>/dev/null | sort -u | grep -E "$SNI_RE" | head -60 || true\n',
|
||
'авто-шов: replace, строк 2',
|
||
),
|
||
(
|
||
'scripts/3x-ui.sh',
|
||
' local me a b c base n names found=0\n',
|
||
False,
|
||
' local me a b c base n names out\n',
|
||
'авто-шов: replace, строк 1',
|
||
),
|
||
(
|
||
'scripts/3x-ui.sh',
|
||
' [[ $me =~ ^[0-9]{1,3}(\\.[0-9]{1,3}){3}$ ]] || return 0\n IFS=. read -r a b c _ <<<"$me"\n',
|
||
False,
|
||
' [[ $me =~ ^[0-9]{1,3}(\\.[0-9]{1,3}){3}$ ]] || return 0\n IFS=. read -r a b c _ <<<"$me"\n # Наш шов: имена проверяются параллельно (8 сразу) — раньше по очереди, и это была основная\n # причина «долго сканирует». Функции передаём в подпроцессы через export -f.\n export -f same_net sni_alive sni_trusted sni_quality sni_ok host_ip\n export HOST\n',
|
||
'авто-шов: insert, строк 2',
|
||
),
|
||
(
|
||
'scripts/3x-ui.sh',
|
||
' names=$(nearby_scan "$base")\n for n in $names; do\n [[ $n == "$me" || $n =~ $SNI_DYN_RE || $n =~ $SNI_BRAND_RE || $n =~ $SNI_BAD_RE ]] && continue\n if same_net "$n" && sni_alive "$n" && sni_trusted "$n" && sni_quality "$n"; then echo "$n"; found=$((found + 1)); ((found >= 8)) && return 0; fi\n done\n ((found > 0)) && return 0\n',
|
||
False,
|
||
' names=$(nearby_scan "$base" | grep -vE "^(${me})$" || true)\n [[ -n $names ]] || continue\n out=$(while IFS= read -r n; do\n [[ $n =~ $SNI_DYN_RE || $n =~ $SNI_BRAND_RE || $n =~ $SNI_BAD_RE ]] && continue\n printf \'%s\\n\' "$n"\n done <<<"$names" | xargs -P 8 -I{} bash -c \'same_net "$1" && sni_alive "$1" && sni_trusted "$1" && sni_quality "$1" && printf "%s\\n" "$1"\' _ {} 2>/dev/null | head -8)\n [[ -n $out ]] && { printf \'%s\\n\' "$out"; return 0; }\n',
|
||
'авто-шов: replace, строк 6',
|
||
),
|
||
(
|
||
'scripts/3x-ui.sh',
|
||
' [[ -x $acme ]] || { warn "Не нашёл acme.sh, которым установщик 3X-UI получает сертификаты: не могу выпустить сертификат для $DOMAIN."; return 1; }\n',
|
||
False,
|
||
' [[ -x $acme ]] || { warn "Не нашёл acme.sh, которым установщик 3X-UI получает сертификаты: не могу выпустить сертификат для $DOMAIN."; return 1; }\n # Наш шов: лимит Let\'s Encrypt считается на точный набор имён (5 сертификатов за 7 суток). Если у acme.sh\n # уже лежит годный сертификат на этот домен — берём его и не заказываем новый, лишний заказ упирается в лимит.\n local need_issue=yes\n if [[ -s /root/.acme.sh/${DOMAIN}_ecc/fullchain.cer ]] \\\n && openssl x509 -in "/root/.acme.sh/${DOMAIN}_ecc/fullchain.cer" -noout -checkhost "$DOMAIN" 2>/dev/null | grep -q \'does match\' \\\n && openssl x509 -in "/root/.acme.sh/${DOMAIN}_ecc/fullchain.cer" -noout -checkend 86400 >/dev/null 2>&1; then\n ok "у acme.sh уже есть годный сертификат для ${B}$DOMAIN${N} — беру его, новый не заказываю"\n need_issue=no\n fi\n',
|
||
'авто-шов: insert, строк 1',
|
||
),
|
||
(
|
||
'scripts/3x-ui.sh',
|
||
' [[ $rc == 0 || $rc == 2 ]] || { warn "Let\'s Encrypt не выдал сертификат для $DOMAIN. Обычно дело в одном из трёх: A-запись ещё не обновилась, порт 80 закрыт у хостера или домен за проксированием Cloudflare. Лог: $log"; return 1; }\n',
|
||
False,
|
||
' [[ $rc == 0 || $rc == 2 ]] || {\n # Наш шов: лимит Let\'s Encrypt — частая причина, и она не про сервер. Говорим прямо и с датой повтора.\n if grep -qE \'rateLimited|too many certificates\' "$log"; then\n local till\n till=$(grep -oE \'retry after [0-9-]+ [0-9:]+ UTC\' "$log" | tail -1 | sed \'s/^retry after //\')\n warn "Лимит Let\'s Encrypt: на имя ${B}$DOMAIN${N} за 7 суток уже выпущено 5 сертификатов${till:+ — повтор возможен после $till}. С вашим сервером всё в порядке: DNS, порты и ufw не при чём. Подождите до этой даты либо возьмите другое имя (например node3.…): лимит считается на точный набор имён."\n else\n warn "Let\'s Encrypt не выдал сертификат для $DOMAIN. Обычно дело в одном из трёх: A-запись ещё не обновилась, порт 80 закрыт у хостера или домен за проксированием Cloudflare. Лог: $log"\n fi\n return 1\n }\n',
|
||
'авто-шов: replace, строк 1',
|
||
),
|
||
(
|
||
'scripts/3x-ui.sh',
|
||
" _ _ _ _ _ _ _\n (_) |_ ___ _ __ ___ | |_| | ___ _| |__ _ __(_) ___| | __\n | | __/ __| '_ \\ / _ \\| __| |/ / | | | '_ \\| '__| |/ __| |/ /\n | | |_\\__ \\ | | | (_) | |_| <| |_| | |_) | | | | (__| <\n |_|\\__|___/_| |_|\\___/ \\__|_|\\_\\\\__,_|_.__/|_| |_|\\___|_|\\_\\\n",
|
||
False,
|
||
" _ _ _ ___ _____\n | | | | __ _ ___| | _____ _ __ / _ \\___ |\n | |_| |/ _` |/ __| |/ / _ \\ '__| | | | | / /\n | _ | (_| | (__| < __/ | | |_| |/ /\n |_| |_|\\__,_|\\___|_|\\_\\___|_| \\___//_/\n",
|
||
'авто-шов: replace, строк 5',
|
||
),
|
||
(
|
||
'scripts/3x-ui.sh',
|
||
' echo "${B}3X-UI KIT $KIT_VERSION${N} · it\'s not Kubrick. it\'s just a VPN."\n',
|
||
False,
|
||
' echo "${B}3X-UI_autoVPS $KIT_VERSION${N} · сборка форка: панель 3X-UI и VPN-протоколы на одном порту"\n',
|
||
'авто-шов: replace, строк 1',
|
||
),
|
||
(
|
||
'scripts/3x-ui.sh',
|
||
' later "Адрес $HOST не найден на сетевых интерфейсах сервера (бывает за NAT). Если ссылки не работают – запустите с --host и верным IP."\n fi\n\n',
|
||
False,
|
||
' later "Адрес $HOST не найден на сетевых интерфейсах сервера (бывает за NAT). Если ссылки не работают – запустите с --host и верным IP."\n fi\n\n# ---------------------------------------------------------------------------\n# Наш шов: выбор сайта маскировки по результатам скана подсети.\n# Печатает выбранное имя в stdout (пусто — оставляем автоподбор автора).\n# role — подпись для человека: REALITY или XHTTP/MTProto.\nkl_sni_menu() {\n local role=$1; shift\n local -a found=("$@")\n local -a known=("${SNI_CANDIDATES[@]}")\n local src=${KL_ASK_SRC:-/dev/tty} ans i k n auto_idx known_idx own_idx try\n n=${#found[@]}\n printf \'\\n%s%s%s\\n\' "$B" "$role" "$N" >&2\n if ((n)); then\n printf \'Найдено в подсети:\\n\\n\' >&2\n for i in "${!found[@]}"; do printf \' %d - %s\\n\' "$((i + 1))" "${found[i]}" >&2; done\n auto_idx=$((n + 1)); known_idx=$((n + 2)); own_idx=$((n + 3))\n printf \' %d - автовыбор из списка\\n\' "$auto_idx" >&2\n printf \' %d - выбрать общедоступный (например %s, %s)\\n\' "$known_idx" "${known[0]}" "${known[1]}" >&2\n printf \' %d - ввести свой домен\\n\\n\' "$own_idx" >&2\n else\n auto_idx=0; known_idx=1; own_idx=2\n printf \'В подсети доменов не найдено, выберите:\\n\\n\' >&2\n printf \' 1 - общедоступный (например %s, %s)\\n\' "${known[0]}" "${known[1]}" >&2\n printf \' 2 - ввести свой домен\\n\\n\' >&2\n fi\n printf \'Ваш выбор (Enter — автовыбор): \' >&2\n for try in 1 2 3; do\n ans=""\n read -r -t 300 ans <"$src" 2>/dev/null || true\n ans=${ans//[[:space:]]/}\n [[ -z $ans ]] && return 0\n [[ $ans =~ ^[0-9]+$ ]] || { printf \' введите номер из списка\\n\' >&2; continue; }\n i=$((10#$ans))\n if ((n)) && ((i >= 1 && i <= n)); then printf \'sni:%s\' "${found[i - 1]}"; return 0; fi\n ((i == auto_idx)) && return 0\n if ((i == known_idx)); then\n printf \'\\nОбщедоступные сайты:\\n\' >&2\n for k in "${!known[@]}"; do printf \' %d - %s\\n\' "$((k + 1))" "${known[k]}" >&2; done\n printf \' 0 - назад\\nВаш выбор: \' >&2\n ans=""\n read -r -t 300 ans <"$src" 2>/dev/null || true\n ans=${ans//[[:space:]]/}\n if [[ $ans =~ ^[0-9]+$ ]] && ((10#$ans >= 1 && 10#$ans <= ${#known[@]})); then\n printf \'sni:%s\' "${known[10#$ans - 1]}"; return 0\n fi\n printf \' ничего не выбрано — оставляю автовыбор\\n\' >&2\n return 0\n fi\n if ((i == own_idx)); then\n printf \' адрес сайта (например vpn.example.com): \' >&2\n ans=""\n read -r -t 300 ans <"$src" 2>/dev/null || true\n ans=${ans//[[:space:]]/}\n [[ $ans =~ ^[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$ ]] && { printf \'own:%s\' "$ans"; return 0; }\n printf \' не похоже на адрес сайта — оставляю автовыбор\\n\' >&2\n return 0\n fi\n printf \' такого номера нет\\n\' >&2\n done\n return 0\n}\n\n',
|
||
'авто-шов: insert, строк 3',
|
||
),
|
||
(
|
||
'scripts/3x-ui.sh',
|
||
' elif [[ -z $SNI && $yes == no && $PANEL_SSL == ip && $multi == no && -t 1 ]] && { : </dev/tty; } 2>/dev/null; then\n',
|
||
False,
|
||
' # Наш шов: когда выбор сайта делает наше меню (KL_PICK_SNI=1), меню автора не показываем — иначе\n # человек отвечает на «Свой домен / Сосед / Свой сайт», и до скана подсети дело не доходит.\n elif [[ -z $SNI && $yes == no && $PANEL_SSL == ip && $multi == no && -t 1 && ${KL_PICK_SNI:-0} != 1 ]] && { : </dev/tty; } 2>/dev/null; then\n',
|
||
'авто-шов: replace, строк 1',
|
||
),
|
||
(
|
||
'scripts/3x-ui.sh',
|
||
' echo "Подбираю сайт для маскировки среди соседей по подсети… (около 250 коротких обращений к порту 443 соседних адресов, около минуты; не нужно – запустите с --sni или --domain)"\n',
|
||
False,
|
||
' echo "Сканирую подсеть: около 250 коротких обращений к порту 443 соседних адресов (около минуты)."\n echo "По результатам предложу сайты для маскировки. Не нужно – запустите с --sni или --domain."\n',
|
||
'авто-шов: replace, строк 1',
|
||
),
|
||
(
|
||
'scripts/3x-ui.sh',
|
||
' if ((${#nb[@]})); then\n SNI=${nb[0]}; SNI2=${nb[1]:-}; SNI3=${nb[2]:-}\n',
|
||
False,
|
||
' # Наш шов: показываем найденное и даём выбрать — отдельно для REALITY и для XHTTP/MTProto.\n if [[ ${KL_PICK_SNI:-} == 1 && $yes == no ]] && { : </dev/tty; } 2>/dev/null; then\n local pick pick2 own_sni=no\n pick=$(kl_sni_menu "REALITY (TCP 443) — под какой сайт маскировать" ${nb[@]+"${nb[@]}"})\n case $pick in\n own:*)\n own_sni=yes\n DOMAIN=${pick#own:}\n # Наш шов: домен вводится заново, пока он не указывает на этот сервер. Раньше при промахе\n # установка молча маскировалась под чужой сайт, а при домене из флага — обрывалась.\n # A-запись у регистратора расходится не мгновенно, поэтому первым пунктом — «проверить снова».\n while :; do\n if domain_points_here "$DOMAIN"; then\n SNI=$DOMAIN\n ok "маскировка под свой домен: ${B}$DOMAIN${N} (сертификат Let\'s Encrypt выпустит установщик)"\n # Наш шов: спрашиваем, где открывать панель и подписку, и при интерактивном вводе домена тоже.\n # У автора этот вопрос задавался только при домене из флага --domain, а при выборе «ввести свой\n # домен» в меню маскировки — нет, и панель молча оставалась по IP с самоподписанным сертификатом.\n panel_on_choice\n break\n fi\n echo " Домен ${B}$DOMAIN${N} пока не указывает на этот сервер." >&2\n echo " ${B}1${N} проверить снова A-запись расходится по DNS от нескольких минут до нескольких часов" >&2\n echo " ${B}2${N} ввести другой домен" >&2\n echo " ${B}3${N} без своего домена: маскировка соседом по подсети" >&2\n ask_tty " Выбор [1]: " || break\n case ${REPLY//[[:space:]]/} in\n 2) ask_tty " адрес сайта (например vpn.example.com): " || break\n local d=${REPLY//[[:space:]]/}\n [[ -n $d ]] && DOMAIN=$d ;;\n 3) own_sni=no; DOMAIN=""; break ;;\n *) sleep 5 ;;\n esac\n done\n ;;\n sni:*) SNI=${pick#sni:} ;;\n esac\n if [[ -n $SNI ]]; then\n if [[ $own_sni == no ]] && ! sni_ok "$SNI"; then\n warn "$SNI не отвечает по TLS 1.3 + HTTP/2 — беру найденное в подсети"\n SNI="${nb[0]:-$SNI}"\n fi\n ok "маскировка REALITY: ${B}$SNI${N}"\n fi\n if [[ " ${PROTOS[*]} " == *" xhttp "* || " ${PROTOS[*]} " == *" mtproto "* ]]; then\n pick2=$(kl_sni_menu "XHTTP и MTProto (различаются по имени сайта) — какой сайт взять" ${nb[@]+"${nb[@]}"})\n [[ $pick2 == sni:* ]] && pick2=${pick2#sni:}\n [[ $pick2 == own:* ]] && pick2=${pick2#own:}\n [[ -n $pick2 ]] && SNI2=$pick2\n [[ -n $SNI2 ]] && ok "маскировка XHTTP/MTProto: ${B}$SNI2${N}"\n fi\n fi\n if ((${#nb[@]})); then ok "подходящих соседей найдено: ${#nb[@]}"\n else warn "в подсети подходящих сайтов не нашлось (адреса проверены, откликов нет)"; fi\n if [[ -z $SNI ]] && ((${#nb[@]})); then\n SNI=${nb[0]}; SNI2=${SNI2:-${nb[1]:-}}; SNI3=${nb[2]:-}\n',
|
||
'авто-шов: replace, строк 2',
|
||
),
|
||
(
|
||
'scripts/3x-ui.sh',
|
||
' ok "найден: ${B}$SNI${N} · та же подсеть · TLS 1.3, h2, сертификат верный"\n else\n',
|
||
False,
|
||
' ok "найден: ${B}$SNI${N} · та же подсеть · TLS 1.3, h2, сертификат верный"\n elif [[ -z $SNI ]]; then\n',
|
||
'авто-шов: replace, строк 2',
|
||
),
|
||
(
|
||
'scripts/3x-ui.sh',
|
||
' if [[ $yes == no && -t 1 ]] && { : </dev/tty; } 2>/dev/null; then\n',
|
||
False,
|
||
' if [[ -n ${KL_PICK_SNI:-} && ${KL_PICK_SNI} == 1 && -z $SNI ]]; then\n : # сайт уже выбран в нашем меню — вопрос автора не нужен\n elif [[ $yes == no && -t 1 ]] && { : </dev/tty; } 2>/dev/null; then\n',
|
||
'авто-шов: replace, строк 1',
|
||
),
|
||
(
|
||
'scripts/3x-ui.sh',
|
||
' if [[ $TRUSTED == yes && $multi == no && ( -z $SNI2 || -z $SNI3 ) ]]; then\n',
|
||
False,
|
||
' # Наш шов: если XHTTP и MTProto не выбраны, имена для них сейчас не нужны — не сканируем соседей\n # по подсети впустую (около 250 обращений к чужим 443). Имена SNI2/SNI3 возьмутся из общего списка ниже.\n if [[ $TRUSTED == yes && $multi == no && ( -z $SNI2 || -z $SNI3 ) ]] \\\n && [[ " ${PROTOS[*]} " == *" xhttp "* || " ${PROTOS[*]} " == *" mtproto "* ]]; then\n',
|
||
'авто-шов: replace, строк 1',
|
||
),
|
||
(
|
||
'scripts/3x-ui.sh',
|
||
' issue_domain_cert || die "Без сертификата для $DOMAIN продолжать нельзя. Исправьте причину и запустите скрипт снова."\n',
|
||
False,
|
||
' # Наш шов: провал сертификата не обрывает установку. Человек может повторить попытку (DNS доехал,\n # лимит Let\'s Encrypt кончился) или ввести другое имя — установка продолжится с него же.\n while ! issue_domain_cert; do\n echo\n echo " ${B}Что делать дальше?${N}"\n echo " ${B}1${N} повторить попытку если ждали обновления A-записи или лимита Let\'s Encrypt"\n echo " ${B}2${N} ввести другой домен проверим, что он указывает на этот сервер, и повторим"\n echo " ${B}3${N} остановиться поставить без своего домена: перезапустите и выберите соседа по подсети"\n ask_tty " Выбор [1]: " || die "Сертификат для $DOMAIN не выпущен. Подробности: /var/log/kit-domain-cert.log"\n case ${REPLY//[[:space:]]/} in\n 2) ask_tty " адрес сайта (например vpn.example.com): " || continue\n local d=${REPLY//[[:space:]]/}\n [[ -n $d ]] || continue\n if domain_points_here "$d"; then DOMAIN=$d; SNI=$DOMAIN\n else warn "оставляю прежний домен $DOMAIN — новый не указывает на этот сервер"; fi ;;\n 3) die "Установка остановлена по вашему выбору. Чтобы поставить без домена — перезапустите и выберите соседа по подсети или общедоступный сайт." ;;\n *) sleep 5 ;;\n esac\n done\n',
|
||
'авто-шов: replace, строк 1',
|
||
),
|
||
(
|
||
'scripts/3x-ui.sh',
|
||
' ok "подключения работают: REALITY, XHTTP и Hysteria2 проверены клиентом с самого сервера"\n',
|
||
False,
|
||
' ok "подключения работают (проверено клиентом с самого сервера): ${CREATED[*]:-нет}"\n',
|
||
'авто-шов: replace, строк 1',
|
||
),
|
||
(
|
||
'scripts/3x-ui.sh',
|
||
' local extra\n extra=$(sub_links "$SUBID-awg" 1; sub_links "$SUBID-tg" 1)\n',
|
||
False,
|
||
' local extra="" # пустая строка, иначе set -u валит скрипт, когда AWG/MTProto не выбраны\n # Наш шов: подписки «-awg»/«-tg» есть только у установок с AmneziaWG и MTProto — не тянем их,\n # если эти протоколы не выбраны (никаких лишних обращений к панели).\n if [[ " ${PROTOS[*]} " == *" awg "* || " ${PROTOS[*]} " == *" awg3 "* || " ${PROTOS[*]} " == *" mtproto "* ]]; then\n extra=$(sub_links "$SUBID-awg" 1; sub_links "$SUBID-tg" 1)\n fi\n',
|
||
'авто-шов: replace, строк 2',
|
||
),
|
||
(
|
||
'scripts/3x-ui.sh',
|
||
' echo "3X-UI KIT (3X-UI $XUI_VERSION) – данные для входа (файл виден только root)"\n',
|
||
False,
|
||
' echo "3X-UI_autoVPS (3X-UI $XUI_VERSION) – данные для входа (файл виден только root)"\n',
|
||
'авто-шов: replace, строк 1',
|
||
),
|
||
(
|
||
'scripts/3x-ui.sh',
|
||
' } >"$RESULT"\n\n echo\n echo "${G}${B}Готово! Сервер работает.${N}"\n [[ -n $DOMAIN ]] && echo "Маскировка: свой домен ${B}$DOMAIN${N}, сертификат Let\'s Encrypt продлевается сам."\n [[ $SELF_IP_CERT == yes ]] && later "Сертификат на IP самоподписанный: пользуйтесь ссылками на отдельные подключения, подписка в приложениях может не открыться."\n',
|
||
False,
|
||
' } >"$RESULT"\n\n echo\n',
|
||
'авто-шов: delete, строк 6',
|
||
),
|
||
(
|
||
'scripts/3x-ui.sh',
|
||
' echo "${B}ПАНЕЛЬ${N}"\n if [[ $TRUSTED == yes ]]; then\n echo " $panel_url"\n else\n echo " Только через SSH-туннель. На вашем компьютере:"\n echo " ssh -L $XUI_PANEL_PORT:127.0.0.1:$XUI_PANEL_PORT root@$HOST"\n echo " Затем в браузере: http://127.0.0.1:$XUI_PANEL_PORT/$XUI_WEB_BASE_PATH"\n fi\n echo " Логин: ${B}$XUI_USERNAME${N} Пароль: ${B}$XUI_PASSWORD${N}"\n echo " ${D}(сохранено в $RESULT, файл виден только root)${N}"\n echo\n if [[ $TRUSTED == yes ]]; then\n echo "${B}ПОДПИСКА${N} – одна ссылка для всех протоколов"\n echo " ${B}$SUB_URL${N}"\n echo " Вставьте в Happ, Hiddify, Karing, v2rayN, Clash Verge или FlClash."\n qrencode -t ANSIUTF8 -m 1 "$SUB_URL" || true\n else\n /usr/local/bin/kit __links "$NAME" "$SUBID" || true\n fi\n local -a tp=(\n "Управление сервером: введите команду ${B}kit${N} – откроется меню. Запоминать команды не нужно."\n "Ссылки на отдельные протоколы (REALITY, Hysteria2, Telegram и другие): kit → Пользователи → Показать ссылки."\n "Добавить пользователя: kit → Пользователи → Добавить."\n "Резервная копия: kit → Резервная копия. Храните её как пароль.")\n tips "Что дальше" "${tp[@]}"\n',
|
||
False,
|
||
' echo "${G}${B}Установка завершена.${N} Доступы к панели, подписка и QR-код — в самом конце вывода."\n',
|
||
'авто-шов: replace, строк 25',
|
||
),
|
||
(
|
||
'scripts/3x-ui.sh',
|
||
'KIT_CLI_URL="$KIT_RAW/scripts/kit.sh"\n',
|
||
False,
|
||
'KIT_CLI_URL="$KIT_RAW/scripts/kit.sh"\nKIT_LOCAL_URL="$KIT_RAW/scripts/kit-local.sh"\n',
|
||
'авто-шов: insert, строк 1',
|
||
),
|
||
(
|
||
'scripts/3x-ui.sh',
|
||
' else curl -fsSL --retry 3 -o /usr/local/bin/kit "$KIT_CLI_URL" && chmod 755 /usr/local/bin/kit; fi\n',
|
||
False,
|
||
' else curl -fsSL --retry 3 -o /usr/local/bin/kit "$KIT_CLI_URL" && chmod 755 /usr/local/bin/kit; fi\n # Форк: ставим наши локальные дополнения (см. scripts/kit-local.sh).\n install -d -m 755 /usr/local/lib/kit\n if [[ -n $src && -f $d/kit-local.sh ]]; then\n install -m 644 "$d/kit-local.sh" /usr/local/lib/kit/kit-local.sh\n else\n curl -fsSL --retry 3 -o /usr/local/lib/kit/kit-local.sh "$KIT_LOCAL_URL" \\\n || warn "Локальные дополнения kit не скачались: kit local не будет работать"\n fi\n',
|
||
'авто-шов: insert, строк 1',
|
||
),
|
||
(
|
||
'scripts/3x-ui.sh',
|
||
'KIT_INSTALL_CMD="bash <(curl -fsSL https://raw.githubusercontent.com/itsnotkubrick/3X-UI_KIT/main/scripts/3x-ui.sh)"\n',
|
||
False,
|
||
'KIT_INSTALL_CMD="bash <(curl -fsSL https://git.rukavishnikoff.ru/hacker07/3X-UI_autoVPS/raw/branch/main/scripts/3x-ui.sh)"\n',
|
||
'авто-шов: replace, строк 1',
|
||
),
|
||
(
|
||
'scripts/3x-ui.sh',
|
||
' echo "# Сгенерировано 3x-ui.sh (3X-UI KIT) – перезаписывается при повторном запуске."\n',
|
||
False,
|
||
' echo "# Сгенерировано 3x-ui.sh (3X-UI_autoVPS) – перезаписывается при повторном запуске."\n',
|
||
'авто-шов: replace, строк 1',
|
||
),
|
||
(
|
||
'scripts/3x-ui.sh',
|
||
'# Сгенерировано 3x-ui.sh (3X-UI KIT) – перезаписывается при повторном запуске.\n',
|
||
False,
|
||
'# Сгенерировано 3x-ui.sh (3X-UI_autoVPS) – перезаписывается при повторном запуске.\n',
|
||
'авто-шов: replace, строк 1',
|
||
),
|
||
(
|
||
'scripts/3x-ui.sh',
|
||
' upd=$(jq -c --arg path "$SUB_PATH" --argjson ip "$SUB_INTERNAL" --arg title "3X-UI KIT" --arg uri "$uri" \'\n',
|
||
False,
|
||
' upd=$(jq -c --arg path "$SUB_PATH" --argjson ip "$SUB_INTERNAL" --arg title "3X-UI_autoVPS" --arg uri "$uri" \'\n',
|
||
'авто-шов: replace, строк 1',
|
||
),
|
||
(
|
||
'scripts/3x-ui.sh',
|
||
' upd=$(jq -c --arg path "$SUB_PATH" --arg title "3X-UI KIT" \'\n',
|
||
False,
|
||
' upd=$(jq -c --arg path "$SUB_PATH" --arg title "3X-UI_autoVPS" \'\n',
|
||
'авто-шов: replace, строк 1',
|
||
),
|
||
(
|
||
'scripts/3x-ui.sh',
|
||
'Description=kit-sub: подписка с учётом приложения (3X-UI KIT)\n',
|
||
False,
|
||
'Description=kit-sub: подписка с учётом приложения (3X-UI_autoVPS)\n',
|
||
'авто-шов: replace, строк 1',
|
||
),
|
||
(
|
||
'scripts/3x-ui.sh',
|
||
' [[ $(LC_ALL=C tr -d \'\\n -~\' <"$f" | wc -c) -eq 0 ]] \\\n || die "Это не резервная копия 3X-UI KIT или она повреждена (файл ${f##*/}). Ничего не менял."\n',
|
||
False,
|
||
' [[ $(LC_ALL=C tr -d \'\\n -~\' <"$f" | wc -c) -eq 0 ]] \\\n || die "Это не резервная копия 3X-UI_autoVPS или она повреждена (файл ${f##*/}). Ничего не менял."\n',
|
||
'авто-шов: replace, строк 2',
|
||
),
|
||
(
|
||
'scripts/3x-ui.sh',
|
||
' [[ $rc -eq 1 ]] \\\n || die "Это не резервная копия 3X-UI KIT или она повреждена (файл ${f##*/}). Ничего не менял."\n',
|
||
False,
|
||
' [[ $rc -eq 1 ]] \\\n || die "Это не резервная копия 3X-UI_autoVPS или она повреждена (файл ${f##*/}). Ничего не менял."\n',
|
||
'авто-шов: replace, строк 2',
|
||
),
|
||
(
|
||
'scripts/3x-ui.sh',
|
||
' [[ " $* " == *" $n "* ]] || die "Это не резервная копия 3X-UI KIT или она повреждена (лишнее имя $n в ${f##*/}). Ничего не менял."\n',
|
||
False,
|
||
' [[ " $* " == *" $n "* ]] || die "Это не резервная копия 3X-UI_autoVPS или она повреждена (лишнее имя $n в ${f##*/}). Ничего не менял."\n',
|
||
'авто-шов: replace, строк 1',
|
||
),
|
||
(
|
||
'scripts/3x-ui.sh',
|
||
' *) die "Это не резервная копия 3X-UI KIT или она повреждена (имя $n в ${f##*/}). Ничего не менял." ;;\n',
|
||
False,
|
||
' *) die "Это не резервная копия 3X-UI_autoVPS или она повреждена (имя $n в ${f##*/}). Ничего не менял." ;;\n',
|
||
'авто-шов: replace, строк 1',
|
||
),
|
||
(
|
||
'scripts/3x-ui.sh',
|
||
' || die "Это не резервная копия 3X-UI KIT или она повреждена (значение $n в ${f##*/}). Ничего не менял."\n',
|
||
False,
|
||
' || die "Это не резервная копия 3X-UI_autoVPS или она повреждена (значение $n в ${f##*/}). Ничего не менял."\n',
|
||
'авто-шов: replace, строк 1',
|
||
),
|
||
(
|
||
'scripts/3x-ui.sh',
|
||
' python3 - "$file" "$tmp" <<\'PY\' || die "Это не резервная копия 3X-UI KIT или она повреждена. Ничего не менял."\n',
|
||
False,
|
||
' python3 - "$file" "$tmp" <<\'PY\' || die "Это не резервная копия 3X-UI_autoVPS или она повреждена. Ничего не менял."\n',
|
||
'авто-шов: replace, строк 1',
|
||
),
|
||
(
|
||
'scripts/3x-ui.sh',
|
||
' || die "Это не резервная копия 3X-UI KIT или она повреждена (порт панели). Ничего не менял."\n',
|
||
False,
|
||
' || die "Это не резервная копия 3X-UI_autoVPS или она повреждена (порт панели). Ничего не менял."\n',
|
||
'авто-шов: replace, строк 1',
|
||
),
|
||
(
|
||
'scripts/3x-ui.sh',
|
||
' [[ $BACKUP_HOST =~ ^[A-Za-z0-9.:-]+$ ]] || die "Это не резервная копия 3X-UI KIT или она повреждена (адрес сервера). Ничего не менял."\n',
|
||
False,
|
||
' [[ $BACKUP_HOST =~ ^[A-Za-z0-9.:-]+$ ]] || die "Это не резервная копия 3X-UI_autoVPS или она повреждена (адрес сервера). Ничего не менял."\n',
|
||
'авто-шов: replace, строк 1',
|
||
),
|
||
(
|
||
'scripts/3x-ui.sh',
|
||
' || die "Это не резервная копия 3X-UI KIT или она повреждена (нет своего сертификата). Ничего не менял."\n',
|
||
False,
|
||
' || die "Это не резервная копия 3X-UI_autoVPS или она повреждена (нет своего сертификата). Ничего не менял."\n',
|
||
'авто-шов: replace, строк 1',
|
||
),
|
||
(
|
||
'scripts/3x-ui.sh',
|
||
' [[ -n $DOMAIN ]] && { issue_domain_cert || die "Не удалось получить сертификат для $DOMAIN: порт 80 должен быть свободен, а A-запись домена указывать на этот сервер. Лог: /var/log/kit-domain-cert.log"; }\n',
|
||
False,
|
||
' # Наш шов: открываем 80 в ufw ПРЯМО ПЕРЕД выпуском сертификата. Установщик открывает список портов\n # позже (после сборки nginx), поэтому на свежем сервере проверка Let\'s Encrypt доходила до сервера и\n # «проглатывалась» — в логе acme это «Timeout during connect (likely firewall problem)».\n command -v ufw >/dev/null 2>&1 && ufw allow 80/tcp >/dev/null 2>&1 || true\n [[ -n $DOMAIN ]] && { issue_domain_cert || die "Без сертификата для $DOMAIN продолжать нельзя — причина выше, подробности в /var/log/kit-domain-cert.log"; }\n',
|
||
'авто-шов: replace, строк 1',
|
||
),
|
||
(
|
||
'scripts/hysteria2.sh',
|
||
'# установку 3X-UI KIT (scripts/3x-ui.sh). Файл оставлен, чтобы старая команда `hy2 update`\n',
|
||
False,
|
||
'# установку 3X-UI_autoVPS (scripts/3x-ui.sh). Файл оставлен, чтобы старая команда `hy2 update`\n',
|
||
'авто-шов: replace, строк 1',
|
||
),
|
||
(
|
||
'scripts/hysteria2.sh',
|
||
'Hysteria2 входит в основную установку 3X-UI KIT (один из 11 протоколов).\n',
|
||
False,
|
||
'Hysteria2 входит в основную установку 3X-UI_autoVPS (один из 11 протоколов).\n',
|
||
'авто-шов: replace, строк 1',
|
||
),
|
||
(
|
||
'scripts/kit-sub.py',
|
||
' data = {"Name": "3X-UI KIT", "GlobalProxy": "false", "DomainStrategy": "IPIfNonMatch", "ProxySites": sites,\n',
|
||
False,
|
||
' data = {"Name": "3X-UI_autoVPS", "GlobalProxy": "false", "DomainStrategy": "IPIfNonMatch", "ProxySites": sites,\n',
|
||
'авто-шов: replace, строк 1',
|
||
),
|
||
(
|
||
'scripts/kit-sub.py',
|
||
' data = {"Name": "3X-UI KIT", "DomainStrategy": "IPIfNonMatch", "RouteOrder": "block-proxy-direct",\n',
|
||
False,
|
||
' data = {"Name": "3X-UI_autoVPS", "DomainStrategy": "IPIfNonMatch", "RouteOrder": "block-proxy-direct",\n',
|
||
'авто-шов: replace, строк 1',
|
||
),
|
||
(
|
||
'scripts/kit-sub.py',
|
||
' main()\n',
|
||
False,
|
||
' main()\n\n',
|
||
'авто-шов: insert, строк 1',
|
||
),
|
||
(
|
||
'scripts/kit.sh',
|
||
version_re,
|
||
True,
|
||
f'KIT_VERSION="{version}"',
|
||
'версия сборки форка',
|
||
),
|
||
(
|
||
'scripts/3x-ui.sh',
|
||
version_re,
|
||
True,
|
||
f'KIT_VERSION="{version}"',
|
||
'версия сборки форка',
|
||
),
|
||
(
|
||
'scripts/hysteria2.sh',
|
||
version_re,
|
||
True,
|
||
f'KIT_VERSION="{version}"',
|
||
'версия сборки форка',
|
||
),
|
||
(
|
||
'scripts/kit.sh',
|
||
'KIT_RAW="https://raw.githubusercontent.com/itsnotkubrick/3X-UI_KIT/main"',
|
||
False,
|
||
'KIT_RAW="https://git.rukavishnikoff.ru/hacker07/3X-UI_autoVPS/raw/branch/main"',
|
||
'канал обновлений: наш Gitea вместо GitHub',
|
||
),
|
||
]
|
||
|
||
bad = 0
|
||
for path, anchor, is_re, new, why in edits:
|
||
p = pathlib.Path(path)
|
||
if not p.exists():
|
||
print("✗ %s: файла нет" % path)
|
||
bad += 1
|
||
continue
|
||
text = p.read_text(encoding="utf-8", newline="")
|
||
if check:
|
||
# --check смотрит на готовое дерево dev: наш шов обязан быть на месте и ровно один раз
|
||
n = text.count(new)
|
||
what, shown = "наш шов", new
|
||
else:
|
||
# применение — только на чистое дерево автора: якорь обязан найтись ровно один раз
|
||
n = count(text, anchor, is_re)
|
||
what, shown = "якорь автора", anchor
|
||
short = shown if len(shown) <= 54 else shown[:51] + "…"
|
||
short = short.replace("\n", "⏎")
|
||
if n != 1:
|
||
print("✗ %-20s %-56s %s найден %d раз (%s)" % (path, short, what, n, why))
|
||
bad += 1
|
||
continue
|
||
if check:
|
||
print("✓ %-20s %-56s (%s)" % (path, short, why))
|
||
continue
|
||
if is_re:
|
||
text = re.sub(anchor, new, text, count=1, flags=re.MULTILINE)
|
||
else:
|
||
text = text.replace(anchor, new, 1)
|
||
p.write_text(text, encoding="utf-8", newline="")
|
||
print("✓ %-20s %-56s (%s)" % (path, short, why))
|
||
|
||
|
||
# Массовая замена бренда в файлах автора: строки, которые видит человек (справка, версия,
|
||
# описания служб, баннер). Строку проверки шапки SHA256SUMS не трогаем — её правит отдельный шов,
|
||
# а сама шапка ещё один релиз пишется в старом виде (иначе уже установленные серверы откажутся
|
||
# принимать обновление: они сверяют точный вид строки).
|
||
globals_ = [
|
||
("scripts/kit.sh", "3X-UI KIT", "3X-UI_autoVPS"),
|
||
("scripts/3x-ui.sh", "3X-UI KIT", "3X-UI_autoVPS"),
|
||
("scripts/hysteria2.sh", "3X-UI KIT", "3X-UI_autoVPS"),
|
||
("scripts/kit-sub.py", "3X-UI KIT", "3X-UI_autoVPS"),
|
||
]
|
||
for path, old, new in globals_:
|
||
p = pathlib.Path(path)
|
||
if not p.exists():
|
||
print("✗ %s: файла нет" % path)
|
||
bad += 1
|
||
continue
|
||
text = p.read_text(encoding="utf-8", newline="")
|
||
# строки, где старое имя — часть служебной сверки, не переименовываем
|
||
keep = lambda line: "grep -qx" in line
|
||
if check:
|
||
if new not in text:
|
||
print("✗ %-20s нет нашего имени «%s»" % (path, new))
|
||
bad += 1
|
||
continue
|
||
left = [l for l in text.splitlines() if old in l and not keep(l)]
|
||
if left:
|
||
print("✗ %-20s осталось старое имя в %d строк(ах)" % (path, len(left)))
|
||
bad += 1
|
||
continue
|
||
print("✓ %-20s бренд: %s" % (path, new))
|
||
continue
|
||
out = []
|
||
changed = 0
|
||
for line in text.splitlines(keepends=True):
|
||
if old in line and not keep(line):
|
||
out.append(line.replace(old, new))
|
||
changed += line.count(old)
|
||
else:
|
||
out.append(line)
|
||
if not changed:
|
||
if new in text:
|
||
print("– %-20s уже переименовано" % path)
|
||
continue
|
||
print("✗ %-20s не нашёл «%s»" % (path, old))
|
||
bad += 1
|
||
continue
|
||
p.write_text("".join(out), encoding="utf-8", newline="")
|
||
print("✓ %-20s переименовано вхождений: %d" % (path, changed))
|
||
|
||
if bad:
|
||
if check:
|
||
die("якорей не найдено: %d — автор изменил код, пересмотри правки в tools/fork_apply.py" % bad)
|
||
die("якорей не найдено: %d — правки не применены" % bad)
|
||
if check:
|
||
ok("все якоря на месте (версия сборки %s)" % version)
|
||
else:
|
||
ok("швы накатаны, версия сборки %s" % version)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|