Compare commits

...

29 Commits
main ... main

Author SHA1 Message Date
voldemort
cc3b37db1d Add frontend build check in precheck module to ensure the frontend is built before running prechecks 2025-07-23 18:04:19 +07:00
voldemort
db7bea7951 Refactor proxy handling in api_decrypt.py to use an empty string instead of None for improved consistency; update returned_keys check to compare against an empty string for better error handling. 2025-07-23 18:00:28 +07:00
voldemort
7f9f04d829 Refactor remote device handling in PlayReady and Widevine modules to improve path management, enhance error handling, and implement consistent response formatting. Add module docstrings for better documentation and clarity. 2025-07-23 18:00:01 +07:00
voldemort
7f84542cfb Enhance upload module with docstring, implement username sanitization, and improve error handling for file uploads. 2025-07-23 16:46:00 +07:00
voldemort
78d59b295c Add module docstring to user_info.py for improved documentation; implement username sanitization and enhance error logging for better debugging. 2025-07-23 16:37:52 +07:00
voldemort
8e076a4298 Add module docstrings to user_changes.py for improved documentation and clarity; enhance error handling in change_password_route and change_api_key_route functions. 2025-07-23 16:37:34 +07:00
voldemort
bbeeffcd9d Refactor database connection handling in user_db.py to utilize os.path.join for improved path management and consistency across user-related functions. 2025-07-23 16:36:10 +07:00
voldemort
6890c6b464 refactor database checks to utilize unified backend operations and streamline database creation logic 2025-07-23 16:35:56 +07:00
voldemort
454429ba7f Update module docstring in build.py to reflect its purpose as the frontend build file for improved clarity. 2025-07-23 16:01:45 +07:00
voldemort
003508aabd Add module docstring to precheck module and its main function for improved documentation and clarity 2025-07-23 01:48:24 +07:00
voldemort
d6cf10ccaf Refactor database checks to enhance path handling, add module docstrings for clarity, and streamline database creation logic 2025-07-23 01:48:14 +07:00
voldemort
c82d23aabc Refactor folder checks to improve path handling and add module docstrings for better documentation and clarity 2025-07-23 01:46:05 +07:00
voldemort
2e520da006 Add module docstring to device allowed check function for improved documentation and clarity 2025-07-23 01:45:22 +07:00
voldemort
a2a12b4c49 Refactor React route handling to improve security and clarity; add module docstring, normalize path to prevent directory traversal, and ensure static folder is configured. 2025-07-23 01:45:05 +07:00
voldemort
8940d57b25 Enhance registration process by adding input validation for username and password length, improve error handling for missing JSON data, and include API key in successful registration response. 2025-07-23 01:42:33 +07:00
voldemort
c756361da0 Add module docstrings to login routes for improved documentation and clarity; streamline login status check 2025-07-23 01:42:17 +07:00
voldemort
c82e493ef1 Refactor config file checks to improve path handling and add module docstring 2025-07-23 01:41:38 +07:00
voldemort
29be40ab95 Add docstrings to user database functions for improved documentation and clarity 2025-07-23 01:40:51 +07:00
voldemort
1ef842978a Refactor main module to improve structure and add module docstring 2025-07-23 01:40:22 +07:00
voldemort
e66e32ef0a Add module docstrings for icon links and index tags 2025-07-23 01:39:46 +07:00
voldemort
5f217f2995 refactor Python checks for improved validation 2025-07-22 21:29:29 +07:00
voldemort
1328805fa5 refactor CDM checks to improve configuration loading, user prompts, and file handling, consolidate CDM download logic into a single function 2025-07-22 21:16:04 +07:00
voldemort
fd2f38fe28 refactor CDM checks to improve configuration loading, user prompts, and file handling, consolidate CDM download logic into a single function 2025-07-22 21:15:50 +07:00
voldemort
802fbdebd1 Enhance build process by adding OS-specific npm command handling, update README with clearer installation instructions and prerequisites 2025-07-22 20:20:47 +07:00
voldemort
e84f43a702 exclude cdrm-frontend/dist directory 2025-07-22 20:01:43 +07:00
voldemort
bafd3db4f4 Update project structure and formatting, add build code, remove /dist 2025-07-22 20:01:22 +07:00
voldemort
84999654ed remove react-helmet package as it is unmaintained 2025-07-22 19:48:46 +07:00
voldemort
2828edd6b7 add prettier formatting 2025-07-22 17:45:48 +07:00
voldemort
a82a3fd106 Update .gitignore, remove version specifiers for some packages 2025-07-22 17:26:02 +07:00
58 changed files with 7598 additions and 6547 deletions

3
.gitignore vendored
View File

@ -8,3 +8,6 @@ build
main.spec
pyinstallericon.ico
icon.ico
venv
frontend-dist
cdrm-frontend/dist/

View File

@ -1,34 +1,51 @@
## CDRM-Project
# 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)
## Prerequisites (from source only)
## 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
> Python 3.13 was used at the time of writing
- [Python](https://www.python.org/downloads/) version 3.12+ with PIP installed
## Installation (Automatic) - Recommended
- Extract contents of CDRM-Project 2.0 git contents into a new folder
Python 3.13 was used at the time of writing
- [Node.js](https://nodejs.org/en/download/) v20+
## Installation (Automatic) - Recommended
- Extract contents of CDRM-Project into a new folder
- Open a terminal and change directory into the new folder
- Run `python main.py`
- Run `python build.py && python main.py`
- 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`
## Installation (Manual)
- Open your terminal and navigate to where you'd like to store the application
- Create a new python virtual environment using `python -m venv CDRM-Project`
- Change directory into the new `CDRM-Project` folder
- Activate the virtual environment
## Installation (Manual)
- Open your terminal and navigate to where you'd like to store the application
- Clone the project with `git clone https://cdm-project.com/tpd94/CDRM-Project.git`
- Navigate to the `CDRM-Project` folder
- Create a new python virtual environment using `python -m venv venv`
- Activate the virtual environment
- Windows:
> Windows - change directory into the `Scripts` directory then `activate.bat`
>
> Linux - `source bin/activate`
```bash
.\venv\Scripts\activate
```
- Linux:
- Extract CDRM-Project 2.0 git contents into the newly created `CDRM-Project` folder
- Install python dependencies `pip install -r requirements.txt`
- (Optional) Create the folder structure `/configs/CDMs/WV` and place your .WVD file into `/configs/CDMs/WV`
- (Optional) Create the folder structure `/config/CDMs/PR` and place your .PRD file into `/configs/CDMs/PR`
- Run the application with `python main.py`
```bash
source venv/bin/activate
```
Verify that the virtual environment is activated by seeing the `(venv)` prefix in your terminal
- Install python dependencies `pip install -r requirements.txt`
- (Optional) Create the folder structure `/configs/CDMs/WV` and place your .WVD file into `/configs/CDMs/WV`
- (Optional) Create the folder structure `/config/CDMs/PR` and place your .PRD file into `/configs/CDMs/PR`
- Build the frontend with `python build.py`
- And finally, run the application with `python main.py`

39
build.py Normal file
View File

@ -0,0 +1,39 @@
"""Main file to build the frontend."""
import os
import subprocess
import shutil
import sys
def get_npm_command():
"""Get the appropriate npm command for the current OS."""
if sys.platform == "win32":
return "npm.cmd"
return "npm"
def build_frontend():
"""Build the frontend."""
frontend_dir = "cdrm-frontend"
npm_cmd = get_npm_command()
# Check and install dependencies if node_modules doesn't exist
if not os.path.exists(f"{frontend_dir}/node_modules"):
print("📦 Installing dependencies...")
subprocess.run([npm_cmd, "install"], cwd=frontend_dir, check=True)
# Always build the frontend to ensure it's up to date
print("🔨 Building frontend...")
subprocess.run([npm_cmd, "run", "build"], cwd=frontend_dir, check=True)
# Move dist to frontend-dist
if os.path.exists("frontend-dist"):
shutil.rmtree("frontend-dist")
shutil.copytree(f"{frontend_dir}/dist", "frontend-dist")
print("✅ Build complete. Run the application with 'python main.py'")
if __name__ == "__main__":
build_frontend()

View File

@ -0,0 +1,5 @@
dist/
node_modules/
src/assets/icons/
src/components/Functions/protobuf.min.js
src/components/Functions/license_protocol.min.js

View File

@ -0,0 +1,8 @@
{
"trailingComma": "es5",
"tabWidth": 4,
"semi": true,
"singleQuote": false,
"useTabs": false,
"printWidth": 100
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 862 B

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 189 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 207 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 302 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 99 KiB

View File

@ -1,33 +1,30 @@
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import js from "@eslint/js";
import globals from "globals";
import reactHooks from "eslint-plugin-react-hooks";
import reactRefresh from "eslint-plugin-react-refresh";
export default [
{ ignores: ['dist'] },
{
files: ['**/*.{js,jsx}'],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
parserOptions: {
ecmaVersion: 'latest',
ecmaFeatures: { jsx: true },
sourceType: 'module',
},
{ ignores: ["dist"] },
{
files: ["**/*.{js,jsx}"],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
parserOptions: {
ecmaVersion: "latest",
ecmaFeatures: { jsx: true },
sourceType: "module",
},
},
plugins: {
"react-hooks": reactHooks,
"react-refresh": reactRefresh,
},
rules: {
...js.configs.recommended.rules,
...reactHooks.configs.recommended.rules,
"no-unused-vars": ["error", { varsIgnorePattern: "^[A-Z_]" }],
"react-refresh/only-export-components": ["warn", { allowConstantExport: true }],
},
},
plugins: {
'react-hooks': reactHooks,
'react-refresh': reactRefresh,
},
rules: {
...js.configs.recommended.rules,
...reactHooks.configs.recommended.rules,
'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }],
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
},
},
]
];

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -1,33 +1,33 @@
{
"name": "cdrm-frontend",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"@tailwindcss/vite": "^4.1.4",
"axios": "^1.9.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-helmet": "^6.1.0",
"react-router-dom": "^7.5.2",
"shaka-player": "^4.14.9",
"tailwindcss": "^4.1.4"
},
"devDependencies": {
"@eslint/js": "^9.22.0",
"@types/react": "^19.0.10",
"@types/react-dom": "^19.0.4",
"@vitejs/plugin-react": "^4.3.4",
"eslint": "^9.22.0",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.19",
"globals": "^16.0.0",
"vite": "^6.3.1"
}
"name": "cdrm-frontend",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"@tailwindcss/vite": "^4.1.11",
"axios": "^1.10.0",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react-router-dom": "^7.7.0",
"shaka-player": "^4.15.8",
"tailwindcss": "^4.1.11"
},
"devDependencies": {
"@eslint/js": "^9.31.0",
"@types/react": "^19.1.8",
"@types/react-dom": "^19.1.6",
"@vitejs/plugin-react": "^4.7.0",
"@vitejs/plugin-react-swc": "^3.11.0",
"eslint": "^9.31.0",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.20",
"globals": "^16.3.0",
"vite": "^7.0.5"
}
}

View File

@ -10,34 +10,40 @@ import Account from "./components/Pages/Account";
import { Routes, Route } from "react-router-dom";
function App() {
const [isMenuOpen, setIsMenuOpen] = useState(false); // Track if the menu is open
const [isMenuOpen, setIsMenuOpen] = useState(false); // Track if the menu is open
return (
<div id="appcontainer" className="flex flex-row w-full h-full bg-black">
{/* The SideMenu should be visible when isMenuOpen is true */}
<SideMenu isMenuOpen={isMenuOpen} setIsMenuOpen={setIsMenuOpen} />
return (
<div id="appcontainer" className="flex flex-row w-full h-full bg-black">
{/* The SideMenu should be visible when isMenuOpen is true */}
<SideMenu isMenuOpen={isMenuOpen} setIsMenuOpen={setIsMenuOpen} />
<div id="navbarcontainer" className="hidden lg:flex lg:w-2xs bg-gray-950/55 border-r border-white/5 shrink-0">
<NavBar />
</div>
<div
id="navbarcontainer"
className="hidden lg:flex lg:w-2xs bg-gray-950/55 border-r border-white/5 shrink-0"
>
<NavBar />
</div>
<div id="maincontainer" className="w-full lg:w-5/6 bg-gray-950/50 flex flex-col grow">
<div id="navbarmaincontainer" className="w-full lg:hidden h-16 bg-gray-950/10 border-b border-white/5 sticky top-0 z-10">
<NavBarMain setIsMenuOpen={setIsMenuOpen} />
<div id="maincontainer" className="w-full lg:w-5/6 bg-gray-950/50 flex flex-col grow">
<div
id="navbarmaincontainer"
className="w-full lg:hidden h-16 bg-gray-950/10 border-b border-white/5 sticky top-0 z-10"
>
<NavBarMain setIsMenuOpen={setIsMenuOpen} />
</div>
<div id="maincontentcontainer" className="w-full grow overflow-y-auto">
<Routes>
<Route path="/" element={<Home />} />
<Route path="/cache" element={<Cache />} />
<Route path="/api" element={<API />} />
<Route path="/testplayer" element={<TestPlayer />} />
<Route path="/account" element={<Account />} />
</Routes>
</div>
</div>
</div>
<div id="maincontentcontainer" className="w-full grow overflow-y-auto">
<Routes>
<Route path="/" element={<Home />} />
<Route path="/cache" element={<Cache />} />
<Route path="/api" element={<API />} />
<Route path="/testplayer" element={<TestPlayer />} />
<Route path="/account" element={<Account />} />
</Routes>
</div>
</div>
</div>
);
);
}
export default App;

View File

@ -4,11 +4,11 @@ import "./license_protocol.min.js";
const { SignedMessage, LicenseRequest } = protobuf.roots.default.license_protocol;
function uint8ArrayToBase64(uint8Array) {
const binaryString = Array.from(uint8Array)
.map(b => String.fromCharCode(b))
.join('');
const binaryString = Array.from(uint8Array)
.map((b) => String.fromCharCode(b))
.join("");
return btoa(binaryString);
return btoa(binaryString);
}
function parseFetch(fetchString) {
@ -17,10 +17,13 @@ function parseFetch(fetchString) {
// Use a more lenient regex to capture the fetch pattern (including complex bodies)
const fetchRegex = /fetch\(['"](.+?)['"],\s*(\{.+?\})\)/s; // Non-greedy match for JSON
const lines = fetchString.split('\n').map(line => line.trim()).filter(Boolean);
const lines = fetchString
.split("\n")
.map((line) => line.trim())
.filter(Boolean);
const result = {
method: 'UNDEFINED',
url: '',
method: "UNDEFINED",
url: "",
headers: {},
body: null,
};
@ -47,9 +50,12 @@ function parseFetch(fetchString) {
return result;
}
const WIDEVINE_SYSTEM_ID = new Uint8Array([0xed, 0xef, 0x8b, 0xa9, 0x79, 0xd6, 0x4a, 0xce, 0xa3, 0xc8, 0x27, 0xdc, 0xd5, 0x1d, 0x21, 0xed]);
const PLAYREADY_SYSTEM_ID = new Uint8Array([0x9a, 0x04, 0xf0, 0x79, 0x98, 0x40, 0x42, 0x86, 0xab, 0x92, 0xe6, 0x5b, 0xe0, 0x88, 0x5f, 0x95]);
const WIDEVINE_SYSTEM_ID = new Uint8Array([
0xed, 0xef, 0x8b, 0xa9, 0x79, 0xd6, 0x4a, 0xce, 0xa3, 0xc8, 0x27, 0xdc, 0xd5, 0x1d, 0x21, 0xed,
]);
const PLAYREADY_SYSTEM_ID = new Uint8Array([
0x9a, 0x04, 0xf0, 0x79, 0x98, 0x40, 0x42, 0x86, 0xab, 0x92, 0xe6, 0x5b, 0xe0, 0x88, 0x5f, 0x95,
]);
const PSSH_MAGIC = new Uint8Array([0x70, 0x73, 0x73, 0x68]);
function intToUint8Array(num, endian) {
@ -75,44 +81,44 @@ function psshDataToPsshBoxB64(pssh_data, system_id) {
...new Uint8Array(4),
...system_id,
...intToUint8Array(dataLength, false),
...pssh_data
...pssh_data,
]);
return uint8ArrayToBase64(pssh);
}
function wrmHeaderToPlayReadyHeader(wrm_header){
function wrmHeaderToPlayReadyHeader(wrm_header) {
const playready_object = new Uint8Array([
...shortToUint8Array(1, true),
...shortToUint8Array(wrm_header.length, true),
...wrm_header
...wrm_header,
]);
return new Uint8Array([
...intToUint8Array(playready_object.length + 2 + 4, true),
...shortToUint8Array(1, true),
...playready_object
...playready_object,
]);
}
function encodeUtf16LE(str) {
const buffer = new Uint8Array(str.length * 2);
for (let i = 0; i < str.length; i++) {
const code = str.charCodeAt(i);
buffer[i * 2] = code & 0xff;
buffer[i * 2 + 1] = code >> 8;
}
return buffer;
const buffer = new Uint8Array(str.length * 2);
for (let i = 0; i < str.length; i++) {
const code = str.charCodeAt(i);
buffer[i * 2] = code & 0xff;
buffer[i * 2 + 1] = code >> 8;
}
return buffer;
}
function stringToUint8Array(string) {
return Uint8Array.from(string.split("").map(x => x.charCodeAt()));
return Uint8Array.from(string.split("").map((x) => x.charCodeAt()));
}
export async function readTextFromClipboard() {
try {
// Request text from the clipboard
const clipboardText = await navigator.clipboard.readText();
const result = parseFetch(clipboardText);
let pssh_data_string;
@ -136,11 +142,15 @@ export async function readTextFromClipboard() {
license_request = LicenseRequest.decode(signed_message.msg);
} catch (decodeError) {
// If error occurs during decoding, return an empty pssh
console.error('Decoding failed, returning empty pssh', decodeError);
pssh_data_string = ''; // Empty pssh if decoding fails
console.error("Decoding failed, returning empty pssh", decodeError);
pssh_data_string = ""; // Empty pssh if decoding fails
}
if (license_request && license_request.contentId && license_request.contentId.widevinePsshData) {
if (
license_request &&
license_request.contentId &&
license_request.contentId.widevinePsshData
) {
const pssh_data = license_request.contentId.widevinePsshData.psshData[0];
pssh_data_string = psshDataToPsshBoxB64(pssh_data, WIDEVINE_SYSTEM_ID);
}
@ -160,14 +170,12 @@ export async function readTextFromClipboard() {
document.getElementById("pssh").value = pssh_data_string;
document.getElementById("data").value = payload_string;
} catch (error) {
console.error('Failed to read clipboard contents:', error);
console.error("Failed to read clipboard contents:", error);
}
}
// Helper function to check if the data is binary
function isBinary(uint8Array) {
// Check for non-text (non-ASCII) bytes (basic heuristic)
return uint8Array.some(byte => byte > 127); // Non-ASCII byte indicates binary
return uint8Array.some((byte) => byte > 127); // Non-ASCII byte indicates binary
}

View File

@ -1,26 +1,26 @@
import { useEffect, useState } from 'react';
import { NavLink } from 'react-router-dom';
import homeIcon from '../assets/icons/home.svg';
import cacheIcon from '../assets/icons/cache.svg';
import apiIcon from '../assets/icons/api.svg';
import testPlayerIcon from '../assets/icons/testplayer.svg';
import accountIcon from '../assets/icons/account.svg';
import discordIcon from '../assets/icons/discord.svg';
import telegramIcon from '../assets/icons/telegram.svg';
import giteaIcon from '../assets/icons/gitea.svg';
import { useEffect, useState } from "react";
import { NavLink } from "react-router-dom";
import homeIcon from "../assets/icons/home.svg";
import cacheIcon from "../assets/icons/cache.svg";
import apiIcon from "../assets/icons/api.svg";
import testPlayerIcon from "../assets/icons/testplayer.svg";
import accountIcon from "../assets/icons/account.svg";
import discordIcon from "../assets/icons/discord.svg";
import telegramIcon from "../assets/icons/telegram.svg";
import giteaIcon from "../assets/icons/gitea.svg";
function NavBar() {
const [externalLinks, setExternalLinks] = useState({
discord: '#',
telegram: '#',
gitea: '#',
discord: "#",
telegram: "#",
gitea: "#",
});
useEffect(() => {
fetch('/api/links')
.then(response => response.json())
.then(data => setExternalLinks(data))
.catch(error => console.error('Error fetching links:', error));
fetch("/api/links")
.then((response) => response.json())
.then((data) => setExternalLinks(data))
.catch((error) => console.error("Error fetching links:", error));
}, []);
return (
@ -40,8 +40,8 @@ function NavBar() {
className={({ isActive }) =>
`flex flex-row p-3 border-l-3 ${
isActive
? 'border-l-sky-500/50 bg-black/50'
: 'hover:border-l-sky-500/50 hover:bg-white/5'
? "border-l-sky-500/50 bg-black/50"
: "hover:border-l-sky-500/50 hover:bg-white/5"
}`
}
>
@ -58,8 +58,8 @@ function NavBar() {
className={({ isActive }) =>
`flex flex-row p-3 border-l-3 ${
isActive
? 'border-l-emerald-500/50 bg-black/50'
: 'hover:border-l-emerald-500/50 hover:bg-white/5'
? "border-l-emerald-500/50 bg-black/50"
: "hover:border-l-emerald-500/50 hover:bg-white/5"
}`
}
>
@ -76,8 +76,8 @@ function NavBar() {
className={({ isActive }) =>
`flex flex-row p-3 border-l-3 ${
isActive
? 'border-l-indigo-500/50 bg-black/50'
: 'hover:border-l-indigo-500/50 hover:bg-white/5'
? "border-l-indigo-500/50 bg-black/50"
: "hover:border-l-indigo-500/50 hover:bg-white/5"
}`
}
>
@ -94,13 +94,17 @@ function NavBar() {
className={({ isActive }) =>
`flex flex-row p-3 border-l-3 ${
isActive
? 'border-l-rose-500/50 bg-black/50'
: 'hover:border-l-rose-500/50 hover:bg-white/5'
? "border-l-rose-500/50 bg-black/50"
: "hover:border-l-rose-500/50 hover:bg-white/5"
}`
}
>
<button className="w-1/3 p-3 flex flex-col items-center justify-center cursor-pointer">
<img src={testPlayerIcon} alt="Test Player" className="w-1/2 cursor-pointer" />
<img
src={testPlayerIcon}
alt="Test Player"
className="w-1/2 cursor-pointer"
/>
</button>
<p className="grow text-white md:text-2xl font-bold flex items-center justify-start">
Test Player
@ -114,8 +118,8 @@ function NavBar() {
className={({ isActive }) =>
`flex flex-row p-3 border-l-3 ${
isActive
? 'border-l-yellow-500/50 bg-black/50'
: 'hover:border-l-yellow-500/50 hover:bg-white/5'
? "border-l-yellow-500/50 bg-black/50"
: "hover:border-l-yellow-500/50 hover:bg-white/5"
}`
}
>
@ -137,7 +141,11 @@ function NavBar() {
rel="noopener noreferrer"
className="w-1/3 p-3 flex flex-col items-center justify-center cursor-pointer hover:bg-blue-950 group"
>
<img src={discordIcon} alt="Discord" className="w-1/2 group-hover:animate-bounce" />
<img
src={discordIcon}
alt="Discord"
className="w-1/2 group-hover:animate-bounce"
/>
</a>
<a
href={externalLinks.telegram}
@ -145,7 +153,11 @@ function NavBar() {
rel="noopener noreferrer"
className="w-1/3 p-3 flex flex-col items-center justify-center cursor-pointer hover:bg-blue-400 group"
>
<img src={telegramIcon} alt="Telegram" className="w-1/2 group-hover:animate-bounce" />
<img
src={telegramIcon}
alt="Telegram"
className="w-1/2 group-hover:animate-bounce"
/>
</a>
<a
href={externalLinks.gitea}

View File

@ -2,21 +2,21 @@ import { useState } from "react";
import hamburgerIcon from "../assets/icons/hamburger.svg";
function NavBarMain({ setIsMenuOpen }) {
const handleMenuToggle = () => {
setIsMenuOpen((prevState) => !prevState); // Toggle the menu state
};
const handleMenuToggle = () => {
setIsMenuOpen((prevState) => !prevState); // Toggle the menu state
};
return (
<div className="flex flex-row w-full h-full bg-white/1">
<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>
<div className="w-24 p-4"></div>
</div>
);
return (
<div className="flex flex-row w-full h-full bg-white/1">
<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>
<div className="w-24 p-4"></div>
</div>
);
}
export default NavBarMain;

View File

@ -1,73 +1,73 @@
import React, { useState, useEffect } from 'react';
import { Helmet } from 'react-helmet'; // Import Helmet
import React, { useEffect, useState } from "react";
const { protocol, hostname, port } = window.location;
let fullHost = `${protocol}//${hostname}`;
if (
(protocol === 'http:' && port !== '80') ||
(protocol === 'https:' && port !== '443' && port !== '')
(protocol === "http:" && port !== "80") ||
(protocol === "https:" && port !== "443" && port !== "")
) {
fullHost += `:${port}`;
fullHost += `:${port}`;
}
function API() {
const [deviceInfo, setDeviceInfo] = useState({
device_type: '',
system_id: '',
security_level: '',
host: '',
secret: '',
device_name: ''
});
const [deviceInfo, setDeviceInfo] = useState({
device_type: "",
system_id: "",
security_level: "",
host: "",
secret: "",
device_name: "",
});
const [prDeviceInfo, setPrDeviceInfo] = useState({
security_level: '',
host: '',
secret: '',
device_name: ''
});
const [prDeviceInfo, setPrDeviceInfo] = useState({
security_level: "",
host: "",
secret: "",
device_name: "",
});
useEffect(() => {
// Fetch Widevine info
fetch('/remotecdm/widevine/deviceinfo')
.then(response => response.json())
.then(data => {
setDeviceInfo({
device_type: data.device_type,
system_id: data.system_id,
security_level: data.security_level,
host: data.host,
secret: data.secret,
device_name: data.device_name
});
})
.catch(error => console.error('Error fetching Widevine info:', error));
useEffect(() => {
// Fetch Widevine info
fetch("/remotecdm/widevine/deviceinfo")
.then((response) => response.json())
.then((data) => {
setDeviceInfo({
device_type: data.device_type,
system_id: data.system_id,
security_level: data.security_level,
host: data.host,
secret: data.secret,
device_name: data.device_name,
});
})
.catch((error) => console.error("Error fetching Widevine info:", error));
// Fetch PlayReady info
fetch('/remotecdm/playready/deviceinfo')
.then(response => response.json())
.then(data => {
setPrDeviceInfo({
security_level: data.security_level,
host: data.host,
secret: data.secret,
device_name: data.device_name
});
})
.catch(error => console.error('Error fetching PlayReady info:', error));
}, []);
// Fetch PlayReady info
fetch("/remotecdm/playready/deviceinfo")
.then((response) => response.json())
.then((data) => {
setPrDeviceInfo({
security_level: data.security_level,
host: data.host,
secret: data.secret,
device_name: data.device_name,
});
})
.catch((error) => console.error("Error fetching PlayReady info:", error));
}, []);
return (
<div className="flex flex-col w-full overflow-y-auto p-4 text-white">
<Helmet>
<title>API</title>
</Helmet>
<details open className='w-full list-none'>
<summary className='text-2xl'>Sending a decryption request</summary>
<div className='mt-5 p-5 rounded-lg border-2 border-indigo-500/50'>
<pre className='rounded-lg font-mono whitespace-pre-wrap text-white overflow-auto'>
{`import requests
useEffect(() => {
document.title = "API | CDRM-Project";
}, []);
return (
<div className="flex flex-col w-full overflow-y-auto p-4 text-white">
<details open className="w-full list-none">
<summary className="text-2xl">Sending a decryption request</summary>
<div className="mt-5 p-5 rounded-lg border-2 border-indigo-500/50">
<pre className="rounded-lg font-mono whitespace-pre-wrap text-white overflow-auto">
{`import requests
print(requests.post(
url='${fullHost}/api/decrypt',
@ -84,14 +84,14 @@ print(requests.post(
})
}
).json()['message'])`}
</pre>
</div>
</details>
<details open className='w-full list-none mt-5'>
<summary className='text-2xl'>Sending a search request</summary>
<div className='mt-5 border-2 border-indigo-500/50 p-5 rounded-lg'>
<pre className="rounded-lg font-mono whitespace-pre text-white overflow-x-auto max-w-full p-5">
{`import requests
</pre>
</div>
</details>
<details open className="w-full list-none mt-5">
<summary className="text-2xl">Sending a search request</summary>
<div className="mt-5 border-2 border-indigo-500/50 p-5 rounded-lg">
<pre className="rounded-lg font-mono whitespace-pre text-white overflow-x-auto max-w-full p-5">
{`import requests
print(requests.post(
url='${fullHost}/api/cache/search',
@ -99,36 +99,40 @@ print(requests.post(
'input': 'AAAAW3Bzc2gAAAAA7e+LqXnWSs6jyCfc1R0h7QAAADsIARIQ62dqu8s0Xpa7z2FmMPGj2hoNd2lkZXZpbmVfdGVzdCIQZmtqM2xqYVNkZmFsa3IzaioCSEQyAA=='
}
).json())`}
</pre>
</div>
</details>
<details open className='w-full list-none mt-5'>
<summary className='text-2xl'>PyWidevine RemoteCDM info</summary>
<div className='mt-5 border-2 border-indigo-500/50 p-5 rounded-lg overflow-x-auto'>
<p>
<strong>Device Type:</strong> '{deviceInfo.device_type}'<br />
<strong>System ID:</strong> {deviceInfo.system_id}<br />
<strong>Security Level:</strong> {deviceInfo.security_level}<br />
<strong>Host:</strong> {fullHost}/remotecdm/widevine<br />
<strong>Secret:</strong> '{deviceInfo.secret}'<br />
<strong>Device Name:</strong> {deviceInfo.device_name}
</p>
</div>
</details>
<details open className='w-full list-none mt-5'>
<summary className='text-2xl'>PyPlayready RemoteCDM info</summary>
<div className='mt-5 border-2 border-indigo-500/50 p-5 rounded-lg overflow-x-auto'>
<p>
<strong>Security Level:</strong> {prDeviceInfo.security_level}<br />
<strong>Host:</strong> {fullHost}/remotecdm/playready<br />
<strong>Secret:</strong> '{prDeviceInfo.secret}'<br />
<strong>Device Name:</strong> {prDeviceInfo.device_name}
</p>
</div>
</details>
</div>
);
</pre>
</div>
</details>
<details open className="w-full list-none mt-5">
<summary className="text-2xl">PyWidevine RemoteCDM info</summary>
<div className="mt-5 border-2 border-indigo-500/50 p-5 rounded-lg overflow-x-auto">
<p>
<strong>Device Type:</strong> '{deviceInfo.device_type}'<br />
<strong>System ID:</strong> {deviceInfo.system_id}
<br />
<strong>Security Level:</strong> {deviceInfo.security_level}
<br />
<strong>Host:</strong> {fullHost}/remotecdm/widevine
<br />
<strong>Secret:</strong> '{deviceInfo.secret}'<br />
<strong>Device Name:</strong> {deviceInfo.device_name}
</p>
</div>
</details>
<details open className="w-full list-none mt-5">
<summary className="text-2xl">PyPlayready RemoteCDM info</summary>
<div className="mt-5 border-2 border-indigo-500/50 p-5 rounded-lg overflow-x-auto">
<p>
<strong>Security Level:</strong> {prDeviceInfo.security_level}
<br />
<strong>Host:</strong> {fullHost}/remotecdm/playready
<br />
<strong>Secret:</strong> '{prDeviceInfo.secret}'<br />
<strong>Device Name:</strong> {prDeviceInfo.device_name}
</p>
</div>
</details>
</div>
);
}
export default API;

View File

@ -3,36 +3,36 @@ import Register from "./Register";
import MyAccount from "./MyAccount"; // <-- Import the MyAccount component
function Account() {
const [isLoggedIn, setIsLoggedIn] = useState(null); // null = loading state
const [isLoggedIn, setIsLoggedIn] = useState(null); // null = loading state
useEffect(() => {
fetch('/login/status', {
method: 'POST',
credentials: 'include', // Sends cookies with request
})
.then(res => res.json())
.then(data => {
if (data.message === 'True') {
setIsLoggedIn(true);
} else {
setIsLoggedIn(false);
}
})
.catch(err => {
console.error("Error checking login status:", err);
setIsLoggedIn(false); // Assume not logged in on error
});
}, []);
useEffect(() => {
fetch("/login/status", {
method: "POST",
credentials: "include", // Sends cookies with request
})
.then((res) => res.json())
.then((data) => {
if (data.message === "True") {
setIsLoggedIn(true);
} else {
setIsLoggedIn(false);
}
})
.catch((err) => {
console.error("Error checking login status:", err);
setIsLoggedIn(false); // Assume not logged in on error
});
}, []);
if (isLoggedIn === null) {
return <div>Loading...</div>; // Optional loading UI
}
if (isLoggedIn === null) {
return <div>Loading...</div>; // Optional loading UI
}
return (
<div id="accountpage" className="w-full h-full flex">
{isLoggedIn ? <MyAccount /> : <Register />}
</div>
);
return (
<div id="accountpage" className="w-full h-full flex">
{isLoggedIn ? <MyAccount /> : <Register />}
</div>
);
}
export default Account;

View File

@ -1,8 +1,7 @@
import { useState, useEffect, useRef } from 'react';
import { Helmet } from 'react-helmet'; // Import Helmet
import { useEffect, useRef, useState } from "react";
function Cache() {
const [searchQuery, setSearchQuery] = useState('');
const [searchQuery, setSearchQuery] = useState("");
const [cacheData, setCacheData] = useState([]);
const [keyCount, setKeyCount] = useState(0); // New state to store the key count
const debounceTimeout = useRef(null);
@ -11,11 +10,11 @@ function Cache() {
useEffect(() => {
const fetchKeyCount = async () => {
try {
const response = await fetch('/api/cache/keycount');
const response = await fetch("/api/cache/keycount");
const data = await response.json();
setKeyCount(data.count); // Update key count
} catch (error) {
console.error('Error fetching key count:', error);
console.error("Error fetching key count:", error);
}
};
@ -25,7 +24,7 @@ function Cache() {
const handleInputChange = (event) => {
const query = event.target.value;
setSearchQuery(query); // Update the search query
// Clear the previous timeout
if (debounceTimeout.current) {
clearTimeout(debounceTimeout.current);
@ -33,7 +32,7 @@ function Cache() {
// Set a new timeout to send the API call after 1 second of no typing
debounceTimeout.current = setTimeout(() => {
if (query.trim() !== '') {
if (query.trim() !== "") {
sendApiCall(query); // Only call the API if the query is not empty
} else {
setCacheData([]); // Clear results if query is empty
@ -42,21 +41,22 @@ function Cache() {
};
const sendApiCall = (text) => {
fetch('/api/cache/search', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
fetch("/api/cache/search", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ input: text }),
})
.then((response) => response.json())
.then((data) => setCacheData(data)) // Update cache data with the results
.catch((error) => console.error('Error:', error));
.catch((error) => console.error("Error:", error));
};
useEffect(() => {
document.title = "Cache | CDRM-Project";
}, []);
return (
<div className="flex flex-col w-full h-full overflow-y-auto p-4">
<Helmet>
<title>Cache</title>
</Helmet>
<div className="flex flex-col lg:flex-row w-full lg:h-12 items-center">
<input
type="text"

View File

@ -1,248 +1,272 @@
import React, { useState, useEffect, useRef } from 'react';
import { readTextFromClipboard } from '../Functions/ParseChallenge';
import { Helmet } from 'react-helmet'; // Import Helmet
import React, { useEffect, useRef, useState } from "react";
import { readTextFromClipboard } from "../Functions/ParseChallenge";
function HomePage() {
const [pssh, setPssh] = useState('');
const [licurl, setLicurl] = useState('');
const [proxy, setProxy] = useState('');
const [headers, setHeaders] = useState('');
const [cookies, setCookies] = useState('');
const [data, setData] = useState('');
const [message, setMessage] = useState('');
const [isVisible, setIsVisible] = useState(false);
const [devices, setDevices] = useState([]);
const [selectedDevice, setSelectedDevice] = useState('default');
const [pssh, setPssh] = useState("");
const [licurl, setLicurl] = useState("");
const [proxy, setProxy] = useState("");
const [headers, setHeaders] = useState("");
const [cookies, setCookies] = useState("");
const [data, setData] = useState("");
const [message, setMessage] = useState("");
const [isVisible, setIsVisible] = useState(false);
const [devices, setDevices] = useState([]);
const [selectedDevice, setSelectedDevice] = useState("default");
const bottomRef = useRef(null);
const messageRef = useRef(null); // Reference to result container
useEffect(() => {
document.title = "Home | CDRM-Project";
}, []);
const handleReset = () => {
if (isVisible) {
setIsVisible(false);
}
setPssh('');
setLicurl('');
setProxy('');
setHeaders('');
setCookies('');
setData('');
};
const bottomRef = useRef(null);
const messageRef = useRef(null); // Reference to result container
const handleSubmitButton = (event) => {
event.preventDefault();
fetch('/api/decrypt', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
pssh: pssh,
licurl: licurl,
proxy: proxy,
headers: headers,
cookies: cookies,
data: data,
device: selectedDevice, // Include selected device in the request
}),
})
.then(response => response.json())
.then(data => {
const resultMessage = data['message'].replace(/\n/g, '<br />');
setMessage(resultMessage);
setIsVisible(true);
})
.catch((error) => {
console.error('Error during decryption request:', error);
setMessage('Error: Unable to process request.');
setIsVisible(true);
});
};
const handleCopy = (event) => {
event.preventDefault();
if (messageRef.current) {
const textToCopy = messageRef.current.innerText; // Grab the plain text (with visual line breaks)
navigator.clipboard.writeText(textToCopy).catch(err => {
alert('Failed to copy!');
console.error(err);
});
}
};
const handleFetchPaste = () => {
event.preventDefault();
readTextFromClipboard().then(() => {
setPssh(document.getElementById("pssh").value);
setLicurl(document.getElementById("licurl").value);
setHeaders(document.getElementById("headers").value);
setData(document.getElementById("data").value);
}).catch(err => {
alert('Failed to paste from fetch!');
});
};
useEffect(() => {
if (isVisible && bottomRef.current) {
bottomRef.current.scrollIntoView({ behavior: 'smooth' });
}
}, [message, isVisible]);
useEffect(() => {
fetch('/login/status', {
method: 'POST',
})
.then(res => res.json())
.then(statusData => {
if (statusData.message === 'True') {
return fetch('/userinfo', { method: 'POST' });
} else {
throw new Error('Not logged in');
const handleReset = () => {
if (isVisible) {
setIsVisible(false);
}
})
.then(res => res.json())
.then(deviceData => {
const combinedDevices = [
...deviceData.Widevine_Devices,
...deviceData.Playready_Devices,
];
setPssh("");
setLicurl("");
setProxy("");
setHeaders("");
setCookies("");
setData("");
};
// Add default devices if logged in
const allDevices = [
"CDRM-Project Public Widevine CDM",
"CDRM-Project Public PlayReady CDM",
...combinedDevices,
];
const handleSubmitButton = (event) => {
event.preventDefault();
// Set devices and select a device if logged in
setDevices(allDevices.length > 0 ? allDevices : []);
setSelectedDevice(allDevices.length > 0 ? allDevices[0] : 'default');
})
.catch(() => {
// User isn't logged in, set default device to 'default'
setDevices([]); // Don't display devices list
setSelectedDevice('default');
});
}, []);
fetch("/api/decrypt", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
pssh: pssh,
licurl: licurl,
proxy: proxy,
headers: headers,
cookies: cookies,
data: data,
device: selectedDevice, // Include selected device in the request
}),
})
.then((response) => response.json())
.then((data) => {
const resultMessage = data["message"].replace(/\n/g, "<br />");
setMessage(resultMessage);
setIsVisible(true);
})
.catch((error) => {
console.error("Error during decryption request:", error);
setMessage("Error: Unable to process request.");
setIsVisible(true);
});
};
return (
<>
<div className="flex flex-col w-full overflow-y-auto p-4 min-h-full">
<Helmet>
<title>CDRM-Project</title>
</Helmet>
<form className="flex flex-col w-full h-full bg-black/5 p-4 overflow-y-auto">
<label htmlFor="pssh" className="text-white w-8/10 self-center">PSSH: </label>
<input
type="text"
id="pssh"
className="w-8/10 border-2 border-sky-500/25 rounded-xl h-10 self-center m-2 text-white p-1"
value={pssh}
onChange={(e) => setPssh(e.target.value)}
/>
<label htmlFor="licurl" className="text-white w-8/10 self-center">License URL: </label>
<input
type="text"
id="licurl"
className="w-8/10 border-2 border-sky-500/25 rounded-xl h-10 self-center m-2 text-white p-1"
value={licurl}
onChange={(e) => setLicurl(e.target.value)}
/>
<label htmlFor="proxy" className="text-white w-8/10 self-center">Proxy: </label>
<input
type="text"
id="proxy"
className="w-8/10 border-2 border-sky-500/25 rounded-xl h-10 self-center m-2 text-white p-1"
value={proxy}
onChange={(e) => setProxy(e.target.value)}
/>
<label htmlFor="headers" className="text-white w-8/10 self-center">Headers: </label>
<textarea
id="headers"
className="w-8/10 border-2 border-sky-500/25 rounded-xl self-center m-2 text-white p-1 h-48"
value={headers}
onChange={(e) => setHeaders(e.target.value)}
/>
<label htmlFor="cookies" className="text-white w-8/10 self-center">Cookies: </label>
<textarea
id="cookies"
className="w-8/10 border-2 border-sky-500/25 rounded-xl self-center m-2 text-white p-1 h-48"
value={cookies}
onChange={(e) => setCookies(e.target.value)}
/>
<label htmlFor="data" className="text-white w-8/10 self-center">Data: </label>
<textarea
id="data"
className="w-8/10 border-2 border-sky-500/25 rounded-xl self-center m-2 text-white p-1 h-48"
value={data}
onChange={(e) => setData(e.target.value)}
/>
const handleCopy = (event) => {
event.preventDefault();
if (messageRef.current) {
const textToCopy = messageRef.current.innerText; // Grab the plain text (with visual line breaks)
navigator.clipboard.writeText(textToCopy).catch((err) => {
alert("Failed to copy!");
console.error(err);
});
}
};
{/* Device Selection Dropdown, only show if logged in */}
{devices.length > 0 && (
<>
<label htmlFor="device" className="text-white w-8/10 self-center">Select Device:</label>
<select
id="device"
className="w-8/10 border-2 border-sky-500/25 rounded-xl h-10 self-center m-2 text-white bg-black p-1"
value={selectedDevice}
onChange={(e) => setSelectedDevice(e.target.value)}
>
{devices.map((device, index) => (
<option key={index} value={device}>{device}</option>
))}
</select>
</>
)}
const handleFetchPaste = () => {
event.preventDefault();
readTextFromClipboard()
.then(() => {
setPssh(document.getElementById("pssh").value);
setLicurl(document.getElementById("licurl").value);
setHeaders(document.getElementById("headers").value);
setData(document.getElementById("data").value);
})
.catch((err) => {
alert("Failed to paste from fetch!");
});
};
<div className="flex flex-col lg:flex-row w-full self-center mt-5 items-center lg:justify-around lg:items-stretch">
<button
type="button"
className="bg-sky-500/50 rounded-xl text-white text-bold text-xl p-1 lg:w-1/5 lg:h-12 truncate w-1/2"
onClick={handleSubmitButton}
>
Submit
</button>
<button onClick={handleFetchPaste} className="bg-yellow-500/50 rounded-xl text-white text-bold text-xl p-1 lg:w-1/5 lg:h-12 truncate mt-5 w-1/2 lg:mt-0">
Paste from fetch
</button>
<button
type="button"
className="bg-red-500/50 rounded-xl text-white text-bold text-xl p-1 lg:w-1/5 lg:h-12 truncate mt-5 w-1/2 lg:mt-0"
onClick={handleReset}
>
Reset
</button>
</div>
</form>
</div>
useEffect(() => {
if (isVisible && bottomRef.current) {
bottomRef.current.scrollIntoView({ behavior: "smooth" });
}
}, [message, isVisible]);
{isVisible && (
<div id="main_content" className="flex-col w-full h-full p-10 items-center justify-center self-center">
<div className="flex flex-col w-full h-full overflow-y-auto items-center">
<div className='w-8/10 grow p-4 text-white text-bold text-center text-xl md:text-3xl border-2 border-sky-500/25 rounded-xl bg-black/5'>
<p className="w-full border-b-2 border-white/75 pb-2">Results:</p>
<p
className="w-full grow pt-10 break-words overflow-y-auto"
ref={messageRef}
dangerouslySetInnerHTML={{ __html: message }}
/>
<div ref={bottomRef} />
useEffect(() => {
fetch("/login/status", {
method: "POST",
})
.then((res) => res.json())
.then((statusData) => {
if (statusData.message === "True") {
return fetch("/userinfo", { method: "POST" });
} else {
throw new Error("Not logged in");
}
})
.then((res) => res.json())
.then((deviceData) => {
const combinedDevices = [
...deviceData.Widevine_Devices,
...deviceData.Playready_Devices,
];
// Add default devices if logged in
const allDevices = [
"CDRM-Project Public Widevine CDM",
"CDRM-Project Public PlayReady CDM",
...combinedDevices,
];
// Set devices and select a device if logged in
setDevices(allDevices.length > 0 ? allDevices : []);
setSelectedDevice(allDevices.length > 0 ? allDevices[0] : "default");
})
.catch(() => {
// User isn't logged in, set default device to 'default'
setDevices([]); // Don't display devices list
setSelectedDevice("default");
});
}, []);
return (
<>
<div className="flex flex-col w-full overflow-y-auto p-4 min-h-full">
<form className="flex flex-col w-full h-full bg-black/5 p-4 overflow-y-auto">
<label htmlFor="pssh" className="text-white w-8/10 self-center">
PSSH:{" "}
</label>
<input
type="text"
id="pssh"
className="w-8/10 border-2 border-sky-500/25 rounded-xl h-10 self-center m-2 text-white p-1"
value={pssh}
onChange={(e) => setPssh(e.target.value)}
/>
<label htmlFor="licurl" className="text-white w-8/10 self-center">
License URL:{" "}
</label>
<input
type="text"
id="licurl"
className="w-8/10 border-2 border-sky-500/25 rounded-xl h-10 self-center m-2 text-white p-1"
value={licurl}
onChange={(e) => setLicurl(e.target.value)}
/>
<label htmlFor="proxy" className="text-white w-8/10 self-center">
Proxy:{" "}
</label>
<input
type="text"
id="proxy"
className="w-8/10 border-2 border-sky-500/25 rounded-xl h-10 self-center m-2 text-white p-1"
value={proxy}
onChange={(e) => setProxy(e.target.value)}
/>
<label htmlFor="headers" className="text-white w-8/10 self-center">
Headers:{" "}
</label>
<textarea
id="headers"
className="w-8/10 border-2 border-sky-500/25 rounded-xl self-center m-2 text-white p-1 h-48"
value={headers}
onChange={(e) => setHeaders(e.target.value)}
/>
<label htmlFor="cookies" className="text-white w-8/10 self-center">
Cookies:{" "}
</label>
<textarea
id="cookies"
className="w-8/10 border-2 border-sky-500/25 rounded-xl self-center m-2 text-white p-1 h-48"
value={cookies}
onChange={(e) => setCookies(e.target.value)}
/>
<label htmlFor="data" className="text-white w-8/10 self-center">
Data:{" "}
</label>
<textarea
id="data"
className="w-8/10 border-2 border-sky-500/25 rounded-xl self-center m-2 text-white p-1 h-48"
value={data}
onChange={(e) => setData(e.target.value)}
/>
{/* Device Selection Dropdown, only show if logged in */}
{devices.length > 0 && (
<>
<label htmlFor="device" className="text-white w-8/10 self-center">
Select Device:
</label>
<select
id="device"
className="w-8/10 border-2 border-sky-500/25 rounded-xl h-10 self-center m-2 text-white bg-black p-1"
value={selectedDevice}
onChange={(e) => setSelectedDevice(e.target.value)}
>
{devices.map((device, index) => (
<option key={index} value={device}>
{device}
</option>
))}
</select>
</>
)}
<div className="flex flex-col lg:flex-row w-full self-center mt-5 items-center lg:justify-around lg:items-stretch">
<button
type="button"
className="bg-sky-500/50 rounded-xl text-white text-bold text-xl p-1 lg:w-1/5 lg:h-12 truncate w-1/2"
onClick={handleSubmitButton}
>
Submit
</button>
<button
onClick={handleFetchPaste}
className="bg-yellow-500/50 rounded-xl text-white text-bold text-xl p-1 lg:w-1/5 lg:h-12 truncate mt-5 w-1/2 lg:mt-0"
>
Paste from fetch
</button>
<button
type="button"
className="bg-red-500/50 rounded-xl text-white text-bold text-xl p-1 lg:w-1/5 lg:h-12 truncate mt-5 w-1/2 lg:mt-0"
onClick={handleReset}
>
Reset
</button>
</div>
</form>
</div>
</div>
<div className="flex flex-col lg:flex-row w-full self-center mt-5 items-center lg:justify-around lg:items-stretch">
<button
className="bg-green-500/50 rounded-xl text-white text-bold text-xl p-1 lg:w-1/5 lg:h-12 truncate w-1/2"
onClick={handleCopy}
>
Copy Results
</button>
</div>
</div>
)}
</>
);
{isVisible && (
<div
id="main_content"
className="flex-col w-full h-full p-10 items-center justify-center self-center"
>
<div className="flex flex-col w-full h-full overflow-y-auto items-center">
<div className="w-8/10 grow p-4 text-white text-bold text-center text-xl md:text-3xl border-2 border-sky-500/25 rounded-xl bg-black/5">
<p className="w-full border-b-2 border-white/75 pb-2">Results:</p>
<p
className="w-full grow pt-10 break-words overflow-y-auto"
ref={messageRef}
dangerouslySetInnerHTML={{ __html: message }}
/>
<div ref={bottomRef} />
</div>
</div>
<div className="flex flex-col lg:flex-row w-full self-center mt-5 items-center lg:justify-around lg:items-stretch">
<button
className="bg-green-500/50 rounded-xl text-white text-bold text-xl p-1 lg:w-1/5 lg:h-12 truncate w-1/2"
onClick={handleCopy}
>
Copy Results
</button>
</div>
</div>
)}
</>
);
}
export default HomePage;

View File

@ -1,262 +1,293 @@
import React, { useState, useEffect } from 'react';
import axios from 'axios';
import axios from "axios";
import React, { useEffect, useState } from "react";
function MyAccount() {
const [wvList, setWvList] = useState([]);
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('');
const [wvList, setWvList] = useState([]);
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 () => {
try {
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 || '');
} catch (err) {
console.error('Failed to fetch user info', err);
}
};
// Fetch user info
const fetchUserInfo = async () => {
try {
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 || "");
} catch (err) {
console.error("Failed to fetch user info", err);
}
};
useEffect(() => {
fetchUserInfo();
}, []);
useEffect(() => {
fetchUserInfo();
}, []);
// Handle file upload
const handleUpload = async (event, cdmType) => {
const file = event.target.files[0];
if (!file) return;
useEffect(() => {
document.title = "My account | CDRM-Project";
}, []);
const extension = file.name.split('.').pop();
if ((cdmType === 'PR' && extension !== 'prd') || (cdmType === 'WV' && extension !== 'wvd')) {
alert(`Please upload a .${cdmType === 'PR' ? 'prd' : 'wvd'} file.`);
return;
}
// Handle file upload
const handleUpload = async (event, cdmType) => {
const file = event.target.files[0];
if (!file) return;
const formData = new FormData();
formData.append('file', file);
const extension = file.name.split(".").pop();
if (
(cdmType === "PR" && extension !== "prd") ||
(cdmType === "WV" && extension !== "wvd")
) {
alert(`Please upload a .${cdmType === "PR" ? "prd" : "wvd"} file.`);
return;
}
setUploading(true);
try {
await axios.post(`/upload/${cdmType}`, formData);
await fetchUserInfo(); // Refresh list after upload
} catch (err) {
console.error('Upload failed', err);
alert('Upload failed');
} finally {
setUploading(false);
}
};
const formData = new FormData();
formData.append("file", file);
// Handle logout
const handleLogout = async () => {
try {
await axios.post('/logout');
window.location.reload();
} catch (error) {
console.error('Logout failed:', error);
alert('Logout failed!');
}
};
setUploading(true);
try {
await axios.post(`/upload/${cdmType}`, formData);
await fetchUserInfo(); // Refresh list after upload
} catch (err) {
console.error("Upload failed", err);
alert("Upload failed");
} finally {
setUploading(false);
}
};
// Handle change password
const handleChangePassword = async () => {
if (passwordError || password === '') {
alert('Please enter a valid password.');
return;
}
// Handle logout
const handleLogout = async () => {
try {
await axios.post("/logout");
window.location.reload();
} catch (error) {
console.error("Logout failed:", error);
alert("Logout failed!");
}
};
try {
const response = await axios.post('/user/change_password', {
new_password: password
});
// Handle change password
const handleChangePassword = async () => {
if (passwordError || password === "") {
alert("Please enter a valid password.");
return;
}
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.');
}
}
};
try {
const response = await axios.post("/user/change_password", {
new_password: password,
});
// Handle change API key
const handleChangeApiKey = async () => {
if (apiKeyError || newApiKey === '') {
alert('Please enter a valid API key.');
return;
}
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.");
}
}
};
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);
}
};
// Handle change API key
const handleChangeApiKey = async () => {
if (apiKeyError || newApiKey === "") {
alert("Please enter a valid API key.");
return;
}
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">
<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>
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);
}
};
{/* 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"
return (
<div
id="myaccount"
className="flex flex-col lg:flex-row gap-4 w-full min-h-full overflow-y-auto p-4"
>
Log out
</button>
</div>
<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">
<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>
<div className="flex flex-col w-full 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>
<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>
) : (
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'}`}
>
{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">
{uploading ? 'Uploading...' : 'Upload CDM'}
<input
type="file"
accept=".wvd"
hidden
onChange={(e) => handleUpload(e, 'WV')}
/>
</label>
</div>
{/* 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"
/>
{/* 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>
<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>
) : (
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'}`}
>
{filename}
{/* 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>
))
)}
</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">
{uploading ? 'Uploading...' : 'Upload CDM'}
<input
type="file"
accept=".prd"
hidden
onChange={(e) => handleUpload(e, 'PR')}
/>
</label>
<button
onClick={handleLogout}
className="mt-auto w-full h-12 bg-yellow-500/50 rounded-2xl text-2xl text-white"
>
Log out
</button>
</div>
<div className="flex flex-col w-full 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>
<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>
) : (
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"
}`}
>
{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">
{uploading ? "Uploading..." : "Upload CDM"}
<input
type="file"
accept=".wvd"
hidden
onChange={(e) => handleUpload(e, "WV")}
/>
</label>
</div>
{/* 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>
<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>
) : (
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"
}`}
>
{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">
{uploading ? "Uploading..." : "Upload CDM"}
<input
type="file"
accept=".prd"
hidden
onChange={(e) => handleUpload(e, "PR")}
/>
</label>
</div>
</div>
</div>
</div>
</div>
);
);
}
export default MyAccount;

View File

@ -1,117 +1,121 @@
import React, { useState } from 'react';
import React, { useEffect, useState } from "react";
function Register() {
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [status, setStatus] = useState('');
const [username, setUsername] = useState("");
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
// 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;
}
useEffect(() => {
document.title = "Register | CDRM-Project";
}, []);
try {
const response = await fetch('/register', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ username, password })
});
const data = await response.json();
if (data.message) {
setStatus(data.message);
} else if (data.error) {
setStatus(data.error);
}
} catch (err) {
setStatus('An error occurred while registering.');
}
};
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;
}
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("/register", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ username, password }),
});
const data = await response.json();
if (data.message) {
setStatus(data.message);
} else if (data.error) {
setStatus(data.error);
}
} catch (err) {
setStatus("An error occurred while registering.");
}
};
try {
const response = await fetch('/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
credentials: 'include', // Important to send cookies
body: JSON.stringify({ username, password })
});
const data = await response.json();
if (data.message) {
// Successful login - reload the page to trigger Account check
window.location.reload();
} else if (data.error) {
setStatus(data.error);
}
} catch (err) {
setStatus('An error occurred while logging in.');
}
};
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;
}
return (
<div className="flex flex-col w-full h-full items-center justify-center p-4">
<div className="flex flex-col w-full h-full lg:w-1/2 lg:h-96 border-2 border-yellow-500/50 rounded-2xl p-4 overflow-x-auto justify-center items-center">
<div className="flex flex-col w-full">
<label htmlFor="username" className="text-lg font-bold mb-2 text-white">Username:</label>
<input
type="text"
value={username}
onChange={e => setUsername(e.target.value)}
placeholder="Username"
className="mb-4 p-2 border border-gray-300 rounded text-white bg-transparent"
/>
<label htmlFor="password" className="text-lg font-bold mb-2 text-white">Password:</label>
<input
type="password"
value={password}
onChange={e => setPassword(e.target.value)}
placeholder="Password"
className="mb-4 p-2 border border-gray-300 rounded text-white bg-transparent"
/>
try {
const response = await fetch("/login", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
credentials: "include", // Important to send cookies
body: JSON.stringify({ username, password }),
});
const data = await response.json();
if (data.message) {
// Successful login - reload the page to trigger Account check
window.location.reload();
} else if (data.error) {
setStatus(data.error);
}
} catch (err) {
setStatus("An error occurred while logging in.");
}
};
return (
<div className="flex flex-col w-full h-full items-center justify-center p-4">
<div className="flex flex-col w-full h-full lg:w-1/2 lg:h-96 border-2 border-yellow-500/50 rounded-2xl p-4 overflow-x-auto justify-center items-center">
<div className="flex flex-col w-full">
<label htmlFor="username" className="text-lg font-bold mb-2 text-white">
Username:
</label>
<input
type="text"
value={username}
onChange={(e) => setUsername(e.target.value)}
placeholder="Username"
className="mb-4 p-2 border border-gray-300 rounded text-white bg-transparent"
/>
<label htmlFor="password" className="text-lg font-bold mb-2 text-white">
Password:
</label>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="Password"
className="mb-4 p-2 border border-gray-300 rounded text-white bg-transparent"
/>
</div>
<div className="flex flex-col lg:flex-row w-8/10 items-center lg:justify-between mt-4">
<button
onClick={handleLogin}
className="bg-yellow-500 hover:bg-yellow-600 text-white font-bold py-2 px-4 rounded mt-4 w-1/3"
>
Login
</button>
<button
onClick={handleRegister}
className="bg-yellow-500 hover:bg-yellow-600 text-white font-bold py-2 px-4 rounded mt-4 w-1/3"
>
Register
</button>
</div>
{status && <p className="text-sm text-white mt-4 p-4">{status}</p>}
</div>
</div>
<div className="flex flex-col lg:flex-row w-8/10 items-center lg:justify-between mt-4">
<button
onClick={handleLogin}
className="bg-yellow-500 hover:bg-yellow-600 text-white font-bold py-2 px-4 rounded mt-4 w-1/3"
>
Login
</button>
<button
onClick={handleRegister}
className="bg-yellow-500 hover:bg-yellow-600 text-white font-bold py-2 px-4 rounded mt-4 w-1/3"
>
Register
</button>
</div>
{status && (
<p className="text-sm text-white mt-4 p-4">
{status}
</p>
)}
</div>
</div>
);
);
}
export default Register;

View File

@ -1,158 +1,152 @@
import React, { useState, useEffect, useRef } from 'react';
import shaka from 'shaka-player';
import { Helmet } from 'react-helmet'; // Import Helmet
import React, { useState, useEffect, useRef } from "react";
import shaka from "shaka-player";
function TestPlayer() {
const [mpdUrl, setMpdUrl] = useState(''); // State to hold the MPD URL
const [kids, setKids] = useState(''); // State to hold KIDs (separated by line breaks)
const [keys, setKeys] = useState(''); // State to hold Keys (separated by line breaks)
const [headers, setHeaders] = useState(''); // State to hold request headers
const [mpdUrl, setMpdUrl] = useState(""); // State to hold the MPD URL
const [kids, setKids] = useState(""); // State to hold KIDs (separated by line breaks)
const [keys, setKeys] = useState(""); // State to hold Keys (separated by line breaks)
const [headers, setHeaders] = useState(""); // State to hold request headers
const videoRef = useRef(null); // Ref for the video element
const playerRef = useRef(null); // Ref for Shaka Player instance
const videoRef = useRef(null); // Ref for the video element
const playerRef = useRef(null); // Ref for Shaka Player instance
// Function to update the MPD URL state
const handleInputChange = (event) => {
setMpdUrl(event.target.value);
};
// Function to update the MPD URL state
const handleInputChange = (event) => {
setMpdUrl(event.target.value);
};
// Function to update KIDs and Keys
const handleKidsChange = (event) => {
setKids(event.target.value);
};
// Function to update KIDs and Keys
const handleKidsChange = (event) => {
setKids(event.target.value);
};
const handleKeysChange = (event) => {
setKeys(event.target.value);
};
const handleKeysChange = (event) => {
setKeys(event.target.value);
};
const handleHeadersChange = (event) => {
setHeaders(event.target.value);
};
const handleHeadersChange = (event) => {
setHeaders(event.target.value);
};
// Function to initialize Shaka Player
const initializePlayer = () => {
if (videoRef.current) {
// Initialize Shaka Player only if it's not already initialized
if (!playerRef.current) {
const player = new shaka.Player(videoRef.current);
playerRef.current = player;
// Function to initialize Shaka Player
const initializePlayer = () => {
if (videoRef.current) {
// Initialize Shaka Player only if it's not already initialized
if (!playerRef.current) {
const player = new shaka.Player(videoRef.current);
playerRef.current = player;
// Add error listener
player.addEventListener('error', (event) => {
console.error('Error code', event.detail.code, 'object', event.detail);
// Add error listener
player.addEventListener("error", (event) => {
console.error("Error code", event.detail.code, "object", event.detail);
});
}
}
};
// Function to handle submit and configure player with DRM keys and headers
const handleSubmit = () => {
if (mpdUrl && kids && keys) {
// Split the KIDs and Keys by new lines
const kidsArray = kids.split("\n").map((k) => k.trim());
const keysArray = keys.split("\n").map((k) => k.trim());
if (kidsArray.length !== keysArray.length) {
console.error("The number of KIDs and Keys must be the same.");
return;
}
// Initialize Shaka Player only when the submit button is pressed
const player = new shaka.Player(videoRef.current);
// Widevine DRM configuration with the provided KIDs and Keys
const config = {
drm: {
clearKeys: {},
},
};
// Map KIDs to Keys
kidsArray.forEach((kid, index) => {
config.drm.clearKeys[kid] = keysArray[index];
});
console.log("Configuring player with the following DRM config and headers:", config);
// Configure the player with ClearKey DRM and custom headers
player.configure(config);
// Load the video stream with MPD URL
player
.load(mpdUrl)
.then(() => {
console.log("Video loaded");
})
.catch((error) => {
console.error("Error loading the video", error);
});
} else {
console.error("MPD URL, KIDs, and Keys are required.");
}
};
// Load the video stream whenever the MPD URL changes
useEffect(() => {
initializePlayer(); // Initialize the player if it's not initialized already
}, []); // This effect runs only once on mount
// Helper function to parse headers from the textarea input
const parseHeaders = (headersText) => {
const headersArr = headersText.split("\n");
const headersObj = {};
headersArr.forEach((line) => {
const [key, value] = line.split(":");
if (key && value) {
headersObj[key.trim()] = value.trim();
}
});
}
}
};
return headersObj;
};
// Function to handle submit and configure player with DRM keys and headers
const handleSubmit = () => {
if (mpdUrl && kids && keys) {
// Split the KIDs and Keys by new lines
const kidsArray = kids.split("\n").map((k) => k.trim());
const keysArray = keys.split("\n").map((k) => k.trim());
useEffect(() => {
document.title = "Test player | CDRM-Project";
}, []);
if (kidsArray.length !== keysArray.length) {
console.error("The number of KIDs and Keys must be the same.");
return;
}
// Initialize Shaka Player only when the submit button is pressed
const player = new shaka.Player(videoRef.current);
// Widevine DRM configuration with the provided KIDs and Keys
const config = {
drm: {
clearKeys: {},
},
};
// Map KIDs to Keys
kidsArray.forEach((kid, index) => {
config.drm.clearKeys[kid] = keysArray[index];
});
console.log("Configuring player with the following DRM config and headers:", config);
// Configure the player with ClearKey DRM and custom headers
player.configure(config);
// Load the video stream with MPD URL
player.load(mpdUrl).then(() => {
console.log('Video loaded');
}).catch((error) => {
console.error('Error loading the video', error);
});
} else {
console.error('MPD URL, KIDs, and Keys are required.');
}
};
// Load the video stream whenever the MPD URL changes
useEffect(() => {
initializePlayer(); // Initialize the player if it's not initialized already
}, []); // This effect runs only once on mount
// Helper function to parse headers from the textarea input
const parseHeaders = (headersText) => {
const headersArr = headersText.split('\n');
const headersObj = {};
headersArr.forEach((line) => {
const [key, value] = line.split(':');
if (key && value) {
headersObj[key.trim()] = value.trim();
}
});
return headersObj;
};
return (
<div className="flex flex-col items-center w-full p-4">
<Helmet>
<title>Test Player</title>
</Helmet>
<div className="w-full flex flex-col">
<video
ref={videoRef}
width="100%"
height="auto"
controls
className="h-96"
/>
<input
type="text"
value={mpdUrl}
onChange={handleInputChange}
placeholder="MPD URL"
className="border-2 border-rose-700/50 mt-2 text-white p-1 rounded transition-all ease-in-out focus:outline-none focus:ring-2 focus:ring-rose-700/50 duration-200"
/>
<textarea
placeholder="KIDs (one per line)"
value={kids}
onChange={handleKidsChange}
className="border-2 border-rose-700/50 mt-2 text-white p-1 overflow-y-auto rounded transition-all ease-in-out focus:outline-none focus:ring-2 focus:ring-rose-700/50 duration-200"
/>
<textarea
placeholder="Keys (one per line)"
value={keys}
onChange={handleKeysChange}
className="border-2 border-rose-700/50 mt-2 text-white p-1 overflow-y-auto rounded transition-all ease-in-out focus:outline-none focus:ring-2 focus:ring-rose-700/50 duration-200"
/>
<textarea
placeholder="Headers (one per line)"
value={headers}
onChange={handleHeadersChange}
className="border-2 border-rose-700/50 mt-2 text-white p-1 overflow-y-auto rounded transition-all ease-in-out focus:outline-none focus:ring-2 focus:ring-rose-700/50 duration-200"
/>
<button
onClick={handleSubmit}
className="mt-4 p-2 bg-blue-500 text-white rounded"
>
Submit
</button>
</div>
</div>
);
return (
<div className="flex flex-col items-center w-full p-4">
<div className="w-full flex flex-col">
<video ref={videoRef} width="100%" height="auto" controls className="h-96" />
<input
type="text"
value={mpdUrl}
onChange={handleInputChange}
placeholder="MPD URL"
className="border-2 border-rose-700/50 mt-2 text-white p-1 rounded transition-all ease-in-out focus:outline-none focus:ring-2 focus:ring-rose-700/50 duration-200"
/>
<textarea
placeholder="KIDs (one per line)"
value={kids}
onChange={handleKidsChange}
className="border-2 border-rose-700/50 mt-2 text-white p-1 overflow-y-auto rounded transition-all ease-in-out focus:outline-none focus:ring-2 focus:ring-rose-700/50 duration-200"
/>
<textarea
placeholder="Keys (one per line)"
value={keys}
onChange={handleKeysChange}
className="border-2 border-rose-700/50 mt-2 text-white p-1 overflow-y-auto rounded transition-all ease-in-out focus:outline-none focus:ring-2 focus:ring-rose-700/50 duration-200"
/>
<textarea
placeholder="Headers (one per line)"
value={headers}
onChange={handleHeadersChange}
className="border-2 border-rose-700/50 mt-2 text-white p-1 overflow-y-auto rounded transition-all ease-in-out focus:outline-none focus:ring-2 focus:ring-rose-700/50 duration-200"
/>
<button onClick={handleSubmit} className="mt-4 p-2 bg-blue-500 text-white rounded">
Submit
</button>
</div>
</div>
);
}
export default TestPlayer;

View File

@ -1,178 +1,182 @@
import { useEffect, useState } from 'react';
import { NavLink } from 'react-router-dom';
import closeIcon from '../assets/icons/close.svg';
import homeIcon from '../assets/icons/home.svg';
import cacheIcon from '../assets/icons/cache.svg';
import apiIcon from '../assets/icons/api.svg';
import testPlayerIcon from '../assets/icons/testplayer.svg';
import accountIcon from '../assets/icons/account.svg';
import discordIcon from '../assets/icons/discord.svg';
import telegramIcon from '../assets/icons/telegram.svg';
import giteaIcon from '../assets/icons/gitea.svg';
import { useEffect, useState } from "react";
import { NavLink } from "react-router-dom";
import closeIcon from "../assets/icons/close.svg";
import homeIcon from "../assets/icons/home.svg";
import cacheIcon from "../assets/icons/cache.svg";
import apiIcon from "../assets/icons/api.svg";
import testPlayerIcon from "../assets/icons/testplayer.svg";
import accountIcon from "../assets/icons/account.svg";
import discordIcon from "../assets/icons/discord.svg";
import telegramIcon from "../assets/icons/telegram.svg";
import giteaIcon from "../assets/icons/gitea.svg";
function SideMenu({ isMenuOpen, setIsMenuOpen }) {
const [externalLinks, setExternalLinks] = useState({
discord: '#',
telegram: '#',
gitea: '#',
});
const [externalLinks, setExternalLinks] = useState({
discord: "#",
telegram: "#",
gitea: "#",
});
useEffect(() => {
fetch('/api/links')
.then((res) => res.json())
.then((data) => setExternalLinks(data))
.catch((err) => console.error('Failed to fetch links:', err));
}, []);
useEffect(() => {
fetch("/api/links")
.then((res) => res.json())
.then((data) => setExternalLinks(data))
.catch((err) => console.error("Failed to fetch links:", err));
}, []);
return (
<div
className={`flex flex-col fixed top-0 left-0 w-full h-full bg-black transition-transform transform ${
isMenuOpen ? 'translate-x-0' : '-translate-x-full'
} z-50`}
style={{ transitionDuration: '0.3s' }}
>
<div className="flex flex-col bg-gray-950/55 h-full">
{/* 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>
<div className="w-1/4 h-full">
<button
className="w-full h-full flex items-center justify-center"
onClick={() => setIsMenuOpen(false)}
>
<img src={closeIcon} alt="Close" className="w-1/2 h-1/2 cursor-pointer" />
</button>
</div>
return (
<div
className={`flex flex-col fixed top-0 left-0 w-full h-full bg-black transition-transform transform ${
isMenuOpen ? "translate-x-0" : "-translate-x-full"
} z-50`}
style={{ transitionDuration: "0.3s" }}
>
<div className="flex flex-col bg-gray-950/55 h-full">
{/* 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>
<div className="w-1/4 h-full">
<button
className="w-full h-full flex items-center justify-center"
onClick={() => setIsMenuOpen(false)}
>
<img
src={closeIcon}
alt="Close"
className="w-1/2 h-1/2 cursor-pointer"
/>
</button>
</div>
</div>
{/* Scrollable Navigation Links */}
<div className="overflow-y-auto flex flex-col p-5 w-full flex-grow">
<div className="flex flex-col space-y-2">
<NavLink
to="/"
className={({ isActive }) =>
`flex flex-row items-center gap-3 p-3 border-l-4 ${
isActive
? "border-l-sky-500/50 bg-black/50 text-white"
: "border-transparent hover:border-l-sky-500/50 hover:bg-white/5 text-white/80"
}`
}
onClick={() => setIsMenuOpen(false)}
>
<img src={homeIcon} alt="Home" className="w-5 h-5" />
<span className="text-lg">Home</span>
</NavLink>
<NavLink
to="/cache"
className={({ isActive }) =>
`flex flex-row items-center gap-3 p-3 border-l-4 ${
isActive
? "border-l-emerald-500/50 bg-black/50 text-white"
: "border-transparent hover:border-l-emerald-500/50 hover:bg-white/5 text-white/80"
}`
}
onClick={() => setIsMenuOpen(false)}
>
<img src={cacheIcon} alt="Cache" className="w-5 h-5" />
<span className="text-lg">Cache</span>
</NavLink>
<NavLink
to="/api"
className={({ isActive }) =>
`flex flex-row items-center gap-3 p-3 border-l-4 ${
isActive
? "border-l-indigo-500/50 bg-black/50 text-white"
: "border-transparent hover:border-l-indigo-500/50 hover:bg-white/5 text-white/80"
}`
}
onClick={() => setIsMenuOpen(false)}
>
<img src={apiIcon} alt="API" className="w-5 h-5" />
<span className="text-lg">API</span>
</NavLink>
<NavLink
to="/testplayer"
className={({ isActive }) =>
`flex flex-row items-center gap-3 p-3 border-l-4 ${
isActive
? "border-l-rose-700/50 bg-black/50 text-white"
: "border-transparent hover:border-l-rose-700/50 hover:bg-white/5 text-white/80"
}`
}
onClick={() => setIsMenuOpen(false)}
>
<img src={testPlayerIcon} alt="Test Player" className="w-5 h-5" />
<span className="text-lg">Test Player</span>
</NavLink>
</div>
{/* My Account Link at the Bottom of Scrollable Area */}
<div className="mt-auto pt-4">
<NavLink
to="/account"
className={({ isActive }) =>
`flex flex-row items-center gap-3 p-3 border-l-4 ${
isActive
? "border-l-yellow-500/50 bg-black/50 text-white"
: "border-transparent hover:border-l-yellow-500/50 hover:bg-white/5 text-white/80"
}`
}
onClick={() => setIsMenuOpen(false)}
>
<img src={accountIcon} alt="My Account" className="w-5 h-5" />
<span className="text-lg">My Account</span>
</NavLink>
</div>
</div>
{/* External Links */}
<div className="h-16 w-full flex flex-row bg-black/5">
<a
href={externalLinks.discord}
target="_blank"
rel="noopener noreferrer"
className="w-1/3 h-full flex items-center justify-center hover:bg-blue-950 group"
>
<img
src={discordIcon}
alt="Discord"
className="w-full h-2/3 p-1 cursor-pointer group-hover:animate-bounce"
/>
</a>
<a
href={externalLinks.telegram}
target="_blank"
rel="noopener noreferrer"
className="w-1/3 h-full flex items-center justify-center hover:bg-blue-400 group"
>
<img
src={telegramIcon}
alt="Telegram"
className="w-full h-2/3 p-1 cursor-pointer group-hover:animate-bounce"
/>
</a>
<a
href={externalLinks.gitea}
target="_blank"
rel="noopener noreferrer"
className="w-1/3 h-full flex items-center justify-center hover:bg-green-700 group"
>
<img
src={giteaIcon}
alt="Gitea"
className="w-full h-2/3 p-1 cursor-pointer group-hover:animate-bounce"
/>
</a>
</div>
</div>
</div>
{/* Scrollable Navigation Links */}
<div className="overflow-y-auto flex flex-col p-5 w-full flex-grow">
<div className="flex flex-col space-y-2">
<NavLink
to="/"
className={({ isActive }) =>
`flex flex-row items-center gap-3 p-3 border-l-4 ${
isActive
? 'border-l-sky-500/50 bg-black/50 text-white'
: 'border-transparent hover:border-l-sky-500/50 hover:bg-white/5 text-white/80'
}`
}
onClick={() => setIsMenuOpen(false)}
>
<img src={homeIcon} alt="Home" className="w-5 h-5" />
<span className="text-lg">Home</span>
</NavLink>
<NavLink
to="/cache"
className={({ isActive }) =>
`flex flex-row items-center gap-3 p-3 border-l-4 ${
isActive
? 'border-l-emerald-500/50 bg-black/50 text-white'
: 'border-transparent hover:border-l-emerald-500/50 hover:bg-white/5 text-white/80'
}`
}
onClick={() => setIsMenuOpen(false)}
>
<img src={cacheIcon} alt="Cache" className="w-5 h-5" />
<span className="text-lg">Cache</span>
</NavLink>
<NavLink
to="/api"
className={({ isActive }) =>
`flex flex-row items-center gap-3 p-3 border-l-4 ${
isActive
? 'border-l-indigo-500/50 bg-black/50 text-white'
: 'border-transparent hover:border-l-indigo-500/50 hover:bg-white/5 text-white/80'
}`
}
onClick={() => setIsMenuOpen(false)}
>
<img src={apiIcon} alt="API" className="w-5 h-5" />
<span className="text-lg">API</span>
</NavLink>
<NavLink
to="/testplayer"
className={({ isActive }) =>
`flex flex-row items-center gap-3 p-3 border-l-4 ${
isActive
? 'border-l-rose-700/50 bg-black/50 text-white'
: 'border-transparent hover:border-l-rose-700/50 hover:bg-white/5 text-white/80'
}`
}
onClick={() => setIsMenuOpen(false)}
>
<img src={testPlayerIcon} alt="Test Player" className="w-5 h-5" />
<span className="text-lg">Test Player</span>
</NavLink>
</div>
{/* My Account Link at the Bottom of Scrollable Area */}
<div className="mt-auto pt-4">
<NavLink
to="/account"
className={({ isActive }) =>
`flex flex-row items-center gap-3 p-3 border-l-4 ${
isActive
? 'border-l-yellow-500/50 bg-black/50 text-white'
: 'border-transparent hover:border-l-yellow-500/50 hover:bg-white/5 text-white/80'
}`
}
onClick={() => setIsMenuOpen(false)}
>
<img src={accountIcon} alt="My Account" className="w-5 h-5" />
<span className="text-lg">My Account</span>
</NavLink>
</div>
</div>
{/* External Links */}
<div className="h-16 w-full flex flex-row bg-black/5">
<a
href={externalLinks.discord}
target="_blank"
rel="noopener noreferrer"
className="w-1/3 h-full flex items-center justify-center hover:bg-blue-950 group"
>
<img
src={discordIcon}
alt="Discord"
className="w-full h-2/3 p-1 cursor-pointer group-hover:animate-bounce"
/>
</a>
<a
href={externalLinks.telegram}
target="_blank"
rel="noopener noreferrer"
className="w-1/3 h-full flex items-center justify-center hover:bg-blue-400 group"
>
<img
src={telegramIcon}
alt="Telegram"
className="w-full h-2/3 p-1 cursor-pointer group-hover:animate-bounce"
/>
</a>
<a
href={externalLinks.gitea}
target="_blank"
rel="noopener noreferrer"
className="w-1/3 h-full flex items-center justify-center hover:bg-green-700 group"
>
<img
src={giteaIcon}
alt="Gitea"
className="w-full h-2/3 p-1 cursor-pointer group-hover:animate-bounce"
/>
</a>
</div>
</div>
</div>
);
);
}
export default SideMenu;

View File

@ -2,9 +2,9 @@
details summary::-webkit-details-marker {
display: none;
}
details summary {
list-style: none;
cursor: pointer;
}
}
details summary {
list-style: none;
cursor: pointer;
}

View File

@ -1,13 +1,13 @@
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import { BrowserRouter } from 'react-router-dom'
import './index.css'
import App from './App.jsx'
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { BrowserRouter } from "react-router-dom";
import "./index.css";
import App from "./App.jsx";
createRoot(document.getElementById('root')).render(
<StrictMode>
<BrowserRouter>
<App />
</BrowserRouter>
</StrictMode>
)
createRoot(document.getElementById("root")).render(
<StrictMode>
<BrowserRouter>
<App />
</BrowserRouter>
</StrictMode>
);

View File

@ -1,8 +1,8 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import tailwindcss from '@tailwindcss/vite'
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react-swc";
import tailwindcss from "@tailwindcss/vite";
// https://vite.dev/config/
export default defineConfig({
plugins: [react(), tailwindcss()],
})
plugins: [react(), tailwindcss()],
});

View File

@ -1,5 +1,7 @@
"""Icon links module for social media links."""
data = {
'discord': 'https://discord.cdrm-project.com/',
'telegram': 'https://telegram.cdrm-project.com/',
'gitea': 'https://cdm-project.com/tpd94/cdrm-project'
}
"discord": "https://discord.cdrm-project.com/",
"telegram": "https://telegram.cdrm-project.com/",
"gitea": "https://cdm-project.com/tpd94/cdrm-project",
}

View File

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

View File

@ -1,28 +1,33 @@
"""Module to cache data to MariaDB."""
import os
import yaml
import mysql.connector
from mysql.connector import Error
def get_db_config():
# Configure your MariaDB connection
with open(f'{os.getcwd()}/configs/config.yaml', 'r') as file:
"""Get the database configuration for MariaDB."""
with open(
os.path.join(os.getcwd(), "configs", "config.yaml"), "r", encoding="utf-8"
) as file:
config = yaml.safe_load(file)
db_config = {
'host': f'{config["mariadb"]["host"]}',
'user': f'{config["mariadb"]["user"]}',
'password': f'{config["mariadb"]["password"]}',
'database': f'{config["mariadb"]["database"]}'
"host": f'{config["mariadb"]["host"]}',
"user": f'{config["mariadb"]["user"]}',
"password": f'{config["mariadb"]["password"]}',
"database": f'{config["mariadb"]["database"]}',
}
return db_config
def create_database():
"""Create the database for MariaDB."""
try:
with mysql.connector.connect(**get_db_config()) as conn:
cursor = conn.cursor()
cursor.execute('''
cursor.execute(
"""
CREATE TABLE IF NOT EXISTS licenses (
SERVICE VARCHAR(255),
PSSH TEXT,
@ -33,20 +38,33 @@ def create_database():
Cookies TEXT,
Data BLOB
)
''')
"""
)
conn.commit()
except Error as e:
print(f"Error: {e}")
def cache_to_db(service=None, pssh=None, kid=None, key=None, license_url=None, headers=None, cookies=None, data=None):
def cache_to_db(
service: str = "",
pssh: str = "",
kid: str = "",
key: str = "",
license_url: str = "",
headers: str = "",
cookies: str = "",
data: str = "",
):
"""Cache data to the database for MariaDB."""
try:
with mysql.connector.connect(**get_db_config()) as conn:
cursor = conn.cursor()
cursor.execute('SELECT 1 FROM licenses WHERE KID = %s', (kid,))
cursor.execute("SELECT 1 FROM licenses WHERE KID = %s", (kid,))
existing_record = cursor.fetchone()
cursor.execute('''
cursor.execute(
"""
INSERT INTO licenses (SERVICE, PSSH, KID, `Key`, License_URL, Headers, Cookies, Data)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s)
ON DUPLICATE KEY UPDATE
@ -57,7 +75,9 @@ def cache_to_db(service=None, pssh=None, kid=None, key=None, license_url=None, h
Headers = VALUES(Headers),
Cookies = VALUES(Cookies),
Data = VALUES(Data)
''', (service, pssh, kid, key, license_url, headers, cookies, data))
""",
(service, pssh, kid, key, license_url, headers, cookies, data),
)
conn.commit()
return True if existing_record else False
@ -65,61 +85,84 @@ def cache_to_db(service=None, pssh=None, kid=None, key=None, license_url=None, h
print(f"Error: {e}")
return False
def search_by_pssh_or_kid(search_filter):
"""Search the database by PSSH or KID for MariaDB."""
results = set()
try:
with mysql.connector.connect(**get_db_config()) as conn:
cursor = conn.cursor()
like_filter = f"%{search_filter}%"
cursor.execute('SELECT PSSH, KID, `Key` FROM licenses WHERE PSSH LIKE %s', (like_filter,))
cursor.execute(
"SELECT PSSH, KID, `Key` FROM licenses WHERE PSSH LIKE %s",
(like_filter,),
)
results.update(cursor.fetchall())
cursor.execute('SELECT PSSH, KID, `Key` FROM licenses WHERE KID LIKE %s', (like_filter,))
cursor.execute(
"SELECT PSSH, KID, `Key` FROM licenses WHERE KID LIKE %s",
(like_filter,),
)
results.update(cursor.fetchall())
final_results = [{'PSSH': row[0], 'KID': row[1], 'Key': row[2]} for row in results]
final_results = [
{"PSSH": row[0], "KID": row[1], "Key": row[2]} for row in results
]
return final_results[:20]
except Error as e:
print(f"Error: {e}")
return []
def get_key_by_kid_and_service(kid, service):
"""Get the key by KID and service for MariaDB."""
try:
with mysql.connector.connect(**get_db_config()) as conn:
cursor = conn.cursor()
cursor.execute('SELECT `Key` FROM licenses WHERE KID = %s AND SERVICE = %s', (kid, service))
cursor.execute(
"SELECT `Key` FROM licenses WHERE KID = %s AND SERVICE = %s",
(kid, service),
)
result = cursor.fetchone()
return result[0] if result else None
except Error as e:
print(f"Error: {e}")
return None
def get_kid_key_dict(service_name):
"""Get the KID and key dictionary for MariaDB."""
try:
with mysql.connector.connect(**get_db_config()) as conn:
cursor = conn.cursor()
cursor.execute('SELECT KID, `Key` FROM licenses WHERE SERVICE = %s', (service_name,))
cursor.execute(
"SELECT KID, `Key` FROM licenses WHERE SERVICE = %s", (service_name,)
)
return {row[0]: row[1] for row in cursor.fetchall()}
except Error as e:
print(f"Error: {e}")
return {}
def get_unique_services():
"""Get the unique services for MariaDB."""
try:
with mysql.connector.connect(**get_db_config()) as conn:
cursor = conn.cursor()
cursor.execute('SELECT DISTINCT SERVICE FROM licenses')
cursor.execute("SELECT DISTINCT SERVICE FROM licenses")
return [row[0] for row in cursor.fetchall()]
except Error as e:
print(f"Error: {e}")
return []
def key_count():
"""Get the key count for MariaDB."""
try:
with mysql.connector.connect(**get_db_config()) as conn:
cursor = conn.cursor()
cursor.execute('SELECT COUNT(KID) FROM licenses')
cursor.execute("SELECT COUNT(KID) FROM licenses")
return cursor.fetchone()[0]
except Error as e:
print(f"Error: {e}")

View File

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

View File

@ -0,0 +1,159 @@
"""Unified database operations module that automatically uses the correct backend."""
import os
from typing import Optional, List, Dict, Any
import yaml
# Import both backend modules
try:
import custom_functions.database.cache_to_db_sqlite as sqlite_db
except ImportError:
sqlite_db = None
try:
import custom_functions.database.cache_to_db_mariadb as mariadb_db
except ImportError:
mariadb_db = None
class DatabaseOperations:
"""Unified database operations class that automatically selects the correct backend."""
def __init__(self):
self.backend = self._get_database_backend()
self.db_module = self._get_db_module()
def _get_database_backend(self) -> str:
"""Get the database backend from config, default to sqlite."""
try:
config_path = os.path.join(os.getcwd(), "configs", "config.yaml")
with open(config_path, "r", encoding="utf-8") as file:
config = yaml.safe_load(file)
return config.get("database_type", "sqlite").lower()
except (FileNotFoundError, KeyError, yaml.YAMLError):
return "sqlite"
def _get_db_module(self):
"""Get the appropriate database module based on backend."""
if self.backend == "mariadb" and mariadb_db:
return mariadb_db
if sqlite_db:
return sqlite_db
raise ImportError(f"Database module for {self.backend} not available")
def get_backend_info(self) -> Dict[str, str]:
"""Get information about the current database backend being used."""
return {
"backend": self.backend,
"module": self.db_module.__name__ if self.db_module else "None",
}
def create_database(self) -> None:
"""Create the database using the configured backend."""
return self.db_module.create_database()
def cache_to_db(
self,
service: str = "",
pssh: str = "",
kid: str = "",
key: str = "",
license_url: str = "",
headers: str = "",
cookies: str = "",
data: str = "",
) -> bool:
"""Cache data to the database using the configured backend."""
return self.db_module.cache_to_db(
service=service,
pssh=pssh,
kid=kid,
key=key,
license_url=license_url,
headers=headers,
cookies=cookies,
data=data,
)
def search_by_pssh_or_kid(self, search_filter: str) -> List[Dict[str, str]]:
"""Search the database by PSSH or KID using the configured backend."""
return self.db_module.search_by_pssh_or_kid(search_filter)
def get_key_by_kid_and_service(self, kid: str, service: str) -> Optional[str]:
"""Get the key by KID and service using the configured backend."""
return self.db_module.get_key_by_kid_and_service(kid, service)
def get_kid_key_dict(self, service_name: str) -> Dict[str, str]:
"""Get the KID and key dictionary using the configured backend."""
return self.db_module.get_kid_key_dict(service_name)
def get_unique_services(self) -> List[str]:
"""Get the unique services using the configured backend."""
return self.db_module.get_unique_services()
def key_count(self) -> int:
"""Get the key count using the configured backend."""
return self.db_module.key_count()
# Create a singleton instance for easy import and use
db_ops = DatabaseOperations()
# Convenience functions that use the singleton instance
def get_backend_info() -> Dict[str, str]:
"""Get information about the current database backend being used."""
return db_ops.get_backend_info()
def create_database() -> None:
"""Create the database using the configured backend."""
return db_ops.create_database()
def cache_to_db(
service: str = "",
pssh: str = "",
kid: str = "",
key: str = "",
license_url: str = "",
headers: str = "",
cookies: str = "",
data: str = "",
) -> bool:
"""Cache data to the database using the configured backend."""
return db_ops.cache_to_db(
service=service,
pssh=pssh,
kid=kid,
key=key,
license_url=license_url,
headers=headers,
cookies=cookies,
data=data,
)
def search_by_pssh_or_kid(search_filter: str) -> List[Dict[str, str]]:
"""Search the database by PSSH or KID using the configured backend."""
return db_ops.search_by_pssh_or_kid(search_filter)
def get_key_by_kid_and_service(kid: str, service: str) -> Optional[str]:
"""Get the key by KID and service using the configured backend."""
return db_ops.get_key_by_kid_and_service(kid, service)
def get_kid_key_dict(service_name: str) -> Dict[str, str]:
"""Get the KID and key dictionary using the configured backend."""
return db_ops.get_kid_key_dict(service_name)
def get_unique_services() -> List[str]:
"""Get the unique services using the configured backend."""
return db_ops.get_unique_services()
def key_count() -> int:
"""Get the key count using the configured backend."""
return db_ops.key_count()

View File

@ -1,30 +1,43 @@
"""Module to manage the user database."""
import sqlite3
import os
import bcrypt
def create_user_database():
os.makedirs(f'{os.getcwd()}/databases/sql', exist_ok=True)
"""Create the user database."""
os.makedirs(os.path.join(os.getcwd(), "databases", "sql"), exist_ok=True)
with sqlite3.connect(f'{os.getcwd()}/databases/sql/users.db') as conn:
with sqlite3.connect(
os.path.join(os.getcwd(), "databases", "sql", "users.db")
) as conn:
cursor = conn.cursor()
cursor.execute('''
cursor.execute(
"""
CREATE TABLE IF NOT EXISTS user_info (
Username TEXT PRIMARY KEY,
Password TEXT,
Styled_Username TEXT,
API_Key TEXT
)
''')
"""
)
def add_user(username, password, api_key):
hashed_pw = bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt())
"""Add a user to the database."""
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(
os.path.join(os.getcwd(), "databases", "sql", "users.db")
) as conn:
cursor = conn.cursor()
try:
cursor.execute('INSERT INTO user_info (Username, Password, Styled_Username, API_Key) VALUES (?, ?, ?, ?)', (username.lower(), hashed_pw, username, api_key))
cursor.execute(
"INSERT INTO user_info (Username, Password, Styled_Username, API_Key) VALUES (?, ?, ?, ?)",
(username.lower(), hashed_pw, username, api_key),
)
conn.commit()
return True
except sqlite3.IntegrityError:
@ -32,69 +45,100 @@ def add_user(username, password, api_key):
def verify_user(username, password):
with sqlite3.connect(f'{os.getcwd()}/databases/sql/users.db') as conn:
"""Verify a user."""
with sqlite3.connect(
os.path.join(os.getcwd(), "databases", "sql", "users.db")
) as conn:
cursor = conn.cursor()
cursor.execute('SELECT Password FROM user_info WHERE Username = ?', (username.lower(),))
cursor.execute(
"SELECT Password FROM user_info WHERE Username = ?", (username.lower(),)
)
result = cursor.fetchone()
if result:
stored_hash = result[0]
# Ensure stored_hash is bytes; decode if it's still a string (SQLite may store as TEXT)
if isinstance(stored_hash, str):
stored_hash = stored_hash.encode('utf-8')
return bcrypt.checkpw(password.encode('utf-8'), stored_hash)
else:
return False
stored_hash = stored_hash.encode("utf-8")
return bcrypt.checkpw(password.encode("utf-8"), stored_hash)
return False
def fetch_api_key(username):
with sqlite3.connect(f'{os.getcwd()}/databases/sql/users.db') as conn:
"""Fetch the API key for a user."""
with sqlite3.connect(
os.path.join(os.getcwd(), "databases", "sql", "users.db")
) as conn:
cursor = conn.cursor()
cursor.execute('SELECT API_Key FROM user_info WHERE Username = ?', (username.lower(),))
cursor.execute(
"SELECT API_Key FROM user_info WHERE Username = ?", (username.lower(),)
)
result = cursor.fetchone()
if result:
return result[0]
else:
return None
return None
def change_password(username, new_password):
"""Change the password for a user."""
# Hash the new password
new_hashed_pw = bcrypt.hashpw(new_password.encode('utf-8'), bcrypt.gensalt())
new_hashed_pw = bcrypt.hashpw(new_password.encode("utf-8"), bcrypt.gensalt())
# Update the password in the database
with sqlite3.connect(f'{os.getcwd()}/databases/sql/users.db') as conn:
with sqlite3.connect(
os.path.join(os.getcwd(), "databases", "sql", "users.db")
) as conn:
cursor = conn.cursor()
cursor.execute('UPDATE user_info SET Password = ? WHERE Username = ?', (new_hashed_pw, username.lower()))
cursor.execute(
"UPDATE user_info SET Password = ? WHERE Username = ?",
(new_hashed_pw, username.lower()),
)
conn.commit()
return True
def change_api_key(username, new_api_key):
"""Change the API key for a user."""
# Update the API key in the database
with sqlite3.connect(f'{os.getcwd()}/databases/sql/users.db') as conn:
with sqlite3.connect(
os.path.join(os.getcwd(), "databases", "sql", "users.db")
) as conn:
cursor = conn.cursor()
cursor.execute('UPDATE user_info SET API_Key = ? WHERE Username = ?', (new_api_key, username.lower()))
cursor.execute(
"UPDATE user_info SET API_Key = ? WHERE Username = ?",
(new_api_key, username.lower()),
)
conn.commit()
return True
def fetch_styled_username(username):
with sqlite3.connect(f'{os.getcwd()}/databases/sql/users.db') as conn:
"""Fetch the styled username for a user."""
with sqlite3.connect(
os.path.join(os.getcwd(), "databases", "sql", "users.db")
) as conn:
cursor = conn.cursor()
cursor.execute('SELECT Styled_Username FROM user_info WHERE Username = ?', (username.lower(),))
cursor.execute(
"SELECT Styled_Username FROM user_info WHERE Username = ?",
(username.lower(),),
)
result = cursor.fetchone()
if result:
return result[0]
else:
return None
return None
def fetch_username_by_api_key(api_key):
with sqlite3.connect(f'{os.getcwd()}/databases/sql/users.db') as conn:
"""Fetch the username for a user by API key."""
with sqlite3.connect(
os.path.join(os.getcwd(), "databases", "sql", "users.db")
) as conn:
cursor = conn.cursor()
cursor.execute('SELECT Username FROM user_info WHERE API_Key = ?', (api_key,))
cursor.execute("SELECT Username FROM user_info WHERE API_Key = ?", (api_key,))
result = cursor.fetchone()
if result:
return result[0] # Return the username
else:
return None # If no user is found for the API key
return None # If no user is found for the API key

View File

@ -1,31 +1,45 @@
"""Module to decrypt the license using the API."""
import base64
import ast
import glob
import json
import os
from urllib.parse import urlparse
import binascii
import requests
from requests.exceptions import Timeout, RequestException
import yaml
from pywidevine.cdm import Cdm as widevineCdm
from pywidevine.device import Device as widevineDevice
from pywidevine.pssh import PSSH as widevinePSSH
from pyplayready.cdm import Cdm as playreadyCdm
from pyplayready.device import Device as playreadyDevice
from pyplayready.system.pssh import PSSH as playreadyPSSH
import requests
import base64
import ast
import glob
import os
import yaml
from urllib.parse import urlparse
from custom_functions.database.unified_db_ops import cache_to_db
def find_license_key(data, keywords=None):
"""Find the license key in the data."""
if keywords is None:
keywords = ["license", "licenseData", "widevine2License"] # Default list of keywords to search for
keywords = [
"license",
"licenseData",
"widevine2License",
] # Default list of keywords to search for
# If the data is a dictionary, check each key
if isinstance(data, dict):
for key, value in data.items():
if any(keyword in key.lower() for keyword in
keywords): # Check if any keyword is in the key (case-insensitive)
return value.replace("-", "+").replace("_", "/") # Return the value immediately when found
if any(
keyword in key.lower() for keyword in keywords
): # Check if any keyword is in the key (case-insensitive)
return value.replace("-", "+").replace(
"_", "/"
) # Return the value immediately when found
# Recursively check if the value is a dictionary or list
if isinstance(value, (dict, list)):
result = find_license_key(value, keywords) # Recursively search
@ -43,37 +57,52 @@ def find_license_key(data, keywords=None):
def find_license_challenge(data, keywords=None, new_value=None):
"""Find the license challenge in the data."""
if keywords is None:
keywords = ["license", "licenseData", "widevine2License", "licenseRequest"] # Default list of keywords to search for
keywords = [
"license",
"licenseData",
"widevine2License",
"licenseRequest",
] # Default list of keywords to search for
# If the data is a dictionary, check each key
if isinstance(data, dict):
for key, value in data.items():
if any(keyword in key.lower() for keyword in keywords): # Check if any keyword is in the key (case-insensitive)
if any(
keyword in key.lower() for keyword in keywords
): # Check if any keyword is in the key (case-insensitive)
data[key] = new_value # Modify the value in-place
# Recursively check if the value is a dictionary or list
elif isinstance(value, (dict, list)):
find_license_challenge(value, keywords, new_value) # Recursively modify in place
find_license_challenge(
value, keywords, new_value
) # Recursively modify in place
# If the data is a list, iterate through each item
elif isinstance(data, list):
for i, item in enumerate(data):
result = find_license_challenge(item, keywords, new_value) # Recursively modify in place
result = find_license_challenge(
item, keywords, new_value
) # Recursively modify in place
return data # Return the modified original data (no new structure is created)
def is_base64(string):
"""Check if the string is base64 encoded."""
try:
# Try decoding the string
decoded_data = base64.b64decode(string)
# Check if the decoded data, when re-encoded, matches the original string
return base64.b64encode(decoded_data).decode('utf-8') == string
except Exception:
return base64.b64encode(decoded_data).decode("utf-8") == string
except (binascii.Error, TypeError):
# If decoding or encoding fails, it's not Base64
return False
def is_url_and_split(input_str):
"""Check if the string is a URL and split it into protocol and FQDN."""
parsed = urlparse(input_str)
# Check if it's a valid URL with scheme and netloc
@ -81,364 +110,286 @@ def is_url_and_split(input_str):
protocol = parsed.scheme
fqdn = parsed.netloc
return True, protocol, fqdn
else:
return False, None, None
return False, None, None
def api_decrypt(pssh:str = None, license_url: str = None, proxy: str = None, headers: str = None, cookies: str = None, json_data: str = None, device: str = 'public', username: str = None):
print(f'Using device {device} for user {username}')
with open(f'{os.getcwd()}/configs/config.yaml', 'r') as file:
config = yaml.safe_load(file)
if config['database_type'].lower() == 'sqlite':
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 pssh is None:
return {
'status': 'error',
'message': 'No PSSH provided'
}
try:
if "</WRMHEADER>".encode("utf-16-le") in base64.b64decode(pssh): # PR
try:
pr_pssh = playreadyPSSH(pssh)
except Exception as error:
return {
'status': 'error',
'message': f'An error occurred processing PSSH\n\n{error}'
}
try:
if device == 'public':
base_name = config["default_pr_cdm"]
if not base_name.endswith(".prd"):
base_name += ".prd"
prd_files = glob.glob(f'{os.getcwd()}/configs/CDMs/PR/{base_name}')
else:
prd_files = glob.glob(f'{os.getcwd()}/configs/CDMs/PR/{base_name}')
if prd_files:
pr_device = playreadyDevice.load(prd_files[0])
else:
return {
'status': 'error',
'message': 'No default .prd file found'
}
else:
base_name = device
if not base_name.endswith(".prd"):
base_name += ".prd"
prd_files = glob.glob(f'{os.getcwd()}/configs/CDMs/{username}/PR/{base_name}')
else:
prd_files = glob.glob(f'{os.getcwd()}/configs/CDMs/{username}/PR/{base_name}')
if prd_files:
pr_device = playreadyDevice.load(prd_files[0])
else:
return {
'status': 'error',
'message': f'{base_name} does not exist'
}
except Exception as error:
return {
'status': 'error',
'message': f'An error occurred location PlayReady CDM file\n\n{error}'
}
try:
pr_cdm = playreadyCdm.from_device(pr_device)
except Exception as error:
return {
'status': 'error',
'message': f'An error occurred loading PlayReady CDM\n\n{error}'
}
try:
pr_session_id = pr_cdm.open()
except Exception as error:
return {
'status': 'error',
'message': f'An error occurred opening a CDM session\n\n{error}'
}
try:
pr_challenge = pr_cdm.get_license_challenge(pr_session_id, pr_pssh.wrm_headers[0])
except Exception as error:
return {
'status': 'error',
'message': f'An error occurred getting license challenge\n\n{error}'
}
try:
if headers:
format_headers = ast.literal_eval(headers)
else:
format_headers = None
except Exception as error:
return {
'status': 'error',
'message': f'An error occurred getting headers\n\n{error}'
}
try:
if cookies:
format_cookies = ast.literal_eval(cookies)
else:
format_cookies = None
except Exception as error:
return {
'status': 'error',
'message': f'An error occurred getting cookies\n\n{error}'
}
try:
if json_data and not is_base64(json_data):
format_json_data = ast.literal_eval(json_data)
else:
format_json_data = None
except Exception as error:
return {
'status': 'error',
'message': f'An error occurred getting json_data\n\n{error}'
}
licence = None
proxies = None
if proxy is not None:
is_url, protocol, fqdn = is_url_and_split(proxy)
if is_url:
proxies = {'http': proxy, 'https': proxy}
else:
return {
'status': 'error',
'message': f'Your proxy is invalid, please put it in the format of http(s)://fqdn.tld:port'
}
try:
licence = requests.post(
url=license_url,
headers=format_headers,
proxies=proxies,
cookies=format_cookies,
json=format_json_data if format_json_data is not None else None,
data=pr_challenge if format_json_data is None else None
)
except requests.exceptions.ConnectionError as error:
return {
'status': 'error',
'message': f'An error occurred sending license challenge through your proxy\n\n{error}'
}
except Exception as error:
return {
'status': 'error',
'message': f'An error occurred sending license reqeust\n\n{error}\n\n{licence.content}'
}
try:
pr_cdm.parse_license(pr_session_id, licence.text)
except Exception as error:
return {
'status': 'error',
'message': f'An error occurred parsing license content\n\n{error}\n\n{licence.content}'
}
returned_keys = ""
try:
keys = list(pr_cdm.get_keys(pr_session_id))
except Exception as error:
return {
'status': 'error',
'message': f'An error occurred getting keys\n\n{error}'
}
try:
for index, key in enumerate(keys):
if key.key_type != 'SIGNING':
cache_to_db(pssh=pssh, license_url=license_url, headers=headers, cookies=cookies,
data=pr_challenge if json_data is None else json_data, kid=key.key_id.hex,
key=key.key.hex())
if index != len(keys) - 1:
returned_keys += f"{key.key_id.hex}:{key.key.hex()}\n"
else:
returned_keys += f"{key.key_id.hex}:{key.key.hex()}"
except Exception as error:
return {
'status': 'error',
'message': f'An error occurred formatting keys\n\n{error}'
}
try:
pr_cdm.close(pr_session_id)
except Exception as error:
return {
'status': 'error',
'message': f'An error occurred closing session\n\n{error}'
}
try:
return {
'status': 'success',
'message': returned_keys
}
except Exception as error:
return {
'status': 'error',
'message': f'An error occurred getting returned_keys\n\n{error}'
}
except Exception as error:
return {
'status': 'error',
'message': f'An error occurred processing PSSH\n\n{error}'
}
def load_device(device_type, device, username, config):
"""Load the appropriate device file for PlayReady or Widevine."""
if device_type == "PR":
ext, config_key, class_loader = ".prd", "default_pr_cdm", playreadyDevice.load
base_dir = "PR"
else:
ext, config_key, class_loader = ".wvd", "default_wv_cdm", widevineDevice.load
base_dir = "WV"
if device == "public":
base_name = config[config_key]
if not base_name.endswith(ext):
base_name += ext
search_path = f"{os.getcwd()}/configs/CDMs/{base_dir}/{base_name}"
else:
base_name = device
if not base_name.endswith(ext):
base_name += ext
search_path = f"{os.getcwd()}/configs/CDMs/{username}/{base_dir}/{base_name}"
files = glob.glob(search_path)
if not files:
return None, f"No {ext} file found for device '{device}'"
try:
return class_loader(files[0]), None
except (IOError, OSError) as e:
return None, f"Failed to read device file: {e}"
except (ValueError, TypeError, AttributeError) as e:
return None, f"Failed to parse device file: {e}"
def prepare_request_data(headers, cookies, json_data, challenge, is_widevine):
"""Prepare headers, cookies, and json_data for the license request."""
try:
format_headers = ast.literal_eval(headers) if headers else None
except (ValueError, SyntaxError) as e:
raise ValueError(f"Invalid headers format: {e}") from e
try:
format_cookies = ast.literal_eval(cookies) if cookies else None
except (ValueError, SyntaxError) as e:
raise ValueError(f"Invalid cookies format: {e}") from e
format_json_data = None
if json_data and not is_base64(json_data):
try:
wv_pssh = widevinePSSH(pssh)
except Exception as error:
return {
'status': 'error',
'message': f'An error occurred processing PSSH\n\n{error}'
}
try:
if device == 'public':
base_name = config["default_wv_cdm"]
if not base_name.endswith(".wvd"):
base_name += ".wvd"
wvd_files = glob.glob(f'{os.getcwd()}/configs/CDMs/WV/{base_name}')
format_json_data = ast.literal_eval(json_data)
if is_widevine:
format_json_data = find_license_challenge(
data=format_json_data,
new_value=base64.b64encode(challenge).decode(),
)
except (ValueError, SyntaxError) as e:
raise ValueError(f"Invalid json_data format: {e}") from e
except (TypeError, AttributeError) as e:
raise ValueError(f"Error processing json_data: {e}") from e
return format_headers, format_cookies, format_json_data
def send_license_request(license_url, headers, cookies, json_data, challenge, proxies):
"""Send the license request and return the response."""
try:
response = requests.post(
url=license_url,
headers=headers,
proxies=proxies,
cookies=cookies,
json=json_data if json_data is not None else None,
data=challenge if json_data is None else None,
timeout=10,
)
return response, None
except ConnectionError as error:
return None, f"Connection error: {error}"
except Timeout as error:
return None, f"Request timeout: {error}"
except RequestException as error:
return None, f"Request error: {error}"
def extract_and_cache_keys(
cdm,
session_id,
cache_to_db,
pssh,
license_url,
headers,
cookies,
challenge,
json_data,
is_widevine,
):
"""Extract keys from the session and cache them."""
returned_keys = ""
try:
keys = list(cdm.get_keys(session_id))
for index, key in enumerate(keys):
# Widevine: key.type, PlayReady: key.key_type
key_type = getattr(key, "type", getattr(key, "key_type", None))
kid = getattr(key, "kid", getattr(key, "key_id", None))
if key_type != "SIGNING" and kid is not None:
cache_to_db(
pssh=pssh,
license_url=license_url,
headers=headers,
cookies=cookies,
data=challenge if json_data is None else json_data,
kid=kid.hex,
key=key.key.hex(),
)
if index != len(keys) - 1:
returned_keys += f"{kid.hex}:{key.key.hex()}\n"
else:
wvd_files = glob.glob(f'{os.getcwd()}/configs/CDMs/WV/{base_name}')
if wvd_files:
wv_device = widevineDevice.load(wvd_files[0])
else:
return {
'status': 'error',
'message': 'No default .wvd file found'
}
else:
base_name = device
if not base_name.endswith(".wvd"):
base_name += ".wvd"
wvd_files = glob.glob(f'{os.getcwd()}/configs/CDMs/{username}/WV/{base_name}')
else:
wvd_files = glob.glob(f'{os.getcwd()}/configs/CDMs/{username}/WV/{base_name}')
if wvd_files:
wv_device = widevineDevice.load(wvd_files[0])
else:
return {
'status': 'error',
'message': f'{base_name} does not exist'
}
except Exception as error:
returned_keys += f"{kid.hex}:{key.key.hex()}"
return returned_keys, None
except AttributeError as error:
return None, f"Error accessing CDM keys: {error}"
except (TypeError, ValueError) as error:
return None, f"Error processing keys: {error}"
def api_decrypt(
pssh: str = "",
license_url: str = "",
proxy: str = "",
headers: str = "",
cookies: str = "",
json_data: str = "",
device: str = "public",
username: str = "",
):
"""Decrypt the license using the API."""
print(f"Using device {device} for user {username}")
with open(f"{os.getcwd()}/configs/config.yaml", "r", encoding="utf-8") as file:
config = yaml.safe_load(file)
if pssh == "":
return {"status": "error", "message": "No PSSH provided"}
# Detect PlayReady or Widevine
try:
is_pr = "</WRMHEADER>".encode("utf-16-le") in base64.b64decode(pssh)
except (binascii.Error, TypeError) as error:
return {
"status": "error",
"message": f"An error occurred processing PSSH\n\n{error}",
}
device_type = "PR" if is_pr else "WV"
cdm_class = playreadyCdm if is_pr else widevineCdm
pssh_class = playreadyPSSH if is_pr else widevinePSSH
# Load device
device_obj, device_err = load_device(device_type, device, username, config)
if device_obj is None:
return {"status": "error", "message": device_err}
# Create CDM
try:
cdm = cdm_class.from_device(device_obj)
except (IOError, ValueError, AttributeError) as error:
return {
"status": "error",
"message": f"An error occurred loading {device_type} CDM\n\n{error}",
}
# Open session
try:
session_id = cdm.open()
except (IOError, ValueError, AttributeError) as error:
return {
"status": "error",
"message": f"An error occurred opening a CDM session\n\n{error}",
}
# Parse PSSH and get challenge
try:
pssh_obj = pssh_class(pssh)
if is_pr:
challenge = cdm.get_license_challenge(session_id, pssh_obj.wrm_headers[0])
else:
challenge = cdm.get_license_challenge(session_id, pssh_obj)
except (ValueError, AttributeError, IndexError) as error:
return {
"status": "error",
"message": f"An error occurred getting license challenge\n\n{error}",
}
# Prepare request data
try:
format_headers, format_cookies, format_json_data = prepare_request_data(
headers, cookies, json_data, challenge, is_widevine=(not is_pr)
)
except (ValueError, SyntaxError) as error:
return {
"status": "error",
"message": f"An error occurred preparing request data\n\n{error}",
}
# Prepare proxies
proxies = ""
if proxy != "":
is_url, protocol, fqdn = is_url_and_split(proxy)
if is_url:
proxies = {"http": proxy, "https": proxy}
else:
return {
'status': 'error',
'message': f'An error occurred location Widevine CDM file\n\n{error}'
"status": "error",
"message": "Your proxy is invalid, please put it in the format of http(s)://fqdn.tld:port",
}
try:
wv_cdm = widevineCdm.from_device(wv_device)
except Exception as error:
return {
'status': 'error',
'message': f'An error occurred loading Widevine CDM\n\n{error}'
}
try:
wv_session_id = wv_cdm.open()
except Exception as error:
return {
'status': 'error',
'message': f'An error occurred opening a CDM session\n\n{error}'
}
try:
wv_challenge = wv_cdm.get_license_challenge(wv_session_id, wv_pssh)
except Exception as error:
return {
'status': 'error',
'message': f'An error occurred getting license challenge\n\n{error}'
}
try:
if headers:
format_headers = ast.literal_eval(headers)
else:
format_headers = None
except Exception as error:
return {
'status': 'error',
'message': f'An error occurred getting headers\n\n{error}'
}
try:
if cookies:
format_cookies = ast.literal_eval(cookies)
else:
format_cookies = None
except Exception as error:
return {
'status': 'error',
'message': f'An error occurred getting cookies\n\n{error}'
}
try:
if json_data and not is_base64(json_data):
format_json_data = ast.literal_eval(json_data)
format_json_data = find_license_challenge(data=format_json_data, new_value=base64.b64encode(wv_challenge).decode())
else:
format_json_data = None
except Exception as error:
return {
'status': 'error',
'message': f'An error occurred getting json_data\n\n{error}'
}
licence = None
proxies = None
if proxy is not None:
is_url, protocol, fqdn = is_url_and_split(proxy)
if is_url:
proxies = {'http': proxy, 'https': proxy}
try:
licence = requests.post(
url=license_url,
headers=format_headers,
proxies=proxies,
cookies=format_cookies,
json=format_json_data if format_json_data is not None else None,
data=wv_challenge if format_json_data is None else None
)
except requests.exceptions.ConnectionError as error:
return {
'status': 'error',
'message': f'An error occurred sending license challenge through your proxy\n\n{error}'
}
except Exception as error:
return {
'status': 'error',
'message': f'An error occurred sending license reqeust\n\n{error}\n\n{licence.content}'
}
try:
wv_cdm.parse_license(wv_session_id, licence.content)
except:
# Send license request
licence, req_err = send_license_request(
license_url,
format_headers,
format_cookies,
format_json_data,
challenge,
proxies,
)
if licence is None:
return {"status": "error", "message": req_err}
# Parse license
try:
if is_pr:
cdm.parse_license(session_id, licence.text)
else:
try:
license_json = licence.json()
license_value = find_license_key(license_json)
wv_cdm.parse_license(wv_session_id, license_value)
except Exception as error:
return {
'status': 'error',
'message': f'An error occurred parsing license content\n\n{error}\n\n{licence.content}'
}
returned_keys = ""
try:
keys = list(wv_cdm.get_keys(wv_session_id))
except Exception as error:
return {
'status': 'error',
'message': f'An error occurred getting keys\n\n{error}'
}
try:
for index, key in enumerate(keys):
if key.type != 'SIGNING':
cache_to_db(pssh=pssh, license_url=license_url, headers=headers, cookies=cookies, data=wv_challenge if json_data is None else json_data, kid=key.kid.hex, key=key.key.hex())
if index != len(keys) - 1:
returned_keys += f"{key.kid.hex}:{key.key.hex()}\n"
cdm.parse_license(session_id, licence.content) # type: ignore[arg-type]
except (ValueError, TypeError):
# Try to extract license from JSON
try:
license_json = licence.json()
license_value = find_license_key(license_json)
if license_value is not None:
cdm.parse_license(session_id, license_value)
else:
returned_keys += f"{key.kid.hex}:{key.key.hex()}"
except Exception as error:
return {
'status': 'error',
'message': f'An error occurred formatting keys\n\n{error}'
}
try:
wv_cdm.close(wv_session_id)
except Exception as error:
return {
'status': 'error',
'message': f'An error occurred closing session\n\n{error}'
}
try:
return {
'status': 'success',
'message': returned_keys
}
except Exception as error:
return {
'status': 'error',
'message': f'An error occurred getting returned_keys\n\n{error}'
}
return {
"status": "error",
"message": f"Could not extract license from JSON: {license_json}",
}
except (ValueError, json.JSONDecodeError, AttributeError) as error:
return {
"status": "error",
"message": f"An error occurred parsing license content\n\n{error}\n\n{licence.content}",
}
except (ValueError, TypeError, AttributeError) as error:
return {
"status": "error",
"message": f"An error occurred parsing license content\n\n{error}\n\n{licence.content}",
}
# Extract and cache keys
returned_keys, key_err = extract_and_cache_keys(
cdm,
session_id,
cache_to_db,
pssh,
license_url,
headers,
cookies,
challenge,
json_data,
is_widevine=(not is_pr),
)
if returned_keys == "":
return {"status": "error", "message": key_err}
# Close session
try:
cdm.close(session_id)
except (IOError, ValueError, AttributeError) as error:
return {
"status": "error",
"message": f"An error occurred closing session\n\n{error}",
}
return {"status": "success", "message": returned_keys}

View File

@ -1,68 +1,106 @@
"""Module to check for and download CDM files."""
import os
import sys
import yaml
import requests
CONFIG_PATH = os.path.join(os.getcwd(), "configs", "config.yaml")
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')
def load_config():
"""Load the config file."""
with open(CONFIG_PATH, "r", encoding="utf-8") as file:
return yaml.safe_load(file)
def save_config(config):
"""Save the config file."""
with open(CONFIG_PATH, "w", encoding="utf-8") as file:
yaml.dump(config, file)
def prompt_yes_no(message):
"""Prompt the user for a yes or no answer."""
answer = " "
while answer[0].upper() not in ["Y", "N"]:
answer = input(message)
return answer[0].upper() == "Y"
def check_for_cdm(config_key, file_ext, download_url, cdm_dir, cdm_name):
"""Check for a CDM file."""
config = load_config()
cdm_value = config.get(config_key, "")
cdm_dir_path = os.path.join(os.getcwd(), "configs", "CDMs", cdm_dir)
os.makedirs(cdm_dir_path, exist_ok=True)
if not cdm_value:
if prompt_yes_no(
f"No default {cdm_name} CDM specified, would you like to download one "
"from The CDM Project? (Y)es / (N)o: "
):
response = requests.get(download_url, timeout=10)
if response.status_code == 200:
with open(f'{os.getcwd()}/configs/CDMs/WV/public.wvd', 'wb') as file:
file_path = os.path.join(cdm_dir_path, f"public.{file_ext}")
with open(file_path, "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")
config[config_key] = "public"
save_config(config)
print(f"Successfully downloaded {cdm_name} 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")
else:
base_name = config["default_wv_cdm"]
if not base_name.endswith(".wvd"):
base_name += ".wvd"
if os.path.exists(f'{os.getcwd()}/configs/CDMs/WV/{base_name}'):
return
sys.exit(
f"Download failed, please try again, or place a .{file_ext} file "
f"in {cdm_dir_path} and specify the name in {CONFIG_PATH}"
)
else:
exit(f"Widevine CDM {base_name} does not exist in {os.getcwd()}/configs/CDMs/WV")
def check_for_prd_cdm():
with open(f'{os.getcwd()}/configs/config.yaml', 'r') as file:
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')
sys.exit(
f"Place a .{file_ext} file in {cdm_dir_path} and specify the name in {CONFIG_PATH}"
)
else:
base_name = (
cdm_value
if cdm_value.endswith(f".{file_ext}")
else f"{cdm_value}.{file_ext}"
)
file_path = os.path.join(cdm_dir_path, base_name)
if os.path.exists(file_path):
return
# Prompt to download if file is missing, even if config has a value
if prompt_yes_no(
f"{cdm_name} CDM {base_name} does not exist in {cdm_dir_path}. Would you like to download it from The CDM Project? (Y)es/(N)o: "
):
response = requests.get(download_url, timeout=10)
if response.status_code == 200:
with open(f'{os.getcwd()}/configs/CDMs/PR/public.prd', 'wb') as file:
with open(file_path, "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")
config[config_key] = base_name.replace(f".{file_ext}", "")
save_config(config)
print(f"Successfully downloaded {cdm_name} 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")
else:
base_name = config["default_pr_cdm"]
if not base_name.endswith(".prd"):
base_name += ".prd"
if os.path.exists(f'{os.getcwd()}/configs/CDMs/PR/{base_name}'):
return
sys.exit(
f"Download failed, please try again, or place a .{file_ext} file "
f"in {cdm_dir_path} and specify the name in {CONFIG_PATH}"
)
else:
exit(f"PlayReady CDM {base_name} does not exist in {os.getcwd()}/configs/CDMs/WV")
sys.exit(
f"Place a .{file_ext} file in {cdm_dir_path} and specify the name in {CONFIG_PATH}"
)
def check_for_cdms():
check_for_wvd_cdm()
check_for_prd_cdm()
"""Check for CDM files."""
check_for_cdm(
config_key="default_wv_cdm",
file_ext="wvd",
download_url="https://cdm-project.com/CDRM-Team/CDMs/raw/branch/main/Widevine/L3/public.wvd",
cdm_dir="WV",
cdm_name="Widevine",
)
check_for_cdm(
config_key="default_pr_cdm",
file_ext="prd",
download_url="https://cdm-project.com/CDRM-Team/CDMs/raw/branch/main/Playready/SL2000/public.prd",
cdm_dir="PR",
cdm_name="PlayReady",
)

View File

@ -1,10 +1,13 @@
"""Module to check for the config file."""
import os
def check_for_config_file():
if os.path.exists(f'{os.getcwd()}/configs/config.yaml'):
"""Check for the config file."""
if os.path.exists(os.path.join(os.getcwd(), "configs", "config.yaml")):
return
else:
default_config = """\
default_config = """
default_wv_cdm: ''
default_pr_cdm: ''
secret_key_flask: 'secretkey'
@ -21,6 +24,8 @@ remote_cdm_secret: ''
# port: ''
# database: ''
"""
with open(f'{os.getcwd()}/configs/config.yaml', 'w') as f:
with open(
os.path.join(os.getcwd(), "configs", "config.yaml"), "w", encoding="utf-8"
) as f:
f.write(default_config)
return
return

View File

@ -1,37 +1,159 @@
"""Module to check for the database with unified backend support."""
import os
from typing import Dict, Any
import yaml
def check_for_sqlite_database():
with open(f'{os.getcwd()}/configs/config.yaml', 'r') as file:
config = yaml.safe_load(file)
if os.path.exists(f'{os.getcwd()}/databases/key_cache.db'):
return
else:
if config['database_type'].lower() != 'mariadb':
from custom_functions.database.cache_to_db_sqlite import create_database
create_database()
return
from custom_functions.database.unified_db_ops import (
db_ops,
get_backend_info,
key_count,
)
from custom_functions.database.user_db import create_user_database
def get_database_config() -> Dict[str, Any]:
"""Get the database configuration from config.yaml."""
try:
config_path = os.path.join(os.getcwd(), "configs", "config.yaml")
with open(config_path, "r", encoding="utf-8") as file:
config = yaml.safe_load(file)
return config
except (FileNotFoundError, KeyError, yaml.YAMLError) as e:
print(f"Warning: Could not load config.yaml: {e}")
return {"database_type": "sqlite"} # Default fallback
def check_for_sqlite_database() -> None:
"""Check for the SQLite database file and create if needed."""
config = get_database_config()
database_type = config.get("database_type", "sqlite").lower()
# Only check for SQLite file if we're using SQLite
if database_type == "sqlite":
sqlite_path = os.path.join(os.getcwd(), "databases", "sql", "key_cache.db")
if not os.path.exists(sqlite_path):
print("SQLite database not found, creating...")
# Ensure directory exists
os.makedirs(os.path.dirname(sqlite_path), exist_ok=True)
db_ops.create_database()
print(f"SQLite database created at: {sqlite_path}")
else:
return
print(f"SQLite database found at: {sqlite_path}")
def check_for_user_database():
if os.path.exists(f'{os.getcwd()}/databases/users.db'):
return
else:
from custom_functions.database.user_db import create_user_database
def check_for_mariadb_database() -> None:
"""Check for the MariaDB database and create if needed."""
config = get_database_config()
database_type = config.get("database_type", "sqlite").lower()
# Only check MariaDB if we're using MariaDB
if database_type == "mariadb":
try:
print("Checking MariaDB connection and creating database if needed...")
db_ops.create_database()
print("MariaDB database check completed successfully")
except Exception as e:
print(f"Error checking/creating MariaDB database: {e}")
print("Falling back to SQLite...")
# Fallback to SQLite if MariaDB fails
fallback_config_path = os.path.join(os.getcwd(), "configs", "config.yaml")
try:
with open(fallback_config_path, "r", encoding="utf-8") as file:
config = yaml.safe_load(file)
config["database_type"] = "sqlite"
with open(fallback_config_path, "w", encoding="utf-8") as file:
yaml.safe_dump(config, file)
check_for_sqlite_database()
except Exception as fallback_error:
print(f"Error during fallback to SQLite: {fallback_error}")
def check_for_user_database() -> None:
"""Check for the user database and create if needed."""
user_db_path = os.path.join(os.getcwd(), "databases", "users.db")
if not os.path.exists(user_db_path):
print("User database not found, creating...")
# Ensure directory exists
os.makedirs(os.path.dirname(user_db_path), exist_ok=True)
create_user_database()
def check_for_mariadb_database():
with open(f'{os.getcwd()}/configs/config.yaml', 'r') as file:
config = yaml.safe_load(file)
if config['database_type'].lower() == 'mariadb':
from custom_functions.database.cache_to_db_mariadb import create_database
create_database()
return
print(f"User database created at: {user_db_path}")
else:
return
print(f"User database found at: {user_db_path}")
def check_for_sql_database():
check_for_sqlite_database()
check_for_mariadb_database()
check_for_user_database()
def check_for_sql_database() -> None:
"""Check for the SQL database based on configuration."""
print("=== Database Check Starting ===")
# Get backend information
backend_info = get_backend_info()
print(f"Database backend: {backend_info['backend']}")
print(f"Using module: {backend_info['module']}")
config = get_database_config()
database_type = config.get("database_type", "sqlite").lower()
# Ensure databases directory exists
os.makedirs(os.path.join(os.getcwd(), "databases"), exist_ok=True)
os.makedirs(os.path.join(os.getcwd(), "databases", "sql"), exist_ok=True)
# Check main database based on type
if database_type == "mariadb":
check_for_mariadb_database()
else: # Default to SQLite
check_for_sqlite_database()
# Always check user database (always SQLite)
check_for_user_database()
print("=== Database Check Completed ===")
def get_database_status() -> Dict[str, Any]:
"""Get the current database status and configuration."""
config = get_database_config()
backend_info = get_backend_info()
status = {
"configured_backend": config.get("database_type", "sqlite").lower(),
"active_backend": backend_info["backend"],
"module_in_use": backend_info["module"],
"sqlite_file_exists": os.path.exists(
os.path.join(os.getcwd(), "databases", "sql", "key_cache.db")
),
"user_db_exists": os.path.exists(
os.path.join(os.getcwd(), "databases", "users.db")
),
}
# Try to get key count to verify database is working
try:
status["key_count"] = key_count()
status["database_operational"] = True
except Exception as e:
status["key_count"] = "Error"
status["database_operational"] = False
status["error"] = str(e)
return status
def print_database_status() -> None:
"""Print a formatted database status report."""
status = get_database_status()
print("\n=== Database Status Report ===")
print(f"Configured Backend: {status['configured_backend']}")
print(f"Active Backend: {status['active_backend']}")
print(f"Module in Use: {status['module_in_use']}")
print(f"SQLite File Exists: {status['sqlite_file_exists']}")
print(f"User DB Exists: {status['user_db_exists']}")
print(f"Database Operational: {status['database_operational']}")
print(f"Key Count: {status['key_count']}")
if not status["database_operational"]:
print(f"Error: {status.get('error', 'Unknown error')}")
print("==============================\n")

View File

@ -1,44 +1,53 @@
"""Module to check for the folders."""
import os
def check_for_config_folder():
if os.path.isdir(f'{os.getcwd()}/configs'):
return
else:
os.mkdir(f'{os.getcwd()}/configs')
"""Check for the config folder."""
if os.path.isdir(os.path.join(os.getcwd(), "configs")):
return
os.mkdir(os.path.join(os.getcwd(), "configs"))
return
def check_for_database_folder():
if os.path.isdir(f'{os.getcwd()}/databases'):
return
else:
os.mkdir(f'{os.getcwd()}/databases')
os.mkdir(f'{os.getcwd()}/databases/sql')
"""Check for the database folder."""
if os.path.isdir(os.path.join(os.getcwd(), "databases")):
return
os.mkdir(os.path.join(os.getcwd(), "databases"))
os.mkdir(os.path.join(os.getcwd(), "databases", "sql"))
return
def check_for_cdm_folder():
if os.path.isdir(f'{os.getcwd()}/configs/CDMs'):
return
else:
os.mkdir(f'{os.getcwd()}/configs/CDMs')
"""Check for the CDM folder."""
if os.path.isdir(os.path.join(os.getcwd(), "configs", "CDMs")):
return
os.mkdir(os.path.join(os.getcwd(), "configs", "CDMs"))
return
def check_for_wv_cdm_folder():
if os.path.isdir(f'{os.getcwd()}/configs/CDMs/WV'):
return
else:
os.mkdir(f'{os.getcwd()}/configs/CDMs/WV')
"""Check for the Widevine CDM folder."""
if os.path.isdir(os.path.join(os.getcwd(), "configs", "CDMs", "WV")):
return
os.mkdir(os.path.join(os.getcwd(), "configs", "CDMs", "WV"))
return
def check_for_cdm_pr_folder():
if os.path.isdir(f'{os.getcwd()}/configs/CDMs/PR'):
return
else:
os.mkdir(f'{os.getcwd()}/configs/CDMs/PR')
"""Check for the PlayReady CDM folder."""
if os.path.isdir(os.path.join(os.getcwd(), "configs", "CDMs", "PR")):
return
os.mkdir(os.path.join(os.getcwd(), "configs", "CDMs", "PR"))
return
def folder_checks():
"""Check for the folders."""
check_for_config_folder()
check_for_database_folder()
check_for_cdm_folder()
check_for_wv_cdm_folder()
check_for_cdm_pr_folder()
check_for_cdm_pr_folder()

View File

@ -1,11 +1,29 @@
"""Module to run the prechecks."""
import os
import subprocess
from custom_functions.prechecks.folder_checks import folder_checks
from custom_functions.prechecks.config_file_checks import check_for_config_file
from custom_functions.prechecks.database_checks import check_for_sql_database
from custom_functions.prechecks.cdm_checks import check_for_cdms
def check_frontend_built():
"""Check if the frontend is built; if not, run build.py."""
frontend_dist = os.path.join(os.getcwd(), "frontend-dist")
frontend_dist = os.path.abspath(frontend_dist)
if not os.path.exists(frontend_dist) or not os.listdir(frontend_dist):
print("Frontend has not been built. Running build.py...")
subprocess.run(["python", "build.py"], check=True)
else:
print("Frontend build found.")
def run_precheck():
"""Run the prechecks."""
check_frontend_built()
folder_checks()
check_for_config_file()
check_for_cdms()
check_for_sql_database()
return

View File

@ -1,49 +1,52 @@
"""Module to check for the Python version and environment."""
import sys
import os
import subprocess
import venv
import importlib.util
def version_check():
major_version = sys.version_info.major
minor_version = sys.version_info.minor
"""Check for the Python version."""
if sys.version_info < (3, 12):
sys.exit("Python version 3.12 or higher is required")
if major_version >= 3:
if minor_version >= 12:
return
else:
exit("Python version 3.12 or higher is required")
else:
exit("Python 2 detected, Python version 3.12 or higher is required")
def pip_check():
try:
import pip
return
except ImportError:
exit("Pip is not installed")
"""Check for the pip installation."""
if importlib.util.find_spec("pip") is None:
sys.exit("Pip is not installed")
def venv_check():
# Check if we're already inside a virtual environment
if hasattr(sys, 'real_prefix') or (hasattr(sys, 'base_prefix') and sys.base_prefix != sys.prefix):
"""Check for the virtual environment."""
if hasattr(sys, "real_prefix") or (
hasattr(sys, "base_prefix") and sys.base_prefix != sys.prefix
):
return
venv_path = os.path.join(os.getcwd(), 'cdrm-venv')
venv_python = os.path.join(venv_path, 'bin', 'python') if os.name != 'nt' else os.path.join(venv_path, 'Scripts', 'python.exe')
venv_path = os.path.join(os.getcwd(), "cdrm-venv")
venv_python = (
os.path.join(venv_path, "bin", "python")
if os.name != "nt"
else os.path.join(venv_path, "Scripts", "python.exe")
)
# If venv already exists, restart script using its Python
if os.path.exists(venv_path):
subprocess.call([venv_python] + sys.argv)
sys.exit()
# Ask user for permission to create a virtual environment
answer = ''
while not answer or answer[0].upper() not in {'Y', 'N'}:
answer = input(
'Program is not running from a venv. To maintain compatibility and dependencies, this program must be run from one.\n'
'Would you like me to create one for you? (Y/N): '
answer = (
input(
"Program is not running from a virtual environment. To maintain "
"compatibility, this program must be run from one.\n"
"Would you like to create one? (Y/N): "
)
if answer[0].upper() == 'Y':
.strip()
.upper()
)
if answer.startswith("Y"):
print("Creating virtual environment...")
venv.create(venv_path, with_pip=True)
subprocess.call([venv_python] + sys.argv)
@ -54,32 +57,57 @@ def venv_check():
def requirements_check():
try:
import pywidevine
import pyplayready
import flask
import flask_cors
import yaml
import mysql.connector
"""Check for the requirements."""
required_packages = [
"pywidevine",
"pyplayready",
"flask",
"flask_cors",
"yaml",
"mysql.connector",
]
missing = []
for pkg in required_packages:
if "." in pkg:
parent, _ = pkg.split(".", 1)
if (
importlib.util.find_spec(parent) is None
or importlib.util.find_spec(pkg) is None
):
missing.append(pkg)
else:
if importlib.util.find_spec(pkg) is None:
missing.append(pkg)
if not missing:
return
except ImportError:
while True:
user_input = input("Missing packages. Do you want to install them? (Y/N): ").strip().upper()
if user_input == 'Y':
print("Installing packages from requirements.txt...")
subprocess.check_call([sys.executable, "-m", "pip", "install", "-r", "requirements.txt"])
print("Installation complete.")
break
elif user_input == 'N':
print("Dependencies required, please install them and run again.")
sys.exit()
else:
print("Invalid input. Please enter 'Y' to install or 'N' to exit.")
while True:
user_input = (
input(
f"Missing packages: {', '.join(missing)}. Do you want to install them? (Y/N): "
)
.strip()
.upper()
)
if user_input == "Y":
print("Installing packages from requirements.txt...")
subprocess.check_call(
[sys.executable, "-m", "pip", "install", "-r", "requirements.txt"]
)
print("Installation complete.")
break
if user_input == "N":
print("Dependencies required, please install them and run again.")
sys.exit()
else:
print("Invalid input. Please enter 'Y' to install or 'N' to exit.")
def run_python_checks():
if getattr(sys, 'frozen', False): # Check if running from PyInstaller
"""Run the Python checks."""
if getattr(sys, "frozen", False): # Check if running from PyInstaller
return
version_check()
pip_check()
venv_check()
requirements_check()
requirements_check()

View File

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

28
main.py
View File

@ -1,9 +1,10 @@
from custom_functions.prechecks.python_checks import run_python_checks
run_python_checks()
from custom_functions.prechecks.precheck import run_precheck
run_precheck()
"""Main module to run the application."""
import os
import yaml
from flask import Flask
from flask_cors import CORS
from routes.react import react_bp
from routes.api import api_bp
from routes.remote_device_wv import remotecdm_wv_bp
@ -13,12 +14,19 @@ 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
from custom_functions.prechecks.python_checks import run_python_checks
from custom_functions.prechecks.precheck import run_precheck
run_python_checks()
run_precheck()
app = Flask(__name__)
with open(f'{os.getcwd()}/configs/config.yaml', 'r') as file:
with open(
os.path.join(os.getcwd(), "configs", "config.yaml"), "r", encoding="utf-8"
) as file:
config = yaml.safe_load(file)
app.secret_key = config['secret_key_flask']
app.secret_key = config["secret_key_flask"]
CORS(app)
@ -33,5 +41,5 @@ app.register_blueprint(remotecdm_wv_bp)
app.register_blueprint(remotecdm_pr_bp)
app.register_blueprint(user_change_bp)
if __name__ == '__main__':
app.run(debug=True, host='0.0.0.0')
if __name__ == "__main__":
app.run(debug=True, host="0.0.0.0")

19
pyproject.toml Normal file
View File

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

View File

@ -1,9 +1,10 @@
Flask~=3.1.0
Flask
Flask-Cors
pywidevine~=1.8.0
pyplayready~=0.6.0
requests~=2.32.3
requests
protobuf~=4.25.6
PyYAML~=6.0.2
PyYAML
mysql-connector-python
bcrypt
bcrypt
black

View File

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

View File

@ -1,37 +1,42 @@
"""Module to handle the login process."""
from flask import Blueprint, request, jsonify, session
from custom_functions.database.user_db import verify_user
login_bp = Blueprint(
'login_bp',
"login_bp",
__name__,
)
@login_bp.route('/login', methods=['POST'])
@login_bp.route("/login", methods=["POST"])
def login():
if request.method == 'POST':
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}'}), 400
"""Handle the login process."""
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}"}),
400,
)
if verify_user(data['username'], data['password']):
session['username'] = data['username'].lower() # Stored securely in a signed cookie
return jsonify({'message': 'Successfully logged in!'})
else:
return jsonify({'error': 'Invalid username or password!'}), 401
if verify_user(data["username"], data["password"]):
session["username"] = data[
"username"
].lower() # Stored securely in a signed cookie
return jsonify({"message": "Successfully logged in!"})
return jsonify({"error": "Invalid username or password!"}), 401
@login_bp.route('/login/status', methods=['POST'])
@login_bp.route("/login/status", methods=["POST"])
def login_status():
try:
username = session.get('username')
if username:
return jsonify({'message': 'True'})
else:
return jsonify({'message': 'False'})
except:
return jsonify({'message': 'False'})
"""Check if the user is logged in."""
username = session.get("username")
return jsonify({"message": "True" if username else "False"})
@login_bp.route('/logout', methods=['POST'])
@login_bp.route("/logout", methods=["POST"])
def logout():
session.pop('username', None)
return jsonify({'message': 'Successfully logged out!'})
"""Logout the user."""
session.pop("username", None)
return jsonify({"message": "Successfully logged out!"})

View File

@ -1,33 +1,48 @@
import sys
"""Module to handle the React routes."""
import os
from flask import Blueprint, send_from_directory, request, render_template
import sys
from flask import Blueprint, send_from_directory, render_template
from configs import index_tags
if getattr(sys, 'frozen', False): # Running as a bundled app
base_path = sys._MEIPASS
if getattr(sys, "frozen", False): # Running as a bundled app
base_path = getattr(sys, "_MEIPASS", os.path.abspath("."))
else: # Running in a normal Python environment
base_path = os.path.abspath(".")
static_folder = os.path.join(base_path, 'cdrm-frontend', 'dist')
static_folder = os.path.join(base_path, "frontend-dist")
react_bp = Blueprint(
'react_bp',
"react_bp",
__name__,
static_folder=static_folder,
static_url_path='/',
template_folder=static_folder
static_url_path="/",
template_folder=static_folder,
)
@react_bp.route('/', methods=['GET'])
@react_bp.route('/<path:path>', methods=["GET"])
@react_bp.route('/<path>', methods=["GET"])
def index(path=''):
if request.method == 'GET':
file_path = os.path.join(react_bp.static_folder, path)
if path != "" and os.path.exists(file_path):
return send_from_directory(react_bp.static_folder, path)
elif path.lower() in ['', 'cache', 'api', 'testplayer', 'account']:
data = index_tags.tags.get(path.lower(), index_tags.tags['index'])
return render_template('index.html', data=data)
else:
return send_from_directory(react_bp.static_folder, 'index.html')
@react_bp.route("/", methods=["GET"])
@react_bp.route("/<path:path>", methods=["GET"])
@react_bp.route("/<path>", methods=["GET"])
def index(path=""):
"""Handle the index route."""
# Ensure static_folder is not None
if react_bp.static_folder is None:
raise ValueError("Static folder is not configured for the blueprint")
# Normalize the path to prevent directory traversal
safe_path = os.path.normpath(path)
file_path = os.path.join(react_bp.static_folder, safe_path)
if path and os.path.exists(file_path):
return send_from_directory(react_bp.static_folder, safe_path)
# Only allow certain paths to render index.html with tags
allowed_paths = ["", "cache", "api", "testplayer", "account"]
if safe_path.lower() in allowed_paths:
data = index_tags.tags.get(safe_path.lower(), index_tags.tags.get("index", {}))
return render_template("index.html", data=data)
# Fallback: serve index.html for all other routes (SPA)
return send_from_directory(react_bp.static_folder, "index.html")

View File

@ -1,42 +1,56 @@
"""Module to handle the register process."""
import re
import uuid
from flask import Blueprint, request, jsonify
from custom_functions.database.user_db import add_user
import uuid
register_bp = Blueprint('register_bp', __name__)
register_bp = Blueprint("register_bp", __name__)
USERNAME_REGEX = re.compile(r'^[A-Za-z0-9_-]+$')
PASSWORD_REGEX = re.compile(r'^\S+$')
USERNAME_REGEX = re.compile(r"^[A-Za-z0-9_-]+$")
PASSWORD_REGEX = re.compile(r"^\S+$")
@register_bp.route('/register', methods=['POST'])
@register_bp.route("/register", methods=["POST"])
def register():
if request.method != 'POST':
return jsonify({'error': 'Method not supported'}), 405
"""Handle the register process."""
data = request.get_json()
if data is None:
return jsonify({"error": "Invalid or missing JSON in request body."}), 400
# Check required fields
for required_field in ['username', 'password']:
for required_field in ["username", "password"]:
if required_field not in data:
return jsonify({'error': f'Missing required field: {required_field}'}), 400
return jsonify({"error": f"Missing required field: {required_field}"}), 400
username = data['username']
password = data['password']
username = data["username"].lower()
password = data["password"]
api_key = str(uuid.uuid4())
# Length checks
if not (3 <= len(username) <= 32):
return jsonify({"error": "Username must be 3-32 characters."}), 400
if not (8 <= len(password) <= 128):
return jsonify({"error": "Password must be 8-128 characters."}), 400
# Validate username and password
if not USERNAME_REGEX.fullmatch(username):
return jsonify({
'error': 'Invalid username. Only letters, numbers, hyphens, and underscores are allowed.'
}), 400
return (
jsonify(
{
"error": "Invalid username. Only letters, numbers, hyphens, and underscores are allowed."
}
),
400,
)
if not PASSWORD_REGEX.fullmatch(password):
return jsonify({
'error': 'Invalid password. Spaces are not allowed.'
}), 400
return jsonify({"error": "Invalid password. Spaces are not allowed."}), 400
# Attempt to add user
if add_user(username, password, api_key):
return jsonify({'message': 'User successfully registered!'}), 201
else:
return jsonify({'error': 'User already exists!'}), 409
return (
jsonify({"message": "User successfully registered!", "api_key": api_key}),
201,
)
return jsonify({"error": "User already exists!"}), 409

View File

@ -1,142 +1,236 @@
import base64
"""Module to handle the remote device PlayReady."""
from flask import Blueprint, jsonify, request, current_app, Response
import base64
import os
from pathlib import Path
import re
import yaml
from flask import Blueprint, jsonify, request, current_app, Response
from pyplayready.device import Device as PlayReadyDevice
from pyplayready.cdm import Cdm as PlayReadyCDM
from pyplayready import PSSH as PlayReadyPSSH
from pyplayready.exceptions import (InvalidSession, TooManySessions, InvalidLicense, InvalidPssh)
from pyplayready.exceptions import (
InvalidSession,
InvalidLicense,
InvalidPssh,
)
from custom_functions.database.user_db import fetch_username_by_api_key
from custom_functions.decrypt.api_decrypt import is_base64
from custom_functions.user_checks.device_allowed import user_allowed_to_use_device
from pathlib import Path
remotecdm_pr_bp = Blueprint('remotecdm_pr', __name__)
with open(f'{os.getcwd()}/configs/config.yaml', 'r') as file:
remotecdm_pr_bp = Blueprint("remotecdm_pr", __name__)
with open(
os.path.join(os.getcwd(), "configs", "config.yaml"), "r", encoding="utf-8"
) as file:
config = yaml.safe_load(file)
@remotecdm_pr_bp.route('/remotecdm/playready', methods=['GET', 'HEAD'])
def make_response(status, message, data=None, http_status=200):
"""Make a response."""
resp = {"status": status, "message": message}
if data is not None:
resp["data"] = data
return jsonify(resp), http_status
def check_required_fields(body, required_fields):
"""Return a response tuple if a required field is missing, else None."""
for field in required_fields:
if not body.get(field):
return make_response(
"Error",
f'Missing required field "{field}" in JSON body',
http_status=400,
)
return None
@remotecdm_pr_bp.route("/remotecdm/playready", methods=["GET", "HEAD"])
def remote_cdm_playready():
if request.method == 'GET':
return jsonify({
'message': 'OK'
})
if request.method == 'HEAD':
"""Handle the remote device PlayReady."""
if request.method == "GET":
return make_response(
"Success",
"OK",
http_status=200,
)
if request.method == "HEAD":
response = Response(status=200)
response.headers['Server'] = 'playready serve'
response.headers["Server"] = "playready serve"
return response
return make_response("Failed", "Method not allowed", http_status=405)
@remotecdm_pr_bp.route('/remotecdm/playready/deviceinfo', methods=['GET'])
@remotecdm_pr_bp.route("/remotecdm/playready/deviceinfo", methods=["GET"])
def remote_cdm_playready_deviceinfo():
"""Handle the remote device PlayReady device info."""
base_name = config["default_pr_cdm"]
if not base_name.endswith(".prd"):
full_file_name = (base_name + ".prd")
device = PlayReadyDevice.load(f'{os.getcwd()}/configs/CDMs/PR/{full_file_name}')
device = PlayReadyDevice.load(
os.path.join(os.getcwd(), "configs", "CDMs", "PR", base_name + ".prd")
)
cdm = PlayReadyCDM.from_device(device)
return jsonify({
'security_level': cdm.security_level,
'host': f'{config["fqdn"]}/remotecdm/playready',
'secret': f'{config["remote_cdm_secret"]}',
'device_name': Path(base_name).stem
})
return jsonify(
{
"security_level": cdm.security_level,
"host": f'{config["fqdn"]}/remotecdm/playready',
"secret": f'{config["remote_cdm_secret"]}',
"device_name": Path(base_name).stem,
}
)
@remotecdm_pr_bp.route('/remotecdm/playready/deviceinfo/<device>', methods=['GET'])
def sanitize_username(username):
"""Sanitize the username."""
return re.sub(r"[^a-zA-Z0-9_\-]", "_", username).lower()
@remotecdm_pr_bp.route("/remotecdm/playready/deviceinfo/<device>", methods=["GET"])
def remote_cdm_playready_deviceinfo_specific(device):
if request.method == 'GET':
base_name = Path(device).with_suffix('.prd').name
api_key = request.headers['X-Secret-Key']
username = fetch_username_by_api_key(api_key)
device = PlayReadyDevice.load(f'{os.getcwd()}/configs/CDMs/{username}/PR/{base_name}')
cdm = PlayReadyCDM.from_device(device)
return jsonify({
'security_level': cdm.security_level,
'host': f'{config["fqdn"]}/remotecdm/widevine',
'secret': f'{api_key}',
'device_name': Path(base_name).stem
})
"""Handle the remote device PlayReady device info specific."""
base_name = Path(device).with_suffix(".prd").name
api_key = request.headers["X-Secret-Key"]
username = fetch_username_by_api_key(api_key)
if not username:
return jsonify({"message": "Invalid or missing API key."}), 403
safe_username = sanitize_username(username)
device = PlayReadyDevice.load(
os.path.join(
os.getcwd(),
"configs",
"CDMs",
"users_uploaded",
safe_username,
"PR",
base_name,
)
)
cdm = PlayReadyCDM.from_device(device)
return jsonify(
{
"security_level": cdm.security_level,
"host": f'{config["fqdn"]}/remotecdm/playready',
"secret": f"{api_key}",
"device_name": Path(base_name).stem,
}
)
@remotecdm_pr_bp.route('/remotecdm/playready/<device>/open', methods=['GET'])
@remotecdm_pr_bp.route("/remotecdm/playready/<device>/open", methods=["GET"])
def remote_cdm_playready_open(device):
if str(device).lower() == config['default_pr_cdm'].lower():
pr_device = PlayReadyDevice.load(f'{os.getcwd()}/configs/CDMs/PR/{config["default_pr_cdm"]}.prd')
cdm = current_app.config['CDM'] = PlayReadyCDM.from_device(pr_device)
session_id = cdm.open()
return jsonify({
'message': 'Success',
'data': {
'session_id': session_id.hex(),
'device': {
'security_level': cdm.security_level
}
}
})
if request.headers['X-Secret-Key'] and str(device).lower() != config['default_pr_cdm'].lower():
api_key = request.headers['X-Secret-Key']
user = fetch_username_by_api_key(api_key=api_key)
if user:
if user_allowed_to_use_device(device=device, username=user):
pr_device = PlayReadyDevice.load(f'{os.getcwd()}/configs/CDMs/{user}/PR/{device}.prd')
cdm = current_app.config['CDM'] = PlayReadyCDM.from_device(pr_device)
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
"""Handle the remote device PlayReady open."""
unauthorized_msg = {
"message": f"Device '{device}' is not found or you are not authorized to use it."
}
@remotecdm_pr_bp.route('/remotecdm/playready/<device>/close/<session_id>', methods=['GET'])
# Default device logic
if str(device).lower() == config["default_pr_cdm"].lower():
pr_device = PlayReadyDevice.load(
os.path.join(
os.getcwd(), "configs", "CDMs", "PR", config["default_pr_cdm"] + ".prd"
)
)
cdm = current_app.config["CDM"] = PlayReadyCDM.from_device(pr_device)
session_id = cdm.open()
return make_response(
"Success",
"Successfully opened the PlayReady CDM session",
{
"session_id": session_id.hex(),
"device": {"security_level": cdm.security_level},
},
http_status=200,
)
# User device logic
api_key = request.headers.get("X-Secret-Key")
if api_key and str(device).lower() != config["default_pr_cdm"].lower():
user = fetch_username_by_api_key(api_key=api_key)
safe_username = sanitize_username(user)
if user and user_allowed_to_use_device(device=device, username=user):
pr_device = PlayReadyDevice.load(
os.path.join(
os.getcwd(),
"configs",
"CDMs",
"users_uploaded",
safe_username,
"PR",
device + ".prd",
)
)
cdm = current_app.config["CDM"] = PlayReadyCDM.from_device(pr_device)
session_id = cdm.open()
return make_response(
"Success",
"Successfully opened the PlayReady CDM session",
{
"session_id": session_id.hex(),
"device": {"security_level": cdm.security_level},
},
http_status=200,
)
return make_response("Failed", unauthorized_msg, http_status=403)
return make_response("Failed", unauthorized_msg, http_status=403)
def get_cdm_or_error(device):
"""Get the CDM or return an error response."""
cdm = current_app.config.get("CDM")
if not cdm:
return make_response(
"Error",
f'No CDM session for "{device}" has been opened yet. No session to use',
http_status=400,
)
return cdm
@remotecdm_pr_bp.route(
"/remotecdm/playready/<device>/close/<session_id>", methods=["GET"]
)
def remote_cdm_playready_close(device, session_id):
"""Handle the remote device PlayReady close."""
try:
session_id = bytes.fromhex(session_id)
cdm = current_app.config["CDM"]
if not cdm:
return jsonify({
'message': f'No CDM for "{device}" has been opened yet. No session to close'
}), 400
cdm = get_cdm_or_error(device)
if isinstance(cdm, tuple): # error response
return cdm
try:
cdm.close(session_id)
except InvalidSession:
return jsonify({
'message': f'Invalid session ID "{session_id.hex()}", it may have expired'
}), 400
return jsonify({
'message': f'Successfully closed Session "{session_id.hex()}".',
}), 200
except Exception as e:
return jsonify({
'message': f'Failed to close Session "{session_id.hex()}".'
}), 400
return make_response(
"Error",
f'Invalid session ID "{session_id.hex()}", it may have expired',
http_status=400,
)
return make_response(
"Success",
f'Successfully closed Session "{session_id.hex()}".',
http_status=200,
)
except Exception as error:
return make_response(
"Error",
f'Failed to close Session "{session_id.hex()}", {error}.',
http_status=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):
"""Handle the remote device PlayReady get license challenge."""
body = request.get_json()
for required_field in ("session_id", "init_data"):
if not body.get(required_field):
return jsonify({
'message': f'Missing required field "{required_field}" in JSON body'
}), 400
cdm = current_app.config["CDM"]
missing_field = check_required_fields(body, ("session_id", "init_data"))
if missing_field:
return missing_field
cdm = get_cdm_or_error(device)
if isinstance(cdm, tuple): # error response
return cdm
session_id = bytes.fromhex(body["session_id"])
init_data = body["init_data"]
if not init_data.startswith("<WRMHEADER"):
@ -144,43 +238,46 @@ def remote_cdm_playready_get_license_challenge(device):
pssh = PlayReadyPSSH(init_data)
if pssh.wrm_headers:
init_data = pssh.wrm_headers[0]
except InvalidPssh as e:
return jsonify({
'message': f'Unable to parse base64 PSSH, {e}'
})
except InvalidPssh as error:
return make_response(
"Error",
f"Unable to parse base64 PSSH, {error}",
http_status=400,
)
try:
license_request = cdm.get_license_challenge(
session_id=session_id,
wrm_header=init_data
session_id=session_id, wrm_header=init_data
)
except InvalidSession:
return jsonify({
'message': f"Invalid Session ID '{session_id.hex()}', it may have expired."
})
except Exception as e:
return jsonify({
'message': f'Error, {e}'
})
return jsonify({
'message': 'success',
'data': {
'challenge': license_request
}
})
return make_response(
"Error",
f"Invalid Session ID '{session_id.hex()}', it may have expired.",
http_status=400,
)
except ValueError as error:
return make_response(
"Error",
f"Invalid License, {error}",
http_status=400,
)
return make_response(
"Success",
"Successfully got the License Challenge",
{"challenge_b64": base64.b64encode(license_request).decode()},
http_status=200,
)
@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):
"""Handle the remote device PlayReady parse license."""
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:
return jsonify({
'message': f"No Cdm session for {device} has been opened yet. No session to use."
})
missing_field = check_required_fields(body, ("license_message", "session_id"))
if missing_field:
return missing_field
cdm = get_cdm_or_error(device)
if isinstance(cdm, tuple): # error response
return cdm
session_id = bytes.fromhex(body["session_id"])
license_message = body["license_message"]
if is_base64(license_message):
@ -188,45 +285,56 @@ def remote_cdm_playready_parse_license(device):
try:
cdm.parse_license(session_id, license_message)
except InvalidSession:
return jsonify({
'message': f"Invalid Session ID '{session_id.hex()}', it may have expired."
})
return make_response(
"Error",
f"Invalid Session ID '{session_id.hex()}', it may have expired.",
http_status=400,
)
except InvalidLicense as e:
return jsonify({
'message': f"Invalid License, {e}"
})
return make_response(
"Error",
f"Invalid License, {e}",
http_status=400,
)
except Exception as e:
return jsonify({
'message': f"Error, {e}"
})
return jsonify({
'message': 'Successfully parsed and loaded the Keys from the License message'
})
return make_response(
"Error",
f"Error, {e}",
http_status=400,
)
return make_response(
"Success",
"Successfully parsed and loaded the Keys from the License message",
http_status=200,
)
@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):
"""Handle the remote device PlayReady get keys."""
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'
})
missing_field = check_required_fields(body, ("session_id",))
if missing_field:
return missing_field
session_id = bytes.fromhex(body["session_id"])
cdm = current_app.config["CDM"]
if not cdm:
return jsonify({
'message': f"Missing required field '{required_field}' in JSON body."
})
key_type = body.get("key_type", None)
cdm = get_cdm_or_error(device)
if isinstance(cdm, tuple): # error response
return cdm
try:
keys = cdm.get_keys(session_id)
keys = cdm.get_keys(session_id, key_type)
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}"
})
return make_response(
"Error",
f"Invalid Session ID '{session_id.hex()}', it may have expired.",
http_status=400,
)
except ValueError as error:
return make_response(
"Error",
f"The Key Type value '{key_type}' is invalid, {error}",
http_status=400,
)
keys_json = [
{
"key_id": key.key_id.hex,
@ -236,10 +344,11 @@ def remote_cdm_playready_get_keys(device):
"key_length": key.key_length,
}
for key in keys
if not key_type or key.type == key_type
]
return jsonify({
'message': 'success',
'data': {
'keys': keys_json
}
})
return make_response(
"Success",
"Successfully got the Keys",
{"keys": keys_json},
http_status=200,
)

View File

@ -1,387 +1,474 @@
"""Module to handle the remote device Widevine."""
import os
from flask import Blueprint, jsonify, request, current_app, Response
import base64
from typing import Any, Optional, Union
import re
from pathlib import Path
import yaml
from flask import Blueprint, jsonify, request, current_app, Response
from google.protobuf.message import DecodeError
from pywidevine.pssh import PSSH as widevinePSSH
from pywidevine import __version__
from pywidevine.cdm import Cdm as widevineCDM
from pywidevine.device import Device as widevineDevice
from pywidevine.exceptions import (InvalidContext, InvalidInitData, InvalidLicenseMessage, InvalidLicenseType,
InvalidSession, SignatureMismatch, TooManySessions)
from pywidevine.exceptions import (
InvalidContext,
InvalidInitData,
InvalidLicenseMessage,
InvalidLicenseType,
InvalidSession,
SignatureMismatch,
)
import yaml
from custom_functions.database.user_db import fetch_api_key, fetch_username_by_api_key
from custom_functions.database.user_db import fetch_username_by_api_key
from custom_functions.database.unified_db_ops import cache_to_db
from custom_functions.user_checks.device_allowed import user_allowed_to_use_device
from pathlib import Path
remotecdm_wv_bp = Blueprint('remotecdm_wv', __name__)
with open(f'{os.getcwd()}/configs/config.yaml', 'r') as file:
remotecdm_wv_bp = Blueprint("remotecdm_wv", __name__)
with open(
os.path.join(os.getcwd(), "configs", "config.yaml"), "r", encoding="utf-8"
) as file:
config = yaml.safe_load(file)
@remotecdm_wv_bp.route('/remotecdm/widevine', methods=['GET', 'HEAD'])
def make_response(status, message, data=None, http_status=200):
"""Make a response."""
resp = {"status": status, "message": message}
if data is not None:
resp["data"] = data
return jsonify(resp), http_status
def check_required_fields(body, required_fields):
"""Return a response if a required field is missing, else None."""
for field in required_fields:
if not body.get(field):
return make_response(
"Error",
f'Missing required field "{field}" in JSON body',
http_status=400,
)
return None
@remotecdm_wv_bp.route("/remotecdm/widevine", methods=["GET", "HEAD"])
def remote_cdm_widevine():
if request.method == 'GET':
return jsonify({
'status': 200,
'message': f"{config['fqdn'].upper()} Remote Widevine CDM."
})
if request.method == 'HEAD':
"""Handle the remote device Widevine."""
if request.method == "GET":
return make_response(
"Success",
f"{config['fqdn'].upper()} Remote Widevine CDM.",
http_status=200,
)
if request.method == "HEAD":
response = Response(status=200)
response.headers['Server'] = f'https://github.com/devine-dl/pywidevine serve v{__version__}'
response.headers["Server"] = (
f"https://github.com/devine-dl/pywidevine serve v{__version__}"
)
return response
return make_response(
"Error",
"Invalid request method",
http_status=405,
)
@remotecdm_wv_bp.route('/remotecdm/widevine/deviceinfo', methods=['GET'])
@remotecdm_wv_bp.route("/remotecdm/widevine/deviceinfo", methods=["GET"])
def remote_cdm_widevine_deviceinfo():
if request.method == 'GET':
base_name = config["default_wv_cdm"]
if not base_name.endswith(".wvd"):
base_name = (base_name + ".wvd")
device = widevineDevice.load(f'{os.getcwd()}/configs/CDMs/WV/{base_name}')
cdm = widevineCDM.from_device(device)
return jsonify({
'device_type': cdm.device_type.name,
'system_id': cdm.system_id,
'security_level': cdm.security_level,
'host': f'{config["fqdn"]}/remotecdm/widevine',
'secret': f'{config["remote_cdm_secret"]}',
'device_name': Path(base_name).stem
})
"""Handle the remote device Widevine device info."""
base_name = config["default_wv_cdm"]
if not base_name.endswith(".wvd"):
base_name = base_name + ".wvd"
device = widevineDevice.load(
os.path.join(os.getcwd(), "configs", "CDMs", "WV", base_name)
)
cdm = widevineCDM.from_device(device)
return make_response(
"Success",
"Successfully got the Widevine CDM device info",
{
"device_type": cdm.device_type.name,
"system_id": cdm.system_id,
"security_level": cdm.security_level,
"host": f'{config["fqdn"]}/remotecdm/widevine',
"secret": f'{config["remote_cdm_secret"]}',
"device_name": Path(base_name).stem,
},
http_status=200,
)
@remotecdm_wv_bp.route('/remotecdm/widevine/deviceinfo/<device>', methods=['GET'])
def sanitize_username(username):
"""Sanitize the username."""
return re.sub(r"[^a-zA-Z0-9_\-]", "_", username).lower()
@remotecdm_wv_bp.route("/remotecdm/widevine/deviceinfo/<device>", methods=["GET"])
def remote_cdm_widevine_deviceinfo_specific(device):
if request.method == 'GET':
base_name = Path(device).with_suffix('.wvd').name
api_key = request.headers['X-Secret-Key']
username = fetch_username_by_api_key(api_key)
device = widevineDevice.load(f'{os.getcwd()}/configs/CDMs/{username}/WV/{base_name}')
cdm = widevineCDM.from_device(device)
return jsonify({
'device_type': cdm.device_type.name,
'system_id': cdm.system_id,
'security_level': cdm.security_level,
'host': f'{config["fqdn"]}/remotecdm/widevine',
'secret': f'{api_key}',
'device_name': Path(base_name).stem
})
"""Handle the remote device Widevine device info specific."""
base_name = Path(device).with_suffix(".wvd").name
api_key = request.headers["X-Secret-Key"]
username = fetch_username_by_api_key(api_key)
safe_username = sanitize_username(username)
device = widevineDevice.load(
os.path.join(
os.getcwd(),
"configs",
"CDMs",
"users_uploaded",
safe_username,
"WV",
base_name,
)
)
cdm = widevineCDM.from_device(device)
return make_response(
"Success",
"Successfully got the Widevine CDM device info (by user)",
{
"device_type": cdm.device_type.name,
"system_id": cdm.system_id,
"security_level": cdm.security_level,
"host": f'{config["fqdn"]}/remotecdm/widevine',
"secret": f"{api_key}",
"device_name": Path(base_name).stem,
},
http_status=200,
)
@remotecdm_wv_bp.route('/remotecdm/widevine/<device>/open', methods=['GET'])
def load_widevine_device(device_name, api_key=None):
"""Load a Widevine device, either default or user-uploaded."""
try:
if device_name.lower() == config["default_wv_cdm"].lower():
path = os.path.join(
os.getcwd(), "configs", "CDMs", "WV", config["default_wv_cdm"] + ".wvd"
)
else:
if not api_key:
return None
username = fetch_username_by_api_key(api_key)
if not username or not user_allowed_to_use_device(
device=device_name, username=username
):
return None
safe_username = sanitize_username(username)
path = os.path.join(
os.getcwd(),
"configs",
"CDMs",
"users_uploaded",
safe_username,
"WV",
device_name + ".wvd",
)
return widevineDevice.load(path)
except (FileNotFoundError, ValueError):
return None
def get_cdm_or_error(device):
"""Get the CDM or return an error response."""
cdm = current_app.config.get("CDM")
if not cdm:
return make_response(
"Error",
f'No CDM session for "{device}" has been opened yet. No session to use',
http_status=400,
)
return cdm
@remotecdm_wv_bp.route("/remotecdm/widevine/<device>/open", methods=["GET"])
def remote_cdm_widevine_open(device):
if str(device).lower() == config['default_wv_cdm'].lower():
wv_device = widevineDevice.load(f'{os.getcwd()}/configs/CDMs/WV/{config["default_wv_cdm"]}.wvd')
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
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
"""Handle the remote device Widevine open."""
api_key = request.headers.get("X-Secret-Key")
wv_device = load_widevine_device(device, api_key)
if not wv_device:
return make_response(
"Error",
f"Device '{device}' is not found or you are not authorized to use it.",
http_status=403,
)
cdm = current_app.config["CDM"] = widevineCDM.from_device(wv_device)
session_id = cdm.open()
return make_response(
"Success",
"Successfully opened the Widevine CDM session",
{
"session_id": session_id.hex(),
"device": {
"system_id": cdm.system_id,
"security_level": cdm.security_level,
},
},
http_status=200,
)
@remotecdm_wv_bp.route('/remotecdm/widevine/<device>/close/<session_id>', methods=['GET'])
@remotecdm_wv_bp.route(
"/remotecdm/widevine/<device>/close/<session_id>", methods=["GET"]
)
def remote_cdm_widevine_close(device, session_id):
session_id = bytes.fromhex(session_id)
cdm = current_app.config["CDM"]
if not cdm:
return jsonify({
'status': 400,
'message': f'No CDM for "{device}" has been opened yet. No session to close'
}), 400
try:
cdm.close(session_id)
except InvalidSession:
return jsonify({
'status': 400,
'message': f'Invalid session ID "{session_id.hex()}", it may have expired'
}), 400
return jsonify({
'status': 200,
'message': f'Successfully closed Session "{session_id.hex()}".',
}), 200
"""Handle the remote device Widevine close."""
session_id = bytes.fromhex(session_id)
cdm = get_cdm_or_error(device)
if isinstance(cdm, tuple): # error response
return cdm
try:
cdm.close(session_id)
except InvalidSession:
return make_response(
"Error",
f'Invalid session ID "{session_id.hex()}", it may have expired',
http_status=400,
)
@remotecdm_wv_bp.route('/remotecdm/widevine/<device>/set_service_certificate', methods=['POST'])
return make_response(
"Success",
f'Successfully closed Session "{session_id.hex()}".',
http_status=200,
)
@remotecdm_wv_bp.route(
"/remotecdm/widevine/<device>/set_service_certificate", methods=["POST"]
)
def remote_cdm_widevine_set_service_certificate(device):
"""Handle the remote device Widevine set service certificate."""
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
missing_field = check_required_fields(body, ("session_id", "certificate"))
if missing_field:
return missing_field
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
cdm = get_cdm_or_error(device)
if isinstance(cdm, tuple): # error response
return cdm
certificate = body["certificate"]
try:
provider_id = cdm.set_service_certificate(session_id, certificate)
except InvalidSession:
return jsonify({
'status': 400,
'message': f'Invalid session id: "{session_id.hex()}", it may have expired'
}), 400
return make_response(
"Error",
f'Invalid session id: "{session_id.hex()}", it may have expired',
http_status=400,
)
except DecodeError as error:
return jsonify({
'status': 400,
'message': f'Invalid Service Certificate, {error}'
}), 400
return make_response(
"Error",
f"Invalid Service Certificate, {error}",
http_status=400,
)
except SignatureMismatch:
return jsonify({
'status': 400,
'message': 'Signature Validation failed on the Service Certificate, rejecting'
}), 400
return jsonify({
'status': 200,
'message': f"Successfully {['set', 'unset'][not certificate]} the Service Certificate.",
'data': {
'provider_id': provider_id,
}
}), 200
return make_response(
"Error",
"Signature Validation failed on the Service Certificate, rejecting",
http_status=400,
)
return make_response(
"Success",
f"Successfully {['set', 'unset'][not certificate]} the Service Certificate.",
{"provider_id": provider_id},
http_status=200,
)
@remotecdm_wv_bp.route('/remotecdm/widevine/<device>/get_service_certificate', methods=['POST'])
@remotecdm_wv_bp.route(
"/remotecdm/widevine/<device>/get_service_certificate", methods=["POST"]
)
def remote_cdm_widevine_get_service_certificate(device):
"""Handle the remote device Widevine get service certificate."""
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
missing_field = check_required_fields(body, ("session_id",))
if missing_field:
return missing_field
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
cdm = get_cdm_or_error(device)
if isinstance(cdm, tuple): # error response
return cdm
try:
service_certificate = cdm.get_service_certificate(session_id)
except InvalidSession:
return jsonify({
'status': 400,
'message': f'Invalid Session ID "{session_id.hex()}", it may have expired'
}), 400
return make_response(
"Error",
f'Invalid Session ID "{session_id.hex()}", it may have expired',
http_status=400,
)
if service_certificate:
service_certificate_b64 = base64.b64encode(service_certificate.SerializeToString()).decode()
service_certificate_b64 = base64.b64encode(
service_certificate.SerializeToString()
).decode()
else:
service_certificate_b64 = None
return jsonify({
'status': 200,
'message': 'Successfully got the Service Certificate',
'data': {
'service_certificate': service_certificate_b64,
}
}), 200
return make_response(
"Success",
"Successfully got the Service Certificate",
{"service_certificate": service_certificate_b64},
http_status=200,
)
@remotecdm_wv_bp.route('/remotecdm/widevine/<device>/get_license_challenge/<license_type>', methods=['POST'])
@remotecdm_wv_bp.route(
"/remotecdm/widevine/<device>/get_license_challenge/<license_type>",
methods=["POST"],
)
def remote_cdm_widevine_get_license_challenge(device, license_type):
"""Handle the remote device Widevine get license challenge."""
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
missing_field = check_required_fields(body, ("session_id", "init_data"))
if missing_field:
return missing_field
session_id = bytes.fromhex(body["session_id"])
privacy_mode = body.get("privacy_mode", True)
cdm = current_app.config["CDM"]
if not cdm:
return jsonify({
'status': 400,
'message': f'No CDM session for "{device}" has been opened yet. No session to use'
}), 400
cdm = get_cdm_or_error(device)
if isinstance(cdm, tuple): # error response
return cdm
if current_app.config.get("force_privacy_mode"):
privacy_mode = True
if not cdm.get_service_certificate(session_id):
return jsonify({
'status': 403,
'message': 'No Service Certificate set but Privacy Mode is Enforced.'
}), 403
return make_response(
"Error",
"No Service Certificate set but Privacy Mode is Enforced.",
http_status=403,
)
current_app.config['pssh'] = body['init_data']
init_data = widevinePSSH(body['init_data'])
current_app.config["pssh"] = body["init_data"]
init_data = widevinePSSH(body["init_data"])
try:
license_request = cdm.get_license_challenge(
session_id=session_id,
pssh=init_data,
license_type=license_type,
privacy_mode=privacy_mode
privacy_mode=privacy_mode,
)
except InvalidSession:
return jsonify({
'status': 400,
'message': f'Invalid Session ID "{session_id.hex()}", it may have expired'
}), 400
return make_response(
"Error",
f'Invalid Session ID "{session_id.hex()}", it may have expired',
http_status=400,
)
except InvalidInitData as error:
return jsonify({
'status': 400,
'message': f'Invalid Init Data, {error}'
}), 400
return make_response(
"Error",
f"Invalid Init Data, {error}",
http_status=400,
)
except InvalidLicenseType:
return jsonify({
'status': 400,
'message': f'Invalid License Type {license_type}'
}), 400
return jsonify({
'status': 200,
'message': 'Success',
'data': {
'challenge_b64': base64.b64encode(license_request).decode()
}
}), 200
return make_response(
"Error",
f"Invalid License Type {license_type}",
http_status=400,
)
return make_response(
"Success",
"Successfully got the License Challenge",
{"challenge_b64": base64.b64encode(license_request).decode()},
http_status=200,
)
@remotecdm_wv_bp.route('/remotecdm/widevine/<device>/parse_license', methods=['POST'])
@remotecdm_wv_bp.route("/remotecdm/widevine/<device>/parse_license", methods=["POST"])
def remote_cdm_widevine_parse_license(device):
"""Handle the remote device Widevine parse license."""
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
missing_field = check_required_fields(body, ("session_id", "license_message"))
if missing_field:
return missing_field
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
cdm = get_cdm_or_error(device)
if isinstance(cdm, tuple): # error response
return cdm
try:
cdm.parse_license(session_id, body['license_message'])
cdm.parse_license(session_id, body["license_message"])
except InvalidLicenseMessage as error:
return jsonify({
'status': 400,
'message': f'Invalid License Message, {error}'
}), 400
return make_response(
"Error",
f"Invalid License Message, {error}",
http_status=400,
)
except InvalidContext as error:
return jsonify({
'status': 400,
'message': f'Invalid Context, {error}'
}), 400
return make_response(
"Error",
f"Invalid Context, {error}",
http_status=400,
)
except InvalidSession:
return jsonify({
'status': 400,
'message': f'Invalid Session ID "{session_id.hex()}", it may have expired'
}), 400
return make_response(
"Error",
f'Invalid Session ID "{session_id.hex()}", it may have expired',
http_status=400,
)
except SignatureMismatch:
return jsonify({
'status': 400,
'message': f'Signature Validation failed on the License Message, rejecting.'
}), 400
return jsonify({
'status': 200,
'message': 'Successfully parsed and loaded the Keys from the License message.',
}), 200
return make_response(
"Error",
"Signature Validation failed on the License Message, rejecting.",
http_status=400,
)
return make_response(
"Success",
"Successfully parsed and loaded the Keys from the License message.",
http_status=200,
)
@remotecdm_wv_bp.route('/remotecdm/widevine/<device>/get_keys/<key_type>', methods=['POST'])
@remotecdm_wv_bp.route(
"/remotecdm/widevine/<device>/get_keys/<key_type>", methods=["POST"]
)
def remote_cdm_widevine_get_keys(device, key_type):
"""Handle the remote device Widevine get keys."""
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
missing_field = check_required_fields(body, ("session_id",))
if missing_field:
return missing_field
session_id = bytes.fromhex(body["session_id"])
key_type: Optional[str] = key_type
if key_type == 'ALL':
if key_type == "ALL":
key_type = None
cdm = current_app.config["CDM"]
if not cdm:
return jsonify({
'status': 400,
'message': f'No CDM session for "{device}" has been opened yet. No session to use'
}), 400
cdm = get_cdm_or_error(device)
if isinstance(cdm, tuple): # error response
return cdm
try:
keys = cdm.get_keys(session_id, key_type)
except InvalidSession:
return jsonify({
'status': 400,
'message': f'Invalid Session ID "{session_id.hex()}", it may have expired'
}), 400
return make_response(
"Error",
f'Invalid Session ID "{session_id.hex()}", it may have expired',
http_status=400,
)
except ValueError as error:
return jsonify({
'status': 400,
'message': f'The Key Type value "{key_type}" is invalid, {error}'
}), 400
return make_response(
"Error",
f'The Key Type value "{key_type}" is invalid, {error}',
http_status=400,
)
keys_json = [
{
"key_id": key.kid.hex,
"key": key.key.hex(),
"type": key.type,
"permissions": key.permissions
"permissions": key.permissions,
}
for key in keys
if not key_type or key.type == key_type
]
for entry in keys_json:
if config['database_type'].lower() != 'mariadb':
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'])
if entry["type"] != "SIGNING":
cache_to_db(
pssh=str(current_app.config["pssh"]),
kid=entry["key_id"],
key=entry["key"],
)
return jsonify({
'status': 200,
'message': 'Success',
'data': {
'keys': keys_json
}
}), 200
return make_response(
"Success",
"Successfully got the Keys",
{"keys": keys_json},
http_status=200,
)

View File

@ -1,42 +1,57 @@
from flask import Blueprint, request, jsonify, session
"""Module to handle the upload process."""
import os
import logging
import re
from flask import Blueprint, request, jsonify, session
upload_bp = Blueprint('upload_bp', __name__)
upload_bp = Blueprint("upload_bp", __name__)
@upload_bp.route('/upload/<cdmtype>', methods=['POST'])
def sanitize_username(username):
"""Sanitize the username."""
return re.sub(r"[^a-zA-Z0-9_\-]", "_", username).lower()
@upload_bp.route("/upload/<cdmtype>", methods=["POST"])
def upload(cdmtype):
"""Handle the upload process."""
try:
username = session.get('username')
username = session.get("username")
if not username:
return jsonify({'message': 'False', 'error': 'No username in session'}), 400
return jsonify({"message": "False", "error": "No username in session"}), 400
safe_username = sanitize_username(username)
# Validate CDM type
if cdmtype not in ['PR', 'WV']:
return jsonify({'message': 'False', 'error': 'Invalid CDM type'}), 400
if cdmtype not in ["PR", "WV"]:
return jsonify({"message": "False", "error": "Invalid CDM type"}), 400
# Set up user directory paths
base_path = os.path.join(os.getcwd(), 'configs', 'CDMs', username)
pr_path = os.path.join(base_path, 'PR')
wv_path = os.path.join(base_path, 'WV')
base_path = os.path.join(
os.getcwd(), "configs", "CDMs", "users_uploaded", safe_username
)
pr_path = os.path.join(base_path, "PR")
wv_path = os.path.join(base_path, "WV")
# Create necessary directories if they don't exist
os.makedirs(pr_path, exist_ok=True)
os.makedirs(wv_path, exist_ok=True)
# Get uploaded file
uploaded_file = request.files.get('file')
uploaded_file = request.files.get("file")
if not uploaded_file:
return jsonify({'message': 'False', 'error': 'No file provided'}), 400
return jsonify({"message": "False", "error": "No file provided"}), 400
# Determine correct save path based on cdmtype
filename = uploaded_file.filename
save_path = os.path.join(pr_path if cdmtype == 'PR' else wv_path, filename)
assert filename is not None
target_path = pr_path if cdmtype == "PR" else wv_path
save_path = os.path.join(target_path, filename)
uploaded_file.save(save_path)
return jsonify({'message': 'Success', 'file_saved_to': save_path})
return jsonify({"message": "Success", "file_saved_to": save_path})
except Exception as e:
logging.exception("Upload failed")
return jsonify({'message': 'False', 'error': 'Server error'}), 500
except (OSError, IOError, ValueError, AttributeError) as e:
logging.exception("Upload failed: %s", {e})
return jsonify({"message": "False", "error": "Server error"}), 500

View File

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

View File

@ -1,34 +1,61 @@
from flask import Blueprint, request, jsonify, session
"""Module to handle the user info request."""
import os
import glob
import logging
from custom_functions.database.user_db import fetch_api_key, fetch_styled_username, fetch_username_by_api_key
import re
from flask import Blueprint, request, jsonify, session
from custom_functions.database.user_db import (
fetch_api_key,
fetch_styled_username,
fetch_username_by_api_key,
)
user_info_bp = Blueprint('user_info_bp', __name__)
user_info_bp = Blueprint("user_info_bp", __name__)
@user_info_bp.route('/userinfo', methods=['POST'])
def sanitize_username(username):
"""Sanitize the username."""
return re.sub(r"[^a-zA-Z0-9_\-]", "_", username).lower()
@user_info_bp.route("/userinfo", methods=["POST"])
def user_info():
username = session.get('username')
"""Handle the user info request."""
username = session.get("username")
if not username:
try:
headers = request.headers
api_key = headers['Api-Key']
api_key = headers["Api-Key"]
username = fetch_username_by_api_key(api_key)
except:
return jsonify({'message': 'False'}), 400
except Exception as e:
logging.exception("Error retrieving username by API key, %s", {e})
return jsonify({"message": "False"}), 400
safe_username = sanitize_username(username)
try:
base_path = os.path.join(os.getcwd(), 'configs', 'CDMs', username.lower())
pr_files = [os.path.basename(f) for f in glob.glob(os.path.join(base_path, 'PR', '*.prd'))]
wv_files = [os.path.basename(f) for f in glob.glob(os.path.join(base_path, 'WV', '*.wvd'))]
base_path = os.path.join(
os.getcwd(), "configs", "CDMs", "users_uploaded", safe_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)
})
return jsonify(
{
"Username": username,
"Widevine_Devices": wv_files,
"Playready_Devices": pr_files,
"API_Key": fetch_api_key(username),
"Styled_Username": fetch_styled_username(username),
}
)
except Exception as e:
logging.exception("Error retrieving device files")
return jsonify({'message': 'False'}), 500
logging.exception("Error retrieving device files, %s", {e})
return jsonify({"message": "False"}), 500