This commit is contained in:
Mateusz Gruszczyński committed 2026-10-10 20:56:36 +02:00
1 parent f213d853e2
commit 76b8bb7ee2
78 files changed
+7750 -5204

No files matched your search

+114
View File
@@ -0,0 +1,114 @@
#!/usr/bin/env python3
"""Fail-closed source regression checks. Not a replacement for running Rust tests."""
from pathlib import Path
import re
root = Path(__file__).resolve().parents[2]
def read(name):
return (root / name).read_text(encoding="utf-8")
def must(source, *text):
for item in text:
if item not in source:
raise AssertionError(f"missing security invariant: {item}")
router = read('src/admin/router.rs')
access = read('src/admin/access.rs')
session = read('src/admin/session.rs')
proxy = read('src/proxy/forward_entry.rs')
maintenance = read('src/proxy/cache_http_invalidation.rs')
operations = read('src/admin/operations.rs')
cache_policy = read('src/cache/model/http_invalidation.rs')
app = read('src/runtime/types.rs') + read('src/runtime/constructors.rs')
must(router,
'fn anonymous_allowed', 'panel_document_path(path)',
'/api/auth/status', '/api/auth/setup', '/api/auth/login',
'.layer(middleware::from_fn_with_state(state.clone(), guard))',
'"content-security-policy"', '"x-frame-options"',
'"x-content-type-options"', '"no-referrer"',
'"/api/system-info", get(system_info)',
'"/api/events", get(events)',
'"/api/cache/invalidate"',
'"/api/config", get(configuration).merge(put(update))')
must(session,
'if !public && session.is_none()',
'if unsafe_method && !external_certificate_api',
'single_header(headers, "origin")',
'single_header(headers, "x-rustproxy-request") != Some("1")',
'single_header(headers, "x-csrf-token")',
'auth::equal_secret',
'session_at_with_browser_lifetime',
'secure(next.run(request).await)',
'h == "cross-site"')
must(access, 'admin_request_authority', 'admin_origin_matches',
'trusted_admin_proxy', 'single_header(headers, "x-forwarded-host")',
'certificate_api_credentials', 'valid_certificate_api_token')
must(session, 'verify_certificate_api_credential')
# Only the intended bootstrap/discovery APIs may appear inside the anonymous allowlist.
allowlist = router.split('fn anonymous_allowed(', 1)[1].split('\n}', 1)[0]
allowed_api_paths = set(re.findall(r'"(/api/[^" ]+)"', allowlist))
assert allowed_api_paths == {'/api/auth/status', '/api/auth/setup', '/api/auth/login'}, allowed_api_paths
must(maintenance,
'permits_http_invalidation(client_ip)',
'snapshot.forwarding.trusted(peer_ip)',
'return error(StatusCode::FORBIDDEN, "RustProxy: missing trusted proxy client identity',
'cache_http_invalidation_request(request.method()',
'duplicate BAN/PURGE control header',
'request.body().is_end_stream()',
'get_all(http::header::CONTENT_LENGTH)',
'http::header::TRANSFER_ENCODING',
'X-RustProxy-Confirm-Wide: 1',
'"{scheme}://{authority}{target}"',
'cache_invalidation_slots.clone().try_acquire_owned()',
'state.store.audit(',
'tokio::task::spawn_blocking',
'let _permit = permit;',
'StatusCode::UNPROCESSABLE_ENTITY',
'StatusCode::TOO_MANY_REQUESTS')
# Axum 0.8 requires the `forward` fallback future to be Send. Its Body is
# not Sync, so an async helper must own Request<Body> rather than retain
# &Request<Body> across the audit/cache await points.
async_maintenance = maintenance.split('async fn cache_http_invalidation(', 1)[1].split(') -> Response {', 1)[0]
assert 'request: Request,' in async_maintenance, 'cache maintenance must own Request<Body> in its async future'
assert 'request: &Request' not in async_maintenance, 'borrowed Request<Body> makes fallback handler non-Send'
must(maintenance, 'let method = request.method().clone();', 'drop(request);')
assert maintenance.index('drop(request);') < maintenance.index('state.store.audit('), 'drop body before asynchronous audit'
assert 'frontend, &host, request, context.client_ip' in proxy, 'forward must transfer request ownership'
must(proxy, 'snapshot.host_allowed(&host, context.client_ip)',
'crate::acl::allows_with_country(',
'context.client_ip, peer.0.ip(), context.scheme',
'return cache_http_invalidation(')
assert proxy.find('snapshot.host_allowed(&host, context.client_ip)') < proxy.find('return cache_http_invalidation(')
assert proxy.find('if cache_http_invalidation_method(request.method())') < proxy.find('return cache_http_invalidation(')
must(cache_policy, 'http_invalidation_allowed_cidrs.len() <= 32',
'net.prefix_len() > 0', 'pub fn permits_http_invalidation')
must(app, 'cache_invalidation_slots: Arc<Semaphore>',
'cache_invalidation_slots: Arc::new(Semaphore::new(2))')
must(operations, 'cache_invalidation_slots.clone().try_acquire_owned()',
'tokio::task::spawn_blocking(move ||',
'"cache.invalidate"', '"cache.purge"',
'"cache.ban.remove"')
start = operations.index('async fn cache_ban_delete(')
end = operations.index('async fn active_tunnels(', start)
assert operations[start:end].index('.audit(') < operations[start:end].index('remove_ban(id)'), 'BAN deletion must audit before mutation'
# Frontend: no HTML-string DOM injection or dynamic code execution in first-party scripts.
js_files = list((root/'src/admin/web').rglob('*.js'))
for path in js_files:
text = path.read_text(encoding='utf-8')
dangerous = (r'\.innerHTML\s*=', r'\.outerHTML\s*=', r'\.insertAdjacentHTML\s*\(',
r'\beval\s*\(', r'\bnew\s+Function\s*\(')
for pattern in dangerous:
if re.search(pattern, text):
raise AssertionError(f'unsafe DOM/code sink: {path.relative_to(root)} matches {pattern}')
html = read('src/admin/web/index.html')
for fragment in ['class="metric-info cache-maintenance-info"',
'cacheHttpInvalidationCurlLabel',
'curl -i -X PURGE', 'curl -i -X BAN']:
must(html, fragment)
print(f'PASS: 0.31.0 admin/HTTP maintenance security source contracts; scanned {len(js_files)} first-party JS files. Actual Rust/wire behavior not tested.')
+13 -13
View File
@@ -24,7 +24,7 @@ def read_docker_agent_source() -> str:
preflight(root)
manifest = tomllib.loads((root / "Cargo.toml").read_text())
assert manifest["package"]["version"] == "0.30.1"
assert manifest["package"]["version"] == "0.31.0"
assert manifest["dependencies"]["quinn"]["version"] == "0.11.12"
h3dep = manifest["dependencies"]["h3"]
assert h3dep["package"] == "hpx-h3" and h3dep["version"] == "=2.5.19" and "quinn" in h3dep["features"]
@@ -32,14 +32,14 @@ assert "h3-quinn" not in manifest["dependencies"]
lock_path = root / "Cargo.lock"
if lock_path.exists():
lock = lock_path.read_text()
assert 'name = "rust-proxy"\nversion = "0.30.1"' in lock
assert 'name = "rust-proxy"\nversion = "0.31.0"' in lock
assert 'name = "systemstat"' in lock and 'name = "metrics-process"' in lock and 'name = "sysinfo"' in lock, "Cargo.lock must be resolver-generated after adding cross-platform statistics libraries"
for release_doc, marker in {
"README.md": "# RustProxy 0.30.1",
"CHANGELOG.md": "## 0.30.1",
"ROADMAP.md": "## 0.30.1",
"AGENTS.md": "Current source version: 0.30.1.",
"docs/TEST_REPORT.md": "# Verification report - RustProxy 0.30.1",
"README.md": "# RustProxy 0.31.0",
"CHANGELOG.md": "## 0.31.0",
"ROADMAP.md": "## 0.31.0",
"AGENTS.md": "Current source version: 0.31.0.",
"docs/TEST_REPORT.md": "# Verification report - RustProxy 0.31.0",
}.items():
assert marker in (root / release_doc).read_text(), f"stale release identity in {release_doc}"
assert tomllib.loads((root / "rust-toolchain.toml").read_text())["toolchain"]["channel"] == "stable"
@@ -162,7 +162,7 @@ assert 'class="vhost-dialog logging-format-dialog"' in html
assert 'data-panel="streams"' in html and 'id="panel-streams"' in html
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")
preferences_source = rust_source(root, "src/admin/preferences.rs")
for marker in ['vhost_list_sort', 'docker_vhost_sort', 'redirect_list_sort', 'notfound_list_sort', 'stream_list_sort', 'VHOST_LIST_SORT_COLUMNS', 'DOCKER_VHOST_SORT_COLUMNS', 'REDIRECT_LIST_SORT_COLUMNS', 'NOTFOUND_LIST_SORT_COLUMNS', 'STREAM_LIST_SORT_COLUMNS']:
assert marker in preferences_source, marker
for source_name, markers in {
@@ -251,7 +251,7 @@ for marker in ['vhBackendDiagnostics', 'backend_header: yes("backend-header")',
assert 'cache: { enabled: false }' in vhosts_js, "new vhost drafts must default cache off"
docker_js = (root / "src/admin/web/docker.js").read_text()
assert 'preserve.checked = assignment?.preserve_host !== false' in docker_js, "new Docker backends must preserve public Host by default"
for marker in ['saveSection("docker-agents"', '/api/docker/refresh', 'docker-publish', 'docker-hostname', 'host-chip-editor docker-alias-editor', 'docker-target-ip', 'default_target_ip', 'docker-agent-fingerprint', 'docker-agent-certificate', 'docker-health-mode', 'docker-passive-enabled', 'docker-passive-5xx', 'docker-search', 'docker-show-hidden', 'dockerHideVhost', 'pool_routes', 'setInterval']:
for marker in ['saveSection("docker-agents"', '/api/docker/refresh', 'docker-publish', 'docker-hostname', 'host-chip-editor docker-alias-editor', 'docker-target-ip', 'default_target_ip', 'docker-agent-fingerprint', 'docker-agent-certificate', 'docker-health-mode', 'docker-passive-enabled', 'docker-passive-5xx', 'docker-search', 'docker-show-hidden', 'dockerHideVhost', 'pool_routes', 'rustProxyRefresh.register("docker"']:
assert marker in docker_js, marker
docker_source = rust_source(root, "src/docker.rs")
for marker in ['pub struct Settings', 'pub struct Observation', 'pub struct HostAddress', 'pub struct Binding', 'aliases: Vec<String>', 'target_ip: Option<IpAddr>', 'tls_fingerprint_sha256', 'certificate_id: Option<String>', 'health_check: crate::balance::HealthCheck', 'passive_health: crate::balance::PassiveHealth', 'PinnedCertificateVerifier', 'fingerprint mismatch', 'effective_config', 'poll_loop', 'address_allowlist']:
@@ -285,7 +285,7 @@ assert 'Opt-in health decisions based on ordinary user traffic. Defaults to disa
assert '"force_ssl"' in openapi and '"forward_client_ip"' in openapi
assert '"backend_header"' in openapi and '"backend_timing_header"' in openapi
assert '"StreamMetrics"' in openapi and '"streams"' in openapi
for marker in ['"version": "0.30.1"', '"/api/traffic"', '"capture_user_agent"', '"capture_country"', '"hot_auto_trim_enabled"', '"hot_auto_trim_after_seconds"', '"hot_auto_trim_target_percent"', '"/api/config/{section}"', '"ConfigSectionUpdate"', '"cold_on_idle"', '"cold_on_age"', '"cold_max_hot_seconds"', '"cold_on_lru"', '"cold_on_shutdown"', '"cold_gc_interval_seconds"', '"cold_gc_max_delete"', '"collapse_enabled"', '"collapse_wait_timeout_ms"', '"collapse_timeout_fallback"', '"stale_if_error_timeout"', '"stale_if_error_connection"', '"stale_if_error_dns"', '"stale_if_error_tls"', '"stale_if_error_statuses"', '"proactive_refresh_enabled"', '"proactive_refresh_before_expiry_seconds"', '"proactive_refresh_percent"', '"proactive_refresh_max_inflight"', '"TransformSettings"', '"TransformOverrides"', '"/api/cache/cold/cleanup"', '"/api/cache/invalidate"', '"/api/cache/bans"', '"/api/cache/bans/{id}"', '"CacheInvalidationRequest"', '"CacheInvalidationResponse"', '"ColdCleanupJob"', '"match_type"', '"regex_insensitive"', '"rewrite"', '"response"', '"covered_hosts"', '"default": true']:
for marker in ['"version": "0.31.0"', '"/api/traffic"', '"capture_user_agent"', '"capture_country"', '"hot_auto_trim_enabled"', '"hot_auto_trim_after_seconds"', '"hot_auto_trim_target_percent"', '"/api/config/{section}"', '"ConfigSectionUpdate"', '"cold_on_idle"', '"cold_on_age"', '"cold_max_hot_seconds"', '"cold_on_lru"', '"cold_on_shutdown"', '"cold_gc_interval_seconds"', '"cold_gc_max_delete"', '"collapse_enabled"', '"collapse_wait_timeout_ms"', '"collapse_timeout_fallback"', '"stale_if_error_timeout"', '"stale_if_error_connection"', '"stale_if_error_dns"', '"stale_if_error_tls"', '"stale_if_error_statuses"', '"proactive_refresh_enabled"', '"proactive_refresh_before_expiry_seconds"', '"proactive_refresh_percent"', '"proactive_refresh_max_inflight"', '"TransformSettings"', '"TransformOverrides"', '"/api/cache/cold/cleanup"', '"/api/cache/invalidate"', '"/api/cache/bans"', '"/api/cache/bans/{id}"', '"CacheInvalidationRequest"', '"CacheInvalidationResponse"', '"ColdCleanupJob"', '"match_type"', '"regex_insensitive"', '"rewrite"', '"response"', '"covered_hosts"', '"default": true']:
assert marker in openapi, marker
for marker in ['"NotFoundHost"', '"not_found_hosts"', '"PrometheusSettings"', '"allow_trusted_network_without_token"', '"include_docker_vhosts"', '"include_streams"', '"include_logging"']:
assert marker in openapi, marker
@@ -1150,7 +1150,7 @@ assert 'transport-action' not in html and '.transport-grid .transport-action' no
assert '#panel-cache .cache-copy-cell' in style_source and 'cursor: copy' in style_source
assert '.operation-message' in style_source and '.operation-message.error' in style_source
for marker_0221 in [
'api("/api/tunnels")', 'tunnel-kill-reason', 'tunnelKillReasonDefault',
'api("/api/tunnels", context?.signal ? { signal: context.signal } : {})', 'tunnel-kill-reason', 'tunnelKillReasonDefault',
'api(`/api/tunnels/${encodeURIComponent(item.id)}/kill`', 'startTunnelRefresh()',
]:
assert marker_0221 in app_js, marker_0221
@@ -1396,7 +1396,7 @@ assert '"error_log" => {' not in admin_0241 and 'request("/api/config/error_log"
assert '"error_log"' in openapi and 'logging, error_log, forwarding' not in openapi
assert 'logging_section_persists_error_log_settings_atomically' in admin_0241 and 'error_log_is_not_a_standalone_configuration_section' in admin_0241
assert 'location.pathname === "/logging"' in settings_js_0241
assert '"RuntimeInformation"' in openapi and '"version": "0.30.1"' in openapi
assert '"RuntimeInformation"' in openapi and '"version": "0.31.0"' in openapi
# 0.24.1 compiler and hostname-cardinality regressions.
tls_0241 = rust_source(root, "src/tls.rs")
@@ -1635,5 +1635,5 @@ for marker_0282 in [
assert 'Zewnętrzne ścieżki plików' in (root / 'langs/pl_pl.json').read_text()
assert 'setfacl -m u:rustproxy:--x /etc/ssl/' in (root / 'docs/V0.28.2_IMPLEMENTATION.md').read_text()
print("PASS: 0.30.1 manifest/package, shared system-statistics/runtime metadata, Settings logging, retained H2, external-packaging/Grafana/Prometheus/404-host/custom-location/path/cache/dashboard/Nagios UI/source contracts, stopped legacy-renewal removal, session/admin/transport contracts, deterministic package source and embedded SQLite schema-v9 migrations.")
print("PASS: 0.31.0 manifest/package, shared system-statistics/runtime metadata, Settings logging, retained H2, external-packaging/Grafana/Prometheus/404-host/custom-location/path/cache/dashboard/Nagios UI/source contracts, stopped legacy-renewal removal, session/admin/transport contracts, deterministic package source and embedded SQLite schema-v9 migrations.")
print("NOT VERIFIED: Rust parsing, types, compilation, linking or runtime behavior. Run ./dev.sh test.")
@@ -0,0 +1,92 @@
#!/usr/bin/env node
// Network-independent browser bootstrap contract: visible recovery and early shell.
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { runInNewContext } from 'node:vm';
import { resolve, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
const root = resolve(dirname(fileURLToPath(import.meta.url)), '../..');
const read = path => readFileSync(resolve(root, path), 'utf8');
const html = read('src/admin/web/index.html');
const css = read('src/admin/web/style.css');
const core = read('src/admin/web/app/core.js');
const runtime = read('src/admin/web/app/runtime.js');
assert.match(html, /<html class="app-loading"/);
assert.match(html, /id="app-startup"[^>]*role="status"/);
assert.match(html, /id="app-startup-title"/);
assert.match(html, /class="app-startup-spinner"/);
assert.match(html, /class="app-startup-recovery"/);
assert.match(html, /class="outline" href="" data-i18n="startupRetry"/);
assert.match(html, /<noscript>/);
assert.doesNotMatch(css, /html\.app-loading\s+body\s*\{\s*visibility:\s*hidden/);
assert.match(css, /html\.app-loading \.shell/);
assert.match(css, /app-startup-reveal 0s linear 12s forwards/);
assert.match(css, /prefers-reduced-motion: reduce/);
assert.match(runtime, /showStartupFailure\(e\)/);
const titleRow = html.match(/<div class="page-intro-title-row">([\s\S]*?)<\/div>/)?.[1];
assert(titleRow, 'title row exists');
assert.match(titleRow, /id="page-title"/);
assert.match(titleRow, /id="overview-refresh"[^>]*hidden/);
assert.equal((html.match(/id="overview-refresh"/g) || []).length, 1);
assert.match(core, /\$\("overview-refresh"\)\.hidden = name !== "overview"/);
assert.match(css, /\.page-intro-title-row\s*\{\s*display:\s*flex/);
assert.match(css, /\.page-intro-title-row \.overview-refresh/);
// Exercise the real activate() and failure handler against a small DOM fake.
const activateSource = core.slice(core.indexOf('async function activate(info) {'), core.indexOf('async function loadAudit()'));
const failureSource = core.slice(core.indexOf('function showStartupFailure(error)'));
assert(activateSource.startsWith('async function activate(info)'));
assert(failureSource.startsWith('function showStartupFailure(error)'));
const nodes = new Map();
const node = key => {
if (!nodes.has(key)) nodes.set(key, { hidden: true, value: 'x', textContent: '' });
return nodes.get(key);
};
const classes = new Set(['app-loading']);
let startCount = 0, configCount = 0, statusCount = 0, prefResolve, configResolve;
const preferencesPromise = new Promise(resolve => { prefResolve = resolve; });
const configPromise = new Promise(resolve => { configResolve = resolve; });
const context = {
$: node,
document: { documentElement: { classList: {
add(...keys) { for (const key of keys) classes.add(key); },
remove(...keys) { for (const key of keys) classes.delete(key); },
} } },
pendingAuthDestination: null,
authMessage() {},
authEpoch: 0, authenticated: false, csrf: '', sessionInfo: null,
renderSession() {},
rustProxyRefresh: { start() { startCount++; } },
loadAdminPreferences: () => preferencesPromise,
loadConfig: () => { configCount++; return configPromise; },
refresh: async () => { statusCount++; },
toast() { throw new Error('Unexpected toast'); },
window: {}, languagePacks: new Map(), t: key => key,
};
runInNewContext(`${activateSource}\n${failureSource}`, context);
const activating = context.activate({ username: 'admin', csrf_token: 'CSRF' });
assert.equal(startCount, 1, 'SSE/polling starts without waiting for preferences');
assert.equal(context.authenticated, true);
assert.equal(node('workspace').hidden, false, 'shell is shown immediately');
assert(!classes.has('app-loading'), 'spinner is cleared before preferences/config/status');
assert.equal(configCount, 0, 'preferences request is still pending');
assert.equal(statusCount, 0);
prefResolve({ refresh: {} });
for (let i = 0; i < 5; i++) await Promise.resolve();
assert.equal(configCount, 1);
assert.equal(node('workspace').hidden, false, 'slow configuration does not hide shell');
configResolve({});
await activating;
for (let i = 0; i < 5; i++) await Promise.resolve();
assert.equal(statusCount, 1);
context.showStartupFailure(new Error('Offline'));
assert(classes.has('app-loading') && classes.has('app-startup-failed'));
assert.equal(node('app-startup-detail').textContent, 'Offline');
assert.equal(node('app-startup-detail').hidden, false);
assert.equal(node('workspace').hidden, true, 'failed shell cannot obscure retry UI');
console.log('PASS: startup spinner/retry, shell before slow API, error recovery and compact Overview title row.');
+3 -2
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
"""Rust-independent documentation/Reader consistency checks for the 0.30.1 release."""
"""Rust-independent documentation/Reader consistency checks for the 0.31.0 release."""
from pathlib import Path
import re
from rust_source import rust_source
@@ -29,6 +29,7 @@ required = [
DOCS / "V0.25.2_AUDIT.md",
DOCS / "V0.25.1_AUDIT.md",
DOCS / "V0.25.0_AUDIT.md",
DOCS / "IMPLEMENTATION_0.31.0.md",
DOCS / "TEST_REPORT.md",
DOCS / "CHANGELOG_TECHNICAL_ARCHIVE.md",
DOCS / "ROADMAP_HISTORY.md",
@@ -64,7 +65,7 @@ assert '"/docs/content.json"' in admin
readme = (ROOT / "README.md").read_text(encoding="utf-8")
roadmap = (ROOT / "ROADMAP.md").read_text(encoding="utf-8")
changelog = (ROOT / "CHANGELOG.md").read_text(encoding="utf-8")
assert readme.startswith("# RustProxy 0.30.1")
assert readme.startswith("# RustProxy 0.31.0")
assert "docs/WEB_SERVER.md" in readme and "docs/README.md" in readme
assert readme.count("## What changed in ") == 1, "README duplicates historical release notes; use CHANGELOG/archive"
assert "## Release history" in readme and "docs/ROADMAP_HISTORY.md" in readme
@@ -0,0 +1,111 @@
#!/usr/bin/env node
// Real browser form callbacks with fake DOM; protocol/security checks are
// source-level assertions only. Rust unit and live HTTP tests need Cargo.
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { resolve, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
import { runInNewContext } from 'node:vm';
const root = resolve(dirname(fileURLToPath(import.meta.url)), '../..');
const read = path => readFileSync(resolve(root, path), 'utf8');
const html = read('src/admin/web/index.html');
const css = read('src/admin/web/style.css');
const administration = read('src/admin/web/settings.js');
const cache = read('src/admin/web/app/settings.js');
const router = read('src/admin/router.rs');
const assets = read('src/admin/panel_assets.rs');
const compression = read('src/admin/panel_content.rs');
const ip = read('src/cache/model/http_invalidation.rs');
const cacheMethods = read('src/proxy/cache_http_invalidation.rs');
for (const codec of ['zstd', 'br', 'gzip']) {
assert.match(html, new RegExp(`id="admin-compression-${codec}"[^>]*checked`));
assert(assets.includes('"/assets/app.js"'));
}
for (const path of ['app.js','management.js','vhosts.js','settings.js','cache-panel.js','traffic-panel.js','style.css','langs.json']) {
assert(router.includes(`"/assets/${path}", get(panel_assets::serve)`), path);
assert(assets.includes(`"/assets/${path}"`), path);
}
assert(compression.includes('preferred_encoding(headers, &settings)'));
assert(compression.includes('HeaderValue::from_static("Accept-Encoding")'));
assert(compression.includes('header::CONTENT_LENGTH'));
assert(compression.includes('header::CONTENT_ENCODING'));
assert(compression.includes('crate::compression::no_transform(headers)'));
assert(html.includes('id="cache-http-invalidation-cidrs"'));
assert(html.includes('class="metric-info cache-maintenance-info"'));
assert(html.includes('data-i18n-aria="cacheHttpInvalidationCurlLabel"'));
for (const snippet of ["curl -i -X PURGE", "curl -i -X BAN", "X-RustProxy-Confirm-Wide: 1"]) {
assert(html.includes(snippet), snippet);
}
assert(css.includes('.cache-maintenance-info .metric-info-popover'));
assert(cache.includes('http_invalidation_allowed_cidrs: $("cache-http-invalidation-cidrs")'));
assert(ip.includes('http_invalidation_allowed_cidrs.len() <= 32'));
assert(ip.includes('net.prefix_len() > 0'));
assert(cacheMethods.includes('snapshot.envelope.config.cache.permits_http_invalidation(client_ip)'));
assert(cacheMethods.includes('state.store.audit('));
assert(cacheMethods.includes('spawn_blocking'));
assert(cacheMethods.includes('x-rustproxy-confirm-wide'));
assert(cacheMethods.includes('InvalidationSelector::Exact'));
assert(cacheMethods.includes('InvalidationSelector::Prefix'));
assert(read('src/proxy/forward_entry.rs').includes('return cache_http_invalidation('));
const heading = html.lastIndexOf('data-i18n="systemInformation"');
assert(heading > 0);
const headerArea = html.slice(heading, heading + 600);
assert(headerArea.includes('<span class="pill">API</span>'));
assert(headerArea.indexOf('<span class="pill">API</span>') < headerArea.indexOf('id="system-info-refresh"'));
assert.match(css, /\.system-info-heading-actions\s*\{[^}]*display:\s*flex/);
for (const lang of ['en_US','pl_pl']) {
const strings = JSON.parse(read(`langs/${lang}.json`));
const t = strings.translations;
assert(t.panelCompressionTitle && t.panelCompressionHelp && t.cacheHttpInvalidationHelp, lang);
assert(t.cacheHttpInvalidationCurlLabel && t.cacheHttpInvalidationCurlIntro && t.cacheHttpInvalidationCurlNote, lang);
}
// Execute the actual callbacks captured from the shipped frontend settings JS.
const fields = new Map();
const field = name => {
if (!fields.has(name)) fields.set(name, { checked: false, value: '' });
return fields.get(name);
};
for (const name of ['zstd', 'br', 'gzip']) field(`admin-compression-${name}`).checked = true;
field('admin-listen').value = '127.0.0.1:9090';
field('admin-session-days').value = '30';
let handlers = new Map();
const bindSettingsForm = (id, handler) => handlers.set(id, handler);
const adminStart = administration.indexOf('bindSettingsForm("administration-form", next => {');
const adminEnd = administration.indexOf('bindSettingsForm("tls-policy-form",', adminStart);
assert(adminStart > 0 && adminEnd > adminStart);
runInNewContext(administration.slice(adminStart, adminEnd), {
$: field, bindSettingsForm, latestStatus: { administration: { listen_locked: false } },
});
const adminNext = {};
handlers.get('administration-form')(adminNext);
assert.deepEqual(JSON.parse(JSON.stringify(adminNext.administration.panel_compression)),
{ zstd: true, br: true, gzip: true });
field('admin-compression-br').checked = false;
field('admin-compression-zstd').checked = false;
const offNext = {};
handlers.get('administration-form')(offNext);
assert.deepEqual(JSON.parse(JSON.stringify(offNext.administration.panel_compression)),
{ zstd: false, br: false, gzip: true });
const cacheStart = cache.indexOf('bindSettingsForm("cache-form", next => {');
const cacheEnd = cache.indexOf('bindSettingsForm("transform-form",', cacheStart);
assert(cacheStart > 0 && cacheEnd > cacheStart);
handlers = new Map();
field('cache-http-invalidation-cidrs').value = ' 192.0.2.50\n 2001:db8:2::/64, 198.51.100.9 \n';
field('cache-sie-statuses').value = '500,502,503,504';
runInNewContext(cache.slice(cacheStart, cacheEnd), {
$: field, bindSettingsForm, selectedCacheMemoryPreset: null, selectedCacheColdPreset: null,
});
const cacheNext = { cache: { max_old_setting: 'kept' } };
handlers.get('cache-form')(cacheNext);
assert.deepEqual(JSON.parse(JSON.stringify(cacheNext.cache.http_invalidation_allowed_cidrs)),
['192.0.2.50', '2001:db8:2::/64', '198.51.100.9']);
assert.equal(cacheNext.cache.max_old_setting, 'kept');
field('cache-http-invalidation-cidrs').value = '\n';
handlers.get('cache-form')(cacheNext);
assert.deepEqual(JSON.parse(JSON.stringify(cacheNext.cache.http_invalidation_allowed_cidrs)), []);
console.log('PASS: cache CIDR/administration codec UI roundtrip, headers, security source wiring, API badge layout');
+2 -2
View File
@@ -75,10 +75,10 @@ def check(root: Path, repair_known: bool = False) -> list[str]:
raise ValueError(f"Conflicting Rust modules: {flat.relative_to(root)} and {module.relative_to(root)}")
for path in ["src/auth/mod.rs", "src/auth/store.rs", "src/backup/snapshot.rs", "src/upstream.rs", "src/tls.rs", "src/config/internal_routing.rs", "src/proxy/websocket.rs", "src/proxy/error_fallback.rs", "src/cache/mod.rs", "src/cache/model.rs", "src/cache/lifecycle.rs", "src/cache/cold.rs", "src/cache/dashboard.rs", "src/cache/tickets.rs", "src/cache/lookup.rs", "src/cache/policy.rs", "src/cache/fill.rs", "src/cache/tests.rs", "src/settings.rs", "src/compression.rs", "src/diagnostics.rs", "src/traffic.rs", "src/system_stats/mod.rs", "src/system_stats/linux.rs", "src/vhost.rs", "src/vhost/error_fallback.rs", "src/web_server/mod.rs", "src/web_server/config.rs", "src/web_server/filesystem.rs", "src/web_server/security.rs", "src/web_server/response.rs", "src/web_server/request.rs", "src/web_server/routing.rs", "src/web_server/fastcgi.rs", "src/web_server/htaccess.rs", "src/web_server/serve.rs", "src/web_server/tests.rs", "src/balance.rs", "src/security.rs", "src/admin/web/vhosts/core.js", "src/admin/web/vhosts/health.js", "src/admin/web/vhosts/editor.js", "src/admin/web/vhosts/render.js", "src/admin/web/web-server.js", "src/admin/web/streams.js", "src/admin/web/host404.js", "src/admin/web/app/core.js", "src/admin/web/app/preferences.js", "src/admin/web/app/config-history.js", "src/admin/web/app/runtime.js", "src/admin/web/app/settings.js", "src/admin/web/management.js", "src/admin/web/drafts.js", "src/pages.rs", "tools/dev/mock_server.py", "tools/dev/mock_auth.py", "src/forwarding.rs", "src/tinyauth.rs", "src/http3.rs", "src/stream.rs", "src/admin/web/integrations.js", "src/admin/web/traffic-panel.js", "src/prometheus.rs", "src/admin/web/settings.js", "src/admin/web/docker.js", "src/docker.rs", "docker-agent/main.go", "docker-agent/model.go", "docker-agent/docker.go", "docker-agent/network.go", "docker-agent/certificate.go", "docker-agent/server.go", "docker-agent/main_test.go", "docker-agent/rustproxy-docker-agent.service", "docker-agent/install.sh", "docs/DOCKER_DISCOVERY.md", "docs/WEB_SERVER.md", "docs/PHP_FPM.md", "docs/README.md", "docs/TODO.md", "docs/V0.25.5_AUDIT.md", "docs/V0.25.4_AUDIT.md", "docs/V0.25.3_AUDIT.md", "docs/V0.25.2_AUDIT.md", "docs/CHANGELOG_TECHNICAL_ARCHIVE.md", "docs/ROADMAP_HISTORY.md", "tools/checks/docs_check.py", "tools/checks/web_server_contracts.py", "tools/checks/rust_source.py", "tools/checks/rust_module_contracts.py", "tools/checks/source_module_contracts.py", "scripts/runtime/acme_provider.py", "src/basic_auth.rs", "scripts/deploy/deploy.py", "scripts/deploy/deploy_core.py", "scripts/deploy/deploy_runtime.py", "scripts/migration/migrate_npm.py", "scripts/migration/migrate_npm_core.py", "scripts/migration/migrate_npm_api.py", "docs/NPM_MIGRATION.md", "docs/STREAMS.md", "install.sh", "install-debian13.sh", "update.sh", "build.rs", "scripts/build/swagger_assets.py", "src/admin/documentation.rs", "src/admin/openapi.json", "langs/en_US.json", "langs/pl_pl.json", "Dockerfile", ".dockerignore", "docs/CONTAINER.md", "docs/PROMETHEUS.md", "docs/NOT_FOUND_HOSTS.md", "grafana/rustproxy-overview.json"]:
if not (root / path).is_file():
raise ValueError(f"Incomplete RustProxy 0.30.1 source tree: missing {path}")
raise ValueError(f"Incomplete RustProxy 0.31.0 source tree: missing {path}")
text = (root / "src/lib.rs").read_text(encoding="utf-8")
if '#[path = "auth/mod.rs"]' not in text:
raise ValueError("src/lib.rs is not the 0.30.1 entry point; extract the complete package, not individual files.")
raise ValueError("src/lib.rs is not the 0.31.0 entry point; extract the complete package, not individual files.")
return messages
+229
View File
@@ -0,0 +1,229 @@
#!/usr/bin/env node
// Deterministic browser-coordinator regression without a production browser or network.
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { runInNewContext } from "node:vm";
import { fileURLToPath } from "node:url";
import { resolve, dirname } from "node:path";
const root = resolve(dirname(fileURLToPath(import.meta.url)), "../..");
const scheduler = readFileSync(resolve(root, "src/admin/web/app/refresh.js"), "utf8");
const core = readFileSync(resolve(root, "src/admin/web/app/core.js"), "utf8");
const bridge = core.slice(core.indexOf("function renderLiveStatus(value)"), core.indexOf("async function refreshCertificateInventory()"));
assert.match(bridge, /function startLive\(/);
function fixture({ withoutSse = false } = {}) {
let time = 0, nextId = 1, renderCount = 0, logCount = 0, diagCount = 0;
const timers = new Map(), listeners = new Map(), streams = [], requests = [], rendered = [], failures = [];
let response = async () => ({});
class FakeEventSource {
constructor(url) { this.url = url; this.closed = false; this.handlers = new Map(); streams.push(this); }
addEventListener(type, callback) { this.handlers.set(type, callback); }
close() { this.closed = true; }
emit(type, value) { this.handlers.get(type)?.({ data: JSON.stringify(value) }); }
fail() { this.onerror?.({ type: "error" }); }
}
const nodes = { "connection-label": { textContent: "" } };
const document = {
hidden: false,
addEventListener(type, callback) { assert(!listeners.has(type)); listeners.set(type, callback); },
removeEventListener(type, callback) { assert.equal(listeners.get(type), callback); listeners.delete(type); }
};
const ctx = {
document, AbortController, URLSearchParams,
EventSource: withoutSse ? undefined : FakeEventSource,
setTimeout(fn, ms) { const id = nextId++; timers.set(id, { at: time + ms, fn }); return id; },
clearTimeout(id) { timers.delete(id); },
console: { warn(...args) { failures.push(args); } },
authenticated: true, authEpoch: 1, currentPanel: "overview", liveConnected: false, liveFallback: false,
liveEvents: null, liveInterval: null, latestStatus: null,
window: { rustProxySettings: { current: () => "listeners", render: () => undefined } },
$: name => nodes[name] || null,
t: name => name,
api: (url, args = {}) => { requests.push({ url, args, time }); return response(url, args); },
renderStatus(value) { renderCount++; rendered.push(value); },
renderLogs() { logCount++; },
renderDiagnostics() { diagCount++; },
renderLiveStatus() { throw new Error("bridge not loaded"); },
revalidateSession: async () => undefined,
loadTunnels: async () => undefined
};
runInNewContext(bridge + "\n" + scheduler, ctx, { filename: "coordinator-browser-bundle.js" });
const refresh = ctx.window.rustProxyRefresh;
const settled = async () => { for (let i = 0; i < 12; i++) await Promise.resolve(); };
async function advance(ms = 0) {
const until = time + ms;
for (let i = 0; i < 1000; i++) {
const due = [...timers.entries()].filter(([, timer]) => timer.at <= until).sort((a, b) => a[1].at - b[1].at)[0];
if (!due) { time = until; await settled(); return; }
const [id, timer] = due; time = timer.at; timers.delete(id); timer.fn(); await settled();
}
throw new Error("unbounded polling timer loop");
}
function switchPanel(name) { ctx.currentPanel = name; refresh.setPanel(name); }
function changeVisibility(hidden) { document.hidden = hidden; listeners.get("visibilitychange")?.(); }
function count(url) { return requests.filter(item => item.url.startsWith(url)).length; }
function registerViews() {
for (const name of ["traffic", "cache"]) {
refresh.register(name, {
interval: () => refresh.current()[`${name}_seconds`],
active: () => ctx.currentPanel === name,
run: async ({ signal, isCurrent }) => {
const value = await ctx.api(`/api/${name}`, { signal });
if (isCurrent()) rendered.push({ name, value });
}
});
}
}
return {
ctx, refresh, requests, rendered, streams, listeners, timers, failures, advance, switchPanel, count,
registerViews, changeVisibility,
setResponse(fn) { response = fn; },
get renderCount() { return renderCount; },
get logCount() { return logCount; },
get diagCount() { return diagCount; },
};
}
const h = fixture();
h.registerViews();
assert.equal(h.refresh.current().overview_seconds, 2);
assert.equal(h.refresh.current().system_info_seconds, 60);
assert.equal(h.refresh.current().traffic_seconds, 5);
assert.equal(h.refresh.current().cache_seconds, 5);
assert.equal(h.refresh.current().live_enabled, true);
assert.equal(h.streams.length, 0);
h.refresh.start(); await h.advance();
assert.equal(h.streams.length, 1, "one shared SSE connection");
assert.equal(h.streams[0].url, "/api/events?interval=2");
assert.equal(h.count("/api/traffic"), 0, "no Traffic on Overview");
assert.equal(h.count("/api/cache"), 0, "no Cache on Overview");
assert.equal(h.count("/api/system-info"), 1, "Overview uses slow diagnostics");
assert.equal(h.count("/api/status"), 0, "no duplicate status polling while SSE connects");
assert.equal(h.count("/api/logs"), 0);
await h.advance(10_000);
assert.equal(h.count("/api/system-info"), 1, "diagnostics not collected at live interval");
h.switchPanel("traffic"); await h.advance();
assert.equal(h.count("/api/traffic"), 1);
assert.equal(h.count("/api/cache"), 0);
for (let i = 0; i < 20; i++) { h.switchPanel("overview"); h.switchPanel("traffic"); }
assert.equal(h.refresh.diagnostics().filter(row => row.id === "traffic").length, 1);
await h.advance(0);
assert.equal(h.count("/api/traffic"), 2, "tab switches do not multiply immediate requests");
await h.advance(15_000);
assert.equal(h.count("/api/traffic"), 5, "one request each 5 seconds");
assert.equal(h.count("/api/cache"), 0);
h.refresh.update({ traffic_seconds: 3, cache_seconds: 0, system_info_seconds: 0, overview_seconds: 5 });
assert(h.streams[0].closed, "SSE is replaced when the administrator updates their interval");
assert.equal(h.streams.at(-1).url, "/api/events?interval=5");
await h.advance();
assert.equal(h.count("/api/traffic"), 6);
await h.advance(6_000);
assert.equal(h.count("/api/traffic"), 8);
h.switchPanel("cache"); await h.advance(20_000);
assert.equal(h.count("/api/cache"), 0, "Cache manual mode cancels polling");
await h.refresh.manual("cache");
assert.equal(h.count("/api/cache"), 1, "manual Cache works with interval=0");
await h.advance(40_000);
assert.equal(h.count("/api/cache"), 1);
assert.equal(h.count("/api/traffic"), 8, "Traffic is stopped in Cache view");
// SSE failures activate both fallbacks and a recovered snapshot switches them off.
h.streams.at(-1).fail(); await h.advance();
assert.equal(h.count("/api/status?view=live"), 1);
assert.equal(h.count("/api/logs"), 1);
await h.advance(5_000);
assert.equal(h.count("/api/status?view=live"), 2);
h.streams.at(-1).emit("snapshot", { status: { metrics: { requests: 5 } }, logs: { entries: [] } });
assert.equal(h.ctx.liveConnected, true);
await h.advance(20_000);
assert.equal(h.count("/api/status?view=live"), 2, "SSE recovery stops fallback polling");
assert.equal(h.count("/api/logs"), 2);
assert.equal(h.renderCount, 3, "two fallback status renders followed by recovered SSE");
// Hidden browser tabs pause even previously active sources, then resume cleanly.
h.refresh.update({ traffic_seconds: 3, cache_seconds: 3, system_info_seconds: 15 });
h.switchPanel("traffic"); await h.advance();
const beforeHidden = h.count("/api/traffic");
h.changeVisibility(true);
assert(h.streams.at(-1).closed, "SSE closes when hidden");
assert.equal(h.refresh.diagnostics().filter(row => row.scheduled).length, 0);
await h.advance(120_000);
assert.equal(h.count("/api/traffic"), beforeHidden);
h.changeVisibility(false); await h.advance();
assert.equal(h.count("/api/traffic"), beforeHidden + 1);
assert.equal(h.streams.filter(stream => !stream.closed).length, 1);
// Off pauses all scheduled work, but retains explicit manual requests.
h.refresh.update({ live_enabled: false, traffic_seconds: 3 });
const requestsBeforeOff = h.requests.length;
await h.advance(120_000);
assert.equal(h.requests.length, requestsBeforeOff);
await h.refresh.manual("traffic");
assert.equal(h.count("/api/traffic"), beforeHidden + 2);
assert.equal(h.streams.filter(stream => !stream.closed).length, 0);
h.refresh.update({ live_enabled: true, traffic_seconds: 3 }); await h.advance();
assert.equal(h.streams.filter(stream => !stream.closed).length, 1);
// Aborting an old view prevents the obsolete response from modifying UI.
let resolveDelayed;
h.setResponse(url => url === "/api/traffic" ? new Promise(resolve => { resolveDelayed = resolve; }) : Promise.resolve({ ok: true }));
await h.advance(3_000);
const staleCount = h.rendered.length;
const request = h.requests.at(-1);
assert.equal(request.url, "/api/traffic");
h.switchPanel("cache");
assert.equal(request.args.signal.aborted, true);
resolveDelayed({ stale: true }); await h.advance();
assert.equal(h.rendered.filter(v => v.name === "traffic").length, h.rendered.slice(0, staleCount).filter(v => v.name === "traffic").length);
assert.equal(h.count("/api/cache"), 2);
// Manual diagnostics still work when their own automatic interval is zero.
h.refresh.update({ live_enabled: false, system_info_seconds: 0 });
const diagnosticRequests = h.count("/api/system-info");
await h.refresh.manual("system-info");
assert.equal(h.count("/api/system-info"), diagnosticRequests + 1);
h.refresh.update({ live_enabled: true });
// A failed source does not stop other source registrations.
const e = fixture({ withoutSse: true });
e.registerViews();
e.setResponse(url => url === "/api/traffic" ? Promise.reject(new Error("Traffic disconnected")) : Promise.resolve({ ok: true }));
e.refresh.start(); e.switchPanel("traffic"); await e.advance();
assert.equal(e.count("/api/traffic"), 1);
assert.equal(e.count("/api/status?view=live"), 1, "browsers without EventSource use status polling");
assert.equal(e.count("/api/logs"), 1, "browsers without EventSource use logs polling");
e.switchPanel("cache"); await e.advance();
assert.equal(e.count("/api/cache"), 1);
assert.equal(e.failures.length, 1);
await e.advance(5_000);
assert.equal(e.count("/api/cache"), 2);
// Multiple registrations with identical IDs must be rejected.
assert.throws(() => h.refresh.register("traffic", { interval: () => 5, run: async () => {} }), /Duplicate refresh task/);
// Logout clears timers, active SSE streams, observer and pending fetches.
h.refresh.stop();
assert.equal(h.listeners.has("visibilitychange"), false);
assert.equal(h.streams.filter(stream => !stream.closed).length, 0);
assert.equal(h.refresh.diagnostics().filter(row => row.scheduled).length, 0);
const afterLogout = h.requests.length;
h.ctx.authenticated = false;
await h.advance(600_000);
assert.equal(h.requests.length, afterLogout);
await h.refresh.manual("traffic");
assert.equal(h.requests.length, afterLogout);
// Validate incoming preferences in the browser, including malformed partial records.
const invalid = runInNewContext(scheduler + ";normalizeRefreshPreferences({ live_enabled: 1, overview_seconds: 0, traffic_seconds: 17, cache_seconds: 0, system_info_seconds: 300 });", {
document: { hidden: true, addEventListener() {}, removeEventListener() {} },
AbortController, window: {}, authenticated: false, authEpoch: 0, currentPanel: "overview", liveConnected: false,
liveFallback: false, stopLive() {}, startLive() {}, $: () => null
});
assert.equal(invalid.live_enabled, true);
assert.equal(invalid.overview_seconds, 2);
assert.equal(invalid.traffic_seconds, 5);
assert.equal(invalid.cache_seconds, 0);
assert.equal(invalid.system_info_seconds, 300);
console.log("PASS: per-account refresh validation, SSE/fallback, 5/3/0-s scheduling, tab/visibility/logout lifecycle, isolation, no duplicate/inflight/stale requests, manual mode.");
+11 -10
View File
@@ -135,7 +135,7 @@ def upgrade_smoke(tmp: Path) -> None:
assert backup.is_dir()
assert (backup / "src/obsolete.rs").is_file()
assert not (target / "src/obsolete.rs").exists()
assert 'version = "0.30.1"' in (target / "Cargo.toml").read_text(encoding="utf-8")
assert 'version = "0.31.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()
@@ -146,7 +146,7 @@ def upgrade_smoke(tmp: Path) -> None:
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.30.1"
assert manifest["to_version"] == "0.31.0"
assert manifest["removed_obsolete_runtime_state"] is True
assert not legacy.exists()
assert not (backup / "legacy-data").exists()
@@ -357,7 +357,7 @@ def ui_contract_smoke() -> None:
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
assert 'traffic: "/traffic"' in app and 'window.rustProxyRefresh?.setPanel(name)' 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
@@ -619,7 +619,7 @@ def source_contract_smoke() -> None:
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.30.1"' in openapi_0241
assert '"RuntimeInformation"' in openapi_0241 and '"version": "0.31.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
@@ -726,7 +726,7 @@ def source_contract_smoke() -> None:
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"]:
for marker in ["docker-agent-fingerprint", "docker-agent-certificate", "docker-health-mode", "docker-search", "docker-show-hidden", "pool_routes", "dockerBackendUp", 'rustProxyRefresh.register("docker"']:
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
@@ -972,8 +972,9 @@ def source_contract_smoke() -> None:
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
admin_assets = (ROOT / "src/admin/panel_assets.rs").read_text(encoding="utf-8")
assert '"metrics": stream_metrics' in admin and '"/assets/streams.js"' in admin and 'include_str!("web/streams.js")' in admin_assets
assert '.route("/404-hosts", get(public::index))' in admin and '"/assets/host404.js"' in admin and 'include_str!("web/host404.js")' in admin_assets 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
@@ -990,7 +991,7 @@ def source_contract_smoke() -> None:
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 '"/assets/redirects.js"' in admin and 'include_str!("web/redirects.js")' in admin_assets 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")
@@ -1109,7 +1110,7 @@ def source_contract_smoke() -> None:
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.30.1"', '"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"']:
for marker in ['"version": "0.31.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"']:
@@ -1314,7 +1315,7 @@ def main() -> None:
source_contract_smoke()
container_contract_smoke()
grafana_contract_smoke()
print("PASS: 0.30.1 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.")
print("PASS: 0.31.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__":
+3 -3
View File
@@ -43,7 +43,7 @@ const code = js.slice(js.indexOf(' const runtime = latestStatus?.runtime || {
const elements = new Map();
const dollar = id => { if (!elements.has(id)) elements.set(id, { textContent: '' }); return elements.get(id); };
const status = {
version: '0.30.1', uptime_seconds: 4201,
version: '0.31.0', uptime_seconds: 4201,
runtime: {
platform: 'Linux', operating_system: 'Debian GNU/Linux 13', kernel_version: '6.12.0', architecture: 'x86_64',
statistics_source: 'systemstat+sysinfo', data_directory: '/var/lib/rustproxy', database_path: '/var/lib/rustproxy/config.sqlite3',
@@ -63,7 +63,7 @@ const context = {
vm.runInNewContext(code, context);
assert.equal(dollar('runtime-operating-system').textContent, 'Debian GNU/Linux 13');
assert.equal(dollar('runtime-kernel-version').textContent, '6.12.0');
assert.equal(dollar('runtime-version').textContent, '0.30.1');
assert.equal(dollar('runtime-version').textContent, '0.31.0');
assert.equal(dollar('runtime-logical-cpus').textContent, '8');
assert.equal(dollar('runtime-cpu-usage').textContent, '17.2%');
assert.equal(dollar('runtime-python').textContent, 'Available · 3.13.5');
@@ -124,4 +124,4 @@ for (const key of ['python_version','pip_version','certbot_version']) {
assert.ok(runtimeSchema.properties.certificate_runtime.properties[key]);
assert.equal(runtimeSchema.properties.certificate_runtime.properties[key].nullable, true);
}
console.log('PASS: 0.30.1 legend palette, About cleanup, system tab/navigation, read-only API schema and simulated status rendering.');
console.log('PASS: 0.31.0 legend palette, About cleanup, system tab/navigation, read-only API schema and simulated status rendering.');
@@ -2,6 +2,7 @@
"""Offline source guard for subprocess-free system information; not a Rust runtime test."""
from pathlib import Path
import tomllib
from rust_source import rust_source
ROOT = Path(__file__).resolve().parents[2]
manifest = tomllib.loads((ROOT / 'Cargo.toml').read_text())
@@ -13,7 +14,7 @@ assert {'network', 'system'} <= set(sysinfo_features), (
)
assert any('systemstat' in target.get('dependencies', {}) for target in manifest.get('target', {}).values())
status = (ROOT / 'src/admin/status.rs').read_text()
status = rust_source(ROOT, 'src/admin/status.rs')
assert 'sysinfo::System::long_os_version()' in status
assert 'sysinfo::System::kernel_version()' in status
assert 'std::sync::OnceLock' in status
+3 -1
View File
@@ -12,7 +12,9 @@ for name in ('src/main/bootstrap.rs','src/main/tasks.rs','src/web_server/fastcgi
assert 'env!("CARGO_PKG_VERSION")' in (root/'src/web_server/fastcgi/protocol.rs').read_text()
assert 'env!("CARGO_PKG_VERSION")' in (root/'src/web_server/response/file.rs').read_text()
assert '__RUSTPROXY_VERSION__' in (root/'src/admin/web/index.html').read_text()
assert 'OnceLock<String>' in (root/'src/admin/public.rs').read_text()
assert 'OnceLock<PanelContent>' in (root/'src/admin/public.rs').read_text()
assert 'PanelContent::new' in (root/'src/admin/public.rs').read_text()
assert 'preferred_encoding' in (root/'src/admin/panel_content.rs').read_text()
assert 'include_str!("web/index.html")' in (root/'src/admin/public.rs').read_text()
assert 'env!("CARGO_PKG_VERSION")' in (root/'src/admin/public.rs').read_text()
assert 'admin_index.html' not in (root/'build.rs').read_text()
+1 -1
View File
@@ -54,7 +54,7 @@ def run_load(
port = parsed.port or (443 if parsed.scheme == "https" else 80)
tls = ssl._create_unverified_context() if insecure else ssl.create_default_context()
connection_type = http.client.HTTPSConnection if parsed.scheme == "https" else http.client.HTTPConnection
headers = {"user-agent": "RustProxy-Memory-Stress/0.30.0", "accept": "*/*"}
headers = {"user-agent": "RustProxy-Memory-Stress/0.31.0", "accept": "*/*"}
if host_header:
headers["host"] = host_header