#!/usr/bin/env python3 """Byte-exact manifest fetcher + concurrency test. curl normalises percent-escapes (%2f -> %2F), which breaks an Akamai path token because the HMAC covers the literal path. This sends the request target verbatim via http.client, and can fire N of them at once to see whether the edge objects to concurrent readers of one token. python probe.py --url "" # single fetch python probe.py --url "<...>" --parallel 3 # concurrency test python probe.py --url "<...>" --cookie "name=val" # with a CDN cookie """ import argparse import http.client import sys import threading from urllib.parse import urlsplit UA = ("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " "(KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36") def fetch(url, cookie=None, referer="https://www.bristolrovers.co.uk/", tag=""): parts = urlsplit(url) # Rebuild the target from the raw split so nothing is re-encoded. target = parts.path + (("?" + parts.query) if parts.query else "") conn = http.client.HTTPSConnection(parts.netloc, timeout=15) try: conn.putrequest("GET", target, skip_accept_encoding=True) conn.putheader("User-Agent", UA) conn.putheader("Accept", "*/*") conn.putheader("Referer", referer) conn.putheader("Origin", referer.rstrip("/")) if cookie: conn.putheader("Cookie", cookie) conn.endheaders() resp = conn.getresponse() body = resp.read() print(f"{tag}HTTP {resp.status} {resp.reason} ({len(body)} bytes)") if resp.status == 200: text = body.decode("utf-8", "replace") variants = [l for l in text.splitlines() if l.startswith("#EXT-X-STREAM-INF") or l.endswith(".m3u8")] segs = [l for l in text.splitlines() if l and not l.startswith("#")] if variants: print(f"{tag} master playlist, {len(variants)//2 or len(variants)} rendition(s):") for v in variants[:12]: print(f"{tag} {v}") else: print(f"{tag} media playlist, {len(segs)} segment(s)") for s in segs[:2]: print(f"{tag} {s[:110]}") else: print(f"{tag} {body.decode('utf-8', 'replace')[:200].strip()}") return resp.status except Exception as exc: print(f"{tag}ERROR {type(exc).__name__}: {exc}") return -1 finally: conn.close() def main(): ap = argparse.ArgumentParser() ap.add_argument("--url", required=True) ap.add_argument("--cookie", default=None) ap.add_argument("--parallel", type=int, default=1, help="fire N simultaneous fetches to test concurrency") args = ap.parse_args() if args.parallel <= 1: sys.exit(0 if fetch(args.url, args.cookie) == 200 else 1) print(f"firing {args.parallel} simultaneous requests " f"(keep the browser playing while this runs)\n") results = {} start = threading.Barrier(args.parallel) def worker(i): start.wait() # release them together results[i] = fetch(args.url, args.cookie, tag=f"[{i}] ") threads = [threading.Thread(target=worker, args=(i,)) for i in range(args.parallel)] for t in threads: t.start() for t in threads: t.join() ok = sum(1 for v in results.values() if v == 200) print(f"\n{ok}/{args.parallel} succeeded concurrently" f"{' -- plus the browser itself' if ok else ''}") if __name__ == "__main__": main()