diff --git a/cdrm-frontend/dist/index.html b/cdrm-frontend/dist/index.html index cbe53ec..26ef02c 100644 --- a/cdrm-frontend/dist/index.html +++ b/cdrm-frontend/dist/index.html @@ -12,8 +12,8 @@ {{ data.tab_title }} - - + +
diff --git a/cdrm-frontend/src/components/Pages/MyAccount.jsx b/cdrm-frontend/src/components/Pages/MyAccount.jsx index fa02027..2c424f2 100644 --- a/cdrm-frontend/src/components/Pages/MyAccount.jsx +++ b/cdrm-frontend/src/components/Pages/MyAccount.jsx @@ -6,6 +6,11 @@ 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 () => { @@ -13,7 +18,8 @@ function MyAccount() { const response = await axios.post('/userinfo'); setWvList(response.data.Widevine_Devices || []); setPrList(response.data.Playready_Devices || []); - setUsername(response.data.Username || ''); + setUsername(response.data.Styled_Username || ''); + setApiKey(response.data.API_Key || ''); } catch (err) { console.error('Failed to fetch user info', err); } @@ -60,83 +66,193 @@ 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 ( -
-
- {/* Left Panel */} -
-

- {username ? `${username}` : 'My Account'} -

+
+
+

+ {username ? `${username}` : 'My Account'} +

+ + {/* API Key Section */} +
+ + + + {/* New API Key Section */} + + { + 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 &&

{apiKeyError}

} + + {/* Change Password Section */} + + { + 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 &&

{passwordError}

} +
- {/* Right Panel */} -
- {/* Widevine Section */} -
-

Widevine CDMs

-
- {wvList.length === 0 ? ( -
No Widevine CDMs uploaded.
- ) : ( - wvList.map((filename, i) => ( -
- {filename} -
- )) - )} -
- -
+ +
- {/* Playready Section */} -
-

Playready CDMs

-
- {prList.length === 0 ? ( -
No Playready CDMs uploaded.
- ) : ( - prList.map((filename, i) => ( -
- {filename} -
- )) - )} -
- +
+ {/* Widevine Section */} +
+

Widevine CDMs

+
+ {wvList.length === 0 ? ( +
No Widevine CDMs uploaded.
+ ) : ( + wvList.map((filename, i) => ( +
+ {filename} +
+ )) + )}
+ +
+ + {/* Playready Section */} +
+

Playready CDMs

+
+ {prList.length === 0 ? ( +
No Playready CDMs uploaded.
+ ) : ( + prList.map((filename, i) => ( +
+ {filename} +
+ )) + )} +
+
diff --git a/cdrm-frontend/src/components/Pages/Register.jsx b/cdrm-frontend/src/components/Pages/Register.jsx index 7bd646c..f337297 100644 --- a/cdrm-frontend/src/components/Pages/Register.jsx +++ b/cdrm-frontend/src/components/Pages/Register.jsx @@ -5,7 +5,20 @@ 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', @@ -26,6 +39,15 @@ 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', diff --git a/custom_functions/database/user_db.py b/custom_functions/database/user_db.py index f8f3775..d668bf6 100644 --- a/custom_functions/database/user_db.py +++ b/custom_functions/database/user_db.py @@ -11,18 +11,20 @@ def create_user_database(): cursor.execute(''' CREATE TABLE IF NOT EXISTS user_info ( 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()) with sqlite3.connect(f'{os.getcwd()}/databases/sql/users.db') as conn: cursor = conn.cursor() 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() return True except sqlite3.IntegrityError: @@ -32,7 +34,7 @@ def add_user(username, password): 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,)) + cursor.execute('SELECT Password FROM user_info WHERE Username = ?', (username.lower(),)) result = cursor.fetchone() if result: @@ -43,3 +45,56 @@ 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 \ No newline at end of file diff --git a/main.py b/main.py index 0edade8..ede50e1 100644 --- a/main.py +++ b/main.py @@ -12,6 +12,7 @@ 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__) @@ -30,6 +31,7 @@ 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') \ No newline at end of file diff --git a/routes/register.py b/routes/register.py index 1b0dcd9..cbbe283 100644 --- a/routes/register.py +++ b/routes/register.py @@ -1,29 +1,42 @@ +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__, -) +register_bp = Blueprint('register_bp', __name__) + +USERNAME_REGEX = re.compile(r'^[A-Za-z0-9_-]+$') +PASSWORD_REGEX = re.compile(r'^\S+$') @register_bp.route('/register', methods=['POST']) def register(): - if request.method == 'POST': - data = request.get_json() - for required_field in ['username', 'password']: - if required_field not in data: - return jsonify({ - 'error': f'Missing required field: {required_field}' - }) - if add_user(data['username'], data['password']): - return jsonify({ - 'message': 'User successfully registered!' - }) - else: - return jsonify({ - 'error': 'User already exists!' - }) - else: + if request.method != 'POST': + return jsonify({'error': 'Method not supported'}), 405 + + 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': 'Method not supported' - }) \ No newline at end of file + '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 + + # Attempt to add user + if add_user(username, password, api_key): + return jsonify({'message': 'User successfully registered!'}), 201 + else: + return jsonify({'error': 'User already exists!'}), 409 diff --git a/routes/remote_device_pr.py b/routes/remote_device_pr.py index 45076fc..29188a3 100644 --- a/routes/remote_device_pr.py +++ b/routes/remote_device_pr.py @@ -5,6 +5,8 @@ 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 @@ -53,148 +55,169 @@ 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//close/', methods=['GET']) def remote_cdm_playready_close(device, session_id): - if str(device).lower() == config['default_pr_cdm'].lower(): + try: 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()}".', - }) - else: + }), 200 + except Exception as e: return jsonify({ - 'status': 400, - 'message': f'Unauthorized' - }) + 'message': f'Failed to close Session "{session_id.hex()}".' + }), 400 @remotecdm_pr_bp.route('/remotecdm/playready//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' - }) - cdm = current_app.config["CDM"] - session_id = bytes.fromhex(body["session_id"]) - init_data = body["init_data"] - if not init_data.startswith("/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): - return jsonify({ - 'message': f'Missing required field "{required_field}" in JSON body' - }) - cdm = current_app.config["CDM"] - if not cdm: + body = request.get_json() + for required_field in ("license_message", "session_id"): + if not body.get(required_field): 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"] - try: - cdm.parse_license(session_id, license_message) - except InvalidSession: - 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}" - }) - except Exception as e: - return jsonify({ - 'message': f"Error, {e}" + 'message': f'Missing required field "{required_field}" in JSON body' }) + cdm = current_app.config["CDM"] + if not cdm: return jsonify({ - 'message': 'Successfully parsed and loaded the Keys from the License message' + '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"] + try: + cdm.parse_license(session_id, license_message) + except InvalidSession: + 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}" + }) + except Exception as e: + 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//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): - 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: + 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." + '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." - }) - except Exception as e: - return jsonify({ - 'message': f"Error, {e}" - }) - keys_json = [ - { - "key_id": key.key_id.hex, - "key": key.key.hex(), - "type": key.key_type.value, - "cipher_type": key.cipher_type.value, - "key_length": key.key_length, - } - for key in keys - ] + session_id = bytes.fromhex(body["session_id"]) + cdm = current_app.config["CDM"] + if not cdm: return jsonify({ - 'message': 'success', - 'data': { - 'keys': keys_json - } - }) \ No newline at end of file + '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." + }) + except Exception as e: + return jsonify({ + 'message': f"Error, {e}" + }) + keys_json = [ + { + "key_id": key.key_id.hex, + "key": key.key.hex(), + "type": key.key_type.value, + "cipher_type": key.cipher_type.value, + "key_length": key.key_length, + } + for key in keys + ] + return jsonify({ + 'message': 'success', + 'data': { + 'keys': keys_json + } + }) \ No newline at end of file diff --git a/routes/remote_device_wv.py b/routes/remote_device_wv.py index 2339846..080bc02 100644 --- a/routes/remote_device_wv.py +++ b/routes/remote_device_wv.py @@ -11,6 +11,8 @@ 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: @@ -61,309 +63,307 @@ 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({ - 'status': 400, - 'message': 'Unauthorized' - }) + 'message': f"Device '{device}' is not found or you are not authorized to use it.", + 'status': 403 + }), 403 + @remotecdm_wv_bp.route('/remotecdm/widevine//close/', 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()}".', - }) - else: - return jsonify({ - 'status': 400, - 'message': f'Unauthorized' - }) + }), 200 @remotecdm_wv_bp.route('/remotecdm/widevine//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": - 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' - }) + 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 + 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 - session_id = bytes.fromhex(body["session_id"]) + 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' - }) - - 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' - }) - except DecodeError as error: - return jsonify({ - 'status': 400, - 'message': f'Invalid Service Certificate, {error}' - }) - except SignatureMismatch: - return jsonify({ - 'status': 400, - 'message': 'Signature Validation failed on the Service Certificate, rejecting' - }) - return jsonify({ - 'status': 200, - 'message': f"Successfully {['set', 'unset'][not certificate]} the Service Certificate.", - 'data': { - 'provider_id': provider_id, - } - }) - else: + cdm = current_app.config["CDM"] + if not cdm: return jsonify({ 'status': 400, - 'message': f'Unauthorized' - }) + '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 + 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 @remotecdm_wv_bp.route('/remotecdm/widevine//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' - }) - - session_id = bytes.fromhex(body["session_id"]) - - cdm = current_app.config["CDM"] - - if not cdm: + body = request.get_json() + for required_field in ("session_id",): + if not body.get(required_field): return jsonify({ 'status': 400, - 'message': f'No CDM session for "{device}" has been opened yet. No session to use' - }) + 'message': f'Missing required field "{required_field}" in JSON body' + }), 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' - }) - if service_certificate: - 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, - } - }) - else: + session_id = bytes.fromhex(body["session_id"]) + + cdm = current_app.config["CDM"] + + if not cdm: return jsonify({ 'status': 400, - 'message': f'Unauthorized' - }) + '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 + if service_certificate: + 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 @remotecdm_wv_bp.route('/remotecdm/widevine//get_license_challenge/', 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' - }) - session_id = bytes.fromhex(body["session_id"]) - privacy_mode = body.get("privacy_mode", True) - cdm = current_app.config["CDM"] - if not cdm: + body = request.get_json() + for required_field in ("session_id", "init_data"): + if not body.get(required_field): return jsonify({ 'status': 400, - 'message': f'No CDM session for "{device}" has been opened yet. No session to use' - }) - 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.' - }) - - 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 - ) - except InvalidSession: - return jsonify({ - 'status': 400, - 'message': f'Invalid Session ID "{session_id.hex()}", it may have expired' - }) - except InvalidInitData as error: - return jsonify({ - 'status': 400, - 'message': f'Invalid Init Data, {error}' - }) - except InvalidLicenseType: - return jsonify({ - 'status': 400, - 'message': f'Invalid License Type {license_type}' - }) - return jsonify({ - 'status': 200, - 'message': 'Success', - 'data': { - 'challenge_b64': base64.b64encode(license_request).decode() - } - }) - else: + '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'Unauthorized' - }) + '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']) + + try: + license_request = cdm.get_license_challenge( + session_id=session_id, + pssh=init_data, + license_type=license_type, + privacy_mode=privacy_mode + ) + except InvalidSession: + 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 @remotecdm_wv_bp.route('/remotecdm/widevine//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' - }) - session_id = bytes.fromhex(body["session_id"]) - cdm = current_app.config["CDM"] - if not cdm: + body = request.get_json() + for required_field in ("session_id", "license_message"): + if not body.get(required_field): return jsonify({ 'status': 400, - 'message': f'No CDM session for "{device}" has been opened yet. No session to use' - }) - try: - cdm.parse_license(session_id, body['license_message']) - except InvalidLicenseMessage as error: - return jsonify({ - 'status': 400, - 'message': f'Invalid License Message, {error}' - }) - except InvalidContext as error: - return jsonify({ - 'status': 400, - 'message': f'Invalid Context, {error}' - }) - except InvalidSession: - return jsonify({ - 'status': 400, - 'message': f'Invalid Session ID "{session_id.hex()}", it may have expired' - }) - except SignatureMismatch: - return jsonify({ - 'status': 400, - 'message': f'Signature Validation failed on the License Message, rejecting.' - }) - return jsonify({ - 'status': 200, - 'message': 'Successfully parsed and loaded the Keys from the License message.', - }) - else: + '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': 'Unauthorized' - }) + '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 @remotecdm_wv_bp.route('/remotecdm/widevine//get_keys/', 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' - }) - session_id = bytes.fromhex(body["session_id"]) - key_type: Optional[str] = key_type - if key_type == 'ALL': - key_type = None - cdm = current_app.config["CDM"] - if not cdm: + body = request.get_json() + for required_field in ("session_id",): + if not body.get(required_field): return jsonify({ 'status': 400, - 'message': f'No CDM session for "{device}" has been opened yet. No session to use' - }) - 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' - }) - except ValueError as error: - return jsonify({ - 'status': 400, - 'message': f'The Key Type value "{key_type}" is invalid, {error}' - }) - keys_json = [ - { - "key_id": key.kid.hex, - "key": key.key.hex(), - "type": key.type, - "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': - from custom_functions.database.cache_to_db_sqlite import cache_to_db - 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']) - - + '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': + key_type = None + cdm = current_app.config["CDM"] + if not cdm: return jsonify({ - 'status': 200, - 'message': 'Success', - 'data': { - 'keys': keys_json - } - }) \ No newline at end of file + '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, + "key": key.key.hex(), + "type": key.type, + "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': + from custom_functions.database.cache_to_db_sqlite import cache_to_db + 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']) + + return jsonify({ + 'status': 200, + 'message': 'Success', + 'data': { + 'keys': keys_json + } + }), 200 \ No newline at end of file diff --git a/routes/user_changes.py b/routes/user_changes.py new file mode 100644 index 0000000..ec4014c --- /dev/null +++ b/routes/user_changes.py @@ -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 \ No newline at end of file diff --git a/routes/user_info.py b/routes/user_info.py index 845af3c..e32611f 100644 --- a/routes/user_info.py +++ b/routes/user_info.py @@ -2,6 +2,7 @@ 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__) @@ -19,7 +20,9 @@ def user_info(): return jsonify({ 'Username': username, '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: logging.exception("Error retrieving device files")