v0.27.3
This commit is contained in:
1 parent
e84df445c6
commit
e87a382a7e
109 files changed
+1092
-2925
No files matched your search
Executable
+147
@@ -0,0 +1,147 @@
|
||||
#!/usr/bin/env python3
|
||||
"""LOOPBACK-ONLY fake Tinyauth protocol peer. NEVER use this as real authentication.
|
||||
A cookie named mock_auth with value allow approves a request. Any client can mint it.
|
||||
This exists to test RustProxy auth_request, identity forwarding and fail-closed paths.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
import argparse
|
||||
from collections import deque
|
||||
from http.cookies import SimpleCookie
|
||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||
import ipaddress
|
||||
import json
|
||||
import threading
|
||||
import time
|
||||
from urllib.parse import urlsplit
|
||||
|
||||
class State:
|
||||
def __init__(self):
|
||||
self.lock = threading.Lock()
|
||||
self.status = None
|
||||
self.delay_ms = 0
|
||||
self.location = None
|
||||
self.calls = 0
|
||||
self.recent = deque(maxlen=16)
|
||||
|
||||
class Server(ThreadingHTTPServer):
|
||||
daemon_threads = True
|
||||
def __init__(self, address, state=None):
|
||||
if not ipaddress.ip_address(address[0]).is_loopback:
|
||||
raise ValueError("The fake auth server may bind only a numeric loopback address")
|
||||
self.state = state or State()
|
||||
super().__init__(address, Handler)
|
||||
|
||||
class Handler(BaseHTTPRequestHandler):
|
||||
protocol_version = "HTTP/1.1"
|
||||
server_version = "RustProxy-Mock-Auth/0.4.0"
|
||||
sys_version = ""
|
||||
def log_message(self, *_):
|
||||
pass
|
||||
def setup(self):
|
||||
super().setup()
|
||||
self.connection.settimeout(5)
|
||||
def reply(self, status, value=None, headers=()):
|
||||
data = b"" if value is None else json.dumps(value, ensure_ascii=True).encode()
|
||||
self.send_response(status)
|
||||
self.send_header("Content-Length", str(len(data)))
|
||||
self.send_header("Cache-Control", "no-store")
|
||||
self.send_header("Content-Type", "application/json")
|
||||
for key, value in headers:
|
||||
self.send_header(key, value)
|
||||
self.end_headers()
|
||||
if self.command != "HEAD":
|
||||
try:
|
||||
self.wfile.write(data)
|
||||
except (BrokenPipeError, ConnectionResetError):
|
||||
pass
|
||||
def do_GET(self):
|
||||
path = urlsplit(self.path).path
|
||||
if self.headers.get("Transfer-Encoding") or self.headers.get("Content-Length", "0") != "0":
|
||||
self.close_connection = True
|
||||
return self.reply(400, {"error":"GET verification requests must have no body"})
|
||||
state = self.server.state
|
||||
if path == "/api/auth/nginx":
|
||||
cookie = SimpleCookie()
|
||||
try:
|
||||
cookie.load(self.headers.get("Cookie", ""))
|
||||
except Exception:
|
||||
pass
|
||||
allowed = "mock_auth" in cookie and cookie["mock_auth"].value == "allow"
|
||||
with state.lock:
|
||||
state.calls += 1
|
||||
# Do not store Cookie, Authorization, API keys, or query strings.
|
||||
state.recent.append({"method":self.headers.get("X-Original-Method"),
|
||||
"client":self.headers.get("X-Real-IP"), "host":self.headers.get("X-Forwarded-Host"),
|
||||
"proto":self.headers.get("X-Forwarded-Proto"),
|
||||
"forwarded":self.headers.get("Forwarded"),"xff":self.headers.get("X-Forwarded-For")})
|
||||
status = state.status if state.status is not None else (204 if allowed else 401)
|
||||
delay_ms, location = state.delay_ms, state.location
|
||||
time.sleep(delay_ms / 1000)
|
||||
headers = []
|
||||
if 200 <= status < 300:
|
||||
headers = [("Remote-User", "mock-user"), ("Remote-Email", "mock@example.test"), ("Remote-Groups", "testers")]
|
||||
if location:
|
||||
headers.append(("X-Tinyauth-Location", location))
|
||||
return self.reply(status, headers=headers)
|
||||
if path == "/state":
|
||||
with state.lock:
|
||||
return self.reply(200, {"calls":state.calls,"status":state.status,"delay_ms":state.delay_ms,"recent":list(state.recent)})
|
||||
if path == "/login":
|
||||
return self.reply(200, {"warning":"Fake login fixture, not authentication", "test_cookie":"mock_auth=allow", "instructions":"Send this cookie to the protected local test vhost."})
|
||||
self.reply(404, {"error":"not found"})
|
||||
def do_POST(self):
|
||||
if urlsplit(self.path).path != "/control":
|
||||
self.close_connection = True
|
||||
return self.reply(405, {"error":"only POST /control is supported"})
|
||||
try:
|
||||
if self.headers.get("Transfer-Encoding") or len(self.headers.get_all("Content-Length", [])) != 1:
|
||||
raise ValueError("one Content-Length is required")
|
||||
size = int(self.headers["Content-Length"])
|
||||
if not 0 <= size <= 4096:
|
||||
raise ValueError("body limit is 4096 bytes")
|
||||
data = self.rfile.read(size)
|
||||
if len(data) != size:
|
||||
raise ValueError("incomplete request")
|
||||
payload = json.loads(data)
|
||||
if not isinstance(payload, dict) or set(payload) - {"status", "delay_ms", "location", "reset"}:
|
||||
raise ValueError("invalid fields")
|
||||
status = payload.get("status")
|
||||
if status is not None and (type(status) is not int or status not in (200, 204, 401, 403, 500, 503)):
|
||||
raise ValueError("unsupported verification status")
|
||||
delay = payload.get("delay_ms", 0)
|
||||
if type(delay) is not int or not 0 <= delay <= 15000:
|
||||
raise ValueError("delay must be 0..15000 ms")
|
||||
location = payload.get("location")
|
||||
if location is not None and (not isinstance(location, str) or len(location) > 2048
|
||||
or any(ord(c) < 32 or ord(c) > 126 for c in location)):
|
||||
raise ValueError("invalid redirect header")
|
||||
with self.server.state.lock:
|
||||
state = self.server.state
|
||||
if payload.get("reset"):
|
||||
state.calls = 0
|
||||
state.recent.clear()
|
||||
state.status, state.delay_ms, state.location = status, delay, location
|
||||
except (ValueError, KeyError, TimeoutError, OSError) as error:
|
||||
self.close_connection = True
|
||||
return self.reply(400, {"error":str(error)})
|
||||
self.reply(200, {"updated":True})
|
||||
def do_HEAD(self):
|
||||
self.do_GET()
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--port", type=int, default=9300)
|
||||
args = parser.parse_args()
|
||||
if not 1 <= args.port <= 65535:
|
||||
parser.error("port must be 1..65535")
|
||||
server = Server(("127.0.0.1", args.port))
|
||||
print(f"FAKE AUTH - LOCAL TESTING ONLY: http://127.0.0.1:{args.port}/api/auth/nginx", flush=True)
|
||||
print("Approve with Cookie: mock_auth=allow. /control configures failures/delays. Ctrl+C stops.", flush=True)
|
||||
try:
|
||||
server.serve_forever()
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
finally:
|
||||
server.server_close()
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Executable
+191
@@ -0,0 +1,191 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Local HTTP/1.1 origin for manual cache, health, compression and pool tests.
|
||||
Only Python's standard library is used. This is not a production application.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
import argparse
|
||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||
import ipaddress
|
||||
import json
|
||||
import secrets
|
||||
import threading
|
||||
import time
|
||||
from urllib.parse import parse_qs, urlsplit
|
||||
|
||||
MAX_BODY = 2 * 1024 * 1024
|
||||
MAX_WORKERS = 32
|
||||
|
||||
class State:
|
||||
def __init__(self, name="origin-a", ttl=30):
|
||||
self.name, self.ttl = name, ttl
|
||||
self.healthy, self.delay_ms, self.sequence = True, 0, 0
|
||||
self.lock = threading.Lock()
|
||||
|
||||
class Server(ThreadingHTTPServer):
|
||||
daemon_threads = True
|
||||
allow_reuse_address = True
|
||||
def __init__(self, address, state):
|
||||
self.state = state
|
||||
self.slots = threading.BoundedSemaphore(MAX_WORKERS)
|
||||
super().__init__(address, Handler)
|
||||
def process_request(self, request, client_address):
|
||||
if not self.slots.acquire(blocking=False):
|
||||
self.shutdown_request(request)
|
||||
return
|
||||
try:
|
||||
super().process_request(request, client_address)
|
||||
except BaseException:
|
||||
self.slots.release()
|
||||
raise
|
||||
def process_request_thread(self, request, client_address):
|
||||
try:
|
||||
super().process_request_thread(request, client_address)
|
||||
finally:
|
||||
self.slots.release()
|
||||
|
||||
class Handler(BaseHTTPRequestHandler):
|
||||
protocol_version = "HTTP/1.1"
|
||||
server_version = "RustProxy-Mock/0.3.0"
|
||||
def setup(self):
|
||||
super().setup()
|
||||
self.connection.settimeout(15)
|
||||
def log_message(self, *_):
|
||||
pass
|
||||
def send(self, status, body=b"", content_type="application/json; charset=utf-8", control="no-store", extra=None):
|
||||
if isinstance(body, dict):
|
||||
body = (json.dumps(body, ensure_ascii=False) + "\n").encode()
|
||||
if isinstance(body, str):
|
||||
body = body.encode()
|
||||
if status in (204, 304):
|
||||
body = b""
|
||||
self.send_response(status)
|
||||
self.send_header("Content-Type", content_type)
|
||||
if status != 204:
|
||||
self.send_header("Content-Length", str(len(body)))
|
||||
self.send_header("Cache-Control", control)
|
||||
self.send_header("X-Mock-Origin", self.server.state.name)
|
||||
for name, value in (extra or {}).items():
|
||||
self.send_header(name, value)
|
||||
self.end_headers()
|
||||
if self.command != "HEAD":
|
||||
try:
|
||||
self.wfile.write(body)
|
||||
except (BrokenPipeError, ConnectionResetError, TimeoutError):
|
||||
self.close_connection = True
|
||||
def integer(self, query, key, default, maximum):
|
||||
value = int(query.get(key, [str(default)])[0])
|
||||
if not 0 <= value <= maximum:
|
||||
raise ValueError(key)
|
||||
return value
|
||||
def do_HEAD(self):
|
||||
self.do_GET()
|
||||
def do_GET(self):
|
||||
target = urlsplit(self.path)
|
||||
q = parse_qs(target.query)
|
||||
state = self.server.state
|
||||
try:
|
||||
delay = self.integer(q, "ms", 800 if target.path == "/slow" else 0, 10000)
|
||||
size = self.integer(q, "size", 65536, MAX_BODY)
|
||||
ttl = self.integer(q, "ttl", state.ttl, 604800)
|
||||
except ValueError:
|
||||
return self.send(400, {"error":"invalid query parameter"})
|
||||
with state.lock:
|
||||
state.sequence += 1
|
||||
sequence, healthy = state.sequence, state.healthy
|
||||
delay += state.delay_ms
|
||||
if delay:
|
||||
time.sleep(min(delay, 10000) / 1000)
|
||||
payload = {"origin":state.name, "sequence":sequence, "generated_at_ns":time.time_ns(),
|
||||
"nonce":secrets.token_hex(16), "path":target.path,
|
||||
"accept_language":self.headers.get("Accept-Language", ""), "healthy":healthy}
|
||||
if target.path == "/health":
|
||||
return self.send(200 if healthy else 503, payload)
|
||||
if target.path == "/state":
|
||||
return self.send(200, {**payload, "delay_ms":state.delay_ms, "ttl":state.ttl})
|
||||
if target.path == "/":
|
||||
return self.send(200, """<!doctype html><meta charset="utf-8"><title>RustProxy mock origin</title>
|
||||
<h1>RustProxy test origin</h1><p>Each uncached request generates a new sequence and nonce.</p>
|
||||
<ul><li><a href="/cache">/cache</a> - public JSON, 30-second freshness by default</li>
|
||||
<li><a href="/nocache">/nocache</a> - no-store JSON</li><li><a href="/private">/private</a> - private JSON</li>
|
||||
<li><a href="/cookie">/cookie</a> - Set-Cookie, bypass shared cache</li><li><a href="/large">/large?size=65536</a> - compressible bytes</li>
|
||||
<li><a href="/slow">/slow?ms=800</a> - controlled delay</li><li><a href="/health">/health</a> - active probe target</li>
|
||||
<li><a href="/status/503">/status/503</a> - custom error page test</li></ul>
|
||||
<p>POST /control with JSON {"healthy":false,"delay_ms":0} to test failover/recovery.</p>""", "text/html; charset=utf-8")
|
||||
if target.path == "/large":
|
||||
prefix = (json.dumps(payload) + "\n").encode()
|
||||
body = (prefix + b"RustProxy compression and cache fixture.\n" * (size // 30 + 1))[:size]
|
||||
return self.send(200, body, "text/plain; charset=utf-8", f"public, max-age={ttl}")
|
||||
if target.path.startswith("/status/"):
|
||||
try:
|
||||
status = int(target.path.rsplit("/", 1)[1])
|
||||
if status not in [200, 204, 400, 401, 403, 404, 408, 413, 429, 500, 502, 503, 504]:
|
||||
raise ValueError()
|
||||
except ValueError:
|
||||
return self.send(400, {"error":"unsupported status"})
|
||||
return self.send(status, payload, extra={"WWW-Authenticate":'Basic realm="mock"'} if status == 401 else None)
|
||||
if target.path == "/cache":
|
||||
return self.send(200, payload, control=f"public, max-age={ttl}", extra={"Vary":"Accept-Language"})
|
||||
if target.path == "/private":
|
||||
return self.send(200, payload, control="private, max-age=60")
|
||||
if target.path == "/cookie":
|
||||
return self.send(200, payload, control=f"public, max-age={ttl}", extra={"Set-Cookie":"mock-session=demo; HttpOnly; SameSite=Lax; Path=/"})
|
||||
self.send(200 if target.path in ("/nocache", "/slow") else 404, payload)
|
||||
def do_POST(self):
|
||||
# Always consume or close a body before reusing an HTTP/1.1 connection.
|
||||
if self.headers.get("Transfer-Encoding") or len(self.headers.get_all("Content-Length", [])) != 1:
|
||||
self.close_connection = True
|
||||
return self.send(411, {"error":"one Content-Length is required"})
|
||||
try:
|
||||
length = int(self.headers["Content-Length"])
|
||||
if not 0 <= length <= 4096:
|
||||
raise ValueError()
|
||||
raw = self.rfile.read(length)
|
||||
if len(raw) != length:
|
||||
raise ValueError()
|
||||
data = json.loads(raw)
|
||||
if self.path != "/control" or not isinstance(data, dict) or set(data) - {"healthy", "delay_ms"}:
|
||||
raise ValueError()
|
||||
if "healthy" in data and not isinstance(data["healthy"], bool):
|
||||
raise ValueError()
|
||||
if "delay_ms" in data and (type(data["delay_ms"]) is not int or not 0 <= data["delay_ms"] <= 10000):
|
||||
raise ValueError()
|
||||
except (ValueError, TimeoutError):
|
||||
self.close_connection = True
|
||||
return self.send(400, {"error":"expected healthy: boolean and/or delay_ms: 0..10000"})
|
||||
with self.server.state.lock:
|
||||
for key, value in data.items():
|
||||
setattr(self.server.state, key, value)
|
||||
self.send(200, {"updated":True})
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--host", default="127.0.0.1")
|
||||
parser.add_argument("--port", type=int, default=9001)
|
||||
parser.add_argument("--name", default="origin-a")
|
||||
parser.add_argument("--ttl", type=int, default=30)
|
||||
parser.add_argument("--allow-remote", action="store_true", help="Explicitly expose this development-only server on a non-loopback IPv4 address")
|
||||
args = parser.parse_args()
|
||||
try:
|
||||
ip = ipaddress.ip_address(args.host)
|
||||
except ValueError:
|
||||
parser.error("--host must be an IPv4 address")
|
||||
if ip.version != 4:
|
||||
parser.error("this development helper accepts IPv4 bindings only")
|
||||
if not ip.is_loopback and not args.allow_remote:
|
||||
parser.error("non-loopback binding requires --allow-remote; /control is unauthenticated")
|
||||
if not 0 <= args.port <= 65535 or not 0 <= args.ttl <= 604800:
|
||||
parser.error("invalid port or TTL")
|
||||
if not args.name or len(args.name) > 64 or any(ord(c) < 32 or ord(c) > 126 for c in args.name):
|
||||
parser.error("--name must contain 1..64 printable ASCII characters")
|
||||
server = Server((args.host, args.port), State(args.name, args.ttl))
|
||||
print(f"Mock origin {args.name}: http://{args.host}:{server.server_port} (development only)", flush=True)
|
||||
print("Endpoints: /cache /nocache /private /cookie /large /slow /health /status/503; POST /control", flush=True)
|
||||
try:
|
||||
server.serve_forever(poll_interval=0.2)
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
finally:
|
||||
server.server_close()
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Executable
+260
@@ -0,0 +1,260 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import argparse
|
||||
import gzip
|
||||
import hashlib
|
||||
import io
|
||||
import time
|
||||
import zlib
|
||||
|
||||
import brotli
|
||||
import requests
|
||||
import zstandard
|
||||
|
||||
|
||||
TESTS = {
|
||||
"identity": "identity",
|
||||
"gzip": "gzip",
|
||||
"deflate": "deflate",
|
||||
"brotli": "br",
|
||||
"zstd": "zstd",
|
||||
"all": "zstd, br, gzip, deflate",
|
||||
}
|
||||
|
||||
|
||||
def decompress(data, content_encoding):
|
||||
encodings = [
|
||||
e.strip().lower()
|
||||
for e in content_encoding.split(",")
|
||||
if e.strip() and e.strip().lower() != "identity"
|
||||
]
|
||||
|
||||
# Content-Encoding usuwa się w odwrotnej kolejności.
|
||||
for encoding in reversed(encodings):
|
||||
if encoding == "gzip":
|
||||
data = gzip.decompress(data)
|
||||
|
||||
elif encoding == "deflate":
|
||||
try:
|
||||
data = zlib.decompress(data)
|
||||
except zlib.error:
|
||||
# Niektóre serwery wysyłają raw DEFLATE.
|
||||
data = zlib.decompress(data, -zlib.MAX_WBITS)
|
||||
|
||||
elif encoding == "br":
|
||||
data = brotli.decompress(data)
|
||||
|
||||
elif encoding == "zstd":
|
||||
dctx = zstandard.ZstdDecompressor()
|
||||
with dctx.stream_reader(io.BytesIO(data)) as reader:
|
||||
data = reader.read()
|
||||
|
||||
else:
|
||||
raise RuntimeError(f"Nieznany Content-Encoding: {encoding}")
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def human_size(size):
|
||||
if size < 1024:
|
||||
return f"{size} B"
|
||||
if size < 1024 * 1024:
|
||||
return f"{size / 1024:.1f} KB"
|
||||
return f"{size / 1024 / 1024:.2f} MB"
|
||||
|
||||
|
||||
def request_test(url, accept_encoding, verify_ssl=True, timeout=15, revalidate=False):
|
||||
headers = {
|
||||
"Accept-Encoding": accept_encoding,
|
||||
"User-Agent": "compression-tester/1.0",
|
||||
}
|
||||
if revalidate:
|
||||
headers["Cache-Control"] = "no-cache"
|
||||
|
||||
start = time.perf_counter()
|
||||
|
||||
r = requests.get(
|
||||
url,
|
||||
headers=headers,
|
||||
timeout=timeout,
|
||||
verify=verify_ssl,
|
||||
stream=True,
|
||||
allow_redirects=True,
|
||||
)
|
||||
|
||||
r.raw.decode_content = False
|
||||
raw = r.raw.read()
|
||||
|
||||
elapsed = time.perf_counter() - start
|
||||
|
||||
content_encoding = r.headers.get("Content-Encoding", "identity")
|
||||
|
||||
try:
|
||||
decoded = decompress(raw, content_encoding)
|
||||
error = None
|
||||
except Exception as exc:
|
||||
decoded = b""
|
||||
error = str(exc)
|
||||
|
||||
return {
|
||||
"status": r.status_code,
|
||||
"requested": accept_encoding,
|
||||
"received": content_encoding,
|
||||
"raw_size": len(raw),
|
||||
"decoded_size": len(decoded),
|
||||
"time": elapsed,
|
||||
"decoded": decoded,
|
||||
"error": error,
|
||||
"headers": r.headers,
|
||||
"final_url": r.url,
|
||||
}
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Tester HTTP Content-Encoding: zstd/br/gzip/deflate"
|
||||
)
|
||||
|
||||
parser.add_argument("url", help="URL do przetestowania")
|
||||
parser.add_argument(
|
||||
"--insecure",
|
||||
action="store_true",
|
||||
help="Wyłącz weryfikację certyfikatu SSL",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--timeout",
|
||||
type=int,
|
||||
default=15,
|
||||
help="Timeout HTTP w sekundach",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--revalidate",
|
||||
action="store_true",
|
||||
help="Wyslij Cache-Control: no-cache (wymusza rewalidacje i moze resetowac Age do 0)",
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.insecure:
|
||||
requests.packages.urllib3.disable_warnings()
|
||||
|
||||
print()
|
||||
print(f"URL: {args.url}")
|
||||
print("=" * 100)
|
||||
|
||||
results = {}
|
||||
|
||||
for name, accept_encoding in TESTS.items():
|
||||
try:
|
||||
result = request_test(
|
||||
args.url,
|
||||
accept_encoding,
|
||||
verify_ssl=not args.insecure,
|
||||
timeout=args.timeout,
|
||||
revalidate=args.revalidate,
|
||||
)
|
||||
results[name] = result
|
||||
except Exception as exc:
|
||||
results[name] = {
|
||||
"error": f"REQUEST ERROR: {exc}"
|
||||
}
|
||||
|
||||
identity_body = None
|
||||
|
||||
if "identity" in results:
|
||||
identity_body = results["identity"].get("decoded")
|
||||
|
||||
print(
|
||||
f"{'TEST':<10}"
|
||||
f"{'HTTP':<7}"
|
||||
f"{'REQUEST':<27}"
|
||||
f"{'RESPONSE':<12}"
|
||||
f"{'RAW':<12}"
|
||||
f"{'DECODED':<12}"
|
||||
f"{'RATIO':<10}"
|
||||
f"{'TIME':<10}"
|
||||
f"{'BODY'}"
|
||||
)
|
||||
|
||||
print("-" * 120)
|
||||
|
||||
failed = False
|
||||
|
||||
for name, result in results.items():
|
||||
if result.get("error"):
|
||||
print(f"{name:<10} ERROR: {result['error']}")
|
||||
failed = True
|
||||
continue
|
||||
|
||||
raw_size = result["raw_size"]
|
||||
decoded_size = result["decoded_size"]
|
||||
|
||||
if decoded_size:
|
||||
ratio = raw_size / decoded_size
|
||||
ratio_text = f"{ratio * 100:.1f}%"
|
||||
else:
|
||||
ratio_text = "-"
|
||||
|
||||
if identity_body is not None and result["decoded"]:
|
||||
if result["decoded"] == identity_body:
|
||||
body_status = "OK"
|
||||
else:
|
||||
body_status = "DIFF"
|
||||
else:
|
||||
body_status = "-"
|
||||
|
||||
print(
|
||||
f"{name:<10}"
|
||||
f"{result['status']:<7}"
|
||||
f"{result['requested']:<27}"
|
||||
f"{result['received']:<12}"
|
||||
f"{human_size(raw_size):<12}"
|
||||
f"{human_size(decoded_size):<12}"
|
||||
f"{ratio_text:<10}"
|
||||
f"{result['time']:.3f}s "
|
||||
f"{body_status}"
|
||||
)
|
||||
|
||||
print()
|
||||
print("=" * 100)
|
||||
print("Szczegóły nagłówków:")
|
||||
print()
|
||||
|
||||
for name, result in results.items():
|
||||
if result.get("error"):
|
||||
continue
|
||||
|
||||
headers = result["headers"]
|
||||
|
||||
print(f"[{name}]")
|
||||
print(f" URL finalny: {result['final_url']}")
|
||||
print(f" HTTP status: {result['status']}")
|
||||
print(f" Content-Encoding: {headers.get('Content-Encoding', '-')}")
|
||||
print(f" Content-Type: {headers.get('Content-Type', '-')}")
|
||||
print(f" Content-Length: {headers.get('Content-Length', '-')}")
|
||||
print(f" Vary: {headers.get('Vary', '-')}")
|
||||
print(f" Cache-Control: {headers.get('Cache-Control', '-')}")
|
||||
print(f" Age: {headers.get('Age', '-')}")
|
||||
print(f" X-Cache: {headers.get('X-Cache', '-')}")
|
||||
print(f" X-Cache-Hits: {headers.get('X-Cache-Hits', '-')}")
|
||||
print(f" Cache-Status: {headers.get('Cache-Status', '-')}")
|
||||
print(f" ETag: {headers.get('ETag', '-')}")
|
||||
print(f" Server: {headers.get('Server', '-')}")
|
||||
print()
|
||||
|
||||
print("SHA256 po dekompresji:")
|
||||
print()
|
||||
|
||||
for name, result in results.items():
|
||||
body = result.get("decoded")
|
||||
|
||||
if body:
|
||||
sha = hashlib.sha256(body).hexdigest()
|
||||
print(f"{name:<10} {sha}")
|
||||
|
||||
if failed:
|
||||
raise SystemExit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in new issue
Block a user