Add manifest URL field, reset keys when manifest changes, organize repo, update to Manifest v3 #3
30
.gitignore
vendored
Normal file
30
.gitignore
vendored
Normal file
@ -0,0 +1,30 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
frontend/node_modules
|
||||
frontend/dist
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
# extension release folder
|
||||
extension-release
|
3
.prettierignore
Normal file
3
.prettierignore
Normal file
@ -0,0 +1,3 @@
|
||||
react/
|
||||
frontend/dist/
|
||||
frontend/src/assets/
|
8
.prettierrc.json
Normal file
8
.prettierrc.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"trailingComma": "es5",
|
||||
"tabWidth": 4,
|
||||
"semi": true,
|
||||
"singleQuote": false,
|
||||
"useTabs": false,
|
||||
"printWidth": 100
|
||||
}
|
52
README.md
Normal file
52
README.md
Normal file
@ -0,0 +1,52 @@
|
||||
# CDRM Extension
|
||||
|
||||
An extension to show keys from DRM protected content, which are used to decrypt content.
|
||||
|
||||
## Notes
|
||||
|
||||
Keep these extension core files inside `src`:
|
||||
|
||||
- `background.js`
|
||||
- `content.js`
|
||||
- `inject.js`
|
||||
- `manifest.json`
|
||||
|
||||
The `mv2` folder is for Manifest v2 backup for legacy reasons.
|
||||
|
||||
Frontend React source stays in `frontend`.
|
||||
|
||||
The build process will take care of everything into `extension-release`.
|
||||
|
||||
To update the version across the entire project, simply change the version number in the root `package.json`. The build script will handle version sync automatically to both the extension's version and the frontend's title bar.
|
||||
|
||||
## Build instructions
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Node.js v21 or higher. [Download Node.js here](https://nodejs.org/en/download).
|
||||
|
||||
### How to build by yourself
|
||||
|
||||
- Open terminal at the project root
|
||||
|
||||
- Run the build script:
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm run buildext
|
||||
```
|
||||
|
||||
This will:
|
||||
|
||||
- Sync the version number from the root `package.json` to `src/manifest.json` and `frontend/package.json`
|
||||
- Install frontend dependencies if needed
|
||||
- Build the React frontend
|
||||
- Clean and prepare the `extension-release` folder
|
||||
- Copy extension files in `src`, built frontend assets, and icons into `extension-release`
|
||||
|
||||
### How to load the extension in Google Chrome or Chromium browsers
|
||||
|
||||
1. Go to `chrome://extensions/`
|
||||
2. Enable **Developer mode**
|
||||
3. Click **Load unpacked** and select the `extension-release` folder
|
||||
4. Verify the extension is working by clicking its icon or opening the developer console (F12) to check for any logs or errors.
|
@ -1,84 +0,0 @@
|
||||
// Open popout window when the extension icon is clicked
|
||||
chrome.browserAction.onClicked.addListener(() => {
|
||||
chrome.windows.create({
|
||||
url: chrome.runtime.getURL("react/index.html"),
|
||||
type: "popup", // opens as a floating window
|
||||
width: 800,
|
||||
height: 600
|
||||
});
|
||||
});
|
||||
|
||||
// Listen for messages and store data in chrome.storage.local
|
||||
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||
const { type, data } = message;
|
||||
|
||||
switch (type) {
|
||||
case "DRM_TYPE":
|
||||
console.log("DRM Type:", data);
|
||||
chrome.storage.local.set({ drmType: data });
|
||||
break;
|
||||
|
||||
case "PSSH_DATA":
|
||||
console.log("Storing PSSH:", data);
|
||||
chrome.storage.local.set({ latestPSSH: data });
|
||||
break;
|
||||
|
||||
case "KEYS_DATA":
|
||||
console.log("Storing Decryption Keys:", data);
|
||||
chrome.storage.local.set({ latestKeys: data });
|
||||
break;
|
||||
|
||||
case "LICENSE_URL":
|
||||
console.log("Storling License URL " + data);
|
||||
chrome.storage.local.set({licenseURL: data});
|
||||
break;
|
||||
|
||||
default:
|
||||
console.warn("Unknown message type received:", type);
|
||||
}
|
||||
});
|
||||
|
||||
// Set initial config and injection type on install
|
||||
chrome.runtime.onInstalled.addListener((details) => {
|
||||
if (details.reason === "install") {
|
||||
chrome.storage.local.set({ valid_config: false }, () => {
|
||||
if (chrome.runtime.lastError) {
|
||||
console.error("Error setting valid_config:", chrome.runtime.lastError);
|
||||
} else {
|
||||
console.log("valid_config set to false on first install.");
|
||||
}
|
||||
});
|
||||
|
||||
chrome.storage.local.set({ injection_type: "LICENSE" }, () => {
|
||||
if (chrome.runtime.lastError) {
|
||||
console.error("Error setting Injection Type:", chrome.runtime.lastError);
|
||||
} else {
|
||||
console.log("Injection type set to LICENSE on first install.");
|
||||
}
|
||||
});
|
||||
|
||||
chrome.storage.local.set({ drm_override: "DISABLED" }, () => {
|
||||
if (chrome.runtime.lastError) {
|
||||
console.error("Error setting DRM Override type:", chrome.runtime.lastError);
|
||||
} else {
|
||||
console.log("DRM Override type set to DISABLED on first install.");
|
||||
}
|
||||
});
|
||||
|
||||
chrome.storage.local.set({ cdrm_instance: null }, () => {
|
||||
if (chrome.runtime.lastError) {
|
||||
console.error("Error setting CDRM instance:", chrome.runtime.lastError);
|
||||
} else {
|
||||
console.log("CDRM instance set to null.");
|
||||
}
|
||||
});
|
||||
|
||||
chrome.storage.local.set({ cdrm_api_key: null }, () => {
|
||||
if (chrome.runtime.lastError) {
|
||||
console.error("Error setting CDRM API Key:", chrome.runtime.lastError);
|
||||
} else {
|
||||
console.log("CDRM API Key set.");
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
119
buildext.js
Normal file
119
buildext.js
Normal file
@ -0,0 +1,119 @@
|
||||
import { execSync } from "child_process";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import { minify } from "terser";
|
||||
import url from "url";
|
||||
import syncVersion from "./syncVersion.js";
|
||||
|
||||
const __dirname = path.dirname(url.fileURLToPath(import.meta.url));
|
||||
const frontendDir = path.join(__dirname, "frontend");
|
||||
const distDir = path.join(frontendDir, "dist");
|
||||
const srcDir = path.join(__dirname, "src");
|
||||
const iconDir = path.join(__dirname, "icons");
|
||||
const releaseDir = path.join(__dirname, "extension-release");
|
||||
|
||||
const run = (cmd, cwd) => {
|
||||
console.log(`🛠️ Running: ${cmd}`);
|
||||
execSync(cmd, { cwd, stdio: "inherit" });
|
||||
};
|
||||
|
||||
const copyDir = async (src, dest) => {
|
||||
await fs.promises.mkdir(dest, { recursive: true });
|
||||
await fs.promises.cp(src, dest, {
|
||||
recursive: true,
|
||||
force: true,
|
||||
filter: (src) => !src.endsWith(".map"),
|
||||
});
|
||||
};
|
||||
|
||||
const minifyJS = async (jsContent) => {
|
||||
try {
|
||||
const result = await minify(jsContent, {
|
||||
compress: {
|
||||
drop_console: false, // Keep console logs for debugging
|
||||
drop_debugger: true,
|
||||
pure_funcs: ["console.debug"],
|
||||
},
|
||||
mangle: {
|
||||
reserved: ["chrome"], // Don't mangle chrome API
|
||||
},
|
||||
});
|
||||
return result.code;
|
||||
} catch (error) {
|
||||
console.warn("⚠️ Minification failed, using original:", error.message);
|
||||
return jsContent;
|
||||
}
|
||||
};
|
||||
|
||||
// Copy and minify JavaScript files from src directory
|
||||
const copyAndMinifySrcFiles = async (src, dest) => {
|
||||
await fs.promises.mkdir(dest, { recursive: true });
|
||||
|
||||
const entries = await fs.promises.readdir(src, { withFileTypes: true });
|
||||
|
||||
for (const entry of entries) {
|
||||
const srcPath = path.join(src, entry.name);
|
||||
const destPath = path.join(dest, entry.name);
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
await copyAndMinifySrcFiles(srcPath, destPath);
|
||||
} else if (entry.name.endsWith(".js")) {
|
||||
// Minify JavaScript files
|
||||
console.log(`🗜️ Minifying ${entry.name}...`);
|
||||
const content = await fs.promises.readFile(srcPath, "utf8");
|
||||
const originalSize = Buffer.byteLength(content, "utf8");
|
||||
const minified = await minifyJS(content, entry.name);
|
||||
const minifiedSize = Buffer.byteLength(minified, "utf8");
|
||||
const savings = (((originalSize - minifiedSize) / originalSize) * 100).toFixed(1);
|
||||
console.log(
|
||||
` 📊 ${entry.name}: ${originalSize} → ${minifiedSize} bytes (${savings}% smaller)`
|
||||
);
|
||||
await fs.promises.writeFile(destPath, minified, "utf8");
|
||||
} else {
|
||||
// Copy other files as-is
|
||||
await fs.promises.copyFile(srcPath, destPath);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const main = async () => {
|
||||
await syncVersion();
|
||||
|
||||
console.log("🚀 Starting extension build...");
|
||||
|
||||
// 1. Install frontend deps if needed
|
||||
if (!fs.existsSync(path.join(frontendDir, "node_modules"))) {
|
||||
console.log("📦 node_modules not found. Running npm install...");
|
||||
run("npm install", frontendDir);
|
||||
}
|
||||
|
||||
// 2. Build frontend
|
||||
console.log("📦 Building frontend...");
|
||||
run("npm run build", frontendDir);
|
||||
|
||||
// 3. Clean release folder
|
||||
if (fs.existsSync(releaseDir)) {
|
||||
console.log("🧹 Cleaning existing extension-release folder...");
|
||||
await fs.promises.rm(releaseDir, { recursive: true, force: true });
|
||||
}
|
||||
await fs.promises.mkdir(releaseDir);
|
||||
|
||||
// 4. Copy and minify src files
|
||||
console.log("📦 Copying and minifying src files...");
|
||||
await copyAndMinifySrcFiles(srcDir, releaseDir);
|
||||
|
||||
// 5. Copy frontend dist files to release (merged at root)
|
||||
console.log("📦 Copying frontend dist files to extension-release...");
|
||||
await copyDir(distDir, releaseDir);
|
||||
|
||||
// 6. Copy icon directory to release (merged at root)
|
||||
console.log("📦 Copying icon directory to extension-release...");
|
||||
await copyDir(iconDir, path.join(releaseDir, "icons"));
|
||||
|
||||
console.log("✅ Build complete! extension-release ready.");
|
||||
};
|
||||
|
||||
main().catch((e) => {
|
||||
console.error("❌ Build failed:", e);
|
||||
process.exit(1);
|
||||
});
|
67
content.js
67
content.js
@ -1,67 +0,0 @@
|
||||
// Inject `inject.js` into the page context
|
||||
(function injectScript() {
|
||||
const script = document.createElement('script');
|
||||
script.src = chrome.runtime.getURL('inject.js');
|
||||
script.type = 'text/javascript';
|
||||
script.onload = () => script.remove(); // Clean up
|
||||
// Inject directly into <html> or <head>
|
||||
(document.documentElement || document.head || document.body).appendChild(script);
|
||||
})();
|
||||
|
||||
// Listen for messages from the injected script
|
||||
window.addEventListener("message", function(event) {
|
||||
if (event.source !== window) return;
|
||||
|
||||
if (["__DRM_TYPE__", "__PSSH_DATA__", "__KEYS_DATA__", "__LICENSE_URL__"].includes(event.data?.type)) {
|
||||
chrome.runtime.sendMessage({
|
||||
type: event.data.type.replace("__", "").replace("__", ""),
|
||||
data: event.data.data
|
||||
});
|
||||
}
|
||||
|
||||
if (event.data.type === "__GET_CDM_DEVICES__") {
|
||||
|
||||
chrome.storage.local.get(["widevine_device", "playready_device"], (result) => {
|
||||
const widevine_device = result.widevine_device || null;
|
||||
const playready_device = result.playready_device || null;
|
||||
|
||||
window.postMessage(
|
||||
{
|
||||
type: "__CDM_DEVICES__",
|
||||
widevine_device,
|
||||
playready_device
|
||||
},
|
||||
"*"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
if (event.data.type === "__GET_INJECTION_TYPE__") {
|
||||
|
||||
chrome.storage.local.get("injection_type", (result) => {
|
||||
const injectionType = result.injection_type || "LICENSE";
|
||||
|
||||
window.postMessage(
|
||||
{
|
||||
type: "__INJECTION_TYPE__",
|
||||
injectionType
|
||||
},
|
||||
"*"
|
||||
);
|
||||
});
|
||||
}
|
||||
if (event.data.type === "__GET_DRM_OVERRIDE__") {
|
||||
|
||||
chrome.storage.local.get("drm_override", (result) => {
|
||||
const drmOverride = result.drm_override || "DISABLED";
|
||||
|
||||
window.postMessage(
|
||||
{
|
||||
type: "__DRM_OVERRIDE__",
|
||||
drmOverride
|
||||
},
|
||||
"*"
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
1
frontend/dist/assets/index-UaipKa9p.css
vendored
1
frontend/dist/assets/index-UaipKa9p.css
vendored
File diff suppressed because one or more lines are too long
13
frontend/dist/index.html
vendored
13
frontend/dist/index.html
vendored
@ -1,13 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>CDRM Decryption Extension</title>
|
||||
<script type="module" crossorigin src="./assets/index-ydPQKJSy.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="./assets/index-UaipKa9p.css">
|
||||
</head>
|
||||
<body class="min-w-full min-h-full w-full h-full">
|
||||
<div class="min-w-full min-h-full w-full h-full" id="root"></div>
|
||||
</body>
|
||||
</html>
|
@ -1,33 +1,30 @@
|
||||
import js from '@eslint/js'
|
||||
import globals from 'globals'
|
||||
import reactHooks from 'eslint-plugin-react-hooks'
|
||||
import reactRefresh from 'eslint-plugin-react-refresh'
|
||||
import js from "@eslint/js";
|
||||
import globals from "globals";
|
||||
import reactHooks from "eslint-plugin-react-hooks";
|
||||
import reactRefresh from "eslint-plugin-react-refresh";
|
||||
|
||||
export default [
|
||||
{ ignores: ['dist'] },
|
||||
{
|
||||
files: ['**/*.{js,jsx}'],
|
||||
languageOptions: {
|
||||
ecmaVersion: 2020,
|
||||
globals: globals.browser,
|
||||
parserOptions: {
|
||||
ecmaVersion: 'latest',
|
||||
ecmaFeatures: { jsx: true },
|
||||
sourceType: 'module',
|
||||
},
|
||||
{ ignores: ["dist"] },
|
||||
{
|
||||
files: ["**/*.{js,jsx}"],
|
||||
languageOptions: {
|
||||
ecmaVersion: 2020,
|
||||
globals: globals.browser,
|
||||
parserOptions: {
|
||||
ecmaVersion: "latest",
|
||||
ecmaFeatures: { jsx: true },
|
||||
sourceType: "module",
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
"react-hooks": reactHooks,
|
||||
"react-refresh": reactRefresh,
|
||||
},
|
||||
rules: {
|
||||
...js.configs.recommended.rules,
|
||||
...reactHooks.configs.recommended.rules,
|
||||
"no-unused-vars": ["error", { varsIgnorePattern: "^[A-Z_]" }],
|
||||
"react-refresh/only-export-components": ["warn", { allowConstantExport: true }],
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
'react-hooks': reactHooks,
|
||||
'react-refresh': reactRefresh,
|
||||
},
|
||||
rules: {
|
||||
...js.configs.recommended.rules,
|
||||
...reactHooks.configs.recommended.rules,
|
||||
'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }],
|
||||
'react-refresh/only-export-components': [
|
||||
'warn',
|
||||
{ allowConstantExport: true },
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
||||
];
|
||||
|
@ -1,12 +1,12 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>CDRM Decryption Extension</title>
|
||||
</head>
|
||||
<body class="min-w-full min-h-full w-full h-full">
|
||||
<div class="min-w-full min-h-full w-full h-full" id="root"></div>
|
||||
<script type="module" src="/src/main.jsx"></script>
|
||||
</body>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<title>CDRM Decryption Extension v%APPVERSION%</title>
|
||||
</head>
|
||||
<body class="min-w-full min-h-full w-full h-full">
|
||||
<div class="min-w-full min-h-full w-full h-full" id="root"></div>
|
||||
<script type="module" src="/src/main.jsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
7046
frontend/package-lock.json
generated
7046
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -1,30 +1,31 @@
|
||||
{
|
||||
"name": "frontend",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"lint": "eslint .",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tailwindcss/vite": "^4.1.7",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"react-router-dom": "^7.6.1",
|
||||
"tailwindcss": "^4.1.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.25.0",
|
||||
"@types/react": "^19.1.2",
|
||||
"@types/react-dom": "^19.1.2",
|
||||
"@vitejs/plugin-react": "^4.4.1",
|
||||
"eslint": "^9.25.0",
|
||||
"eslint-plugin-react-hooks": "^5.2.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.19",
|
||||
"globals": "^16.0.0",
|
||||
"vite": "^6.3.5"
|
||||
}
|
||||
"name": "frontend",
|
||||
"private": true,
|
||||
"version": "2.1.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"lint": "eslint .",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tailwindcss/vite": "^4.1.11",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"react-router-dom": "^7.7.0",
|
||||
"tailwindcss": "^4.1.11"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.31.0",
|
||||
"@types/react": "^19.1.8",
|
||||
"@types/react-dom": "^19.1.6",
|
||||
"@vitejs/plugin-react": "^4.7.0",
|
||||
"@vitejs/plugin-react-swc": "^3.11.0",
|
||||
"eslint": "^9.31.0",
|
||||
"eslint-plugin-react-hooks": "^5.2.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.20",
|
||||
"globals": "^16.3.0",
|
||||
"vite": "^7.0.5"
|
||||
}
|
||||
}
|
||||
|
@ -1,77 +1,72 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import {
|
||||
HashRouter as Router,
|
||||
Routes,
|
||||
Route,
|
||||
Navigate,
|
||||
} from "react-router-dom";
|
||||
import { HashRouter as Router, Routes, Route, Navigate } from "react-router-dom";
|
||||
import TopNav from "./components/topnav";
|
||||
import SideNav from "./components/sidenav";
|
||||
import Results from "./components/results";
|
||||
import Settings from "./components/settings";
|
||||
|
||||
function App() {
|
||||
const [isSideNavOpen, setIsSideNavOpen] = useState(false);
|
||||
const [validConfig, setValidConfig] = useState(null); // null = loading
|
||||
const [isSideNavOpen, setIsSideNavOpen] = useState(false);
|
||||
const [validConfig, setValidConfig] = useState(null); // null = loading
|
||||
|
||||
useEffect(() => {
|
||||
chrome.storage.local.get("valid_config", (result) => {
|
||||
if (chrome.runtime.lastError) {
|
||||
console.error("Error reading valid_config:", chrome.runtime.lastError);
|
||||
setValidConfig(false); // fallback
|
||||
} else {
|
||||
setValidConfig(result.valid_config === true);
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
chrome.storage.local.get("valid_config", (result) => {
|
||||
if (chrome.runtime.lastError) {
|
||||
console.error("Error reading valid_config:", chrome.runtime.lastError);
|
||||
setValidConfig(false); // fallback
|
||||
} else {
|
||||
setValidConfig(result.valid_config === true);
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
if (validConfig === null) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-screen bg-black text-white">
|
||||
Loading...
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (validConfig === null) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-screen bg-black text-white">
|
||||
Loading...
|
||||
</div>
|
||||
<Router>
|
||||
<div className="min-w-full min-h-full w-full h-full flex flex-grow bg-black/95 flex-col relative">
|
||||
<div className="w-full min-h-16 max-h-16 h-16 shrink-0 flex sticky top-0 z-20 border-b border-b-white bg-black overflow-x-hidden">
|
||||
<TopNav onMenuClick={() => setIsSideNavOpen(true)} />
|
||||
</div>
|
||||
|
||||
<div id="currentpagecontainer" className="w-full grow overflow-y-auto">
|
||||
<Routes>
|
||||
{!validConfig ? (
|
||||
<>
|
||||
<Route
|
||||
path="/settings"
|
||||
element={
|
||||
<Settings onConfigSaved={() => setValidConfig(true)} />
|
||||
}
|
||||
/>
|
||||
<Route path="*" element={<Navigate to="/settings" replace />} />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Route path="/" element={<Navigate to="/results" replace />} />
|
||||
<Route path="/results" element={<Results />} />
|
||||
<Route path="/settings" element={<Settings />} />
|
||||
</>
|
||||
)}
|
||||
</Routes>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={`fixed top-0 left-0 w-full h-full z-50 bg-black transform transition-transform duration-300 ease-in-out ${
|
||||
isSideNavOpen ? "translate-x-0" : "-translate-x-full"
|
||||
}`}
|
||||
>
|
||||
<SideNav onClose={() => setIsSideNavOpen(false)} />
|
||||
</div>
|
||||
</div>
|
||||
</Router>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Router>
|
||||
<div className="min-w-full min-h-full w-full h-full flex flex-grow bg-black/95 flex-col relative">
|
||||
<div className="w-full min-h-16 max-h-16 h-16 shrink-0 flex sticky top-0 z-20 border-b border-b-white bg-black overflow-x-hidden">
|
||||
<TopNav onMenuClick={() => setIsSideNavOpen(true)} />
|
||||
</div>
|
||||
|
||||
<div id="currentpagecontainer" className="w-full grow overflow-y-auto">
|
||||
<Routes>
|
||||
{!validConfig ? (
|
||||
<>
|
||||
<Route
|
||||
path="/settings"
|
||||
element={
|
||||
<Settings onConfigSaved={() => setValidConfig(true)} />
|
||||
}
|
||||
/>
|
||||
<Route path="*" element={<Navigate to="/settings" replace />} />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Route path="/" element={<Navigate to="/results" replace />} />
|
||||
<Route path="/results" element={<Results />} />
|
||||
<Route path="/settings" element={<Settings />} />
|
||||
</>
|
||||
)}
|
||||
</Routes>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={`fixed top-0 left-0 w-full h-full z-50 bg-black transform transition-transform duration-300 ease-in-out ${
|
||||
isSideNavOpen ? "translate-x-0" : "-translate-x-full"
|
||||
}`}
|
||||
>
|
||||
<SideNav onClose={() => setIsSideNavOpen(false)} />
|
||||
</div>
|
||||
</div>
|
||||
</Router>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
|
@ -1,149 +1,250 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
|
||||
function Results() {
|
||||
const [drmType, setDrmType] = useState("");
|
||||
const [pssh, setPssh] = useState("");
|
||||
const [licenseUrl, setLicenseUrl] = useState("");
|
||||
const [keys, setKeys] = useState([]);
|
||||
const [drmType, setDrmType] = useState("");
|
||||
const [pssh, setPssh] = useState("");
|
||||
const [licenseUrl, setLicenseUrl] = useState("");
|
||||
const [keys, setKeys] = useState([]);
|
||||
const [manifestUrl, setManifestUrl] = useState("");
|
||||
const [currentTabUrl, setCurrentTabUrl] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
chrome.storage.local.get(
|
||||
[
|
||||
"drmType",
|
||||
"latestPSSH",
|
||||
"latestLicenseRequest",
|
||||
"latestKeys",
|
||||
"licenseURL",
|
||||
],
|
||||
(result) => {
|
||||
if (result.drmType) setDrmType(result.drmType);
|
||||
if (result.latestPSSH) setPssh(result.latestPSSH);
|
||||
if (result.licenseURL) setLicenseUrl(result.licenseURL);
|
||||
if (result.latestKeys) {
|
||||
try {
|
||||
const parsed = Array.isArray(result.latestKeys)
|
||||
? result.latestKeys
|
||||
: JSON.parse(result.latestKeys);
|
||||
setKeys(parsed);
|
||||
} catch (e) {
|
||||
console.error("Failed to parse keys:", e);
|
||||
setKeys([]);
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const handleChange = (changes, area) => {
|
||||
if (area === "local") {
|
||||
if (changes.drmType) {
|
||||
setDrmType(changes.drmType.newValue);
|
||||
}
|
||||
if (changes.latestPSSH) {
|
||||
setPssh(changes.latestPSSH.newValue);
|
||||
}
|
||||
if (changes.licenseURL) {
|
||||
setLicenseUrl(changes.licenseURL.newValue);
|
||||
}
|
||||
if (changes.latestKeys) {
|
||||
setKeys(changes.latestKeys.newValue);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
chrome.storage.onChanged.addListener(handleChange);
|
||||
return () => chrome.storage.onChanged.removeListener(handleChange);
|
||||
}, []);
|
||||
|
||||
const handleCapture = () => {
|
||||
// Reset stored values
|
||||
chrome.storage.local.set({
|
||||
drmType: "None",
|
||||
latestPSSH: "None",
|
||||
licenseURL: "None",
|
||||
latestKeys: [],
|
||||
});
|
||||
|
||||
// Get all normal windows to exclude your popup
|
||||
chrome.windows.getAll(
|
||||
{ populate: true, windowTypes: ["normal"] },
|
||||
(windows) => {
|
||||
if (!windows || windows.length === 0) {
|
||||
console.warn("No normal Chrome windows found");
|
||||
return;
|
||||
}
|
||||
|
||||
// Find the last focused normal window
|
||||
const lastFocusedWindow = windows.find((w) => w.focused) || windows[0];
|
||||
|
||||
if (!lastFocusedWindow) {
|
||||
console.warn("No focused normal window found");
|
||||
return;
|
||||
}
|
||||
|
||||
// Find the active tab in that window (that is a regular webpage)
|
||||
const activeTab = lastFocusedWindow.tabs.find(
|
||||
(tab) => tab.active && tab.url && /^https?:\/\//.test(tab.url)
|
||||
useEffect(() => {
|
||||
chrome.storage.local.get(
|
||||
[
|
||||
"drmType",
|
||||
"latestPSSH",
|
||||
"latestLicenseRequest",
|
||||
"latestKeys",
|
||||
"licenseURL",
|
||||
"manifestURL",
|
||||
],
|
||||
(result) => {
|
||||
if (result.drmType) setDrmType(result.drmType || "");
|
||||
if (result.latestPSSH) setPssh(result.latestPSSH || "");
|
||||
if (result.licenseURL) setLicenseUrl(result.licenseURL || "");
|
||||
if (result.manifestURL) setManifestUrl(result.manifestURL || "");
|
||||
if (result.latestKeys) {
|
||||
try {
|
||||
const parsed = Array.isArray(result.latestKeys)
|
||||
? result.latestKeys
|
||||
: JSON.parse(result.latestKeys);
|
||||
setKeys(parsed);
|
||||
} catch (e) {
|
||||
console.error("Failed to parse keys:", e);
|
||||
setKeys([]);
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
if (activeTab?.id) {
|
||||
chrome.tabs.reload(activeTab.id, () => {
|
||||
if (chrome.runtime.lastError) {
|
||||
console.error("Failed to reload tab:", chrome.runtime.lastError);
|
||||
// Get current tab URL when component mounts
|
||||
chrome.windows.getAll({ populate: true, windowTypes: ["normal"] }, (windows) => {
|
||||
if (windows && windows.length > 0) {
|
||||
const lastFocusedWindow = windows.find((w) => w.focused) || windows[0];
|
||||
if (lastFocusedWindow) {
|
||||
const activeTab = lastFocusedWindow.tabs.find(
|
||||
(tab) => tab.active && tab.url && /^https?:\/\//.test(tab.url)
|
||||
);
|
||||
if (activeTab?.url) {
|
||||
setCurrentTabUrl(activeTab.url);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
console.warn("No active tab found in the last focused normal window");
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="w-full grow flex h-full overflow-y-auto overflow-x-auto flex-col text-white p-4">
|
||||
<button
|
||||
onClick={handleCapture}
|
||||
className="w-full h-10 bg-sky-500 rounded-md p-2 mt-2 text-white cursor-pointer hover:bg-sky-600"
|
||||
>
|
||||
Capture current tab
|
||||
</button>
|
||||
<p className="text-2xl mt-5">DRM Type</p>
|
||||
<input
|
||||
type="text"
|
||||
value={drmType}
|
||||
className="w-full h-10 bg-slate-800/50 rounded-md p-2 mt-2 text-white"
|
||||
placeholder="None"
|
||||
disabled
|
||||
/>
|
||||
<p className="text-2xl mt-5">PSSH</p>
|
||||
<input
|
||||
type="text"
|
||||
value={pssh}
|
||||
className="w-full h-10 bg-slate-800/50 rounded-md p-2 mt-2 text-white"
|
||||
placeholder="None"
|
||||
disabled
|
||||
/>
|
||||
<p className="text-2xl mt-5">License URL</p>
|
||||
<input
|
||||
type="text"
|
||||
value={licenseUrl}
|
||||
className="w-full h-10 bg-slate-800/50 rounded-md p-2 mt-2 text-white"
|
||||
placeholder="None"
|
||||
disabled
|
||||
/>
|
||||
<p className="text-2xl mt-5">Keys</p>
|
||||
<div className="w-full min-h-64 h-64 flex items-center justify-center text-center overflow-y-auto bg-slate-800/50 rounded-md p-2 mt-2 text-white whitespace-pre-line">
|
||||
{Array.isArray(keys) &&
|
||||
keys.filter((k) => k.type !== "SIGNING").length > 0 ? (
|
||||
keys
|
||||
.filter((k) => k.type !== "SIGNING")
|
||||
.map((k) => `${k.key_id || k.keyId}:${k.key}`)
|
||||
.join("\n")
|
||||
) : (
|
||||
<span className="text-gray-400">None</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
const handleChange = (changes, area) => {
|
||||
if (area === "local") {
|
||||
if (changes.drmType) {
|
||||
setDrmType(changes.drmType.newValue || "");
|
||||
}
|
||||
if (changes.latestPSSH) {
|
||||
setPssh(changes.latestPSSH.newValue || "");
|
||||
}
|
||||
if (changes.licenseURL) {
|
||||
setLicenseUrl(changes.licenseURL.newValue || "");
|
||||
}
|
||||
if (changes.manifestURL) {
|
||||
setManifestUrl(changes.manifestURL.newValue || "");
|
||||
}
|
||||
if (changes.latestKeys) {
|
||||
setKeys(changes.latestKeys.newValue || []);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
chrome.storage.onChanged.addListener(handleChange);
|
||||
return () => chrome.storage.onChanged.removeListener(handleChange);
|
||||
}, []);
|
||||
|
||||
const handleCapture = () => {
|
||||
// Reset stored values
|
||||
chrome.storage.local.set({
|
||||
drmType: "",
|
||||
latestPSSH: "",
|
||||
licenseURL: "",
|
||||
manifestURL: "",
|
||||
latestKeys: [],
|
||||
});
|
||||
|
||||
// Get all normal windows to exclude your popup
|
||||
chrome.windows.getAll({ populate: true, windowTypes: ["normal"] }, (windows) => {
|
||||
if (!windows || windows.length === 0) {
|
||||
console.warn("No normal Chrome windows found");
|
||||
return;
|
||||
}
|
||||
|
||||
// Find the last focused normal window
|
||||
const lastFocusedWindow = windows.find((w) => w.focused) || windows[0];
|
||||
|
||||
if (!lastFocusedWindow) {
|
||||
console.warn("No focused normal window found");
|
||||
return;
|
||||
}
|
||||
|
||||
// Find the active tab in that window (that is a regular webpage)
|
||||
const activeTab = lastFocusedWindow.tabs.find(
|
||||
(tab) => tab.active && tab.url && /^https?:\/\//.test(tab.url)
|
||||
);
|
||||
|
||||
if (activeTab?.id) {
|
||||
chrome.tabs.reload(activeTab.id, () => {
|
||||
if (chrome.runtime.lastError) {
|
||||
console.error("Failed to reload tab:", chrome.runtime.lastError);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
console.warn("No active tab found in the last focused normal window");
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Check if current tab is YouTube
|
||||
const isYouTube = () => {
|
||||
return currentTabUrl.includes("youtube.com") || currentTabUrl.includes("youtu.be");
|
||||
};
|
||||
|
||||
// Get manifest URL display value
|
||||
const getManifestDisplayValue = () => {
|
||||
if (manifestUrl) {
|
||||
return manifestUrl;
|
||||
}
|
||||
if (isYouTube()) {
|
||||
return "[Use yt-dlp to download video]";
|
||||
}
|
||||
return "";
|
||||
};
|
||||
|
||||
// Get manifest URL placeholder
|
||||
const getManifestPlaceholder = () => {
|
||||
if (isYouTube() && !manifestUrl) {
|
||||
return "[Use yt-dlp to download video]";
|
||||
}
|
||||
return "[Not available]";
|
||||
};
|
||||
|
||||
// Export to JSON file
|
||||
const hasData = () => {
|
||||
return Array.isArray(keys) && keys.filter((k) => k.type !== "SIGNING").length > 0;
|
||||
};
|
||||
|
||||
const handleExportJSON = () => {
|
||||
const exportData = {
|
||||
drmType: drmType || null,
|
||||
manifestUrl: manifestUrl || null,
|
||||
pssh: pssh || null,
|
||||
licenseUrl: licenseUrl || null,
|
||||
keys:
|
||||
Array.isArray(keys) && keys.length > 0
|
||||
? keys
|
||||
.filter((k) => k.type !== "SIGNING")
|
||||
.map((k) => `${k.key_id || k.keyId}:${k.key}`)
|
||||
: null,
|
||||
exportedAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
const blob = new Blob([JSON.stringify(exportData, null, 2)], {
|
||||
type: "application/json",
|
||||
});
|
||||
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = `drm-data-${new Date().toISOString().slice(0, 19).replace(/:/g, "-")}.json`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full grow flex h-full overflow-y-auto overflow-x-auto flex-col text-white p-4">
|
||||
<button
|
||||
onClick={handleCapture}
|
||||
className="w-full h-10 bg-sky-500 rounded-md p-2 mt-2 text-white cursor-pointer font-bold hover:bg-sky-600"
|
||||
>
|
||||
Capture current tab
|
||||
</button>
|
||||
|
||||
<p className="text-2xl mt-5">DRM Type</p>
|
||||
<input
|
||||
type="text"
|
||||
value={drmType}
|
||||
className="w-full h-10 bg-slate-800/50 rounded-md p-2 mt-2 text-white font-mono"
|
||||
placeholder="[Not available]"
|
||||
disabled
|
||||
/>
|
||||
|
||||
<p className="text-2xl mt-5">Manifest URL</p>
|
||||
<input
|
||||
type="text"
|
||||
value={getManifestDisplayValue()}
|
||||
className={`w-full h-10 bg-slate-800/50 rounded-md p-2 mt-2 font-mono ${
|
||||
isYouTube() && !manifestUrl ? "text-yellow-400" : "text-white"
|
||||
}`}
|
||||
placeholder={getManifestPlaceholder()}
|
||||
disabled
|
||||
/>
|
||||
|
||||
<p className="text-2xl mt-5">PSSH</p>
|
||||
<input
|
||||
type="text"
|
||||
value={pssh}
|
||||
className="w-full h-10 bg-slate-800/50 rounded-md p-2 mt-2 text-white font-mono"
|
||||
placeholder="[Not available]"
|
||||
disabled
|
||||
/>
|
||||
|
||||
<p className="text-2xl mt-5">License URL</p>
|
||||
<input
|
||||
type="text"
|
||||
value={licenseUrl}
|
||||
className="w-full h-10 bg-slate-800/50 rounded-md p-2 mt-2 text-white font-mono"
|
||||
placeholder="[Not available]"
|
||||
disabled
|
||||
/>
|
||||
|
||||
<p className="text-2xl mt-5">Keys</p>
|
||||
<div className="w-full min-h-64 h-64 flex items-center justify-center text-center overflow-y-auto bg-slate-800/50 rounded-md p-2 mt-2 text-white whitespace-pre-line font-mono">
|
||||
{Array.isArray(keys) && keys.filter((k) => k.type !== "SIGNING").length > 0 ? (
|
||||
keys
|
||||
.filter((k) => k.type !== "SIGNING")
|
||||
.map((k) => `${k.key_id || k.keyId}:${k.key}`)
|
||||
.join("\n")
|
||||
) : (
|
||||
<span className="text-gray-400">[Not available]</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{hasData() && (
|
||||
<button
|
||||
onClick={handleExportJSON}
|
||||
className="w-full h-10 bg-green-500 rounded-md p-2 mt-5 text-white cursor-pointer font-bold hover:bg-green-600"
|
||||
>
|
||||
Export as JSON
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Results;
|
||||
|
@ -2,149 +2,143 @@ import { useState, useEffect } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
function Settings({ onConfigSaved }) {
|
||||
const [instanceUrl, setInstanceUrl] = useState("");
|
||||
const [storedUrl, setStoredUrl] = useState(null);
|
||||
const [message, setMessage] = useState(null);
|
||||
const [messageType, setMessageType] = useState(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [instanceUrl, setInstanceUrl] = useState("");
|
||||
const [storedUrl, setStoredUrl] = useState(null);
|
||||
const [message, setMessage] = useState(null);
|
||||
const [messageType, setMessageType] = useState(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const navigate = useNavigate();
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
chrome.storage.local.get("cdrm_instance", (result) => {
|
||||
if (chrome.runtime.lastError) {
|
||||
console.error(
|
||||
"Error fetching CDRM instance:",
|
||||
chrome.runtime.lastError
|
||||
);
|
||||
} else if (result.cdrm_instance) {
|
||||
setStoredUrl(result.cdrm_instance);
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleSave = async () => {
|
||||
const trimmedUrl = instanceUrl.trim().replace(/\/+$/, "");
|
||||
if (!trimmedUrl) {
|
||||
setMessage("Please enter a valid URL.");
|
||||
setMessageType("error");
|
||||
return;
|
||||
}
|
||||
|
||||
const endpoint = trimmedUrl + "/api/extension";
|
||||
setLoading(true);
|
||||
setMessage(null);
|
||||
|
||||
try {
|
||||
const response = await fetch(endpoint, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.status === true) {
|
||||
setMessage("Successfully connected to CDRM Instance.");
|
||||
setMessageType("success");
|
||||
|
||||
const widevineRes = await fetch(
|
||||
`${trimmedUrl}/remotecdm/widevine/deviceinfo`
|
||||
);
|
||||
if (!widevineRes.ok)
|
||||
throw new Error("Failed to fetch Widevine device info");
|
||||
const widevineData = await widevineRes.json();
|
||||
|
||||
const playreadyRes = await fetch(
|
||||
`${trimmedUrl}/remotecdm/playready/deviceinfo`
|
||||
);
|
||||
if (!playreadyRes.ok)
|
||||
throw new Error("Failed to fetch PlayReady device info");
|
||||
const playreadyData = await playreadyRes.json();
|
||||
|
||||
chrome.storage.local.set(
|
||||
{
|
||||
valid_config: true,
|
||||
cdrm_instance: trimmedUrl,
|
||||
widevine_device: {
|
||||
device_type: widevineData.device_type,
|
||||
system_id: widevineData.system_id,
|
||||
security_level: widevineData.security_level,
|
||||
secret: widevineData.secret,
|
||||
device_name: widevineData.device_name,
|
||||
host: trimmedUrl,
|
||||
},
|
||||
playready_device: {
|
||||
security_level: playreadyData.security_level,
|
||||
secret: playreadyData.secret,
|
||||
device_name: playreadyData.device_name,
|
||||
host: trimmedUrl,
|
||||
},
|
||||
},
|
||||
() => {
|
||||
useEffect(() => {
|
||||
chrome.storage.local.get("cdrm_instance", (result) => {
|
||||
if (chrome.runtime.lastError) {
|
||||
console.error(
|
||||
"Error saving to chrome.storage:",
|
||||
chrome.runtime.lastError
|
||||
);
|
||||
setMessage("Error saving configuration.");
|
||||
setMessageType("error");
|
||||
} else {
|
||||
console.log("Configuration saved.");
|
||||
setStoredUrl(trimmedUrl);
|
||||
setInstanceUrl("");
|
||||
if (onConfigSaved) onConfigSaved();
|
||||
navigate("/results"); // Automatically redirect after success
|
||||
console.error("Error fetching CDRM instance:", chrome.runtime.lastError);
|
||||
} else if (result.cdrm_instance) {
|
||||
setStoredUrl(result.cdrm_instance);
|
||||
}
|
||||
}
|
||||
);
|
||||
} else {
|
||||
throw new Error("Invalid response from endpoint.");
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Connection error:", err);
|
||||
setMessage("Invalid endpoint or device info could not be retrieved.");
|
||||
setMessageType("error");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="w-full h-full overflow-y-auto overflow-x-auto flex flex-col p-4">
|
||||
<input
|
||||
type="text"
|
||||
value={instanceUrl}
|
||||
onChange={(e) => setInstanceUrl(e.target.value)}
|
||||
placeholder={
|
||||
storedUrl
|
||||
? `Current CDRM Instance: ${storedUrl}`
|
||||
: "CDRM Instance URL (e.g., https://cdrm-project.com/, http://127.0.0.1:5000/)"
|
||||
const handleSave = async () => {
|
||||
const trimmedUrl = instanceUrl.trim().replace(/\/+$/, "");
|
||||
if (!trimmedUrl) {
|
||||
setMessage("Please enter a valid URL.");
|
||||
setMessageType("error");
|
||||
return;
|
||||
}
|
||||
className="w-full p-4 text-lg bg-gray-800 text-white border border-gray-700 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 mt-4"
|
||||
/>
|
||||
<button
|
||||
onClick={handleSave}
|
||||
disabled={loading}
|
||||
className={`mt-4 p-2 ${
|
||||
loading ? "bg-blue-400" : "bg-blue-600 hover:bg-blue-700"
|
||||
} text-white rounded-md transition duration-300`}
|
||||
>
|
||||
{loading ? "Connecting..." : "Save Settings"}
|
||||
</button>
|
||||
|
||||
{message && (
|
||||
<p
|
||||
className={`mt-2 text-sm text-center ${
|
||||
messageType === "success" ? "text-green-400" : "text-red-400"
|
||||
}`}
|
||||
>
|
||||
{message}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
const endpoint = trimmedUrl + "/api/extension";
|
||||
setLoading(true);
|
||||
setMessage(null);
|
||||
|
||||
try {
|
||||
const response = await fetch(endpoint, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.status === true) {
|
||||
setMessage("Successfully connected to CDRM Instance.");
|
||||
setMessageType("success");
|
||||
|
||||
const widevineRes = await fetch(`${trimmedUrl}/remotecdm/widevine/deviceinfo`);
|
||||
if (!widevineRes.ok) throw new Error("Failed to fetch Widevine device info");
|
||||
const widevineData = await widevineRes.json();
|
||||
|
||||
const playreadyRes = await fetch(`${trimmedUrl}/remotecdm/playready/deviceinfo`);
|
||||
if (!playreadyRes.ok) throw new Error("Failed to fetch PlayReady device info");
|
||||
const playreadyData = await playreadyRes.json();
|
||||
|
||||
chrome.storage.local.set(
|
||||
{
|
||||
valid_config: true,
|
||||
cdrm_instance: trimmedUrl,
|
||||
widevine_device: {
|
||||
device_type: widevineData.device_type,
|
||||
system_id: widevineData.system_id,
|
||||
security_level: widevineData.security_level,
|
||||
secret: widevineData.secret,
|
||||
device_name: widevineData.device_name,
|
||||
host: trimmedUrl,
|
||||
},
|
||||
playready_device: {
|
||||
security_level: playreadyData.security_level,
|
||||
secret: playreadyData.secret,
|
||||
device_name: playreadyData.device_name,
|
||||
host: trimmedUrl,
|
||||
},
|
||||
},
|
||||
() => {
|
||||
if (chrome.runtime.lastError) {
|
||||
console.error(
|
||||
"Error saving to chrome.storage:",
|
||||
chrome.runtime.lastError
|
||||
);
|
||||
setMessage("Error saving configuration.");
|
||||
setMessageType("error");
|
||||
} else {
|
||||
console.log("Configuration saved.");
|
||||
setStoredUrl(trimmedUrl);
|
||||
setInstanceUrl("");
|
||||
if (onConfigSaved) onConfigSaved();
|
||||
navigate("/results"); // Automatically redirect after success
|
||||
}
|
||||
}
|
||||
);
|
||||
} else {
|
||||
throw new Error("Invalid response from endpoint.");
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Connection error:", err);
|
||||
setMessage("Invalid endpoint or device info could not be retrieved.");
|
||||
setMessageType("error");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full h-full overflow-y-auto overflow-x-auto flex flex-col p-4">
|
||||
{storedUrl && (
|
||||
<p className="text-gray-300 mb-2">
|
||||
Current instance: <span className="text-white font-semibold">{storedUrl}</span>
|
||||
</p>
|
||||
)}
|
||||
|
||||
<p className="mt-3 text-white">New instance URL:</p>
|
||||
<input
|
||||
type="text"
|
||||
value={instanceUrl}
|
||||
onChange={(e) => setInstanceUrl(e.target.value)}
|
||||
placeholder="https://cdrm-project.com/, http://127.0.0.1:5000/"
|
||||
className="w-full p-4 text-lg bg-gray-800 text-white border border-gray-700 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 mt-4 font-mono"
|
||||
/>
|
||||
<button
|
||||
onClick={handleSave}
|
||||
disabled={loading}
|
||||
className={`mt-4 p-2 font-bold ${
|
||||
loading ? "bg-blue-400" : "bg-blue-600 hover:bg-blue-700"
|
||||
} text-white rounded-md transition duration-300`}
|
||||
>
|
||||
{loading ? "Connecting..." : "Save settings"}
|
||||
</button>
|
||||
|
||||
{message && (
|
||||
<p
|
||||
className={`mt-2 text-sm text-center ${
|
||||
messageType === "success" ? "text-green-400" : "text-red-400"
|
||||
}`}
|
||||
>
|
||||
{message}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Settings;
|
||||
|
@ -4,48 +4,45 @@ import settingsIcon from "../assets/settings.svg";
|
||||
import closeIcon from "../assets/close.svg";
|
||||
|
||||
function SideNav({ onClose }) {
|
||||
return (
|
||||
<div className="w-full h-full overflow-y-auto overflow-x-auto flex flex-col bg-black">
|
||||
<div className="w-full min-h-16 max-h-16 h-16 shrink-0 flex sticky top-0 z-20 border-b border-b-white bg-black">
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="h-full ml-auto p-3 hover:cursor-pointer"
|
||||
>
|
||||
<img src={closeIcon} alt="Close" className="h-full" />
|
||||
</button>
|
||||
</div>
|
||||
return (
|
||||
<div className="w-full h-full overflow-y-auto overflow-x-auto flex flex-col bg-black">
|
||||
<div className="w-full min-h-16 max-h-16 h-16 shrink-0 flex sticky top-0 z-20 border-b border-b-white bg-black">
|
||||
<button onClick={onClose} className="h-full ml-auto p-3 hover:cursor-pointer">
|
||||
<img src={closeIcon} alt="Close" className="h-full" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="w-full h-16 flex items-center justify-center mt-2">
|
||||
<NavLink
|
||||
to="/results"
|
||||
onClick={onClose}
|
||||
className="text-white text-2xl font-bold flex flex-row items-center border-l-white hover:border-l-1 w-full hover:bg-black/50 transition duration-300 ease-in-out p-2"
|
||||
>
|
||||
<img
|
||||
src={homeIcon}
|
||||
alt="Home"
|
||||
className="h-full w-16 p-2 flex items-center cursor-pointer"
|
||||
/>
|
||||
Home
|
||||
</NavLink>
|
||||
</div>
|
||||
<div className="w-full h-16 flex items-center justify-center mt-2">
|
||||
<NavLink
|
||||
to="/results"
|
||||
onClick={onClose}
|
||||
className="text-white text-2xl font-bold flex flex-row items-center border-l-white hover:border-l-1 w-full hover:bg-black/50 transition duration-300 ease-in-out p-2"
|
||||
>
|
||||
<img
|
||||
src={homeIcon}
|
||||
alt="Home"
|
||||
className="h-full w-16 p-2 flex items-center cursor-pointer"
|
||||
/>
|
||||
Home
|
||||
</NavLink>
|
||||
</div>
|
||||
|
||||
<div className="w-full h-16 flex items-center justify-center mt-2">
|
||||
<NavLink
|
||||
to="/settings"
|
||||
onClick={onClose}
|
||||
className="text-white text-2xl font-bold flex flex-row items-center hover:border-l-1 border-l-white w-full hover:bg-black/50 transition duration-300 ease-in-out p-2"
|
||||
>
|
||||
<img
|
||||
src={settingsIcon}
|
||||
alt="Settings"
|
||||
className="h-full w-16 p-2 flex items-center cursor-pointer"
|
||||
/>
|
||||
Settings
|
||||
</NavLink>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
<div className="w-full h-16 flex items-center justify-center mt-2">
|
||||
<NavLink
|
||||
to="/settings"
|
||||
onClick={onClose}
|
||||
className="text-white text-2xl font-bold flex flex-row items-center hover:border-l-1 border-l-white w-full hover:bg-black/50 transition duration-300 ease-in-out p-2"
|
||||
>
|
||||
<img
|
||||
src={settingsIcon}
|
||||
alt="Settings"
|
||||
className="h-full w-16 p-2 flex items-center cursor-pointer"
|
||||
/>
|
||||
Settings
|
||||
</NavLink>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default SideNav;
|
||||
|
@ -2,84 +2,81 @@ import { useEffect, useState } from "react";
|
||||
import hamburgerIcon from "../assets/hamburger.svg";
|
||||
|
||||
function TopNav({ onMenuClick }) {
|
||||
const [injectionType, setInjectionType] = useState("LICENSE");
|
||||
const [drmOverride, setDrmOverride] = useState("DISABLED");
|
||||
const [injectionType, setInjectionType] = useState("LICENSE");
|
||||
const [drmOverride, setDrmOverride] = useState("DISABLED");
|
||||
|
||||
useEffect(() => {
|
||||
chrome.storage.local.get(["injection_type", "drm_override"], (result) => {
|
||||
if (result.injection_type !== undefined) {
|
||||
setInjectionType(result.injection_type);
|
||||
}
|
||||
if (result.drm_override !== undefined) {
|
||||
setDrmOverride(result.drm_override);
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
chrome.storage.local.get(["injection_type", "drm_override"], (result) => {
|
||||
if (result.injection_type !== undefined) {
|
||||
setInjectionType(result.injection_type);
|
||||
}
|
||||
if (result.drm_override !== undefined) {
|
||||
setDrmOverride(result.drm_override);
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleInjectionTypeChange = (type) => {
|
||||
chrome.storage.local.set({ injection_type: type }, () => {
|
||||
if (chrome.runtime.lastError) {
|
||||
console.error(
|
||||
"Error updating injection_type:",
|
||||
chrome.runtime.lastError
|
||||
);
|
||||
} else {
|
||||
setInjectionType(type);
|
||||
console.log(`Injection type updated to ${type}`);
|
||||
}
|
||||
});
|
||||
};
|
||||
const handleInjectionTypeChange = (type) => {
|
||||
chrome.storage.local.set({ injection_type: type }, () => {
|
||||
if (chrome.runtime.lastError) {
|
||||
console.error("Error updating injection_type:", chrome.runtime.lastError);
|
||||
} else {
|
||||
setInjectionType(type);
|
||||
console.log(`Injection type updated to ${type}`);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleDrmOverrideChange = (type) => {
|
||||
chrome.storage.local.set({ drm_override: type }, () => {
|
||||
if (chrome.runtime.lastError) {
|
||||
console.error("Error updating drm_override:", chrome.runtime.lastError);
|
||||
} else {
|
||||
setDrmOverride(type);
|
||||
console.log(`DRM Override updated to ${type}`);
|
||||
}
|
||||
});
|
||||
};
|
||||
const handleDrmOverrideChange = (type) => {
|
||||
chrome.storage.local.set({ drm_override: type }, () => {
|
||||
if (chrome.runtime.lastError) {
|
||||
console.error("Error updating drm_override:", chrome.runtime.lastError);
|
||||
} else {
|
||||
setDrmOverride(type);
|
||||
console.log(`DRM Override updated to ${type}`);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full h-full flex flex-row overflow-x-hidden">
|
||||
<img
|
||||
src={hamburgerIcon}
|
||||
alt="Menu"
|
||||
className="h-full w-16 p-2 flex items-center cursor-pointer"
|
||||
onClick={onMenuClick}
|
||||
/>
|
||||
<div className="flex flex-row h-full justify-center items-center ml-auto mr-2">
|
||||
<p className="text-white text-lg p-2 mr-2 border-r-2 border-r-white text-nowrap">
|
||||
Injection Type:
|
||||
</p>
|
||||
<button
|
||||
onClick={() => handleInjectionTypeChange("LICENSE")}
|
||||
className={`text-white text-lg p-2 rounded-md m-1 cursor-pointer ${
|
||||
injectionType === "LICENSE" ? "bg-sky-500/70" : "bg-black"
|
||||
}`}
|
||||
>
|
||||
License
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleInjectionTypeChange("EME")}
|
||||
className={`text-white text-lg p-2 rounded-md m-1 cursor-pointer ${
|
||||
injectionType === "EME" ? "bg-green-500/70" : "bg-black"
|
||||
}`}
|
||||
>
|
||||
EME
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleInjectionTypeChange("DISABLED")}
|
||||
className={`text-white text-lg p-2 rounded-md m-1 cursor-pointer ${
|
||||
injectionType === "DISABLED" ? "bg-red-500/70" : "bg-black"
|
||||
}`}
|
||||
>
|
||||
Disabled
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div className="w-full h-full flex flex-row overflow-x-hidden">
|
||||
<img
|
||||
src={hamburgerIcon}
|
||||
alt="Menu"
|
||||
className="h-full w-16 p-2 flex items-center cursor-pointer"
|
||||
onClick={onMenuClick}
|
||||
/>
|
||||
<div className="flex flex-row h-full justify-center items-center ml-auto mr-2">
|
||||
<p className="text-white text-lg p-2 mr-2 border-r-2 border-r-white text-nowrap">
|
||||
Injection Type:
|
||||
</p>
|
||||
<button
|
||||
onClick={() => handleInjectionTypeChange("LICENSE")}
|
||||
className={`text-white text-lg p-2 rounded-md m-1 cursor-pointer ${
|
||||
injectionType === "LICENSE" ? "bg-sky-500/70" : "bg-black"
|
||||
}`}
|
||||
>
|
||||
License
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleInjectionTypeChange("EME")}
|
||||
className={`text-white text-lg p-2 rounded-md m-1 cursor-pointer ${
|
||||
injectionType === "EME" ? "bg-green-500/70" : "bg-black"
|
||||
}`}
|
||||
>
|
||||
EME
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleInjectionTypeChange("DISABLED")}
|
||||
className={`text-white text-lg p-2 rounded-md m-1 cursor-pointer ${
|
||||
injectionType === "DISABLED" ? "bg-red-500/70" : "bg-black"
|
||||
}`}
|
||||
>
|
||||
Disabled
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default TopNav;
|
||||
|
@ -1,8 +1,10 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
html, body, #root {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
html,
|
||||
body,
|
||||
#root {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
@ -1,10 +1,10 @@
|
||||
import { StrictMode } from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import './index.css'
|
||||
import App from './App.jsx'
|
||||
import { StrictMode } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import "./index.css";
|
||||
import App from "./App.jsx";
|
||||
|
||||
createRoot(document.getElementById('root')).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>,
|
||||
)
|
||||
createRoot(document.getElementById("root")).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>
|
||||
);
|
||||
|
@ -1,9 +1,21 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
import tailwindcss from '@tailwindcss/vite'
|
||||
import tailwindcss from "@tailwindcss/vite";
|
||||
import react from "@vitejs/plugin-react-swc";
|
||||
import { readFileSync } from "fs";
|
||||
import { defineConfig } from "vite";
|
||||
|
||||
const packageJson = JSON.parse(readFileSync("./package.json", "utf8"));
|
||||
|
||||
const replaceVersionPlugin = () => {
|
||||
return {
|
||||
name: "replace-version",
|
||||
transformIndexHtml(html) {
|
||||
return html.replace("%APPVERSION%", packageJson.version);
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
base: './',
|
||||
plugins: [react(), tailwindcss()],
|
||||
})
|
||||
base: "./",
|
||||
plugins: [react(), tailwindcss(), replaceVersionPlugin()],
|
||||
});
|
||||
|
813
inject.js
813
inject.js
@ -1,813 +0,0 @@
|
||||
let widevineDeviceInfo = null;
|
||||
let playreadyDeviceInfo = null;
|
||||
let originalChallenge = null
|
||||
let serviceCertFound = false;
|
||||
let drmType = "NONE";
|
||||
let psshFound = false;
|
||||
let foundWidevinePssh = null;
|
||||
let foundPlayreadyPssh = null;
|
||||
let drmDecided = null;
|
||||
let drmOverride = "DISABLED";
|
||||
let interceptType = "DISABLED";
|
||||
let remoteCDM = null;
|
||||
let generateRequestCalled = false;
|
||||
let remoteListenerMounted = false;
|
||||
let injectionSuccess = false;
|
||||
let foundChallengeInBody = false;
|
||||
let licenseResponseCounter = 0;
|
||||
let keysRetrieved = false;
|
||||
|
||||
// Post message to content.js to get DRM override
|
||||
window.postMessage({ type: "__GET_DRM_OVERRIDE__" }, "*");
|
||||
|
||||
// Add listener for DRM override messages
|
||||
window.addEventListener("message", function(event) {
|
||||
if (event.source !== window) return;
|
||||
if (event.data.type === "__DRM_OVERRIDE__") {
|
||||
drmOverride = event.data.drmOverride || "DISABLED";
|
||||
console.log("DRM Override set to:", drmOverride);
|
||||
}
|
||||
});
|
||||
|
||||
// Post message to content.js to get injection type
|
||||
window.postMessage({ type: "__GET_INJECTION_TYPE__" }, "*");
|
||||
|
||||
// Add listener for injection type messages
|
||||
window.addEventListener("message", function(event) {
|
||||
if (event.source !== window) return;
|
||||
|
||||
if (event.data.type === "__INJECTION_TYPE__") {
|
||||
interceptType = event.data.injectionType || "DISABLED";
|
||||
console.log("Injection type set to:", interceptType);
|
||||
}
|
||||
});
|
||||
|
||||
// Post message to get CDM devices
|
||||
window.postMessage({ type: "__GET_CDM_DEVICES__" }, "*");
|
||||
|
||||
// Add listener for CDM device messages
|
||||
window.addEventListener("message", function(event) {
|
||||
if (event.source !== window) return;
|
||||
|
||||
if (event.data.type === "__CDM_DEVICES__") {
|
||||
const { widevine_device, playready_device } = event.data;
|
||||
|
||||
console.log("Received device info:", widevine_device, playready_device);
|
||||
|
||||
widevineDeviceInfo = widevine_device;
|
||||
playreadyDeviceInfo = playready_device;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// PlayReady Remote CDM Class
|
||||
class remotePlayReadyCDM {
|
||||
constructor(security_level, host, secret, device_name) {
|
||||
this.security_level = security_level;
|
||||
this.host = host;
|
||||
this.secret = secret;
|
||||
this.device_name = device_name;
|
||||
this.session_id = null;
|
||||
this.challenge = null;
|
||||
this.keys = null;
|
||||
}
|
||||
|
||||
// Open PlayReady session
|
||||
openSession() {
|
||||
const url = `${this.host}/remotecdm/playready/${this.device_name}/open`;
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open('GET', url, false);
|
||||
xhr.setRequestHeader('Content-Type', 'application/json');
|
||||
xhr.send();
|
||||
const jsonData = JSON.parse(xhr.responseText);
|
||||
if (jsonData.data?.session_id) {
|
||||
this.session_id = jsonData.data.session_id;
|
||||
console.log("PlayReady session opened:", this.session_id);
|
||||
} else {
|
||||
console.error("Failed to open PlayReady session:", jsonData.message);
|
||||
throw new Error("Failed to open PlayReady session");
|
||||
}
|
||||
}
|
||||
|
||||
// Get PlayReady challenge
|
||||
getChallenge(init_data) {
|
||||
const url = `${this.host}/remotecdm/playready/${this.device_name}/get_license_challenge`;
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open('POST', url, false);
|
||||
xhr.setRequestHeader('Content-Type', 'application/json');
|
||||
const body = {
|
||||
session_id: this.session_id,
|
||||
init_data: init_data
|
||||
};
|
||||
xhr.send(JSON.stringify(body));
|
||||
const jsonData = JSON.parse(xhr.responseText);
|
||||
if (jsonData.data?.challenge) {
|
||||
this.challenge = btoa(jsonData.data.challenge);
|
||||
console.log("PlayReady challenge received:", this.challenge);
|
||||
} else {
|
||||
console.error("Failed to get PlayReady challenge:", jsonData.message);
|
||||
throw new Error("Failed to get PlayReady challenge");
|
||||
}
|
||||
}
|
||||
|
||||
// Parse PlayReady license response
|
||||
parseLicense(license_message) {
|
||||
const url = `${this.host}/remotecdm/playready/${this.device_name}/parse_license`;
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open('POST', url, false);
|
||||
xhr.setRequestHeader('Content-Type', 'application/json');
|
||||
const body = {
|
||||
session_id: this.session_id,
|
||||
license_message: license_message
|
||||
}
|
||||
xhr.send(JSON.stringify(body));
|
||||
const jsonData = JSON.parse(xhr.responseText);
|
||||
if (jsonData.message === "Successfully parsed and loaded the Keys from the License message")
|
||||
{
|
||||
console.log("PlayReady license response parsed successfully");
|
||||
return true;
|
||||
} else {
|
||||
console.error("Failed to parse PlayReady license response:", jsonData.message);
|
||||
throw new Error("Failed to parse PlayReady license response");
|
||||
}
|
||||
}
|
||||
|
||||
// Get PlayReady keys
|
||||
getKeys() {
|
||||
const url = `${this.host}/remotecdm/playready/${this.device_name}/get_keys`;
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open('POST', url, false);
|
||||
xhr.setRequestHeader('Content-Type', 'application/json');
|
||||
const body = {
|
||||
session_id: this.session_id
|
||||
}
|
||||
xhr.send(JSON.stringify(body));
|
||||
const jsonData = JSON.parse(xhr.responseText);
|
||||
if (jsonData.data?.keys) {
|
||||
this.keys = jsonData.data.keys;
|
||||
console.log("PlayReady keys received:", this.keys);
|
||||
} else {
|
||||
console.error("Failed to get PlayReady keys:", jsonData.message);
|
||||
throw new Error("Failed to get PlayReady keys");
|
||||
}
|
||||
}
|
||||
|
||||
// Close PlayReady session
|
||||
closeSession () {
|
||||
const url = `${this.host}/remotecdm/playready/${this.device_name}/close/${this.session_id}`;
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open('GET', url, false);
|
||||
xhr.setRequestHeader('Content-Type', 'application/json');
|
||||
xhr.send();
|
||||
const jsonData = JSON.parse(xhr.responseText);
|
||||
if (jsonData) {
|
||||
console.log("PlayReady session closed successfully");
|
||||
} else {
|
||||
console.error("Failed to close PlayReady session:", jsonData.message);
|
||||
throw new Error("Failed to close PlayReady session");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Widevine Remote CDM Class
|
||||
class remoteWidevineCDM {
|
||||
constructor(device_type, system_id, security_level, host, secret, device_name) {
|
||||
this.device_type = device_type;
|
||||
this.system_id = system_id;
|
||||
this.security_level = security_level;
|
||||
this.host = host;
|
||||
this.secret = secret;
|
||||
this.device_name = device_name;
|
||||
this.session_id = null;
|
||||
this.challenge = null;
|
||||
this.keys = null;
|
||||
}
|
||||
|
||||
// Open Widevine session
|
||||
openSession () {
|
||||
const url = `${this.host}/remotecdm/widevine/${this.device_name}/open`;
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open('GET', url, false);
|
||||
xhr.setRequestHeader('Content-Type', 'application/json');
|
||||
xhr.send();
|
||||
const jsonData = JSON.parse(xhr.responseText);
|
||||
if (jsonData.data?.session_id) {
|
||||
this.session_id = jsonData.data.session_id;
|
||||
console.log("Widevine session opened:", this.session_id);
|
||||
} else {
|
||||
console.error("Failed to open Widevine session:", jsonData.message);
|
||||
throw new Error("Failed to open Widevine session");
|
||||
}
|
||||
}
|
||||
|
||||
// Set Widevine service certificate
|
||||
setServiceCertificate(certificate) {
|
||||
const url = `${this.host}/remotecdm/widevine/${this.device_name}/set_service_certificate`;
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open('POST', url, false);
|
||||
xhr.setRequestHeader('Content-Type', 'application/json');
|
||||
const body = {
|
||||
session_id: this.session_id,
|
||||
certificate: certificate ?? null
|
||||
}
|
||||
xhr.send(JSON.stringify(body));
|
||||
const jsonData = JSON.parse(xhr.responseText);
|
||||
if (jsonData.status === 200) {
|
||||
console.log("Service certificate set successfully");
|
||||
} else {
|
||||
console.error("Failed to set service certificate:", jsonData.message);
|
||||
throw new Error("Failed to set service certificate");
|
||||
}
|
||||
}
|
||||
|
||||
// Get Widevine challenge
|
||||
getChallenge(init_data, license_type = 'STREAMING') {
|
||||
const url = `${this.host}/remotecdm/widevine/${this.device_name}/get_license_challenge/${license_type}`;
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open('POST', url, false);
|
||||
xhr.setRequestHeader('Content-Type', 'application/json');
|
||||
const body = {
|
||||
session_id: this.session_id,
|
||||
init_data: init_data,
|
||||
privacy_mode: serviceCertFound
|
||||
};
|
||||
xhr.send(JSON.stringify(body));
|
||||
const jsonData = JSON.parse(xhr.responseText);
|
||||
if (jsonData.data?.challenge_b64) {
|
||||
this.challenge = jsonData.data.challenge_b64;
|
||||
console.log("Widevine challenge received:", this.challenge);
|
||||
} else {
|
||||
console.error("Failed to get Widevine challenge:", jsonData.message);
|
||||
throw new Error("Failed to get Widevine challenge");
|
||||
}
|
||||
}
|
||||
|
||||
// Parse Widevine license response
|
||||
parseLicense(license_message) {
|
||||
const url = `${this.host}/remotecdm/widevine/${this.device_name}/parse_license`;
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open('POST', url, false);
|
||||
xhr.setRequestHeader('Content-Type', 'application/json');
|
||||
const body = {
|
||||
session_id: this.session_id,
|
||||
license_message: license_message
|
||||
};
|
||||
xhr.send(JSON.stringify(body));
|
||||
const jsonData = JSON.parse(xhr.responseText);
|
||||
if (jsonData.status === 200) {
|
||||
console.log("Widevine license response parsed successfully");
|
||||
return true;
|
||||
} else {
|
||||
console.error("Failed to parse Widevine license response:", jsonData.message);
|
||||
throw new Error("Failed to parse Widevine license response");
|
||||
}
|
||||
}
|
||||
|
||||
// Get Widevine keys
|
||||
getKeys() {
|
||||
const url = `${this.host}/remotecdm/widevine/${this.device_name}/get_keys/ALL`;
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open('POST', url, false);
|
||||
xhr.setRequestHeader('Content-Type', 'application/json');
|
||||
const body = {
|
||||
session_id: this.session_id
|
||||
};
|
||||
xhr.send(JSON.stringify(body));
|
||||
const jsonData = JSON.parse(xhr.responseText);
|
||||
if (jsonData.data?.keys) {
|
||||
this.keys = jsonData.data.keys;
|
||||
console.log("Widevine keys received:", this.keys);
|
||||
} else {
|
||||
console.error("Failed to get Widevine keys:", jsonData.message);
|
||||
throw new Error("Failed to get Widevine keys");
|
||||
}
|
||||
}
|
||||
|
||||
// Close Widevine session
|
||||
closeSession() {
|
||||
const url = `${this.host}/remotecdm/widevine/${this.device_name}/close/${this.session_id}`;
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open('GET', url, false);
|
||||
xhr.setRequestHeader('Content-Type', 'application/json');
|
||||
xhr.send();
|
||||
const jsonData = JSON.parse(xhr.responseText);
|
||||
if (jsonData) {
|
||||
console.log("Widevine session closed successfully");
|
||||
} else {
|
||||
console.error("Failed to close Widevine session:", jsonData.message);
|
||||
throw new Error("Failed to close Widevine session");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Utility functions
|
||||
function hexStrToU8(hexString) {
|
||||
return Uint8Array.from(hexString.match(/.{1,2}/g).map(byte => parseInt(byte, 16)));
|
||||
}
|
||||
|
||||
function u8ToHexStr(bytes) {
|
||||
return bytes.reduce((str, byte) => str + byte.toString(16).padStart(2, '0'), '');
|
||||
}
|
||||
|
||||
function b64ToHexStr(b64) {
|
||||
return [...atob(b64)].map(c => c.charCodeAt(0).toString(16).padStart(2, '0')).join``;
|
||||
}
|
||||
|
||||
function jsonContainsValue(obj, prefix = "CAES") {
|
||||
if (typeof obj === "string") return obj.startsWith(prefix);
|
||||
if (Array.isArray(obj)) return obj.some(val => jsonContainsValue(val, prefix));
|
||||
if (typeof obj === "object" && obj !== null) {
|
||||
return Object.values(obj).some(val => jsonContainsValue(val, prefix));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function jsonReplaceValue(obj, newValue) {
|
||||
if (typeof obj === "string") {
|
||||
return obj.startsWith("CAES") || obj.startsWith("PD94") ? newValue : obj;
|
||||
}
|
||||
|
||||
if (Array.isArray(obj)) {
|
||||
return obj.map(item => jsonReplaceValue(item, newValue));
|
||||
}
|
||||
|
||||
if (typeof obj === "object" && obj !== null) {
|
||||
const newObj = {};
|
||||
for (const key in obj) {
|
||||
if (Object.hasOwn(obj, key)) {
|
||||
newObj[key] = jsonReplaceValue(obj[key], newValue);
|
||||
}
|
||||
}
|
||||
return newObj;
|
||||
}
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
function isJson(str) {
|
||||
try {
|
||||
JSON.parse(str);
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function getWidevinePssh(buffer) {
|
||||
const hex = u8ToHexStr(new Uint8Array(buffer));
|
||||
const match = hex.match(/000000(..)?70737368.*/);
|
||||
if (!match) return null;
|
||||
|
||||
const boxHex = match[0];
|
||||
const bytes = hexStrToU8(boxHex);
|
||||
return window.btoa(String.fromCharCode(...bytes));
|
||||
}
|
||||
|
||||
function getPlayReadyPssh(buffer) {
|
||||
const u8 = new Uint8Array(buffer);
|
||||
const systemId = "9a04f07998404286ab92e65be0885f95";
|
||||
const hex = u8ToHexStr(u8);
|
||||
const index = hex.indexOf(systemId);
|
||||
if (index === -1) return null;
|
||||
const psshBoxStart = hex.lastIndexOf("70737368", index);
|
||||
if (psshBoxStart === -1) return null;
|
||||
const lenStart = psshBoxStart - 8;
|
||||
const boxLen = parseInt(hex.substr(lenStart, 8), 16) * 2;
|
||||
const psshHex = hex.substr(lenStart, boxLen);
|
||||
const psshBytes = hexStrToU8(psshHex);
|
||||
return window.btoa(String.fromCharCode(...psshBytes));
|
||||
}
|
||||
|
||||
function getClearkey(response) {
|
||||
let obj = JSON.parse((new TextDecoder("utf-8")).decode(response));
|
||||
return obj["keys"].map(o => ({
|
||||
key_id: b64ToHexStr(o["kid"].replace(/-/g, '+').replace(/_/g, '/')),
|
||||
key: b64ToHexStr(o["k"].replace(/-/g, '+').replace(/_/g, '/')),
|
||||
}));
|
||||
}
|
||||
|
||||
function base64ToUint8Array(base64) {
|
||||
const binaryStr = atob(base64);
|
||||
const len = binaryStr.length;
|
||||
const bytes = new Uint8Array(len);
|
||||
for (let i = 0; i < len; i++) {
|
||||
bytes[i] = binaryStr.charCodeAt(i);
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
function arrayBufferToBase64(uint8array) {
|
||||
let binary = '';
|
||||
const len = uint8array.length;
|
||||
|
||||
for (let i = 0; i < len; i++) {
|
||||
binary += String.fromCharCode(uint8array[i]);
|
||||
}
|
||||
|
||||
return window.btoa(binary);
|
||||
}
|
||||
|
||||
// Challenge generator interceptor
|
||||
const originalGenerateRequest = MediaKeySession.prototype.generateRequest;
|
||||
MediaKeySession.prototype.generateRequest = function(initDataType, initData) {
|
||||
const session = this;
|
||||
let playReadyPssh = getPlayReadyPssh(initData);
|
||||
if (playReadyPssh) {
|
||||
console.log("[DRM Detected] PlayReady");
|
||||
foundPlayreadyPssh = playReadyPssh;
|
||||
console.log("[PlayReady PSSH found] " + playReadyPssh)
|
||||
}
|
||||
let wideVinePssh = getWidevinePssh(initData)
|
||||
if (wideVinePssh) {
|
||||
// Widevine code
|
||||
console.log("[DRM Detected] Widevine");
|
||||
foundWidevinePssh = wideVinePssh;
|
||||
console.log("[Widevine PSSH found] " + wideVinePssh)
|
||||
}
|
||||
// Challenge message interceptor
|
||||
if (!remoteListenerMounted) {
|
||||
remoteListenerMounted = true;
|
||||
session.addEventListener("message", function messageInterceptor(event) {
|
||||
event.stopImmediatePropagation();
|
||||
const uint8Array = new Uint8Array(event.message);
|
||||
const base64challenge = arrayBufferToBase64(uint8Array);
|
||||
if (base64challenge === "CAQ=" && interceptType !== "DISABLED" && !serviceCertFound) {
|
||||
const {
|
||||
device_type, system_id, security_level, host, secret, device_name
|
||||
} = widevineDeviceInfo;
|
||||
remoteCDM = new remoteWidevineCDM(device_type, system_id, security_level, host, secret, device_name);
|
||||
remoteCDM.openSession();
|
||||
}
|
||||
if (!injectionSuccess && base64challenge !== "CAQ=" && interceptType !== "DISABLED") {
|
||||
if (interceptType === "EME") {
|
||||
injectionSuccess = true;
|
||||
}
|
||||
if (!originalChallenge) {
|
||||
originalChallenge = base64challenge;
|
||||
}
|
||||
if (originalChallenge.startsWith("CAES")) {
|
||||
window.postMessage({ type: "__DRM_TYPE__", data: "Widevine" }, "*");
|
||||
window.postMessage({ type: "__PSSH_DATA__", data: foundWidevinePssh }, "*");
|
||||
if (interceptType === "EME" && !remoteCDM) {
|
||||
const {
|
||||
device_type, system_id, security_level, host, secret, device_name
|
||||
} = widevineDeviceInfo;
|
||||
remoteCDM = new remoteWidevineCDM(device_type, system_id, security_level, host, secret, device_name);
|
||||
remoteCDM.openSession();
|
||||
remoteCDM.getChallenge(foundWidevinePssh);
|
||||
}}
|
||||
if (!originalChallenge.startsWith("CAES")) {
|
||||
const buffer = event.message;
|
||||
const decoder = new TextDecoder('utf-16');
|
||||
const decodedText = decoder.decode(buffer);
|
||||
const match = decodedText.match(/<Challenge encoding="base64encoded">([^<]+)<\/Challenge>/);
|
||||
if (match) {
|
||||
window.postMessage({ type: "__DRM_TYPE__", data: "PlayReady" }, "*");
|
||||
window.postMessage({ type: "__PSSH_DATA__", data: foundPlayreadyPssh }, "*");
|
||||
originalChallenge = match[1];
|
||||
if (interceptType === "EME" && !remoteCDM) {
|
||||
const {
|
||||
security_level, host, secret, device_name
|
||||
} = playreadyDeviceInfo;
|
||||
remoteCDM = new remotePlayReadyCDM(security_level, host, secret, device_name)
|
||||
remoteCDM.openSession();
|
||||
remoteCDM.getChallenge(foundPlayreadyPssh);
|
||||
}
|
||||
}}
|
||||
if (interceptType === "EME" && remoteCDM) {
|
||||
const uint8challenge = base64ToUint8Array(remoteCDM.challenge);
|
||||
const challengeBuffer = uint8challenge.buffer;
|
||||
const syntheticEvent = new MessageEvent("message", {
|
||||
data: event.data,
|
||||
origin: event.origin,
|
||||
lastEventId: event.lastEventId,
|
||||
source: event.source,
|
||||
ports: event.ports
|
||||
});
|
||||
Object.defineProperty(syntheticEvent, "message", {
|
||||
get: () => challengeBuffer
|
||||
});
|
||||
console.log("Intercepted EME Challenge and injected custom one.")
|
||||
session.dispatchEvent(syntheticEvent);
|
||||
}
|
||||
}
|
||||
})
|
||||
console.log("Message interceptor mounted.");
|
||||
}
|
||||
return originalGenerateRequest.call(session, initDataType, initData);
|
||||
};
|
||||
|
||||
// Message update interceptors
|
||||
const originalUpdate = MediaKeySession.prototype.update;
|
||||
MediaKeySession.prototype.update = function(response) {
|
||||
const uint8 = response instanceof Uint8Array ? response : new Uint8Array(response);
|
||||
const base64Response = window.btoa(String.fromCharCode(...uint8));
|
||||
if (base64Response.startsWith("CAUS") && foundWidevinePssh && remoteCDM && !serviceCertFound) {
|
||||
remoteCDM.setServiceCertificate(base64Response);
|
||||
if (interceptType === "EME" && !remoteCDM.challenge) {
|
||||
remoteCDM.getChallenge(foundWidevinePssh);
|
||||
}
|
||||
window.postMessage({ type: "__DRM_TYPE__", data: "Widevine" }, "*");
|
||||
window.postMessage({ type: "__PSSH_DATA__", data: foundWidevinePssh }, "*");
|
||||
serviceCertFound = true;
|
||||
}
|
||||
if (!base64Response.startsWith("CAUS") && (foundWidevinePssh || foundPlayreadyPssh) && !keysRetrieved) {
|
||||
if (licenseResponseCounter === 1 || foundChallengeInBody) {
|
||||
remoteCDM.parseLicense(base64Response);
|
||||
remoteCDM.getKeys();
|
||||
remoteCDM.closeSession();
|
||||
keysRetrieved = true;
|
||||
window.postMessage({ type: "__KEYS_DATA__", data: remoteCDM.keys }, "*");
|
||||
}
|
||||
licenseResponseCounter++;
|
||||
}
|
||||
const updatePromise = originalUpdate.call(this, response);
|
||||
if (!foundPlayreadyPssh && !foundWidevinePssh) {
|
||||
updatePromise
|
||||
.then(() => {
|
||||
let clearKeys = getClearkey(response);
|
||||
if (clearKeys && clearKeys.length > 0) {
|
||||
console.log("[CLEARKEY] ", clearKeys);
|
||||
const drmType = {
|
||||
type: "__DRM_TYPE__",
|
||||
data: 'ClearKey'
|
||||
};
|
||||
window.postMessage(drmType, "*");
|
||||
const keysData = {
|
||||
type: "__KEYS_DATA__",
|
||||
data: clearKeys
|
||||
};
|
||||
window.postMessage(keysData, "*");
|
||||
}
|
||||
})
|
||||
.catch(e => {
|
||||
console.log("[CLEARKEY] Not found");
|
||||
});
|
||||
}
|
||||
|
||||
return updatePromise;
|
||||
};
|
||||
|
||||
// fetch POST interceptor
|
||||
(function() {
|
||||
const originalFetch = window.fetch;
|
||||
|
||||
window.fetch = async function(resource, config = {}) {
|
||||
const method = (config.method || 'GET').toUpperCase();
|
||||
|
||||
if (method === 'POST') {
|
||||
let body = config.body;
|
||||
if (body) {
|
||||
if (body instanceof ArrayBuffer || body instanceof Uint8Array) {
|
||||
const buffer = body instanceof Uint8Array ? body : new Uint8Array(body);
|
||||
const base64Body = window.btoa(String.fromCharCode(...buffer));
|
||||
if ((base64Body.startsWith("CAES") || base64Body.startsWith("PD94")) && (!remoteCDM || remoteCDM.challenge === null || base64Body !== remoteCDM.challenge) && interceptType === "EME") {
|
||||
foundChallengeInBody = true;
|
||||
window.postMessage({ type: "__LICENSE_URL__", data: resource }, "*");
|
||||
// Block the request
|
||||
return;
|
||||
}
|
||||
if ((base64Body.startsWith("CAES") || base64Body.startsWith("PD94")) && interceptType == "LICENSE" &&!foundChallengeInBody) {
|
||||
foundChallengeInBody = true;
|
||||
window.postMessage({ type: "__LICENSE_URL__", data: resource }, "*");
|
||||
if (!remoteCDM) {
|
||||
if (base64Body.startsWith("CAES")) {
|
||||
const {
|
||||
device_type, system_id, security_level, host, secret, device_name
|
||||
} = widevineDeviceInfo;
|
||||
remoteCDM = new remoteWidevineCDM(device_type, system_id, security_level, host, secret, device_name);
|
||||
remoteCDM.openSession();
|
||||
remoteCDM.getChallenge(foundWidevinePssh);
|
||||
}
|
||||
if (base64Body.startsWith("PD94")) {
|
||||
const {
|
||||
security_level, host, secret, device_name
|
||||
} = playreadyDeviceInfo;
|
||||
remoteCDM = new remotePlayReadyCDM(security_level, host, secret, device_name);
|
||||
remoteCDM.openSession();
|
||||
remoteCDM.getChallenge(foundPlayreadyPssh);
|
||||
}
|
||||
}
|
||||
if (remoteCDM && remoteCDM.challenge === null) {
|
||||
remoteCDM.getChallenge(foundWidevinePssh);
|
||||
}
|
||||
const injectedBody = base64ToUint8Array(remoteCDM.challenge);
|
||||
config.body = injectedBody;
|
||||
return originalFetch(resource, config);
|
||||
}
|
||||
}
|
||||
if (typeof body === 'string' && !isJson(body)) {
|
||||
const base64EncodedBody = btoa(body);
|
||||
if ((base64EncodedBody.startsWith("CAES") || base64EncodedBody.startsWith("PD94")) && (!remoteCDM || remoteCDM.challenge === null || base64EncodedBody !== remoteCDM.challenge) && interceptType === "EME") {
|
||||
foundChallengeInBody = true;
|
||||
window.postMessage({ type: "__LICENSE_URL__", data: resource }, "*");
|
||||
// Block the request
|
||||
return;
|
||||
}
|
||||
if ((base64EncodedBody.startsWith("CAES") || base64EncodedBody.startsWith("PD94")) && interceptType == "LICENSE" && !foundChallengeInBody) {
|
||||
foundChallengeInBody = true;
|
||||
window.postMessage({ type: "__LICENSE_URL__", data: resource }, "*");
|
||||
if (!remoteCDM) {
|
||||
if (base64EncodedBody.startsWith("CAES")) {
|
||||
const {
|
||||
device_type, system_id, security_level, host, secret, device_name
|
||||
} = widevineDeviceInfo;
|
||||
remoteCDM = new remoteWidevineCDM(device_type, system_id, security_level, host, secret, device_name);
|
||||
remoteCDM.openSession();
|
||||
remoteCDM.getChallenge(foundWidevinePssh);
|
||||
}
|
||||
if (base64EncodedBody.startsWith("PD94")) {
|
||||
const {
|
||||
security_level, host, secret, device_name
|
||||
} = playreadyDeviceInfo;
|
||||
remoteCDM = new remotePlayReadyCDM(security_level, host, secret, device_name);
|
||||
remoteCDM.openSession();
|
||||
remoteCDM.getChallenge(foundPlayreadyPssh);
|
||||
}
|
||||
}
|
||||
if (remoteCDM && remoteCDM.challenge === null) {
|
||||
remoteCDM.getChallenge(foundWidevinePssh);
|
||||
}
|
||||
const injectedBody = atob(remoteCDM.challenge);
|
||||
config.body = injectedBody;
|
||||
return originalFetch(resource, config);
|
||||
}
|
||||
}
|
||||
if (typeof body === 'string' && isJson(body)) {
|
||||
const jsonBody = JSON.parse(body);
|
||||
|
||||
if ((jsonContainsValue(jsonBody, "CAES") || jsonContainsValue(jsonBody, "PD94")) && (!remoteCDM || remoteCDM.challenge === null) && interceptType === "EME") {
|
||||
foundChallengeInBody = true;
|
||||
window.postMessage({ type: "__LICENSE_URL__", data: resource }, "*");
|
||||
// Block the request
|
||||
return;
|
||||
}
|
||||
|
||||
if ((jsonContainsValue(jsonBody, "CAES") || jsonContainsValue(jsonBody, "PD94")) && interceptType === "LICENSE" && !foundChallengeInBody) {
|
||||
foundChallengeInBody = true;
|
||||
window.postMessage({ type: "__LICENSE_URL__", data: resource }, "*");
|
||||
if (!remoteCDM) {
|
||||
if (jsonContainsValue(jsonBody, "CAES")) {
|
||||
const {
|
||||
device_type, system_id, security_level, host, secret, device_name
|
||||
} = widevineDeviceInfo;
|
||||
remoteCDM = new remoteWidevineCDM(device_type, system_id, security_level, host, secret, device_name);
|
||||
remoteCDM.openSession();
|
||||
remoteCDM.getChallenge(foundWidevinePssh);
|
||||
}
|
||||
if (jsonContainsValue(jsonBody, "PD94")) {
|
||||
const {
|
||||
security_level, host, secret, device_name
|
||||
} = playreadyDeviceInfo;
|
||||
remoteCDM = new remotePlayReadyCDM(security_level, host, secret, device_name);
|
||||
remoteCDM.openSession();
|
||||
remoteCDM.getChallenge(foundPlayreadyPssh);
|
||||
}
|
||||
}
|
||||
if (remoteCDM && remoteCDM.challenge === null) {
|
||||
remoteCDM.getChallenge(foundWidevinePssh);
|
||||
}
|
||||
const injectedBody = jsonReplaceValue(jsonBody, remoteCDM.challenge);
|
||||
config.body = JSON.stringify(injectedBody);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return originalFetch(resource, config);
|
||||
};
|
||||
})();
|
||||
|
||||
// XHR POST interceptor
|
||||
(function() {
|
||||
const originalOpen = XMLHttpRequest.prototype.open;
|
||||
const originalSend = XMLHttpRequest.prototype.send;
|
||||
|
||||
XMLHttpRequest.prototype.open = function(method, url, async, user, password) {
|
||||
this._method = method;
|
||||
this._url = url;
|
||||
return originalOpen.apply(this, arguments);
|
||||
};
|
||||
|
||||
XMLHttpRequest.prototype.send = function(body) {
|
||||
if (this._method && this._method.toUpperCase() === 'POST') {
|
||||
if (body) {
|
||||
|
||||
if (body instanceof ArrayBuffer || body instanceof Uint8Array) {
|
||||
const buffer = body instanceof Uint8Array ? body : new Uint8Array(body);
|
||||
const base64Body = window.btoa(String.fromCharCode(...buffer));
|
||||
if ((base64Body.startsWith("CAES") || base64Body.startsWith("PD94")) && (!remoteCDM || remoteCDM.challenge === null || base64Body !== remoteCDM.challenge) && interceptType === "EME") {
|
||||
foundChallengeInBody = true;
|
||||
window.postMessage({ type: "__LICENSE_URL__", data: this._url }, "*");
|
||||
// Block the request
|
||||
return;
|
||||
}
|
||||
if ((base64Body.startsWith("CAES") || base64Body.startsWith("PD94")) && interceptType == "LICENSE" &&!foundChallengeInBody) {
|
||||
foundChallengeInBody = true;
|
||||
window.postMessage({ type: "__LICENSE_URL__", data: this._url }, "*");
|
||||
if (!remoteCDM) {
|
||||
if (base64Body.startsWith("CAES")) {
|
||||
const {
|
||||
device_type, system_id, security_level, host, secret, device_name
|
||||
} = widevineDeviceInfo;
|
||||
remoteCDM = new remoteWidevineCDM(device_type, system_id, security_level, host, secret, device_name);
|
||||
remoteCDM.openSession();
|
||||
remoteCDM.getChallenge(foundWidevinePssh);
|
||||
}
|
||||
if (base64Body.startsWith("PD94")) {
|
||||
const {
|
||||
security_level, host, secret, device_name
|
||||
} = playreadyDeviceInfo;
|
||||
remoteCDM = new remotePlayReadyCDM(security_level, host, secret, device_name);
|
||||
remoteCDM.openSession();
|
||||
remoteCDM.getChallenge(foundPlayreadyPssh);
|
||||
}
|
||||
}
|
||||
if (remoteCDM && remoteCDM.challenge === null) {
|
||||
remoteCDM.getChallenge(foundWidevinePssh);
|
||||
}
|
||||
const injectedBody = base64ToUint8Array(remoteCDM.challenge);
|
||||
return originalSend.call(this, injectedBody);
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof body === 'string' && !isJson(body)) {
|
||||
const base64EncodedBody = btoa(body);
|
||||
if ((base64EncodedBody.startsWith("CAES") || base64EncodedBody.startsWith("PD94")) && (!remoteCDM || remoteCDM.challenge === null || base64EncodedBody !== remoteCDM.challenge) && interceptType === "EME") {
|
||||
foundChallengeInBody = true;
|
||||
window.postMessage({ type: "__LICENSE_URL__", data: this._url }, "*");
|
||||
// Block the request
|
||||
return;
|
||||
}
|
||||
if ((base64EncodedBody.startsWith("CAES") || base64EncodedBody.startsWith("PD94")) && interceptType == "LICENSE" && !foundChallengeInBody) {
|
||||
foundChallengeInBody = true;
|
||||
window.postMessage({ type: "__LICENSE_URL__", data: this._url }, "*");
|
||||
if (!remoteCDM) {
|
||||
if (base64EncodedBody.startsWith("CAES")) {
|
||||
const {
|
||||
device_type, system_id, security_level, host, secret, device_name
|
||||
} = widevineDeviceInfo;
|
||||
remoteCDM = new remoteWidevineCDM(device_type, system_id, security_level, host, secret, device_name);
|
||||
remoteCDM.openSession();
|
||||
remoteCDM.getChallenge(foundWidevinePssh);
|
||||
}
|
||||
if (base64EncodedBody.startsWith("PD94")) {
|
||||
const {
|
||||
security_level, host, secret, device_name
|
||||
} = playreadyDeviceInfo;
|
||||
remoteCDM = new remotePlayReadyCDM(security_level, host, secret, device_name);
|
||||
remoteCDM.openSession();
|
||||
remoteCDM.getChallenge(foundPlayreadyPssh);
|
||||
}
|
||||
}
|
||||
if (remoteCDM && remoteCDM.challenge === null) {
|
||||
remoteCDM.getChallenge(foundWidevinePssh);
|
||||
}
|
||||
const injectedBody = atob(remoteCDM.challenge);
|
||||
return originalSend.call(this, injectedBody);
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof body === 'string' && isJson(body)) {
|
||||
const jsonBody = JSON.parse(body);
|
||||
|
||||
if ((jsonContainsValue(jsonBody, "CAES") || jsonContainsValue(jsonBody, "PD94")) && (!remoteCDM || remoteCDM.challenge === null) && interceptType === "EME") {
|
||||
foundChallengeInBody = true;
|
||||
window.postMessage({ type: "__LICENSE_URL__", data: this._url }, "*");
|
||||
// Block the request
|
||||
return;
|
||||
}
|
||||
|
||||
if ((jsonContainsValue(jsonBody, "CAES") || jsonContainsValue(jsonBody, "PD94")) && interceptType === "LICENSE" && !foundChallengeInBody) {
|
||||
foundChallengeInBody = true;
|
||||
window.postMessage({ type: "__LICENSE_URL__", data: this._url }, "*");
|
||||
if (!remoteCDM) {
|
||||
if (jsonContainsValue(jsonBody, "CAES")) {
|
||||
const {
|
||||
device_type, system_id, security_level, host, secret, device_name
|
||||
} = widevineDeviceInfo;
|
||||
remoteCDM = new remoteWidevineCDM(device_type, system_id, security_level, host, secret, device_name);
|
||||
remoteCDM.openSession();
|
||||
remoteCDM.getChallenge(foundWidevinePssh);
|
||||
}
|
||||
if (jsonContainsValue(jsonBody, "PD94")) {
|
||||
const {
|
||||
security_level, host, secret, device_name
|
||||
} = playreadyDeviceInfo;
|
||||
remoteCDM = new remotePlayReadyCDM(security_level, host, secret, device_name);
|
||||
remoteCDM.openSession();
|
||||
remoteCDM.getChallenge(foundPlayreadyPssh);
|
||||
}
|
||||
}
|
||||
if (remoteCDM && remoteCDM.challenge === null) {
|
||||
remoteCDM.getChallenge(foundWidevinePssh);
|
||||
}
|
||||
const injectedBody = jsonReplaceValue(jsonBody, remoteCDM.challenge);
|
||||
return originalSend.call(this, JSON.stringify(injectedBody));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return originalSend.apply(this, arguments);
|
||||
};
|
||||
})();
|
@ -1,41 +0,0 @@
|
||||
{
|
||||
"manifest_version": 2,
|
||||
"name": "CDRM Extension 2.0",
|
||||
"version": "2.0",
|
||||
"description": "Decrypt DRM Protected content",
|
||||
"permissions": [
|
||||
"webRequest",
|
||||
"webRequestBlocking",
|
||||
"<all_urls>",
|
||||
"activeTab",
|
||||
"storage",
|
||||
"tabs",
|
||||
"contextMenus"
|
||||
],
|
||||
"background": {
|
||||
"scripts": ["background.js"],
|
||||
"persistent": true
|
||||
},
|
||||
"content_scripts": [
|
||||
{
|
||||
"matches": ["<all_urls>"],
|
||||
"js": ["content.js"],
|
||||
"run_at": "document_start",
|
||||
"all_frames": true
|
||||
}
|
||||
],
|
||||
"web_accessible_resources": ["inject.js"],
|
||||
"content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self'",
|
||||
"browser_action": {
|
||||
"default_icon": {
|
||||
"16": "icons/icon16.png",
|
||||
"32": "icons/icon32.png",
|
||||
"128": "icons/icon128.png"
|
||||
}
|
||||
},
|
||||
"icons": {
|
||||
"16": "icons/icon16.png",
|
||||
"32": "icons/icon32.png",
|
||||
"128": "icons/icon128.png"
|
||||
}
|
||||
}
|
89
mv2/background.js
Normal file
89
mv2/background.js
Normal file
@ -0,0 +1,89 @@
|
||||
// Open popout window when the extension icon is clicked
|
||||
chrome.browserAction.onClicked.addListener(() => {
|
||||
chrome.windows.create({
|
||||
url: chrome.runtime.getURL("index.html"),
|
||||
type: "popup", // opens as a floating window
|
||||
width: 800,
|
||||
height: 600,
|
||||
});
|
||||
});
|
||||
|
||||
// Listen for messages and store data in chrome.storage.local
|
||||
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||
const { type, data } = message;
|
||||
|
||||
switch (type) {
|
||||
case "DRM_TYPE":
|
||||
console.log("DRM Type:", data);
|
||||
chrome.storage.local.set({ drmType: data });
|
||||
break;
|
||||
|
||||
case "PSSH_DATA":
|
||||
console.log("Storing PSSH:", data);
|
||||
chrome.storage.local.set({ latestPSSH: data });
|
||||
break;
|
||||
|
||||
case "KEYS_DATA":
|
||||
console.log("Storing Decryption Keys:", data);
|
||||
chrome.storage.local.set({ latestKeys: data });
|
||||
break;
|
||||
|
||||
case "LICENSE_URL":
|
||||
console.log("Storling License URL " + data);
|
||||
chrome.storage.local.set({ licenseURL: data });
|
||||
break;
|
||||
|
||||
case "MANIFEST_URL_FOUND":
|
||||
console.log("Storing Manifest URL:", data);
|
||||
chrome.storage.local.set({ manifestURL: data });
|
||||
break;
|
||||
|
||||
default:
|
||||
console.warn("Unknown message type received:", type);
|
||||
}
|
||||
});
|
||||
|
||||
// Set initial config and injection type on install
|
||||
chrome.runtime.onInstalled.addListener((details) => {
|
||||
if (details.reason === "install") {
|
||||
chrome.storage.local.set({ valid_config: false }, () => {
|
||||
if (chrome.runtime.lastError) {
|
||||
console.error("Error setting valid_config:", chrome.runtime.lastError);
|
||||
} else {
|
||||
console.log("valid_config set to false on first install.");
|
||||
}
|
||||
});
|
||||
|
||||
chrome.storage.local.set({ injection_type: "LICENSE" }, () => {
|
||||
if (chrome.runtime.lastError) {
|
||||
console.error("Error setting Injection Type:", chrome.runtime.lastError);
|
||||
} else {
|
||||
console.log("Injection type set to LICENSE on first install.");
|
||||
}
|
||||
});
|
||||
|
||||
chrome.storage.local.set({ drm_override: "DISABLED" }, () => {
|
||||
if (chrome.runtime.lastError) {
|
||||
console.error("Error setting DRM Override type:", chrome.runtime.lastError);
|
||||
} else {
|
||||
console.log("DRM Override type set to DISABLED on first install.");
|
||||
}
|
||||
});
|
||||
|
||||
chrome.storage.local.set({ cdrm_instance: null }, () => {
|
||||
if (chrome.runtime.lastError) {
|
||||
console.error("Error setting CDRM instance:", chrome.runtime.lastError);
|
||||
} else {
|
||||
console.log("CDRM instance set to null.");
|
||||
}
|
||||
});
|
||||
|
||||
chrome.storage.local.set({ cdrm_api_key: null }, () => {
|
||||
if (chrome.runtime.lastError) {
|
||||
console.error("Error setting CDRM API Key:", chrome.runtime.lastError);
|
||||
} else {
|
||||
console.log("CDRM API Key set.");
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
96
mv2/content.js
Normal file
96
mv2/content.js
Normal file
@ -0,0 +1,96 @@
|
||||
// Inject `inject.js` into the page context
|
||||
(function injectScript() {
|
||||
const script = document.createElement("script");
|
||||
script.src = chrome.runtime.getURL("inject.js");
|
||||
script.type = "text/javascript";
|
||||
script.onload = () => script.remove(); // Clean up
|
||||
// Inject directly into <html> or <head>
|
||||
(document.documentElement || document.head || document.body).appendChild(script);
|
||||
})();
|
||||
|
||||
// Listen for messages from the injected script
|
||||
window.addEventListener("message", function (event) {
|
||||
if (event.source !== window) return;
|
||||
|
||||
if (
|
||||
["__DRM_TYPE__", "__PSSH_DATA__", "__KEYS_DATA__", "__LICENSE_URL__"].includes(
|
||||
event.data?.type
|
||||
)
|
||||
) {
|
||||
chrome.runtime.sendMessage({
|
||||
type: event.data.type.replace("__", "").replace("__", ""),
|
||||
data: event.data.data,
|
||||
});
|
||||
}
|
||||
|
||||
if (event.data.type === "__GET_CDM_DEVICES__") {
|
||||
chrome.storage.local.get(["widevine_device", "playready_device"], (result) => {
|
||||
const widevine_device = result.widevine_device || null;
|
||||
const playready_device = result.playready_device || null;
|
||||
|
||||
window.postMessage(
|
||||
{
|
||||
type: "__CDM_DEVICES__",
|
||||
widevine_device,
|
||||
playready_device,
|
||||
},
|
||||
"*"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
if (event.data.type === "__GET_INJECTION_TYPE__") {
|
||||
chrome.storage.local.get("injection_type", (result) => {
|
||||
const injectionType = result.injection_type || "LICENSE";
|
||||
|
||||
window.postMessage(
|
||||
{
|
||||
type: "__INJECTION_TYPE__",
|
||||
injectionType,
|
||||
},
|
||||
"*"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
if (event.data.type === "__GET_DRM_OVERRIDE__") {
|
||||
chrome.storage.local.get("drm_override", (result) => {
|
||||
const drmOverride = result.drm_override || "DISABLED";
|
||||
|
||||
window.postMessage(
|
||||
{
|
||||
type: "__DRM_OVERRIDE__",
|
||||
drmOverride,
|
||||
},
|
||||
"*"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// Manifest header and URL
|
||||
|
||||
const seenManifestUrls = new Set();
|
||||
|
||||
if (event.data?.type === "__MANIFEST_URL__") {
|
||||
const url = event.data.data;
|
||||
if (seenManifestUrls.has(url)) return;
|
||||
seenManifestUrls.add(url);
|
||||
console.log("✅ [Content] Unique manifest URL:", url);
|
||||
|
||||
chrome.runtime.sendMessage({
|
||||
type: "MANIFEST_URL_FOUND",
|
||||
data: url,
|
||||
});
|
||||
}
|
||||
|
||||
if (event.data?.type === "__MANIFEST_HEADERS__") {
|
||||
const { url, headers } = event.data;
|
||||
console.log("[Content.js] Manifest Headers:", url, headers);
|
||||
|
||||
chrome.runtime.sendMessage({
|
||||
type: "MANIFEST_HEADERS",
|
||||
url,
|
||||
headers,
|
||||
});
|
||||
}
|
||||
});
|
1143
mv2/inject.js
Normal file
1143
mv2/inject.js
Normal file
File diff suppressed because it is too large
Load Diff
41
mv2/manifest.json
Normal file
41
mv2/manifest.json
Normal file
@ -0,0 +1,41 @@
|
||||
{
|
||||
"manifest_version": 2,
|
||||
"name": "CDRM Extension",
|
||||
"version": "2.1.0",
|
||||
"description": "Decrypt DRM protected content",
|
||||
"permissions": [
|
||||
"webRequest",
|
||||
"webRequestBlocking",
|
||||
"<all_urls>",
|
||||
"activeTab",
|
||||
"storage",
|
||||
"tabs",
|
||||
"contextMenus"
|
||||
],
|
||||
"background": {
|
||||
"scripts": ["background.js"],
|
||||
"persistent": true
|
||||
},
|
||||
"content_scripts": [
|
||||
{
|
||||
"matches": ["<all_urls>"],
|
||||
"js": ["content.js"],
|
||||
"run_at": "document_start",
|
||||
"all_frames": true
|
||||
}
|
||||
],
|
||||
"web_accessible_resources": ["inject.js"],
|
||||
"content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self'",
|
||||
"browser_action": {
|
||||
"default_icon": {
|
||||
"16": "icons/icon16.png",
|
||||
"32": "icons/icon32.png",
|
||||
"128": "icons/icon128.png"
|
||||
}
|
||||
},
|
||||
"icons": {
|
||||
"16": "icons/icon16.png",
|
||||
"32": "icons/icon32.png",
|
||||
"128": "icons/icon128.png"
|
||||
}
|
||||
}
|
136
package-lock.json
generated
Normal file
136
package-lock.json
generated
Normal file
@ -0,0 +1,136 @@
|
||||
{
|
||||
"name": "cdrm-extension",
|
||||
"version": "2.1.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "cdrm-extension",
|
||||
"version": "2.1.0",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"terser": "^5.43.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=21.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/gen-mapping": {
|
||||
"version": "0.3.12",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.12.tgz",
|
||||
"integrity": "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/sourcemap-codec": "^1.5.0",
|
||||
"@jridgewell/trace-mapping": "^0.3.24"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/resolve-uri": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
|
||||
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/source-map": {
|
||||
"version": "0.3.10",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.10.tgz",
|
||||
"integrity": "sha512-0pPkgz9dY+bijgistcTTJ5mR+ocqRXLuhXHYdzoMmmoJ2C9S46RCm2GMUbatPEUK9Yjy26IrAy8D/M00lLkv+Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/gen-mapping": "^0.3.5",
|
||||
"@jridgewell/trace-mapping": "^0.3.25"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/sourcemap-codec": {
|
||||
"version": "1.5.4",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.4.tgz",
|
||||
"integrity": "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@jridgewell/trace-mapping": {
|
||||
"version": "0.3.29",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.29.tgz",
|
||||
"integrity": "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/resolve-uri": "^3.1.0",
|
||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||
}
|
||||
},
|
||||
"node_modules/acorn": {
|
||||
"version": "8.15.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/buffer-from": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
|
||||
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/commander": {
|
||||
"version": "2.20.3",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
|
||||
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/source-map": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
||||
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/source-map-support": {
|
||||
"version": "0.5.21",
|
||||
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
|
||||
"integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"buffer-from": "^1.0.0",
|
||||
"source-map": "^0.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/terser": {
|
||||
"version": "5.43.1",
|
||||
"resolved": "https://registry.npmjs.org/terser/-/terser-5.43.1.tgz",
|
||||
"integrity": "sha512-+6erLbBm0+LROX2sPXlUYx/ux5PyE9K/a92Wrt6oA+WDAoFTdpHE5tCYCI5PNzq2y8df4rA+QgHLJuR4jNymsg==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"@jridgewell/source-map": "^0.3.3",
|
||||
"acorn": "^8.14.0",
|
||||
"commander": "^2.20.0",
|
||||
"source-map-support": "~0.5.20"
|
||||
},
|
||||
"bin": {
|
||||
"terser": "bin/terser"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
24
package.json
Normal file
24
package.json
Normal file
@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "cdrm-extension",
|
||||
"version": "2.1.0",
|
||||
"description": "",
|
||||
"main": "background.js",
|
||||
"scripts": {
|
||||
"buildext": "node buildext.js",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://cdm-project.com/tpd94/CDRM-Extension.git"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"type": "module",
|
||||
"engines": {
|
||||
"node": ">=21.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"terser": "^5.43.1"
|
||||
}
|
||||
}
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -1,13 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>CDRM Decryption Extension</title>
|
||||
<script type="module" crossorigin src="./assets/index-ydPQKJSy.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="./assets/index-UaipKa9p.css">
|
||||
</head>
|
||||
<body class="min-w-full min-h-full w-full h-full">
|
||||
<div class="min-w-full min-h-full w-full h-full" id="root"></div>
|
||||
</body>
|
||||
</html>
|
88
src/background.js
Normal file
88
src/background.js
Normal file
@ -0,0 +1,88 @@
|
||||
chrome.action.onClicked.addListener(() => {
|
||||
chrome.windows.create({
|
||||
url: chrome.runtime.getURL("index.html"),
|
||||
type: "popup",
|
||||
width: 800,
|
||||
height: 600,
|
||||
});
|
||||
});
|
||||
|
||||
// Listen for messages and store data in chrome.storage.local
|
||||
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||
const { type, data } = message;
|
||||
|
||||
switch (type) {
|
||||
case "DRM_TYPE":
|
||||
console.log("[CDRM-Extension] DRM Type:", data);
|
||||
chrome.storage.local.set({ drmType: data });
|
||||
break;
|
||||
|
||||
case "PSSH_DATA":
|
||||
console.log("[CDRM-Extension] Storing PSSH:", data);
|
||||
chrome.storage.local.set({ latestPSSH: data });
|
||||
break;
|
||||
|
||||
case "KEYS_DATA":
|
||||
console.log("[CDRM-Extension] Storing Decryption Keys:", data);
|
||||
chrome.storage.local.set({ latestKeys: data });
|
||||
break;
|
||||
|
||||
case "LICENSE_URL":
|
||||
console.log("[CDRM-Extension] Storing License URL " + data);
|
||||
chrome.storage.local.set({ licenseURL: data });
|
||||
break;
|
||||
|
||||
case "MANIFEST_URL":
|
||||
console.log("[CDRM-Extension] Storing Manifest URL:", data);
|
||||
chrome.storage.local.set({ manifestURL: data });
|
||||
break;
|
||||
|
||||
default:
|
||||
console.warn("[CDRM-Extension] Unknown message type received:", type);
|
||||
}
|
||||
});
|
||||
|
||||
// Set initial config and injection type on install
|
||||
chrome.runtime.onInstalled.addListener((details) => {
|
||||
if (details.reason === "install") {
|
||||
chrome.storage.local.set({ valid_config: false }, () => {
|
||||
if (chrome.runtime.lastError) {
|
||||
console.error("[CDRM-Extension] Error setting valid_config:", chrome.runtime.lastError);
|
||||
} else {
|
||||
console.log("[CDRM-Extension] valid_config set to false on first install.");
|
||||
}
|
||||
});
|
||||
|
||||
chrome.storage.local.set({ injection_type: "LICENSE" }, () => {
|
||||
if (chrome.runtime.lastError) {
|
||||
console.error("[CDRM-Extension] Error setting Injection Type:", chrome.runtime.lastError);
|
||||
} else {
|
||||
console.log("[CDRM-Extension] Injection type set to LICENSE on first install.");
|
||||
}
|
||||
});
|
||||
|
||||
chrome.storage.local.set({ drm_override: "DISABLED" }, () => {
|
||||
if (chrome.runtime.lastError) {
|
||||
console.error("[CDRM-Extension] Error setting DRM Override type:", chrome.runtime.lastError);
|
||||
} else {
|
||||
console.log("[CDRM-Extension] DRM Override type set to DISABLED on first install.");
|
||||
}
|
||||
});
|
||||
|
||||
chrome.storage.local.set({ cdrm_instance: null }, () => {
|
||||
if (chrome.runtime.lastError) {
|
||||
console.error("[CDRM-Extension] Error setting CDRM instance:", chrome.runtime.lastError);
|
||||
} else {
|
||||
console.log("[CDRM-Extension] CDRM instance set to null.");
|
||||
}
|
||||
});
|
||||
|
||||
chrome.storage.local.set({ cdrm_api_key: null }, () => {
|
||||
if (chrome.runtime.lastError) {
|
||||
console.error("[CDRM-Extension] Error setting CDRM API Key:", chrome.runtime.lastError);
|
||||
} else {
|
||||
console.log("[CDRM-Extension] CDRM API Key set.");
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
96
src/content.js
Normal file
96
src/content.js
Normal file
@ -0,0 +1,96 @@
|
||||
// Inject `inject.js` into the page context
|
||||
(function injectScript() {
|
||||
const script = document.createElement("script");
|
||||
script.src = chrome.runtime.getURL("inject.js");
|
||||
script.type = "text/javascript";
|
||||
script.onload = () => script.remove(); // Clean up
|
||||
// Inject directly into <html> or <head>
|
||||
(document.documentElement || document.head || document.body).appendChild(script);
|
||||
})();
|
||||
|
||||
// Listen for messages from the injected script
|
||||
window.addEventListener("message", function (event) {
|
||||
if (event.source !== window) return;
|
||||
|
||||
if (
|
||||
["__DRM_TYPE__", "__PSSH_DATA__", "__KEYS_DATA__", "__LICENSE_URL__"].includes(
|
||||
event.data?.type
|
||||
)
|
||||
) {
|
||||
chrome.runtime.sendMessage({
|
||||
type: event.data.type.replace("__", "").replace("__", ""),
|
||||
data: event.data.data,
|
||||
});
|
||||
}
|
||||
|
||||
if (event.data.type === "__GET_CDM_DEVICES__") {
|
||||
chrome.storage.local.get(["widevine_device", "playready_device"], (result) => {
|
||||
const widevine_device = result.widevine_device || null;
|
||||
const playready_device = result.playready_device || null;
|
||||
|
||||
window.postMessage(
|
||||
{
|
||||
type: "__CDM_DEVICES__",
|
||||
widevine_device,
|
||||
playready_device,
|
||||
},
|
||||
"*"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
if (event.data.type === "__GET_INJECTION_TYPE__") {
|
||||
chrome.storage.local.get("injection_type", (result) => {
|
||||
const injectionType = result.injection_type || "LICENSE";
|
||||
|
||||
window.postMessage(
|
||||
{
|
||||
type: "__INJECTION_TYPE__",
|
||||
injectionType,
|
||||
},
|
||||
"*"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
if (event.data.type === "__GET_DRM_OVERRIDE__") {
|
||||
chrome.storage.local.get("drm_override", (result) => {
|
||||
const drmOverride = result.drm_override || "DISABLED";
|
||||
|
||||
window.postMessage(
|
||||
{
|
||||
type: "__DRM_OVERRIDE__",
|
||||
drmOverride,
|
||||
},
|
||||
"*"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// Manifest header and URL
|
||||
|
||||
const seenManifestUrls = new Set();
|
||||
|
||||
if (event.data?.type === "__MANIFEST_URL__") {
|
||||
const url = event.data.data;
|
||||
if (seenManifestUrls.has(url)) return;
|
||||
seenManifestUrls.add(url);
|
||||
console.log("[CDRM-Extension] ✅ [content.js] Unique manifest URL:", url);
|
||||
|
||||
chrome.runtime.sendMessage({
|
||||
type: "MANIFEST_URL",
|
||||
data: url,
|
||||
});
|
||||
}
|
||||
|
||||
if (event.data?.type === "__MANIFEST_HEADERS__") {
|
||||
const { url, headers } = event.data;
|
||||
console.log("[CDRM-Extension] [content.js] Manifest headers:", url, headers);
|
||||
|
||||
chrome.runtime.sendMessage({
|
||||
type: "MANIFEST_HEADERS",
|
||||
url,
|
||||
headers,
|
||||
});
|
||||
}
|
||||
});
|
925
src/inject.js
Normal file
925
src/inject.js
Normal file
@ -0,0 +1,925 @@
|
||||
let widevineDeviceInfo = null;
|
||||
let playreadyDeviceInfo = null;
|
||||
let originalChallenge = null;
|
||||
let serviceCertFound = false;
|
||||
let drmType = "NONE";
|
||||
let psshFound = false;
|
||||
let foundWidevinePssh = null;
|
||||
let foundPlayreadyPssh = null;
|
||||
let drmDecided = null;
|
||||
let drmOverride = "DISABLED";
|
||||
let interceptType = "DISABLED";
|
||||
let remoteCDM = null;
|
||||
let generateRequestCalled = false;
|
||||
let remoteListenerMounted = false;
|
||||
let injectionSuccess = false;
|
||||
let foundChallengeInBody = false;
|
||||
let licenseResponseCounter = 0;
|
||||
let keysRetrieved = false;
|
||||
|
||||
const DRM_SIGNATURES = {
|
||||
WIDEVINE: "CAES",
|
||||
PLAYREADY: "PD94",
|
||||
SERVICE_CERT: "CAUS",
|
||||
WIDEVINE_INIT: "CAQ=",
|
||||
};
|
||||
|
||||
const EXTENSION_PREFIX = "[CDRM EXTENSION]";
|
||||
const PREFIX_COLOR = "black";
|
||||
const PREFIX_BACKGROUND_COLOR = "yellow";
|
||||
|
||||
const logWithPrefix = (...args) => {
|
||||
const style = `color: ${PREFIX_COLOR}; background: ${PREFIX_BACKGROUND_COLOR}; font-weight: bold; padding: 2px 4px; border-radius: 2px;`;
|
||||
if (typeof args[0] === "string") {
|
||||
// If the first arg is a string, prepend the prefix
|
||||
console.log(`%c${EXTENSION_PREFIX}%c ${args[0]}`, style, "", ...args.slice(1));
|
||||
} else {
|
||||
// If not, just log the prefix and the rest
|
||||
console.log(`%c${EXTENSION_PREFIX}`, style, ...args);
|
||||
}
|
||||
};
|
||||
|
||||
function resetDRMState() {
|
||||
logWithPrefix("Resetting DRM state for new manifest...");
|
||||
|
||||
// Reset DRM detection state
|
||||
originalChallenge = null;
|
||||
serviceCertFound = false;
|
||||
drmType = "NONE";
|
||||
psshFound = false;
|
||||
foundWidevinePssh = null;
|
||||
foundPlayreadyPssh = null;
|
||||
drmDecided = null;
|
||||
|
||||
// Reset CDM and session state
|
||||
if (remoteCDM) {
|
||||
try {
|
||||
// Try to close the existing session if it exists
|
||||
if (remoteCDM.session_id) {
|
||||
remoteCDM.closeSession();
|
||||
}
|
||||
} catch (e) {
|
||||
// Ignore errors when closing session
|
||||
logWithPrefix("Error closing previous CDM session:", e.message);
|
||||
}
|
||||
remoteCDM = null;
|
||||
}
|
||||
|
||||
// Reset interceptor state
|
||||
generateRequestCalled = false;
|
||||
remoteListenerMounted = false;
|
||||
injectionSuccess = false;
|
||||
foundChallengeInBody = false;
|
||||
licenseResponseCounter = 0;
|
||||
keysRetrieved = false;
|
||||
|
||||
// Post reset messages to clear UI state
|
||||
window.postMessage({ type: "__DRM_TYPE__", data: "" }, "*");
|
||||
window.postMessage({ type: "__PSSH_DATA__", data: "" }, "*");
|
||||
window.postMessage({ type: "__KEYS_DATA__", data: "" }, "*");
|
||||
window.postMessage({ type: "__LICENSE_URL__", data: "" }, "*");
|
||||
}
|
||||
|
||||
window.postMessage({ type: "__GET_DRM_OVERRIDE__" }, "*");
|
||||
window.postMessage({ type: "__GET_INJECTION_TYPE__" }, "*");
|
||||
window.postMessage({ type: "__GET_CDM_DEVICES__" }, "*");
|
||||
|
||||
function createMessageHandler(handlers) {
|
||||
window.addEventListener("message", function (event) {
|
||||
if (event.source !== window) return;
|
||||
|
||||
const handler = handlers[event.data.type];
|
||||
if (handler) {
|
||||
handler(event.data);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
createMessageHandler({
|
||||
__DRM_OVERRIDE__: (data) => {
|
||||
drmOverride = data.drmOverride || "DISABLED";
|
||||
logWithPrefix("DRM Override set to:", drmOverride);
|
||||
},
|
||||
__INJECTION_TYPE__: (data) => {
|
||||
interceptType = data.injectionType || "DISABLED";
|
||||
logWithPrefix("Injection type set to:", interceptType);
|
||||
},
|
||||
__CDM_DEVICES__: (data) => {
|
||||
const { widevine_device, playready_device } = data;
|
||||
logWithPrefix("Received device info:", widevine_device, playready_device);
|
||||
widevineDeviceInfo = widevine_device;
|
||||
playreadyDeviceInfo = playready_device;
|
||||
},
|
||||
});
|
||||
|
||||
function safeHeaderShellEscape(str) {
|
||||
return str
|
||||
.replace(/\\/g, "\\\\")
|
||||
.replace(/"/g, '\\"')
|
||||
.replace(/\$/g, "\\$") // escape shell expansion
|
||||
.replace(/`/g, "\\`")
|
||||
.replace(/\n/g, ""); // strip newlines
|
||||
}
|
||||
|
||||
function headersToFlags(headersObj) {
|
||||
return Object.entries(headersObj)
|
||||
.map(
|
||||
([key, val]) =>
|
||||
'--add-headers "' +
|
||||
safeHeaderShellEscape(key) +
|
||||
": " +
|
||||
safeHeaderShellEscape(val) +
|
||||
'"'
|
||||
)
|
||||
.join(" ");
|
||||
}
|
||||
|
||||
function handleManifestDetection(url, headersObj, contentType, source) {
|
||||
// Reset DRM state when new manifest is detected
|
||||
resetDRMState();
|
||||
|
||||
window.postMessage({ type: "__MANIFEST_URL__", data: url }, "*");
|
||||
logWithPrefix(`[Manifest][${source}]`, url, contentType);
|
||||
|
||||
const headerFlags = headersToFlags(headersObj);
|
||||
|
||||
window.postMessage(
|
||||
{
|
||||
type: "__MANIFEST_HEADERS__",
|
||||
url,
|
||||
headers: headerFlags,
|
||||
},
|
||||
"*"
|
||||
);
|
||||
}
|
||||
|
||||
// Intercep network to find manifest
|
||||
function injectManifestInterceptor() {
|
||||
// Execute the interceptor code directly instead of injecting a script
|
||||
(function () {
|
||||
function isProbablyManifest(text = "", contentType = "") {
|
||||
const lowerCT = contentType?.toLowerCase() ?? "";
|
||||
const sample = text.slice(0, 2000);
|
||||
|
||||
const isHLSMime = lowerCT.includes("mpegurl");
|
||||
const isDASHMime = lowerCT.includes("dash+xml");
|
||||
const isSmoothMime = lowerCT.includes("sstr+xml");
|
||||
|
||||
const isHLSKeyword = sample.includes("#EXTM3U") || sample.includes("#EXT-X-STREAM-INF");
|
||||
const isDASHKeyword = sample.includes("<MPD") || sample.includes("<AdaptationSet");
|
||||
const isSmoothKeyword = sample.includes("<SmoothStreamingMedia");
|
||||
const isJsonManifest = sample.includes('"playlist"') && sample.includes('"segments"');
|
||||
|
||||
return (
|
||||
isHLSMime ||
|
||||
isDASHMime ||
|
||||
isSmoothMime ||
|
||||
isHLSKeyword ||
|
||||
isDASHKeyword ||
|
||||
isSmoothKeyword ||
|
||||
isJsonManifest
|
||||
);
|
||||
}
|
||||
|
||||
const originalFetch = window.fetch;
|
||||
window.fetch = async function (input, init) {
|
||||
const response = await originalFetch.apply(this, arguments);
|
||||
|
||||
try {
|
||||
const clone = response.clone();
|
||||
const contentType = clone.headers.get("content-type") || "";
|
||||
const text = await clone.text();
|
||||
|
||||
const url = typeof input === "string" ? input : input.url;
|
||||
|
||||
if (isProbablyManifest(text, contentType)) {
|
||||
const headersObj = {};
|
||||
clone.headers.forEach((value, key) => {
|
||||
headersObj[key] = value;
|
||||
});
|
||||
handleManifestDetection(url, headersObj, contentType, "fetch");
|
||||
}
|
||||
} catch (e) {}
|
||||
|
||||
return response;
|
||||
};
|
||||
|
||||
const originalXHROpen = XMLHttpRequest.prototype.open;
|
||||
const originalXHRSend = XMLHttpRequest.prototype.send;
|
||||
|
||||
XMLHttpRequest.prototype.open = function (method, url) {
|
||||
this.__url = url;
|
||||
return originalXHROpen.apply(this, arguments);
|
||||
};
|
||||
|
||||
XMLHttpRequest.prototype.send = function (body) {
|
||||
this.addEventListener("load", function () {
|
||||
try {
|
||||
const contentType = this.getResponseHeader("content-type") || "";
|
||||
const text = this.responseText;
|
||||
|
||||
if (isProbablyManifest(text, contentType)) {
|
||||
const xhrHeaders = {};
|
||||
const rawHeaders = this.getAllResponseHeaders().trim().split(/\r?\n/);
|
||||
rawHeaders.forEach((line) => {
|
||||
const parts = line.split(": ");
|
||||
if (parts.length === 2) {
|
||||
xhrHeaders[parts[0]] = parts[1];
|
||||
}
|
||||
});
|
||||
handleManifestDetection(this.__url, xhrHeaders, contentType, "xhr");
|
||||
}
|
||||
} catch (e) {}
|
||||
});
|
||||
return originalXHRSend.apply(this, arguments);
|
||||
};
|
||||
})();
|
||||
}
|
||||
|
||||
injectManifestInterceptor();
|
||||
|
||||
class RemoteCDMBase {
|
||||
constructor({ host, secret, device_name, security_level }) {
|
||||
this.host = host;
|
||||
this.secret = secret;
|
||||
this.device_name = device_name;
|
||||
this.security_level = security_level;
|
||||
this.session_id = null;
|
||||
this.challenge = null;
|
||||
this.keys = null;
|
||||
}
|
||||
|
||||
openSession(path) {
|
||||
const url = `${this.host}${path}/open`;
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open("GET", url, false);
|
||||
xhr.setRequestHeader("Content-Type", "application/json");
|
||||
xhr.send();
|
||||
const jsonData = JSON.parse(xhr.responseText);
|
||||
if (jsonData.data?.session_id) {
|
||||
this.session_id = jsonData.data.session_id;
|
||||
logWithPrefix("Session opened:", this.session_id);
|
||||
} else {
|
||||
console.error("Failed to open session:", jsonData.message);
|
||||
throw new Error("Failed to open session");
|
||||
}
|
||||
}
|
||||
|
||||
getChallenge(path, body) {
|
||||
const url = `${this.host}${path}/get_license_challenge`;
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open("POST", url, false);
|
||||
xhr.setRequestHeader("Content-Type", "application/json");
|
||||
xhr.send(JSON.stringify(body));
|
||||
const jsonData = JSON.parse(xhr.responseText);
|
||||
if (jsonData.data?.challenge) {
|
||||
this.challenge = btoa(jsonData.data.challenge);
|
||||
logWithPrefix("Challenge received:", this.challenge);
|
||||
} else if (jsonData.data?.challenge_b64) {
|
||||
this.challenge = jsonData.data.challenge_b64;
|
||||
logWithPrefix("Challenge received:", this.challenge);
|
||||
} else {
|
||||
console.error("Failed to get challenge:", jsonData.message);
|
||||
throw new Error("Failed to get challenge");
|
||||
}
|
||||
}
|
||||
|
||||
parseLicense(path, body) {
|
||||
const url = `${this.host}${path}/parse_license`;
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open("POST", url, false);
|
||||
xhr.setRequestHeader("Content-Type", "application/json");
|
||||
xhr.send(JSON.stringify(body));
|
||||
const jsonData = JSON.parse(xhr.responseText);
|
||||
if (jsonData.status === 200 || jsonData.message?.includes("parsed and loaded")) {
|
||||
logWithPrefix("License response parsed successfully");
|
||||
return true;
|
||||
} else {
|
||||
console.error("Failed to parse license response:", jsonData.message);
|
||||
throw new Error("Failed to parse license response");
|
||||
}
|
||||
}
|
||||
|
||||
getKeys(path, body, extraPath = "") {
|
||||
const url = `${this.host}${path}/get_keys${extraPath}`;
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open("POST", url, false);
|
||||
xhr.setRequestHeader("Content-Type", "application/json");
|
||||
xhr.send(JSON.stringify(body));
|
||||
const jsonData = JSON.parse(xhr.responseText);
|
||||
if (jsonData.data?.keys) {
|
||||
this.keys = jsonData.data.keys;
|
||||
logWithPrefix("Keys received:", this.keys);
|
||||
} else {
|
||||
console.error("Failed to get keys:", jsonData.message);
|
||||
throw new Error("Failed to get keys");
|
||||
}
|
||||
}
|
||||
|
||||
closeSession(path) {
|
||||
const url = `${this.host}${path}/close/${this.session_id}`;
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open("GET", url, false);
|
||||
xhr.setRequestHeader("Content-Type", "application/json");
|
||||
xhr.send();
|
||||
const jsonData = JSON.parse(xhr.responseText);
|
||||
if (jsonData) {
|
||||
logWithPrefix("Session closed successfully");
|
||||
} else {
|
||||
console.error("Failed to close session:", jsonData.message);
|
||||
throw new Error("Failed to close session");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// PlayReady Remote CDM Class
|
||||
class remotePlayReadyCDM extends RemoteCDMBase {
|
||||
constructor(security_level, host, secret, device_name) {
|
||||
super({ host, secret, device_name, security_level });
|
||||
}
|
||||
|
||||
openSession() {
|
||||
super.openSession(`/remotecdm/playready/${this.device_name}`);
|
||||
}
|
||||
|
||||
getChallenge(init_data) {
|
||||
super.getChallenge(`/remotecdm/playready/${this.device_name}`, {
|
||||
session_id: this.session_id,
|
||||
init_data: init_data,
|
||||
});
|
||||
}
|
||||
|
||||
parseLicense(license_message) {
|
||||
return super.parseLicense(`/remotecdm/playready/${this.device_name}`, {
|
||||
session_id: this.session_id,
|
||||
license_message: license_message,
|
||||
});
|
||||
}
|
||||
|
||||
getKeys() {
|
||||
super.getKeys(`/remotecdm/playready/${this.device_name}`, {
|
||||
session_id: this.session_id,
|
||||
});
|
||||
}
|
||||
|
||||
closeSession() {
|
||||
super.closeSession(`/remotecdm/playready/${this.device_name}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Widevine Remote CDM Class
|
||||
class remoteWidevineCDM extends RemoteCDMBase {
|
||||
constructor(device_type, system_id, security_level, host, secret, device_name) {
|
||||
super({ host, secret, device_name, security_level });
|
||||
this.device_type = device_type;
|
||||
this.system_id = system_id;
|
||||
}
|
||||
|
||||
openSession() {
|
||||
super.openSession(`/remotecdm/widevine/${this.device_name}`);
|
||||
}
|
||||
|
||||
setServiceCertificate(certificate) {
|
||||
const url = `${this.host}/remotecdm/widevine/${this.device_name}/set_service_certificate`;
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open("POST", url, false);
|
||||
xhr.setRequestHeader("Content-Type", "application/json");
|
||||
const body = {
|
||||
session_id: this.session_id,
|
||||
certificate: certificate ?? null,
|
||||
};
|
||||
xhr.send(JSON.stringify(body));
|
||||
const jsonData = JSON.parse(xhr.responseText);
|
||||
if (jsonData.status === 200) {
|
||||
logWithPrefix("Service certificate set successfully");
|
||||
} else {
|
||||
console.error("Failed to set service certificate:", jsonData.message);
|
||||
throw new Error("Failed to set service certificate");
|
||||
}
|
||||
}
|
||||
|
||||
getChallenge(init_data, license_type = "STREAMING") {
|
||||
const url = `${this.host}/remotecdm/widevine/${this.device_name}/get_license_challenge/${license_type}`;
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open("POST", url, false);
|
||||
xhr.setRequestHeader("Content-Type", "application/json");
|
||||
const body = {
|
||||
session_id: this.session_id,
|
||||
init_data: init_data,
|
||||
privacy_mode: serviceCertFound,
|
||||
};
|
||||
xhr.send(JSON.stringify(body));
|
||||
const jsonData = JSON.parse(xhr.responseText);
|
||||
if (jsonData.data?.challenge_b64) {
|
||||
this.challenge = jsonData.data.challenge_b64;
|
||||
logWithPrefix("Widevine challenge received:", this.challenge);
|
||||
} else {
|
||||
console.error("Failed to get Widevine challenge:", jsonData.message);
|
||||
throw new Error("Failed to get Widevine challenge");
|
||||
}
|
||||
}
|
||||
|
||||
parseLicense(license_message) {
|
||||
return super.parseLicense(`/remotecdm/widevine/${this.device_name}`, {
|
||||
session_id: this.session_id,
|
||||
license_message: license_message,
|
||||
});
|
||||
}
|
||||
|
||||
getKeys() {
|
||||
super.getKeys(
|
||||
`/remotecdm/widevine/${this.device_name}`,
|
||||
{
|
||||
session_id: this.session_id,
|
||||
},
|
||||
"/ALL"
|
||||
);
|
||||
}
|
||||
|
||||
closeSession() {
|
||||
super.closeSession(`/remotecdm/widevine/${this.device_name}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Utility functions
|
||||
function hexStrToU8(hexString) {
|
||||
return Uint8Array.from(hexString.match(/.{1,2}/g).map((byte) => parseInt(byte, 16)));
|
||||
}
|
||||
|
||||
function u8ToHexStr(bytes) {
|
||||
return bytes.reduce((str, byte) => str + byte.toString(16).padStart(2, "0"), "");
|
||||
}
|
||||
|
||||
function b64ToHexStr(b64) {
|
||||
return [...atob(b64)].map((c) => c.charCodeAt(0).toString(16).padStart(2, "0")).join``;
|
||||
}
|
||||
|
||||
function jsonContainsValue(obj, prefix = DRM_SIGNATURES.WIDEVINE) {
|
||||
if (typeof obj === "string") return obj.startsWith(prefix);
|
||||
if (Array.isArray(obj)) return obj.some((val) => jsonContainsValue(val, prefix));
|
||||
if (typeof obj === "object" && obj !== null) {
|
||||
return Object.values(obj).some((val) => jsonContainsValue(val, prefix));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function jsonReplaceValue(obj, newValue) {
|
||||
if (typeof obj === "string") {
|
||||
return obj.startsWith(DRM_SIGNATURES.WIDEVINE) || obj.startsWith(DRM_SIGNATURES.PLAYREADY)
|
||||
? newValue
|
||||
: obj;
|
||||
}
|
||||
|
||||
if (Array.isArray(obj)) {
|
||||
return obj.map((item) => jsonReplaceValue(item, newValue));
|
||||
}
|
||||
|
||||
if (typeof obj === "object" && obj !== null) {
|
||||
const newObj = {};
|
||||
for (const key in obj) {
|
||||
if (Object.hasOwn(obj, key)) {
|
||||
newObj[key] = jsonReplaceValue(obj[key], newValue);
|
||||
}
|
||||
}
|
||||
return newObj;
|
||||
}
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
function isJson(str) {
|
||||
try {
|
||||
JSON.parse(str);
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function getWidevinePssh(buffer) {
|
||||
const hex = u8ToHexStr(new Uint8Array(buffer));
|
||||
const match = hex.match(/000000(..)?70737368.*/);
|
||||
if (!match) return null;
|
||||
|
||||
const boxHex = match[0];
|
||||
const bytes = hexStrToU8(boxHex);
|
||||
return window.btoa(String.fromCharCode(...bytes));
|
||||
}
|
||||
|
||||
function getPlayReadyPssh(buffer) {
|
||||
const u8 = new Uint8Array(buffer);
|
||||
const systemId = "9a04f07998404286ab92e65be0885f95";
|
||||
const hex = u8ToHexStr(u8);
|
||||
const index = hex.indexOf(systemId);
|
||||
if (index === -1) return null;
|
||||
const psshBoxStart = hex.lastIndexOf("70737368", index);
|
||||
if (psshBoxStart === -1) return null;
|
||||
const lenStart = psshBoxStart - 8;
|
||||
const boxLen = parseInt(hex.substr(lenStart, 8), 16) * 2;
|
||||
const psshHex = hex.substr(lenStart, boxLen);
|
||||
const psshBytes = hexStrToU8(psshHex);
|
||||
return window.btoa(String.fromCharCode(...psshBytes));
|
||||
}
|
||||
|
||||
function getClearkey(response) {
|
||||
let obj = JSON.parse(new TextDecoder("utf-8").decode(response));
|
||||
return obj["keys"].map((o) => ({
|
||||
key_id: b64ToHexStr(o["kid"].replace(/-/g, "+").replace(/_/g, "/")),
|
||||
key: b64ToHexStr(o["k"].replace(/-/g, "+").replace(/_/g, "/")),
|
||||
}));
|
||||
}
|
||||
|
||||
function base64ToUint8Array(base64) {
|
||||
const binaryStr = atob(base64);
|
||||
const len = binaryStr.length;
|
||||
const bytes = new Uint8Array(len);
|
||||
for (let i = 0; i < len; i++) {
|
||||
bytes[i] = binaryStr.charCodeAt(i);
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
function arrayBufferToBase64(uint8array) {
|
||||
let binary = "";
|
||||
const len = uint8array.length;
|
||||
|
||||
for (let i = 0; i < len; i++) {
|
||||
binary += String.fromCharCode(uint8array[i]);
|
||||
}
|
||||
|
||||
return window.btoa(binary);
|
||||
}
|
||||
|
||||
function bufferToBase64(buffer) {
|
||||
const uint8 = buffer instanceof Uint8Array ? buffer : new Uint8Array(buffer);
|
||||
return window.btoa(String.fromCharCode(...uint8));
|
||||
}
|
||||
|
||||
// DRM type detection
|
||||
function isWidevine(base64str) {
|
||||
return base64str.startsWith(DRM_SIGNATURES.WIDEVINE);
|
||||
}
|
||||
function isPlayReady(base64str) {
|
||||
return base64str.startsWith(DRM_SIGNATURES.PLAYREADY);
|
||||
}
|
||||
function isServiceCertificate(base64str) {
|
||||
return base64str.startsWith(DRM_SIGNATURES.SERVICE_CERT);
|
||||
}
|
||||
|
||||
function postDRMTypeAndPssh(type, pssh) {
|
||||
window.postMessage({ type: "__DRM_TYPE__", data: type }, "*");
|
||||
window.postMessage({ type: "__PSSH_DATA__", data: pssh }, "*");
|
||||
}
|
||||
|
||||
function createAndOpenRemoteCDM(type, deviceInfo, pssh) {
|
||||
let cdm;
|
||||
if (type === "Widevine") {
|
||||
const { device_type, system_id, security_level, host, secret, device_name } = deviceInfo;
|
||||
cdm = new remoteWidevineCDM(
|
||||
device_type,
|
||||
system_id,
|
||||
security_level,
|
||||
host,
|
||||
secret,
|
||||
device_name
|
||||
);
|
||||
cdm.openSession();
|
||||
cdm.getChallenge(pssh);
|
||||
} else if (type === "PlayReady") {
|
||||
const { security_level, host, secret, device_name } = deviceInfo;
|
||||
cdm = new remotePlayReadyCDM(security_level, host, secret, device_name);
|
||||
cdm.openSession();
|
||||
cdm.getChallenge(pssh);
|
||||
}
|
||||
return cdm;
|
||||
}
|
||||
|
||||
function ensureRemoteCDM(type, deviceInfo, pssh) {
|
||||
if (!remoteCDM) {
|
||||
remoteCDM = createAndOpenRemoteCDM(type, deviceInfo, pssh);
|
||||
}
|
||||
}
|
||||
|
||||
function detectAndStorePssh(initData) {
|
||||
const detections = [
|
||||
{
|
||||
type: "PlayReady",
|
||||
getter: getPlayReadyPssh,
|
||||
store: (pssh) => (foundPlayreadyPssh = pssh),
|
||||
},
|
||||
{ type: "Widevine", getter: getWidevinePssh, store: (pssh) => (foundWidevinePssh = pssh) },
|
||||
];
|
||||
|
||||
detections.forEach(({ type, getter, store }) => {
|
||||
const pssh = getter(initData);
|
||||
if (pssh) {
|
||||
logWithPrefix(`[DRM Detected] ${type}`);
|
||||
store(pssh);
|
||||
logWithPrefix(`[${type} PSSH found] ${pssh}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Challenge generator interceptor
|
||||
const originalGenerateRequest = MediaKeySession.prototype.generateRequest;
|
||||
MediaKeySession.prototype.generateRequest = function (initDataType, initData) {
|
||||
const session = this;
|
||||
detectAndStorePssh(initData);
|
||||
|
||||
// Challenge message interceptor
|
||||
if (!remoteListenerMounted) {
|
||||
remoteListenerMounted = true;
|
||||
session.addEventListener("message", function messageInterceptor(event) {
|
||||
event.stopImmediatePropagation();
|
||||
const base64challenge = bufferToBase64(event.message);
|
||||
if (
|
||||
base64challenge === DRM_SIGNATURES.WIDEVINE_INIT &&
|
||||
interceptType !== "DISABLED" &&
|
||||
!serviceCertFound
|
||||
) {
|
||||
remoteCDM = createAndOpenRemoteCDM(
|
||||
"Widevine",
|
||||
widevineDeviceInfo,
|
||||
foundWidevinePssh
|
||||
);
|
||||
}
|
||||
if (
|
||||
!injectionSuccess &&
|
||||
base64challenge !== DRM_SIGNATURES.WIDEVINE_INIT &&
|
||||
interceptType !== "DISABLED"
|
||||
) {
|
||||
if (interceptType === "EME") {
|
||||
injectionSuccess = true;
|
||||
}
|
||||
if (!originalChallenge) {
|
||||
originalChallenge = base64challenge;
|
||||
}
|
||||
if (originalChallenge.startsWith(DRM_SIGNATURES.WIDEVINE)) {
|
||||
postDRMTypeAndPssh("Widevine", foundWidevinePssh);
|
||||
if (interceptType === "EME") {
|
||||
ensureRemoteCDM("Widevine", widevineDeviceInfo, foundWidevinePssh);
|
||||
}
|
||||
}
|
||||
if (!originalChallenge.startsWith(DRM_SIGNATURES.WIDEVINE)) {
|
||||
const buffer = event.message;
|
||||
const decoder = new TextDecoder("utf-16");
|
||||
const decodedText = decoder.decode(buffer);
|
||||
const match = decodedText.match(
|
||||
/<Challenge encoding="base64encoded">([^<]+)<\/Challenge>/
|
||||
);
|
||||
if (match) {
|
||||
postDRMTypeAndPssh("PlayReady", foundPlayreadyPssh);
|
||||
originalChallenge = match[1];
|
||||
if (interceptType === "EME") {
|
||||
ensureRemoteCDM("PlayReady", playreadyDeviceInfo, foundPlayreadyPssh);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (interceptType === "EME" && remoteCDM) {
|
||||
const uint8challenge = base64ToUint8Array(remoteCDM.challenge);
|
||||
const challengeBuffer = uint8challenge.buffer;
|
||||
const syntheticEvent = new MessageEvent("message", {
|
||||
data: event.data,
|
||||
origin: event.origin,
|
||||
lastEventId: event.lastEventId,
|
||||
source: event.source,
|
||||
ports: event.ports,
|
||||
});
|
||||
Object.defineProperty(syntheticEvent, "message", {
|
||||
get: () => challengeBuffer,
|
||||
});
|
||||
logWithPrefix("Intercepted EME Challenge and injected custom one.");
|
||||
session.dispatchEvent(syntheticEvent);
|
||||
}
|
||||
}
|
||||
});
|
||||
logWithPrefix("Message interceptor mounted.");
|
||||
}
|
||||
return originalGenerateRequest.call(session, initDataType, initData);
|
||||
};
|
||||
|
||||
// Message update interceptors
|
||||
const originalUpdate = MediaKeySession.prototype.update;
|
||||
MediaKeySession.prototype.update = function (response) {
|
||||
const base64Response = bufferToBase64(response);
|
||||
if (
|
||||
base64Response.startsWith(DRM_SIGNATURES.SERVICE_CERT) &&
|
||||
foundWidevinePssh &&
|
||||
remoteCDM &&
|
||||
!serviceCertFound
|
||||
) {
|
||||
remoteCDM.setServiceCertificate(base64Response);
|
||||
if (interceptType === "EME" && !remoteCDM.challenge) {
|
||||
remoteCDM.getChallenge(foundWidevinePssh);
|
||||
}
|
||||
window.postMessage({ type: "__DRM_TYPE__", data: "Widevine" }, "*");
|
||||
window.postMessage({ type: "__PSSH_DATA__", data: foundWidevinePssh }, "*");
|
||||
serviceCertFound = true;
|
||||
}
|
||||
if (
|
||||
!base64Response.startsWith(DRM_SIGNATURES.SERVICE_CERT) &&
|
||||
(foundWidevinePssh || foundPlayreadyPssh) &&
|
||||
!keysRetrieved
|
||||
) {
|
||||
if (licenseResponseCounter === 1 || foundChallengeInBody) {
|
||||
remoteCDM.parseLicense(base64Response);
|
||||
remoteCDM.getKeys();
|
||||
remoteCDM.closeSession();
|
||||
keysRetrieved = true;
|
||||
window.postMessage({ type: "__KEYS_DATA__", data: remoteCDM.keys }, "*");
|
||||
}
|
||||
licenseResponseCounter++;
|
||||
}
|
||||
const updatePromise = originalUpdate.call(this, response);
|
||||
if (!foundPlayreadyPssh && !foundWidevinePssh) {
|
||||
updatePromise
|
||||
.then(() => {
|
||||
let clearKeys = getClearkey(response);
|
||||
if (clearKeys && clearKeys.length > 0) {
|
||||
logWithPrefix("[CLEARKEY] ", clearKeys);
|
||||
const drmType = {
|
||||
type: "__DRM_TYPE__",
|
||||
data: "ClearKey",
|
||||
};
|
||||
window.postMessage(drmType, "*");
|
||||
const keysData = {
|
||||
type: "__KEYS_DATA__",
|
||||
data: clearKeys,
|
||||
};
|
||||
window.postMessage(keysData, "*");
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
logWithPrefix("[CLEARKEY] Not found");
|
||||
});
|
||||
}
|
||||
|
||||
return updatePromise;
|
||||
};
|
||||
|
||||
// Helpers
|
||||
function detectDRMChallenge(body) {
|
||||
// Handles ArrayBuffer, Uint8Array, string, and JSON string
|
||||
// Returns: { type: "Widevine"|"PlayReady"|null, base64: string|null, bodyType: "buffer"|"string"|"json"|null }
|
||||
if (body instanceof ArrayBuffer || body instanceof Uint8Array) {
|
||||
const buffer = body instanceof Uint8Array ? body : new Uint8Array(body);
|
||||
const base64Body = window.btoa(String.fromCharCode(...buffer));
|
||||
if (base64Body.startsWith(DRM_SIGNATURES.WIDEVINE)) {
|
||||
return { type: "Widevine", base64: base64Body, bodyType: "buffer" };
|
||||
}
|
||||
if (base64Body.startsWith(DRM_SIGNATURES.PLAYREADY)) {
|
||||
return { type: "PlayReady", base64: base64Body, bodyType: "buffer" };
|
||||
}
|
||||
} else if (typeof body === "string" && !isJson(body)) {
|
||||
const base64EncodedBody = btoa(body);
|
||||
if (base64EncodedBody.startsWith(DRM_SIGNATURES.WIDEVINE)) {
|
||||
return { type: "Widevine", base64: base64EncodedBody, bodyType: "string" };
|
||||
}
|
||||
if (base64EncodedBody.startsWith(DRM_SIGNATURES.PLAYREADY)) {
|
||||
return { type: "PlayReady", base64: base64EncodedBody, bodyType: "string" };
|
||||
}
|
||||
} else if (typeof body === "string" && isJson(body)) {
|
||||
const jsonBody = JSON.parse(body);
|
||||
if (jsonContainsValue(jsonBody, DRM_SIGNATURES.WIDEVINE)) {
|
||||
return { type: "Widevine", base64: null, bodyType: "json" };
|
||||
}
|
||||
if (jsonContainsValue(jsonBody, DRM_SIGNATURES.PLAYREADY)) {
|
||||
return { type: "PlayReady", base64: null, bodyType: "json" };
|
||||
}
|
||||
}
|
||||
return { type: null, base64: null, bodyType: null };
|
||||
}
|
||||
|
||||
function handleLicenseMode({
|
||||
drmInfo,
|
||||
body,
|
||||
setBody, // function to set the new body (for fetch: (b) => config.body = b, for XHR: (b) => originalSend.call(this, b))
|
||||
urlOrResource,
|
||||
getWidevinePssh,
|
||||
getPlayreadyPssh,
|
||||
widevineDeviceInfo,
|
||||
playreadyDeviceInfo,
|
||||
}) {
|
||||
foundChallengeInBody = true;
|
||||
window.postMessage({ type: "__LICENSE_URL__", data: urlOrResource }, "*");
|
||||
|
||||
// Create remoteCDM if needed
|
||||
if (!remoteCDM) {
|
||||
if (drmInfo.type === "Widevine") {
|
||||
remoteCDM = createAndOpenRemoteCDM("Widevine", widevineDeviceInfo, getWidevinePssh());
|
||||
}
|
||||
if (drmInfo.type === "PlayReady") {
|
||||
remoteCDM = createAndOpenRemoteCDM(
|
||||
"PlayReady",
|
||||
playreadyDeviceInfo,
|
||||
getPlayreadyPssh()
|
||||
);
|
||||
}
|
||||
}
|
||||
if (remoteCDM && remoteCDM.challenge === null) {
|
||||
remoteCDM.getChallenge(getWidevinePssh());
|
||||
}
|
||||
|
||||
// Inject the new challenge into the request body
|
||||
if (drmInfo.bodyType === "json") {
|
||||
const jsonBody = JSON.parse(body);
|
||||
const injectedBody = jsonReplaceValue(jsonBody, remoteCDM.challenge);
|
||||
setBody(JSON.stringify(injectedBody));
|
||||
} else if (drmInfo.bodyType === "buffer") {
|
||||
setBody(base64ToUint8Array(remoteCDM.challenge));
|
||||
} else {
|
||||
setBody(atob(remoteCDM.challenge));
|
||||
}
|
||||
}
|
||||
|
||||
function handleDRMInterception(drmInfo, body, url, setBodyCallback, continueRequestCallback) {
|
||||
// EME mode: block the request if a DRM challenge is detected
|
||||
if (
|
||||
drmInfo.type &&
|
||||
(!remoteCDM || remoteCDM.challenge === null || drmInfo.base64 !== remoteCDM.challenge) &&
|
||||
interceptType === "EME"
|
||||
) {
|
||||
foundChallengeInBody = true;
|
||||
window.postMessage({ type: "__LICENSE_URL__", data: url }, "*");
|
||||
// Block the request
|
||||
return { shouldBlock: true };
|
||||
}
|
||||
|
||||
// LICENSE mode: replace the challenge in the request
|
||||
if (drmInfo.type && interceptType === "LICENSE" && !foundChallengeInBody) {
|
||||
handleLicenseMode({
|
||||
drmInfo,
|
||||
body,
|
||||
setBody: setBodyCallback,
|
||||
urlOrResource: url,
|
||||
getWidevinePssh: () => foundWidevinePssh,
|
||||
getPlayreadyPssh: () => foundPlayreadyPssh,
|
||||
widevineDeviceInfo,
|
||||
playreadyDeviceInfo,
|
||||
});
|
||||
return { shouldIntercept: true, result: continueRequestCallback() };
|
||||
}
|
||||
|
||||
return { shouldContinue: true };
|
||||
}
|
||||
|
||||
// fetch POST interceptor
|
||||
(function () {
|
||||
const originalFetch = window.fetch;
|
||||
|
||||
window.fetch = async function (resource, config = {}) {
|
||||
const method = (config.method || "GET").toUpperCase();
|
||||
|
||||
if (method === "POST" && config.body) {
|
||||
logWithPrefix("[FETCH] Intercepting POST request to:", resource);
|
||||
const drmInfo = detectDRMChallenge(config.body);
|
||||
|
||||
const result = handleDRMInterception(
|
||||
drmInfo,
|
||||
config.body,
|
||||
resource,
|
||||
(b) => {
|
||||
config.body = b;
|
||||
},
|
||||
() => originalFetch(resource, config)
|
||||
);
|
||||
|
||||
if (result.shouldBlock) return;
|
||||
if (result.shouldIntercept) return result.result;
|
||||
}
|
||||
|
||||
return originalFetch(resource, config);
|
||||
};
|
||||
})();
|
||||
|
||||
// XHR POST interceptor
|
||||
(function () {
|
||||
const originalOpen = XMLHttpRequest.prototype.open;
|
||||
const originalSend = XMLHttpRequest.prototype.send;
|
||||
|
||||
XMLHttpRequest.prototype.open = function (method, url, async, user, password) {
|
||||
this._method = method;
|
||||
this._url = url;
|
||||
return originalOpen.apply(this, arguments);
|
||||
};
|
||||
|
||||
XMLHttpRequest.prototype.send = function (body) {
|
||||
if (this._method && this._method.toUpperCase() === "POST" && body) {
|
||||
logWithPrefix("[XHR] Intercepting POST request to:", this._url);
|
||||
const drmInfo = detectDRMChallenge(body);
|
||||
|
||||
const result = handleDRMInterception(
|
||||
drmInfo,
|
||||
body,
|
||||
this._url,
|
||||
(b) => originalSend.call(this, b),
|
||||
() => {} // XHR doesn't need continuation callback
|
||||
);
|
||||
|
||||
if (result.shouldBlock) return;
|
||||
if (result.shouldIntercept) return result.result;
|
||||
}
|
||||
|
||||
return originalSend.apply(this, arguments);
|
||||
};
|
||||
})();
|
37
src/manifest.json
Normal file
37
src/manifest.json
Normal file
@ -0,0 +1,37 @@
|
||||
{
|
||||
"manifest_version": 3,
|
||||
"name": "CDRM Extension",
|
||||
"version": "2.1.0",
|
||||
"description": "Decrypt DRM protected content",
|
||||
"permissions": ["storage", "activeTab", "contextMenus"],
|
||||
"host_permissions": ["<all_urls>"],
|
||||
"background": {
|
||||
"service_worker": "background.js"
|
||||
},
|
||||
"content_scripts": [
|
||||
{
|
||||
"matches": ["<all_urls>"],
|
||||
"js": ["content.js"],
|
||||
"run_at": "document_start",
|
||||
"all_frames": true
|
||||
}
|
||||
],
|
||||
"web_accessible_resources": [
|
||||
{
|
||||
"resources": ["inject.js"],
|
||||
"matches": ["<all_urls>"]
|
||||
}
|
||||
],
|
||||
"action": {
|
||||
"default_icon": {
|
||||
"16": "icons/icon16.png",
|
||||
"32": "icons/icon32.png",
|
||||
"128": "icons/icon128.png"
|
||||
}
|
||||
},
|
||||
"icons": {
|
||||
"16": "icons/icon16.png",
|
||||
"32": "icons/icon32.png",
|
||||
"128": "icons/icon128.png"
|
||||
}
|
||||
}
|
66
syncVersion.js
Normal file
66
syncVersion.js
Normal file
@ -0,0 +1,66 @@
|
||||
import fs from "fs/promises";
|
||||
import path from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
const updateVersionWithRegex = async (filePath, newVersion) => {
|
||||
try {
|
||||
const content = await fs.readFile(filePath, "utf-8");
|
||||
|
||||
// Regex to match "version": "any.version.number"
|
||||
const versionRegex = /("version"\s*:\s*")([^"]+)(")/;
|
||||
|
||||
if (!versionRegex.test(content)) {
|
||||
console.warn(`⚠️ No version field found in ${filePath}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
const updatedContent = content.replace(versionRegex, `$1${newVersion}$3`);
|
||||
|
||||
if (content !== updatedContent) {
|
||||
await fs.writeFile(filePath, updatedContent);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
} catch (err) {
|
||||
console.error(`❌ Failed to update ${filePath}: ${err.message}`);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const syncVersion = async () => {
|
||||
const rootPkgPath = path.join(__dirname, "package.json");
|
||||
const frontendPkgPath = path.join(__dirname, "frontend", "package.json");
|
||||
const manifestPath = path.join(__dirname, "src", "manifest.json");
|
||||
|
||||
// Read root package.json version
|
||||
const rootPkgRaw = await fs.readFile(rootPkgPath, "utf-8");
|
||||
const rootPkg = JSON.parse(rootPkgRaw);
|
||||
const version = rootPkg.version;
|
||||
|
||||
if (!version) {
|
||||
console.warn("⚠️ No version field found in root package.json, skipping sync.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Update frontend/package.json using regex
|
||||
const frontendUpdated = await updateVersionWithRegex(frontendPkgPath, version);
|
||||
if (frontendUpdated) {
|
||||
console.log(`🔄 Updated frontend/package.json version to ${version}`);
|
||||
} else {
|
||||
console.log("ℹ️ frontend/package.json not found or no changes needed.");
|
||||
}
|
||||
|
||||
// Update src/manifest.json using regex
|
||||
const manifestUpdated = await updateVersionWithRegex(manifestPath, version);
|
||||
if (manifestUpdated) {
|
||||
console.log(`🔄 Updated src/manifest.json version to ${version}`);
|
||||
} else {
|
||||
console.log("ℹ️ src/manifest.json not found or no changes needed.");
|
||||
}
|
||||
};
|
||||
|
||||
export default syncVersion;
|
Loading…
x
Reference in New Issue
Block a user