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
icon.ico
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 = {
'discord': 'https://discord.cdrm-project.com/',
'telegram': 'https://telegram.cdrm-project.com/',
'gitea': 'https://cdm-project.com/tpd94/cdrm-project'
"discord": "https://discord.cdrm-project.com/",
"telegram": "https://telegram.cdrm-project.com/",
"gitea": "https://cdm-project.com/tpd94/cdrm-project",
}

View File

@ -1,47 +1,47 @@
tags = {
'index': {
'description': 'Decrypt Widevine and PlayReady protected content',
'keywords': 'CDRM, Widevine, PlayReady, DRM, Decrypt, CDM, CDM-Project, CDRM-Project, TPD94, Decryption',
'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_image': 'https://cdrm-project.com/og-home.jpg',
'opengraph_url': 'https://cdm-project.com/tpd94/cdrm-project',
'tab_title': 'CDRM-Project',
"index": {
"description": "Decrypt Widevine and PlayReady protected content",
"keywords": "CDRM, Widevine, PlayReady, DRM, Decrypt, CDM, CDM-Project, CDRM-Project, TPD94, Decryption",
"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_image": "https://cdrm-project.com/og-home.jpg",
"opengraph_url": "https://cdm-project.com/tpd94/cdrm-project",
"tab_title": "CDRM-Project",
},
'cache': {
'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',
'opengraph_title': 'Search the Cache',
'opengraph_description': 'Search the cache by KID or PSSH for decryption keys',
'opengraph_image': 'https://cdrm-project.com/og-cache.jpg',
'opengraph_url': 'https://cdrm-project.com/cache',
'tab_title': 'Cache',
"cache": {
"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",
"opengraph_title": "Search the Cache",
"opengraph_description": "Search the cache by KID or PSSH for decryption keys",
"opengraph_image": "https://cdrm-project.com/og-cache.jpg",
"opengraph_url": "https://cdrm-project.com/cache",
"tab_title": "Cache",
},
'testplayer': {
'description': 'Shaka Player for testing decryption keys',
'keywords': 'Shaka, Player, DRM, CDRM, CDM, CDRM-Project, TPD94, Decryption, CDM-Project, KID, KEY',
'opengraph_title': 'Test Player',
'opengraph_description': 'Shaka Player for testing decryption keys',
'opengraph_image': 'https://cdrm-project.com/og-testplayer.jpg',
'opengraph_url': 'https://cdrm-project.com/testplayer',
'tab_title': 'Test Player',
"testplayer": {
"description": "Shaka Player for testing decryption keys",
"keywords": "Shaka, Player, DRM, CDRM, CDM, CDRM-Project, TPD94, Decryption, CDM-Project, KID, KEY",
"opengraph_title": "Test Player",
"opengraph_description": "Shaka Player for testing decryption keys",
"opengraph_image": "https://cdrm-project.com/og-testplayer.jpg",
"opengraph_url": "https://cdrm-project.com/testplayer",
"tab_title": "Test Player",
},
'api': {
'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',
'opengraph_title': 'API',
'opengraph_description': 'Documentation for the program "CDRM-Project"',
'opengraph_image': 'https://cdrm-project.com/og-api.jpg',
'opengraph_url': 'https://cdrm-project.com/api',
'tab_title': 'API',
"api": {
"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",
"opengraph_title": "API",
"opengraph_description": 'Documentation for the program "CDRM-Project"',
"opengraph_image": "https://cdrm-project.com/og-api.jpg",
"opengraph_url": "https://cdrm-project.com/api",
"tab_title": "API",
},
"account": {
"description": "Account for CDRM-Project",
"keywords": "Login, CDRM, CDM, CDRM-Project, register, account",
"opengraph_title": "My account",
"opengraph_description": "Account for CDRM-Project",
"opengraph_image": "https://cdrm-project.com/og-home.jpg",
"opengraph_url": "https://cdrm-project.com/account",
"tab_title": "My account",
},
'account': {
'description': 'Account for CDRM-Project',
'keywords': 'Login, CDRM, CDM, CDRM-Project, register, account',
'opengraph_title': 'My account',
'opengraph_description': 'Account for CDRM-Project',
'opengraph_image': 'https://cdrm-project.com/og-home.jpg',
'opengraph_url': 'https://cdrm-project.com/account',
'tab_title': 'My account',
}
}

View File

@ -4,16 +4,15 @@ import mysql.connector
from mysql.connector import Error
def get_db_config():
# 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)
db_config = {
'host': f'{config["mariadb"]["host"]}',
'user': f'{config["mariadb"]["user"]}',
'password': f'{config["mariadb"]["password"]}',
'database': f'{config["mariadb"]["database"]}'
"host": f'{config["mariadb"]["host"]}',
"user": f'{config["mariadb"]["user"]}',
"password": f'{config["mariadb"]["password"]}',
"database": f'{config["mariadb"]["database"]}',
}
return db_config
@ -22,7 +21,8 @@ def create_database():
try:
with mysql.connector.connect(**get_db_config()) as conn:
cursor = conn.cursor()
cursor.execute('''
cursor.execute(
"""
CREATE TABLE IF NOT EXISTS licenses (
SERVICE VARCHAR(255),
PSSH TEXT,
@ -33,20 +33,32 @@ def create_database():
Cookies TEXT,
Data BLOB
)
''')
"""
)
conn.commit()
except Error as 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:
with mysql.connector.connect(**get_db_config()) as conn:
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()
cursor.execute('''
cursor.execute(
"""
INSERT INTO licenses (SERVICE, PSSH, KID, `Key`, License_URL, Headers, Cookies, Data)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s)
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),
Cookies = VALUES(Cookies),
Data = VALUES(Data)
''', (service, pssh, kid, key, license_url, headers, cookies, data))
""",
(service, pssh, kid, key, license_url, headers, cookies, data),
)
conn.commit()
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}")
return False
def search_by_pssh_or_kid(search_filter):
results = set()
try:
@ -72,54 +87,71 @@ def search_by_pssh_or_kid(search_filter):
cursor = conn.cursor()
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())
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())
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]
except Error as e:
print(f"Error: {e}")
return []
def get_key_by_kid_and_service(kid, service):
try:
with mysql.connector.connect(**get_db_config()) as conn:
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()
return result[0] if result else None
except Error as e:
print(f"Error: {e}")
return None
def get_kid_key_dict(service_name):
try:
with mysql.connector.connect(**get_db_config()) as conn:
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()}
except Error as e:
print(f"Error: {e}")
return {}
def get_unique_services():
try:
with mysql.connector.connect(**get_db_config()) as conn:
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()]
except Error as e:
print(f"Error: {e}")
return []
def key_count():
try:
with mysql.connector.connect(**get_db_config()) as conn:
cursor = conn.cursor()
cursor.execute('SELECT COUNT(KID) FROM licenses')
cursor.execute("SELECT COUNT(KID) FROM licenses")
return cursor.fetchone()[0]
except Error as e:
print(f"Error: {e}")

View File

@ -1,11 +1,13 @@
import sqlite3
import os
def create_database():
# 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.execute('''
cursor.execute(
"""
CREATE TABLE IF NOT EXISTS licenses (
SERVICE TEXT,
PSSH TEXT,
@ -16,92 +18,127 @@ def create_database():
Cookies 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()
# 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()
# Insert or replace the record
cursor.execute('''
cursor.execute(
"""
INSERT OR REPLACE INTO licenses (SERVICE, PSSH, KID, Key, License_URL, Headers, Cookies, Data)
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)
return True if existing_record else False
def search_by_pssh_or_kid(search_filter):
# 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()
# Initialize a set to store unique matching records
results = set()
# Search for records where PSSH contains the search_filter
cursor.execute('''
cursor.execute(
"""
SELECT * FROM licenses WHERE PSSH LIKE ?
''', ('%' + search_filter + '%',))
""",
("%" + search_filter + "%",),
)
rows = cursor.fetchall()
for row in rows:
results.add((row[1], row[2], row[3])) # (PSSH, KID, Key)
# Search for records where KID contains the search_filter
cursor.execute('''
cursor.execute(
"""
SELECT * FROM licenses WHERE KID LIKE ?
''', ('%' + search_filter + '%',))
""",
("%" + search_filter + "%",),
)
rows = cursor.fetchall()
for row in rows:
results.add((row[1], row[2], row[3])) # (PSSH, KID, Key)
# 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]
def get_key_by_kid_and_service(kid, service):
# 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()
# Query to search by KID and SERVICE
cursor.execute('''
cursor.execute(
"""
SELECT Key FROM licenses WHERE KID = ? AND SERVICE = ?
''', (kid, service))
""",
(kid, service),
)
# Fetch the result
result = cursor.fetchone()
# 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):
# 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()
# Query to fetch KID and Key for the selected service
cursor.execute('''
cursor.execute(
"""
SELECT KID, Key FROM licenses WHERE SERVICE = ?
''', (service_name,))
""",
(service_name,),
)
# Fetch all results and create the dictionary
kid_key_dict = {row[0]: row[1] for row in cursor.fetchall()}
return kid_key_dict
def get_unique_services():
# 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()
# 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
services = cursor.fetchall()
@ -111,13 +148,14 @@ def get_unique_services():
return unique_services
def key_count():
# 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()
# 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
return count

View File

@ -4,27 +4,32 @@ import bcrypt
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.execute('''
cursor.execute(
"""
CREATE TABLE IF NOT EXISTS user_info (
Username TEXT PRIMARY KEY,
Password TEXT,
Styled_Username TEXT,
API_Key TEXT
)
''')
"""
)
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()
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()
return True
except sqlite3.IntegrityError:
@ -32,24 +37,29 @@ def add_user(username, password, api_key):
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.execute('SELECT Password FROM user_info WHERE Username = ?', (username.lower(),))
cursor.execute(
"SELECT Password FROM user_info WHERE Username = ?", (username.lower(),)
)
result = cursor.fetchone()
if result:
stored_hash = result[0]
# Ensure stored_hash is bytes; decode if it's still a string (SQLite may store as TEXT)
if isinstance(stored_hash, str):
stored_hash = stored_hash.encode('utf-8')
return bcrypt.checkpw(password.encode('utf-8'), stored_hash)
stored_hash = stored_hash.encode("utf-8")
return bcrypt.checkpw(password.encode("utf-8"), stored_hash)
else:
return False
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.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()
if result:
@ -57,30 +67,42 @@ def fetch_api_key(username):
else:
return None
def change_password(username, 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
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.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()
return True
def change_api_key(username, new_api_key):
# 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.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()
return True
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.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()
if result:
@ -88,10 +110,11 @@ def fetch_styled_username(username):
else:
return None
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.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()
if result:

View File

@ -13,19 +13,23 @@ import yaml
from urllib.parse import urlparse
def find_license_key(data, keywords=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 isinstance(data, dict):
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)
return value.replace("-", "+").replace("_", "/") # Return the value immediately when found
if any(
keyword in key.lower() for keyword in keywords
): # 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
if isinstance(value, (dict, list)):
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):
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 isinstance(data, dict):
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
# Recursively check if the value is a dictionary or 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
elif isinstance(data, list):
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)
@ -68,11 +83,12 @@ def is_base64(string):
# Try decoding the string
decoded_data = base64.b64decode(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:
# If decoding or encoding fails, it's not Base64
return False
def is_url_and_split(input_str):
parsed = urlparse(input_str)
@ -84,82 +100,99 @@ def is_url_and_split(input_str):
else:
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}')
with open(f'{os.getcwd()}/configs/config.yaml', 'r') as file:
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}")
with open(f"{os.getcwd()}/configs/config.yaml", "r") as 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
elif config['database_type'].lower() == 'mariadb':
elif config["database_type"].lower() == "mariadb":
from custom_functions.database.cache_to_db_mariadb import cache_to_db
if pssh is None:
return {
'status': 'error',
'message': 'No PSSH provided'
}
return {"status": "error", "message": "No PSSH provided"}
try:
if "</WRMHEADER>".encode("utf-16-le") in base64.b64decode(pssh): # PR
try:
pr_pssh = playreadyPSSH(pssh)
except Exception as error:
return {
'status': 'error',
'message': f'An error occurred processing PSSH\n\n{error}'
"status": "error",
"message": f"An error occurred processing PSSH\n\n{error}",
}
try:
if device == 'public':
if device == "public":
base_name = config["default_pr_cdm"]
if not base_name.endswith(".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:
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:
pr_device = playreadyDevice.load(prd_files[0])
else:
return {
'status': 'error',
'message': 'No default .prd file found'
"status": "error",
"message": "No default .prd file found",
}
else:
base_name = device
if not base_name.endswith(".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:
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:
pr_device = playreadyDevice.load(prd_files[0])
else:
return {
'status': 'error',
'message': f'{base_name} does not exist'
"status": "error",
"message": f"{base_name} does not exist",
}
except Exception as error:
return {
'status': 'error',
'message': f'An error occurred location PlayReady CDM file\n\n{error}'
"status": "error",
"message": f"An error occurred location PlayReady CDM file\n\n{error}",
}
try:
pr_cdm = playreadyCdm.from_device(pr_device)
except Exception as error:
return {
'status': 'error',
'message': f'An error occurred loading PlayReady CDM\n\n{error}'
"status": "error",
"message": f"An error occurred loading PlayReady CDM\n\n{error}",
}
try:
pr_session_id = pr_cdm.open()
except Exception as error:
return {
'status': 'error',
'message': f'An error occurred opening a CDM session\n\n{error}'
"status": "error",
"message": f"An error occurred opening a CDM session\n\n{error}",
}
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:
return {
'status': 'error',
'message': f'An error occurred getting license challenge\n\n{error}'
"status": "error",
"message": f"An error occurred getting license challenge\n\n{error}",
}
try:
if headers:
@ -168,8 +201,8 @@ def api_decrypt(pssh:str = None, license_url: str = None, proxy: str = None, hea
format_headers = None
except Exception as error:
return {
'status': 'error',
'message': f'An error occurred getting headers\n\n{error}'
"status": "error",
"message": f"An error occurred getting headers\n\n{error}",
}
try:
if cookies:
@ -178,8 +211,8 @@ def api_decrypt(pssh:str = None, license_url: str = None, proxy: str = None, hea
format_cookies = None
except Exception as error:
return {
'status': 'error',
'message': f'An error occurred getting cookies\n\n{error}'
"status": "error",
"message": f"An error occurred getting cookies\n\n{error}",
}
try:
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
except Exception as error:
return {
'status': 'error',
'message': f'An error occurred getting json_data\n\n{error}'
"status": "error",
"message": f"An error occurred getting json_data\n\n{error}",
}
licence = None
proxies = None
if proxy is not None:
is_url, protocol, fqdn = is_url_and_split(proxy)
if is_url:
proxies = {'http': proxy, 'https': proxy}
proxies = {"http": proxy, "https": proxy}
else:
return {
'status': 'error',
'message': f'Your proxy is invalid, please put it in the format of http(s)://fqdn.tld:port'
"status": "error",
"message": f"Your proxy is invalid, please put it in the format of http(s)://fqdn.tld:port",
}
try:
licence = requests.post(
@ -209,132 +242,133 @@ def api_decrypt(pssh:str = None, license_url: str = None, proxy: str = None, hea
proxies=proxies,
cookies=format_cookies,
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:
return {
'status': 'error',
'message': f'An error occurred sending license challenge through your proxy\n\n{error}'
"status": "error",
"message": f"An error occurred sending license challenge through your proxy\n\n{error}",
}
except Exception as error:
return {
'status': 'error',
'message': f'An error occurred sending license reqeust\n\n{error}\n\n{licence.content}'
"status": "error",
"message": f"An error occurred sending license reqeust\n\n{error}\n\n{licence.content}",
}
try:
pr_cdm.parse_license(pr_session_id, licence.text)
except Exception as error:
return {
'status': 'error',
'message': f'An error occurred parsing license content\n\n{error}\n\n{licence.content}'
"status": "error",
"message": f"An error occurred parsing license content\n\n{error}\n\n{licence.content}",
}
returned_keys = ""
try:
keys = list(pr_cdm.get_keys(pr_session_id))
except Exception as error:
return {
'status': 'error',
'message': f'An error occurred getting keys\n\n{error}'
"status": "error",
"message": f"An error occurred getting keys\n\n{error}",
}
try:
for index, key in enumerate(keys):
if key.key_type != 'SIGNING':
cache_to_db(pssh=pssh, 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 key.key_type != "SIGNING":
cache_to_db(
pssh=pssh,
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:
returned_keys += f"{key.key_id.hex}:{key.key.hex()}\n"
else:
returned_keys += f"{key.key_id.hex}:{key.key.hex()}"
except Exception as error:
return {
'status': 'error',
'message': f'An error occurred formatting keys\n\n{error}'
"status": "error",
"message": f"An error occurred formatting keys\n\n{error}",
}
try:
pr_cdm.close(pr_session_id)
except Exception as error:
return {
'status': 'error',
'message': f'An error occurred closing session\n\n{error}'
"status": "error",
"message": f"An error occurred closing session\n\n{error}",
}
try:
return {
'status': 'success',
'message': returned_keys
}
return {"status": "success", "message": returned_keys}
except Exception as error:
return {
'status': 'error',
'message': f'An error occurred getting returned_keys\n\n{error}'
"status": "error",
"message": f"An error occurred getting returned_keys\n\n{error}",
}
except Exception as error:
return {
'status': 'error',
'message': f'An error occurred processing PSSH\n\n{error}'
"status": "error",
"message": f"An error occurred processing PSSH\n\n{error}",
}
else:
try:
wv_pssh = widevinePSSH(pssh)
except Exception as error:
return {
'status': 'error',
'message': f'An error occurred processing PSSH\n\n{error}'
"status": "error",
"message": f"An error occurred processing PSSH\n\n{error}",
}
try:
if device == 'public':
if device == "public":
base_name = config["default_wv_cdm"]
if not base_name.endswith(".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:
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:
wv_device = widevineDevice.load(wvd_files[0])
else:
return {
'status': 'error',
'message': 'No default .wvd file found'
}
return {"status": "error", "message": "No default .wvd file found"}
else:
base_name = device
if not base_name.endswith(".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:
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:
wv_device = widevineDevice.load(wvd_files[0])
else:
return {
'status': 'error',
'message': f'{base_name} does not exist'
}
return {"status": "error", "message": f"{base_name} does not exist"}
except Exception as error:
return {
'status': 'error',
'message': f'An error occurred location Widevine CDM file\n\n{error}'
"status": "error",
"message": f"An error occurred location Widevine CDM file\n\n{error}",
}
try:
wv_cdm = widevineCdm.from_device(wv_device)
except Exception as error:
return {
'status': 'error',
'message': f'An error occurred loading Widevine CDM\n\n{error}'
"status": "error",
"message": f"An error occurred loading Widevine CDM\n\n{error}",
}
try:
wv_session_id = wv_cdm.open()
except Exception as error:
return {
'status': 'error',
'message': f'An error occurred opening a CDM session\n\n{error}'
"status": "error",
"message": f"An error occurred opening a CDM session\n\n{error}",
}
try:
wv_challenge = wv_cdm.get_license_challenge(wv_session_id, wv_pssh)
except Exception as error:
return {
'status': 'error',
'message': f'An error occurred getting license challenge\n\n{error}'
"status": "error",
"message": f"An error occurred getting license challenge\n\n{error}",
}
try:
if headers:
@ -343,8 +377,8 @@ def api_decrypt(pssh:str = None, license_url: str = None, proxy: str = None, hea
format_headers = None
except Exception as error:
return {
'status': 'error',
'message': f'An error occurred getting headers\n\n{error}'
"status": "error",
"message": f"An error occurred getting headers\n\n{error}",
}
try:
if cookies:
@ -353,26 +387,29 @@ def api_decrypt(pssh:str = None, license_url: str = None, proxy: str = None, hea
format_cookies = None
except Exception as error:
return {
'status': 'error',
'message': f'An error occurred getting cookies\n\n{error}'
"status": "error",
"message": f"An error occurred getting cookies\n\n{error}",
}
try:
if json_data and not is_base64(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:
format_json_data = None
except Exception as error:
return {
'status': 'error',
'message': f'An error occurred getting json_data\n\n{error}'
"status": "error",
"message": f"An error occurred getting json_data\n\n{error}",
}
licence = None
proxies = None
if proxy is not None:
is_url, protocol, fqdn = is_url_and_split(proxy)
if is_url:
proxies = {'http': proxy, 'https': proxy}
proxies = {"http": proxy, "https": proxy}
try:
licence = requests.post(
url=license_url,
@ -380,17 +417,17 @@ def api_decrypt(pssh:str = None, license_url: str = None, proxy: str = None, hea
proxies=proxies,
cookies=format_cookies,
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:
return {
'status': 'error',
'message': f'An error occurred sending license challenge through your proxy\n\n{error}'
"status": "error",
"message": f"An error occurred sending license challenge through your proxy\n\n{error}",
}
except Exception as error:
return {
'status': 'error',
'message': f'An error occurred sending license reqeust\n\n{error}\n\n{licence.content}'
"status": "error",
"message": f"An error occurred sending license reqeust\n\n{error}\n\n{licence.content}",
}
try:
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)
except Exception as error:
return {
'status': 'error',
'message': f'An error occurred parsing license content\n\n{error}\n\n{licence.content}'
"status": "error",
"message": f"An error occurred parsing license content\n\n{error}\n\n{licence.content}",
}
returned_keys = ""
try:
keys = list(wv_cdm.get_keys(wv_session_id))
except Exception as error:
return {
'status': 'error',
'message': f'An error occurred getting keys\n\n{error}'
"status": "error",
"message": f"An error occurred getting keys\n\n{error}",
}
try:
for index, key in enumerate(keys):
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())
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(),
)
if index != len(keys) - 1:
returned_keys += f"{key.kid.hex}:{key.key.hex()}\n"
else:
returned_keys += f"{key.kid.hex}:{key.key.hex()}"
except Exception as error:
return {
'status': 'error',
'message': f'An error occurred formatting keys\n\n{error}'
"status": "error",
"message": f"An error occurred formatting keys\n\n{error}",
}
try:
wv_cdm.close(wv_session_id)
wv_cdm.close(wv_session_id)
except Exception as error:
return {
'status': 'error',
'message': f'An error occurred closing session\n\n{error}'
"status": "error",
"message": f"An error occurred closing session\n\n{error}",
}
try:
return {
'status': 'success',
'message': returned_keys
}
return {"status": "success", "message": returned_keys}
except Exception as error:
return {
'status': 'error',
'message': f'An error occurred getting returned_keys\n\n{error}'
"status": "error",
"message": f"An error occurred getting returned_keys\n\n{error}",
}

View File

@ -3,64 +3,84 @@ import yaml
import requests
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)
if config['default_wv_cdm'] == '':
answer = ' '
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: ')
if answer[0].upper() == 'Y':
response = requests.get(url='https://cdm-project.com/CDRM-Team/CDMs/raw/branch/main/Widevine/L3/public.wvd')
if config["default_wv_cdm"] == "":
answer = " "
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: "
)
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:
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)
config['default_wv_cdm'] = 'public'
with open(f'{os.getcwd()}/configs/config.yaml', 'w') as file:
config["default_wv_cdm"] = "public"
with open(f"{os.getcwd()}/configs/config.yaml", "w") as file:
yaml.dump(config, file)
print("Successfully downloaded Widevine CDM")
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")
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")
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"
)
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:
base_name = config["default_wv_cdm"]
if not base_name.endswith(".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
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():
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)
if config['default_pr_cdm'] == '':
answer = ' '
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: ')
if answer[0].upper() == 'Y':
response = requests.get(url='https://cdm-project.com/CDRM-Team/CDMs/raw/branch/main/Playready/SL2000/public.prd')
if config["default_pr_cdm"] == "":
answer = " "
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: "
)
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:
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)
config['default_pr_cdm'] = 'public'
with open(f'{os.getcwd()}/configs/config.yaml', 'w') as file:
config["default_pr_cdm"] = "public"
with open(f"{os.getcwd()}/configs/config.yaml", "w") as file:
yaml.dump(config, file)
print("Successfully downloaded PlayReady CDM")
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")
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")
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"
)
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:
base_name = config["default_pr_cdm"]
if not base_name.endswith(".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
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():

View File

@ -1,7 +1,8 @@
import os
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
else:
default_config = """\
@ -21,6 +22,6 @@ remote_cdm_secret: ''
# port: ''
# 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)
return

View File

@ -1,36 +1,43 @@
import os
import yaml
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)
if os.path.exists(f'{os.getcwd()}/databases/key_cache.db'):
if os.path.exists(f"{os.getcwd()}/databases/key_cache.db"):
return
else:
if config['database_type'].lower() != 'mariadb':
if config["database_type"].lower() != "mariadb":
from custom_functions.database.cache_to_db_sqlite import create_database
create_database()
return
else:
return
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
else:
from custom_functions.database.user_db import create_user_database
create_user_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)
if config['database_type'].lower() == 'mariadb':
if config["database_type"].lower() == "mariadb":
from custom_functions.database.cache_to_db_mariadb import create_database
create_database()
return
else:
return
def check_for_sql_database():
check_for_sqlite_database()
check_for_mariadb_database()

View File

@ -1,41 +1,47 @@
import os
def check_for_config_folder():
if os.path.isdir(f'{os.getcwd()}/configs'):
if os.path.isdir(f"{os.getcwd()}/configs"):
return
else:
os.mkdir(f'{os.getcwd()}/configs')
os.mkdir(f"{os.getcwd()}/configs")
return
def check_for_database_folder():
if os.path.isdir(f'{os.getcwd()}/databases'):
if os.path.isdir(f"{os.getcwd()}/databases"):
return
else:
os.mkdir(f'{os.getcwd()}/databases')
os.mkdir(f'{os.getcwd()}/databases/sql')
os.mkdir(f"{os.getcwd()}/databases")
os.mkdir(f"{os.getcwd()}/databases/sql")
return
def check_for_cdm_folder():
if os.path.isdir(f'{os.getcwd()}/configs/CDMs'):
if os.path.isdir(f"{os.getcwd()}/configs/CDMs"):
return
else:
os.mkdir(f'{os.getcwd()}/configs/CDMs')
os.mkdir(f"{os.getcwd()}/configs/CDMs")
return
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
else:
os.mkdir(f'{os.getcwd()}/configs/CDMs/WV')
os.mkdir(f"{os.getcwd()}/configs/CDMs/WV")
return
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
else:
os.mkdir(f'{os.getcwd()}/configs/CDMs/PR')
os.mkdir(f"{os.getcwd()}/configs/CDMs/PR")
return
def folder_checks():
check_for_config_folder()
check_for_database_folder()

View File

@ -3,6 +3,7 @@ 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.cdm_checks import check_for_cdms
def run_precheck():
folder_checks()
check_for_config_file()

View File

@ -3,6 +3,7 @@ import os
import subprocess
import venv
def version_check():
major_version = sys.version_info.major
minor_version = sys.version_info.minor
@ -15,20 +16,29 @@ def version_check():
else:
exit("Python 2 detected, Python version 3.12 or higher is required")
def pip_check():
try:
import pip
return
except ImportError:
exit("Pip is not installed")
def venv_check():
# 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
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_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")
)
# If venv already exists, restart script using its Python
if os.path.exists(venv_path):
@ -36,14 +46,14 @@ def venv_check():
sys.exit()
# Ask user for permission to create a virtual environment
answer = ''
while not answer or answer[0].upper() not in {'Y', 'N'}:
answer = ""
while not answer or answer[0].upper() not in {"Y", "N"}:
answer = input(
'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): '
"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): "
)
if answer[0].upper() == 'Y':
if answer[0].upper() == "Y":
print("Creating virtual environment...")
venv.create(venv_path, with_pip=True)
subprocess.call([venv_python] + sys.argv)
@ -61,23 +71,31 @@ def requirements_check():
import flask_cors
import yaml
import mysql.connector
return
except ImportError:
while True:
user_input = input("Missing packages. Do you want to install them? (Y/N): ").strip().upper()
if user_input == 'Y':
user_input = (
input("Missing packages. Do you want to install them? (Y/N): ")
.strip()
.upper()
)
if user_input == "Y":
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.")
break
elif user_input == 'N':
elif user_input == "N":
print("Dependencies required, please install them and run again.")
sys.exit()
else:
print("Invalid input. Please enter 'Y' to install or 'N' to exit.")
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
version_check()
pip_check()

View File

@ -1,12 +1,17 @@
import os
import glob
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
pr_files = [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'))]
pr_files = [
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
all_files = pr_files + wv_files

11
main.py
View File

@ -1,6 +1,8 @@
from custom_functions.prechecks.python_checks import run_python_checks
run_python_checks()
from custom_functions.prechecks.precheck import run_precheck
run_precheck()
from flask import Flask
from flask_cors import CORS
@ -15,10 +17,11 @@ from routes.login import login_bp
from routes.user_changes import user_change_bp
import os
import yaml
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)
app.secret_key = config['secret_key_flask']
app.secret_key = config["secret_key_flask"]
CORS(app)
@ -33,5 +36,5 @@ app.register_blueprint(remotecdm_wv_bp)
app.register_blueprint(remotecdm_pr_bp)
app.register_blueprint(user_change_bp)
if __name__ == '__main__':
app.run(debug=True, host='0.0.0.0')
if __name__ == "__main__":
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
mysql-connector-python
bcrypt
black

View File

@ -13,101 +13,126 @@ import tempfile
import time
from configs.icon_links import data as icon_data
api_bp = Blueprint('api', __name__)
with open(f'{os.getcwd()}/configs/config.yaml', 'r') as file:
api_bp = Blueprint("api", __name__)
with open(f"{os.getcwd()}/configs/config.yaml", "r") as file:
config = yaml.safe_load(file)
if config['database_type'].lower() != 'mariadb':
from custom_functions.database.cache_to_db_sqlite import search_by_pssh_or_kid, cache_to_db, \
get_key_by_kid_and_service, 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
if config["database_type"].lower() != "mariadb":
from custom_functions.database.cache_to_db_sqlite import (
search_by_pssh_or_kid,
cache_to_db,
get_key_by_kid_and_service,
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():
# 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)
db_config = {
'host': f'{config["mariadb"]["host"]}',
'user': f'{config["mariadb"]["user"]}',
'password': f'{config["mariadb"]["password"]}',
'database': f'{config["mariadb"]["database"]}'
"host": f'{config["mariadb"]["host"]}',
"user": f'{config["mariadb"]["user"]}',
"password": f'{config["mariadb"]["password"]}',
"database": f'{config["mariadb"]["database"]}',
}
return db_config
@api_bp.route('/api/cache/search', methods=['POST'])
@api_bp.route("/api/cache/search", methods=["POST"])
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)
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):
result = get_key_by_kid_and_service(kid=kid, service=service)
return jsonify({
'code': 0,
'content_key': result,
})
return jsonify(
{
"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):
result = get_kid_key_dict(service_name=service)
pages = math.ceil(len(result) / 10)
return jsonify({
'code': 0,
'content_keys': result,
'pages': pages
})
return jsonify({"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):
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)
if result:
return jsonify({
'code': 0,
'updated': True,
})
return jsonify(
{
"code": 0,
"updated": True,
}
)
elif result is False:
return jsonify({
'code': 0,
'updated': True,
})
return jsonify(
{
"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):
body = request.get_json()
keys_added = 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)
if result is True:
keys_updated += 1
elif result is False:
keys_added += 1
return jsonify({
'code': 0,
'added': str(keys_added),
'updated': str(keys_updated),
})
return jsonify(
{
"code": 0,
"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():
services = get_unique_services()
return jsonify({
'code': 0,
'service_list': services,
})
return jsonify(
{
"code": 0,
"service_list": services,
}
)
@api_bp.route('/api/cache/download', methods=['GET'])
@api_bp.route("/api/cache/download", methods=["GET"])
def download_database():
if config['database_type'].lower() != 'mariadb':
original_database_path = f'{os.getcwd()}/databases/sql/key_cache.db'
if config["database_type"].lower() != "mariadb":
original_database_path = f"{os.getcwd()}/databases/sql/key_cache.db"
# 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.)
shutil.copy2(original_database_path, modified_database_path)
@ -117,34 +142,40 @@ def download_database():
cursor = conn.cursor()
# Update all rows to remove Headers and Cookies (set them to NULL or empty strings)
cursor.execute('''
cursor.execute(
"""
UPDATE licenses
SET Headers = NULL,
Cookies = NULL
''')
"""
)
# 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
# Send the modified database as an attachment
return send_file(modified_database_path, as_attachment=True, download_name='key_cache.db')
if config['database_type'].lower() == 'mariadb':
return send_file(
modified_database_path, as_attachment=True, download_name="key_cache.db"
)
if config["database_type"].lower() == "mariadb":
try:
# Connect to MariaDB
conn = mysql.connector.connect(**get_db_config())
cursor = conn.cursor()
# Update sensitive data (this updates the live DB, you may want to duplicate rows instead)
cursor.execute('''
cursor.execute(
"""
UPDATE licenses
SET Headers = NULL,
Cookies = NULL
''')
"""
)
conn.commit()
# Now export the table
cursor.execute('SELECT * FROM licenses')
cursor.execute("SELECT * FROM licenses")
rows = cursor.fetchall()
column_names = [desc[0] for desc in cursor.description]
@ -152,116 +183,135 @@ def download_database():
output = StringIO()
output.write(f"-- Dump of `licenses` table\n")
for row in rows:
values = ', '.join(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")
values = ", ".join(
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
temp_dir = tempfile.gettempdir()
temp_path = os.path.join(temp_dir, 'key_cache.sql')
with open(temp_path, 'w', encoding='utf-8') as f:
temp_path = os.path.join(temp_dir, "key_cache.sql")
with open(temp_path, "w", encoding="utf-8") as f:
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:
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():
now = time.time()
if now - _keycount_cache['timestamp'] > 10 or _keycount_cache['count'] is None:
_keycount_cache['count'] = key_count()
_keycount_cache['timestamp'] = now
return jsonify({
'count': _keycount_cache['count']
})
if now - _keycount_cache["timestamp"] > 10 or _keycount_cache["count"] is None:
_keycount_cache["count"] = key_count()
_keycount_cache["timestamp"] = now
return jsonify({"count": _keycount_cache["count"]})
@api_bp.route('/api/decrypt', methods=['POST'])
@api_bp.route("/api/decrypt", methods=["POST"])
def decrypt_data():
api_request_data = json.loads(request.data)
if 'pssh' in api_request_data:
if api_request_data['pssh'] == '':
if "pssh" in api_request_data:
if api_request_data["pssh"] == "":
api_request_pssh = None
else:
api_request_pssh = api_request_data['pssh']
api_request_pssh = api_request_data["pssh"]
else:
api_request_pssh = None
if 'licurl' in api_request_data:
if api_request_data['licurl'] == '':
if "licurl" in api_request_data:
if api_request_data["licurl"] == "":
api_request_licurl = None
else:
api_request_licurl = api_request_data['licurl']
api_request_licurl = api_request_data["licurl"]
else:
api_request_licurl = None
if 'proxy' in api_request_data:
if api_request_data['proxy'] == '':
if "proxy" in api_request_data:
if api_request_data["proxy"] == "":
api_request_proxy = None
else:
api_request_proxy = api_request_data['proxy']
api_request_proxy = api_request_data["proxy"]
else:
api_request_proxy = None
if 'headers' in api_request_data:
if api_request_data['headers'] == '':
if "headers" in api_request_data:
if api_request_data["headers"] == "":
api_request_headers = None
else:
api_request_headers = api_request_data['headers']
api_request_headers = api_request_data["headers"]
else:
api_request_headers = None
if 'cookies' in api_request_data:
if api_request_data['cookies'] == '':
if "cookies" in api_request_data:
if api_request_data["cookies"] == "":
api_request_cookies = None
else:
api_request_cookies = api_request_data['cookies']
api_request_cookies = api_request_data["cookies"]
else:
api_request_cookies = None
if 'data' in api_request_data:
if api_request_data['data'] == '':
if "data" in api_request_data:
if api_request_data["data"] == "":
api_request_data_func = None
else:
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']
api_request_data_func = api_request_data["data"]
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
if api_request_device != 'public':
username = session.get('username')
if api_request_device != "public":
username = session.get("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):
api_request_device = api_request_device
else:
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)
if result['status'] == 'success':
return jsonify({
'status': 'success',
'message': result['message']
})
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,
)
if result["status"] == "success":
return jsonify({"status": "success", "message": result["message"]})
else:
return jsonify({
'status': 'fail',
'message': result['message']
})
return jsonify({"status": "fail", "message": result["message"]})
@api_bp.route('/api/links', methods=['GET'])
@api_bp.route("/api/links", methods=["GET"])
def get_links():
return jsonify({
'discord': icon_data['discord'],
'telegram': icon_data['telegram'],
'gitea': icon_data['gitea'],
})
return jsonify(
{
"discord": icon_data["discord"],
"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():
return jsonify({
'status': True,
})
return jsonify(
{
"status": True,
}
)

View File

@ -2,36 +2,44 @@ from flask import Blueprint, request, jsonify, session
from custom_functions.database.user_db import verify_user
login_bp = Blueprint(
'login_bp',
"login_bp",
__name__,
)
@login_bp.route('/login', methods=['POST'])
@login_bp.route("/login", methods=["POST"])
def login():
if request.method == 'POST':
if request.method == "POST":
data = request.get_json()
for required_field in ['username', 'password']:
for required_field in ["username", "password"]:
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']):
session['username'] = data['username'].lower() # Stored securely in a signed cookie
return jsonify({'message': 'Successfully logged in!'})
if verify_user(data["username"], data["password"]):
session["username"] = data[
"username"
].lower() # Stored securely in a signed cookie
return jsonify({"message": "Successfully logged in!"})
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():
try:
username = session.get('username')
username = session.get("username")
if username:
return jsonify({'message': 'True'})
return jsonify({"message": "True"})
else:
return jsonify({'message': 'False'})
return jsonify({"message": "False"})
except:
return jsonify({'message': 'False'})
return jsonify({"message": "False"})
@login_bp.route('/logout', methods=['POST'])
@login_bp.route("/logout", methods=["POST"])
def logout():
session.pop('username', None)
return jsonify({'message': 'Successfully logged out!'})
session.pop("username", None)
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 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
else: # Running in a normal Python environment
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',
"react_bp",
__name__,
static_folder=static_folder,
static_url_path='/',
template_folder=static_folder
static_url_path="/",
template_folder=static_folder,
)
@react_bp.route('/', methods=['GET'])
@react_bp.route('/<path:path>', methods=["GET"])
@react_bp.route('/<path>', methods=["GET"])
def index(path=''):
if request.method == 'GET':
@react_bp.route("/", methods=["GET"])
@react_bp.route("/<path:path>", methods=["GET"])
@react_bp.route("/<path>", methods=["GET"])
def index(path=""):
if request.method == "GET":
file_path = os.path.join(react_bp.static_folder, path)
if path != "" and os.path.exists(file_path):
return send_from_directory(react_bp.static_folder, path)
elif path.lower() in ['', 'cache', 'api', 'testplayer', 'account']:
data = index_tags.tags.get(path.lower(), index_tags.tags['index'])
return render_template('index.html', data=data)
elif path.lower() in ["", "cache", "api", "testplayer", "account"]:
data = index_tags.tags.get(path.lower(), index_tags.tags["index"])
return render_template("index.html", data=data)
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
import uuid
register_bp = Blueprint('register_bp', __name__)
register_bp = Blueprint("register_bp", __name__)
USERNAME_REGEX = re.compile(r'^[A-Za-z0-9_-]+$')
PASSWORD_REGEX = re.compile(r'^\S+$')
USERNAME_REGEX = re.compile(r"^[A-Za-z0-9_-]+$")
PASSWORD_REGEX = re.compile(r"^\S+$")
@register_bp.route('/register', methods=['POST'])
@register_bp.route("/register", methods=["POST"])
def register():
if request.method != 'POST':
return jsonify({'error': 'Method not supported'}), 405
if request.method != "POST":
return jsonify({"error": "Method not supported"}), 405
data = request.get_json()
# Check required fields
for required_field in ['username', 'password']:
for required_field in ["username", "password"]:
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']
password = data['password']
username = data["username"]
password = data["password"]
api_key = str(uuid.uuid4())
# Validate username and password
if not USERNAME_REGEX.fullmatch(username):
return jsonify({
'error': 'Invalid username. Only letters, numbers, hyphens, and underscores are allowed.'
}), 400
return (
jsonify(
{
"error": "Invalid username. Only letters, numbers, hyphens, and underscores are allowed."
}
),
400,
)
if not PASSWORD_REGEX.fullmatch(password):
return jsonify({
'error': 'Invalid password. Spaces are not allowed.'
}), 400
return jsonify({"error": "Invalid password. Spaces are not allowed."}), 400
# Attempt to add user
if add_user(username, password, api_key):
return jsonify({'message': 'User successfully registered!'}), 201
return jsonify({"message": "User successfully registered!"}), 201
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.cdm import Cdm as PlayReadyCDM
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.decrypt.api_decrypt import is_base64
from custom_functions.user_checks.device_allowed import user_allowed_to_use_device
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)
@remotecdm_pr_bp.route('/remotecdm/playready', methods=['GET', 'HEAD'])
@remotecdm_pr_bp.route("/remotecdm/playready", methods=["GET", "HEAD"])
def remote_cdm_playready():
if request.method == 'GET':
return jsonify({
'message': 'OK'
})
if request.method == 'HEAD':
if request.method == "GET":
return jsonify({"message": "OK"})
if request.method == "HEAD":
response = Response(status=200)
response.headers['Server'] = 'playready serve'
response.headers["Server"] = "playready serve"
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():
base_name = config["default_pr_cdm"]
if not base_name.endswith(".prd"):
full_file_name = (base_name + ".prd")
device = PlayReadyDevice.load(f'{os.getcwd()}/configs/CDMs/PR/{full_file_name}')
full_file_name = base_name + ".prd"
device = PlayReadyDevice.load(f"{os.getcwd()}/configs/CDMs/PR/{full_file_name}")
cdm = PlayReadyCDM.from_device(device)
return jsonify({
'security_level': cdm.security_level,
'host': f'{config["fqdn"]}/remotecdm/playready',
'secret': f'{config["remote_cdm_secret"]}',
'device_name': Path(base_name).stem
})
return jsonify(
{
"security_level": cdm.security_level,
"host": f'{config["fqdn"]}/remotecdm/playready',
"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):
if request.method == 'GET':
base_name = Path(device).with_suffix('.prd').name
api_key = request.headers['X-Secret-Key']
if request.method == "GET":
base_name = Path(device).with_suffix(".prd").name
api_key = request.headers["X-Secret-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)
return jsonify({
'security_level': cdm.security_level,
'host': f'{config["fqdn"]}/remotecdm/widevine',
'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
}
return jsonify(
{
"security_level": cdm.security_level,
"host": f'{config["fqdn"]}/remotecdm/widevine',
"secret": f"{api_key}",
"device_name": Path(base_name).stem,
}
})
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)
if user:
if user_allowed_to_use_device(device=device, username=user):
pr_device = PlayReadyDevice.load(f'{os.getcwd()}/configs/CDMs/{user}/PR/{device}.prd')
cdm = current_app.config['CDM'] = PlayReadyCDM.from_device(pr_device)
pr_device = PlayReadyDevice.load(
f"{os.getcwd()}/configs/CDMs/{user}/PR/{device}.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
}
return jsonify(
{
"message": "Success",
"data": {
"session_id": session_id.hex(),
"device": {"security_level": cdm.security_level},
},
}
})
)
else:
return jsonify({
'message': f"Device '{device}' is not found or you are not authorized to use it.",
}), 403
return (
jsonify(
{
"message": f"Device '{device}' is not found or you are not authorized to use it.",
}
),
403,
)
else:
return jsonify({
'message': f"Device '{device}' is not found or you are not authorized to use it.",
}), 403
return (
jsonify(
{
"message": f"Device '{device}' is not found or you are not authorized to use it.",
}
),
403,
)
else:
return jsonify({
'message': f"Device '{device}' is not found or you are not authorized to use it.",
}), 403
return (
jsonify(
{
"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):
try:
session_id = bytes.fromhex(session_id)
cdm = current_app.config["CDM"]
if not cdm:
return jsonify({
'message': f'No CDM for "{device}" has been opened yet. No session to close'
}), 400
return (
jsonify(
{
"message": f'No CDM for "{device}" has been opened yet. No session to close'
}
),
400,
)
try:
cdm.close(session_id)
except InvalidSession:
return jsonify({
'message': f'Invalid session ID "{session_id.hex()}", it may have expired'
}), 400
return jsonify({
'message': f'Successfully closed Session "{session_id.hex()}".',
}), 200
return (
jsonify(
{
"message": f'Invalid session ID "{session_id.hex()}", it may have expired'
}
),
400,
)
return (
jsonify(
{
"message": f'Successfully closed Session "{session_id.hex()}".',
}
),
200,
)
except Exception as e:
return jsonify({
'message': f'Failed to close Session "{session_id.hex()}".'
}), 400
return (
jsonify({"message": f'Failed to close Session "{session_id.hex()}".'}),
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):
body = request.get_json()
for required_field in ("session_id", "init_data"):
if not body.get(required_field):
return jsonify({
'message': f'Missing required field "{required_field}" in JSON body'
}), 400
return (
jsonify(
{
"message": f'Missing required field "{required_field}" in JSON body'
}
),
400,
)
cdm = current_app.config["CDM"]
session_id = bytes.fromhex(body["session_id"])
init_data = body["init_data"]
@ -145,42 +204,37 @@ def remote_cdm_playready_get_license_challenge(device):
if pssh.wrm_headers:
init_data = pssh.wrm_headers[0]
except InvalidPssh as e:
return jsonify({
'message': f'Unable to parse base64 PSSH, {e}'
})
return jsonify({"message": f"Unable to parse base64 PSSH, {e}"})
try:
license_request = cdm.get_license_challenge(
session_id=session_id,
wrm_header=init_data
session_id=session_id, wrm_header=init_data
)
except InvalidSession:
return jsonify({
'message': f"Invalid Session ID '{session_id.hex()}', it may have expired."
})
return jsonify(
{
"message": f"Invalid Session ID '{session_id.hex()}', it may have expired."
}
)
except Exception as e:
return jsonify({
'message': f'Error, {e}'
})
return jsonify({
'message': 'success',
'data': {
'challenge': license_request
}
})
return jsonify({"message": f"Error, {e}"})
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):
body = request.get_json()
for required_field in ("license_message", "session_id"):
if not body.get(required_field):
return jsonify({
'message': f'Missing required field "{required_field}" in JSON body'
})
return jsonify(
{"message": f'Missing required field "{required_field}" in JSON body'}
)
cdm = current_app.config["CDM"]
if not cdm:
return jsonify({
'message': f"No Cdm session for {device} has been opened yet. No session to use."
})
return jsonify(
{
"message": f"No Cdm session for {device} has been opened yet. No session to use."
}
)
session_id = bytes.fromhex(body["session_id"])
license_message = body["license_message"]
if is_base64(license_message):
@ -188,45 +242,44 @@ def remote_cdm_playready_parse_license(device):
try:
cdm.parse_license(session_id, license_message)
except InvalidSession:
return jsonify({
'message': f"Invalid Session ID '{session_id.hex()}', it may have expired."
})
return jsonify(
{
"message": f"Invalid Session ID '{session_id.hex()}', it may have expired."
}
)
except InvalidLicense as e:
return jsonify({
'message': f"Invalid License, {e}"
})
return jsonify({"message": f"Invalid License, {e}"})
except Exception as e:
return jsonify({
'message': f"Error, {e}"
})
return jsonify({
'message': 'Successfully parsed and loaded the Keys from the License message'
})
return jsonify({"message": f"Error, {e}"})
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):
body = request.get_json()
for required_field in ("session_id",):
if not body.get(required_field):
return jsonify({
'message': f'Missing required field "{required_field}" in JSON body'
})
return jsonify(
{"message": f'Missing required field "{required_field}" in JSON body'}
)
session_id = bytes.fromhex(body["session_id"])
cdm = current_app.config["CDM"]
if not cdm:
return jsonify({
'message': f"Missing required field '{required_field}' in JSON body."
})
return jsonify(
{"message": f"Missing required field '{required_field}' in JSON body."}
)
try:
keys = cdm.get_keys(session_id)
except InvalidSession:
return jsonify({
'message': f"Invalid Session ID '{session_id.hex()}', it may have expired."
})
return jsonify(
{
"message": f"Invalid Session ID '{session_id.hex()}', it may have expired."
}
)
except Exception as e:
return jsonify({
'message': f"Error, {e}"
})
return jsonify({"message": f"Error, {e}"})
keys_json = [
{
"key_id": key.key_id.hex,
@ -237,9 +290,4 @@ def remote_cdm_playready_get_keys(device):
}
for key in keys
]
return jsonify({
'message': 'success',
'data': {
'keys': keys_json
}
})
return jsonify({"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.cdm import Cdm as widevineCDM
from pywidevine.device import Device as widevineDevice
from pywidevine.exceptions import (InvalidContext, InvalidInitData, InvalidLicenseMessage, InvalidLicenseType,
InvalidSession, SignatureMismatch, TooManySessions)
from pywidevine.exceptions import (
InvalidContext,
InvalidInitData,
InvalidLicenseMessage,
InvalidLicenseType,
InvalidSession,
SignatureMismatch,
TooManySessions,
)
import yaml
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 pathlib import Path
remotecdm_wv_bp = Blueprint('remotecdm_wv', __name__)
with open(f'{os.getcwd()}/configs/config.yaml', 'r') as file:
remotecdm_wv_bp = Blueprint("remotecdm_wv", __name__)
with open(f"{os.getcwd()}/configs/config.yaml", "r") as 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():
if request.method == 'GET':
return jsonify({
'status': 200,
'message': f"{config['fqdn'].upper()} Remote Widevine CDM."
})
if request.method == 'HEAD':
if request.method == "GET":
return jsonify(
{"status": 200, "message": f"{config['fqdn'].upper()} Remote Widevine CDM."}
)
if request.method == "HEAD":
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
@remotecdm_wv_bp.route('/remotecdm/widevine/deviceinfo', methods=['GET'])
@remotecdm_wv_bp.route("/remotecdm/widevine/deviceinfo", methods=["GET"])
def remote_cdm_widevine_deviceinfo():
if request.method == 'GET':
if request.method == "GET":
base_name = config["default_wv_cdm"]
if not base_name.endswith(".wvd"):
base_name = (base_name + ".wvd")
device = widevineDevice.load(f'{os.getcwd()}/configs/CDMs/WV/{base_name}')
base_name = base_name + ".wvd"
device = widevineDevice.load(f"{os.getcwd()}/configs/CDMs/WV/{base_name}")
cdm = widevineCDM.from_device(device)
return jsonify({
'device_type': cdm.device_type.name,
'system_id': cdm.system_id,
'security_level': cdm.security_level,
'host': f'{config["fqdn"]}/remotecdm/widevine',
'secret': f'{config["remote_cdm_secret"]}',
'device_name': Path(base_name).stem
})
return jsonify(
{
"device_type": cdm.device_type.name,
"system_id": cdm.system_id,
"security_level": cdm.security_level,
"host": f'{config["fqdn"]}/remotecdm/widevine',
"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):
if request.method == 'GET':
base_name = Path(device).with_suffix('.wvd').name
api_key = request.headers['X-Secret-Key']
if request.method == "GET":
base_name = Path(device).with_suffix(".wvd").name
api_key = request.headers["X-Secret-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)
return jsonify({
'device_type': cdm.device_type.name,
'system_id': cdm.system_id,
'security_level': cdm.security_level,
'host': f'{config["fqdn"]}/remotecdm/widevine',
'secret': f'{api_key}',
'device_name': Path(base_name).stem
})
return jsonify(
{
"device_type": cdm.device_type.name,
"system_id": cdm.system_id,
"security_level": cdm.security_level,
"host": f'{config["fqdn"]}/remotecdm/widevine',
"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):
if str(device).lower() == config['default_wv_cdm'].lower():
wv_device = widevineDevice.load(f'{os.getcwd()}/configs/CDMs/WV/{config["default_wv_cdm"]}.wvd')
if str(device).lower() == config["default_wv_cdm"].lower():
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)
session_id = cdm.open()
return jsonify({
'status': 200,
'message': 'Success',
'data': {
'session_id': session_id.hex(),
'device': {
'system_id': cdm.system_id,
'security_level': cdm.security_level,
return (
jsonify(
{
"status": 200,
"message": "Success",
"data": {
"session_id": session_id.hex(),
"device": {
"system_id": cdm.system_id,
"security_level": cdm.security_level,
},
},
}
}
}), 200
if request.headers['X-Secret-Key'] and str(device).lower() != config['default_wv_cdm'].lower():
api_key = request.headers['X-Secret-Key']
),
200,
)
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)
if 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)
session_id = cdm.open()
return jsonify({
'status': 200,
'message': 'Success',
'data': {
'session_id': session_id.hex(),
'device': {
'system_id': cdm.system_id,
'security_level': cdm.security_level,
return (
jsonify(
{
"status": 200,
"message": "Success",
"data": {
"session_id": session_id.hex(),
"device": {
"system_id": cdm.system_id,
"security_level": cdm.security_level,
},
},
}
}
}), 200
),
200,
)
else:
return jsonify({
'message': f"Device '{device}' is not found or you are not authorized to use it.",
'status': 403
}), 403
return (
jsonify(
{
"message": f"Device '{device}' is not found or you are not authorized to use it.",
"status": 403,
}
),
403,
)
else:
return jsonify({
'message': f"Device '{device}' is not found or you are not authorized to use it.",
'status': 403
}), 403
return (
jsonify(
{
"message": f"Device '{device}' is not found or you are not authorized to use it.",
"status": 403,
}
),
403,
)
else:
return jsonify({
'message': f"Device '{device}' is not found or you are not authorized to use it.",
'status': 403
}), 403
return (
jsonify(
{
"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):
session_id = bytes.fromhex(session_id)
cdm = current_app.config["CDM"]
if not cdm:
return jsonify({
'status': 400,
'message': f'No CDM for "{device}" has been opened yet. No session to close'
}), 400
try:
cdm.close(session_id)
except InvalidSession:
return jsonify({
'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
session_id = bytes.fromhex(session_id)
cdm = current_app.config["CDM"]
if not cdm:
return (
jsonify(
{
"status": 400,
"message": f'No CDM for "{device}" has been opened yet. No session to close',
}
),
400,
)
try:
cdm.close(session_id)
except InvalidSession:
return (
jsonify(
{
"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):
body = request.get_json()
for required_field in ("session_id", "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:
has_field = body.get(required_field)
if not has_field:
return jsonify({
'status': 400,
'message': f'Missing required field "{required_field}" in JSON body'
}), 400
return (
jsonify(
{
"status": 400,
"message": f'Missing required field "{required_field}" in JSON body',
}
),
400,
)
session_id = bytes.fromhex(body["session_id"])
cdm = current_app.config["CDM"]
if not cdm:
return jsonify({
'status': 400,
'message': f'No CDM session for "{device}" has been opened yet. No session to use'
}), 400
return (
jsonify(
{
"status": 400,
"message": f'No CDM session for "{device}" has been opened yet. No session to use',
}
),
400,
)
certificate = body["certificate"]
try:
provider_id = cdm.set_service_certificate(session_id, certificate)
except InvalidSession:
return jsonify({
'status': 400,
'message': f'Invalid session id: "{session_id.hex()}", it may have expired'
}), 400
return (
jsonify(
{
"status": 400,
"message": f'Invalid session id: "{session_id.hex()}", it may have expired',
}
),
400,
)
except DecodeError as error:
return jsonify({
'status': 400,
'message': f'Invalid Service Certificate, {error}'
}), 400
return (
jsonify(
{"status": 400, "message": f"Invalid Service Certificate, {error}"}
),
400,
)
except SignatureMismatch:
return jsonify({
'status': 400,
'message': 'Signature Validation failed on the Service Certificate, rejecting'
}), 400
return jsonify({
'status': 200,
'message': f"Successfully {['set', 'unset'][not certificate]} the Service Certificate.",
'data': {
'provider_id': provider_id,
}
}), 200
return (
jsonify(
{
"status": 400,
"message": "Signature Validation failed on the Service Certificate, rejecting",
}
),
400,
)
return (
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):
body = request.get_json()
for required_field in ("session_id",):
if not body.get(required_field):
return jsonify({
'status': 400,
'message': f'Missing required field "{required_field}" in JSON body'
}), 400
return (
jsonify(
{
"status": 400,
"message": f'Missing required field "{required_field}" in JSON body',
}
),
400,
)
session_id = bytes.fromhex(body["session_id"])
cdm = current_app.config["CDM"]
if not cdm:
return jsonify({
'status': 400,
'message': f'No CDM session for "{device}" has been opened yet. No session to use'
}), 400
return (
jsonify(
{
"status": 400,
"message": f'No CDM session for "{device}" has been opened yet. No session to use',
}
),
400,
)
try:
service_certificate = cdm.get_service_certificate(session_id)
except InvalidSession:
return jsonify({
'status': 400,
'message': f'Invalid Session ID "{session_id.hex()}", it may have expired'
}), 400
return (
jsonify(
{
"status": 400,
"message": f'Invalid Session ID "{session_id.hex()}", it may have expired',
}
),
400,
)
if service_certificate:
service_certificate_b64 = base64.b64encode(service_certificate.SerializeToString()).decode()
service_certificate_b64 = base64.b64encode(
service_certificate.SerializeToString()
).decode()
else:
service_certificate_b64 = None
return jsonify({
'status': 200,
'message': 'Successfully got the Service Certificate',
'data': {
'service_certificate': service_certificate_b64,
}
}), 200
return (
jsonify(
{
"status": 200,
"message": "Successfully got the Service Certificate",
"data": {
"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):
body = request.get_json()
for required_field in ("session_id", "init_data"):
if not body.get(required_field):
return jsonify({
'status': 400,
'message': f'Missing required field "{required_field}" in JSON body'
}), 400
return (
jsonify(
{
"status": 400,
"message": f'Missing required field "{required_field}" in JSON body',
}
),
400,
)
session_id = bytes.fromhex(body["session_id"])
privacy_mode = body.get("privacy_mode", True)
cdm = current_app.config["CDM"]
if not cdm:
return jsonify({
'status': 400,
'message': f'No CDM session for "{device}" has been opened yet. No session to use'
}), 400
return (
jsonify(
{
"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"):
privacy_mode = True
if not cdm.get_service_certificate(session_id):
return jsonify({
'status': 403,
'message': 'No Service Certificate set but Privacy Mode is Enforced.'
}), 403
return (
jsonify(
{
"status": 403,
"message": "No Service Certificate set but Privacy Mode is Enforced.",
}
),
403,
)
current_app.config['pssh'] = body['init_data']
init_data = widevinePSSH(body['init_data'])
current_app.config["pssh"] = body["init_data"]
init_data = widevinePSSH(body["init_data"])
try:
license_request = cdm.get_license_challenge(
session_id=session_id,
pssh=init_data,
license_type=license_type,
privacy_mode=privacy_mode
privacy_mode=privacy_mode,
)
except InvalidSession:
return jsonify({
'status': 400,
'message': f'Invalid Session ID "{session_id.hex()}", it may have expired'
}), 400
return (
jsonify(
{
"status": 400,
"message": f'Invalid Session ID "{session_id.hex()}", it may have expired',
}
),
400,
)
except InvalidInitData as error:
return jsonify({
'status': 400,
'message': f'Invalid Init Data, {error}'
}), 400
return jsonify({"status": 400, "message": f"Invalid Init Data, {error}"}), 400
except InvalidLicenseType:
return jsonify({
'status': 400,
'message': f'Invalid License Type {license_type}'
}), 400
return jsonify({
'status': 200,
'message': 'Success',
'data': {
'challenge_b64': base64.b64encode(license_request).decode()
}
}), 200
return (
jsonify({"status": 400, "message": f"Invalid License Type {license_type}"}),
400,
)
return (
jsonify(
{
"status": 200,
"message": "Success",
"data": {"challenge_b64": base64.b64encode(license_request).decode()},
}
),
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):
body = request.get_json()
for required_field in ("session_id", "license_message"):
if not body.get(required_field):
return jsonify({
'status': 400,
'message': f'Missing required field "{required_field}" in JSON body'
}), 400
return (
jsonify(
{
"status": 400,
"message": f'Missing required field "{required_field}" in JSON body',
}
),
400,
)
session_id = bytes.fromhex(body["session_id"])
cdm = current_app.config["CDM"]
if not cdm:
return jsonify({
'status': 400,
'message': f'No CDM session for "{device}" has been opened yet. No session to use'
}), 400
return (
jsonify(
{
"status": 400,
"message": f'No CDM session for "{device}" has been opened yet. No session to use',
}
),
400,
)
try:
cdm.parse_license(session_id, body['license_message'])
cdm.parse_license(session_id, body["license_message"])
except InvalidLicenseMessage as error:
return jsonify({
'status': 400,
'message': f'Invalid License Message, {error}'
}), 400
return (
jsonify({"status": 400, "message": f"Invalid License Message, {error}"}),
400,
)
except InvalidContext as error:
return jsonify({
'status': 400,
'message': f'Invalid Context, {error}'
}), 400
return jsonify({"status": 400, "message": f"Invalid Context, {error}"}), 400
except InvalidSession:
return jsonify({
'status': 400,
'message': f'Invalid Session ID "{session_id.hex()}", it may have expired'
}), 400
return (
jsonify(
{
"status": 400,
"message": f'Invalid Session ID "{session_id.hex()}", it may have expired',
}
),
400,
)
except SignatureMismatch:
return jsonify({
'status': 400,
'message': f'Signature Validation failed on the License Message, rejecting.'
}), 400
return jsonify({
'status': 200,
'message': 'Successfully parsed and loaded the Keys from the License message.',
}), 200
return (
jsonify(
{
"status": 400,
"message": f"Signature Validation failed on the License Message, rejecting.",
}
),
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):
body = request.get_json()
for required_field in ("session_id",):
if not body.get(required_field):
return jsonify({
'status': 400,
'message': f'Missing required field "{required_field}" in JSON body'
}), 400
return (
jsonify(
{
"status": 400,
"message": f'Missing required field "{required_field}" in JSON body',
}
),
400,
)
session_id = bytes.fromhex(body["session_id"])
key_type: Optional[str] = key_type
if key_type == 'ALL':
if key_type == "ALL":
key_type = None
cdm = current_app.config["CDM"]
if not cdm:
return jsonify({
'status': 400,
'message': f'No CDM session for "{device}" has been opened yet. No session to use'
}), 400
return (
jsonify(
{
"status": 400,
"message": f'No CDM session for "{device}" has been opened yet. No session to use',
}
),
400,
)
try:
keys = cdm.get_keys(session_id, key_type)
except InvalidSession:
return jsonify({
'status': 400,
'message': f'Invalid Session ID "{session_id.hex()}", it may have expired'
}), 400
return (
jsonify(
{
"status": 400,
"message": f'Invalid Session ID "{session_id.hex()}", it may have expired',
}
),
400,
)
except ValueError as error:
return jsonify({
'status': 400,
'message': f'The Key Type value "{key_type}" is invalid, {error}'
}), 400
return (
jsonify(
{
"status": 400,
"message": f'The Key Type value "{key_type}" is invalid, {error}',
}
),
400,
)
keys_json = [
{
"key_id": key.kid.hex,
"key": key.key.hex(),
"type": key.type,
"permissions": key.permissions
"permissions": key.permissions,
}
for key in keys
if not key_type or key.type == key_type
]
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
elif config['database_type'].lower() == 'mariadb':
elif config["database_type"].lower() == "mariadb":
from custom_functions.database.cache_to_db_mariadb import cache_to_db
if entry['type'] != 'SIGNING':
cache_to_db(pssh=str(current_app.config['pssh']), kid=entry['key_id'], key=entry['key'])
if entry["type"] != "SIGNING":
cache_to_db(
pssh=str(current_app.config["pssh"]),
kid=entry["key_id"],
key=entry["key"],
)
return jsonify({
'status': 200,
'message': 'Success',
'data': {
'keys': keys_json
}
}), 200
return (
jsonify({"status": 200, "message": "Success", "data": {"keys": keys_json}}),
200,
)

View File

@ -2,41 +2,41 @@ from flask import Blueprint, request, jsonify, session
import os
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):
try:
username = session.get('username')
username = session.get("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
if cdmtype not in ['PR', 'WV']:
return jsonify({'message': 'False', 'error': 'Invalid CDM type'}), 400
if cdmtype not in ["PR", "WV"]:
return jsonify({"message": "False", "error": "Invalid CDM type"}), 400
# Set up user directory paths
base_path = os.path.join(os.getcwd(), 'configs', 'CDMs', username)
pr_path = os.path.join(base_path, 'PR')
wv_path = os.path.join(base_path, 'WV')
base_path = os.path.join(os.getcwd(), "configs", "CDMs", username)
pr_path = os.path.join(base_path, "PR")
wv_path = os.path.join(base_path, "WV")
# Create necessary directories if they don't exist
os.makedirs(pr_path, exist_ok=True)
os.makedirs(wv_path, exist_ok=True)
# Get uploaded file
uploaded_file = request.files.get('file')
uploaded_file = request.files.get("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
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)
return jsonify({'message': 'Success', 'file_saved_to': save_path})
return jsonify({"message": "Success", "file_saved_to": save_path})
except Exception as e:
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 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)
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():
username = session.get('username')
username = session.get("username")
if not username:
return jsonify({'message': 'False'}), 400
return jsonify({"message": "False"}), 400
try:
data = request.get_json()
new_password = data.get('new_password', '')
new_password = data.get("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)
return jsonify({'message': 'True'}), 200
return jsonify({"message": "True"}), 200
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():
# Ensure the user is logged in by checking session for 'username'
username = session.get('username')
username = session.get("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
new_api_key = request.json.get('new_api_key')
new_api_key = request.json.get("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:
# Call the function to update the API key in the database
success = change_api_key(username=username, new_api_key=new_api_key)
if success:
return jsonify({'message': 'True', 'success': 'API key changed successfully'}), 200
return (
jsonify({"message": "True", "success": "API key changed successfully"}),
200,
)
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:
# 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 glob
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():
username = session.get('username')
username = session.get("username")
if not username:
try:
headers = request.headers
api_key = headers['Api-Key']
api_key = headers["Api-Key"]
username = fetch_username_by_api_key(api_key)
except:
return jsonify({'message': 'False'}), 400
return jsonify({"message": "False"}), 400
try:
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'))]
wv_files = [os.path.basename(f) for f in glob.glob(os.path.join(base_path, 'WV', '*.wvd'))]
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"))
]
wv_files = [
os.path.basename(f)
for f in glob.glob(os.path.join(base_path, "WV", "*.wvd"))
]
return jsonify({
'Username': username,
'Widevine_Devices': wv_files,
'Playready_Devices': pr_files,
'API_Key': fetch_api_key(username),
'Styled_Username': fetch_styled_username(username)
})
return jsonify(
{
"Username": username,
"Widevine_Devices": wv_files,
"Playready_Devices": pr_files,
"API_Key": fetch_api_key(username),
"Styled_Username": fetch_styled_username(username),
}
)
except Exception as e:
logging.exception("Error retrieving device files")
return jsonify({'message': 'False'}), 500
return jsonify({"message": "False"}), 500