Files
video_extractor/capture_server.py
T
claudeandClaude Opus 4.8 453ca2f85a Initial commit of video_extractor toolkit
Widevine/DRM research and video download scripts (Apple, CDA, Sky, TVP),
CDM tooling, and helper utilities. Excludes downloads/.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WFUhtZEpkGZJjNCGFS9r4Z
2026-08-08 12:35:28 +02:00

124 lines
5.4 KiB
Python
Executable File

#!/usr/bin/env python3
"""
HTTP server on port 8765:
- serves static files (drm_test.html etc.)
- POST /capture → saves challenge, extracts ClientIdentification as client_id.bin
"""
import http.server, socketserver, os, sys, urllib.request, urllib.error
from pathlib import Path
OUT = Path(__file__).parent / 'L3'
OUT.mkdir(exist_ok=True)
PORT = 8765
class Handler(http.server.SimpleHTTPRequestHandler):
def do_POST(self):
if self.path == '/proxy':
# CORS proxy: forward binary body to TVP license server and return response
TVP_LICENSE = 'https://vod.tvp.pl/api/products/2993670/drm/widevine/external?platform=BROWSER&type=MOVIE'
length = int(self.headers.get('Content-Length', 0))
body = self.rfile.read(length)
print(f' [proxy] {len(body)}b → {TVP_LICENSE[:60]}')
try:
req = urllib.request.Request(TVP_LICENSE, data=body,
headers={'Content-Type': 'application/octet-stream'}, method='POST')
with urllib.request.urlopen(req, timeout=10) as resp:
resp_body = resp.read()
print(f' [proxy] response: {resp.status} {len(resp_body)}b')
self.send_response(resp.status)
self.send_header('Access-Control-Allow-Origin', '*')
self.send_header('Content-Type', 'application/octet-stream')
self.send_header('Content-Length', str(len(resp_body)))
self.end_headers()
self.wfile.write(resp_body)
except Exception as e:
print(f' [proxy] error: {e}')
self.send_response(502)
self.send_header('Access-Control-Allow-Origin', '*')
self.end_headers()
return
if self.path == '/capture':
length = int(self.headers.get('Content-Length', 0))
data = self.rfile.read(length)
print(f'\n[+] Received {len(data)} bytes challenge head={data[:16].hex()}')
(OUT / 'challenge.bin').write_bytes(data)
print(f' Saved → L3/challenge.bin')
# Parse SignedMessage → LicenseRequest → ClientIdentification
try:
from pywidevine import license_protocol_pb2 as pb
sm = pb.SignedMessage()
sm.MergeFromString(data)
print(f' SignedMessage.type={sm.type} msg={len(sm.msg)}b sig={len(sm.signature)}b')
lr = pb.LicenseRequest()
lr.MergeFromString(sm.msg)
ci = lr.client_id
ci_bytes = ci.SerializeToString()
print(f' ClientIdentification: type={ci.type} token={len(ci.token)}b total={len(ci_bytes)}b')
print(f' CI head: {ci_bytes[:16].hex()}')
if len(ci_bytes) > 50:
(OUT / 'client_id.bin').write_bytes(ci_bytes)
print(f'[+] Saved → L3/client_id.bin')
# Try pywidevine create-device
import subprocess
r = subprocess.run(['pywidevine', 'create-device',
'-t', 'android', '-l', '3',
'-k', str(OUT / 'private_key.der'),
'-c', str(OUT / 'client_id.bin'),
'-o', str(OUT)], capture_output=True, text=True)
if r.returncode == 0:
print('\n[+] pywidevine create-device SUCCESS!')
print(r.stdout[-600:])
else:
print('\n[!] pywidevine create-device FAILED:')
print(r.stderr[-400:])
else:
print(f'[!] ClientIdentification too small ({len(ci_bytes)} bytes)')
print(f' Full LicenseRequest fields:')
for f in lr.DESCRIPTOR.fields:
try:
val = getattr(lr, f.name)
if val:
print(f' {f.name}: {str(val)[:80]}')
except: pass
except Exception as e:
import traceback
print(f'[!] Parse error: {e}')
traceback.print_exc()
self.send_response(200)
self.send_header('Access-Control-Allow-Origin', '*')
self.send_header('Content-Type', 'text/plain')
self.end_headers()
self.wfile.write(b'OK')
else:
self.send_response(404)
self.end_headers()
def do_OPTIONS(self):
self.send_response(200)
self.send_header('Access-Control-Allow-Origin', '*')
self.send_header('Access-Control-Allow-Methods', 'POST, GET, OPTIONS')
self.send_header('Access-Control-Allow-Headers', 'Content-Type')
self.end_headers()
def log_message(self, fmt, *args):
if '/capture' in str(args):
pass # already logged above
else:
super().log_message(fmt, *args)
os.chdir(Path(__file__).parent)
print(f'[*] Serving on port {PORT} (static files + POST /capture)')
print(f'[*] In emulator, run:')
print(f' adb reverse tcp:{PORT} tcp:{PORT}')
print(f' Then open http://localhost:{PORT}/drm_test.html\n')
with socketserver.TCPServer(('', PORT), Handler) as httpd:
try:
httpd.serve_forever()
except KeyboardInterrupt:
print('\n[!] Stopped.')