360 lines
19 KiB
Python
360 lines
19 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 -------------------------------------------------
|
||
("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()
|