#!/usr/bin/env python3 """Rust-independent 0.25 Web Server architecture/security contract checks.""" from pathlib import Path import json from rust_source import rust_source, rust_tree from web_assets import read_app_js, read_vhosts_js ROOT = Path(__file__).resolve().parents[1] def need(text: str, *markers: str) -> None: for marker in markers: assert marker in text, marker config = rust_source(ROOT, "src/config.rs") vhost = rust_source(ROOT, "src/vhost.rs") web_dir = ROOT / "src/web_server" assert not (ROOT / "src/web_server.rs").exists(), "Web Server monolith must not coexist with src/web_server/mod.rs" 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 (web_dir / module).is_file(), module web = rust_tree(ROOT, "src/web_server") proxy = rust_source(ROOT, "src/proxy/mod.rs") admin = rust_source(ROOT, "src/admin/mod.rs") ui = read_vhosts_js(ROOT) web_ui = (ROOT / "src/admin/web/web-server.js").read_text() app_ui = read_app_js(ROOT) html = (ROOT / "src/admin/web/index.html").read_text() settings_ui = (ROOT / "src/admin/web/settings.js").read_text() logging = rust_source(ROOT, "src/logging.rs") metrics = rust_source(ROOT, "src/metrics.rs") acl = rust_source(ROOT, "src/acl.rs") openapi = json.loads((ROOT / "src/admin/openapi.json").read_text()) need(config, "pub web_services: Vec", "pub enum CompiledSource", "CompiledSource::WebServer", "unknown Web Service", "web_server_main_source_needs_no_backend_pool", "web_server_and_reverse_proxy_can_share_one_vhost", "one_web_service_can_be_reused_by_multiple_vhosts", "missing_source_fields_remain_reverse_proxy_for_old_configs") need(vhost, "pub enum Source", "WebServer { web_service_id: String }", "pub source: Source", "vhost requires at least one backend member for Reverse Proxy sources", "Web Server location cannot pin a reverse-proxy backend") need(web, "pub struct Service", "pub document_root: PathBuf", "pub index: Vec", "pub directory_listing: bool", "pub mime_types: BTreeMap", "pub default_mime_type: String", "pub cache_control: Option", "pub cache_rules: Vec", "pub response_headers: BTreeMap", "pub try_files: Vec", "pub rewrites: Vec", "pub precompressed: Precompressed", "pub download_extensions: Vec", "pub symlink_policy: SymlinkPolicy", "pub php: Php", "pub htaccess: HtaccessConfig", "pub hardening: Hardening", "hardening_blocks_common_probe_paths_and_trace", "hardening_strips_fingerprint_headers_after_source_rules", "HTTP_PROXY is a well-known CGI/FastCGI injection vector", 'if name.as_str() == "proxy"', '"content-length"', '"proxy-authenticate"', '"proxy-authorization"', "mod htaccess", "htaccess::apply_rewrites", "htaccess::protected_component", "htaccess_wordpress_style_front_controller_and_protected_files_work", "htaccess_directory_index_mime_headers_expires_and_nested_scope_work", "htaccess_strict_mode_rejects_unsupported_directives", "htaccess_security_sensitive_unsupported_directives_fail_closed", "php_source_extensions_are_not_served_without_php_fpm", "htaccess_php_addhandler_requires_php_fpm", "FastCgiEndpoint", "FCGI_BEGIN_REQUEST", "try_acquire_owned", "CONTENT_DISPOSITION", "CONTENT_ENCODING", "libc::openat", "libc::O_NOFOLLOW", "ReaderStream::new(file.take(body_length))", "IF_NONE_MATCH", "IF_MODIFIED_SINCE", "IF_RANGE", "RANGE", "StatusCode::NOT_MODIFIED", "StatusCode::PARTIAL_CONTENT", "StatusCode::RANGE_NOT_SATISFIABLE", "listing_escapes_html_and_encodes_urls", "root_directory_listing_is_repeatable_across_refreshes", "index_precedes_listing_and_missing_index_falls_back_to_listing_per_directory", "mime_overrides_default_mime_and_cache_control_are_applied_to_files", 'HeaderValue::from_static("no-store")', "builtin_error_response(StatusCode::FORBIDDEN, &method)", "empty_index_can_go_directly_to_directory_listing", "RustProxy 0.26.6", "traversal_and_symlink_escape_are_not_served", "try_files_rewrites_cache_rules_headers_and_downloads_work", "precompressed_sidecars_keep_mime_validators_and_range_semantics", "php_fpm_executes_index_and_try_files_front_controller_without_source_disclosure", "php_fpm_executes_configured_directory_index", "php_fpm_failure_returns_gateway_error_instead_of_php_source", "inside_root_symlink_policy_allows_only_targets_beneath_root", "secure filesystem confinement currently requires a Unix platform") need(proxy, "if let crate::config::CompiledSource::WebServer(service) = &route.source", "let static_method = request.method().clone()", "crate::web_server::serve(", "static_headers", "response.extensions_mut().insert(StaticSourceResponse)", "let custom_error =", "crate::pages::replace_error", "apply_response_rules(", "finish_headers(", "crate::compression::encode(") assert "crate::web_server::serve(service, &request" not in proxy assert proxy.index("basic_identity =") < proxy.index("CompiledSource::WebServer(service)") assert proxy.index("let identity =") < proxy.index("CompiledSource::WebServer(service)") assert proxy.index("CompiledSource::WebServer(service)") < proxy.index("let (backend, backend_lease, affinity_cookie)") custom_error_pos = proxy.index("let custom_error =") assert proxy.find("apply_response_rules(", custom_error_pos) > custom_error_pos need(acl, "ambiguous encoded path character", '"/%2e%2e/secret"', '"/a%2fb"', '"/a%5cb"', '"/bad%"') need(admin, '"/web-server"', '"/assets/web-server.js"', '"web-services"', '"vhost_metrics"') need(html, 'data-panel="web-server"', 'id="panel-web-server"', 'id="web-service-dialog"', 'id="web-service-tabs"', 'data-web-service-page="general"', 'data-web-service-page="routing"', 'data-web-service-page="mime"', 'data-web-service-page="cache"', 'data-web-service-page="delivery"', 'data-web-service-page="php"', 'data-web-service-page="security"', 'data-web-service-page="htaccess"', 'id="web-service-htaccess-enabled"', 'id="web-service-htaccess-help"', 'id="htaccess-help-dialog"', 'id="web-service-try-files"', 'id="web-service-cache-rules"', 'id="web-service-precompressed"', 'id="web-service-php-enabled"', 'id="web-service-php-pass-authorization"', 'id="web-service-php-body-limit"', 'id="web-service-php-header-limit"', 'id="web-service-php-stderr-limit"', 'id="web-service-php-custom-params"', 'id="web-service-php-values"', 'id="php-help-dialog"', 'id="web-service-symlink-policy"', 'id="web-service-preset"', 'id="web-service-preset-apply"', 'id="web-service-hardening-attacks"', 'id="web-service-hardening-headers"', 'data-logging-settings-tab="outputs"', 'data-logging-settings-tab="format"', 'data-logging-settings-tab="errors"') need(settings_ui, "selectLoggingTab", 'data-logging-settings-page', 'selectLoggingTab("outputs")') need(web_ui, 'saveSection("web-services"', "directory_listing", "web_service_id", "mime_types", "default_mime_type", "cache_control", "cache_rules", "response_headers", "try_files", "rewrites", "precompressed", "download_extensions", "symlink_policy", "php", "htaccess", "hardening", "presetConfig", "phpFrontController", "staticCacheRules", "pass_authorization", "max_request_body_bytes", "max_response_header_bytes", "stderr_log_bytes", "custom_params", "php_values", "syncHtaccess", "htaccess-help-dialog", "php-help-dialog", "parseFastCgiParams", "parsePhpValues", "validatePhpCustomData", "activateTab", "initTabs", "presetChanged", 'button = el("button", t(key), "quiet")', 'button.dataset.tab = name', 'button.setAttribute("role", "tab")', "vhost-detail", 'el("span", t(key), "small-label")', "if (envelope) render();") need(logging, '"route"', '"cache_effective_enabled"', '"transform_effective_enabled"', 'value["cache_enabled"]', 'value["transform_enabled"]') need(metrics, "pub struct VhostMetricsView", "ewma_duration_us", "pub fn view(&self) -> VhostMetricsView") assert 'item.append(text("span"' not in web_ui need(app_ui, 'function showStartupFailure(error)', '$("login").hidden = true', 'const info = await api("/api/auth/session")') assert '$("login").hidden = false; document.documentElement.classList.remove("app-loading")' not in app_ui need(ui, 'type: "web_server"', "web_service_id", "sourceReverseProxy", "vhBackendRequired", "backend-diagnostics-card", "status.vhost_metrics", "vhostTrafficRate") assert "WebService" in openapi.get("components", {}).get("schemas", {}) web_service_schema = openapi["components"]["schemas"]["WebService"] assert web_service_schema["properties"]["index"].get("minItems", 0) == 0 assert web_service_schema["properties"]["index"]["maxItems"] == 16 assert web_service_schema["properties"]["mime_types"]["maxProperties"] == 64 assert web_service_schema["properties"]["default_mime_type"]["default"] == "application/octet-stream" assert web_service_schema["properties"]["cache_control"]["nullable"] is True for prop in ["cache_rules", "response_headers", "try_files", "rewrites", "precompressed", "download_extensions", "symlink_policy", "php", "htaccess", "hardening"]: assert prop in web_service_schema["properties"], prop assert web_service_schema["properties"]["symlink_policy"]["default"] == "deny" assert web_service_schema["properties"]["php"]["properties"]["enabled"]["default"] is False php_schema = web_service_schema["properties"]["php"]["properties"] assert php_schema["pass_authorization"]["default"] is True assert php_schema["max_request_body_bytes"]["default"] == 0 assert php_schema["max_response_header_bytes"]["default"] == 65536 assert php_schema["stderr_log_bytes"]["default"] == 4096 assert php_schema["custom_params"]["maxProperties"] == 64 assert php_schema["php_values"]["maxProperties"] == 64 assert web_service_schema["properties"]["htaccess"]["properties"]["enabled"]["default"] is False assert web_service_schema["properties"]["htaccess"]["properties"]["filename"]["default"] == ".htaccess" assert web_service_schema["properties"]["hardening"]["properties"]["block_common_attacks"]["default"] is False assert web_service_schema["properties"]["hardening"]["properties"]["strip_unnecessary_headers"]["default"] is False htaccess_source = rust_source(ROOT, "src/web_server/htaccess.rs") need(htaccess_source, "MAX_HTACCESS_FILES", "MAX_HTACCESS_TOTAL_BYTES", "RewriteEngine", "RewriteCond", "RewriteRule", "DirectoryIndex", "AddType", "AddHandler", "ExpiresByType", "Require all granted/denied", "general Files/FilesMatch semantics are not supported", "security/handler directive", "apply_response", "apply_rewrites") assert "Source" in openapi.get("components", {}).get("schemas", {}) assert "client_ips" in openapi["components"]["schemas"]["TrafficCounters"]["properties"] assert (ROOT / "docs/WEB_SERVER.md").is_file() assert (ROOT / "docs/PHP_FPM.md").is_file() assert (ROOT / "docs/V0.25.5_AUDIT.md").is_file() assert (ROOT / "docs/V0.25.4_AUDIT.md").is_file() assert (ROOT / "docs/V0.25.2_AUDIT.md").is_file() assert (ROOT / "docs/V0.25.0_AUDIT.md").is_file() assert (ROOT / "docs/TODO.md").is_file() docs_html = (ROOT / "src/admin/web/docs.html").read_text() docs_js = (ROOT / "src/admin/web/docs.js").read_text() need(docs_html, "docs-tab-reader", "docs-reader-content", "docs-tab-api") need(docs_js, 'get("/docs/content.json")', "renderMarkdown", "selectDocument") print("PASS: 0.25 Web Server separation, pipeline, filesystem, HTTP, UI/API and regression contracts are present.")