This commit is contained in:
Mateusz Gruszczyński committed 2026-10-08 22:22:46 +02:00
1 parent 76cb4acef0
commit 68678e98be
84 files changed
+1519 -325

No files matched your search

+2 -1
View File
@@ -50,7 +50,8 @@ def main() -> None:
'start_service':not args.no_start,
'geoip_action':'skip' if args.skip_geoip else ('download-and-verify' if args.mode == 'install' else 'verify-and-refresh-if-stale'),
'shell_path_profile':str(SHELL_PATH_PROFILE),
'shell_path_action':'install' if args.mode == 'install' else 'check-only',
'shell_path_action':'install-or-repair',
'logrotate_path':str(LOGROTATE_PATH), 'logrotate_action':'install-or-repair',
'reset_saved_config':bool(args.reset_config), 'changes_made':False}, indent=2)); return
if sys.platform != 'linux' or os.geteuid() != 0 or not shutil.which('systemctl') or not shutil.which('runuser'):
raise ValueError('Installation requires root on Linux with systemd and runuser. Use --plan for inspection.')
+20 -1
View File
@@ -34,6 +34,7 @@ import urllib.parse
ROOT = Path(__file__).resolve().parents[2]
UNIT_PATH = Path('/etc/systemd/system/rustproxy.service')
SHELL_PATH_PROFILE = Path('/etc/profile.d/rustproxy-path.sh')
LOGROTATE_PATH = Path('/etc/logrotate.d/rustproxy')
SERVICE = 'rustproxy.service'
GEOIP_SOURCES = {
'user-country-ipv4.csv': 'https://github.com/sapics/ip-location-db/releases/download/latest/user-country-ipv4.csv',
@@ -95,7 +96,7 @@ def digest(path: Path) -> str:
def fetch_https(url: str, max_bytes: int) -> bytes:
if urllib.parse.urlparse(url).scheme != 'https':
raise ValueError('GeoIP downloads require HTTPS.')
request = urllib.request.Request(url, headers={'User-Agent': 'RustProxy-installer/0.29.0'})
request = urllib.request.Request(url, headers={'User-Agent': 'RustProxy-installer'})
with urllib.request.urlopen(request, timeout=30) as response:
if urllib.parse.urlparse(response.geturl()).scheme != 'https':
raise ValueError('GeoIP download redirected away from HTTPS.')
@@ -301,6 +302,24 @@ def install_shell_path(prefix: Path) -> None:
write_private(SHELL_PATH_PROFILE, shell_path_profile(prefix), 0o644)
def logrotate_config(user: str, group: str) -> bytes:
# User/group are validated system account names, never interpolated from a request.
for value in (user, group):
if not re.fullmatch(r'[a-z_][a-z0-9_-]*\$?', value):
raise ValueError('Unsafe service account for logrotate configuration.')
return (f"# Managed by RustProxy installer; service account: {user}:{group}\n"
'/var/log/rustproxy/*.jsonl /var/log/rustproxy/*.log /var/log/rustproxy/vhosts/*.jsonl /var/log/rustproxy/vhosts/*.log {\n'
' daily\n maxsize 100M\n rotate 14\n missingok\n notifempty\n'
' compress\n delaycompress\n copytruncate\n'
f' su {user} {group}\n'
'}\n').encode()
def install_logrotate(record: dict) -> None:
"""Keep logrotate in sync with the actual managed service account."""
write_private(LOGROTATE_PATH, logrotate_config(record['user'], record['group']), 0o644)
def check_shell_path(prefix: Path) -> bool:
expected = shell_path_profile(prefix)
try:
+5 -2
View File
@@ -359,8 +359,8 @@ def update(record: dict, binary: Path, sha: str, start: bool, refresh_geoip: boo
geoip_lock_fd: int | None = None
geoip_stage: Path | None = None
try:
# Updates deliberately do not rewrite shell environment. Check the installer-
# managed PATH profile early and report the exact remediation target instead.
# Repair missing or stale installer-owned shell setup on upgrades.
install_shell_path(prefix)
check_shell_path(prefix)
# Repair stale root:root/0700 metadata before staging/network work and before
# stopping the currently healthy service.
@@ -412,6 +412,8 @@ def update(record: dict, binary: Path, sha: str, start: bool, refresh_geoip: boo
if not healthy(record, record['version']):
raise RuntimeError(f'Update failed: {error}. Rollback restored code/data/unit but previous service health also failed. Backup: {backup}; failed state: {failed}') from error
raise RuntimeError(f'Update failed: {error}. Previous code, private data and service unit were restored. Installation is still present; do not run install.sh. Fix the cause and rerun update.sh. Backup: {backup}; failed state: {failed}') from error
install_shell_path(prefix)
install_logrotate(replacement)
print(f'Backup (contains private keys): {backup}')
return replacement
finally:
@@ -443,6 +445,7 @@ def install(record: dict, binary: Path, sha: str, start: bool, install_geoip: bo
record.update(release=release, version=version, sha256=sha)
switch(prefix, release)
install_shell_path(prefix)
install_logrotate(record)
# Record is written even if initial startup fails, so an update can repair it.
write_private(prefix / 'install.json', json.dumps(record, indent=2).encode())
write_private(UNIT_PATH, service_unit(record).encode(), 0o644)
+5 -3
View File
@@ -8,6 +8,8 @@ from pathlib import Path
import shutil
import sys
import uuid
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
from scripts.version import project_version
MANAGED = ["build.rs", "langs", "deploy", "debian", "grafana", "make_zip.py", "src", "tests", "scripts", "tools", "integrations", "nagios", "docker-agent", "migrations", "examples", "docs", ".github", ".cargo",
"Cargo.toml", "Cargo.lock", "rust-toolchain.toml", "dev.sh", "upgrade.sh", "install.sh", "update.sh",
@@ -46,7 +48,7 @@ def upgrade(source: Path, target: Path, *, stopped: bool, data_dir: Path | None
for name in MANAGED:
if (target / name).is_symlink():
raise ValueError(f"Managed target {name} is a symlink; refusing to replace it.")
backup = target / ".upgrade-backup" / ("before-0.29.0-" + uuid.uuid4().hex[:12])
backup = target / ".upgrade-backup" / ("before-" + project_version(target) + "-" + uuid.uuid4().hex[:12])
backup.mkdir(parents=True, mode=0o700)
installed: list[str] = []
moved: list[str] = []
@@ -84,7 +86,7 @@ def upgrade(source: Path, target: Path, *, stopped: bool, data_dir: Path | None
remove_legacy = legacy.exists()
if remove_legacy and (legacy.is_symlink() or not legacy.is_file()):
raise ValueError("Obsolete renewal observation sidecar must be a regular file.")
(backup / "upgrade.json").write_text(json.dumps({"from_directory":str(target), "to_version":"0.29.0", "managed_paths":MANAGED, "removed_obsolete_runtime_state": remove_legacy}, indent=2)+"\n", encoding="utf-8")
(backup / "upgrade.json").write_text(json.dumps({"from_directory":str(target), "to_version":project_version(source), "managed_paths":MANAGED, "removed_obsolete_runtime_state": remove_legacy}, indent=2)+"\n", encoding="utf-8")
if remove_legacy:
legacy.unlink()
except BaseException:
@@ -115,7 +117,7 @@ def main() -> None:
backup = upgrade(Path(__file__).resolve().parents[2], args.target, stopped=args.stopped, data_dir=data_dir)
except (OSError, ValueError) as error:
parser.exit(2, f"Upgrade refused or rolled back: {error}\n")
print(f"Installed RustProxy 0.29.0. Previous managed source files: {backup}")
print(f"Installed RustProxy {project_version(args.target)}. Previous managed source files: {backup}")
print("Preserved data, .env, .git, target and other unmanaged files. No database migration was run.")
print(f"Next: cd {args.target}; ./dev.sh test; ./dev.sh")