Files
3X-UI_autoVPS/tools/fork_apply.py
T

388 lines
25 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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, если протоколы не выбраны',
),
(
'scripts/3x-ui.sh',
'SNI_CANDIDATES=(dl.google.com www.amazon.com www.samsung.com www.yahoo.com)',
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)',
'список известных сайтов расширен',
),
(
'scripts/3x-ui.sh',
' # Маскировка: из флагов, по вопросу пользователю или автоматически (сосед по подсети).',
False,
'# ---------------------------------------------------------------------------\n# Наш шов: выбор сайта маскировки по результатам скана подсети.\n# Печатает выбранное имя в stdout (пусто — оставляем автоподбор автора).\n# Роль: reality (только найденные и ручной ввод) или xhttp (плюс известные сайты).\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 n try\n [[ $role == xhttp ]] || known=() # REALITY маскируем только под соседа или введённый вручную\n printf \'\\n%s%s%s\\n\' "$B" "$role" "$N" >&2\n if ((${#found[@]})); then\n printf \' найдено в вашей подсети (TLS 1.3, h2, сертификат верный):\\n\' >&2\n for i in "${!found[@]}"; do printf \' %2d) %s\\n\' "$((i + 1))" "${found[i]}" >&2; done\n else\n printf \' в подсети подходящих сайтов не нашлось\\n\' >&2\n fi\n if [[ $role == xhttp ]]; then\n n=${#found[@]}\n printf \' известные сайты (заметнее, чем соседи по подсети):\\n\' >&2\n for i in "${!known[@]}"; do printf \' %2d) %s\\n\' "$((i + 1 + n))" "${known[i]}" >&2; done\n fi\n printf \' 0) ввести адрес сайта вручную\\n\' >&2\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 if [[ $ans == 0 ]]; 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 if [[ $ans =~ ^[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$ ]]; then printf \'%s\' "$ans"; return 0; fi\n printf \' не похоже на адрес сайта\\n\' >&2\n elif [[ $ans =~ ^[0-9]+$ ]]; then\n local total=${#found[@]}\n ((total)) && known=() # для reality известные не предлагаются\n local idx=$((10#$ans))\n if ((idx >= 1 && idx <= total)); then printf \'%s\' "${found[idx - 1]}"; return 0; fi\n if ((idx > total && idx <= total + ${#known[@]})); then printf \'%s\' "${known[idx - total - 1]}"; return 0; fi\n printf \' такого номера нет\\n\' >&2\n else\n printf \' не понял «%s» — введите номер или 0\\n\' "$ans" >&2\n fi\n printf \' ещё раз (Enter — автоподбор): \' >&2\n done\n return 0\n}\n\n # Маскировка: из флагов, по вопросу пользователю или автоматически (сосед по подсети).',
'меню выбора сайта маскировки (наш код)',
),
(
'scripts/3x-ui.sh',
' echo\n echo "Подбираю сайт для маскировки среди соседей по подсети… (около 250 коротких обращений к порту 443 соседних адресов, около минуты; не нужно – запустите с --sni или --domain)"',
False,
' echo\n echo "Сканирую подсеть: около 250 коротких обращений к порту 443 соседних адресов (около минуты)."\n echo "По результатам предложу сайты для маскировки. Не нужно – запустите с --sni или --domain."\n local -a nb=()\n mapfile -t nb < <(nearby_sites | shuf)\n NB_ALL=("${nb[@]}")\n',
'скан + выбор сайта вместо автоподбора',
),
(
'scripts/3x-ui.sh',
' SNI=${nb[0]}; SNI2=${nb[1]:-}; SNI3=${nb[2]:-}\n ok "найден: ${B}$SNI${N} · та же подсеть · TLS 1.3, h2, сертификат верный"',
False,
' SNI=${nb[0]}; SNI2=${SNI2:-${nb[1]:-}}; SNI3=${nb[2]:-}\n ok "найден: ${B}$SNI${N} · та же подсеть · TLS 1.3, h2, сертификат верный"',
'не перетирать выбранный SNI2',
),
]
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()