Initial commit: Null DRM Official

Capture, decrypt, and restream toolkit with compiled-in app modules
(RTE, TG4, BBC), on-device MITM proxy, streamd control plane, and www.
BBC module.yaml is published (clear streams); other module values stay local.
This commit is contained in:
404errordeveloper 2026-10-06 00:25:35 +02:00
commit 2fa8f2435f
121 changed files with 17802 additions and 0 deletions

284
apps/pkg/capture/capture.go Normal file
View file

@ -0,0 +1,284 @@
package capture
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"regexp"
"strings"
"time"
"drmdecryption/adb"
)
// Hints tell the waiter which fields / log tags mean a capture is complete.
type Hints struct {
RemoteCaps []string // paths on device to pull
RemoteLog string
Require []string // json keys: auth, pid, pssh, mpd
// BestEffort: return early when any useful field appears; at timeout return
// whatever was collected (error only if completely empty).
BestEffort bool
MPDLogRE *regexp.Regexp
// Score ranks candidate manifest URLs. Zero value falls back to
// DefaultScoreCfg, which has no provider hosts.
Score ScoreCfg
}
// RemoteCapPaths are the on-device locations the MITM may persist its capture
// JSON to. The rte_cap.json entries are legacy names still written by proxies
// already deployed on phones in the field.
var RemoteCapPaths = []string{
"/data/local/tmp/appproxy_cap.json",
"/data/local/tmp/rte_cap.json",
"/sdcard/Download/rte_cap.json",
"/storage/emulated/0/Download/rte_cap.json",
}
// RemoteLogPath is where the on-device MITM writes its log.
const RemoteLogPath = "/data/local/tmp/appproxy.log"
// DefaultHints is the device-layout baseline every app starts from. Callers set
// Require (and optionally Score) for their own DRM shape.
func DefaultHints() Hints {
return Hints{
RemoteCaps: append([]string{}, RemoteCapPaths...),
RemoteLog: RemoteLogPath,
MPDLogRE: regexp.MustCompile(`\[MPD\] (https://\S+)`),
Score: DefaultScoreCfg(),
}
}
// DefaultPassiveHints is for a bare capture run: dump whatever the MITM sees.
func DefaultPassiveHints() Hints {
h := DefaultHints()
h.BestEffort = true
return h
}
// score applies the hints' URL scoring, defaulting when unset.
func (h Hints) score(u string) int {
if len(h.Score.Deny) == 0 && len(h.Score.Hosts) == 0 {
return DefaultScoreCfg().Score(u)
}
return h.Score.Score(u)
}
// Data is the merged capture payload before key fetch.
type Data map[string]string
func (d Data) Get(k string) string { return d[k] }
// Wait pulls device JSON + local mirrored log until required fields exist.
func Wait(c *adb.Client, hints Hints, localLog string, localCap string, timeout time.Duration) (Data, error) {
deadline := time.Now().Add(timeout)
lastNote := ""
for time.Now().Before(deadline) {
cap := Data{}
for _, remote := range hints.RemoteCaps {
_ = os.Remove(localCap)
if err := c.Pull(remote, localCap); err != nil {
continue
}
raw, err := os.ReadFile(localCap)
if err != nil {
continue
}
var m map[string]any
if json.Unmarshal(raw, &m) != nil {
continue
}
for k, v := range m {
if s, ok := v.(string); ok && s != "" {
cap[k] = s
}
}
break
}
logText := ""
if b, err := os.ReadFile(localLog); err == nil {
logText = string(b)
}
enrichFromProxyLog(cap, logText, hints)
if hints.BestEffort {
if hasUseful(cap, hints) {
_ = os.WriteFile(localCap, mustJSON(cap), 0o644)
return cap, nil
}
} else {
missing := false
for _, k := range hints.Require {
if strings.TrimSpace(cap[k]) == "" {
missing = true
break
}
}
if !missing {
_ = os.WriteFile(localCap, mustJSON(cap), 0o644)
return cap, nil
}
}
have := []string{}
keys := hints.Require
if hints.BestEffort {
keys = []string{"mpd", "license_url", "pssh", "auth", "pid"}
}
for _, k := range keys {
if cap[k] != "" {
have = append(have, k)
}
}
note := "json=" + strings.Join(have, ",")
if note == "json=" {
note = "json=none"
}
if strings.Contains(logText, "[LIC]") {
note += " log=LIC"
}
if strings.Contains(logText, "[PSSH]") {
note += " log=PSSH"
}
if strings.Contains(logText, "[MPD]") || strings.Contains(logText, "[MAN]") || strings.Contains(logText, "[MEDIA]") || strings.Contains(logText, "[MS]") {
note += " log=MAN"
}
if note != lastNote {
fmt.Println(" …" + note)
lastNote = note
}
time.Sleep(time.Second)
}
if hints.BestEffort {
// Final pull after timeout — return whatever we got.
cap := Data{}
for _, remote := range hints.RemoteCaps {
_ = os.Remove(localCap)
if err := c.Pull(remote, localCap); err != nil {
continue
}
raw, err := os.ReadFile(localCap)
if err != nil {
continue
}
var m map[string]any
if json.Unmarshal(raw, &m) != nil {
continue
}
for k, v := range m {
if s, ok := v.(string); ok && s != "" {
cap[k] = s
}
}
break
}
logText := ""
if b, err := os.ReadFile(localLog); err == nil {
logText = string(b)
}
enrichFromProxyLog(cap, logText, hints)
if len(cap) > 0 {
_ = os.WriteFile(localCap, mustJSON(cap), 0o644)
return cap, nil
}
return nil, fmt.Errorf("timed out — no DRM/manifest traffic seen (play something on the phone)")
}
return nil, fmt.Errorf("timed out waiting for capture (%v)", hints.Require)
}
func hasUseful(cap Data, hints Hints) bool {
// Real stream signal only — ignore catalog/EPG URLs parked in "mpd".
if mpd := strings.TrimSpace(cap["mpd"]); mpd != "" && hints.score(mpd) > 0 {
return true
}
for _, k := range []string{"license_url", "pssh", "auth", "pid"} {
if strings.TrimSpace(cap[k]) != "" {
return true
}
}
return false
}
var (
reLogLIC = regexp.MustCompile(`\[LIC\]\s+(?:POST|GET)\s+(https://\S+)`)
reLogMPD = regexp.MustCompile(`\[MPD\]\s+(https://\S+)`)
reLogMAN = regexp.MustCompile(`\[MAN\]\s+GET\s+(https://\S+)`)
reLogMEDIA = regexp.MustCompile(`\[MEDIA\]\s+(https://\S+)`)
reLogPSSH = regexp.MustCompile(`\[PSSH\]\s+(\S+)`)
)
func enrichFromProxyLog(cap Data, logText string, hints Hints) {
if logText == "" {
return
}
bestMPD := ""
bestScore := 0
consider := func(u string) {
u = strings.TrimRight(u, ".,)")
if u == "" || !strings.HasPrefix(u, "http") {
return
}
sc := hints.score(u)
// Ignore EPG/schedule/metrics noise (score <= 0).
if sc <= 0 {
return
}
if sc > bestScore {
bestScore = sc
bestMPD = u
}
}
if hints.MPDLogRE != nil {
for _, m := range hints.MPDLogRE.FindAllStringSubmatch(logText, -1) {
consider(m[1])
}
}
for _, m := range reLogMPD.FindAllStringSubmatch(logText, -1) {
consider(m[1])
}
for _, m := range reLogMAN.FindAllStringSubmatch(logText, -1) {
consider(m[1])
}
for _, m := range reLogMEDIA.FindAllStringSubmatch(logText, -1) {
consider(m[1])
}
if bestMPD != "" && (cap["mpd"] == "" || hints.score(bestMPD) > hints.score(cap["mpd"])) {
cap["mpd"] = bestMPD
}
// Drop a previously stored non-manifest "mpd" (e.g. schedules feed).
if cap["mpd"] != "" && hints.score(cap["mpd"]) <= 0 {
delete(cap, "mpd")
}
if cap["license_url"] == "" {
if ms := reLogLIC.FindAllStringSubmatch(logText, -1); len(ms) > 0 {
cap["license_url"] = strings.TrimRight(ms[len(ms)-1][1], ".,)")
}
}
if cap["pssh"] == "" {
if ms := reLogPSSH.FindAllStringSubmatch(logText, -1); len(ms) > 0 {
cap["pssh"] = ms[len(ms)-1][1]
}
}
}
func mustJSON(d Data) []byte {
b, _ := json.MarshalIndent(map[string]string(d), "", " ")
return append(b, '\n')
}
// MirrorLog starts a background `adb exec-out log` equivalent via continuous pull.
// For v1 we periodically pull the remote log file into localLog.
func MirrorLogLoop(c *adb.Client, remote, local string, stop <-chan struct{}) {
_ = os.MkdirAll(filepath.Dir(local), 0o755)
for {
select {
case <-stop:
return
default:
_ = c.Pull(remote, local)
time.Sleep(800 * time.Millisecond)
}
}
}