1322 lines
93 KiB
Python
Executable File
1322 lines
93 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Offline release checks that do not require Rust/Cargo or network access.
|
|
|
|
Exercises the shipped SQLite migration/backup and stopped-source upgrade path,
|
|
then checks the 0.24.1 shared-statistics/runtime/settings contracts and retained legacy-WebSocket and retained control-plane/cache-UX/live-log contracts plus retained transport contracts.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import hashlib
|
|
import importlib.util
|
|
import json
|
|
import os
|
|
import re
|
|
from pathlib import Path
|
|
import sqlite3
|
|
import subprocess
|
|
import tempfile
|
|
import tomllib
|
|
import zipfile
|
|
import sys
|
|
|
|
ROOT = Path(__file__).resolve().parents[2]
|
|
for relative in ("tools/legacy", "scripts/deploy", "scripts/migration"):
|
|
sys.path.insert(0, str(ROOT / relative))
|
|
|
|
import backup as backup_module
|
|
import deploy
|
|
import migrate_npm
|
|
import upgrade as upgrade_module
|
|
from rust_source import rust_source
|
|
from web_assets import read_app_js, read_vhosts_js
|
|
|
|
def read_cache_source() -> str:
|
|
return rust_source(ROOT, "src/cache/mod.rs")
|
|
|
|
def read_deploy_source() -> str:
|
|
return "\n".join((ROOT / name).read_text(encoding="utf-8") for name in ("scripts/deploy/deploy_core.py", "scripts/deploy/deploy_runtime.py", "scripts/deploy/deploy.py"))
|
|
|
|
def read_docker_agent_source() -> str:
|
|
return "\n".join(path.read_text(encoding="utf-8") for path in sorted((ROOT / "docker-agent").glob("*.go")) if not path.name.endswith("_test.go"))
|
|
|
|
|
|
def sql_and_backup_smoke(tmp: Path) -> None:
|
|
data = tmp / "data"
|
|
data.mkdir(mode=0o700, parents=True)
|
|
db = data / "config.sqlite3"
|
|
connection = sqlite3.connect(db)
|
|
try:
|
|
connection.executescript((ROOT / "migrations/0002_admin.sql").read_text(encoding="utf-8"))
|
|
connection.executescript((ROOT / "migrations/0003_certificate_renewals.sql").read_text(encoding="utf-8"))
|
|
document = json.dumps({"listeners": {"endpoints": []}, "vhosts": []})
|
|
connection.execute(
|
|
"INSERT INTO configuration(id, revision, document) VALUES(1, 4, ?)",
|
|
(document,),
|
|
)
|
|
connection.execute(
|
|
"INSERT INTO administrator(id,username,password_hash,generation,created_at) VALUES(1,'owner','test-hash',1,1)"
|
|
)
|
|
connection.execute(
|
|
"INSERT INTO certificate_renewals(certificate_id,last_checked,next_check,last_renewed,not_after,failures,status,message) VALUES(?,?,?,?,?,?,?,?)",
|
|
("site-cert", 100, 200, 50, 500, 1, "retry", "saved observation"),
|
|
)
|
|
connection.execute("PRAGMA user_version = 3")
|
|
connection.commit()
|
|
# Simulate additive startup migrations through the current schema-v9.
|
|
connection.executescript((ROOT / "migrations/0004_admin_preferences.sql").read_text(encoding="utf-8"))
|
|
connection.executescript((ROOT / "migrations/0005_certificate_api.sql").read_text(encoding="utf-8"))
|
|
connection.executescript((ROOT / "migrations/0006_certificate_api_credentials.sql").read_text(encoding="utf-8"))
|
|
connection.executescript((ROOT / "migrations/0007_secret_store.sql").read_text(encoding="utf-8"))
|
|
connection.executescript((ROOT / "migrations/0008_config_history.sql").read_text(encoding="utf-8"))
|
|
connection.execute(
|
|
"INSERT INTO certificate_api_credentials(key_id,token_hash,comment,created_at,last_used_at) VALUES(?,?,?,?,NULL)",
|
|
("legacy-hook", "0" * 64, "pre-0.28.2", 1),
|
|
)
|
|
connection.executescript((ROOT / "migrations/0009_certificate_api_grants.sql").read_text(encoding="utf-8"))
|
|
connection.execute(
|
|
"INSERT INTO admin_preferences(administrator_id,document) VALUES(1,?)",
|
|
(json.dumps({"live_log_columns": ["time", "status"]}),),
|
|
)
|
|
connection.execute("PRAGMA user_version = 9")
|
|
connection.commit()
|
|
assert connection.execute("PRAGMA quick_check").fetchone()[0] == "ok"
|
|
tables = {
|
|
row[0]
|
|
for row in connection.execute(
|
|
"SELECT name FROM sqlite_master WHERE type='table'"
|
|
)
|
|
}
|
|
for table in ("configuration", "administrator", "admin_sessions", "admin_audit", "auth_throttle", "certificate_renewals", "admin_preferences", "certificate_api_credential", "certificate_api_credentials", "certificate_api_grants", "secret_store", "configuration_history"):
|
|
assert table in tables, table
|
|
assert connection.execute(
|
|
"SELECT allow_deploy, certificate_ids FROM certificate_api_grants WHERE key_id='legacy-hook'"
|
|
).fetchone() == (0, '["*"]')
|
|
finally:
|
|
connection.close()
|
|
os.chmod(db, 0o600)
|
|
|
|
destination = tmp / "backup"
|
|
copied = backup_module.backup(data, destination)
|
|
assert copied.is_file()
|
|
metadata = json.loads((destination / "backup.json").read_text(encoding="utf-8"))
|
|
assert metadata["project"] == "RustProxy"
|
|
assert metadata["schema"] == 9
|
|
assert metadata["configuration_revision"] == 4
|
|
restored = sqlite3.connect(copied)
|
|
try:
|
|
assert restored.execute("PRAGMA quick_check").fetchone()[0] == "ok"
|
|
assert restored.execute("SELECT revision FROM configuration WHERE id=1").fetchone()[0] == 4
|
|
assert restored.execute("SELECT status, message FROM certificate_renewals WHERE certificate_id='site-cert'").fetchone() == ("retry", "saved observation")
|
|
assert json.loads(restored.execute("SELECT document FROM admin_preferences WHERE administrator_id=1").fetchone()[0]) == {"live_log_columns": ["time", "status"]}
|
|
assert restored.execute("SELECT username FROM administrator WHERE id=1").fetchone()[0] == "owner"
|
|
finally:
|
|
restored.close()
|
|
|
|
|
|
def upgrade_smoke(tmp: Path) -> None:
|
|
target = tmp / "installed"
|
|
target.mkdir(parents=True)
|
|
(target / "Cargo.toml").write_text(
|
|
'[package]\nname = "rust-proxy"\nversion = "0.8.1"\n', encoding="utf-8"
|
|
)
|
|
(target / "src").mkdir()
|
|
(target / "src/obsolete.rs").write_text("obsolete\n", encoding="utf-8")
|
|
(target / "langs").mkdir()
|
|
custom_pack = {"meta": {"code": "zz_ZZ", "name": "Site test"}, "strings": {}}
|
|
(target / "langs/site.json").write_text(json.dumps(custom_pack), encoding="utf-8")
|
|
(target / "data").mkdir(mode=0o700)
|
|
(target / "data/keep.txt").write_text("preserve\n", encoding="utf-8")
|
|
legacy = target / "data/certs/renewals.json"
|
|
legacy.parent.mkdir(parents=True)
|
|
legacy.write_text('{"legacy":true}\n', encoding="utf-8")
|
|
(target / ".env").write_text("RUST_PROXY_DEMO=0\n", encoding="utf-8")
|
|
|
|
backup = upgrade_module.upgrade(ROOT, target, stopped=True)
|
|
assert backup.is_dir()
|
|
assert (backup / "src/obsolete.rs").is_file()
|
|
assert not (target / "src/obsolete.rs").exists()
|
|
assert 'version = "0.29.0"' in (target / "Cargo.toml").read_text(encoding="utf-8")
|
|
assert (target / "data/keep.txt").read_text(encoding="utf-8") == "preserve\n"
|
|
assert (target / ".env").read_text(encoding="utf-8") == "RUST_PROXY_DEMO=0\n"
|
|
assert (target / "langs/site.json").is_file()
|
|
assert (target / "docker-agent/main.go").is_file()
|
|
assert (target / "docker-agent/model.go").is_file()
|
|
assert (target / "docker-agent/docker.go").is_file()
|
|
assert (target / "docker-agent/network.go").is_file()
|
|
assert (target / "docker-agent/certificate.go").is_file()
|
|
assert (target / "docker-agent/server.go").is_file()
|
|
manifest = json.loads((backup / "upgrade.json").read_text(encoding="utf-8"))
|
|
assert manifest["to_version"] == "0.29.0"
|
|
assert manifest["removed_obsolete_runtime_state"] is True
|
|
assert not legacy.exists()
|
|
assert not (backup / "legacy-data").exists()
|
|
|
|
# Legacy cleanup must never follow a symlinked certs directory outside the data root.
|
|
unsafe = tmp / "unsafe-installed"
|
|
unsafe.mkdir()
|
|
(unsafe / "Cargo.toml").write_text('[package]\nname = "rust-proxy"\nversion = "0.8.1"\n', encoding="utf-8")
|
|
(unsafe / "src").mkdir(); (unsafe / "src/old.rs").write_text("old\n", encoding="utf-8")
|
|
(unsafe / "data").mkdir()
|
|
outside = tmp / "outside-certs"; outside.mkdir()
|
|
outside_legacy = outside / "renewals.json"; outside_legacy.write_text('{"outside":true}\n', encoding="utf-8")
|
|
(unsafe / "data/certs").symlink_to(outside, target_is_directory=True)
|
|
try:
|
|
upgrade_module.upgrade(ROOT, unsafe, stopped=True)
|
|
except ValueError as error:
|
|
assert "must not be a symlink" in str(error)
|
|
else:
|
|
raise AssertionError("symlinked certs directory was accepted during legacy cleanup")
|
|
assert outside_legacy.read_text(encoding="utf-8") == '{"outside":true}\n'
|
|
assert 'version = "0.8.1"' in (unsafe / "Cargo.toml").read_text(encoding="utf-8")
|
|
assert (unsafe / "src/old.rs").is_file()
|
|
|
|
|
|
def deployment_gate_smoke(tmp: Path) -> None:
|
|
assert deploy.admin_address("127.0.0.1:9090") == ("127.0.0.1:9090", False)
|
|
assert deploy.admin_address("0.0.0.0:9090", True) == ("0.0.0.0:9090", True)
|
|
try:
|
|
deploy.admin_address("0.0.0.0:9090", False)
|
|
except ValueError:
|
|
pass
|
|
else:
|
|
raise AssertionError("remote administration bind accepted without --allow-remote-admin")
|
|
assert deploy.admin_address("192.0.2.10:9090", True) == ("192.0.2.10:9090", True)
|
|
|
|
remote_unit = deploy.service_unit({"prefix":"/opt/rustproxy", "data_dir":"/var/lib/rustproxy", "user":"rustproxy", "group":"rustproxy", "admin":"0.0.0.0:9090", "remote_admin":True, "http":"0.0.0.0:80", "https":"0.0.0.0:443"})
|
|
assert "ExecStart=/opt/rustproxy/current/rust-proxy --data-dir /var/lib/rustproxy --admin-listen 0.0.0.0:9090 serve --proxy-listen 0.0.0.0:80 --tls-listen 0.0.0.0:443 --allow-remote-admin" in remote_unit
|
|
assert "ReadWritePaths=/var/lib/rustproxy /var/log/rustproxy" in remote_unit
|
|
assert "ReadOnlyPaths=-/etc/letsencrypt -/etc/ssl" in remote_unit
|
|
assert "TimeoutStopSec=120" in remote_unit
|
|
for marker in ["PrivateDevices=true", "ProtectKernelLogs=true", "RestrictNamespaces=true", "RestrictRealtime=true", "SystemCallArchitectures=native"]:
|
|
assert marker in remote_unit, marker
|
|
deploy_source = read_deploy_source()
|
|
assert "def stop_for_managed_update" in deploy_source and ".managed-update" in deploy_source
|
|
assert "systemctl', 'stop', SERVICE" in deploy_source
|
|
assert "def ensure_service_data_access" in deploy_source
|
|
assert "status=200/CHDIR" in deploy_source
|
|
assert "runuser', '-u', account.pw_name, '--', 'test', '-x'" in deploy_source
|
|
assert deploy_source.index("ensure_service_data_access(data, account)") < deploy_source.index("stop_for_managed_update(data)")
|
|
assert deploy_source.index("stop_for_managed_update(data)") < deploy_source.index("backup = backup_stopped(record, prefix)")
|
|
|
|
data = tmp / "admin-health"
|
|
data.mkdir(parents=True)
|
|
database = data / "config.sqlite3"
|
|
connection = sqlite3.connect(database)
|
|
connection.execute("CREATE TABLE configuration(id INTEGER PRIMARY KEY, revision INTEGER NOT NULL, document TEXT NOT NULL)")
|
|
connection.execute(
|
|
"INSERT INTO configuration VALUES(1, 7, ?)",
|
|
(json.dumps({"administration": {"listen": "0.0.0.0:9443", "tls": True, "allowed_hosts": ["panel.example.test"]}}),),
|
|
)
|
|
connection.commit(); connection.close()
|
|
record = {"admin": "127.0.0.1:9090", "data_dir": str(data)}
|
|
assert deploy.admin_health_endpoint(record) == ("127.0.0.1:9443", True, "panel.example.test:9443")
|
|
|
|
|
|
def npm_migration_smoke(tmp: Path) -> None:
|
|
database = tmp / "npm.sqlite"
|
|
connection = sqlite3.connect(database)
|
|
connection.execute(
|
|
"CREATE TABLE proxy_host("
|
|
"id INTEGER, domain_names TEXT, forward_scheme TEXT, forward_host TEXT, forward_port INTEGER, "
|
|
"allow_websocket_upgrade INTEGER, http2_support INTEGER, enabled INTEGER, certificate_id INTEGER, "
|
|
"ssl_forced INTEGER, access_list_id INTEGER, advanced_config TEXT, locations TEXT, "
|
|
"caching_enabled INTEGER, block_exploits INTEGER, meta TEXT)"
|
|
)
|
|
connection.execute(
|
|
"INSERT INTO proxy_host VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",
|
|
(12, json.dumps(["App.Example.test", "*.apps.example.test"]), "http", "10.20.30.40", 8080,
|
|
1, 1, 1, 9, 0, 4, "proxy_read_timeout 90s;", "[]", 1, 0, "{}"),
|
|
)
|
|
connection.execute(
|
|
"INSERT INTO proxy_host VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",
|
|
(13, json.dumps(["secure.example.test"]), "http", "10.20.30.41", 8081,
|
|
1, 1, 1, 10, 1, 0, "", "[]", 0, 0, "{}"),
|
|
)
|
|
connection.commit(); connection.close()
|
|
rows = migrate_npm.load_npm_hosts(database)
|
|
config = {
|
|
"listeners": {"endpoints": [
|
|
{"id": "public-http", "enabled": True, "binding": {"address": "0.0.0.0:80", "tls": False}},
|
|
{"id": "public-https", "enabled": True, "binding": {"address": "0.0.0.0:443", "tls": True}},
|
|
]},
|
|
"vhosts": [],
|
|
}
|
|
merged, decisions = migrate_npm.build_migration(rows, config)
|
|
assert len(decisions) == 2 and all(item.action == "import" for item in decisions)
|
|
migrated = merged["vhosts"][0]
|
|
assert migrated["frontend"]["hosts"] == ["app.example.test", "*.apps.example.test"]
|
|
assert migrated["frontend"]["listener_ids"] == ["public-http", "public-https"]
|
|
assert migrated["backends"][0]["url"] == "http://10.20.30.40:8080"
|
|
assert migrated["backends"][0]["allow_private"] is True
|
|
assert migrated["websockets"] is True
|
|
assert migrated["force_ssl"] is False
|
|
assert migrated["forward_client_ip"] is True
|
|
assert any("access_list_id=4" in warning for warning in decisions[0].warnings or [])
|
|
assert any("advanced_config" in warning for warning in decisions[0].warnings or [])
|
|
secure = merged["vhosts"][1]
|
|
assert secure["force_ssl"] is True
|
|
assert secure["frontend"]["listener_ids"] == ["public-http", "public-https"]
|
|
assert any("Force SSL" in warning for warning in decisions[1].warnings or [])
|
|
|
|
|
|
def ui_contract_smoke() -> None:
|
|
html = (ROOT / "src/admin/web/index.html").read_text(encoding="utf-8")
|
|
app = read_app_js(ROOT)
|
|
settings = (ROOT / "src/admin/web/settings.js").read_text(encoding="utf-8")
|
|
vhosts = read_vhosts_js(ROOT)
|
|
streams = (ROOT / "src/admin/web/streams.js").read_text(encoding="utf-8")
|
|
redirects = (ROOT / "src/admin/web/redirects.js").read_text(encoding="utf-8")
|
|
host404 = (ROOT / "src/admin/web/host404.js").read_text(encoding="utf-8")
|
|
integrations = (ROOT / "src/admin/web/integrations.js").read_text(encoding="utf-8")
|
|
acme_ui = (ROOT / "src/admin/web/acme.js").read_text(encoding="utf-8")
|
|
traffic = (ROOT / "src/admin/web/traffic-panel.js").read_text(encoding="utf-8")
|
|
cache = (ROOT / "src/admin/web/cache-panel.js").read_text(encoding="utf-8")
|
|
transform_panel = (ROOT / "src/admin/web/transform-panel.js").read_text(encoding="utf-8")
|
|
css = (ROOT / "src/admin/web/style.css").read_text(encoding="utf-8")
|
|
required_html = [
|
|
'id="panel-traffic"',
|
|
'id="traffic-scope"',
|
|
'id="traffic-chart-countries"',
|
|
'id="traffic-chart-client-ips"',
|
|
'id="traffic-tabs"',
|
|
'data-traffic-tab="overview"',
|
|
'data-traffic-tab="clients"',
|
|
'data-traffic-tab="requests"',
|
|
'data-traffic-tab="delivery"',
|
|
'id="traffic-chart-user-agents"',
|
|
'id="traffic-chart-paths"',
|
|
'id="traffic-chart-routes"',
|
|
'id="traffic-chart-volume"',
|
|
'id="log-capture-user-agent"',
|
|
'id="log-capture-country"',
|
|
'id="cache-auto-trim-enabled"',
|
|
'id="settings-tab-logging"',
|
|
'id="settings-page-logging"',
|
|
'id="logging-form"',
|
|
'id="error-log-form"',
|
|
'id="settings-tab-administration"',
|
|
'id="settings-tab-tls-policy"',
|
|
'id="admin-tls"',
|
|
'id="tls-policy-12"',
|
|
'id="tls-policy-13"',
|
|
'id="cache-resident-age-header"',
|
|
'id="cache-cold-enabled"',
|
|
'id="cache-transient-memory"',
|
|
'id="cache-cold-queue-bytes"',
|
|
'id="cache-eviction-limit"',
|
|
'data-cache-memory-preset="recommended"',
|
|
'id="cache-cold-path"',
|
|
'id="cache-cold-bytes"',
|
|
'id="cache-cold-entries"',
|
|
'id="cp-hot-entries-meter"',
|
|
'id="cp-memory"',
|
|
'id="cp-cold-entries-meter"',
|
|
'id="cp-cold-disk"',
|
|
'id="cache-cold-after"',
|
|
'id="log-format-preset"',
|
|
'id="log-format-open"',
|
|
'id="logging-format-dialog"',
|
|
'id="logging-field-rows"',
|
|
'id="panel-acl-profiles"',
|
|
'id="acl-profile-form"',
|
|
'id="acl-profile-list"',
|
|
'id="acl-profile-rule-rows"',
|
|
'id="acl-profile-rule-add"',
|
|
'id="acl-profile-import-rules"',
|
|
'id="vhost-search"',
|
|
'id="panel-redirects"',
|
|
'id="redirect-search"',
|
|
'id="redirect-list"',
|
|
'id="redirect-new"',
|
|
'id="redirect-dialog"',
|
|
'id="redirect-form"',
|
|
'id="redirect-rules"',
|
|
'id="redirect-rule-add"',
|
|
'id="panel-notfound"',
|
|
'id="notfound-search"',
|
|
'id="notfound-list"',
|
|
'id="notfound-new"',
|
|
'id="notfound-dialog"',
|
|
'id="notfound-form"',
|
|
'id="notfound-hosts-editor"',
|
|
'id="notfound-listeners"',
|
|
'id="notfound-certificate"',
|
|
'id="notfound-mode"',
|
|
'id="notfound-status"',
|
|
'id="panel-streams"',
|
|
'id="stream-search"',
|
|
'id="stream-list"',
|
|
'id="stream-new"',
|
|
'id="stream-listener-add"',
|
|
'id="stream-dialog"',
|
|
'id="stream-form"',
|
|
'class="vhost-dialog logging-format-dialog"',
|
|
]
|
|
for marker in required_html:
|
|
assert marker in html, marker
|
|
assert '/api/traffic' in traffic and 'rustProxyTrafficPanel' in traffic
|
|
for marker in ['function selectTab(', 'function renderDonut(', 'function renderBars(', 'renderBars("client-ips"', 'renderBars("user-agents"', 'renderBars("paths"', 'renderBars("routes"', 'renderBars("volume"']:
|
|
assert marker in traffic, marker
|
|
assert 'traffic: "/traffic"' in app and 'window.rustProxyTrafficPanel?.refresh()' in app
|
|
for marker in ['id="settings-tab-manual-config"', 'id="settings-page-manual-config"', 'id="config-editor"', 'id="save-json"', 'id="export-config"']:
|
|
assert marker in html, marker
|
|
assert 'data-panel="configuration"' not in html and 'id="panel-configuration"' not in html
|
|
assert html.index('data-panel="vhosts"') < html.index('data-panel="redirects"') < html.index('data-panel="notfound"') < html.index('data-panel="streams"') < html.index('data-panel="acl-profiles"')
|
|
assert "matching_resident_bytes" in cache and "matching_body_bytes" in cache
|
|
for marker in ["data.settings?.max_entries", "data.settings?.cold_max_entries", "data.settings?.cold_max_bytes", "setMeter("]:
|
|
assert marker in cache, marker
|
|
for marker in ["cache-memory-breakdown", "cache-transient-breakdown", "cache-cold-storage", "cache-transient-sparkline", "cache-fill-sparkline", "cache-queue-sparkline", "cache-io-sparkline", "cache-cold-sparkline", "metric-info", "process-rss-total", "memory-cache-hot", "memory-cache-transient", "memory-transform", "memory-transform-inflight", "memory-non-cache-rss", "process-rss-anon", "process-rss-file", "process-data-swap", "memoryAccountingHint"]:
|
|
assert marker in html, marker
|
|
for marker in ["transient_bytes", "fill_bytes_active", "cold_queue_bytes", "cold_write_bytes_active", "cold_read_bytes_active", "process_rss_anon_bytes", "process_rss_file_bytes"]:
|
|
assert marker in app or marker in cache, marker
|
|
assert "matching_cold_bytes" in cache and "cold_promotions" in cache and "hot_demotions" in cache
|
|
assert "next.logging" in app and "logstash:" in app and "loki:" in app and "file:" in app
|
|
assert "LOGGING_PRESETS" in app and "selectedLoggingMapping" in app and "renderLoggingPreview" in app
|
|
assert "administration" in settings and "tls-policy" in settings
|
|
assert "additional_addresses" in integrations
|
|
assert "addressTagEditor" in integrations and "listener-address-chip-editor" in integrations
|
|
assert 'const h2 = field(grid, "listener-h2", "vhHttp2"' in integrations
|
|
assert 'h2Configure' in integrations and 'selectHttpTab?.("http2")' in integrations
|
|
assert 'renderHttp2Settings' in integrations and 'collectHttp2Listeners' in integrations
|
|
assert 'const h3 = field(grid, "listener-h3", "h3Enable"' in integrations
|
|
assert 'h3Configure' in integrations and 'selectHttpTab?.("http3")' in integrations
|
|
h3_tuning = integrations.split('function http3SettingsControls', 1)[1].split('function addRow', 1)[0]
|
|
assert 'h3Enable' not in h3_tuning
|
|
assert 'data-i18n="processDiagnostics">Process<' in html
|
|
for marker in ['function setOptionalMetric', 'setOptionalMetric("process-rss-anon"', 'setOptionalMetric("system-swap"', 'setOptionalMetric("system-network"']:
|
|
assert marker in app, marker
|
|
assert 'const http80 = prefer(false, 80);' in integrations
|
|
assert 'if (http80) autoIds.add(http80.id);' in integrations and 'candidates.find(endpoint => !endpoint.binding?.tls)' in integrations
|
|
assert 'https443' not in integrations
|
|
assert 'window.rustProxyIntegrations.bindings(holder, selected, !editing)' in redirects
|
|
assert 'window.rustProxyIntegrations.hostnames(hostHolder' in redirects
|
|
assert 'function hostnames(parent, initial = [], options = {})' in integrations
|
|
assert 'class="protocol-toggle-row"' in html
|
|
assert "stream-host-rows" not in integrations and 'id="settings-page-streams"' not in html and 'id="streams-form"' not in html
|
|
for marker in ["rustProxyStreams", "streamTrafficChart", "streamPendingRestart", "chartHistory", "sessions_total", "bytes_in", "bytes_out"]:
|
|
assert marker in streams, marker
|
|
assert '$("stream-listener-add").addEventListener("click", () => { setPanel("settings"); window.rustProxySettings?.select("listeners"); });' in streams
|
|
assert "/assets/streams.js" in html
|
|
assert "/assets/redirects.js" in html
|
|
assert "/assets/host404.js" in html
|
|
for marker in ["rustProxyRedirects", 'saveSection("redirects"', "redirectTargetType", "redirectPathMode", "redirectQueryMode", "redirectCacheSeconds"]:
|
|
assert marker in redirects, marker
|
|
for marker in ["rustProxyNotFound", 'saveSection("not_found_hosts"', "rustProxyIntegrations.hostnames", "rustProxyIntegrations.bindings", "notfound-mode", "notfound-status", "BUILTIN_STATUSES", "ERROR_STATUSES", 'rustProxyDrafts.register("notfound"', 'rustProxyEnableControl($("notfound-enabled"))']:
|
|
assert marker in host404, marker
|
|
assert 'for (let code = 200; code <= 599; code++)' not in host404
|
|
assert 'id="docker-search"' in html and 'id="docker-show-hidden"' in html
|
|
assert 'preserve_host: true' in vhosts
|
|
assert "tls_policy" in vhosts and "vhost-live-grid" in vhosts
|
|
assert '"force-ssl", "vhForceSsl"' in vhosts and 'force_ssl: yes("force-ssl")' in vhosts
|
|
assert '"forward-client-ip", "vhForwardClientIp"' in vhosts and 'forward_client_ip: yes("forward-client-ip")' in vhosts
|
|
assert "vhForceSslNeedsTls" in vhosts and "vhForceSslNeedsHttp" in vhosts and "vhForceSslPortConflict" in vhosts
|
|
assert "tlsPorts.has(443)" in vhosts
|
|
assert '$("vhost-search").addEventListener("input", render)' in vhosts and "vhSearchEmpty" in vhosts
|
|
for marker in ['toggleVhostEnabled', 'vhQuickEnable', 'vhQuickDisable', 'backend_table_columns', 'applyPoolTableView', 'rustProxyEnableControl']:
|
|
assert marker in vhosts, marker
|
|
assert 'rustproxy-backend-table-v1' not in vhosts and 'poolTablePreferenceKey' not in vhosts
|
|
assert 'window.rustProxyPreferences.update({' in vhosts and 'backend_table_sort' in vhosts and 'backend_table_descending' in vhosts
|
|
for marker in ['id="vhost-sort"', 'id="vhost-sort-direction"', 'id="docker-vhost-sort"', 'id="docker-vhost-sort-direction"', 'id="redirect-sort"', 'id="redirect-sort-direction"', 'id="notfound-sort"', 'id="notfound-sort-direction"', 'id="stream-sort"', 'id="stream-sort-direction"']:
|
|
assert marker in html, marker
|
|
preferences_source = (ROOT / 'src/admin/preferences.rs').read_text(encoding='utf-8')
|
|
for marker in ['vhost_list_sort', 'docker_vhost_sort', 'redirect_list_sort', 'notfound_list_sort', 'stream_list_sort']:
|
|
assert marker in preferences_source, marker
|
|
assert 'vhost_list_sort' in vhosts and 'vhost_list_descending' in vhosts
|
|
list_sort_sources = {
|
|
'docker': (ROOT / 'src/admin/web/docker.js').read_text(encoding='utf-8'),
|
|
'redirects': (ROOT / 'src/admin/web/redirects.js').read_text(encoding='utf-8'),
|
|
'notfound': (ROOT / 'src/admin/web/host404.js').read_text(encoding='utf-8'),
|
|
'streams': (ROOT / 'src/admin/web/streams.js').read_text(encoding='utf-8'),
|
|
}
|
|
assert 'docker_vhost_sort' in list_sort_sources['docker'] and 'sortServiceRows' in list_sort_sources['docker']
|
|
assert 'redirect_list_sort' in list_sort_sources['redirects']
|
|
assert 'notfound_list_sort' in list_sort_sources['notfound']
|
|
assert 'stream_list_sort' in list_sort_sources['streams']
|
|
for marker in ['id="pool-search"', 'id="pool-sort"', 'data-pool-column-toggle="vhost"']:
|
|
assert marker in html, marker
|
|
for marker in ['https://git.linuxiarz.pl/gru/RustProxy', 'https://rustproxy.linuxiarz.pl/', 'id="about-python-runtime"', 'id="runtime-python"', 'id="acme-runtime-components"']:
|
|
assert marker in html, marker
|
|
for language in ['langs/en_US.json', 'langs/pl_pl.json']:
|
|
text = (ROOT / language).read_text(encoding='utf-8')
|
|
assert './dev.sh tls deploy' not in text
|
|
assert '/api/certificates/{certificate_id}/reload' in text
|
|
assert '/etc/letsencrypt/live/example.com/fullchain.pem' in text
|
|
startup_source = (ROOT / 'src/main/admin_runtime.rs').read_text(encoding='utf-8')
|
|
for marker in ['https://git.linuxiarz.pl/gru/RustProxy', 'https://rustproxy.linuxiarz.pl/']:
|
|
assert marker in startup_source, marker
|
|
assert 'Python: {} (ACME mode:' not in startup_source
|
|
for document in ['docs/RENEWAL.md', 'docs/INSTALL.md', 'docs/API.md']:
|
|
document_text = (ROOT / document).read_text(encoding='utf-8')
|
|
assert './dev.sh tls deploy' not in document_text
|
|
assert '/opt/rustproxy/current/rust-proxy --data-dir /var/lib/rustproxy tls deploy' in document_text
|
|
style = (ROOT / "src/admin/web/style.css").read_text(encoding="utf-8")
|
|
for marker in ['--scrollbar-thumb', '*::-webkit-scrollbar-thumb', '.backend-table-tools', '.vhost-quick-toggle', '.dashboard-widget-toggle', '--toolbar-control-height: min(var(--button-min-height), var(--ui-field-min-height))', 'height: var(--toolbar-control-height)']:
|
|
assert marker in style, marker
|
|
assert "pool-vhost-disabled" in vhosts
|
|
for marker in ["locationMatchRegex", "headersVisualBuilder", "headersAdvancedJson", "openLocationHelp", "cloudflare-client-ip", "timing-headers"]:
|
|
assert marker in vhosts, marker
|
|
for marker in ["location-help-dialog", "cache-memory-sparkline", "network-rx-sparkline", "latency-sparkline", "sourceIp"]:
|
|
assert marker in html, marker
|
|
for marker in ["lastApiLatencyMs", "cacheMemorySamples", "networkRxSamples", "responseLatencySamples"]:
|
|
assert marker in app, marker
|
|
assert '<details class="mini-live-chart mini-live-chart-collapsible">' not in html
|
|
assert 'data-i18n="responseLatencyHistory"' in html and 'data-i18n="responseLatencyHint"' in html and 'data-i18n-aria="responseLatencyInfo"' in html
|
|
assert 'bindSettingsForm("error-log-form"' in app and '}, "logging", selectLoggingSection);' in app
|
|
assert 'viewBox: "0 0 300 60"' in app and 'M0 15H300 M0 30H300 M0 45H300' in app
|
|
assert '.network-interface-row {' in css and 'background: transparent' in css
|
|
assert "hostTagEditor" in vhosts and "vhost-chart-line" in vhosts and "vhLifetimeChart" in vhosts
|
|
for marker in ["refreshCertificateInventory", "certificateDetails", "certificate-domain-pills", "certificateCoverageUnknown", "async function revalidateSession", 'Object.values(PANEL_PATHS), "/configuration"', "recoverInvalidOrigin", "auth.invalid_origin", "ORIGIN_RECOVERY_KEY"]:
|
|
assert marker in app, marker
|
|
for marker in ['data-certificate-tab="installed"', 'data-certificate-tab="renewal"', 'data-certificate-tab="generate"', 'id="certificate-page-installed"', 'id="certificate-page-renewal"', 'id="certificate-page-generate"']:
|
|
assert marker in html, marker
|
|
assert 'id="certificate-dialog"' not in html and 'id="certificate-close"' not in html
|
|
for marker in ['function selectTab(name, focus = false, persist = true)', 'log_directories', 'certbotLogs', 'value?.kind?.startsWith("renewal")']:
|
|
assert marker in acme_ui, marker
|
|
for marker in ['id="acme-runtime-card"', 'id="acme-runtime-message"', 'id="acme-runtime-state"', 'id="acme-runtime-path"', 'rust-proxy --data-dir ./data admin setup-token', 'rust-proxy --data-dir /var/lib/rustproxy admin setup-token']:
|
|
assert marker in html, marker
|
|
for marker in ['function renderRuntime(value)', 'runtimeAvailable', 'certificateRuntimeUnavailable']:
|
|
assert marker in acme_ui, marker
|
|
for marker in ['.certificate-operation-card', '.renewal-meta', '#panel-tls .certificate-tabs']:
|
|
assert marker in css, marker
|
|
login = (ROOT / "src/admin/web/login.js").read_text(encoding="utf-8")
|
|
settings_ui = (ROOT / "src/admin/web/settings.js").read_text(encoding="utf-8")
|
|
assert "async function redirectExistingSession" in login and "/api/auth/session" in login
|
|
assert '["/settings", "/configuration", "/logging"].includes(location.pathname)' in settings_ui
|
|
assert 'new URLSearchParams(location.search).get("tab")' in settings_ui and 'function syncLocation()' in settings_ui and 'history.replaceState' in settings_ui
|
|
assert '"/docker-vhosts"' in login and 'settingsTabs = new Set' in login and '`/settings?tab=${encodeURIComponent(tab)}`' in login
|
|
login_html = (ROOT / 'src/admin/web/login.html').read_text(encoding='utf-8')
|
|
assert 'class="auth-page auth-loading"' in login_html and 'document.body.classList.remove("auth-loading")' in login
|
|
assert 'class="app-loading"' in html and 'document.documentElement.classList.remove("app-loading")' in app
|
|
assert 'url.pathname = "/settings"' in settings_ui and 'url.searchParams.set("tab", active)' in settings_ui
|
|
assert 'function withFormSubmitState(form, submitter, task)' in app and 'button.textContent = t("saving")' in app
|
|
assert 'function fieldError(target, message)' in app and 'function showFieldError(error, form = null)' in app
|
|
assert 'function authReturnDestination()' in app and 'pendingAuthDestination || authReturnDestination()' in app
|
|
assert '$("login").hidden = navigating' in app and 'id="auth-message"' in html
|
|
assert 'settings-tabs button.has-unsaved::after' in css and 'function syncSettingsDirtyIndicators()' in app
|
|
assert 'setSettingsDirty("config-editor")' in app and '!settingsDirty.has("config-editor")' in app
|
|
assert 'throw fieldError($("tls-policy-12")' in settings_ui
|
|
assert 'input[type="checkbox"],\ninput[type="radio"] {' in css and 'width: 15px;' in css and '.log-column-option input[type="checkbox"]' not in css and 'overflow: visible' in css and 'max-height: min(38dvh, 220px)' not in css
|
|
assert '.log-tools input:not([type="checkbox"]):not([type="radio"])' in css
|
|
assert '.log-tools input {\n' not in css
|
|
footer = app.split('function renderFooter(s) {', 1)[1].split('let lastLogs', 1)[0]
|
|
assert 'SQLite / HTTP/1.1' not in footer and 'coldQ:' not in footer and 'streamFallback' not in footer
|
|
assert '`${apiLatency} / ${t("logsDropped")}: ${s.logs_dropped}`' in footer
|
|
assert 'update?.update_available && update.latest_version' in footer and 'footer-update' in footer
|
|
assert '#docker-agents-form>.form-actions {' in css and 'justify-content: flex-start' in css
|
|
for marker in ['prom-trusted-no-token', 'prom-streams', 'prom-logging']:
|
|
assert marker in html and marker in settings_ui, marker
|
|
for marker in [".certificate-details", ".certificate-domain-pills", ".certificate-card .pill"]:
|
|
assert marker in css, marker
|
|
integrations_0194 = (ROOT / "src/admin/web/integrations.js").read_text(encoding="utf-8")
|
|
docker_0194 = (ROOT / "src/admin/web/docker.js").read_text(encoding="utf-8")
|
|
assert 'readAddresses.input' in integrations_0194
|
|
assert 'form.reset(); window.rustProxyCertificates?.close();' in integrations_0194
|
|
assert '$("cert-import-pem").value = ""' not in integrations_0194 and '$("cert-import-key").value = ""' not in integrations_0194
|
|
assert 'throw fieldError(row.querySelector(".docker-agent-listener-choices")' in docker_0194
|
|
management = (ROOT / "src/admin/web/management.js").read_text(encoding="utf-8")
|
|
assert "rustProxyAclProfiles" in management and "acl_profiles" in management
|
|
for marker in ["collectRuleRows", "parseBulkRules", "allow_exceptions", "block_exceptions", "renderPreview", "normalizeNetwork"]:
|
|
assert marker in management, marker
|
|
assert "function refreshTranslations()" in management and "refreshTranslations }" in management
|
|
assert "window.rustProxyAclProfiles?.refreshTranslations?.();" in app
|
|
assert "normalizeAclRuleText" in app and "ruleSyntaxLine" in app
|
|
assert ".settings-tabs" in css and "grid-template-columns" in css and "width: 100%" in css
|
|
assert ".logging-grid fieldset" not in css
|
|
assert ".pool-vhost-disabled" in css and ".acl-profile-rule" in css
|
|
assert ".stream-live-grid" in css and ".stream-chart-rx" in css and ".listener-address-chip-editor" in css
|
|
for marker in ['.vh-backend-card', '.auth-subcard', '.vh-cache-options[hidden]', '.protocol-toggle-row', '.listener-choices']:
|
|
assert marker in css, marker
|
|
assert html.count('<h3 class="subheading"') >= 3
|
|
|
|
|
|
def source_contract_smoke() -> None:
|
|
config = rust_source(ROOT, "src/config.rs")
|
|
tls = rust_source(ROOT, "src/tls.rs")
|
|
main = rust_source(ROOT, "src/main.rs")
|
|
server = rust_source(ROOT, "src/server.rs")
|
|
logging = rust_source(ROOT, "src/logging.rs")
|
|
secret_store = rust_source(ROOT, "src/secret_store.rs")
|
|
backup = rust_source(ROOT, "src/backup.rs")
|
|
acme_worker = (ROOT / "scripts/runtime/acme_provider.py").read_text(encoding="utf-8")
|
|
for feature_marker in ["AES_256_GCM", "master.key", "O_NOFOLLOW", "migrate_acme_credentials"]:
|
|
assert feature_marker in secret_store, feature_marker
|
|
for feature_marker in ["Argon2", "RPROXYB1", "secret-store-key.enc", "validate_backup_version"]:
|
|
assert feature_marker in backup, feature_marker
|
|
for feature_marker in ["ErrorLogSettings", "ERROR_LOG_QUEUE", "RedactingStderr", "open_error_file", "ErrorLogLayer"]:
|
|
assert feature_marker in logging, feature_marker
|
|
assert "credential_ini_b64" in acme_worker and "credential_file.unlink" in acme_worker
|
|
cache = read_cache_source()
|
|
settings = rust_source(ROOT, "src/settings.rs")
|
|
websocket = rust_source(ROOT, "src/proxy/websocket.rs")
|
|
proxy = rust_source(ROOT, "src/proxy/mod.rs")
|
|
assert '.insert("x-rustproxy-backend-time", value)' in proxy and '.insert("x-rust-proxy-backend-time", value)' not in proxy
|
|
pages = rust_source(ROOT, "src/pages.rs")
|
|
admin = rust_source(ROOT, "src/admin/mod.rs")
|
|
stream = rust_source(ROOT, "src/stream.rs")
|
|
runtime = rust_source(ROOT, "src/runtime.rs")
|
|
acl = rust_source(ROOT, "src/acl.rs")
|
|
policies = (ROOT / "tests/policies.rs").read_text(encoding="utf-8")
|
|
http3 = rust_source(ROOT, "src/http3.rs")
|
|
redirect = rust_source(ROOT, "src/redirect.rs")
|
|
not_found = rust_source(ROOT, "src/not_found.rs")
|
|
prometheus = rust_source(ROOT, "src/prometheus.rs")
|
|
auth = rust_source(ROOT, "src/auth/mod.rs")
|
|
auth_store = rust_source(ROOT, "src/auth/store.rs")
|
|
upstream = rust_source(ROOT, "src/upstream.rs")
|
|
vhosts_js = read_vhosts_js(ROOT)
|
|
docker_source = rust_source(ROOT, "src/docker.rs")
|
|
docker_js = (ROOT / "src/admin/web/docker.js").read_text(encoding="utf-8")
|
|
docker_agent = read_docker_agent_source()
|
|
html = (ROOT / "src/admin/web/index.html").read_text(encoding="utf-8")
|
|
cache_panel_ui = (ROOT / "src/admin/web/cache-panel.js").read_text(encoding="utf-8")
|
|
transform_panel = (ROOT / "src/admin/web/transform-panel.js").read_text(encoding="utf-8")
|
|
app_js = read_app_js(ROOT)
|
|
|
|
# 0.24.1: shared library statistics, authenticated runtime metadata and Logging in Settings.
|
|
manifest = tomllib.loads((ROOT / "Cargo.toml").read_text(encoding="utf-8"))
|
|
system_stats = rust_source(ROOT, "src/system_stats/mod.rs")
|
|
linux_stats = rust_source(ROOT, "src/system_stats/linux.rs")
|
|
settings_ui = (ROOT / "src/admin/web/settings.js").read_text(encoding="utf-8")
|
|
openapi_0241 = (ROOT / "src/admin/openapi.json").read_text(encoding="utf-8")
|
|
assert not (ROOT / "src/system_stats/bsd.rs").exists()
|
|
metrics_process = manifest["dependencies"]["metrics-process"]
|
|
assert metrics_process["version"] == "=2.4.3"
|
|
assert metrics_process["default-features"] is False
|
|
assert metrics_process["features"] == ["dummy"]
|
|
sysinfo_network = manifest["dependencies"]["sysinfo"]
|
|
assert sysinfo_network["version"] == "=0.38.4"
|
|
assert sysinfo_network["default-features"] is False
|
|
assert sysinfo_network["features"] == ["network"]
|
|
target_key = 'cfg(any(target_os = "linux", target_os = "freebsd", target_os = "openbsd", target_os = "netbsd", target_os = "dragonfly", target_os = "macos", target_os = "windows"))'
|
|
assert manifest["target"][target_key]["dependencies"]["systemstat"] == "=0.2.7"
|
|
for marker in [
|
|
"systemstat::{Platform, System}",
|
|
"metrics_process::collector::collect",
|
|
"sysinfo::Networks::new_with_refreshed_list",
|
|
"collect_network_systemstat",
|
|
"merge_network_interfaces",
|
|
"system_swap_total_bytes",
|
|
"system_uptime_seconds",
|
|
"process_virtual_memory_bytes",
|
|
"process_start_time_seconds",
|
|
"max_fds",
|
|
]:
|
|
assert marker in system_stats, marker
|
|
for forbidden in ["sysctl", "netstat", "Command::new"]:
|
|
assert forbidden not in system_stats, forbidden
|
|
for marker in ["/proc/stat", "/proc/meminfo", "RssAnon", "VmData", "VmSwap", "pub(crate) fn augment"]:
|
|
assert marker in linux_stats, marker
|
|
for marker in [
|
|
"LinuxEnvironment::Lxc", "process_start_ticks", "CLOCK_BOOTTIME", "/proc/1/stat",
|
|
"/proc/loadavg", "lxc_load_average", "view.environment = Some(environment.label())",
|
|
"if environment == LinuxEnvironment::Lxc",
|
|
]:
|
|
assert marker in linux_stats, marker
|
|
assert 'const processPlatform = [d.platform, d.environment].filter(Boolean).join(" / ");' in app_js
|
|
for marker in ["installed_data_dir_candidates", "resolve_installed_data_dir", '"/var/lib/rustproxy"', '"/var/db/rustproxy"']:
|
|
assert marker in main, marker
|
|
for marker in ['payload["runtime"]', '"statistics_source"', '"data_directory"', '"database_path"']:
|
|
assert marker in admin, marker
|
|
anonymous_block = admin.split("fn anonymous_allowed", 1)[1].split("const CERTIFICATE_API_KEY_HEADER", 1)[0]
|
|
assert '"/api/status"' not in anonymous_block
|
|
assert '"RuntimeInformation"' in openapi_0241 and '"version": "0.29.0"' in openapi_0241
|
|
assert 'id="panel-logging"' not in html
|
|
for marker in ['id="settings-tab-logging"', 'id="settings-page-logging"', 'id="logging-form"', 'id="error-log-form"']:
|
|
assert marker in html, marker
|
|
assert 'location.pathname === "/logging"' in settings_ui
|
|
|
|
# 0.22.3 h2c Upgrade and externally owned certificate deployment contracts.
|
|
for marker in [
|
|
'StatusCode::SWITCHING_PROTOCOLS',
|
|
'struct H2cUpgradeIo',
|
|
'HTTP2-Settings',
|
|
'http1_h2c_upgrade_bootstraps_request_as_http2_stream_one',
|
|
'malformed_h2c_upgrade_is_rejected_before_switching_protocols',
|
|
]:
|
|
assert marker in server, marker
|
|
for marker in [
|
|
'"/api/certificates/reload"',
|
|
'certificate_reload',
|
|
'"/api/certificates/deploy"',
|
|
'certificate_deploy',
|
|
'certificate.deploy',
|
|
'certificate.reload',
|
|
'reload_certificates().await',
|
|
'"operation": "unchanged"',
|
|
'config.tls.certificates.push(crate::tls::Certificate',
|
|
]:
|
|
assert marker in admin, marker
|
|
for marker in ['pub async fn reload_certificates', 'configured certificates reloaded from disk']:
|
|
assert marker in runtime, marker
|
|
for marker in ['TlsCommand::Deploy', '/api/certificates/deploy', '/api/certificates/reload', 'read_external_pem']:
|
|
assert marker in main, marker
|
|
for marker in ['pub fn stage_external_pair', 'pub fn reconcile_external_pair', 'ExternalPairDeployment::Unchanged', 'pub fn remove_external_pair', 'external_pair_stages_generation_and_rolls_back_cleanly', 'external_pair_reconcile_is_idempotent_and_repairs_missing_generation']:
|
|
assert marker in tls, marker
|
|
assert 'api("/api/certificates/reload"' in app_js
|
|
transport_0223 = (ROOT / "docs/TRANSPORT.md").read_text(encoding="utf-8")
|
|
assert 'Upgrade: h2c' in transport_0223 and 'stream 1' in transport_0223
|
|
assert 'legacy HTTP/1.1 Upgrade-to-h2c is not implemented' not in transport_0223
|
|
audit_0233 = (ROOT / "docs/V0.24.0_AUDIT.md").read_text(encoding="utf-8")
|
|
audit_0232 = (ROOT / "docs/V0.23.2_AUDIT.md").read_text(encoding="utf-8")
|
|
audit_0223 = (ROOT / "docs/V0.22.3_AUDIT.md").read_text(encoding="utf-8")
|
|
for marker in ["legacy websocket", "origin", "http/1.1", "sec-websocket-accept", "ilo4"]:
|
|
assert marker.lower() in audit_0233.lower(), marker
|
|
for marker in ["600 samples", "308", "HTTPS", "<data-dir>/certs", "--livelogs"]:
|
|
assert marker.lower() in audit_0232.lower(), marker
|
|
assert 'external certificate ownership model' in audit_0223.lower() and 'release gate' in audit_0223.lower()
|
|
|
|
update_check = rust_source(ROOT, "src/update_check.rs")
|
|
update_defaults = tomllib.loads((ROOT / "UPDATE_CHECK.toml").read_text(encoding="utf-8"))
|
|
assert update_defaults["repository_url"] == "https://git.linuxiarz.pl/gru/RustProxy"
|
|
assert update_defaults["version_url"].endswith("/raw/branch/master/Cargo.toml")
|
|
assert update_defaults["interval_seconds"] == 3600
|
|
for marker in ['include_str!("../UPDATE_CHECK.toml")', 'pub async fn poll_loop', 'tokio::time::sleep(interval)', 'package_version(&document)', 'MAX_VERSION_DOCUMENT_BYTES']:
|
|
assert marker in update_check, marker
|
|
assert 'update version document is too large or incomplete: {error}' in update_check
|
|
assert '.context("update version document is too large or incomplete")' not in update_check
|
|
for marker in ['tracing::warn!(', 'MAX_REDIRECTS: usize = 5', 'SSL_CERT_FILE', 'CURL_CA_BUNDLE', '/usr/local/share/certs/ca-root-nss.crt', 'response.status().is_redirection()', 'redirect_target(¤t, location)']:
|
|
assert marker in update_check, marker
|
|
assert 'pub last_error:' not in update_check
|
|
for marker in ['RUST_PROXY_UPDATE_REPOSITORY_URL', 'RUST_PROXY_UPDATE_VERSION_URL', 'RUST_PROXY_UPDATE_CHECK_INTERVAL_SECS', 'state.start_update_check(update_check);']:
|
|
assert marker in main, marker
|
|
assert 'pub software_update: Arc<crate::update_check::SharedStatus>' in runtime
|
|
assert 'payload["software_update"] = json!(state.software_update.snapshot());' in admin
|
|
|
|
for marker in [
|
|
"a persisted remote administration listener requires administration TLS",
|
|
"validate_narrower_than",
|
|
"tls_policy",
|
|
"AclProfile",
|
|
"normalize_host_pattern",
|
|
"profile_country_access",
|
|
"https_redirect_port",
|
|
"forward_client_ip",
|
|
"plain HTTP listener",
|
|
"tls_ports.contains(&443)",
|
|
"backend_tls_verification_defaults_off_and_can_be_enabled_explicitly",
|
|
"pub accept_local_listener_risk: bool",
|
|
"backend_diagnostic_flags_compile_to_frontend",
|
|
"backend_header: host.backend_header",
|
|
"backend_timing_header: host.backend_timing_header",
|
|
]:
|
|
assert marker in config, marker
|
|
for marker in ["compile_admin", "ProtocolPolicy", "policy_only_configuration_does_not_require_certificate_directory", "certificate_covered_hosts", "legacy_certificate_coverage_can_be_read_from_existing_pem"]:
|
|
assert marker in tls, marker
|
|
assert "compile_admin" in main and "serve_tls_with_plain_redirect" in main
|
|
assert "TlsMode::Fixed" in server and "TlsMode::Frontend" in server and "plaintext_router" in server
|
|
assert "additional_addresses" in settings and "multi-address listeners require explicit IPs" in settings
|
|
for marker in ["pub session_days: u16", "session_days: 30", "(7..=30).contains(&self.session_days)", "administration_session_lifetime_is_7_to_30_days_and_hot_apply_only"]:
|
|
assert marker in settings, marker
|
|
for marker in ["30 * 24 * 60 * 60", "browser_session_lifetime_is_configurable_without_sliding_expiry", "session_cookie_uses_configured_remaining_lifetime"]:
|
|
assert marker in auth, marker
|
|
for marker in ["create_browser_session", "session_at_with_browser_lifetime", "browser session lifetime must be 7..30 days"]:
|
|
assert marker in auth_store, marker
|
|
for marker in ["accept_local_listener_risk", "!self_target || self.accept_local_listener_risk", "explicit_risk_acceptance_allows_local_listener_but_not_private_policy_bypass"]:
|
|
assert marker in upstream, marker
|
|
assert "member-local-risk" in vhosts_js and "vhLocalListenerRiskConfirm" in vhosts_js
|
|
assert 'function administrationListenCandidates()' in vhosts_js and 'function adminListenerBackendCandidate(raw)' in vhosts_js
|
|
for marker in ['latestStatus?.administration?.locked_listen', 'latestStatus?.administration?.active?.listen', 'latestStatus?.admin_listen', "value === 'localhost'", '/^127(?:\\.\\d{1,3}){3}$/.test(value)', 'riskGate.hidden = show !== true', 'row.syncLocalRisk = () => syncRisk(false)']:
|
|
assert marker in vhosts_js, marker
|
|
assert 'const saved = editing ? (envelope?.config.vhosts || []).find(item => item.id === editing) : null;' in vhosts_js
|
|
assert 'load(saved ? structuredClone(saved) : defaults());' in vhosts_js
|
|
assert 'backends: [{ id: "origin-1", url: ""' in vhosts_js
|
|
assert 'preserve_host: true' in vhosts_js
|
|
for marker in ['vhBackendDiagnostics', 'backend_header: yes("backend-header")', 'backend_timing_header: yes("backend-timing-header")', 'protocol-toggle-row', 'auth-subcard', 'vh-cache-options', 'syncCacheVisibility', 'cache: { enabled: false }']:
|
|
assert marker in vhosts_js, marker
|
|
assert 'values.length ? values : [{ id: "root"' not in vhosts_js
|
|
for marker in ["PinnedCertificateVerifier", "tls_fingerprint_sha256", "health_check: crate::balance::HealthCheck", "certificate fingerprint mismatch", "generated_id"]:
|
|
assert marker in docker_source, marker
|
|
for marker in ["docker-agent-fingerprint", "docker-agent-certificate", "docker-health-mode", "docker-search", "docker-show-hidden", "pool_routes", "dockerBackendUp", "setInterval"]:
|
|
assert marker in docker_js, marker
|
|
assert 'function checkboxField(key, control)' in docker_js
|
|
assert 'checkboxField("dockerPreserveHost", preserve)' in docker_js and 'checkboxField("dockerAclInline", inline)' in docker_js
|
|
assert 'label = el("label", undefined, "check vh-check")' in docker_js
|
|
ui_css = (ROOT / "src/admin/web/style.css").read_text(encoding="utf-8")
|
|
assert '.docker-route-grid input:not([type="checkbox"])' in ui_css and '.docker-checkbox-field .vh-check {' in ui_css
|
|
assert 'docker-switch-row' in docker_js and '.docker-switch-row {' in ui_css and 'repeat(5, minmax(110px, 160px))' in ui_css
|
|
assert '.docker-passive-health-editor .docker-health-grid {' in ui_css
|
|
assert '.docker-health-grid input:not([type="checkbox"])' in ui_css and '.docker-passive-health-editor .docker-checkbox-field input[type="checkbox"]' in ui_css
|
|
assert 'id="sparkline-transform"' in html and 'id="rate-transform"' in html
|
|
assert 'id="panel-transform"' in html and 'data-panel="transform"' in html and '/assets/transform-panel.js' in html
|
|
assert '/api/transform' in transform_panel and 'tp-cache-hits' in transform_panel
|
|
for marker in ['transform-memory-budget', 'transform-max-inflight', 'data-transform-preset="conservative"', 'data-transform-preset="recommended"', 'data-transform-preset="performance"', 'data-transform-preset="uber"']:
|
|
assert marker in html, marker
|
|
for marker in ['selectedTransformPreset', 'transformPresetValues', 'applyTransformPreset', 'updateTransformResourceSizing', 'memory_budget_bytes', 'max_inflight']:
|
|
assert marker in app_js, marker
|
|
assert '<details class="card function-live-chart" id="cache-live-chart">' in html
|
|
assert '<details class="card function-live-chart" id="transform-live-chart">' in html
|
|
for marker in ['cache-live-hit-line', 'cache-live-miss-line', 'cache-live-bypass-line']:
|
|
assert marker in html and marker in cache_panel_ui, marker
|
|
for marker in ['transform-live-applied-line', 'transform-live-cache-line', 'transform-live-skipped-line', 'transform-live-error-line']:
|
|
assert marker in html and marker in transform_panel, marker
|
|
assert 'sparklinePoints(liveChartSamples.hits, 900, 180, 10, peak)' in cache_panel_ui
|
|
assert 'sparklinePoints(liveChartSamples.applied, 900, 180, 10, peak)' in transform_panel
|
|
assert '.function-live-chart svg {' in ui_css and 'height: clamp(180px, 22vw, 270px)' in ui_css
|
|
assert '--transform-chart:' in ui_css and '--chart-quaternary:' in ui_css and 'stroke: var(--chart-quaternary)' in ui_css and 'background: var(--transform-chart)' in ui_css
|
|
assert '.cache-transform-badge {' in ui_css and 'border: 1px solid var(--pill-line)' in ui_css and 'border-radius: 3px' in ui_css
|
|
html = (ROOT / 'src/admin/web/index.html').read_text(encoding='utf-8')
|
|
cache_panel_js = (ROOT / 'src/admin/web/cache-panel.js').read_text(encoding='utf-8')
|
|
management_js = (ROOT / 'src/admin/web/management.js').read_text(encoding='utf-8')
|
|
app_js = read_app_js(ROOT)
|
|
vhosts_js = read_vhosts_js(ROOT)
|
|
cache_rs = read_cache_source()
|
|
admin_rs = rust_source(ROOT, 'src/admin/mod.rs')
|
|
logging_rs = rust_source(ROOT, 'src/logging.rs')
|
|
assert 'bindSettingsForm("pages-form", next => { next.pages = global.read(); }, "pages")' in management_js
|
|
assert '"pages" => {' in admin_rs and 'default_pages_section_accepts_the_standard_value_payload' in admin_rs
|
|
assert 'pub const LIVE_WINDOW: usize = 256;' in logging_rs and 'pub const HISTORY_CAPACITY: usize = 4096;' in logging_rs
|
|
assert 'api("/api/logs?limit=4096")' in management_js
|
|
assert 'const STATUS_NAMES = { 200: "OK"' in management_js and '404: "Not Found"' in management_js and 'statusLabel(code)' in management_js
|
|
assert 'let paused = null, expanded = null' in management_js and 'function mergeExpanded(data)' in management_js and 'entries.length >= 4096' in management_js
|
|
assert 'dialog.addEventListener("close", () => { if (pinning) return; expanded = null; paused = null;' in management_js
|
|
assert 'hash_scheme.as_bytes()' in cache_rs and 'cache_key_separates_frontend_scheme' in cache_rs
|
|
assert not (ROOT / 'src/cache.rs').exists() and (ROOT / 'src/cache/mod.rs').is_file()
|
|
cache_mod = rust_source(ROOT, 'src/cache/mod.rs')
|
|
assert 'impl Cache {\n include!(' not in cache_mod
|
|
for fragment in ['lifecycle.rs', 'cold.rs', 'dashboard.rs', 'tickets.rs', 'lookup.rs']:
|
|
assert rust_source(ROOT, f'src/cache/{fragment}').lstrip().startswith('impl Cache {')
|
|
assert 'payload["transform_metrics"] = json!(state.transform_metrics.total())' in admin_rs
|
|
for marker in ['payload["transform_resources"]', 'payload["memory_accounting"]', 'non_cache_rss_includes_transform', 'cold_limit_bytes', 'cold_limit_entries']:
|
|
assert marker in admin_rs or marker in cache, marker
|
|
assert 'const transformed = s.transform_metrics || {}' in app_js and 'transformSamples.push(rates.transform)' in app_js
|
|
for marker in ['status.memory_accounting || {}', 'memory.cache_hot_bytes', 'memory.transform_reserved_bytes', 'cache-cold-storage']:
|
|
assert marker in app_js, marker
|
|
for marker in ['data?.resources || {}', 'tp-memory', 'tp-inflight']:
|
|
assert marker in transform_panel, marker
|
|
assert 'function uptime(value)' in app_js and 'setOptionalMetric("process-uptime", status.uptime_seconds != null' in app_js
|
|
assert 'connection-uptime-footer' not in html and 'id="process-uptime"' in html
|
|
assert 'function rateBytes(value)' in app_js and 'if (value === 0) return "0.0 KiB";' in app_js and 'return "<0.1 KiB";' in app_js
|
|
assert 'function backendTrafficRate(' in vhosts_js and 'member.bytes_in' in vhosts_js and 'member.bytes_out' in vhosts_js and 'rateBytes(memberTraffic)' in vhosts_js and 'rateBytes(d.traffic)' in vhosts_js
|
|
assert 'id="cache-domain-filter"' in html and 'cacheDomainFilterEnabled' in html and 'cacheDomainFilterDisabled' in html
|
|
assert 'const visibleDomains = (data, groups) =>' in cache_panel_js and 'scoped?.settings?.enabled ?? data.settings?.enabled ?? false' in cache_panel_js
|
|
for marker in ['effectiveCacheMemory', 'cacheMemoryHeadroom', 'cachePresetValues', 'applyCacheMemoryPreset', 'selectedCacheMemoryPreset', 'selectedCacheColdPreset', 'applyRecommendedColdCachePreset', 'cacheMemoryPresetTooSmall', 'cacheObjectOverTransientWarning', 'cgroup_memory_max_bytes', 'transient_max_bytes', 'cold_queue_max_bytes', 'hot_eviction_max_objects']:
|
|
assert marker in app_js, marker
|
|
assert 'envelope?.config.cache?.enabled === true && cacheToggle.value !== "off"' in vhosts_js
|
|
assert 'groups = d.domains || [], shownGroups = visibleDomains(data, groups)' in cache_panel_js and 'replaceChildren(...shownGroups.map(row =>' in cache_panel_js
|
|
balance_rs = rust_source(ROOT, 'src/balance.rs')
|
|
assert 'traffic_rate_drops_to_zero_after_an_idle_sample_window' in balance_rs and 'if delta == 0 {' in balance_rs and 'sample.rate = 0.0;' in balance_rs
|
|
all_admin_js = "\n".join(path.read_text(encoding='utf-8') for path in (ROOT / 'src/admin/web').glob('*.js'))
|
|
assert not re.search(r'(?<![A-Za-z])(?:window\.)?(?:alert|confirm)\s*\(', all_admin_js)
|
|
assert 'function appConfirm(message, options = {})' in app_js and 'function apiErrorMessage(path, response, value)' in app_js
|
|
deploy_py = read_deploy_source()
|
|
assert "SHELL_PATH_PROFILE = Path('/etc/profile.d/rustproxy-path.sh')" in deploy_py and 'install_shell_path(prefix)' in deploy_py and 'check_shell_path(prefix)' in deploy_py
|
|
assert 'grid-column: span 3' in ui_css and '.check>span {' in ui_css and 'transform: translateY(-1px)' in ui_css
|
|
assert '.cache-toolbar-status .check' in ui_css and '.docker-toolbar .check' in ui_css and '.docker-alias-field>span:first-child {' in ui_css and 'margin-bottom: 0' in ui_css and '.docker-alias-editor {' in ui_css and 'min-height: 38px' in ui_css and 'height: 30px' in ui_css
|
|
assert 'readOnly: !!editing' not in vhosts_js and 'vhIdRenameConfirm' in vhosts_js and 'v.id === value.id && v.id !== editing' in vhosts_js
|
|
assert 'enforce_local_page_no_cache' in proxy and 'LocalGeneratedResponse' in proxy and 'locally_generated_pages_remove_cache_metadata' in proxy
|
|
assert 'local_page_method_rejection_is_still_marked_non_cacheable' in pages and 'response.extensions_mut().insert(PageServed);' in pages
|
|
screenshots = {path.name for path in (ROOT / 'docs/screenshots').iterdir() if path.is_file()}
|
|
assert 'README.md' in screenshots
|
|
assert screenshots <= {'README.md', 'login.png', 'location-headers.png'}
|
|
assert 'generated_vhost_inherits_global_server_header_policy' in rust_source(ROOT, 'src/docker.rs')
|
|
assert '.docker-route-grid {' in ui_css and 'align-items: start' in ui_css
|
|
assert '.docker-acl-grid {' in ui_css and 'height: 38px' in ui_css
|
|
assert 'preserve.checked = assignment?.preserve_host !== false' in docker_js
|
|
for marker in ["ensureSelfSignedCertificate", "ListenAndServeTLS", "CertificateSHA256", "RUSTPROXY_DOCKER_AGENT_TLS"]:
|
|
assert marker in docker_agent, marker
|
|
for marker in [
|
|
'fallbacks: HashMap<String, Vec<CompiledRoute>>',
|
|
'id: format!("{}/main", host.id)',
|
|
'custom_locations_override_but_main_vhost_remains_fallback',
|
|
'direct_admin_listener_backend_is_marked_for_guarded_control_plane_use',
|
|
'pub targets_admin_listener: bool',
|
|
]:
|
|
assert marker in config, marker
|
|
vhost_source = rust_source(ROOT, "src/vhost.rs")
|
|
assert 'is_legacy_main_fallback' in vhost_source
|
|
assert "benign_tunnel_close" in websocket and "ConnectionReset" in websocket
|
|
assert 'StatusCode::UNAUTHORIZED => Some(include_str!("admin/web/error_401.html"))' in pages
|
|
assert 'StatusCode::NOT_FOUND => Some(include_str!("admin/web/error_404.html"))' in pages
|
|
assert 'pub struct MinimalWelcome' in pages and 'builtin_welcome_is_minimal_and_never_cacheable' in pages and 'HeaderValue::from_static("no-store")' in pages
|
|
for marker in [
|
|
"fn admin_request_authority",
|
|
"Version::HTTP_2 | Version::HTTP_3",
|
|
"request.uri().authority().ok_or(())?",
|
|
"administration_https_health_accepts_http2_authority_without_host_header",
|
|
]:
|
|
assert marker in admin, marker
|
|
assert "fn panel_document_path" in admin and "panel_document_path(&path)" in admin and "panel_subpage_routes_survive_browser_refresh" in admin
|
|
assert 'response.extensions().get::<LocalHostResponse>().is_none()' in proxy
|
|
assert '"cdn-cache-control"' in proxy and '"x-cache-resident-age"' in proxy
|
|
for marker in ["OriginBackend", "apply_backend_diagnostic_headers", "x-rustproxy-backend-time", "backend_diagnostics_are_opt_in_and_strip_spoofed_values"]:
|
|
assert marker in proxy, marker
|
|
for marker in ["force_ssl_redirect", "force_ssl_location", "StatusCode::PERMANENT_REDIRECT", "https_redirect_port", "apply_upstream"]:
|
|
assert marker in proxy, marker
|
|
forwarding = rust_source(ROOT, "src/forwarding.rs")
|
|
for marker in ["pub fn apply_upstream", 'headers.remove("x-forwarded-for")', 'headers.remove("x-real-ip")', "upstream_can_withhold_client_ip_without_losing_origin_metadata"]:
|
|
assert marker in forwarding, marker
|
|
for marker in ["SINK_QUEUE", "FileSink", "LogstashSink", "LokiSink", "Limited::new"]:
|
|
assert marker in logging, marker
|
|
for marker in [
|
|
"matching_body_bytes", "matching_resident_bytes", "matching_cold_bytes",
|
|
"expose_resident_age", "x-cache-resident-age", "COLD_QUEUE",
|
|
"ColdFile", "body_sha256", "get_tiered", "revalidation_tiered",
|
|
"cold_queue_dropped_total", "cold_after_seconds", "cold_on_age", "cold_max_hot_seconds", "cold_policy_can_demote_by_total_hot_residency_age",
|
|
"lru_demotion_persists_and_survives_restart",
|
|
"managed_update_flush_ignores_shutdown_toggle_when_cold_tier_is_enabled",
|
|
"flush_hot_to_cold_for_update",
|
|
"disabled_cache_strips_diagnostics_without_exposing_bypass_headers",
|
|
"vary_selects_variants_without_fragmenting_the_primary_hash",
|
|
"vary_schema_change_removes_incompatible_fallbacks",
|
|
"varnish_primary_hash_uses_host_and_url_not_unrelated_request_headers",
|
|
"resident_age_survives_successful_revalidation",
|
|
"COLD_FORMAT_VERSION: u8 = 3",
|
|
"cold_cache_uses_two_hex_character_shards",
|
|
"legacy_flat_cold_cache_is_not_migrated",
|
|
"manual_cold_cleanup_reports_removed_objects_and_freed_bytes",
|
|
"rfc5861_windows_and_revalidation_flags_are_separate_metadata",
|
|
"request_max_stale_is_bounded_and_respects_revalidation_flags",
|
|
"max_stale_can_reuse_an_object_after_normal_ttl_expiry",
|
|
"request_max_age_only_revalidates_when_cached_age_exceeds_limit",
|
|
"transformed_plan_uses_transformed_content_length",
|
|
"transformed_representation_is_stored_and_reused",
|
|
"every_enabled_transform_kind_can_fill_and_reuse_the_cached_representation",
|
|
"query_strings_remain_distinct_and_bypass_query_is_only_a_request_policy",
|
|
"transformed_client_conditionals_use_the_representation_etag_not_the_origin_etag",
|
|
"no_transform_does_not_disable_shared_storage",
|
|
"duplicate_and_argument_immutable_do_not_invalidate_cache_control",
|
|
"fill_incomplete", "transform_length_mismatch", "object_too_large",
|
|
"cold_cleanup_skips_an_entry_while_a_read_is_pinned",
|
|
"stale_if_error_is_only_available_inside_its_window",
|
|
"matching_hot_key",
|
|
"matching_cold_key",
|
|
]:
|
|
assert marker in cache, marker
|
|
transform = rust_source(ROOT, "src/transform/mod.rs")
|
|
for marker in [
|
|
"pub struct Settings", "pub struct Overrides",
|
|
"resolved.enabled = global.enabled && self.enabled",
|
|
"pub fn no_transform", "OriginValidators", "fn decode<'a>",
|
|
"Cow::Borrowed(body)", "trim_allocator_after_large_transform",
|
|
"pub memory_budget_bytes: usize", "pub max_inflight: usize", "Preset::Uber",
|
|
"try_reserve_transform_memory(required_memory, settings.memory_budget_bytes)",
|
|
"try_reserve_transform_slot(settings.max_inflight)",
|
|
"drop(decoded);", "drop(raw);",
|
|
"rustproxy-",
|
|
"unchanged_compressed_body_is_canonicalized_to_identity",
|
|
"origin_no_transform_blocks_body_changes_and_keeps_validator",
|
|
"final_no_transform_blocks_body_changes",
|
|
"compressed_no_transform_preserves_origin_encoding_and_bytes",
|
|
"empty_head_body_with_origin_length_keeps_origin_representation_headers",
|
|
"CanonicalIdentity",
|
|
]:
|
|
assert marker in transform, marker
|
|
assert 'collect bounded static response for transformation: {error}' in transform
|
|
assert '.context("collect bounded static response for transformation")' not in transform
|
|
lookup = rust_source(ROOT, 'src/cache/lookup.rs')
|
|
model = rust_source(ROOT, 'src/cache/model.rs')
|
|
css_minifier = rust_source(ROOT, 'src/transform/css.rs')
|
|
assert 'let Some(_read_lease) = self.try_reserve_transient' in lookup
|
|
assert 'struct TransientLease {\n _inner: Arc<TransientLeaseInner>,' in model
|
|
assert 'struct TransientLease(Arc<TransientLeaseInner>);' not in model
|
|
assert 'last_char = out.chars().last();' not in css_minifier
|
|
assert css_minifier.count("last_char = Some(' ');") == 1
|
|
assert "pub mod transform;" in rust_source(ROOT, "src/lib.rs")
|
|
for name in ["js.rs", "css.rs"]:
|
|
minifier = (ROOT / "src/transform" / name).read_text(encoding="utf-8")
|
|
assert "Vec<char>" not in minifier and ".chars().collect" not in minifier
|
|
assert "into_bytes()" in minifier
|
|
balance = rust_source(ROOT, "src/balance.rs")
|
|
for marker in [
|
|
"pub struct PassiveHealth", "enabled: false", "pub passive_health: PassiveHealth",
|
|
"passive_health_is_opt_in_and_uses_separate_thresholds",
|
|
"legacy_settings_without_passive_health_default_to_off",
|
|
"slow_5xx_counts_both_observability_classes_when_passive_is_off",
|
|
"unhealthy_source_hands_off_when_the_original_health_domain_recovers",
|
|
"active_health_failures_total", "passive_health_failures_total",
|
|
"http_5xx_total", "timeouts_total", "connect_errors_total", "io_errors_total", "slow_responses_total",
|
|
]:
|
|
assert marker in balance, marker
|
|
for marker in [
|
|
'fill_bytes_active', 'cold_queue_items', 'cold_queue_bytes',
|
|
'cold_write_bytes_active', 'cold_read_bytes_active', 'transient_bytes',
|
|
'transient_max_bytes', 'cold_queue_max_bytes', 'hot_eviction_max_objects',
|
|
'hot_logical_bytes', 'hot_allocated_bytes', 'memory_pressure_rejections',
|
|
'struct FillGauge', 'struct TransientLease', 'reserve_fill_allocation',
|
|
'release_allocator_memory()', 'fn hot_entry_cost(',
|
|
]:
|
|
assert marker in cache, marker
|
|
assert 'impl Drop for Candidate' not in cache
|
|
diagnostics = rust_source(ROOT, "src/diagnostics.rs") + rust_source(ROOT, "src/system_stats/mod.rs") + rust_source(ROOT, "src/system_stats/linux.rs")
|
|
for marker in ['process_rss_anon_bytes', 'process_rss_file_bytes', 'process_rss_shmem_bytes', 'process_data_bytes', 'process_swap_bytes', 'RssAnon', 'RssFile', 'RssShmem', 'VmData', 'VmSwap']:
|
|
assert marker in diagnostics, marker
|
|
for marker in ['admin_session_passthrough', 'admin_control_plane', 'restore_admin_cookie', 'BEFORE returning a successful RFC 9220 response', 'websocket_phase("tunnel_ready")']:
|
|
assert marker in proxy, marker
|
|
headers_source = rust_source(ROOT, "src/proxy/headers.rs")
|
|
assert 'pub fn restore_admin_cookie' in headers_source and 'admin_cookie_can_be_restored_only_from_captured_fragment' in headers_source
|
|
assert "state.cache.purge(None)" not in proxy
|
|
assert "request.method() == Method::HEAD && cache_ticket.is_some()" in proxy
|
|
for marker in ["cache_effective_enabled", "transform_effective_enabled", "Some(route.id.clone())"]:
|
|
assert marker in proxy, marker
|
|
for marker in ["pub struct Metrics", "pub struct MetricsView", "struct ActiveSession", "sessions_total", "sessions_completed", "bytes_in", "bytes_out", "packets_in", "packets_out", "metrics_view_reports_l4_counters"]:
|
|
assert marker in stream, marker
|
|
for marker in ["pub enum TargetType", "pub enum PathMode", "pub enum QueryMode", "StatusCode::from_u16", "cache_seconds", "unconditional self-redirect"]:
|
|
assert marker in redirect, marker
|
|
assert "pub redirects: Vec<crate::redirect::Host>" in config and "redirect_on" in config
|
|
assert "pub not_found_hosts: Vec<crate::not_found::Host>" in config and "not_found_host_on" in config
|
|
for marker in ["pub struct Host", "pub struct CompiledHost", "crate::pages::Page"]:
|
|
assert marker in not_found, marker
|
|
assert "RedirectResponse" in proxy and "snapshot.redirect_on" in proxy
|
|
assert "#[serde(default)]\n pub tls_verify: bool" in config
|
|
upstream = rust_source(ROOT, "src/upstream.rs")
|
|
assert "BackendCertificateVerificationDisabled" in upstream and "supported_schemes" in upstream
|
|
disabled_verifier = upstream.split("impl ServerCertVerifier for BackendCertificateVerificationDisabled", 1)[1].split("pub fn client(", 1)[0]
|
|
assert disabled_verifier.count("Ok(HandshakeSignatureValid::assertion())") == 2
|
|
assert "WebPkiServerVerifier" not in disabled_verifier
|
|
assert "stream_metrics" in runtime and "stream_metrics" in main
|
|
for marker in ["PrefixPriority", "RegexInsensitive", "LocationResponse", "pub rewrite: PathRewrite"]:
|
|
assert marker in rust_source(ROOT, "src/vhost.rs"), marker
|
|
for marker in ["rewritten_path", "nginx_style_location_precedence_exact_priority_regex_prefix", "location_rewrites_are_normalized_and_regex_captures_expand", "custom_location_response_is_validated"]:
|
|
assert marker in config, marker
|
|
for marker in ["local_location_response", "route.rewritten_path(&path)", "encoded_origin_path", "client_ip.to_string()"]:
|
|
assert marker in proxy, marker
|
|
assert proxy.index("RustProxy: denied by access policy") < proxy.index("if let Some(spec) = &route.response") < proxy.index("let (backend, backend_lease, affinity_cookie)")
|
|
assert "pub client_ip: Option<String>" in logging and "client_ip" in logging
|
|
assert '"metrics": stream_metrics' in admin and '"/assets/streams.js"' in admin and 'include_str!("web/streams.js")' in admin
|
|
assert '.route("/404-hosts", get(public::index))' in admin and '"/assets/host404.js"' in admin and 'include_str!("web/host404.js")' in admin and '"not_found_hosts"' in admin
|
|
assert "session_at_with_browser_lifetime" in admin and "session_lifetime_seconds" in admin
|
|
assert '/api/preferences' in admin and 'live_log_columns' in admin
|
|
assert 'default_admin_theme' in admin and '"dark" | "light"' in admin
|
|
html = (ROOT / 'src/admin/web/index.html').read_text(encoding='utf-8')
|
|
assert 'id="ui-theme"' in html and 'html[data-theme="light"] {' in ui_css
|
|
assert 'body[data-density="compact"] input:not([type="checkbox"]):not([type="radio"])' in ui_css
|
|
assert '<div class="edition">INDEPENDENT BY DESIGN <span>' not in html
|
|
assert '<div class="edition">INDEPENDENT BY DESIGN</div>' in html
|
|
assert "inspected_metadata" in admin and "certificate_metadata" in admin
|
|
balance = rust_source(ROOT, "src/balance.rs")
|
|
assert "max_concurrent_requests" in balance and "queue_timeout_ms" in balance and "select_retry_wait" in balance
|
|
for marker in ["dns_errors_total", "tls_errors_total", "response_header_timeouts_total", "last_failure_detail", "last_recovered_ms"]:
|
|
assert marker in balance, marker
|
|
tls_source = rust_source(ROOT, "src/tls.rs")
|
|
for marker in ["CertificateMetadata", "not_before_unix", "not_after_unix", "days_remaining", "expiry_status"]:
|
|
assert marker in tls_source, marker
|
|
assert '"/assets/redirects.js"' in admin and 'include_str!("web/redirects.js")' in admin and '.route("/redirects", get(public::index))' in admin
|
|
assert '/Ubuntu%2014.04%20LTS/' in acl and 'repeated_slashes_are_normalized_for_routing' in acl and 'percent_encoded_spaces_are_valid_request_paths' in policies and 'repeated_slashes_route_normally_but_reach_upstream_unchanged' in policies and 'local_location_response_uses_response_rules_without_opening_backend' in policies and 'local_location_response_still_respects_route_acl' in policies
|
|
db_source = rust_source(ROOT, "src/db.rs")
|
|
acme_source = rust_source(ROOT, "src/acme.rs")
|
|
acme_runtime = (ROOT / "src/acme/runtime_env.rs").read_text(encoding="utf-8") + (ROOT / "src/acme/runtime_bootstrap.rs").read_text(encoding="utf-8")
|
|
assert 'runtime_setup_once' in acme_source
|
|
assert 'include!("acme/runtime_env.rs")' in (ROOT / 'src/acme.rs').read_text(encoding='utf-8')
|
|
assert 'include!("runtime_bootstrap.rs")' in (ROOT / 'src/acme/runtime_env.rs').read_text(encoding='utf-8')
|
|
for marker in ['CERTBOT_PACKAGES', 'join("acme-python")', 'virtualenv>=20,<21', 'distro_certbot_available', 'acme.runtime_python_missing', 'acme.runtime_pip_missing', 'acme.runtime_venv_failed', 'acme.runtime_certbot_install_failed', '--break-system-packages']:
|
|
assert marker in acme_runtime, marker
|
|
for marker in ['ACME runtime check complete', 'status = runtime.status', 'mode = runtime.mode', 'python = runtime.python', 'pip = runtime.pip', 'certbot = runtime.certbot']:
|
|
assert marker in acme_runtime, marker
|
|
assert 'Python: {} (ACME mode:' not in main
|
|
app_runtime = (ROOT / 'src/admin/web/app/runtime.js').read_text(encoding='utf-8')
|
|
for marker in ['const protocols = ["H1"]', 'protocols.push("H2")', 'protocols.push("H2C")', 'protocols.push("H3/UDP")']:
|
|
assert marker in app_runtime, marker
|
|
assert "SCHEMA_VERSION: i64 = 9" in db_source and "certificate_renewals" in db_source and "0004_admin_preferences.sql" in db_source and "0005_certificate_api.sql" in db_source and "0006_certificate_api_credentials.sql" in db_source and "0007_secret_store.sql" in db_source and "0008_config_history.sql" in db_source and "0009_certificate_api_grants.sql" in db_source
|
|
runtime_source = rust_source(ROOT, "src/runtime.rs")
|
|
assert 'replace_as_restore' in db_source and 'config.restore' in db_source and 'source_revision' in db_source
|
|
assert 'restore_config_history' in admin and '/api/config/history/restore' in admin
|
|
assert 'pub async fn restore_as' in runtime_source and 'replace_as_restore' in runtime_source
|
|
assert "load_renewal_observations" not in acme_source and "renewals.json" not in acme_source
|
|
for marker in ["pub kind: &'static str", "pub log_directory: String", "pub(crate) fn certbot_log_directory", 'kind: "issue"', '"renewal-dry-run"']:
|
|
assert marker in acme_source, marker
|
|
assert '"log_directories":log_directories' in admin and 'crate::acme::certbot_log_directory' in admin
|
|
assert '"runtime":state.acme.runtime_view()' in admin
|
|
assert '"certificate_runtime": state.acme.runtime_view()' in rust_source(ROOT, 'src/admin/status.rs')
|
|
assert "store.load_certificate_renewals().await?" in main and "state.acme.restore_renewals(renewal_observations)" in main
|
|
assert 'accept-remote-admin-risk' in main and 'hide = true' in main
|
|
settings_rust = rust_source(ROOT, 'src/settings.rs')
|
|
assert 'pub struct StartupLocks' in settings_rust and 'fixed by systemd/startup' in settings_rust
|
|
logging_rust = rust_source(ROOT, 'src/logging.rs')
|
|
assert 'pub per_vhost: bool' in logging_rust and 'default_vhost_log_directory' in logging_rust
|
|
pages_rust = rust_source(ROOT, 'src/pages.rs')
|
|
assert 'validate_local_response' in pages_rust and '(200..=599).contains(&status)' in pages_rust
|
|
assert 'Kind::Builtin' in pages_rust and 'builtin_application_page' in pages_rust
|
|
docker_rust = rust_source(ROOT, 'src/docker.rs')
|
|
for marker in ['pub poll_interval_seconds: u64', 'pub acl_profile_id: Option<String>', 'pub proxy_timeouts: crate::vhost::ProxyTimeouts', 'refresh(&self, settings: &Settings, force: bool)']:
|
|
assert marker in docker_rust, marker
|
|
docker_ui = (ROOT / 'src/admin/web/docker.js').read_text(encoding='utf-8')
|
|
for marker in ['docker-live-refresh', 'saveAssignment(agentId, row, publish)', 'docker-acl-profile', 'docker-proxy-read', 'dockerOpenApp', 'proxiedUrl(agent, hostname)']:
|
|
assert marker in docker_ui, marker
|
|
compression_source = rust_source(ROOT, 'src/compression.rs')
|
|
metrics_source = rust_source(ROOT, 'src/metrics.rs')
|
|
style_source = (ROOT / 'src/admin/web/style.css').read_text(encoding='utf-8')
|
|
app_source = read_app_js(ROOT)
|
|
assert 'restoreConfigRevision' in app_source and 'configHistoryRestoreConfirm' in app_source
|
|
assert 'function setMobileMenu(open)' in app_source and 'setPanel(button.dataset.panel); setMobileMenu(false);' in app_source
|
|
assert 'id="mobile-menu-toggle"' in html and 'id="main-navigation"' in html
|
|
assert '.sidebar.mobile-nav-open .main-navigation' in style_source
|
|
assert 'id="about-open"' in html and 'id="about-dialog"' in html and 'Mateusz Gruszczynski' in html
|
|
assert 'https://git.linuxiarz.pl/gru/RustProxy' in html and 'data-i18n="documentation" id="api-docs"' in html
|
|
assert '$("about-open").addEventListener("click"' in app_source and '.about-dialog {' in style_source
|
|
assert 'id="config-history-restore"' in html
|
|
vhosts_ui = read_vhosts_js(ROOT)
|
|
for marker in ['pub enum AppliedEncoding', 'AppliedEncoding::Gzip', 'AppliedEncoding::Brotli', 'AppliedEncoding::Zstd', 'AppliedEncoding::Deflate']:
|
|
assert marker in compression_source, marker
|
|
for marker in [
|
|
'gzip: true', 'br: true', 'zstd: true', 'deflate: false',
|
|
'const PREFERENCE: [Self; 4] = [Self::Zstd, Self::Brotli, Self::Gzip, Self::Deflate]',
|
|
'quality_weights_win_before_server_priority',
|
|
'equal_quality_uses_zstd_then_br_then_gzip_then_deflate',
|
|
'cookies_authorization_and_private_cache_policy_do_not_disable_compression',
|
|
'existing_content_encoding_is_never_recompressed',
|
|
]:
|
|
assert marker in compression_source, marker
|
|
assert 'AllowCookie' not in compression_source
|
|
diagnostics_source = rust_source(ROOT, 'src/diagnostics.rs') + rust_source(ROOT, 'src/system_stats/mod.rs') + rust_source(ROOT, 'src/system_stats/linux.rs')
|
|
for marker in ['pub struct NetworkInterfaceView', 'pub network_interfaces: Vec<NetworkInterfaceView>', 'sysinfo::Networks::new_with_refreshed_list', 'system.network_stats', 'collect_network_systemstat', 'normalize_network_interfaces', 'merge_network_interfaces']:
|
|
assert marker in diagnostics_source, marker
|
|
assert 'interfaces.truncate(64)' not in diagnostics_source, 'network inventory must not silently truncate interfaces'
|
|
for marker in ['pub http1_requests: AtomicU64', 'pub fn record_compression', 'each_codec_has_an_independent_counter', 'http1_requests_total']:
|
|
assert marker in metrics_source, marker
|
|
assert 'state.metrics.record_compression(encoding)' in proxy
|
|
assert 'protocolSamples' in app_source and 'm.http1_requests' in app_source and '$("open-proxy")' not in app_source
|
|
assert 'renderNetworkInterfaceCharts' in app_source and 'networkInterfaceSamples' in app_source
|
|
assert 'rp-hidden-network-interfaces' in app_source and 'network-interface-hide' in style_source
|
|
ui_html = (ROOT / 'src/admin/web/index.html').read_text(encoding='utf-8')
|
|
for marker in ['metric-bytes-down', 'metric-bytes-up', 'connection-chart', 'certificate-api-credentials']:
|
|
assert marker in ui_html, marker
|
|
assert 'id="network-interface-charts"' in ui_html
|
|
assert 'compressionCountersHelp' not in ui_html
|
|
assert 'sparkline-h1' in (ROOT / 'src/admin/web/index.html').read_text(encoding='utf-8')
|
|
assert 'body[data-density="compact"]' in style_source and '--ui-control: 22px' in style_source
|
|
compact_style_0270 = style_source.replace(' ', '')
|
|
for marker in ['progress::-webkit-progress-bar', 'progress::-webkit-progress-value', 'progress::-moz-progress-bar', '-webkit-appearance:none']:
|
|
assert marker in compact_style_0270, marker
|
|
assert 'status.backend_pool_domains' in vhosts_ui and 'payload["backend_pool_domains"]' in admin
|
|
docker_source = rust_source(ROOT, 'src/docker.rs')
|
|
management_ui = (ROOT / 'src/admin/web/management.js').read_text(encoding='utf-8')
|
|
for marker in ['function aliasEditor(', 'host-chip-editor docker-alias-editor', '.docker-force-ssl', '.docker-websockets', '.docker-cache-enabled', '.docker-compression-enabled', '.docker-passive-enabled', '.docker-passive-5xx', '.docker-agent-acl-profile']:
|
|
assert marker in docker_ui, marker
|
|
for forbidden in ['.docker-compression-zstd', '.docker-compression-br', '.docker-compression-gzip', '.docker-compression-deflate', 'compressionSelect(']:
|
|
assert forbidden not in docker_ui, forbidden
|
|
assert 'compressionEnabled.checked = assignment?.compression_enabled !== false' in docker_ui
|
|
assert 'compression_zstd: null, compression_br: null, compression_gzip: null, compression_deflate: null' in docker_ui
|
|
for marker in ['"force_ssl": assignment.force_ssl.unwrap_or(false)', '"websockets": assignment.websockets', '"enabled": assignment.compression_enabled', '"cache": {"enabled": assignment.cache_enabled}', '"passive_health": assignment.passive_health.clone()', 'assignment.acl_profile_id.clone().or_else(|| agent.acl_profile_id.clone())', 'host.transient_unavailable = target.is_none()', 'inventory: previous_inventory']:
|
|
assert marker in docker_source, marker
|
|
route_cache_0270 = rust_source(ROOT, 'src/proxy/forward/route_cache.rs')
|
|
for marker in ['if route.unavailable', 'StatusCode::SERVICE_UNAVAILABLE', 'published Docker service is currently unavailable', 'retry-after']:
|
|
assert marker in route_cache_0270, marker
|
|
assert 'agent.acl_profile_id === profile.id' in management_ui
|
|
vhost_source = rust_source(ROOT, 'src/vhost.rs')
|
|
for marker in ['DEFAULT_PROXY_CONNECT_TIMEOUT_SECONDS: u64 = 10', 'DEFAULT_PROXY_RESPONSE_HEADER_TIMEOUT_SECONDS: u64 = 120', 'DEFAULT_PROXY_READ_TIMEOUT_SECONDS: u64 = 60', 'DEFAULT_PROXY_SEND_TIMEOUT_SECONDS: u64 = 60', 'proxy_timeouts_have_explicit_safe_defaults_but_keep_null_compatibility']:
|
|
assert marker in vhost_source, marker
|
|
assert 'defaultProxyTimeouts = () => ({ connect_timeout_seconds: 10, response_header_timeout_seconds: 120, read_timeout_seconds: 60, send_timeout_seconds: 60 })' in vhosts_ui
|
|
assert 'actions.append(enabledLabel, stateBadge, remove)' in docker_ui
|
|
assert 'row._readListeners = listenerControl.read' in docker_ui and '.docker-agent-listeners' not in docker_ui
|
|
assert 'docker-agent-status-badge' in style_source and 'justify-content: center' in style_source
|
|
assert '.docker-agent-grid>label' in style_source and '#docker-agent-add' in style_source and 'margin-top: 14px' in style_source
|
|
assert 'Bearer token:' in (ROOT / 'docker-agent/install.sh').read_text(encoding='utf-8')
|
|
tls_rust = rust_source(ROOT, 'src/tls.rs')
|
|
assert 'pub fn validate_certificate_removals' in tls_rust and 'pub fn effective_cipher_suites' in tls_rust
|
|
assert "renewals.json" not in main
|
|
upgrade_source = (ROOT / "scripts/deploy/upgrade.py").read_text(encoding="utf-8")
|
|
assert "removed_obsolete_runtime_state" in upgrade_source and "legacy-data" not in upgrade_source and "certs_dir.is_symlink()" in upgrade_source
|
|
assert '.managed-update' in main and "flush_hot_to_cold_for_update" in main
|
|
openapi = (ROOT / "src/admin/openapi.json").read_text(encoding="utf-8")
|
|
assert '"/api/config/history/restore"' in openapi
|
|
for marker in ['"version": "0.29.0"', '"memory_preset"', '"cold_preset"', '"memory_budget_bytes"', '"max_inflight"', '"preset"', '"session_days"', '"session_lifetime_seconds"', '"accept_local_listener_risk"', '"backend_header"', '"backend_timing_header"', '"cold_on_age"', '"match_type"', '"regex_insensitive"', '"NotFoundHost"', '"not_found_hosts"', '"PrometheusSettings"', '"allow_trusted_network_without_token"', '"include_docker_vhosts"', '"include_streams"', '"include_logging"']:
|
|
assert marker in openapi, marker
|
|
assert '"software_update"' in openapi and '"update_available"' in openapi
|
|
for marker in ['"/api/certificates/deploy"', '"CertificateDeploy"', '"CertificateDeployResult"', '"/api/certificates/reload"']:
|
|
assert marker in openapi, marker
|
|
for marker in ['"/api/certificate-api"', '"/api/certificate-api/{key_id}"', '"/api/certificates/{certificate_id}/reload"', '"CertificateReloadOneResult"', '"CertificateApiBearer"', '"CertificateApiKey"', '"/api/live-logs"']:
|
|
assert marker in openapi, marker
|
|
websocket_source = rust_source(ROOT, 'src/proxy/websocket.rs')
|
|
proxy_source = rust_source(ROOT, 'src/proxy/mod.rs')
|
|
for marker in ['websocket_legacy_support', 'vhWebsocketLegacySupport', 'vhWebsocketLegacySupportHelp']:
|
|
assert marker in vhosts_ui or marker in openapi, marker
|
|
for marker in ['fn legacy_websocket_origin(', 'request.headers_mut().insert("origin", origin)', 'websocket_legacy_support && !websocket_upstream_h2']:
|
|
assert marker in proxy_source, marker
|
|
for marker in ['legacy_support: bool', 'legacy 101 response declared a non-zero body', 'legacy_upstream_response_accepts_old_101_syntax_but_keeps_accept_proof_strict']:
|
|
assert marker in websocket_source, marker
|
|
for marker in ['certificate_api_credentials', 'upsert_certificate_api_credential', 'revoke_certificate_api_credential', 'verify_certificate_api_credential']:
|
|
assert marker in db_source, marker
|
|
logging_source = rust_source(ROOT, 'src/logging.rs')
|
|
for marker in ['LIVE_STREAM_CAPACITY', 'broadcast::Sender<LogEntry>', 'pub fn subscribe']:
|
|
assert marker in logging_source, marker
|
|
for marker in ['livelogs: bool', 'LiveLogFilters', 'live_logs_command', 'LocalAdminCertificateVerifier', 'detect_local_admin_scheme', 'admin.uri("/api/live-logs")']:
|
|
assert marker in main, marker
|
|
assert '.uri(format!("http://{addr}/api/live-logs"))' not in main
|
|
for marker in [
|
|
"allow_trusted_network_without_token", "include_docker_vhosts", "include_streams", "include_logging",
|
|
"rust_proxy_docker_vhost_{name}", "rust_proxy_docker_backend_{name}", "rust_proxy_geoip_loaded", "not_after_timestamp_seconds",
|
|
"ewma_latency_seconds", "passive_failures", "active_health_failures_total",
|
|
"passive_health_failures_total", "http_5xx_total", "timeouts_total",
|
|
"connect_errors_total", "io_errors_total", "slow_responses_total",
|
|
"rust_proxy_log_sink_queue_dropped_total", "rust_proxy_not_found_hosts_configured",
|
|
]:
|
|
assert marker in prometheus, marker
|
|
assert "if docker_targets.contains_key(&pool.id)" in prometheus
|
|
assert "if docker_targets.contains_key(id)" in prometheus
|
|
assert "rust_proxy_docker_vhost_exchange_duration_seconds" in prometheus
|
|
assert "PeerAddr" in prometheus and "Forwarded headers never participate in this decision" in prometheus
|
|
transport_doc = (ROOT / "docs/TRANSPORT.md").read_text(encoding="utf-8")
|
|
assert '127.0.0.1:9090' in transport_doc and 'accept_local_listener_risk: true' in transport_doc
|
|
drop_impl = http3.split('impl Drop for ReceiveBody', 1)[1].split('#[cfg(test)]', 1)[0]
|
|
assert 'self.stream.stop_sending(' not in drop_impl
|
|
websocket_source = rust_source(ROOT, 'src/proxy/websocket.rs')
|
|
for marker in ['h3::quinn::RecvStream', 'websocket_rfc9220_on(local)', 'run_h3', 'H3_REQUEST_CANCELLED']:
|
|
assert marker in http3 or marker in websocket_source, marker
|
|
assert 'upstream: Upgraded' in websocket_source
|
|
assert 'timeout(Duration::from_secs(10), upstream)' not in websocket_source
|
|
for marker in ['websocket_phase("tunnel_started")', 'websocket_phase("tunnel_closed")', 'h3_reset("H3_REQUEST_CANCELLED")']:
|
|
assert marker in websocket_source, marker
|
|
logging_source = rust_source(ROOT, 'src/logging.rs')
|
|
for marker in ['http_version', 'transport', 'websocket_bootstrap', 'websocket_phase', 'websocket_error', 'h3_reset_code', 'upstream_status']:
|
|
assert marker in logging_source, marker
|
|
assert 'h3_quinn' not in http3 and 'h3_quinn' not in (ROOT / 'tests/http3.rs').read_text(encoding='utf-8')
|
|
h3_test = (ROOT / 'tests/http3.rs').read_text(encoding='utf-8')
|
|
assert 'RFC 9220: Extended CONNECT negotiates WebSocket on the same H3 stream.' in h3_test
|
|
assert 'Method::CONNECT' in h3_test and 'h3::ext::Protocol' in h3_test and '.send_data(' in h3_test
|
|
for marker in ['external_admin_authority', 'external_admin_scheme', 'trusted_admin_forwarded_proto', 'reverse_proxy_dns_allowlist_uses_external_port_and_keeps_direct_port_available']:
|
|
assert marker in admin, marker
|
|
|
|
|
|
def container_contract_smoke() -> None:
|
|
dockerfile = (ROOT / "Dockerfile").read_text(encoding="utf-8")
|
|
unit = (ROOT / "deploy/rustproxy.service").read_text(encoding="utf-8")
|
|
assert "LogsDirectory=rustproxy" in unit and "LogsDirectoryMode=0770" in unit
|
|
assert "Environment=MALLOC_ARENA_MAX=2" in unit
|
|
assert "TimeoutStopSec=120" in unit
|
|
assert "ReadWritePaths=/var/lib/rustproxy /var/log/rustproxy" in unit
|
|
generated_unit = deploy.service_unit({"prefix":"/opt/rustproxy", "data_dir":"/var/lib/rustproxy", "user":"rustproxy", "group":"rustproxy", "admin":"127.0.0.1:9090", "remote_admin":False, "http":"0.0.0.0:80", "https":"0.0.0.0:443"})
|
|
assert "LogsDirectory=rustproxy" in generated_unit and "/var/log/rustproxy" in generated_unit
|
|
assert "Environment=MALLOC_ARENA_MAX=2" in generated_unit
|
|
for marker in [
|
|
"FROM rust:1.88-trixie AS build",
|
|
"cargo generate-lockfile",
|
|
"cargo build --locked --release",
|
|
"FROM debian:trixie-slim AS runtime",
|
|
"python3-venv",
|
|
"python3-pip",
|
|
"certbot",
|
|
"python3-certbot-dns-cloudflare",
|
|
"python3-certbot-dns-ovh",
|
|
"python3-certbot-dns-rfc2136",
|
|
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
|
|
"USER rustproxy:rustproxy",
|
|
'VOLUME ["/var/lib/rustproxy"]',
|
|
]:
|
|
assert marker in dockerfile, marker
|
|
ignore = set((ROOT / ".dockerignore").read_text(encoding="utf-8").splitlines())
|
|
for marker in {"data", "target", ".env", "*.pem", "*.key"}:
|
|
assert marker in ignore, marker
|
|
assert (ROOT / "docs/CONTAINER.md").is_file()
|
|
|
|
def grafana_contract_smoke() -> None:
|
|
assert not (ROOT / "debian").exists(), "Debian packaging belongs to the external packaging repository"
|
|
dashboard = json.loads((ROOT / "grafana/rustproxy-overview.json").read_text(encoding="utf-8"))
|
|
assert dashboard.get("uid") == "rustproxy-0170"
|
|
queries = json.dumps(dashboard)
|
|
for marker in ["rust_proxy_process_rss_bytes", "rust_proxy_cache_transient_bytes", "rust_proxy_backend_ewma_latency_seconds", "rust_proxy_certificate_renewal_not_after_timestamp_seconds", "rust_proxy_not_found_hosts_configured", "rust_proxy_http1_requests_total", "rust_proxy_backend_upstream_client_generation_requests", "rust_proxy_backend_upstream_client_generation_age_seconds", "rust_proxy_backend_retry_attempts_total", "rust_proxy_backend_upstream_client_rotations_total", "rust_proxy_docker_backend_upstream_client_generation_requests", "rust_proxy_docker_backend_retry_attempts_total"]:
|
|
assert marker in queries, marker
|
|
|
|
|
|
def reproducible_archive_smoke(tmp: Path) -> None:
|
|
first = tmp / "first.zip"
|
|
second = tmp / "second.zip"
|
|
env = dict(os.environ, SOURCE_DATE_EPOCH="1700000000")
|
|
subprocess.run(["python3", str(ROOT / "make_zip.py"), str(first)], check=True, env=env, stdout=subprocess.DEVNULL)
|
|
subprocess.run(["python3", str(ROOT / "make_zip.py"), str(second)], check=True, env=env, stdout=subprocess.DEVNULL)
|
|
first_bytes, second_bytes = first.read_bytes(), second.read_bytes()
|
|
assert hashlib.sha256(first_bytes).digest() == hashlib.sha256(second_bytes).digest()
|
|
with zipfile.ZipFile(first) as archive:
|
|
names = archive.namelist()
|
|
assert "RustProxy/SHA256SUMS" in names
|
|
assert "RustProxy/tools/checks/rust_source.py" in names
|
|
assert "RustProxy/tools/checks/rust_module_contracts.py" in names
|
|
assert "RustProxy/tools/checks/source_module_contracts.py" in names
|
|
for name in names:
|
|
if name.startswith("RustProxy/") and name.endswith(".rs"):
|
|
assert len(archive.read(name).decode("utf-8").splitlines()) <= 500, name
|
|
if name.startswith("RustProxy/src/admin/web/") and name.endswith(".js"):
|
|
assert len(archive.read(name).decode("utf-8").splitlines()) <= 500, name
|
|
if name.startswith("RustProxy/docker-agent/") and name.endswith(".go"):
|
|
assert len(archive.read(name).decode("utf-8").splitlines()) <= 500, name
|
|
assert "RustProxy/src/admin/web/error_401.html" in names
|
|
assert "RustProxy/src/admin/web/error_404.html" in names
|
|
assert "RustProxy/docs/CUSTOM_LOCATIONS.md" in names
|
|
assert "RustProxy/docs/WEB_SERVER.md" in names
|
|
assert "RustProxy/docs/PHP_FPM.md" in names
|
|
for module in ["mod.rs", "config.rs", "filesystem.rs", "security.rs", "response.rs", "request.rs", "routing.rs", "fastcgi.rs", "serve.rs", "htaccess.rs", "tests.rs"]:
|
|
assert f"RustProxy/src/web_server/{module}" in names
|
|
assert "RustProxy/src/web_server.rs" not in names
|
|
assert "RustProxy/src/admin/web/web-server.js" in names
|
|
for module in ["app/core.js", "app/preferences.js", "app/config-history.js", "app/runtime.js", "app/settings.js", "drafts.js", "vhosts/core.js", "vhosts/editor.js", "vhosts/render.js"]:
|
|
assert f"RustProxy/src/admin/web/{module}" in names
|
|
assert "RustProxy/src/admin/web/app.js" not in names and "RustProxy/src/admin/web/vhosts.js" not in names
|
|
for module in ["model.go", "docker.go", "network.go", "certificate.go", "server.go", "main.go", "main_test.go"]:
|
|
assert f"RustProxy/docker-agent/{module}" in names
|
|
for module in ["deploy.py", "deploy_core.py", "deploy_runtime.py", "upgrade.py"]:
|
|
assert f"RustProxy/scripts/deploy/{module}" in names
|
|
for module in ["migrate_npm.py", "migrate_npm_core.py", "migrate_npm_api.py"]:
|
|
assert f"RustProxy/scripts/migration/{module}" in names
|
|
assert "RustProxy/scripts/runtime/acme_provider.py" in names
|
|
assert "RustProxy/scripts/build/swagger_assets.py" in names
|
|
assert "RustProxy/tools/audit/admin_audit.py" in names
|
|
assert "RustProxy/tools/legacy/backup.py" in names
|
|
assert "RustProxy/integrations/opnsense-acme/opnsense-rustproxy-cert-deploy.sh" in names
|
|
assert "RustProxy/integrations/routeros-oci-build/build-routeros-oci.sh" in names
|
|
assert "RustProxy/nagios/check_rustproxy.py" in names
|
|
assert "RustProxy/nagios/check_rustproxy_api" in names
|
|
assert "RustProxy/src/admin/web/host404.js" in names
|
|
assert "RustProxy/docs/NOT_FOUND_HOSTS.md" in names
|
|
assert not any(name.startswith("RustProxy/debian/") for name in names)
|
|
assert "RustProxy/grafana/rustproxy-overview.json" in names
|
|
assert "RustProxy/UPDATE_CHECK.toml" in names
|
|
assert "RustProxy/src/update_check.rs" in names
|
|
assert "RustProxy/docker-agent/docker-agent" not in names
|
|
assert all("/data/" not in name and "/target/" not in name for name in names)
|
|
assert all(not name.endswith("/.env") for name in names), "local .env leaked into release archive"
|
|
assert "RustProxy/.env.example" in names
|
|
timestamps = {item.date_time for item in archive.infolist()}
|
|
assert len(timestamps) == 1
|
|
|
|
|
|
def managed_acme_launch_smoke(tmp: Path) -> None:
|
|
"""Managed ACME workers must execute the venv console script, not `python -m certbot`."""
|
|
final = tmp / "acme-python"
|
|
bindir = final / "bin"
|
|
bindir.mkdir(parents=True)
|
|
python = bindir / "python"
|
|
python.write_text("#!/bin/sh\nexec /bin/sh \"$@\"\n", encoding="utf-8")
|
|
python.chmod(0o755)
|
|
console = bindir / "certbot"
|
|
console.write_text("#!/bin/sh\ntest \"$1\" = --version || exit 2\necho 'certbot 5.8.0'\n", encoding="utf-8")
|
|
console.chmod(0o755)
|
|
|
|
worker_path = ROOT / "scripts/runtime/acme_provider.py"
|
|
spec = importlib.util.spec_from_file_location("rustproxy_acme_provider", worker_path)
|
|
assert spec and spec.loader
|
|
module = importlib.util.module_from_spec(spec)
|
|
old = os.environ.get("RUSTPROXY_CERTBOT_EXECUTABLE")
|
|
os.environ["RUSTPROXY_CERTBOT_EXECUTABLE"] = str(console)
|
|
try:
|
|
spec.loader.exec_module(module)
|
|
prefix = module.certbot_prefix()
|
|
assert prefix == [str(console)]
|
|
result = subprocess.run(prefix + ["--version"], check=True, capture_output=True, text=True)
|
|
assert result.stdout.strip() == "certbot 5.8.0"
|
|
finally:
|
|
if old is None:
|
|
os.environ.pop("RUSTPROXY_CERTBOT_EXECUTABLE", None)
|
|
else:
|
|
os.environ["RUSTPROXY_CERTBOT_EXECUTABLE"] = old
|
|
|
|
|
|
def main() -> None:
|
|
subprocess.run(["python3", str(ROOT / "tools/checks/rust_module_contracts.py")], check=True)
|
|
subprocess.run(["python3", str(ROOT / "tools/checks/source_module_contracts.py")], check=True)
|
|
with tempfile.TemporaryDirectory(prefix="rustproxy-release-smoke-") as directory:
|
|
tmp = Path(directory)
|
|
sql_and_backup_smoke(tmp / "sql")
|
|
upgrade_smoke(tmp / "upgrade")
|
|
managed_acme_launch_smoke(tmp / "acme-launch")
|
|
reproducible_archive_smoke(tmp / "archives")
|
|
deployment_gate_smoke(tmp)
|
|
npm_migration_smoke(tmp)
|
|
ui_contract_smoke()
|
|
source_contract_smoke()
|
|
container_contract_smoke()
|
|
grafana_contract_smoke()
|
|
print("PASS: 0.29.0 SQLite/schema-9/upgrade, local-response 404 hosts, Prometheus/Grafana diagnostics, external Debian-packaging separation, cache/dashboard/logging/Nagios, retained admin/H3/stream/TLS contracts and reproducible source archives.")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|