Compare commits

...

5 Commits
github ... main

Author SHA1 Message Date
55f1bd9d9b fixed storing login as lowercase to maintain compatibility 2025-05-01 17:01:20 -04:00
6b78ac120f Update user_info.py 2025-04-30 22:16:38 -04:00
3f7538838d Update icon_links.py 2025-04-30 22:02:16 -04:00
c25a891ea9 Dist 2025-04-30 20:18:25 -04:00
29b61668a4 User updates 2025-04-30 20:11:17 -04:00
15 changed files with 909 additions and 461 deletions

View File

@ -8,7 +8,6 @@ pnpm-debug.log*
lerna-debug.log* lerna-debug.log*
node_modules node_modules
dist
dist-ssr dist-ssr
*.local *.local

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:url' content="{{ data.opengraph_url }}" />
<meta property='og:locale' content='en_US' /> <meta property='og:locale' content='en_US' />
<title>{{ data.tab_title }}</title> <title>{{ data.tab_title }}</title>
<script type="module" crossorigin src="/assets/index-C2DUB5KK.js"></script> <script type="module" crossorigin src="/assets/index-DWCLK6jB.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-BXlb7x7c.css"> <link rel="stylesheet" crossorigin href="/assets/index-DQNyIeaF.css">
</head> </head>
<body class="w-full h-full"> <body class="w-full h-full">
<div id="root" class="w-full h-full"></div> <div id="root" class="w-full h-full"></div>

View File

@ -6,6 +6,11 @@ function MyAccount() {
const [prList, setPrList] = useState([]); const [prList, setPrList] = useState([]);
const [uploading, setUploading] = useState(false); const [uploading, setUploading] = useState(false);
const [username, setUsername] = useState(''); 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 // Fetch user info
const fetchUserInfo = async () => { const fetchUserInfo = async () => {
@ -13,7 +18,8 @@ function MyAccount() {
const response = await axios.post('/userinfo'); const response = await axios.post('/userinfo');
setWvList(response.data.Widevine_Devices || []); setWvList(response.data.Widevine_Devices || []);
setPrList(response.data.Playready_Devices || []); setPrList(response.data.Playready_Devices || []);
setUsername(response.data.Username || ''); setUsername(response.data.Styled_Username || '');
setApiKey(response.data.API_Key || '');
} catch (err) { } catch (err) {
console.error('Failed to fetch user info', err); console.error('Failed to fetch user info', err);
} }
@ -60,14 +66,130 @@ 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 ( return (
<div id="myaccount" className="flex flex-row w-full min-h-full overflow-y-auto p-4"> <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 flex-col w-full min-h-full lg:flex-row"> <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">
{/* 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"> <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'} {username ? `${username}` : 'My Account'}
</h1> </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 <button
onClick={handleLogout} onClick={handleLogout}
className="mt-auto w-full h-12 bg-yellow-500/50 rounded-2xl text-2xl text-white" className="mt-auto w-full h-12 bg-yellow-500/50 rounded-2xl text-2xl text-white"
@ -76,11 +198,10 @@ function MyAccount() {
</button> </button>
</div> </div>
{/* Right Panel */} <div className="flex flex-col w-full lg:ml-2 mt-2 lg:mt-0">
<div className="flex flex-col grow lg:ml-2 mt-2 lg:mt-0">
{/* Widevine Section */} {/* 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"> <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="text-2xl font-bold text-white border-b-2 border-white p-2">Widevine CDMs</h1> <h1 className="bg-black 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"> <div className="flex flex-col w-full grow p-2 bg-white/5 rounded-2xl mt-2 text-white text-left">
{wvList.length === 0 ? ( {wvList.length === 0 ? (
<div className="text-white text-center font-bold">No Widevine CDMs uploaded.</div> <div className="text-white text-center font-bold">No Widevine CDMs uploaded.</div>
@ -88,16 +209,14 @@ function MyAccount() {
wvList.map((filename, i) => ( wvList.map((filename, i) => (
<div <div
key={i} key={i}
className={`text-center font-bold text-white p-2 rounded ${ className={`text-center font-bold text-white p-2 rounded ${i % 2 === 0 ? 'bg-black/30' : 'bg-black/60'}`}
i % 2 === 0 ? 'bg-black/30' : 'bg-black/60'
}`}
> >
{filename} {filename}
</div> </div>
)) ))
)} )}
</div> </div>
<label className="bg-yellow-500 text-white w-full h-16 mt-4 rounded-2xl flex items-center justify-center cursor-pointer"> <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">
{uploading ? 'Uploading...' : 'Upload CDM'} {uploading ? 'Uploading...' : 'Upload CDM'}
<input <input
type="file" type="file"
@ -110,7 +229,7 @@ function MyAccount() {
{/* Playready Section */} {/* 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"> <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">Playready CDMs</h1> <h1 className="text-2xl font-bold text-white border-b-2 border-white p-2 bg-black">Playready CDMs</h1>
<div className="flex flex-col w-full bg-white/5 grow rounded-2xl mt-2 text-white text-left p-2"> <div className="flex flex-col w-full bg-white/5 grow rounded-2xl mt-2 text-white text-left p-2">
{prList.length === 0 ? ( {prList.length === 0 ? (
<div className="text-white text-center font-bold">No Playready CDMs uploaded.</div> <div className="text-white text-center font-bold">No Playready CDMs uploaded.</div>
@ -118,16 +237,14 @@ function MyAccount() {
prList.map((filename, i) => ( prList.map((filename, i) => (
<div <div
key={i} key={i}
className={`text-center font-bold text-white p-2 rounded ${ className={`text-center font-bold text-white p-2 rounded ${i % 2 === 0 ? 'bg-black/30' : 'bg-black/60'}`}
i % 2 === 0 ? 'bg-black/30' : 'bg-black/60'
}`}
> >
{filename} {filename}
</div> </div>
)) ))
)} )}
</div> </div>
<label className="bg-yellow-500 text-white w-full h-16 mt-4 rounded-2xl flex items-center justify-center cursor-pointer"> <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">
{uploading ? 'Uploading...' : 'Upload CDM'} {uploading ? 'Uploading...' : 'Upload CDM'}
<input <input
type="file" type="file"
@ -139,7 +256,6 @@ function MyAccount() {
</div> </div>
</div> </div>
</div> </div>
</div>
); );
} }

View File

@ -5,7 +5,20 @@ function Register() {
const [password, setPassword] = useState(''); const [password, setPassword] = useState('');
const [status, setStatus] = 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 () => { 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 { try {
const response = await fetch('/register', { const response = await fetch('/register', {
method: 'POST', method: 'POST',
@ -26,6 +39,15 @@ function Register() {
}; };
const handleLogin = async () => { 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 { try {
const response = await fetch('/login', { const response = await fetch('/login', {
method: 'POST', method: 'POST',

View File

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

View File

@ -11,18 +11,20 @@ def create_user_database():
cursor.execute(''' cursor.execute('''
CREATE TABLE IF NOT EXISTS user_info ( CREATE TABLE IF NOT EXISTS user_info (
Username TEXT PRIMARY KEY, Username TEXT PRIMARY KEY,
Password TEXT Password TEXT,
Styled_Username TEXT,
API_Key TEXT
) )
''') ''')
def add_user(username, password): def add_user(username, password, api_key):
hashed_pw = bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt()) hashed_pw = bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt())
with sqlite3.connect(f'{os.getcwd()}/databases/sql/users.db') as conn: with sqlite3.connect(f'{os.getcwd()}/databases/sql/users.db') as conn:
cursor = conn.cursor() cursor = conn.cursor()
try: try:
cursor.execute('INSERT INTO user_info (Username, Password) VALUES (?, ?)', (username, hashed_pw)) cursor.execute('INSERT INTO user_info (Username, Password, Styled_Username, API_Key) VALUES (?, ?, ?, ?)', (username.lower(), hashed_pw, username, api_key))
conn.commit() conn.commit()
return True return True
except sqlite3.IntegrityError: except sqlite3.IntegrityError:
@ -32,7 +34,7 @@ def add_user(username, password):
def verify_user(username, password): def verify_user(username, password):
with sqlite3.connect(f'{os.getcwd()}/databases/sql/users.db') as conn: with sqlite3.connect(f'{os.getcwd()}/databases/sql/users.db') as conn:
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute('SELECT Password FROM user_info WHERE Username = ?', (username,)) cursor.execute('SELECT Password FROM user_info WHERE Username = ?', (username.lower(),))
result = cursor.fetchone() result = cursor.fetchone()
if result: if result:
@ -43,3 +45,56 @@ def verify_user(username, password):
return bcrypt.checkpw(password.encode('utf-8'), stored_hash) return bcrypt.checkpw(password.encode('utf-8'), stored_hash)
else: else:
return False return False
def fetch_api_key(username):
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

@ -12,6 +12,7 @@ from routes.upload import upload_bp
from routes.user_info import user_info_bp from routes.user_info import user_info_bp
from routes.register import register_bp from routes.register import register_bp
from routes.login import login_bp from routes.login import login_bp
from routes.user_changes import user_change_bp
import os import os
import yaml import yaml
app = Flask(__name__) app = Flask(__name__)
@ -30,6 +31,7 @@ app.register_blueprint(user_info_bp)
app.register_blueprint(upload_bp) app.register_blueprint(upload_bp)
app.register_blueprint(remotecdm_wv_bp) app.register_blueprint(remotecdm_wv_bp)
app.register_blueprint(remotecdm_pr_bp) app.register_blueprint(remotecdm_pr_bp)
app.register_blueprint(user_change_bp)
if __name__ == '__main__': if __name__ == '__main__':
app.run(debug=True, host='0.0.0.0') app.run(debug=True, host='0.0.0.0')

View File

@ -15,7 +15,7 @@ def login():
return jsonify({'error': f'Missing required field: {required_field}'}), 400 return jsonify({'error': f'Missing required field: {required_field}'}), 400
if verify_user(data['username'], data['password']): if verify_user(data['username'], data['password']):
session['username'] = data['username'] # Stored securely in a signed cookie session['username'] = data['username'].lower() # Stored securely in a signed cookie
return jsonify({'message': 'Successfully logged in!'}) return jsonify({'message': 'Successfully logged in!'})
else: else:
return jsonify({'error': 'Invalid username or password!'}), 401 return jsonify({'error': 'Invalid username or password!'}), 401

View File

@ -1,29 +1,42 @@
import re
from flask import Blueprint, request, jsonify from flask import Blueprint, request, jsonify
from custom_functions.database.user_db import add_user from custom_functions.database.user_db import add_user
import uuid
register_bp = Blueprint( register_bp = Blueprint('register_bp', __name__)
'register_bp',
__name__, 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(): def register():
if request.method == 'POST': if request.method != 'POST':
return jsonify({'error': 'Method not supported'}), 405
data = request.get_json() 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: 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({ return jsonify({
'error': f'Missing required field: {required_field}' 'error': 'Invalid username. Only letters, numbers, hyphens, and underscores are allowed.'
}) }), 400
if add_user(data['username'], data['password']):
if not PASSWORD_REGEX.fullmatch(password):
return jsonify({ return jsonify({
'message': 'User successfully registered!' '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
else: else:
return jsonify({ return jsonify({'error': 'User already exists!'}), 409
'error': 'User already exists!'
})
else:
return jsonify({
'error': 'Method not supported'
})

View File

@ -5,6 +5,8 @@ from pyplayready.device import Device as PlayReadyDevice
from pyplayready.cdm import Cdm as PlayReadyCDM from pyplayready.cdm import Cdm as PlayReadyCDM
from pyplayready import PSSH as PlayReadyPSSH from pyplayready import PSSH as PlayReadyPSSH
from pyplayready.exceptions import (InvalidSession, TooManySessions, InvalidLicense, InvalidPssh) from pyplayready.exceptions import (InvalidSession, TooManySessions, InvalidLicense, InvalidPssh)
from custom_functions.database.user_db import fetch_username_by_api_key
from custom_functions.user_checks.device_allowed import user_allowed_to_use_device
@ -53,44 +55,67 @@ 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']) @remotecdm_pr_bp.route('/remotecdm/playready/<device>/close/<session_id>', methods=['GET'])
def remote_cdm_playready_close(device, session_id): def remote_cdm_playready_close(device, session_id):
if str(device).lower() == config['default_pr_cdm'].lower(): try:
session_id = bytes.fromhex(session_id) session_id = bytes.fromhex(session_id)
cdm = current_app.config["CDM"] cdm = current_app.config["CDM"]
if not cdm: if not cdm:
return jsonify({ return jsonify({
'status': 400,
'message': f'No CDM for "{device}" has been opened yet. No session to close' 'message': f'No CDM for "{device}" has been opened yet. No session to close'
}) }), 400
try: try:
cdm.close(session_id) cdm.close(session_id)
except InvalidSession: except InvalidSession:
return jsonify({ return jsonify({
'status': 400,
'message': f'Invalid session ID "{session_id.hex()}", it may have expired' 'message': f'Invalid session ID "{session_id.hex()}", it may have expired'
}) }), 400
return jsonify({ return jsonify({
'status': 200,
'message': f'Successfully closed Session "{session_id.hex()}".', 'message': f'Successfully closed Session "{session_id.hex()}".',
}) }), 200
else: except Exception as e:
return jsonify({ return jsonify({
'status': 400, 'message': f'Failed to close Session "{session_id.hex()}".'
'message': f'Unauthorized' }), 400
})
@remotecdm_pr_bp.route('/remotecdm/playready/<device>/get_license_challenge', methods=['POST']) @remotecdm_pr_bp.route('/remotecdm/playready/<device>/get_license_challenge', methods=['POST'])
def remote_cdm_playready_get_license_challenge(device): def remote_cdm_playready_get_license_challenge(device):
if str(device).lower() == config['default_pr_cdm'].lower():
body = request.get_json() body = request.get_json()
for required_field in ("session_id", "init_data"): for required_field in ("session_id", "init_data"):
if not body.get(required_field): if not body.get(required_field):
return jsonify({ return jsonify({
'status': 400,
'message': f'Missing required field "{required_field}" in JSON body' 'message': f'Missing required field "{required_field}" in JSON body'
}) }), 400
cdm = current_app.config["CDM"] cdm = current_app.config["CDM"]
session_id = bytes.fromhex(body["session_id"]) session_id = bytes.fromhex(body["session_id"])
init_data = body["init_data"] init_data = body["init_data"]
@ -125,7 +150,6 @@ def remote_cdm_playready_get_license_challenge(device):
@remotecdm_pr_bp.route('/remotecdm/playready/<device>/parse_license', methods=['POST']) @remotecdm_pr_bp.route('/remotecdm/playready/<device>/parse_license', methods=['POST'])
def remote_cdm_playready_parse_license(device): def remote_cdm_playready_parse_license(device):
if str(device).lower() == config['default_pr_cdm'].lower():
body = request.get_json() body = request.get_json()
for required_field in ("license_message", "session_id"): for required_field in ("license_message", "session_id"):
if not body.get(required_field): if not body.get(required_field):
@ -159,7 +183,6 @@ def remote_cdm_playready_parse_license(device):
@remotecdm_pr_bp.route('/remotecdm/playready/<device>/get_keys', methods=['POST']) @remotecdm_pr_bp.route('/remotecdm/playready/<device>/get_keys', methods=['POST'])
def remote_cdm_playready_get_keys(device): def remote_cdm_playready_get_keys(device):
if str(device).lower() == config['default_pr_cdm'].lower():
body = request.get_json() body = request.get_json()
for required_field in ("session_id",): for required_field in ("session_id",):
if not body.get(required_field): if not body.get(required_field):

View File

@ -11,6 +11,8 @@ from pywidevine.exceptions import (InvalidContext, InvalidInitData, InvalidLicen
InvalidSession, SignatureMismatch, TooManySessions) InvalidSession, SignatureMismatch, TooManySessions)
import yaml import yaml
from custom_functions.database.user_db import fetch_api_key, fetch_username_by_api_key
from custom_functions.user_checks.device_allowed import user_allowed_to_use_device
remotecdm_wv_bp = Blueprint('remotecdm_wv', __name__) remotecdm_wv_bp = Blueprint('remotecdm_wv', __name__)
with open(f'{os.getcwd()}/configs/config.yaml', 'r') as file: with open(f'{os.getcwd()}/configs/config.yaml', 'r') as file:
@ -61,43 +63,66 @@ def remote_cdm_widevine_open(device):
'security_level': cdm.security_level, '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: else:
return jsonify({ return jsonify({
'status': 400, 'message': f"Device '{device}' is not found or you are not authorized to use it.",
'message': 'Unauthorized' '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
@remotecdm_wv_bp.route('/remotecdm/widevine/<device>/close/<session_id>', methods=['GET']) @remotecdm_wv_bp.route('/remotecdm/widevine/<device>/close/<session_id>', methods=['GET'])
def remote_cdm_widevine_close(device, session_id): def remote_cdm_widevine_close(device, session_id):
if str(device).lower() == config['default_wv_cdm'].lower():
session_id = bytes.fromhex(session_id) session_id = bytes.fromhex(session_id)
cdm = current_app.config["CDM"] cdm = current_app.config["CDM"]
if not cdm: if not cdm:
return jsonify({ return jsonify({
'status': 400, 'status': 400,
'message': f'No CDM for "{device}" has been opened yet. No session to close' 'message': f'No CDM for "{device}" has been opened yet. No session to close'
}) }), 400
try: try:
cdm.close(session_id) cdm.close(session_id)
except InvalidSession: except InvalidSession:
return jsonify({ return jsonify({
'status': 400, 'status': 400,
'message': f'Invalid session ID "{session_id.hex()}", it may have expired' 'message': f'Invalid session ID "{session_id.hex()}", it may have expired'
}) }), 400
return jsonify({ return jsonify({
'status': 200, 'status': 200,
'message': f'Successfully closed Session "{session_id.hex()}".', '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']) @remotecdm_wv_bp.route('/remotecdm/widevine/<device>/set_service_certificate', methods=['POST'])
def remote_cdm_widevine_set_service_certificate(device): def remote_cdm_widevine_set_service_certificate(device):
if str(device).lower() == config['default_wv_cdm'].lower():
body = request.get_json() body = request.get_json()
for required_field in ("session_id", "certificate"): for required_field in ("session_id", "certificate"):
if required_field == "certificate": if required_field == "certificate":
@ -108,7 +133,7 @@ def remote_cdm_widevine_set_service_certificate(device):
return jsonify({ return jsonify({
'status': 400, 'status': 400,
'message': f'Missing required field "{required_field}" in JSON body' 'message': f'Missing required field "{required_field}" in JSON body'
}) }), 400
session_id = bytes.fromhex(body["session_id"]) session_id = bytes.fromhex(body["session_id"])
@ -117,7 +142,7 @@ def remote_cdm_widevine_set_service_certificate(device):
return jsonify({ return jsonify({
'status': 400, 'status': 400,
'message': f'No CDM session for "{device}" has been opened yet. No session to use' 'message': f'No CDM session for "{device}" has been opened yet. No session to use'
}) }), 400
certificate = body["certificate"] certificate = body["certificate"]
try: try:
@ -126,40 +151,34 @@ def remote_cdm_widevine_set_service_certificate(device):
return jsonify({ return jsonify({
'status': 400, 'status': 400,
'message': f'Invalid session id: "{session_id.hex()}", it may have expired' 'message': f'Invalid session id: "{session_id.hex()}", it may have expired'
}) }), 400
except DecodeError as error: except DecodeError as error:
return jsonify({ return jsonify({
'status': 400, 'status': 400,
'message': f'Invalid Service Certificate, {error}' 'message': f'Invalid Service Certificate, {error}'
}) }), 400
except SignatureMismatch: except SignatureMismatch:
return jsonify({ return jsonify({
'status': 400, 'status': 400,
'message': 'Signature Validation failed on the Service Certificate, rejecting' 'message': 'Signature Validation failed on the Service Certificate, rejecting'
}) }), 400
return jsonify({ return jsonify({
'status': 200, 'status': 200,
'message': f"Successfully {['set', 'unset'][not certificate]} the Service Certificate.", 'message': f"Successfully {['set', 'unset'][not certificate]} the Service Certificate.",
'data': { 'data': {
'provider_id': provider_id, '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']) @remotecdm_wv_bp.route('/remotecdm/widevine/<device>/get_service_certificate', methods=['POST'])
def remote_cdm_widevine_get_service_certificate(device): def remote_cdm_widevine_get_service_certificate(device):
if str(device).lower() == config['default_wv_cdm'].lower():
body = request.get_json() body = request.get_json()
for required_field in ("session_id",): for required_field in ("session_id",):
if not body.get(required_field): if not body.get(required_field):
return jsonify({ return jsonify({
'status': 400, 'status': 400,
'message': f'Missing required field "{required_field}" in JSON body' 'message': f'Missing required field "{required_field}" in JSON body'
}) }), 400
session_id = bytes.fromhex(body["session_id"]) session_id = bytes.fromhex(body["session_id"])
@ -169,7 +188,7 @@ def remote_cdm_widevine_get_service_certificate(device):
return jsonify({ return jsonify({
'status': 400, 'status': 400,
'message': f'No CDM session for "{device}" has been opened yet. No session to use' 'message': f'No CDM session for "{device}" has been opened yet. No session to use'
}) }), 400
try: try:
service_certificate = cdm.get_service_certificate(session_id) service_certificate = cdm.get_service_certificate(session_id)
@ -177,7 +196,7 @@ def remote_cdm_widevine_get_service_certificate(device):
return jsonify({ return jsonify({
'status': 400, 'status': 400,
'message': f'Invalid Session ID "{session_id.hex()}", it may have expired' 'message': f'Invalid Session ID "{session_id.hex()}", it may have expired'
}) }), 400
if service_certificate: if service_certificate:
service_certificate_b64 = base64.b64encode(service_certificate.SerializeToString()).decode() service_certificate_b64 = base64.b64encode(service_certificate.SerializeToString()).decode()
else: else:
@ -188,23 +207,17 @@ def remote_cdm_widevine_get_service_certificate(device):
'data': { 'data': {
'service_certificate': service_certificate_b64, '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']) @remotecdm_wv_bp.route('/remotecdm/widevine/<device>/get_license_challenge/<license_type>', methods=['POST'])
def remote_cdm_widevine_get_license_challenge(device, license_type): def remote_cdm_widevine_get_license_challenge(device, license_type):
if str(device).lower() == config['default_wv_cdm'].lower():
body = request.get_json() body = request.get_json()
for required_field in ("session_id", "init_data"): for required_field in ("session_id", "init_data"):
if not body.get(required_field): if not body.get(required_field):
return jsonify({ return jsonify({
'status': 400, 'status': 400,
'message': f'Missing required field "{required_field}" in JSON body' 'message': f'Missing required field "{required_field}" in JSON body'
}) }), 400
session_id = bytes.fromhex(body["session_id"]) session_id = bytes.fromhex(body["session_id"])
privacy_mode = body.get("privacy_mode", True) privacy_mode = body.get("privacy_mode", True)
cdm = current_app.config["CDM"] cdm = current_app.config["CDM"]
@ -212,14 +225,14 @@ def remote_cdm_widevine_get_license_challenge(device, license_type):
return jsonify({ return jsonify({
'status': 400, 'status': 400,
'message': f'No CDM session for "{device}" has been opened yet. No session to use' 'message': f'No CDM session for "{device}" has been opened yet. No session to use'
}) }), 400
if current_app.config.get("force_privacy_mode"): if current_app.config.get("force_privacy_mode"):
privacy_mode = True privacy_mode = True
if not cdm.get_service_certificate(session_id): if not cdm.get_service_certificate(session_id):
return jsonify({ return jsonify({
'status': 403, 'status': 403,
'message': 'No Service Certificate set but Privacy Mode is Enforced.' 'message': 'No Service Certificate set but Privacy Mode is Enforced.'
}) }), 403
current_app.config['pssh'] = body['init_data'] current_app.config['pssh'] = body['init_data']
init_data = widevinePSSH(body['init_data']) init_data = widevinePSSH(body['init_data'])
@ -235,90 +248,78 @@ def remote_cdm_widevine_get_license_challenge(device, license_type):
return jsonify({ return jsonify({
'status': 400, 'status': 400,
'message': f'Invalid Session ID "{session_id.hex()}", it may have expired' 'message': f'Invalid Session ID "{session_id.hex()}", it may have expired'
}) }), 400
except InvalidInitData as error: except InvalidInitData as error:
return jsonify({ return jsonify({
'status': 400, 'status': 400,
'message': f'Invalid Init Data, {error}' 'message': f'Invalid Init Data, {error}'
}) }), 400
except InvalidLicenseType: except InvalidLicenseType:
return jsonify({ return jsonify({
'status': 400, 'status': 400,
'message': f'Invalid License Type {license_type}' 'message': f'Invalid License Type {license_type}'
}) }), 400
return jsonify({ return jsonify({
'status': 200, 'status': 200,
'message': 'Success', 'message': 'Success',
'data': { 'data': {
'challenge_b64': base64.b64encode(license_request).decode() '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']) @remotecdm_wv_bp.route('/remotecdm/widevine/<device>/parse_license', methods=['POST'])
def remote_cdm_widevine_parse_license(device): def remote_cdm_widevine_parse_license(device):
if str(device).lower() == config['default_wv_cdm'].lower():
body = request.get_json() body = request.get_json()
for required_field in ("session_id", "license_message"): for required_field in ("session_id", "license_message"):
if not body.get(required_field): if not body.get(required_field):
return jsonify({ return jsonify({
'status': 400, 'status': 400,
'message': f'Missing required field "{required_field}" in JSON body' 'message': f'Missing required field "{required_field}" in JSON body'
}) }), 400
session_id = bytes.fromhex(body["session_id"]) session_id = bytes.fromhex(body["session_id"])
cdm = current_app.config["CDM"] cdm = current_app.config["CDM"]
if not cdm: if not cdm:
return jsonify({ return jsonify({
'status': 400, 'status': 400,
'message': f'No CDM session for "{device}" has been opened yet. No session to use' 'message': f'No CDM session for "{device}" has been opened yet. No session to use'
}) }), 400
try: try:
cdm.parse_license(session_id, body['license_message']) cdm.parse_license(session_id, body['license_message'])
except InvalidLicenseMessage as error: except InvalidLicenseMessage as error:
return jsonify({ return jsonify({
'status': 400, 'status': 400,
'message': f'Invalid License Message, {error}' 'message': f'Invalid License Message, {error}'
}) }), 400
except InvalidContext as error: except InvalidContext as error:
return jsonify({ return jsonify({
'status': 400, 'status': 400,
'message': f'Invalid Context, {error}' 'message': f'Invalid Context, {error}'
}) }), 400
except InvalidSession: except InvalidSession:
return jsonify({ return jsonify({
'status': 400, 'status': 400,
'message': f'Invalid Session ID "{session_id.hex()}", it may have expired' 'message': f'Invalid Session ID "{session_id.hex()}", it may have expired'
}) }), 400
except SignatureMismatch: except SignatureMismatch:
return jsonify({ return jsonify({
'status': 400, 'status': 400,
'message': f'Signature Validation failed on the License Message, rejecting.' 'message': f'Signature Validation failed on the License Message, rejecting.'
}) }), 400
return jsonify({ return jsonify({
'status': 200, 'status': 200,
'message': 'Successfully parsed and loaded the Keys from the License message.', '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']) @remotecdm_wv_bp.route('/remotecdm/widevine/<device>/get_keys/<key_type>', methods=['POST'])
def remote_cdm_widevine_get_keys(device, key_type): def remote_cdm_widevine_get_keys(device, key_type):
if str(device).lower() == config['default_wv_cdm'].lower():
body = request.get_json() body = request.get_json()
for required_field in ("session_id",): for required_field in ("session_id",):
if not body.get(required_field): if not body.get(required_field):
return jsonify({ return jsonify({
'status': 400, 'status': 400,
'message': f'Missing required field "{required_field}" in JSON body' 'message': f'Missing required field "{required_field}" in JSON body'
}) }), 400
session_id = bytes.fromhex(body["session_id"]) session_id = bytes.fromhex(body["session_id"])
key_type: Optional[str] = key_type key_type: Optional[str] = key_type
if key_type == 'ALL': if key_type == 'ALL':
@ -328,19 +329,19 @@ def remote_cdm_widevine_get_keys(device, key_type):
return jsonify({ return jsonify({
'status': 400, 'status': 400,
'message': f'No CDM session for "{device}" has been opened yet. No session to use' 'message': f'No CDM session for "{device}" has been opened yet. No session to use'
}) }), 400
try: try:
keys = cdm.get_keys(session_id, key_type) keys = cdm.get_keys(session_id, key_type)
except InvalidSession: except InvalidSession:
return jsonify({ return jsonify({
'status': 400, 'status': 400,
'message': f'Invalid Session ID "{session_id.hex()}", it may have expired' 'message': f'Invalid Session ID "{session_id.hex()}", it may have expired'
}) }), 400
except ValueError as error: except ValueError as error:
return jsonify({ return jsonify({
'status': 400, 'status': 400,
'message': f'The Key Type value "{key_type}" is invalid, {error}' 'message': f'The Key Type value "{key_type}" is invalid, {error}'
}) }), 400
keys_json = [ keys_json = [
{ {
"key_id": key.kid.hex, "key_id": key.kid.hex,
@ -359,11 +360,10 @@ def remote_cdm_widevine_get_keys(device, key_type):
if entry['type'] != 'SIGNING': if entry['type'] != 'SIGNING':
cache_to_db(pssh=str(current_app.config['pssh']), kid=entry['key_id'], key=entry['key']) cache_to_db(pssh=str(current_app.config['pssh']), kid=entry['key_id'], key=entry['key'])
return jsonify({ return jsonify({
'status': 200, 'status': 200,
'message': 'Success', 'message': 'Success',
'data': { 'data': {
'keys': keys_json 'keys': keys_json
} }
}) }), 200

54
routes/user_changes.py Normal file
View File

@ -0,0 +1,54 @@
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,6 +2,7 @@ from flask import Blueprint, request, jsonify, session
import os import os
import glob import glob
import logging import logging
from custom_functions.database.user_db import fetch_api_key, fetch_styled_username
user_info_bp = Blueprint('user_info_bp', __name__) user_info_bp = Blueprint('user_info_bp', __name__)
@ -12,14 +13,16 @@ def user_info():
return jsonify({'message': 'False'}), 400 return jsonify({'message': 'False'}), 400
try: try:
base_path = os.path.join(os.getcwd(), 'configs', 'CDMs', username) base_path = os.path.join(os.getcwd(), 'configs', 'CDMs', username.lower())
pr_files = [os.path.basename(f) for f in glob.glob(os.path.join(base_path, 'PR', '*.prd'))] pr_files = [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'))] wv_files = [os.path.basename(f) for f in glob.glob(os.path.join(base_path, 'WV', '*.wvd'))]
return jsonify({ return jsonify({
'Username': username, 'Username': username,
'Widevine_Devices': wv_files, 'Widevine_Devices': wv_files,
'Playready_Devices': pr_files 'Playready_Devices': pr_files,
'API_Key': fetch_api_key(username),
'Styled_Username': fetch_styled_username(username)
}) })
except Exception as e: except Exception as e:
logging.exception("Error retrieving device files") logging.exception("Error retrieving device files")