1 Commits
Author SHA1 Message Date
Pierre 28cb433724 add auth
Build and Push Docker Image / build (push) Failing after 13s
2026-04-26 15:26:17 +02:00
2 changed files with 73 additions and 10 deletions
+70 -8
View File
@@ -28,6 +28,7 @@ import json
import signal import signal
import sys import sys
import base64 import base64
import hashlib
from http.server import HTTPServer, BaseHTTPRequestHandler from http.server import HTTPServer, BaseHTTPRequestHandler
from urllib.parse import urlparse, unquote from urllib.parse import urlparse, unquote
import threading import threading
@@ -44,6 +45,23 @@ SCHEME = os.environ.get('SCHEME', 'auto').lower().strip()
META_CORE_PATH = os.environ.get('META_CORE_PATH', '/meta-core') META_CORE_PATH = os.environ.get('META_CORE_PATH', '/meta-core')
BASE_URL = os.environ.get('BASE_URL', '') BASE_URL = os.environ.get('BASE_URL', '')
# Stremio addon path-token protection. When HASH_API_SEED is set, every addon
# path (manifest, catalog, meta, stream, transcode, direct, file, poster) must
# be prefixed with /s/<token>. Dashboard paths bypass this — Caddy + the
# nginx-hash-lock sidecar gate those with OIDC, since browsers can carry
# cookies but Stremio clients cannot.
HASH_API_SEED = os.environ.get('HASH_API_SEED', '').strip()
API_KEY = hashlib.sha256(HASH_API_SEED.encode()).hexdigest()[:16] if HASH_API_SEED else ''
API_PREFIX = f'/s/{API_KEY}' if API_KEY else ''
# Paths the addon serves for browsers, not Stremio clients. These are gated
# upstream by hash-lock/OIDC instead of the path-token.
DASHBOARD_PATHS = frozenset({
'/', '/index.html', '/configure', '/health',
'/api/stats', '/api/library', '/api/services', '/api/languages',
'/transcode/metrics',
})
# Initialize storage # Initialize storage
storage = stremio.init_storage() storage = stremio.init_storage()
@@ -133,10 +151,32 @@ class Handler(BaseHTTPRequestHandler):
self.send_header('Access-Control-Allow-Headers', 'Content-Type, Range') self.send_header('Access-Control-Allow-Headers', 'Content-Type, Range')
self.end_headers() self.end_headers()
def strip_api_prefix(self, path: str) -> str | None:
"""Remove the /s/<token> prefix from addon paths.
Returns the de-prefixed path, or None when the token is missing/wrong
(caller should send 401). Dashboard paths are returned untouched —
they are protected by hash-lock/OIDC upstream, not by the token.
"""
if not API_KEY:
return path
if path in DASHBOARD_PATHS:
return path
if path.startswith(API_PREFIX + '/'):
return path[len(API_PREFIX):]
if path == API_PREFIX:
return '/'
return None
def do_HEAD(self): def do_HEAD(self):
parsed = urlparse(self.path) parsed = urlparse(self.path)
path = unquote(parsed.path) path = unquote(parsed.path)
path = self.strip_api_prefix(path)
if path is None:
self.send_error(401, "Unauthorized")
return
if path.startswith('/stremio/') or path == '/' or path.startswith('/manifest.json'): if path.startswith('/stremio/') or path == '/' or path.startswith('/manifest.json'):
self.send_response(200) self.send_response(200)
self.send_header('Access-Control-Allow-Origin', '*') self.send_header('Access-Control-Allow-Origin', '*')
@@ -181,6 +221,11 @@ class Handler(BaseHTTPRequestHandler):
parsed = urlparse(self.path) parsed = urlparse(self.path)
path = unquote(parsed.path) path = unquote(parsed.path)
path = self.strip_api_prefix(path)
if path is None:
self.send_error(401, "Unauthorized")
return
# Reset metrics # Reset metrics
if path == '/transcode/reset-metrics': if path == '/transcode/reset-metrics':
transcoder.reset_metrics() transcoder.reset_metrics()
@@ -192,8 +237,16 @@ class Handler(BaseHTTPRequestHandler):
parsed = urlparse(self.path) parsed = urlparse(self.path)
raw_path = unquote(parsed.path) raw_path = unquote(parsed.path)
# Strip path-token (when HASH_API_SEED is set) before any further
# parsing; the existing base64-config-in-path scheme then runs on the
# tail, so URLs look like /s/<token>/<configB64>/manifest.json.
stripped = self.strip_api_prefix(raw_path)
if stripped is None:
self.send_error(401, "Unauthorized")
return
# Parse config from path (e.g., /eyJsYW5n.../manifest.json) # Parse config from path (e.g., /eyJsYW5n.../manifest.json)
path, config = parse_config_from_path(raw_path) path, config = parse_config_from_path(stripped)
# Root - serve setup page # Root - serve setup page
if path == '/' or path == '/index.html': if path == '/' or path == '/index.html':
@@ -325,11 +378,14 @@ class Handler(BaseHTTPRequestHandler):
if os.path.exists(html_path): if os.path.exists(html_path):
with open(html_path, 'r', encoding='utf-8') as f: with open(html_path, 'r', encoding='utf-8') as f:
content = f.read() content = f.read()
prefix_script = f'<script>window.API_PREFIX = "{API_PREFIX}";</script>'
content = content.replace('</head>', f'{prefix_script}\n</head>', 1)
encoded = content.encode('utf-8')
self.send_response(200) self.send_response(200)
self.send_header('Content-Type', 'text/html') self.send_header('Content-Type', 'text/html')
self.send_header('Content-Length', len(content.encode('utf-8'))) self.send_header('Content-Length', len(encoded))
self.end_headers() self.end_headers()
self.wfile.write(content.encode('utf-8')) self.wfile.write(encoded)
else: else:
# Fallback minimal page # Fallback minimal page
content = f"""<!DOCTYPE html> content = f"""<!DOCTYPE html>
@@ -338,7 +394,7 @@ class Handler(BaseHTTPRequestHandler):
<body style="font-family: sans-serif; background: #1a1a2e; color: #fff; padding: 2rem;"> <body style="font-family: sans-serif; background: #1a1a2e; color: #fff; padding: 2rem;">
<h1>Meta-Stremio</h1> <h1>Meta-Stremio</h1>
<p>Install URL: <code>{self.get_base_url()}/manifest.json</code></p> <p>Install URL: <code>{self.get_base_url()}/manifest.json</code></p>
<p><a href="stremio://{self.headers.get('Host', 'localhost')}/manifest.json" style="color: #4dabf7;">Install in Stremio</a></p> <p><a href="stremio://{self.headers.get('Host', 'localhost')}{API_PREFIX}/manifest.json" style="color: #4dabf7;">Install in Stremio</a></p>
</body> </body>
</html>""" </html>"""
self.send_response(200) self.send_response(200)
@@ -364,6 +420,7 @@ class Handler(BaseHTTPRequestHandler):
# Inline configure page (fallback) # Inline configure page (fallback)
base_url = self.get_base_url() base_url = self.get_base_url()
host = self.headers.get('Host', 'localhost') host = self.headers.get('Host', 'localhost')
api_prefix = API_PREFIX
languages = stremio.get_supported_languages() languages = stremio.get_supported_languages()
lang_options = '\n'.join( lang_options = '\n'.join(
@@ -508,6 +565,7 @@ class Handler(BaseHTTPRequestHandler):
<script> <script>
const baseUrl = '{base_url}'; const baseUrl = '{base_url}';
const host = '{host}'; const host = '{host}';
const apiPrefix = '{api_prefix}';
function encodeConfig(config) {{ function encodeConfig(config) {{
const json = JSON.stringify(config); const json = JSON.stringify(config);
@@ -523,12 +581,12 @@ class Handler(BaseHTTPRequestHandler):
if (displayLang === 'en') {{ if (displayLang === 'en') {{
// No config needed for default // No config needed for default
url = baseUrl + '/manifest.json'; url = baseUrl + '/manifest.json';
stremioUrl = 'stremio://' + host + '/manifest.json'; stremioUrl = 'stremio://' + host + apiPrefix + '/manifest.json';
}} else {{ }} else {{
const config = {{ displayLanguage: displayLang }}; const config = {{ displayLanguage: displayLang }};
const encoded = encodeConfig(config); const encoded = encodeConfig(config);
url = baseUrl + '/' + encoded + '/manifest.json'; url = baseUrl + '/' + encoded + '/manifest.json';
stremioUrl = 'stremio://' + host + '/' + encoded + '/manifest.json'; stremioUrl = 'stremio://' + host + apiPrefix + '/' + encoded + '/manifest.json';
}} }}
document.getElementById('installUrl').textContent = url; document.getElementById('installUrl').textContent = url;
@@ -827,7 +885,7 @@ class Handler(BaseHTTPRequestHandler):
proto = 'http' if is_localhost else 'https' proto = 'http' if is_localhost else 'https'
clean_host = host.replace(':80', '').replace(':443', '') clean_host = host.replace(':80', '').replace(':443', '')
return f"{proto}://{clean_host}" return f"{proto}://{clean_host}{API_PREFIX}"
class ThreadedServer(HTTPServer): class ThreadedServer(HTTPServer):
@@ -875,8 +933,12 @@ def main():
print(f"Storage: {storage_type} | Videos: {video_count}") print(f"Storage: {storage_type} | Videos: {video_count}")
print(f"Media: {transcoder.MEDIA_DIR} | Cache: {transcoder.CACHE_DIR}") print(f"Media: {transcoder.MEDIA_DIR} | Cache: {transcoder.CACHE_DIR}")
print(f"Segment: {transcoder.SEGMENT_DURATION}s | Prefetch: {transcoder.PREFETCH_SEGMENTS} segments") print(f"Segment: {transcoder.SEGMENT_DURATION}s | Prefetch: {transcoder.PREFETCH_SEGMENTS} segments")
print(f"Manifest URL: http://localhost:{PORT}/manifest.json") print(f"Manifest URL: http://localhost:{PORT}{API_PREFIX}/manifest.json")
print(f"Dashboard: http://localhost:{PORT}/") print(f"Dashboard: http://localhost:{PORT}/")
if API_KEY:
print(f"Path-token protection: enabled (prefix: {API_PREFIX})")
else:
print("Path-token protection: disabled (set HASH_API_SEED to enable)")
print("Adaptive quality: target 60-80% transcode ratio") print("Adaptive quality: target 60-80% transcode ratio")
if service_discovery: if service_discovery:
+3 -2
View File
@@ -477,11 +477,12 @@
<script> <script>
const baseUrl = window.location.origin; const baseUrl = window.location.origin;
const manifestUrl = `${baseUrl}/manifest.json`; const apiPrefix = window.API_PREFIX || '';
const manifestUrl = `${baseUrl}${apiPrefix}/manifest.json`;
// Set URLs // Set URLs
document.getElementById('manifestUrl').textContent = manifestUrl; document.getElementById('manifestUrl').textContent = manifestUrl;
document.getElementById('installLink').href = `stremio://${window.location.host}/manifest.json`; document.getElementById('installLink').href = `stremio://${window.location.host}${apiPrefix}/manifest.json`;
function copyUrl() { function copyUrl() {
navigator.clipboard.writeText(manifestUrl).then(() => { navigator.clipboard.writeText(manifestUrl).then(() => {