2024-10-27 12:50:59 +01:00
import argparse
import json
import time
import logging
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
from keydive.__main__ import configure_logging
# Suppress urllib3 warnings
2025-01-19 14:13:07 +01: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-01-19 14:13:07 +01:00
VERSION = "1.0.1"
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:
Response: A simple "pong" message with a 200 OK status.
"""
2025-01-19 14:13:07 +01: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-01-19 14:13:07 +01:00
Parameters:
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-01-19 14:13:07 +01:00
logger = logging . getLogger ( "Shaka" )
logger . info ( " %s %s " , request . method , request . path )
2024-10-27 12:50:59 +01:00
try :
2025-01-19 14:13:07 +01: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-01-19 14:13:07 +01:00
url = f "https://storage.googleapis.com/shaka-demo-assets/angel-one-widevine/ { file } " ,
2024-10-27 12:50:59 +01:00
headers = {
2025-01-19 14:13:07 +01: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-01-19 14:13:07 +01:00
logger . debug ( "Downloaded assets: %s " , path )
2024-10-27 12:50:59 +01:00
2025-01-19 14:13:07 +01: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-01-19 14:13:07 +01: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-01-19 14:13:07 +01:00
logger = logging . getLogger ( "Google" )
logger . info ( " %s %s " , request . method , request . path )
2024-10-27 12:50:59 +01:00
if KEYBOX :
2025-01-19 14:13:07 +01: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-01-19 14:13:07 +01: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-01-19 14:13:07 +01: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-01-19 14:13:07 +01: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-01-19 14:13:07 +01: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-01-19 14:13:07 +01: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-01-19 14:13:07 +01: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-01-19 14:13:07 +01:00
parser = argparse . ArgumentParser ( description = "Local DRM provisioning video player." )
2024-10-27 12:50:59 +01:00
# Global arguments for the application
2025-01-19 14:13:07 +01:00
group_global = parser . add_argument_group ( "Global" )
group_global . add_argument ( '--host' , required = False , type = str , default = "127.0.0.1" , metavar = "<host>" , help = "Host address for the server to bind to." )
group_global . add_argument ( '--port' , required = False , type = int , default = 9090 , metavar = "<port>" , help = "Port number for the server to listen on." )
group_global . add_argument ( '-v' , '--verbose' , required = False , action = "store_true" , help = "Enable verbose logging for detailed debug output." )
group_global . add_argument ( '-l' , '--log' , required = False , type = Path , metavar = "<dir>" , help = "Directory to store log files." )
group_global . add_argument ( '--version' , required = False , action = "store_true" , help = "Display Server version information." )
2024-10-27 12:50:59 +01:00
# Advanced options
2025-01-19 14:13:07 +01:00
group_advanced = parser . add_argument_group ( "Advanced" )
group_advanced . add_argument ( '-d' , '--delay' , required = False , type = int , metavar = "<delay>" , default = 10 , help = "Delay (in seconds) between successive checks for provisioning responses." )
group_advanced . add_argument ( '-k' , '--keybox' , required = False , 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-01-19 14:13:07 +01:00
# Handle version display
2024-10-27 12:50:59 +01:00
if args . version :
2025-01-19 14:13:07 +01:00
print ( f "Server { VERSION } " )
2024-10-27 12:50:59 +01:00
exit ( 0 )
2025-01-19 14:13:07 +01:00
# Configure logging (file and console)
2024-10-27 12:50:59 +01:00
log_path = configure_logging ( path = args . log , verbose = args . verbose )
2025-01-19 14:13:07 +01: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-01-19 14:13:07 +01: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 :
pass
except Exception as e :
logger . critical ( e , exc_info = args . verbose )
# Final logging and exit
if log_path :
2025-01-19 14:13:07 +01:00
logger . info ( "Log file: %s " % log_path )
logger . info ( "Exiting" )
2024-10-27 12:50:59 +01:00
2025-01-19 14:13:07 +01:00
if __name__ == "__main__" :
2024-10-27 12:50:59 +01:00
main ()