Files
dotfiles/tests/test_proxy_client.py
T
ddadmin 6e431d875e refactor(proxy-client): переименован интерфейс прокси
- Зачем:
  - прежнее имя выпячивало Xray, хотя клиент поддерживает также внешний прокси, а два каталога конфигурации были неочевидны.
- Что:
  - пакет, CLI, конфигурация, состояние, тесты и документация переименованы в proxy-client.
  - машинный конфиг, профиль и два явных примера собраны в одном пользовательском каталоге.
  - свежая установка защищена от сворачивания каталога конфигурации внутрь репозитория.
- Проверка:
  - bash tests/test_proxy_client_full.sh.
  - bash tests/test_install.sh.
  - ручные start, status и xray run -test; незавершённая сетевая приёмка записана в handoff.
2026-07-21 09:31:21 -04:00

1236 lines
50 KiB
Python

#!/usr/bin/env python3
import fcntl
import http.client
import json
import hashlib
import os
import runpy
import subprocess
import sys
import tempfile
import unittest
import urllib.error
import zipfile
from unittest import mock
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[1]
CLI = REPO_ROOT / "proxy-client/.local/bin/proxy-client"
class FakeUrlResponse:
def __init__(self, data=b"", content_length=None, read_error=None):
self.data = data
self.position = 0
self.read_error = read_error
self.headers = {}
if content_length is not None:
self.headers["Content-Length"] = str(content_length)
def __enter__(self):
return self
def __exit__(self, _type, _value, _traceback):
return False
def read(self, size=-1):
if self.read_error is not None:
raise self.read_error
if size < 0:
size = len(self.data) - self.position
chunk = self.data[self.position : self.position + size]
self.position += len(chunk)
return chunk
def checking_socket_setup(outcome="success"):
if outcome == "success":
outcome_line = " return Connection()\n"
elif outcome == "refused":
outcome_line = " raise OSError('connection refused')\n"
else:
raise ValueError("unknown socket outcome: {}".format(outcome))
return (
"import json, os, socket\n"
"expected_connection = json.loads(os.environ['TEST_EXPECTED_CONNECTION'])\n"
"class Connection:\n"
" def close(self):\n"
" pass\n"
"def checked_create_connection(endpoint, timeout):\n"
" actual_connection = [endpoint[0], endpoint[1], timeout]\n"
" if actual_connection != expected_connection:\n"
" raise AssertionError('socket call mismatch: expected %r, got %r' "
"% (expected_connection, actual_connection))\n"
+ outcome_line
+ "socket.create_connection = checked_create_connection"
)
SUCCESSFUL_SOCKET_SETUP = checking_socket_setup()
REFUSED_SOCKET_SETUP = checking_socket_setup("refused")
class ProxyClientCliTest(unittest.TestCase):
def setUp(self):
self.temp_dir = tempfile.TemporaryDirectory()
self.config_home = Path(self.temp_dir.name)
self.environment = os.environ.copy()
self.environment["XDG_CONFIG_HOME"] = str(self.config_home)
self.environment["PYTHONDONTWRITEBYTECODE"] = "1"
def tearDown(self):
self.temp_dir.cleanup()
def write_config(
self,
port,
proxy_type="external",
name="Test proxy",
address="127.0.0.1",
no_proxy_extra=".example.test",
):
config_dir = self.config_home / "proxy-client"
config_dir.mkdir(exist_ok=True)
(config_dir / "config").write_text(
"[proxy]\n"
"type = {}\n"
"name = {}\n"
"address = {}\n"
"port = {}\n"
"no_proxy_extra = {}\n".format(
proxy_type, name, address, port, no_proxy_extra
),
encoding="utf-8",
)
def write_raw_config(self, content):
config_dir = self.config_home / "proxy-client"
config_dir.mkdir(exist_ok=True)
path = config_dir / "config"
if isinstance(content, bytes):
path.write_bytes(content)
else:
path.write_text(content, encoding="utf-8")
def run_cli(self, *arguments, environment=None):
command_environment = self.environment.copy()
if environment:
command_environment.update(environment)
return subprocess.run(
[str(CLI)] + list(arguments),
text=True,
capture_output=True,
env=command_environment,
check=False,
)
def run_cli_with_socket_setup(
self, arguments, setup, environment=None, expected_connection=None
):
command_environment = self.environment.copy()
if environment:
command_environment.update(environment)
command_environment["TEST_CLI_ARGUMENTS"] = json.dumps(list(arguments))
if expected_connection is not None:
command_environment["TEST_EXPECTED_CONNECTION"] = json.dumps(
list(expected_connection)
)
code = (
"import json, os, runpy, sys\n"
+ setup
+ "\nsys.argv = [sys.argv[1]] + "
"json.loads(os.environ['TEST_CLI_ARGUMENTS'])\n"
"runpy.run_path(sys.argv[0], run_name='__main__')"
)
return subprocess.run(
[sys.executable, "-c", code, str(CLI)],
text=True,
capture_output=True,
env=command_environment,
check=False,
)
def test_socket_fixture_rejects_wrong_endpoint_and_timeout(self):
self.write_config(23456)
result = self.run_cli_with_socket_setup(
("status",),
SUCCESSFUL_SOCKET_SETUP,
expected_connection=("wrong.example", 1, 0.5),
)
self.assertNotEqual(0, result.returncode)
self.assertIn("AssertionError: socket call mismatch", result.stderr)
def test_no_subcommand_has_no_side_effect_and_reports_usage(self):
result = self.run_cli()
self.assertEqual(2, result.returncode)
self.assertEqual("", result.stdout)
self.assertIn("укажите явную подкоманду", result.stderr)
self.assertFalse((self.config_home / "proxy-client").exists())
def test_old_python_version_gets_clear_error_before_main_program(self):
code = (
"import runpy, sys; "
"sys.version_info = (3, 7, 9); "
"runpy.run_path(sys.argv[1], run_name='__main__')"
)
result = subprocess.run(
[sys.executable, "-c", code, str(CLI)],
text=True,
capture_output=True,
check=False,
)
self.assertEqual(1, result.returncode)
self.assertEqual("", result.stdout)
self.assertIn("требует Python 3.8", result.stderr)
self.assertIn("Python 3.7", result.stderr)
def test_proc_stat_parser_handles_spaces_and_closing_parentheses_in_comm(self):
module = runpy.run_path(str(CLI))
parse_starttime = module["parse_proc_stat_starttime"]
tail = ["S"] + [str(value) for value in range(4, 22)] + ["987654"]
for comm in ("worker", "worker name", "worker name ) with parens"):
with self.subTest(comm=comm):
self.assertEqual(
987654,
parse_starttime("321 ({}) ".format(comm) + " ".join(tail)),
)
for malformed in ("321 worker S 1 2", "321 (worker) S 1 2"):
with self.subTest(malformed=malformed):
with self.assertRaises(ValueError):
parse_starttime(malformed)
def test_process_permission_error_is_not_treated_as_dead(self):
module = runpy.run_path(str(CLI))
with mock.patch.object(module["Path"], "stat", side_effect=PermissionError()):
self.assertTrue(module["process_alive"](12345))
def test_same_pid_cannot_be_a_healthy_managed_pair(self):
module = runpy.run_path(str(CLI))
function_globals = module["managed_state_outcome"].__globals__
state = {"supervisor_pid": 12345, "xray_pid": 12345, "boot_id": "boot"}
replacements = {
"read_boot_id": mock.Mock(return_value="boot"),
"process_alive": mock.Mock(return_value=True),
"supervisor_identity_matches": mock.Mock(return_value=True),
"xray_identity_matches": mock.Mock(return_value=True),
}
with mock.patch.dict(function_globals, replacements):
self.assertEqual("unverifiable", module["managed_state_outcome"](state))
def test_identity_reads_starttime_after_process_paths(self):
module = runpy.run_path(str(CLI))
function_globals = module["xray_identity_matches"].__globals__
state = {
"supervisor_pid": 101,
"supervisor_starttime": 201,
"supervisor_path": "/cli",
"xray_pid": 102,
"xray_starttime": 202,
"xray_path": "/xray",
}
xray_events = []
xray_replacements = {
"process_executable": lambda _pid: xray_events.append("exe") or "/xray",
"process_starttime": lambda _pid: xray_events.append("stat") or 202,
}
with mock.patch.dict(function_globals, xray_replacements):
self.assertTrue(module["xray_identity_matches"](state))
self.assertEqual(["exe", "stat"], xray_events)
supervisor_events = []
supervisor_replacements = {
"process_cmdline": lambda _pid: supervisor_events.append("cmdline")
or ["python", "/cli"],
"process_argument_resolves_to": lambda *_args: supervisor_events.append(
"script"
)
or True,
"process_starttime": lambda _pid: supervisor_events.append("stat") or 201,
}
with mock.patch.dict(function_globals, supervisor_replacements):
self.assertTrue(module["supervisor_identity_matches"](state))
self.assertEqual(["cmdline", "script", "stat"], supervisor_events)
def test_initial_child_starttime_retries_while_child_is_alive(self):
module = runpy.run_path(str(CLI))
wait_for_starttime = module["wait_for_child_starttime"]
function_globals = wait_for_starttime.__globals__
child = mock.Mock(pid=12345)
child.poll.return_value = None
replacements = {
"process_starttime": mock.Mock(side_effect=[None, 7654321]),
"time": mock.Mock(),
}
with mock.patch.dict(function_globals, replacements):
self.assertEqual(7654321, wait_for_starttime(child))
def test_broken_config_symlink_is_not_treated_as_missing_config(self):
config_dir = self.config_home / "proxy-client"
config_dir.mkdir()
(config_dir / "config").symlink_to(self.config_home / "missing-config")
result = self.run_cli("start")
self.assertNotEqual(0, result.returncode)
self.assertIn("отсутствующий файл", result.stderr)
self.assertNotIn("только для type=xray", result.stderr)
def test_missing_config_keeps_legacy_external_hiddify_endpoint(self):
result = self.run_cli("status", environment={"PROXY_PREFLIGHT_TIMEOUT": "0.05"})
output = result.stdout + result.stderr
self.assertIn("Hiddify", output)
self.assertIn("hproxy.local:12334", output)
def test_status_reports_reachable_external_proxy(self):
self.write_config(23456)
result = self.run_cli_with_socket_setup(
("status",),
SUCCESSFUL_SOCKET_SETUP,
expected_connection=("127.0.0.1", 23456, 2.0),
)
self.assertEqual(0, result.returncode, result.stderr)
self.assertIn("Test proxy (external) доступен", result.stdout)
self.assertEqual("", result.stderr)
def test_env_prints_complete_proxy_environment_for_external_proxy(self):
self.write_config(
23456, no_proxy_extra=" .example.test , git.example.test "
)
result = self.run_cli_with_socket_setup(
("env",),
SUCCESSFUL_SOCKET_SETUP,
expected_connection=("127.0.0.1", 23456, 2.0),
)
proxy_url = "http://127.0.0.1:23456"
socks_url = "socks5://127.0.0.1:23456"
no_proxy = (
"127.0.0.1,localhost,::1,[::1],.example.test,git.example.test"
)
self.assertEqual(0, result.returncode, result.stderr)
self.assertEqual("", result.stderr)
self.assertEqual(
[
"HTTP_PROXY=" + proxy_url,
"HTTPS_PROXY=" + proxy_url,
"ALL_PROXY=" + socks_url,
"NO_PROXY=" + no_proxy,
"http_proxy=" + proxy_url,
"https_proxy=" + proxy_url,
"all_proxy=" + socks_url,
"no_proxy=" + no_proxy,
],
result.stdout.splitlines(),
)
def test_env_brackets_ipv6_without_requiring_host_ipv6(self):
self.write_config(23456, address="2001:db8::1", no_proxy_extra="")
result = self.run_cli_with_socket_setup(
("env",),
SUCCESSFUL_SOCKET_SETUP,
expected_connection=("2001:db8::1", 23456, 2.0),
)
self.assertEqual(0, result.returncode, result.stderr)
self.assertIn("HTTP_PROXY=http://[2001:db8::1]:23456\n", result.stdout)
self.assertIn("ALL_PROXY=socks5://[2001:db8::1]:23456\n", result.stdout)
def test_scoped_ipv6_is_rejected_when_ipaddress_lacks_scope_support(self):
self.write_config(23456, address="fe80::1%eth0", no_proxy_extra="")
setup = (
"import ipaddress\n"
"real_ip_address = ipaddress.ip_address\n"
"def python38_ip_address(value):\n"
" if '%' in value:\n"
" raise ValueError('scope IDs unsupported')\n"
" return real_ip_address(value)\n"
"ipaddress.ip_address = python38_ip_address\n"
+ SUCCESSFUL_SOCKET_SETUP
)
result = self.run_cli_with_socket_setup(
("env",),
setup,
expected_connection=("fe80::1%eth0", 23456, 2.0),
)
self.assertNotEqual(0, result.returncode)
self.assertEqual("", result.stdout)
self.assertIn("address", result.stderr)
def test_unreachable_env_prints_no_partial_output(self):
self.write_config(23456)
result = self.run_cli_with_socket_setup(
("env",),
REFUSED_SOCKET_SETUP,
{"PROXY_PREFLIGHT_TIMEOUT": "0.1"},
expected_connection=("127.0.0.1", 23456, 0.1),
)
self.assertNotEqual(0, result.returncode)
self.assertEqual("", result.stdout)
self.assertIn("недоступен", result.stderr)
def test_unreachable_external_proxy_never_runs_child(self):
self.write_config(23456)
marker = self.config_home / "external-child-ran"
helper = self.config_home / "external-child"
helper.write_text(
"#!/usr/bin/env bash\nprintf ran > \"$1\"\n", encoding="ascii"
)
helper.chmod(0o755)
result = self.run_cli_with_socket_setup(
("run", "--", str(helper), str(marker)),
REFUSED_SOCKET_SETUP,
{"PROXY_PREFLIGHT_TIMEOUT": "0.1"},
expected_connection=("127.0.0.1", 23456, 0.1),
)
self.assertNotEqual(0, result.returncode)
self.assertIn("недоступен", result.stderr)
self.assertFalse(marker.exists())
def test_xray_type_fails_closed_for_all_ticket_commands(self):
self.write_config(12334, proxy_type="xray")
marker = self.config_home / "child-ran"
helper = self.config_home / "child"
helper.write_text(
"#!/usr/bin/env bash\nprintf ran > \"$1\"\n", encoding="ascii"
)
helper.chmod(0o755)
commands = [
("status",),
("env",),
("run", "--", str(helper), str(marker)),
("start",),
]
for command in commands:
with self.subTest(command=command):
result = self.run_cli(*command)
self.assertNotEqual(0, result.returncode)
self.assertEqual("", result.stdout)
self.assertIn("Ошибка:", result.stderr)
self.assertFalse(marker.exists())
def test_external_start_fails_without_starting_anything(self):
self.write_config(12334)
result = self.run_cli("start")
self.assertNotEqual(0, result.returncode)
self.assertEqual("", result.stdout)
self.assertIn("только для type=xray", result.stderr)
def test_run_preserves_arguments_exit_code_and_child_only_environment(self):
self.write_config(23456)
code = (
"import json, os, sys; "
"print(json.dumps({'args': sys.argv[1:], "
"'http': os.environ['HTTP_PROXY'], "
"'lower': os.environ['http_proxy']})); sys.exit(42)"
)
original_http = os.environ.get("HTTP_PROXY")
result = self.run_cli_with_socket_setup(
("run", "--", sys.executable, "-c", code, "a b", "--", "--flag"),
SUCCESSFUL_SOCKET_SETUP,
expected_connection=("127.0.0.1", 23456, 2.0),
)
self.assertEqual(42, result.returncode, result.stderr)
child = json.loads(result.stdout)
self.assertEqual(["a b", "--", "--flag"], child["args"])
expected_url = "http://127.0.0.1:23456"
self.assertEqual(expected_url, child["http"])
self.assertEqual(expected_url, child["lower"])
self.assertEqual(original_http, os.environ.get("HTTP_PROXY"))
def test_missing_run_command_returns_127(self):
self.write_config(12334)
result = self.run_cli("run", "--", "definitely-no-such-command-xyz")
self.assertEqual(127, result.returncode)
self.assertEqual("", result.stdout)
self.assertIn("не найдена в PATH", result.stderr)
def test_run_maps_child_signal_to_shell_exit_code(self):
self.write_config(23456)
result = self.run_cli_with_socket_setup(
(
"run",
"--",
sys.executable,
"-c",
"import os, signal; os.kill(os.getpid(), signal.SIGTERM)",
),
SUCCESSFUL_SOCKET_SETUP,
expected_connection=("127.0.0.1", 23456, 2.0),
)
self.assertEqual(143, result.returncode, result.stderr)
def test_run_requires_explicit_separator_and_command(self):
self.write_config(12334)
for command in (("run",), ("run", "true"), ("run", "--")):
with self.subTest(command=command):
result = self.run_cli(*command)
self.assertNotEqual(0, result.returncode)
self.assertIn("run -- COMMAND", result.stderr)
def test_invalid_preflight_timeout_is_rejected_clearly(self):
self.write_config(12334)
result = self.run_cli(
"status", environment={"PROXY_PREFLIGHT_TIMEOUT": "not-a-number"}
)
self.assertNotEqual(0, result.returncode)
self.assertIn("положительным числом", result.stderr)
def test_ini_rejects_structure_key_and_value_grammar_errors(self):
valid = (
"[proxy]\n"
"type = external\n"
"name = Test\n"
"address = 127.0.0.1\n"
"port = 12334\n"
"no_proxy_extra =\n"
)
invalid_configs = {
"unknown section": valid + "[other]\nkey = value\n",
"unknown key": valid + "extra = value\n",
"duplicate key": valid + "port = 12335\n",
"missing key": valid.replace("name = Test\n", ""),
"empty name": valid.replace("name = Test", "name = "),
"empty address": valid.replace("address = 127.0.0.1", "address = "),
"port zero": valid.replace("port = 12334", "port = 0"),
"port high": valid.replace("port = 12334", "port = 65536"),
"port syntax": valid.replace("port = 12334", "port = +80"),
"empty no-proxy item": valid.replace(
"no_proxy_extra =", "no_proxy_extra = one.example,,two.example"
),
}
for label, content in invalid_configs.items():
with self.subTest(label=label):
self.write_raw_config(content)
result = self.run_cli("start")
self.assertNotEqual(0, result.returncode)
self.assertEqual("", result.stdout)
self.assertIn("Ошибка:", result.stderr)
def test_address_accepts_ip_literals_and_simple_ascii_names(self):
valid_addresses = (
"127.0.0.1",
"2001:db8::1",
"proxy-1.example",
".leading.example",
"trailing.example.",
"bad..example",
"-bad.example",
"bad-.example",
)
for address in valid_addresses:
with self.subTest(address=address):
self.write_config(12334, address=address)
result = self.run_cli("start")
self.assertIn("только для type=xray", result.stderr)
invalid_addresses = (
"http://proxy.example",
"[::1]",
"has space",
"proxy/path",
"user@proxy",
"прокси.example",
"fe80::1%eth0",
)
for address in invalid_addresses:
with self.subTest(address=address):
self.write_config(12334, address=address)
result = self.run_cli("start")
self.assertNotEqual(0, result.returncode)
self.assertNotIn("только для type=xray", result.stderr)
self.assertIn("address", result.stderr)
class XrayInstallTest(unittest.TestCase):
def setUp(self):
self.temporary = tempfile.TemporaryDirectory()
self.root = Path(self.temporary.name)
self.home = self.root / "home"
self.state_home = self.root / "state"
self.home.mkdir()
self.environment = {
"HOME": str(self.home),
"XDG_STATE_HOME": str(self.state_home),
"PATH": str(self.home / ".local/bin"),
}
self.module = runpy.run_path(str(CLI))
self.destination = self.home / ".local/bin/xray"
self.archive = self.root / "fixture.zip"
self.write_archive("installed")
def tearDown(self):
self.temporary.cleanup()
@staticmethod
def fake_xray(label="fixture", version="26.3.27"):
return (
"#!/bin/sh\n"
"if [ \"$1\" = version ]; then\n"
" printf '%s\\n' 'Xray {} ({})'\n"
" exit 0\n"
"fi\n"
"exit 64\n".format(version, label)
).encode("ascii")
def write_archive(self, label="fixture", version="26.3.27", member="xray"):
with zipfile.ZipFile(str(self.archive), "w") as archive:
archive.writestr(member, self.fake_xray(label, version))
@staticmethod
def official_digest(archive_hash):
return (
"MD5= {}\n"
"SHA1= {}\n"
"SHA2-256= {}\n"
"SHA2-512= {}\n".format(
"1" * 32,
"2" * 40,
archive_hash,
"3" * 128,
)
)
def downloader(self, bad_hash=False, digest_text=None, fail_after_archive=False):
calls = []
def download(url, destination, _max_bytes):
calls.append(url)
if url.endswith(".dgst"):
if fail_after_archive:
raise self.module["ProxyError"]("инъецированная ошибка загрузки")
archive_hash = hashlib.sha256(self.archive.read_bytes()).hexdigest()
if bad_hash:
archive_hash = "0" * 64
text = digest_text
if text is None:
text = self.official_digest(archive_hash)
destination.write_text(text, encoding="ascii")
else:
destination.write_bytes(self.archive.read_bytes())
return download, calls
def install(self, replace=False, downloader=None, machine="x86_64", path=None):
if downloader is None:
downloader, _calls = self.downloader()
environment = dict(self.environment)
if path is not None:
environment["PATH"] = path
globals_ = self.module["command_install"].__globals__
with mock.patch.dict(os.environ, environment, clear=False):
with mock.patch.object(globals_["platform"], "machine", return_value=machine):
return self.module["command_install"](replace, downloader)
def write_destination(self, content=None, executable=True):
self.destination.parent.mkdir(parents=True, exist_ok=True)
self.destination.write_bytes(content or self.fake_xray("existing"))
self.destination.chmod(0o755 if executable else 0o644)
def tree_snapshot(self):
snapshot = {}
for path in self.root.rglob("*"):
relative = path.relative_to(self.root).as_posix()
info = path.lstat()
mode = info.st_mode & 0o777
if path.is_symlink():
snapshot[relative] = ("symlink", mode, os.readlink(str(path)))
elif path.is_dir():
snapshot[relative] = ("directory", mode)
elif path.is_file():
digest = hashlib.sha256(path.read_bytes()).hexdigest()
snapshot[relative] = ("file", mode, digest)
else:
snapshot[relative] = ("other", mode)
return snapshot
def tree_snapshot_without_install_lock(self):
snapshot = self.tree_snapshot()
for path in ("state/proxy-client/lock", "state/proxy-client", "state"):
snapshot.pop(path, None)
return snapshot
def assert_no_staging(self):
if self.destination.parent.exists():
self.assertEqual(
[], list(self.destination.parent.glob(".xray-install-*"))
)
def assert_install_lock_released(self):
lock_path = self.state_home / "proxy-client/lock"
with lock_path.open("r+", encoding="ascii") as lock:
fcntl.flock(lock, fcntl.LOCK_EX | fcntl.LOCK_NB)
def test_architecture_mapping_is_pure_and_rejects_unknown(self):
asset_for = self.module["xray_asset_for_architecture"]
for machine in ("x86_64", "amd64"):
self.assertEqual("Xray-linux-64.zip", asset_for(machine))
for machine in ("aarch64", "arm64"):
self.assertEqual("Xray-linux-arm64-v8a.zip", asset_for(machine))
with self.assertRaisesRegex(self.module["ProxyError"], "не поддерживается"):
asset_for("riscv64")
def test_valid_digest_installs_verified_executable_and_only_expected_files(self):
downloader, calls = self.downloader()
self.assertEqual(0, self.install(downloader=downloader))
self.assertEqual(0o755, self.destination.stat().st_mode & 0o777)
self.assertIn(b"installed", self.destination.read_bytes())
self.assertEqual(2, len(calls))
self.assertTrue(calls[0].endswith("/Xray-linux-64.zip"))
self.assertEqual(calls[0] + ".dgst", calls[1])
self.assertEqual(
{
"fixture.zip",
"home",
"home/.local",
"home/.local/bin",
"home/.local/bin/xray",
"state",
"state/proxy-client",
"state/proxy-client/lock",
},
set(self.tree_snapshot()),
)
self.assert_no_staging()
def test_platform_machine_selects_arm64_asset(self):
downloader, calls = self.downloader()
self.assertEqual(
0, self.install(downloader=downloader, machine="aarch64")
)
self.assertTrue(calls[0].endswith("/Xray-linux-arm64-v8a.zip"))
self.assertEqual(calls[0] + ".dgst", calls[1])
def test_candidate_is_private_while_zip_bytes_are_written(self):
os_module = self.module["extract_xray_candidate"].__globals__["os"]
real_fdopen = os_module.fdopen
observed_modes = []
def checked_fdopen(descriptor, *arguments, **keywords):
descriptor_path = os.readlink("/proc/self/fd/{}".format(descriptor))
if Path(descriptor_path).name == "xray":
observed_modes.append(os.fstat(descriptor).st_mode & 0o777)
return real_fdopen(descriptor, *arguments, **keywords)
with mock.patch.object(
os_module, "fdopen", side_effect=checked_fdopen
):
self.assertEqual(0, self.install())
self.assertEqual([0o600], observed_modes)
self.assertEqual(0o755, self.destination.stat().st_mode & 0o777)
def test_bad_hash_and_digest_format_preserve_destination_before_extraction(self):
valid_hash = "a" * 64
cases = (
self.downloader(bad_hash=True)[0],
self.downloader(digest_text="SHA256= " + valid_hash + "\n")[0],
self.downloader(
digest_text=self.official_digest(valid_hash).replace(
"SHA2-256= " + valid_hash + "\n", ""
)
)[0],
self.downloader(
digest_text=self.official_digest(valid_hash)
+ "SHA2-256= "
+ valid_hash
+ "\n"
)[0],
self.downloader(
digest_text=self.official_digest(valid_hash) + "garbage\n"
)[0],
self.downloader(
digest_text=self.official_digest(valid_hash).replace(
valid_hash, "a" * 63
)
)[0],
self.downloader(
digest_text=self.official_digest(valid_hash).replace(
valid_hash, "a" * 65
)
)[0],
)
for downloader in cases:
with self.subTest(downloader=downloader):
sentinel = b"destination sentinel"
self.write_destination(sentinel)
before = self.tree_snapshot_without_install_lock()
with self.assertRaises(self.module["ProxyError"]):
self.install(replace=True, downloader=downloader)
self.assertEqual(sentinel, self.destination.read_bytes())
self.assert_no_staging()
self.assertEqual(before, self.tree_snapshot_without_install_lock())
def test_bad_or_truncated_archive_is_rejected_before_zip_open(self):
full_archive = self.archive.read_bytes()
def truncated_downloader(url, destination, _max_bytes):
if url.endswith(".dgst"):
digest = hashlib.sha256(full_archive).hexdigest()
destination.write_text(self.official_digest(digest), encoding="ascii")
else:
destination.write_bytes(full_archive[: len(full_archive) // 2])
zip_file = self.module["extract_xray_candidate"].__globals__["zipfile"]
with mock.patch.object(
zip_file, "ZipFile", side_effect=AssertionError("ZIP открыт до SHA-256")
) as opened:
with self.assertRaisesRegex(self.module["ProxyError"], "SHA-256"):
self.install(downloader=truncated_downloader)
opened.assert_not_called()
self.assertFalse(self.destination.exists())
self.assert_no_staging()
def test_real_downloader_installs_complete_declared_responses(self):
archive = self.archive.read_bytes()
digest = self.official_digest(hashlib.sha256(archive).hexdigest()).encode(
"ascii"
)
responses = [
FakeUrlResponse(archive, content_length=len(archive)),
FakeUrlResponse(digest, content_length=len(digest)),
]
urllib_request = self.module["download_file"].__globals__["urllib"].request
with mock.patch.object(urllib_request, "urlopen", side_effect=responses):
self.assertEqual(
0, self.install(downloader=self.module["download_file"])
)
def test_real_downloader_refuses_length_errors_and_releases_install(self):
archive = self.archive.read_bytes()
cases = (
(
"early EOF",
FakeUrlResponse(archive, content_length=len(archive) + 1),
),
(
"extra bytes",
FakeUrlResponse(archive, content_length=len(archive) - 1),
),
(
"declared oversized",
FakeUrlResponse(
b"",
content_length=self.module["MAX_ARCHIVE_BYTES"] + 1,
),
),
("url error", urllib.error.URLError("offline")),
(
"http error",
FakeUrlResponse(read_error=http.client.HTTPException("broken HTTP")),
),
(
"incomplete read",
FakeUrlResponse(
read_error=http.client.IncompleteRead(b"partial", 10)
),
),
)
urllib_request = self.module["download_file"].__globals__["urllib"].request
for label, outcome in cases:
with self.subTest(label=label):
sentinel = b"destination sentinel"
self.write_destination(sentinel)
before = self.tree_snapshot_without_install_lock()
with mock.patch.object(urllib_request, "urlopen", side_effect=[outcome]):
with self.assertRaises(self.module["ProxyError"]):
self.install(
replace=True, downloader=self.module["download_file"]
)
self.assertEqual(sentinel, self.destination.read_bytes())
self.assert_no_staging()
self.assert_install_lock_released()
self.assertEqual(before, self.tree_snapshot_without_install_lock())
def test_real_downloader_counts_limit_plus_one_without_content_length(self):
urllib_request = self.module["download_file"].__globals__["urllib"].request
globals_ = self.module["command_install"].__globals__
sentinel = b"destination sentinel"
self.write_destination(sentinel)
with mock.patch.dict(globals_, {"MAX_ARCHIVE_BYTES": 8}):
response = FakeUrlResponse(b"123456789")
with mock.patch.object(urllib_request, "urlopen", return_value=response):
with self.assertRaisesRegex(self.module["ProxyError"], "размер"):
self.install(
replace=True, downloader=self.module["download_file"]
)
self.assertEqual(sentinel, self.destination.read_bytes())
self.assert_no_staging()
self.assert_install_lock_released()
def test_uppercase_digest_is_rejected_before_zip_open(self):
digest = hashlib.sha256(self.archive.read_bytes()).hexdigest().upper()
downloader, _calls = self.downloader(
digest_text=self.official_digest(digest)
)
zip_file = self.module["extract_xray_candidate"].__globals__["zipfile"]
with mock.patch.object(
zip_file, "ZipFile", side_effect=AssertionError("ZIP открыт до digest")
) as opened:
with self.assertRaisesRegex(self.module["ProxyError"], "SHA-256"):
self.install(downloader=downloader)
opened.assert_not_called()
self.assertFalse(self.destination.exists())
self.assert_no_staging()
def test_existing_verified_install_is_noop_without_downloader(self):
self.write_destination()
downloader = mock.Mock(side_effect=AssertionError("network used"))
self.assertEqual(0, self.install(downloader=downloader))
downloader.assert_not_called()
def test_existing_version_gate_keeps_timeout_and_avoids_downloader(self):
self.write_destination()
downloader = mock.Mock(side_effect=AssertionError("network used"))
subprocess_module = self.module["xray_version"].__globals__["subprocess"]
timeout = subprocess.TimeoutExpired([str(self.destination), "version"], 5)
with mock.patch.object(subprocess_module, "run", side_effect=timeout) as run:
with self.assertRaisesRegex(self.module["ProxyError"], "--replace"):
self.install(downloader=downloader)
downloader.assert_not_called()
self.assertEqual(5, run.call_args.kwargs["timeout"])
self.assertEqual(self.fake_xray("existing"), self.destination.read_bytes())
def test_replace_reinstalls_verified_existing_file(self):
self.write_destination(self.fake_xray("old"))
downloader, calls = self.downloader()
self.assertEqual(0, self.install(replace=True, downloader=downloader))
self.assertIn(b"installed", self.destination.read_bytes())
self.assertEqual(2, len(calls))
def test_destination_table_for_symlink_directory_and_bad_files(self):
self.destination.parent.mkdir(parents=True)
target = self.root / "target"
target.write_bytes(b"target sentinel")
self.destination.symlink_to(target)
with self.assertRaisesRegex(self.module["ProxyError"], "--replace"):
self.install()
self.assertTrue(self.destination.is_symlink())
self.assertEqual(0, self.install(replace=True))
self.assertFalse(self.destination.is_symlink())
self.assertEqual(b"target sentinel", target.read_bytes())
self.destination.unlink()
self.destination.mkdir()
for replace in (False, True):
with self.subTest(directory=True, replace=replace):
with self.assertRaisesRegex(self.module["ProxyError"], "каталог"):
self.install(replace=replace)
self.destination.rmdir()
for content, executable in ((b"broken", True), (self.fake_xray(), False)):
with self.subTest(content=content, executable=executable):
self.write_destination(content, executable)
with self.assertRaisesRegex(self.module["ProxyError"], "--replace"):
self.install()
self.assertEqual(0, self.install(replace=True))
self.assertIn(b"installed", self.destination.read_bytes())
self.destination.unlink()
def test_parent_symlink_cannot_redirect_install_outside_home(self):
outside = self.root / "outside"
outside.mkdir()
(self.home / ".local").symlink_to(outside, target_is_directory=True)
downloader = mock.Mock()
with self.assertRaisesRegex(self.module["ProxyError"], "домашнего каталога"):
self.install(downloader=downloader)
downloader.assert_not_called()
self.assertEqual([], list(outside.iterdir()))
def test_managed_state_guard_only_allows_absent_or_stale(self):
guard = self.module["require_install_safe_managed_state"]
globals_ = guard.__globals__
present = mock.Mock()
present.lstat.return_value = mock.Mock()
with mock.patch.dict(
globals_,
{
"state_path": mock.Mock(return_value=present),
"read_managed_state": mock.Mock(return_value={"state": "fixture"}),
},
):
for outcome in ("healthy", "orphan", "unverifiable"):
with self.subTest(outcome=outcome):
globals_["managed_state_outcome"] = mock.Mock(return_value=outcome)
with self.assertRaisesRegex(self.module["ProxyError"], "stop"):
guard()
globals_["managed_state_outcome"] = mock.Mock(return_value="stale")
guard()
absent = mock.Mock()
absent.lstat.side_effect = FileNotFoundError()
with mock.patch.dict(
globals_, {"state_path": mock.Mock(return_value=absent)}
):
guard()
def test_command_classifies_managed_state_before_even_noop_destination(self):
self.write_destination()
downloader = mock.Mock(side_effect=AssertionError("network used"))
guard = mock.Mock(
side_effect=self.module["ProxyError"](
"managed Xray работает; сначала выполните proxy-client stop"
)
)
globals_ = self.module["command_install"].__globals__
with mock.patch.dict(
globals_, {"require_install_safe_managed_state": guard}
):
with self.assertRaisesRegex(self.module["ProxyError"], "stop"):
self.install(downloader=downloader)
guard.assert_called_once_with()
downloader.assert_not_called()
self.assertEqual(self.fake_xray("existing"), self.destination.read_bytes())
def test_download_extraction_and_version_errors_are_atomic_and_clean_staging(self):
failures = []
failures.append(self.downloader(fail_after_archive=True)[0])
missing_member = self.root / "missing-member.zip"
with zipfile.ZipFile(str(missing_member), "w") as archive:
archive.writestr("not-xray", b"data")
def missing_member_downloader(url, destination, _max_bytes):
source = missing_member if not url.endswith(".dgst") else None
if source is not None:
destination.write_bytes(source.read_bytes())
else:
digest = hashlib.sha256(missing_member.read_bytes()).hexdigest()
destination.write_text(self.official_digest(digest), encoding="ascii")
failures.append(missing_member_downloader)
self.write_archive("wrong", version="26.3.26")
failures.append(self.downloader()[0])
for downloader in failures:
with self.subTest(downloader=downloader):
sentinel = b"destination sentinel"
self.write_destination(sentinel)
before = self.tree_snapshot_without_install_lock()
with self.assertRaises(self.module["ProxyError"]):
self.install(replace=True, downloader=downloader)
self.assertEqual(sentinel, self.destination.read_bytes())
self.assert_no_staging()
self.assertEqual(before, self.tree_snapshot_without_install_lock())
def test_corrupt_zip_with_matching_digest_is_atomic_and_cleaned(self):
corrupt = b"not a ZIP archive"
def corrupt_downloader(url, destination, _max_bytes):
if url.endswith(".dgst"):
digest = hashlib.sha256(corrupt).hexdigest()
destination.write_text(self.official_digest(digest), encoding="ascii")
else:
destination.write_bytes(corrupt)
sentinel = b"destination sentinel"
self.write_destination(sentinel)
before = self.tree_snapshot_without_install_lock()
with self.assertRaisesRegex(self.module["ProxyError"], "извлечь"):
self.install(replace=True, downloader=corrupt_downloader)
self.assertEqual(sentinel, self.destination.read_bytes())
self.assert_no_staging()
self.assertEqual(before, self.tree_snapshot_without_install_lock())
def test_injected_oversized_archive_and_digest_are_bounded(self):
globals_ = self.module["command_install"].__globals__
cases = (
("MAX_ARCHIVE_BYTES", len(self.archive.read_bytes()) - 1, self.downloader()[0]),
(
"MAX_DIGEST_BYTES",
8,
self.downloader(digest_text=self.official_digest("a" * 64))[0],
),
)
for constant, limit, downloader in cases:
with self.subTest(constant=constant):
sentinel = b"destination sentinel"
self.write_destination(sentinel)
with mock.patch.dict(globals_, {constant: limit}):
with self.assertRaisesRegex(self.module["ProxyError"], "размер"):
self.install(replace=True, downloader=downloader)
self.assertEqual(sentinel, self.destination.read_bytes())
self.assert_no_staging()
def test_zip_member_declared_over_limit_is_rejected_before_extraction(self):
globals_ = self.module["command_install"].__globals__
binary_size = len(self.fake_xray())
sentinel = b"destination sentinel"
self.write_destination(sentinel)
with mock.patch.dict(globals_, {"MAX_XRAY_BYTES": binary_size - 1}):
with self.assertRaisesRegex(self.module["ProxyError"], "размер"):
self.install(replace=True)
self.assertEqual(sentinel, self.destination.read_bytes())
self.assert_no_staging()
def test_streamed_zip_output_cannot_exceed_limit_even_if_metadata_lies(self):
class FakeInfo:
filename = "xray"
file_size = 8
@staticmethod
def is_dir():
return False
class FakeArchive:
def __enter__(self):
return self
def __exit__(self, _type, _value, _traceback):
return False
@staticmethod
def infolist():
return [FakeInfo()]
@staticmethod
def open(_member):
return FakeUrlResponse(b"123456789")
globals_ = self.module["command_install"].__globals__
zip_file = globals_["zipfile"]
sentinel = b"destination sentinel"
self.write_destination(sentinel)
with mock.patch.dict(globals_, {"MAX_XRAY_BYTES": 8}):
with mock.patch.object(zip_file, "ZipFile", return_value=FakeArchive()):
with self.assertRaisesRegex(self.module["ProxyError"], "размер"):
self.install(replace=True)
self.assertEqual(sentinel, self.destination.read_bytes())
self.assert_no_staging()
def test_interruption_preserves_destination_cleans_staging_and_releases_lock(self):
sentinel = b"destination sentinel"
self.write_destination(sentinel)
before = self.tree_snapshot_without_install_lock()
def interrupted_downloader(_url, destination, _max_bytes):
destination.write_bytes(b"partial")
raise KeyboardInterrupt()
with self.assertRaises(KeyboardInterrupt):
self.install(replace=True, downloader=interrupted_downloader)
self.assertEqual(sentinel, self.destination.read_bytes())
self.assert_no_staging()
lock_path = self.state_home / "proxy-client/lock"
with lock_path.open("r+", encoding="ascii") as lock:
fcntl.flock(lock, fcntl.LOCK_EX | fcntl.LOCK_NB)
self.assertEqual(before, self.tree_snapshot_without_install_lock())
def test_start_lock_is_held_during_download_and_through_replace(self):
downloader, _calls = self.downloader()
lock_path = self.state_home / "proxy-client/lock"
def assert_lock_held():
with lock_path.open("r+", encoding="ascii") as competing:
with self.assertRaises(BlockingIOError):
fcntl.flock(competing, fcntl.LOCK_EX | fcntl.LOCK_NB)
def checked_downloader(url, destination, max_bytes):
assert_lock_held()
downloader(url, destination, max_bytes)
os_module = self.module["command_install"].__globals__["os"]
real_replace = os_module.replace
def checked_replace(source, destination):
real_replace(source, destination)
assert_lock_held()
with mock.patch.object(os_module, "replace", side_effect=checked_replace):
self.assertEqual(0, self.install(downloader=checked_downloader))
with lock_path.open("r+", encoding="ascii") as released:
fcntl.flock(released, fcntl.LOCK_EX | fcntl.LOCK_NB)
def test_unsupported_architecture_refuses_before_downloading(self):
downloader = mock.Mock()
with self.assertRaisesRegex(self.module["ProxyError"], "не поддерживается"):
self.install(downloader=downloader, machine="riscv64")
downloader.assert_not_called()
self.assertFalse(self.destination.exists())
def test_path_missing_or_competing_returns_error_without_rollback(self):
cases = (("empty", False), ("competitor", True))
for directory_name, competing in cases:
with self.subTest(directory_name=directory_name):
path_dir = self.root / directory_name
path_dir.mkdir()
if competing:
competitor = path_dir / "xray"
competitor.write_bytes(self.fake_xray("competitor"))
competitor.chmod(0o755)
path = str(path_dir) + os.pathsep + str(self.destination.parent)
else:
path = str(path_dir)
with self.assertRaisesRegex(self.module["ProxyError"], "PATH"):
self.install(path=path)
self.assertTrue(self.destination.is_file())
self.destination.unlink()
def test_busy_install_hint_uses_exception_type_not_message_text(self):
globals_ = self.module["acquire_install_lock"].__globals__
busy_error = self.module["StartLockBusyError"]("completely changed text")
with mock.patch.dict(
globals_, {"acquire_start_lock": mock.Mock(side_effect=busy_error)}
):
with self.assertRaisesRegex(self.module["ProxyError"], "stop"):
self.module["acquire_install_lock"]()
def test_path_comparison_handles_relative_entry_and_internal_bin_symlink(self):
local = self.home / ".local"
actual_bin = self.home / "private-bin"
local.mkdir()
actual_bin.mkdir()
(local / "bin").symlink_to(actual_bin, target_is_directory=True)
self.assertEqual(0, self.install())
self.assertTrue((actual_bin / "xray").is_file())
previous_cwd = Path.cwd()
try:
os.chdir(str(actual_bin))
self.assertEqual(0, self.install(path="."))
finally:
os.chdir(str(previous_cwd))
def test_install_dispatches_before_machine_config_loading(self):
self.write_destination()
config = self.root / "config/proxy-client"
config.mkdir(parents=True)
(config / "config").write_text("not an ini\n", encoding="ascii")
environment = os.environ.copy()
environment.update(self.environment)
environment["PATH"] = self.environment["PATH"] + os.pathsep + os.environ["PATH"]
environment["XDG_CONFIG_HOME"] = str(self.root / "config")
result = subprocess.run(
[str(CLI), "install"],
text=True,
capture_output=True,
env=environment,
check=False,
)
self.assertEqual(0, result.returncode, result.stderr)
self.assertIn("уже установлен", result.stdout)
if __name__ == "__main__":
unittest.main()