mirror of
https://github.com/Ap0dexMe0/o11pro-unpacked.git
synced 2026-07-15 18:30:02 +02:00
released v1.3.5
This commit is contained in:
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
.vscode/
|
.vscode/
|
||||||
launcher.go
|
launcher.go
|
||||||
/build/
|
/build/
|
||||||
o11pro
|
|
||||||
workspace-69e17692-9d10-431b-adee-503d25eb1a71.tar
|
workspace-69e17692-9d10-431b-adee-503d25eb1a71.tar
|
||||||
venv/
|
venv/
|
||||||
|
cache/
|
||||||
@@ -71,61 +71,20 @@ if [ -f "$RUNME" ]; then
|
|||||||
# Make it executable
|
# Make it executable
|
||||||
chmod +x "$RUNME"
|
chmod +x "$RUNME"
|
||||||
|
|
||||||
# Check if we already patched it (look for our marker)
|
echo "RunMe.sh PATH is configured dynamically."
|
||||||
if ! grep -q "# VENV_PATCHED" "$RUNME"; then
|
|
||||||
echo "Patching RunMe.sh to use virtual environment..."
|
|
||||||
# Insert venv PATH right after the existing export PATH line
|
|
||||||
sed -i.bak \
|
|
||||||
-e 's|^export PATH="/usr/bin:/bin:/usr/local/bin:${PATH:-}"|# VENV_PATCHED\nexport PATH="'"$VENV_DIR"'/bin:/usr/bin:/bin:/usr/local/bin:${PATH:-}"|' \
|
|
||||||
"$RUNME"
|
|
||||||
echo "RunMe.sh patched (backup saved as RunMe.sh.bak)"
|
|
||||||
else
|
|
||||||
echo "RunMe.sh already patched."
|
|
||||||
fi
|
|
||||||
else
|
else
|
||||||
echo "ERROR: RunMe.sh not found at $RUNME"
|
echo "ERROR: RunMe.sh not found at $RUNME"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "Checking required files in $SRC_DIR ..."
|
echo "Checking required files in $SRC_DIR ..."
|
||||||
REQUIRED_FILES=("o11pro" "o11.cfg" "providers/sample.cfg")
|
if [ ! -f "$SRC_DIR/o11pro" ]; then
|
||||||
MISSING=0
|
echo "ERROR: Required file 'src/o11pro' not found."
|
||||||
for f in "${REQUIRED_FILES[@]}"; do
|
echo "Please place o11pro binary under src/."
|
||||||
if [ ! -f "$SRC_DIR/$f" ]; then
|
|
||||||
echo "ERROR: Required file 'src/$f' not found."
|
|
||||||
MISSING=1
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
if [ $MISSING -eq 1 ]; then
|
|
||||||
echo "Please place all required files under src/."
|
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
echo "All required files present."
|
echo "All required files present."
|
||||||
|
|
||||||
echo "Building hls_proxy channel URL config ..."
|
|
||||||
"$VENV_DIR/bin/python3" -c "
|
|
||||||
import json, sys
|
|
||||||
cfg_path = '$SRC_DIR/providers/sample.cfg'
|
|
||||||
out_path = '/tmp/o11pro_orig_urls.json'
|
|
||||||
try:
|
|
||||||
d = json.load(open(cfg_path))
|
|
||||||
urls = {}
|
|
||||||
for s in d.get('Streams', []):
|
|
||||||
name = s.get('Name', '')
|
|
||||||
manifest = s.get('Manifest', '')
|
|
||||||
if name and manifest:
|
|
||||||
urls[name] = manifest
|
|
||||||
if urls:
|
|
||||||
json.dump(urls, open(out_path, 'w'), indent=2, ensure_ascii=False)
|
|
||||||
print(f' Wrote {len(urls)} channel URLs to {out_path}')
|
|
||||||
else:
|
|
||||||
print(' WARNING: No channel URLs found in provider config')
|
|
||||||
json.dump({}, open(out_path, 'w'))
|
|
||||||
except Exception as e:
|
|
||||||
print(f' WARNING: Could not build URL config: {e}')
|
|
||||||
json.dump({}, open(out_path, 'w'))
|
|
||||||
"
|
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
MODE_PARTS=("o11pro :${1:-19999}") # FIX: was $1 (unbound when no args)
|
MODE_PARTS=("o11pro :${1:-19999}") # FIX: was $1 (unbound when no args)
|
||||||
if [ "${HLS_PROXY:-true}" = "true" ]; then
|
if [ "${HLS_PROXY:-true}" = "true" ]; then
|
||||||
|
|||||||
+25
-61
@@ -10,7 +10,7 @@
|
|||||||
# - Correct PATH for python3.13 dependencies
|
# - Correct PATH for python3.13 dependencies
|
||||||
#
|
#
|
||||||
# Usage:
|
# Usage:
|
||||||
# ./RunMe.sh # Run with defaults (port 19999, verbose=2)
|
# ./RunMe.sh # Run with defaults (port 1337, verbose=2)
|
||||||
# ./RunMe.sh 8080 # Run on custom port
|
# ./RunMe.sh 8080 # Run on custom port
|
||||||
# ./RunMe.sh 8080 4 # Run on port 8080 with verbose=4 (trace)
|
# ./RunMe.sh 8080 4 # Run on port 8080 with verbose=4 (trace)
|
||||||
# GOMEMLIMIT=4G ./RunMe.sh # Override memory limit
|
# GOMEMLIMIT=4G ./RunMe.sh # Override memory limit
|
||||||
@@ -20,8 +20,6 @@
|
|||||||
#
|
#
|
||||||
# Files required (in same directory as this script):
|
# Files required (in same directory as this script):
|
||||||
# o11pro Patched binary
|
# o11pro Patched binary
|
||||||
# o11.cfg Global config
|
|
||||||
# providers/sample.cfg Provider config
|
|
||||||
# keys.txt DRM KID:key pairs (optional)
|
# keys.txt DRM KID:key pairs (optional)
|
||||||
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
@@ -29,32 +27,31 @@ set -euo pipefail
|
|||||||
# Configuration
|
# Configuration
|
||||||
|
|
||||||
# Security monitor: set to true to run through the HTTP proxy/monitor
|
# Security monitor: set to true to run through the HTTP proxy/monitor
|
||||||
# Architecture: Client -> :19998 (proxy/monitor) -> :19999 (real o11 API)
|
# Architecture: Client -> :1339 (proxy/monitor) -> :1337 (real o11 API)
|
||||||
MONITOR="${MONITOR:-false}"
|
MONITOR="${MONITOR:-true}"
|
||||||
|
|
||||||
# Monitor proxy listen port (only used when MONITOR=true)
|
# Monitor proxy listen port (only used when MONITOR=true)
|
||||||
MONITOR_PORT="${MONITOR_PORT:-19998}"
|
MONITOR_PORT="${MONITOR_PORT:-1339}"
|
||||||
|
|
||||||
# FIX: HLS Proxy config was missing entirely
|
# FIX: HLS Proxy config was missing entirely
|
||||||
# Architecture: Player -> :9999 (hls_proxy) -> upstream CDN
|
# Architecture: Player -> :1338 (hls_proxy) -> upstream CDN
|
||||||
HLS_PROXY="${HLS_PROXY:-true}"
|
HLS_PROXY="${HLS_PROXY:-true}"
|
||||||
HLS_PROXY_PORT="${HLS_PROXY_PORT:-9999}"
|
HLS_PROXY_PORT="${HLS_PROXY_PORT:-1338}"
|
||||||
HLS_PROXY_BIND="${HLS_PROXY_BIND:-0.0.0.0}"
|
HLS_PROXY_BIND="${HLS_PROXY_BIND:-127.0.0.1}"
|
||||||
HLS_PROXY_CONFIG="${HLS_PROXY_CONFIG:-/tmp/o11pro_orig_urls.json}"
|
|
||||||
|
|
||||||
# Path setup: need /usr/bin first so binary spawns python3.13 (has deps)
|
# Path setup: need /usr/bin first so binary spawns python3.13 (has deps)
|
||||||
# instead of venv python3.12 (lacks deps like curl_cffi, cloudscraper)
|
# instead of venv python3.12 (lacks deps like curl_cffi, cloudscraper)
|
||||||
# VENV_PATCHED
|
# VENV_PATCHED
|
||||||
export PATH="/home/z/my-project/o11/venv/bin:/usr/bin:/bin:/usr/local/bin:${PATH:-}"
|
|
||||||
|
|
||||||
# Port for HTTP API and streaming (default 19999, override with $1)
|
KID_PATCH_OFFSET="${KID_PATCH_OFFSET:-0x15625cd}"
|
||||||
PORT="${1:-19999}"
|
|
||||||
|
# Port for HTTP API and streaming (default 1337, override with $1)
|
||||||
|
PORT="${1:-1337}"
|
||||||
|
|
||||||
# FIX: was VERBOSE="${2:-2}" (always defaulted to 2 even when $2 was set)
|
# FIX: was VERBOSE="${2:-2}" (always defaulted to 2 even when $2 was set)
|
||||||
VERBOSE="${2:-2}"
|
VERBOSE="${2:-2}"
|
||||||
|
|
||||||
# Bind address: 0.0.0.0 = all interfaces (use 127.0.0.1 for localhost-only)
|
# Bind address: 127.0.0.1 = all interfaces (use 127.0.0.1 for localhost-only)
|
||||||
BIND="${BIND:-0.0.0.0}"
|
BIND="${BIND:-127.0.0.1}"
|
||||||
|
|
||||||
GOMEMLIMIT="${GOMEMLIMIT:-2GiB}"
|
GOMEMLIMIT="${GOMEMLIMIT:-2GiB}"
|
||||||
KEEP_FALSE=true
|
KEEP_FALSE=true
|
||||||
@@ -65,6 +62,9 @@ ADMIN_PASS="${ADMIN_PASS:-}"
|
|||||||
|
|
||||||
# Working directory (where binary and configs live)
|
# Working directory (where binary and configs live)
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||||
|
export PATH="$PROJECT_ROOT/venv/bin:/usr/bin:/bin:/usr/local/bin:${PATH:-}"
|
||||||
|
HLS_PROXY_CONFIG="${HLS_PROXY_CONFIG:-$PROJECT_ROOT/cache/orig_urls.json}"
|
||||||
cd "$SCRIPT_DIR"
|
cd "$SCRIPT_DIR"
|
||||||
|
|
||||||
# Pre-flight checks
|
# Pre-flight checks
|
||||||
@@ -83,7 +83,7 @@ fi
|
|||||||
# Verify the binary has the KID patch (not the original REDA bug)
|
# Verify the binary has the KID patch (not the original REDA bug)
|
||||||
KID_PATCH=$(python3 -c "
|
KID_PATCH=$(python3 -c "
|
||||||
with open('$BINARY','rb') as f:
|
with open('$BINARY','rb') as f:
|
||||||
f.seek(0x15625cd)
|
f.seek($KID_PATCH_OFFSET)
|
||||||
b = f.read(4)
|
b = f.read(4)
|
||||||
print('OK' if b == b'%02x' else 'MISSING')
|
print('OK' if b == b'%02x' else 'MISSING')
|
||||||
" 2>/dev/null || echo "CHECK_FAILED")
|
" 2>/dev/null || echo "CHECK_FAILED")
|
||||||
@@ -93,31 +93,8 @@ if [ "$KID_PATCH" != "OK" ]; then
|
|||||||
echo ""
|
echo ""
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Verify the string patch (nulled!! -> Ap0dexMe0)
|
|
||||||
STRING_PATCH=$(python3 -c "
|
|
||||||
with open('$BINARY','rb') as f:
|
|
||||||
f.seek(0x1a6c350)
|
|
||||||
b = f.read(9)
|
|
||||||
print('OK' if b == b'Ap0dexMe0' else 'MISSING')
|
|
||||||
" 2>/dev/null || echo "CHECK_FAILED")
|
|
||||||
|
|
||||||
if [ "$STRING_PATCH" != "OK" ]; then
|
|
||||||
echo "NOTE: String patch (nulled!! -> Ap0dexMe0) not detected."
|
|
||||||
echo ""
|
|
||||||
fi
|
|
||||||
|
|
||||||
chmod +x "$BINARY"
|
chmod +x "$BINARY"
|
||||||
|
|
||||||
if [ ! -f "o11.cfg" ]; then
|
|
||||||
echo "ERROR: Global config 'o11.cfg' not found"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ ! -f "providers/sample.cfg" ]; then
|
|
||||||
echo "ERROR: Provider config 'providers/sample.cfg' not found"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ ! -f "keys.txt" ]; then
|
if [ ! -f "keys.txt" ]; then
|
||||||
echo "WARNING: keys.txt not found encrypted streams will fail"
|
echo "WARNING: keys.txt not found encrypted streams will fail"
|
||||||
echo ""
|
echo ""
|
||||||
@@ -142,29 +119,15 @@ fi
|
|||||||
# Prepare directories
|
# Prepare directories
|
||||||
|
|
||||||
echo "Preparing directories..."
|
echo "Preparing directories..."
|
||||||
mkdir -p hls/live keys epg dl manifests offair overlay logos fonts rec scripts logs providers
|
mkdir -p hls/live keys epg dl manifests offair overlay logos fonts rec scripts logs providers cache
|
||||||
|
|
||||||
if [ -f "keys.txt" ] && [ ! -f "keys/keys.txt" ]; then
|
if [ -f "keys.txt" ] && [ ! -f "keys/keys.txt" ]; then
|
||||||
cp keys.txt keys/ 2>/dev/null || true
|
cp keys.txt keys/ 2>/dev/null || true
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Apply config overrides
|
|
||||||
|
|
||||||
if [ "$MAX_STREAMS" != "0" ]; then
|
|
||||||
echo "Setting MaxConcurrentStreams=$MAX_STREAMS in provider config..."
|
|
||||||
python3 -c "
|
|
||||||
import json
|
|
||||||
with open('providers/sample.cfg') as f:
|
|
||||||
cfg = json.load(f)
|
|
||||||
cfg['MaxConcurrentStreams'] = $MAX_STREAMS
|
|
||||||
with open('providers/sample.cfg', 'w') as f:
|
|
||||||
json.dump(cfg, f, indent=4, ensure_ascii=False)
|
|
||||||
" 2>/dev/null || echo " (failed to patch config continuing with existing value)"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Build command line
|
# Build command line
|
||||||
|
|
||||||
ARGS="-c o11.cfg -p $PORT -b $BIND -headless -stdout -v $VERBOSE"
|
ARGS="-c o11.cfg -p $PORT -b $BIND -stdout -v $VERBOSE"
|
||||||
|
|
||||||
if [ "$KEEP_FALSE" = "true" ]; then
|
if [ "$KEEP_FALSE" = "true" ]; then
|
||||||
ARGS="$ARGS -keep=false"
|
ARGS="$ARGS -keep=false"
|
||||||
@@ -234,7 +197,6 @@ echo " Max streams: ${MAX_STREAMS:-unlimited}"
|
|||||||
echo " HTTPS: $HTTPS"
|
echo " HTTPS: $HTTPS"
|
||||||
echo " HLS Proxy: $HLS_PROXY${HLS_PROXY:+ (port $HLS_PROXY_PORT)}"
|
echo " HLS Proxy: $HLS_PROXY${HLS_PROXY:+ (port $HLS_PROXY_PORT)}"
|
||||||
echo " Security mon: $MONITOR${MONITOR:+ (port $MONITOR_PORT)}"
|
echo " Security mon: $MONITOR${MONITOR:+ (port $MONITOR_PORT)}"
|
||||||
echo " Config: o11.cfg + providers/sample.cfg"
|
|
||||||
echo "=========================================="
|
echo "=========================================="
|
||||||
echo ""
|
echo ""
|
||||||
echo "Service endpoints:"
|
echo "Service endpoints:"
|
||||||
@@ -251,7 +213,7 @@ if [ "$MONITOR" = "true" ]; then
|
|||||||
echo " Audit log: logs/audit.log"
|
echo " Audit log: logs/audit.log"
|
||||||
echo " Alert log: logs/audit_alerts.log"
|
echo " Alert log: logs/audit_alerts.log"
|
||||||
fi
|
fi
|
||||||
echo " (use 127.0.0.1 instead of 0.0.0.0 in your browser)"
|
echo " (use 127.0.0.1 instead of 127.0.0.1 in your browser)"
|
||||||
echo ""
|
echo ""
|
||||||
echo "Login: admin / <see temp password in log below>"
|
echo "Login: admin / <see temp password in log below>"
|
||||||
echo ""
|
echo ""
|
||||||
@@ -284,12 +246,14 @@ if [ "$MONITOR" = "true" ] || [ "$HLS_PROXY" = "true" ]; then
|
|||||||
|
|
||||||
# Wait for o11pro to be ready
|
# Wait for o11pro to be ready
|
||||||
echo "Waiting for o11pro to be ready on port $PORT..."
|
echo "Waiting for o11pro to be ready on port $PORT..."
|
||||||
for i in $(seq 1 30); do
|
_O11_TRIES=0
|
||||||
|
while [ $_O11_TRIES -lt 30 ]; do
|
||||||
|
_O11_TRIES=$((_O11_TRIES + 1))
|
||||||
if python3 -c "import socket; s=socket.socket(); s.settimeout(1); s.connect(('127.0.0.1',$PORT)); s.close()" 2>/dev/null; then
|
if python3 -c "import socket; s=socket.socket(); s.settimeout(1); s.connect(('127.0.0.1',$PORT)); s.close()" 2>/dev/null; then
|
||||||
echo "o11pro is ready"
|
echo "o11pro is ready"
|
||||||
break
|
break
|
||||||
fi
|
fi
|
||||||
if [ $i -eq 30 ]; then
|
if [ $_O11_TRIES -eq 30 ]; then
|
||||||
echo "ERROR: o11pro did not start in time"
|
echo "ERROR: o11pro did not start in time"
|
||||||
kill "$O11_PID" 2>/dev/null || true
|
kill "$O11_PID" 2>/dev/null || true
|
||||||
exit 1
|
exit 1
|
||||||
@@ -343,7 +307,7 @@ if [ "$MONITOR" = "true" ] || [ "$HLS_PROXY" = "true" ]; then
|
|||||||
echo "Security monitor enabled"
|
echo "Security monitor enabled"
|
||||||
echo " Proxy port: $MONITOR_PORT (-> o11 API :$PORT)"
|
echo " Proxy port: $MONITOR_PORT (-> o11 API :$PORT)"
|
||||||
echo " Process scan: PID $O11_PID"
|
echo " Process scan: PID $O11_PID"
|
||||||
echo " File watch: keys.txt, providers/, o11.cfg, logs/, hls/"
|
echo " File watch: keys.txt, providers/, logs/, hls/"
|
||||||
echo " Audit log: logs/audit.log"
|
echo " Audit log: logs/audit.log"
|
||||||
echo " Alert log: logs/audit_alerts.log"
|
echo " Alert log: logs/audit_alerts.log"
|
||||||
echo ""
|
echo ""
|
||||||
|
|||||||
+95
-22
@@ -10,7 +10,7 @@ import base64
|
|||||||
import time
|
import time
|
||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
import argparse # FIX: was missing
|
import argparse
|
||||||
import threading
|
import threading
|
||||||
import traceback
|
import traceback
|
||||||
import gc
|
import gc
|
||||||
@@ -20,7 +20,7 @@ from http.server import HTTPServer, BaseHTTPRequestHandler
|
|||||||
from urllib.parse import urlparse, parse_qs
|
from urllib.parse import urlparse, parse_qs
|
||||||
from concurrent.futures import ThreadPoolExecutor
|
from concurrent.futures import ThreadPoolExecutor
|
||||||
|
|
||||||
CONFIG_FILE = '/tmp/o11pro_orig_urls.json'
|
CONFIG_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', '..', 'cache', 'orig_urls.json')
|
||||||
PROXY_PORT = 9999
|
PROXY_PORT = 9999
|
||||||
PROXY_BIND = '127.0.0.1'
|
PROXY_BIND = '127.0.0.1'
|
||||||
UA = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36'
|
UA = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36'
|
||||||
@@ -32,6 +32,9 @@ MAX_THREAD_POOL = 32
|
|||||||
GC_INTERVAL_SEC = 60
|
GC_INTERVAL_SEC = 60
|
||||||
SEGMENT_STREAM_BUF = 65536
|
SEGMENT_STREAM_BUF = 65536
|
||||||
|
|
||||||
|
# Global default auth (user, pass)
|
||||||
|
_global_auth = None
|
||||||
|
|
||||||
# Cached channel URLs (loaded once)
|
# Cached channel URLs (loaded once)
|
||||||
_channel_urls = {}
|
_channel_urls = {}
|
||||||
_channel_urls_lock = threading.Lock()
|
_channel_urls_lock = threading.Lock()
|
||||||
@@ -41,10 +44,25 @@ def _load_channel_urls():
|
|||||||
with _channel_urls_lock:
|
with _channel_urls_lock:
|
||||||
if not _channel_urls:
|
if not _channel_urls:
|
||||||
with open(CONFIG_FILE) as f:
|
with open(CONFIG_FILE) as f:
|
||||||
_channel_urls = json.load(f)
|
raw = json.load(f)
|
||||||
|
# Support both {"name": "url"} and {"name": {"url":"...","auth":[...]}}
|
||||||
|
for k, v in raw.items():
|
||||||
|
_channel_urls[k] = v
|
||||||
log(f"Loaded {len(_channel_urls)} channel URLs")
|
log(f"Loaded {len(_channel_urls)} channel URLs")
|
||||||
return _channel_urls
|
return _channel_urls
|
||||||
|
|
||||||
|
def _get_channel_entry(name):
|
||||||
|
"""Return (url, auth) tuple for a channel. auth may be None."""
|
||||||
|
urls = _load_channel_urls()
|
||||||
|
entry = urls.get(name)
|
||||||
|
if entry is None:
|
||||||
|
return None, None
|
||||||
|
if isinstance(entry, dict):
|
||||||
|
url = entry.get('url', '')
|
||||||
|
auth = entry.get('auth') # list [user, pass] or None
|
||||||
|
return url, auth
|
||||||
|
return entry, None # plain string URL
|
||||||
|
|
||||||
# Bounded CookieJar
|
# Bounded CookieJar
|
||||||
class BoundedCookieJar(http.cookiejar.CookieJar):
|
class BoundedCookieJar(http.cookiejar.CookieJar):
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
@@ -101,35 +119,64 @@ def _get_opener(cookie_jar=None):
|
|||||||
_opener_cache.popitem(last=False)
|
_opener_cache.popitem(last=False)
|
||||||
return opener
|
return opener
|
||||||
|
|
||||||
# URL fetching
|
# URL fetching — returns (body, status, content_type, final_url)
|
||||||
def fetch_url(url, timeout=15, cookie_jar=None, referer=None):
|
def fetch_url(url, timeout=15, cookie_jar=None, referer=None, auth=None):
|
||||||
|
"""Fetch a URL, following redirects. Returns (body, status, content_type, final_url).
|
||||||
|
|
||||||
|
final_url is the URL after any HTTP redirects, which must be used as the
|
||||||
|
base for resolving relative URLs in HLS manifests.
|
||||||
|
"""
|
||||||
opener = _get_opener(cookie_jar)
|
opener = _get_opener(cookie_jar)
|
||||||
headers = {'User-Agent': UA}
|
headers = {
|
||||||
|
'User-Agent': UA,
|
||||||
|
'Accept-Encoding': 'identity', # prevent transparent gzip to avoid double-decode issues
|
||||||
|
}
|
||||||
if referer:
|
if referer:
|
||||||
headers['Referer'] = referer
|
headers['Referer'] = referer
|
||||||
|
# Auth: per-request > global default
|
||||||
|
effective_auth = auth or _global_auth
|
||||||
|
if effective_auth:
|
||||||
|
if isinstance(effective_auth, (list, tuple)) and len(effective_auth) == 2:
|
||||||
|
cred = base64.b64encode(f"{effective_auth[0]}:{effective_auth[1]}".encode()).decode()
|
||||||
|
headers['Authorization'] = f"Basic {cred}"
|
||||||
|
elif isinstance(effective_auth, str) and ':' in effective_auth:
|
||||||
|
cred = base64.b64encode(effective_auth.encode()).decode()
|
||||||
|
headers['Authorization'] = f"Basic {cred}"
|
||||||
req = urllib.request.Request(url, headers=headers)
|
req = urllib.request.Request(url, headers=headers)
|
||||||
try:
|
try:
|
||||||
with opener.open(req, timeout=timeout) as resp:
|
with opener.open(req, timeout=timeout) as resp:
|
||||||
return resp.read(), resp.status, resp.headers.get('Content-Type', 'application/octet-stream')
|
# Capture the final URL after any redirects
|
||||||
|
final_url = getattr(resp, 'url', None) or getattr(resp, 'geturl', lambda: url)() or url
|
||||||
|
return resp.read(), resp.status, resp.headers.get('Content-Type', 'application/octet-stream'), final_url
|
||||||
except urllib.error.HTTPError as e:
|
except urllib.error.HTTPError as e:
|
||||||
body = e.read() if e.fp else b''
|
body = e.read() if e.fp else b''
|
||||||
ct = e.headers.get('Content-Type', 'application/octet-stream') if e.headers else 'text/html'
|
ct = e.headers.get('Content-Type', 'application/octet-stream') if e.headers else 'text/html'
|
||||||
return body, e.code, ct
|
final_url = getattr(e, 'url', None) or url
|
||||||
|
return body, e.code, ct, final_url
|
||||||
except (urllib.error.URLError, OSError) as e:
|
except (urllib.error.URLError, OSError) as e:
|
||||||
return b'', 0, 'text/plain'
|
return b'', 0, 'text/plain', url
|
||||||
|
|
||||||
# Manifest URL rewriting
|
# Manifest URL rewriting
|
||||||
_compiled_uri_re = re.compile(r'URI="([^"]+)"')
|
_compiled_uri_re = re.compile(r'URI="([^"]+)"')
|
||||||
|
|
||||||
def rewrite_manifest(body, base_url):
|
def rewrite_manifest(body, base_url):
|
||||||
|
"""Rewrite all relative URLs in an HLS manifest to absolute proxy URLs."""
|
||||||
lines = body.split('\n')
|
lines = body.split('\n')
|
||||||
rewritten = []
|
rewritten = []
|
||||||
for line in lines:
|
for line in lines:
|
||||||
stripped = line.strip()
|
stripped = line.strip()
|
||||||
if 'URI="' in stripped and stripped.startswith('#EXT-X-'):
|
if not stripped:
|
||||||
|
rewritten.append(line)
|
||||||
|
continue
|
||||||
|
# Handle #EXT-X-* tags that contain URI="..."
|
||||||
|
if stripped.startswith('#EXT-X-') and 'URI="' in stripped:
|
||||||
rewritten.append(_compiled_uri_re.sub(
|
rewritten.append(_compiled_uri_re.sub(
|
||||||
lambda m: f'URI="{_abs(m.group(1), base_url)}"', stripped))
|
lambda m: f'URI="{_abs(m.group(1), base_url)}"', stripped))
|
||||||
elif stripped and not stripped.startswith('#'):
|
elif stripped.startswith('#'):
|
||||||
|
# Other comment/tag lines — keep as-is
|
||||||
|
rewritten.append(line)
|
||||||
|
elif not stripped.startswith('#'):
|
||||||
|
# Non-comment, non-empty line — treat as a resource URL
|
||||||
rewritten.append(_abs(stripped, base_url))
|
rewritten.append(_abs(stripped, base_url))
|
||||||
else:
|
else:
|
||||||
rewritten.append(line)
|
rewritten.append(line)
|
||||||
@@ -194,6 +241,7 @@ class RobustHandler(BaseHTTPRequestHandler):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
with _stats_lock:
|
with _stats_lock:
|
||||||
_stats['errors'] += 1
|
_stats['errors'] += 1
|
||||||
|
log(f"Handler error: {traceback.format_exc()}")
|
||||||
try:
|
try:
|
||||||
self._err(500, str(e))
|
self._err(500, str(e))
|
||||||
except:
|
except:
|
||||||
@@ -203,20 +251,26 @@ class RobustHandler(BaseHTTPRequestHandler):
|
|||||||
parts = path.split('/')
|
parts = path.split('/')
|
||||||
if len(parts) >= 4 and parts[3] == 'master.m3u8':
|
if len(parts) >= 4 and parts[3] == 'master.m3u8':
|
||||||
name = urllib.parse.unquote(parts[2])
|
name = urllib.parse.unquote(parts[2])
|
||||||
urls = _load_channel_urls()
|
url, auth = _get_channel_entry(name)
|
||||||
if name in urls:
|
if url:
|
||||||
url = urls[name]
|
|
||||||
q = query
|
q = query
|
||||||
if q:
|
if q:
|
||||||
url += ('&' if '?' in url else '?') + q
|
url += ('&' if '?' in url else '?') + q
|
||||||
jar = _get_jar(urlparse(url).netloc)
|
jar = _get_jar(urlparse(url).netloc)
|
||||||
body, st, ct = fetch_url(url, cookie_jar=jar)
|
body, st, ct, final_url = fetch_url(url, cookie_jar=jar, auth=auth)
|
||||||
if st == 200:
|
if st == 200:
|
||||||
with _stats_lock:
|
with _stats_lock:
|
||||||
_stats['manifests'] += 1
|
_stats['manifests'] += 1
|
||||||
rw = rewrite_manifest(body.decode('utf-8', 'replace'), url)
|
# Use final_url (after redirects) as the base for resolving relative URLs
|
||||||
|
text = body.decode('utf-8', 'replace')
|
||||||
|
# Strip BOM if present
|
||||||
|
if text and text[0] == '\ufeff':
|
||||||
|
text = text[1:]
|
||||||
|
rw = rewrite_manifest(text, final_url)
|
||||||
|
log(f"[channel/{name}] base={url} final={final_url}")
|
||||||
self._ok('application/vnd.apple.mpegurl', rw.encode())
|
self._ok('application/vnd.apple.mpegurl', rw.encode())
|
||||||
else:
|
else:
|
||||||
|
log(f"[channel/{name}] upstream returned {st} for {url}")
|
||||||
self._err(502, f"Upstream {st}")
|
self._err(502, f"Upstream {st}")
|
||||||
else:
|
else:
|
||||||
self._err(404, "Channel not found")
|
self._err(404, "Channel not found")
|
||||||
@@ -227,13 +281,18 @@ class RobustHandler(BaseHTTPRequestHandler):
|
|||||||
target = _from_proxy(path[3:])
|
target = _from_proxy(path[3:])
|
||||||
referer = _get_referer(query)
|
referer = _get_referer(query)
|
||||||
jar = _get_jar(urlparse(target).netloc)
|
jar = _get_jar(urlparse(target).netloc)
|
||||||
body, st, ct = fetch_url(target, cookie_jar=jar, referer=referer)
|
body, st, ct, final_url = fetch_url(target, cookie_jar=jar, referer=referer)
|
||||||
if st == 200:
|
if st == 200:
|
||||||
with _stats_lock:
|
with _stats_lock:
|
||||||
_stats['manifests'] += 1
|
_stats['manifests'] += 1
|
||||||
rw = rewrite_manifest(body.decode('utf-8', 'replace'), target)
|
# Use final_url (after redirects) as the base for resolving relative URLs
|
||||||
|
text = body.decode('utf-8', 'replace')
|
||||||
|
if text and text[0] == '\ufeff':
|
||||||
|
text = text[1:]
|
||||||
|
rw = rewrite_manifest(text, final_url)
|
||||||
self._ok('application/vnd.apple.mpegurl', rw.encode())
|
self._ok('application/vnd.apple.mpegurl', rw.encode())
|
||||||
else:
|
else:
|
||||||
|
log(f"[manifest] upstream returned {st} for {target}")
|
||||||
self._err(502, f"Upstream {st}")
|
self._err(502, f"Upstream {st}")
|
||||||
|
|
||||||
def _handle_segment(self, path, query):
|
def _handle_segment(self, path, query):
|
||||||
@@ -247,9 +306,15 @@ class RobustHandler(BaseHTTPRequestHandler):
|
|||||||
return
|
return
|
||||||
referer = _get_referer(query)
|
referer = _get_referer(query)
|
||||||
jar = _get_jar(urlparse(target).netloc)
|
jar = _get_jar(urlparse(target).netloc)
|
||||||
headers = {'User-Agent': UA}
|
headers = {
|
||||||
|
'User-Agent': UA,
|
||||||
|
'Accept-Encoding': 'identity',
|
||||||
|
}
|
||||||
if referer:
|
if referer:
|
||||||
headers['Referer'] = referer
|
headers['Referer'] = referer
|
||||||
|
if _global_auth:
|
||||||
|
cred = base64.b64encode(_global_auth.encode()).decode()
|
||||||
|
headers['Authorization'] = f"Basic {cred}"
|
||||||
opener = _get_opener(jar)
|
opener = _get_opener(jar)
|
||||||
req = urllib.request.Request(target, headers=headers)
|
req = urllib.request.Request(target, headers=headers)
|
||||||
try:
|
try:
|
||||||
@@ -374,33 +439,41 @@ def log(msg):
|
|||||||
|
|
||||||
# Main
|
# Main
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
# FIX: was hardcoded, now accepts CLI arguments
|
parser = argparse.ArgumentParser(description='HLS Proxy for o11pro (redirect-aware + auth)')
|
||||||
parser = argparse.ArgumentParser(description='HLS Proxy for o11pro (memory-leak fixed)')
|
|
||||||
parser.add_argument('--config', default=CONFIG_FILE,
|
parser.add_argument('--config', default=CONFIG_FILE,
|
||||||
help=f'Path to channel URLs JSON (default: {CONFIG_FILE})')
|
help=f'Path to channel URLs JSON (default: {CONFIG_FILE})')
|
||||||
parser.add_argument('--port', type=int, default=PROXY_PORT,
|
parser.add_argument('--port', type=int, default=PROXY_PORT,
|
||||||
help=f'Proxy listen port (default: {PROXY_PORT})')
|
help=f'Proxy listen port (default: {PROXY_PORT})')
|
||||||
parser.add_argument('--bind', default=PROXY_BIND,
|
parser.add_argument('--bind', default=PROXY_BIND,
|
||||||
help=f'Proxy bind address (default: {PROXY_BIND})')
|
help=f'Proxy bind address (default: {PROXY_BIND})')
|
||||||
|
parser.add_argument('--auth', default=None,
|
||||||
|
help='Global Basic Auth credentials as user:pass '
|
||||||
|
'(e.g. 1acd2d7afd8cb34099cb832862e3c08d:b85491a27c2852323ac704a07cf7b779)')
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
CONFIG_FILE = args.config
|
CONFIG_FILE = args.config
|
||||||
PROXY_PORT = args.port
|
PROXY_PORT = args.port
|
||||||
PROXY_BIND = args.bind
|
PROXY_BIND = args.bind
|
||||||
|
|
||||||
|
if args.auth:
|
||||||
|
_global_auth = args.auth
|
||||||
|
log(f"Global auth enabled")
|
||||||
|
|
||||||
_load_channel_urls()
|
_load_channel_urls()
|
||||||
assert _compiled_uri_re is not None
|
assert _compiled_uri_re is not None
|
||||||
|
|
||||||
maint = threading.Thread(target=_maintenance_loop, daemon=True)
|
maint = threading.Thread(target=_maintenance_loop, daemon=True)
|
||||||
maint.start()
|
maint.start()
|
||||||
|
|
||||||
print(f"HLS Proxy on {PROXY_BIND}:{PROXY_PORT} (memory-optimized)", flush=True)
|
print(f"HLS Proxy on {PROXY_BIND}:{PROXY_PORT} (redirect-aware + auth)", flush=True)
|
||||||
print(f" Config: {CONFIG_FILE}", flush=True)
|
print(f" Config: {CONFIG_FILE}", flush=True)
|
||||||
print(f" Channels: {len(_channel_urls)}", flush=True)
|
print(f" Channels: {len(_channel_urls)}", flush=True)
|
||||||
print(f" Thread pool: {MAX_THREAD_POOL} workers", flush=True)
|
print(f" Thread pool: {MAX_THREAD_POOL} workers", flush=True)
|
||||||
print(f" Cookie limit: {MAX_COOKIES_PER_JAR}/jar", flush=True)
|
print(f" Cookie limit: {MAX_COOKIES_PER_JAR}/jar", flush=True)
|
||||||
print(f" Streaming buffer: {SEGMENT_STREAM_BUF} bytes", flush=True)
|
print(f" Streaming buffer: {SEGMENT_STREAM_BUF} bytes", flush=True)
|
||||||
print(f" GC interval: {GC_INTERVAL_SEC}s", flush=True)
|
print(f" GC interval: {GC_INTERVAL_SEC}s", flush=True)
|
||||||
|
if _global_auth:
|
||||||
|
print(f" Global auth: enabled", flush=True)
|
||||||
print(f"", flush=True)
|
print(f"", flush=True)
|
||||||
print(f" Routes:", flush=True)
|
print(f" Routes:", flush=True)
|
||||||
print(f" /channel/{{name}}/master.m3u8 - Channel master manifest", flush=True)
|
print(f" /channel/{{name}}/master.m3u8 - Channel master manifest", flush=True)
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ from http.server import HTTPServer, BaseHTTPRequestHandler
|
|||||||
from socketserver import ThreadingMixIn
|
from socketserver import ThreadingMixIn
|
||||||
|
|
||||||
VERSION = "1.0.0"
|
VERSION = "1.0.0"
|
||||||
DEFAULT_PID_FILE = "/tmp/o11_pid"
|
DEFAULT_PID_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', '..', 'cache', 'o11_pid')
|
||||||
DEFAULT_AUDIT_LOG = "audit.log"
|
DEFAULT_AUDIT_LOG = "audit.log"
|
||||||
DEFAULT_ALERT_LOG = "audit_alerts.log"
|
DEFAULT_ALERT_LOG = "audit_alerts.log"
|
||||||
DEFAULT_PROXY_PORT = 19998
|
DEFAULT_PROXY_PORT = 19998
|
||||||
@@ -777,7 +777,7 @@ def find_o11_pid():
|
|||||||
except (FileNotFoundError, ValueError):
|
except (FileNotFoundError, ValueError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
for pid_file in (DEFAULT_PID_FILE, '/tmp/o11e2e/pid'):
|
for pid_file in (DEFAULT_PID_FILE, os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', '..', 'cache', 'o11_pid')):
|
||||||
try:
|
try:
|
||||||
with open(pid_file) as f:
|
with open(pid_file) as f:
|
||||||
pid = int(f.read().strip())
|
pid = int(f.read().strip())
|
||||||
|
|||||||
+1021
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user