Update project structure and formatting, add build code, remove /dist

This commit is contained in:
voldemort 2025-07-22 20:01:22 +07:00
parent 84999654ed
commit bafd3db4f4
35 changed files with 1489 additions and 1134 deletions

1
.gitignore vendored
View File

@ -9,3 +9,4 @@ main.spec
pyinstallericon.ico pyinstallericon.ico
icon.ico icon.ico
venv venv
frontend-dist

28
build.py Normal file
View File

@ -0,0 +1,28 @@
"""Main file to run the application."""
import os
import subprocess
import shutil
def build_frontend():
"""Build the frontend."""
frontend_dir = "cdrm-frontend"
# Check and run npm commands if needed
if not os.path.exists(f"{frontend_dir}/node_modules"):
subprocess.run(["npm", "install"], cwd=frontend_dir, check=False)
if not os.path.exists(f"{frontend_dir}/dist"):
subprocess.run(["npm", "run", "build"], cwd=frontend_dir, check=False)
# Move dist to frontend-dist
if os.path.exists("frontend-dist"):
shutil.rmtree("frontend-dist")
shutil.copytree(f"{frontend_dir}/dist", "frontend-dist")
print("✅ Build complete. Run the application with 'python main.py'")
if __name__ == "__main__":
build_frontend()

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 862 B

View File

@ -1,21 +0,0 @@
<!doctype html>
<html lang="en" class="w-full h-full">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favico.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content="{{ data.description }}" />
<meta name="keywords" content="{{ data.keywords }}" />
<meta property="og:title" content="{{ data.opengraph_title }}" />
<meta property="og:description" content="{{ data.opengraph_description }}" />
<meta property="og:image" content="{{ data.opengraph_image }}" />
<meta property="og:url" content="{{ data.opengraph_url }}" />
<meta property="og:locale" content="en_US" />
<title>{{ data.tab_title }}</title>
<script type="module" crossorigin src="/assets/index-1tbqhIbb.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-C8jLRdm9.css">
</head>
<body class="w-full h-full">
<div id="root" class="w-full h-full"></div>
</body>
</html>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 189 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 207 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 302 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 99 KiB

View File

@ -1,5 +1,5 @@
data = { data = {
'discord': 'https://discord.cdrm-project.com/', "discord": "https://discord.cdrm-project.com/",
'telegram': 'https://telegram.cdrm-project.com/', "telegram": "https://telegram.cdrm-project.com/",
'gitea': 'https://cdm-project.com/tpd94/cdrm-project' "gitea": "https://cdm-project.com/tpd94/cdrm-project",
} }

View File

@ -1,47 +1,47 @@
tags = { tags = {
'index': { "index": {
'description': 'Decrypt Widevine and PlayReady protected content', "description": "Decrypt Widevine and PlayReady protected content",
'keywords': 'CDRM, Widevine, PlayReady, DRM, Decrypt, CDM, CDM-Project, CDRM-Project, TPD94, Decryption', "keywords": "CDRM, Widevine, PlayReady, DRM, Decrypt, CDM, CDM-Project, CDRM-Project, TPD94, Decryption",
'opengraph_title': 'CDRM-Project', "opengraph_title": "CDRM-Project",
'opengraph_description': 'Self Hosted web application written in Python/JavaScript utilizing the Flask/Tailwind Framework and ReactJS library to decrypt Widevine & Playready content', "opengraph_description": "Self Hosted web application written in Python/JavaScript utilizing the Flask/Tailwind Framework and ReactJS library to decrypt Widevine & Playready content",
'opengraph_image': 'https://cdrm-project.com/og-home.jpg', "opengraph_image": "https://cdrm-project.com/og-home.jpg",
'opengraph_url': 'https://cdm-project.com/tpd94/cdrm-project', "opengraph_url": "https://cdm-project.com/tpd94/cdrm-project",
'tab_title': 'CDRM-Project', "tab_title": "CDRM-Project",
}, },
'cache': { "cache": {
'description': 'Search the cache by KID or PSSH for decryption keys', "description": "Search the cache by KID or PSSH for decryption keys",
'keywords': 'Cache, Vault, Widevine, PlayReady, DRM, Decryption, CDM, CDRM-Project, CDRM-Project, TPD94, Decryption', "keywords": "Cache, Vault, Widevine, PlayReady, DRM, Decryption, CDM, CDRM-Project, CDRM-Project, TPD94, Decryption",
'opengraph_title': 'Search the Cache', "opengraph_title": "Search the Cache",
'opengraph_description': 'Search the cache by KID or PSSH for decryption keys', "opengraph_description": "Search the cache by KID or PSSH for decryption keys",
'opengraph_image': 'https://cdrm-project.com/og-cache.jpg', "opengraph_image": "https://cdrm-project.com/og-cache.jpg",
'opengraph_url': 'https://cdrm-project.com/cache', "opengraph_url": "https://cdrm-project.com/cache",
'tab_title': 'Cache', "tab_title": "Cache",
}, },
'testplayer': { "testplayer": {
'description': 'Shaka Player for testing decryption keys', "description": "Shaka Player for testing decryption keys",
'keywords': 'Shaka, Player, DRM, CDRM, CDM, CDRM-Project, TPD94, Decryption, CDM-Project, KID, KEY', "keywords": "Shaka, Player, DRM, CDRM, CDM, CDRM-Project, TPD94, Decryption, CDM-Project, KID, KEY",
'opengraph_title': 'Test Player', "opengraph_title": "Test Player",
'opengraph_description': 'Shaka Player for testing decryption keys', "opengraph_description": "Shaka Player for testing decryption keys",
'opengraph_image': 'https://cdrm-project.com/og-testplayer.jpg', "opengraph_image": "https://cdrm-project.com/og-testplayer.jpg",
'opengraph_url': 'https://cdrm-project.com/testplayer', "opengraph_url": "https://cdrm-project.com/testplayer",
'tab_title': 'Test Player', "tab_title": "Test Player",
}, },
'api': { "api": {
'description': 'API documentation for the program "CDRM-Project"', "description": 'API documentation for the program "CDRM-Project"',
'keywords': 'API, python, requests, send, remotecdm, remote, cdm, CDM-Project, CDRM-Project, TPD94, Decryption, DRM, Web, Vault', "keywords": "API, python, requests, send, remotecdm, remote, cdm, CDM-Project, CDRM-Project, TPD94, Decryption, DRM, Web, Vault",
'opengraph_title': 'API', "opengraph_title": "API",
'opengraph_description': 'Documentation for the program "CDRM-Project"', "opengraph_description": 'Documentation for the program "CDRM-Project"',
'opengraph_image': 'https://cdrm-project.com/og-api.jpg', "opengraph_image": "https://cdrm-project.com/og-api.jpg",
'opengraph_url': 'https://cdrm-project.com/api', "opengraph_url": "https://cdrm-project.com/api",
'tab_title': 'API', "tab_title": "API",
}, },
'account': { "account": {
'description': 'Account for CDRM-Project', "description": "Account for CDRM-Project",
'keywords': 'Login, CDRM, CDM, CDRM-Project, register, account', "keywords": "Login, CDRM, CDM, CDRM-Project, register, account",
'opengraph_title': 'My account', "opengraph_title": "My account",
'opengraph_description': 'Account for CDRM-Project', "opengraph_description": "Account for CDRM-Project",
'opengraph_image': 'https://cdrm-project.com/og-home.jpg', "opengraph_image": "https://cdrm-project.com/og-home.jpg",
'opengraph_url': 'https://cdrm-project.com/account', "opengraph_url": "https://cdrm-project.com/account",
'tab_title': 'My account', "tab_title": "My account",
} },
} }

View File

@ -4,16 +4,15 @@ import mysql.connector
from mysql.connector import Error from mysql.connector import Error
def get_db_config(): def get_db_config():
# Configure your MariaDB connection # Configure your MariaDB connection
with open(f'{os.getcwd()}/configs/config.yaml', 'r') as file: with open(f"{os.getcwd()}/configs/config.yaml", "r") as file:
config = yaml.safe_load(file) config = yaml.safe_load(file)
db_config = { db_config = {
'host': f'{config["mariadb"]["host"]}', "host": f'{config["mariadb"]["host"]}',
'user': f'{config["mariadb"]["user"]}', "user": f'{config["mariadb"]["user"]}',
'password': f'{config["mariadb"]["password"]}', "password": f'{config["mariadb"]["password"]}',
'database': f'{config["mariadb"]["database"]}' "database": f'{config["mariadb"]["database"]}',
} }
return db_config return db_config
@ -22,7 +21,8 @@ def create_database():
try: try:
with mysql.connector.connect(**get_db_config()) as conn: with mysql.connector.connect(**get_db_config()) as conn:
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute(''' cursor.execute(
"""
CREATE TABLE IF NOT EXISTS licenses ( CREATE TABLE IF NOT EXISTS licenses (
SERVICE VARCHAR(255), SERVICE VARCHAR(255),
PSSH TEXT, PSSH TEXT,
@ -33,20 +33,32 @@ def create_database():
Cookies TEXT, Cookies TEXT,
Data BLOB Data BLOB
) )
''') """
)
conn.commit() conn.commit()
except Error as e: except Error as e:
print(f"Error: {e}") print(f"Error: {e}")
def cache_to_db(service=None, pssh=None, kid=None, key=None, license_url=None, headers=None, cookies=None, data=None):
def cache_to_db(
service=None,
pssh=None,
kid=None,
key=None,
license_url=None,
headers=None,
cookies=None,
data=None,
):
try: try:
with mysql.connector.connect(**get_db_config()) as conn: with mysql.connector.connect(**get_db_config()) as conn:
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute('SELECT 1 FROM licenses WHERE KID = %s', (kid,)) cursor.execute("SELECT 1 FROM licenses WHERE KID = %s", (kid,))
existing_record = cursor.fetchone() existing_record = cursor.fetchone()
cursor.execute(''' cursor.execute(
"""
INSERT INTO licenses (SERVICE, PSSH, KID, `Key`, License_URL, Headers, Cookies, Data) INSERT INTO licenses (SERVICE, PSSH, KID, `Key`, License_URL, Headers, Cookies, Data)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s) VALUES (%s, %s, %s, %s, %s, %s, %s, %s)
ON DUPLICATE KEY UPDATE ON DUPLICATE KEY UPDATE
@ -57,7 +69,9 @@ def cache_to_db(service=None, pssh=None, kid=None, key=None, license_url=None, h
Headers = VALUES(Headers), Headers = VALUES(Headers),
Cookies = VALUES(Cookies), Cookies = VALUES(Cookies),
Data = VALUES(Data) Data = VALUES(Data)
''', (service, pssh, kid, key, license_url, headers, cookies, data)) """,
(service, pssh, kid, key, license_url, headers, cookies, data),
)
conn.commit() conn.commit()
return True if existing_record else False return True if existing_record else False
@ -65,6 +79,7 @@ def cache_to_db(service=None, pssh=None, kid=None, key=None, license_url=None, h
print(f"Error: {e}") print(f"Error: {e}")
return False return False
def search_by_pssh_or_kid(search_filter): def search_by_pssh_or_kid(search_filter):
results = set() results = set()
try: try:
@ -72,54 +87,71 @@ def search_by_pssh_or_kid(search_filter):
cursor = conn.cursor() cursor = conn.cursor()
like_filter = f"%{search_filter}%" like_filter = f"%{search_filter}%"
cursor.execute('SELECT PSSH, KID, `Key` FROM licenses WHERE PSSH LIKE %s', (like_filter,)) cursor.execute(
"SELECT PSSH, KID, `Key` FROM licenses WHERE PSSH LIKE %s",
(like_filter,),
)
results.update(cursor.fetchall()) results.update(cursor.fetchall())
cursor.execute('SELECT PSSH, KID, `Key` FROM licenses WHERE KID LIKE %s', (like_filter,)) cursor.execute(
"SELECT PSSH, KID, `Key` FROM licenses WHERE KID LIKE %s",
(like_filter,),
)
results.update(cursor.fetchall()) results.update(cursor.fetchall())
final_results = [{'PSSH': row[0], 'KID': row[1], 'Key': row[2]} for row in results] final_results = [
{"PSSH": row[0], "KID": row[1], "Key": row[2]} for row in results
]
return final_results[:20] return final_results[:20]
except Error as e: except Error as e:
print(f"Error: {e}") print(f"Error: {e}")
return [] return []
def get_key_by_kid_and_service(kid, service): def get_key_by_kid_and_service(kid, service):
try: try:
with mysql.connector.connect(**get_db_config()) as conn: with mysql.connector.connect(**get_db_config()) as conn:
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute('SELECT `Key` FROM licenses WHERE KID = %s AND SERVICE = %s', (kid, service)) cursor.execute(
"SELECT `Key` FROM licenses WHERE KID = %s AND SERVICE = %s",
(kid, service),
)
result = cursor.fetchone() result = cursor.fetchone()
return result[0] if result else None return result[0] if result else None
except Error as e: except Error as e:
print(f"Error: {e}") print(f"Error: {e}")
return None return None
def get_kid_key_dict(service_name): def get_kid_key_dict(service_name):
try: try:
with mysql.connector.connect(**get_db_config()) as conn: with mysql.connector.connect(**get_db_config()) as conn:
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute('SELECT KID, `Key` FROM licenses WHERE SERVICE = %s', (service_name,)) cursor.execute(
"SELECT KID, `Key` FROM licenses WHERE SERVICE = %s", (service_name,)
)
return {row[0]: row[1] for row in cursor.fetchall()} return {row[0]: row[1] for row in cursor.fetchall()}
except Error as e: except Error as e:
print(f"Error: {e}") print(f"Error: {e}")
return {} return {}
def get_unique_services(): def get_unique_services():
try: try:
with mysql.connector.connect(**get_db_config()) as conn: with mysql.connector.connect(**get_db_config()) as conn:
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute('SELECT DISTINCT SERVICE FROM licenses') cursor.execute("SELECT DISTINCT SERVICE FROM licenses")
return [row[0] for row in cursor.fetchall()] return [row[0] for row in cursor.fetchall()]
except Error as e: except Error as e:
print(f"Error: {e}") print(f"Error: {e}")
return [] return []
def key_count(): def key_count():
try: try:
with mysql.connector.connect(**get_db_config()) as conn: with mysql.connector.connect(**get_db_config()) as conn:
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute('SELECT COUNT(KID) FROM licenses') cursor.execute("SELECT COUNT(KID) FROM licenses")
return cursor.fetchone()[0] return cursor.fetchone()[0]
except Error as e: except Error as e:
print(f"Error: {e}") print(f"Error: {e}")

View File

@ -1,11 +1,13 @@
import sqlite3 import sqlite3
import os import os
def create_database(): def create_database():
# Using with statement to manage the connection and cursor # Using with statement to manage the connection and cursor
with sqlite3.connect(f'{os.getcwd()}/databases/sql/key_cache.db') as conn: with sqlite3.connect(f"{os.getcwd()}/databases/sql/key_cache.db") as conn:
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute(''' cursor.execute(
"""
CREATE TABLE IF NOT EXISTS licenses ( CREATE TABLE IF NOT EXISTS licenses (
SERVICE TEXT, SERVICE TEXT,
PSSH TEXT, PSSH TEXT,
@ -16,92 +18,127 @@ def create_database():
Cookies TEXT, Cookies TEXT,
Data TEXT Data TEXT
) )
''') """
)
def cache_to_db(service: str = None, pssh: str = None, kid: str = None, key: str = None, license_url: str = None, headers: str = None, cookies: str = None, data: str = None):
with sqlite3.connect(f'{os.getcwd()}/databases/sql/key_cache.db') as conn: def cache_to_db(
service: str = None,
pssh: str = None,
kid: str = None,
key: str = None,
license_url: str = None,
headers: str = None,
cookies: str = None,
data: str = None,
):
with sqlite3.connect(f"{os.getcwd()}/databases/sql/key_cache.db") as conn:
cursor = conn.cursor() cursor = conn.cursor()
# Check if the record with the given KID already exists # Check if the record with the given KID already exists
cursor.execute('''SELECT 1 FROM licenses WHERE KID = ?''', (kid,)) cursor.execute("""SELECT 1 FROM licenses WHERE KID = ?""", (kid,))
existing_record = cursor.fetchone() existing_record = cursor.fetchone()
# Insert or replace the record # Insert or replace the record
cursor.execute(''' cursor.execute(
"""
INSERT OR REPLACE INTO licenses (SERVICE, PSSH, KID, Key, License_URL, Headers, Cookies, Data) INSERT OR REPLACE INTO licenses (SERVICE, PSSH, KID, Key, License_URL, Headers, Cookies, Data)
VALUES (?, ?, ?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
''', (service, pssh, kid, key, license_url, headers, cookies, data)) """,
(service, pssh, kid, key, license_url, headers, cookies, data),
)
# If the record was existing and updated, return True (updated), else return False (added) # If the record was existing and updated, return True (updated), else return False (added)
return True if existing_record else False return True if existing_record else False
def search_by_pssh_or_kid(search_filter): def search_by_pssh_or_kid(search_filter):
# Using with statement to automatically close the connection # Using with statement to automatically close the connection
with sqlite3.connect(f'{os.getcwd()}/databases/sql/key_cache.db') as conn: with sqlite3.connect(f"{os.getcwd()}/databases/sql/key_cache.db") as conn:
cursor = conn.cursor() cursor = conn.cursor()
# Initialize a set to store unique matching records # Initialize a set to store unique matching records
results = set() results = set()
# Search for records where PSSH contains the search_filter # Search for records where PSSH contains the search_filter
cursor.execute(''' cursor.execute(
"""
SELECT * FROM licenses WHERE PSSH LIKE ? SELECT * FROM licenses WHERE PSSH LIKE ?
''', ('%' + search_filter + '%',)) """,
("%" + search_filter + "%",),
)
rows = cursor.fetchall() rows = cursor.fetchall()
for row in rows: for row in rows:
results.add((row[1], row[2], row[3])) # (PSSH, KID, Key) results.add((row[1], row[2], row[3])) # (PSSH, KID, Key)
# Search for records where KID contains the search_filter # Search for records where KID contains the search_filter
cursor.execute(''' cursor.execute(
"""
SELECT * FROM licenses WHERE KID LIKE ? SELECT * FROM licenses WHERE KID LIKE ?
''', ('%' + search_filter + '%',)) """,
("%" + search_filter + "%",),
)
rows = cursor.fetchall() rows = cursor.fetchall()
for row in rows: for row in rows:
results.add((row[1], row[2], row[3])) # (PSSH, KID, Key) results.add((row[1], row[2], row[3])) # (PSSH, KID, Key)
# Convert the set of results to a list of dictionaries for output # Convert the set of results to a list of dictionaries for output
final_results = [{'PSSH': result[0], 'KID': result[1], 'Key': result[2]} for result in results] final_results = [
{"PSSH": result[0], "KID": result[1], "Key": result[2]}
for result in results
]
return final_results[:20] return final_results[:20]
def get_key_by_kid_and_service(kid, service): def get_key_by_kid_and_service(kid, service):
# Using 'with' to automatically close the connection when done # Using 'with' to automatically close the connection when done
with sqlite3.connect(f'{os.getcwd()}/databases/sql/key_cache.db') as conn: with sqlite3.connect(f"{os.getcwd()}/databases/sql/key_cache.db") as conn:
cursor = conn.cursor() cursor = conn.cursor()
# Query to search by KID and SERVICE # Query to search by KID and SERVICE
cursor.execute(''' cursor.execute(
"""
SELECT Key FROM licenses WHERE KID = ? AND SERVICE = ? SELECT Key FROM licenses WHERE KID = ? AND SERVICE = ?
''', (kid, service)) """,
(kid, service),
)
# Fetch the result # Fetch the result
result = cursor.fetchone() result = cursor.fetchone()
# Check if a result was found # Check if a result was found
return result[0] if result else None # The 'Key' is the first (and only) column returned in the result return (
result[0] if result else None
) # The 'Key' is the first (and only) column returned in the result
def get_kid_key_dict(service_name): def get_kid_key_dict(service_name):
# Using with statement to automatically manage the connection and cursor # Using with statement to automatically manage the connection and cursor
with sqlite3.connect(f'{os.getcwd()}/databases/sql/key_cache.db') as conn: with sqlite3.connect(f"{os.getcwd()}/databases/sql/key_cache.db") as conn:
cursor = conn.cursor() cursor = conn.cursor()
# Query to fetch KID and Key for the selected service # Query to fetch KID and Key for the selected service
cursor.execute(''' cursor.execute(
"""
SELECT KID, Key FROM licenses WHERE SERVICE = ? SELECT KID, Key FROM licenses WHERE SERVICE = ?
''', (service_name,)) """,
(service_name,),
)
# Fetch all results and create the dictionary # Fetch all results and create the dictionary
kid_key_dict = {row[0]: row[1] for row in cursor.fetchall()} kid_key_dict = {row[0]: row[1] for row in cursor.fetchall()}
return kid_key_dict return kid_key_dict
def get_unique_services(): def get_unique_services():
# Using with statement to automatically manage the connection and cursor # Using with statement to automatically manage the connection and cursor
with sqlite3.connect(f'{os.getcwd()}/databases/sql/key_cache.db') as conn: with sqlite3.connect(f"{os.getcwd()}/databases/sql/key_cache.db") as conn:
cursor = conn.cursor() cursor = conn.cursor()
# Query to get distinct services from the 'licenses' table # Query to get distinct services from the 'licenses' table
cursor.execute('SELECT DISTINCT SERVICE FROM licenses') cursor.execute("SELECT DISTINCT SERVICE FROM licenses")
# Fetch all results and extract the unique services # Fetch all results and extract the unique services
services = cursor.fetchall() services = cursor.fetchall()
@ -111,13 +148,14 @@ def get_unique_services():
return unique_services return unique_services
def key_count(): def key_count():
# Using with statement to automatically manage the connection and cursor # Using with statement to automatically manage the connection and cursor
with sqlite3.connect(f'{os.getcwd()}/databases/sql/key_cache.db') as conn: with sqlite3.connect(f"{os.getcwd()}/databases/sql/key_cache.db") as conn:
cursor = conn.cursor() cursor = conn.cursor()
# Count the number of KID entries in the licenses table # Count the number of KID entries in the licenses table
cursor.execute('SELECT COUNT(KID) FROM licenses') cursor.execute("SELECT COUNT(KID) FROM licenses")
count = cursor.fetchone()[0] # Fetch the result and get the count count = cursor.fetchone()[0] # Fetch the result and get the count
return count return count

View File

@ -4,27 +4,32 @@ import bcrypt
def create_user_database(): def create_user_database():
os.makedirs(f'{os.getcwd()}/databases/sql', exist_ok=True) os.makedirs(f"{os.getcwd()}/databases/sql", exist_ok=True)
with sqlite3.connect(f'{os.getcwd()}/databases/sql/users.db') as conn: with sqlite3.connect(f"{os.getcwd()}/databases/sql/users.db") as conn:
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute(''' cursor.execute(
"""
CREATE TABLE IF NOT EXISTS user_info ( CREATE TABLE IF NOT EXISTS user_info (
Username TEXT PRIMARY KEY, Username TEXT PRIMARY KEY,
Password TEXT, Password TEXT,
Styled_Username TEXT, Styled_Username TEXT,
API_Key TEXT API_Key TEXT
) )
''') """
)
def add_user(username, password, api_key): def add_user(username, password, api_key):
hashed_pw = bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt()) hashed_pw = bcrypt.hashpw(password.encode("utf-8"), bcrypt.gensalt())
with sqlite3.connect(f'{os.getcwd()}/databases/sql/users.db') as conn: with sqlite3.connect(f"{os.getcwd()}/databases/sql/users.db") as conn:
cursor = conn.cursor() cursor = conn.cursor()
try: try:
cursor.execute('INSERT INTO user_info (Username, Password, Styled_Username, API_Key) VALUES (?, ?, ?, ?)', (username.lower(), hashed_pw, username, api_key)) cursor.execute(
"INSERT INTO user_info (Username, Password, Styled_Username, API_Key) VALUES (?, ?, ?, ?)",
(username.lower(), hashed_pw, username, api_key),
)
conn.commit() conn.commit()
return True return True
except sqlite3.IntegrityError: except sqlite3.IntegrityError:
@ -32,24 +37,29 @@ def add_user(username, password, api_key):
def verify_user(username, password): def verify_user(username, password):
with sqlite3.connect(f'{os.getcwd()}/databases/sql/users.db') as conn: with sqlite3.connect(f"{os.getcwd()}/databases/sql/users.db") as conn:
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute('SELECT Password FROM user_info WHERE Username = ?', (username.lower(),)) cursor.execute(
"SELECT Password FROM user_info WHERE Username = ?", (username.lower(),)
)
result = cursor.fetchone() result = cursor.fetchone()
if result: if result:
stored_hash = result[0] stored_hash = result[0]
# Ensure stored_hash is bytes; decode if it's still a string (SQLite may store as TEXT) # Ensure stored_hash is bytes; decode if it's still a string (SQLite may store as TEXT)
if isinstance(stored_hash, str): if isinstance(stored_hash, str):
stored_hash = stored_hash.encode('utf-8') stored_hash = stored_hash.encode("utf-8")
return bcrypt.checkpw(password.encode('utf-8'), stored_hash) return bcrypt.checkpw(password.encode("utf-8"), stored_hash)
else: else:
return False return False
def fetch_api_key(username): def fetch_api_key(username):
with sqlite3.connect(f'{os.getcwd()}/databases/sql/users.db') as conn: with sqlite3.connect(f"{os.getcwd()}/databases/sql/users.db") as conn:
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute('SELECT API_Key FROM user_info WHERE Username = ?', (username.lower(),)) cursor.execute(
"SELECT API_Key FROM user_info WHERE Username = ?", (username.lower(),)
)
result = cursor.fetchone() result = cursor.fetchone()
if result: if result:
@ -57,30 +67,42 @@ def fetch_api_key(username):
else: else:
return None return None
def change_password(username, new_password): def change_password(username, new_password):
# Hash the new password # Hash the new password
new_hashed_pw = bcrypt.hashpw(new_password.encode('utf-8'), bcrypt.gensalt()) new_hashed_pw = bcrypt.hashpw(new_password.encode("utf-8"), bcrypt.gensalt())
# Update the password in the database # Update the password in the database
with sqlite3.connect(f'{os.getcwd()}/databases/sql/users.db') as conn: with sqlite3.connect(f"{os.getcwd()}/databases/sql/users.db") as conn:
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute('UPDATE user_info SET Password = ? WHERE Username = ?', (new_hashed_pw, username.lower())) cursor.execute(
"UPDATE user_info SET Password = ? WHERE Username = ?",
(new_hashed_pw, username.lower()),
)
conn.commit() conn.commit()
return True return True
def change_api_key(username, new_api_key): def change_api_key(username, new_api_key):
# Update the API key in the database # Update the API key in the database
with sqlite3.connect(f'{os.getcwd()}/databases/sql/users.db') as conn: with sqlite3.connect(f"{os.getcwd()}/databases/sql/users.db") as conn:
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute('UPDATE user_info SET API_Key = ? WHERE Username = ?', (new_api_key, username.lower())) cursor.execute(
"UPDATE user_info SET API_Key = ? WHERE Username = ?",
(new_api_key, username.lower()),
)
conn.commit() conn.commit()
return True return True
def fetch_styled_username(username): def fetch_styled_username(username):
with sqlite3.connect(f'{os.getcwd()}/databases/sql/users.db') as conn: with sqlite3.connect(f"{os.getcwd()}/databases/sql/users.db") as conn:
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute('SELECT Styled_Username FROM user_info WHERE Username = ?', (username.lower(),)) cursor.execute(
"SELECT Styled_Username FROM user_info WHERE Username = ?",
(username.lower(),),
)
result = cursor.fetchone() result = cursor.fetchone()
if result: if result:
@ -88,13 +110,14 @@ def fetch_styled_username(username):
else: else:
return None return None
def fetch_username_by_api_key(api_key): def fetch_username_by_api_key(api_key):
with sqlite3.connect(f'{os.getcwd()}/databases/sql/users.db') as conn: with sqlite3.connect(f"{os.getcwd()}/databases/sql/users.db") as conn:
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute('SELECT Username FROM user_info WHERE API_Key = ?', (api_key,)) cursor.execute("SELECT Username FROM user_info WHERE API_Key = ?", (api_key,))
result = cursor.fetchone() result = cursor.fetchone()
if result: if result:
return result[0] # Return the username return result[0] # Return the username
else: else:
return None # If no user is found for the API key return None # If no user is found for the API key

View File

@ -13,19 +13,23 @@ import yaml
from urllib.parse import urlparse from urllib.parse import urlparse
def find_license_key(data, keywords=None): def find_license_key(data, keywords=None):
if keywords is None: if keywords is None:
keywords = ["license", "licenseData", "widevine2License"] # Default list of keywords to search for keywords = [
"license",
"licenseData",
"widevine2License",
] # Default list of keywords to search for
# If the data is a dictionary, check each key # If the data is a dictionary, check each key
if isinstance(data, dict): if isinstance(data, dict):
for key, value in data.items(): for key, value in data.items():
if any(keyword in key.lower() for keyword in if any(
keywords): # Check if any keyword is in the key (case-insensitive) keyword in key.lower() for keyword in keywords
return value.replace("-", "+").replace("_", "/") # Return the value immediately when found ): # Check if any keyword is in the key (case-insensitive)
return value.replace("-", "+").replace(
"_", "/"
) # Return the value immediately when found
# Recursively check if the value is a dictionary or list # Recursively check if the value is a dictionary or list
if isinstance(value, (dict, list)): if isinstance(value, (dict, list)):
result = find_license_key(value, keywords) # Recursively search result = find_license_key(value, keywords) # Recursively search
@ -44,21 +48,32 @@ def find_license_key(data, keywords=None):
def find_license_challenge(data, keywords=None, new_value=None): def find_license_challenge(data, keywords=None, new_value=None):
if keywords is None: if keywords is None:
keywords = ["license", "licenseData", "widevine2License", "licenseRequest"] # Default list of keywords to search for keywords = [
"license",
"licenseData",
"widevine2License",
"licenseRequest",
] # Default list of keywords to search for
# If the data is a dictionary, check each key # If the data is a dictionary, check each key
if isinstance(data, dict): if isinstance(data, dict):
for key, value in data.items(): for key, value in data.items():
if any(keyword in key.lower() for keyword in keywords): # Check if any keyword is in the key (case-insensitive) if any(
keyword in key.lower() for keyword in keywords
): # Check if any keyword is in the key (case-insensitive)
data[key] = new_value # Modify the value in-place data[key] = new_value # Modify the value in-place
# Recursively check if the value is a dictionary or list # Recursively check if the value is a dictionary or list
elif isinstance(value, (dict, list)): elif isinstance(value, (dict, list)):
find_license_challenge(value, keywords, new_value) # Recursively modify in place find_license_challenge(
value, keywords, new_value
) # Recursively modify in place
# If the data is a list, iterate through each item # If the data is a list, iterate through each item
elif isinstance(data, list): elif isinstance(data, list):
for i, item in enumerate(data): for i, item in enumerate(data):
result = find_license_challenge(item, keywords, new_value) # Recursively modify in place result = find_license_challenge(
item, keywords, new_value
) # Recursively modify in place
return data # Return the modified original data (no new structure is created) return data # Return the modified original data (no new structure is created)
@ -68,11 +83,12 @@ def is_base64(string):
# Try decoding the string # Try decoding the string
decoded_data = base64.b64decode(string) decoded_data = base64.b64decode(string)
# Check if the decoded data, when re-encoded, matches the original string # Check if the decoded data, when re-encoded, matches the original string
return base64.b64encode(decoded_data).decode('utf-8') == string return base64.b64encode(decoded_data).decode("utf-8") == string
except Exception: except Exception:
# If decoding or encoding fails, it's not Base64 # If decoding or encoding fails, it's not Base64
return False return False
def is_url_and_split(input_str): def is_url_and_split(input_str):
parsed = urlparse(input_str) parsed = urlparse(input_str)
@ -84,82 +100,99 @@ def is_url_and_split(input_str):
else: else:
return False, None, None return False, None, None
def api_decrypt(pssh:str = None, license_url: str = None, proxy: str = None, headers: str = None, cookies: str = None, json_data: str = None, device: str = 'public', username: str = None):
print(f'Using device {device} for user {username}') def api_decrypt(
with open(f'{os.getcwd()}/configs/config.yaml', 'r') as file: pssh: str = None,
license_url: str = None,
proxy: str = None,
headers: str = None,
cookies: str = None,
json_data: str = None,
device: str = "public",
username: str = None,
):
print(f"Using device {device} for user {username}")
with open(f"{os.getcwd()}/configs/config.yaml", "r") as file:
config = yaml.safe_load(file) config = yaml.safe_load(file)
if config['database_type'].lower() == 'sqlite': if config["database_type"].lower() == "sqlite":
from custom_functions.database.cache_to_db_sqlite import cache_to_db from custom_functions.database.cache_to_db_sqlite import cache_to_db
elif config['database_type'].lower() == 'mariadb': elif config["database_type"].lower() == "mariadb":
from custom_functions.database.cache_to_db_mariadb import cache_to_db from custom_functions.database.cache_to_db_mariadb import cache_to_db
if pssh is None: if pssh is None:
return { return {"status": "error", "message": "No PSSH provided"}
'status': 'error',
'message': 'No PSSH provided'
}
try: try:
if "</WRMHEADER>".encode("utf-16-le") in base64.b64decode(pssh): # PR if "</WRMHEADER>".encode("utf-16-le") in base64.b64decode(pssh): # PR
try: try:
pr_pssh = playreadyPSSH(pssh) pr_pssh = playreadyPSSH(pssh)
except Exception as error: except Exception as error:
return { return {
'status': 'error', "status": "error",
'message': f'An error occurred processing PSSH\n\n{error}' "message": f"An error occurred processing PSSH\n\n{error}",
} }
try: try:
if device == 'public': if device == "public":
base_name = config["default_pr_cdm"] base_name = config["default_pr_cdm"]
if not base_name.endswith(".prd"): if not base_name.endswith(".prd"):
base_name += ".prd" base_name += ".prd"
prd_files = glob.glob(f'{os.getcwd()}/configs/CDMs/PR/{base_name}') prd_files = glob.glob(
f"{os.getcwd()}/configs/CDMs/PR/{base_name}"
)
else: else:
prd_files = glob.glob(f'{os.getcwd()}/configs/CDMs/PR/{base_name}') prd_files = glob.glob(
f"{os.getcwd()}/configs/CDMs/PR/{base_name}"
)
if prd_files: if prd_files:
pr_device = playreadyDevice.load(prd_files[0]) pr_device = playreadyDevice.load(prd_files[0])
else: else:
return { return {
'status': 'error', "status": "error",
'message': 'No default .prd file found' "message": "No default .prd file found",
} }
else: else:
base_name = device base_name = device
if not base_name.endswith(".prd"): if not base_name.endswith(".prd"):
base_name += ".prd" base_name += ".prd"
prd_files = glob.glob(f'{os.getcwd()}/configs/CDMs/{username}/PR/{base_name}') prd_files = glob.glob(
f"{os.getcwd()}/configs/CDMs/{username}/PR/{base_name}"
)
else: else:
prd_files = glob.glob(f'{os.getcwd()}/configs/CDMs/{username}/PR/{base_name}') prd_files = glob.glob(
f"{os.getcwd()}/configs/CDMs/{username}/PR/{base_name}"
)
if prd_files: if prd_files:
pr_device = playreadyDevice.load(prd_files[0]) pr_device = playreadyDevice.load(prd_files[0])
else: else:
return { return {
'status': 'error', "status": "error",
'message': f'{base_name} does not exist' "message": f"{base_name} does not exist",
} }
except Exception as error: except Exception as error:
return { return {
'status': 'error', "status": "error",
'message': f'An error occurred location PlayReady CDM file\n\n{error}' "message": f"An error occurred location PlayReady CDM file\n\n{error}",
} }
try: try:
pr_cdm = playreadyCdm.from_device(pr_device) pr_cdm = playreadyCdm.from_device(pr_device)
except Exception as error: except Exception as error:
return { return {
'status': 'error', "status": "error",
'message': f'An error occurred loading PlayReady CDM\n\n{error}' "message": f"An error occurred loading PlayReady CDM\n\n{error}",
} }
try: try:
pr_session_id = pr_cdm.open() pr_session_id = pr_cdm.open()
except Exception as error: except Exception as error:
return { return {
'status': 'error', "status": "error",
'message': f'An error occurred opening a CDM session\n\n{error}' "message": f"An error occurred opening a CDM session\n\n{error}",
} }
try: try:
pr_challenge = pr_cdm.get_license_challenge(pr_session_id, pr_pssh.wrm_headers[0]) pr_challenge = pr_cdm.get_license_challenge(
pr_session_id, pr_pssh.wrm_headers[0]
)
except Exception as error: except Exception as error:
return { return {
'status': 'error', "status": "error",
'message': f'An error occurred getting license challenge\n\n{error}' "message": f"An error occurred getting license challenge\n\n{error}",
} }
try: try:
if headers: if headers:
@ -168,8 +201,8 @@ def api_decrypt(pssh:str = None, license_url: str = None, proxy: str = None, hea
format_headers = None format_headers = None
except Exception as error: except Exception as error:
return { return {
'status': 'error', "status": "error",
'message': f'An error occurred getting headers\n\n{error}' "message": f"An error occurred getting headers\n\n{error}",
} }
try: try:
if cookies: if cookies:
@ -178,8 +211,8 @@ def api_decrypt(pssh:str = None, license_url: str = None, proxy: str = None, hea
format_cookies = None format_cookies = None
except Exception as error: except Exception as error:
return { return {
'status': 'error', "status": "error",
'message': f'An error occurred getting cookies\n\n{error}' "message": f"An error occurred getting cookies\n\n{error}",
} }
try: try:
if json_data and not is_base64(json_data): if json_data and not is_base64(json_data):
@ -188,19 +221,19 @@ def api_decrypt(pssh:str = None, license_url: str = None, proxy: str = None, hea
format_json_data = None format_json_data = None
except Exception as error: except Exception as error:
return { return {
'status': 'error', "status": "error",
'message': f'An error occurred getting json_data\n\n{error}' "message": f"An error occurred getting json_data\n\n{error}",
} }
licence = None licence = None
proxies = None proxies = None
if proxy is not None: if proxy is not None:
is_url, protocol, fqdn = is_url_and_split(proxy) is_url, protocol, fqdn = is_url_and_split(proxy)
if is_url: if is_url:
proxies = {'http': proxy, 'https': proxy} proxies = {"http": proxy, "https": proxy}
else: else:
return { return {
'status': 'error', "status": "error",
'message': f'Your proxy is invalid, please put it in the format of http(s)://fqdn.tld:port' "message": f"Your proxy is invalid, please put it in the format of http(s)://fqdn.tld:port",
} }
try: try:
licence = requests.post( licence = requests.post(
@ -209,132 +242,133 @@ def api_decrypt(pssh:str = None, license_url: str = None, proxy: str = None, hea
proxies=proxies, proxies=proxies,
cookies=format_cookies, cookies=format_cookies,
json=format_json_data if format_json_data is not None else None, json=format_json_data if format_json_data is not None else None,
data=pr_challenge if format_json_data is None else None data=pr_challenge if format_json_data is None else None,
) )
except requests.exceptions.ConnectionError as error: except requests.exceptions.ConnectionError as error:
return { return {
'status': 'error', "status": "error",
'message': f'An error occurred sending license challenge through your proxy\n\n{error}' "message": f"An error occurred sending license challenge through your proxy\n\n{error}",
} }
except Exception as error: except Exception as error:
return { return {
'status': 'error', "status": "error",
'message': f'An error occurred sending license reqeust\n\n{error}\n\n{licence.content}' "message": f"An error occurred sending license reqeust\n\n{error}\n\n{licence.content}",
} }
try: try:
pr_cdm.parse_license(pr_session_id, licence.text) pr_cdm.parse_license(pr_session_id, licence.text)
except Exception as error: except Exception as error:
return { return {
'status': 'error', "status": "error",
'message': f'An error occurred parsing license content\n\n{error}\n\n{licence.content}' "message": f"An error occurred parsing license content\n\n{error}\n\n{licence.content}",
} }
returned_keys = "" returned_keys = ""
try: try:
keys = list(pr_cdm.get_keys(pr_session_id)) keys = list(pr_cdm.get_keys(pr_session_id))
except Exception as error: except Exception as error:
return { return {
'status': 'error', "status": "error",
'message': f'An error occurred getting keys\n\n{error}' "message": f"An error occurred getting keys\n\n{error}",
} }
try: try:
for index, key in enumerate(keys): for index, key in enumerate(keys):
if key.key_type != 'SIGNING': if key.key_type != "SIGNING":
cache_to_db(pssh=pssh, license_url=license_url, headers=headers, cookies=cookies, cache_to_db(
data=pr_challenge if json_data is None else json_data, kid=key.key_id.hex, pssh=pssh,
key=key.key.hex()) license_url=license_url,
headers=headers,
cookies=cookies,
data=pr_challenge if json_data is None else json_data,
kid=key.key_id.hex,
key=key.key.hex(),
)
if index != len(keys) - 1: if index != len(keys) - 1:
returned_keys += f"{key.key_id.hex}:{key.key.hex()}\n" returned_keys += f"{key.key_id.hex}:{key.key.hex()}\n"
else: else:
returned_keys += f"{key.key_id.hex}:{key.key.hex()}" returned_keys += f"{key.key_id.hex}:{key.key.hex()}"
except Exception as error: except Exception as error:
return { return {
'status': 'error', "status": "error",
'message': f'An error occurred formatting keys\n\n{error}' "message": f"An error occurred formatting keys\n\n{error}",
} }
try: try:
pr_cdm.close(pr_session_id) pr_cdm.close(pr_session_id)
except Exception as error: except Exception as error:
return { return {
'status': 'error', "status": "error",
'message': f'An error occurred closing session\n\n{error}' "message": f"An error occurred closing session\n\n{error}",
} }
try: try:
return { return {"status": "success", "message": returned_keys}
'status': 'success',
'message': returned_keys
}
except Exception as error: except Exception as error:
return { return {
'status': 'error', "status": "error",
'message': f'An error occurred getting returned_keys\n\n{error}' "message": f"An error occurred getting returned_keys\n\n{error}",
} }
except Exception as error: except Exception as error:
return { return {
'status': 'error', "status": "error",
'message': f'An error occurred processing PSSH\n\n{error}' "message": f"An error occurred processing PSSH\n\n{error}",
} }
else: else:
try: try:
wv_pssh = widevinePSSH(pssh) wv_pssh = widevinePSSH(pssh)
except Exception as error: except Exception as error:
return { return {
'status': 'error', "status": "error",
'message': f'An error occurred processing PSSH\n\n{error}' "message": f"An error occurred processing PSSH\n\n{error}",
} }
try: try:
if device == 'public': if device == "public":
base_name = config["default_wv_cdm"] base_name = config["default_wv_cdm"]
if not base_name.endswith(".wvd"): if not base_name.endswith(".wvd"):
base_name += ".wvd" base_name += ".wvd"
wvd_files = glob.glob(f'{os.getcwd()}/configs/CDMs/WV/{base_name}') wvd_files = glob.glob(f"{os.getcwd()}/configs/CDMs/WV/{base_name}")
else: else:
wvd_files = glob.glob(f'{os.getcwd()}/configs/CDMs/WV/{base_name}') wvd_files = glob.glob(f"{os.getcwd()}/configs/CDMs/WV/{base_name}")
if wvd_files: if wvd_files:
wv_device = widevineDevice.load(wvd_files[0]) wv_device = widevineDevice.load(wvd_files[0])
else: else:
return { return {"status": "error", "message": "No default .wvd file found"}
'status': 'error',
'message': 'No default .wvd file found'
}
else: else:
base_name = device base_name = device
if not base_name.endswith(".wvd"): if not base_name.endswith(".wvd"):
base_name += ".wvd" base_name += ".wvd"
wvd_files = glob.glob(f'{os.getcwd()}/configs/CDMs/{username}/WV/{base_name}') wvd_files = glob.glob(
f"{os.getcwd()}/configs/CDMs/{username}/WV/{base_name}"
)
else: else:
wvd_files = glob.glob(f'{os.getcwd()}/configs/CDMs/{username}/WV/{base_name}') wvd_files = glob.glob(
f"{os.getcwd()}/configs/CDMs/{username}/WV/{base_name}"
)
if wvd_files: if wvd_files:
wv_device = widevineDevice.load(wvd_files[0]) wv_device = widevineDevice.load(wvd_files[0])
else: else:
return { return {"status": "error", "message": f"{base_name} does not exist"}
'status': 'error',
'message': f'{base_name} does not exist'
}
except Exception as error: except Exception as error:
return { return {
'status': 'error', "status": "error",
'message': f'An error occurred location Widevine CDM file\n\n{error}' "message": f"An error occurred location Widevine CDM file\n\n{error}",
} }
try: try:
wv_cdm = widevineCdm.from_device(wv_device) wv_cdm = widevineCdm.from_device(wv_device)
except Exception as error: except Exception as error:
return { return {
'status': 'error', "status": "error",
'message': f'An error occurred loading Widevine CDM\n\n{error}' "message": f"An error occurred loading Widevine CDM\n\n{error}",
} }
try: try:
wv_session_id = wv_cdm.open() wv_session_id = wv_cdm.open()
except Exception as error: except Exception as error:
return { return {
'status': 'error', "status": "error",
'message': f'An error occurred opening a CDM session\n\n{error}' "message": f"An error occurred opening a CDM session\n\n{error}",
} }
try: try:
wv_challenge = wv_cdm.get_license_challenge(wv_session_id, wv_pssh) wv_challenge = wv_cdm.get_license_challenge(wv_session_id, wv_pssh)
except Exception as error: except Exception as error:
return { return {
'status': 'error', "status": "error",
'message': f'An error occurred getting license challenge\n\n{error}' "message": f"An error occurred getting license challenge\n\n{error}",
} }
try: try:
if headers: if headers:
@ -343,8 +377,8 @@ def api_decrypt(pssh:str = None, license_url: str = None, proxy: str = None, hea
format_headers = None format_headers = None
except Exception as error: except Exception as error:
return { return {
'status': 'error', "status": "error",
'message': f'An error occurred getting headers\n\n{error}' "message": f"An error occurred getting headers\n\n{error}",
} }
try: try:
if cookies: if cookies:
@ -353,26 +387,29 @@ def api_decrypt(pssh:str = None, license_url: str = None, proxy: str = None, hea
format_cookies = None format_cookies = None
except Exception as error: except Exception as error:
return { return {
'status': 'error', "status": "error",
'message': f'An error occurred getting cookies\n\n{error}' "message": f"An error occurred getting cookies\n\n{error}",
} }
try: try:
if json_data and not is_base64(json_data): if json_data and not is_base64(json_data):
format_json_data = ast.literal_eval(json_data) format_json_data = ast.literal_eval(json_data)
format_json_data = find_license_challenge(data=format_json_data, new_value=base64.b64encode(wv_challenge).decode()) format_json_data = find_license_challenge(
data=format_json_data,
new_value=base64.b64encode(wv_challenge).decode(),
)
else: else:
format_json_data = None format_json_data = None
except Exception as error: except Exception as error:
return { return {
'status': 'error', "status": "error",
'message': f'An error occurred getting json_data\n\n{error}' "message": f"An error occurred getting json_data\n\n{error}",
} }
licence = None licence = None
proxies = None proxies = None
if proxy is not None: if proxy is not None:
is_url, protocol, fqdn = is_url_and_split(proxy) is_url, protocol, fqdn = is_url_and_split(proxy)
if is_url: if is_url:
proxies = {'http': proxy, 'https': proxy} proxies = {"http": proxy, "https": proxy}
try: try:
licence = requests.post( licence = requests.post(
url=license_url, url=license_url,
@ -380,17 +417,17 @@ def api_decrypt(pssh:str = None, license_url: str = None, proxy: str = None, hea
proxies=proxies, proxies=proxies,
cookies=format_cookies, cookies=format_cookies,
json=format_json_data if format_json_data is not None else None, json=format_json_data if format_json_data is not None else None,
data=wv_challenge if format_json_data is None else None data=wv_challenge if format_json_data is None else None,
) )
except requests.exceptions.ConnectionError as error: except requests.exceptions.ConnectionError as error:
return { return {
'status': 'error', "status": "error",
'message': f'An error occurred sending license challenge through your proxy\n\n{error}' "message": f"An error occurred sending license challenge through your proxy\n\n{error}",
} }
except Exception as error: except Exception as error:
return { return {
'status': 'error', "status": "error",
'message': f'An error occurred sending license reqeust\n\n{error}\n\n{licence.content}' "message": f"An error occurred sending license reqeust\n\n{error}\n\n{licence.content}",
} }
try: try:
wv_cdm.parse_license(wv_session_id, licence.content) wv_cdm.parse_license(wv_session_id, licence.content)
@ -401,44 +438,49 @@ def api_decrypt(pssh:str = None, license_url: str = None, proxy: str = None, hea
wv_cdm.parse_license(wv_session_id, license_value) wv_cdm.parse_license(wv_session_id, license_value)
except Exception as error: except Exception as error:
return { return {
'status': 'error', "status": "error",
'message': f'An error occurred parsing license content\n\n{error}\n\n{licence.content}' "message": f"An error occurred parsing license content\n\n{error}\n\n{licence.content}",
} }
returned_keys = "" returned_keys = ""
try: try:
keys = list(wv_cdm.get_keys(wv_session_id)) keys = list(wv_cdm.get_keys(wv_session_id))
except Exception as error: except Exception as error:
return { return {
'status': 'error', "status": "error",
'message': f'An error occurred getting keys\n\n{error}' "message": f"An error occurred getting keys\n\n{error}",
} }
try: try:
for index, key in enumerate(keys): for index, key in enumerate(keys):
if key.type != 'SIGNING': if key.type != "SIGNING":
cache_to_db(pssh=pssh, license_url=license_url, headers=headers, cookies=cookies, data=wv_challenge if json_data is None else json_data, kid=key.kid.hex, key=key.key.hex()) cache_to_db(
pssh=pssh,
license_url=license_url,
headers=headers,
cookies=cookies,
data=wv_challenge if json_data is None else json_data,
kid=key.kid.hex,
key=key.key.hex(),
)
if index != len(keys) - 1: if index != len(keys) - 1:
returned_keys += f"{key.kid.hex}:{key.key.hex()}\n" returned_keys += f"{key.kid.hex}:{key.key.hex()}\n"
else: else:
returned_keys += f"{key.kid.hex}:{key.key.hex()}" returned_keys += f"{key.kid.hex}:{key.key.hex()}"
except Exception as error: except Exception as error:
return { return {
'status': 'error', "status": "error",
'message': f'An error occurred formatting keys\n\n{error}' "message": f"An error occurred formatting keys\n\n{error}",
} }
try: try:
wv_cdm.close(wv_session_id) wv_cdm.close(wv_session_id)
except Exception as error: except Exception as error:
return { return {
'status': 'error', "status": "error",
'message': f'An error occurred closing session\n\n{error}' "message": f"An error occurred closing session\n\n{error}",
} }
try: try:
return { return {"status": "success", "message": returned_keys}
'status': 'success',
'message': returned_keys
}
except Exception as error: except Exception as error:
return { return {
'status': 'error', "status": "error",
'message': f'An error occurred getting returned_keys\n\n{error}' "message": f"An error occurred getting returned_keys\n\n{error}",
} }

View File

@ -3,66 +3,86 @@ import yaml
import requests import requests
def check_for_wvd_cdm(): def check_for_wvd_cdm():
with open(f'{os.getcwd()}/configs/config.yaml', 'r') as file: with open(f"{os.getcwd()}/configs/config.yaml", "r") as file:
config = yaml.safe_load(file) config = yaml.safe_load(file)
if config['default_wv_cdm'] == '': if config["default_wv_cdm"] == "":
answer = ' ' answer = " "
while answer[0].upper() != 'Y' and answer[0].upper() != 'N': while answer[0].upper() != "Y" and answer[0].upper() != "N":
answer = input('No default Widevine CDM specified, would you like to download one from The CDM Project? (Y)es/(N)o: ') answer = input(
if answer[0].upper() == 'Y': "No default Widevine CDM specified, would you like to download one from The CDM Project? (Y)es/(N)o: "
response = requests.get(url='https://cdm-project.com/CDRM-Team/CDMs/raw/branch/main/Widevine/L3/public.wvd') )
if answer[0].upper() == "Y":
response = requests.get(
url="https://cdm-project.com/CDRM-Team/CDMs/raw/branch/main/Widevine/L3/public.wvd"
)
if response.status_code == 200: if response.status_code == 200:
with open(f'{os.getcwd()}/configs/CDMs/WV/public.wvd', 'wb') as file: with open(f"{os.getcwd()}/configs/CDMs/WV/public.wvd", "wb") as file:
file.write(response.content) file.write(response.content)
config['default_wv_cdm'] = 'public' config["default_wv_cdm"] = "public"
with open(f'{os.getcwd()}/configs/config.yaml', 'w') as file: with open(f"{os.getcwd()}/configs/config.yaml", "w") as file:
yaml.dump(config, file) yaml.dump(config, file)
print("Successfully downloaded Widevine CDM") print("Successfully downloaded Widevine CDM")
else: else:
exit(f"Download failed, please try again or place a .wvd file in {os.getcwd()}/configs/CDMs/WV and specify the name in {os.getcwd()}/configs/config.yaml") exit(
if answer[0].upper() == 'N': f"Download failed, please try again or place a .wvd file in {os.getcwd()}/configs/CDMs/WV and specify the name in {os.getcwd()}/configs/config.yaml"
exit(f"Place a .wvd file in {os.getcwd()}/configs/CDMs/WV and specify the name in {os.getcwd()}/configs/config.yaml") )
if answer[0].upper() == "N":
exit(
f"Place a .wvd file in {os.getcwd()}/configs/CDMs/WV and specify the name in {os.getcwd()}/configs/config.yaml"
)
else: else:
base_name = config["default_wv_cdm"] base_name = config["default_wv_cdm"]
if not base_name.endswith(".wvd"): if not base_name.endswith(".wvd"):
base_name += ".wvd" base_name += ".wvd"
if os.path.exists(f'{os.getcwd()}/configs/CDMs/WV/{base_name}'): if os.path.exists(f"{os.getcwd()}/configs/CDMs/WV/{base_name}"):
return return
else: else:
exit(f"Widevine CDM {base_name} does not exist in {os.getcwd()}/configs/CDMs/WV") exit(
f"Widevine CDM {base_name} does not exist in {os.getcwd()}/configs/CDMs/WV"
)
def check_for_prd_cdm(): def check_for_prd_cdm():
with open(f'{os.getcwd()}/configs/config.yaml', 'r') as file: with open(f"{os.getcwd()}/configs/config.yaml", "r") as file:
config = yaml.safe_load(file) config = yaml.safe_load(file)
if config['default_pr_cdm'] == '': if config["default_pr_cdm"] == "":
answer = ' ' answer = " "
while answer[0].upper() != 'Y' and answer[0].upper() != 'N': while answer[0].upper() != "Y" and answer[0].upper() != "N":
answer = input('No default PlayReady CDM specified, would you like to download one from The CDM Project? (Y)es/(N)o: ') answer = input(
if answer[0].upper() == 'Y': "No default PlayReady CDM specified, would you like to download one from The CDM Project? (Y)es/(N)o: "
response = requests.get(url='https://cdm-project.com/CDRM-Team/CDMs/raw/branch/main/Playready/SL2000/public.prd') )
if answer[0].upper() == "Y":
response = requests.get(
url="https://cdm-project.com/CDRM-Team/CDMs/raw/branch/main/Playready/SL2000/public.prd"
)
if response.status_code == 200: if response.status_code == 200:
with open(f'{os.getcwd()}/configs/CDMs/PR/public.prd', 'wb') as file: with open(f"{os.getcwd()}/configs/CDMs/PR/public.prd", "wb") as file:
file.write(response.content) file.write(response.content)
config['default_pr_cdm'] = 'public' config["default_pr_cdm"] = "public"
with open(f'{os.getcwd()}/configs/config.yaml', 'w') as file: with open(f"{os.getcwd()}/configs/config.yaml", "w") as file:
yaml.dump(config, file) yaml.dump(config, file)
print("Successfully downloaded PlayReady CDM") print("Successfully downloaded PlayReady CDM")
else: else:
exit(f"Download failed, please try again or place a .prd file in {os.getcwd()}/configs/CDMs/PR and specify the name in {os.getcwd()}/configs/config.yaml") exit(
if answer[0].upper() == 'N': f"Download failed, please try again or place a .prd file in {os.getcwd()}/configs/CDMs/PR and specify the name in {os.getcwd()}/configs/config.yaml"
exit(f"Place a .prd file in {os.getcwd()}/configs/CDMs/PR and specify the name in {os.getcwd()}/configs/config.yaml") )
if answer[0].upper() == "N":
exit(
f"Place a .prd file in {os.getcwd()}/configs/CDMs/PR and specify the name in {os.getcwd()}/configs/config.yaml"
)
else: else:
base_name = config["default_pr_cdm"] base_name = config["default_pr_cdm"]
if not base_name.endswith(".prd"): if not base_name.endswith(".prd"):
base_name += ".prd" base_name += ".prd"
if os.path.exists(f'{os.getcwd()}/configs/CDMs/PR/{base_name}'): if os.path.exists(f"{os.getcwd()}/configs/CDMs/PR/{base_name}"):
return return
else: else:
exit(f"PlayReady CDM {base_name} does not exist in {os.getcwd()}/configs/CDMs/WV") exit(
f"PlayReady CDM {base_name} does not exist in {os.getcwd()}/configs/CDMs/WV"
)
def check_for_cdms(): def check_for_cdms():
check_for_wvd_cdm() check_for_wvd_cdm()
check_for_prd_cdm() check_for_prd_cdm()

View File

@ -1,7 +1,8 @@
import os import os
def check_for_config_file(): def check_for_config_file():
if os.path.exists(f'{os.getcwd()}/configs/config.yaml'): if os.path.exists(f"{os.getcwd()}/configs/config.yaml"):
return return
else: else:
default_config = """\ default_config = """\
@ -21,6 +22,6 @@ remote_cdm_secret: ''
# port: '' # port: ''
# database: '' # database: ''
""" """
with open(f'{os.getcwd()}/configs/config.yaml', 'w') as f: with open(f"{os.getcwd()}/configs/config.yaml", "w") as f:
f.write(default_config) f.write(default_config)
return return

View File

@ -1,37 +1,44 @@
import os import os
import yaml import yaml
def check_for_sqlite_database(): def check_for_sqlite_database():
with open(f'{os.getcwd()}/configs/config.yaml', 'r') as file: with open(f"{os.getcwd()}/configs/config.yaml", "r") as file:
config = yaml.safe_load(file) config = yaml.safe_load(file)
if os.path.exists(f'{os.getcwd()}/databases/key_cache.db'): if os.path.exists(f"{os.getcwd()}/databases/key_cache.db"):
return return
else: else:
if config['database_type'].lower() != 'mariadb': if config["database_type"].lower() != "mariadb":
from custom_functions.database.cache_to_db_sqlite import create_database from custom_functions.database.cache_to_db_sqlite import create_database
create_database() create_database()
return return
else: else:
return return
def check_for_user_database(): def check_for_user_database():
if os.path.exists(f'{os.getcwd()}/databases/users.db'): if os.path.exists(f"{os.getcwd()}/databases/users.db"):
return return
else: else:
from custom_functions.database.user_db import create_user_database from custom_functions.database.user_db import create_user_database
create_user_database() create_user_database()
def check_for_mariadb_database(): def check_for_mariadb_database():
with open(f'{os.getcwd()}/configs/config.yaml', 'r') as file: with open(f"{os.getcwd()}/configs/config.yaml", "r") as file:
config = yaml.safe_load(file) config = yaml.safe_load(file)
if config['database_type'].lower() == 'mariadb': if config["database_type"].lower() == "mariadb":
from custom_functions.database.cache_to_db_mariadb import create_database from custom_functions.database.cache_to_db_mariadb import create_database
create_database() create_database()
return return
else: else:
return return
def check_for_sql_database(): def check_for_sql_database():
check_for_sqlite_database() check_for_sqlite_database()
check_for_mariadb_database() check_for_mariadb_database()
check_for_user_database() check_for_user_database()

View File

@ -1,44 +1,50 @@
import os import os
def check_for_config_folder(): def check_for_config_folder():
if os.path.isdir(f'{os.getcwd()}/configs'): if os.path.isdir(f"{os.getcwd()}/configs"):
return return
else: else:
os.mkdir(f'{os.getcwd()}/configs') os.mkdir(f"{os.getcwd()}/configs")
return return
def check_for_database_folder(): def check_for_database_folder():
if os.path.isdir(f'{os.getcwd()}/databases'): if os.path.isdir(f"{os.getcwd()}/databases"):
return return
else: else:
os.mkdir(f'{os.getcwd()}/databases') os.mkdir(f"{os.getcwd()}/databases")
os.mkdir(f'{os.getcwd()}/databases/sql') os.mkdir(f"{os.getcwd()}/databases/sql")
return return
def check_for_cdm_folder(): def check_for_cdm_folder():
if os.path.isdir(f'{os.getcwd()}/configs/CDMs'): if os.path.isdir(f"{os.getcwd()}/configs/CDMs"):
return return
else: else:
os.mkdir(f'{os.getcwd()}/configs/CDMs') os.mkdir(f"{os.getcwd()}/configs/CDMs")
return return
def check_for_wv_cdm_folder(): def check_for_wv_cdm_folder():
if os.path.isdir(f'{os.getcwd()}/configs/CDMs/WV'): if os.path.isdir(f"{os.getcwd()}/configs/CDMs/WV"):
return return
else: else:
os.mkdir(f'{os.getcwd()}/configs/CDMs/WV') os.mkdir(f"{os.getcwd()}/configs/CDMs/WV")
return return
def check_for_cdm_pr_folder(): def check_for_cdm_pr_folder():
if os.path.isdir(f'{os.getcwd()}/configs/CDMs/PR'): if os.path.isdir(f"{os.getcwd()}/configs/CDMs/PR"):
return return
else: else:
os.mkdir(f'{os.getcwd()}/configs/CDMs/PR') os.mkdir(f"{os.getcwd()}/configs/CDMs/PR")
return return
def folder_checks(): def folder_checks():
check_for_config_folder() check_for_config_folder()
check_for_database_folder() check_for_database_folder()
check_for_cdm_folder() check_for_cdm_folder()
check_for_wv_cdm_folder() check_for_wv_cdm_folder()
check_for_cdm_pr_folder() check_for_cdm_pr_folder()

View File

@ -3,9 +3,10 @@ from custom_functions.prechecks.config_file_checks import check_for_config_file
from custom_functions.prechecks.database_checks import check_for_sql_database from custom_functions.prechecks.database_checks import check_for_sql_database
from custom_functions.prechecks.cdm_checks import check_for_cdms from custom_functions.prechecks.cdm_checks import check_for_cdms
def run_precheck(): def run_precheck():
folder_checks() folder_checks()
check_for_config_file() check_for_config_file()
check_for_cdms() check_for_cdms()
check_for_sql_database() check_for_sql_database()
return return

View File

@ -3,6 +3,7 @@ import os
import subprocess import subprocess
import venv import venv
def version_check(): def version_check():
major_version = sys.version_info.major major_version = sys.version_info.major
minor_version = sys.version_info.minor minor_version = sys.version_info.minor
@ -15,20 +16,29 @@ def version_check():
else: else:
exit("Python 2 detected, Python version 3.12 or higher is required") exit("Python 2 detected, Python version 3.12 or higher is required")
def pip_check(): def pip_check():
try: try:
import pip import pip
return return
except ImportError: except ImportError:
exit("Pip is not installed") exit("Pip is not installed")
def venv_check(): def venv_check():
# Check if we're already inside a virtual environment # Check if we're already inside a virtual environment
if hasattr(sys, 'real_prefix') or (hasattr(sys, 'base_prefix') and sys.base_prefix != sys.prefix): if hasattr(sys, "real_prefix") or (
hasattr(sys, "base_prefix") and sys.base_prefix != sys.prefix
):
return return
venv_path = os.path.join(os.getcwd(), 'cdrm-venv') venv_path = os.path.join(os.getcwd(), "cdrm-venv")
venv_python = os.path.join(venv_path, 'bin', 'python') if os.name != 'nt' else os.path.join(venv_path, 'Scripts', 'python.exe') venv_python = (
os.path.join(venv_path, "bin", "python")
if os.name != "nt"
else os.path.join(venv_path, "Scripts", "python.exe")
)
# If venv already exists, restart script using its Python # If venv already exists, restart script using its Python
if os.path.exists(venv_path): if os.path.exists(venv_path):
@ -36,14 +46,14 @@ def venv_check():
sys.exit() sys.exit()
# Ask user for permission to create a virtual environment # Ask user for permission to create a virtual environment
answer = '' answer = ""
while not answer or answer[0].upper() not in {'Y', 'N'}: while not answer or answer[0].upper() not in {"Y", "N"}:
answer = input( answer = input(
'Program is not running from a venv. To maintain compatibility and dependencies, this program must be run from one.\n' "Program is not running from a venv. To maintain compatibility and dependencies, this program must be run from one.\n"
'Would you like me to create one for you? (Y/N): ' "Would you like me to create one for you? (Y/N): "
) )
if answer[0].upper() == 'Y': if answer[0].upper() == "Y":
print("Creating virtual environment...") print("Creating virtual environment...")
venv.create(venv_path, with_pip=True) venv.create(venv_path, with_pip=True)
subprocess.call([venv_python] + sys.argv) subprocess.call([venv_python] + sys.argv)
@ -61,25 +71,33 @@ def requirements_check():
import flask_cors import flask_cors
import yaml import yaml
import mysql.connector import mysql.connector
return return
except ImportError: except ImportError:
while True: while True:
user_input = input("Missing packages. Do you want to install them? (Y/N): ").strip().upper() user_input = (
if user_input == 'Y': input("Missing packages. Do you want to install them? (Y/N): ")
.strip()
.upper()
)
if user_input == "Y":
print("Installing packages from requirements.txt...") print("Installing packages from requirements.txt...")
subprocess.check_call([sys.executable, "-m", "pip", "install", "-r", "requirements.txt"]) subprocess.check_call(
[sys.executable, "-m", "pip", "install", "-r", "requirements.txt"]
)
print("Installation complete.") print("Installation complete.")
break break
elif user_input == 'N': elif user_input == "N":
print("Dependencies required, please install them and run again.") print("Dependencies required, please install them and run again.")
sys.exit() sys.exit()
else: else:
print("Invalid input. Please enter 'Y' to install or 'N' to exit.") print("Invalid input. Please enter 'Y' to install or 'N' to exit.")
def run_python_checks(): def run_python_checks():
if getattr(sys, 'frozen', False): # Check if running from PyInstaller if getattr(sys, "frozen", False): # Check if running from PyInstaller
return return
version_check() version_check()
pip_check() pip_check()
venv_check() venv_check()
requirements_check() requirements_check()

View File

@ -1,12 +1,17 @@
import os import os
import glob import glob
def user_allowed_to_use_device(device, username): def user_allowed_to_use_device(device, username):
base_path = os.path.join(os.getcwd(), 'configs', 'CDMs', username) base_path = os.path.join(os.getcwd(), "configs", "CDMs", username)
# Get filenames with extensions # Get filenames with extensions
pr_files = [os.path.basename(f) for f in glob.glob(os.path.join(base_path, 'PR', '*.prd'))] pr_files = [
wv_files = [os.path.basename(f) for f in glob.glob(os.path.join(base_path, 'WV', '*.wvd'))] os.path.basename(f) for f in glob.glob(os.path.join(base_path, "PR", "*.prd"))
]
wv_files = [
os.path.basename(f) for f in glob.glob(os.path.join(base_path, "WV", "*.wvd"))
]
# Combine all filenames # Combine all filenames
all_files = pr_files + wv_files all_files = pr_files + wv_files
@ -14,4 +19,4 @@ def user_allowed_to_use_device(device, username):
# Check if filename matches directly or by adding extensions # Check if filename matches directly or by adding extensions
possible_names = {device, f"{device}.prd", f"{device}.wvd"} possible_names = {device, f"{device}.prd", f"{device}.wvd"}
return any(name in all_files for name in possible_names) return any(name in all_files for name in possible_names)

11
main.py
View File

@ -1,6 +1,8 @@
from custom_functions.prechecks.python_checks import run_python_checks from custom_functions.prechecks.python_checks import run_python_checks
run_python_checks() run_python_checks()
from custom_functions.prechecks.precheck import run_precheck from custom_functions.prechecks.precheck import run_precheck
run_precheck() run_precheck()
from flask import Flask from flask import Flask
from flask_cors import CORS from flask_cors import CORS
@ -15,10 +17,11 @@ from routes.login import login_bp
from routes.user_changes import user_change_bp from routes.user_changes import user_change_bp
import os import os
import yaml import yaml
app = Flask(__name__) app = Flask(__name__)
with open(f'{os.getcwd()}/configs/config.yaml', 'r') as file: with open(f"{os.getcwd()}/configs/config.yaml", "r") as file:
config = yaml.safe_load(file) config = yaml.safe_load(file)
app.secret_key = config['secret_key_flask'] app.secret_key = config["secret_key_flask"]
CORS(app) CORS(app)
@ -33,5 +36,5 @@ app.register_blueprint(remotecdm_wv_bp)
app.register_blueprint(remotecdm_pr_bp) app.register_blueprint(remotecdm_pr_bp)
app.register_blueprint(user_change_bp) app.register_blueprint(user_change_bp)
if __name__ == '__main__': if __name__ == "__main__":
app.run(debug=True, host='0.0.0.0') app.run(debug=True, host="0.0.0.0")

19
pyproject.toml Normal file
View File

@ -0,0 +1,19 @@
[tool.black]
line-length = 88
target-version = ['py38']
include = '\.pyi?$'
exclude = '''
/(
\.eggs
| \.git
| \.hg
| \.mypy_cache
| \.tox
| \.venv
| _build
| buck-out
| build
| dist
| cdrm-frontend
)/
'''

View File

@ -7,3 +7,4 @@ protobuf~=4.25.6
PyYAML PyYAML
mysql-connector-python mysql-connector-python
bcrypt bcrypt
black

View File

@ -13,101 +13,126 @@ import tempfile
import time import time
from configs.icon_links import data as icon_data from configs.icon_links import data as icon_data
api_bp = Blueprint('api', __name__) api_bp = Blueprint("api", __name__)
with open(f'{os.getcwd()}/configs/config.yaml', 'r') as file: with open(f"{os.getcwd()}/configs/config.yaml", "r") as file:
config = yaml.safe_load(file) config = yaml.safe_load(file)
if config['database_type'].lower() != 'mariadb': if config["database_type"].lower() != "mariadb":
from custom_functions.database.cache_to_db_sqlite import search_by_pssh_or_kid, cache_to_db, \ from custom_functions.database.cache_to_db_sqlite import (
get_key_by_kid_and_service, get_unique_services, get_kid_key_dict, key_count search_by_pssh_or_kid,
elif config['database_type'].lower() == 'mariadb': cache_to_db,
from custom_functions.database.cache_to_db_mariadb import search_by_pssh_or_kid, cache_to_db, \ get_key_by_kid_and_service,
get_key_by_kid_and_service, get_unique_services, get_kid_key_dict, key_count get_unique_services,
get_kid_key_dict,
key_count,
)
elif config["database_type"].lower() == "mariadb":
from custom_functions.database.cache_to_db_mariadb import (
search_by_pssh_or_kid,
cache_to_db,
get_key_by_kid_and_service,
get_unique_services,
get_kid_key_dict,
key_count,
)
def get_db_config(): def get_db_config():
# Configure your MariaDB connection # Configure your MariaDB connection
with open(f'{os.getcwd()}/configs/config.yaml', 'r') as file: with open(f"{os.getcwd()}/configs/config.yaml", "r") as file:
config = yaml.safe_load(file) config = yaml.safe_load(file)
db_config = { db_config = {
'host': f'{config["mariadb"]["host"]}', "host": f'{config["mariadb"]["host"]}',
'user': f'{config["mariadb"]["user"]}', "user": f'{config["mariadb"]["user"]}',
'password': f'{config["mariadb"]["password"]}', "password": f'{config["mariadb"]["password"]}',
'database': f'{config["mariadb"]["database"]}' "database": f'{config["mariadb"]["database"]}',
} }
return db_config return db_config
@api_bp.route('/api/cache/search', methods=['POST'])
@api_bp.route("/api/cache/search", methods=["POST"])
def get_data(): def get_data():
search_argument = json.loads(request.data)['input'] search_argument = json.loads(request.data)["input"]
results = search_by_pssh_or_kid(search_filter=search_argument) results = search_by_pssh_or_kid(search_filter=search_argument)
return jsonify(results) return jsonify(results)
@api_bp.route('/api/cache/<service>/<kid>', methods=['GET'])
@api_bp.route("/api/cache/<service>/<kid>", methods=["GET"])
def get_single_key_service(service, kid): def get_single_key_service(service, kid):
result = get_key_by_kid_and_service(kid=kid, service=service) result = get_key_by_kid_and_service(kid=kid, service=service)
return jsonify({ return jsonify(
'code': 0, {
'content_key': result, "code": 0,
}) "content_key": result,
}
)
@api_bp.route('/api/cache/<service>', methods=['GET'])
@api_bp.route("/api/cache/<service>", methods=["GET"])
def get_multiple_key_service(service): def get_multiple_key_service(service):
result = get_kid_key_dict(service_name=service) result = get_kid_key_dict(service_name=service)
pages = math.ceil(len(result) / 10) pages = math.ceil(len(result) / 10)
return jsonify({ return jsonify({"code": 0, "content_keys": result, "pages": pages})
'code': 0,
'content_keys': result,
'pages': pages
})
@api_bp.route('/api/cache/<service>/<kid>', methods=['POST'])
@api_bp.route("/api/cache/<service>/<kid>", methods=["POST"])
def add_single_key_service(service, kid): def add_single_key_service(service, kid):
body = request.get_json() body = request.get_json()
content_key = body['content_key'] content_key = body["content_key"]
result = cache_to_db(service=service, kid=kid, key=content_key) result = cache_to_db(service=service, kid=kid, key=content_key)
if result: if result:
return jsonify({ return jsonify(
'code': 0, {
'updated': True, "code": 0,
}) "updated": True,
}
)
elif result is False: elif result is False:
return jsonify({ return jsonify(
'code': 0, {
'updated': True, "code": 0,
}) "updated": True,
}
)
@api_bp.route('/api/cache/<service>', methods=['POST'])
@api_bp.route("/api/cache/<service>", methods=["POST"])
def add_multiple_key_service(service): def add_multiple_key_service(service):
body = request.get_json() body = request.get_json()
keys_added = 0 keys_added = 0
keys_updated = 0 keys_updated = 0
for kid, key in body['content_keys'].items(): for kid, key in body["content_keys"].items():
result = cache_to_db(service=service, kid=kid, key=key) result = cache_to_db(service=service, kid=kid, key=key)
if result is True: if result is True:
keys_updated += 1 keys_updated += 1
elif result is False: elif result is False:
keys_added += 1 keys_added += 1
return jsonify({ return jsonify(
'code': 0, {
'added': str(keys_added), "code": 0,
'updated': str(keys_updated), "added": str(keys_added),
}) "updated": str(keys_updated),
}
)
@api_bp.route('/api/cache', methods=['POST'])
@api_bp.route("/api/cache", methods=["POST"])
def unique_service(): def unique_service():
services = get_unique_services() services = get_unique_services()
return jsonify({ return jsonify(
'code': 0, {
'service_list': services, "code": 0,
}) "service_list": services,
}
)
@api_bp.route('/api/cache/download', methods=['GET']) @api_bp.route("/api/cache/download", methods=["GET"])
def download_database(): def download_database():
if config['database_type'].lower() != 'mariadb': if config["database_type"].lower() != "mariadb":
original_database_path = f'{os.getcwd()}/databases/sql/key_cache.db' original_database_path = f"{os.getcwd()}/databases/sql/key_cache.db"
# Make a copy of the original database (without locking the original) # Make a copy of the original database (without locking the original)
modified_database_path = f'{os.getcwd()}/databases/sql/key_cache_modified.db' modified_database_path = f"{os.getcwd()}/databases/sql/key_cache_modified.db"
# Using shutil.copy2 to preserve metadata (timestamps, etc.) # Using shutil.copy2 to preserve metadata (timestamps, etc.)
shutil.copy2(original_database_path, modified_database_path) shutil.copy2(original_database_path, modified_database_path)
@ -117,34 +142,40 @@ def download_database():
cursor = conn.cursor() cursor = conn.cursor()
# Update all rows to remove Headers and Cookies (set them to NULL or empty strings) # Update all rows to remove Headers and Cookies (set them to NULL or empty strings)
cursor.execute(''' cursor.execute(
"""
UPDATE licenses UPDATE licenses
SET Headers = NULL, SET Headers = NULL,
Cookies = NULL Cookies = NULL
''') """
)
# No need for explicit commit, it's done automatically with the 'with' block # No need for explicit commit, it's done automatically with the 'with' block
# The connection will automatically be committed and closed when the block ends # The connection will automatically be committed and closed when the block ends
# Send the modified database as an attachment # Send the modified database as an attachment
return send_file(modified_database_path, as_attachment=True, download_name='key_cache.db') return send_file(
if config['database_type'].lower() == 'mariadb': modified_database_path, as_attachment=True, download_name="key_cache.db"
)
if config["database_type"].lower() == "mariadb":
try: try:
# Connect to MariaDB # Connect to MariaDB
conn = mysql.connector.connect(**get_db_config()) conn = mysql.connector.connect(**get_db_config())
cursor = conn.cursor() cursor = conn.cursor()
# Update sensitive data (this updates the live DB, you may want to duplicate rows instead) # Update sensitive data (this updates the live DB, you may want to duplicate rows instead)
cursor.execute(''' cursor.execute(
"""
UPDATE licenses UPDATE licenses
SET Headers = NULL, SET Headers = NULL,
Cookies = NULL Cookies = NULL
''') """
)
conn.commit() conn.commit()
# Now export the table # Now export the table
cursor.execute('SELECT * FROM licenses') cursor.execute("SELECT * FROM licenses")
rows = cursor.fetchall() rows = cursor.fetchall()
column_names = [desc[0] for desc in cursor.description] column_names = [desc[0] for desc in cursor.description]
@ -152,116 +183,135 @@ def download_database():
output = StringIO() output = StringIO()
output.write(f"-- Dump of `licenses` table\n") output.write(f"-- Dump of `licenses` table\n")
for row in rows: for row in rows:
values = ', '.join(f"'{str(v).replace('\'', '\\\'')}'" if v is not None else 'NULL' for v in row) values = ", ".join(
output.write(f"INSERT INTO licenses ({', '.join(column_names)}) VALUES ({values});\n") f"'{str(v).replace('\'', '\\\'')}'" if v is not None else "NULL"
for v in row
)
output.write(
f"INSERT INTO licenses ({', '.join(column_names)}) VALUES ({values});\n"
)
# Write to a temp file for download # Write to a temp file for download
temp_dir = tempfile.gettempdir() temp_dir = tempfile.gettempdir()
temp_path = os.path.join(temp_dir, 'key_cache.sql') temp_path = os.path.join(temp_dir, "key_cache.sql")
with open(temp_path, 'w', encoding='utf-8') as f: with open(temp_path, "w", encoding="utf-8") as f:
f.write(output.getvalue()) f.write(output.getvalue())
return send_file(temp_path, as_attachment=True, download_name='licenses_dump.sql') return send_file(
temp_path, as_attachment=True, download_name="licenses_dump.sql"
)
except mysql.connector.Error as err: except mysql.connector.Error as err:
return {"error": str(err)}, 500 return {"error": str(err)}, 500
_keycount_cache = {
'count': None,
'timestamp': 0
}
@api_bp.route('/api/cache/keycount', methods=['GET']) _keycount_cache = {"count": None, "timestamp": 0}
@api_bp.route("/api/cache/keycount", methods=["GET"])
def get_count(): def get_count():
now = time.time() now = time.time()
if now - _keycount_cache['timestamp'] > 10 or _keycount_cache['count'] is None: if now - _keycount_cache["timestamp"] > 10 or _keycount_cache["count"] is None:
_keycount_cache['count'] = key_count() _keycount_cache["count"] = key_count()
_keycount_cache['timestamp'] = now _keycount_cache["timestamp"] = now
return jsonify({ return jsonify({"count": _keycount_cache["count"]})
'count': _keycount_cache['count']
})
@api_bp.route('/api/decrypt', methods=['POST'])
@api_bp.route("/api/decrypt", methods=["POST"])
def decrypt_data(): def decrypt_data():
api_request_data = json.loads(request.data) api_request_data = json.loads(request.data)
if 'pssh' in api_request_data: if "pssh" in api_request_data:
if api_request_data['pssh'] == '': if api_request_data["pssh"] == "":
api_request_pssh = None api_request_pssh = None
else: else:
api_request_pssh = api_request_data['pssh'] api_request_pssh = api_request_data["pssh"]
else: else:
api_request_pssh = None api_request_pssh = None
if 'licurl' in api_request_data: if "licurl" in api_request_data:
if api_request_data['licurl'] == '': if api_request_data["licurl"] == "":
api_request_licurl = None api_request_licurl = None
else: else:
api_request_licurl = api_request_data['licurl'] api_request_licurl = api_request_data["licurl"]
else: else:
api_request_licurl = None api_request_licurl = None
if 'proxy' in api_request_data: if "proxy" in api_request_data:
if api_request_data['proxy'] == '': if api_request_data["proxy"] == "":
api_request_proxy = None api_request_proxy = None
else: else:
api_request_proxy = api_request_data['proxy'] api_request_proxy = api_request_data["proxy"]
else: else:
api_request_proxy = None api_request_proxy = None
if 'headers' in api_request_data: if "headers" in api_request_data:
if api_request_data['headers'] == '': if api_request_data["headers"] == "":
api_request_headers = None api_request_headers = None
else: else:
api_request_headers = api_request_data['headers'] api_request_headers = api_request_data["headers"]
else: else:
api_request_headers = None api_request_headers = None
if 'cookies' in api_request_data: if "cookies" in api_request_data:
if api_request_data['cookies'] == '': if api_request_data["cookies"] == "":
api_request_cookies = None api_request_cookies = None
else: else:
api_request_cookies = api_request_data['cookies'] api_request_cookies = api_request_data["cookies"]
else: else:
api_request_cookies = None api_request_cookies = None
if 'data' in api_request_data: if "data" in api_request_data:
if api_request_data['data'] == '': if api_request_data["data"] == "":
api_request_data_func = None api_request_data_func = None
else: else:
api_request_data_func = api_request_data['data'] api_request_data_func = api_request_data["data"]
else: api_request_data_func = None
if 'device' in api_request_data:
if api_request_data['device'] == 'default' or api_request_data['device'] == 'CDRM-Project Public Widevine CDM' or api_request_data['device'] == 'CDRM-Project Public PlayReady CDM':
api_request_device = 'public'
else:
api_request_device = api_request_data['device']
else: else:
api_request_device = 'public' api_request_data_func = None
if "device" in api_request_data:
if (
api_request_data["device"] == "default"
or api_request_data["device"] == "CDRM-Project Public Widevine CDM"
or api_request_data["device"] == "CDRM-Project Public PlayReady CDM"
):
api_request_device = "public"
else:
api_request_device = api_request_data["device"]
else:
api_request_device = "public"
username = None username = None
if api_request_device != 'public': if api_request_device != "public":
username = session.get('username') username = session.get("username")
if not username: if not username:
return jsonify({'message': 'Not logged in, not allowed'}), 400 return jsonify({"message": "Not logged in, not allowed"}), 400
if user_allowed_to_use_device(device=api_request_device, username=username): if user_allowed_to_use_device(device=api_request_device, username=username):
api_request_device = api_request_device api_request_device = api_request_device
else: else:
return jsonify({'message': f'Not authorized / Not found'}), 403 return jsonify({"message": f"Not authorized / Not found"}), 403
result = api_decrypt(pssh=api_request_pssh, proxy=api_request_proxy, license_url=api_request_licurl, headers=api_request_headers, cookies=api_request_cookies, json_data=api_request_data_func, device=api_request_device, username=username) result = api_decrypt(
if result['status'] == 'success': pssh=api_request_pssh,
return jsonify({ proxy=api_request_proxy,
'status': 'success', license_url=api_request_licurl,
'message': result['message'] headers=api_request_headers,
}) cookies=api_request_cookies,
json_data=api_request_data_func,
device=api_request_device,
username=username,
)
if result["status"] == "success":
return jsonify({"status": "success", "message": result["message"]})
else: else:
return jsonify({ return jsonify({"status": "fail", "message": result["message"]})
'status': 'fail',
'message': result['message']
})
@api_bp.route('/api/links', methods=['GET'])
@api_bp.route("/api/links", methods=["GET"])
def get_links(): def get_links():
return jsonify({ return jsonify(
'discord': icon_data['discord'], {
'telegram': icon_data['telegram'], "discord": icon_data["discord"],
'gitea': icon_data['gitea'], "telegram": icon_data["telegram"],
}) "gitea": icon_data["gitea"],
}
)
@api_bp.route('/api/extension', methods=['POST'])
@api_bp.route("/api/extension", methods=["POST"])
def verify_extension(): def verify_extension():
return jsonify({ return jsonify(
'status': True, {
}) "status": True,
}
)

View File

@ -2,36 +2,44 @@ from flask import Blueprint, request, jsonify, session
from custom_functions.database.user_db import verify_user from custom_functions.database.user_db import verify_user
login_bp = Blueprint( login_bp = Blueprint(
'login_bp', "login_bp",
__name__, __name__,
) )
@login_bp.route('/login', methods=['POST'])
@login_bp.route("/login", methods=["POST"])
def login(): def login():
if request.method == 'POST': if request.method == "POST":
data = request.get_json() data = request.get_json()
for required_field in ['username', 'password']: for required_field in ["username", "password"]:
if required_field not in data: if required_field not in data:
return jsonify({'error': f'Missing required field: {required_field}'}), 400 return (
jsonify({"error": f"Missing required field: {required_field}"}),
400,
)
if verify_user(data['username'], data['password']): if verify_user(data["username"], data["password"]):
session['username'] = data['username'].lower() # Stored securely in a signed cookie session["username"] = data[
return jsonify({'message': 'Successfully logged in!'}) "username"
].lower() # Stored securely in a signed cookie
return jsonify({"message": "Successfully logged in!"})
else: else:
return jsonify({'error': 'Invalid username or password!'}), 401 return jsonify({"error": "Invalid username or password!"}), 401
@login_bp.route('/login/status', methods=['POST'])
@login_bp.route("/login/status", methods=["POST"])
def login_status(): def login_status():
try: try:
username = session.get('username') username = session.get("username")
if username: if username:
return jsonify({'message': 'True'}) return jsonify({"message": "True"})
else: else:
return jsonify({'message': 'False'}) return jsonify({"message": "False"})
except: except:
return jsonify({'message': 'False'}) return jsonify({"message": "False"})
@login_bp.route('/logout', methods=['POST'])
@login_bp.route("/logout", methods=["POST"])
def logout(): def logout():
session.pop('username', None) session.pop("username", None)
return jsonify({'message': 'Successfully logged out!'}) return jsonify({"message": "Successfully logged out!"})

View File

@ -3,31 +3,32 @@ import os
from flask import Blueprint, send_from_directory, request, render_template from flask import Blueprint, send_from_directory, request, render_template
from configs import index_tags from configs import index_tags
if getattr(sys, 'frozen', False): # Running as a bundled app if getattr(sys, "frozen", False): # Running as a bundled app
base_path = sys._MEIPASS base_path = sys._MEIPASS
else: # Running in a normal Python environment else: # Running in a normal Python environment
base_path = os.path.abspath(".") base_path = os.path.abspath(".")
static_folder = os.path.join(base_path, 'cdrm-frontend', 'dist') static_folder = os.path.join(base_path, "frontend-dist")
react_bp = Blueprint( react_bp = Blueprint(
'react_bp', "react_bp",
__name__, __name__,
static_folder=static_folder, static_folder=static_folder,
static_url_path='/', static_url_path="/",
template_folder=static_folder template_folder=static_folder,
) )
@react_bp.route('/', methods=['GET'])
@react_bp.route('/<path:path>', methods=["GET"]) @react_bp.route("/", methods=["GET"])
@react_bp.route('/<path>', methods=["GET"]) @react_bp.route("/<path:path>", methods=["GET"])
def index(path=''): @react_bp.route("/<path>", methods=["GET"])
if request.method == 'GET': def index(path=""):
if request.method == "GET":
file_path = os.path.join(react_bp.static_folder, path) file_path = os.path.join(react_bp.static_folder, path)
if path != "" and os.path.exists(file_path): if path != "" and os.path.exists(file_path):
return send_from_directory(react_bp.static_folder, path) return send_from_directory(react_bp.static_folder, path)
elif path.lower() in ['', 'cache', 'api', 'testplayer', 'account']: elif path.lower() in ["", "cache", "api", "testplayer", "account"]:
data = index_tags.tags.get(path.lower(), index_tags.tags['index']) data = index_tags.tags.get(path.lower(), index_tags.tags["index"])
return render_template('index.html', data=data) return render_template("index.html", data=data)
else: else:
return send_from_directory(react_bp.static_folder, 'index.html') return send_from_directory(react_bp.static_folder, "index.html")

View File

@ -3,40 +3,44 @@ from flask import Blueprint, request, jsonify
from custom_functions.database.user_db import add_user from custom_functions.database.user_db import add_user
import uuid import uuid
register_bp = Blueprint('register_bp', __name__) register_bp = Blueprint("register_bp", __name__)
USERNAME_REGEX = re.compile(r'^[A-Za-z0-9_-]+$') USERNAME_REGEX = re.compile(r"^[A-Za-z0-9_-]+$")
PASSWORD_REGEX = re.compile(r'^\S+$') PASSWORD_REGEX = re.compile(r"^\S+$")
@register_bp.route('/register', methods=['POST'])
@register_bp.route("/register", methods=["POST"])
def register(): def register():
if request.method != 'POST': if request.method != "POST":
return jsonify({'error': 'Method not supported'}), 405 return jsonify({"error": "Method not supported"}), 405
data = request.get_json() data = request.get_json()
# Check required fields # Check required fields
for required_field in ['username', 'password']: for required_field in ["username", "password"]:
if required_field not in data: if required_field not in data:
return jsonify({'error': f'Missing required field: {required_field}'}), 400 return jsonify({"error": f"Missing required field: {required_field}"}), 400
username = data['username'] username = data["username"]
password = data['password'] password = data["password"]
api_key = str(uuid.uuid4()) api_key = str(uuid.uuid4())
# Validate username and password # Validate username and password
if not USERNAME_REGEX.fullmatch(username): if not USERNAME_REGEX.fullmatch(username):
return jsonify({ return (
'error': 'Invalid username. Only letters, numbers, hyphens, and underscores are allowed.' jsonify(
}), 400 {
"error": "Invalid username. Only letters, numbers, hyphens, and underscores are allowed."
}
),
400,
)
if not PASSWORD_REGEX.fullmatch(password): if not PASSWORD_REGEX.fullmatch(password):
return jsonify({ return jsonify({"error": "Invalid password. Spaces are not allowed."}), 400
'error': 'Invalid password. Spaces are not allowed.'
}), 400
# Attempt to add user # Attempt to add user
if add_user(username, password, api_key): if add_user(username, password, api_key):
return jsonify({'message': 'User successfully registered!'}), 201 return jsonify({"message": "User successfully registered!"}), 201
else: else:
return jsonify({'error': 'User already exists!'}), 409 return jsonify({"error": "User already exists!"}), 409

View File

@ -6,136 +6,195 @@ import yaml
from pyplayready.device import Device as PlayReadyDevice from pyplayready.device import Device as PlayReadyDevice
from pyplayready.cdm import Cdm as PlayReadyCDM from pyplayready.cdm import Cdm as PlayReadyCDM
from pyplayready import PSSH as PlayReadyPSSH from pyplayready import PSSH as PlayReadyPSSH
from pyplayready.exceptions import (InvalidSession, TooManySessions, InvalidLicense, InvalidPssh) from pyplayready.exceptions import (
InvalidSession,
TooManySessions,
InvalidLicense,
InvalidPssh,
)
from custom_functions.database.user_db import fetch_username_by_api_key from custom_functions.database.user_db import fetch_username_by_api_key
from custom_functions.decrypt.api_decrypt import is_base64 from custom_functions.decrypt.api_decrypt import is_base64
from custom_functions.user_checks.device_allowed import user_allowed_to_use_device from custom_functions.user_checks.device_allowed import user_allowed_to_use_device
from pathlib import Path from pathlib import Path
remotecdm_pr_bp = Blueprint("remotecdm_pr", __name__)
with open(f"{os.getcwd()}/configs/config.yaml", "r") as file:
remotecdm_pr_bp = Blueprint('remotecdm_pr', __name__)
with open(f'{os.getcwd()}/configs/config.yaml', 'r') as file:
config = yaml.safe_load(file) config = yaml.safe_load(file)
@remotecdm_pr_bp.route('/remotecdm/playready', methods=['GET', 'HEAD'])
@remotecdm_pr_bp.route("/remotecdm/playready", methods=["GET", "HEAD"])
def remote_cdm_playready(): def remote_cdm_playready():
if request.method == 'GET': if request.method == "GET":
return jsonify({ return jsonify({"message": "OK"})
'message': 'OK' if request.method == "HEAD":
})
if request.method == 'HEAD':
response = Response(status=200) response = Response(status=200)
response.headers['Server'] = 'playready serve' response.headers["Server"] = "playready serve"
return response return response
@remotecdm_pr_bp.route('/remotecdm/playready/deviceinfo', methods=['GET']) @remotecdm_pr_bp.route("/remotecdm/playready/deviceinfo", methods=["GET"])
def remote_cdm_playready_deviceinfo(): def remote_cdm_playready_deviceinfo():
base_name = config["default_pr_cdm"] base_name = config["default_pr_cdm"]
if not base_name.endswith(".prd"): if not base_name.endswith(".prd"):
full_file_name = (base_name + ".prd") full_file_name = base_name + ".prd"
device = PlayReadyDevice.load(f'{os.getcwd()}/configs/CDMs/PR/{full_file_name}') device = PlayReadyDevice.load(f"{os.getcwd()}/configs/CDMs/PR/{full_file_name}")
cdm = PlayReadyCDM.from_device(device) cdm = PlayReadyCDM.from_device(device)
return jsonify({ return jsonify(
'security_level': cdm.security_level, {
'host': f'{config["fqdn"]}/remotecdm/playready', "security_level": cdm.security_level,
'secret': f'{config["remote_cdm_secret"]}', "host": f'{config["fqdn"]}/remotecdm/playready',
'device_name': Path(base_name).stem "secret": f'{config["remote_cdm_secret"]}',
}) "device_name": Path(base_name).stem,
}
)
@remotecdm_pr_bp.route('/remotecdm/playready/deviceinfo/<device>', methods=['GET'])
@remotecdm_pr_bp.route("/remotecdm/playready/deviceinfo/<device>", methods=["GET"])
def remote_cdm_playready_deviceinfo_specific(device): def remote_cdm_playready_deviceinfo_specific(device):
if request.method == 'GET': if request.method == "GET":
base_name = Path(device).with_suffix('.prd').name base_name = Path(device).with_suffix(".prd").name
api_key = request.headers['X-Secret-Key'] api_key = request.headers["X-Secret-Key"]
username = fetch_username_by_api_key(api_key) username = fetch_username_by_api_key(api_key)
device = PlayReadyDevice.load(f'{os.getcwd()}/configs/CDMs/{username}/PR/{base_name}') device = PlayReadyDevice.load(
f"{os.getcwd()}/configs/CDMs/{username}/PR/{base_name}"
)
cdm = PlayReadyCDM.from_device(device) cdm = PlayReadyCDM.from_device(device)
return jsonify({ return jsonify(
'security_level': cdm.security_level, {
'host': f'{config["fqdn"]}/remotecdm/widevine', "security_level": cdm.security_level,
'secret': f'{api_key}', "host": f'{config["fqdn"]}/remotecdm/widevine',
'device_name': Path(base_name).stem "secret": f"{api_key}",
}) "device_name": Path(base_name).stem,
@remotecdm_pr_bp.route('/remotecdm/playready/<device>/open', methods=['GET'])
def remote_cdm_playready_open(device):
if str(device).lower() == config['default_pr_cdm'].lower():
pr_device = PlayReadyDevice.load(f'{os.getcwd()}/configs/CDMs/PR/{config["default_pr_cdm"]}.prd')
cdm = current_app.config['CDM'] = PlayReadyCDM.from_device(pr_device)
session_id = cdm.open()
return jsonify({
'message': 'Success',
'data': {
'session_id': session_id.hex(),
'device': {
'security_level': cdm.security_level
}
} }
}) )
if request.headers['X-Secret-Key'] and str(device).lower() != config['default_pr_cdm'].lower():
api_key = request.headers['X-Secret-Key']
@remotecdm_pr_bp.route("/remotecdm/playready/<device>/open", methods=["GET"])
def remote_cdm_playready_open(device):
if str(device).lower() == config["default_pr_cdm"].lower():
pr_device = PlayReadyDevice.load(
f'{os.getcwd()}/configs/CDMs/PR/{config["default_pr_cdm"]}.prd'
)
cdm = current_app.config["CDM"] = PlayReadyCDM.from_device(pr_device)
session_id = cdm.open()
return jsonify(
{
"message": "Success",
"data": {
"session_id": session_id.hex(),
"device": {"security_level": cdm.security_level},
},
}
)
if (
request.headers["X-Secret-Key"]
and str(device).lower() != config["default_pr_cdm"].lower()
):
api_key = request.headers["X-Secret-Key"]
user = fetch_username_by_api_key(api_key=api_key) user = fetch_username_by_api_key(api_key=api_key)
if user: if user:
if user_allowed_to_use_device(device=device, username=user): if user_allowed_to_use_device(device=device, username=user):
pr_device = PlayReadyDevice.load(f'{os.getcwd()}/configs/CDMs/{user}/PR/{device}.prd') pr_device = PlayReadyDevice.load(
cdm = current_app.config['CDM'] = PlayReadyCDM.from_device(pr_device) f"{os.getcwd()}/configs/CDMs/{user}/PR/{device}.prd"
)
cdm = current_app.config["CDM"] = PlayReadyCDM.from_device(pr_device)
session_id = cdm.open() session_id = cdm.open()
return jsonify({ return jsonify(
'message': 'Success', {
'data': { "message": "Success",
'session_id': session_id.hex(), "data": {
'device': { "session_id": session_id.hex(),
'security_level': cdm.security_level "device": {"security_level": cdm.security_level},
} },
} }
}) )
else: else:
return jsonify({ return (
'message': f"Device '{device}' is not found or you are not authorized to use it.", jsonify(
}), 403 {
"message": f"Device '{device}' is not found or you are not authorized to use it.",
}
),
403,
)
else: else:
return jsonify({ return (
'message': f"Device '{device}' is not found or you are not authorized to use it.", jsonify(
}), 403 {
"message": f"Device '{device}' is not found or you are not authorized to use it.",
}
),
403,
)
else: else:
return jsonify({ return (
'message': f"Device '{device}' is not found or you are not authorized to use it.", jsonify(
}), 403 {
"message": f"Device '{device}' is not found or you are not authorized to use it.",
}
),
403,
)
@remotecdm_pr_bp.route('/remotecdm/playready/<device>/close/<session_id>', methods=['GET'])
@remotecdm_pr_bp.route(
"/remotecdm/playready/<device>/close/<session_id>", methods=["GET"]
)
def remote_cdm_playready_close(device, session_id): def remote_cdm_playready_close(device, session_id):
try: try:
session_id = bytes.fromhex(session_id) session_id = bytes.fromhex(session_id)
cdm = current_app.config["CDM"] cdm = current_app.config["CDM"]
if not cdm: if not cdm:
return jsonify({ return (
'message': f'No CDM for "{device}" has been opened yet. No session to close' jsonify(
}), 400 {
"message": f'No CDM for "{device}" has been opened yet. No session to close'
}
),
400,
)
try: try:
cdm.close(session_id) cdm.close(session_id)
except InvalidSession: except InvalidSession:
return jsonify({ return (
'message': f'Invalid session ID "{session_id.hex()}", it may have expired' jsonify(
}), 400 {
return jsonify({ "message": f'Invalid session ID "{session_id.hex()}", it may have expired'
'message': f'Successfully closed Session "{session_id.hex()}".', }
}), 200 ),
400,
)
return (
jsonify(
{
"message": f'Successfully closed Session "{session_id.hex()}".',
}
),
200,
)
except Exception as e: except Exception as e:
return jsonify({ return (
'message': f'Failed to close Session "{session_id.hex()}".' jsonify({"message": f'Failed to close Session "{session_id.hex()}".'}),
}), 400 400,
)
@remotecdm_pr_bp.route('/remotecdm/playready/<device>/get_license_challenge', methods=['POST'])
@remotecdm_pr_bp.route(
"/remotecdm/playready/<device>/get_license_challenge", methods=["POST"]
)
def remote_cdm_playready_get_license_challenge(device): def remote_cdm_playready_get_license_challenge(device):
body = request.get_json() body = request.get_json()
for required_field in ("session_id", "init_data"): for required_field in ("session_id", "init_data"):
if not body.get(required_field): if not body.get(required_field):
return jsonify({ return (
'message': f'Missing required field "{required_field}" in JSON body' jsonify(
}), 400 {
"message": f'Missing required field "{required_field}" in JSON body'
}
),
400,
)
cdm = current_app.config["CDM"] cdm = current_app.config["CDM"]
session_id = bytes.fromhex(body["session_id"]) session_id = bytes.fromhex(body["session_id"])
init_data = body["init_data"] init_data = body["init_data"]
@ -145,42 +204,37 @@ def remote_cdm_playready_get_license_challenge(device):
if pssh.wrm_headers: if pssh.wrm_headers:
init_data = pssh.wrm_headers[0] init_data = pssh.wrm_headers[0]
except InvalidPssh as e: except InvalidPssh as e:
return jsonify({ return jsonify({"message": f"Unable to parse base64 PSSH, {e}"})
'message': f'Unable to parse base64 PSSH, {e}'
})
try: try:
license_request = cdm.get_license_challenge( license_request = cdm.get_license_challenge(
session_id=session_id, session_id=session_id, wrm_header=init_data
wrm_header=init_data
) )
except InvalidSession: except InvalidSession:
return jsonify({ return jsonify(
'message': f"Invalid Session ID '{session_id.hex()}', it may have expired." {
}) "message": f"Invalid Session ID '{session_id.hex()}', it may have expired."
}
)
except Exception as e: except Exception as e:
return jsonify({ return jsonify({"message": f"Error, {e}"})
'message': f'Error, {e}' return jsonify({"message": "success", "data": {"challenge": license_request}})
})
return jsonify({
'message': 'success',
'data': {
'challenge': license_request
}
})
@remotecdm_pr_bp.route('/remotecdm/playready/<device>/parse_license', methods=['POST'])
@remotecdm_pr_bp.route("/remotecdm/playready/<device>/parse_license", methods=["POST"])
def remote_cdm_playready_parse_license(device): def remote_cdm_playready_parse_license(device):
body = request.get_json() body = request.get_json()
for required_field in ("license_message", "session_id"): for required_field in ("license_message", "session_id"):
if not body.get(required_field): if not body.get(required_field):
return jsonify({ return jsonify(
'message': f'Missing required field "{required_field}" in JSON body' {"message": f'Missing required field "{required_field}" in JSON body'}
}) )
cdm = current_app.config["CDM"] cdm = current_app.config["CDM"]
if not cdm: if not cdm:
return jsonify({ return jsonify(
'message': f"No Cdm session for {device} has been opened yet. No session to use." {
}) "message": f"No Cdm session for {device} has been opened yet. No session to use."
}
)
session_id = bytes.fromhex(body["session_id"]) session_id = bytes.fromhex(body["session_id"])
license_message = body["license_message"] license_message = body["license_message"]
if is_base64(license_message): if is_base64(license_message):
@ -188,45 +242,44 @@ def remote_cdm_playready_parse_license(device):
try: try:
cdm.parse_license(session_id, license_message) cdm.parse_license(session_id, license_message)
except InvalidSession: except InvalidSession:
return jsonify({ return jsonify(
'message': f"Invalid Session ID '{session_id.hex()}', it may have expired." {
}) "message": f"Invalid Session ID '{session_id.hex()}', it may have expired."
}
)
except InvalidLicense as e: except InvalidLicense as e:
return jsonify({ return jsonify({"message": f"Invalid License, {e}"})
'message': f"Invalid License, {e}"
})
except Exception as e: except Exception as e:
return jsonify({ return jsonify({"message": f"Error, {e}"})
'message': f"Error, {e}" return jsonify(
}) {"message": "Successfully parsed and loaded the Keys from the License message"}
return jsonify({ )
'message': 'Successfully parsed and loaded the Keys from the License message'
})
@remotecdm_pr_bp.route('/remotecdm/playready/<device>/get_keys', methods=['POST'])
@remotecdm_pr_bp.route("/remotecdm/playready/<device>/get_keys", methods=["POST"])
def remote_cdm_playready_get_keys(device): def remote_cdm_playready_get_keys(device):
body = request.get_json() body = request.get_json()
for required_field in ("session_id",): for required_field in ("session_id",):
if not body.get(required_field): if not body.get(required_field):
return jsonify({ return jsonify(
'message': f'Missing required field "{required_field}" in JSON body' {"message": f'Missing required field "{required_field}" in JSON body'}
}) )
session_id = bytes.fromhex(body["session_id"]) session_id = bytes.fromhex(body["session_id"])
cdm = current_app.config["CDM"] cdm = current_app.config["CDM"]
if not cdm: if not cdm:
return jsonify({ return jsonify(
'message': f"Missing required field '{required_field}' in JSON body." {"message": f"Missing required field '{required_field}' in JSON body."}
}) )
try: try:
keys = cdm.get_keys(session_id) keys = cdm.get_keys(session_id)
except InvalidSession: except InvalidSession:
return jsonify({ return jsonify(
'message': f"Invalid Session ID '{session_id.hex()}', it may have expired." {
}) "message": f"Invalid Session ID '{session_id.hex()}', it may have expired."
}
)
except Exception as e: except Exception as e:
return jsonify({ return jsonify({"message": f"Error, {e}"})
'message': f"Error, {e}"
})
keys_json = [ keys_json = [
{ {
"key_id": key.key_id.hex, "key_id": key.key_id.hex,
@ -237,9 +290,4 @@ def remote_cdm_playready_get_keys(device):
} }
for key in keys for key in keys
] ]
return jsonify({ return jsonify({"message": "success", "data": {"keys": keys_json}})
'message': 'success',
'data': {
'keys': keys_json
}
})

View File

@ -7,381 +7,575 @@ from pywidevine.pssh import PSSH as widevinePSSH
from pywidevine import __version__ from pywidevine import __version__
from pywidevine.cdm import Cdm as widevineCDM from pywidevine.cdm import Cdm as widevineCDM
from pywidevine.device import Device as widevineDevice from pywidevine.device import Device as widevineDevice
from pywidevine.exceptions import (InvalidContext, InvalidInitData, InvalidLicenseMessage, InvalidLicenseType, from pywidevine.exceptions import (
InvalidSession, SignatureMismatch, TooManySessions) InvalidContext,
InvalidInitData,
InvalidLicenseMessage,
InvalidLicenseType,
InvalidSession,
SignatureMismatch,
TooManySessions,
)
import yaml import yaml
from custom_functions.database.user_db import fetch_api_key, fetch_username_by_api_key from custom_functions.database.user_db import fetch_api_key, fetch_username_by_api_key
from custom_functions.user_checks.device_allowed import user_allowed_to_use_device from custom_functions.user_checks.device_allowed import user_allowed_to_use_device
from pathlib import Path from pathlib import Path
remotecdm_wv_bp = Blueprint('remotecdm_wv', __name__) remotecdm_wv_bp = Blueprint("remotecdm_wv", __name__)
with open(f'{os.getcwd()}/configs/config.yaml', 'r') as file: with open(f"{os.getcwd()}/configs/config.yaml", "r") as file:
config = yaml.safe_load(file) config = yaml.safe_load(file)
@remotecdm_wv_bp.route('/remotecdm/widevine', methods=['GET', 'HEAD'])
@remotecdm_wv_bp.route("/remotecdm/widevine", methods=["GET", "HEAD"])
def remote_cdm_widevine(): def remote_cdm_widevine():
if request.method == 'GET': if request.method == "GET":
return jsonify({ return jsonify(
'status': 200, {"status": 200, "message": f"{config['fqdn'].upper()} Remote Widevine CDM."}
'message': f"{config['fqdn'].upper()} Remote Widevine CDM." )
}) if request.method == "HEAD":
if request.method == 'HEAD':
response = Response(status=200) response = Response(status=200)
response.headers['Server'] = f'https://github.com/devine-dl/pywidevine serve v{__version__}' response.headers["Server"] = (
f"https://github.com/devine-dl/pywidevine serve v{__version__}"
)
return response return response
@remotecdm_wv_bp.route('/remotecdm/widevine/deviceinfo', methods=['GET'])
@remotecdm_wv_bp.route("/remotecdm/widevine/deviceinfo", methods=["GET"])
def remote_cdm_widevine_deviceinfo(): def remote_cdm_widevine_deviceinfo():
if request.method == 'GET': if request.method == "GET":
base_name = config["default_wv_cdm"] base_name = config["default_wv_cdm"]
if not base_name.endswith(".wvd"): if not base_name.endswith(".wvd"):
base_name = (base_name + ".wvd") base_name = base_name + ".wvd"
device = widevineDevice.load(f'{os.getcwd()}/configs/CDMs/WV/{base_name}') device = widevineDevice.load(f"{os.getcwd()}/configs/CDMs/WV/{base_name}")
cdm = widevineCDM.from_device(device) cdm = widevineCDM.from_device(device)
return jsonify({ return jsonify(
'device_type': cdm.device_type.name, {
'system_id': cdm.system_id, "device_type": cdm.device_type.name,
'security_level': cdm.security_level, "system_id": cdm.system_id,
'host': f'{config["fqdn"]}/remotecdm/widevine', "security_level": cdm.security_level,
'secret': f'{config["remote_cdm_secret"]}', "host": f'{config["fqdn"]}/remotecdm/widevine',
'device_name': Path(base_name).stem "secret": f'{config["remote_cdm_secret"]}',
}) "device_name": Path(base_name).stem,
}
)
@remotecdm_wv_bp.route('/remotecdm/widevine/deviceinfo/<device>', methods=['GET'])
@remotecdm_wv_bp.route("/remotecdm/widevine/deviceinfo/<device>", methods=["GET"])
def remote_cdm_widevine_deviceinfo_specific(device): def remote_cdm_widevine_deviceinfo_specific(device):
if request.method == 'GET': if request.method == "GET":
base_name = Path(device).with_suffix('.wvd').name base_name = Path(device).with_suffix(".wvd").name
api_key = request.headers['X-Secret-Key'] api_key = request.headers["X-Secret-Key"]
username = fetch_username_by_api_key(api_key) username = fetch_username_by_api_key(api_key)
device = widevineDevice.load(f'{os.getcwd()}/configs/CDMs/{username}/WV/{base_name}') device = widevineDevice.load(
f"{os.getcwd()}/configs/CDMs/{username}/WV/{base_name}"
)
cdm = widevineCDM.from_device(device) cdm = widevineCDM.from_device(device)
return jsonify({ return jsonify(
'device_type': cdm.device_type.name, {
'system_id': cdm.system_id, "device_type": cdm.device_type.name,
'security_level': cdm.security_level, "system_id": cdm.system_id,
'host': f'{config["fqdn"]}/remotecdm/widevine', "security_level": cdm.security_level,
'secret': f'{api_key}', "host": f'{config["fqdn"]}/remotecdm/widevine',
'device_name': Path(base_name).stem "secret": f"{api_key}",
}) "device_name": Path(base_name).stem,
}
)
@remotecdm_wv_bp.route('/remotecdm/widevine/<device>/open', methods=['GET'])
@remotecdm_wv_bp.route("/remotecdm/widevine/<device>/open", methods=["GET"])
def remote_cdm_widevine_open(device): def remote_cdm_widevine_open(device):
if str(device).lower() == config['default_wv_cdm'].lower(): if str(device).lower() == config["default_wv_cdm"].lower():
wv_device = widevineDevice.load(f'{os.getcwd()}/configs/CDMs/WV/{config["default_wv_cdm"]}.wvd') wv_device = widevineDevice.load(
f'{os.getcwd()}/configs/CDMs/WV/{config["default_wv_cdm"]}.wvd'
)
cdm = current_app.config["CDM"] = widevineCDM.from_device(wv_device) cdm = current_app.config["CDM"] = widevineCDM.from_device(wv_device)
session_id = cdm.open() session_id = cdm.open()
return jsonify({ return (
'status': 200, jsonify(
'message': 'Success', {
'data': { "status": 200,
'session_id': session_id.hex(), "message": "Success",
'device': { "data": {
'system_id': cdm.system_id, "session_id": session_id.hex(),
'security_level': cdm.security_level, "device": {
"system_id": cdm.system_id,
"security_level": cdm.security_level,
},
},
} }
} ),
}), 200 200,
if request.headers['X-Secret-Key'] and str(device).lower() != config['default_wv_cdm'].lower(): )
api_key = request.headers['X-Secret-Key'] if (
request.headers["X-Secret-Key"]
and str(device).lower() != config["default_wv_cdm"].lower()
):
api_key = request.headers["X-Secret-Key"]
user = fetch_username_by_api_key(api_key=api_key) user = fetch_username_by_api_key(api_key=api_key)
if user: if user:
if user_allowed_to_use_device(device=device, username=user): if user_allowed_to_use_device(device=device, username=user):
wv_device = widevineDevice.load(f'{os.getcwd()}/configs/CDMs/{user}/WV/{device}.wvd') wv_device = widevineDevice.load(
f"{os.getcwd()}/configs/CDMs/{user}/WV/{device}.wvd"
)
cdm = current_app.config["CDM"] = widevineCDM.from_device(wv_device) cdm = current_app.config["CDM"] = widevineCDM.from_device(wv_device)
session_id = cdm.open() session_id = cdm.open()
return jsonify({ return (
'status': 200, jsonify(
'message': 'Success', {
'data': { "status": 200,
'session_id': session_id.hex(), "message": "Success",
'device': { "data": {
'system_id': cdm.system_id, "session_id": session_id.hex(),
'security_level': cdm.security_level, "device": {
"system_id": cdm.system_id,
"security_level": cdm.security_level,
},
},
} }
} ),
}), 200 200,
)
else: else:
return jsonify({ return (
'message': f"Device '{device}' is not found or you are not authorized to use it.", jsonify(
'status': 403 {
}), 403 "message": f"Device '{device}' is not found or you are not authorized to use it.",
"status": 403,
}
),
403,
)
else: else:
return jsonify({ return (
'message': f"Device '{device}' is not found or you are not authorized to use it.", jsonify(
'status': 403 {
}), 403 "message": f"Device '{device}' is not found or you are not authorized to use it.",
"status": 403,
}
),
403,
)
else: else:
return jsonify({ return (
'message': f"Device '{device}' is not found or you are not authorized to use it.", jsonify(
'status': 403 {
}), 403 "message": f"Device '{device}' is not found or you are not authorized to use it.",
"status": 403,
}
),
403,
)
@remotecdm_wv_bp.route('/remotecdm/widevine/<device>/close/<session_id>', methods=['GET']) @remotecdm_wv_bp.route(
"/remotecdm/widevine/<device>/close/<session_id>", methods=["GET"]
)
def remote_cdm_widevine_close(device, session_id): def remote_cdm_widevine_close(device, session_id):
session_id = bytes.fromhex(session_id) session_id = bytes.fromhex(session_id)
cdm = current_app.config["CDM"] cdm = current_app.config["CDM"]
if not cdm: if not cdm:
return jsonify({ return (
'status': 400, jsonify(
'message': f'No CDM for "{device}" has been opened yet. No session to close' {
}), 400 "status": 400,
try: "message": f'No CDM for "{device}" has been opened yet. No session to close',
cdm.close(session_id) }
except InvalidSession: ),
return jsonify({ 400,
'status': 400, )
'message': f'Invalid session ID "{session_id.hex()}", it may have expired' try:
}), 400 cdm.close(session_id)
return jsonify({ except InvalidSession:
'status': 200, return (
'message': f'Successfully closed Session "{session_id.hex()}".', jsonify(
}), 200 {
"status": 400,
"message": f'Invalid session ID "{session_id.hex()}", it may have expired',
}
),
400,
)
return (
jsonify(
{
"status": 200,
"message": f'Successfully closed Session "{session_id.hex()}".',
}
),
200,
)
@remotecdm_wv_bp.route('/remotecdm/widevine/<device>/set_service_certificate', methods=['POST'])
@remotecdm_wv_bp.route(
"/remotecdm/widevine/<device>/set_service_certificate", methods=["POST"]
)
def remote_cdm_widevine_set_service_certificate(device): def remote_cdm_widevine_set_service_certificate(device):
body = request.get_json() body = request.get_json()
for required_field in ("session_id", "certificate"): for required_field in ("session_id", "certificate"):
if required_field == "certificate": if required_field == "certificate":
has_field = required_field in body # it needs the key, but can be empty/null has_field = (
required_field in body
) # it needs the key, but can be empty/null
else: else:
has_field = body.get(required_field) has_field = body.get(required_field)
if not has_field: if not has_field:
return jsonify({ return (
'status': 400, jsonify(
'message': f'Missing required field "{required_field}" in JSON body' {
}), 400 "status": 400,
"message": f'Missing required field "{required_field}" in JSON body',
}
),
400,
)
session_id = bytes.fromhex(body["session_id"]) session_id = bytes.fromhex(body["session_id"])
cdm = current_app.config["CDM"] cdm = current_app.config["CDM"]
if not cdm: if not cdm:
return jsonify({ return (
'status': 400, jsonify(
'message': f'No CDM session for "{device}" has been opened yet. No session to use' {
}), 400 "status": 400,
"message": f'No CDM session for "{device}" has been opened yet. No session to use',
}
),
400,
)
certificate = body["certificate"] certificate = body["certificate"]
try: try:
provider_id = cdm.set_service_certificate(session_id, certificate) provider_id = cdm.set_service_certificate(session_id, certificate)
except InvalidSession: except InvalidSession:
return jsonify({ return (
'status': 400, jsonify(
'message': f'Invalid session id: "{session_id.hex()}", it may have expired' {
}), 400 "status": 400,
"message": f'Invalid session id: "{session_id.hex()}", it may have expired',
}
),
400,
)
except DecodeError as error: except DecodeError as error:
return jsonify({ return (
'status': 400, jsonify(
'message': f'Invalid Service Certificate, {error}' {"status": 400, "message": f"Invalid Service Certificate, {error}"}
}), 400 ),
400,
)
except SignatureMismatch: except SignatureMismatch:
return jsonify({ return (
'status': 400, jsonify(
'message': 'Signature Validation failed on the Service Certificate, rejecting' {
}), 400 "status": 400,
return jsonify({ "message": "Signature Validation failed on the Service Certificate, rejecting",
'status': 200, }
'message': f"Successfully {['set', 'unset'][not certificate]} the Service Certificate.", ),
'data': { 400,
'provider_id': provider_id, )
} return (
}), 200 jsonify(
{
"status": 200,
"message": f"Successfully {['set', 'unset'][not certificate]} the Service Certificate.",
"data": {
"provider_id": provider_id,
},
}
),
200,
)
@remotecdm_wv_bp.route('/remotecdm/widevine/<device>/get_service_certificate', methods=['POST'])
@remotecdm_wv_bp.route(
"/remotecdm/widevine/<device>/get_service_certificate", methods=["POST"]
)
def remote_cdm_widevine_get_service_certificate(device): def remote_cdm_widevine_get_service_certificate(device):
body = request.get_json() body = request.get_json()
for required_field in ("session_id",): for required_field in ("session_id",):
if not body.get(required_field): if not body.get(required_field):
return jsonify({ return (
'status': 400, jsonify(
'message': f'Missing required field "{required_field}" in JSON body' {
}), 400 "status": 400,
"message": f'Missing required field "{required_field}" in JSON body',
}
),
400,
)
session_id = bytes.fromhex(body["session_id"]) session_id = bytes.fromhex(body["session_id"])
cdm = current_app.config["CDM"] cdm = current_app.config["CDM"]
if not cdm: if not cdm:
return jsonify({ return (
'status': 400, jsonify(
'message': f'No CDM session for "{device}" has been opened yet. No session to use' {
}), 400 "status": 400,
"message": f'No CDM session for "{device}" has been opened yet. No session to use',
}
),
400,
)
try: try:
service_certificate = cdm.get_service_certificate(session_id) service_certificate = cdm.get_service_certificate(session_id)
except InvalidSession: except InvalidSession:
return jsonify({ return (
'status': 400, jsonify(
'message': f'Invalid Session ID "{session_id.hex()}", it may have expired' {
}), 400 "status": 400,
"message": f'Invalid Session ID "{session_id.hex()}", it may have expired',
}
),
400,
)
if service_certificate: if service_certificate:
service_certificate_b64 = base64.b64encode(service_certificate.SerializeToString()).decode() service_certificate_b64 = base64.b64encode(
service_certificate.SerializeToString()
).decode()
else: else:
service_certificate_b64 = None service_certificate_b64 = None
return jsonify({ return (
'status': 200, jsonify(
'message': 'Successfully got the Service Certificate', {
'data': { "status": 200,
'service_certificate': service_certificate_b64, "message": "Successfully got the Service Certificate",
} "data": {
}), 200 "service_certificate": service_certificate_b64,
},
}
),
200,
)
@remotecdm_wv_bp.route('/remotecdm/widevine/<device>/get_license_challenge/<license_type>', methods=['POST'])
@remotecdm_wv_bp.route(
"/remotecdm/widevine/<device>/get_license_challenge/<license_type>",
methods=["POST"],
)
def remote_cdm_widevine_get_license_challenge(device, license_type): def remote_cdm_widevine_get_license_challenge(device, license_type):
body = request.get_json() body = request.get_json()
for required_field in ("session_id", "init_data"): for required_field in ("session_id", "init_data"):
if not body.get(required_field): if not body.get(required_field):
return jsonify({ return (
'status': 400, jsonify(
'message': f'Missing required field "{required_field}" in JSON body' {
}), 400 "status": 400,
"message": f'Missing required field "{required_field}" in JSON body',
}
),
400,
)
session_id = bytes.fromhex(body["session_id"]) session_id = bytes.fromhex(body["session_id"])
privacy_mode = body.get("privacy_mode", True) privacy_mode = body.get("privacy_mode", True)
cdm = current_app.config["CDM"] cdm = current_app.config["CDM"]
if not cdm: if not cdm:
return jsonify({ return (
'status': 400, jsonify(
'message': f'No CDM session for "{device}" has been opened yet. No session to use' {
}), 400 "status": 400,
"message": f'No CDM session for "{device}" has been opened yet. No session to use',
}
),
400,
)
if current_app.config.get("force_privacy_mode"): if current_app.config.get("force_privacy_mode"):
privacy_mode = True privacy_mode = True
if not cdm.get_service_certificate(session_id): if not cdm.get_service_certificate(session_id):
return jsonify({ return (
'status': 403, jsonify(
'message': 'No Service Certificate set but Privacy Mode is Enforced.' {
}), 403 "status": 403,
"message": "No Service Certificate set but Privacy Mode is Enforced.",
}
),
403,
)
current_app.config['pssh'] = body['init_data'] current_app.config["pssh"] = body["init_data"]
init_data = widevinePSSH(body['init_data']) init_data = widevinePSSH(body["init_data"])
try: try:
license_request = cdm.get_license_challenge( license_request = cdm.get_license_challenge(
session_id=session_id, session_id=session_id,
pssh=init_data, pssh=init_data,
license_type=license_type, license_type=license_type,
privacy_mode=privacy_mode privacy_mode=privacy_mode,
) )
except InvalidSession: except InvalidSession:
return jsonify({ return (
'status': 400, jsonify(
'message': f'Invalid Session ID "{session_id.hex()}", it may have expired' {
}), 400 "status": 400,
"message": f'Invalid Session ID "{session_id.hex()}", it may have expired',
}
),
400,
)
except InvalidInitData as error: except InvalidInitData as error:
return jsonify({ return jsonify({"status": 400, "message": f"Invalid Init Data, {error}"}), 400
'status': 400,
'message': f'Invalid Init Data, {error}'
}), 400
except InvalidLicenseType: except InvalidLicenseType:
return jsonify({ return (
'status': 400, jsonify({"status": 400, "message": f"Invalid License Type {license_type}"}),
'message': f'Invalid License Type {license_type}' 400,
}), 400 )
return jsonify({ return (
'status': 200, jsonify(
'message': 'Success', {
'data': { "status": 200,
'challenge_b64': base64.b64encode(license_request).decode() "message": "Success",
} "data": {"challenge_b64": base64.b64encode(license_request).decode()},
}), 200 }
),
200,
)
@remotecdm_wv_bp.route('/remotecdm/widevine/<device>/parse_license', methods=['POST']) @remotecdm_wv_bp.route("/remotecdm/widevine/<device>/parse_license", methods=["POST"])
def remote_cdm_widevine_parse_license(device): def remote_cdm_widevine_parse_license(device):
body = request.get_json() body = request.get_json()
for required_field in ("session_id", "license_message"): for required_field in ("session_id", "license_message"):
if not body.get(required_field): if not body.get(required_field):
return jsonify({ return (
'status': 400, jsonify(
'message': f'Missing required field "{required_field}" in JSON body' {
}), 400 "status": 400,
"message": f'Missing required field "{required_field}" in JSON body',
}
),
400,
)
session_id = bytes.fromhex(body["session_id"]) session_id = bytes.fromhex(body["session_id"])
cdm = current_app.config["CDM"] cdm = current_app.config["CDM"]
if not cdm: if not cdm:
return jsonify({ return (
'status': 400, jsonify(
'message': f'No CDM session for "{device}" has been opened yet. No session to use' {
}), 400 "status": 400,
"message": f'No CDM session for "{device}" has been opened yet. No session to use',
}
),
400,
)
try: try:
cdm.parse_license(session_id, body['license_message']) cdm.parse_license(session_id, body["license_message"])
except InvalidLicenseMessage as error: except InvalidLicenseMessage as error:
return jsonify({ return (
'status': 400, jsonify({"status": 400, "message": f"Invalid License Message, {error}"}),
'message': f'Invalid License Message, {error}' 400,
}), 400 )
except InvalidContext as error: except InvalidContext as error:
return jsonify({ return jsonify({"status": 400, "message": f"Invalid Context, {error}"}), 400
'status': 400,
'message': f'Invalid Context, {error}'
}), 400
except InvalidSession: except InvalidSession:
return jsonify({ return (
'status': 400, jsonify(
'message': f'Invalid Session ID "{session_id.hex()}", it may have expired' {
}), 400 "status": 400,
"message": f'Invalid Session ID "{session_id.hex()}", it may have expired',
}
),
400,
)
except SignatureMismatch: except SignatureMismatch:
return jsonify({ return (
'status': 400, jsonify(
'message': f'Signature Validation failed on the License Message, rejecting.' {
}), 400 "status": 400,
return jsonify({ "message": f"Signature Validation failed on the License Message, rejecting.",
'status': 200, }
'message': 'Successfully parsed and loaded the Keys from the License message.', ),
}), 200 400,
)
return (
jsonify(
{
"status": 200,
"message": "Successfully parsed and loaded the Keys from the License message.",
}
),
200,
)
@remotecdm_wv_bp.route('/remotecdm/widevine/<device>/get_keys/<key_type>', methods=['POST'])
@remotecdm_wv_bp.route(
"/remotecdm/widevine/<device>/get_keys/<key_type>", methods=["POST"]
)
def remote_cdm_widevine_get_keys(device, key_type): def remote_cdm_widevine_get_keys(device, key_type):
body = request.get_json() body = request.get_json()
for required_field in ("session_id",): for required_field in ("session_id",):
if not body.get(required_field): if not body.get(required_field):
return jsonify({ return (
'status': 400, jsonify(
'message': f'Missing required field "{required_field}" in JSON body' {
}), 400 "status": 400,
"message": f'Missing required field "{required_field}" in JSON body',
}
),
400,
)
session_id = bytes.fromhex(body["session_id"]) session_id = bytes.fromhex(body["session_id"])
key_type: Optional[str] = key_type key_type: Optional[str] = key_type
if key_type == 'ALL': if key_type == "ALL":
key_type = None key_type = None
cdm = current_app.config["CDM"] cdm = current_app.config["CDM"]
if not cdm: if not cdm:
return jsonify({ return (
'status': 400, jsonify(
'message': f'No CDM session for "{device}" has been opened yet. No session to use' {
}), 400 "status": 400,
"message": f'No CDM session for "{device}" has been opened yet. No session to use',
}
),
400,
)
try: try:
keys = cdm.get_keys(session_id, key_type) keys = cdm.get_keys(session_id, key_type)
except InvalidSession: except InvalidSession:
return jsonify({ return (
'status': 400, jsonify(
'message': f'Invalid Session ID "{session_id.hex()}", it may have expired' {
}), 400 "status": 400,
"message": f'Invalid Session ID "{session_id.hex()}", it may have expired',
}
),
400,
)
except ValueError as error: except ValueError as error:
return jsonify({ return (
'status': 400, jsonify(
'message': f'The Key Type value "{key_type}" is invalid, {error}' {
}), 400 "status": 400,
"message": f'The Key Type value "{key_type}" is invalid, {error}',
}
),
400,
)
keys_json = [ keys_json = [
{ {
"key_id": key.kid.hex, "key_id": key.kid.hex,
"key": key.key.hex(), "key": key.key.hex(),
"type": key.type, "type": key.type,
"permissions": key.permissions "permissions": key.permissions,
} }
for key in keys for key in keys
if not key_type or key.type == key_type if not key_type or key.type == key_type
] ]
for entry in keys_json: for entry in keys_json:
if config['database_type'].lower() != 'mariadb': if config["database_type"].lower() != "mariadb":
from custom_functions.database.cache_to_db_sqlite import cache_to_db from custom_functions.database.cache_to_db_sqlite import cache_to_db
elif config['database_type'].lower() == 'mariadb': elif config["database_type"].lower() == "mariadb":
from custom_functions.database.cache_to_db_mariadb import cache_to_db from custom_functions.database.cache_to_db_mariadb import cache_to_db
if entry['type'] != 'SIGNING': if entry["type"] != "SIGNING":
cache_to_db(pssh=str(current_app.config['pssh']), kid=entry['key_id'], key=entry['key']) cache_to_db(
pssh=str(current_app.config["pssh"]),
kid=entry["key_id"],
key=entry["key"],
)
return jsonify({ return (
'status': 200, jsonify({"status": 200, "message": "Success", "data": {"keys": keys_json}}),
'message': 'Success', 200,
'data': { )
'keys': keys_json
}
}), 200

View File

@ -2,41 +2,41 @@ from flask import Blueprint, request, jsonify, session
import os import os
import logging import logging
upload_bp = Blueprint('upload_bp', __name__) upload_bp = Blueprint("upload_bp", __name__)
@upload_bp.route('/upload/<cdmtype>', methods=['POST']) @upload_bp.route("/upload/<cdmtype>", methods=["POST"])
def upload(cdmtype): def upload(cdmtype):
try: try:
username = session.get('username') username = session.get("username")
if not username: if not username:
return jsonify({'message': 'False', 'error': 'No username in session'}), 400 return jsonify({"message": "False", "error": "No username in session"}), 400
# Validate CDM type # Validate CDM type
if cdmtype not in ['PR', 'WV']: if cdmtype not in ["PR", "WV"]:
return jsonify({'message': 'False', 'error': 'Invalid CDM type'}), 400 return jsonify({"message": "False", "error": "Invalid CDM type"}), 400
# Set up user directory paths # Set up user directory paths
base_path = os.path.join(os.getcwd(), 'configs', 'CDMs', username) base_path = os.path.join(os.getcwd(), "configs", "CDMs", username)
pr_path = os.path.join(base_path, 'PR') pr_path = os.path.join(base_path, "PR")
wv_path = os.path.join(base_path, 'WV') wv_path = os.path.join(base_path, "WV")
# Create necessary directories if they don't exist # Create necessary directories if they don't exist
os.makedirs(pr_path, exist_ok=True) os.makedirs(pr_path, exist_ok=True)
os.makedirs(wv_path, exist_ok=True) os.makedirs(wv_path, exist_ok=True)
# Get uploaded file # Get uploaded file
uploaded_file = request.files.get('file') uploaded_file = request.files.get("file")
if not uploaded_file: if not uploaded_file:
return jsonify({'message': 'False', 'error': 'No file provided'}), 400 return jsonify({"message": "False", "error": "No file provided"}), 400
# Determine correct save path based on cdmtype # Determine correct save path based on cdmtype
filename = uploaded_file.filename filename = uploaded_file.filename
save_path = os.path.join(pr_path if cdmtype == 'PR' else wv_path, filename) save_path = os.path.join(pr_path if cdmtype == "PR" else wv_path, filename)
uploaded_file.save(save_path) uploaded_file.save(save_path)
return jsonify({'message': 'Success', 'file_saved_to': save_path}) return jsonify({"message": "Success", "file_saved_to": save_path})
except Exception as e: except Exception as e:
logging.exception("Upload failed") logging.exception("Upload failed")
return jsonify({'message': 'False', 'error': 'Server error'}), 500 return jsonify({"message": "False", "error": "Server error"}), 500

View File

@ -2,53 +2,60 @@ import re
from flask import Blueprint, request, jsonify, session from flask import Blueprint, request, jsonify, session
from custom_functions.database.user_db import change_password, change_api_key from custom_functions.database.user_db import change_password, change_api_key
user_change_bp = Blueprint('user_change_bp', __name__) user_change_bp = Blueprint("user_change_bp", __name__)
# Define allowed characters regex (no spaces allowed) # Define allowed characters regex (no spaces allowed)
PASSWORD_REGEX = re.compile(r'^[A-Za-z0-9!@#$%^&*()_+\-=\[\]{};\'":\\|,.<>\/?`~]+$') PASSWORD_REGEX = re.compile(r'^[A-Za-z0-9!@#$%^&*()_+\-=\[\]{};\'":\\|,.<>\/?`~]+$')
@user_change_bp.route('/user/change_password', methods=['POST'])
@user_change_bp.route("/user/change_password", methods=["POST"])
def change_password_route(): def change_password_route():
username = session.get('username') username = session.get("username")
if not username: if not username:
return jsonify({'message': 'False'}), 400 return jsonify({"message": "False"}), 400
try: try:
data = request.get_json() data = request.get_json()
new_password = data.get('new_password', '') new_password = data.get("new_password", "")
if not PASSWORD_REGEX.match(new_password): if not PASSWORD_REGEX.match(new_password):
return jsonify({'message': 'Invalid password format'}), 400 return jsonify({"message": "Invalid password format"}), 400
change_password(username=username, new_password=new_password) change_password(username=username, new_password=new_password)
return jsonify({'message': 'True'}), 200 return jsonify({"message": "True"}), 200
except Exception as e: except Exception as e:
return jsonify({'message': 'False'}), 400 return jsonify({"message": "False"}), 400
@user_change_bp.route('/user/change_api_key', methods=['POST']) @user_change_bp.route("/user/change_api_key", methods=["POST"])
def change_api_key_route(): def change_api_key_route():
# Ensure the user is logged in by checking session for 'username' # Ensure the user is logged in by checking session for 'username'
username = session.get('username') username = session.get("username")
if not username: if not username:
return jsonify({'message': 'False', 'error': 'User not logged in'}), 400 return jsonify({"message": "False", "error": "User not logged in"}), 400
# Get the new API key from the request body # Get the new API key from the request body
new_api_key = request.json.get('new_api_key') new_api_key = request.json.get("new_api_key")
if not new_api_key: if not new_api_key:
return jsonify({'message': 'False', 'error': 'New API key not provided'}), 400 return jsonify({"message": "False", "error": "New API key not provided"}), 400
try: try:
# Call the function to update the API key in the database # Call the function to update the API key in the database
success = change_api_key(username=username, new_api_key=new_api_key) success = change_api_key(username=username, new_api_key=new_api_key)
if success: if success:
return jsonify({'message': 'True', 'success': 'API key changed successfully'}), 200 return (
jsonify({"message": "True", "success": "API key changed successfully"}),
200,
)
else: else:
return jsonify({'message': 'False', 'error': 'Failed to change API key'}), 500 return (
jsonify({"message": "False", "error": "Failed to change API key"}),
500,
)
except Exception as e: except Exception as e:
# Catch any unexpected errors and return a response # Catch any unexpected errors and return a response
return jsonify({'message': 'False', 'error': str(e)}), 500 return jsonify({"message": "False", "error": str(e)}), 500

View File

@ -2,33 +2,46 @@ from flask import Blueprint, request, jsonify, session
import os import os
import glob import glob
import logging import logging
from custom_functions.database.user_db import fetch_api_key, fetch_styled_username, fetch_username_by_api_key from custom_functions.database.user_db import (
fetch_api_key,
fetch_styled_username,
fetch_username_by_api_key,
)
user_info_bp = Blueprint('user_info_bp', __name__) user_info_bp = Blueprint("user_info_bp", __name__)
@user_info_bp.route('/userinfo', methods=['POST'])
@user_info_bp.route("/userinfo", methods=["POST"])
def user_info(): def user_info():
username = session.get('username') username = session.get("username")
if not username: if not username:
try: try:
headers = request.headers headers = request.headers
api_key = headers['Api-Key'] api_key = headers["Api-Key"]
username = fetch_username_by_api_key(api_key) username = fetch_username_by_api_key(api_key)
except: except:
return jsonify({'message': 'False'}), 400 return jsonify({"message": "False"}), 400
try: try:
base_path = os.path.join(os.getcwd(), 'configs', 'CDMs', username.lower()) base_path = os.path.join(os.getcwd(), "configs", "CDMs", username.lower())
pr_files = [os.path.basename(f) for f in glob.glob(os.path.join(base_path, 'PR', '*.prd'))] pr_files = [
wv_files = [os.path.basename(f) for f in glob.glob(os.path.join(base_path, 'WV', '*.wvd'))] os.path.basename(f)
for f in glob.glob(os.path.join(base_path, "PR", "*.prd"))
]
wv_files = [
os.path.basename(f)
for f in glob.glob(os.path.join(base_path, "WV", "*.wvd"))
]
return jsonify({ return jsonify(
'Username': username, {
'Widevine_Devices': wv_files, "Username": username,
'Playready_Devices': pr_files, "Widevine_Devices": wv_files,
'API_Key': fetch_api_key(username), "Playready_Devices": pr_files,
'Styled_Username': fetch_styled_username(username) "API_Key": fetch_api_key(username),
}) "Styled_Username": fetch_styled_username(username),
}
)
except Exception as e: except Exception as e:
logging.exception("Error retrieving device files") logging.exception("Error retrieving device files")
return jsonify({'message': 'False'}), 500 return jsonify({"message": "False"}), 500