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

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: { plugins: {
"react-hooks": reactHooks, 'react-hooks': reactHooks,
"react-refresh": reactRefresh, 'react-refresh': reactRefresh,
}, },
rules: { rules: {
...js.configs.recommended.rules, ...js.configs.recommended.rules,
...reactHooks.configs.recommended.rules, ...reactHooks.configs.recommended.rules,
"no-unused-vars": ["error", { varsIgnorePattern: "^[A-Z_]" }], 'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }],
"react-refresh/only-export-components": ["warn", { allowConstantExport: true }], 'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
}, },
}, },
]; ]

View File

@ -4,13 +4,13 @@
<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">

File diff suppressed because it is too large Load Diff

View File

@ -10,28 +10,24 @@
"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.31.0", "@eslint/js": "^9.22.0",
"@types/react": "^19.1.8", "@types/react": "^19.0.10",
"@types/react-dom": "^19.1.6", "@types/react-dom": "^19.0.4",
"@vitejs/plugin-react": "^4.7.0", "@vitejs/plugin-react": "^4.3.4",
"@vitejs/plugin-react-swc": "^3.11.0", "eslint": "^9.22.0",
"daisyui": "^5.0.46",
"eslint": "^9.31.0",
"eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.20", "eslint-plugin-react-refresh": "^0.4.19",
"globals": "^16.3.0", "globals": "^16.0.0",
"prettier-plugin-tailwindcss": "^0.6.14", "vite": "^6.3.1"
"vite": "^7.0.5"
} }
} }

View File

@ -1,12 +1,32 @@
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() {
const [isMenuOpen, setIsMenuOpen] = useState(false); // Track if the menu is open
return ( 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> <Routes>
<Route path="/" element={<Home />} /> <Route path="/" element={<Home />} />
<Route path="/cache" element={<Cache />} /> <Route path="/cache" element={<Cache />} />
@ -14,6 +34,9 @@ function App() {
<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>
); );
} }

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

@ -5,8 +5,8 @@ const { SignedMessage, LicenseRequest } = protobuf.roots.default.license_protoco
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);
} }
@ -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,22 +75,22 @@ 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
]); ]);
} }
@ -111,7 +105,7 @@ function encodeUtf16LE(str) {
} }
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() {
@ -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 ( return (
<li> <div className="flex flex-col w-full h-full bg-white/1">
<NavLink to={to} className={({ isActive }) => (isActive ? "menu-active" : "")}> {/* Header */}
{children} <div>
<p className="text-white text-2xl font-bold p-3 text-center mb-5">
<a href="/">CDRM-Project</a><br /><span className="text-sm">Github Edition</span>
</p>
</div>
{/* Scrollable navigation area */}
<div className="overflow-y-auto grow flex flex-col">
{/* Main NavLinks */}
<NavLink
to="/"
className={({ isActive }) =>
`flex flex-row p-3 border-l-3 ${
isActive
? 'border-l-sky-500/50 bg-black/50'
: 'hover:border-l-sky-500/50 hover:bg-white/5'
}`
}
>
<button className="w-1/3 p-3 flex flex-col items-center justify-center cursor-pointer">
<img src={homeIcon} alt="Home" className="w-1/2 cursor-pointer" />
</button>
<p className="grow text-white md:text-2xl font-bold flex items-center justify-start">
Home
</p>
</NavLink> </NavLink>
</li>
);
};
return ( <NavLink
<> to="/cache"
<div className="navbar sticky top-0 z-300 bg-slate-700 shadow-sm text-white"> className={({ isActive }) =>
<div className="navbar-start"> `flex flex-row p-3 border-l-3 ${
<div className="dropdown"> isActive
<div tabIndex={0} role="button" className="btn btn-ghost lg:hidden"> ? 'border-l-emerald-500/50 bg-black/50'
<svg : 'hover:border-l-emerald-500/50 hover:bg-white/5'
xmlns="http://www.w3.org/2000/svg" }`
className="h-5 w-5" }
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
> >
{" "} <button className="w-1/3 p-3 flex flex-col items-center justify-center cursor-pointer">
<path <img src={cacheIcon} alt="Cache" className="w-1/2 cursor-pointer" />
strokeLinecap="round" </button>
strokeLinejoin="round" <p className="grow text-white md:text-2xl font-bold flex items-center justify-start">
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 Cache
</MenuItem> </p>
<MenuItem to="/api"> </NavLink>
<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> <NavLink
<li> to="/api"
className={({ isActive }) =>
`flex flex-row p-3 border-l-3 ${
isActive
? 'border-l-indigo-500/50 bg-black/50'
: 'hover:border-l-indigo-500/50 hover:bg-white/5'
}`
}
>
<button className="w-1/3 p-3 flex flex-col items-center justify-center cursor-pointer">
<img src={apiIcon} alt="API" className="w-1/2 cursor-pointer" />
</button>
<p className="grow text-white md:text-2xl font-bold flex items-center justify-start">
API
</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
</p>
</NavLink>
</div>
</div>
{/* External links at very bottom */}
<div className="flex flex-row w-full h-16 bg-black/25">
<a <a
href={externalLinks.discord} href={externalLinks.discord}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="w-1/3 p-3 flex flex-col items-center justify-center cursor-pointer hover:bg-blue-950 group"
> >
<FaDiscord alt="Discord" width={20} height={20} /> <img src={discordIcon} alt="Discord" className="w-1/2 group-hover:animate-bounce" />
Discord
</a> </a>
</li>
<li>
<a <a
href={externalLinks.telegram} href={externalLinks.telegram}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="w-1/3 p-3 flex flex-col items-center justify-center cursor-pointer hover:bg-blue-400 group"
> >
<FaTelegram alt="Telegram" width={20} height={20} /> <img src={telegramIcon} alt="Telegram" className="w-1/2 group-hover:animate-bounce" />
Telegram
</a> </a>
</li>
<li>
<a <a
href={externalLinks.gitea} href={externalLinks.gitea}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="w-1/3 p-3 flex flex-col items-center justify-center cursor-pointer hover:bg-green-700 group"
> >
<SiGitea alt="Gitea" width={20} height={20} /> <img src={giteaIcon} alt="Gitea" className="w-1/2 group-hover:animate-bounce" />
Gitea
</a>
</li>
</ul>
</div>
<a className="btn btn-ghost text-xl">CDRM-Project</a>
</div>
<div className="navbar-center hidden lg:flex">
<ul className="menu menu-horizontal px-1">
<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>
</ul>
</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> </a>
</div> </div>
</div> </div>
</>
); );
} }

View File

@ -11,8 +11,9 @@ function NavBarMain({ setIsMenuOpen }) {
<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 />
<span className="text-sm">Github Edition</span>
</p> </p>
<div className="w-24 p-4"></div> <div className="w-24 p-4"></div>
</div> </div>

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,154 +83,51 @@ 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 (
<>
<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>
<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>
</>
); );
} }

View File

@ -1,46 +1,37 @@
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); setIsLoggedIn(false); // Assume not logged in on error
}); });
}, []); }, []);
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 />
<Container>
<div id="accountpage" className="flex h-full w-full">
{isLoggedIn ? <MyAccount /> : <Register />} {isLoggedIn ? <MyAccount /> : <Register />}
</div> </div>
</Container>
</>
); );
} }

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(() => {
sendApiCall(query); if (query.trim() !== '') {
}, 1000); sendApiCall(query); // Only call the API if the query is not empty
} else { } else {
setHasSearched(false); // Reset state when input is cleared setCacheData([]); // Clear results if query is empty
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";
}}
> >
<FaDownload /> Download Cache
Download keys cache </a>
</button>
</div> </div>
<div className="w-full grow p-4 border-2 border-emerald-500/50 rounded-2xl mt-5 overflow-y-auto">
{loading ? ( <table className="min-w-full text-white">
<div className="flex justify-center py-16">
<span className="loading loading-spinner loading-md me-2"></span>
Searching...
</div>
) : cacheData.length > 0 ? (
<div className="my-4 flex justify-center">
<div className="overflow-x-auto">
<table className="table">
<thead> <thead>
<tr> <tr>
<th></th> <th className="p-2 border border-black">PSSH</th>
<th className="text-center">PSSH</th> <th className="p-2 border border-black">KID</th>
<th className="text-center">key ID:key pair</th> <th className="p-2 border border-black">Key</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{cacheData.map((item, index) => ( {cacheData.length > 0 ? (
cacheData.map((item, index) => (
<tr key={index}> <tr key={index}>
<th>{index + 1}</th> <td className="p-2 border border-black">{item.PSSH}</td>
<td className="font-mono">{item.PSSH}</td> <td className="p-2 border border-black">{item.KID}</td>
<td className="font-mono"> <td className="p-2 border border-black">{item.Key}</td>
{item.KID}:{item.Key} </tr>
))
) : (
<tr>
<td colSpan="3" className="p-2 border border-black text-center">
No data found
</td> </td>
</tr> </tr>
))} )}
</tbody> </tbody>
</table> </table>
</div> </div>
</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,25 +1,18 @@
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(() => {
document.title = "Home | CDRM-Project";
}, []);
const bottomRef = useRef(null); const bottomRef = useRef(null);
const messageRef = useRef(null); // Reference to result container const messageRef = useRef(null); // Reference to result container
@ -28,21 +21,21 @@ function HomePage() {
if (isVisible) { if (isVisible) {
setIsVisible(false); setIsVisible(false);
} }
setPssh(""); setPssh('');
setLicurl(""); setLicurl('');
setProxy(""); setProxy('');
setHeaders(""); setHeaders('');
setCookies(""); setCookies('');
setData(""); setData('');
}; };
const handleSubmitButton = (event) => { const handleSubmitButton = (event) => {
event.preventDefault(); event.preventDefault();
fetch("/api/decrypt", { fetch('/api/decrypt', {
method: "POST", method: 'POST',
headers: { headers: {
"Content-Type": "application/json", 'Content-Type': 'application/json',
}, },
body: JSON.stringify({ body: JSON.stringify({
pssh: pssh, pssh: pssh,
@ -54,15 +47,15 @@ function HomePage() {
device: selectedDevice, // Include selected device in the request device: selectedDevice, // Include selected device in the request
}), }),
}) })
.then((response) => response.json()) .then(response => response.json())
.then((data) => { .then(data => {
const resultMessage = data["message"].replace(/\n/g, "<br />"); const resultMessage = data['message'].replace(/\n/g, '<br />');
setMessage(resultMessage); setMessage(resultMessage);
setIsVisible(true); setIsVisible(true);
}) })
.catch((error) => { .catch((error) => {
console.error("Error during decryption request:", error); console.error('Error during decryption request:', error);
setMessage(`Error: Unable to process request. Reason: ${error.message}`); setMessage('Error: Unable to process request.');
setIsVisible(true); setIsVisible(true);
}); });
}; };
@ -71,49 +64,45 @@ function HomePage() {
event.preventDefault(); event.preventDefault();
if (messageRef.current) { if (messageRef.current) {
const textToCopy = messageRef.current.innerText; // Grab the plain text (with visual line breaks) const textToCopy = messageRef.current.innerText; // Grab the plain text (with visual line breaks)
toast.success("Copied to clipboard"); navigator.clipboard.writeText(textToCopy).catch(err => {
navigator.clipboard.writeText(textToCopy).catch((err) => { alert('Failed to copy!');
toast.error(`Failed to copy. Reason: ${err.message}`);
console.error(err); console.error(err);
}); });
} }
}; };
const handleFetchPaste = (event) => { const handleFetchPaste = () => {
event.preventDefault(); event.preventDefault();
readTextFromClipboard() readTextFromClipboard().then(() => {
.then(() => {
setPssh(document.getElementById("pssh").value); setPssh(document.getElementById("pssh").value);
setLicurl(document.getElementById("licurl").value); setLicurl(document.getElementById("licurl").value);
setHeaders(document.getElementById("headers").value); setHeaders(document.getElementById("headers").value);
setData(document.getElementById("data").value); setData(document.getElementById("data").value);
}) }).catch(err => {
.catch((err) => { alert('Failed to paste from fetch!');
toast.error(`Failed to paste from fetch. Reason: ${err.message}`);
console.error("Failed to paste from fetch:", err);
}); });
}; };
useEffect(() => { useEffect(() => {
if (isVisible && bottomRef.current) { if (isVisible && bottomRef.current) {
bottomRef.current.scrollIntoView({ behavior: "smooth" }); bottomRef.current.scrollIntoView({ behavior: 'smooth' });
} }
}, [message, isVisible]); }, [message, isVisible]);
useEffect(() => { useEffect(() => {
fetch("/login/status", { fetch('/login/status', {
method: "POST", method: 'POST',
}) })
.then((res) => res.json()) .then(res => res.json())
.then((statusData) => { .then(statusData => {
if (statusData.message === "True") { if (statusData.message === 'True') {
return fetch("/userinfo", { method: "POST" }); return fetch('/userinfo', { method: 'POST' });
} else { } else {
throw new Error("Not logged in"); throw new Error('Not logged in');
} }
}) })
.then((res) => res.json()) .then(res => res.json())
.then((deviceData) => { .then(deviceData => {
const combinedDevices = [ const combinedDevices = [
...deviceData.Widevine_Devices, ...deviceData.Widevine_Devices,
...deviceData.Playready_Devices, ...deviceData.Playready_Devices,
@ -128,161 +117,130 @@ function HomePage() {
// Set devices and select a device if logged in // Set devices and select a device if logged in
setDevices(allDevices.length > 0 ? allDevices : []); setDevices(allDevices.length > 0 ? allDevices : []);
setSelectedDevice(allDevices.length > 0 ? allDevices[0] : "default"); setSelectedDevice(allDevices.length > 0 ? allDevices[0] : 'default');
}) })
.catch(() => { .catch(() => {
// User isn't logged in, set default device to 'default' // User isn't logged in, set default device to 'default'
setDevices([]); // Don't display devices list setDevices([]); // Don't display devices list
setSelectedDevice("default"); setSelectedDevice('default');
}); });
}, []); }, []);
return ( return (
<> <>
<NavBar /> <div className="flex flex-col w-full overflow-y-auto p-4 min-h-full">
<Container> <Helmet>
<div className="mx-auto flex w-full max-w-2xl flex-col justify-center"> <title>CDRM-Project</title>
<fieldset className="fieldset"> </Helmet>
<legend className="fieldset-legend text-base">PSSH*</legend> <form className="flex flex-col w-full h-full bg-black/5 p-4 overflow-y-auto">
<label htmlFor="pssh" className="text-white w-8/10 self-center">PSSH: </label>
<input <input
type="text" type="text"
className="input w-full font-mono" id="pssh"
placeholder="Enter PSSH here" className="w-8/10 border-2 border-sky-500/25 rounded-xl h-10 self-center m-2 text-white p-1"
value={pssh} value={pssh}
onChange={(e) => setPssh(e.target.value)} onChange={(e) => setPssh(e.target.value)}
required
/> />
<p className="label text-red-500">* Required</p> <label htmlFor="licurl" className="text-white w-8/10 self-center">License URL: </label>
</fieldset>
<fieldset className="fieldset">
<legend className="fieldset-legend text-base">License URL*</legend>
<input <input
type="text" type="text"
className="input w-full font-mono" id="licurl"
placeholder="Enter License URL here" className="w-8/10 border-2 border-sky-500/25 rounded-xl h-10 self-center m-2 text-white p-1"
value={licurl} value={licurl}
onChange={(e) => setLicurl(e.target.value)} onChange={(e) => setLicurl(e.target.value)}
required
/> />
<p className="label text-red-500">* Required</p> <label htmlFor="proxy" className="text-white w-8/10 self-center">Proxy: </label>
</fieldset>
<fieldset className="fieldset">
<legend className="fieldset-legend text-base">Proxy</legend>
<input <input
type="text" type="text"
className="input w-full font-mono" id="proxy"
placeholder="Enter Proxy here (https://example.com:8080)" className="w-8/10 border-2 border-sky-500/25 rounded-xl h-10 self-center m-2 text-white p-1"
value={proxy} value={proxy}
onChange={(e) => setProxy(e.target.value)} onChange={(e) => setProxy(e.target.value)}
/> />
</fieldset> <label htmlFor="headers" className="text-white w-8/10 self-center">Headers: </label>
<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 <textarea
className="textarea h-48 w-full font-mono" id="headers"
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'}" className="w-8/10 border-2 border-sky-500/25 rounded-xl self-center m-2 text-white p-1 h-48"
value={headers} value={headers}
onChange={(e) => setHeaders(e.target.value)} onChange={(e) => setHeaders(e.target.value)}
required
/> />
<p className="label text-red-500">* Required</p> <label htmlFor="cookies" className="text-white w-8/10 self-center">Cookies: </label>
</fieldset>
<fieldset className="fieldset">
<legend className="fieldset-legend text-base">Cookies</legend>
<textarea <textarea
className="textarea h-48 w-full font-mono" id="cookies"
placeholder="Enter cookies here (JSON format)" className="w-8/10 border-2 border-sky-500/25 rounded-xl self-center m-2 text-white p-1 h-48"
value={cookies} value={cookies}
onChange={(e) => setCookies(e.target.value)} onChange={(e) => setCookies(e.target.value)}
/> />
</fieldset> <label htmlFor="data" className="text-white w-8/10 self-center">Data: </label>
<fieldset className="fieldset">
<legend className="fieldset-legend text-base">Data</legend>
<textarea <textarea
className="textarea h-48 w-full font-mono" id="data"
placeholder="Enter data here (JSON format)" className="w-8/10 border-2 border-sky-500/25 rounded-xl self-center m-2 text-white p-1 h-48"
value={data} value={data}
onChange={(e) => setData(e.target.value)} onChange={(e) => setData(e.target.value)}
/> />
</fieldset>
{/* Device Selection Dropdown, only show if logged in */} {/* Device Selection Dropdown, only show if logged in */}
{devices.length > 0 && ( {devices.length > 0 && (
<> <>
<fieldset className="fieldset"> <label htmlFor="device" className="text-white w-8/10 self-center">Select Device:</label>
<legend className="fieldset-legend text-base">Select device</legend>
<select <select
className="select w-full" id="device"
className="w-8/10 border-2 border-sky-500/25 rounded-xl h-10 self-center m-2 text-white bg-black p-1"
value={selectedDevice} value={selectedDevice}
onChange={(e) => setSelectedDevice(e.target.value)} onChange={(e) => setSelectedDevice(e.target.value)}
> >
{devices.map((device, index) => ( {devices.map((device, index) => (
<option key={index} value={device}> <option key={index} value={device}>{device}</option>
{device}
</option>
))} ))}
</select> </select>
</fieldset>
</> </>
)} )}
<div className="mx-auto my-4 flex w-full flex-col items-center justify-center gap-2 lg:flex-row">
<div className="flex flex-col lg:flex-row w-full self-center mt-5 items-center lg:justify-around lg:items-stretch">
<button <button
type="button" type="button"
className="btn btn-primary btn-wide" className="bg-sky-500/50 rounded-xl text-white text-bold text-xl p-1 lg:w-1/5 lg:h-12 truncate w-1/2"
onClick={handleSubmitButton} onClick={handleSubmitButton}
disabled={pssh === "" || licurl === "" || headers === ""}
> >
Submit Submit
</button> </button>
<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">
type="button"
className="btn btn-info btn-wide"
onClick={handleFetchPaste}
>
Paste from fetch Paste from fetch
</button> </button>
<button <button
type="button" type="button"
className="btn btn-error btn-wide" 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} onClick={handleReset}
> >
Reset Reset
</button> </button>
</div> </div>
</form>
</div> </div>
{isVisible && ( {isVisible && (
<> <div id="main_content" className="flex-col w-full h-full p-10 items-center justify-center self-center">
<div className="mx-auto my-4 flex w-full max-w-2xl flex-col justify-center"> <div className="flex flex-col w-full h-full overflow-y-auto items-center">
<div className="card bg-base-100 card-lg border border-gray-500 shadow-sm"> <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'>
<div className="card-body"> <p className="w-full border-b-2 border-white/75 pb-2">Results:</p>
<h2 className="card-title">Result</h2>
<div className="divider"></div>
<p <p
className="w-full grow overflow-y-auto font-mono break-words" className="w-full grow pt-10 break-words overflow-y-auto"
ref={messageRef} ref={messageRef}
dangerouslySetInnerHTML={{ __html: message }} dangerouslySetInnerHTML={{ __html: message }}
/> />
<div ref={bottomRef} /> <div ref={bottomRef} />
<div </div>
className="card-actions mt-4 justify-end" </div>
<div className="flex flex-col lg:flex-row w-full self-center mt-5 items-center lg:justify-around lg:items-stretch">
<button
className="bg-green-500/50 rounded-xl text-white text-bold text-xl p-1 lg:w-1/5 lg:h-12 truncate w-1/2"
onClick={handleCopy} onClick={handleCopy}
> >
<button className="btn btn-success">Copy results</button> Copy Results
</button>
</div> </div>
</div> </div>
</div>
</div>
</>
)} )}
</Container>
</> </>
); );
} }

View File

@ -1,30 +1,21 @@
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) {
toast.error(`Failed to fetch user info. Reason: ${err.message}`); console.error('Failed to fetch user info', err);
console.error("Failed to fetch user info", err);
} }
}; };
@ -32,36 +23,28 @@ function MyAccount() {
fetchUserInfo(); fetchUserInfo();
}, []); }, []);
useEffect(() => {
document.title = "My account | CDRM-Project";
}, []);
// Handle file upload // Handle file upload
const handleUpload = async (event, cdmType) => { const handleUpload = async (event, cdmType) => {
const file = event.target.files[0]; const file = event.target.files[0];
if (!file) return; if (!file) return;
const extension = file.name.split(".").pop(); const extension = file.name.split('.').pop();
if ( if ((cdmType === 'PR' && extension !== 'prd') || (cdmType === 'WV' && extension !== 'wvd')) {
(cdmType === "PR" && extension !== "prd") || alert(`Please upload a .${cdmType === 'PR' ? 'prd' : 'wvd'} file.`);
(cdmType === "WV" && extension !== "wvd")
) {
toast.error(`Please upload a .${cdmType === "PR" ? "prd" : "wvd"} file.`);
return; return;
} }
const formData = new FormData(); const formData = new FormData();
formData.append("file", file); formData.append('file', file);
setUploading(true); setUploading(true);
try { try {
await axios.post(`/upload/${cdmType}`, formData); await axios.post(`/upload/${cdmType}`, formData);
await fetchUserInfo(); // Refresh list after upload await fetchUserInfo(); // Refresh list after upload
} catch (err) { } catch (err) {
toast.error(`Upload failed. Reason: ${err.message}`); console.error('Upload failed', err);
console.error("Upload failed", err); alert('Upload failed');
} finally { } finally {
toast.success(`${cdmType} CDM uploaded successfully`);
setUploading(false); setUploading(false);
} }
}; };
@ -69,192 +52,44 @@ function MyAccount() {
// Handle logout // Handle logout
const handleLogout = async () => { const handleLogout = async () => {
try { try {
await axios.post("/logout"); await axios.post('/logout');
toast.success("Logged out successfully. Reloading page...");
window.location.reload(); window.location.reload();
} catch (error) { } catch (error) {
toast.error(`Logout failed. Reason: ${error.message}`); console.error('Logout failed:', error);
console.error("Logout failed:", error); alert('Logout failed!');
}
};
// Handle change password
const handleChangePassword = async () => {
if (passwordError || password === "") {
toast.error("Please enter a valid password");
return;
}
try {
const response = await axios.post("/user/change_password", {
new_password: password,
});
if (response.data.message === "True") {
toast.success("Password changed successfully");
setPassword("");
} else {
toast.error("Failed to change password");
}
} catch (error) {
if (error.response && error.response.data?.message === "Invalid password format") {
toast.error("Password format is invalid. Please try again.");
} else {
toast.error("Error occurred while changing password");
}
}
};
// Handle change API key
const handleChangeApiKey = async () => {
if (apiKeyError || newApiKey === "") {
toast.error("Please enter a valid API key");
return;
}
try {
const response = await axios.post("/user/change_api_key", {
new_api_key: newApiKey,
});
if (response.data.message === "True") {
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 ( return (
<> <div id="myaccount" className="flex flex-row w-full min-h-full overflow-y-auto p-4">
<Container> <div className="flex flex-col w-full min-h-full lg:flex-row">
<div className="flex flex-col gap-4 p-4 lg:flex-row"> {/* Left Panel */}
{/* Left Panel - Account Settings */} <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">
<div className="w-full lg:w-96"> <h1 className="text-2xl font-bold text-white border-b-2 border-white p-2 w-full text-center mb-2">
<div className="card bg-base-200 shadow-xl"> {username ? `${username}` : 'My Account'}
<div className="card-body"> </h1>
<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 <button
className="btn btn-primary btn-block mt-2" onClick={handleLogout}
onClick={handleChangeApiKey} className="mt-auto w-full h-12 bg-yellow-500/50 rounded-2xl text-2xl text-white"
> >
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 Log out
</button> </button>
</div> </div>
</div>
</div>
{/* Right Panel - CDM Uploads */} {/* Right Panel */}
<div className="flex w-full flex-col gap-4"> <div className="flex flex-col grow lg:ml-2 mt-2 lg:mt-0">
{/* Widevine CDM */} {/* Widevine Section */}
<div className="card bg-base-200 shadow-xl"> <div className="border-2 border-yellow-500/50 flex flex-col w-full min-h-1/2 text-center rounded-2xl lg:p-4 p-2 overflow-y-auto">
<div className="card-body"> <h1 className="text-2xl font-bold text-white border-b-2 border-white p-2">Widevine CDMs</h1>
<h2 className="card-title">Widevine CDMs</h2> <div className="flex flex-col w-full grow p-2 bg-white/5 rounded-2xl mt-2 text-white text-left">
<div className="divider"></div>
<div className="max-h-60 space-y-2 overflow-y-auto">
{wvList.length === 0 ? ( {wvList.length === 0 ? (
<div className="text-center text-sm"> <div className="text-white text-center font-bold">No Widevine CDMs uploaded.</div>
No Widevine CDMs uploaded.
</div>
) : ( ) : (
wvList.map((filename, i) => ( wvList.map((filename, i) => (
<div <div
key={i} key={i}
className={`rounded px-2 py-1 text-sm ${ className={`text-center font-bold text-white p-2 rounded ${
i % 2 === 0 ? "bg-base-100" : "bg-base-300" i % 2 === 0 ? 'bg-black/30' : 'bg-black/60'
}`} }`}
> >
{filename} {filename}
@ -262,34 +97,29 @@ function MyAccount() {
)) ))
)} )}
</div> </div>
<label className="btn btn-accent mt-4"> <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"} {uploading ? 'Uploading...' : 'Upload CDM'}
<input <input
type="file" type="file"
accept=".wvd" accept=".wvd"
hidden hidden
onChange={(e) => handleUpload(e, "WV")} onChange={(e) => handleUpload(e, 'WV')}
/> />
</label> </label>
</div> </div>
</div>
{/* PlayReady CDM */} {/* Playready Section */}
<div className="card bg-base-200 shadow-xl"> <div className="border-2 border-yellow-500/50 flex flex-col w-full min-h-1/2 text-center rounded-2xl p-2 mt-2 lg:mt-2 overflow-y-auto">
<div className="card-body"> <h1 className="text-2xl font-bold text-white border-b-2 border-white p-2">Playready CDMs</h1>
<h2 className="card-title">PlayReady CDMs</h2> <div className="flex flex-col w-full bg-white/5 grow rounded-2xl mt-2 text-white text-left p-2">
<div className="divider"></div>
<div className="max-h-60 space-y-2 overflow-y-auto">
{prList.length === 0 ? ( {prList.length === 0 ? (
<div className="text-center text-sm"> <div className="text-white text-center font-bold">No Playready CDMs uploaded.</div>
No PlayReady CDMs uploaded.
</div>
) : ( ) : (
prList.map((filename, i) => ( prList.map((filename, i) => (
<div <div
key={i} key={i}
className={`rounded px-2 py-1 text-sm ${ className={`text-center font-bold text-white p-2 rounded ${
i % 2 === 0 ? "bg-base-100" : "bg-base-300" i % 2 === 0 ? 'bg-black/30' : 'bg-black/60'
}`} }`}
> >
{filename} {filename}
@ -297,21 +127,19 @@ function MyAccount() {
)) ))
)} )}
</div> </div>
<label className="btn btn-accent mt-4"> <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"} {uploading ? 'Uploading...' : 'Upload CDM'}
<input <input
type="file" type="file"
accept=".prd" accept=".prd"
hidden hidden
onChange={(e) => handleUpload(e, "PR")} onChange={(e) => handleUpload(e, 'PR')}
/> />
</label> </label>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</Container>
</>
); );
} }

View File

@ -1,146 +1,92 @@
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";
}, []);
const validateUsername = (name) => /^[A-Za-z0-9_-]+$/.test(name);
const validatePassword = (pass) => /^\S+$/.test(pass); // No spaces
const handleSubmit = async (e) => {
e.preventDefault();
if (!validateUsername(username)) {
toast.error("Invalid username. Use only letters, numbers, hyphens, or underscores.");
return;
}
if (!validatePassword(password)) {
toast.error("Invalid password. Spaces are not allowed.");
return;
}
if (tab === "register") {
if (password !== confirmPassword) {
toast.error("Passwords do not match.");
return;
}
try { try {
const res = await fetch("/register", { const response = await fetch('/register', {
method: "POST", method: 'POST',
headers: { headers: {
"Content-Type": "application/json", 'Content-Type': 'application/json'
}, },
body: JSON.stringify({ username, password }), body: JSON.stringify({ username, password })
}); });
const data = await res.json(); const data = await response.json();
if (data.message) { if (data.message) {
toast.success(data.message); setStatus(data.message);
} else { } else if (data.error) {
toast.error(data.error || "Unknown error"); setStatus(data.error);
} }
} catch (err) { } catch (err) {
toast.error(`Register error: ${err.message}`); setStatus('An error occurred while registering.');
} }
} else { };
const handleLogin = async () => {
try { try {
const res = await fetch("/login", { const response = await fetch('/login', {
method: "POST", method: 'POST',
headers: { headers: {
"Content-Type": "application/json", 'Content-Type': 'application/json'
}, },
credentials: "include", credentials: 'include', // Important to send cookies
body: JSON.stringify({ username, password }), body: JSON.stringify({ username, password })
}); });
const data = await res.json(); const data = await response.json();
if (data.message) { if (data.message) {
// Successful login - reload the page to trigger Account check
window.location.reload(); window.location.reload();
} else { } else if (data.error) {
toast.error(data.error || "Login failed"); setStatus(data.error);
} }
} catch (err) { } catch (err) {
toast.error(`Login error: ${err.message}`); setStatus('An error occurred while logging in.');
}
} }
}; };
return ( 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="flex flex-col w-full h-full items-center justify-center p-4">
<div className="mx-auto"> <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">
{/* Tabs */} <div className="flex flex-col w-full">
<div className="tabs tabs-box justify-center"> <label htmlFor="username" className="text-lg font-bold mb-2 text-white">Username:</label>
<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 <input
type="text" type="text"
value={username} value={username}
onChange={(e) => setUsername(e.target.value)} onChange={e => setUsername(e.target.value)}
className="input w-full" placeholder="Username"
placeholder="Enter your username" className="mb-4 p-2 border border-gray-300 rounded text-white bg-transparent"
required
/> />
</fieldset> <label htmlFor="password" className="text-lg font-bold mb-2 text-white">Password:</label>
<fieldset className="fieldset">
<legend className="fieldset-legend text-base">Password</legend>
<input <input
type="password" type="password"
value={password} value={password}
onChange={(e) => setPassword(e.target.value)} onChange={e => setPassword(e.target.value)}
className="input w-full" placeholder="Password"
placeholder="Enter your password" className="mb-4 p-2 border border-gray-300 rounded text-white bg-transparent"
required
/> />
</fieldset> </div>
<div className="flex flex-col lg:flex-row w-8/10 items-center lg:justify-between mt-4">
{tab === "register" && ( <button
<fieldset className="fieldset"> onClick={handleLogin}
<legend className="fieldset-legend text-base">Confirm password</legend> className="bg-yellow-500 hover:bg-yellow-600 text-white font-bold py-2 px-4 rounded mt-4 w-1/3"
<input >
type="password" Login
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> </button>
</form> <button
onClick={handleRegister}
className="bg-yellow-500 hover:bg-yellow-600 text-white font-bold py-2 px-4 rounded mt-4 w-1/3"
>
Register
</button>
</div>
{status && (
<p className="text-sm text-white mt-4 p-4">
{status}
</p>
)}
</div> </div>
</div> </div>
); );

View File

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

View File

@ -1,53 +1,50 @@
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 />
<span className="text-sm">Github Edition</span>
</p> </p>
<div className="w-1/4 h-full"> <div className="w-1/4 h-full">
<button <button
className="w-full h-full flex items-center justify-center" className="w-full h-full flex items-center justify-center"
onClick={() => setIsMenuOpen(false)} onClick={() => setIsMenuOpen(false)}
> >
<img <img src={closeIcon} alt="Close" className="w-1/2 h-1/2 cursor-pointer" />
src={closeIcon}
alt="Close"
className="w-1/2 h-1/2 cursor-pointer"
/>
</button> </button>
</div> </div>
</div> </div>
@ -60,8 +57,8 @@ function SideMenu({ isMenuOpen, setIsMenuOpen }) {
className={({ isActive }) => className={({ isActive }) =>
`flex flex-row items-center gap-3 p-3 border-l-4 ${ `flex flex-row items-center gap-3 p-3 border-l-4 ${
isActive isActive
? "border-l-sky-500/50 bg-black/50 text-white" ? '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" : 'border-transparent hover:border-l-sky-500/50 hover:bg-white/5 text-white/80'
}` }`
} }
onClick={() => setIsMenuOpen(false)} onClick={() => setIsMenuOpen(false)}
@ -75,8 +72,8 @@ function SideMenu({ isMenuOpen, setIsMenuOpen }) {
className={({ isActive }) => className={({ isActive }) =>
`flex flex-row items-center gap-3 p-3 border-l-4 ${ `flex flex-row items-center gap-3 p-3 border-l-4 ${
isActive isActive
? "border-l-emerald-500/50 bg-black/50 text-white" ? '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" : 'border-transparent hover:border-l-emerald-500/50 hover:bg-white/5 text-white/80'
}` }`
} }
onClick={() => setIsMenuOpen(false)} onClick={() => setIsMenuOpen(false)}
@ -90,8 +87,8 @@ function SideMenu({ isMenuOpen, setIsMenuOpen }) {
className={({ isActive }) => className={({ isActive }) =>
`flex flex-row items-center gap-3 p-3 border-l-4 ${ `flex flex-row items-center gap-3 p-3 border-l-4 ${
isActive isActive
? "border-l-indigo-500/50 bg-black/50 text-white" ? '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" : 'border-transparent hover:border-l-indigo-500/50 hover:bg-white/5 text-white/80'
}` }`
} }
onClick={() => setIsMenuOpen(false)} onClick={() => setIsMenuOpen(false)}
@ -105,8 +102,8 @@ function SideMenu({ isMenuOpen, setIsMenuOpen }) {
className={({ isActive }) => className={({ isActive }) =>
`flex flex-row items-center gap-3 p-3 border-l-4 ${ `flex flex-row items-center gap-3 p-3 border-l-4 ${
isActive isActive
? "border-l-rose-700/50 bg-black/50 text-white" ? '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" : 'border-transparent hover:border-l-rose-700/50 hover:bg-white/5 text-white/80'
}` }`
} }
onClick={() => setIsMenuOpen(false)} onClick={() => setIsMenuOpen(false)}
@ -123,8 +120,8 @@ function SideMenu({ isMenuOpen, setIsMenuOpen }) {
className={({ isActive }) => className={({ isActive }) =>
`flex flex-row items-center gap-3 p-3 border-l-4 ${ `flex flex-row items-center gap-3 p-3 border-l-4 ${
isActive isActive
? "border-l-yellow-500/50 bg-black/50 text-white" ? '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" : 'border-transparent hover:border-l-yellow-500/50 hover:bg-white/5 text-white/80'
}` }`
} }
onClick={() => setIsMenuOpen(false)} onClick={() => setIsMenuOpen(false)}

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
richColors
className="flex justify-center"
position="bottom-center"
duration="7000"
theme="dark"
/>
</BrowserRouter> </BrowserRouter>
</StrictMode> </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": {
"description": "Account for CDRM-Project",
"keywords": "Login, CDRM, CDM, CDRM-Project, register, account",
"opengraph_title": "My account",
"opengraph_description": "Account for CDRM-Project",
"opengraph_image": "https://cdrm-project.com/og-home.jpg",
"opengraph_url": "https://cdrm-project.com/account",
"tab_title": "My account",
}, },
'account': {
'description': 'Account for CDRM-Project',
'keywords': 'Login, CDRM, CDM, CDRM-Project, register, account',
'opengraph_title': 'My account',
'opengraph_description': 'Account for CDRM-Project',
'opengraph_image': 'https://cdrm-project.com/og-home.jpg',
'opengraph_url': 'https://cdrm-project.com/account',
'tab_title': 'My account',
}
} }

View File

@ -1,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)
else:
return False 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
else:
return False, None, None return False, None, None
def api_decrypt(pssh:str = None, license_url: str = None, proxy: str = None, headers: str = None, cookies: str = None, json_data: str = None, device: str = 'public', username: str = None):
def sanitize_username(username): print(f'Using device {device} for user {username}')
"""Sanitize the username.""" with open(f'{os.getcwd()}/configs/config.yaml', 'r') as file:
return re.sub(r"[^a-zA-Z0-9_\-]", "_", username).lower() config = yaml.safe_load(file)
if config['database_type'].lower() == 'sqlite':
from custom_functions.database.cache_to_db_sqlite import cache_to_db
def load_device(device_type, device, username, config): elif config['database_type'].lower() == 'mariadb':
"""Load the appropriate device file for PlayReady or Widevine.""" from custom_functions.database.cache_to_db_mariadb import cache_to_db
if device_type == "PR": if pssh is None:
ext, config_key, class_loader = ".prd", "default_pr_cdm", playreadyDevice.load return {
base_dir = "PR" 'status': 'error',
'message': 'No PSSH provided'
}
try:
if "</WRMHEADER>".encode("utf-16-le") in base64.b64decode(pssh): # PR
try:
pr_pssh = playreadyPSSH(pssh)
except Exception as error:
return {
'status': 'error',
'message': f'An error occurred processing PSSH\n\n{error}'
}
try:
if device == 'public':
base_name = config["default_pr_cdm"]
if not base_name.endswith(".prd"):
base_name += ".prd"
prd_files = glob.glob(f'{os.getcwd()}/configs/CDMs/PR/{base_name}')
else: else:
ext, config_key, class_loader = ".wvd", "default_wv_cdm", widevineDevice.load prd_files = glob.glob(f'{os.getcwd()}/configs/CDMs/PR/{base_name}')
base_dir = "WV" if prd_files:
pr_device = playreadyDevice.load(prd_files[0])
if device == "public": else:
base_name = config[config_key] return {
if not base_name.endswith(ext): 'status': 'error',
base_name += ext 'message': 'No default .prd file found'
search_path = os.path.join(os.getcwd(), "configs", "CDMs", base_dir, base_name) }
else: else:
base_name = device base_name = device
if not base_name.endswith(ext): if not base_name.endswith(".prd"):
base_name += ext base_name += ".prd"
safe_username = sanitize_username(username) prd_files = glob.glob(f'{os.getcwd()}/configs/CDMs/{username}/PR/{base_name}')
search_path = os.path.join( else:
os.getcwd(), prd_files = glob.glob(f'{os.getcwd()}/configs/CDMs/{username}/PR/{base_name}')
"configs", if prd_files:
"CDMs", pr_device = playreadyDevice.load(prd_files[0])
"users_uploaded", else:
safe_username, return {
base_dir, 'status': 'error',
base_name, 'message': f'{base_name} does not exist'
) }
except Exception as error:
files = glob.glob(search_path) return {
if not files: 'status': 'error',
return None, f"No {ext} file found for device '{device}'" 'message': f'An error occurred location PlayReady CDM file\n\n{error}'
}
try: try:
return class_loader(files[0]), None pr_cdm = playreadyCdm.from_device(pr_device)
except (IOError, OSError) as e: except Exception as error:
return None, f"Failed to read device file: {e}" return {
except (ValueError, TypeError, AttributeError) as e: 'status': 'error',
return None, f"Failed to parse device file: {e}" 'message': f'An error occurred loading PlayReady CDM\n\n{error}'
}
def prepare_request_data(headers, cookies, json_data, challenge, is_widevine):
"""Prepare headers, cookies, and json_data for the license request."""
try: try:
format_headers = ast.literal_eval(headers) if headers else None pr_session_id = pr_cdm.open()
except (ValueError, SyntaxError) as e: except Exception as error:
raise ValueError(f"Invalid headers format: {e}") from e 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: 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): if json_data and not is_base64(json_data):
try:
format_json_data = ast.literal_eval(json_data) 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: else:
returned_keys += f"{kid.hex}:{key.key.hex()}" format_json_data = None
return returned_keys, None except Exception as error:
except AttributeError as error:
return None, f"Error accessing CDM keys: {error}"
except (TypeError, ValueError) as error:
return None, f"Error processing keys: {error}"
def api_decrypt(
pssh: str = "",
license_url: str = "",
proxy: str = "",
headers: str = "",
cookies: str = "",
json_data: str = "",
device: str = "public",
username: str = "",
):
"""Decrypt the license using the API."""
print(f"Using device {device} for user {username}")
with open(f"{os.getcwd()}/configs/config.yaml", "r", encoding="utf-8") as file:
config = yaml.safe_load(file)
if pssh == "":
return {"status": "error", "message": "No PSSH provided"}
# Detect PlayReady or Widevine
try:
is_pr = "</WRMHEADER>".encode("utf-16-le") in base64.b64decode(pssh)
except (binascii.Error, TypeError) as error:
return { return {
"status": "error", 'status': 'error',
"message": f"An error occurred processing PSSH\n\n{error}", 'message': f'An error occurred getting json_data\n\n{error}'
} }
licence = None
device_type = "PR" if is_pr else "WV" proxies = None
cdm_class = playreadyCdm if is_pr else widevineCdm if proxy is not None:
pssh_class = playreadyPSSH if is_pr else widevinePSSH
# Load device
device_obj, device_err = load_device(device_type, device, username, config)
if device_obj is None:
return {"status": "error", "message": device_err}
# Create CDM
try:
cdm = cdm_class.from_device(device_obj)
except (IOError, ValueError, AttributeError) as error:
return {
"status": "error",
"message": f"An error occurred loading {device_type} CDM\n\n{error}",
}
# Open session
try:
session_id = cdm.open()
except (IOError, ValueError, AttributeError) as error:
return {
"status": "error",
"message": f"An error occurred opening a CDM session\n\n{error}",
}
# Parse PSSH and get challenge
try:
pssh_obj = pssh_class(pssh)
if is_pr:
challenge = cdm.get_license_challenge(session_id, pssh_obj.wrm_headers[0])
else:
challenge = cdm.get_license_challenge(session_id, pssh_obj)
except (ValueError, AttributeError, IndexError) as error:
return {
"status": "error",
"message": f"An error occurred getting license challenge\n\n{error}",
}
# Prepare request data
try:
format_headers, format_cookies, format_json_data = prepare_request_data(
headers, cookies, json_data, challenge, is_widevine=(not is_pr)
)
except (ValueError, SyntaxError) as error:
return {
"status": "error",
"message": f"An error occurred preparing request data\n\n{error}",
}
# Prepare proxies
proxies = ""
if proxy != "":
is_url, protocol, fqdn = is_url_and_split(proxy) is_url, protocol, fqdn = is_url_and_split(proxy)
if is_url: if is_url:
proxies = {"http": proxy, "https": proxy} proxies = {'http': proxy, 'https': proxy}
else: else:
return { return {
"status": "error", 'status': 'error',
"message": "Your proxy is invalid, please put it in the format of http(s)://fqdn.tld:port", 'message': f'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: try:
if is_pr: licence = requests.post(
cdm.parse_license(session_id, licence.text) url=license_url,
headers=format_headers,
proxies=proxies,
cookies=format_cookies,
json=format_json_data if format_json_data is not None else None,
data=pr_challenge if format_json_data is None else None
)
except requests.exceptions.ConnectionError as error:
return {
'status': 'error',
'message': f'An error occurred sending license challenge through your proxy\n\n{error}'
}
except Exception as error:
return {
'status': 'error',
'message': f'An error occurred sending license reqeust\n\n{error}\n\n{licence.content}'
}
try:
pr_cdm.parse_license(pr_session_id, licence.text)
except Exception as error:
return {
'status': 'error',
'message': f'An error occurred parsing license content\n\n{error}\n\n{licence.content}'
}
returned_keys = ""
try:
keys = list(pr_cdm.get_keys(pr_session_id))
except Exception as error:
return {
'status': 'error',
'message': f'An error occurred getting keys\n\n{error}'
}
try:
for index, key in enumerate(keys):
if key.key_type != 'SIGNING':
cache_to_db(pssh=pssh, license_url=license_url, headers=headers, cookies=cookies,
data=pr_challenge if json_data is None else json_data, kid=key.key_id.hex,
key=key.key.hex())
if index != len(keys) - 1:
returned_keys += f"{key.key_id.hex}:{key.key.hex()}\n"
else:
returned_keys += f"{key.key_id.hex}:{key.key.hex()}"
except Exception as error:
return {
'status': 'error',
'message': f'An error occurred formatting keys\n\n{error}'
}
try:
pr_cdm.close(pr_session_id)
except Exception as error:
return {
'status': 'error',
'message': f'An error occurred closing session\n\n{error}'
}
try:
return {
'status': 'success',
'message': returned_keys
}
except Exception as error:
return {
'status': 'error',
'message': f'An error occurred getting returned_keys\n\n{error}'
}
except Exception as error:
return {
'status': 'error',
'message': f'An error occurred processing PSSH\n\n{error}'
}
else: else:
try: try:
cdm.parse_license(session_id, licence.content) # type: ignore[arg-type] wv_pssh = widevinePSSH(pssh)
except (ValueError, TypeError): except Exception as error:
# Try to extract license from JSON return {
'status': 'error',
'message': f'An error occurred processing PSSH\n\n{error}'
}
try:
if device == 'public':
base_name = config["default_wv_cdm"]
if not base_name.endswith(".wvd"):
base_name += ".wvd"
wvd_files = glob.glob(f'{os.getcwd()}/configs/CDMs/WV/{base_name}')
else:
wvd_files = glob.glob(f'{os.getcwd()}/configs/CDMs/WV/{base_name}')
if wvd_files:
wv_device = widevineDevice.load(wvd_files[0])
else:
return {
'status': 'error',
'message': 'No default .wvd file found'
}
else:
base_name = device
if not base_name.endswith(".wvd"):
base_name += ".wvd"
wvd_files = glob.glob(f'{os.getcwd()}/configs/CDMs/{username}/WV/{base_name}')
else:
wvd_files = glob.glob(f'{os.getcwd()}/configs/CDMs/{username}/WV/{base_name}')
if wvd_files:
wv_device = widevineDevice.load(wvd_files[0])
else:
return {
'status': 'error',
'message': f'{base_name} does not exist'
}
except Exception as error:
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: try:
license_json = licence.json() license_json = licence.json()
license_value = find_license_key(license_json) license_value = find_license_key(license_json)
if license_value is not None: wv_cdm.parse_license(wv_session_id, license_value)
cdm.parse_license(session_id, license_value) except Exception as error:
else:
return { return {
"status": "error", 'status': 'error',
"message": f"Could not extract license from JSON: {license_json}", 'message': f'An error occurred parsing license content\n\n{error}\n\n{licence.content}'
} }
except (ValueError, json.JSONDecodeError, AttributeError) as error: returned_keys = ""
return {
"status": "error",
"message": f"An error occurred parsing license content\n\n{error}\n\n{licence.content}",
}
except (ValueError, TypeError, AttributeError) as error:
return {
"status": "error",
"message": f"An error occurred parsing license content\n\n{error}\n\n{licence.content}",
}
# Extract and cache keys
returned_keys, key_err = extract_and_cache_keys(
cdm,
session_id,
cache_to_db,
pssh,
license_url,
headers,
cookies,
challenge,
json_data,
is_widevine=(not is_pr),
)
if returned_keys == "":
return {"status": "error", "message": key_err}
# Close session
try: try:
cdm.close(session_id) keys = list(wv_cdm.get_keys(wv_session_id))
except (IOError, ValueError, AttributeError) as error: except Exception as error:
return { return {
"status": "error", 'status': 'error',
"message": f"An error occurred closing session\n\n{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}'
} }
return {"status": "success", "message": returned_keys}

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: else:
sys.exit( base_name = config["default_wv_cdm"]
f"Download failed, please try again, or place a .{file_ext} file " if not base_name.endswith(".wvd"):
f"in {cdm_dir_path} and specify the name in {CONFIG_PATH}" base_name += ".wvd"
) if os.path.exists(f'{os.getcwd()}/configs/CDMs/WV/{base_name}'):
else:
sys.exit(
f"Place a .{file_ext} file in {cdm_dir_path} and specify the name in {CONFIG_PATH}"
)
else:
base_name = (
cdm_value
if cdm_value.endswith(f".{file_ext}")
else f"{cdm_value}.{file_ext}"
)
file_path = os.path.join(cdm_dir_path, base_name)
if os.path.exists(file_path):
return 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: else:
sys.exit( exit(f"Widevine CDM {base_name} does not exist in {os.getcwd()}/configs/CDMs/WV")
f"Download failed, please try again, or place a .{file_ext} file "
f"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: else:
sys.exit( base_name = config["default_pr_cdm"]
f"Place a .{file_ext} file in {cdm_dir_path} and specify the name in {CONFIG_PATH}" 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,
key_count,
)
from custom_functions.database.user_db import create_user_database
def get_database_config() -> Dict[str, Any]:
"""Get the database configuration from config.yaml."""
try:
config_path = os.path.join(os.getcwd(), "configs", "config.yaml")
with open(config_path, "r", encoding="utf-8") as file:
config = yaml.safe_load(file) config = yaml.safe_load(file)
return config if os.path.exists(f'{os.getcwd()}/databases/key_cache.db'):
except (FileNotFoundError, KeyError, yaml.YAMLError) as e: return
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: else:
print(f"SQLite database found at: {sqlite_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():
def check_for_mariadb_database() -> None: if os.path.exists(f'{os.getcwd()}/databases/users.db'):
"""Check for the MariaDB database and create if needed.""" return
config = get_database_config() else:
database_type = config.get("database_type", "sqlite").lower() from custom_functions.database.user_db import create_user_database
# 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() create_user_database()
print(f"User database created at: {user_db_path}")
def check_for_mariadb_database():
with open(f'{os.getcwd()}/configs/config.yaml', 'r') as file:
config = yaml.safe_load(file)
if config['database_type'].lower() == 'mariadb':
from custom_functions.database.cache_to_db_mariadb import create_database
create_database()
return
else: else:
print(f"User database found at: {user_db_path}") return
def check_for_sql_database():
def check_for_sql_database() -> None:
"""Check for the SQL database based on configuration."""
print("=== Database Check Starting ===")
# Get backend information
backend_info = get_backend_info()
print(f"Database backend: {backend_info['backend']}")
print(f"Using module: {backend_info['module']}")
config = get_database_config()
database_type = config.get("database_type", "sqlite").lower()
# Ensure databases directory exists
os.makedirs(os.path.join(os.getcwd(), "databases"), exist_ok=True)
os.makedirs(os.path.join(os.getcwd(), "databases", "sql"), exist_ok=True)
# Check main database based on type
if database_type == "mariadb":
check_for_mariadb_database()
else: # Default to SQLite
check_for_sqlite_database() check_for_sqlite_database()
check_for_mariadb_database()
# Always check user database (always SQLite)
check_for_user_database() 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,51 +1,42 @@
"""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 return
os.mkdir(os.path.join(os.getcwd(), "configs")) else:
os.mkdir(f'{os.getcwd()}/configs')
return 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 return
os.mkdir(os.path.join(os.getcwd(), "databases")) else:
os.mkdir(os.path.join(os.getcwd(), "databases", "sql")) os.mkdir(f'{os.getcwd()}/databases')
os.mkdir(f'{os.getcwd()}/databases/sql')
return 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 return
os.mkdir(os.path.join(os.getcwd(), "configs", "CDMs")) else:
os.mkdir(f'{os.getcwd()}/configs/CDMs')
return 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 return
os.mkdir(os.path.join(os.getcwd(), "configs", "CDMs", "WV")) else:
os.mkdir(f'{os.getcwd()}/configs/CDMs/WV')
return 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 return
os.mkdir(os.path.join(os.getcwd(), "configs", "CDMs", "PR")) else:
os.mkdir(f'{os.getcwd()}/configs/CDMs/PR')
return 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()

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,55 +54,30 @@ 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): "
)
.strip()
.upper()
)
if user_input == "Y":
print("Installing packages from requirements.txt...") print("Installing packages from requirements.txt...")
subprocess.check_call( subprocess.check_call([sys.executable, "-m", "pip", "install", "-r", "requirements.txt"])
[sys.executable, "-m", "pip", "install", "-r", "requirements.txt"]
)
print("Installation complete.") print("Installation complete.")
break break
if user_input == "N": elif user_input == 'N':
print("Dependencies required, please install them and run again.") print("Dependencies required, please install them and run again.")
sys.exit() sys.exit()
else: else:
print("Invalid input. Please enter 'Y' to install or 'N' to exit.") 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()

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
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: # Now export the table
if col.lower() in ("headers", "cookies"): cursor.execute('SELECT * FROM licenses')
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() rows = cursor.fetchall()
column_names = [desc[0] for desc in cursor.description]
# Dump to SQL-like format # Dump to SQL-like format
output = StringIO() output = StringIO()
output.write("-- Dump of `licenses` table (Headers and Cookies are NULL)\n") output.write(f"-- Dump of `licenses` table\n")
for row in rows: for row in rows:
values = ", ".join( values = ', '.join(f"'{str(v).replace('\'', '\\\'')}'" if v is not None else 'NULL' for v in row)
f"'{str(v).replace('\'', '\\\'')}'" if v is not None else "NULL" output.write(f"INSERT INTO licenses ({', '.join(column_names)}) VALUES ({values});\n")
for v in row
)
output.write(
f"INSERT INTO licenses ({', '.join(columns)}) VALUES ({values});\n"
)
# Write to a temp file for download # Write to a temp file for download
temp_dir = tempfile.gettempdir() temp_dir = tempfile.gettempdir()
temp_path = os.path.join(temp_dir, "key_cache.sql") temp_path = os.path.join(temp_dir, 'key_cache.sql')
with open(temp_path, "w", encoding="utf-8") as f: with open(temp_path, 'w', encoding='utf-8') as f:
f.write(output.getvalue()) f.write(output.getvalue())
@after_this_request return send_file(temp_path, as_attachment=True, download_name='licenses_dump.sql')
def remove_file(response):
try:
os.remove(temp_path)
except Exception:
pass
return response
return send_file(
temp_path, as_attachment=True, download_name="licenses_dump.sql"
)
except mysql.connector.Error as err: except mysql.connector.Error as err:
return {"error": str(err)}, 500 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=""):
value = api_request_data.get(key, default)
return value if value != "" else default
api_request_pssh = get_field("pssh")
api_request_licurl = get_field("licurl")
api_request_proxy = get_field("proxy")
api_request_headers = get_field("headers")
api_request_cookies = get_field("cookies")
api_request_data_func = get_field("data")
# Device logic
device = get_field("device", "public")
if device in [
"default",
"CDRM-Project Public Widevine CDM",
"CDRM-Project Public PlayReady CDM",
"",
None,
]:
api_request_device = "public"
else: else:
api_request_device = device api_request_pssh = api_request_data['pssh']
else:
username = "" api_request_pssh = None
if api_request_device != "public": if 'licurl' in api_request_data:
username = session.get("username") if api_request_data['licurl'] == '':
api_request_licurl = None
else:
api_request_licurl = api_request_data['licurl']
else:
api_request_licurl = None
if 'proxy' in api_request_data:
if api_request_data['proxy'] == '':
api_request_proxy = None
else:
api_request_proxy = api_request_data['proxy']
else:
api_request_proxy = None
if 'headers' in api_request_data:
if api_request_data['headers'] == '':
api_request_headers = None
else:
api_request_headers = api_request_data['headers']
else:
api_request_headers = None
if 'cookies' in api_request_data:
if api_request_data['cookies'] == '':
api_request_cookies = None
else:
api_request_cookies = api_request_data['cookies']
else:
api_request_cookies = None
if 'data' in api_request_data:
if api_request_data['data'] == '':
api_request_data_func = None
else:
api_request_data_func = api_request_data['data']
else: api_request_data_func = None
if 'device' in api_request_data:
if api_request_data['device'] == 'default' or api_request_data['device'] == 'CDRM-Project Public Widevine CDM' or api_request_data['device'] == 'CDRM-Project Public PlayReady CDM':
api_request_device = 'public'
else:
api_request_device = api_request_data['device']
else:
api_request_device = 'public'
username = None
if api_request_device != 'public':
username = session.get('username')
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
# Check required fields
for required_field in ["username", "password"]:
if required_field not in data: if required_field not in data:
return jsonify({"error": f"Missing required field: {required_field}"}), 400 return jsonify({
'error': f'Missing required field: {required_field}'
username = data["username"].lower() })
password = data["password"] if add_user(data['username'], data['password']):
api_key = str(uuid.uuid4()) return jsonify({
'message': 'User successfully registered!'
# 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': 'User already exists!'
return jsonify({"error": "Password must be 8-128 characters."}), 400 })
else:
# Validate username and password return jsonify({
if not USERNAME_REGEX.fullmatch(username): 'error': 'Method not supported'
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,236 +1,97 @@
"""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)
session_id = cdm.open()
return jsonify({
'message': 'Success',
'data': {
'session_id': session_id.hex(),
'device': {
'security_level': cdm.security_level
} }
}
})
# Default device logic @remotecdm_pr_bp.route('/remotecdm/playready/<device>/close/<session_id>', methods=['GET'])
if str(device).lower() == config["default_pr_cdm"].lower():
pr_device = PlayReadyDevice.load(
os.path.join(
os.getcwd(), "configs", "CDMs", "PR", config["default_pr_cdm"] + ".prd"
)
)
cdm = current_app.config["CDM"] = PlayReadyCDM.from_device(pr_device)
session_id = cdm.open()
return make_response(
"Success",
"Successfully opened the PlayReady CDM session",
{
"session_id": session_id.hex(),
"device": {"security_level": cdm.security_level},
},
http_status=200,
)
# User device logic
api_key = request.headers.get("X-Secret-Key")
if api_key and str(device).lower() != config["default_pr_cdm"].lower():
user = fetch_username_by_api_key(api_key=api_key)
safe_username = sanitize_username(user)
if user and user_allowed_to_use_device(device=device, username=user):
pr_device = PlayReadyDevice.load(
os.path.join(
os.getcwd(),
"configs",
"CDMs",
"users_uploaded",
safe_username,
"PR",
device + ".prd",
)
)
cdm = current_app.config["CDM"] = PlayReadyCDM.from_device(pr_device)
session_id = cdm.open()
return make_response(
"Success",
"Successfully opened the PlayReady CDM session",
{
"session_id": session_id.hex(),
"device": {"security_level": cdm.security_level},
},
http_status=200,
)
return make_response("Failed", unauthorized_msg, http_status=403)
return make_response("Failed", unauthorized_msg, http_status=403)
def get_cdm_or_error(device):
"""Get the CDM or return an error response."""
cdm = current_app.config.get("CDM")
if not cdm:
return make_response(
"Error",
f'No CDM session for "{device}" has been opened yet. No session to use',
http_status=400,
)
return cdm
@remotecdm_pr_bp.route(
"/remotecdm/playready/<device>/close/<session_id>", methods=["GET"]
)
def remote_cdm_playready_close(device, session_id): 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 })
cdm = current_app.config["CDM"]
session_id = bytes.fromhex(body["session_id"]) session_id = bytes.fromhex(body["session_id"])
init_data = body["init_data"] init_data = body["init_data"]
if not init_data.startswith("<WRMHEADER"): if not init_data.startswith("<WRMHEADER"):
@ -238,96 +99,89 @@ def remote_cdm_playready_get_license_challenge(device):
pssh = PlayReadyPSSH(init_data) pssh = PlayReadyPSSH(init_data)
if pssh.wrm_headers: if pssh.wrm_headers:
init_data = pssh.wrm_headers[0] init_data = pssh.wrm_headers[0]
except InvalidPssh as error: except InvalidPssh as e:
return make_response( return jsonify({
"Error", 'message': f'Unable to parse base64 PSSH, {e}'
f"Unable to parse base64 PSSH, {error}", })
http_status=400,
)
try: try:
license_request = cdm.get_license_challenge( license_request = cdm.get_license_challenge(
session_id=session_id, wrm_header=init_data session_id=session_id,
wrm_header=init_data
) )
except InvalidSession: except InvalidSession:
return make_response( return jsonify({
"Error", 'message': f"Invalid Session ID '{session_id.hex()}', it may have expired."
f"Invalid Session ID '{session_id.hex()}', it may have expired.", })
http_status=400, except Exception as e:
) return jsonify({
except ValueError as error: 'message': f'Error, {e}'
return make_response( })
"Error", return jsonify({
f"Invalid License, {error}", 'message': 'success',
http_status=400, 'data': {
) 'challenge': license_request
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"]
if not cdm:
return jsonify({
'message': f"No Cdm session for {device} has been opened yet. No session to use."
})
session_id = bytes.fromhex(body["session_id"]) session_id = bytes.fromhex(body["session_id"])
license_message = body["license_message"] license_message = body["license_message"]
if is_base64(license_message):
license_message = base64.b64decode(license_message).decode("utf-8")
try: try:
cdm.parse_license(session_id, license_message) cdm.parse_license(session_id, license_message)
except InvalidSession: except InvalidSession:
return make_response( return jsonify({
"Error", 'message': f"Invalid Session ID '{session_id.hex()}', it may have expired."
f"Invalid Session ID '{session_id.hex()}', it may have expired.", })
http_status=400,
)
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: except Exception as e:
return make_response( return jsonify({
"Error", 'message': f"Error, {e}"
f"Error, {e}", })
http_status=400, return jsonify({
) 'message': 'Successfully parsed and loaded the Keys from the License message'
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({
'message': f'Missing required field "{required_field}" in JSON body'
})
session_id = bytes.fromhex(body["session_id"]) session_id = bytes.fromhex(body["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({
'message': f"Missing required field '{required_field}' in JSON body."
})
try: try:
keys = cdm.get_keys(session_id) keys = cdm.get_keys(session_id)
except InvalidSession: except InvalidSession:
return make_response( return jsonify({
"Error", 'message': f"Invalid Session ID '{session_id.hex()}', it may have expired."
f"Invalid Session ID '{session_id.hex()}', it may have expired.", })
http_status=400, except Exception as e:
) return jsonify({
'message': f"Error, {e}"
})
keys_json = [ keys_json = [
{ {
"key_id": key.key_id.hex, "key_id": key.key_id.hex,
@ -338,9 +192,9 @@ def remote_cdm_playready_get_keys(device):
} }
for key in keys for key in keys
] ]
return make_response( return jsonify({
"Success", 'message': 'success',
"Successfully got the Keys", 'data': {
{"keys": keys_json}, 'keys': keys_json
http_status=200, }
) })

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) cdm = widevineCDM.from_device(device)
return jsonify( return jsonify({
{ 'device_type': cdm.device_type.name,
"device_type": cdm.device_type.name, 'system_id': cdm.system_id,
"system_id": cdm.system_id, 'security_level': cdm.security_level,
"security_level": cdm.security_level, 'host': f'{config["fqdn"]}/remotecdm/widevine',
"host": f'{config["fqdn"]}/remotecdm/widevine', 'secret': f'{config["remote_cdm_secret"]}',
"secret": f'{config["remote_cdm_secret"]}', 'device_name': f'{base_name}'
"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, })
) else:
if ( return jsonify({
request.headers["X-Secret-Key"] 'status': 400,
and str(device).lower() != config["default_wv_cdm"].lower() 'message': 'Unauthorized'
): })
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( @remotecdm_wv_bp.route('/remotecdm/widevine/<device>/close/<session_id>', methods=['GET'])
"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): def remote_cdm_widevine_close(device, session_id):
"""Handle the remote device Widevine close.""" if str(device).lower() == config['default_wv_cdm'].lower():
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({
'status': 200,
'message': f'Successfully closed Session "{session_id.hex()}".',
})
else:
return jsonify({
'status': 400,
'message': f'Unauthorized'
})
return make_response( @remotecdm_wv_bp.route('/remotecdm/widevine/<device>/set_service_certificate', methods=['POST'])
"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): def remote_cdm_widevine_set_service_certificate(device):
"""Handle the remote device Widevine set service certificate.""" 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", "certificate")) for required_field in ("session_id", "certificate"):
if missing_field: if required_field == "certificate":
return missing_field 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"]) session_id = bytes.fromhex(body["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 session for "{device}" has been opened yet. No session to use'
})
certificate = body["certificate"] certificate = body["certificate"]
try: try:
provider_id = cdm.set_service_certificate(session_id, certificate) provider_id = cdm.set_service_certificate(session_id, certificate)
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, })
)
except DecodeError as error: except DecodeError as error:
return make_response( return jsonify({
"Error", 'status': 400,
f"Invalid Service Certificate, {error}", 'message': f'Invalid Service Certificate, {error}'
http_status=400, })
)
except SignatureMismatch: except SignatureMismatch:
return make_response( return jsonify({
"Error", 'status': 400,
"Signature Validation failed on the Service Certificate, rejecting", 'message': 'Signature Validation failed on the Service Certificate, rejecting'
http_status=400, })
) 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'
})
return make_response( @remotecdm_wv_bp.route('/remotecdm/widevine/<device>/get_service_certificate', methods=['POST'])
"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): def remote_cdm_widevine_get_service_certificate(device):
"""Handle the remote device Widevine get service certificate.""" 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"]) session_id = bytes.fromhex(body["session_id"])
cdm = get_cdm_or_error(device) cdm = current_app.config["CDM"]
if isinstance(cdm, tuple): # error response
return cdm if not cdm:
return jsonify({
'status': 400,
'message': f'No CDM session for "{device}" has been opened yet. No session to use'
})
try: try:
service_certificate = cdm.get_service_certificate(session_id) service_certificate = cdm.get_service_certificate(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, })
)
if service_certificate: if service_certificate:
service_certificate_b64 = base64.b64encode( service_certificate_b64 = base64.b64encode(service_certificate.SerializeToString()).decode()
service_certificate.SerializeToString()
).decode()
else: else:
service_certificate_b64 = None service_certificate_b64 = None
return make_response( return jsonify({
"Success", 'status': 200,
"Successfully got the Service Certificate", 'message': 'Successfully got the Service Certificate',
{"service_certificate": service_certificate_b64}, 'data': {
http_status=200, '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'])
@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"]) session_id = bytes.fromhex(body["session_id"])
privacy_mode = body.get("privacy_mode", True) privacy_mode = body.get("privacy_mode", True)
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 session for "{device}" has been opened yet. No session to use'
})
if current_app.config.get("force_privacy_mode"): if current_app.config.get("force_privacy_mode"):
privacy_mode = True privacy_mode = True
if not cdm.get_service_certificate(session_id): if not cdm.get_service_certificate(session_id):
return ( return jsonify({
jsonify( 'status': 403,
{ 'message': 'No Service Certificate set but Privacy Mode is Enforced.'
"status": 403, })
"message": "No Service Certificate set but Privacy Mode is Enforced.",
}
),
403,
)
current_app.config["pssh"] = body["init_data"] current_app.config['pssh'] = body['init_data']
init_data = widevinePSSH(body["init_data"]) init_data = widevinePSSH(body['init_data'])
try: try:
license_request = cdm.get_license_challenge( license_request = cdm.get_license_challenge(
session_id=session_id, session_id=session_id,
pssh=init_data, pssh=init_data,
license_type=license_type, license_type=license_type,
privacy_mode=privacy_mode, privacy_mode=privacy_mode
) )
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, })
)
except InvalidInitData as error: except InvalidInitData as error:
return make_response( return jsonify({
"Error", 'status': 400,
f"Invalid Init Data, {error}", 'message': f'Invalid Init Data, {error}'
http_status=400, })
)
except InvalidLicenseType: except InvalidLicenseType:
return make_response( return jsonify({
"Error", 'status': 400,
f"Invalid License Type {license_type}", 'message': f'Invalid License Type {license_type}'
http_status=400, })
) return jsonify({
'status': 200,
return make_response( 'message': 'Success',
"Success", 'data': {
"Successfully got the License Challenge", 'challenge_b64': base64.b64encode(license_request).decode()
{"challenge_b64": base64.b64encode(license_request).decode()}, }
http_status=200, })
) else:
return jsonify({
'status': 400,
'message': f'Unauthorized'
})
@remotecdm_wv_bp.route("/remotecdm/widevine/<device>/parse_license", methods=["POST"]) @remotecdm_wv_bp.route('/remotecdm/widevine/<device>/parse_license', methods=['POST'])
def remote_cdm_widevine_parse_license(device): def remote_cdm_widevine_parse_license(device):
"""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"]) session_id = bytes.fromhex(body["session_id"])
cdm = current_app.config["CDM"]
cdm = get_cdm_or_error(device) if not cdm:
if isinstance(cdm, tuple): # error response return jsonify({
return cdm 'status': 400,
'message': f'No CDM session for "{device}" has been opened yet. No session to use'
})
try: try:
cdm.parse_license(session_id, body["license_message"]) cdm.parse_license(session_id, body['license_message'])
except InvalidLicenseMessage as error: except InvalidLicenseMessage as error:
return make_response( return jsonify({
"Error", 'status': 400,
f"Invalid License Message, {error}", 'message': f'Invalid License Message, {error}'
http_status=400, })
)
except InvalidContext as error: except InvalidContext as error:
return jsonify({"status": 400, "message": f"Invalid Context, {error}"}), 400 return jsonify({
'status': 400,
'message': f'Invalid Context, {error}'
})
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, })
)
except SignatureMismatch: except SignatureMismatch:
return make_response( return jsonify({
"Error", 'status': 400,
"Signature Validation failed on the License Message, rejecting.", 'message': f'Signature Validation failed on the License Message, rejecting.'
http_status=400, })
) return jsonify({
'status': 200,
'message': 'Successfully parsed and loaded the Keys from the License message.',
})
else:
return jsonify({
'status': 400,
'message': 'Unauthorized'
})
return make_response( @remotecdm_wv_bp.route('/remotecdm/widevine/<device>/get_keys/<key_type>', methods=['POST'])
"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"]) session_id = bytes.fromhex(body["session_id"])
if key_type == "ALL": key_type: Optional[str] = key_type
if key_type == 'ALL':
key_type = None key_type = None
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 session for "{device}" has been opened yet. No session to use'
})
try: try:
keys = cdm.get_keys(session_id, key_type) keys = cdm.get_keys(session_id, key_type)
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, })
)
except ValueError as error: except ValueError as error:
return make_response( return jsonify({
"Error", 'status': 400,
f'The Key Type value "{key_type}" is invalid, {error}', 'message': f'The Key Type value "{key_type}" is invalid, {error}'
http_status=400, })
)
keys_json = [ keys_json = [
{ {
"key_id": key.kid.hex, "key_id": key.kid.hex,
"key": key.key.hex(), "key": key.key.hex(),
"type": key.type, "type": key.type,
"permissions": key.permissions, "permissions": key.permissions
} }
for key in keys for key in keys
if not key_type or key.type == key_type if not key_type or key.type == key_type
] ]
for entry in keys_json: for entry in keys_json:
if entry["type"] != "SIGNING": if config['database_type'].lower() != 'mariadb':
cache_to_db( from custom_functions.database.cache_to_db_sqlite import cache_to_db
pssh=str(current_app.config["pssh"]), elif config['database_type'].lower() == 'mariadb':
kid=entry["key_id"], from custom_functions.database.cache_to_db_mariadb import cache_to_db
key=entry["key"], if entry['type'] != 'SIGNING':
) cache_to_db(pssh=str(current_app.config['pssh']), kid=entry['key_id'], key=entry['key'])
return make_response(
"Success", return jsonify({
"Successfully got the Keys", 'status': 200,
{"keys": keys_json}, 'message': 'Success',
http_status=200, 'data': {
) 'keys': keys_json
}
})

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