Compare commits

..

6 Commits
main ... github

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

3
.gitignore vendored
View File

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

View File

@ -1,51 +1,39 @@
# 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) ![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)
## GITHUB EDITION
> This version **DOES NOT** come with CDM's (Content Decryption Modules) or the link to automatically download them - A simple web search should help you find what you're looking for.
>
## Prerequisites (from source only)
- [Python](https://www.python.org/downloads/) version 3.12+ with PIP installed - [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 3.13 was used at the time of writing ## Installation (Automatic) - Recommended
- Extract contents of CDRM-Project 2.0 git contents into a new folder
- [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 - Open a terminal and change directory into the new folder
- Run `python build.py && python main.py` - Run `python main.py`
- Follow the on-screen prompts - Follow the on-screen prompts
## Installation (From binary) ## Installation (From binary)
- Download the latest release from the [releases](https://github.com/TPD94/CDRM-Project-2.0/releases) page and run the `.exe`
- 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
## Installation (Manual) - Create a new python virtual environment using `python -m venv CDRM-Project`
- Change directory into the new `CDRM-Project` folder
- Open your terminal and navigate to where you'd like to store the application - Activate the virtual environment
- 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:
```bash > Windows - change directory into the `Scripts` directory then `activate.bat`
.\venv\Scripts\activate >
``` > Linux - `source bin/activate`
- Linux:
```bash - Extract CDRM-Project 2.0 git contents into the newly created `CDRM-Project` folder
source venv/bin/activate - 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`
Verify that the virtual environment is activated by seeing the `(venv)` prefix in your terminal - Run the application with `python main.py`
- 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`

View File

@ -1,39 +0,0 @@
"""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

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

View File

@ -1,9 +0,0 @@
{
"trailingComma": "es5",
"tabWidth": 4,
"semi": true,
"singleQuote": false,
"useTabs": false,
"printWidth": 100,
"plugins": ["prettier-plugin-tailwindcss"]
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

BIN
cdrm-frontend/dist/favico.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 862 B

21
cdrm-frontend/dist/index.html vendored Normal file
View File

@ -0,0 +1,21 @@
<!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-C4QO27se.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-BLw4WNgn.css">
</head>
<body class="w-full h-full">
<div id="root" class="w-full h-full"></div>
</body>
</html>

BIN
cdrm-frontend/dist/og-api.jpg vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 189 KiB

BIN
cdrm-frontend/dist/og-cache.jpg vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 207 KiB

BIN
cdrm-frontend/dist/og-home.jpg vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 302 KiB

BIN
cdrm-frontend/dist/og-testplayer.jpg vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

View File

@ -1,30 +1,33 @@
import js from "@eslint/js"; import js from '@eslint/js'
import globals from "globals"; import globals from 'globals'
import reactHooks from "eslint-plugin-react-hooks"; import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from "eslint-plugin-react-refresh"; import reactRefresh from 'eslint-plugin-react-refresh'
export default [ export default [
{ ignores: ["dist"] }, { ignores: ['dist'] },
{ {
files: ["**/*.{js,jsx}"], files: ['**/*.{js,jsx}'],
languageOptions: { languageOptions: {
ecmaVersion: 2020, ecmaVersion: 2020,
globals: globals.browser, globals: globals.browser,
parserOptions: { parserOptions: {
ecmaVersion: "latest", ecmaVersion: 'latest',
ecmaFeatures: { jsx: true }, ecmaFeatures: { jsx: true },
sourceType: "module", 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> <!doctype html>
<html lang="en" class="w-full h-full"> <html lang="en" class="w-full h-full">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favico.png" /> <link rel="icon" type="image/svg+xml" href="/favico.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content="{{ data.description }}" /> <meta name="description" content="{{ data.description }}"/>
<meta name="keywords" content="{{ data.keywords }}" /> <meta name="keywords" content="{{ data.keywords }}"/>
<meta property="og:title" content="{{ data.opengraph_title }}" /> <meta property='og:title' content="{{ data.opengraph_title }}" />
<meta property="og:description" content="{{ data.opengraph_description }}" /> <meta property='og:description' content="{{ data.opengraph_description }}" />
<meta property="og:image" content="{{ data.opengraph_image }}" /> <meta property='og:image' content="{{ data.opengraph_image }}" />
<meta property="og:url" content="{{ data.opengraph_url }}" /> <meta property='og:url' content="{{ data.opengraph_url }}" />
<meta property="og:locale" content="en_US" /> <meta property='og:locale' content='en_US' />
<title>{{ data.tab_title }}</title> <title>{{ data.tab_title }}</title>
</head> </head>
<body class="w-full h-full"> <body class="w-full h-full">
<div id="root" class="w-full h-full"></div> <div id="root" class="w-full h-full"></div>
<script type="module" src="/src/main.jsx"></script> <script type="module" src="/src/main.jsx"></script>
</body> </body>
</html> </html>

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -1,20 +1,43 @@
import { Route, Routes } from "react-router-dom"; import { useState } from "react";
import Account from "./components/Pages/Account";
import API from "./components/Pages/API";
import Cache from "./components/Pages/Cache";
import Home from "./components/Pages/HomePage"; import Home from "./components/Pages/HomePage";
import Cache from "./components/Pages/Cache";
import API from "./components/Pages/API";
import TestPlayer from "./components/Pages/TestPlayer"; import TestPlayer from "./components/Pages/TestPlayer";
import NavBar from "./components/NavBar";
import NavBarMain from "./components/NavBarMain";
import SideMenu from "./components/SideMenu"; // Add this import
import Account from "./components/Pages/Account";
import { Routes, Route } from "react-router-dom";
function App() { function App() {
return ( const [isMenuOpen, setIsMenuOpen] = useState(false); // Track if the menu is open
<Routes>
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="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="/" element={<Home />} />
<Route path="/cache" element={<Cache />} /> <Route path="/cache" element={<Cache />} />
<Route path="/api" element={<API />} /> <Route path="/api" element={<API />} />
<Route path="/testplayer" element={<TestPlayer />} /> <Route path="/testplayer" element={<TestPlayer />} />
<Route path="/account" element={<Account />} /> <Route path="/account" element={<Account />} />
</Routes> </Routes>
); </div>
</div>
</div>
);
} }
export default App; export default App;

View File

@ -1,15 +0,0 @@
@font-face {
font-family: Inter;
src: url("./InterVariable.woff2");
font-style: normal;
font-weight: 300 900;
font-display: swap;
}
@font-face {
font-family: Inter;
src: url("./InterVariable-Italic.woff2");
font-style: italic;
font-weight: 300 900;
font-display: swap;
}

View File

@ -1,9 +0,0 @@
const Container = ({ children, className = "", ...props }) => {
return (
<main className={`container mx-auto p-4 mb-5 ${className}`} {...props}>
{children}
</main>
);
};
export default Container;

View File

@ -4,11 +4,11 @@ import "./license_protocol.min.js";
const { SignedMessage, LicenseRequest } = protobuf.roots.default.license_protocol; const { SignedMessage, LicenseRequest } = protobuf.roots.default.license_protocol;
function uint8ArrayToBase64(uint8Array) { function uint8ArrayToBase64(uint8Array) {
const binaryString = Array.from(uint8Array) const binaryString = Array.from(uint8Array)
.map((b) => String.fromCharCode(b)) .map(b => String.fromCharCode(b))
.join(""); .join('');
return btoa(binaryString); return btoa(binaryString);
} }
function parseFetch(fetchString) { function parseFetch(fetchString) {
@ -17,13 +17,10 @@ function parseFetch(fetchString) {
// Use a more lenient regex to capture the fetch pattern (including complex bodies) // Use a more lenient regex to capture the fetch pattern (including complex bodies)
const fetchRegex = /fetch\(['"](.+?)['"],\s*(\{.+?\})\)/s; // Non-greedy match for JSON const fetchRegex = /fetch\(['"](.+?)['"],\s*(\{.+?\})\)/s; // Non-greedy match for JSON
const lines = fetchString const lines = fetchString.split('\n').map(line => line.trim()).filter(Boolean);
.split("\n")
.map((line) => line.trim())
.filter(Boolean);
const result = { const result = {
method: "UNDEFINED", method: 'UNDEFINED',
url: "", url: '',
headers: {}, headers: {},
body: null, body: null,
}; };
@ -50,12 +47,9 @@ function parseFetch(fetchString) {
return result; 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 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 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]); const PSSH_MAGIC = new Uint8Array([0x70, 0x73, 0x73, 0x68]);
function intToUint8Array(num, endian) { function intToUint8Array(num, endian) {
@ -81,44 +75,44 @@ function psshDataToPsshBoxB64(pssh_data, system_id) {
...new Uint8Array(4), ...new Uint8Array(4),
...system_id, ...system_id,
...intToUint8Array(dataLength, false), ...intToUint8Array(dataLength, false),
...pssh_data, ...pssh_data
]); ]);
return uint8ArrayToBase64(pssh); return uint8ArrayToBase64(pssh);
} }
function wrmHeaderToPlayReadyHeader(wrm_header) { function wrmHeaderToPlayReadyHeader(wrm_header){
const playready_object = new Uint8Array([ const playready_object = new Uint8Array([
...shortToUint8Array(1, true), ...shortToUint8Array(1, true),
...shortToUint8Array(wrm_header.length, true), ...shortToUint8Array(wrm_header.length, true),
...wrm_header, ...wrm_header
]); ]);
return new Uint8Array([ return new Uint8Array([
...intToUint8Array(playready_object.length + 2 + 4, true), ...intToUint8Array(playready_object.length + 2 + 4, true),
...shortToUint8Array(1, true), ...shortToUint8Array(1, true),
...playready_object, ...playready_object
]); ]);
} }
function encodeUtf16LE(str) { function encodeUtf16LE(str) {
const buffer = new Uint8Array(str.length * 2); const buffer = new Uint8Array(str.length * 2);
for (let i = 0; i < str.length; i++) { for (let i = 0; i < str.length; i++) {
const code = str.charCodeAt(i); const code = str.charCodeAt(i);
buffer[i * 2] = code & 0xff; buffer[i * 2] = code & 0xff;
buffer[i * 2 + 1] = code >> 8; buffer[i * 2 + 1] = code >> 8;
} }
return buffer; return buffer;
} }
function stringToUint8Array(string) { 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() { export async function readTextFromClipboard() {
try { try {
// Request text from the clipboard // Request text from the clipboard
const clipboardText = await navigator.clipboard.readText(); const clipboardText = await navigator.clipboard.readText();
const result = parseFetch(clipboardText); const result = parseFetch(clipboardText);
let pssh_data_string; let pssh_data_string;
@ -142,15 +136,11 @@ export async function readTextFromClipboard() {
license_request = LicenseRequest.decode(signed_message.msg); license_request = LicenseRequest.decode(signed_message.msg);
} catch (decodeError) { } catch (decodeError) {
// If error occurs during decoding, return an empty pssh // If error occurs during decoding, return an empty pssh
console.error("Decoding failed, returning empty pssh", decodeError); console.error('Decoding failed, returning empty pssh', decodeError);
pssh_data_string = ""; // Empty pssh if decoding fails pssh_data_string = ''; // Empty pssh if decoding fails
} }
if ( if (license_request && license_request.contentId && license_request.contentId.widevinePsshData) {
license_request &&
license_request.contentId &&
license_request.contentId.widevinePsshData
) {
const pssh_data = license_request.contentId.widevinePsshData.psshData[0]; const pssh_data = license_request.contentId.widevinePsshData.psshData[0];
pssh_data_string = psshDataToPsshBoxB64(pssh_data, WIDEVINE_SYSTEM_ID); pssh_data_string = psshDataToPsshBoxB64(pssh_data, WIDEVINE_SYSTEM_ID);
} }
@ -170,12 +160,14 @@ export async function readTextFromClipboard() {
document.getElementById("pssh").value = pssh_data_string; document.getElementById("pssh").value = pssh_data_string;
document.getElementById("data").value = payload_string; document.getElementById("data").value = payload_string;
} catch (error) { } 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 // Helper function to check if the data is binary
function isBinary(uint8Array) { function isBinary(uint8Array) {
// Check for non-text (non-ASCII) bytes (basic heuristic) // 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,178 +1,162 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from 'react';
import { NavLink } from "react-router-dom"; import { NavLink } from 'react-router-dom';
import { FaDiscord } from "react-icons/fa"; import homeIcon from '../assets/icons/home.svg';
import { FaTelegram } from "react-icons/fa"; import cacheIcon from '../assets/icons/cache.svg';
import { SiGitea } from "react-icons/si"; import apiIcon from '../assets/icons/api.svg';
import { FaHome } from "react-icons/fa"; import testPlayerIcon from '../assets/icons/testplayer.svg';
import { FaDatabase } from "react-icons/fa"; import accountIcon from '../assets/icons/account.svg';
import { IoCodeSlashSharp } from "react-icons/io5"; import discordIcon from '../assets/icons/discord.svg';
import { FaVideo } from "react-icons/fa"; import telegramIcon from '../assets/icons/telegram.svg';
import { RiAccountCircleFill } from "react-icons/ri"; import giteaIcon from '../assets/icons/gitea.svg';
function NavBar() { function NavBar() {
const [externalLinks, setExternalLinks] = useState({ const [externalLinks, setExternalLinks] = useState({
discord: "#", discord: '#',
telegram: "#", telegram: '#',
gitea: "#", gitea: '#',
}); });
useEffect(() => { useEffect(() => {
fetch("/api/links") fetch('/api/links')
.then((response) => response.json()) .then(response => response.json())
.then((data) => setExternalLinks(data)) .then(data => setExternalLinks(data))
.catch((error) => console.error("Error fetching links:", error)); .catch(error => console.error('Error fetching links:', error));
}, []); }, []);
const MenuItem = ({ to, children }) => {
return (
<li>
<NavLink to={to} className={({ isActive }) => (isActive ? "menu-active" : "")}>
{children}
</NavLink>
</li>
);
};
return ( return (
<> <div className="flex flex-col w-full h-full bg-white/1">
<div className="navbar sticky top-0 z-300 bg-slate-700 shadow-sm text-white"> {/* Header */}
<div className="navbar-start"> <div>
<div className="dropdown"> <p className="text-white text-2xl font-bold p-3 text-center mb-5">
<div tabIndex={0} role="button" className="btn btn-ghost lg:hidden"> <a href="/">CDRM-Project</a><br /><span className="text-sm">Github Edition</span>
<svg </p>
xmlns="http://www.w3.org/2000/svg" </div>
className="h-5 w-5"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
{" "}
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M4 6h16M4 12h8m-8 6h16"
/>{" "}
</svg>
</div>
<ul
tabIndex={0}
className="menu menu-sm dropdown-content bg-base-100 rounded-box z-1 mt-3 w-52 p-2 shadow"
>
<MenuItem to="/">
<FaHome alt="Home" width={20} height={20} />
Home
</MenuItem>
<MenuItem to="/cache">
<FaDatabase alt="Cache" width={20} height={20} />
Cache
</MenuItem>
<MenuItem to="/api">
<IoCodeSlashSharp alt="API" width={20} height={20} />
API
</MenuItem>
<MenuItem to="/testplayer">
<FaVideo alt="Test Player" width={20} height={20} />
Test Player
</MenuItem>
<MenuItem to="/account">
<RiAccountCircleFill alt="My Account" width={20} height={20} />
My Account
</MenuItem>
<div className="divider">Social links</div> {/* Scrollable navigation area */}
<li> <div className="overflow-y-auto grow flex flex-col">
<a {/* Main NavLinks */}
href={externalLinks.discord} <NavLink
target="_blank" to="/"
rel="noopener noreferrer" className={({ isActive }) =>
> `flex flex-row p-3 border-l-3 ${
<FaDiscord alt="Discord" width={20} height={20} /> isActive
Discord ? 'border-l-sky-500/50 bg-black/50'
</a> : 'hover:border-l-sky-500/50 hover:bg-white/5'
</li> }`
<li> }
<a >
href={externalLinks.telegram} <button className="w-1/3 p-3 flex flex-col items-center justify-center cursor-pointer">
target="_blank" <img src={homeIcon} alt="Home" className="w-1/2 cursor-pointer" />
rel="noopener noreferrer" </button>
> <p className="grow text-white md:text-2xl font-bold flex items-center justify-start">
<FaTelegram alt="Telegram" width={20} height={20} /> Home
Telegram </p>
</a> </NavLink>
</li>
<li> <NavLink
<a to="/cache"
href={externalLinks.gitea} className={({ isActive }) =>
target="_blank" `flex flex-row p-3 border-l-3 ${
rel="noopener noreferrer" isActive
> ? 'border-l-emerald-500/50 bg-black/50'
<SiGitea alt="Gitea" width={20} height={20} /> : 'hover:border-l-emerald-500/50 hover:bg-white/5'
Gitea }`
</a> }
</li> >
</ul> <button className="w-1/3 p-3 flex flex-col items-center justify-center cursor-pointer">
</div> <img src={cacheIcon} alt="Cache" className="w-1/2 cursor-pointer" />
<a className="btn btn-ghost text-xl">CDRM-Project</a> </button>
</div> <p className="grow text-white md:text-2xl font-bold flex items-center justify-start">
<div className="navbar-center hidden lg:flex"> Cache
<ul className="menu menu-horizontal px-1"> </p>
<MenuItem to="/"> </NavLink>
<FaHome alt="Home" width={20} height={20} />
Home <NavLink
</MenuItem> to="/api"
<MenuItem to="/cache"> className={({ isActive }) =>
<FaDatabase alt="Cache" width={20} height={20} /> `flex flex-row p-3 border-l-3 ${
Cache isActive
</MenuItem> ? 'border-l-indigo-500/50 bg-black/50'
<MenuItem to="/api"> : 'hover:border-l-indigo-500/50 hover:bg-white/5'
<IoCodeSlashSharp alt="API" width={20} height={20} /> }`
API }
</MenuItem> >
<MenuItem to="/testplayer"> <button className="w-1/3 p-3 flex flex-col items-center justify-center cursor-pointer">
<FaVideo alt="Test Player" width={20} height={20} /> <img src={apiIcon} alt="API" className="w-1/2 cursor-pointer" />
Test Player </button>
</MenuItem> <p className="grow text-white md:text-2xl font-bold flex items-center justify-start">
<MenuItem to="/account"> API
<RiAccountCircleFill alt="My Account" width={20} height={20} /> </p>
</NavLink>
<NavLink
to="/testplayer"
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'
}`
}
>
<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" />
</button>
<p className="grow text-white md:text-2xl font-bold flex items-center justify-start">
Test Player
</p>
</NavLink>
{/* Account link at bottom of scrollable area */}
<div className="mt-auto">
<NavLink
to="/account"
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'
}`
}
>
<button className="w-1/3 p-3 flex flex-col items-center justify-center cursor-pointer">
<img src={accountIcon} alt="Account" className="w-1/2 cursor-pointer" />
</button>
<p className="grow text-white md:text-2xl font-bold flex items-center justify-start">
My Account My Account
</MenuItem> </p>
</ul> </NavLink>
</div>
<div className="navbar-end hidden lg:flex">
<a
className="btn btn-ghost hover:text-indigo-400"
href={externalLinks.discord}
target="_blank"
rel="noopener noreferrer"
>
<div className="tooltip tooltip-bottom" data-tip="CDRM Discord">
<FaDiscord className="h-6 w-6" />
</div>
</a>
<a
className="btn btn-ghost hover:text-sky-400"
href={externalLinks.telegram}
target="_blank"
rel="noopener noreferrer"
>
<div className="tooltip tooltip-bottom" data-tip="CDRM Telegram">
<FaTelegram className="h-6 w-6" />
</div>
</a>
<a
className="btn btn-ghost hover:text-lime-400"
href={externalLinks.gitea}
target="_blank"
rel="noopener noreferrer"
>
<div className="tooltip tooltip-left" data-tip="CDRM Gitea">
<SiGitea className="h-6 w-6" />
</div>
</a>
</div> </div>
</div> </div>
</>
{/* External links at very bottom */}
<div className="flex flex-row w-full h-16 bg-black/25">
<a
href={externalLinks.discord}
target="_blank"
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" />
</a>
<a
href={externalLinks.telegram}
target="_blank"
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" />
</a>
<a
href={externalLinks.gitea}
target="_blank"
rel="noopener noreferrer"
className="w-1/3 p-3 flex flex-col items-center justify-center cursor-pointer hover:bg-green-700 group"
>
<img src={giteaIcon} alt="Gitea" className="w-1/2 group-hover:animate-bounce" />
</a>
</div>
</div>
); );
} }

View File

@ -2,21 +2,22 @@ import { useState } from "react";
import hamburgerIcon from "../assets/icons/hamburger.svg"; import hamburgerIcon from "../assets/icons/hamburger.svg";
function NavBarMain({ setIsMenuOpen }) { function NavBarMain({ setIsMenuOpen }) {
const handleMenuToggle = () => { const handleMenuToggle = () => {
setIsMenuOpen((prevState) => !prevState); // Toggle the menu state setIsMenuOpen((prevState) => !prevState); // Toggle the menu state
}; };
return ( return (
<div className="flex flex-row w-full h-full bg-white/1"> <div className="flex flex-row w-full h-full bg-white/1">
<button className="w-24 p-4" onClick={handleMenuToggle}> <button className="w-24 p-4" onClick={handleMenuToggle}>
<img src={hamburgerIcon} alt="Menu" className="w-full h-full cursor-pointer" /> <img src={hamburgerIcon} alt="Menu" className="w-full h-full cursor-pointer" />
</button> </button>
<p className="grow text-white md:text-2xl font-bold text-center flex items-center justify-center p-4"> <p className="grow text-white md:text-2xl font-bold text-center flex flex-col items-center justify-center p-4">
CDRM-Project CDRM-Project<br />
</p> <span className="text-sm">Github Edition</span>
<div className="w-24 p-4"></div> </p>
</div> <div className="w-24 p-4"></div>
); </div>
);
} }
export default NavBarMain; export default NavBarMain;

View File

@ -1,76 +1,73 @@
import { useEffect, useState } from "react"; import React, { useState, useEffect } from 'react';
import NavBar from "../NavBar"; import { Helmet } from 'react-helmet'; // Import Helmet
import Container from "../Container";
import { FaCopy } from "react-icons/fa";
import { toast } from "sonner";
const { protocol, hostname, port } = window.location; const { protocol, hostname, port } = window.location;
let fullHost = `${protocol}//${hostname}`; let fullHost = `${protocol}//${hostname}`;
if ( if (
(protocol === "http:" && port !== "80") || (protocol === 'http:' && port !== '80') ||
(protocol === "https:" && port !== "443" && port !== "") (protocol === 'https:' && port !== '443' && port !== '')
) { ) {
fullHost += `:${port}`; fullHost += `:${port}`;
} }
const handleCopy = (text) => {
navigator.clipboard.writeText(text);
toast.success("Copied to clipboard");
};
function API() { function API() {
const [deviceInfo, setDeviceInfo] = useState({ const [deviceInfo, setDeviceInfo] = useState({
device_type: "", device_type: '',
system_id: "", system_id: '',
security_level: "", security_level: '',
host: "", host: '',
secret: "", secret: '',
device_name: "", device_name: ''
}); });
const [prDeviceInfo, setPrDeviceInfo] = useState({ const [prDeviceInfo, setPrDeviceInfo] = useState({
security_level: "", security_level: '',
host: "", host: '',
secret: "", secret: '',
device_name: "", device_name: ''
}); });
useEffect(() => { useEffect(() => {
// Fetch Widevine info // Fetch Widevine info
fetch("/remotecdm/widevine/deviceinfo") fetch('/remotecdm/widevine/deviceinfo')
.then((response) => response.json()) .then(response => response.json())
.then((data) => { .then(data => {
setDeviceInfo({ setDeviceInfo({
device_type: data.device_type, device_type: data.device_type,
system_id: data.system_id, system_id: data.system_id,
security_level: data.security_level, security_level: data.security_level,
host: data.host, host: data.host,
secret: data.secret, secret: data.secret,
device_name: data.device_name, device_name: data.device_name
}); });
}) })
.catch((error) => console.error("Error fetching Widevine info:", error)); .catch(error => console.error('Error fetching Widevine info:', error));
// Fetch PlayReady info // Fetch PlayReady info
fetch("/remotecdm/playready/deviceinfo") fetch('/remotecdm/playready/deviceinfo')
.then((response) => response.json()) .then(response => response.json())
.then((data) => { .then(data => {
setPrDeviceInfo({ setPrDeviceInfo({
security_level: data.security_level, security_level: data.security_level,
host: data.host, host: data.host,
secret: data.secret, secret: data.secret,
device_name: data.device_name, device_name: data.device_name
}); });
}) })
.catch((error) => console.error("Error fetching PlayReady info:", error)); .catch(error => console.error('Error fetching PlayReady info:', error));
}, []); }, []);
useEffect(() => { return (
document.title = "API | CDRM-Project"; <div className="flex flex-col w-full overflow-y-auto p-4 text-white">
}, []); <Helmet>
<title>API</title>
const decryptRequest = `import requests </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
print(requests.post( print(requests.post(
url='${fullHost}/api/decrypt', url='${fullHost}/api/decrypt',
@ -86,155 +83,52 @@ print(requests.post(
'Accept-Language': 'en-US,en;q=0.5', 'Accept-Language': 'en-US,en;q=0.5',
}) })
} }
).json()['message'])`; ).json()['message'])`}
</pre>
const searchRequest = `import requests </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( print(requests.post(
url='${fullHost}/api/cache/search', url='${fullHost}/api/cache/search',
json={ json={
'input': 'AAAAW3Bzc2gAAAAA7e+LqXnWSs6jyCfc1R0h7QAAADsIARIQ62dqu8s0Xpa7z2FmMPGj2hoNd2lkZXZpbmVfdGVzdCIQZmtqM2xqYVNkZmFsa3IzaioCSEQyAA==' 'input': 'AAAAW3Bzc2gAAAAA7e+LqXnWSs6jyCfc1R0h7QAAADsIARIQ62dqu8s0Xpa7z2FmMPGj2hoNd2lkZXZpbmVfdGVzdCIQZmtqM2xqYVNkZmFsa3IzaioCSEQyAA=='
} }
).json())`; ).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>
return ( </div>
<> );
<NavBar />
<Container>
<div className="mx-auto flex w-full max-w-2xl flex-col justify-center py-8">
<div className="join join-vertical w-full max-w-2xl">
<div
tabIndex={0}
className="collapse-arrow join-item collapse border border-gray-600"
>
<input type="checkbox" defaultChecked />
<div className="collapse-title text-lg font-semibold">
Sending a decryption request
</div>
<div className="collapse-content text-slate-300">
<pre className="my-4 overflow-auto rounded-lg font-mono break-all whitespace-pre-wrap">
{decryptRequest}
</pre>
<div className="flex justify-end">
<button
type="button"
className="btn btn-primary"
onClick={() => handleCopy(decryptRequest)}
>
<FaCopy /> Copy
</button>
</div>
</div>
</div>
<div
tabIndex={0}
className="collapse-arrow join-item collapse border border-gray-600"
>
<input type="checkbox" defaultChecked />
<div className="collapse-title text-lg font-semibold">
Sending a search request
</div>
<div className="collapse-content text-slate-300">
<pre className="my-4 overflow-auto rounded-lg font-mono break-all whitespace-pre-wrap">
{searchRequest}
</pre>
<div className="flex justify-end">
<button
type="button"
className="btn btn-primary"
onClick={() => handleCopy(searchRequest)}
>
<FaCopy /> Copy
</button>
</div>
</div>
</div>
<div
tabIndex={0}
className="collapse-arrow join-item collapse border border-gray-600"
>
<input type="checkbox" defaultChecked />
<div className="collapse-title text-lg font-semibold">
PyWidevine RemoteCDM info
</div>
<div className="collapse-content text-slate-300">
<p>
<strong>Device Type:</strong>{" "}
<span className="font-mono">
{deviceInfo.device_type || "N/A"}
</span>
</p>
<p>
<strong>System ID:</strong>{" "}
<span className="font-mono">
{deviceInfo.system_id || "N/A"}
</span>
</p>
<p>
<strong>Security Level:</strong>{" "}
<span className="font-mono">
{deviceInfo.security_level || "N/A"}
</span>
</p>
<p>
<strong>Host:</strong>{" "}
<span className="font-mono">{fullHost}/remotecdm/widevine</span>
</p>
<p>
<strong>Secret:</strong>{" "}
<span className="font-mono">{deviceInfo.secret || "N/A"}</span>
</p>
<p>
<strong>Device Name:</strong>{" "}
<span className="font-mono">
{deviceInfo.device_name || "N/A"}
</span>
</p>
</div>
</div>
<div
tabIndex={0}
className="collapse-arrow join-item collapse border border-gray-600"
>
<input type="checkbox" defaultChecked />
<div className="collapse-title text-lg font-semibold">
PyPlayready RemoteCDM info
</div>
<div className="collapse-content text-slate-300">
<p>
<strong>Security Level:</strong>{" "}
<span className="font-mono">
{prDeviceInfo.security_level || "N/A"}
</span>
</p>
<p>
<strong>Host:</strong>{" "}
<span className="font-mono">
{fullHost}/remotecdm/playready
</span>
</p>
<p>
<strong>Secret:</strong>{" "}
<span className="font-mono">
{prDeviceInfo.secret || "N/A"}
</span>
</p>
<p>
<strong>Device Name:</strong>{" "}
<span className="font-mono">
{prDeviceInfo.device_name || "N/A"}
</span>
</p>
</div>
</div>
</div>
</div>
</Container>
</>
);
} }
export default API; export default API;

View File

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

View File

@ -1,27 +1,21 @@
import { useEffect, useRef, useState } from "react"; import { useState, useEffect, useRef } from 'react';
import { FaDownload } from "react-icons/fa"; import { Helmet } from 'react-helmet'; // Import Helmet
import { toast } from "sonner";
import Container from "../Container";
import NavBar from "../NavBar";
function Cache() { function Cache() {
const [searchQuery, setSearchQuery] = useState(""); const [searchQuery, setSearchQuery] = useState('');
const [cacheData, setCacheData] = useState([]); const [cacheData, setCacheData] = useState([]);
const [keyCount, setKeyCount] = useState(0); const [keyCount, setKeyCount] = useState(0); // New state to store the key count
const [loading, setLoading] = useState(false);
const [hasSearched, setHasSearched] = useState(false);
const debounceTimeout = useRef(null); const debounceTimeout = useRef(null);
// Fetch the key count when the component mounts // Fetch the key count when the component mounts
useEffect(() => { useEffect(() => {
const fetchKeyCount = async () => { const fetchKeyCount = async () => {
try { try {
const response = await fetch("/api/cache/keycount"); const response = await fetch('/api/cache/keycount');
const data = await response.json(); const data = await response.json();
setKeyCount(data.count); // Update key count setKeyCount(data.count); // Update key count
} catch (error) { } catch (error) {
console.error("Error fetching key count:", error); console.error('Error fetching key count:', error);
} }
}; };
@ -30,112 +24,83 @@ function Cache() {
const handleInputChange = (event) => { const handleInputChange = (event) => {
const query = event.target.value; const query = event.target.value;
setSearchQuery(query); setSearchQuery(query); // Update the search query
// Clear the previous timeout
if (debounceTimeout.current) { if (debounceTimeout.current) {
clearTimeout(debounceTimeout.current); clearTimeout(debounceTimeout.current);
} }
if (query.trim() !== "") { // Set a new timeout to send the API call after 1 second of no typing
setLoading(true); // Show spinner immediately debounceTimeout.current = setTimeout(() => {
debounceTimeout.current = setTimeout(() => { if (query.trim() !== '') {
sendApiCall(query); sendApiCall(query); // Only call the API if the query is not empty
}, 1000); } else {
} else { setCacheData([]); // Clear results if query is empty
setHasSearched(false); // Reset state when input is cleared }
setCacheData([]); }, 1000); // 1 second delay
}
}; };
const sendApiCall = (text) => { const sendApiCall = (text) => {
setLoading(true); fetch('/api/cache/search', {
fetch("/api/cache/search", { method: 'POST',
method: "POST", headers: { 'Content-Type': 'application/json' },
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ input: text }), body: JSON.stringify({ input: text }),
}) })
.then((response) => response.json()) .then((response) => response.json())
.then((data) => { .then((data) => setCacheData(data)) // Update cache data with the results
setCacheData(data); .catch((error) => console.error('Error:', error));
setHasSearched(true);
})
.catch((error) => {
toast.error(`Error: ${error.message}`);
console.error("Error:", error);
})
.finally(() => setLoading(false));
}; };
useEffect(() => {
document.title = "Cache | CDRM-Project";
}, []);
return ( return (
<> <div className="flex flex-col w-full h-full overflow-y-auto p-4">
<NavBar /> <Helmet>
<Container> <title>Cache</title>
<div className="my-4 flex w-full flex-col items-center justify-center gap-2 lg:flex-row"> </Helmet>
<fieldset className="fieldset w-full max-w-2xl"> <div className="flex flex-col lg:flex-row w-full lg:h-12 items-center">
<input <input
type="text" type="text"
value={searchQuery} value={searchQuery}
onChange={handleInputChange} onChange={handleInputChange}
placeholder={`Search ${keyCount} keys...`} placeholder={`Search ${keyCount} keys...`} // Dynamic placeholder
className="input w-full max-w-2xl font-mono" className="lg:grow w-full border-2 border-emerald-500/25 rounded-xl h-10 self-center m-2 text-white p-1 focus:outline-none focus:ring-2 focus:ring-emerald-500/50 transition-all duration-200 ease-in-out"
/> />
</fieldset> <a
<button href="/api/cache/download"
className="btn btn-success" className="bg-emerald-500/50 rounded-xl text-white text-bold text-xl p-1 lg:w-1/5 lg:h-10 truncate w-full text-center flex items-center justify-center m-2"
onClick={() => { >
window.location.href = "/api/cache/download"; Download Cache
}} </a>
> </div>
<FaDownload /> <div className="w-full grow p-4 border-2 border-emerald-500/50 rounded-2xl mt-5 overflow-y-auto">
Download keys cache <table className="min-w-full text-white">
</button> <thead>
</div> <tr>
<th className="p-2 border border-black">PSSH</th>
{loading ? ( <th className="p-2 border border-black">KID</th>
<div className="flex justify-center py-16"> <th className="p-2 border border-black">Key</th>
<span className="loading loading-spinner loading-md me-2"></span> </tr>
Searching... </thead>
</div> <tbody>
) : cacheData.length > 0 ? ( {cacheData.length > 0 ? (
<div className="my-4 flex justify-center"> cacheData.map((item, index) => (
<div className="overflow-x-auto"> <tr key={index}>
<table className="table"> <td className="p-2 border border-black">{item.PSSH}</td>
<thead> <td className="p-2 border border-black">{item.KID}</td>
<tr> <td className="p-2 border border-black">{item.Key}</td>
<th></th> </tr>
<th className="text-center">PSSH</th> ))
<th className="text-center">key ID:key pair</th> ) : (
</tr> <tr>
</thead> <td colSpan="3" className="p-2 border border-black text-center">
<tbody> No data found
{cacheData.map((item, index) => ( </td>
<tr key={index}> </tr>
<th>{index + 1}</th> )}
<td className="font-mono">{item.PSSH}</td> </tbody>
<td className="font-mono"> </table>
{item.KID}:{item.Key} </div>
</td> </div>
</tr>
))}
</tbody>
</table>
</div>
</div>
) : hasSearched ? (
<div className="flex justify-center py-16">
<div className="text-center">No data found in the database</div>
</div>
) : (
<div className="flex justify-center py-16">
<div className="text-center">Enter a search query to see results</div>
</div>
)}
</Container>
</>
); );
} }

View File

@ -1,290 +1,248 @@
import { useEffect, useRef, useState } from "react"; import React, { useState, useEffect, useRef } from 'react';
import { readTextFromClipboard } from "../Functions/ParseChallenge"; import { readTextFromClipboard } from '../Functions/ParseChallenge';
import NavBar from "../NavBar"; import { Helmet } from 'react-helmet'; // Import Helmet
import Container from "../Container";
import { toast } from "sonner";
import { IoInformationCircleOutline } from "react-icons/io5";
function HomePage() { function HomePage() {
const [pssh, setPssh] = useState(""); const [pssh, setPssh] = useState('');
const [licurl, setLicurl] = useState(""); const [licurl, setLicurl] = useState('');
const [proxy, setProxy] = useState(""); const [proxy, setProxy] = useState('');
const [headers, setHeaders] = useState(""); const [headers, setHeaders] = useState('');
const [cookies, setCookies] = useState(""); const [cookies, setCookies] = useState('');
const [data, setData] = useState(""); const [data, setData] = useState('');
const [message, setMessage] = useState(""); const [message, setMessage] = useState('');
const [isVisible, setIsVisible] = useState(false); const [isVisible, setIsVisible] = useState(false);
const [devices, setDevices] = useState([]); const [devices, setDevices] = useState([]);
const [selectedDevice, setSelectedDevice] = useState("default"); const [selectedDevice, setSelectedDevice] = useState('default');
useEffect(() => { const bottomRef = useRef(null);
document.title = "Home | CDRM-Project"; const messageRef = useRef(null); // Reference to result container
}, []);
const bottomRef = useRef(null); const handleReset = () => {
const messageRef = useRef(null); // Reference to result container if (isVisible) {
setIsVisible(false);
}
setPssh('');
setLicurl('');
setProxy('');
setHeaders('');
setCookies('');
setData('');
};
const handleReset = () => { const handleSubmitButton = (event) => {
if (isVisible) { event.preventDefault();
setIsVisible(false);
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');
} }
setPssh(""); })
setLicurl(""); .then(res => res.json())
setProxy(""); .then(deviceData => {
setHeaders(""); const combinedDevices = [
setCookies(""); ...deviceData.Widevine_Devices,
setData(""); ...deviceData.Playready_Devices,
}; ];
const handleSubmitButton = (event) => { // Add default devices if logged in
event.preventDefault(); const allDevices = [
"CDRM-Project Public Widevine CDM",
"CDRM-Project Public PlayReady CDM",
...combinedDevices,
];
fetch("/api/decrypt", { // Set devices and select a device if logged in
method: "POST", setDevices(allDevices.length > 0 ? allDevices : []);
headers: { setSelectedDevice(allDevices.length > 0 ? allDevices[0] : 'default');
"Content-Type": "application/json", })
}, .catch(() => {
body: JSON.stringify({ // User isn't logged in, set default device to 'default'
pssh: pssh, setDevices([]); // Don't display devices list
licurl: licurl, setSelectedDevice('default');
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. Reason: ${error.message}`);
setIsVisible(true);
});
};
const handleCopy = (event) => { return (
event.preventDefault(); <>
if (messageRef.current) { <div className="flex flex-col w-full overflow-y-auto p-4 min-h-full">
const textToCopy = messageRef.current.innerText; // Grab the plain text (with visual line breaks) <Helmet>
toast.success("Copied to clipboard"); <title>CDRM-Project</title>
navigator.clipboard.writeText(textToCopy).catch((err) => { </Helmet>
toast.error(`Failed to copy. Reason: ${err.message}`); <form className="flex flex-col w-full h-full bg-black/5 p-4 overflow-y-auto">
console.error(err); <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 handleFetchPaste = (event) => { {/* Device Selection Dropdown, only show if logged in */}
event.preventDefault(); {devices.length > 0 && (
readTextFromClipboard() <>
.then(() => { <label htmlFor="device" className="text-white w-8/10 self-center">Select Device:</label>
setPssh(document.getElementById("pssh").value); <select
setLicurl(document.getElementById("licurl").value); id="device"
setHeaders(document.getElementById("headers").value); className="w-8/10 border-2 border-sky-500/25 rounded-xl h-10 self-center m-2 text-white bg-black p-1"
setData(document.getElementById("data").value); value={selectedDevice}
}) onChange={(e) => setSelectedDevice(e.target.value)}
.catch((err) => { >
toast.error(`Failed to paste from fetch. Reason: ${err.message}`); {devices.map((device, index) => (
console.error("Failed to paste from fetch:", err); <option key={index} value={device}>{device}</option>
}); ))}
}; </select>
</>
)}
useEffect(() => { <div className="flex flex-col lg:flex-row w-full self-center mt-5 items-center lg:justify-around lg:items-stretch">
if (isVisible && bottomRef.current) { <button
bottomRef.current.scrollIntoView({ behavior: "smooth" }); 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"
}, [message, isVisible]); 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(() => { {isVisible && (
fetch("/login/status", { <div id="main_content" className="flex-col w-full h-full p-10 items-center justify-center self-center">
method: "POST", <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'>
.then((res) => res.json()) <p className="w-full border-b-2 border-white/75 pb-2">Results:</p>
.then((statusData) => { <p
if (statusData.message === "True") { className="w-full grow pt-10 break-words overflow-y-auto"
return fetch("/userinfo", { method: "POST" }); ref={messageRef}
} else { dangerouslySetInnerHTML={{ __html: message }}
throw new Error("Not logged in"); />
} <div ref={bottomRef} />
}) </div>
.then((res) => res.json()) </div>
.then((deviceData) => { <div className="flex flex-col lg:flex-row w-full self-center mt-5 items-center lg:justify-around lg:items-stretch">
const combinedDevices = [ <button
...deviceData.Widevine_Devices, 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"
...deviceData.Playready_Devices, onClick={handleCopy}
]; >
Copy Results
// Add default devices if logged in </button>
const allDevices = [ </div>
"CDRM-Project Public Widevine CDM", </div>
"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 (
<>
<NavBar />
<Container>
<div className="mx-auto flex w-full max-w-2xl flex-col justify-center">
<fieldset className="fieldset">
<legend className="fieldset-legend text-base">PSSH*</legend>
<input
type="text"
className="input w-full font-mono"
placeholder="Enter PSSH here"
value={pssh}
onChange={(e) => setPssh(e.target.value)}
required
/>
<p className="label text-red-500">* Required</p>
</fieldset>
<fieldset className="fieldset">
<legend className="fieldset-legend text-base">License URL*</legend>
<input
type="text"
className="input w-full font-mono"
placeholder="Enter License URL here"
value={licurl}
onChange={(e) => setLicurl(e.target.value)}
required
/>
<p className="label text-red-500">* Required</p>
</fieldset>
<fieldset className="fieldset">
<legend className="fieldset-legend text-base">Proxy</legend>
<input
type="text"
className="input w-full font-mono"
placeholder="Enter Proxy here (https://example.com:8080)"
value={proxy}
onChange={(e) => setProxy(e.target.value)}
/>
</fieldset>
<fieldset className="fieldset">
<legend className="fieldset-legend text-base">
Headers*
<div
className="tooltip"
data-tip="You can use https://curlconverter.com/python/ to paste the header values here"
>
<IoInformationCircleOutline className="h-5 w-5" />
</div>
</legend>
<textarea
className="textarea h-48 w-full font-mono"
placeholder="Enter headers here (JSON format). E.g. {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'}"
value={headers}
onChange={(e) => setHeaders(e.target.value)}
required
/>
<p className="label text-red-500">* Required</p>
</fieldset>
<fieldset className="fieldset">
<legend className="fieldset-legend text-base">Cookies</legend>
<textarea
className="textarea h-48 w-full font-mono"
placeholder="Enter cookies here (JSON format)"
value={cookies}
onChange={(e) => setCookies(e.target.value)}
/>
</fieldset>
<fieldset className="fieldset">
<legend className="fieldset-legend text-base">Data</legend>
<textarea
className="textarea h-48 w-full font-mono"
placeholder="Enter data here (JSON format)"
value={data}
onChange={(e) => setData(e.target.value)}
/>
</fieldset>
{/* Device Selection Dropdown, only show if logged in */}
{devices.length > 0 && (
<>
<fieldset className="fieldset">
<legend className="fieldset-legend text-base">Select device</legend>
<select
className="select w-full"
value={selectedDevice}
onChange={(e) => setSelectedDevice(e.target.value)}
>
{devices.map((device, index) => (
<option key={index} value={device}>
{device}
</option>
))}
</select>
</fieldset>
</>
)}
<div className="mx-auto my-4 flex w-full flex-col items-center justify-center gap-2 lg:flex-row">
<button
type="button"
className="btn btn-primary btn-wide"
onClick={handleSubmitButton}
disabled={pssh === "" || licurl === "" || headers === ""}
>
Submit
</button>
<button
type="button"
className="btn btn-info btn-wide"
onClick={handleFetchPaste}
>
Paste from fetch
</button>
<button
type="button"
className="btn btn-error btn-wide"
onClick={handleReset}
>
Reset
</button>
</div>
</div>
{isVisible && (
<>
<div className="mx-auto my-4 flex w-full max-w-2xl flex-col justify-center">
<div className="card bg-base-100 card-lg border border-gray-500 shadow-sm">
<div className="card-body">
<h2 className="card-title">Result</h2>
<div className="divider"></div>
<p
className="w-full grow overflow-y-auto font-mono break-words"
ref={messageRef}
dangerouslySetInnerHTML={{ __html: message }}
/>
<div ref={bottomRef} />
<div
className="card-actions mt-4 justify-end"
onClick={handleCopy}
>
<button className="btn btn-success">Copy results</button>
</div>
</div>
</div>
</div>
</>
)}
</Container>
</>
);
} }
export default HomePage; export default HomePage;

View File

@ -1,318 +1,146 @@
import axios from "axios"; import React, { useState, useEffect } from 'react';
import { useEffect, useState } from "react"; import axios from 'axios';
import { toast } from "sonner";
import Container from "../Container";
function MyAccount() { function MyAccount() {
const [wvList, setWvList] = useState([]); const [wvList, setWvList] = useState([]);
const [prList, setPrList] = useState([]); const [prList, setPrList] = useState([]);
const [uploading, setUploading] = useState(false); const [uploading, setUploading] = useState(false);
const [username, setUsername] = useState(""); const [username, setUsername] = useState('');
const [apiKey, setApiKey] = useState("");
const [password, setPassword] = useState("");
const [passwordError, setPasswordError] = useState("");
const [newApiKey, setNewApiKey] = useState("");
const [apiKeyError, setApiKeyError] = useState("");
// Fetch user info // Fetch user info
const fetchUserInfo = async () => { const fetchUserInfo = async () => {
try { try {
const response = await axios.post("/userinfo"); const response = await axios.post('/userinfo');
setWvList(response.data.Widevine_Devices || []); setWvList(response.data.Widevine_Devices || []);
setPrList(response.data.Playready_Devices || []); setPrList(response.data.Playready_Devices || []);
setUsername(response.data.Styled_Username || ""); setUsername(response.data.Username || '');
setApiKey(response.data.API_Key || ""); } catch (err) {
} catch (err) { console.error('Failed to fetch user info', err);
toast.error(`Failed to fetch user info. Reason: ${err.message}`); }
console.error("Failed to fetch user info", err); };
}
};
useEffect(() => { useEffect(() => {
fetchUserInfo(); fetchUserInfo();
}, []); }, []);
useEffect(() => { // Handle file upload
document.title = "My account | CDRM-Project"; const handleUpload = async (event, cdmType) => {
}, []); const file = event.target.files[0];
if (!file) return;
// Handle file upload const extension = file.name.split('.').pop();
const handleUpload = async (event, cdmType) => { if ((cdmType === 'PR' && extension !== 'prd') || (cdmType === 'WV' && extension !== 'wvd')) {
const file = event.target.files[0]; alert(`Please upload a .${cdmType === 'PR' ? 'prd' : 'wvd'} file.`);
if (!file) return; return;
}
const extension = file.name.split(".").pop(); const formData = new FormData();
if ( formData.append('file', file);
(cdmType === "PR" && extension !== "prd") ||
(cdmType === "WV" && extension !== "wvd")
) {
toast.error(`Please upload a .${cdmType === "PR" ? "prd" : "wvd"} file.`);
return;
}
const formData = new FormData(); setUploading(true);
formData.append("file", file); 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);
}
};
setUploading(true); // Handle logout
try { const handleLogout = async () => {
await axios.post(`/upload/${cdmType}`, formData); try {
await fetchUserInfo(); // Refresh list after upload await axios.post('/logout');
} catch (err) { window.location.reload();
toast.error(`Upload failed. Reason: ${err.message}`); } catch (error) {
console.error("Upload failed", err); console.error('Logout failed:', error);
} finally { alert('Logout failed!');
toast.success(`${cdmType} CDM uploaded successfully`); }
setUploading(false); };
}
};
// Handle logout return (
const handleLogout = async () => { <div id="myaccount" className="flex flex-row w-full min-h-full overflow-y-auto p-4">
try { <div className="flex flex-col w-full min-h-full lg:flex-row">
await axios.post("/logout"); {/* Left Panel */}
toast.success("Logged out successfully. Reloading page..."); <div className="border-2 border-yellow-500/50 lg:h-full lg:w-96 w-full rounded-2xl p-4 flex flex-col items-center overflow-y-auto">
window.location.reload(); <h1 className="text-2xl font-bold text-white border-b-2 border-white p-2 w-full text-center mb-2">
} catch (error) { {username ? `${username}` : 'My Account'}
toast.error(`Logout failed. Reason: ${error.message}`); </h1>
console.error("Logout failed:", error); <button
} onClick={handleLogout}
}; className="mt-auto w-full h-12 bg-yellow-500/50 rounded-2xl text-2xl text-white"
>
Log out
</button>
</div>
// Handle change password {/* Right Panel */}
const handleChangePassword = async () => { <div className="flex flex-col grow lg:ml-2 mt-2 lg:mt-0">
if (passwordError || password === "") { {/* Widevine Section */}
toast.error("Please enter a valid password"); <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">
return; <h1 className="text-2xl font-bold text-white border-b-2 border-white p-2">Widevine CDMs</h1>
} <div className="flex flex-col w-full grow p-2 bg-white/5 rounded-2xl mt-2 text-white text-left">
{wvList.length === 0 ? (
<div className="text-white text-center font-bold">No Widevine CDMs uploaded.</div>
) : (
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 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>
try { {/* Playready Section */}
const response = await axios.post("/user/change_password", { <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">
new_password: password, <h1 className="text-2xl font-bold text-white border-b-2 border-white p-2">Playready CDMs</h1>
}); <div className="flex flex-col w-full bg-white/5 grow rounded-2xl mt-2 text-white text-left p-2">
{prList.length === 0 ? (
if (response.data.message === "True") { <div className="text-white text-center font-bold">No Playready CDMs uploaded.</div>
toast.success("Password changed successfully"); ) : (
setPassword(""); prList.map((filename, i) => (
} else { <div
toast.error("Failed to change password"); key={i}
} className={`text-center font-bold text-white p-2 rounded ${
} catch (error) { i % 2 === 0 ? 'bg-black/30' : 'bg-black/60'
if (error.response && error.response.data?.message === "Invalid password format") { }`}
toast.error("Password format is invalid. Please try again."); >
} else { {filename}
toast.error("Error occurred while changing password"); </div>
} ))
} )}
}; </div>
<label className="bg-yellow-500 text-white w-full h-16 mt-4 rounded-2xl flex items-center justify-center cursor-pointer">
// Handle change API key {uploading ? 'Uploading...' : 'Upload CDM'}
const handleChangeApiKey = async () => { <input
if (apiKeyError || newApiKey === "") { type="file"
toast.error("Please enter a valid API key"); accept=".prd"
return; hidden
} onChange={(e) => handleUpload(e, 'PR')}
/>
try { </label>
const response = await axios.post("/user/change_api_key", { </div>
new_api_key: newApiKey, </div>
}); </div>
if (response.data.message === "True") { </div>
toast.success("API key changed successfully"); );
setApiKey(newApiKey);
setNewApiKey("");
} else {
toast.error("Failed to change API key");
}
} catch (error) {
toast.error("Error occurred while changing API key");
console.error(error);
}
};
return (
<>
<Container>
<div className="flex flex-col gap-4 p-4 lg:flex-row">
{/* Left Panel - Account Settings */}
<div className="w-full lg:w-96">
<div className="card bg-base-200 shadow-xl">
<div className="card-body">
<p className="text-center text-sm">Username:</p>
<h2 className="card-title justify-center text-center font-bold">
{username}
</h2>
<div className="divider"></div>
<fieldset className="fieldset">
<legend className="fieldset-legend text-base" htmlFor="apiKey">
API Key
</legend>
<input
name="apiKey"
type="text"
value={apiKey}
readOnly
className="input input-bordered text-center"
/>
<legend
className="fieldset-legend text-base"
htmlFor="newApiKey"
>
New API Key
</legend>
<input
name="newApiKey"
type="text"
value={newApiKey}
onChange={(e) => {
const value = e.target.value;
const isValid = /^[^\s]+$/.test(value);
if (!isValid) {
setApiKeyError("API key must not contain spaces");
} else {
setApiKeyError("");
}
setNewApiKey(value);
}}
placeholder="Enter new API key"
className="input input-bordered"
/>
{apiKeyError && (
<p className="label text-error">{apiKeyError}</p>
)}
<button
className="btn btn-primary btn-block mt-2"
onClick={handleChangeApiKey}
>
Change API key
</button>
</fieldset>
<fieldset className="fieldset">
<legend
className="fieldset-legend text-base"
htmlFor="passwordChange"
>
Change password
</legend>
<input
name="passwordChange"
type="password"
value={password}
onChange={(e) => {
const value = e.target.value;
const isValid =
/^[A-Za-z0-9!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?`~]*$/.test(
value
);
if (!isValid) {
setPasswordError("Invalid password characters");
} else {
setPasswordError("");
}
setPassword(value);
}}
placeholder="New password"
className="input input-bordered"
/>
{passwordError && (
<p className="label text-error">{passwordError}</p>
)}
<button
className="btn btn-secondary btn-block mt-2"
onClick={handleChangePassword}
>
Change password
</button>
</fieldset>
<div className="divider"></div>
<button className="btn btn-error mt-auto" onClick={handleLogout}>
Log out
</button>
</div>
</div>
</div>
{/* Right Panel - CDM Uploads */}
<div className="flex w-full flex-col gap-4">
{/* Widevine CDM */}
<div className="card bg-base-200 shadow-xl">
<div className="card-body">
<h2 className="card-title">Widevine CDMs</h2>
<div className="divider"></div>
<div className="max-h-60 space-y-2 overflow-y-auto">
{wvList.length === 0 ? (
<div className="text-center text-sm">
No Widevine CDMs uploaded.
</div>
) : (
wvList.map((filename, i) => (
<div
key={i}
className={`rounded px-2 py-1 text-sm ${
i % 2 === 0 ? "bg-base-100" : "bg-base-300"
}`}
>
{filename}
</div>
))
)}
</div>
<label className="btn btn-accent mt-4">
{uploading ? "Uploading..." : "Upload CDM"}
<input
type="file"
accept=".wvd"
hidden
onChange={(e) => handleUpload(e, "WV")}
/>
</label>
</div>
</div>
{/* PlayReady CDM */}
<div className="card bg-base-200 shadow-xl">
<div className="card-body">
<h2 className="card-title">PlayReady CDMs</h2>
<div className="divider"></div>
<div className="max-h-60 space-y-2 overflow-y-auto">
{prList.length === 0 ? (
<div className="text-center text-sm">
No PlayReady CDMs uploaded.
</div>
) : (
prList.map((filename, i) => (
<div
key={i}
className={`rounded px-2 py-1 text-sm ${
i % 2 === 0 ? "bg-base-100" : "bg-base-300"
}`}
>
{filename}
</div>
))
)}
</div>
<label className="btn btn-accent mt-4">
{uploading ? "Uploading..." : "Upload CDM"}
<input
type="file"
accept=".prd"
hidden
onChange={(e) => handleUpload(e, "PR")}
/>
</label>
</div>
</div>
</div>
</div>
</Container>
</>
);
} }
export default MyAccount; export default MyAccount;

View File

@ -1,149 +1,95 @@
import { useEffect, useState } from "react"; import React, { useState } from 'react';
import { toast } from "sonner";
import { IoIosLogIn } from "react-icons/io";
import { PiUserCirclePlus } from "react-icons/pi";
function Register() { function Register() {
const [username, setUsername] = useState(""); const [username, setUsername] = useState('');
const [password, setPassword] = useState(""); const [password, setPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState(""); const [status, setStatus] = useState('');
const [tab, setTab] = useState("login"); // 'login' or 'register'
useEffect(() => { const handleRegister = async () => {
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 validateUsername = (name) => /^[A-Za-z0-9_-]+$/.test(name); const handleLogin = async () => {
const validatePassword = (pass) => /^\S+$/.test(pass); // No spaces 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 handleSubmit = async (e) => { return (
e.preventDefault(); <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">
if (!validateUsername(username)) { <div className="flex flex-col w-full">
toast.error("Invalid username. Use only letters, numbers, hyphens, or underscores."); <label htmlFor="username" className="text-lg font-bold mb-2 text-white">Username:</label>
return; <input
} type="text"
if (!validatePassword(password)) { value={username}
toast.error("Invalid password. Spaces are not allowed."); onChange={e => setUsername(e.target.value)}
return; placeholder="Username"
} className="mb-4 p-2 border border-gray-300 rounded text-white bg-transparent"
/>
if (tab === "register") { <label htmlFor="password" className="text-lg font-bold mb-2 text-white">Password:</label>
if (password !== confirmPassword) { <input
toast.error("Passwords do not match."); type="password"
return; value={password}
} onChange={e => setPassword(e.target.value)}
try { placeholder="Password"
const res = await fetch("/register", { className="mb-4 p-2 border border-gray-300 rounded text-white bg-transparent"
method: "POST", />
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ username, password }),
});
const data = await res.json();
if (data.message) {
toast.success(data.message);
} else {
toast.error(data.error || "Unknown error");
}
} catch (err) {
toast.error(`Register error: ${err.message}`);
}
} else {
try {
const res = await fetch("/login", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
credentials: "include",
body: JSON.stringify({ username, password }),
});
const data = await res.json();
if (data.message) {
window.location.reload();
} else {
toast.error(data.error || "Login failed");
}
} catch (err) {
toast.error(`Login error: ${err.message}`);
}
}
};
return (
<div className="mx-auto flex min-h-full w-full max-w-xl flex-col justify-center px-6 py-12 lg:px-8">
<div className="mx-auto">
{/* Tabs */}
<div className="tabs tabs-box justify-center">
<button
className={`tab ${tab === "login" ? "tab-active" : ""}`}
onClick={() => setTab("login")}
>
<IoIosLogIn className="h-6 w-6 me-1"/>
Sign in
</button>
<button
className={`tab ${tab === "register" ? "tab-active" : ""}`}
onClick={() => setTab("register")}
>
<PiUserCirclePlus className="h-6 w-6 me-1" />
Register
</button>
</div>
<h2 className="mt-10 text-center text-2xl font-bold tracking-tight">
{tab === "login" ? "Sign in" : "Register"}
</h2>
</div>
<div className="mx-auto mt-10 w-full max-w-xl">
<form className="space-y-6" onSubmit={handleSubmit}>
<fieldset className="fieldset">
<legend className="fieldset-legend text-base">Username</legend>
<input
type="text"
value={username}
onChange={(e) => setUsername(e.target.value)}
className="input w-full"
placeholder="Enter your username"
required
/>
</fieldset>
<fieldset className="fieldset">
<legend className="fieldset-legend text-base">Password</legend>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
className="input w-full"
placeholder="Enter your password"
required
/>
</fieldset>
{tab === "register" && (
<fieldset className="fieldset">
<legend className="fieldset-legend text-base">Confirm password</legend>
<input
type="password"
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
className="input w-full"
placeholder="Confirm your password"
required
/>
</fieldset>
)}
<button type="submit" className="btn btn-primary btn-block">
{tab === "login" ? "Sign in" : "Register"}
</button>
</form>
</div>
</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; export default Register;

View File

@ -1,178 +1,158 @@
import { useEffect, useRef, useState } from "react"; import React, { useState, useEffect, useRef } from 'react';
import shaka from "shaka-player"; import shaka from 'shaka-player';
import { toast } from "sonner"; import { Helmet } from 'react-helmet'; // Import Helmet
import Container from "../Container";
import NavBar from "../NavBar";
function TestPlayer() { function TestPlayer() {
const [mpdUrl, setMpdUrl] = useState(""); const [mpdUrl, setMpdUrl] = useState(''); // State to hold the MPD URL
const [headers, setHeaders] = useState(""); const [kids, setKids] = useState(''); // State to hold KIDs (separated by line breaks)
const [keyPairs, setKeyPairs] = useState(""); // "kid:key" per line 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); const videoRef = useRef(null); // Ref for the video element
const playerRef = useRef(null); const playerRef = useRef(null); // Ref for Shaka Player instance
const handleInputChange = (event) => { // Function to update the MPD URL state
setMpdUrl(event.target.value); const handleInputChange = (event) => {
}; setMpdUrl(event.target.value);
};
const handleKeyPairsChange = (event) => { // Function to update KIDs and Keys
setKeyPairs(event.target.value); const handleKidsChange = (event) => {
}; setKids(event.target.value);
};
const handleHeadersChange = (event) => { const handleKeysChange = (event) => {
setHeaders(event.target.value); setKeys(event.target.value);
}; };
// Function to initialize Shaka Player const handleHeadersChange = (event) => {
const initializePlayer = async () => { setHeaders(event.target.value);
if (videoRef.current && !playerRef.current) { };
const player = new shaka.Player(); // no mediaElement
await player.attach(videoRef.current); // attach later
playerRef.current = player;
player.addEventListener("error", (event) => { // Function to initialize Shaka Player
toast.error(`Error code ${event.detail.code}: ${event.detail.message}`); const initializePlayer = () => {
console.error("Error code", event.detail.code, "object", event.detail); 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 handle submit and configure player with DRM keys and headers // Add error listener
const handleSubmit = () => { player.addEventListener('error', (event) => {
if (mpdUrl && keyPairs) { console.error('Error code', event.detail.code, 'object', event.detail);
// Parse KID:KEY pairs
const lines = keyPairs
.split("\n")
.map((line) => line.trim())
.filter(Boolean);
const clearKeys = {};
for (const line of lines) {
const [kid, key] = line.split(":").map((part) => part.trim());
if (!kid || !key) {
toast.error(`Invalid line (expected format keyId:key) at line "${line}"`);
console.error(`Invalid line (expected format keyId:key) at line "${line}"`);
return;
}
clearKeys[kid] = key;
}
// 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: clearKeys,
},
};
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");
toast.success("Video successfully loaded");
})
.catch((error) => {
toast.error(`Error loading the video. Reason: ${error.message}`);
console.error("Error loading the video", error);
});
} else {
toast.error("Manifest URL and key pairs are required");
console.error("Manifest URL and key pairs 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; }
}; }
};
useEffect(() => { // Function to handle submit and configure player with DRM keys and headers
document.title = "Test player | CDRM-Project"; 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());
return ( if (kidsArray.length !== keysArray.length) {
<> console.error("The number of KIDs and Keys must be the same.");
<NavBar /> return;
<Container> }
<div className="flex w-full flex-col items-center justify-center py-8">
<div className="flex w-full flex-col items-center lg:flex-row lg:items-start lg:gap-4">
{/* Video Section */}
<div className="w-full lg:w-1/2">
<video
ref={videoRef}
width="100%"
height="auto"
controls
className="aspect-video max-h-96 w-full"
/>
</div>
{/* Inputs Section */} // Initialize Shaka Player only when the submit button is pressed
<div className="mt-4 flex w-full flex-col items-center lg:mt-0 lg:w-1/2"> const player = new shaka.Player(videoRef.current);
<fieldset className="fieldset w-full">
<legend className="fieldset-legend text-base">Manifest URL*</legend> // Widevine DRM configuration with the provided KIDs and Keys
<input const config = {
type="text" drm: {
value={mpdUrl} clearKeys: {},
onChange={handleInputChange} },
placeholder="Enter manifest URL here" };
className="input w-full font-mono"
/> // Map KIDs to Keys
<p className="label text-red-500">* Required</p> kidsArray.forEach((kid, index) => {
</fieldset> config.drm.clearKeys[kid] = keysArray[index];
<fieldset className="fieldset w-full"> });
<legend className="fieldset-legend text-base">Key pairs*</legend>
<textarea console.log("Configuring player with the following DRM config and headers:", config);
placeholder="keyId:key pair (one per line)"
value={keyPairs} // Configure the player with ClearKey DRM and custom headers
onChange={handleKeyPairsChange} player.configure(config);
className="textarea w-full font-mono"
/> // Load the video stream with MPD URL
<p className="label text-red-500">* Required</p> player.load(mpdUrl).then(() => {
</fieldset> console.log('Video loaded');
<fieldset className="fieldset w-full"> }).catch((error) => {
<legend className="fieldset-legend text-base">Headers</legend> console.error('Error loading the video', error);
<textarea });
placeholder="Headers (one per line)" } else {
value={headers} console.error('MPD URL, KIDs, and Keys are required.');
onChange={handleHeadersChange} }
className="textarea w-full font-mono" };
/>
</fieldset> // Load the video stream whenever the MPD URL changes
<button useEffect(() => {
onClick={handleSubmit} initializePlayer(); // Initialize the player if it's not initialized already
className="btn btn-primary btn-wide my-4" }, []); // This effect runs only once on mount
>
Submit // Helper function to parse headers from the textarea input
</button> const parseHeaders = (headersText) => {
</div> const headersArr = headersText.split('\n');
</div> const headersObj = {};
</div> headersArr.forEach((line) => {
</Container> 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>
);
} }
export default TestPlayer; export default TestPlayer;

View File

@ -1,182 +1,179 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from 'react';
import { NavLink } from "react-router-dom"; import { NavLink } from 'react-router-dom';
import closeIcon from "../assets/icons/close.svg"; import closeIcon from '../assets/icons/close.svg';
import homeIcon from "../assets/icons/home.svg"; import homeIcon from '../assets/icons/home.svg';
import cacheIcon from "../assets/icons/cache.svg"; import cacheIcon from '../assets/icons/cache.svg';
import apiIcon from "../assets/icons/api.svg"; import apiIcon from '../assets/icons/api.svg';
import testPlayerIcon from "../assets/icons/testplayer.svg"; import testPlayerIcon from '../assets/icons/testplayer.svg';
import accountIcon from "../assets/icons/account.svg"; import accountIcon from '../assets/icons/account.svg';
import discordIcon from "../assets/icons/discord.svg"; import discordIcon from '../assets/icons/discord.svg';
import telegramIcon from "../assets/icons/telegram.svg"; import telegramIcon from '../assets/icons/telegram.svg';
import giteaIcon from "../assets/icons/gitea.svg"; import giteaIcon from '../assets/icons/gitea.svg';
function SideMenu({ isMenuOpen, setIsMenuOpen }) { function SideMenu({ isMenuOpen, setIsMenuOpen }) {
const [externalLinks, setExternalLinks] = useState({ const [externalLinks, setExternalLinks] = useState({
discord: "#", discord: '#',
telegram: "#", telegram: '#',
gitea: "#", gitea: '#',
}); });
useEffect(() => { useEffect(() => {
fetch("/api/links") fetch('/api/links')
.then((res) => res.json()) .then((res) => res.json())
.then((data) => setExternalLinks(data)) .then((data) => setExternalLinks(data))
.catch((err) => console.error("Failed to fetch links:", err)); .catch((err) => console.error('Failed to fetch links:', err));
}, []); }, []);
return ( return (
<div <div
className={`flex flex-col fixed top-0 left-0 w-full h-full bg-black transition-transform transform ${ 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" isMenuOpen ? 'translate-x-0' : '-translate-x-full'
} z-50`} } z-50`}
style={{ transitionDuration: "0.3s" }} style={{ transitionDuration: '0.3s' }}
> >
<div className="flex flex-col bg-gray-950/55 h-full"> <div className="flex flex-col bg-gray-950/55 h-full">
{/* Header */} {/* Header */}
<div className="h-16 w-full border-b-2 border-white/5 flex flex-row"> <div className="h-16 w-full border-b-2 border-white/5 flex flex-row">
<div className="w-1/4 h-full"></div> <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"> <p className="grow text-white md:text-2xl font-bold text-center flex items-center justify-center p-4 flex-col">
CDRM-Project CDRM-Project<br />
</p> <span className="text-sm">Github Edition</span>
<div className="w-1/4 h-full"> </p>
<button <div className="w-1/4 h-full">
className="w-full h-full flex items-center justify-center" <button
onClick={() => setIsMenuOpen(false)} className="w-full h-full flex items-center justify-center"
> onClick={() => setIsMenuOpen(false)}
<img >
src={closeIcon} <img src={closeIcon} alt="Close" className="w-1/2 h-1/2 cursor-pointer" />
alt="Close" </button>
className="w-1/2 h-1/2 cursor-pointer" </div>
/>
</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> </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; export default SideMenu;

View File

@ -1,37 +1,10 @@
@import "tailwindcss"; @import "tailwindcss";
@plugin "daisyui";
@plugin "daisyui/theme" {
name: "dim";
default: true;
--radius-selector: 0.5rem;
--radius-field: 0.5rem;
--radius-box: 0.5rem;
--size-selector: 0.25rem;
--size-field: 0.25rem;
--depth: 0;
--noise: 0;
}
:root {
--font-sans:
"Inter", system-ui, -apple-system, Roboto, "Segoe UI", "Helvetica Neue", "Noto Sans",
Oxygen, Ubuntu, Cantarell, "Open Sans", Arial, sans-serif;
font-family: var(--font-sans);
}
/* Force Sonner toast to use Inter first */
[data-sonner-toast],
.sonner-toast,
:where([data-sonner-toast]) :where([data-title]) :where([data-description]) {
font-family: var(--font-sans) !important;
}
details summary::-webkit-details-marker { details summary::-webkit-details-marker {
display: none; display: none;
} }
details summary { details summary {
list-style: none; list-style: none;
cursor: pointer; cursor: pointer;
} }

View File

@ -1,22 +1,13 @@
import { StrictMode } from "react"; import { StrictMode } from 'react'
import { createRoot } from "react-dom/client"; import { createRoot } from 'react-dom/client'
import { BrowserRouter } from "react-router-dom"; import { BrowserRouter } from 'react-router-dom'
import { Toaster } from "sonner"; import './index.css'
import App from "./App.jsx"; import App from './App.jsx'
import "./assets/fonts/font-face.css";
import "./index.css";
createRoot(document.getElementById("root")).render( createRoot(document.getElementById('root')).render(
<StrictMode> <StrictMode>
<BrowserRouter> <BrowserRouter>
<App /> <App />
<Toaster </BrowserRouter>
richColors </StrictMode>
className="flex justify-center" )
position="bottom-center"
duration="7000"
theme="dark"
/>
</BrowserRouter>
</StrictMode>
);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,46 +1,31 @@
"""Module to decrypt the license using the API."""
import base64
import ast
import glob
import json
import os
import re
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.cdm import Cdm as widevineCdm
from pywidevine.device import Device as widevineDevice from pywidevine.device import Device as widevineDevice
from pywidevine.pssh import PSSH as widevinePSSH from pywidevine.pssh import PSSH as widevinePSSH
from pyplayready.cdm import Cdm as playreadyCdm from pyplayready.cdm import Cdm as playreadyCdm
from pyplayready.device import Device as playreadyDevice from pyplayready.device import Device as playreadyDevice
from pyplayready.system.pssh import PSSH as playreadyPSSH 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): def find_license_key(data, keywords=None):
"""Find the license key in the data."""
if keywords is None: if keywords is None:
keywords = [ keywords = ["license", "licenseData", "widevine2License"] # Default list of keywords to search for
"license",
"licenseData",
"widevine2License",
] # Default list of keywords to search for
# If the data is a dictionary, check each key # If the data is a dictionary, check each key
if isinstance(data, dict): if isinstance(data, dict):
for key, value in data.items(): for key, value in data.items():
if any( if any(keyword in key.lower() for keyword in
keyword in key.lower() for keyword in keywords keywords): # Check if any keyword is in the key (case-insensitive)
): # Check if any keyword is in the key (case-insensitive) return value.replace("-", "+").replace("_", "/") # Return the value immediately when found
return value.replace("-", "+").replace(
"_", "/"
) # Return the value immediately when found
# Recursively check if the value is a dictionary or list # Recursively check if the value is a dictionary or list
if isinstance(value, (dict, list)): if isinstance(value, (dict, list)):
result = find_license_key(value, keywords) # Recursively search result = find_license_key(value, keywords) # Recursively search
@ -58,52 +43,37 @@ def find_license_key(data, keywords=None):
def find_license_challenge(data, keywords=None, new_value=None): def find_license_challenge(data, keywords=None, new_value=None):
"""Find the license challenge in the data."""
if keywords is None: if keywords is None:
keywords = [ keywords = ["license", "licenseData", "widevine2License", "licenseRequest"] # Default list of keywords to search for
"license",
"licenseData",
"widevine2License",
"licenseRequest",
] # Default list of keywords to search for
# If the data is a dictionary, check each key # If the data is a dictionary, check each key
if isinstance(data, dict): if isinstance(data, dict):
for key, value in data.items(): for key, value in data.items():
if any( if any(keyword in key.lower() for keyword in keywords): # Check if any keyword is in the key (case-insensitive)
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 data[key] = new_value # Modify the value in-place
# Recursively check if the value is a dictionary or list # Recursively check if the value is a dictionary or list
elif isinstance(value, (dict, list)): elif isinstance(value, (dict, list)):
find_license_challenge( find_license_challenge(value, keywords, new_value) # Recursively modify in place
value, keywords, new_value
) # Recursively modify in place
# If the data is a list, iterate through each item # If the data is a list, iterate through each item
elif isinstance(data, list): elif isinstance(data, list):
for i, item in enumerate(data): for i, item in enumerate(data):
result = find_license_challenge( result = find_license_challenge(item, keywords, new_value) # Recursively modify in place
item, keywords, new_value
) # Recursively modify in place
return data # Return the modified original data (no new structure is created) return data # Return the modified original data (no new structure is created)
def is_base64(string): def is_base64(string):
"""Check if the string is base64 encoded."""
try: try:
# Try decoding the string # Try decoding the string
decoded_data = base64.b64decode(string) decoded_data = base64.b64decode(string)
# Check if the decoded data, when re-encoded, matches the original string # Check if the decoded data, when re-encoded, matches the original string
return base64.b64encode(decoded_data).decode("utf-8") == string return base64.b64encode(decoded_data).decode('utf-8') == string
except (binascii.Error, TypeError): except Exception:
# If decoding or encoding fails, it's not Base64 # If decoding or encoding fails, it's not Base64
return False return False
def is_url_and_split(input_str): 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) parsed = urlparse(input_str)
# Check if it's a valid URL with scheme and netloc # Check if it's a valid URL with scheme and netloc
@ -111,300 +81,364 @@ def is_url_and_split(input_str):
protocol = parsed.scheme protocol = parsed.scheme
fqdn = parsed.netloc fqdn = parsed.netloc
return True, protocol, fqdn return True, protocol, fqdn
return False, None, None
def sanitize_username(username):
"""Sanitize the username."""
return re.sub(r"[^a-zA-Z0-9_\-]", "_", username).lower()
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: else:
ext, config_key, class_loader = ".wvd", "default_wv_cdm", widevineDevice.load return False, None, None
base_dir = "WV"
if device == "public": 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):
base_name = config[config_key] print(f'Using device {device} for user {username}')
if not base_name.endswith(ext): with open(f'{os.getcwd()}/configs/config.yaml', 'r') as file:
base_name += ext
search_path = os.path.join(os.getcwd(), "configs", "CDMs", base_dir, base_name)
else:
base_name = device
if not base_name.endswith(ext):
base_name += ext
safe_username = sanitize_username(username)
search_path = os.path.join(
os.getcwd(),
"configs",
"CDMs",
"users_uploaded",
safe_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:
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:
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) config = yaml.safe_load(file)
if config['database_type'].lower() == 'sqlite':
if pssh == "": from custom_functions.database.cache_to_db_sqlite import cache_to_db
return {"status": "error", "message": "No PSSH provided"} elif config['database_type'].lower() == 'mariadb':
from custom_functions.database.cache_to_db_mariadb import cache_to_db
# Detect PlayReady or Widevine if pssh is None:
try:
is_pr = "</WRMHEADER>".encode("utf-16-le") in base64.b64decode(pssh)
except (binascii.Error, TypeError) as error:
return { return {
"status": "error", 'status': 'error',
"message": f"An error occurred processing PSSH\n\n{error}", 'message': 'No PSSH provided'
} }
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: try:
cdm = cdm_class.from_device(device_obj) if "</WRMHEADER>".encode("utf-16-le") in base64.b64decode(pssh): # PR
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": "Your proxy is invalid, please put it in the format of http(s)://fqdn.tld:port",
}
# 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: try:
cdm.parse_license(session_id, licence.content) # type: ignore[arg-type] pr_pssh = playreadyPSSH(pssh)
except (ValueError, TypeError): except Exception as error:
# Try to extract license from JSON return {
try: 'status': 'error',
license_json = licence.json() 'message': f'An error occurred processing PSSH\n\n{error}'
license_value = find_license_key(license_json) }
if license_value is not None: try:
cdm.parse_license(session_id, license_value) 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: else:
return { return {
"status": "error", 'status': 'error',
"message": f"Could not extract license from JSON: {license_json}", 'message': 'No default .prd file found'
} }
except (ValueError, json.JSONDecodeError, AttributeError) as error: 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 { return {
"status": "error", 'status': 'error',
"message": f"An error occurred parsing license content\n\n{error}\n\n{licence.content}", 'message': f'Your proxy is invalid, please put it in the format of http(s)://fqdn.tld:port'
} }
except (ValueError, TypeError, AttributeError) as error: 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 { return {
"status": "error", 'status': 'error',
"message": f"An error occurred parsing license content\n\n{error}\n\n{licence.content}", 'message': f'An error occurred processing PSSH\n\n{error}'
} }
else:
# Extract and cache keys try:
returned_keys, key_err = extract_and_cache_keys( wv_pssh = widevinePSSH(pssh)
cdm, except Exception as error:
session_id, return {
cache_to_db, 'status': 'error',
pssh, 'message': f'An error occurred processing PSSH\n\n{error}'
license_url, }
headers, try:
cookies, if device == 'public':
challenge, base_name = config["default_wv_cdm"]
json_data, if not base_name.endswith(".wvd"):
is_widevine=(not is_pr), base_name += ".wvd"
) wvd_files = glob.glob(f'{os.getcwd()}/configs/CDMs/WV/{base_name}')
if returned_keys == "": else:
return {"status": "error", "message": key_err} wvd_files = glob.glob(f'{os.getcwd()}/configs/CDMs/WV/{base_name}')
if wvd_files:
# Close session wv_device = widevineDevice.load(wvd_files[0])
try: else:
cdm.close(session_id) return {
except (IOError, ValueError, AttributeError) as error: 'status': 'error',
return { 'message': 'No default .wvd file found'
"status": "error", }
"message": f"An error occurred closing session\n\n{error}", else:
} base_name = device
if not base_name.endswith(".wvd"):
return {"status": "success", "message": returned_keys} 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:
return {
'status': 'error',
'message': f'An error occurred location Widevine CDM file\n\n{error}'
}
try:
wv_cdm = widevineCdm.from_device(wv_device)
except Exception as error:
return {
'status': 'error',
'message': f'An error occurred loading Widevine CDM\n\n{error}'
}
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:
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"
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}'
}

View File

@ -1,106 +1,38 @@
"""Module to check for and download CDM files."""
import os import os
import sys
import yaml import yaml
import requests import requests
CONFIG_PATH = os.path.join(os.getcwd(), "configs", "config.yaml")
def load_config(): def check_for_wvd_cdm():
"""Load the config file.""" with open(f'{os.getcwd()}/configs/config.yaml', 'r') as file:
with open(CONFIG_PATH, "r", encoding="utf-8") as file: config = yaml.safe_load(file)
return yaml.safe_load(file) if config['default_wv_cdm'] == '':
exit(f"Please put the name of your Widevine CDM inside of {os.getcwd()}/configs/config.yaml")
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:
file_path = os.path.join(cdm_dir_path, f"public.{file_ext}")
with open(file_path, "wb") as file:
file.write(response.content)
config[config_key] = "public"
save_config(config)
print(f"Successfully downloaded {cdm_name} CDM")
else:
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:
sys.exit(
f"Place a .{file_ext} file in {cdm_dir_path} and specify the name in {CONFIG_PATH}"
)
else: else:
base_name = ( base_name = config["default_wv_cdm"]
cdm_value if not base_name.endswith(".wvd"):
if cdm_value.endswith(f".{file_ext}") base_name += ".wvd"
else f"{cdm_value}.{file_ext}" if os.path.exists(f'{os.getcwd()}/configs/CDMs/WV/{base_name}'):
)
file_path = os.path.join(cdm_dir_path, base_name)
if os.path.exists(file_path):
return 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(file_path, "wb") as file:
file.write(response.content)
config[config_key] = base_name.replace(f".{file_ext}", "")
save_config(config)
print(f"Successfully downloaded {cdm_name} CDM")
else:
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: else:
sys.exit( exit(f"Widevine CDM {base_name} does not exist in {os.getcwd()}/configs/CDMs/WV")
f"Place a .{file_ext} file in {cdm_dir_path} and specify the name in {CONFIG_PATH}"
) 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'] == '':
exit(f"Please put the name of your PlayReady CDM inside of {os.getcwd()}/configs/config.yaml")
else:
base_name = config["default_pr_cdm"]
if not base_name.endswith(".prd"):
base_name += ".prd"
if os.path.exists(f'{os.getcwd()}/configs/CDMs/PR/{base_name}'):
return
else:
exit(f"PlayReady CDM {base_name} does not exist in {os.getcwd()}/configs/CDMs/WV")
def check_for_cdms(): def check_for_cdms():
"""Check for CDM files.""" check_for_wvd_cdm()
check_for_cdm( check_for_prd_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,13 +1,10 @@
"""Module to check for the config file."""
import os import os
def check_for_config_file(): def check_for_config_file():
"""Check for the config file.""" if os.path.exists(f'{os.getcwd()}/configs/config.yaml'):
if os.path.exists(os.path.join(os.getcwd(), "configs", "config.yaml")):
return return
default_config = """ else:
default_config = """\
default_wv_cdm: '' default_wv_cdm: ''
default_pr_cdm: '' default_pr_cdm: ''
secret_key_flask: 'secretkey' secret_key_flask: 'secretkey'
@ -24,8 +21,6 @@ remote_cdm_secret: ''
# port: '' # port: ''
# database: '' # database: ''
""" """
with open( with open(f'{os.getcwd()}/configs/config.yaml', 'w') as f:
os.path.join(os.getcwd(), "configs", "config.yaml"), "w", encoding="utf-8"
) as f:
f.write(default_config) f.write(default_config)
return return

View File

@ -1,159 +1,37 @@
"""Module to check for the database with unified backend support."""
import os import os
from typing import Dict, Any
import yaml import yaml
from custom_functions.database.unified_db_ops import ( def check_for_sqlite_database():
db_ops, with open(f'{os.getcwd()}/configs/config.yaml', 'r') as file:
get_backend_info, config = yaml.safe_load(file)
key_count, if os.path.exists(f'{os.getcwd()}/databases/key_cache.db'):
) return
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:
print(f"SQLite database found at: {sqlite_path}")
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()
print(f"User database created at: {user_db_path}")
else: else:
print(f"User database found at: {user_db_path}") if config['database_type'].lower() != 'mariadb':
from custom_functions.database.cache_to_db_sqlite import create_database
create_database()
return
else:
return
def check_for_user_database():
if os.path.exists(f'{os.getcwd()}/databases/users.db'):
return
else:
from custom_functions.database.user_db import create_user_database
create_user_database()
def check_for_sql_database() -> None: def check_for_mariadb_database():
"""Check for the SQL database based on configuration.""" with open(f'{os.getcwd()}/configs/config.yaml', 'r') as file:
print("=== Database Check Starting ===") 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
else:
return
# Get backend information def check_for_sql_database():
backend_info = get_backend_info() check_for_sqlite_database()
print(f"Database backend: {backend_info['backend']}") check_for_mariadb_database()
print(f"Using module: {backend_info['module']}") check_for_user_database()
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,53 +1,44 @@
"""Module to check for the folders."""
import os import os
def check_for_config_folder(): def check_for_config_folder():
"""Check for the config folder.""" if os.path.isdir(f'{os.getcwd()}/configs'):
if os.path.isdir(os.path.join(os.getcwd(), "configs")): return
else:
os.mkdir(f'{os.getcwd()}/configs')
return return
os.mkdir(os.path.join(os.getcwd(), "configs"))
return
def check_for_database_folder(): def check_for_database_folder():
"""Check for the database folder.""" if os.path.isdir(f'{os.getcwd()}/databases'):
if os.path.isdir(os.path.join(os.getcwd(), "databases")): return
else:
os.mkdir(f'{os.getcwd()}/databases')
os.mkdir(f'{os.getcwd()}/databases/sql')
return return
os.mkdir(os.path.join(os.getcwd(), "databases"))
os.mkdir(os.path.join(os.getcwd(), "databases", "sql"))
return
def check_for_cdm_folder(): def check_for_cdm_folder():
"""Check for the CDM folder.""" if os.path.isdir(f'{os.getcwd()}/configs/CDMs'):
if os.path.isdir(os.path.join(os.getcwd(), "configs", "CDMs")): return
else:
os.mkdir(f'{os.getcwd()}/configs/CDMs')
return return
os.mkdir(os.path.join(os.getcwd(), "configs", "CDMs"))
return
def check_for_wv_cdm_folder(): def check_for_wv_cdm_folder():
"""Check for the Widevine CDM folder.""" if os.path.isdir(f'{os.getcwd()}/configs/CDMs/WV'):
if os.path.isdir(os.path.join(os.getcwd(), "configs", "CDMs", "WV")): return
else:
os.mkdir(f'{os.getcwd()}/configs/CDMs/WV')
return return
os.mkdir(os.path.join(os.getcwd(), "configs", "CDMs", "WV"))
return
def check_for_cdm_pr_folder(): def check_for_cdm_pr_folder():
"""Check for the PlayReady CDM folder.""" if os.path.isdir(f'{os.getcwd()}/configs/CDMs/PR'):
if os.path.isdir(os.path.join(os.getcwd(), "configs", "CDMs", "PR")): return
else:
os.mkdir(f'{os.getcwd()}/configs/CDMs/PR')
return return
os.mkdir(os.path.join(os.getcwd(), "configs", "CDMs", "PR"))
return
def folder_checks(): def folder_checks():
"""Check for the folders."""
check_for_config_folder() check_for_config_folder()
check_for_database_folder() check_for_database_folder()
check_for_cdm_folder() check_for_cdm_folder()
check_for_wv_cdm_folder() check_for_wv_cdm_folder()
check_for_cdm_pr_folder() check_for_cdm_pr_folder()

View File

@ -1,29 +1,11 @@
"""Module to run the prechecks."""
import os
import subprocess
from custom_functions.prechecks.folder_checks import folder_checks 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.config_file_checks import check_for_config_file
from custom_functions.prechecks.database_checks import check_for_sql_database from custom_functions.prechecks.database_checks import check_for_sql_database
from custom_functions.prechecks.cdm_checks import check_for_cdms 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(): def run_precheck():
"""Run the prechecks."""
check_frontend_built()
folder_checks() folder_checks()
check_for_config_file() check_for_config_file()
check_for_cdms() check_for_cdms()
check_for_sql_database() check_for_sql_database()
return

View File

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

View File

@ -1,28 +1,12 @@
"""Module to check if the user is allowed to use the device."""
import os import os
import glob import glob
import re
def sanitize_username(username):
"""Sanitize the username."""
return re.sub(r"[^a-zA-Z0-9_\-]", "_", username).lower()
def user_allowed_to_use_device(device, username): def user_allowed_to_use_device(device, username):
"""Check if the user is allowed to use the device.""" base_path = os.path.join(os.getcwd(), 'configs', 'CDMs', username)
base_path = os.path.join(
os.getcwd(), "configs", "CDMs", "users_uploaded", sanitize_username(username)
)
# Get filenames with extensions # Get filenames with extensions
pr_files = [ pr_files = [os.path.basename(f) for f in glob.glob(os.path.join(base_path, 'PR', '*.prd'))]
os.path.basename(f) for f in glob.glob(os.path.join(base_path, "PR", "*.prd")) wv_files = [os.path.basename(f) for f in glob.glob(os.path.join(base_path, 'WV', '*.wvd'))]
]
wv_files = [
os.path.basename(f) for f in glob.glob(os.path.join(base_path, "WV", "*.wvd"))
]
# Combine all filenames # Combine all filenames
all_files = pr_files + wv_files all_files = pr_files + wv_files
@ -30,4 +14,4 @@ def user_allowed_to_use_device(device, username):
# Check if filename matches directly or by adding extensions # Check if filename matches directly or by adding extensions
possible_names = {device, f"{device}.prd", f"{device}.wvd"} 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)

30
main.py
View File

@ -1,10 +1,9 @@
"""Main module to run the application.""" from custom_functions.prechecks.python_checks import run_python_checks
run_python_checks()
import os from custom_functions.prechecks.precheck import run_precheck
import yaml run_precheck()
from flask import Flask from flask import Flask
from flask_cors import CORS from flask_cors import CORS
from routes.react import react_bp from routes.react import react_bp
from routes.api import api_bp from routes.api import api_bp
from routes.remote_device_wv import remotecdm_wv_bp from routes.remote_device_wv import remotecdm_wv_bp
@ -13,20 +12,12 @@ from routes.upload import upload_bp
from routes.user_info import user_info_bp from routes.user_info import user_info_bp
from routes.register import register_bp from routes.register import register_bp
from routes.login import login_bp from routes.login import login_bp
from routes.user_changes import user_change_bp import os
from custom_functions.prechecks.python_checks import run_python_checks import yaml
from custom_functions.prechecks.precheck import run_precheck
run_python_checks()
run_precheck()
app = Flask(__name__) app = Flask(__name__)
with open( with open(f'{os.getcwd()}/configs/config.yaml', 'r') as file:
os.path.join(os.getcwd(), "configs", "config.yaml"), "r", encoding="utf-8"
) as file:
config = yaml.safe_load(file) config = yaml.safe_load(file)
app.secret_key = config["secret_key_flask"] app.secret_key = config['secret_key_flask']
CORS(app) CORS(app)
@ -39,7 +30,6 @@ app.register_blueprint(user_info_bp)
app.register_blueprint(upload_bp) app.register_blueprint(upload_bp)
app.register_blueprint(remotecdm_wv_bp) app.register_blueprint(remotecdm_wv_bp)
app.register_blueprint(remotecdm_pr_bp) app.register_blueprint(remotecdm_pr_bp)
app.register_blueprint(user_change_bp)
if __name__ == "__main__": if __name__ == '__main__':
app.run(debug=True, host="0.0.0.0") app.run(debug=True, host='0.0.0.0')

View File

@ -1,19 +0,0 @@
[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,10 +1,9 @@
Flask Flask~=3.1.0
Flask-Cors Flask-Cors
pywidevine~=1.8.0 pywidevine~=1.8.0
pyplayready~=0.6.0 pyplayready~=0.6.0
requests requests~=2.32.3
protobuf~=4.25.6 protobuf~=4.25.6
PyYAML PyYAML~=6.0.2
mysql-connector-python mysql-connector-python
bcrypt bcrypt
black

View File

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

View File

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

View File

@ -1,48 +1,33 @@
"""Module to handle the React routes."""
import os
import sys import sys
import os
from flask import Blueprint, send_from_directory, render_template from flask import Blueprint, send_from_directory, request, render_template
from configs import index_tags from configs import index_tags
if getattr(sys, "frozen", False): # Running as a bundled app if getattr(sys, 'frozen', False): # Running as a bundled app
base_path = getattr(sys, "_MEIPASS", os.path.abspath(".")) base_path = sys._MEIPASS
else: # Running in a normal Python environment else: # Running in a normal Python environment
base_path = os.path.abspath(".") base_path = os.path.abspath(".")
static_folder = os.path.join(base_path, "frontend-dist") static_folder = os.path.join(base_path, 'cdrm-frontend', 'dist')
react_bp = Blueprint( react_bp = Blueprint(
"react_bp", 'react_bp',
__name__, __name__,
static_folder=static_folder, static_folder=static_folder,
static_url_path="/", static_url_path='/',
template_folder=static_folder, template_folder=static_folder
) )
@react_bp.route('/', methods=['GET'])
@react_bp.route("/", methods=["GET"]) @react_bp.route('/<path:path>', methods=["GET"])
@react_bp.route("/<path:path>", methods=["GET"]) @react_bp.route('/<path>', methods=["GET"])
@react_bp.route("/<path>", methods=["GET"]) def index(path=''):
def index(path=""): if request.method == 'GET':
"""Handle the index route.""" file_path = os.path.join(react_bp.static_folder, path)
# Ensure static_folder is not None if path != "" and os.path.exists(file_path):
if react_bp.static_folder is None: return send_from_directory(react_bp.static_folder, path)
raise ValueError("Static folder is not configured for the blueprint") elif path.lower() in ['', 'cache', 'api', 'testplayer', 'account']:
data = index_tags.tags.get(path.lower(), index_tags.tags['index'])
# Normalize the path to prevent directory traversal return render_template('index.html', data=data)
safe_path = os.path.normpath(path) else:
file_path = os.path.join(react_bp.static_folder, safe_path) return send_from_directory(react_bp.static_folder, 'index.html')
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,56 +1,29 @@
"""Module to handle the register process."""
import re
import uuid
from flask import Blueprint, request, jsonify from flask import Blueprint, request, jsonify
from custom_functions.database.user_db import add_user from custom_functions.database.user_db import add_user
register_bp = Blueprint("register_bp", __name__) register_bp = Blueprint(
'register_bp',
__name__,
)
USERNAME_REGEX = re.compile(r"^[A-Za-z0-9_-]+$") @register_bp.route('/register', methods=['POST'])
PASSWORD_REGEX = re.compile(r"^\S+$")
@register_bp.route("/register", methods=["POST"])
def register(): def register():
"""Handle the register process.""" if request.method == 'POST':
data = request.get_json() data = request.get_json()
if data is None: for required_field in ['username', 'password']:
return jsonify({"error": "Invalid or missing JSON in request body."}), 400 if required_field not in data:
return jsonify({
# Check required fields 'error': f'Missing required field: {required_field}'
for required_field in ["username", "password"]: })
if required_field not in data: if add_user(data['username'], data['password']):
return jsonify({"error": f"Missing required field: {required_field}"}), 400 return jsonify({
'message': 'User successfully registered!'
username = data["username"].lower() })
password = data["password"] else:
api_key = str(uuid.uuid4()) return jsonify({
'error': 'User already exists!'
# Length checks })
if not (3 <= len(username) <= 32): else:
return jsonify({"error": "Username must be 3-32 characters."}), 400 return jsonify({
if not (8 <= len(password) <= 128): 'error': 'Method not supported'
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,
)
if not PASSWORD_REGEX.fullmatch(password):
return jsonify({"error": "Invalid password. Spaces are not allowed."}), 400
# Attempt to add user
if add_user(username, password, api_key):
return (
jsonify({"message": "User successfully registered!", "api_key": api_key}),
201,
)
return jsonify({"error": "User already exists!"}), 409

View File

@ -1,346 +1,200 @@
"""Module to handle the remote device PlayReady."""
import base64
import os
from pathlib import Path
import re
import yaml
from flask import Blueprint, jsonify, request, current_app, Response from flask import Blueprint, jsonify, request, current_app, Response
import os
import yaml
from pyplayready.device import Device as PlayReadyDevice from pyplayready.device import Device as PlayReadyDevice
from pyplayready.cdm import Cdm as PlayReadyCDM from pyplayready.cdm import Cdm as PlayReadyCDM
from pyplayready import PSSH as PlayReadyPSSH from pyplayready import PSSH as PlayReadyPSSH
from pyplayready.exceptions import ( from pyplayready.exceptions import (InvalidSession, TooManySessions, InvalidLicense, InvalidPssh)
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
remotecdm_pr_bp = Blueprint("remotecdm_pr", __name__)
with open( remotecdm_pr_bp = Blueprint('remotecdm_pr', __name__)
os.path.join(os.getcwd(), "configs", "config.yaml"), "r", encoding="utf-8" with open(f'{os.getcwd()}/configs/config.yaml', 'r') as file:
) as file:
config = yaml.safe_load(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(): def remote_cdm_playready():
"""Handle the remote device PlayReady.""" if request.method == 'GET':
if request.method == "GET": return jsonify({
return make_response( 'message': 'OK'
"Success", })
"OK", if request.method == 'HEAD':
http_status=200,
)
if request.method == "HEAD":
response = Response(status=200) response = Response(status=200)
response.headers["Server"] = "playready serve" response.headers['Server'] = 'playready serve'
return response 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(): def remote_cdm_playready_deviceinfo():
"""Handle the remote device PlayReady device info."""
base_name = config["default_pr_cdm"] base_name = config["default_pr_cdm"]
device = PlayReadyDevice.load( if not base_name.endswith(".prd"):
os.path.join(os.getcwd(), "configs", "CDMs", "PR", base_name + ".prd") full_file_name = (base_name + ".prd")
) device = PlayReadyDevice.load(f'{os.getcwd()}/configs/CDMs/PR/{full_file_name}')
cdm = PlayReadyCDM.from_device(device) cdm = PlayReadyCDM.from_device(device)
return jsonify( return jsonify({
{ 'security_level': cdm.security_level,
"security_level": cdm.security_level, 'host': f'{config["fqdn"]}/remotecdm/playready',
"host": f'{config["fqdn"]}/remotecdm/playready', 'secret': f'{config["remote_cdm_secret"]}',
"secret": f'{config["remote_cdm_secret"]}', 'device_name': f'{base_name}'
"device_name": Path(base_name).stem, })
}
)
@remotecdm_pr_bp.route('/remotecdm/playready/<device>/open', 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):
"""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"])
def remote_cdm_playready_open(device): def remote_cdm_playready_open(device):
"""Handle the remote device PlayReady open.""" if str(device).lower() == config['default_pr_cdm'].lower():
unauthorized_msg = { pr_device = PlayReadyDevice.load(f'{os.getcwd()}/configs/CDMs/PR/{config["default_pr_cdm"]}.prd')
"message": f"Device '{device}' is not found or you are not authorized to use it." cdm = current_app.config['CDM'] = PlayReadyCDM.from_device(pr_device)
}
# 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() session_id = cdm.open()
return make_response( return jsonify({
"Success", 'message': 'Success',
"Successfully opened the PlayReady CDM session", 'data': {
{ 'session_id': session_id.hex(),
"session_id": session_id.hex(), 'device': {
"device": {"security_level": cdm.security_level}, 'security_level': cdm.security_level
}, }
http_status=200, }
) })
# User device logic @remotecdm_pr_bp.route('/remotecdm/playready/<device>/close/<session_id>', methods=['GET'])
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): def remote_cdm_playready_close(device, session_id):
"""Handle the remote device PlayReady close.""" if str(device).lower() == config['default_pr_cdm'].lower():
try:
session_id = bytes.fromhex(session_id) session_id = bytes.fromhex(session_id)
cdm = get_cdm_or_error(device) cdm = current_app.config["CDM"]
if isinstance(cdm, tuple): # error response if not cdm:
return cdm return jsonify({
'status': 400,
'message': f'No CDM for "{device}" has been opened yet. No session to close'
})
try: try:
cdm.close(session_id) cdm.close(session_id)
except InvalidSession: except InvalidSession:
return make_response( return jsonify({
"Error", 'status': 400,
f'Invalid session ID "{session_id.hex()}", it may have expired', 'message': f'Invalid session ID "{session_id.hex()}", it may have expired'
http_status=400, })
) return jsonify({
return make_response( 'status': 200,
"Success", 'message': f'Successfully closed Session "{session_id.hex()}".',
f'Successfully closed Session "{session_id.hex()}".', })
http_status=200, else:
) return jsonify({
except Exception as error: 'status': 400,
return make_response( 'message': f'Unauthorized'
"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): def remote_cdm_playready_get_license_challenge(device):
"""Handle the remote device PlayReady get license challenge.""" if str(device).lower() == config['default_pr_cdm'].lower():
body = request.get_json() body = request.get_json()
missing_field = check_required_fields(body, ("session_id", "init_data")) for required_field in ("session_id", "init_data"):
if missing_field: if not body.get(required_field):
return missing_field return jsonify({
cdm = get_cdm_or_error(device) 'status': 400,
if isinstance(cdm, tuple): # error response 'message': f'Missing required field "{required_field}" in JSON body'
return cdm })
session_id = bytes.fromhex(body["session_id"]) cdm = current_app.config["CDM"]
init_data = body["init_data"] session_id = bytes.fromhex(body["session_id"])
if not init_data.startswith("<WRMHEADER"): init_data = body["init_data"]
if not init_data.startswith("<WRMHEADER"):
try:
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}'
})
try: try:
pssh = PlayReadyPSSH(init_data) license_request = cdm.get_license_challenge(
if pssh.wrm_headers: session_id=session_id,
init_data = pssh.wrm_headers[0] wrm_header=init_data
except InvalidPssh as error:
return make_response(
"Error",
f"Unable to parse base64 PSSH, {error}",
http_status=400,
) )
try: except InvalidSession:
license_request = cdm.get_license_challenge( return jsonify({
session_id=session_id, wrm_header=init_data 'message': f"Invalid Session ID '{session_id.hex()}', it may have expired."
) })
except InvalidSession: except Exception as e:
return make_response( return jsonify({
"Error", 'message': f'Error, {e}'
f"Invalid Session ID '{session_id.hex()}', it may have expired.", })
http_status=400, return jsonify({
) 'message': 'success',
except ValueError as error: 'data': {
return make_response( 'challenge': license_request
"Error", }
f"Invalid License, {error}", })
http_status=400,
)
return make_response(
"Success",
"Successfully got the License Challenge",
{"challenge_b64": base64.b64encode(license_request.encode("utf-8")).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): def remote_cdm_playready_parse_license(device):
"""Handle the remote device PlayReady parse license.""" if str(device).lower() == config['default_pr_cdm'].lower():
body = request.get_json() body = request.get_json()
missing_field = check_required_fields(body, ("license_message", "session_id")) for required_field in ("license_message", "session_id"):
if missing_field: if not body.get(required_field):
return missing_field return jsonify({
cdm = get_cdm_or_error(device) 'message': f'Missing required field "{required_field}" in JSON body'
if isinstance(cdm, tuple): # error response })
return cdm cdm = current_app.config["CDM"]
session_id = bytes.fromhex(body["session_id"]) if not cdm:
license_message = body["license_message"] return jsonify({
if is_base64(license_message): 'message': f"No Cdm session for {device} has been opened yet. No session to use."
license_message = base64.b64decode(license_message).decode("utf-8") })
try: session_id = bytes.fromhex(body["session_id"])
cdm.parse_license(session_id, license_message) license_message = body["license_message"]
except InvalidSession: try:
return make_response( cdm.parse_license(session_id, license_message)
"Error", except InvalidSession:
f"Invalid Session ID '{session_id.hex()}', it may have expired.", return jsonify({
http_status=400, 'message': f"Invalid Session ID '{session_id.hex()}', it may have expired."
) })
except InvalidLicense as e: except InvalidLicense as e:
return make_response( return jsonify({
"Error", 'message': f"Invalid License, {e}"
f"Invalid License, {e}", })
http_status=400, except Exception as e:
) return jsonify({
except Exception as e: 'message': f"Error, {e}"
return make_response( })
"Error", return jsonify({
f"Error, {e}", 'message': 'Successfully parsed and loaded the Keys from the License message'
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): def remote_cdm_playready_get_keys(device):
"""Handle the remote device PlayReady get keys.""" if str(device).lower() == config['default_pr_cdm'].lower():
body = request.get_json() body = request.get_json()
missing_field = check_required_fields(body, ("session_id",)) for required_field in ("session_id",):
if missing_field: if not body.get(required_field):
return missing_field return jsonify({
session_id = bytes.fromhex(body["session_id"]) 'message': f'Missing required field "{required_field}" in JSON body'
cdm = get_cdm_or_error(device) })
if isinstance(cdm, tuple): # error response session_id = bytes.fromhex(body["session_id"])
return cdm cdm = current_app.config["CDM"]
try: if not cdm:
keys = cdm.get_keys(session_id) return jsonify({
except InvalidSession: 'message': f"Missing required field '{required_field}' in JSON body."
return make_response( })
"Error", try:
f"Invalid Session ID '{session_id.hex()}', it may have expired.", keys = cdm.get_keys(session_id)
http_status=400, except InvalidSession:
) return jsonify({
keys_json = [ 'message': f"Invalid Session ID '{session_id.hex()}', it may have expired."
{ })
"key_id": key.key_id.hex, except Exception as e:
"key": key.key.hex(), return jsonify({
"type": key.key_type.value, 'message': f"Error, {e}"
"cipher_type": key.cipher_type.value, })
"key_length": key.key_length, keys_json = [
} {
for key in keys "key_id": key.key_id.hex,
] "key": key.key.hex(),
return make_response( "type": key.key_type.value,
"Success", "cipher_type": key.cipher_type.value,
"Successfully got the Keys", "key_length": key.key_length,
{"keys": keys_json}, }
http_status=200, for key in keys
) ]
return jsonify({
'message': 'success',
'data': {
'keys': keys_json
}
})

View File

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

View File

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

View File

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

View File

@ -1,61 +1,26 @@
"""Module to handle the user info request.""" from flask import Blueprint, request, jsonify, session
import os import os
import glob import glob
import logging import logging
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(): def user_info():
"""Handle the user info request.""" username = session.get('username')
username = session.get("username")
if not username: if not username:
try: return jsonify({'message': 'False'}), 400
headers = request.headers
api_key = headers["Api-Key"]
username = fetch_username_by_api_key(api_key)
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: try:
base_path = os.path.join( base_path = os.path.join(os.getcwd(), 'configs', 'CDMs', username)
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'))]
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( return jsonify({
{ 'Username': username,
"Username": username, 'Widevine_Devices': wv_files,
"Widevine_Devices": wv_files, 'Playready_Devices': pr_files
"Playready_Devices": pr_files, })
"API_Key": fetch_api_key(username),
"Styled_Username": fetch_styled_username(username),
}
)
except Exception as e: except Exception as e:
logging.exception("Error retrieving device files, %s", {e}) logging.exception("Error retrieving device files")
return jsonify({"message": "False"}), 500 return jsonify({'message': 'False'}), 500