--restore: копия с пустой папкой сертификата отклоняется до установки

Архив с root/cert/self или root/cert/custom без fullchain.pem/privkey.pem проходил
проверку, ставил 3X-UI и падал на копировании сертификата. Теперь проверка архива
требует оба непустых файла, а для копии со своим сертификатом (BACKUP_SSL=custom) –
папку root/cert/custom. Отрицательные тесты – в tools/test/restore_archive_test.py.
This commit is contained in:
maxdmitrov
2026-10-06 17:00:59 +03:00
parent 25848a24c1
commit 1aa9a84e96
3 changed files with 70 additions and 3 deletions
+1 -1
View File
@@ -14,7 +14,7 @@
* Сайт-заглушка теперь у каждого сервера своя: при установке она собирается из двадцати тем и пяти вёрсток со случайным названием, текстами и цветами. Пекарня, переводчик или мастерская по велосипедам – как повезёт. Раньше у многих серверов был один и тот же «облачный» сосед. * Сайт-заглушка теперь у каждого сервера своя: при установке она собирается из двадцати тем и пяти вёрсток со случайным названием, текстами и цветами. Пекарня, переводчик или мастерская по велосипедам – как повезёт. Раньше у многих серверов был один и тот же «облачный» сосед.
* `kit-sub` на своём порту (`--multi-port`) отвечает по HTTP/1.1 и с `Server: nginx` без лишнего пробела – как настоящий nginx, а не его двоюродный брат. * `kit-sub` на своём порту (`--multi-port`) отвечает по HTTP/1.1 и с `Server: nginx` без лишнего пробела – как настоящий nginx, а не его двоюродный брат.
* Исправил `kit net masq on`: Hysteria2 после него переставала слушать порт до перезапуска панели, а `kit net fp` и `kit net on` спотыкались о то же. Панель меняет подключение на ходу, а ядро такую правку не принимает. Теперь `kit` проверяет порты после каждой правки и, если что-то закрылось, перезапускает ядро и честно об этом говорит. А `kit check` научился смотреть, запущено ли ядро Xray и слушает ли каждое подключение свой порт, TCP и UDP отдельно: nginx на 443/tcp больше не выдаёт себя за Hysteria2 на 443/udp. * Исправил `kit net masq on`: Hysteria2 после него переставала слушать порт до перезапуска панели, а `kit net fp` и `kit net on` спотыкались о то же. Панель меняет подключение на ходу, а ядро такую правку не принимает. Теперь `kit` проверяет порты после каждой правки и, если что-то закрылось, перезапускает ядро и честно об этом говорит. А `kit check` научился смотреть, запущено ли ядро Xray и слушает ли каждое подключение свой порт, TCP и UDP отдельно: nginx на 443/tcp больше не выдаёт себя за Hysteria2 на 443/udp.
* `--restore` на новый IP, где Let's Encrypt не выдал сертификат (лимит или закрытый порт 80), раньше говорил «Готово», а ядро Xray не запускалось: подключения ждали сертификат, которого нет. Теперь я ставлю самоподписанный, закрепляю его отпечаток в ссылках и прямо говорю об этом. А если ядро всё же не поднялось, «Готово» не будет: покажу, где смотреть. * `--restore` на новый IP, где Let's Encrypt не выдал сертификат (лимит или закрытый порт 80), раньше говорил «Готово», а ядро Xray не запускалось: подключения ждали сертификат, которого нет. Теперь я ставлю самоподписанный, закрепляю его отпечаток в ссылках и прямо говорю об этом. А если ядро всё же не поднялось, «Готово» не будет: покажу, где смотреть. Копию с пустой папкой сертификата `--restore` теперь отклоняет сразу, а не на полпути установки.
* JSON-подписка снова открывается в приложениях на Xray: там, где у vless не было `encryption`, подписка теперь дописывает `none`. * JSON-подписка снова открывается в приложениях на Xray: там, где у vless не было `encryption`, подписка теперь дописывает `none`.
* Отдельный установщик Hysteria2 ушёл на пенсию: Hysteria2 и так входит в основную установку, один из 11 протоколов. Кто ставил его отдельно – сервер работает как работал, а `hy2 update` вежливо расскажет, что дальше. Нужен один Hysteria2 без панели – официальный установщик: `bash <(curl -fsSL https://get.hy2.sh/)`. * Отдельный установщик Hysteria2 ушёл на пенсию: Hysteria2 и так входит в основную установку, один из 11 протоколов. Кто ставил его отдельно – сервер работает как работал, а `hy2 update` вежливо расскажет, что дальше. Нужен один Hysteria2 без панели – официальный установщик: `bash <(curl -fsSL https://get.hy2.sh/)`.
+9
View File
@@ -1861,6 +1861,13 @@ with tarfile.open(sys.argv[1], "r:gz") as t:
for need in ("etc/x-ui/x-ui.db", "etc/x-ui/install-result.env", "kit-backup.env"): for need in ("etc/x-ui/x-ui.db", "etc/x-ui/install-result.env", "kit-backup.env"):
if need not in names: if need not in names:
sys.exit("нет " + need) sys.exit("нет " + need)
# Каталог сертификата есть – в нём оба файла, иначе установка споткнётся на полпути.
files = {m.name for m in ms if m.isfile() and m.size > 0}
for c in ("root/cert/self", "root/cert/custom"):
if any(n == c or n.startswith(c + "/") for n in names):
for f in ("fullchain.pem", "privkey.pem"):
if c + "/" + f not in files:
sys.exit("нет " + c + "/" + f)
if hasattr(tarfile, "data_filter"): if hasattr(tarfile, "data_filter"):
t.extractall(sys.argv[2], members=ms, filter="data") t.extractall(sys.argv[2], members=ms, filter="data")
else: else:
@@ -1884,6 +1891,8 @@ PY
BACKUP_KIT_VERSION=$(env_get "$tmp/kit-backup.env" BACKUP_KIT_VERSION) BACKUP_KIT_VERSION=$(env_get "$tmp/kit-backup.env" BACKUP_KIT_VERSION)
[[ $BACKUP_HOST =~ ^[A-Za-z0-9.:-]+$ ]] || die "Это не резервная копия 3X-UI KIT или она повреждена (адрес сервера). Ничего не менял." [[ $BACKUP_HOST =~ ^[A-Za-z0-9.:-]+$ ]] || die "Это не резервная копия 3X-UI KIT или она повреждена (адрес сервера). Ничего не менял."
[[ $BACKUP_SSL =~ ^(ip|custom|none)$ ]] || die "В копии нет данных о сертификате." [[ $BACKUP_SSL =~ ^(ip|custom|none)$ ]] || die "В копии нет данных о сертификате."
[[ $BACKUP_SSL != custom || -f $tmp/root/cert/custom/fullchain.pem ]] \
|| die "Это не резервная копия 3X-UI KIT или она повреждена (нет своего сертификата). Ничего не менял."
PANEL_SSL=$BACKUP_SSL PANEL_SSL=$BACKUP_SSL
# Домен переезжает вместе с сервером (поменяйте A-запись), IP – нет. # Домен переезжает вместе с сервером (поменяйте A-запись), IP – нет.
if [[ $BACKUP_HOST =~ ^[0-9.]+$ ]]; then if [[ $BACKUP_HOST =~ ^[0-9.]+$ ]]; then
+60 -2
View File
@@ -1,5 +1,5 @@
"""Сертификаты при --restore (3x-ui.sh): python3 tools/test/restore_archive_test.py""" """Проверка архива копии и сертификатов при --restore (3x-ui.sh): python3 tools/test/restore_archive_test.py"""
import json, os, sqlite3, subprocess, tempfile, unittest import io, json, os, re, sqlite3, subprocess, tarfile, tempfile, unittest
root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
src = open(os.path.join(root, "scripts", "3x-ui.sh"), encoding="utf-8").read() src = open(os.path.join(root, "scripts", "3x-ui.sh"), encoding="utf-8").read()
@@ -12,10 +12,68 @@ def heredoc(after):
return body[:body.index("\nPY\n") + 1] return body[:body.index("\nPY\n") + 1]
CHECK = heredoc('python3 - "$file" "$tmp"')
CERT_FILES = heredoc("db_cert_files() {") CERT_FILES = heredoc("db_cert_files() {")
PIN = heredoc("pin_cert_in_db() {") PIN = heredoc("pin_cert_in_db() {")
def archive(extra=(), drop=()):
files = {"etc/x-ui/x-ui.db": b"db", "etc/x-ui/install-result.env": b"X=1\n", "kit-backup.env": b"BACKUP_SSL=ip\n"}
files.update({k: v for k, v in extra if v is not None})
d = tempfile.mkdtemp()
p = os.path.join(d, "b.tar.gz")
with tarfile.open(p, "w:gz") as t:
for name, data in list(files.items()) + [(k, v) for k, v in extra if v is None]:
if name in drop:
continue
ti = tarfile.TarInfo(name)
if data is None:
ti.type = tarfile.DIRTYPE
t.addfile(ti)
else:
ti.size = len(data)
t.addfile(ti, io.BytesIO(data))
return p
def check(p):
out = tempfile.mkdtemp()
return subprocess.run(["python3", "-", p, out], input=CHECK, capture_output=True, text=True)
PEM = b"-----BEGIN CERTIFICATE-----\nAA==\n-----END CERTIFICATE-----\n"
class Archive(unittest.TestCase):
def test_good(self):
r = check(archive())
self.assertEqual(r.returncode, 0, r.stderr)
def test_good_with_self_cert(self):
r = check(archive([("root/cert/self", None), ("root/cert/self/fullchain.pem", PEM), ("root/cert/self/privkey.pem", PEM)]))
self.assertEqual(r.returncode, 0, r.stderr)
def test_empty_self_dir(self):
r = check(archive([("root/cert/self", None)]))
self.assertNotEqual(r.returncode, 0)
self.assertIn("root/cert/self/fullchain.pem", r.stderr)
def test_empty_custom_dir(self):
self.assertNotEqual(check(archive([("root/cert/custom", None)])).returncode, 0)
def test_self_without_key(self):
self.assertNotEqual(check(archive([("root/cert/self/fullchain.pem", PEM)])).returncode, 0)
def test_self_empty_key(self):
self.assertNotEqual(check(archive([("root/cert/self/fullchain.pem", PEM), ("root/cert/self/privkey.pem", b"")])).returncode, 0)
def test_no_db(self):
self.assertNotEqual(check(archive(drop=("etc/x-ui/x-ui.db",))).returncode, 0)
def test_dotdot(self):
self.assertNotEqual(check(archive([("etc/x-ui/../../tmp/x", b"x")])).returncode, 0)
def make_db(): def make_db():
d = tempfile.mkdtemp() d = tempfile.mkdtemp()
p = os.path.join(d, "x-ui.db") p = os.path.join(d, "x-ui.db")