Reupload after github DMCA
6
.gitignore
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
*.pyc
|
||||
*.prd
|
||||
*.wvd
|
||||
*.db
|
||||
.idea/
|
||||
configs/config.yaml
|
30
README.md
@ -1,3 +1,29 @@
|
||||
# CDRM-Project
|
||||
## CDRM-Project
|
||||
   
|
||||
## What is this?
|
||||
|
||||
An open source web application written in python to decrypt Widevine and PlayReady protected content.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- [Python](https://www.python.org/downloads/) with PIP installed
|
||||
|
||||
> Python 3.13 was used at the time of writing
|
||||
|
||||
## Installation
|
||||
|
||||
- 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
|
||||
|
||||
> Windows - change directory into the `Scripts` directory then `activate.bat`
|
||||
>
|
||||
> Linux - `source bin/activate`
|
||||
|
||||
- Extract CDRM-Project 2.0 git contents into the newly created `CDRM-Project` folder
|
||||
- Install python dependencies `pip install -r requirements.txt`
|
||||
- (Optional) Place your .WVD file into `/configs/CDMs/WV`
|
||||
- (Optional) Place your .PRD file into `/configs/CDMs/PR`
|
||||
- Run the application `python main.py`
|
||||
|
||||
Self Hosted Widevine & Playready web application written in Python/JavaScript utilizing the Flask/Tailwind Framework and ReactJS library.
|
23
cdrm-frontend/.gitignore
vendored
Normal file
@ -0,0 +1,23 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
8
cdrm-frontend/README.md
Normal file
@ -0,0 +1,8 @@
|
||||
# React + Vite
|
||||
|
||||
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
|
||||
|
||||
Currently, two official plugins are available:
|
||||
|
||||
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
|
||||
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
|
1
cdrm-frontend/dist/assets/index-0Rv9u7Qs.css
vendored
Normal file
160
cdrm-frontend/dist/assets/index-BCBsxJZZ.js
vendored
Normal file
5
cdrm-frontend/dist/discord.svg
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="512" cy="512" r="512" style="fill:#5865f2"/>
|
||||
<path d="M689.43 349a422.21 422.21 0 0 0-104.22-32.32 1.58 1.58 0 0 0-1.68.79 294.11 294.11 0 0 0-13 26.66 389.78 389.78 0 0 0-117.05 0 269.75 269.75 0 0 0-13.18-26.66 1.64 1.64 0 0 0-1.68-.79A421 421 0 0 0 334.44 349a1.49 1.49 0 0 0-.69.59c-66.37 99.17-84.55 195.9-75.63 291.41a1.76 1.76 0 0 0 .67 1.2 424.58 424.58 0 0 0 127.85 64.63 1.66 1.66 0 0 0 1.8-.59 303.45 303.45 0 0 0 26.15-42.54 1.62 1.62 0 0 0-.89-2.25 279.6 279.6 0 0 1-39.94-19 1.64 1.64 0 0 1-.16-2.72c2.68-2 5.37-4.1 7.93-6.22a1.58 1.58 0 0 1 1.65-.22c83.79 38.26 174.51 38.26 257.31 0a1.58 1.58 0 0 1 1.68.2c2.56 2.11 5.25 4.23 8 6.24a1.64 1.64 0 0 1-.14 2.72 262.37 262.37 0 0 1-40 19 1.63 1.63 0 0 0-.87 2.28 340.72 340.72 0 0 0 26.13 42.52 1.62 1.62 0 0 0 1.8.61 423.17 423.17 0 0 0 128-64.63 1.64 1.64 0 0 0 .67-1.18c10.68-110.44-17.88-206.38-75.7-291.42a1.3 1.3 0 0 0-.63-.63zM427.09 582.85c-25.23 0-46-23.16-46-51.6s20.38-51.6 46-51.6c25.83 0 46.42 23.36 46 51.6.02 28.44-20.37 51.6-46 51.6zm170.13 0c-25.23 0-46-23.16-46-51.6s20.38-51.6 46-51.6c25.83 0 46.42 23.36 46 51.6.01 28.44-20.17 51.6-46 51.6z" style="fill:#fff"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.3 KiB |
9
cdrm-frontend/dist/docu_logo.svg
vendored
Normal file
@ -0,0 +1,9 @@
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Transformed by: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 0 1024 1024" class="icon" version="1.1" xmlns="http://www.w3.org/2000/svg" fill="#000000">
|
||||
<g id="SVGRepo_bgCarrier" stroke-width="0"/>
|
||||
<g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<g id="SVGRepo_iconCarrier">
|
||||
<path d="M877.685565 727.913127l-0.584863-0.365539a32.898541 32.898541 0 0 1-8.041866-46.423497 411.816631 411.816631 0 1 0-141.829267 145.777092c14.621574-8.992268 33.62962-5.117551 43.645398 8.772944l0.146216 0.073108a30.412874 30.412874 0 0 1-7.968758 43.206751l-6.141061 4.020933a475.201154 475.201154 0 1 1 163.615412-164.419599 29.974227 29.974227 0 0 1-42.841211 9.357807z m-537.342843-398.584106c7.164571-7.091463 24.71046-9.650239 33.26408 0 10.600641 11.185504 7.164571 29.462472 0 37.138798l-110.612207 107.468569L370.901811 576.14119c7.164571 7.091463 8.114974 27.342343 0 35.384209-9.796455 9.723347-29.828011 8.188081-36.480827 1.535265L208.309909 487.388236a18.423183 18.423183 0 0 1 0-25.953294l132.032813-132.032813z m343.314556 0l132.032813 132.032813a18.423183 18.423183 0 0 1 0 25.953294L689.652124 613.133772c-6.652816 6.579708-25.587754 10.746857-36.553935 0-10.30821-10.235102-7.091463-31.290168 0-38.381632l108.345863-100.669537-111.855041-108.638294c-7.164571-7.676326-9.504023-26.611265 0-36.04218 9.284699-9.138484 26.903696-7.091463 34.068267 0z m-135.54199-26.318833c3.582286-9.504023 21.347498-15.498868 32.679217-11.258612 10.819965 4.020933 17.180349 19.008046 14.256035 28.512069l-119.896906 329.716493c-3.509178 9.504023-20.616419 13.305632-30.193551 9.723347-10.161994-3.509178-21.201282-17.545889-17.545888-26.976804l120.627985-329.716493z" fill="#ffffff"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.8 KiB |
BIN
cdrm-frontend/dist/favico.png
vendored
Normal file
After Width: | Height: | Size: 862 B |
19
cdrm-frontend/dist/github.svg
vendored
Normal file
@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 0 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
|
||||
<title>github [#142]</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<defs>
|
||||
|
||||
</defs>
|
||||
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="Dribbble-Light-Preview" transform="translate(-140.000000, -7559.000000)" fill="#000000">
|
||||
<g id="icons" transform="translate(56.000000, 160.000000)">
|
||||
<path d="M94,7399 C99.523,7399 104,7403.59 104,7409.253 C104,7413.782 101.138,7417.624 97.167,7418.981 C96.66,7419.082 96.48,7418.762 96.48,7418.489 C96.48,7418.151 96.492,7417.047 96.492,7415.675 C96.492,7414.719 96.172,7414.095 95.813,7413.777 C98.04,7413.523 100.38,7412.656 100.38,7408.718 C100.38,7407.598 99.992,7406.684 99.35,7405.966 C99.454,7405.707 99.797,7404.664 99.252,7403.252 C99.252,7403.252 98.414,7402.977 96.505,7404.303 C95.706,7404.076 94.85,7403.962 94,7403.958 C93.15,7403.962 92.295,7404.076 91.497,7404.303 C89.586,7402.977 88.746,7403.252 88.746,7403.252 C88.203,7404.664 88.546,7405.707 88.649,7405.966 C88.01,7406.684 87.619,7407.598 87.619,7408.718 C87.619,7412.646 89.954,7413.526 92.175,7413.785 C91.889,7414.041 91.63,7414.493 91.54,7415.156 C90.97,7415.418 89.522,7415.871 88.63,7414.304 C88.63,7414.304 88.101,7413.319 87.097,7413.247 C87.097,7413.247 86.122,7413.234 87.029,7413.87 C87.029,7413.87 87.684,7414.185 88.139,7415.37 C88.139,7415.37 88.726,7417.2 91.508,7416.58 C91.513,7417.437 91.522,7418.245 91.522,7418.489 C91.522,7418.76 91.338,7419.077 90.839,7418.982 C86.865,7417.627 84,7413.783 84,7409.253 C84,7403.59 88.478,7399 94,7399" id="github-[#142]">
|
||||
|
||||
</path>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.9 KiB |
1
cdrm-frontend/dist/home.svg
vendored
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0,0,256,256" width="50px" height="50px" fill-rule="nonzero"><g fill="#ffffff" fill-rule="nonzero" stroke="none" stroke-width="1" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="10" stroke-dasharray="" stroke-dashoffset="0" font-family="none" font-weight="none" font-size="none" text-anchor="none" style="mix-blend-mode: normal"><g transform="scale(5.12,5.12)"><path d="M24.96289,1.05469c-0.20987,0.00724 -0.41214,0.08036 -0.57812,0.20898l-23,17.94727c-0.43579,0.33978 -0.51361,0.96851 -0.17383,1.4043c0.33978,0.43579 0.96851,0.51361 1.4043,0.17383l1.38477,-1.08008v26.29102c0.00006,0.55226 0.44774,0.99994 1,1h13.83203c0.10799,0.01785 0.21818,0.01785 0.32617,0h11.67383c0.10799,0.01785 0.21818,0.01785 0.32617,0h13.8418c0.55226,-0.00006 0.99994,-0.44774 1,-1v-26.29102l1.38477,1.08008c0.2819,0.21983 0.65967,0.27257 0.991,0.13833c0.33133,-0.13423 0.56586,-0.43504 0.61526,-0.7891c0.0494,-0.35406 -0.09386,-0.70757 -0.37579,-0.92736l-7.61523,-5.94141v-7.26953h-6v2.58594l-9.38477,-7.32227c-0.18607,-0.14428 -0.41707,-0.21828 -0.65234,-0.20898zM25,3.32227l19,14.82617v26.85156h-12v-19h-14v19h-12v-26.85156zM37,8h2v3.70898l-2,-1.5625zM20,28h10v17h-10z"></path></g></g></svg>
|
After Width: | Height: | Size: 1.3 KiB |
21
cdrm-frontend/dist/index.html
vendored
Normal file
@ -0,0 +1,21 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<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="Decrypt Widevine and PlayReady protected content." />
|
||||
<meta name="keywords" content="CDRM, Widevine, PlayReady, DRM, Decrypt, CDM, CDM-Project, CDRM-Project, TPD94, Decryption" />
|
||||
<meta property='og:title' content='CDRM-Project' />
|
||||
<meta property='og:description' content='Decrypt Widevine & PlayReady Content' />
|
||||
<meta property='og:image' content='https://cdrm-project.com/lockforog.png' />
|
||||
<meta property='og:url' content='https://cdrm-project.com/' />
|
||||
<meta property='og:locale' content='en_US' />
|
||||
<title>CDRM-Project</title>
|
||||
<script type="module" crossorigin src="/assets/index-BCBsxJZZ.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-0Rv9u7Qs.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
</html>
|
BIN
cdrm-frontend/dist/lockforog.png
vendored
Normal file
After Width: | Height: | Size: 1.4 MiB |
1
cdrm-frontend/dist/logo.svg
vendored
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-0.5 13.100000381469727 321.79998779296875 36.69999694824219" data-asc="0.984"><g fill="#ffffff"><g fill="#ffffff" transform="translate(0, 0)"><path d="M13.15 49.80Q10.10 49.80 7.60 48.58Q5.10 47.35 3.28 45.10Q1.45 42.85 0.47 39.68Q-0.50 36.50-0.50 32.60Q-0.50 27.40 1.25 23.58Q3 19.75 6.13 17.68Q9.25 15.60 13.45 15.60Q16.30 15.60 18.45 16.80Q20.60 18 21.85 19.50L20.85 20.60Q19.50 19 17.65 18.05Q15.80 17.10 13.45 17.10Q9.65 17.10 6.90 19Q4.15 20.90 2.67 24.38Q1.20 27.85 1.20 32.60Q1.20 37.35 2.67 40.88Q4.15 44.40 6.85 46.35Q9.55 48.30 13.25 48.30Q15.90 48.30 17.95 47.25Q20.00 46.20 21.85 44.10L22.85 45.10Q21.00 47.25 18.70 48.53Q16.40 49.80 13.15 49.80ZM30.80 49.20L30.80 16.20L38.20 16.20Q43.30 16.20 46.58 18.25Q49.85 20.30 51.42 24Q53 27.70 53 32.60Q53 37.50 51.42 41.25Q49.85 45 46.58 47.10Q43.30 49.20 38.20 49.20L30.80 49.20M32.40 47.80L38 47.80Q42.70 47.80 45.63 45.80Q48.55 43.80 49.92 40.38Q51.30 36.95 51.30 32.60Q51.30 28.25 49.92 24.85Q48.55 21.45 45.63 19.53Q42.70 17.60 38 17.60L32.40 17.60L32.40 47.80ZM62 49.20L62 16.20L71.10 16.20Q74.35 16.20 76.72 17.05Q79.10 17.90 80.40 19.78Q81.70 21.65 81.70 24.70Q81.70 27.60 80.40 29.58Q79.10 31.55 76.72 32.58Q74.35 33.60 71.10 33.60L63.60 33.60L63.60 49.20L62 49.20M63.60 32.20L70.50 32.20Q75.10 32.20 77.55 30.35Q80 28.50 80 24.70Q80 20.80 77.55 19.20Q75.10 17.60 70.50 17.60L63.60 17.60L63.60 32.20M70.70 33.10L72.50 33L82.20 49.20L80.30 49.20L70.70 33.10ZM90 49.20L90 16.20L92.30 16.20L98.50 32.20L100.70 38.10L100.90 38.10L103 32.20L109.30 16.20L111.60 16.20L111.60 49.20L110 49.20L110 24.45Q110 23.70 110.03 22.88Q110.05 22.05 110.08 21.25Q110.10 20.45 110.13 19.63Q110.15 18.80 110.15 18.05L110 18.05L108.20 23.40L101.45 40.40L100.10 40.40L93.40 23.40L91.45 18.05L91.35 18.05Q91.35 18.80 91.40 19.63Q91.45 20.45 91.47 21.25Q91.50 22.05 91.52 22.88Q91.55 23.70 91.55 24.45L91.55 49.20L90 49.20ZM152 49.20L152 16.20L161.10 16.20Q164.35 16.20 166.73 17.05Q169.10 17.90 170.40 19.78Q171.70 21.65 171.70 24.70Q171.70 27.60 170.40 29.58Q169.10 31.55 166.73 32.58Q164.35 33.60 161.10 33.60L153.60 33.60L153.60 49.20L152 49.20M153.60 32.20L160.50 32.20Q165.10 32.20 167.55 30.35Q170 28.50 170 24.70Q170 20.80 167.55 19.20Q165.10 17.60 160.50 17.60L153.60 17.60L153.60 32.20M160.70 33.10L162.50 33L172.20 49.20L170.30 49.20L160.70 33.10ZM192.30 49.80Q188.90 49.80 186.05 48.30Q183.20 46.80 181.50 44Q179.80 41.20 179.80 37.30Q179.80 33.40 181.48 30.58Q183.15 27.75 185.88 26.23Q188.60 24.70 191.70 24.70Q194.90 24.70 197.28 26Q199.65 27.30 200.98 29.78Q202.30 32.25 202.30 35.70Q202.30 36.05 202.30 36.45Q202.30 36.85 202.20 37.30L180.30 37.30L180.30 35.90L201.80 35.90L200.90 36.70Q200.90 31.30 198.33 28.70Q195.75 26.10 191.70 26.10Q189.10 26.10 186.75 27.43Q184.40 28.75 182.90 31.23Q181.40 33.70 181.40 37.20Q181.40 40.80 182.93 43.30Q184.45 45.80 186.93 47.10Q189.40 48.40 192.30 48.40Q194.85 48.40 196.88 47.65Q198.90 46.90 200.60 45.70L201.30 47Q199.75 48 197.65 48.90Q195.55 49.80 192.30 49.80ZM221.10 49.80Q219.25 49.80 217 48.90Q214.75 48 212.90 46.50L212.80 46.50L212.60 49.20L211.30 49.20L211.30 13.10L212.80 13.10L212.80 23.80L212.70 29.10L212.80 29.10Q214.70 27.15 217.20 25.93Q219.70 24.70 222.10 24.70Q225.55 24.70 227.80 26.20Q230.05 27.70 231.18 30.45Q232.30 33.20 232.30 36.90Q232.30 40.95 230.75 43.83Q229.20 46.70 226.65 48.25Q224.10 49.80 221.10 49.80M221.20 48.40Q223.95 48.40 226.10 46.93Q228.25 45.45 229.48 42.85Q230.70 40.25 230.70 36.90Q230.70 33.85 229.83 31.40Q228.95 28.95 227.05 27.53Q225.15 26.10 222 26.10Q220 26.10 217.60 27.25Q215.20 28.40 212.80 30.80L212.80 44.60Q215.05 46.65 217.35 47.53Q219.65 48.40 221.20 48.40ZM250.80 49.80Q247.65 49.80 245.05 48.33Q242.45 46.85 240.88 44.05Q239.30 41.25 239.30 37.30Q239.30 33.30 240.88 30.48Q242.45 27.65 245.05 26.18Q247.65 24.70 250.80 24.70Q253.95 24.70 256.55 26.18Q259.15 27.65 260.73 30.48Q262.30 33.30 262.30 37.30Q262.30 41.25 260.73 44.05Q259.15 46.85 256.55 48.33Q253.95 49.80 250.80 49.80M250.80 48.40Q253.65 48.40 255.90 47Q258.15 45.60 259.43 43.10Q260.70 40.60 260.70 37.30Q260.70 34 259.43 31.48Q258.15 28.95 255.90 27.53Q253.65 26.10 250.80 26.10Q247.95 26.10 245.73 27.53Q243.50 28.95 242.20 31.48Q240.90 34 240.90 37.30Q240.90 40.60 242.20 43.10Q243.50 45.60 245.73 47Q247.95 48.40 250.80 48.40ZM274.30 49.20L274.30 25.30L275.60 25.30L275.80 31.30L275.90 31.30Q277.85 28.45 280.78 26.58Q283.70 24.70 287.35 24.70Q288.50 24.70 289.78 24.90Q291.05 25.10 292.20 25.70L291.70 27.10Q290.35 26.60 289.48 26.40Q288.60 26.20 287.15 26.20Q284.05 26.20 281.25 27.95Q278.45 29.70 275.80 33.90L275.80 49.20L274.30 49.20ZM301.30 49.20L301.30 25.30L302.60 25.30L302.80 30.10L303 30.10Q305.15 27.75 307.43 26.23Q309.70 24.70 312.60 24.70Q317 24.70 319.15 27.05Q321.30 29.40 321.30 34.30L321.30 49.20L319.80 49.20L319.80 34.50Q319.80 30.25 318.10 28.18Q316.40 26.10 312.60 26.10Q309.90 26.10 307.75 27.53Q305.60 28.95 302.80 31.90L302.80 49.20L301.30 49.20Z"/></g></g></svg>
|
After Width: | Height: | Size: 4.9 KiB |
7
cdrm-frontend/dist/profile.svg
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Transformed by: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 0 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="#ffffff">
|
||||
<g id="SVGRepo_bgCarrier" stroke-width="0"/>
|
||||
<g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<g id="SVGRepo_iconCarrier"> <title>profile_round [#ffffff]</title> <desc>Created with Sketch.</desc> <defs> </defs> <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> <g id="Dribbble-Light-Preview" transform="translate(-140.000000, -2159.000000)" fill="#ffffff"> <g id="icons" transform="translate(56.000000, 160.000000)"> <path d="M100.562548,2016.99998 L87.4381713,2016.99998 C86.7317804,2016.99998 86.2101535,2016.30298 86.4765813,2015.66198 C87.7127655,2012.69798 90.6169306,2010.99998 93.9998492,2010.99998 C97.3837885,2010.99998 100.287954,2012.69798 101.524138,2015.66198 C101.790566,2016.30298 101.268939,2016.99998 100.562548,2016.99998 M89.9166645,2004.99998 C89.9166645,2002.79398 91.7489936,2000.99998 93.9998492,2000.99998 C96.2517256,2000.99998 98.0830339,2002.79398 98.0830339,2004.99998 C98.0830339,2007.20598 96.2517256,2008.99998 93.9998492,2008.99998 C91.7489936,2008.99998 89.9166645,2007.20598 89.9166645,2004.99998 M103.955674,2016.63598 C103.213556,2013.27698 100.892265,2010.79798 97.837022,2009.67298 C99.4560048,2008.39598 100.400241,2006.33098 100.053171,2004.06998 C99.6509769,2001.44698 97.4235996,1999.34798 94.7348224,1999.04198 C91.0232075,1998.61898 87.8750721,2001.44898 87.8750721,2004.99998 C87.8750721,2006.88998 88.7692896,2008.57398 90.1636971,2009.67298 C87.1074334,2010.79798 84.7871636,2013.27698 84.044024,2016.63598 C83.7745338,2017.85698 84.7789973,2018.99998 86.0539717,2018.99998 L101.945727,2018.99998 C103.221722,2018.99998 104.226185,2017.85698 103.955674,2016.63598" id="profile_round-[#ffffff]"> </path> </g> </g> </g> </g>
|
||||
</svg>
|
After Width: | Height: | Size: 2.0 KiB |
7
cdrm-frontend/dist/search.svg
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Transformed by: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="SVGRepo_bgCarrier" stroke-width="0"/>
|
||||
<g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<g id="SVGRepo_iconCarrier"> <g id="Interface / Search_Magnifying_Glass"> <path id="Vector" d="M15 15L21 21M10 17C6.13401 17 3 13.866 3 10C3 6.13401 6.13401 3 10 3C13.866 3 17 6.13401 17 10C17 13.866 13.866 17 10 17Z" stroke="#ffffff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> </g> </g>
|
||||
</svg>
|
After Width: | Height: | Size: 733 B |
11
cdrm-frontend/dist/telegram.svg
vendored
Normal file
@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="16" cy="16" r="14" fill="url(#paint0_linear_87_7225)"/>
|
||||
<path d="M22.9866 10.2088C23.1112 9.40332 22.3454 8.76755 21.6292 9.082L7.36482 15.3448C6.85123 15.5703 6.8888 16.3483 7.42147 16.5179L10.3631 17.4547C10.9246 17.6335 11.5325 17.541 12.0228 17.2023L18.655 12.6203C18.855 12.4821 19.073 12.7665 18.9021 12.9426L14.1281 17.8646C13.665 18.3421 13.7569 19.1512 14.314 19.5005L19.659 22.8523C20.2585 23.2282 21.0297 22.8506 21.1418 22.1261L22.9866 10.2088Z" fill="white"/>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_87_7225" x1="16" y1="2" x2="16" y2="30" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#37BBFE"/>
|
||||
<stop offset="1" stop-color="#007DBB"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
After Width: | Height: | Size: 920 B |
7
cdrm-frontend/dist/video.svg
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Transformed by: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="SVGRepo_bgCarrier" stroke-width="0"/>
|
||||
<g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<g id="SVGRepo_iconCarrier"> <path fill-rule="evenodd" clip-rule="evenodd" d="M16 2H0V14H16V2ZM6.5 5V11H7.5L11 8L7.5 5H6.5Z" fill="#ffffff"/> </g>
|
||||
</svg>
|
After Width: | Height: | Size: 569 B |
38
cdrm-frontend/eslint.config.js
Normal file
@ -0,0 +1,38 @@
|
||||
import js from '@eslint/js'
|
||||
import globals from 'globals'
|
||||
import react from 'eslint-plugin-react'
|
||||
import reactHooks from 'eslint-plugin-react-hooks'
|
||||
import reactRefresh from 'eslint-plugin-react-refresh'
|
||||
|
||||
export default [
|
||||
{ ignores: ['dist'] },
|
||||
{
|
||||
files: ['**/*.{js,jsx}'],
|
||||
languageOptions: {
|
||||
ecmaVersion: 2020,
|
||||
globals: globals.browser,
|
||||
parserOptions: {
|
||||
ecmaVersion: 'latest',
|
||||
ecmaFeatures: { jsx: true },
|
||||
sourceType: 'module',
|
||||
},
|
||||
},
|
||||
settings: { react: { version: '18.3' } },
|
||||
plugins: {
|
||||
react,
|
||||
'react-hooks': reactHooks,
|
||||
'react-refresh': reactRefresh,
|
||||
},
|
||||
rules: {
|
||||
...js.configs.recommended.rules,
|
||||
...react.configs.recommended.rules,
|
||||
...react.configs['jsx-runtime'].rules,
|
||||
...reactHooks.configs.recommended.rules,
|
||||
'react/jsx-no-target-blank': 'off',
|
||||
'react-refresh/only-export-components': [
|
||||
'warn',
|
||||
{ allowConstantExport: true },
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
20
cdrm-frontend/index.html
Normal file
@ -0,0 +1,20 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<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="Decrypt Widevine and PlayReady protected content." />
|
||||
<meta name="keywords" content="CDRM, Widevine, PlayReady, DRM, Decrypt, CDM, CDM-Project, CDRM-Project, TPD94, Decryption" />
|
||||
<meta property='og:title' content='CDRM-Project' />
|
||||
<meta property='og:description' content='Decrypt Widevine & PlayReady Content' />
|
||||
<meta property='og:image' content='https://cdrm-project.com/lockforog.png' />
|
||||
<meta property='og:url' content='https://cdrm-project.com/' />
|
||||
<meta property='og:locale' content='en_US' />
|
||||
<title>CDRM-Project</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.jsx"></script>
|
||||
</body>
|
||||
</html>
|
5285
cdrm-frontend/package-lock.json
generated
Normal file
36
cdrm-frontend/package.json
Normal file
@ -0,0 +1,36 @@
|
||||
{
|
||||
"name": "cdrm",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"lint": "eslint .",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tailwindcss/vite": "^4.0.0",
|
||||
"atob": "^2.1.2",
|
||||
"helmet": "^8.1.0",
|
||||
"proto.js": "^0.2.2",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-helmet": "^6.1.0",
|
||||
"react-router-dom": "^7.1.3",
|
||||
"shaka-player": "^4.13.0",
|
||||
"tailwindcss": "^4.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.17.0",
|
||||
"@types/react": "^18.3.18",
|
||||
"@types/react-dom": "^18.3.5",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"eslint": "^9.17.0",
|
||||
"eslint-plugin-react": "^7.37.2",
|
||||
"eslint-plugin-react-hooks": "^5.0.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.16",
|
||||
"globals": "^15.14.0",
|
||||
"vite": "^6.3.2"
|
||||
}
|
||||
}
|
5
cdrm-frontend/public/discord.svg
Normal file
@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="512" cy="512" r="512" style="fill:#5865f2"/>
|
||||
<path d="M689.43 349a422.21 422.21 0 0 0-104.22-32.32 1.58 1.58 0 0 0-1.68.79 294.11 294.11 0 0 0-13 26.66 389.78 389.78 0 0 0-117.05 0 269.75 269.75 0 0 0-13.18-26.66 1.64 1.64 0 0 0-1.68-.79A421 421 0 0 0 334.44 349a1.49 1.49 0 0 0-.69.59c-66.37 99.17-84.55 195.9-75.63 291.41a1.76 1.76 0 0 0 .67 1.2 424.58 424.58 0 0 0 127.85 64.63 1.66 1.66 0 0 0 1.8-.59 303.45 303.45 0 0 0 26.15-42.54 1.62 1.62 0 0 0-.89-2.25 279.6 279.6 0 0 1-39.94-19 1.64 1.64 0 0 1-.16-2.72c2.68-2 5.37-4.1 7.93-6.22a1.58 1.58 0 0 1 1.65-.22c83.79 38.26 174.51 38.26 257.31 0a1.58 1.58 0 0 1 1.68.2c2.56 2.11 5.25 4.23 8 6.24a1.64 1.64 0 0 1-.14 2.72 262.37 262.37 0 0 1-40 19 1.63 1.63 0 0 0-.87 2.28 340.72 340.72 0 0 0 26.13 42.52 1.62 1.62 0 0 0 1.8.61 423.17 423.17 0 0 0 128-64.63 1.64 1.64 0 0 0 .67-1.18c10.68-110.44-17.88-206.38-75.7-291.42a1.3 1.3 0 0 0-.63-.63zM427.09 582.85c-25.23 0-46-23.16-46-51.6s20.38-51.6 46-51.6c25.83 0 46.42 23.36 46 51.6.02 28.44-20.37 51.6-46 51.6zm170.13 0c-25.23 0-46-23.16-46-51.6s20.38-51.6 46-51.6c25.83 0 46.42 23.36 46 51.6.01 28.44-20.17 51.6-46 51.6z" style="fill:#fff"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.3 KiB |
9
cdrm-frontend/public/docu_logo.svg
Normal file
@ -0,0 +1,9 @@
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Transformed by: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 0 1024 1024" class="icon" version="1.1" xmlns="http://www.w3.org/2000/svg" fill="#000000">
|
||||
<g id="SVGRepo_bgCarrier" stroke-width="0"/>
|
||||
<g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<g id="SVGRepo_iconCarrier">
|
||||
<path d="M877.685565 727.913127l-0.584863-0.365539a32.898541 32.898541 0 0 1-8.041866-46.423497 411.816631 411.816631 0 1 0-141.829267 145.777092c14.621574-8.992268 33.62962-5.117551 43.645398 8.772944l0.146216 0.073108a30.412874 30.412874 0 0 1-7.968758 43.206751l-6.141061 4.020933a475.201154 475.201154 0 1 1 163.615412-164.419599 29.974227 29.974227 0 0 1-42.841211 9.357807z m-537.342843-398.584106c7.164571-7.091463 24.71046-9.650239 33.26408 0 10.600641 11.185504 7.164571 29.462472 0 37.138798l-110.612207 107.468569L370.901811 576.14119c7.164571 7.091463 8.114974 27.342343 0 35.384209-9.796455 9.723347-29.828011 8.188081-36.480827 1.535265L208.309909 487.388236a18.423183 18.423183 0 0 1 0-25.953294l132.032813-132.032813z m343.314556 0l132.032813 132.032813a18.423183 18.423183 0 0 1 0 25.953294L689.652124 613.133772c-6.652816 6.579708-25.587754 10.746857-36.553935 0-10.30821-10.235102-7.091463-31.290168 0-38.381632l108.345863-100.669537-111.855041-108.638294c-7.164571-7.676326-9.504023-26.611265 0-36.04218 9.284699-9.138484 26.903696-7.091463 34.068267 0z m-135.54199-26.318833c3.582286-9.504023 21.347498-15.498868 32.679217-11.258612 10.819965 4.020933 17.180349 19.008046 14.256035 28.512069l-119.896906 329.716493c-3.509178 9.504023-20.616419 13.305632-30.193551 9.723347-10.161994-3.509178-21.201282-17.545889-17.545888-26.976804l120.627985-329.716493z" fill="#ffffff"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.8 KiB |
BIN
cdrm-frontend/public/favico.png
Normal file
After Width: | Height: | Size: 862 B |
19
cdrm-frontend/public/github.svg
Normal file
@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 0 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
|
||||
<title>github [#142]</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<defs>
|
||||
|
||||
</defs>
|
||||
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="Dribbble-Light-Preview" transform="translate(-140.000000, -7559.000000)" fill="#000000">
|
||||
<g id="icons" transform="translate(56.000000, 160.000000)">
|
||||
<path d="M94,7399 C99.523,7399 104,7403.59 104,7409.253 C104,7413.782 101.138,7417.624 97.167,7418.981 C96.66,7419.082 96.48,7418.762 96.48,7418.489 C96.48,7418.151 96.492,7417.047 96.492,7415.675 C96.492,7414.719 96.172,7414.095 95.813,7413.777 C98.04,7413.523 100.38,7412.656 100.38,7408.718 C100.38,7407.598 99.992,7406.684 99.35,7405.966 C99.454,7405.707 99.797,7404.664 99.252,7403.252 C99.252,7403.252 98.414,7402.977 96.505,7404.303 C95.706,7404.076 94.85,7403.962 94,7403.958 C93.15,7403.962 92.295,7404.076 91.497,7404.303 C89.586,7402.977 88.746,7403.252 88.746,7403.252 C88.203,7404.664 88.546,7405.707 88.649,7405.966 C88.01,7406.684 87.619,7407.598 87.619,7408.718 C87.619,7412.646 89.954,7413.526 92.175,7413.785 C91.889,7414.041 91.63,7414.493 91.54,7415.156 C90.97,7415.418 89.522,7415.871 88.63,7414.304 C88.63,7414.304 88.101,7413.319 87.097,7413.247 C87.097,7413.247 86.122,7413.234 87.029,7413.87 C87.029,7413.87 87.684,7414.185 88.139,7415.37 C88.139,7415.37 88.726,7417.2 91.508,7416.58 C91.513,7417.437 91.522,7418.245 91.522,7418.489 C91.522,7418.76 91.338,7419.077 90.839,7418.982 C86.865,7417.627 84,7413.783 84,7409.253 C84,7403.59 88.478,7399 94,7399" id="github-[#142]">
|
||||
|
||||
</path>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.9 KiB |
1
cdrm-frontend/public/home.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0,0,256,256" width="50px" height="50px" fill-rule="nonzero"><g fill="#ffffff" fill-rule="nonzero" stroke="none" stroke-width="1" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="10" stroke-dasharray="" stroke-dashoffset="0" font-family="none" font-weight="none" font-size="none" text-anchor="none" style="mix-blend-mode: normal"><g transform="scale(5.12,5.12)"><path d="M24.96289,1.05469c-0.20987,0.00724 -0.41214,0.08036 -0.57812,0.20898l-23,17.94727c-0.43579,0.33978 -0.51361,0.96851 -0.17383,1.4043c0.33978,0.43579 0.96851,0.51361 1.4043,0.17383l1.38477,-1.08008v26.29102c0.00006,0.55226 0.44774,0.99994 1,1h13.83203c0.10799,0.01785 0.21818,0.01785 0.32617,0h11.67383c0.10799,0.01785 0.21818,0.01785 0.32617,0h13.8418c0.55226,-0.00006 0.99994,-0.44774 1,-1v-26.29102l1.38477,1.08008c0.2819,0.21983 0.65967,0.27257 0.991,0.13833c0.33133,-0.13423 0.56586,-0.43504 0.61526,-0.7891c0.0494,-0.35406 -0.09386,-0.70757 -0.37579,-0.92736l-7.61523,-5.94141v-7.26953h-6v2.58594l-9.38477,-7.32227c-0.18607,-0.14428 -0.41707,-0.21828 -0.65234,-0.20898zM25,3.32227l19,14.82617v26.85156h-12v-19h-14v19h-12v-26.85156zM37,8h2v3.70898l-2,-1.5625zM20,28h10v17h-10z"></path></g></g></svg>
|
After Width: | Height: | Size: 1.3 KiB |
BIN
cdrm-frontend/public/lockforog.png
Normal file
After Width: | Height: | Size: 1.4 MiB |
1
cdrm-frontend/public/logo.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-0.5 13.100000381469727 321.79998779296875 36.69999694824219" data-asc="0.984"><g fill="#ffffff"><g fill="#ffffff" transform="translate(0, 0)"><path d="M13.15 49.80Q10.10 49.80 7.60 48.58Q5.10 47.35 3.28 45.10Q1.45 42.85 0.47 39.68Q-0.50 36.50-0.50 32.60Q-0.50 27.40 1.25 23.58Q3 19.75 6.13 17.68Q9.25 15.60 13.45 15.60Q16.30 15.60 18.45 16.80Q20.60 18 21.85 19.50L20.85 20.60Q19.50 19 17.65 18.05Q15.80 17.10 13.45 17.10Q9.65 17.10 6.90 19Q4.15 20.90 2.67 24.38Q1.20 27.85 1.20 32.60Q1.20 37.35 2.67 40.88Q4.15 44.40 6.85 46.35Q9.55 48.30 13.25 48.30Q15.90 48.30 17.95 47.25Q20.00 46.20 21.85 44.10L22.85 45.10Q21.00 47.25 18.70 48.53Q16.40 49.80 13.15 49.80ZM30.80 49.20L30.80 16.20L38.20 16.20Q43.30 16.20 46.58 18.25Q49.85 20.30 51.42 24Q53 27.70 53 32.60Q53 37.50 51.42 41.25Q49.85 45 46.58 47.10Q43.30 49.20 38.20 49.20L30.80 49.20M32.40 47.80L38 47.80Q42.70 47.80 45.63 45.80Q48.55 43.80 49.92 40.38Q51.30 36.95 51.30 32.60Q51.30 28.25 49.92 24.85Q48.55 21.45 45.63 19.53Q42.70 17.60 38 17.60L32.40 17.60L32.40 47.80ZM62 49.20L62 16.20L71.10 16.20Q74.35 16.20 76.72 17.05Q79.10 17.90 80.40 19.78Q81.70 21.65 81.70 24.70Q81.70 27.60 80.40 29.58Q79.10 31.55 76.72 32.58Q74.35 33.60 71.10 33.60L63.60 33.60L63.60 49.20L62 49.20M63.60 32.20L70.50 32.20Q75.10 32.20 77.55 30.35Q80 28.50 80 24.70Q80 20.80 77.55 19.20Q75.10 17.60 70.50 17.60L63.60 17.60L63.60 32.20M70.70 33.10L72.50 33L82.20 49.20L80.30 49.20L70.70 33.10ZM90 49.20L90 16.20L92.30 16.20L98.50 32.20L100.70 38.10L100.90 38.10L103 32.20L109.30 16.20L111.60 16.20L111.60 49.20L110 49.20L110 24.45Q110 23.70 110.03 22.88Q110.05 22.05 110.08 21.25Q110.10 20.45 110.13 19.63Q110.15 18.80 110.15 18.05L110 18.05L108.20 23.40L101.45 40.40L100.10 40.40L93.40 23.40L91.45 18.05L91.35 18.05Q91.35 18.80 91.40 19.63Q91.45 20.45 91.47 21.25Q91.50 22.05 91.52 22.88Q91.55 23.70 91.55 24.45L91.55 49.20L90 49.20ZM152 49.20L152 16.20L161.10 16.20Q164.35 16.20 166.73 17.05Q169.10 17.90 170.40 19.78Q171.70 21.65 171.70 24.70Q171.70 27.60 170.40 29.58Q169.10 31.55 166.73 32.58Q164.35 33.60 161.10 33.60L153.60 33.60L153.60 49.20L152 49.20M153.60 32.20L160.50 32.20Q165.10 32.20 167.55 30.35Q170 28.50 170 24.70Q170 20.80 167.55 19.20Q165.10 17.60 160.50 17.60L153.60 17.60L153.60 32.20M160.70 33.10L162.50 33L172.20 49.20L170.30 49.20L160.70 33.10ZM192.30 49.80Q188.90 49.80 186.05 48.30Q183.20 46.80 181.50 44Q179.80 41.20 179.80 37.30Q179.80 33.40 181.48 30.58Q183.15 27.75 185.88 26.23Q188.60 24.70 191.70 24.70Q194.90 24.70 197.28 26Q199.65 27.30 200.98 29.78Q202.30 32.25 202.30 35.70Q202.30 36.05 202.30 36.45Q202.30 36.85 202.20 37.30L180.30 37.30L180.30 35.90L201.80 35.90L200.90 36.70Q200.90 31.30 198.33 28.70Q195.75 26.10 191.70 26.10Q189.10 26.10 186.75 27.43Q184.40 28.75 182.90 31.23Q181.40 33.70 181.40 37.20Q181.40 40.80 182.93 43.30Q184.45 45.80 186.93 47.10Q189.40 48.40 192.30 48.40Q194.85 48.40 196.88 47.65Q198.90 46.90 200.60 45.70L201.30 47Q199.75 48 197.65 48.90Q195.55 49.80 192.30 49.80ZM221.10 49.80Q219.25 49.80 217 48.90Q214.75 48 212.90 46.50L212.80 46.50L212.60 49.20L211.30 49.20L211.30 13.10L212.80 13.10L212.80 23.80L212.70 29.10L212.80 29.10Q214.70 27.15 217.20 25.93Q219.70 24.70 222.10 24.70Q225.55 24.70 227.80 26.20Q230.05 27.70 231.18 30.45Q232.30 33.20 232.30 36.90Q232.30 40.95 230.75 43.83Q229.20 46.70 226.65 48.25Q224.10 49.80 221.10 49.80M221.20 48.40Q223.95 48.40 226.10 46.93Q228.25 45.45 229.48 42.85Q230.70 40.25 230.70 36.90Q230.70 33.85 229.83 31.40Q228.95 28.95 227.05 27.53Q225.15 26.10 222 26.10Q220 26.10 217.60 27.25Q215.20 28.40 212.80 30.80L212.80 44.60Q215.05 46.65 217.35 47.53Q219.65 48.40 221.20 48.40ZM250.80 49.80Q247.65 49.80 245.05 48.33Q242.45 46.85 240.88 44.05Q239.30 41.25 239.30 37.30Q239.30 33.30 240.88 30.48Q242.45 27.65 245.05 26.18Q247.65 24.70 250.80 24.70Q253.95 24.70 256.55 26.18Q259.15 27.65 260.73 30.48Q262.30 33.30 262.30 37.30Q262.30 41.25 260.73 44.05Q259.15 46.85 256.55 48.33Q253.95 49.80 250.80 49.80M250.80 48.40Q253.65 48.40 255.90 47Q258.15 45.60 259.43 43.10Q260.70 40.60 260.70 37.30Q260.70 34 259.43 31.48Q258.15 28.95 255.90 27.53Q253.65 26.10 250.80 26.10Q247.95 26.10 245.73 27.53Q243.50 28.95 242.20 31.48Q240.90 34 240.90 37.30Q240.90 40.60 242.20 43.10Q243.50 45.60 245.73 47Q247.95 48.40 250.80 48.40ZM274.30 49.20L274.30 25.30L275.60 25.30L275.80 31.30L275.90 31.30Q277.85 28.45 280.78 26.58Q283.70 24.70 287.35 24.70Q288.50 24.70 289.78 24.90Q291.05 25.10 292.20 25.70L291.70 27.10Q290.35 26.60 289.48 26.40Q288.60 26.20 287.15 26.20Q284.05 26.20 281.25 27.95Q278.45 29.70 275.80 33.90L275.80 49.20L274.30 49.20ZM301.30 49.20L301.30 25.30L302.60 25.30L302.80 30.10L303 30.10Q305.15 27.75 307.43 26.23Q309.70 24.70 312.60 24.70Q317 24.70 319.15 27.05Q321.30 29.40 321.30 34.30L321.30 49.20L319.80 49.20L319.80 34.50Q319.80 30.25 318.10 28.18Q316.40 26.10 312.60 26.10Q309.90 26.10 307.75 27.53Q305.60 28.95 302.80 31.90L302.80 49.20L301.30 49.20Z"/></g></g></svg>
|
After Width: | Height: | Size: 4.9 KiB |
7
cdrm-frontend/public/profile.svg
Normal file
@ -0,0 +1,7 @@
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Transformed by: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 0 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="#ffffff">
|
||||
<g id="SVGRepo_bgCarrier" stroke-width="0"/>
|
||||
<g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<g id="SVGRepo_iconCarrier"> <title>profile_round [#ffffff]</title> <desc>Created with Sketch.</desc> <defs> </defs> <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> <g id="Dribbble-Light-Preview" transform="translate(-140.000000, -2159.000000)" fill="#ffffff"> <g id="icons" transform="translate(56.000000, 160.000000)"> <path d="M100.562548,2016.99998 L87.4381713,2016.99998 C86.7317804,2016.99998 86.2101535,2016.30298 86.4765813,2015.66198 C87.7127655,2012.69798 90.6169306,2010.99998 93.9998492,2010.99998 C97.3837885,2010.99998 100.287954,2012.69798 101.524138,2015.66198 C101.790566,2016.30298 101.268939,2016.99998 100.562548,2016.99998 M89.9166645,2004.99998 C89.9166645,2002.79398 91.7489936,2000.99998 93.9998492,2000.99998 C96.2517256,2000.99998 98.0830339,2002.79398 98.0830339,2004.99998 C98.0830339,2007.20598 96.2517256,2008.99998 93.9998492,2008.99998 C91.7489936,2008.99998 89.9166645,2007.20598 89.9166645,2004.99998 M103.955674,2016.63598 C103.213556,2013.27698 100.892265,2010.79798 97.837022,2009.67298 C99.4560048,2008.39598 100.400241,2006.33098 100.053171,2004.06998 C99.6509769,2001.44698 97.4235996,1999.34798 94.7348224,1999.04198 C91.0232075,1998.61898 87.8750721,2001.44898 87.8750721,2004.99998 C87.8750721,2006.88998 88.7692896,2008.57398 90.1636971,2009.67298 C87.1074334,2010.79798 84.7871636,2013.27698 84.044024,2016.63598 C83.7745338,2017.85698 84.7789973,2018.99998 86.0539717,2018.99998 L101.945727,2018.99998 C103.221722,2018.99998 104.226185,2017.85698 103.955674,2016.63598" id="profile_round-[#ffffff]"> </path> </g> </g> </g> </g>
|
||||
</svg>
|
After Width: | Height: | Size: 2.0 KiB |
7
cdrm-frontend/public/search.svg
Normal file
@ -0,0 +1,7 @@
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Transformed by: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="SVGRepo_bgCarrier" stroke-width="0"/>
|
||||
<g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<g id="SVGRepo_iconCarrier"> <g id="Interface / Search_Magnifying_Glass"> <path id="Vector" d="M15 15L21 21M10 17C6.13401 17 3 13.866 3 10C3 6.13401 6.13401 3 10 3C13.866 3 17 6.13401 17 10C17 13.866 13.866 17 10 17Z" stroke="#ffffff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> </g> </g>
|
||||
</svg>
|
After Width: | Height: | Size: 733 B |
11
cdrm-frontend/public/telegram.svg
Normal file
@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="16" cy="16" r="14" fill="url(#paint0_linear_87_7225)"/>
|
||||
<path d="M22.9866 10.2088C23.1112 9.40332 22.3454 8.76755 21.6292 9.082L7.36482 15.3448C6.85123 15.5703 6.8888 16.3483 7.42147 16.5179L10.3631 17.4547C10.9246 17.6335 11.5325 17.541 12.0228 17.2023L18.655 12.6203C18.855 12.4821 19.073 12.7665 18.9021 12.9426L14.1281 17.8646C13.665 18.3421 13.7569 19.1512 14.314 19.5005L19.659 22.8523C20.2585 23.2282 21.0297 22.8506 21.1418 22.1261L22.9866 10.2088Z" fill="white"/>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_87_7225" x1="16" y1="2" x2="16" y2="30" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#37BBFE"/>
|
||||
<stop offset="1" stop-color="#007DBB"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
After Width: | Height: | Size: 920 B |
7
cdrm-frontend/public/video.svg
Normal file
@ -0,0 +1,7 @@
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Transformed by: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="SVGRepo_bgCarrier" stroke-width="0"/>
|
||||
<g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<g id="SVGRepo_iconCarrier"> <path fill-rule="evenodd" clip-rule="evenodd" d="M16 2H0V14H16V2ZM6.5 5V11H7.5L11 8L7.5 5H6.5Z" fill="#ffffff"/> </g>
|
||||
</svg>
|
After Width: | Height: | Size: 569 B |
10
cdrm-frontend/src/App.css
Normal file
@ -0,0 +1,10 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html, body, #root {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
13
cdrm-frontend/src/App.jsx
Normal file
@ -0,0 +1,13 @@
|
||||
import React from 'react'
|
||||
import './App.css'
|
||||
import AppContainer from './components/AppContainer/AppContainer.jsx'
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<>
|
||||
<AppContainer />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default App
|
1
cdrm-frontend/src/assets/react.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>
|
After Width: | Height: | Size: 4.0 KiB |
29
cdrm-frontend/src/components/AppContainer/AppContainer.jsx
Normal file
@ -0,0 +1,29 @@
|
||||
import React from 'react'
|
||||
import Sidebar from '../Sidebar/Sidebar'
|
||||
import Home from '../Pages/Home'
|
||||
import API from '../Pages/API'
|
||||
import Cache from '../Pages/Cache'
|
||||
import TestPlayer from '../Pages/TestPlayer'
|
||||
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
|
||||
|
||||
function AppContainer() {
|
||||
return (
|
||||
<Router>
|
||||
<div className='flex flex-row'>
|
||||
<div className='w-1/8 h-dvh overflow-y-auto'>
|
||||
<Sidebar />
|
||||
</div>
|
||||
<div className='w-7/8 h-dvh overflow-y-auto scroll-smooth' id='main_content'>
|
||||
<Routes>
|
||||
<Route path="/" element={<Home />} />
|
||||
<Route path="/api" element={<API />} />
|
||||
<Route path="/testplayer" element={<TestPlayer />} />
|
||||
<Route path="/cache" element={<Cache />} />
|
||||
</Routes>
|
||||
</div>
|
||||
</div>
|
||||
</Router>
|
||||
)
|
||||
}
|
||||
|
||||
export default AppContainer
|
173
cdrm-frontend/src/components/Functions/ParseChallenge.jsx
Normal file
@ -0,0 +1,173 @@
|
||||
import "./protobuf.min.js";
|
||||
import "./license_protocol.min.js";
|
||||
|
||||
const { SignedMessage, LicenseRequest } = protobuf.roots.default.license_protocol;
|
||||
|
||||
function uint8ArrayToBase64(uint8Array) {
|
||||
const binaryString = Array.from(uint8Array)
|
||||
.map(b => String.fromCharCode(b))
|
||||
.join('');
|
||||
|
||||
return btoa(binaryString);
|
||||
}
|
||||
|
||||
function parseFetch(fetchString) {
|
||||
// Remove `await` if it exists in the string
|
||||
fetchString = fetchString.replace(/^await\s+/, "");
|
||||
|
||||
// Use a more lenient regex to capture the fetch pattern (including complex bodies)
|
||||
const fetchRegex = /fetch\(['"](.+?)['"],\s*(\{.+?\})\)/s; // Non-greedy match for JSON
|
||||
const lines = fetchString.split('\n').map(line => line.trim()).filter(Boolean);
|
||||
const result = {
|
||||
method: 'UNDEFINED',
|
||||
url: '',
|
||||
headers: {},
|
||||
body: null,
|
||||
};
|
||||
|
||||
// Try matching the regex
|
||||
const fetchMatch = fetchString.match(fetchRegex);
|
||||
if (!fetchMatch) {
|
||||
console.log(fetchString);
|
||||
throw new Error("Invalid 'Copy as fetch' string.");
|
||||
}
|
||||
|
||||
// Extract URL from the match
|
||||
result.url = fetchMatch[1];
|
||||
|
||||
// Parse the options JSON from the match (this will include headers, body, etc.)
|
||||
const optionsString = fetchMatch[2];
|
||||
const options = JSON.parse(optionsString);
|
||||
|
||||
// Assign method, headers, and body if available
|
||||
if (options.method) result.method = options.method;
|
||||
if (options.headers) result.headers = options.headers;
|
||||
if (options.body) result.body = options.body;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
const WIDEVINE_SYSTEM_ID = new Uint8Array([0xed, 0xef, 0x8b, 0xa9, 0x79, 0xd6, 0x4a, 0xce, 0xa3, 0xc8, 0x27, 0xdc, 0xd5, 0x1d, 0x21, 0xed]);
|
||||
const PLAYREADY_SYSTEM_ID = new Uint8Array([0x9a, 0x04, 0xf0, 0x79, 0x98, 0x40, 0x42, 0x86, 0xab, 0x92, 0xe6, 0x5b, 0xe0, 0x88, 0x5f, 0x95]);
|
||||
const PSSH_MAGIC = new Uint8Array([0x70, 0x73, 0x73, 0x68]);
|
||||
|
||||
function intToUint8Array(num, endian) {
|
||||
const buffer = new ArrayBuffer(4);
|
||||
const view = new DataView(buffer);
|
||||
view.setUint32(0, num, endian);
|
||||
return new Uint8Array(buffer);
|
||||
}
|
||||
|
||||
function shortToUint8Array(num, endian) {
|
||||
const buffer = new ArrayBuffer(2);
|
||||
const view = new DataView(buffer);
|
||||
view.setUint16(0, num, endian);
|
||||
return new Uint8Array(buffer);
|
||||
}
|
||||
|
||||
function psshDataToPsshBoxB64(pssh_data, system_id) {
|
||||
const dataLength = pssh_data.length;
|
||||
const totalLength = dataLength + 32;
|
||||
const pssh = new Uint8Array([
|
||||
...intToUint8Array(totalLength, false),
|
||||
...PSSH_MAGIC,
|
||||
...new Uint8Array(4),
|
||||
...system_id,
|
||||
...intToUint8Array(dataLength, false),
|
||||
...pssh_data
|
||||
]);
|
||||
return uint8ArrayToBase64(pssh);
|
||||
}
|
||||
|
||||
function wrmHeaderToPlayReadyHeader(wrm_header){
|
||||
const playready_object = new Uint8Array([
|
||||
...shortToUint8Array(1, true),
|
||||
...shortToUint8Array(wrm_header.length, true),
|
||||
...wrm_header
|
||||
]);
|
||||
|
||||
return new Uint8Array([
|
||||
...intToUint8Array(playready_object.length + 2 + 4, true),
|
||||
...shortToUint8Array(1, true),
|
||||
...playready_object
|
||||
]);
|
||||
}
|
||||
|
||||
function encodeUtf16LE(str) {
|
||||
const buffer = new Uint8Array(str.length * 2);
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
const code = str.charCodeAt(i);
|
||||
buffer[i * 2] = code & 0xff;
|
||||
buffer[i * 2 + 1] = code >> 8;
|
||||
}
|
||||
return buffer;
|
||||
}
|
||||
|
||||
function stringToUint8Array(string) {
|
||||
return Uint8Array.from(string.split("").map(x => x.charCodeAt()));
|
||||
}
|
||||
|
||||
export async function readTextFromClipboard() {
|
||||
try {
|
||||
// Request text from the clipboard
|
||||
const clipboardText = await navigator.clipboard.readText();
|
||||
|
||||
const result = parseFetch(clipboardText);
|
||||
|
||||
let pssh_data_string;
|
||||
let payload_string;
|
||||
|
||||
if (result.body.startsWith("<")) {
|
||||
// If body starts with "<", process it as PlayReady content
|
||||
payload_string = result.body;
|
||||
const wrmHeaderMatch = payload_string.match(/.*(<WRMHEADER.*<\/WRMHEADER>).*/);
|
||||
const wrmHeader = wrmHeaderMatch ? wrmHeaderMatch[1] : null;
|
||||
const encodedWrmHeader = encodeUtf16LE(wrmHeader);
|
||||
const playreadyHeader = wrmHeaderToPlayReadyHeader(encodedWrmHeader);
|
||||
pssh_data_string = psshDataToPsshBoxB64(playreadyHeader, PLAYREADY_SYSTEM_ID);
|
||||
} else {
|
||||
// If body is in a different format, process as Widevine content
|
||||
const uint8Array = stringToUint8Array(result.body);
|
||||
let signed_message;
|
||||
let license_request;
|
||||
try {
|
||||
signed_message = SignedMessage.decode(uint8Array);
|
||||
license_request = LicenseRequest.decode(signed_message.msg);
|
||||
} catch (decodeError) {
|
||||
// If error occurs during decoding, return an empty pssh
|
||||
console.error('Decoding failed, returning empty pssh', decodeError);
|
||||
pssh_data_string = ''; // Empty pssh if decoding fails
|
||||
}
|
||||
|
||||
if (license_request && license_request.contentId && license_request.contentId.widevinePsshData) {
|
||||
const pssh_data = license_request.contentId.widevinePsshData.psshData[0];
|
||||
pssh_data_string = psshDataToPsshBoxB64(pssh_data, WIDEVINE_SYSTEM_ID);
|
||||
}
|
||||
|
||||
// Check if the body contains binary data (non-UTF-8 characters)
|
||||
if (isBinary(uint8Array)) {
|
||||
payload_string = uint8ArrayToBase64(uint8Array);
|
||||
} else {
|
||||
// If it's text, return it as is
|
||||
payload_string = result.body;
|
||||
}
|
||||
}
|
||||
|
||||
// Output the result
|
||||
document.getElementById("licurl").value = result.url;
|
||||
document.getElementById("headers").value = JSON.stringify(result.headers);
|
||||
document.getElementById("pssh").value = pssh_data_string;
|
||||
document.getElementById("data").value = payload_string;
|
||||
} catch (error) {
|
||||
console.error('Failed to read clipboard contents:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to check if the data is binary
|
||||
function isBinary(uint8Array) {
|
||||
// Check for non-text (non-ASCII) bytes (basic heuristic)
|
||||
return uint8Array.some(byte => byte > 127); // Non-ASCII byte indicates binary
|
||||
}
|
||||
|
||||
|
1
cdrm-frontend/src/components/Functions/license_protocol.min.js
vendored
Normal file
8
cdrm-frontend/src/components/Functions/protobuf.min.js
vendored
Normal file
176
cdrm-frontend/src/components/Pages/API.jsx
Normal file
@ -0,0 +1,176 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { data } from 'react-router-dom';
|
||||
const { protocol, hostname, port } = window.location;
|
||||
|
||||
let fullHost = `${protocol}//${hostname}`;
|
||||
if (
|
||||
(protocol === 'http:' && port !== '80') ||
|
||||
(protocol === 'https:' && port !== '443' && port !== '')
|
||||
) {
|
||||
fullHost += `:${port}`;
|
||||
}
|
||||
|
||||
function API() {
|
||||
// Widevine device info
|
||||
const [deviceInfo, setDeviceInfo] = useState({
|
||||
device_type: '',
|
||||
system_id: '',
|
||||
security_level: '',
|
||||
host: '',
|
||||
secret: '',
|
||||
device_name: ''
|
||||
});
|
||||
|
||||
// PlayReady device info
|
||||
const [prDeviceInfo, setPrDeviceInfo] = useState({
|
||||
security_level: '',
|
||||
host: '',
|
||||
secret: '',
|
||||
device_name: ''
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
// Fetch Widevine info
|
||||
fetch('/remotecdm/widevine/deviceinfo')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
setDeviceInfo({
|
||||
device_type: data.device_type,
|
||||
system_id: data.system_id,
|
||||
security_level: data.security_level,
|
||||
host: data.host,
|
||||
secret: data.secret,
|
||||
device_name: data.device_name
|
||||
});
|
||||
})
|
||||
.catch(error => console.error('Error fetching Widevine info:', error));
|
||||
|
||||
// Fetch PlayReady info
|
||||
fetch('/remotecdm/playready/deviceinfo')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
setPrDeviceInfo({
|
||||
security_level: data.security_level,
|
||||
host: data.host,
|
||||
secret: data.secret,
|
||||
device_name: data.device_name
|
||||
});
|
||||
})
|
||||
.catch(error => console.error('Error fetching PlayReady info:', error));
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='min-w-full w-full min-h-full overflow-x-auto bg-zinc-900 shadow-lg shadow-black flex flex-col flex-wrap p-10 justify-around'>
|
||||
|
||||
{/* Decryption Request Section */}
|
||||
<details open className='p-5 mb-5 border shadow-lg shadow-black overflow-y-auto'>
|
||||
<summary className='bg-[rgba(0,0,0,0.2)] p-2 rounded text-white flex shadow-purple-900 shadow-sm'>
|
||||
Sending a decryption request | {`(Python)`}
|
||||
</summary>
|
||||
<br />
|
||||
<div className='h-9/10 bg-[rgba(0,0,0,0.2)] p-2 rounded text-white shadow-sm shadow-purple-900 w-full overflow-x-auto overflow-y-auto'>
|
||||
<pre className='p-2'>
|
||||
{`import requests
|
||||
|
||||
print(requests.post(
|
||||
url='${fullHost}/api/decrypt',
|
||||
headers={
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
json={
|
||||
'pssh': 'AAAAW3Bzc2gAAAAA7e+LqXnWSs6jyCfc1R0h7QAAADsIARIQ62dqu8s0Xpa7z2FmMPGj2hoNd2lkZXZpbmVfdGVzdCIQZmtqM2xqYVNkZmFsa3IzaioCSEQyAA==',
|
||||
'licurl': 'https://cwip-shaka-proxy.appspot.com/no_auth',
|
||||
'headers': str({
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:134.0) Gecko/20100101 Firefox/134.0',
|
||||
'Accept': '*/*',
|
||||
'Accept-Language': 'en-US,en;q=0.5',
|
||||
})
|
||||
}
|
||||
).json()['message'])`}
|
||||
</pre>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
{/* Search Request Section */}
|
||||
<details open className=' p-5 mb-5 shadow-lg shadow-black border overflow-y-auto' >
|
||||
<summary className='bg-[rgba(0,0,0,0.2)] p-2 rounded text-white flex shadow-sm shadow-purple-900'>
|
||||
Sending a search request | {`(Python)`}
|
||||
</summary>
|
||||
<br />
|
||||
<div className='h-9/10 bg-[rgba(0,0,0,0.2)] p-2 rounded text-white shadow-sm shadow-purple-900'>
|
||||
<pre className='p-2'>{`import requests
|
||||
|
||||
print(requests.post(
|
||||
url='${fullHost}/api/cache/search',
|
||||
json={
|
||||
'input': 'AAAAW3Bzc2gAAAAA7e+LqXnWSs6jyCfc1R0h7QAAADsIARIQ62dqu8s0Xpa7z2FmMPGj2hoNd2lkZXZpbmVfdGVzdCIQZmtqM2xqYVNkZmFsa3IzaioCSEQyAA=='
|
||||
}
|
||||
).json())`}</pre>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
{/* Remote CDM Configuration Section */}
|
||||
<details open className=' p-5 mb-5 shadow-lg shadow-black border overflow-y-auto'>
|
||||
<summary className='bg-[rgba(0,0,0,0.2)] p-2 rounded text-white flex shadow-sm shadow-purple-900'>
|
||||
Remote CDM configuration (Widevine) | {`(For PyWidevine / Devine / VineTrimmer / Extensions)`}
|
||||
</summary>
|
||||
<br />
|
||||
<div className='h-9/10 bg-[rgba(0,0,0,0.2)] p-2 rounded text-white shadow-sm shadow-purple-900'>
|
||||
<p className='p-2'>
|
||||
device_type: <span id='wv_device_type'>"{deviceInfo.device_type}"</span>
|
||||
<br></br>
|
||||
system_id: <span id='wv_system_id'>{deviceInfo.system_id}</span>
|
||||
<br></br>
|
||||
security_level: <span id='wv_security_level'>{deviceInfo.security_level}</span>
|
||||
<br></br>
|
||||
host: <span id='wv_host'>"{fullHost}/remotecdm/widevine"</span>
|
||||
<br></br>
|
||||
secret: <span id='wv_secret'>"{deviceInfo.secret}"</span>
|
||||
<br></br>
|
||||
device_name: <span id='wv_device_name'>{deviceInfo.device_name}</span>
|
||||
</p>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
{/* Remote CDM Configuration Section */}
|
||||
<details open className=' p-5 mb-5 shadow-lg shadow-black border overflow-y-auto'>
|
||||
<summary className='bg-[rgba(0,0,0,0.2)] p-2 rounded text-white flex shadow-sm shadow-purple-900'>
|
||||
Remote CDM configuration (PlayReady) | {`(For PyPlayReady / Extensions / PlayReady Proxy)`}
|
||||
</summary>
|
||||
<br />
|
||||
<div className='h-9/10 bg-[rgba(0,0,0,0.2)] p-2 rounded text-white shadow-sm shadow-purple-900'>
|
||||
<p className='p-2'>
|
||||
device_name: <span id='pr_device_name'>{prDeviceInfo.device_name}</span>
|
||||
<br></br>
|
||||
security_level: <span id='pr_security_level'>{prDeviceInfo.security_level}</span>
|
||||
<br></br>
|
||||
host: <span id='pr_host'>"{fullHost}/remotecdm/playready"</span>
|
||||
<br></br>
|
||||
secret: <span id='pr_secret'>"{prDeviceInfo.secret}"</span>
|
||||
<br></br>
|
||||
</p>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
{/* Webvault Configuration Section */}
|
||||
<details open className='p-5 mb-5 shadow-lg shadow-black border overflow-y-auto'>
|
||||
<summary className='bg-[rgba(0,0,0,0.2)] p-2 rounded text-white flex shadow-sm shadow-purple-900 transition-all transition-300 ease-in'>
|
||||
Webvault configuration | {`(For Devine / VineTrimmer)`}
|
||||
</summary>
|
||||
<br />
|
||||
<div className='h-9/10 bg-[rgba(0,0,0,0.2)] p-2 rounded text-white shadow-sm shadow-purple-900'>
|
||||
<pre className='p-2'>{`key_vaults:
|
||||
- type: API
|
||||
name: "Online Vault"
|
||||
uri: "${fullHost}/api/cache"
|
||||
token: "${deviceInfo.secret}"`}</pre>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default API;
|
118
cdrm-frontend/src/components/Pages/Cache.jsx
Normal file
@ -0,0 +1,118 @@
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
|
||||
function Cache() {
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [cacheData, setCacheData] = useState([]);
|
||||
const [keyCount, setKeyCount] = useState(0);
|
||||
const debounceTimeout = useRef(null); // ← store the timeout ID
|
||||
|
||||
const handleInputChange = (event) => {
|
||||
const query = event.target.value;
|
||||
setSearchQuery(query);
|
||||
|
||||
// Clear the previous timeout
|
||||
if (debounceTimeout.current) {
|
||||
clearTimeout(debounceTimeout.current);
|
||||
}
|
||||
|
||||
// Set a new timeout
|
||||
debounceTimeout.current = setTimeout(() => {
|
||||
if (query.trim() !== '') {
|
||||
sendApiCall(query);
|
||||
} else {
|
||||
setCacheData([]); // optional: clear results on empty query
|
||||
}
|
||||
}, 1000); // 2 seconds
|
||||
};
|
||||
|
||||
const fetchKeyCount = () => {
|
||||
fetch('/api/cache/keycount')
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
setKeyCount(data.count);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Error fetching key count:', error);
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const fetchInterval = setInterval(fetchKeyCount, 10000);
|
||||
fetchKeyCount();
|
||||
return () => clearInterval(fetchInterval);
|
||||
}, []);
|
||||
|
||||
const sendApiCall = (text) => {
|
||||
fetch('/api/cache/search', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ input: text }),
|
||||
})
|
||||
.then((response) => response.json())
|
||||
.then((data) => setCacheData(data))
|
||||
.catch((error) => console.error('Error:', error));
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='w-full h-full bg-zinc-900 flex flex-col p-0'>
|
||||
<div className='flex flex-row w-full'>
|
||||
<form className='flex flex-row w-8/10 p-10 h-full rounded-xl self-start'>
|
||||
<input
|
||||
type='text'
|
||||
id='search'
|
||||
name='search'
|
||||
value={searchQuery}
|
||||
onChange={handleInputChange}
|
||||
placeholder='Search by PSSH/KID'
|
||||
className='w-full text-white bg-[rgba(0,0,0,0.2)] focus:outline-none rounded focus:shadow-sm focus:shadow-green-700/50 transition-shadow duration-300 ease-in-out p-2'
|
||||
/>
|
||||
</form>
|
||||
<p className='text-white w-2/10 p-10 rounded-xl self-start flex flex-col h-full'>
|
||||
<span className='text-white w-1/1 text-center'>
|
||||
Cached Keys: {keyCount} {/* Display the count of cached keys */}
|
||||
</span>
|
||||
<a href='/api/cache/download'>
|
||||
<button className=' self-start w-1/1 bg-green-700 rounded-md mt-1 active:transform active:scale-95 cursor-pointer hover:bg-green-600/50 pt-1 pb-1'>
|
||||
Download
|
||||
</button>
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
<div className='h-full w-full p-10 overflow-y-auto'>
|
||||
<div className="overflow-x-auto border p-10 rounded-2xl bg-[rgba(0,0,0,0.2)] shadow-md shadow-green-700 min-h-full overflow-y-auto">
|
||||
<table className='min-w-full text-white'>
|
||||
<thead>
|
||||
<tr>
|
||||
<th className='p-2 border border-black'>PSSH</th>
|
||||
<th className='p-2 border border-black'>KID</th>
|
||||
<th className='p-2 border border-black'>Key</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{cacheData.length > 0 ? (
|
||||
cacheData.map((item, index) => (
|
||||
<tr key={index}>
|
||||
<td className='p-2 border border-black'>{item.PSSH}</td>
|
||||
<td className='p-2 border border-black'>{item.KID}</td>
|
||||
<td className='p-2 border border-black'>{item.Key}</td>
|
||||
</tr>
|
||||
))
|
||||
) : (
|
||||
<tr>
|
||||
<td colSpan="3" className='p-2 border border-black text-center'>
|
||||
No data found
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default Cache;
|
124
cdrm-frontend/src/components/Pages/Home.jsx
Normal file
@ -0,0 +1,124 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import { readTextFromClipboard } from '../functions/ParseChallenge';
|
||||
|
||||
function Home() {
|
||||
const [message, setMessage] = useState('');
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
|
||||
const scrollToTop = () => {
|
||||
let divToScroll = document.getElementById('main_content');
|
||||
divToScroll.scrollTop = 0; // Scroll to the top of the div
|
||||
};
|
||||
|
||||
const copyToClipboard = (event) => {
|
||||
let message = document.getElementById('messageresults').innerHTML;
|
||||
|
||||
// Replace <br> tags with newline characters
|
||||
message = message.replace(/<br\s*\/?>/gi, '\n');
|
||||
|
||||
// Use textContent to get the actual text without HTML tags
|
||||
navigator.clipboard.writeText(message); // Copy the message to the clipboard
|
||||
console.log(message);
|
||||
};
|
||||
|
||||
// Handlers for form submission and reset
|
||||
const handleSubmit = (event) => {
|
||||
event.preventDefault();
|
||||
};
|
||||
|
||||
const handleSubmitButton = (event) => {
|
||||
let pssh = document.getElementById('pssh').value;
|
||||
let licurl = document.getElementById('licurl').value;
|
||||
let headers = document.getElementById('headers').value;
|
||||
let cookies = document.getElementById('cookies').value;
|
||||
let data = document.getElementById('data').value;
|
||||
|
||||
fetch('/api/decrypt', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
pssh: pssh,
|
||||
licurl: licurl,
|
||||
headers: headers,
|
||||
cookies: cookies,
|
||||
data: data
|
||||
}),
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
const resultMessage = data['message'].replace(/\n/g, '<br />'); // Format the message as HTML
|
||||
setMessage(resultMessage);
|
||||
setIsVisible(true);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Error during decryption request:', error);
|
||||
setMessage('Error: Unable to process request.');
|
||||
setIsVisible(true);
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
const handleResetButton = (event) => {
|
||||
let pssh = document.getElementById('pssh');
|
||||
let licurl = document.getElementById('licurl');
|
||||
let headers = document.getElementById('headers');
|
||||
let cookies = document.getElementById('cookies');
|
||||
let data = document.getElementById('data');
|
||||
pssh.value = '';
|
||||
licurl.value = '';
|
||||
headers.value = '';
|
||||
cookies.value = '';
|
||||
data.value = '';
|
||||
setMessage('');
|
||||
setIsVisible(false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (isVisible) {
|
||||
let divToScroll = document.getElementById('main_content');
|
||||
divToScroll.scrollTop = divToScroll.scrollHeight;
|
||||
}
|
||||
}, [message, isVisible]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='w-full min-h-full bg-zinc-900 flex flex-col items-center justify-center'>
|
||||
<form className='flex flex-col w-8/10 min-h-8/10 bg-[rgba(0,0,0,0.2)] p-10 border-black border-1 rounded-xl shadow-lg shadow-cyan-500/50 overflow-y-auto' onSubmit={handleSubmit}>
|
||||
<label htmlFor='pssh' className='text-white mb-1'>PSSH:</label>
|
||||
<input type='text' id='pssh' name='pssh' className='text-white bg-[rgba(0,0,0,0.2)] focus:outline-none rounded focus:shadow-sm focus:shadow-cyan-500/50 transition-shadow duration-300 ease-in-out p-2' />
|
||||
<label htmlFor='licurl' className='text-white mb-1 mt-1'>License URL:</label>
|
||||
<input type='text' id='licurl' name='licurl' className='text-white bg-[rgba(0,0,0,0.2)] focus:outline-none rounded focus:shadow-sm focus:shadow-cyan-500/50 transition-shadow duration-300 ease-in-out p-2' />
|
||||
<label htmlFor='headers' className='text-white mb-1 mt-1'>Headers:</label>
|
||||
<textarea id='headers' name='headers' className='text-white bg-[rgba(0,0,0,0.2)] h-24 focus:h-92 focus:outline-none rounded focus:shadow-sm focus:shadow-cyan-500/50 transition-all duration-300 ease-in-out p-2 resize-none' />
|
||||
<label htmlFor='cookies' className='text-white mb-1 mt-1'>Cookies:</label>
|
||||
<textarea id='cookies' name='cookies' className='text-white bg-[rgba(0,0,0,0.2)] h-24 focus:h-92 focus:outline-none rounded focus:shadow-sm focus:shadow-cyan-500/50 transition-all duration-300 ease-in-out p-2 resize-none' />
|
||||
<label htmlFor='data' className='text-white mb-1 mt-1'>Data:</label>
|
||||
<textarea id='data' name='data' className='text-white bg-[rgba(0,0,0,0.2)] h-24 focus:h-92 focus:outline-none rounded focus:shadow-sm focus:shadow-cyan-500/50 transition-all duration-300 ease-in-out p-2 resize-none' />
|
||||
<div className='flex flex-row w-full justify-evenly mt-5 mb-5'>
|
||||
<button type='button' onClick={handleSubmitButton} className='bg-cyan-500 text-white rounded p-2 hover:bg-cyan-600 transition-colors duration-300 ease-in-out w-1/6 cursor-pointer active:transform active:scale-95 overflow-x-hidden overflow-y-hidden'>Submit</button>
|
||||
<button type='button' onClick={readTextFromClipboard} className='bg-yellow-500 text-white rounded p-2 hover:bg-yellow-600 transition-colors duration-300 ease-in-out w-1/6 cursor-pointer active:transform active:scale-95 overflow-x-hidden overflow-y-hidden'>Paste from fetch</button>
|
||||
<button type='button' onClick={handleResetButton} className='bg-red-500 text-white rounded p-2 hover:bg-red-600 transition-colors duration-300 ease-in-out w-1/6 cursor-pointer active:transform active:scale-95 overflow-x-hidden overflow-y-hidden'>Reset</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{isVisible && (
|
||||
<div className='w-full min-h-full h-full bg-zinc-900 flex flex-col items-center justify-center'>
|
||||
<div className='w-8/10 min-h-8/10 flex flex-col bg-[rgba(0,0,0,0.2)] items-center p-10 border-black border-1 rounded-xl shadow-lg shadow-cyan-500/50 overflow-y-auto'>
|
||||
<p className='w-full text-center text-white text-2xl overflow-hidden mb-10 pb-2 border-b'>Results:</p>
|
||||
<p id='messageresults' className='w-8/10 h-6/10 text-center text-white text-2xl overflow-hidden bg-[rgba(0,0,0,0.2)] rounded-xl p-5' dangerouslySetInnerHTML={{ __html: message }}></p>
|
||||
<div className='w-full h-1/10 flex justify-evenly mt-5 , mb-5'>
|
||||
<button type='button' onClick={copyToClipboard} className='bg-green-500 text-white rounded p-2 hover:bg-green-600 transition-colors duration-300 ease-in-out min-w-1/6 w-1/6 min-h-4/6 h-4/6 cursor-pointer active:transform active:scale-95 overflow-x-hidden overflow-y-hidden'>Copy</button>
|
||||
<button type='button' onClick={scrollToTop} className='bg-yellow-600 text-white rounded p-2 hover:bg-yellow-700 transition-colors duration-300 ease-in-out min-w-1/6 w-1/6 min-h-4/6 h-4/6 cursor-pointer active:transform active:scale-95 overflow-x-hidden overflow-y-hidden'>Back to top</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default Home;
|
106
cdrm-frontend/src/components/Pages/TestPlayer.jsx
Normal file
@ -0,0 +1,106 @@
|
||||
import React, { useState, useRef } from "react";
|
||||
import shaka from "shaka-player"; // Import the Shaka Player library
|
||||
|
||||
function TestPlayer() {
|
||||
const [mpdUrl, setMpdUrl] = useState("");
|
||||
const [kid, setKid] = useState("");
|
||||
const [key, setKey] = useState("");
|
||||
const videoRef = useRef(null); // Ref to the video element
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (mpdUrl && kid && key) {
|
||||
// Split the KIDs and Keys by new lines
|
||||
const kidsArray = kid.split("\n").map((k) => k.trim());
|
||||
const keysArray = key.split("\n").map((k) => k.trim());
|
||||
|
||||
if (kidsArray.length !== keysArray.length) {
|
||||
console.error("The number of KIDs and Keys must be the same.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Initialize Shaka Player only when the submit button is pressed
|
||||
const player = new shaka.Player(videoRef.current);
|
||||
|
||||
// Widevine DRM configuration with the provided KIDs and Keys
|
||||
const config = {
|
||||
drm: {
|
||||
clearKeys: {},
|
||||
},
|
||||
};
|
||||
|
||||
// Map KIDs to Keys
|
||||
kidsArray.forEach((kid, index) => {
|
||||
config.drm.clearKeys[kid] = keysArray[index];
|
||||
});
|
||||
|
||||
console.log("Configuring player with the following DRM config:", config);
|
||||
|
||||
player.configure(config);
|
||||
|
||||
const manifestUri = mpdUrl; // Get the MPD URL from state
|
||||
|
||||
player.load(manifestUri)
|
||||
.then(() => {
|
||||
console.log("Shaka Player loaded successfully.");
|
||||
// Start playback once the stream is loaded successfully
|
||||
videoRef.current.play();
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Error loading the stream:", error);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-w-full w-full min-h-full h-full bg-zinc-900 shadow-lg shadow-black flex flex-row overflow-y-auto pt-5 pl-5 pr-5 items-center justify-around">
|
||||
<div className="min-8/10 w-8/10 min-h-8/10 h-8/10 flex flex-row overflow-y-auto items-center justify-around border shadow-lg shadow-red-700 rounded-2xl">
|
||||
<div className="w-7/10 h-7/10 border border-black rounded-2xl p-5 bg-[rgba(0,0,0,0.2)] shadow-lg shadow-black">
|
||||
<video
|
||||
ref={videoRef}
|
||||
className="w-full h-full"
|
||||
controls
|
||||
// Removed autoPlay so it doesn't start until submit is pressed
|
||||
/>
|
||||
</div>
|
||||
<div className="w-2/10 h-7/10 flex flex-col border border-black rounded-2xl p-5 bg-[rgba(0,0,0,0.2)] shadow-lg shadow-black focus-within:w-3/10 transition-all duration-300 ease-in-out">
|
||||
<form className="h-full flex flex-col gap-2">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="MPD URL"
|
||||
id="player_mpd"
|
||||
name="player_mpd"
|
||||
value={mpdUrl}
|
||||
onChange={(e) => setMpdUrl(e.target.value)}
|
||||
className="text-white bg-[rgba(0,0,0,0.2)] focus:outline-none rounded focus:shadow-sm focus:shadow-red-700/50 transition-all duration-300 ease-in-out p-2"
|
||||
/>
|
||||
<textarea
|
||||
placeholder="KIDs, separated by new lines"
|
||||
id="player_kid"
|
||||
name="player_kid"
|
||||
value={kid}
|
||||
onChange={(e) => setKid(e.target.value)}
|
||||
className="text-white bg-[rgba(0,0,0,0.2)] focus:outline-none rounded h-1/4 focus:h-1/3 overflow-y-auto focus:shadow-sm focus:shadow-red-700/50 transition-all duration-300 ease-in-out p-2 resize-none"
|
||||
/>
|
||||
<textarea
|
||||
placeholder="Keys, separated by new lines"
|
||||
id="player_key"
|
||||
name="player_key"
|
||||
value={key}
|
||||
onChange={(e) => setKey(e.target.value)}
|
||||
className="text-white bg-[rgba(0,0,0,0.2)] focus:outline-none rounded h-1/4 focus:h-1/3 focus:shadow-sm focus:shadow-red-700/50 transition-all duration-300 ease-in-out p-2 resize-none"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleSubmit}
|
||||
className="mt-auto bg-white text-black rounded p-2 hover:bg-slate-200 transition-colors duration-300 ease-in-out w-full cursor-pointer active:transform active:scale-95 overflow-x-hidden overflow-y-hidden"
|
||||
>
|
||||
Submit
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default TestPlayer;
|
19
cdrm-frontend/src/components/Sidebar/Footer/Footer.jsx
Normal file
@ -0,0 +1,19 @@
|
||||
import React from 'react'
|
||||
|
||||
function Footer() {
|
||||
return (
|
||||
<div className='flex flex-row w-full h-full items-center justify-around overflow-hidden'>
|
||||
<a href='https://discord.cdrm-project.com' className='w-1/6 h-1/6 hover:animate-bounce'>
|
||||
<img src='/discord.svg' alt='Discord Logo'/>
|
||||
</a>
|
||||
<a href='https://telegram.cdrm-project.com' className='w-1/6 h-1/6 hover:animate-bounce'>
|
||||
<img src='/telegram.svg' alt='Telegram Logo' />
|
||||
</a>
|
||||
<a href='https://github.com/tpd94' className='w-1/6 h-1/6 hover:animate-bounce'>
|
||||
<img src='/github.svg' alt='Github Logo' />
|
||||
</a>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Footer
|
13
cdrm-frontend/src/components/Sidebar/Header/Header.jsx
Normal file
@ -0,0 +1,13 @@
|
||||
import React from 'react'
|
||||
|
||||
function NavHead() {
|
||||
return (
|
||||
<div className='flex flex-row w-full h-full shadow-lg shadow-black items-center justify-center overflow-hidden'>
|
||||
<a href='/' className='w-2/3 h-2/3 flex'>
|
||||
<img src='/logo.svg' alt='CDRM Logo'/>
|
||||
</a>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default NavHead
|
12
cdrm-frontend/src/components/Sidebar/MainNav/MainNav.jsx
Normal file
@ -0,0 +1,12 @@
|
||||
import React from 'react'
|
||||
import NavItems from './NavItems'
|
||||
|
||||
function MainNav() {
|
||||
return (
|
||||
<div className='flex flex-col w-full h-full overflow-y-auto shadow-lg shadow-black'>
|
||||
< NavItems />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default MainNav
|
37
cdrm-frontend/src/components/Sidebar/MainNav/NavItems.jsx
Normal file
@ -0,0 +1,37 @@
|
||||
import React from 'react'
|
||||
import { Link, useLocation } from 'react-router-dom';
|
||||
|
||||
function NavItems() {
|
||||
const location = useLocation();
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={`w-full h-1/10 flex flex-col items-center ${location.pathname === '/' ? 'bg-black/50' : 'hover:bg-black/35'} transition-all duration-300 ease-in-out overflow-hidden mt-5`}>
|
||||
<Link to='/' className='flex w-8/10 items-center'>
|
||||
<img src='/home.svg' alt='Home' className='flex h-5/10 w-3/10'/>
|
||||
<p className='w-7/10 h-full flex items-center text-xl text-white'>Home</p>
|
||||
</Link>
|
||||
</div>
|
||||
<div className={`w-full h-1/10 flex flex-col items-center ${location.pathname === '/Cache' ? 'bg-black/50' : 'hover:bg-black/35'} transition-all duration-300 ease-in-out overflow-hidden`}>
|
||||
<Link to='/cache' className='flex w-8/10 items-center'>
|
||||
<img src='/search.svg' alt='Search' className='flex h-5/10 w-3/10'/>
|
||||
<p className='w-7/10 h-full flex items-center text-xl text-white'>Cache</p>
|
||||
</Link>
|
||||
</div>
|
||||
<div className={`w-full h-1/10 flex flex-col items-center ${location.pathname === '/TestPlayer' ? 'bg-black/50' : 'hover:bg-black/35'} transition-all duration-300 ease-in-out overflow-hidden`}>
|
||||
<Link to='/testplayer' className='flex w-8/10 items-center'>
|
||||
<img src='/video.svg' alt='Test Player' className='flex h-5/10 w-3/10'/>
|
||||
<p className='w-7/10 h-full flex items-center text-xl text-white'>Test Player</p>
|
||||
</Link>
|
||||
</div>
|
||||
<div className={`w-full h-1/10 flex flex-col items-center ${location.pathname === '/API' ? 'bg-black/50' : 'hover:bg-black/35'} transition-all duration-300 ease-in-out overflow-hidden`}>
|
||||
<Link to='/api' className='flex w-8/10 items-center'>
|
||||
<img src='/docu_logo.svg' alt='API' className='flex h-5/10 w-3/10'/>
|
||||
<p className='w-7/10 h-full flex items-center text-xl text-white'>API</p>
|
||||
</Link>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default NavItems
|
22
cdrm-frontend/src/components/Sidebar/Sidebar.jsx
Normal file
@ -0,0 +1,22 @@
|
||||
import React from 'react'
|
||||
import MainNav from './MainNav/MainNav'
|
||||
import Footer from './Footer/Footer'
|
||||
import NavHead from './Header/Header'
|
||||
|
||||
function Sidebar() {
|
||||
return (
|
||||
<div className='flex flex-col w-full h-full bg-zinc-900 border-r-1'>
|
||||
<div className='w-full h-1/10'>
|
||||
<NavHead />
|
||||
</div>
|
||||
<div className='w-full h-8/10'>
|
||||
<MainNav />
|
||||
</div>
|
||||
<div className='w-full h-1/10'>
|
||||
<Footer />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Sidebar
|
9
cdrm-frontend/src/main.jsx
Normal file
@ -0,0 +1,9 @@
|
||||
import { StrictMode } from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import App from './App.jsx'
|
||||
|
||||
createRoot(document.getElementById('root')).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>,
|
||||
)
|
21
cdrm-frontend/vite.config.js
Normal file
@ -0,0 +1,21 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
import tailwindcss from '@tailwindcss/vite'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react(), tailwindcss(),],
|
||||
server: {
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://127.0.0.1:5000',
|
||||
changeOrigin: true,
|
||||
secure: false,
|
||||
},
|
||||
'/remotecdm': {
|
||||
target: 'http://127.0.0.1:5000',
|
||||
changeOrigin: true,
|
||||
secure: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
126
custom_functions/database/cache_to_db_mariadb.py
Normal file
@ -0,0 +1,126 @@
|
||||
import os
|
||||
import yaml
|
||||
import mysql.connector
|
||||
from mysql.connector import Error
|
||||
|
||||
|
||||
|
||||
def get_db_config():
|
||||
# Configure your MariaDB connection
|
||||
with open(f'{os.getcwd()}/configs/config.yaml', 'r') as file:
|
||||
config = yaml.safe_load(file)
|
||||
db_config = {
|
||||
'host': f'{config["mariadb"]["host"]}',
|
||||
'user': f'{config["mariadb"]["user"]}',
|
||||
'password': f'{config["mariadb"]["password"]}',
|
||||
'database': f'{config["mariadb"]["database"]}'
|
||||
}
|
||||
return db_config
|
||||
|
||||
|
||||
def create_database():
|
||||
try:
|
||||
with mysql.connector.connect(**get_db_config()) as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS licenses (
|
||||
SERVICE VARCHAR(255),
|
||||
PSSH TEXT,
|
||||
KID VARCHAR(255) PRIMARY KEY,
|
||||
`Key` TEXT,
|
||||
License_URL TEXT,
|
||||
Headers TEXT,
|
||||
Cookies TEXT,
|
||||
Data BLOB
|
||||
)
|
||||
''')
|
||||
conn.commit()
|
||||
except Error as e:
|
||||
print(f"Error: {e}")
|
||||
|
||||
def cache_to_db(service=None, pssh=None, kid=None, key=None, license_url=None, headers=None, cookies=None, data=None):
|
||||
try:
|
||||
with mysql.connector.connect(**get_db_config()) as conn:
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute('SELECT 1 FROM licenses WHERE KID = %s', (kid,))
|
||||
existing_record = cursor.fetchone()
|
||||
|
||||
cursor.execute('''
|
||||
INSERT INTO licenses (SERVICE, PSSH, KID, `Key`, License_URL, Headers, Cookies, Data)
|
||||
VALUES (%s, %s, %s, %s, %s, %s, %s, %s)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
SERVICE = VALUES(SERVICE),
|
||||
PSSH = VALUES(PSSH),
|
||||
`Key` = VALUES(`Key`),
|
||||
License_URL = VALUES(License_URL),
|
||||
Headers = VALUES(Headers),
|
||||
Cookies = VALUES(Cookies),
|
||||
Data = VALUES(Data)
|
||||
''', (service, pssh, kid, key, license_url, headers, cookies, data))
|
||||
conn.commit()
|
||||
|
||||
return True if existing_record else False
|
||||
except Error as e:
|
||||
print(f"Error: {e}")
|
||||
return False
|
||||
|
||||
def search_by_pssh_or_kid(search_filter):
|
||||
results = set()
|
||||
try:
|
||||
with mysql.connector.connect(**get_db_config()) as conn:
|
||||
cursor = conn.cursor()
|
||||
like_filter = f"%{search_filter}%"
|
||||
|
||||
cursor.execute('SELECT PSSH, KID, `Key` FROM licenses WHERE PSSH LIKE %s', (like_filter,))
|
||||
results.update(cursor.fetchall())
|
||||
|
||||
cursor.execute('SELECT PSSH, KID, `Key` FROM licenses WHERE KID LIKE %s', (like_filter,))
|
||||
results.update(cursor.fetchall())
|
||||
|
||||
final_results = [{'PSSH': row[0], 'KID': row[1], 'Key': row[2]} for row in results]
|
||||
return final_results[:20]
|
||||
except Error as e:
|
||||
print(f"Error: {e}")
|
||||
return []
|
||||
|
||||
def get_key_by_kid_and_service(kid, service):
|
||||
try:
|
||||
with mysql.connector.connect(**get_db_config()) as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('SELECT `Key` FROM licenses WHERE KID = %s AND SERVICE = %s', (kid, service))
|
||||
result = cursor.fetchone()
|
||||
return result[0] if result else None
|
||||
except Error as e:
|
||||
print(f"Error: {e}")
|
||||
return None
|
||||
|
||||
def get_kid_key_dict(service_name):
|
||||
try:
|
||||
with mysql.connector.connect(**get_db_config()) as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('SELECT KID, `Key` FROM licenses WHERE SERVICE = %s', (service_name,))
|
||||
return {row[0]: row[1] for row in cursor.fetchall()}
|
||||
except Error as e:
|
||||
print(f"Error: {e}")
|
||||
return {}
|
||||
|
||||
def get_unique_services():
|
||||
try:
|
||||
with mysql.connector.connect(**get_db_config()) as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('SELECT DISTINCT SERVICE FROM licenses')
|
||||
return [row[0] for row in cursor.fetchall()]
|
||||
except Error as e:
|
||||
print(f"Error: {e}")
|
||||
return []
|
||||
|
||||
def key_count():
|
||||
try:
|
||||
with mysql.connector.connect(**get_db_config()) as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('SELECT COUNT(KID) FROM licenses')
|
||||
return cursor.fetchone()[0]
|
||||
except Error as e:
|
||||
print(f"Error: {e}")
|
||||
return 0
|
123
custom_functions/database/cache_to_db_sqlite.py
Normal file
@ -0,0 +1,123 @@
|
||||
import sqlite3
|
||||
import os
|
||||
|
||||
def create_database():
|
||||
# Using with statement to manage the connection and cursor
|
||||
with sqlite3.connect(f'{os.getcwd()}/databases/sql/key_cache.db') as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS licenses (
|
||||
SERVICE TEXT,
|
||||
PSSH TEXT,
|
||||
KID TEXT PRIMARY KEY,
|
||||
Key TEXT,
|
||||
License_URL TEXT,
|
||||
Headers TEXT,
|
||||
Cookies TEXT,
|
||||
Data TEXT
|
||||
)
|
||||
''')
|
||||
|
||||
def cache_to_db(service: str = None, pssh: str = None, kid: str = None, key: str = None, license_url: str = None, headers: str = None, cookies: str = None, data: str = None):
|
||||
with sqlite3.connect(f'{os.getcwd()}/databases/sql/key_cache.db') as conn:
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Check if the record with the given KID already exists
|
||||
cursor.execute('''SELECT 1 FROM licenses WHERE KID = ?''', (kid,))
|
||||
existing_record = cursor.fetchone()
|
||||
|
||||
# Insert or replace the record
|
||||
cursor.execute('''
|
||||
INSERT OR REPLACE INTO licenses (SERVICE, PSSH, KID, Key, License_URL, Headers, Cookies, Data)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
''', (service, pssh, kid, key, license_url, headers, cookies, data))
|
||||
|
||||
# If the record was existing and updated, return True (updated), else return False (added)
|
||||
return True if existing_record else False
|
||||
|
||||
def search_by_pssh_or_kid(search_filter):
|
||||
# Using with statement to automatically close the connection
|
||||
with sqlite3.connect(f'{os.getcwd()}/databases/sql/key_cache.db') as conn:
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Initialize a set to store unique matching records
|
||||
results = set()
|
||||
|
||||
# Search for records where PSSH contains the search_filter
|
||||
cursor.execute('''
|
||||
SELECT * FROM licenses WHERE PSSH LIKE ?
|
||||
''', ('%' + search_filter + '%',))
|
||||
rows = cursor.fetchall()
|
||||
for row in rows:
|
||||
results.add((row[1], row[2], row[3])) # (PSSH, KID, Key)
|
||||
|
||||
# Search for records where KID contains the search_filter
|
||||
cursor.execute('''
|
||||
SELECT * FROM licenses WHERE KID LIKE ?
|
||||
''', ('%' + search_filter + '%',))
|
||||
rows = cursor.fetchall()
|
||||
for row in rows:
|
||||
results.add((row[1], row[2], row[3])) # (PSSH, KID, Key)
|
||||
|
||||
# Convert the set of results to a list of dictionaries for output
|
||||
final_results = [{'PSSH': result[0], 'KID': result[1], 'Key': result[2]} for result in results]
|
||||
|
||||
return final_results[:20]
|
||||
|
||||
def get_key_by_kid_and_service(kid, service):
|
||||
# Using 'with' to automatically close the connection when done
|
||||
with sqlite3.connect(f'{os.getcwd()}/databases/sql/key_cache.db') as conn:
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Query to search by KID and SERVICE
|
||||
cursor.execute('''
|
||||
SELECT Key FROM licenses WHERE KID = ? AND SERVICE = ?
|
||||
''', (kid, service))
|
||||
|
||||
# Fetch the result
|
||||
result = cursor.fetchone()
|
||||
|
||||
# Check if a result was found
|
||||
return result[0] if result else None # The 'Key' is the first (and only) column returned in the result
|
||||
|
||||
def get_kid_key_dict(service_name):
|
||||
# Using with statement to automatically manage the connection and cursor
|
||||
with sqlite3.connect(f'{os.getcwd()}/databases/sql/key_cache.db') as conn:
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Query to fetch KID and Key for the selected service
|
||||
cursor.execute('''
|
||||
SELECT KID, Key FROM licenses WHERE SERVICE = ?
|
||||
''', (service_name,))
|
||||
|
||||
# Fetch all results and create the dictionary
|
||||
kid_key_dict = {row[0]: row[1] for row in cursor.fetchall()}
|
||||
|
||||
return kid_key_dict
|
||||
|
||||
def get_unique_services():
|
||||
# Using with statement to automatically manage the connection and cursor
|
||||
with sqlite3.connect(f'{os.getcwd()}/databases/sql/key_cache.db') as conn:
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Query to get distinct services from the 'licenses' table
|
||||
cursor.execute('SELECT DISTINCT SERVICE FROM licenses')
|
||||
|
||||
# Fetch all results and extract the unique services
|
||||
services = cursor.fetchall()
|
||||
|
||||
# Extract the service names from the tuple list
|
||||
unique_services = [service[0] for service in services]
|
||||
|
||||
return unique_services
|
||||
|
||||
def key_count():
|
||||
# Using with statement to automatically manage the connection and cursor
|
||||
with sqlite3.connect(f'{os.getcwd()}/databases/sql/key_cache.db') as conn:
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Count the number of KID entries in the licenses table
|
||||
cursor.execute('SELECT COUNT(KID) FROM licenses')
|
||||
count = cursor.fetchone()[0] # Fetch the result and get the count
|
||||
|
||||
return count
|
375
custom_functions/decrypt/api_decrypt.py
Normal file
@ -0,0 +1,375 @@
|
||||
from pywidevine.cdm import Cdm as widevineCdm
|
||||
from pywidevine.device import Device as widevineDevice
|
||||
from pywidevine.pssh import PSSH as widevinePSSH
|
||||
from pyplayready.cdm import Cdm as playreadyCdm
|
||||
from pyplayready.device import Device as playreadyDevice
|
||||
from pyplayready.system.pssh import PSSH as playreadyPSSH
|
||||
import requests
|
||||
import base64
|
||||
import ast
|
||||
import glob
|
||||
import os
|
||||
import yaml
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
def find_license_key(data, keywords=None):
|
||||
if keywords is None:
|
||||
keywords = ["license", "licenseData", "widevine2License"] # Default list of keywords to search for
|
||||
|
||||
# If the data is a dictionary, check each key
|
||||
if isinstance(data, dict):
|
||||
for key, value in data.items():
|
||||
if any(keyword in key.lower() for keyword in
|
||||
keywords): # Check if any keyword is in the key (case-insensitive)
|
||||
return value.replace("-", "+").replace("_", "/") # Return the value immediately when found
|
||||
# Recursively check if the value is a dictionary or list
|
||||
if isinstance(value, (dict, list)):
|
||||
result = find_license_key(value, keywords) # Recursively search
|
||||
if result: # If a value is found, return it
|
||||
return result.replace("-", "+").replace("_", "/")
|
||||
|
||||
# If the data is a list, iterate through each item
|
||||
elif isinstance(data, list):
|
||||
for item in data:
|
||||
result = find_license_key(item, keywords) # Recursively search
|
||||
if result: # If a value is found, return it
|
||||
return result.replace("-", "+").replace("_", "/")
|
||||
|
||||
return None # Return None if no matching key is found
|
||||
|
||||
|
||||
def find_license_challenge(data, keywords=None, new_value=None):
|
||||
if keywords is None:
|
||||
keywords = ["license", "licenseData", "widevine2License", "licenseRequest"] # Default list of keywords to search for
|
||||
|
||||
# If the data is a dictionary, check each key
|
||||
if isinstance(data, dict):
|
||||
for key, value in data.items():
|
||||
if any(keyword in key.lower() for keyword in keywords): # Check if any keyword is in the key (case-insensitive)
|
||||
data[key] = new_value # Modify the value in-place
|
||||
# Recursively check if the value is a dictionary or list
|
||||
elif isinstance(value, (dict, list)):
|
||||
find_license_challenge(value, keywords, new_value) # Recursively modify in place
|
||||
|
||||
# If the data is a list, iterate through each item
|
||||
elif isinstance(data, list):
|
||||
for i, item in enumerate(data):
|
||||
result = find_license_challenge(item, keywords, new_value) # Recursively modify in place
|
||||
|
||||
return data # Return the modified original data (no new structure is created)
|
||||
|
||||
|
||||
def is_base64(string):
|
||||
try:
|
||||
# Try decoding the string
|
||||
decoded_data = base64.b64decode(string)
|
||||
# Check if the decoded data, when re-encoded, matches the original string
|
||||
return base64.b64encode(decoded_data).decode('utf-8') == string
|
||||
except Exception:
|
||||
# If decoding or encoding fails, it's not Base64
|
||||
return False
|
||||
|
||||
def api_decrypt(pssh:str = None, license_url: str = None, headers: str = None, cookies: str = None, json_data: str = None):
|
||||
with open(f'{os.getcwd()}/configs/config.yaml', 'r') as file:
|
||||
config = yaml.safe_load(file)
|
||||
if config['database_type'].lower() == 'sqlite':
|
||||
from custom_functions.database.cache_to_db_sqlite import cache_to_db
|
||||
elif config['database_type'].lower() == 'mariadb':
|
||||
from custom_functions.database.cache_to_db_mariadb import cache_to_db
|
||||
if pssh is None:
|
||||
return {
|
||||
'status': 'error',
|
||||
'message': 'No PSSH provided'
|
||||
}
|
||||
try:
|
||||
if "</WRMHEADER>".encode("utf-16-le") in base64.b64decode(pssh): # PR
|
||||
try:
|
||||
pr_pssh = playreadyPSSH(pssh)
|
||||
except Exception as error:
|
||||
return {
|
||||
'status': 'error',
|
||||
'message': f'An error occurred processing PSSH\n\n{error}'
|
||||
}
|
||||
try:
|
||||
base_name = config["default_pr_cdm"]
|
||||
if not base_name.endswith(".prd"):
|
||||
base_name += ".prd"
|
||||
prd_files = glob.glob(f'{os.getcwd()}/configs/CDMs/PR/{base_name}')
|
||||
else:
|
||||
prd_files = glob.glob(f'{os.getcwd()}/configs/CDMs/PR/{base_name}')
|
||||
if prd_files:
|
||||
pr_device = playreadyDevice.load(prd_files[0])
|
||||
else:
|
||||
return {
|
||||
'status': 'error',
|
||||
'message': 'No default .prd file found'
|
||||
}
|
||||
except Exception as error:
|
||||
return {
|
||||
'status': 'error',
|
||||
'message': f'An error occurred location PlayReady CDM file\n\n{error}'
|
||||
}
|
||||
try:
|
||||
pr_cdm = playreadyCdm.from_device(pr_device)
|
||||
except Exception as error:
|
||||
return {
|
||||
'status': 'error',
|
||||
'message': f'An error occurred loading PlayReady CDM\n\n{error}'
|
||||
}
|
||||
try:
|
||||
pr_session_id = pr_cdm.open()
|
||||
except Exception as error:
|
||||
return {
|
||||
'status': 'error',
|
||||
'message': f'An error occurred opening a CDM session\n\n{error}'
|
||||
}
|
||||
try:
|
||||
pr_challenge = pr_cdm.get_license_challenge(pr_session_id, pr_pssh.wrm_headers[0])
|
||||
except Exception as error:
|
||||
return {
|
||||
'status': 'error',
|
||||
'message': f'An error occurred getting license challenge\n\n{error}'
|
||||
}
|
||||
try:
|
||||
if headers:
|
||||
format_headers = ast.literal_eval(headers)
|
||||
else:
|
||||
format_headers = None
|
||||
except Exception as error:
|
||||
return {
|
||||
'status': 'error',
|
||||
'message': f'An error occurred getting headers\n\n{error}'
|
||||
}
|
||||
try:
|
||||
if cookies:
|
||||
format_cookies = ast.literal_eval(cookies)
|
||||
else:
|
||||
format_cookies = None
|
||||
except Exception as error:
|
||||
return {
|
||||
'status': 'error',
|
||||
'message': f'An error occurred getting cookies\n\n{error}'
|
||||
}
|
||||
try:
|
||||
if json_data and not is_base64(json_data):
|
||||
format_json_data = ast.literal_eval(json_data)
|
||||
else:
|
||||
format_json_data = None
|
||||
except Exception as error:
|
||||
return {
|
||||
'status': 'error',
|
||||
'message': f'An error occurred getting json_data\n\n{error}'
|
||||
}
|
||||
licence = None
|
||||
try:
|
||||
licence = requests.post(
|
||||
url=license_url,
|
||||
headers=format_headers,
|
||||
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 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:
|
||||
try:
|
||||
wv_pssh = widevinePSSH(pssh)
|
||||
except Exception as error:
|
||||
return {
|
||||
'status': 'error',
|
||||
'message': f'An error occurred processing PSSH\n\n{error}'
|
||||
}
|
||||
try:
|
||||
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'
|
||||
}
|
||||
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
|
||||
try:
|
||||
licence = requests.post(
|
||||
url=license_url,
|
||||
headers=format_headers,
|
||||
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 Exception as error:
|
||||
return {
|
||||
'status': 'error',
|
||||
'message': f'An error occurred sending license reqeust\n\n{error}\n\n{licence.content}'
|
||||
}
|
||||
try:
|
||||
wv_cdm.parse_license(wv_session_id, licence.content)
|
||||
except:
|
||||
try:
|
||||
license_json = licence.json()
|
||||
license_value = find_license_key(license_json)
|
||||
wv_cdm.parse_license(wv_session_id, license_value)
|
||||
except Exception as error:
|
||||
return {
|
||||
'status': 'error',
|
||||
'message': f'An error occurred parsing license content\n\n{error}\n\n{licence.content}'
|
||||
}
|
||||
returned_keys = ""
|
||||
try:
|
||||
keys = list(wv_cdm.get_keys(wv_session_id))
|
||||
except Exception as error:
|
||||
return {
|
||||
'status': 'error',
|
||||
'message': f'An error occurred getting keys\n\n{error}'
|
||||
}
|
||||
try:
|
||||
for index, key in enumerate(keys):
|
||||
if key.type != 'SIGNING':
|
||||
cache_to_db(pssh=pssh, license_url=license_url, headers=headers, cookies=cookies, data=wv_challenge if json_data is None else json_data, kid=key.kid.hex, key=key.key.hex())
|
||||
if index != len(keys) - 1:
|
||||
returned_keys += f"{key.kid.hex}:{key.key.hex()}\n"
|
||||
else:
|
||||
returned_keys += f"{key.kid.hex}:{key.key.hex()}"
|
||||
except Exception as error:
|
||||
return {
|
||||
'status': 'error',
|
||||
'message': f'An error occurred formatting keys\n\n{error}'
|
||||
}
|
||||
try:
|
||||
wv_cdm.close(wv_session_id)
|
||||
except Exception as error:
|
||||
return {
|
||||
'status': 'error',
|
||||
'message': f'An error occurred closing session\n\n{error}'
|
||||
}
|
||||
try:
|
||||
return {
|
||||
'status': 'success',
|
||||
'message': returned_keys
|
||||
}
|
||||
except Exception as error:
|
||||
return {
|
||||
'status': 'error',
|
||||
'message': f'An error occurred getting returned_keys\n\n{error}'
|
||||
}
|
68
custom_functions/prechecks/cdm_checks.py
Normal file
@ -0,0 +1,68 @@
|
||||
import os
|
||||
import yaml
|
||||
import requests
|
||||
|
||||
|
||||
|
||||
def check_for_wvd_cdm():
|
||||
with open(f'{os.getcwd()}/configs/config.yaml', 'r') as file:
|
||||
config = yaml.safe_load(file)
|
||||
if config['default_wv_cdm'] == '':
|
||||
answer = ' '
|
||||
while answer[0].upper() != 'Y' and answer[0].upper() != 'N':
|
||||
answer = input('No default Widevine CDM specified, would you like to download one from The CDM Project? (Y)es/(N)o: ')
|
||||
if answer[0].upper() == 'Y':
|
||||
response = requests.get(url='https://cdm-project.com/CDRM-Team/CDMs/raw/branch/main/Widevine/L3/public.wvd')
|
||||
if response.status_code == 200:
|
||||
with open(f'{os.getcwd()}/configs/CDMs/WV/public.wvd', 'wb') as file:
|
||||
file.write(response.content)
|
||||
config['default_wv_cdm'] = 'public'
|
||||
with open(f'{os.getcwd()}/configs/config.yaml', 'w') as file:
|
||||
yaml.dump(config, file)
|
||||
print("Successfully downloaded Widevine CDM")
|
||||
else:
|
||||
exit(f"Download failed, please try again or place a .wvd file in {os.getcwd()}/configs/CDMs/WV and specify the name in {os.getcwd()}/configs/config.yaml")
|
||||
if answer[0].upper() == 'N':
|
||||
exit(f"Place a .wvd file in {os.getcwd()}/configs/CDMs/WV and specify the name in {os.getcwd()}/configs/config.yaml")
|
||||
else:
|
||||
base_name = config["default_wv_cdm"]
|
||||
if not base_name.endswith(".wvd"):
|
||||
base_name += ".wvd"
|
||||
if os.path.exists(f'{os.getcwd()}/configs/CDMs/WV/{base_name}'):
|
||||
return
|
||||
else:
|
||||
exit(f"Widevine CDM {base_name} does not exist in {os.getcwd()}/configs/CDMs/WV")
|
||||
|
||||
def check_for_prd_cdm():
|
||||
with open(f'{os.getcwd()}/configs/config.yaml', 'r') as file:
|
||||
config = yaml.safe_load(file)
|
||||
if config['default_pr_cdm'] == '':
|
||||
answer = ' '
|
||||
while answer[0].upper() != 'Y' and answer[0].upper() != 'N':
|
||||
answer = input('No default PlayReady CDM specified, would you like to download one from The CDM Project? (Y)es/(N)o: ')
|
||||
if answer[0].upper() == 'Y':
|
||||
response = requests.get(url='https://cdm-project.com/CDRM-Team/CDMs/raw/branch/main/Playready/SL2000/public.prd')
|
||||
if response.status_code == 200:
|
||||
with open(f'{os.getcwd()}/configs/CDMs/PR/public.prd', 'wb') as file:
|
||||
file.write(response.content)
|
||||
config['default_pr_cdm'] = 'public'
|
||||
with open(f'{os.getcwd()}/configs/config.yaml', 'w') as file:
|
||||
yaml.dump(config, file)
|
||||
print("Successfully downloaded PlayReady CDM")
|
||||
else:
|
||||
exit(f"Download failed, please try again or place a .prd file in {os.getcwd()}/configs/CDMs/PR and specify the name in {os.getcwd()}/configs/config.yaml")
|
||||
if answer[0].upper() == 'N':
|
||||
exit(f"Place a .prd file in {os.getcwd()}/configs/CDMs/PR and specify the name in {os.getcwd()}/configs/config.yaml")
|
||||
else:
|
||||
base_name = config["default_pr_cdm"]
|
||||
if not base_name.endswith(".prd"):
|
||||
base_name += ".prd"
|
||||
if os.path.exists(f'{os.getcwd()}/configs/CDMs/PR/{base_name}'):
|
||||
return
|
||||
else:
|
||||
exit(f"PlayReady CDM {base_name} does not exist in {os.getcwd()}/configs/CDMs/WV")
|
||||
|
||||
|
||||
def check_for_cdms():
|
||||
check_for_wvd_cdm()
|
||||
check_for_prd_cdm()
|
25
custom_functions/prechecks/config_file_checks.py
Normal file
@ -0,0 +1,25 @@
|
||||
import os
|
||||
|
||||
def check_for_config_file():
|
||||
if os.path.exists(f'{os.getcwd()}/configs/config.yaml'):
|
||||
return
|
||||
else:
|
||||
default_config = """\
|
||||
default_wv_cdm: ''
|
||||
default_pr_cdm: ''
|
||||
# change the type to mariadb to use mariadb below
|
||||
database_type: 'sqlite'
|
||||
fqdn: ''
|
||||
remote_cdm_secret: ''
|
||||
|
||||
# uncomment all the lines below to use mariadb and fill out the information
|
||||
#mariadb:
|
||||
# user: ''
|
||||
# password: ''
|
||||
# host: ''
|
||||
# port: ''
|
||||
# database: ''
|
||||
"""
|
||||
with open(f'{os.getcwd()}/configs/config.yaml', 'w') as f:
|
||||
f.write(default_config)
|
||||
return
|
29
custom_functions/prechecks/database_checks.py
Normal file
@ -0,0 +1,29 @@
|
||||
import os
|
||||
import yaml
|
||||
|
||||
def check_for_sqlite_database():
|
||||
with open(f'{os.getcwd()}/configs/config.yaml', 'r') as file:
|
||||
config = yaml.safe_load(file)
|
||||
if os.path.exists(f'{os.getcwd()}/databases/key_cache.db'):
|
||||
return
|
||||
else:
|
||||
if config['database_type'].lower() != 'mariadb':
|
||||
from custom_functions.database.cache_to_db_sqlite import create_database
|
||||
create_database()
|
||||
return
|
||||
else:
|
||||
return
|
||||
|
||||
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:
|
||||
return
|
||||
|
||||
def check_for_sql_database():
|
||||
check_for_sqlite_database()
|
||||
check_for_mariadb_database()
|
44
custom_functions/prechecks/folder_checks.py
Normal file
@ -0,0 +1,44 @@
|
||||
import os
|
||||
|
||||
def check_for_config_folder():
|
||||
if os.path.isdir(f'{os.getcwd()}/configs'):
|
||||
return
|
||||
else:
|
||||
os.mkdir(f'{os.getcwd()}/configs')
|
||||
return
|
||||
|
||||
def check_for_database_folder():
|
||||
if os.path.isdir(f'{os.getcwd()}/databases'):
|
||||
return
|
||||
else:
|
||||
os.mkdir(f'{os.getcwd()}/databases')
|
||||
os.mkdir(f'{os.getcwd()}/databases/sql')
|
||||
return
|
||||
|
||||
def check_for_cdm_folder():
|
||||
if os.path.isdir(f'{os.getcwd()}/configs/CDMs'):
|
||||
return
|
||||
else:
|
||||
os.mkdir(f'{os.getcwd()}/configs/CDMs')
|
||||
return
|
||||
|
||||
def check_for_wv_cdm_folder():
|
||||
if os.path.isdir(f'{os.getcwd()}/configs/CDMs/WV'):
|
||||
return
|
||||
else:
|
||||
os.mkdir(f'{os.getcwd()}/configs/CDMs/WV')
|
||||
return
|
||||
|
||||
def check_for_cdm_pr_folder():
|
||||
if os.path.isdir(f'{os.getcwd()}/configs/CDMs/PR'):
|
||||
return
|
||||
else:
|
||||
os.mkdir(f'{os.getcwd()}/configs/CDMs/PR')
|
||||
return
|
||||
|
||||
def folder_checks():
|
||||
check_for_config_folder()
|
||||
check_for_database_folder()
|
||||
check_for_cdm_folder()
|
||||
check_for_wv_cdm_folder()
|
||||
check_for_cdm_pr_folder()
|
11
custom_functions/prechecks/precheck.py
Normal file
@ -0,0 +1,11 @@
|
||||
from custom_functions.prechecks.folder_checks import folder_checks
|
||||
from custom_functions.prechecks.config_file_checks import check_for_config_file
|
||||
from custom_functions.prechecks.database_checks import check_for_sql_database
|
||||
from custom_functions.prechecks.cdm_checks import check_for_cdms
|
||||
|
||||
def run_precheck():
|
||||
folder_checks()
|
||||
check_for_config_file()
|
||||
check_for_cdms()
|
||||
check_for_sql_database()
|
||||
return
|
13
examples/config.yaml
Normal file
@ -0,0 +1,13 @@
|
||||
default_wv_cdm: 'public'
|
||||
default_pr_cdm: 'public'
|
||||
# change the type to mariadb to use mariadb below
|
||||
database_type: 'mariadb'
|
||||
fqdn: 'cdrm-project.com'
|
||||
remote_cdm_secret: 'CDRM'
|
||||
|
||||
# uncomment all the lines below to use mariadb and fill out the information
|
||||
mariadb:
|
||||
user: 'cdrm'
|
||||
password: 'password'
|
||||
host: '127.0.0.1'
|
||||
database: 'cdrm_project'
|
21
main.py
Normal file
@ -0,0 +1,21 @@
|
||||
from custom_functions.prechecks.precheck import run_precheck
|
||||
run_precheck()
|
||||
from flask import Flask
|
||||
from flask_cors import CORS
|
||||
from routes.react import react_bp
|
||||
from routes.api import api_bp
|
||||
from routes.remote_device_wv import remotecdm_wv_bp
|
||||
from routes.remote_device_pr import remotecdm_pr_bp
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
CORS(app)
|
||||
|
||||
# Register the blueprint
|
||||
app.register_blueprint(react_bp)
|
||||
app.register_blueprint(api_bp)
|
||||
app.register_blueprint(remotecdm_wv_bp)
|
||||
app.register_blueprint(remotecdm_pr_bp)
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(debug=True)
|
8
requirements.txt
Normal file
@ -0,0 +1,8 @@
|
||||
Flask~=3.1.0
|
||||
Flask-Cors
|
||||
pywidevine~=1.8.0
|
||||
pyplayready~=0.6.0
|
||||
requests~=2.32.3
|
||||
protobuf~=4.25.6
|
||||
PyYAML~=6.0.2
|
||||
mysql-connector-python
|
229
routes/api.py
Normal file
@ -0,0 +1,229 @@
|
||||
import os
|
||||
import sqlite3
|
||||
from flask import Blueprint, jsonify, request, send_file
|
||||
import json
|
||||
from custom_functions.decrypt.api_decrypt import api_decrypt
|
||||
import shutil
|
||||
import math
|
||||
import yaml
|
||||
import mysql.connector
|
||||
from io import StringIO
|
||||
import tempfile
|
||||
import time
|
||||
|
||||
api_bp = Blueprint('api', __name__)
|
||||
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_sqlite import search_by_pssh_or_kid, cache_to_db, \
|
||||
get_key_by_kid_and_service, get_unique_services, get_kid_key_dict, key_count
|
||||
elif config['database_type'].lower() == 'mariadb':
|
||||
from custom_functions.database.cache_to_db_mariadb import search_by_pssh_or_kid, cache_to_db, \
|
||||
get_key_by_kid_and_service, get_unique_services, get_kid_key_dict, key_count
|
||||
|
||||
def get_db_config():
|
||||
# Configure your MariaDB connection
|
||||
with open(f'{os.getcwd()}/configs/config.yaml', 'r') as file:
|
||||
config = yaml.safe_load(file)
|
||||
db_config = {
|
||||
'host': f'{config["mariadb"]["host"]}',
|
||||
'user': f'{config["mariadb"]["user"]}',
|
||||
'password': f'{config["mariadb"]["password"]}',
|
||||
'database': f'{config["mariadb"]["database"]}'
|
||||
}
|
||||
return db_config
|
||||
|
||||
@api_bp.route('/api/cache/search', methods=['POST'])
|
||||
def get_data():
|
||||
search_argument = json.loads(request.data)['input']
|
||||
results = search_by_pssh_or_kid(search_filter=search_argument)
|
||||
return jsonify(results)
|
||||
|
||||
@api_bp.route('/api/cache/<service>/<kid>', methods=['GET'])
|
||||
def get_single_key_service(service, kid):
|
||||
result = get_key_by_kid_and_service(kid=kid, service=service)
|
||||
return jsonify({
|
||||
'code': 0,
|
||||
'content_key': result,
|
||||
})
|
||||
|
||||
@api_bp.route('/api/cache/<service>', methods=['GET'])
|
||||
def get_multiple_key_service(service):
|
||||
result = get_kid_key_dict(service_name=service)
|
||||
pages = math.ceil(len(result) / 10)
|
||||
return jsonify({
|
||||
'code': 0,
|
||||
'content_keys': result,
|
||||
'pages': pages
|
||||
})
|
||||
|
||||
@api_bp.route('/api/cache/<service>/<kid>', methods=['POST'])
|
||||
def add_single_key_service(service, kid):
|
||||
body = request.get_json()
|
||||
content_key = body['content_key']
|
||||
result = cache_to_db(service=service, kid=kid, key=content_key)
|
||||
if result:
|
||||
return jsonify({
|
||||
'code': 0,
|
||||
'updated': True,
|
||||
})
|
||||
elif result is False:
|
||||
return jsonify({
|
||||
'code': 0,
|
||||
'updated': True,
|
||||
})
|
||||
|
||||
@api_bp.route('/api/cache/<service>', methods=['POST'])
|
||||
def add_multiple_key_service(service):
|
||||
body = request.get_json()
|
||||
keys_added = 0
|
||||
keys_updated = 0
|
||||
for kid, key in body['content_keys'].items():
|
||||
result = cache_to_db(service=service, kid=kid, key=key)
|
||||
if result is True:
|
||||
keys_updated += 1
|
||||
elif result is False:
|
||||
keys_added += 1
|
||||
return jsonify({
|
||||
'code': 0,
|
||||
'added': str(keys_added),
|
||||
'updated': str(keys_updated),
|
||||
})
|
||||
|
||||
@api_bp.route('/api/cache', methods=['POST'])
|
||||
def unique_service():
|
||||
services = get_unique_services()
|
||||
return jsonify({
|
||||
'code': 0,
|
||||
'service_list': services,
|
||||
})
|
||||
|
||||
|
||||
@api_bp.route('/api/cache/download', methods=['GET'])
|
||||
def download_database():
|
||||
if config['database_type'].lower() != 'mariadb':
|
||||
original_database_path = f'{os.getcwd()}/databases/sql/key_cache.db'
|
||||
|
||||
# Make a copy of the original database (without locking the original)
|
||||
modified_database_path = f'{os.getcwd()}/databases/sql/key_cache_modified.db'
|
||||
|
||||
# Using shutil.copy2 to preserve metadata (timestamps, etc.)
|
||||
shutil.copy2(original_database_path, modified_database_path)
|
||||
|
||||
# Open the copied database for modification using 'with' statement to avoid locks
|
||||
with sqlite3.connect(modified_database_path) as conn:
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Update all rows to remove Headers and Cookies (set them to NULL or empty strings)
|
||||
cursor.execute('''
|
||||
UPDATE licenses
|
||||
SET Headers = NULL,
|
||||
Cookies = NULL
|
||||
''')
|
||||
|
||||
# No need for explicit commit, it's done automatically with the 'with' block
|
||||
# The connection will automatically be committed and closed when the block ends
|
||||
|
||||
# Send the modified database as an attachment
|
||||
return send_file(modified_database_path, as_attachment=True, download_name='key_cache.db')
|
||||
if config['database_type'].lower() == 'mariadb':
|
||||
try:
|
||||
# Connect to MariaDB
|
||||
conn = mysql.connector.connect(**get_db_config())
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Update sensitive data (this updates the live DB, you may want to duplicate rows instead)
|
||||
cursor.execute('''
|
||||
UPDATE licenses
|
||||
SET Headers = NULL,
|
||||
Cookies = NULL
|
||||
''')
|
||||
|
||||
conn.commit()
|
||||
|
||||
# Now export the table
|
||||
cursor.execute('SELECT * FROM licenses')
|
||||
rows = cursor.fetchall()
|
||||
column_names = [desc[0] for desc in cursor.description]
|
||||
|
||||
# Dump to SQL-like format
|
||||
output = StringIO()
|
||||
output.write(f"-- Dump of `licenses` table\n")
|
||||
for row in rows:
|
||||
values = ', '.join(f"'{str(v).replace('\'', '\\\'')}'" if v is not None else 'NULL' for v in row)
|
||||
output.write(f"INSERT INTO licenses ({', '.join(column_names)}) VALUES ({values});\n")
|
||||
|
||||
# Write to a temp file for download
|
||||
temp_dir = tempfile.gettempdir()
|
||||
temp_path = os.path.join(temp_dir, 'key_cache.sql')
|
||||
with open(temp_path, 'w', encoding='utf-8') as f:
|
||||
f.write(output.getvalue())
|
||||
|
||||
return send_file(temp_path, as_attachment=True, download_name='licenses_dump.sql')
|
||||
except mysql.connector.Error as err:
|
||||
return {"error": str(err)}, 500
|
||||
|
||||
_keycount_cache = {
|
||||
'count': None,
|
||||
'timestamp': 0
|
||||
}
|
||||
|
||||
@api_bp.route('/api/cache/keycount', methods=['GET'])
|
||||
def get_count():
|
||||
now = time.time()
|
||||
if now - _keycount_cache['timestamp'] > 10 or _keycount_cache['count'] is None:
|
||||
_keycount_cache['count'] = key_count()
|
||||
_keycount_cache['timestamp'] = now
|
||||
return jsonify({
|
||||
'count': _keycount_cache['count']
|
||||
})
|
||||
|
||||
@api_bp.route('/api/decrypt', methods=['POST'])
|
||||
def decrypt_data():
|
||||
api_request_data = json.loads(request.data)
|
||||
if 'pssh' in api_request_data:
|
||||
if api_request_data['pssh'] == '':
|
||||
api_request_pssh = None
|
||||
else:
|
||||
api_request_pssh = api_request_data['pssh']
|
||||
else:
|
||||
api_request_pssh = None
|
||||
if 'licurl' in api_request_data:
|
||||
if api_request_data['licurl'] == '':
|
||||
api_request_licurl = None
|
||||
else:
|
||||
api_request_licurl = api_request_data['licurl']
|
||||
else:
|
||||
api_request_licurl = None
|
||||
if '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 = None
|
||||
else:
|
||||
api_request_data = api_request_data['data']
|
||||
else:
|
||||
api_request_data = None
|
||||
result = api_decrypt(pssh=api_request_pssh, license_url=api_request_licurl, headers=api_request_headers, cookies=api_request_cookies, json_data=api_request_data)
|
||||
if result['status'] == 'success':
|
||||
return jsonify({
|
||||
'status': 'success',
|
||||
'message': result['message']
|
||||
})
|
||||
else:
|
||||
return jsonify({
|
||||
'status': 'fail',
|
||||
'message': result['message']
|
||||
})
|
16
routes/react.py
Normal file
@ -0,0 +1,16 @@
|
||||
from flask import Blueprint, send_from_directory, request
|
||||
import os
|
||||
|
||||
react_bp = Blueprint('react_bp', __name__, static_folder=f'{os.getcwd()}/cdrm-frontend/dist', static_url_path='/')
|
||||
|
||||
@react_bp.route('/', methods=['GET'])
|
||||
@react_bp.route('/<path:path>', methods=["GET"])
|
||||
@react_bp.route('/<path>', methods=["GET"])
|
||||
def index(path=''):
|
||||
if request.method == 'GET':
|
||||
if path != "" and os.path.exists(react_bp.static_folder + '/' + path):
|
||||
return send_from_directory(react_bp.static_folder, path)
|
||||
else:
|
||||
return send_from_directory(react_bp.static_folder, 'index.html')
|
||||
else:
|
||||
return
|
200
routes/remote_device_pr.py
Normal file
@ -0,0 +1,200 @@
|
||||
from flask import Blueprint, jsonify, request, current_app, Response
|
||||
import os
|
||||
import yaml
|
||||
from pyplayready.device import Device as PlayReadyDevice
|
||||
from pyplayready.cdm import Cdm as PlayReadyCDM
|
||||
from pyplayready import PSSH as PlayReadyPSSH
|
||||
from pyplayready.exceptions import (InvalidSession, TooManySessions, InvalidLicense, InvalidPssh)
|
||||
|
||||
|
||||
|
||||
remotecdm_pr_bp = Blueprint('remotecdm_pr', __name__)
|
||||
with open(f'{os.getcwd()}/configs/config.yaml', 'r') as file:
|
||||
config = yaml.safe_load(file)
|
||||
|
||||
@remotecdm_pr_bp.route('/remotecdm/playready', methods=['GET', 'HEAD'])
|
||||
def remote_cdm_playready():
|
||||
if request.method == 'GET':
|
||||
return jsonify({
|
||||
'message': 'OK'
|
||||
})
|
||||
if request.method == 'HEAD':
|
||||
response = Response(status=200)
|
||||
response.headers['Server'] = 'playready serve'
|
||||
return response
|
||||
|
||||
|
||||
@remotecdm_pr_bp.route('/remotecdm/playready/deviceinfo', methods=['GET'])
|
||||
def remote_cdm_playready_deviceinfo():
|
||||
base_name = config["default_pr_cdm"]
|
||||
if not base_name.endswith(".prd"):
|
||||
full_file_name = (base_name + ".prd")
|
||||
device = PlayReadyDevice.load(f'{os.getcwd()}/configs/CDMs/PR/{full_file_name}')
|
||||
cdm = PlayReadyCDM.from_device(device)
|
||||
return jsonify({
|
||||
'security_level': cdm.security_level,
|
||||
'host': f'{config["fqdn"]}/remotecdm/playready',
|
||||
'secret': f'{config["remote_cdm_secret"]}',
|
||||
'device_name': f'{base_name}'
|
||||
})
|
||||
|
||||
@remotecdm_pr_bp.route('/remotecdm/playready/<device>/open', methods=['GET'])
|
||||
def remote_cdm_playready_open(device):
|
||||
if str(device).lower() == config['default_pr_cdm'].lower():
|
||||
pr_device = PlayReadyDevice.load(f'{os.getcwd()}/configs/CDMs/PR/{config["default_pr_cdm"]}.prd')
|
||||
cdm = current_app.config['CDM'] = PlayReadyCDM.from_device(pr_device)
|
||||
session_id = cdm.open()
|
||||
return jsonify({
|
||||
'message': 'Success',
|
||||
'data': {
|
||||
'session_id': session_id.hex(),
|
||||
'device': {
|
||||
'security_level': cdm.security_level
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@remotecdm_pr_bp.route('/remotecdm/playready/<device>/close/<session_id>', methods=['GET'])
|
||||
def remote_cdm_playready_close(device, session_id):
|
||||
if str(device).lower() == config['default_pr_cdm'].lower():
|
||||
session_id = bytes.fromhex(session_id)
|
||||
cdm = current_app.config["CDM"]
|
||||
if not cdm:
|
||||
return jsonify({
|
||||
'status': 400,
|
||||
'message': f'No CDM for "{device}" has been opened yet. No session to close'
|
||||
})
|
||||
try:
|
||||
cdm.close(session_id)
|
||||
except InvalidSession:
|
||||
return jsonify({
|
||||
'status': 400,
|
||||
'message': f'Invalid session ID "{session_id.hex()}", it may have expired'
|
||||
})
|
||||
return jsonify({
|
||||
'status': 200,
|
||||
'message': f'Successfully closed Session "{session_id.hex()}".',
|
||||
})
|
||||
else:
|
||||
return jsonify({
|
||||
'status': 400,
|
||||
'message': f'Unauthorized'
|
||||
})
|
||||
|
||||
@remotecdm_pr_bp.route('/remotecdm/playready/<device>/get_license_challenge', methods=['POST'])
|
||||
def remote_cdm_playready_get_license_challenge(device):
|
||||
if str(device).lower() == config['default_pr_cdm'].lower():
|
||||
body = request.get_json()
|
||||
for required_field in ("session_id", "init_data"):
|
||||
if not body.get(required_field):
|
||||
return jsonify({
|
||||
'status': 400,
|
||||
'message': f'Missing required field "{required_field}" in JSON body'
|
||||
})
|
||||
cdm = current_app.config["CDM"]
|
||||
session_id = bytes.fromhex(body["session_id"])
|
||||
init_data = body["init_data"]
|
||||
if not init_data.startswith("<WRMHEADER"):
|
||||
try:
|
||||
pssh = PlayReadyPSSH(init_data)
|
||||
if pssh.wrm_headers:
|
||||
init_data = pssh.wrm_headers[0]
|
||||
except InvalidPssh as e:
|
||||
return jsonify({
|
||||
'message': f'Unable to parse base64 PSSH, {e}'
|
||||
})
|
||||
try:
|
||||
license_request = cdm.get_license_challenge(
|
||||
session_id=session_id,
|
||||
wrm_header=init_data
|
||||
)
|
||||
except InvalidSession:
|
||||
return jsonify({
|
||||
'message': f"Invalid Session ID '{session_id.hex()}', it may have expired."
|
||||
})
|
||||
except Exception as e:
|
||||
return jsonify({
|
||||
'message': f'Error, {e}'
|
||||
})
|
||||
return jsonify({
|
||||
'message': 'success',
|
||||
'data': {
|
||||
'challenge': license_request
|
||||
}
|
||||
})
|
||||
|
||||
@remotecdm_pr_bp.route('/remotecdm/playready/<device>/parse_license', methods=['POST'])
|
||||
def remote_cdm_playready_parse_license(device):
|
||||
if str(device).lower() == config['default_pr_cdm'].lower():
|
||||
body = request.get_json()
|
||||
for required_field in ("license_message", "session_id"):
|
||||
if not body.get(required_field):
|
||||
return jsonify({
|
||||
'message': f'Missing required field "{required_field}" in JSON body'
|
||||
})
|
||||
cdm = current_app.config["CDM"]
|
||||
if not cdm:
|
||||
return jsonify({
|
||||
'message': f"No Cdm session for {device} has been opened yet. No session to use."
|
||||
})
|
||||
session_id = bytes.fromhex(body["session_id"])
|
||||
license_message = body["license_message"]
|
||||
try:
|
||||
cdm.parse_license(session_id, license_message)
|
||||
except InvalidSession:
|
||||
return jsonify({
|
||||
'message': f"Invalid Session ID '{session_id.hex()}', it may have expired."
|
||||
})
|
||||
except InvalidLicense as e:
|
||||
return jsonify({
|
||||
'message': f"Invalid License, {e}"
|
||||
})
|
||||
except Exception as e:
|
||||
return jsonify({
|
||||
'message': f"Error, {e}"
|
||||
})
|
||||
return jsonify({
|
||||
'message': 'Successfully parsed and loaded the Keys from the License message'
|
||||
})
|
||||
|
||||
@remotecdm_pr_bp.route('/remotecdm/playready/<device>/get_keys', methods=['POST'])
|
||||
def remote_cdm_playready_get_keys(device):
|
||||
if str(device).lower() == config['default_pr_cdm'].lower():
|
||||
body = request.get_json()
|
||||
for required_field in ("session_id",):
|
||||
if not body.get(required_field):
|
||||
return jsonify({
|
||||
'message': f'Missing required field "{required_field}" in JSON body'
|
||||
})
|
||||
session_id = bytes.fromhex(body["session_id"])
|
||||
cdm = current_app.config["CDM"]
|
||||
if not cdm:
|
||||
return jsonify({
|
||||
'message': f"Missing required field '{required_field}' in JSON body."
|
||||
})
|
||||
try:
|
||||
keys = cdm.get_keys(session_id)
|
||||
except InvalidSession:
|
||||
return jsonify({
|
||||
'message': f"Invalid Session ID '{session_id.hex()}', it may have expired."
|
||||
})
|
||||
except Exception as e:
|
||||
return jsonify({
|
||||
'message': f"Error, {e}"
|
||||
})
|
||||
keys_json = [
|
||||
{
|
||||
"key_id": key.key_id.hex,
|
||||
"key": key.key.hex(),
|
||||
"type": key.key_type.value,
|
||||
"cipher_type": key.cipher_type.value,
|
||||
"key_length": key.key_length,
|
||||
}
|
||||
for key in keys
|
||||
]
|
||||
return jsonify({
|
||||
'message': 'success',
|
||||
'data': {
|
||||
'keys': keys_json
|
||||
}
|
||||
})
|
369
routes/remote_device_wv.py
Normal file
@ -0,0 +1,369 @@
|
||||
import os
|
||||
from flask import Blueprint, jsonify, request, current_app, Response
|
||||
import base64
|
||||
from typing import Any, Optional, Union
|
||||
from google.protobuf.message import DecodeError
|
||||
from pywidevine.pssh import PSSH as widevinePSSH
|
||||
from pywidevine import __version__
|
||||
from pywidevine.cdm import Cdm as widevineCDM
|
||||
from pywidevine.device import Device as widevineDevice
|
||||
from pywidevine.exceptions import (InvalidContext, InvalidInitData, InvalidLicenseMessage, InvalidLicenseType,
|
||||
InvalidSession, SignatureMismatch, TooManySessions)
|
||||
|
||||
import yaml
|
||||
|
||||
remotecdm_wv_bp = Blueprint('remotecdm_wv', __name__)
|
||||
with open(f'{os.getcwd()}/configs/config.yaml', 'r') as file:
|
||||
config = yaml.safe_load(file)
|
||||
|
||||
@remotecdm_wv_bp.route('/remotecdm/widevine', methods=['GET', 'HEAD'])
|
||||
def remote_cdm_widevine():
|
||||
if request.method == 'GET':
|
||||
return jsonify({
|
||||
'status': 200,
|
||||
'message': f"{config['fqdn'].upper()} Remote Widevine CDM."
|
||||
})
|
||||
if request.method == 'HEAD':
|
||||
response = Response(status=200)
|
||||
response.headers['Server'] = f'https://github.com/devine-dl/pywidevine serve v{__version__}'
|
||||
return response
|
||||
|
||||
@remotecdm_wv_bp.route('/remotecdm/widevine/deviceinfo', methods=['GET'])
|
||||
def remote_cdm_widevine_deviceinfo():
|
||||
if request.method == 'GET':
|
||||
base_name = config["default_wv_cdm"]
|
||||
if not base_name.endswith(".wvd"):
|
||||
full_file_name = (base_name + ".wvd")
|
||||
device = widevineDevice.load(f'{os.getcwd()}/configs/CDMs/WV/{full_file_name}')
|
||||
cdm = widevineCDM.from_device(device)
|
||||
return jsonify({
|
||||
'device_type': cdm.device_type.name,
|
||||
'system_id': cdm.system_id,
|
||||
'security_level': cdm.security_level,
|
||||
'host': f'{config["fqdn"]}/remotecdm/widevine',
|
||||
'secret': f'{config["remote_cdm_secret"]}',
|
||||
'device_name': f'{base_name}'
|
||||
})
|
||||
|
||||
@remotecdm_wv_bp.route('/remotecdm/widevine/<device>/open', methods=['GET'])
|
||||
def remote_cdm_widevine_open(device):
|
||||
if str(device).lower() == config['default_wv_cdm'].lower():
|
||||
wv_device = widevineDevice.load(f'{os.getcwd()}/configs/CDMs/WV/{config["default_wv_cdm"]}.wvd')
|
||||
cdm = current_app.config["CDM"] = widevineCDM.from_device(wv_device)
|
||||
session_id = cdm.open()
|
||||
return jsonify({
|
||||
'status': 200,
|
||||
'message': 'Success',
|
||||
'data': {
|
||||
'session_id': session_id.hex(),
|
||||
'device': {
|
||||
'system_id': cdm.system_id,
|
||||
'security_level': cdm.security_level,
|
||||
}
|
||||
}
|
||||
})
|
||||
else:
|
||||
return jsonify({
|
||||
'status': 400,
|
||||
'message': 'Unauthorized'
|
||||
})
|
||||
|
||||
@remotecdm_wv_bp.route('/remotecdm/widevine/<device>/close/<session_id>', methods=['GET'])
|
||||
def remote_cdm_widevine_close(device, session_id):
|
||||
if str(device).lower() == config['default_wv_cdm'].lower():
|
||||
session_id = bytes.fromhex(session_id)
|
||||
cdm = current_app.config["CDM"]
|
||||
if not cdm:
|
||||
return jsonify({
|
||||
'status': 400,
|
||||
'message': f'No CDM for "{device}" has been opened yet. No session to close'
|
||||
})
|
||||
try:
|
||||
cdm.close(session_id)
|
||||
except InvalidSession:
|
||||
return jsonify({
|
||||
'status': 400,
|
||||
'message': f'Invalid session ID "{session_id.hex()}", it may have expired'
|
||||
})
|
||||
return jsonify({
|
||||
'status': 200,
|
||||
'message': f'Successfully closed Session "{session_id.hex()}".',
|
||||
})
|
||||
else:
|
||||
return jsonify({
|
||||
'status': 400,
|
||||
'message': f'Unauthorized'
|
||||
})
|
||||
|
||||
@remotecdm_wv_bp.route('/remotecdm/widevine/<device>/set_service_certificate', methods=['POST'])
|
||||
def remote_cdm_widevine_set_service_certificate(device):
|
||||
if str(device).lower() == config['default_wv_cdm'].lower():
|
||||
body = request.get_json()
|
||||
for required_field in ("session_id", "certificate"):
|
||||
if required_field == "certificate":
|
||||
has_field = required_field in body # it needs the key, but can be empty/null
|
||||
else:
|
||||
has_field = body.get(required_field)
|
||||
if not has_field:
|
||||
return jsonify({
|
||||
'status': 400,
|
||||
'message': f'Missing required field "{required_field}" in JSON body'
|
||||
})
|
||||
|
||||
session_id = bytes.fromhex(body["session_id"])
|
||||
|
||||
cdm = current_app.config["CDM"]
|
||||
if not cdm:
|
||||
return jsonify({
|
||||
'status': 400,
|
||||
'message': f'No CDM session for "{device}" has been opened yet. No session to use'
|
||||
})
|
||||
|
||||
certificate = body["certificate"]
|
||||
try:
|
||||
provider_id = cdm.set_service_certificate(session_id, certificate)
|
||||
except InvalidSession:
|
||||
return jsonify({
|
||||
'status': 400,
|
||||
'message': f'Invalid session id: "{session_id.hex()}", it may have expired'
|
||||
})
|
||||
except DecodeError as error:
|
||||
return jsonify({
|
||||
'status': 400,
|
||||
'message': f'Invalid Service Certificate, {error}'
|
||||
})
|
||||
except SignatureMismatch:
|
||||
return jsonify({
|
||||
'status': 400,
|
||||
'message': 'Signature Validation failed on the Service Certificate, rejecting'
|
||||
})
|
||||
return jsonify({
|
||||
'status': 200,
|
||||
'message': f"Successfully {['set', 'unset'][not certificate]} the Service Certificate.",
|
||||
'data': {
|
||||
'provider_id': provider_id,
|
||||
}
|
||||
})
|
||||
else:
|
||||
return jsonify({
|
||||
'status': 400,
|
||||
'message': f'Unauthorized'
|
||||
})
|
||||
|
||||
@remotecdm_wv_bp.route('/remotecdm/widevine/<device>/get_service_certificate', methods=['POST'])
|
||||
def remote_cdm_widevine_get_service_certificate(device):
|
||||
if str(device).lower() == config['default_wv_cdm'].lower():
|
||||
body = request.get_json()
|
||||
for required_field in ("session_id",):
|
||||
if not body.get(required_field):
|
||||
return jsonify({
|
||||
'status': 400,
|
||||
'message': f'Missing required field "{required_field}" in JSON body'
|
||||
})
|
||||
|
||||
session_id = bytes.fromhex(body["session_id"])
|
||||
|
||||
cdm = current_app.config["CDM"]
|
||||
|
||||
if not cdm:
|
||||
return jsonify({
|
||||
'status': 400,
|
||||
'message': f'No CDM session for "{device}" has been opened yet. No session to use'
|
||||
})
|
||||
|
||||
try:
|
||||
service_certificate = cdm.get_service_certificate(session_id)
|
||||
except InvalidSession:
|
||||
return jsonify({
|
||||
'status': 400,
|
||||
'message': f'Invalid Session ID "{session_id.hex()}", it may have expired'
|
||||
})
|
||||
if service_certificate:
|
||||
service_certificate_b64 = base64.b64encode(service_certificate.SerializeToString()).decode()
|
||||
else:
|
||||
service_certificate_b64 = None
|
||||
return jsonify({
|
||||
'status': 200,
|
||||
'message': 'Successfully got the Service Certificate',
|
||||
'data': {
|
||||
'service_certificate': service_certificate_b64,
|
||||
}
|
||||
})
|
||||
else:
|
||||
return jsonify({
|
||||
'status': 400,
|
||||
'message': f'Unauthorized'
|
||||
})
|
||||
|
||||
@remotecdm_wv_bp.route('/remotecdm/widevine/<device>/get_license_challenge/<license_type>', methods=['POST'])
|
||||
def remote_cdm_widevine_get_license_challenge(device, license_type):
|
||||
if str(device).lower() == config['default_wv_cdm'].lower():
|
||||
body = request.get_json()
|
||||
for required_field in ("session_id", "init_data"):
|
||||
if not body.get(required_field):
|
||||
return jsonify({
|
||||
'status': 400,
|
||||
'message': f'Missing required field "{required_field}" in JSON body'
|
||||
})
|
||||
session_id = bytes.fromhex(body["session_id"])
|
||||
privacy_mode = body.get("privacy_mode", True)
|
||||
cdm = current_app.config["CDM"]
|
||||
if not cdm:
|
||||
return jsonify({
|
||||
'status': 400,
|
||||
'message': f'No CDM session for "{device}" has been opened yet. No session to use'
|
||||
})
|
||||
if current_app.config.get("force_privacy_mode"):
|
||||
privacy_mode = True
|
||||
if not cdm.get_service_certificate(session_id):
|
||||
return jsonify({
|
||||
'status': 403,
|
||||
'message': 'No Service Certificate set but Privacy Mode is Enforced.'
|
||||
})
|
||||
|
||||
current_app.config['pssh'] = body['init_data']
|
||||
init_data = widevinePSSH(body['init_data'])
|
||||
|
||||
try:
|
||||
license_request = cdm.get_license_challenge(
|
||||
session_id=session_id,
|
||||
pssh=init_data,
|
||||
license_type=license_type,
|
||||
privacy_mode=privacy_mode
|
||||
)
|
||||
except InvalidSession:
|
||||
return jsonify({
|
||||
'status': 400,
|
||||
'message': f'Invalid Session ID "{session_id.hex()}", it may have expired'
|
||||
})
|
||||
except InvalidInitData as error:
|
||||
return jsonify({
|
||||
'status': 400,
|
||||
'message': f'Invalid Init Data, {error}'
|
||||
})
|
||||
except InvalidLicenseType:
|
||||
return jsonify({
|
||||
'status': 400,
|
||||
'message': f'Invalid License Type {license_type}'
|
||||
})
|
||||
return jsonify({
|
||||
'status': 200,
|
||||
'message': 'Success',
|
||||
'data': {
|
||||
'challenge_b64': base64.b64encode(license_request).decode()
|
||||
}
|
||||
})
|
||||
else:
|
||||
return jsonify({
|
||||
'status': 400,
|
||||
'message': f'Unauthorized'
|
||||
})
|
||||
|
||||
|
||||
@remotecdm_wv_bp.route('/remotecdm/widevine/<device>/parse_license', methods=['POST'])
|
||||
def remote_cdm_widevine_parse_license(device):
|
||||
if str(device).lower() == config['default_wv_cdm'].lower():
|
||||
body = request.get_json()
|
||||
for required_field in ("session_id", "license_message"):
|
||||
if not body.get(required_field):
|
||||
return jsonify({
|
||||
'status': 400,
|
||||
'message': f'Missing required field "{required_field}" in JSON body'
|
||||
})
|
||||
session_id = bytes.fromhex(body["session_id"])
|
||||
cdm = current_app.config["CDM"]
|
||||
if not cdm:
|
||||
return jsonify({
|
||||
'status': 400,
|
||||
'message': f'No CDM session for "{device}" has been opened yet. No session to use'
|
||||
})
|
||||
try:
|
||||
cdm.parse_license(session_id, body['license_message'])
|
||||
except InvalidLicenseMessage as error:
|
||||
return jsonify({
|
||||
'status': 400,
|
||||
'message': f'Invalid License Message, {error}'
|
||||
})
|
||||
except InvalidContext as error:
|
||||
return jsonify({
|
||||
'status': 400,
|
||||
'message': f'Invalid Context, {error}'
|
||||
})
|
||||
except InvalidSession:
|
||||
return jsonify({
|
||||
'status': 400,
|
||||
'message': f'Invalid Session ID "{session_id.hex()}", it may have expired'
|
||||
})
|
||||
except SignatureMismatch:
|
||||
return jsonify({
|
||||
'status': 400,
|
||||
'message': f'Signature Validation failed on the License Message, rejecting.'
|
||||
})
|
||||
return jsonify({
|
||||
'status': 200,
|
||||
'message': 'Successfully parsed and loaded the Keys from the License message.',
|
||||
})
|
||||
else:
|
||||
return jsonify({
|
||||
'status': 400,
|
||||
'message': 'Unauthorized'
|
||||
})
|
||||
|
||||
@remotecdm_wv_bp.route('/remotecdm/widevine/<device>/get_keys/<key_type>', methods=['POST'])
|
||||
def remote_cdm_widevine_get_keys(device, key_type):
|
||||
if str(device).lower() == config['default_wv_cdm'].lower():
|
||||
body = request.get_json()
|
||||
for required_field in ("session_id",):
|
||||
if not body.get(required_field):
|
||||
return jsonify({
|
||||
'status': 400,
|
||||
'message': f'Missing required field "{required_field}" in JSON body'
|
||||
})
|
||||
session_id = bytes.fromhex(body["session_id"])
|
||||
key_type: Optional[str] = key_type
|
||||
if key_type == 'ALL':
|
||||
key_type = None
|
||||
cdm = current_app.config["CDM"]
|
||||
if not cdm:
|
||||
return jsonify({
|
||||
'status': 400,
|
||||
'message': f'No CDM session for "{device}" has been opened yet. No session to use'
|
||||
})
|
||||
try:
|
||||
keys = cdm.get_keys(session_id, key_type)
|
||||
except InvalidSession:
|
||||
return jsonify({
|
||||
'status': 400,
|
||||
'message': f'Invalid Session ID "{session_id.hex()}", it may have expired'
|
||||
})
|
||||
except ValueError as error:
|
||||
return jsonify({
|
||||
'status': 400,
|
||||
'message': f'The Key Type value "{key_type}" is invalid, {error}'
|
||||
})
|
||||
keys_json = [
|
||||
{
|
||||
"key_id": key.kid.hex,
|
||||
"key": key.key.hex(),
|
||||
"type": key.type,
|
||||
"permissions": key.permissions
|
||||
}
|
||||
for key in keys
|
||||
if not key_type or key.type == key_type
|
||||
]
|
||||
for entry in keys_json:
|
||||
if config['database_type'].lower() != 'mariadb':
|
||||
from custom_functions.database.cache_to_db_sqlite import cache_to_db
|
||||
elif config['database_type'].lower() == 'mariadb':
|
||||
from custom_functions.database.cache_to_db_mariadb import cache_to_db
|
||||
if entry['type'] != 'SIGNING':
|
||||
cache_to_db(pssh=str(current_app.config['pssh']), kid=entry['key_id'], key=entry['key'])
|
||||
|
||||
|
||||
return jsonify({
|
||||
'status': 200,
|
||||
'message': 'Success',
|
||||
'data': {
|
||||
'keys': keys_json
|
||||
}
|
||||
})
|