133 lines
7.4 KiB
Python
Executable File
133 lines
7.4 KiB
Python
Executable File
"""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)
|