#!/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 ------------------------------------------------- ("scripts/kit.sh", version_re, True, 'KIT_VERSION="%s"' % version, "версия сборки форка"), ( "scripts/kit.sh", 'KIT_RAW="https://raw.githubusercontent.com/itsnotkubrick/3X-UI_KIT/main"', False, 'KIT_RAW="%s/raw/branch/main"' % GITEA, "источник VERSION и подсказки об обновлении", ), ( "scripts/kit.sh", 'kit_ref_raw() { echo "https://raw.githubusercontent.com/itsnotkubrick/3X-UI_KIT/v$1"; }', False, 'kit_ref_raw() { echo "%s/raw/tag/v$1"; }' % GITEA + "\n" + HOOK, "файлы релиза из нашего тега + подключение kit-local.sh", ), ( "scripts/kit.sh", r' # SHA256:[^\n]*\n "ssh-ed25519 [^\n]*"\n', True, ' # Ключ подписи форка (владелец: hacker07). Закрытая часть — только на рабочей станции.\n' ' "%s"\n' % pub, "ключ проверки подписи релизов", ), ( "scripts/kit.sh", "for f in scripts/kit.sh scripts/kit-sub.py; do", False, "for f in scripts/kit.sh scripts/kit-sub.py scripts/kit-local.sh; do", "наш файл в списке проверяемых и скачиваемых", ), ( "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', 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', "установка kit-local.sh при обновлении", ), # --- scripts/3x-ui.sh ----------------------------------------------- ("scripts/3x-ui.sh", version_re, True, 'KIT_VERSION="%s"' % version, "версия сборки форка"), ( "scripts/3x-ui.sh", 'KIT_RAW="https://raw.githubusercontent.com/itsnotkubrick/3X-UI_KIT/v$KIT_VERSION"', False, 'KIT_RAW="%s/raw/tag/v$KIT_VERSION"' % GITEA, "запасная загрузка kit из нашего релиза", ), ( "scripts/3x-ui.sh", 'KIT_CLI_URL="$KIT_RAW/scripts/kit.sh"', False, 'KIT_CLI_URL="$KIT_RAW/scripts/kit.sh"\nKIT_LOCAL_URL="$KIT_RAW/scripts/kit-local.sh"', "адрес нашего файла для установщика", ), ( "scripts/3x-ui.sh", 'KIT_INSTALL_CMD="bash <(curl -fsSL https://raw.githubusercontent.com/itsnotkubrick/3X-UI_KIT/main/scripts/3x-ui.sh)"', False, 'KIT_INSTALL_CMD="bash <(curl -fsSL %s/raw/branch/main/scripts/3x-ui.sh)"' % GITEA, "команда установки, которую показывает панель", ), ( "scripts/3x-ui.sh", ' bash -n /usr/local/bin/kit || die "Команда kit скачалась повреждённой"', False, " # Форк: ставим наши локальные дополнения (см. 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" ' bash -n /usr/local/bin/kit || die "Команда kit скачалась повреждённой"', "установка kit-local.sh установщиком", ), # --- scripts/hysteria2.sh ------------------------------------------- ("scripts/hysteria2.sh", version_re, True, 'KIT_VERSION="%s"' % version, "версия сборки форка"), # --- tools/release.sh ----------------------------------------------- ( "tools/release.sh", "FILES=(scripts/3x-ui.sh scripts/hysteria2.sh scripts/kit.sh scripts/kit-sub.py VERSION)", False, "FILES=(scripts/3x-ui.sh scripts/hysteria2.sh scripts/kit.sh scripts/kit-sub.py scripts/kit-local.sh scripts/3x-ui-local.sh VERSION)", "наш файл в подписанном списке релиза", ), ( "tools/release.sh", '{ echo "# 3X-UI KIT $v"; echo "# 3X-UI_autoVPS $v"; sha "${FILES[@]}"; } >SHA256SUMS', False, "{ echo \"# 3X-UI KIT $v\"; echo \"# 3X-UI_autoVPS $v\"; sha \"${FILES[@]}\" | awk '{ sub(/^\\*/, \"\", $2); print $1\" \"$2 }'; } >SHA256SUMS", "суммы в POSIX-виде без звёздочки (на Windows sha256sum её ставит, и kit её не понимает)", ), # --- свои записи о версии (наш файл, авторский CHANGELOG не трогаем) ---- ( "tools/release.sh", 'grep -q "^## v$v" CHANGELOG.md || die "В CHANGELOG.md нет раздела ## v$v"', False, '(grep -q "^## v$v" CHANGELOG.md || grep -q "^## v$v" FORK-CHANGELOG.md)' ' || die "Нет раздела ## v$v ни в CHANGELOG.md, ни в FORK-CHANGELOG.md (форк ведёт свои записи в FORK-CHANGELOG.md)"', "раздел версии форка — в нашем файле", ), ( "tools/release.sh", '[[ -z $(git status --porcelain -- "${FILES[@]}" CHANGELOG.md) ]]', False, '[[ -z $(git status --porcelain -- "${FILES[@]}" CHANGELOG.md FORK-CHANGELOG.md) ]]', "наш файл записей тоже должен быть закоммичен", ), ( "scripts/kit.sh", ' curl -fsS -m 5 "$KIT_RAW/CHANGELOG.md" 2>/dev/null | awk -v v="## v$1" \'', 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" \'', "«что нового» ищет и в наших записях", ), ( "scripts/3x-ui.sh", " _ _ _ _ _ _ _\n (_) |_ ___ _ __ ___ | |_| | ___ _| |__ _ __(_) ___| | __\n | | __/ __| '_ \\ / _ \\| __| |/ / | | | '_ \\| '__| |/ __| |/ /\n | | |_\\__ \\ | | | (_) | |_| <| |_| | |_) | | | | (__| <\n |_|\\__|___/_| |_|\\___/ \\__|_|\\_\\\\__,_|_.__/|_| |_|\\___|_|\\_\\", False, " _ _ _ ___ _____\n | | | | __ _ ___| | _____ _ __ / _ \\___ |\n | |_| |/ _` |/ __| |/ / _ \\ '__| | | | | / /\n | _ | (_| | (__| < __/ | | |_| |/ /\n |_| |_|\\__,_|\\___|_|\\_\\___|_| \\___//_/", "баннер сервера: наша надпись вместо авторской", ), ( "scripts/3x-ui.sh", ' echo "${B}3X-UI KIT $KIT_VERSION${N} · it\'s not Kubrick. it\'s just a VPN."', False, ' echo "${B}3X-UI_autoVPS $KIT_VERSION${N} · сборка форка: панель 3X-UI и VPN-протоколы на одном порту"', "подпись под баннером: наше имя сборки", ), ( "scripts/kit.sh", ' grep -qx "# 3X-UI KIT $v" "$d/SHA256SUMS"', False, ' grep -qxE "# (3X-UI KIT|3X-UI_autoVPS) $v" "$d/SHA256SUMS"', "проверка шапки подписанного релиза принимает и старое, и новое имя", ), ( 'scripts/kit.sh', ' "7. Резервная копия" "8. Версии" "" "9. Панель 3X-UI (меню x-ui)" "0. Выход"', False, ' "7. Резервная копия" "8. Версии" "" "9. Панель 3X-UI (меню x-ui)" "0. Выход"\n "10. Удалить всё установленное (наш пункт)"', 'пункт меню: снос установки', ), ( 'scripts/kit.sh', ' 9) command -v x-ui >/dev/null && x-ui || echo "Меню x-ui не найдено." ;;', False, ' 9) command -v x-ui >/dev/null && x-ui || echo "Меню x-ui не найдено." ;;\n 10) kit_local_uninstall ;;', 'пункт меню: снос установки (обработчик)', ), ( "scripts/kit.sh", 'ask_num "Выбор [0-9]: "', False, 'ask_num "Выбор [0-9, 10]: "', "подсказка выбора в меню учитывает новый пункт", ), ( 'scripts/3x-ui.sh', ' if [[ $TRUSTED == yes && $multi == no && ( -z $SNI2 || -z $SNI3 ) ]]; then', False, ' # Наш шов: если XHTTP и MTProto не выбраны, имена для них сейчас не нужны — не сканируем соседей\n # по подсети впустую (около 250 обращений к чужим 443). Имена SNI2/SNI3 возьмутся из общего списка ниже.\n if [[ $TRUSTED == yes && $multi == no && ( -z $SNI2 || -z $SNI3 ) ]] \\\n && [[ " ${PROTOS[*]} " == *" xhttp "* || " ${PROTOS[*]} " == *" mtproto "* ]]; then', 'не сканировать соседей, если XHTTP и MTProto не выбраны', ), ( 'scripts/3x-ui.sh', ' extra=$(sub_links "$SUBID-awg" 1; sub_links "$SUBID-tg" 1)', 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', 'не тянуть legacy-подписки awg/tg, если протоколы не выбраны', ), ] 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()