Files

197 lines
7.3 KiB
Python
Raw Permalink Normal View History

2024-10-27 12:50:59 +01:00
import json
import time
import logging
2025-06-09 18:12:49 +02:00
from argparse import ArgumentParser
2025-01-19 14:13:07 +01:00
from pathlib import Path
2024-10-27 12:50:59 +01:00
import requests
from flask import Flask, Response, request, redirect
2025-06-09 18:12:49 +02:00
from rich_argparse import RichHelpFormatter
2024-10-27 12:50:59 +01:00
2025-06-09 18:12:49 +02:00
from keydive.utils import configure_logging
2024-10-27 12:50:59 +01:00
# Suppress urllib3 warnings
2025-06-09 18:12:49 +02:00
logging.getLogger('urllib3.connectionpool').setLevel(logging.ERROR)
2024-10-27 12:50:59 +01:00
# Initialize Flask application
app = Flask(__name__)
# Define paths, constants, and global flags
PARENT = Path(__file__).parent
2025-06-09 18:12:49 +02:00
VERSION = '1.0.2'
2024-10-27 12:50:59 +01:00
KEYBOX = False
DELAY = 10
@app.route('/', methods=['GET'])
def health_check() -> Response:
"""
Health check endpoint to confirm the server is running.
Returns:
2025-06-09 18:12:49 +02:00
Response: A simple 'pong' message with a 200 OK status.
2024-10-27 12:50:59 +01:00
"""
2025-06-09 18:12:49 +02:00
return Response(response='pong', status=200, content_type='text/html; charset=utf-8')
2024-10-27 12:50:59 +01:00
@app.route('/shaka-demo-assets/angel-one-widevine/<path:file>', methods=['GET'])
def shaka_demo_assets(file) -> Response:
"""
Serves cached assets for Widevine demo content. If the requested file is
not available locally, it fetches it from a remote server and caches it.
2025-06-09 18:12:49 +02:00
Args:
2024-10-27 12:50:59 +01:00
file (str): File path requested by the client.
Returns:
Response: File content as a byte stream, or a 404 error if not found.
"""
2025-06-09 18:12:49 +02:00
logger = logging.getLogger('Shaka')
logger.info('%s %s', request.method, request.path)
2024-10-27 12:50:59 +01:00
try:
2025-06-09 18:12:49 +02:00
path = PARENT / '.assets' / file
2024-10-27 12:50:59 +01:00
path.parent.mkdir(parents=True, exist_ok=True)
if path.is_file():
# Serve cached file content if available
content = path.read_bytes()
else:
# Fetch the file from remote storage if not cached locally
r = requests.get(
2025-06-09 18:12:49 +02:00
url=f'https://storage.googleapis.com/shaka-demo-assets/angel-one-widevine/{file}',
2024-10-27 12:50:59 +01:00
headers={
2025-06-09 18:12:49 +02:00
'Accept': '*/*',
'User-Agent': 'KalturaDeviceInfo/1.4.1 (Linux;Android 10) ExoPlayerLib/2.9.3'
2024-10-27 12:50:59 +01:00
}
)
r.raise_for_status()
path.write_bytes(r.content) # Cache the downloaded content
content = r.content
2025-06-09 18:12:49 +02:00
logger.debug('Downloaded assets: %s', path)
2024-10-27 12:50:59 +01:00
2025-06-09 18:12:49 +02:00
return Response(response=content, status=200, content_type='application/octet-stream')
2024-10-27 12:50:59 +01:00
except Exception as e:
2025-06-09 18:12:49 +02:00
return Response(response=str(e), status=404, content_type='text/html; charset=utf-8')
2024-10-27 12:50:59 +01:00
@app.route('/certificateprovisioning/v1/devicecertificates/create', methods=['POST'])
def certificate_provisioning() -> Response:
"""
Handles device certificate provisioning requests by intercepting the request,
saving it as a curl command, and then responding based on cached data
or redirecting if no cached response is available.
Returns:
Response: JSON response if provisioning is complete, else a redirection.
"""
global KEYBOX, DELAY
2025-06-09 18:12:49 +02:00
logger = logging.getLogger('Google')
logger.info('%s %s', request.method, request.path)
2024-10-27 12:50:59 +01:00
if KEYBOX:
2025-06-09 18:12:49 +02:00
logger.warning('Provisioning request aborted to prevent keybox spam')
return Response(response='Internal Server Error', status=500, content_type='text/html; charset=utf-8')
2024-10-27 12:50:59 +01:00
# Generate a curl command from the incoming request for debugging or testing
2025-06-09 18:12:49 +02:00
user_agent = request.headers.get('User-Agent', 'Unknown')
url = request.url.replace('http://', 'https://')
2024-10-27 12:50:59 +01:00
prompt = [
2025-01-19 14:13:07 +01:00
'curl',
'--request', 'POST',
'--compressed',
'--header', '"Accept-Encoding: gzip"',
'--header', '"Connection: Keep-Alive"',
'--header', '"Content-Type: application/x-www-form-urlencoded"',
'--header', '"Host: www.googleapis.com"',
'--header', f'"User-Agent: {user_agent}"'
2024-10-27 12:50:59 +01:00
]
# Save the curl command for potential replay or inspection
2025-06-09 18:12:49 +02:00
curl = PARENT / 'curl.txt'
curl.write_text(' \\\n '.join(prompt))
logger.debug('Saved curl command to: %s', curl)
2024-10-27 12:50:59 +01:00
# Wait for provisioning response data with retries
2025-06-09 18:12:49 +02:00
logger.warning('Waiting for provisioning response...')
provision = PARENT / 'provisioning.json'
2024-10-27 12:50:59 +01:00
provision.unlink(missing_ok=True)
2025-06-09 18:12:49 +02:00
provision.write_bytes(b'') # Create empty file for manual input if needed
2024-10-27 12:50:59 +01:00
# Poll for the presence of a response up to DELAY times with 1-second intervals
for _ in range(DELAY):
try:
content = json.loads(provision.read_bytes())
if content:
# Cleanup after successful response
curl.unlink(missing_ok=True)
provision.unlink(missing_ok=True)
2025-06-09 18:12:49 +02:00
return Response(response=content, status=200, content_type='application/json')
2024-10-27 12:50:59 +01:00
except Exception as e:
pass # Continue waiting if file is empty or not yet ready
time.sleep(1)
# Redirect to the secure URL if response is not available
2025-06-09 18:12:49 +02:00
logger.warning('Redirecting to avoid timeout')
2024-10-27 12:50:59 +01:00
return redirect(url, code=302)
def main() -> None:
"""
Main entry point for the application. Parses command-line arguments
to set global parameters and configures logging, then starts the Flask server.
"""
global VERSION, DELAY, KEYBOX
2025-06-09 18:12:49 +02:00
parser = ArgumentParser(
description='Local DRM provisioning video player.',
formatter_class=RichHelpFormatter)
2024-10-27 12:50:59 +01:00
2025-06-09 18:12:49 +02:00
global_group = parser.add_argument_group('Global Options')
global_group.add_argument('--host', type=str, default='127.0.0.1', metavar='<host>', help='Host address for the server to bind to.')
global_group.add_argument('--port', type=int, default=9090, metavar='<port>', help='Port number for the server to listen on.')
global_group.add_argument('-v', '--verbose', action='store_true', help='Enable detailed logging for debugging.')
global_group.add_argument('-l', '--log', type=Path, metavar='<dir>', help='Directory to save log files.')
global_group.add_argument('--version', action='store_true', help='Show tool version and exit.')
2024-10-27 12:50:59 +01:00
2025-06-09 18:12:49 +02:00
advanced_group = parser.add_argument_group('Advanced Options')
advanced_group.add_argument('-d', '--delay', type=int, metavar='<delay>', default=10, help='Delay (in seconds) between successive checks for provisioning responses.')
advanced_group.add_argument('-k', '--keybox', action='store_true', help='Enable keybox mode, which aborts provisioning requests to prevent spam.')
2024-10-27 12:50:59 +01:00
args = parser.parse_args()
2025-06-09 18:12:49 +02:00
# Handle version flag early and exit
2024-10-27 12:50:59 +01:00
if args.version:
2025-06-09 18:12:49 +02:00
print(f'Server {VERSION}')
return
2024-10-27 12:50:59 +01:00
2025-06-09 18:12:49 +02:00
# Set up logging (to file if specified, otherwise stdout)
2024-10-27 12:50:59 +01:00
log_path = configure_logging(path=args.log, verbose=args.verbose)
2025-06-09 18:12:49 +02:00
logger = logging.getLogger('Server')
logger.info('Version: %s', VERSION)
2024-10-27 12:50:59 +01:00
try:
# Set global variables based on parsed arguments
DELAY = args.delay
KEYBOX = args.keybox
# Start Flask app with specified host, port, and debug mode
2025-06-09 18:12:49 +02:00
logging.getLogger('werkzeug').setLevel(logging.INFO if args.verbose else logging.ERROR)
2024-10-27 12:50:59 +01:00
app.run(host=args.host, port=args.port, debug=False)
except KeyboardInterrupt:
2025-06-09 18:12:49 +02:00
# Graceful exit on user interrupt (Ctrl+C)
2024-10-27 12:50:59 +01:00
pass
except Exception as e:
logger.critical(e, exc_info=args.verbose)
2025-06-09 18:12:49 +02:00
# Log the path to the log file if it was created
2024-10-27 12:50:59 +01:00
if log_path:
2025-06-09 18:12:49 +02:00
logger.info('Log file: %s' % log_path)
2024-10-27 12:50:59 +01:00
2025-06-09 18:12:49 +02:00
logger.info('Exiting')
2024-10-27 12:50:59 +01:00
2025-06-09 18:12:49 +02:00
if __name__ == '__main__':
2024-10-27 12:50:59 +01:00
main()