#!/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:VDuGgJ8dOeCNXB4nBZf3+kRlWthw3+vh8rfMxzMSRIM (itsnotkubrick, 2026-09-30)\n "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGrTGCDhhnm8XO1ekpPJuSWRVCJiFiupEspfQxcbEBmz 3X-UI KIT releases"\n', False, ' # Ключ подписи форка (владелец: hacker07). Закрытая часть — только на рабочей станции.\n "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKM0Sfn/UQeqjvUvY9wVgg7QtG1RR2pEUkqrUD5Yy6EF 3X-UI_autoVPS fork releases (hacker07)"\n', 'авто-шов: replace, строк 2', ), ( '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/ с Name "3X-UI KIT"\n', False, 'split_panel_ours() { # наш профиль: JSON или ссылка happ://routing/onadd/ с 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', ' "7. Резервная копия" "8. Версии" "" "9. Панель 3X-UI (меню x-ui)" "0. Выход"\n', False, ' "7. Резервная копия" "8. Версии" "" "9. Удалить всё установленное" "10. Выход" \\\n "11. Панель 3X-UI (меню x-ui)"\n', 'авто-шов: replace, строк 1', ), ( 'scripts/kit.sh', ' ask_num "Выбор [0-9]: "; c=$REPLY\n', False, ' ask_num "Выбор [1-11]: "; c=$REPLY\n', 'авто-шов: replace, строк 1', ), ( 'scripts/kit.sh', ' 9) command -v x-ui >/dev/null && x-ui || echo "Меню x-ui не найдено." ;;\n 0 | "") return 0 ;;\n', False, ' 9) kit_local_uninstall ;;\n 10 | 0 | "") return 0 ;;\n 11) command -v x-ui >/dev/null && x-ui || echo "Меню x-ui не найдено." ;;\n', 'авто-шов: replace, строк 2', ), ( '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', " _ _ _ _ _ _ _\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 && ! -f $XUI_ENV && -t 1 ]] && { : /dev/null; then\n', False, ' # Наш шов: когда выбор сайта делает наше меню (KL_PICK_SNI=1), меню автора не показываем — иначе\n # человек отвечает на «Свой домен / Сосед / Свой сайт», и до скана подсети дело не доходит.\n elif [[ -z $SNI && $yes == no && $PANEL_SSL == ip && $multi == no && ! -f $XUI_ENV && -t 1 && ${KL_PICK_SNI:-0} != 1 ]] && { : /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/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 if domain_points_here "$DOMAIN" 2>/dev/null; then\n SNI=$DOMAIN; ok "маскировка под свой домен: ${B}$DOMAIN${N} (сертификат Let\'s Encrypt выпустит установщик)"\n else\n SNI=$DOMAIN; later "домен $DOMAIN не указывает на этот сервер — маскируюсь под него как под чужой сайт"\n fi\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/null; then\n', False, ' if [[ -n ${KL_PICK_SNI:-} && ${KL_PICK_SNI} == 1 && -z $SNI ]]; then\n : # сайт уже выбран в нашем меню — вопрос автора не нужен\n elif [[ $yes == no && -t 1 ]] && { : /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', ' extra=$(sub_links "$SUBID-awg" 1; sub_links "$SUBID-tg" 1)\n', False, ' # Наш шов: подписки «-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, строк 1', ), ( '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', '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', ' || die "Это не резервная копия 3X-UI KIT или она повреждена (файл ${f##*/}). Ничего не менял."\n', False, ' || die "Это не резервная копия 3X-UI_autoVPS или она повреждена (файл ${f##*/}). Ничего не менял."\n', 'авто-шов: replace, строк 1', ), ( 'scripts/3x-ui.sh', ' && die "Это не резервная копия 3X-UI KIT или она повреждена (файл ${f##*/}). Ничего не менял."\n', False, ' && die "Это не резервная копия 3X-UI_autoVPS или она повреждена (файл ${f##*/}). Ничего не менял."\n', 'авто-шов: replace, строк 1', ), ( '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/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()