refactor(proxy-client): упростить клиент до личного инструмента #1
No files matched your search
@@ -2,577 +2,219 @@
|
|||||||
|
|
||||||
Дата: 2026-07-15
|
Дата: 2026-07-15
|
||||||
|
|
||||||
Статус: основная реализация принята вручную; самостоятельный установщик
|
Статус: реализовано; 2026-09-28 упрощено по принципу ниже
|
||||||
спроектирован 2026-07-21, но ещё не реализован
|
|
||||||
|
Точное поведение, форматы, сообщения и команды описаны в коде,
|
||||||
|
`proxy-client --help` и [`proxy-client/README.md`](../../proxy-client/README.md).
|
||||||
|
Здесь — то, чего по коду не видно: задача, границы, выбор и его причины, риски.
|
||||||
|
|
||||||
|
## Принцип: личный инструмент
|
||||||
|
|
||||||
|
`proxy-client` пользуются владелец и несколько друзей на своих машинах. Код
|
||||||
|
защищается от реальных ошибок и опасностей:
|
||||||
|
|
||||||
|
- скачанный Xray проверяется по SHA-256 и заменяется атомарно;
|
||||||
|
- секреты профиля не попадают в вывод, служебные файлы закрыты правами 600/700;
|
||||||
|
- локальный порт Xray слушает только loopback;
|
||||||
|
- чужие файлы не перезаписываются;
|
||||||
|
- сигнал уходит только процессам из `state.json`: Xray — если его
|
||||||
|
`/proc/PID/exe` совпадает с запущенным файлом, супервизору — если Xray
|
||||||
|
подтверждён и в командной строке супервизора есть `proxy-client`.
|
||||||
|
|
||||||
|
Пользователь и его собственные файлы — не противник. Сообщение об ошибке
|
||||||
|
называет файл полным путём и подсказывает готовую команду.
|
||||||
|
|
||||||
|
Новая защита появляется вместе со сценарием, который реально случается у
|
||||||
|
пользователя, и этот сценарий записывается рядом с ней. Так же проверяется и
|
||||||
|
прежний код: в 2026-09-28 убраны защиты без такого сценария — повторное
|
||||||
|
использование PID, подложный `state.json`, zip-бомба в проверенном архиве,
|
||||||
|
гонка `status` с параллельным `start`.
|
||||||
|
|
||||||
|
Тесты проверяют поведение через команду `proxy-client`, разбор ссылки
|
||||||
|
(`parse_vless` + `build_xray_config`), установку Xray с подставным
|
||||||
|
скачивателем, функции Bash с поддельным `PROXY_CLIENT_BIN` и установщик пакета
|
||||||
|
во временном HOME.
|
||||||
|
|
||||||
## Проблема
|
## Проблема
|
||||||
|
|
||||||
На удалённых Linux-серверах нет GUI-клиента Hiddify. Перед запуском Claude Code
|
На удалённых Linux-серверах нет GUI-клиента Hiddify. Перед запуском Claude Code
|
||||||
или Codex приходится вручную запускать Xray и выставлять proxy-переменные.
|
или Codex приходится вручную запускать Xray и выставлять proxy-переменные.
|
||||||
Переменные легко забыть, после чего агент стартует без прокси.
|
Переменные легко забыть, и агент стартует без прокси.
|
||||||
|
|
||||||
Нужны привычные команды `pclaude` и `pcodex`, которые не запускают агента,
|
Нужны привычные команды `pclaude` и `pcodex`, которые не запускают агента,
|
||||||
если явно настроенный прокси недоступен. Решение должно работать как часть этого
|
если явно настроенный прокси недоступен. Решение работает и в составе этого
|
||||||
репозитория и отдельно от остальных dotfiles.
|
репозитория, и отдельно от остальных dotfiles.
|
||||||
|
|
||||||
## Контекст
|
## Контекст
|
||||||
|
|
||||||
Существующий дизайн
|
Прежний дизайн
|
||||||
[`pclaude` / `pcodex`](../superpowers/specs/2026-05-19-proxy-cli-launchers-design.md)
|
[`pclaude` / `pcodex`](../superpowers/specs/2026-05-19-proxy-cli-launchers-design.md)
|
||||||
задаёт важный контракт: proxy-переменные видны только дочернему процессу,
|
задаёт договор: proxy-переменные видны только дочернему процессу, аргументы и
|
||||||
аргументы и код возврата сохраняются, а недоступный прокси приводит к отказу
|
код возврата сохраняются, недоступный прокси — отказ до запуска агента. Эта
|
||||||
до запуска агента.
|
спецификация заменяет его Linux-часть; Windows-часть не меняется.
|
||||||
|
|
||||||
Эта спецификация заменяет Linux-часть прежнего дизайна в вопросах расположения
|
Xray принимает конфигурационные файлы, а не `vless://`-ссылки. Его SOCKS-вход
|
||||||
функций, источника настроек и вывода `proxy_on`/`proxy_off`. Windows-часть
|
принимает SOCKS и HTTP на одном порту, поэтому прежняя схема proxy-переменных
|
||||||
прежнего дизайна не меняется.
|
подходит локальному Xray без второго порта. См.
|
||||||
|
|
||||||
Сейчас Bash-функции жёстко используют внешний Hiddify на
|
|
||||||
`hproxy.local:12334`. Принесённый прототип `scripts/setup-xray-proxy.sh`
|
|
||||||
умеет установить и запустить Xray из готового JSON, но смешивает установку и
|
|
||||||
запуск, не разбирает ссылки Hiddify и небезопасно ищет чужие процессы через
|
|
||||||
`pkill`.
|
|
||||||
|
|
||||||
Xray принимает конфигурационные файлы, а не `vless://`-ссылки. При этом его
|
|
||||||
SOCKS-вход принимает SOCKS и HTTP на одном порту, поэтому существующая схема
|
|
||||||
proxy-переменных совместима с локальным Xray без второго порта. См.
|
|
||||||
[командную строку Xray](https://xtls.github.io/en/document/command.html) и
|
[командную строку Xray](https://xtls.github.io/en/document/command.html) и
|
||||||
[SOCKS-вход](https://xtls.github.io/en/config/inbounds/socks.html).
|
[SOCKS-вход](https://xtls.github.io/en/config/inbounds/socks.html).
|
||||||
|
|
||||||
## Цели
|
## Цели
|
||||||
|
|
||||||
- Дать headless Linux-серверу управляемый пользовательский Xray без systemd и
|
- Управляемый пользовательский Xray на Linux-сервере без systemd и root.
|
||||||
прав root.
|
- Явные `pclaude` и `pcodex`, которые нельзя случайно запустить через другой
|
||||||
- Сохранить явные `pclaude` и `pcodex`, которые нельзя случайно запустить через
|
прокси или без прокси.
|
||||||
другой прокси или без настроенного прокси.
|
- Одна VLESS-ссылка Hiddify превращается в клиентский JSON Xray.
|
||||||
- Принимать одну точечную VLESS-ссылку Hiddify и преобразовывать её в клиентский
|
- Внешний Hiddify работает, как раньше.
|
||||||
JSON Xray.
|
- Машинный `NO_PROXY` настраивается без правки файлов репозитория.
|
||||||
- Сохранить совместимость Bash-функций с внешним Hiddify.
|
- Каталог `proxy-client/` можно скопировать отдельно и поставить своим
|
||||||
- Позволить настраивать машинный `NO_PROXY` без правки файлов репозитория.
|
установщиком, без остальных dotfiles и без Stow.
|
||||||
- Поддержать самостоятельную установку из отдельно скопированного каталога
|
|
||||||
`proxy-client/` без зависимости от общего установщика dotfiles и без
|
|
||||||
обязательного stow.
|
|
||||||
|
|
||||||
## Не входит в объём
|
## Не входит в объём
|
||||||
|
|
||||||
- Windows, PowerShell, Windows Codex launcher и VS Code Server.
|
- Windows, PowerShell, Windows Codex launcher и VS Code Server.
|
||||||
- systemd, автозапуск после перезагрузки, sudo, firewall, TUN и изменение
|
- systemd, автозапуск, sudo, firewall, TUN и системная маршрутизация.
|
||||||
системной маршрутизации.
|
- Автоматический выбор между Xray и Hiddify или запасной прокси.
|
||||||
- Автоматический выбор между Xray и Hiddify или переход на запасной прокси.
|
- Несколько одновременных профилей у одного Unix-пользователя.
|
||||||
- Одновременное управление несколькими профилями одного Unix-пользователя.
|
- Импорт подписок или полного JSON-экспорта Hiddify.
|
||||||
- Универсальный импорт подписок или полного JSON-экспорта Hiddify.
|
- Протоколы за пределами двух поддержанных семейств VLESS.
|
||||||
- Протоколы и сочетания за пределами двух явно поддержанных семейств.
|
- pip, venv и сторонние Python-библиотеки.
|
||||||
- pip, venv и любые сторонние Python-библиотеки.
|
- Сетевая изоляция голых `claude` и `codex`: намерение выражают команды с
|
||||||
- Сетевая изоляция голых `claude` и `codex`: как и раньше, гарантию намерения
|
префиксом `p`.
|
||||||
дают явные команды с префиксом `p`.
|
|
||||||
|
|
||||||
## Рассмотренные варианты
|
## Рассмотренные варианты
|
||||||
|
|
||||||
### Только Bash
|
- **Только Bash.** URL-кодирование, JSON, контрольные суммы и управление
|
||||||
|
процессом делают Bash хрупким и плохо тестируемым.
|
||||||
|
- **Bash-контроллер и Python-преобразователь.** Граница между двумя языками
|
||||||
|
усложняет ошибки, тесты и единый источник настроек.
|
||||||
|
- **Один Python CLI и тонкий Bash-модуль.** Выбран. Python отвечает за данные,
|
||||||
|
установку Xray, процессы и запуск дочерних команд. Bash нужен только там, где
|
||||||
|
меняется окружение текущей shell-сессии.
|
||||||
|
- **Без менеджера**: готовые JSON и Xray вручную в tmux. Почти нет кода, но не
|
||||||
|
решает главного — безопасного `pclaude`/`pcodex` из ссылки Hiddify.
|
||||||
|
|
||||||
Ближе всего к принесённому прототипу, но URL-кодирование, JSON, проверка схемы,
|
Для отдельно скопированного пакета рассматривались: только инструкция для
|
||||||
контрольные суммы и безопасное управление процессом делают Bash-реализацию
|
ручных ссылок (легко забыть шаги), копирование файлов в `$HOME` (сложно
|
||||||
хрупкой и плохо тестируемой.
|
обновлять), обязательный Stow (лишняя зависимость на серверах) и свой
|
||||||
|
установщик. Выбран свой установщик, который ставит прямые ссылки: один способ,
|
||||||
|
нет второй копии файлов.
|
||||||
|
|
||||||
### Bash-контроллер и отдельный Python-преобразователь
|
## Решение
|
||||||
|
|
||||||
Оставляет жизненный цикл в Bash, а разбор VLESS переносит в Python. Граница
|
### Компоненты
|
||||||
между двумя реализациями усложняет ошибки, тесты и единый источник настроек.
|
|
||||||
|
|
||||||
### Один Python CLI и тонкий Bash-модуль
|
`proxy-client` — один исполняемый файл на Python 3.8+ без внешних библиотек:
|
||||||
|
установка Xray, запуск и остановка, проверка доступности, разбор VLESS, запуск
|
||||||
|
дочерней команды через прокси. Команды: `install`, `start`, `stop`, `status`,
|
||||||
|
`env`, `run`. Без подкоманды ничего не делается.
|
||||||
|
|
||||||
Python отвечает за данные, установку Xray, процессы и запуск дочерних команд.
|
`shell.bash` — единственный владелец `proxy_on`, `proxy_off`, `pclaude`,
|
||||||
Bash нужен только там, где необходимо изменить окружение текущей shell-сессии.
|
`pcodex` и `myip`; в `bash/.bash_functions` их нет, чтобы не зависеть от
|
||||||
Это выбранный вариант.
|
порядка `source`. Модуль сам конфиг не читает:
|
||||||
|
|
||||||
### Не создавать менеджер
|
- `pclaude` и `pcodex` вызывают `proxy-client run -- COMMAND ARG...`;
|
||||||
|
- `proxy_on` берёт готовые переменные из `proxy-client env` и применяет их,
|
||||||
|
только если команда завершилась успешно; `eval` не используется;
|
||||||
|
- `proxy_off` очищает переменные в обоих регистрах.
|
||||||
|
|
||||||
Можно хранить готовые JSON и вручную запускать Xray в tmux. Вариант почти не
|
`proxy_on`/`proxy_off` больше не запрашивают внешний IP: это замедляло
|
||||||
требует кода, но не решает основную боль: безопасный запуск `pclaude` и
|
переключение. Для проверки есть `myip` и `proxy-client status`.
|
||||||
`pcodex` из точечной ссылки Hiddify.
|
|
||||||
|
|
||||||
### Доставка отдельного пакета
|
### Настройки
|
||||||
|
|
||||||
Рассмотрены четыре способа подключить отдельно скопированный каталог:
|
Два файла в `~/.config/proxy-client/`:
|
||||||
|
|
||||||
- оставить только инструкции для stow и ручных симлинков — просто, но сохраняет
|
- `config` — несекретный машинный INI: тип `xray` или `external`, имя, адрес,
|
||||||
легко забываемые шаги;
|
порт, добавка к `NO_PROXY`. Если файла нет, используется прежний Hiddify на
|
||||||
- копировать файлы в домашний каталог — не требует сохранять исходный каталог,
|
`hproxy.local:12334`.
|
||||||
но усложняет обновления и расходится с моделью dotfiles;
|
- `default.url` — секретная VLESS-ссылка с правами 600; `start -f` берёт другой
|
||||||
- требовать stow — даёт привычное управление ссылками, но добавляет
|
файл.
|
||||||
необязательную зависимость на удалённых серверах;
|
|
||||||
- дать пакету собственный установщик, который использует stow при наличии и
|
|
||||||
создаёт те же симлинки напрямую без него.
|
|
||||||
|
|
||||||
Выбран последний вариант. Он сохраняет один результат установки при разном
|
Они разделены, потому что первый описывает политику машины, а второй — секрет,
|
||||||
составе машины и не требует второй копии обновляемых файлов.
|
который меняется независимо. Выбран INI, а не TOML: `tomllib` появился только
|
||||||
|
в Python 3.11.
|
||||||
|
|
||||||
## Рекомендуемое решение
|
### Явный тип прокси
|
||||||
|
|
||||||
### Границы компонентов
|
`xray` значит: нужен Xray, запущенный через `proxy-client start`. Если его нет,
|
||||||
|
агент не запускается. `external` значит: проверяется только доступность адреса
|
||||||
|
и порта. Ни один тип не переходит на другой сам — предсказуемость канала важнее
|
||||||
|
удобства.
|
||||||
|
|
||||||
`proxy-client` — один исполняемый файл на Python 3.8+ без внешних библиотек. Он
|
`start` работает только при `type=xray` и loopback-адресе в конфиге. Поэтому
|
||||||
отвечает за установку Xray, запуск и остановку процесса, диагностику,
|
нельзя поднять локальный Xray, оставив `pclaude` направленным во внешний
|
||||||
преобразование VLESS и запуск произвольной дочерней команды через прокси.
|
Hiddify. `stop` от типа не зависит: после переключения на `external` старый
|
||||||
Нейтральное публичное имя описывает клиентскую прокси-подсистему целиком:
|
Xray всё равно можно остановить.
|
||||||
внешний прокси и управляемый Xray. Название `xray` остаётся у конкретного
|
|
||||||
движка и значения `type = xray`, где оно действительно важно.
|
|
||||||
|
|
||||||
Переносимый `shell.bash` — единственный владелец `proxy_on`, `proxy_off`,
|
Один порт на HTTP и SOCKS, по умолчанию `12334`; порт настраивается, чтобы
|
||||||
`pclaude`, `pcodex`, `myip` и внутренних proxy-помощников. При реализации они
|
разные пользователи одной машины не мешали друг другу.
|
||||||
удаляются из `bash/.bash_functions`, чтобы результат не зависел от порядка
|
|
||||||
`source`. `update_hproxy` остаётся там как отдельная WSL-утилита управления
|
|
||||||
`/etc/hosts`, а не часть переносимого интерфейса прокси. Модуль не разбирает
|
|
||||||
машинный конфиг самостоятельно:
|
|
||||||
|
|
||||||
- `pclaude` и `pcodex` делегируют запуск форме
|
### VLESS-ссылки
|
||||||
`proxy-client run -- COMMAND ARG...`, где `--` отделяет параметры клиента;
|
|
||||||
- `proxy_on` получает от `proxy-client env` проверенный набор proxy-переменных и
|
|
||||||
применяет его к текущей Bash-сессии;
|
|
||||||
- `proxy_off` очищает верхний и нижний регистр proxy-переменных.
|
|
||||||
|
|
||||||
Общий `bash/.bash_functions` и модуль клиента не зависят друг от друга. В составе
|
Поддержаны две семьи: TCP/RAW с REALITY и XHTTP с TLS. Неизвестный или
|
||||||
dotfiles `.bash_custom` подключает их двумя независимыми строками. Без
|
повторный query-параметр — отказ: он может менять смысл соединения, и его
|
||||||
остальных dotfiles пользователь добавляет только один `source` модуля клиента в
|
нужно добавить в разбор явно. Остальное проверяет `xray run -test`.
|
||||||
`.bashrc`. Для неинтерактивных сценариев и других оболочек остаётся прямой
|
|
||||||
`proxy-client run`.
|
|
||||||
|
|
||||||
`proxy-client env` сначала выполняет ту же проверку доступности, что и `run`, а
|
Hiddify Manager добавляет `allowInsecure`/`insecure` даже к профилям с
|
||||||
затем печатает восемь строк `KEY=value`: HTTP, HTTPS, ALL и NO_PROXY в верхнем
|
нормальным сертификатом
|
||||||
и нижнем регистре. `shell.bash` сначала целиком получает успешный ответ, затем
|
([Hiddify-Manager#5438](https://github.com/hiddify/Hiddify-Manager/issues/5438)),
|
||||||
разбирает строки по первому `=`, принимает только известные ключи и лишь после
|
а Xray [26.2.6](https://github.com/XTLS/Xray-core/releases/tag/v26.2.6) эту
|
||||||
полной проверки экспортирует полученные значения через `printf -v`. `eval` не
|
опцию удалил. Поэтому поля принимаются и игнорируются с предупреждением;
|
||||||
используется, частичного изменения окружения при ошибке нет.
|
TLS-проверку задают `pcs` и `vcn`.
|
||||||
|
|
||||||
У `env` stdout зарезервирован только для восьми машинно-читаемых строк.
|
Сводка перед запуском не показывает UUID, ключи REALITY и содержимое XHTTP
|
||||||
Предупреждения и ошибки идут в stderr; обычная безопасная сводка этой командой
|
`path` (там бывает токен). Ссылка не передаётся аргументом командной строки.
|
||||||
не печатается.
|
|
||||||
|
|
||||||
Совместимость существующих Bash-команд означает сохранение имён функций,
|
|
||||||
набора переменных, fail-fast перед `pclaude`/`pcodex` и изоляции окружения
|
|
||||||
дочернего процесса. Автоматический запрос внешнего IP из `proxy_on` и
|
|
||||||
`proxy_off` удаляется: он замедляет переключение и смешивает изменение
|
|
||||||
окружения с диагностикой. Для проверки остаются `myip` и `proxy-client status`.
|
|
||||||
|
|
||||||
### Два независимых файла настройки
|
|
||||||
|
|
||||||
Машинный файл `~/.config/proxy-client/config` хранит несекретное описание
|
|
||||||
активного прокси: явный тип `xray` или `external`, отображаемое имя, адрес, порт
|
|
||||||
и добавку к `NO_PROXY`. Формат — INI из стандартной библиотеки Python. Если
|
|
||||||
файла нет, прокси-подсистема сохраняет прежний внешний адрес
|
|
||||||
`hproxy.local:12334` ради обратной совместимости.
|
|
||||||
|
|
||||||
Секретный `~/.config/proxy-client/default.url` содержит одну VLESS-ссылку.
|
|
||||||
Команда запуска принимает другой файл через `-f`. Профили можно хранить в
|
|
||||||
нескольких файлах, но активным у пользователя бывает только один.
|
|
||||||
CLI следует за симлинком, но проверяет права конечного файла: доступ группы или
|
|
||||||
остальных пользователей считается ошибкой с подсказкой выполнить `chmod 600`.
|
|
||||||
Файл должен содержать ровно одну непустую строку без комментариев и служебного
|
|
||||||
текста.
|
|
||||||
|
|
||||||
Базовый `NO_PROXY` всегда включает loopback-адреса и узел самого прокси.
|
|
||||||
Машинная добавка расширяет его локальными IP и доменами. Документация должна
|
|
||||||
предупреждать, что поддержка CIDR в `NO_PROXY` различается между программами.
|
|
||||||
|
|
||||||
### Явная семантика прокси
|
|
||||||
|
|
||||||
Тип `xray` означает, что `pclaude` и `pcodex` требуют запущенный процесс,
|
|
||||||
которым управляет `proxy-client`. Если его нет, агент не запускается, а сообщение
|
|
||||||
предлагает выполнить `proxy-client start`.
|
|
||||||
|
|
||||||
Тип `external` означает внешний процесс, например Hiddify. Проверяется только
|
|
||||||
доступность явно заданного адреса и порта. Ни один тип не переходит на другой
|
|
||||||
автоматически.
|
|
||||||
|
|
||||||
`start` разрешён только при `type=xray`; при `type=external` он завершается с
|
|
||||||
подсказкой изменить машинный конфиг. Для Xray адрес машинного конфига обязан
|
|
||||||
быть loopback-адресом, а порт обязан совпадать с создаваемым входом. Так нельзя
|
|
||||||
случайно поднять локальный Xray, оставив `pclaude` направленным во внешний
|
|
||||||
Hiddify.
|
|
||||||
|
|
||||||
`status` описывает именно активный тип из машинного конфига: состояние процесса
|
|
||||||
и порта для Xray либо доступность адреса для external. `stop` от машинного типа
|
|
||||||
не зависит и завершает записанный управляемый Xray, если он существует. Поэтому
|
|
||||||
после явного переключения на external старый локальный процесс можно штатно
|
|
||||||
остановить.
|
|
||||||
|
|
||||||
HTTP, HTTPS и SOCKS используют один локальный порт, по умолчанию `12334`.
|
|
||||||
Порт остаётся настраиваемым, чтобы разные Unix-пользователи одной машины могли
|
|
||||||
запускать свои экземпляры без конфликта.
|
|
||||||
|
|
||||||
Локальный вход Xray слушает только loopback: `127.0.0.1` и, если поддержано
|
|
||||||
выбранной конфигурацией, `::1`. Он никогда не привязывается к `0.0.0.0`, адресу
|
|
||||||
сетевого интерфейса или имени узла. Это закрывает вход от удалённой сети, но не
|
|
||||||
изолирует его от других пользователей того же Linux-узла. Первая версия не
|
|
||||||
добавляет локальную аутентификацию и предполагает доверенных пользователей
|
|
||||||
машины.
|
|
||||||
|
|
||||||
### Поддерживаемые VLESS-ссылки
|
|
||||||
|
|
||||||
Первая версия поддерживает две семьи:
|
|
||||||
|
|
||||||
- VLESS с TCP/RAW и REALITY;
|
|
||||||
- VLESS с XHTTP и TLS, включая HTTP/1.1, h2 и h3 через ALPN.
|
|
||||||
|
|
||||||
Преобразователь сохраняет значимые параметры ссылки, проверяет обязательные
|
|
||||||
поля и отклоняет неизвестное сочетание целиком. Он не должен молча пропускать
|
|
||||||
неизвестные или несовместимые параметры. Поддержка REALITY опирается на
|
|
||||||
[официальную схему REALITY](https://xtls.github.io/en/config/transports/reality.html),
|
|
||||||
а XHTTP с TLS — на
|
|
||||||
[схему TLS](https://xtls.github.io/en/config/transports/tls.html).
|
|
||||||
|
|
||||||
Для REALITY преобразователь явно сопоставляет UUID с VLESS `id`, узел и порт с
|
|
||||||
адресом назначения, `type=tcp` с RAW, `flow` с режимом VLESS Vision, `sni` с
|
|
||||||
`serverName`, `fp` с `fingerprint`, `pbk` с `password`, а `sid` с `shortId`.
|
|
||||||
`headerType=none` принимается как нейтральное значение, другое значение
|
|
||||||
отклоняется. `hiddify`, `host` и `alpn` для TCP/REALITY считаются метаданными и
|
|
||||||
не меняют Xray JSON; безопасная сводка явно помечает `host` и `alpn` как
|
|
||||||
неиспользованные. Это не скрытая потеря параметра: в целевой
|
|
||||||
[схеме REALITY Xray 26.3.27](https://github.com/XTLS/Xray-core/blob/v26.3.27/infra/conf/transport_internet.go#L3591-L3636)
|
|
||||||
нет полей Host или ALPN. Повтор одного query-параметра считается ошибкой.
|
|
||||||
Остальные правила соответствуют
|
|
||||||
[стандарту VLESS-ссылок Xray](https://github.com/XTLS/Xray-core/discussions/716).
|
|
||||||
|
|
||||||
Основной сценарий XHTTP/TLS — одиночная ссылка Hiddify с `type=xhttp`,
|
|
||||||
`security=tls`, `sni`, `alpn`, `path`, `host`, `encryption`, `fp` и, возможно,
|
|
||||||
`extra`. UUID, узел и порт преобразуются так же, как для REALITY. Остальные
|
|
||||||
поля сопоставляются явно:
|
|
||||||
|
|
||||||
- `type=xhttp` задаёт транспорт XHTTP;
|
|
||||||
- `security=tls` включает TLS;
|
|
||||||
- `sni`, `alpn` и `fp` задают `serverName`, список ALPN и fingerprint;
|
|
||||||
- `path`, `host` и необязательный `mode` переходят в настройки XHTTP;
|
|
||||||
- `encryption` задаёт шифрование VLESS и в первой версии должно быть `none`;
|
|
||||||
- `extra` URL-декодируется, разбирается как JSON-объект без повторяющихся
|
|
||||||
ключей и целиком передаётся в одноимённую секцию XHTTP;
|
|
||||||
- `pcs` и `vcn` для Xray 26.3.27 переходят в `pinnedPeerCertSha256` и
|
|
||||||
`verifyPeerCertByName`;
|
|
||||||
- `hiddify=1` и `core=xray` принимаются как метаданные источника и не меняют
|
|
||||||
JSON;
|
|
||||||
- `headerType=none` принимается как устаревшее нейтральное поле Hiddify, другое
|
|
||||||
значение отклоняется;
|
|
||||||
- fragment после `#` используется только как отображаемое имя профиля.
|
|
||||||
|
|
||||||
Если `mode` отсутствует, преобразователь не придумывает его и оставляет выбор
|
|
||||||
Xray. Параметры query не зависят от порядка, но не могут повторяться. После
|
|
||||||
перечисленных полей неизвестный параметр считается ошибкой: он может менять
|
|
||||||
смысл соединения и требует явного добавления в преобразователь. `path`, `host`,
|
|
||||||
`mode` и `extra` входят в официальный стандарт ссылок Xray; параметры TLS
|
|
||||||
`pcs`/`vcn` заменяют удалённый `allowInsecure`:
|
|
||||||
[стандарт VLESS-ссылок](https://github.com/XTLS/Xray-core/discussions/716).
|
|
||||||
|
|
||||||
Перед запуском CLI печатает компактную безопасную сводку: сервер, порт,
|
|
||||||
транспорт, защиту, SNI, ALPN, Host, наличие и длину пути, fingerprint и локальный
|
|
||||||
порт. XHTTP `path` может содержать tenant ID или bearer-подобный токен, поэтому
|
|
||||||
его содержимое никогда не печатается. Обычный режим не выводит подробный журнал
|
|
||||||
Xray. UUID, REALITY password/public key, short ID и другие чувствительные
|
|
||||||
значения обозначаются без фрагментов исходного значения.
|
|
||||||
В создаваемом JSON журнал доступа отключён, а уровень ошибок по умолчанию —
|
|
||||||
`warning`.
|
|
||||||
|
|
||||||
Hiddify Manager может добавлять `allowInsecure=true` и `insecure=true` даже к
|
|
||||||
профилю с действительным публичным сертификатом — этот дефект описан в
|
|
||||||
[hiddify/Hiddify-Manager#5438](https://github.com/hiddify/Hiddify-Manager/issues/5438).
|
|
||||||
Современный Xray больше не поддерживает `allowInsecure`; удаление зафиксировано
|
|
||||||
в [релизе 26.2.6](https://github.com/XTLS/Xray-core/releases/tag/v26.2.6).
|
|
||||||
Преобразователь принимает для обоих полей только точные строчные значения
|
|
||||||
`true` и `false`. Если присутствуют оба поля, их значения должны совпадать;
|
|
||||||
конфликт и любое другое написание отклоняются до создания JSON. Для Xray
|
|
||||||
26.3.27 поля не переносятся в JSON; истинное значение даёт предупреждение, что
|
|
||||||
сертификат будет проверен обычным способом, а ложное не требует предупреждения.
|
|
||||||
Соединение после этого может успешно заработать или завершиться ошибкой
|
|
||||||
сертификата; отдельного раннего отказа нет.
|
|
||||||
|
|
||||||
### Установка Xray
|
### Установка Xray
|
||||||
|
|
||||||
`proxy-client install` — отдельная явная операция. Она устанавливает Xray в
|
`proxy-client install` ставит в `~/.local/bin/xray` официальный архив одного
|
||||||
`~/.local/bin`, скачивает официальный архив и файл контрольной суммы, затем
|
зафиксированного релиза (сейчас 26.3.27), проверяет SHA-256 по `.dgst` и
|
||||||
проверяет SHA-256. Поддерживается только зафиксированный релиз 26.3.27;
|
заменяет файл атомарно. Официальный установщик Xray не подходит: он требует
|
||||||
параметра версии нет. Официальные релизы публикуют необходимые файлы `.dgst`:
|
root и создаёт systemd-службы. `start` принимает только ту же версию и никогда
|
||||||
[релиз 26.3.27](https://github.com/XTLS/Xray-core/releases/tag/v26.3.27).
|
ничего не скачивает.
|
||||||
|
|
||||||
Для Linux `x86_64`/`amd64` используется `Xray-linux-64.zip`, для
|
|
||||||
`aarch64`/`arm64` — `Xray-linux-arm64-v8a.zip`; остальные архитектуры
|
|
||||||
отклоняются. Официальный `.dgst` содержит записи `MD5`, `SHA1`, `SHA2-256` и
|
|
||||||
`SHA2-512`; принимается ровно один полный набор со строчными hex-значениями,
|
|
||||||
для проверки архива используется `SHA2-256`. Дубликаты, uppercase, отсутствие
|
|
||||||
записи и мусор отклоняются. Архив и извлечённый бинарник ограничены 64 MiB,
|
|
||||||
`.dgst` — 4 KiB; фактический размер считается с limit+1 и должен совпадать с
|
|
||||||
объявленным `Content-Length`. Архив распаковывается в закрытый staging-каталог
|
|
||||||
внутри `~/.local/bin`, проверенный бинарник запускается с `version`, после чего
|
|
||||||
одним `os.replace` атомарно переносится в `~/.local/bin/xray`.
|
|
||||||
|
|
||||||
Корректная существующая версия 26.3.27 даёт no-op без сети. Другой файл или
|
|
||||||
симлинк не заменяется без `--replace`, а каталог не заменяется никогда.
|
|
||||||
Start-lock берётся до проверки назначения и удерживается до завершения
|
|
||||||
`os.replace`; живой, сиротский или неподтверждённый managed-state запрещает
|
|
||||||
установку с подсказкой сначала выполнить `stop`.
|
|
||||||
После установки CLI проверяет, что первый `xray` из `PATH` разрешается в
|
|
||||||
установленный файл; иначе завершается с подсказкой поставить `~/.local/bin` в
|
|
||||||
начало `PATH`, не откатывая готовую установку. `start` всегда показывает
|
|
||||||
фактический путь и версию выбранного бинарника.
|
|
||||||
|
|
||||||
Перед созданием JSON CLI разрешает первый `xray` из `PATH` до одного пути и
|
|
||||||
запускает этим файлом `xray version`. Первая строка должна начинаться с точного
|
|
||||||
токена `Xray VERSION`, после полного токена версии допускается только конец
|
|
||||||
строки либо ASCII-пробел. Принимается только версия 26.3.27; ведущий
|
|
||||||
мусор, похожий более длинный токен, пустой, неверный, завершившийся ошибкой или
|
|
||||||
содержащий другую версию вывод запрещает запуск. Тот же разрешённый файл
|
|
||||||
используется для `run -test` и основного процесса. Версия никогда не выводится
|
|
||||||
из содержимого VLESS-ссылки.
|
|
||||||
|
|
||||||
`start` никогда не скачивает бинарник. Если `xray` отсутствует в `PATH`, команда
|
|
||||||
завершается с подсказкой выполнить `proxy-client install`.
|
|
||||||
|
|
||||||
### Жизненный цикл
|
### Жизненный цикл
|
||||||
|
|
||||||
CLI предоставляет `install`, `start`, `stop`, `status`, `env` и `run`. Запуск
|
`start` остаётся на переднем плане как небольшой Python-супервизор с одним
|
||||||
без подкоманды не устанавливает и не стартует ничего.
|
дочерним Xray; ожидаемое место — панель tmux. Отдельный журнал и команда `logs`
|
||||||
|
не нужны: хватает истории tmux. Второй `start` отсекает блокировка `flock`.
|
||||||
|
`state.json` хранит PID супервизора и Xray, чтобы `stop` работал из любого
|
||||||
|
терминала и мог добить Xray, если супервизор убит.
|
||||||
|
|
||||||
`start` остаётся на переднем плане как небольшой Python-супервизор и занимает
|
Перед запуском обязательны разбор ссылки, `xray run -test` и открытый порт
|
||||||
текущую консоль. Он запускает один дочерний Xray без демонизации и без новой
|
после старта. Проверка внешнего IP — только диагностика: её сбой даёт
|
||||||
сессии процесса, ждёт его завершения и передаёт ему SIGINT, SIGTERM и SIGHUP.
|
предупреждение.
|
||||||
Ожидаемый способ работы — отдельная панель tmux или другого мультиплексора.
|
|
||||||
Отсоединение от tmux не останавливает процесс, но закрытие панели или сессии
|
|
||||||
останавливает Xray. Отдельный постоянный журнал и команда `logs` не нужны:
|
|
||||||
достаточно истории tmux.
|
|
||||||
|
|
||||||
Супервизор на всё время работы удерживает неблокирующую эксклюзивную блокировку
|
`run` никогда не запускает Xray, поэтому у `pclaude` и `pcodex` нет скрытого
|
||||||
`fcntl.flock` в служебном каталоге `~/.local/state/proxy-client/`. Второй `start`
|
автозапуска.
|
||||||
отказывает до запуска процессов и изменения state или сгенерированного
|
|
||||||
конфига. `state.json` нужен для восстановления после сбоя, но не служит
|
|
||||||
блокировкой.
|
|
||||||
|
|
||||||
Файл состояния содержит ровно `supervisor_pid`, `xray_pid`, `xray_path`,
|
|
||||||
`config_path`, `address`, `port`, `supervisor_path`, `supervisor_starttime`,
|
|
||||||
`xray_starttime` и `boot_id`. Время старта — поле 22 `/proc/PID/stat`; поле
|
|
||||||
`comm` сначала отделяется по последней `)`. Для обоих процессов проверяются PID,
|
|
||||||
точное время старта и `boot_id`. У ребёнка дополнительно сверяется канонический
|
|
||||||
`/proc/PID/exe` с `xray_path`. У Python-супервизора операнд скрипта из
|
|
||||||
`/proc/PID/cmdline` разрешается относительно `/proc/PID/cwd` и сравнивается с
|
|
||||||
каноническим `supervisor_path`. Ошибка чтения или разбора означает несовпадение.
|
|
||||||
Непосредственно перед каждым сигналом идентичность читается повторно.
|
|
||||||
|
|
||||||
Проверка состояния различает три исхода. Для здоровой пары `stop` посылает
|
|
||||||
SIGTERM супервизору. Если супервизор исчез, но ребёнок подтверждён, `status`
|
|
||||||
сообщает о сироте с ненулевым кодом, `env` и `run` отказывают, а `stop` посылает
|
|
||||||
сигнал ребёнку напрямую без требования доступности порта и удаляет state и
|
|
||||||
конфиг. При живом, но неподтверждённом PID сигналы не посылаются, файлы
|
|
||||||
сохраняются, а диагностика требует ручной проверки и удаления. Если оба PID
|
|
||||||
мертвы или `boot_id` не совпадает, устаревшие state и конфиг можно безопасно
|
|
||||||
удалить без сигналов.
|
|
||||||
|
|
||||||
У пользователя может быть только один управляемый экземпляр. Повторный `start`
|
|
||||||
не заменяет его другим профилем. Для смены профиля нужны явные `stop` и `start`.
|
|
||||||
|
|
||||||
Перед успешным запуском должны пройти разбор ссылки, проверка поддерживаемого
|
|
||||||
сочетания и `xray run -test`. После запуска процесс должен оставаться живым, а
|
|
||||||
локальный порт — принимать соединения. Проверка внешнего IP через прокси носит
|
|
||||||
диагностический характер: её сбой даёт предупреждение, но не останавливает уже
|
|
||||||
прошедший обязательные проверки Xray.
|
|
||||||
|
|
||||||
Сгенерированный JSON и временные файлы создаются внутри служебного каталога и
|
|
||||||
удаляются супервизором после завершения Xray. Каталоги создаются с правами
|
|
||||||
`0700`, файлы с конфигурацией и состоянием — `0600`. Исходная VLESS-ссылка не
|
|
||||||
попадает в stdout или stderr при успехе, ошибках разбора и сбое `run -test` и
|
|
||||||
никогда не передаётся аргументом командной строки. Те же поверхности не содержат
|
|
||||||
UUID, ключи, short ID и другие уникальные секретные маркеры профиля.
|
|
||||||
|
|
||||||
`run` никогда не запускает Xray. Для типа `xray` он проверяет PID управляемого
|
|
||||||
процесса и локальный порт; для типа `external` — только явно настроенный адрес и
|
|
||||||
порт. Лишь после успешной проверки он выставляет proxy-переменные для дочерней
|
|
||||||
команды. Поэтому `pclaude` и `pcodex` не имеют скрытого автозапуска.
|
|
||||||
|
|
||||||
### Доставка
|
### Доставка
|
||||||
|
|
||||||
Компоненты образуют отдельный stow-совместимый каталог `proxy-client/`. Он
|
Каталог `proxy-client/` раскладывается в `$HOME` ссылками: CLI в
|
||||||
доставляет CLI, Bash-модуль и три шаблона: машинного INI для внешнего прокси,
|
`~/.local/bin/`, `shell.bash` и `examples/` в `~/.config/proxy-client/`.
|
||||||
машинного INI для Xray и файла VLESS-ссылки.
|
Машинный `config` и `default.url` пользователь создаёт сам; в Git они не
|
||||||
Реальные пользовательские файлы не управляются stow и не входят в Git.
|
входят.
|
||||||
CLI раскладывается как `~/.local/bin/proxy-client`. Машинный конфиг, профиль,
|
|
||||||
модуль `shell.bash` и каталог `examples/` находятся вместе в
|
|
||||||
`~/.config/proxy-client/`. Шаблоны не подменяют настоящие пользовательские
|
|
||||||
файлы.
|
|
||||||
|
|
||||||
В корне этого каталога находятся самостоятельный `README.md` и пакетный
|
- Отдельно: `proxy-client/install.sh` ставит эти три ссылки и по умолчанию
|
||||||
`install.sh`. Оба файла относятся к исходному каталогу и исключаются из
|
дописывает блок в `~/.bashrc`. Чужие файлы не трогает. Режим прокси, Xray и
|
||||||
раскладки stow. README остаётся рядом с кодом при выборочном клонировании или
|
сеть не настраивает — это делают отдельные команды по README.
|
||||||
копировании каталога. Корневой README репозитория только кратко описывает
|
- В составе dotfiles: корневой `install.sh` подключает тот же пакет через Stow,
|
||||||
подсистему и ссылается на него.
|
а `.bash_custom` загружает `shell.bash`.
|
||||||
|
|
||||||
Пакетный установщик связывает отдельно скопированный каталог с домашним
|
## Риски
|
||||||
каталогом и не зависит от положения или наличия остальных dotfiles. Если stow
|
|
||||||
доступен, установщик использует его; если нет — создаёт тот же набор симлинков
|
|
||||||
напрямую. Выбранный механизм явно показывается пользователю, но не меняет
|
|
||||||
результат. Исходный каталог должен оставаться на прежнем пути всё время
|
|
||||||
использования ссылок.
|
|
||||||
|
|
||||||
Установщик управляет только тремя публичными артефактами: CLI, `shell.bash` и
|
- Формат ссылок Hiddify расширяется. Строгий отказ на неизвестном параметре
|
||||||
каталогом шаблонов. Машинный `config` и закрытый `default.url` не входят в
|
лучше частично неверного JSON, но новые ссылки могут потребовать правки
|
||||||
раскладку stow, никогда не создаются, не заменяются и не связываются
|
разбора.
|
||||||
установщиком. Их наличие в исходном каталоге также не должно расширять набор
|
- Открытый порт не доказывает, что маршрут работает. Проверка внешнего IP
|
||||||
управляемых путей.
|
отделена от обязательных проверок.
|
||||||
|
- Loopback без аутентификации открыт другим пользователям того же узла.
|
||||||
Домашние `~/.local/bin` и `~/.config/proxy-client` остаются настоящими
|
Пользователи машины считаются доверенными.
|
||||||
каталогами. Stow не должен сворачивать их или их родителей в одну ссылку на
|
- `NO_PROXY` программы понимают по-разному, особенно CIDR. README советует
|
||||||
исходное дерево: ссылками становятся только три перечисленных артефакта. Это
|
точные IP и домены.
|
||||||
одновременно сохраняет место для машинных файлов и делает результат stow
|
- Ссылки установки зависят от пути исходного каталога: переносить его нельзя.
|
||||||
совпадающим с прямой раскладкой через `ln -s`.
|
- Поддержка РЕД ОС, Astra Linux и Alt Linux заявляется только после проверки
|
||||||
|
версии Python и прогона `tests/test_proxy_client_full.sh` на них.
|
||||||
Перед записью установщик проверяет весь набор назначений. Правильная ссылка на
|
- Шаблоны в `examples/` содержат только вымышленные значения; настоящие ссылки,
|
||||||
текущий исходный каталог считается уже установленной; любой другой файл,
|
UUID и ключи в репозиторий не попадают.
|
||||||
каталог или симлинк считается конфликтом. При конфликте установщик ничего не
|
|
||||||
меняет и сообщает все пути, которые пользователь должен разобрать вручную.
|
|
||||||
Принудительной замены и автоматических резервных копий нет.
|
|
||||||
|
|
||||||
По умолчанию установщик идемпотентно подключает Bash-модуль в `~/.bashrc` и при
|
|
||||||
необходимости добавляет `~/.local/bin` в `PATH`. Уже существующее подключение
|
|
||||||
через dotfiles не дублируется. Управляемый блок помечается, повторно не
|
|
||||||
добавляется и может быть полностью отключён параметром `--no-shell-setup`.
|
|
||||||
|
|
||||||
Установщик не выбирает режим прокси, не создаёт конфигурацию, не читает VLESS,
|
|
||||||
не устанавливает Xray и не проверяет сеть. После раскладки он проверяет только
|
|
||||||
локальные требования и исправность CLI и Bash-модуля, затем печатает краткую
|
|
||||||
подсказку для текущей shell-сессии и направляет пользователя в README за
|
|
||||||
настройкой режима. Недоступный прокси не делает установку неуспешной.
|
|
||||||
|
|
||||||
Полный корневой `install.sh` продолжает подключать тот же stow-пакет вместе с
|
|
||||||
остальными dotfiles и не получает отдельной реализации этой логики.
|
|
||||||
|
|
||||||
## Решения и отклонённые альтернативы
|
|
||||||
|
|
||||||
- Выбран Python 3.8+, потому что стандартной библиотеки достаточно для URL,
|
|
||||||
JSON, INI, ZIP, SHA-256, HTTP, процессов и тестов. Более новый синтаксис и
|
|
||||||
сторонние пакеты запрещены ради старых и непредсказуемых серверных систем.
|
|
||||||
- Выбран INI, а не TOML: модуль
|
|
||||||
[`tomllib`](https://docs.python.org/3/library/tomllib.html) добавлен только в
|
|
||||||
Python 3.11, что исключило бы штатный Python 3.10 в Ubuntu 22.04.
|
|
||||||
- Bash оставлен отдельным тонким модулем, потому что дочерний Python-процесс не
|
|
||||||
может изменить окружение родительской shell-сессии.
|
|
||||||
- Выбрано публичное имя `proxy-client`, потому что подсистема обслуживает и
|
|
||||||
внешний прокси, и управляемый Xray. `xray-proxy` отклонено как имя,
|
|
||||||
выпячивающее одну реализацию, а `proxyctl` — как менее понятное пользователю.
|
|
||||||
- Все пользовательские файлы собраны в `~/.config/proxy-client/`. Разделение
|
|
||||||
между общим машинным конфигом и Xray-профилем остаётся логическим, но не
|
|
||||||
требует помнить два каталога.
|
|
||||||
- Машинный прокси и VLESS-ссылка разделены: первый файл описывает локальную
|
|
||||||
политику shell, второй содержит секрет и меняется независимо.
|
|
||||||
- Выбран один порт, потому что SOCKS-вход Xray совместим с HTTP и не требует
|
|
||||||
второго локального слушателя.
|
|
||||||
- Выбран явный тип прокси вместо автообнаружения. Предсказуемость канала важнее
|
|
||||||
удобства автоматического перехода.
|
|
||||||
- Выбрана одна активная конфигурация на пользователя. Именованные процессы и
|
|
||||||
автоматическое распределение портов отложены до появления реальной нужды.
|
|
||||||
- Официальный системный установщик отклонён: он требует root и создаёт
|
|
||||||
systemd-службы. Пользовательская установка соответствует выбранной модели.
|
|
||||||
- Копирование файлов пакета отклонено: оно создаёт отдельные экземпляры,
|
|
||||||
усложняет обновление и нарушает общую с dotfiles модель симлинков.
|
|
||||||
- Обязательный stow отклонён как лишняя зависимость. Прямые симлинки дают тот же
|
|
||||||
результат на машине без stow, а автоматический выбор механизма не меняет
|
|
||||||
публичное поведение пакета.
|
|
||||||
- Автоматическая настройка режима, установка Xray и проверка сети отклонены для
|
|
||||||
установщика пакета. Эти операции зависят от машины, могут требовать секретов
|
|
||||||
или сети и уже имеют отдельные явные команды и инструкции.
|
|
||||||
- Готовые JSON и полный импорт Hiddify отклонены как основной интерфейс: точечная
|
|
||||||
VLESS-ссылка проще для пользователя и ограничивает область преобразования.
|
|
||||||
|
|
||||||
## Риски и меры
|
|
||||||
|
|
||||||
- Формат ссылок Hiddify может расширяться. Строгая проверка и явный отказ
|
|
||||||
безопаснее частично неверного JSON.
|
|
||||||
- Проверка открытого порта не доказывает работоспособность маршрута. CLI
|
|
||||||
разделяет обязательную проверку процесса и необязательную внешнюю диагностику.
|
|
||||||
- VLESS-ссылка содержит чувствительные данные. CLI сокращает секреты в выводе,
|
|
||||||
не журналирует их и использует закрытые пользовательские файлы.
|
|
||||||
- Устаревшие Hiddify-поля `allowInsecure` и `insecure` неоднозначны. Они не
|
|
||||||
переносятся в Xray 26.3.27, сертификат проверяется обычным способом, а
|
|
||||||
истинное значение сопровождается предупреждением.
|
|
||||||
- Старые дистрибутивы могут иметь Python ниже 3.8. CLI проверяет версию до
|
|
||||||
основной работы и сообщает обнаруженную и требуемую версии.
|
|
||||||
- Симлинки зависят от постоянного пути исходного каталога. Установщик явно
|
|
||||||
сообщает эту зависимость и при повторном запуске принимает только ссылки на
|
|
||||||
текущий источник.
|
|
||||||
- Изменение `~/.bashrc` может конфликтовать с пользовательским способом загрузки
|
|
||||||
оболочки. Установщик распознаёт собственный блок и известное подключение из
|
|
||||||
dotfiles, не дублирует их и предоставляет `--no-shell-setup`.
|
|
||||||
- Частичная установка может оставить неоднозначное состояние. Все конфликты
|
|
||||||
назначений проверяются до изменений; проверки охватывают отсутствие новых
|
|
||||||
ссылок и правок shell при отказе.
|
|
||||||
- `NO_PROXY` реализуется программами неодинаково. README рекомендует точные IP
|
|
||||||
и домены, а CIDR описывает как совместимость best-effort.
|
|
||||||
- Разные пользователи делят сетевое пространство имён. Настраиваемый порт и
|
|
||||||
явная ошибка конфликта предотвращают случайный запуск на занятом порту, но
|
|
||||||
loopback без аутентификации не запрещает намеренное подключение другого
|
|
||||||
локального пользователя. Это принятое ограничение первой версии.
|
|
||||||
|
|
||||||
## Проверка реализации
|
|
||||||
|
|
||||||
- Модульные тесты разбирают обезличенные ссылки обеих поддерживаемых семей и
|
|
||||||
все показанные варианты ALPN. Отдельный регрессионный пример повторяет форму
|
|
||||||
типичной ссылки Hiddify XHTTP/TLS с `hiddify`, `core`, `extra`,
|
|
||||||
`headerType=none`, `allowInsecure` и `insecure`.
|
|
||||||
- Неподдерживаемые транспорт, защита, отсутствующие обязательные поля и любой
|
|
||||||
неизвестный query-параметр дают понятный отказ до запуска Xray.
|
|
||||||
- Сгенерированный JSON проходит `xray run -test` на Xray 26.3.27 для заявленных
|
|
||||||
сочетаний. Интеграционная проверка подтверждает, что один локальный вход
|
|
||||||
принимает HTTP и SOCKS.
|
|
||||||
- Тесты отдельно проверяют удаление устаревших insecure-полей.
|
|
||||||
- Тесты подтверждают сокращение секретов и отсутствие полной VLESS-ссылки в
|
|
||||||
stdout, stderr, служебных файлах и аргументах дочернего процесса.
|
|
||||||
- Установка проверяется на корректной и повреждённой SHA-256 без записи вне
|
|
||||||
пользовательского каталога.
|
|
||||||
- Управление процессом покрывает повторный запуск, устаревший PID, чужой PID,
|
|
||||||
повторно использованный PID, передачу сигналов, аварийно исчезнувший
|
|
||||||
супервизор, очистку файлов, смену профиля, остановку и конфликт порта.
|
|
||||||
- Сбой внешней проверки IP не отменяет успешный запуск, но отображается отдельно
|
|
||||||
от состояния процесса и порта.
|
|
||||||
- `run` сохраняет аргументы и код возврата, не оставляет proxy-переменные в
|
|
||||||
родительской Bash-сессии и не запускает команду при недоступном прокси.
|
|
||||||
- `env` не печатает частичный результат при ошибке, а `shell.bash` отклоняет
|
|
||||||
неизвестные строки без `eval` и без частичного экспорта.
|
|
||||||
- Регрессионные Bash-тесты подтверждают прежнее поведение Hiddify при отсутствии
|
|
||||||
машинного конфига, оба регистра proxy-переменных, настраиваемый `NO_PROXY` и
|
|
||||||
отсутствие автоматического запроса внешнего IP в `proxy_on`/`proxy_off`.
|
|
||||||
- Проверки самостоятельной установки покрывают путь со stow и без него,
|
|
||||||
одинаковый набор итоговых ссылок, запуск не из каталога пакета и каталог с
|
|
||||||
пробелами в пути.
|
|
||||||
- В обоих путях домашние каталоги остаются обычными каталогами, а пакетные
|
|
||||||
`README.md` и `install.sh` не появляются в домашнем каталоге через stow.
|
|
||||||
- Повторный запуск не меняет уже правильную установку. Конфликт любого
|
|
||||||
назначения обнаруживается до записи; чужие файлы, каталоги и ссылки остаются
|
|
||||||
неизменными.
|
|
||||||
- Машинный `config` и закрытый `default.url` не управляются установщиком даже
|
|
||||||
при их наличии в исходном каталоге или домашнем каталоге.
|
|
||||||
- Настройка Bash проверяется для нового `~/.bashrc`, уже помеченного блока,
|
|
||||||
подключения из полного dotfiles и режима `--no-shell-setup`. Добавление
|
|
||||||
`~/.local/bin` не дублируется.
|
|
||||||
- Локальная проверка установки не обращается к сети и не требует доступного
|
|
||||||
прокси. Неисправный или отсутствующий машинный конфиг не отменяет успешную
|
|
||||||
раскладку пакета.
|
|
||||||
- Ручная проверка выполняется минимум на Ubuntu с Python 3.10 и на одной целевой
|
|
||||||
удалённой системе. РЕД ОС, Astra Linux и Alt Linux проверяются по мере
|
|
||||||
доступности: недоступная среда явно записывается как неподтверждённая, а
|
|
||||||
поддержка заявляется только после проверки версии Python и полного прогона.
|
|
||||||
|
|
||||||
## Влияние на документацию
|
|
||||||
|
|
||||||
Основная инструкция находится в `proxy-client/README.md` рядом с
|
|
||||||
реализацией. Она должна описывать:
|
|
||||||
|
|
||||||
- различие внешнего Hiddify и управляемого Xray;
|
|
||||||
- самостоятельный `install.sh`, его автоматический выбор stow или прямых
|
|
||||||
симлинков, требование сохранять исходный каталог и режим
|
|
||||||
`--no-shell-setup`;
|
|
||||||
- границу между управляемыми файлами пакета и создаваемыми вручную машинным
|
|
||||||
конфигом и закрытым профилем;
|
|
||||||
- создание машинного INI из шаблона и отдельного файла VLESS-ссылки;
|
|
||||||
- команды управления, `proxy_on/off`, `pclaude/pcodex`;
|
|
||||||
- настройка `NO_PROXY`, ограничения CIDR и диагностика;
|
|
||||||
- поддерживаемые семейства VLESS и современную проверку сертификата;
|
|
||||||
- отсутствие systemd, автозапуска и автоматического перехода между прокси.
|
|
||||||
|
|
||||||
Корневой README получает только короткий обзор, пример подключения пакета и
|
|
||||||
ссылку на полную инструкцию клиента.
|
|
||||||
|
|
||||||
Шаблоны должны содержать только вымышленные значения. Настоящие VLESS-ссылки,
|
|
||||||
UUID и ключи никогда не входят в репозиторий.
|
|
||||||
@@ -5,21 +5,17 @@ _proxy_cli() {
|
|||||||
"${PROXY_CLIENT_BIN:-proxy-client}" "$@"
|
"${PROXY_CLIENT_BIN:-proxy-client}" "$@"
|
||||||
}
|
}
|
||||||
|
|
||||||
_proxy_apply_env_file() {
|
# Применяет вывод "proxy-client env" целиком или не применяет ничего.
|
||||||
local output_file="$1" line key value
|
_proxy_apply_env() {
|
||||||
|
local line key
|
||||||
local -A proxy_values=()
|
local -A proxy_values=()
|
||||||
local -a allowed_keys=(
|
|
||||||
HTTP_PROXY HTTPS_PROXY ALL_PROXY NO_PROXY
|
|
||||||
http_proxy https_proxy all_proxy no_proxy
|
|
||||||
)
|
|
||||||
|
|
||||||
while IFS= read -r line || [ -n "$line" ]; do
|
while IFS= read -r line; do
|
||||||
if [[ "$line" != *=* ]]; then
|
if [[ "$line" != *=* ]]; then
|
||||||
echo "Ошибка: proxy-client env вернул строку без '='" >&2
|
echo "Ошибка: proxy-client env вернул строку без '='" >&2
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
key="${line%%=*}"
|
key="${line%%=*}"
|
||||||
value="${line#*=}"
|
|
||||||
case "$key" in
|
case "$key" in
|
||||||
HTTP_PROXY|HTTPS_PROXY|ALL_PROXY|NO_PROXY|http_proxy|https_proxy|all_proxy|no_proxy) ;;
|
HTTP_PROXY|HTTPS_PROXY|ALL_PROXY|NO_PROXY|http_proxy|https_proxy|all_proxy|no_proxy) ;;
|
||||||
*)
|
*)
|
||||||
@@ -27,38 +23,20 @@ _proxy_apply_env_file() {
|
|||||||
return 1
|
return 1
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
proxy_values["$key"]="$value"
|
proxy_values["$key"]="${line#*=}"
|
||||||
done < "$output_file"
|
done <<< "$1"
|
||||||
|
|
||||||
for key in "${allowed_keys[@]}"; do
|
for key in "${!proxy_values[@]}"; do
|
||||||
if [[ -n "${proxy_values[$key]+set}" ]]; then
|
export "$key=${proxy_values[$key]}"
|
||||||
printf -v "$key" '%s' "${proxy_values[$key]}"
|
|
||||||
export "$key"
|
|
||||||
fi
|
|
||||||
done
|
done
|
||||||
}
|
}
|
||||||
|
|
||||||
proxy_on() {
|
proxy_on() {
|
||||||
local output_file old_umask
|
local output
|
||||||
old_umask="$(umask)"
|
if ! output="$(_proxy_cli env)" || ! _proxy_apply_env "$output"; then
|
||||||
umask 077
|
|
||||||
if ! output_file="$(mktemp "${TMPDIR:-/tmp}/proxy-client-env.XXXXXX")"; then
|
|
||||||
umask "$old_umask"
|
|
||||||
echo "Ошибка: не удалось создать временный файл для proxy-env" >&2
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
umask "$old_umask"
|
|
||||||
if ! _proxy_cli env > "$output_file"; then
|
|
||||||
rm -f "$output_file"
|
|
||||||
echo "Ошибка: прокси не включён" >&2
|
echo "Ошибка: прокси не включён" >&2
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
if ! _proxy_apply_env_file "$output_file"; then
|
|
||||||
rm -f "$output_file"
|
|
||||||
echo "Ошибка: прокси не включён" >&2
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
rm -f "$output_file"
|
|
||||||
echo "Прокси включён"
|
echo "Прокси включён"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+405
-1030
File diff suppressed because it is too large.
Load diff
+49
-21
@@ -19,13 +19,16 @@ CLI требует Python 3.8+ и использует только станда
|
|||||||
~/src/proxy-client/install.sh
|
~/src/proxy-client/install.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
При наличии Stow установщик использует его, иначе создаёт те же три симлинка
|
Установщик создаёт три ссылки на исходный каталог: CLI, Bash-модуль и каталог
|
||||||
напрямую: CLI, Bash-модуль и каталог шаблонов. Исходный каталог после установки
|
шаблонов. Поэтому исходный каталог после установки нельзя перемещать. Если
|
||||||
нельзя перемещать: домашние ссылки зависят от его постоянного пути.
|
какой-то из путей уже занят чужим файлом или ссылкой, установщик ничего не
|
||||||
|
меняет и перечисляет все такие пути. Повторный запуск безопасен.
|
||||||
|
|
||||||
По умолчанию установщик добавляет в `~/.bashrc` помеченный блок для
|
По умолчанию установщик дописывает в `~/.bashrc` блок между строками
|
||||||
`~/.local/bin` и Bash-модуля. После установки откройте новый Bash или примените
|
`# >>> proxy-client >>>` и `# <<< proxy-client <<<`: он добавляет
|
||||||
блок в текущей сессии:
|
`~/.local/bin` в `PATH` и подключает Bash-модуль. Если блок уже есть, файл не
|
||||||
|
меняется. После установки откройте новый Bash или примените блок в текущей
|
||||||
|
сессии:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
source ~/.bashrc
|
source ~/.bashrc
|
||||||
@@ -253,8 +256,13 @@ proxy-client stop
|
|||||||
плане. Systemd, фоновый автозапуск и скрытый запуск из `run` не используются.
|
плане. Systemd, фоновый автозапуск и скрытый запуск из `run` не используются.
|
||||||
Один loopback-порт принимает HTTP CONNECT и SOCKS5.
|
Один loopback-порт принимает HTTP CONNECT и SOCKS5.
|
||||||
|
|
||||||
`stop` завершает управляемые процессы и удаляет созданные служебные файлы. Он
|
Остановить Xray можно двумя способами: `Ctrl+C` в окне `start` или
|
||||||
работает и после изменения машинного типа на `external`.
|
`proxy-client stop` из любого терминала. Оба гасят Xray и удаляют служебные
|
||||||
|
файлы. Если процесс `start` был убит (например, закрыли окно tmux), Xray может
|
||||||
|
остаться работать: `status` и `env` тогда сообщат об ошибке, а `stop` добьёт его
|
||||||
|
и уберёт файлы. `stop` работает и после изменения машинного типа на `external`.
|
||||||
|
|
||||||
|
Второй `start`, пока первый работает, завершится ошибкой.
|
||||||
|
|
||||||
## Диагностика
|
## Диагностика
|
||||||
|
|
||||||
@@ -343,7 +351,8 @@ source ~/.config/proxy-client/shell.bash
|
|||||||
|
|
||||||
Поддерживаются VLESS TCP/RAW с REALITY и VLESS XHTTP с TLS. Неизвестные,
|
Поддерживаются VLESS TCP/RAW с REALITY и VLESS XHTTP с TLS. Неизвестные,
|
||||||
повторные и несовместимые query-параметры отклоняются. XHTTP поддерживает ALPN
|
повторные и несовместимые query-параметры отклоняются. XHTTP поддерживает ALPN
|
||||||
`http/1.1`, `h2` и `h3`; `extra` должен быть JSON-объектом без повторных ключей.
|
`http/1.1`, `h2` и `h3`; `extra` должен быть JSON-объектом, его содержимое
|
||||||
|
проверяет `xray run -test`.
|
||||||
|
|
||||||
Кроме `encryption=none`, принимается VLESS-шифрование
|
Кроме `encryption=none`, принимается VLESS-шифрование
|
||||||
`encryption=mlkem768x25519plus.…` из новых ссылок Hiddify; остальную часть
|
`encryption=mlkem768x25519plus.…` из новых ссылок Hiddify; остальную часть
|
||||||
@@ -351,9 +360,9 @@ source ~/.config/proxy-client/shell.bash
|
|||||||
`flow=xtls-rprx-vision`. Косая черта перед параметрами (`…:443/?type=…`)
|
`flow=xtls-rprx-vision`. Косая черта перед параметрами (`…:443/?type=…`)
|
||||||
допустима, другой путь в ссылке — нет.
|
допустима, другой путь в ссылке — нет.
|
||||||
|
|
||||||
Xray 26.3.27 использует `pcs` и `vcn`, а устаревшие `allowInsecure` и
|
Xray 26.3.27 использует `pcs` и `vcn`. Устаревшие `allowInsecure` и `insecure`
|
||||||
`insecure` не переносит. Истинное legacy-значение даёт предупреждение, но
|
принимаются с любым значением, но не переносятся: при значении `1` или `true`
|
||||||
обычная проверка сертификата остаётся включённой.
|
выводится предупреждение, а сертификат проверяется обычным способом.
|
||||||
|
|
||||||
Последняя версия Xray, где `allowInsecure` ещё применяется, —
|
Последняя версия Xray, где `allowInsecure` ещё применяется, —
|
||||||
[25.12.8](https://github.com/XTLS/Xray-core/releases/tag/v25.12.8); опция
|
[25.12.8](https://github.com/XTLS/Xray-core/releases/tag/v25.12.8); опция
|
||||||
@@ -366,14 +375,18 @@ Xray 26.3.27 использует `pcs` и `vcn`, а устаревшие `allow
|
|||||||
`x86_64`/`amd64` используется `Xray-linux-64.zip`, для `aarch64`/`arm64` —
|
`x86_64`/`amd64` используется `Xray-linux-64.zip`, для `aarch64`/`arm64` —
|
||||||
`Xray-linux-arm64-v8a.zip`; остальные архитектуры отклоняются.
|
`Xray-linux-arm64-v8a.zip`; остальные архитектуры отклоняются.
|
||||||
|
|
||||||
CLI скачивает официальный архив и файл `.dgst`, проверяет SHA-256, ограничения
|
CLI скачивает официальный архив и файл `.dgst`, сверяет SHA-256 и фактическую
|
||||||
размера и фактическую версию бинарника, а затем атомарно заменяет назначение.
|
версию бинарника, а затем атомарно заменяет назначение. При любой ошибке старый
|
||||||
Корректная уже установленная версия даёт no-op без сети. Другой файл или симлинк
|
файл остаётся на месте. Если нужная версия уже установлена, сеть не нужна и
|
||||||
не заменяется без `--replace`; каталог не заменяется никогда.
|
ничего не меняется. Другой файл или симлинк не заменяется без `--replace`
|
||||||
|
(симлинк заменяется сам, файл по ссылке не трогается); каталог не заменяется
|
||||||
|
никогда. Если после установки `PATH` находит другой `xray`, `install` сообщает об
|
||||||
|
ошибке и подсказывает команду.
|
||||||
|
|
||||||
|
Устанавливать можно и при работающем Xray: он продолжит работу со старым файлом,
|
||||||
|
а `install` напомнит перезапустить `start`.
|
||||||
|
|
||||||
`start` использует первый `xray` из `PATH` и принимает только версию 26.3.27.
|
`start` использует первый `xray` из `PATH` и принимает только версию 26.3.27.
|
||||||
Если `PATH` выбирает другой бинарник, команда сообщает об ошибке. Во время
|
|
||||||
живого или неподтверждённого управляемого процесса установка запрещена.
|
|
||||||
|
|
||||||
### Файлы и безопасность
|
### Файлы и безопасность
|
||||||
|
|
||||||
@@ -385,6 +398,21 @@ CLI скачивает официальный архив и файл `.dgst`, п
|
|||||||
| `$XDG_STATE_HOME/proxy-client/` или `~/.local/state/proxy-client/` | Состояние и сгенерированный JSON |
|
| `$XDG_STATE_HOME/proxy-client/` или `~/.local/state/proxy-client/` | Состояние и сгенерированный JSON |
|
||||||
|
|
||||||
Сводка скрывает UUID и ключи, а для XHTTP показывает только длину `path`.
|
Сводка скрывает UUID и ключи, а для XHTTP показывает только длину `path`.
|
||||||
Служебный каталог закрыт для других пользователей и очищается после штатной
|
Если файл профиля доступен другим пользователям, `start` не запустится и
|
||||||
остановки Xray. Сбой определения внешнего IP показывается как предупреждение и
|
покажет готовую команду `chmod 600` для конечного файла (у симлинка — для файла,
|
||||||
не останавливает уже готовый процесс.
|
на который он указывает). Служебный каталог закрыт для других пользователей
|
||||||
|
(`700`, файлы в нём — `600`) и очищается после остановки Xray. Сбой
|
||||||
|
определения внешнего IP показывается как предупреждение и не останавливает уже
|
||||||
|
готовый процесс.
|
||||||
|
|
||||||
|
## Изменение кода
|
||||||
|
|
||||||
|
Перед правкой proxy-client прочитайте
|
||||||
|
[спецификацию](../docs/specs/2026-07-15-proxy-client-design.md): там сказано,
|
||||||
|
от чего клиент защищается, от чего нет и почему, и где живут тесты.
|
||||||
|
|
||||||
|
Все тесты пакета (из корня dotfiles):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
tests/test_proxy_client_full.sh
|
||||||
|
```
|
||||||
+33
-156
@@ -1,8 +1,9 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
# Ставит proxy-client в домашний каталог: три ссылки на этот каталог и
|
||||||
|
# (по умолчанию) блок в ~/.bashrc. Повторный запуск ничего не меняет.
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
PACKAGE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
|
PACKAGE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
|
||||||
STOW_BIN="${STOW_BIN:-stow}"
|
|
||||||
SETUP_SHELL=1
|
SETUP_SHELL=1
|
||||||
|
|
||||||
usage() {
|
usage() {
|
||||||
@@ -15,9 +16,7 @@ EOF
|
|||||||
|
|
||||||
while [ "$#" -gt 0 ]; do
|
while [ "$#" -gt 0 ]; do
|
||||||
case "$1" in
|
case "$1" in
|
||||||
--no-shell-setup)
|
--no-shell-setup) SETUP_SHELL=0 ;;
|
||||||
SETUP_SHELL=0
|
|
||||||
;;
|
|
||||||
-h|--help)
|
-h|--help)
|
||||||
usage
|
usage
|
||||||
exit 0
|
exit 0
|
||||||
@@ -31,180 +30,58 @@ while [ "$#" -gt 0 ]; do
|
|||||||
shift
|
shift
|
||||||
done
|
done
|
||||||
|
|
||||||
SOURCES=(
|
# Машинные config и default.url сюда не входят: они живут только в $HOME.
|
||||||
"$PACKAGE_DIR/.local/bin/proxy-client"
|
LINKS=(
|
||||||
"$PACKAGE_DIR/.config/proxy-client/shell.bash"
|
.local/bin/proxy-client
|
||||||
"$PACKAGE_DIR/.config/proxy-client/examples"
|
.config/proxy-client/shell.bash
|
||||||
)
|
.config/proxy-client/examples
|
||||||
TARGETS=(
|
|
||||||
"$HOME/.local/bin/proxy-client"
|
|
||||||
"$HOME/.config/proxy-client/shell.bash"
|
|
||||||
"$HOME/.config/proxy-client/examples"
|
|
||||||
)
|
|
||||||
STOW_IGNORE_PATTERNS=(
|
|
||||||
'^\.local/bin/proxy-client$'
|
|
||||||
'^\.config/proxy-client/shell\.bash$'
|
|
||||||
'^\.config/proxy-client/examples$'
|
|
||||||
)
|
)
|
||||||
|
|
||||||
source_errors=()
|
# Существующая ссылка на этот же файл пакета — не конфликт, всё остальное
|
||||||
[ -x "${SOURCES[0]}" ] || source_errors+=("${SOURCES[0]}")
|
# (включая битую ссылку) не трогаем.
|
||||||
[ -f "${SOURCES[1]}" ] || source_errors+=("${SOURCES[1]}")
|
|
||||||
[ -d "${SOURCES[2]}" ] || source_errors+=("${SOURCES[2]}")
|
|
||||||
if [ "${#source_errors[@]}" -gt 0 ]; then
|
|
||||||
echo "Ошибка: отсутствуют обязательные файлы исходного пакета:" >&2
|
|
||||||
printf ' %s\n' "${source_errors[@]}" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
"${SOURCES[0]}" --help >/dev/null
|
|
||||||
bash -n "${SOURCES[1]}"
|
|
||||||
|
|
||||||
SHELL_BLOCK_START='# >>> proxy-client >>>'
|
|
||||||
SHELL_BLOCK_END='# <<< proxy-client <<<'
|
|
||||||
SHELL_SETUP_NEEDED=0
|
|
||||||
BASH_CUSTOM_HAS_MODULE=0
|
|
||||||
SHELL_PATH_NEEDED=1
|
|
||||||
SHELL_SOURCE_NEEDED=1
|
|
||||||
SHELL_BLOCK_CONFLICT=0
|
|
||||||
if [ "$SETUP_SHELL" -eq 1 ]; then
|
|
||||||
if [ -f "$HOME/.bash_custom" ] \
|
|
||||||
&& grep -Fq 'proxy-client/shell.bash' "$HOME/.bash_custom"; then
|
|
||||||
BASH_CUSTOM_HAS_MODULE=1
|
|
||||||
SHELL_SOURCE_NEEDED=0
|
|
||||||
fi
|
|
||||||
|
|
||||||
bashrc_start_count=0
|
|
||||||
bashrc_end_count=0
|
|
||||||
if [ -f "$HOME/.bashrc" ]; then
|
|
||||||
bashrc_start_count="$(
|
|
||||||
grep -Fxc "$SHELL_BLOCK_START" "$HOME/.bashrc" || true
|
|
||||||
)"
|
|
||||||
bashrc_end_count="$(
|
|
||||||
grep -Fxc "$SHELL_BLOCK_END" "$HOME/.bashrc" || true
|
|
||||||
)"
|
|
||||||
if grep -Fq 'export PATH="$HOME/.local/bin:$PATH"' "$HOME/.bashrc"; then
|
|
||||||
SHELL_PATH_NEEDED=0
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ "$bashrc_start_count" -eq 0 ] && [ "$bashrc_end_count" -eq 0 ]; then
|
|
||||||
if [ "$SHELL_PATH_NEEDED" -eq 1 ] \
|
|
||||||
|| [ "$SHELL_SOURCE_NEEDED" -eq 1 ]; then
|
|
||||||
SHELL_SETUP_NEEDED=1
|
|
||||||
fi
|
|
||||||
elif [ "$bashrc_start_count" -eq 1 ] \
|
|
||||||
&& [ "$bashrc_end_count" -eq 1 ] \
|
|
||||||
&& [ "$SHELL_PATH_NEEDED" -eq 0 ] \
|
|
||||||
&& { [ "$BASH_CUSTOM_HAS_MODULE" -eq 1 ] \
|
|
||||||
|| grep -Fq 'proxy-client/shell.bash' "$HOME/.bashrc"; }; then
|
|
||||||
:
|
|
||||||
else
|
|
||||||
SHELL_BLOCK_CONFLICT=1
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
conflicts=()
|
conflicts=()
|
||||||
missing_targets=0
|
for link in "${LINKS[@]}"; do
|
||||||
stow_ignore_args=()
|
target="$HOME/$link"
|
||||||
PARENT_DIRS=(
|
|
||||||
"$HOME/.local"
|
|
||||||
"$HOME/.local/bin"
|
|
||||||
"$HOME/.config"
|
|
||||||
"$HOME/.config/proxy-client"
|
|
||||||
)
|
|
||||||
for parent_dir in "${PARENT_DIRS[@]}"; do
|
|
||||||
if { [ -e "$parent_dir" ] || [ -L "$parent_dir" ]; } \
|
|
||||||
&& { [ ! -d "$parent_dir" ] || [ -L "$parent_dir" ]; }; then
|
|
||||||
conflicts+=("$parent_dir")
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
for index in "${!TARGETS[@]}"; do
|
|
||||||
target="${TARGETS[$index]}"
|
|
||||||
source_path="${SOURCES[$index]}"
|
|
||||||
if [ -e "$target" ] || [ -L "$target" ]; then
|
if [ -e "$target" ] || [ -L "$target" ]; then
|
||||||
if [ ! -L "$target" ] \
|
if [ "$(readlink -f "$target" 2>/dev/null)" != "$PACKAGE_DIR/$link" ]; then
|
||||||
|| [ "$(readlink -f "$target" 2>/dev/null || true)" != "$source_path" ]; then
|
|
||||||
conflicts+=("$target")
|
conflicts+=("$target")
|
||||||
else
|
|
||||||
stow_ignore_args+=("--ignore=${STOW_IGNORE_PATTERNS[$index]}")
|
|
||||||
fi
|
fi
|
||||||
else
|
|
||||||
missing_targets=$((missing_targets + 1))
|
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
if [ "$SHELL_SETUP_NEEDED" -eq 1 ] \
|
|
||||||
&& { [ -e "$HOME/.bashrc" ] || [ -L "$HOME/.bashrc" ]; } \
|
|
||||||
&& [ ! -f "$HOME/.bashrc" ]; then
|
|
||||||
conflicts+=("$HOME/.bashrc")
|
|
||||||
fi
|
|
||||||
if [ "$SHELL_BLOCK_CONFLICT" -eq 1 ]; then
|
|
||||||
conflicts+=("$HOME/.bashrc")
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ "${#conflicts[@]}" -gt 0 ]; then
|
if [ "${#conflicts[@]}" -gt 0 ]; then
|
||||||
echo "Ошибка: найдены конфликтующие пути:" >&2
|
echo "Ошибка: пути уже заняты, установка не выполнена:" >&2
|
||||||
printf ' %s\n' "${conflicts[@]}" >&2
|
printf ' %s\n' "${conflicts[@]}" >&2
|
||||||
echo "Разберите их вручную и повторите установку." >&2
|
echo "Удалите или переименуйте их и повторите установку." >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
mkdir -p "$HOME/.local/bin" "$HOME/.config/proxy-client"
|
mkdir -p "$HOME/.local/bin" "$HOME/.config/proxy-client"
|
||||||
|
for link in "${LINKS[@]}"; do
|
||||||
|
[ -e "$HOME/$link" ] || ln -s "$PACKAGE_DIR/$link" "$HOME/$link"
|
||||||
|
done
|
||||||
|
|
||||||
if command -v "$STOW_BIN" >/dev/null 2>&1; then
|
SHELL_BLOCK_START='# >>> proxy-client >>>'
|
||||||
echo "Установка с помощью Stow."
|
if [ "$SETUP_SHELL" -eq 0 ]; then
|
||||||
if [ "$missing_targets" -gt 0 ]; then
|
echo "Настройка Bash пропущена (--no-shell-setup)."
|
||||||
"$STOW_BIN" --dir="$(dirname "$PACKAGE_DIR")" --target="$HOME" \
|
# Команда для пользователя: $HOME и $PATH раскроются в его сессии.
|
||||||
"${stow_ignore_args[@]}" \
|
# shellcheck disable=SC2016
|
||||||
"$(basename "$PACKAGE_DIR")"
|
RELOAD_COMMAND='export PATH="$HOME/.local/bin:$PATH"; source ~/.config/proxy-client/shell.bash'
|
||||||
fi
|
|
||||||
else
|
else
|
||||||
echo "Установка без Stow: создаю символические ссылки напрямую."
|
if ! grep -Fqx "$SHELL_BLOCK_START" "$HOME/.bashrc" 2>/dev/null; then
|
||||||
for index in "${!TARGETS[@]}"; do
|
cat >> "$HOME/.bashrc" <<EOF
|
||||||
target="${TARGETS[$index]}"
|
|
||||||
source_path="${SOURCES[$index]}"
|
|
||||||
if [ ! -L "$target" ]; then
|
|
||||||
ln -s "$source_path" "$target"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
fi
|
|
||||||
|
|
||||||
"$HOME/.local/bin/proxy-client" --help >/dev/null
|
$SHELL_BLOCK_START
|
||||||
bash -n "$HOME/.config/proxy-client/shell.bash"
|
case ":\$PATH:" in
|
||||||
|
*":\$HOME/.local/bin:"*) ;;
|
||||||
if [ "$SHELL_SETUP_NEEDED" -eq 1 ]; then
|
*) export PATH="\$HOME/.local/bin:\$PATH" ;;
|
||||||
cat >> "$HOME/.bashrc" <<'EOF'
|
|
||||||
|
|
||||||
# >>> proxy-client >>>
|
|
||||||
EOF
|
|
||||||
if [ "$SHELL_PATH_NEEDED" -eq 1 ]; then
|
|
||||||
cat >> "$HOME/.bashrc" <<'EOF'
|
|
||||||
case ":$PATH:" in
|
|
||||||
*":$HOME/.local/bin:"*) ;;
|
|
||||||
*)
|
|
||||||
export PATH="$HOME/.local/bin:$PATH"
|
|
||||||
;;
|
|
||||||
esac
|
esac
|
||||||
EOF
|
[ -f "\$HOME/.config/proxy-client/shell.bash" ] && source "\$HOME/.config/proxy-client/shell.bash"
|
||||||
fi
|
|
||||||
if [ "$SHELL_SOURCE_NEEDED" -eq 1 ]; then
|
|
||||||
cat >> "$HOME/.bashrc" <<'EOF'
|
|
||||||
[ -f "$HOME/.config/proxy-client/shell.bash" ] && source "$HOME/.config/proxy-client/shell.bash"
|
|
||||||
EOF
|
|
||||||
fi
|
|
||||||
cat >> "$HOME/.bashrc" <<'EOF'
|
|
||||||
# <<< proxy-client <<<
|
# <<< proxy-client <<<
|
||||||
EOF
|
EOF
|
||||||
elif [ "$SETUP_SHELL" -eq 0 ]; then
|
echo "В ~/.bashrc добавлен блок proxy-client."
|
||||||
echo "Настройка Bash пропущена (--no-shell-setup)."
|
fi
|
||||||
fi
|
|
||||||
|
|
||||||
if [ "$SETUP_SHELL" -eq 1 ]; then
|
|
||||||
RELOAD_COMMAND='source ~/.bashrc'
|
RELOAD_COMMAND='source ~/.bashrc'
|
||||||
else
|
|
||||||
RELOAD_COMMAND='export PATH="$HOME/.local/bin:$PATH"; source ~/.config/proxy-client/shell.bash'
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
cat <<EOF
|
cat <<EOF
|
||||||
|
|||||||
+417
-1158
File diff suppressed because it is too large.
Load diff
@@ -1,19 +1,17 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
# Все тесты proxy-client: Python-тесты (текущий python3 и, если есть uv,
|
||||||
|
# Python 3.8 — минимальная поддерживаемая версия) и shell-тесты.
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
cd "$(dirname "${BASH_SOURCE[0]}")/.."
|
||||||
cd "$REPO_ROOT"
|
|
||||||
|
|
||||||
if ! command -v uv >/dev/null 2>&1; then
|
|
||||||
echo "Ошибка: для полной проверки требуется uv" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
export PYTHONDONTWRITEBYTECODE=1
|
export PYTHONDONTWRITEBYTECODE=1
|
||||||
|
|
||||||
python3 -m unittest discover -s tests
|
python3 -m unittest discover -s tests -p 'test_proxy_*.py'
|
||||||
|
if command -v uv >/dev/null 2>&1; then
|
||||||
|
uv run --python 3.8 --no-project \
|
||||||
|
python -m unittest discover -s tests -p 'test_proxy_*.py'
|
||||||
|
else
|
||||||
|
echo "Предупреждение: uv не найден, прогон под Python 3.8 пропущен" >&2
|
||||||
|
fi
|
||||||
bash tests/test_proxy_launchers.sh
|
bash tests/test_proxy_launchers.sh
|
||||||
bash tests/test_chatgpt_proxy_launcher.sh
|
|
||||||
bash tests/test_proxy_client_install.sh
|
bash tests/test_proxy_client_install.sh
|
||||||
bash tests/test_install.sh
|
|
||||||
uv run --python 3.8 --no-project python -m unittest discover -s tests
|
|
||||||
+143
-367
@@ -1,4 +1,5 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
# Тесты пакетного установщика proxy-client/install.sh во временном HOME.
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
@@ -10,393 +11,168 @@ fail() {
|
|||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ok() {
|
||||||
|
echo "ok - $*"
|
||||||
|
}
|
||||||
|
|
||||||
assert_link_points_to() {
|
assert_link_points_to() {
|
||||||
local link_path="$1" expected_path="$2" actual_path
|
local link_path="$1" expected_path="$2"
|
||||||
[ -L "$link_path" ] || fail "Expected symlink: $link_path"
|
[ -L "$link_path" ] || fail "нет ссылки $link_path"
|
||||||
actual_path="$(readlink -f "$link_path")"
|
[ "$(readlink -f "$link_path")" = "$expected_path" ] \
|
||||||
[ "$actual_path" = "$expected_path" ] \
|
|| fail "$link_path указывает не на $expected_path"
|
||||||
|| fail "$link_path points to $actual_path, expected $expected_path"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
assert_not_exists() {
|
assert_not_exists() {
|
||||||
local path="$1"
|
[ ! -e "$1" ] && [ ! -L "$1" ] || fail "путь не должен существовать: $1"
|
||||||
[ ! -e "$path" ] && [ ! -L "$path" ] || fail "Path should not exist: $path"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setup_package() {
|
assert_package_links() {
|
||||||
local fixture_root="$1" package_dir="$fixture_root/source with spaces/proxy-client"
|
local package_dir="$1" home_dir="$2" link
|
||||||
mkdir -p \
|
for link in .local/bin/proxy-client .config/proxy-client/shell.bash \
|
||||||
"$package_dir/.local/bin" \
|
.config/proxy-client/examples; do
|
||||||
"$package_dir/.config/proxy-client"
|
assert_link_points_to "$home_dir/$link" "$package_dir/$link"
|
||||||
cp -a \
|
|
||||||
"$REPO_ROOT/proxy-client/.local/bin/proxy-client" \
|
|
||||||
"$package_dir/.local/bin/proxy-client"
|
|
||||||
cp -a \
|
|
||||||
"$REPO_ROOT/proxy-client/.config/proxy-client/shell.bash" \
|
|
||||||
"$REPO_ROOT/proxy-client/.config/proxy-client/examples" \
|
|
||||||
"$package_dir/.config/proxy-client/"
|
|
||||||
cp -a \
|
|
||||||
"$REPO_ROOT/proxy-client/.stow-local-ignore" \
|
|
||||||
"$REPO_ROOT/proxy-client/README.md" \
|
|
||||||
"$REPO_ROOT/proxy-client/install.sh" \
|
|
||||||
"$package_dir/"
|
|
||||||
printf '%s\n' 'MACHINE_CONFIG_SENTINEL' > "$package_dir/.config/proxy-client/config"
|
|
||||||
printf '%s\n' 'PRIVATE_PROFILE_SENTINEL' > "$package_dir/.config/proxy-client/default.url"
|
|
||||||
printf '%s\n' "$package_dir"
|
|
||||||
}
|
|
||||||
|
|
||||||
run_package_install() {
|
|
||||||
local package_dir="$1" home_dir="$2" stow_bin="$3"
|
|
||||||
shift 3
|
|
||||||
(
|
|
||||||
cd "$TEST_TMP_ROOT"
|
|
||||||
HOME="$home_dir" STOW_BIN="$stow_bin" \
|
|
||||||
"$package_dir/install.sh" "$@"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
test_installs_same_public_artifacts_without_stow() {
|
|
||||||
local fixture_root package_dir home_dir output
|
|
||||||
fixture_root="$(mktemp -d "$TEST_TMP_ROOT/fixture.XXXXXX")"
|
|
||||||
package_dir="$(setup_package "$fixture_root")"
|
|
||||||
home_dir="$fixture_root/home"
|
|
||||||
mkdir -p "$home_dir"
|
|
||||||
|
|
||||||
output="$(
|
|
||||||
run_package_install "$package_dir" "$home_dir" missing-stow --no-shell-setup
|
|
||||||
)"
|
|
||||||
|
|
||||||
printf '%s' "$output" | grep -Fq 'без Stow' \
|
|
||||||
|| fail "Expected direct-symlink method in output"
|
|
||||||
printf '%s' "$output" | grep -Fq \
|
|
||||||
'export PATH="$HOME/.local/bin:$PATH"; source ~/.config/proxy-client/shell.bash' \
|
|
||||||
|| fail "Expected complete current-session hint with --no-shell-setup"
|
|
||||||
[ -d "$home_dir/.local/bin" ] && [ ! -L "$home_dir/.local/bin" ] \
|
|
||||||
|| fail "Expected ~/.local/bin to be a real directory"
|
|
||||||
[ -d "$home_dir/.config/proxy-client" ] \
|
|
||||||
&& [ ! -L "$home_dir/.config/proxy-client" ] \
|
|
||||||
|| fail "Expected proxy-client config home to be a real directory"
|
|
||||||
assert_link_points_to \
|
|
||||||
"$home_dir/.local/bin/proxy-client" \
|
|
||||||
"$package_dir/.local/bin/proxy-client"
|
|
||||||
assert_link_points_to \
|
|
||||||
"$home_dir/.config/proxy-client/shell.bash" \
|
|
||||||
"$package_dir/.config/proxy-client/shell.bash"
|
|
||||||
assert_link_points_to \
|
|
||||||
"$home_dir/.config/proxy-client/examples" \
|
|
||||||
"$package_dir/.config/proxy-client/examples"
|
|
||||||
assert_not_exists "$home_dir/.config/proxy-client/config"
|
|
||||||
assert_not_exists "$home_dir/.config/proxy-client/default.url"
|
|
||||||
assert_not_exists "$home_dir/README.md"
|
|
||||||
assert_not_exists "$home_dir/install.sh"
|
|
||||||
assert_not_exists "$home_dir/.bashrc"
|
|
||||||
}
|
|
||||||
|
|
||||||
test_configures_bash_once_by_default() {
|
|
||||||
local fixture_root package_dir home_dir output marker_count path_count source_count
|
|
||||||
fixture_root="$(mktemp -d "$TEST_TMP_ROOT/fixture.XXXXXX")"
|
|
||||||
package_dir="$(setup_package "$fixture_root")"
|
|
||||||
home_dir="$fixture_root/home"
|
|
||||||
mkdir -p "$home_dir"
|
|
||||||
|
|
||||||
run_package_install "$package_dir" "$home_dir" missing-stow >/dev/null
|
|
||||||
output="$(run_package_install "$package_dir" "$home_dir" missing-stow)"
|
|
||||||
|
|
||||||
marker_count="$(grep -Fxc '# >>> proxy-client >>>' "$home_dir/.bashrc")"
|
|
||||||
path_count="$(grep -Fc 'export PATH="$HOME/.local/bin:$PATH"' "$home_dir/.bashrc")"
|
|
||||||
source_count="$(
|
|
||||||
grep -Fc \
|
|
||||||
'[ -f "$HOME/.config/proxy-client/shell.bash" ] && source "$HOME/.config/proxy-client/shell.bash"' \
|
|
||||||
"$home_dir/.bashrc"
|
|
||||||
)"
|
|
||||||
[ "$marker_count" -eq 1 ] || fail "Expected one managed Bash block"
|
|
||||||
[ "$path_count" -eq 1 ] || fail "Expected one ~/.local/bin PATH line"
|
|
||||||
[ "$source_count" -eq 1 ] || fail "Expected one shell module source line"
|
|
||||||
printf '%s' "$output" | grep -Fq 'source ~/.bashrc' \
|
|
||||||
|| fail "Expected current-session reload hint"
|
|
||||||
}
|
|
||||||
|
|
||||||
test_installs_same_public_artifacts_with_stow() {
|
|
||||||
local fixture_root package_dir home_dir output
|
|
||||||
fixture_root="$(mktemp -d "$TEST_TMP_ROOT/fixture.XXXXXX")"
|
|
||||||
package_dir="$(setup_package "$fixture_root")"
|
|
||||||
home_dir="$fixture_root/home"
|
|
||||||
mkdir -p "$home_dir"
|
|
||||||
|
|
||||||
output="$(run_package_install "$package_dir" "$home_dir" stow --no-shell-setup)"
|
|
||||||
|
|
||||||
printf '%s' "$output" | grep -Fq 'с помощью Stow' \
|
|
||||||
|| fail "Expected Stow method in output"
|
|
||||||
[ -d "$home_dir/.local/bin" ] && [ ! -L "$home_dir/.local/bin" ] \
|
|
||||||
|| fail "Expected ~/.local/bin to be a real directory"
|
|
||||||
[ -d "$home_dir/.config/proxy-client" ] \
|
|
||||||
&& [ ! -L "$home_dir/.config/proxy-client" ] \
|
|
||||||
|| fail "Expected proxy-client config home to be a real directory"
|
|
||||||
assert_link_points_to \
|
|
||||||
"$home_dir/.local/bin/proxy-client" \
|
|
||||||
"$package_dir/.local/bin/proxy-client"
|
|
||||||
assert_link_points_to \
|
|
||||||
"$home_dir/.config/proxy-client/shell.bash" \
|
|
||||||
"$package_dir/.config/proxy-client/shell.bash"
|
|
||||||
assert_link_points_to \
|
|
||||||
"$home_dir/.config/proxy-client/examples" \
|
|
||||||
"$package_dir/.config/proxy-client/examples"
|
|
||||||
assert_not_exists "$home_dir/.config/proxy-client/config"
|
|
||||||
assert_not_exists "$home_dir/.config/proxy-client/default.url"
|
|
||||||
assert_not_exists "$home_dir/README.md"
|
|
||||||
assert_not_exists "$home_dir/install.sh"
|
|
||||||
}
|
|
||||||
|
|
||||||
test_rejects_linked_parent_directories_before_changes() {
|
|
||||||
local fixture_root package_dir home_dir external_local external_config output
|
|
||||||
fixture_root="$(mktemp -d "$TEST_TMP_ROOT/fixture.XXXXXX")"
|
|
||||||
package_dir="$(setup_package "$fixture_root")"
|
|
||||||
home_dir="$fixture_root/home"
|
|
||||||
external_local="$fixture_root/external-local"
|
|
||||||
external_config="$fixture_root/external-config"
|
|
||||||
mkdir -p "$home_dir" "$external_local" "$external_config"
|
|
||||||
ln -s "$external_local" "$home_dir/.local"
|
|
||||||
ln -s "$external_config" "$home_dir/.config"
|
|
||||||
|
|
||||||
if output="$(
|
|
||||||
run_package_install "$package_dir" "$home_dir" missing-stow 2>&1
|
|
||||||
)"; then
|
|
||||||
fail "Expected linked parent directories to be rejected"
|
|
||||||
fi
|
|
||||||
|
|
||||||
printf '%s' "$output" | grep -Fq "$home_dir/.local" \
|
|
||||||
|| fail "Expected ~/.local conflict in output"
|
|
||||||
printf '%s' "$output" | grep -Fq "$home_dir/.config" \
|
|
||||||
|| fail "Expected ~/.config conflict in output"
|
|
||||||
assert_not_exists "$external_local/bin"
|
|
||||||
assert_not_exists "$external_config/proxy-client"
|
|
||||||
assert_not_exists "$home_dir/.bashrc"
|
|
||||||
}
|
|
||||||
|
|
||||||
test_reports_all_target_conflicts_without_partial_install() {
|
|
||||||
local fixture_root package_dir home_dir other_target output
|
|
||||||
fixture_root="$(mktemp -d "$TEST_TMP_ROOT/fixture.XXXXXX")"
|
|
||||||
package_dir="$(setup_package "$fixture_root")"
|
|
||||||
home_dir="$fixture_root/home"
|
|
||||||
other_target="$fixture_root/other-target"
|
|
||||||
mkdir -p "$home_dir/.local/bin" "$home_dir/.config/proxy-client/shell.bash"
|
|
||||||
printf '%s\n' 'KEEP_CLI' > "$home_dir/.local/bin/proxy-client"
|
|
||||||
printf '%s\n' 'KEEP_TARGET' > "$other_target"
|
|
||||||
ln -s "$other_target" "$home_dir/.config/proxy-client/examples"
|
|
||||||
|
|
||||||
if output="$(
|
|
||||||
run_package_install "$package_dir" "$home_dir" missing-stow 2>&1
|
|
||||||
)"; then
|
|
||||||
fail "Expected all target conflicts to be rejected"
|
|
||||||
fi
|
|
||||||
|
|
||||||
printf '%s' "$output" | grep -Fq "$home_dir/.local/bin/proxy-client" \
|
|
||||||
|| fail "Expected CLI conflict in output"
|
|
||||||
printf '%s' "$output" | grep -Fq "$home_dir/.config/proxy-client/shell.bash" \
|
|
||||||
|| fail "Expected shell module conflict in output"
|
|
||||||
printf '%s' "$output" | grep -Fq "$home_dir/.config/proxy-client/examples" \
|
|
||||||
|| fail "Expected examples conflict in output"
|
|
||||||
grep -Fq 'KEEP_CLI' "$home_dir/.local/bin/proxy-client" \
|
|
||||||
|| fail "Expected conflicting CLI file to stay unchanged"
|
|
||||||
[ -d "$home_dir/.config/proxy-client/shell.bash" ] \
|
|
||||||
|| fail "Expected conflicting shell directory to stay unchanged"
|
|
||||||
assert_link_points_to "$home_dir/.config/proxy-client/examples" "$other_target"
|
|
||||||
assert_not_exists "$home_dir/.bashrc"
|
|
||||||
}
|
|
||||||
|
|
||||||
test_rejects_broken_link_without_creating_other_targets() {
|
|
||||||
local fixture_root package_dir home_dir output
|
|
||||||
fixture_root="$(mktemp -d "$TEST_TMP_ROOT/fixture.XXXXXX")"
|
|
||||||
package_dir="$(setup_package "$fixture_root")"
|
|
||||||
home_dir="$fixture_root/home"
|
|
||||||
mkdir -p "$home_dir/.local/bin"
|
|
||||||
ln -s "$fixture_root/missing-cli" "$home_dir/.local/bin/proxy-client"
|
|
||||||
|
|
||||||
if output="$(
|
|
||||||
run_package_install "$package_dir" "$home_dir" missing-stow 2>&1
|
|
||||||
)"; then
|
|
||||||
fail "Expected broken target link to be rejected"
|
|
||||||
fi
|
|
||||||
|
|
||||||
printf '%s' "$output" | grep -Fq "$home_dir/.local/bin/proxy-client" \
|
|
||||||
|| fail "Expected broken CLI link in output"
|
|
||||||
[ -L "$home_dir/.local/bin/proxy-client" ] \
|
|
||||||
|| fail "Expected broken CLI link to stay unchanged"
|
|
||||||
assert_not_exists "$home_dir/.config"
|
|
||||||
assert_not_exists "$home_dir/.bashrc"
|
|
||||||
}
|
|
||||||
|
|
||||||
test_preserves_machine_files_with_both_methods() {
|
|
||||||
local stow_bin fixture_root package_dir home_dir
|
|
||||||
for stow_bin in missing-stow stow; do
|
|
||||||
fixture_root="$(mktemp -d "$TEST_TMP_ROOT/fixture.XXXXXX")"
|
|
||||||
package_dir="$(setup_package "$fixture_root")"
|
|
||||||
home_dir="$fixture_root/home"
|
|
||||||
mkdir -p "$home_dir/.config/proxy-client"
|
|
||||||
printf '%s\n' 'HOME_CONFIG_SENTINEL' \
|
|
||||||
> "$home_dir/.config/proxy-client/config"
|
|
||||||
printf '%s\n' 'HOME_PROFILE_SENTINEL' \
|
|
||||||
> "$home_dir/.config/proxy-client/default.url"
|
|
||||||
|
|
||||||
run_package_install "$package_dir" "$home_dir" "$stow_bin" \
|
|
||||||
--no-shell-setup >/dev/null
|
|
||||||
|
|
||||||
[ ! -L "$home_dir/.config/proxy-client/config" ] \
|
|
||||||
|| fail "Machine config must not become a symlink"
|
|
||||||
[ ! -L "$home_dir/.config/proxy-client/default.url" ] \
|
|
||||||
|| fail "Private profile must not become a symlink"
|
|
||||||
grep -Fq 'HOME_CONFIG_SENTINEL' "$home_dir/.config/proxy-client/config" \
|
|
||||||
|| fail "Machine config changed"
|
|
||||||
grep -Fq 'HOME_PROFILE_SENTINEL' "$home_dir/.config/proxy-client/default.url" \
|
|
||||||
|| fail "Private profile changed"
|
|
||||||
done
|
done
|
||||||
}
|
}
|
||||||
|
|
||||||
test_accepts_existing_links_when_method_changes() {
|
# Копия пакета в пути с пробелами и машинными файлами, которые установщик
|
||||||
local fixture_root package_dir home_dir
|
# не должен трогать. Печатает путь к пакету; HOME создаётся рядом.
|
||||||
fixture_root="$(mktemp -d "$TEST_TMP_ROOT/fixture.XXXXXX")"
|
new_fixture() {
|
||||||
package_dir="$(setup_package "$fixture_root")"
|
local fixture package_dir
|
||||||
home_dir="$fixture_root/home"
|
fixture="$(mktemp -d "$TEST_TMP_ROOT/fixture.XXXXXX")"
|
||||||
mkdir -p "$home_dir"
|
package_dir="$fixture/source with spaces/proxy-client"
|
||||||
|
mkdir -p "$package_dir" "$fixture/home"
|
||||||
run_package_install "$package_dir" "$home_dir" missing-stow \
|
cp -a "$REPO_ROOT/proxy-client/." "$package_dir/"
|
||||||
--no-shell-setup >/dev/null
|
printf 'MACHINE_CONFIG_SENTINEL\n' > "$package_dir/.config/proxy-client/config"
|
||||||
run_package_install "$package_dir" "$home_dir" stow \
|
printf 'PRIVATE_PROFILE_SENTINEL\n' > "$package_dir/.config/proxy-client/default.url"
|
||||||
--no-shell-setup >/dev/null
|
printf '%s\n' "$fixture"
|
||||||
|
|
||||||
assert_link_points_to \
|
|
||||||
"$home_dir/.local/bin/proxy-client" \
|
|
||||||
"$package_dir/.local/bin/proxy-client"
|
|
||||||
assert_link_points_to \
|
|
||||||
"$home_dir/.config/proxy-client/shell.bash" \
|
|
||||||
"$package_dir/.config/proxy-client/shell.bash"
|
|
||||||
assert_link_points_to \
|
|
||||||
"$home_dir/.config/proxy-client/examples" \
|
|
||||||
"$package_dir/.config/proxy-client/examples"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
test_stow_completes_partial_correct_installation() {
|
run_install() {
|
||||||
local fixture_root package_dir home_dir
|
local fixture="$1"
|
||||||
fixture_root="$(mktemp -d "$TEST_TMP_ROOT/fixture.XXXXXX")"
|
shift
|
||||||
package_dir="$(setup_package "$fixture_root")"
|
(cd "$TEST_TMP_ROOT" && HOME="$fixture/home" \
|
||||||
home_dir="$fixture_root/home"
|
"$fixture/source with spaces/proxy-client/install.sh" "$@")
|
||||||
mkdir -p "$home_dir"
|
|
||||||
|
|
||||||
run_package_install "$package_dir" "$home_dir" missing-stow \
|
|
||||||
--no-shell-setup >/dev/null
|
|
||||||
rm "$home_dir/.config/proxy-client/shell.bash"
|
|
||||||
|
|
||||||
run_package_install "$package_dir" "$home_dir" stow \
|
|
||||||
--no-shell-setup >/dev/null
|
|
||||||
|
|
||||||
assert_link_points_to \
|
|
||||||
"$home_dir/.local/bin/proxy-client" \
|
|
||||||
"$package_dir/.local/bin/proxy-client"
|
|
||||||
assert_link_points_to \
|
|
||||||
"$home_dir/.config/proxy-client/shell.bash" \
|
|
||||||
"$package_dir/.config/proxy-client/shell.bash"
|
|
||||||
assert_link_points_to \
|
|
||||||
"$home_dir/.config/proxy-client/examples" \
|
|
||||||
"$package_dir/.config/proxy-client/examples"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
test_does_not_duplicate_full_dotfiles_shell_setup() {
|
home_snapshot() {
|
||||||
local fixture_root package_dir home_dir source_count
|
find "$1" -printf '%p %y %l %s %T@\n' | sort
|
||||||
fixture_root="$(mktemp -d "$TEST_TMP_ROOT/fixture.XXXXXX")"
|
|
||||||
package_dir="$(setup_package "$fixture_root")"
|
|
||||||
home_dir="$fixture_root/home"
|
|
||||||
mkdir -p "$home_dir"
|
|
||||||
printf '%s\n' \
|
|
||||||
'[ -f ~/.config/proxy-client/shell.bash ] && source ~/.config/proxy-client/shell.bash' \
|
|
||||||
> "$home_dir/.bash_custom"
|
|
||||||
printf '%s\n' '[ -f ~/.bash_custom ] && source ~/.bash_custom' \
|
|
||||||
> "$home_dir/.bashrc"
|
|
||||||
|
|
||||||
run_package_install "$package_dir" "$home_dir" missing-stow >/dev/null
|
|
||||||
|
|
||||||
grep -Fq '# >>> proxy-client >>>' "$home_dir/.bashrc" \
|
|
||||||
|| fail "Expected managed PATH block with full dotfiles"
|
|
||||||
grep -Fq 'export PATH="$HOME/.local/bin:$PATH"' "$home_dir/.bashrc" \
|
|
||||||
|| fail "Expected ~/.local/bin setup with full dotfiles"
|
|
||||||
source_count="$(grep -Fc 'proxy-client/shell.bash' "$home_dir/.bashrc" || true)"
|
|
||||||
[ "$source_count" -eq 0 ] \
|
|
||||||
|| fail "Standalone block duplicated full dotfiles module source"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
test_rejects_damaged_managed_bash_block_before_changes() {
|
test_fresh_install() {
|
||||||
local fixture_root package_dir home_dir output
|
local fixture home_dir package_dir output
|
||||||
fixture_root="$(mktemp -d "$TEST_TMP_ROOT/fixture.XXXXXX")"
|
fixture="$(new_fixture)"
|
||||||
package_dir="$(setup_package "$fixture_root")"
|
home_dir="$fixture/home"
|
||||||
home_dir="$fixture_root/home"
|
package_dir="$fixture/source with spaces/proxy-client"
|
||||||
mkdir -p "$home_dir"
|
|
||||||
printf '%s\n' '# >>> proxy-client >>>' > "$home_dir/.bashrc"
|
|
||||||
|
|
||||||
if output="$(
|
run_install "$fixture" >/dev/null
|
||||||
run_package_install "$package_dir" "$home_dir" missing-stow 2>&1
|
|
||||||
)"; then
|
assert_package_links "$package_dir" "$home_dir"
|
||||||
fail "Expected damaged managed Bash block to be rejected"
|
[ -x "$home_dir/.local/bin/proxy-client" ] || fail "CLI не исполняемый"
|
||||||
|
assert_not_exists "$home_dir/.config/proxy-client/config"
|
||||||
|
assert_not_exists "$home_dir/.config/proxy-client/default.url"
|
||||||
|
output="$(
|
||||||
|
env -i HOME="$home_dir" PATH=/usr/bin:/bin TERM=dumb bash -ic '
|
||||||
|
command -v proxy-client && declare -F proxy_on
|
||||||
|
' 2>/dev/null
|
||||||
|
)"
|
||||||
|
[ "$output" = "$home_dir/.local/bin/proxy-client"$'\n'proxy_on ] \
|
||||||
|
|| fail "новый Bash не нашёл proxy-client и proxy_on: $output"
|
||||||
|
ok "свежая установка: три ссылки и рабочий блок в ~/.bashrc"
|
||||||
|
}
|
||||||
|
|
||||||
|
test_repeat_changes_nothing() {
|
||||||
|
local fixture home_dir before
|
||||||
|
fixture="$(new_fixture)"
|
||||||
|
home_dir="$fixture/home"
|
||||||
|
run_install "$fixture" >/dev/null
|
||||||
|
before="$(home_snapshot "$home_dir")"
|
||||||
|
|
||||||
|
run_install "$fixture" >/dev/null
|
||||||
|
|
||||||
|
[ "$(home_snapshot "$home_dir")" = "$before" ] \
|
||||||
|
|| fail "повторный запуск изменил HOME"
|
||||||
|
ok "повторный запуск ничего не меняет"
|
||||||
|
}
|
||||||
|
|
||||||
|
test_conflicts_block_whole_install() {
|
||||||
|
local fixture home_dir output
|
||||||
|
fixture="$(new_fixture)"
|
||||||
|
home_dir="$fixture/home"
|
||||||
|
mkdir -p "$home_dir/.local/bin" "$home_dir/.config/proxy-client"
|
||||||
|
printf 'KEEP_CLI\n' > "$home_dir/.local/bin/proxy-client"
|
||||||
|
ln -s "$fixture/missing" "$home_dir/.config/proxy-client/examples"
|
||||||
|
|
||||||
|
if output="$(run_install "$fixture" 2>&1)"; then
|
||||||
|
fail "установка прошла поверх чужих файлов"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
printf '%s' "$output" | grep -Fq "$home_dir/.bashrc" \
|
printf '%s' "$output" | grep -Fq "$home_dir/.local/bin/proxy-client" \
|
||||||
|| fail "Expected ~/.bashrc conflict in output"
|
|| fail "не назван конфликт CLI"
|
||||||
[ "$(wc -l < "$home_dir/.bashrc")" -eq 1 ] \
|
printf '%s' "$output" | grep -Fq "$home_dir/.config/proxy-client/examples" \
|
||||||
|| fail "Expected damaged Bash block to stay unchanged"
|
|| fail "не названа битая ссылка"
|
||||||
assert_not_exists "$home_dir/.local"
|
grep -Fqx KEEP_CLI "$home_dir/.local/bin/proxy-client" \
|
||||||
assert_not_exists "$home_dir/.config"
|
|| fail "чужой CLI изменён"
|
||||||
}
|
[ "$(readlink "$home_dir/.config/proxy-client/examples")" = "$fixture/missing" ] \
|
||||||
|
|| fail "битая ссылка изменена"
|
||||||
test_does_not_duplicate_existing_local_bin_path() {
|
assert_not_exists "$home_dir/.config/proxy-client/shell.bash"
|
||||||
local fixture_root package_dir home_dir path_count source_count
|
|
||||||
fixture_root="$(mktemp -d "$TEST_TMP_ROOT/fixture.XXXXXX")"
|
|
||||||
package_dir="$(setup_package "$fixture_root")"
|
|
||||||
home_dir="$fixture_root/home"
|
|
||||||
mkdir -p "$home_dir"
|
|
||||||
printf '%s\n' 'export PATH="$HOME/.local/bin:$PATH"' > "$home_dir/.bashrc"
|
|
||||||
|
|
||||||
run_package_install "$package_dir" "$home_dir" missing-stow >/dev/null
|
|
||||||
|
|
||||||
path_count="$(grep -Fc 'export PATH="$HOME/.local/bin:$PATH"' "$home_dir/.bashrc")"
|
|
||||||
source_count="$(grep -Fc 'proxy-client/shell.bash' "$home_dir/.bashrc")"
|
|
||||||
[ "$path_count" -eq 1 ] || fail "Existing ~/.local/bin PATH line was duplicated"
|
|
||||||
[ "$source_count" -eq 1 ] || fail "Expected shell module source in managed block"
|
|
||||||
}
|
|
||||||
|
|
||||||
test_rejects_incomplete_source_before_changes() {
|
|
||||||
local fixture_root package_dir home_dir output
|
|
||||||
fixture_root="$(mktemp -d "$TEST_TMP_ROOT/fixture.XXXXXX")"
|
|
||||||
package_dir="$(setup_package "$fixture_root")"
|
|
||||||
home_dir="$fixture_root/home"
|
|
||||||
mkdir -p "$home_dir"
|
|
||||||
rm "$package_dir/.config/proxy-client/shell.bash"
|
|
||||||
|
|
||||||
if output="$(
|
|
||||||
run_package_install "$package_dir" "$home_dir" missing-stow 2>&1
|
|
||||||
)"; then
|
|
||||||
fail "Expected incomplete source package to be rejected"
|
|
||||||
fi
|
|
||||||
|
|
||||||
printf '%s' "$output" | grep -Fq 'исходного пакета' \
|
|
||||||
|| fail "Expected source package error"
|
|
||||||
assert_not_exists "$home_dir/.local"
|
|
||||||
assert_not_exists "$home_dir/.config"
|
|
||||||
assert_not_exists "$home_dir/.bashrc"
|
assert_not_exists "$home_dir/.bashrc"
|
||||||
|
ok "все конфликты названы сразу, частичной установки нет"
|
||||||
}
|
}
|
||||||
|
|
||||||
main() {
|
test_keeps_machine_files() {
|
||||||
test_installs_same_public_artifacts_without_stow
|
local fixture home_dir
|
||||||
test_configures_bash_once_by_default
|
fixture="$(new_fixture)"
|
||||||
test_installs_same_public_artifacts_with_stow
|
home_dir="$fixture/home"
|
||||||
test_rejects_linked_parent_directories_before_changes
|
mkdir -p "$home_dir/.config/proxy-client"
|
||||||
test_reports_all_target_conflicts_without_partial_install
|
printf 'HOME_CONFIG\n' > "$home_dir/.config/proxy-client/config"
|
||||||
test_rejects_broken_link_without_creating_other_targets
|
printf 'HOME_PROFILE\n' > "$home_dir/.config/proxy-client/default.url"
|
||||||
test_preserves_machine_files_with_both_methods
|
|
||||||
test_accepts_existing_links_when_method_changes
|
run_install "$fixture" --no-shell-setup >/dev/null
|
||||||
test_stow_completes_partial_correct_installation
|
|
||||||
test_does_not_duplicate_full_dotfiles_shell_setup
|
[ ! -L "$home_dir/.config/proxy-client/config" ] || fail "config стал ссылкой"
|
||||||
test_rejects_damaged_managed_bash_block_before_changes
|
[ ! -L "$home_dir/.config/proxy-client/default.url" ] \
|
||||||
test_does_not_duplicate_existing_local_bin_path
|
|| fail "default.url стал ссылкой"
|
||||||
test_rejects_incomplete_source_before_changes
|
grep -Fqx HOME_CONFIG "$home_dir/.config/proxy-client/config" \
|
||||||
echo "All standalone proxy-client install tests passed"
|
|| fail "машинный config изменён"
|
||||||
|
grep -Fqx HOME_PROFILE "$home_dir/.config/proxy-client/default.url" \
|
||||||
|
|| fail "default.url изменён"
|
||||||
|
ok "машинные config и default.url не трогаются"
|
||||||
}
|
}
|
||||||
|
|
||||||
main "$@"
|
test_no_shell_setup() {
|
||||||
|
local fixture home_dir output
|
||||||
|
fixture="$(new_fixture)"
|
||||||
|
home_dir="$fixture/home"
|
||||||
|
|
||||||
|
output="$(run_install "$fixture" --no-shell-setup)"
|
||||||
|
|
||||||
|
assert_not_exists "$home_dir/.bashrc"
|
||||||
|
printf '%s' "$output" | grep -Fq 'proxy-client/shell.bash' \
|
||||||
|
|| fail "нет подсказки для текущей сессии"
|
||||||
|
ok "--no-shell-setup не трогает ~/.bashrc"
|
||||||
|
}
|
||||||
|
|
||||||
|
test_linked_parent_directories() {
|
||||||
|
local fixture home_dir package_dir
|
||||||
|
fixture="$(new_fixture)"
|
||||||
|
home_dir="$fixture/home"
|
||||||
|
package_dir="$fixture/source with spaces/proxy-client"
|
||||||
|
mkdir -p "$fixture/external-local" "$fixture/external-config"
|
||||||
|
ln -s "$fixture/external-local" "$home_dir/.local"
|
||||||
|
ln -s "$fixture/external-config" "$home_dir/.config"
|
||||||
|
|
||||||
|
run_install "$fixture" --no-shell-setup >/dev/null
|
||||||
|
|
||||||
|
assert_package_links "$package_dir" "$home_dir"
|
||||||
|
[ -L "$fixture/external-local/bin/proxy-client" ] \
|
||||||
|
|| fail "ссылка не попала в каталог, на который указывает ~/.local"
|
||||||
|
ok "каталоги ~/.local и ~/.config могут быть ссылками"
|
||||||
|
}
|
||||||
|
|
||||||
|
test_fresh_install
|
||||||
|
test_repeat_changes_nothing
|
||||||
|
test_conflicts_block_whole_install
|
||||||
|
test_keeps_machine_files
|
||||||
|
test_no_shell_setup
|
||||||
|
test_linked_parent_directories
|
||||||
|
echo "PASS: тесты установщика proxy-client"
|
||||||
+311
-1061
File diff suppressed because it is too large.
Load diff
+85
-131
@@ -1,12 +1,19 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
# Тесты Bash-функций proxy-client: вместо настоящего CLI — поддельный
|
||||||
|
# исполняемый файл в PROXY_CLIENT_BIN.
|
||||||
|
# Строчные proxy-переменные задаёт proxy_on, shellcheck этого не видит.
|
||||||
|
# shellcheck disable=SC2154
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
FUNCTIONS_FILE="$REPO_ROOT/bash/.bash_functions"
|
FUNCTIONS_FILE="$REPO_ROOT/bash/.bash_functions"
|
||||||
CUSTOM_FILE="$REPO_ROOT/bash/.bash_custom"
|
CUSTOM_FILE="$REPO_ROOT/bash/.bash_custom"
|
||||||
SHELL_MODULE="$REPO_ROOT/proxy-client/.config/proxy-client/shell.bash"
|
SHELL_MODULE="$REPO_ROOT/proxy-client/.config/proxy-client/shell.bash"
|
||||||
|
TEST_ROOT="$(mktemp -d /tmp/proxy-launchers-test.XXXXXX)"
|
||||||
|
trap 'rm -rf "$TEST_ROOT"' EXIT
|
||||||
|
|
||||||
pass_count=0
|
PROXY_KEYS=(HTTP_PROXY HTTPS_PROXY ALL_PROXY NO_PROXY
|
||||||
|
http_proxy https_proxy all_proxy no_proxy)
|
||||||
|
|
||||||
fail() {
|
fail() {
|
||||||
echo "FAIL: $*" >&2
|
echo "FAIL: $*" >&2
|
||||||
@@ -14,177 +21,124 @@ fail() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ok() {
|
ok() {
|
||||||
pass_count=$((pass_count + 1))
|
|
||||||
echo "ok - $*"
|
echo "ok - $*"
|
||||||
}
|
}
|
||||||
|
|
||||||
assert_eq() {
|
assert_eq() {
|
||||||
local expected="$1" actual="$2" message="$3"
|
[ "$1" = "$2" ] || fail "$3 (ожидалось '$1', получено '$2')"
|
||||||
[ "$expected" = "$actual" ] || fail "$message (ожидалось '$expected', получено '$actual')"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
valid_env() {
|
# Поддельный CLI: на "env" печатает $FAKE_ENV_OUTPUT, на остальное — свои
|
||||||
cat <<'EOF'
|
# аргументы по одному в строке; завершается с кодом $FAKE_RC.
|
||||||
HTTP_PROXY=http://proxy.test:1234
|
PROXY_CLIENT_BIN="$TEST_ROOT/proxy-client"
|
||||||
|
cat > "$PROXY_CLIENT_BIN" <<'EOF'
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
if [ "$1" = env ]; then
|
||||||
|
printf '%s\n' "$FAKE_ENV_OUTPUT"
|
||||||
|
else
|
||||||
|
printf '<%s>\n' "$@"
|
||||||
|
fi
|
||||||
|
exit "${FAKE_RC:-0}"
|
||||||
|
EOF
|
||||||
|
chmod +x "$PROXY_CLIENT_BIN"
|
||||||
|
export PROXY_CLIENT_BIN
|
||||||
|
|
||||||
|
VALID_ENV='HTTP_PROXY=http://proxy.test:1234
|
||||||
HTTPS_PROXY=http://proxy.test:1234
|
HTTPS_PROXY=http://proxy.test:1234
|
||||||
ALL_PROXY=socks5://proxy.test:1234
|
ALL_PROXY=socks5://proxy.test:1234
|
||||||
NO_PROXY=proxy.test,localhost,127.0.0.1,::1,[::1],.example.test
|
NO_PROXY=proxy.test,localhost,.example.test
|
||||||
http_proxy=http://proxy.test:1234
|
http_proxy=http://proxy.test:1234
|
||||||
https_proxy=http://proxy.test:1234
|
https_proxy=http://proxy.test:1234
|
||||||
all_proxy=socks5://proxy.test:1234
|
all_proxy=socks5://proxy.test:1234
|
||||||
no_proxy=proxy.test,localhost,127.0.0.1,::1,[::1],.example.test
|
no_proxy=proxy.test,localhost,.example.test'
|
||||||
EOF
|
|
||||||
}
|
|
||||||
|
|
||||||
assert_five_proxy_commands() {
|
# shellcheck source=/dev/null
|
||||||
local command_name
|
source "$SHELL_MODULE"
|
||||||
for command_name in proxy_on proxy_off pclaude pcodex myip; do
|
|
||||||
declare -F "$command_name" >/dev/null || fail "нет функции $command_name"
|
# proxy_on применяет все восемь переменных.
|
||||||
|
(
|
||||||
|
unset "${PROXY_KEYS[@]}"
|
||||||
|
FAKE_ENV_OUTPUT="$VALID_ENV" proxy_on >/dev/null
|
||||||
|
assert_eq http://proxy.test:1234 "$HTTP_PROXY" HTTP_PROXY
|
||||||
|
assert_eq http://proxy.test:1234 "$https_proxy" https_proxy
|
||||||
|
assert_eq socks5://proxy.test:1234 "$ALL_PROXY" ALL_PROXY
|
||||||
|
assert_eq proxy.test,localhost,.example.test "$no_proxy" no_proxy
|
||||||
|
for key in "${PROXY_KEYS[@]}"; do
|
||||||
|
[ -n "$(printenv "$key")" ] || fail "$key не экспортирован"
|
||||||
done
|
done
|
||||||
}
|
)
|
||||||
|
ok "proxy_on применяет восемь переменных"
|
||||||
|
|
||||||
# Самостоятельный модуль предоставляет весь публичный Bash-интерфейс.
|
# Ошибка CLI или непонятный вывод не меняют окружение ни на одну переменную.
|
||||||
(
|
for case_spec in "1|$VALID_ENV" "0|$VALID_ENV
|
||||||
source "$SHELL_MODULE"
|
EXTRA_PROXY=bad" "0|BROKEN-LINE"; do
|
||||||
assert_five_proxy_commands
|
|
||||||
) && ok "самостоятельное подключение предоставляет пять proxy-команд"
|
|
||||||
|
|
||||||
# Обычная .bash_custom независимо подключает общие функции и proxy-модуль.
|
|
||||||
(
|
|
||||||
fixture="$(mktemp -d /tmp/proxy-bash-custom-test.XXXXXX)"
|
|
||||||
mkdir -p "$fixture/.config/proxy-client"
|
|
||||||
ln -s "$CUSTOM_FILE" "$fixture/.bash_custom"
|
|
||||||
ln -s "$FUNCTIONS_FILE" "$fixture/.bash_functions"
|
|
||||||
ln -s "$SHELL_MODULE" "$fixture/.config/proxy-client/shell.bash"
|
|
||||||
output="$(
|
|
||||||
HOME="$fixture" bash --noprofile --norc -ic '
|
|
||||||
source "$HOME/.bash_custom"
|
|
||||||
for name in proxy_on proxy_off pclaude pcodex myip update_hproxy; do
|
|
||||||
declare -F "$name" >/dev/null || exit 1
|
|
||||||
done
|
|
||||||
printf ready
|
|
||||||
' 2>/dev/null
|
|
||||||
)"
|
|
||||||
assert_eq ready "$output" ".bash_custom не подключил оба независимых модуля"
|
|
||||||
) && ok "обычное подключение предоставляет proxy-команды и update_hproxy"
|
|
||||||
|
|
||||||
# proxy_on применяет восемь известных переменных и не запрашивает IP.
|
|
||||||
(
|
|
||||||
source "$SHELL_MODULE"
|
|
||||||
_proxy_cli() {
|
|
||||||
[ "$1" = env ] || return 99
|
|
||||||
valid_env
|
|
||||||
}
|
|
||||||
curl() { fail "proxy_on не должен вызывать curl"; }
|
|
||||||
unset HTTP_PROXY HTTPS_PROXY ALL_PROXY NO_PROXY \
|
|
||||||
http_proxy https_proxy all_proxy no_proxy || true
|
|
||||||
proxy_on >/dev/null
|
|
||||||
assert_eq "http://proxy.test:1234" "$HTTP_PROXY" "HTTP_PROXY"
|
|
||||||
assert_eq "$HTTP_PROXY" "$http_proxy" "http_proxy"
|
|
||||||
assert_eq "http://proxy.test:1234" "$HTTPS_PROXY" "HTTPS_PROXY"
|
|
||||||
assert_eq "$HTTPS_PROXY" "$https_proxy" "https_proxy"
|
|
||||||
assert_eq "socks5://proxy.test:1234" "$ALL_PROXY" "ALL_PROXY"
|
|
||||||
assert_eq "$ALL_PROXY" "$all_proxy" "all_proxy"
|
|
||||||
assert_eq "proxy.test,localhost,127.0.0.1,::1,[::1],.example.test" "$NO_PROXY" "NO_PROXY"
|
|
||||||
assert_eq "$NO_PROXY" "$no_proxy" "no_proxy"
|
|
||||||
) && ok "proxy_on применяет известные ключи без запроса IP"
|
|
||||||
|
|
||||||
# Неизвестная строка в машинном выводе оставляет прежнее окружение целиком.
|
|
||||||
unknown_output="$(valid_env; printf 'EXTRA_PROXY=bad\n')"
|
|
||||||
for bad_output in \
|
|
||||||
"$unknown_output" \
|
|
||||||
"BROKEN-LINE"; do
|
|
||||||
(
|
(
|
||||||
source "$SHELL_MODULE"
|
for key in "${PROXY_KEYS[@]}"; do export "$key=old"; done
|
||||||
export HTTP_PROXY=old-http HTTPS_PROXY=old-https ALL_PROXY=old-all NO_PROXY=old-no
|
if FAKE_RC="${case_spec%%|*}" FAKE_ENV_OUTPUT="${case_spec#*|}" \
|
||||||
export http_proxy=old-http-lower https_proxy=old-https-lower
|
proxy_on >/dev/null 2>&1; then
|
||||||
export all_proxy=old-all-lower no_proxy=old-no-lower
|
fail "proxy_on принял ошибочный ответ: ${case_spec#*|}"
|
||||||
_proxy_cli() { printf '%s\n' "$bad_output"; }
|
|
||||||
if proxy_on >/dev/null 2>&1; then
|
|
||||||
fail "proxy_on принял неверный вывод"
|
|
||||||
fi
|
fi
|
||||||
assert_eq old-http "$HTTP_PROXY" "HTTP_PROXY изменился при ошибке"
|
for key in "${PROXY_KEYS[@]}"; do
|
||||||
assert_eq old-https "$HTTPS_PROXY" "HTTPS_PROXY изменился при ошибке"
|
assert_eq old "${!key}" "$key изменился после ошибки"
|
||||||
assert_eq old-all "$ALL_PROXY" "ALL_PROXY изменился при ошибке"
|
done
|
||||||
assert_eq old-no "$NO_PROXY" "NO_PROXY изменился при ошибке"
|
)
|
||||||
assert_eq old-http-lower "$http_proxy" "http_proxy изменился при ошибке"
|
|
||||||
assert_eq old-https-lower "$https_proxy" "https_proxy изменился при ошибке"
|
|
||||||
assert_eq old-all-lower "$all_proxy" "all_proxy изменился при ошибке"
|
|
||||||
assert_eq old-no-lower "$no_proxy" "no_proxy изменился при ошибке"
|
|
||||||
) || fail "атомарность нарушена для неверного вывода"
|
|
||||||
done
|
done
|
||||||
ok "неизвестная env-строка отклоняется без частичного изменения окружения"
|
ok "неуспешный env не меняет окружение"
|
||||||
|
|
||||||
# Даже полный stdout не применяется, если CLI завершился с ошибкой.
|
# proxy_off очищает оба регистра.
|
||||||
(
|
(
|
||||||
source "$SHELL_MODULE"
|
for key in "${PROXY_KEYS[@]}"; do export "$key=x"; done
|
||||||
export HTTP_PROXY=old HTTPS_PROXY=old ALL_PROXY=old NO_PROXY=old
|
proxy_off >/dev/null
|
||||||
export http_proxy=old https_proxy=old all_proxy=old no_proxy=old
|
for key in "${PROXY_KEYS[@]}"; do
|
||||||
_proxy_cli() {
|
[ -z "${!key+set}" ] || fail "$key не очищен"
|
||||||
valid_env
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
if proxy_on >/dev/null 2>&1; then
|
|
||||||
fail "proxy_on принял вывод неуспешного CLI"
|
|
||||||
fi
|
|
||||||
for key in HTTP_PROXY HTTPS_PROXY ALL_PROXY NO_PROXY \
|
|
||||||
http_proxy https_proxy all_proxy no_proxy; do
|
|
||||||
assert_eq old "${!key}" "$key изменился после ошибки CLI"
|
|
||||||
done
|
done
|
||||||
) && ok "stdout неуспешного env не изменяет окружение"
|
)
|
||||||
|
ok "proxy_off очищает восемь переменных"
|
||||||
|
|
||||||
# Обёртки передают CLI команду, аргументы и код возврата без искажений.
|
# Обёртки передают CLI команду, аргументы и код возврата без искажений.
|
||||||
(
|
(
|
||||||
source "$SHELL_MODULE"
|
export FAKE_RC=42
|
||||||
_proxy_cli() {
|
|
||||||
printf '<%s>\n' "$@"
|
|
||||||
return 42
|
|
||||||
}
|
|
||||||
set +e
|
set +e
|
||||||
output="$(pclaude "a b" -- --flag)"
|
output="$(pclaude "a b" -- --flag)"
|
||||||
rc=$?
|
rc=$?
|
||||||
set -e
|
set -e
|
||||||
assert_eq 42 "$rc" "pclaude не сохранил код возврата"
|
assert_eq 42 "$rc" "pclaude не сохранил код возврата"
|
||||||
assert_eq $'<run>\n<-->\n<claude>\n<a b>\n<-->\n<--flag>' "$output" "pclaude исказил аргументы"
|
assert_eq $'<run>\n<-->\n<claude>\n<a b>\n<-->\n<--flag>' "$output" \
|
||||||
|
"pclaude исказил аргументы"
|
||||||
|
|
||||||
set +e
|
set +e
|
||||||
output="$(pcodex --search)"
|
output="$(pcodex --search)"
|
||||||
rc=$?
|
rc=$?
|
||||||
set -e
|
set -e
|
||||||
assert_eq 42 "$rc" "pcodex не сохранил код возврата"
|
assert_eq 42 "$rc" "pcodex не сохранил код возврата"
|
||||||
assert_eq $'<run>\n<-->\n<codex>\n<--search>' "$output" "pcodex исказил аргументы"
|
assert_eq $'<run>\n<-->\n<codex>\n<--search>' "$output" \
|
||||||
) && ok "pclaude и pcodex делегируют run и сохраняют аргументы/код"
|
"pcodex исказил аргументы"
|
||||||
|
)
|
||||||
|
ok "pclaude и pcodex передают аргументы и код выхода"
|
||||||
|
|
||||||
# proxy_off очищает оба регистра и не смешивает действие с диагностикой.
|
# .bash_custom подключает и общие функции, и proxy-модуль.
|
||||||
(
|
(
|
||||||
source "$SHELL_MODULE"
|
home="$TEST_ROOT/home"
|
||||||
export HTTP_PROXY=x HTTPS_PROXY=x ALL_PROXY=x NO_PROXY=x
|
mkdir -p "$home/.config/proxy-client"
|
||||||
export http_proxy=x https_proxy=x all_proxy=x no_proxy=x
|
ln -s "$CUSTOM_FILE" "$home/.bash_custom"
|
||||||
curl() { fail "proxy_off не должен вызывать curl"; }
|
ln -s "$FUNCTIONS_FILE" "$home/.bash_functions"
|
||||||
proxy_off >/dev/null
|
ln -s "$SHELL_MODULE" "$home/.config/proxy-client/shell.bash"
|
||||||
for key in HTTP_PROXY HTTPS_PROXY ALL_PROXY NO_PROXY \
|
output="$(
|
||||||
http_proxy https_proxy all_proxy no_proxy; do
|
HOME="$home" bash --noprofile --norc -ic '
|
||||||
[ -z "${!key+x}" ] || fail "$key не очищен"
|
source "$HOME/.bash_custom"
|
||||||
done
|
declare -F proxy_on update_hproxy >/dev/null && printf ready
|
||||||
) && ok "proxy_off очищает восемь переменных без запроса IP"
|
' 2>/dev/null
|
||||||
|
)"
|
||||||
# Общий файл больше не владеет proxy-интерфейсом, но сохраняет update_hproxy.
|
assert_eq ready "$output" ".bash_custom не подключил модули"
|
||||||
(
|
)
|
||||||
source "$FUNCTIONS_FILE"
|
ok ".bash_custom подключает общие функции и proxy-модуль"
|
||||||
declare -F update_hproxy >/dev/null || fail "update_hproxy потерян"
|
|
||||||
for name in proxy_on proxy_off pclaude pcodex myip _proxy_run _proxy_cli; do
|
|
||||||
if declare -F "$name" >/dev/null; then
|
|
||||||
fail "$name всё ещё определён в bash/.bash_functions"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
) && ok "владение proxy-функциями перенесено, update_hproxy сохранён"
|
|
||||||
|
|
||||||
if command -v shellcheck >/dev/null 2>&1; then
|
if command -v shellcheck >/dev/null 2>&1; then
|
||||||
shellcheck -s bash "$FUNCTIONS_FILE" "$CUSTOM_FILE" "$SHELL_MODULE" "$0" \
|
shellcheck -s bash "$SHELL_MODULE" "$0" \
|
||||||
|| fail "shellcheck нашёл проблемы"
|
|| fail "shellcheck нашёл проблемы"
|
||||||
ok "shellcheck чист"
|
ok "shellcheck чист"
|
||||||
else
|
else
|
||||||
echo "skip - shellcheck не установлен"
|
echo "skip - shellcheck не установлен"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "PASS: $pass_count проверок пройдено"
|
echo "PASS: тесты Bash-функций proxy-client"
|
||||||
Reference in new issue
Block a user