96 lines
3.6 KiB
Python
96 lines
3.6 KiB
Python
|
|
#!/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 "<manifest 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()
|