"""Live static-file regression cases against RustProxy's network listener.""" import copy from email.parser import BytesParser from email.policy import default import gzip from common import http def run(check, rust, peer): port, fixture = rust.static_port, rust.fixture for name in ["small.html", "large.bin", "huge.bin", "empty.txt"]: expected = (fixture.site / name).read_bytes() status, headers, body = http(port, "/" + name) check(f"static.get.{name}", status == 200 and body == expected, status=status, bytes=len(body)) head_status, head_headers, head_body = http(port, "/" + name, "HEAD") check(f"static.head.{name}", head_status == 200 and head_body == b"" and head_headers.get("content-length") == str(len(expected)), status=head_status, content_length=head_headers.get("content-length")) status, headers, body = http(port) tag = headers.get("etag") modified = headers.get("last-modified") check("static.validators_present", bool(tag and modified), etag=tag, last_modified=modified) for label, condition in [("etag", {"If-None-Match": tag}), ("modified", {"If-Modified-Since": modified})]: s, h, b = http(port, headers=condition) check("static.conditional304." + label, s == 304 and b == b"", status=s, bytes=len(b)) s, h, b = http(port, headers={"If-None-Match": '"missing"'}) check("static.conditional_miss", s == 200 and b == body, status=s) data = (fixture.site / "large.bin").read_bytes() cases = [("prefix", "bytes=0-127", data[:128], "bytes 0-127/65536"), ("suffix", "bytes=-37", data[-37:], "bytes 65499-65535/65536"), ("open", "bytes=65500-", data[65500:], "bytes 65500-65535/65536")] for name, range_header, expected, content_range in cases: s, h, b = http(port, "/large.bin", headers={"Range": range_header}) check("static.range." + name, s == 206 and b == expected and h.get("content-range") == content_range, status=s, bytes=len(b), content_range=h.get("content-range")) s, h, b = http(port, "/large.bin", headers={"Range": "bytes=999999-"}) check("static.range.unsatisfiable", s == 416 and h.get("content-range") == "bytes */65536", status=s, content_range=h.get("content-range")) s, h, b = http(port, "/large.bin", headers={"Range": "bytes=0-15,100-115"}) mime = BytesParser(policy=default).parsebytes( b"Content-Type: " + h.get("content-type", "").encode() + b"\r\n\r\n" + b) parts = list(mime.iter_parts()) check("static.range.multipart", s == 206 and len(parts) == 2 and parts[0].get_payload(decode=True) == data[:16] and parts[1].get_payload(decode=True) == data[100:116], status=s, parts=len(parts)) for name, value, expected_status in [("match", tag, 206), ("mismatch", '"other"', 200)]: s, h, b = http(port, headers={"Range": "bytes=0-15", "If-Range": value}) expected = body[:16] if expected_status == 206 else body check("static.if_range." + name, s == expected_status and b == expected, status=s, bytes=len(b)) s, h, b = http(port, "/directory") check("static.directory_slash", s in (301, 308) and h.get("location", "").endswith("/directory/"), status=s, location=h.get("location")) s, h, b = http(port, "/directory/") check("static.directory_index", s == 200 and b == b"directory index\n", status=s) s, h, b = http(port, "/listing/") check("static.directory_listing_denied", s == 403, status=s) for path in ["/escape-link.txt", "/inside-link.txt"]: s, h, b = http(port, path) check("static.symlink_deny." + path, s == 403 and b"MUST-NOT" not in b, status=s) for path in ["/%2e%2e/outside-secret.txt", "/%2fetc/passwd", "/%252e%252e/outside-secret.txt", "/%00", "/..%5coutside-secret.txt"]: s, h, b = http(port, path) check("static.invalid_path." + path, s in (400, 403, 404) and b"MUST-NOT" not in b, status=s) config = copy.deepcopy(rust.base) service = config["web_services"][0] service["symlink_policy"] = "inside_root" service["directory_listing"] = True rust.apply(config, "static-inside-root") s, h, b = http(port, "/inside-link.txt") check("static.symlink_inside_root_allowed", s == 200 and b == b"download fixture\n", status=s) s, h, b = http(port, "/escape-link.txt") check("static.symlink_inside_root_escape_denied", s == 403 and b"MUST-NOT" not in b, status=s) s, h, b = http(port, "/listing/") check("static.directory_listing_enabled", s == 200 and b"visible.txt" in b, status=s) # Alternate a symlink target between valid and outside-root files. The file # read must stay confined even when a pathname changes between requests. link = fixture.site / "changing-link.txt" race_statuses = [] escaped = False for index in range(12): if link.is_symlink(): link.unlink() link.symlink_to(fixture.outside if index % 2 else fixture.site / "download.txt") s, h, b = http(port, "/changing-link.txt") race_statuses.append(s) escaped = escaped or b"MUST-NOT" in b check("static.changing_symlink_confinement", not escaped and all(s in (200, 403) for s in race_statuses), statuses=race_statuses, note="Sequential target replacement; not a concurrent rename-race proof") config = copy.deepcopy(rust.base) config["web_services"][0]["precompressed"] = {"enabled": True, "gzip": True, "brotli": False} rust.apply(config, "static-precompressed") s, h, b = http(port, headers={"Accept-Encoding": "gzip"}) sidecar = (fixture.site / "small.html.gz").read_bytes() check("static.precompressed_gzip", s == 200 and h.get("content-encoding") == "gzip" and b == sidecar and gzip.decompress(b) == body, status=s, bytes=len(b)) s, h, b = http(port, headers={"Accept-Encoding": "gzip", "Range": "bytes=0-9"}) check("static.precompressed_range", s == 206 and b == sidecar[:10], status=s, bytes=len(b)) config = copy.deepcopy(rust.base) config["web_services"][0]["try_files"] = ["$uri", "/small.html"] config["web_services"][0]["rewrites"] = [{"pattern": "^/old$", "replacement": "/small.html", "status": 302}] config["web_services"][0]["download_extensions"] = ["txt"] config["web_services"][0]["mime_types"] = {"bin": "application/x-fixture"} rust.apply(config, "static-routing") s, h, b = http(port, "/spa/deep/link") check("static.try_files_spa", s == 200 and b == body, status=s) s, h, b = http(port, "/old") check("static.rewrite_redirect", s == 302 and h.get("location") == "/small.html", status=s) s, h, b = http(port, "/download.txt") check("static.download_header", s == 200 and "attachment" in h.get("content-disposition", ""), status=s) s, h, b = http(port, "/large.bin") check("static.mime_override", s == 200 and h.get("content-type") == "application/x-fixture", status=s) config = copy.deepcopy(rust.base) (fixture.site / ".htaccess").write_text("Header set X-Fixture-Htaccess yes\nDirectoryIndex small.html\n") config["web_services"][0]["htaccess"] = {"enabled": True, "strict": True} rust.apply(config, "static-htaccess") s, h, b = http(port) check("static.htaccess_header", s == 200 and h.get("x-fixture-htaccess") == "yes", status=s) s, h, b = http(port, "/.htaccess") check("static.htaccess_not_served", s in (403, 404) and b"Header set" not in b, status=s)