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 ()