Compare commits

..

6 Commits
main ... github

Author SHA1 Message Date
57625445d8 dist 2025-04-30 05:02:34 -04:00
3bccf4d165 Update README.md 2025-04-30 04:57:27 -04:00
082e7eba53 Update cdm_checks.py 2025-04-30 04:56:25 -04:00
778455cb3e GE 2025-04-30 04:55:14 -04:00
4415372992 Merge branch 'github' of https://cdm-project.com/tpd94/CDRM-Project into github 2025-04-30 04:44:48 -04:00
8af432add4 Merge pull request 'Added dist' (#8) from main into github
Reviewed-on: #8
2025-04-28 22:26:46 +00:00
20 changed files with 512 additions and 823 deletions

View File

@ -2,6 +2,11 @@
## CDRM-Project
![forthebadge](https://forthebadge.com/images/badges/uses-html.svg) ![forthebadge](https://forthebadge.com/images/badges/uses-css.svg) ![forthebadge](https://forthebadge.com/images/badges/uses-javascript.svg) ![forthebadge](https://forthebadge.com/images/badges/made-with-python.svg)
## GITHUB EDITION
> This version **DOES NOT** come with CDM's (Content Decryption Modules) or the link to automatically download them - A simple web search should help you find what you're looking for.
>
## Prerequisites (from source only)
- [Python](https://www.python.org/downloads/) version [3.12](https://www.python.org/downloads/release/python-3120/)+ with PIP and VENV installed
@ -15,7 +20,7 @@
- Follow the on-screen prompts
## Installation (From binary)
- Download the latest release from the [releases](https://cdm-project.com/tpd94/CDRM-Project/releases) page and run the `.exe`
- Download the latest release from the [releases](https://github.com/TPD94/CDRM-Project-2.0/releases) page and run the `.exe`
## Installation (Manual)
- Open your terminal and navigate to where you'd like to store the application

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -12,8 +12,8 @@
<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-DWCLK6jB.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-DQNyIeaF.css">
<script type="module" crossorigin src="/assets/index-C4QO27se.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-BLw4WNgn.css">
</head>
<body class="w-full h-full">
<div id="root" class="w-full h-full"></div>

View File

@ -28,7 +28,7 @@ function NavBar() {
{/* Header */}
<div>
<p className="text-white text-2xl font-bold p-3 text-center mb-5">
<a href="/">CDRM-Project</a>
<a href="/">CDRM-Project</a><br /><span className="text-sm">Github Edition</span>
</p>
</div>

View File

@ -11,8 +11,9 @@ function NavBarMain({ setIsMenuOpen }) {
<button className="w-24 p-4" onClick={handleMenuToggle}>
<img src={hamburgerIcon} alt="Menu" className="w-full h-full cursor-pointer" />
</button>
<p className="grow text-white md:text-2xl font-bold text-center flex items-center justify-center p-4">
CDRM-Project
<p className="grow text-white md:text-2xl font-bold text-center flex flex-col items-center justify-center p-4">
CDRM-Project<br />
<span className="text-sm">Github Edition</span>
</p>
<div className="w-24 p-4"></div>
</div>

View File

@ -6,11 +6,6 @@ function MyAccount() {
const [prList, setPrList] = useState([]);
const [uploading, setUploading] = useState(false);
const [username, setUsername] = useState('');
const [apiKey, setApiKey] = useState('');
const [password, setPassword] = useState('');
const [passwordError, setPasswordError] = useState('');
const [newApiKey, setNewApiKey] = useState('');
const [apiKeyError, setApiKeyError] = useState('');
// Fetch user info
const fetchUserInfo = async () => {
@ -18,8 +13,7 @@ function MyAccount() {
const response = await axios.post('/userinfo');
setWvList(response.data.Widevine_Devices || []);
setPrList(response.data.Playready_Devices || []);
setUsername(response.data.Styled_Username || '');
setApiKey(response.data.API_Key || '');
setUsername(response.data.Username || '');
} catch (err) {
console.error('Failed to fetch user info', err);
}
@ -66,130 +60,14 @@ function MyAccount() {
}
};
// Handle change password
const handleChangePassword = async () => {
if (passwordError || password === '') {
alert('Please enter a valid password.');
return;
}
try {
const response = await axios.post('/user/change_password', {
new_password: password
});
if (response.data.message === 'True') {
alert('Password changed successfully.');
setPassword('');
} else {
alert('Failed to change password.');
}
} catch (error) {
if (error.response && error.response.data?.message === 'Invalid password format') {
alert('Password format is invalid. Please try again.');
} else {
alert('Error occurred while changing password.');
}
}
};
// Handle change API key
const handleChangeApiKey = async () => {
if (apiKeyError || newApiKey === '') {
alert('Please enter a valid API key.');
return;
}
try {
const response = await axios.post('/user/change_api_key', {
new_api_key: newApiKey,
});
if (response.data.message === 'True') {
alert('API key changed successfully.');
setApiKey(newApiKey);
setNewApiKey('');
} else {
alert('Failed to change API key.');
}
} catch (error) {
alert('Error occurred while changing API key.');
console.error(error);
}
};
return (
<div id="myaccount" className="flex flex-col lg:flex-row gap-4 w-full min-h-full overflow-y-auto p-4">
<div className="flex-col w-full min-h-164 lg:h-full lg:w-96 border-2 border-yellow-500/50 rounded-2xl p-4 flex items-center overflow-y-auto">
<div id="myaccount" className="flex flex-row w-full min-h-full overflow-y-auto p-4">
<div className="flex flex-col w-full min-h-full lg:flex-row">
{/* Left Panel */}
<div className="border-2 border-yellow-500/50 lg:h-full lg:w-96 w-full rounded-2xl p-4 flex flex-col items-center overflow-y-auto">
<h1 className="text-2xl font-bold text-white border-b-2 border-white p-2 w-full text-center mb-2">
{username ? `${username}` : 'My Account'}
</h1>
{/* API Key Section */}
<div className="w-full flex flex-col items-center">
<label htmlFor="apiKey" className="text-white font-semibold mb-1">API Key</label>
<input
id="apiKey"
type="text"
value={apiKey}
readOnly
className="w-full p-2 mb-4 rounded bg-gray-800 text-white border border-gray-600 text-center"
/>
{/* New API Key Section */}
<label htmlFor="newApiKey" className="text-white font-semibold mt-4 mb-1">New API Key</label>
<input
id="newApiKey"
type="text"
value={newApiKey}
onChange={(e) => {
const value = e.target.value;
const isValid = /^[^\s]+$/.test(value); // No spaces
if (!isValid) {
setApiKeyError('API key must not contain spaces.');
} else {
setApiKeyError('');
}
setNewApiKey(value);
}}
placeholder="Enter new API key"
className="w-full p-2 mb-1 rounded bg-gray-800 text-white border border-gray-600 text-center"
/>
{apiKeyError && <p className="text-red-500 text-sm mb-3">{apiKeyError}</p>}
<button
className="w-full h-12 bg-yellow-500/50 rounded-2xl text-2xl text-white"
onClick={handleChangeApiKey}
>
Change API Key
</button>
{/* Change Password Section */}
<label htmlFor="password" className="text-white font-semibold mt-4 mb-1">Change Password</label>
<input
id="password"
type="password"
value={password}
onChange={(e) => {
const value = e.target.value;
const isValid = /^[A-Za-z0-9!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?`~]*$/.test(value);
if (!isValid) {
setPasswordError('Password must not contain spaces or invalid characters.');
} else {
setPasswordError('');
}
setPassword(value);
}}
placeholder="New Password"
className="w-full p-2 mb-1 rounded bg-gray-800 text-white border border-gray-600 text-center"
/>
{passwordError && <p className="text-red-500 text-sm mb-3">{passwordError}</p>}
<button
className="w-full h-12 bg-yellow-500/50 rounded-2xl text-2xl text-white"
onClick={handleChangePassword}
>
Change Password
</button>
</div>
<button
onClick={handleLogout}
className="mt-auto w-full h-12 bg-yellow-500/50 rounded-2xl text-2xl text-white"
@ -198,10 +76,11 @@ function MyAccount() {
</button>
</div>
<div className="flex flex-col w-full lg:ml-2 mt-2 lg:mt-0">
{/* Right Panel */}
<div className="flex flex-col grow lg:ml-2 mt-2 lg:mt-0">
{/* Widevine Section */}
<div className="border-2 border-yellow-500/50 flex flex-col w-full min-h-1/2 text-center rounded-2xl lg:p-4 p-2 overflow-y-auto">
<h1 className="bg-black text-2xl font-bold text-white border-b-2 border-white p-2">Widevine CDMs</h1>
<h1 className="text-2xl font-bold text-white border-b-2 border-white p-2">Widevine CDMs</h1>
<div className="flex flex-col w-full grow p-2 bg-white/5 rounded-2xl mt-2 text-white text-left">
{wvList.length === 0 ? (
<div className="text-white text-center font-bold">No Widevine CDMs uploaded.</div>
@ -209,14 +88,16 @@ function MyAccount() {
wvList.map((filename, i) => (
<div
key={i}
className={`text-center font-bold text-white p-2 rounded ${i % 2 === 0 ? 'bg-black/30' : 'bg-black/60'}`}
className={`text-center font-bold text-white p-2 rounded ${
i % 2 === 0 ? 'bg-black/30' : 'bg-black/60'
}`}
>
{filename}
</div>
))
)}
</div>
<label className="bg-yellow-500 text-white w-full min-h-16 lg:min-h-16 mt-4 rounded-2xl flex items-center justify-center cursor-pointer">
<label className="bg-yellow-500 text-white w-full h-16 mt-4 rounded-2xl flex items-center justify-center cursor-pointer">
{uploading ? 'Uploading...' : 'Upload CDM'}
<input
type="file"
@ -229,7 +110,7 @@ function MyAccount() {
{/* Playready Section */}
<div className="border-2 border-yellow-500/50 flex flex-col w-full min-h-1/2 text-center rounded-2xl p-2 mt-2 lg:mt-2 overflow-y-auto">
<h1 className="text-2xl font-bold text-white border-b-2 border-white p-2 bg-black">Playready CDMs</h1>
<h1 className="text-2xl font-bold text-white border-b-2 border-white p-2">Playready CDMs</h1>
<div className="flex flex-col w-full bg-white/5 grow rounded-2xl mt-2 text-white text-left p-2">
{prList.length === 0 ? (
<div className="text-white text-center font-bold">No Playready CDMs uploaded.</div>
@ -237,14 +118,16 @@ function MyAccount() {
prList.map((filename, i) => (
<div
key={i}
className={`text-center font-bold text-white p-2 rounded ${i % 2 === 0 ? 'bg-black/30' : 'bg-black/60'}`}
className={`text-center font-bold text-white p-2 rounded ${
i % 2 === 0 ? 'bg-black/30' : 'bg-black/60'
}`}
>
{filename}
</div>
))
)}
</div>
<label className="bg-yellow-500 text-white w-full min-h-16 lg:min-h-16 mt-4 rounded-2xl flex items-center justify-center cursor-pointer">
<label className="bg-yellow-500 text-white w-full h-16 mt-4 rounded-2xl flex items-center justify-center cursor-pointer">
{uploading ? 'Uploading...' : 'Upload CDM'}
<input
type="file"
@ -256,6 +139,7 @@ function MyAccount() {
</div>
</div>
</div>
</div>
);
}

View File

@ -5,20 +5,7 @@ function Register() {
const [password, setPassword] = useState('');
const [status, setStatus] = useState('');
// Validation functions
const validateUsername = (name) => /^[A-Za-z0-9_-]+$/.test(name);
const validatePassword = (pass) => /^\S+$/.test(pass); // No spaces
const handleRegister = async () => {
if (!validateUsername(username)) {
setStatus("Invalid username. Use only letters, numbers, hyphens, or underscores.");
return;
}
if (!validatePassword(password)) {
setStatus("Invalid password. Spaces are not allowed.");
return;
}
try {
const response = await fetch('/register', {
method: 'POST',
@ -39,15 +26,6 @@ function Register() {
};
const handleLogin = async () => {
if (!validateUsername(username)) {
setStatus("Invalid username. Use only letters, numbers, hyphens, or underscores.");
return;
}
if (!validatePassword(password)) {
setStatus("Invalid password. Spaces are not allowed.");
return;
}
try {
const response = await fetch('/login', {
method: 'POST',

View File

@ -35,8 +35,9 @@ function SideMenu({ isMenuOpen, setIsMenuOpen }) {
{/* Header */}
<div className="h-16 w-full border-b-2 border-white/5 flex flex-row">
<div className="w-1/4 h-full"></div>
<p className="grow text-white md:text-2xl font-bold text-center flex items-center justify-center p-4">
CDRM-Project
<p className="grow text-white md:text-2xl font-bold text-center flex items-center justify-center p-4 flex-col">
CDRM-Project<br />
<span className="text-sm">Github Edition</span>
</p>
<div className="w-1/4 h-full">
<button

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'
'gitea': 'https://cdm-project.com/tpd94/cdm-project'
}

View File

@ -11,20 +11,18 @@ def create_user_database():
cursor.execute('''
CREATE TABLE IF NOT EXISTS user_info (
Username TEXT PRIMARY KEY,
Password TEXT,
Styled_Username TEXT,
API_Key TEXT
Password TEXT
)
''')
def add_user(username, password, api_key):
def add_user(username, password):
hashed_pw = bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt())
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) VALUES (?, ?)', (username, hashed_pw))
conn.commit()
return True
except sqlite3.IntegrityError:
@ -34,7 +32,7 @@ def add_user(username, password, api_key):
def verify_user(username, password):
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,))
result = cursor.fetchone()
if result:
@ -45,56 +43,3 @@ def verify_user(username, password):
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:
cursor = conn.cursor()
cursor.execute('SELECT API_Key FROM user_info WHERE Username = ?', (username.lower(),))
result = cursor.fetchone()
if result:
return result[0]
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())
# Update the password in the database
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()))
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:
cursor = conn.cursor()
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:
cursor = conn.cursor()
cursor.execute('SELECT Styled_Username FROM user_info WHERE Username = ?', (username.lower(),))
result = cursor.fetchone()
if result:
return result[0]
else:
return None
def fetch_username_by_api_key(api_key):
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,))
result = cursor.fetchone()
if result:
return result[0] # Return the username
else:
return None # If no user is found for the API key

View File

@ -8,22 +8,7 @@ def check_for_wvd_cdm():
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 response.status_code == 200:
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:
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"Please put the name of your Widevine CDM inside of {os.getcwd()}/configs/config.yaml")
else:
base_name = config["default_wv_cdm"]
if not base_name.endswith(".wvd"):
@ -37,22 +22,7 @@ def check_for_prd_cdm():
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 response.status_code == 200:
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:
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"Please put the name of your PlayReady CDM inside of {os.getcwd()}/configs/config.yaml")
else:
base_name = config["default_pr_cdm"]
if not base_name.endswith(".prd"):

View File

@ -12,7 +12,6 @@ from routes.upload import upload_bp
from routes.user_info import user_info_bp
from routes.register import register_bp
from routes.login import login_bp
from routes.user_changes import user_change_bp
import os
import yaml
app = Flask(__name__)
@ -31,7 +30,6 @@ app.register_blueprint(user_info_bp)
app.register_blueprint(upload_bp)
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')

View File

@ -15,7 +15,7 @@ def login():
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
session['username'] = data['username'] # Stored securely in a signed cookie
return jsonify({'message': 'Successfully logged in!'})
else:
return jsonify({'error': 'Invalid username or password!'}), 401

View File

@ -1,42 +1,29 @@
import re
from flask import Blueprint, request, jsonify
from custom_functions.database.user_db import add_user
import uuid
register_bp = Blueprint('register_bp', __name__)
USERNAME_REGEX = re.compile(r'^[A-Za-z0-9_-]+$')
PASSWORD_REGEX = re.compile(r'^\S+$')
register_bp = Blueprint(
'register_bp',
__name__,
)
@register_bp.route('/register', methods=['POST'])
def register():
if request.method != 'POST':
return jsonify({'error': 'Method not supported'}), 405
if request.method == 'POST':
data = request.get_json()
# Check required fields
for required_field in ['username', 'password']:
if required_field not in data:
return jsonify({'error': f'Missing required field: {required_field}'}), 400
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
if not PASSWORD_REGEX.fullmatch(password):
'error': f'Missing required field: {required_field}'
})
if add_user(data['username'], data['password']):
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
'message': 'User successfully registered!'
})
else:
return jsonify({'error': 'User already exists!'}), 409
return jsonify({
'error': 'User already exists!'
})
else:
return jsonify({
'error': 'Method not supported'
})

View File

@ -5,8 +5,6 @@ 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 custom_functions.database.user_db import fetch_username_by_api_key
from custom_functions.user_checks.device_allowed import user_allowed_to_use_device
@ -55,67 +53,44 @@ def remote_cdm_playready_open(device):
}
}
})
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)
session_id = cdm.open()
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
else:
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
@remotecdm_pr_bp.route('/remotecdm/playready/<device>/close/<session_id>', methods=['GET'])
def remote_cdm_playready_close(device, session_id):
try:
if str(device).lower() == config['default_pr_cdm'].lower():
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
except Exception as e:
})
else:
return jsonify({
'message': f'Failed to close Session "{session_id.hex()}".'
}), 400
'status': 400,
'message': f'Unauthorized'
})
@remotecdm_pr_bp.route('/remotecdm/playready/<device>/get_license_challenge', methods=['POST'])
def remote_cdm_playready_get_license_challenge(device):
if str(device).lower() == config['default_pr_cdm'].lower():
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
})
cdm = current_app.config["CDM"]
session_id = bytes.fromhex(body["session_id"])
init_data = body["init_data"]
@ -150,6 +125,7 @@ def remote_cdm_playready_get_license_challenge(device):
@remotecdm_pr_bp.route('/remotecdm/playready/<device>/parse_license', methods=['POST'])
def remote_cdm_playready_parse_license(device):
if str(device).lower() == config['default_pr_cdm'].lower():
body = request.get_json()
for required_field in ("license_message", "session_id"):
if not body.get(required_field):
@ -183,6 +159,7 @@ def remote_cdm_playready_parse_license(device):
@remotecdm_pr_bp.route('/remotecdm/playready/<device>/get_keys', methods=['POST'])
def remote_cdm_playready_get_keys(device):
if str(device).lower() == config['default_pr_cdm'].lower():
body = request.get_json()
for required_field in ("session_id",):
if not body.get(required_field):

View File

@ -11,8 +11,6 @@ from pywidevine.exceptions import (InvalidContext, InvalidInitData, InvalidLicen
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
remotecdm_wv_bp = Blueprint('remotecdm_wv', __name__)
with open(f'{os.getcwd()}/configs/config.yaml', 'r') as file:
@ -63,66 +61,43 @@ def remote_cdm_widevine_open(device):
'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']
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')
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,
}
}
}), 200
})
else:
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
else:
return jsonify({
'message': f"Device '{device}' is not found or you are not authorized to use it.",
'status': 403
}), 403
'status': 400,
'message': 'Unauthorized'
})
@remotecdm_wv_bp.route('/remotecdm/widevine/<device>/close/<session_id>', methods=['GET'])
def remote_cdm_widevine_close(device, session_id):
if str(device).lower() == config['default_wv_cdm'].lower():
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
})
else:
return jsonify({
'status': 400,
'message': f'Unauthorized'
})
@remotecdm_wv_bp.route('/remotecdm/widevine/<device>/set_service_certificate', methods=['POST'])
def remote_cdm_widevine_set_service_certificate(device):
if str(device).lower() == config['default_wv_cdm'].lower():
body = request.get_json()
for required_field in ("session_id", "certificate"):
if required_field == "certificate":
@ -133,7 +108,7 @@ def remote_cdm_widevine_set_service_certificate(device):
return jsonify({
'status': 400,
'message': f'Missing required field "{required_field}" in JSON body'
}), 400
})
session_id = bytes.fromhex(body["session_id"])
@ -142,7 +117,7 @@ def remote_cdm_widevine_set_service_certificate(device):
return jsonify({
'status': 400,
'message': f'No CDM session for "{device}" has been opened yet. No session to use'
}), 400
})
certificate = body["certificate"]
try:
@ -151,34 +126,40 @@ def remote_cdm_widevine_set_service_certificate(device):
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
})
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
})
else:
return jsonify({
'status': 400,
'message': f'Unauthorized'
})
@remotecdm_wv_bp.route('/remotecdm/widevine/<device>/get_service_certificate', methods=['POST'])
def remote_cdm_widevine_get_service_certificate(device):
if str(device).lower() == config['default_wv_cdm'].lower():
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
})
session_id = bytes.fromhex(body["session_id"])
@ -188,7 +169,7 @@ def remote_cdm_widevine_get_service_certificate(device):
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)
@ -196,7 +177,7 @@ def remote_cdm_widevine_get_service_certificate(device):
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()
else:
@ -207,17 +188,23 @@ def remote_cdm_widevine_get_service_certificate(device):
'data': {
'service_certificate': service_certificate_b64,
}
}), 200
})
else:
return jsonify({
'status': 400,
'message': f'Unauthorized'
})
@remotecdm_wv_bp.route('/remotecdm/widevine/<device>/get_license_challenge/<license_type>', methods=['POST'])
def remote_cdm_widevine_get_license_challenge(device, license_type):
if str(device).lower() == config['default_wv_cdm'].lower():
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
})
session_id = bytes.fromhex(body["session_id"])
privacy_mode = body.get("privacy_mode", True)
cdm = current_app.config["CDM"]
@ -225,14 +212,14 @@ def remote_cdm_widevine_get_license_challenge(device, license_type):
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
})
current_app.config['pssh'] = body['init_data']
init_data = widevinePSSH(body['init_data'])
@ -248,78 +235,90 @@ def remote_cdm_widevine_get_license_challenge(device, license_type):
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
})
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
})
else:
return jsonify({
'status': 400,
'message': f'Unauthorized'
})
@remotecdm_wv_bp.route('/remotecdm/widevine/<device>/parse_license', methods=['POST'])
def remote_cdm_widevine_parse_license(device):
if str(device).lower() == config['default_wv_cdm'].lower():
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
})
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
})
try:
cdm.parse_license(session_id, body['license_message'])
except InvalidLicenseMessage as error:
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
})
except InvalidSession:
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
})
else:
return jsonify({
'status': 400,
'message': 'Unauthorized'
})
@remotecdm_wv_bp.route('/remotecdm/widevine/<device>/get_keys/<key_type>', methods=['POST'])
def remote_cdm_widevine_get_keys(device, key_type):
if str(device).lower() == config['default_wv_cdm'].lower():
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
})
session_id = bytes.fromhex(body["session_id"])
key_type: Optional[str] = key_type
if key_type == 'ALL':
@ -329,19 +328,19 @@ def remote_cdm_widevine_get_keys(device, key_type):
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
})
except ValueError as error:
return jsonify({
'status': 400,
'message': f'The Key Type value "{key_type}" is invalid, {error}'
}), 400
})
keys_json = [
{
"key_id": key.kid.hex,
@ -360,10 +359,11 @@ def remote_cdm_widevine_get_keys(device, key_type):
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
})

View File

@ -1,54 +0,0 @@
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__)
# 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'])
def change_password_route():
username = session.get('username')
if not username:
return jsonify({'message': 'False'}), 400
try:
data = request.get_json()
new_password = data.get('new_password', '')
if not PASSWORD_REGEX.match(new_password):
return jsonify({'message': 'Invalid password format'}), 400
change_password(username=username, new_password=new_password)
return jsonify({'message': 'True'}), 200
except Exception as e:
return jsonify({'message': 'False'}), 400
@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')
if not username:
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')
if not new_api_key:
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
else:
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

View File

@ -2,7 +2,6 @@ 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
user_info_bp = Blueprint('user_info_bp', __name__)
@ -13,16 +12,14 @@ def user_info():
return jsonify({'message': 'False'}), 400
try:
base_path = os.path.join(os.getcwd(), 'configs', 'CDMs', username.lower())
base_path = os.path.join(os.getcwd(), 'configs', 'CDMs', username)
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)
'Playready_Devices': pr_files
})
except Exception as e:
logging.exception("Error retrieving device files")