forked from tpd94/CDRM-Extension
Compare commits
No commits in common. "main" and "main" have entirely different histories.
30
.gitignore
vendored
30
.gitignore
vendored
@ -1,30 +0,0 @@
|
|||||||
# 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
|
|
@ -1,3 +0,0 @@
|
|||||||
react/
|
|
||||||
frontend/dist/
|
|
||||||
frontend/src/assets/
|
|
@ -1,8 +0,0 @@
|
|||||||
{
|
|
||||||
"trailingComma": "es5",
|
|
||||||
"tabWidth": 4,
|
|
||||||
"semi": true,
|
|
||||||
"singleQuote": false,
|
|
||||||
"useTabs": false,
|
|
||||||
"printWidth": 100
|
|
||||||
}
|
|
52
README.md
52
README.md
@ -1,52 +0,0 @@
|
|||||||
# 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.
|
|
84
background.js
Normal file
84
background.js
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
// 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
119
buildext.js
@ -1,119 +0,0 @@
|
|||||||
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
Normal file
67
content.js
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
// 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
Normal file
1
frontend/dist/assets/index-UaipKa9p.css
vendored
Normal file
File diff suppressed because one or more lines are too long
13
frontend/dist/index.html
vendored
Normal file
13
frontend/dist/index.html
vendored
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<!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,30 +1,33 @@
|
|||||||
import js from "@eslint/js";
|
import js from '@eslint/js'
|
||||||
import globals from "globals";
|
import globals from 'globals'
|
||||||
import reactHooks from "eslint-plugin-react-hooks";
|
import reactHooks from 'eslint-plugin-react-hooks'
|
||||||
import reactRefresh from "eslint-plugin-react-refresh";
|
import reactRefresh from 'eslint-plugin-react-refresh'
|
||||||
|
|
||||||
export default [
|
export default [
|
||||||
{ ignores: ["dist"] },
|
{ ignores: ['dist'] },
|
||||||
{
|
{
|
||||||
files: ["**/*.{js,jsx}"],
|
files: ['**/*.{js,jsx}'],
|
||||||
languageOptions: {
|
languageOptions: {
|
||||||
ecmaVersion: 2020,
|
ecmaVersion: 2020,
|
||||||
globals: globals.browser,
|
globals: globals.browser,
|
||||||
parserOptions: {
|
parserOptions: {
|
||||||
ecmaVersion: "latest",
|
ecmaVersion: 'latest',
|
||||||
ecmaFeatures: { jsx: true },
|
ecmaFeatures: { jsx: true },
|
||||||
sourceType: "module",
|
sourceType: 'module',
|
||||||
},
|
},
|
||||||
},
|
|
||||||
plugins: {
|
|
||||||
"react-hooks": reactHooks,
|
|
||||||
"react-refresh": reactRefresh,
|
|
||||||
},
|
|
||||||
rules: {
|
|
||||||
...js.configs.recommended.rules,
|
|
||||||
...reactHooks.configs.recommended.rules,
|
|
||||||
"no-unused-vars": ["error", { varsIgnorePattern: "^[A-Z_]" }],
|
|
||||||
"react-refresh/only-export-components": ["warn", { allowConstantExport: true }],
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
];
|
plugins: {
|
||||||
|
'react-hooks': reactHooks,
|
||||||
|
'react-refresh': reactRefresh,
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
...js.configs.recommended.rules,
|
||||||
|
...reactHooks.configs.recommended.rules,
|
||||||
|
'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }],
|
||||||
|
'react-refresh/only-export-components': [
|
||||||
|
'warn',
|
||||||
|
{ allowConstantExport: true },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>CDRM Decryption Extension v%APPVERSION%</title>
|
<title>CDRM Decryption Extension</title>
|
||||||
</head>
|
</head>
|
||||||
<body class="min-w-full min-h-full w-full h-full">
|
<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>
|
<div class="min-w-full min-h-full w-full h-full" id="root"></div>
|
||||||
<script type="module" src="/src/main.jsx"></script>
|
<script type="module" src="/src/main.jsx"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
7050
frontend/package-lock.json
generated
7050
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -1,31 +1,30 @@
|
|||||||
{
|
{
|
||||||
"name": "frontend",
|
"name": "frontend",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "2.1.0",
|
"version": "0.0.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"lint": "eslint .",
|
"lint": "eslint .",
|
||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tailwindcss/vite": "^4.1.11",
|
"@tailwindcss/vite": "^4.1.7",
|
||||||
"react": "^19.1.0",
|
"react": "^19.1.0",
|
||||||
"react-dom": "^19.1.0",
|
"react-dom": "^19.1.0",
|
||||||
"react-router-dom": "^7.7.0",
|
"react-router-dom": "^7.6.1",
|
||||||
"tailwindcss": "^4.1.11"
|
"tailwindcss": "^4.1.7"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.31.0",
|
"@eslint/js": "^9.25.0",
|
||||||
"@types/react": "^19.1.8",
|
"@types/react": "^19.1.2",
|
||||||
"@types/react-dom": "^19.1.6",
|
"@types/react-dom": "^19.1.2",
|
||||||
"@vitejs/plugin-react": "^4.7.0",
|
"@vitejs/plugin-react": "^4.4.1",
|
||||||
"@vitejs/plugin-react-swc": "^3.11.0",
|
"eslint": "^9.25.0",
|
||||||
"eslint": "^9.31.0",
|
"eslint-plugin-react-hooks": "^5.2.0",
|
||||||
"eslint-plugin-react-hooks": "^5.2.0",
|
"eslint-plugin-react-refresh": "^0.4.19",
|
||||||
"eslint-plugin-react-refresh": "^0.4.20",
|
"globals": "^16.0.0",
|
||||||
"globals": "^16.3.0",
|
"vite": "^6.3.5"
|
||||||
"vite": "^7.0.5"
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,72 +1,77 @@
|
|||||||
import { useState, useEffect } from "react";
|
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 TopNav from "./components/topnav";
|
||||||
import SideNav from "./components/sidenav";
|
import SideNav from "./components/sidenav";
|
||||||
import Results from "./components/results";
|
import Results from "./components/results";
|
||||||
import Settings from "./components/settings";
|
import Settings from "./components/settings";
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const [isSideNavOpen, setIsSideNavOpen] = useState(false);
|
const [isSideNavOpen, setIsSideNavOpen] = useState(false);
|
||||||
const [validConfig, setValidConfig] = useState(null); // null = loading
|
const [validConfig, setValidConfig] = useState(null); // null = loading
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
chrome.storage.local.get("valid_config", (result) => {
|
chrome.storage.local.get("valid_config", (result) => {
|
||||||
if (chrome.runtime.lastError) {
|
if (chrome.runtime.lastError) {
|
||||||
console.error("Error reading valid_config:", chrome.runtime.lastError);
|
console.error("Error reading valid_config:", chrome.runtime.lastError);
|
||||||
setValidConfig(false); // fallback
|
setValidConfig(false); // fallback
|
||||||
} else {
|
} else {
|
||||||
setValidConfig(result.valid_config === true);
|
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 (
|
return (
|
||||||
<Router>
|
<div className="flex items-center justify-center h-screen bg-black text-white">
|
||||||
<div className="min-w-full min-h-full w-full h-full flex flex-grow bg-black/95 flex-col relative">
|
Loading...
|
||||||
<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">
|
</div>
|
||||||
<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;
|
export default App;
|
||||||
|
@ -1,250 +1,149 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
|
|
||||||
function Results() {
|
function Results() {
|
||||||
const [drmType, setDrmType] = useState("");
|
const [drmType, setDrmType] = useState("");
|
||||||
const [pssh, setPssh] = useState("");
|
const [pssh, setPssh] = useState("");
|
||||||
const [licenseUrl, setLicenseUrl] = useState("");
|
const [licenseUrl, setLicenseUrl] = useState("");
|
||||||
const [keys, setKeys] = useState([]);
|
const [keys, setKeys] = useState([]);
|
||||||
const [manifestUrl, setManifestUrl] = useState("");
|
|
||||||
const [currentTabUrl, setCurrentTabUrl] = useState("");
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
chrome.storage.local.get(
|
chrome.storage.local.get(
|
||||||
[
|
[
|
||||||
"drmType",
|
"drmType",
|
||||||
"latestPSSH",
|
"latestPSSH",
|
||||||
"latestLicenseRequest",
|
"latestLicenseRequest",
|
||||||
"latestKeys",
|
"latestKeys",
|
||||||
"licenseURL",
|
"licenseURL",
|
||||||
"manifestURL",
|
],
|
||||||
],
|
(result) => {
|
||||||
(result) => {
|
if (result.drmType) setDrmType(result.drmType);
|
||||||
if (result.drmType) setDrmType(result.drmType || "");
|
if (result.latestPSSH) setPssh(result.latestPSSH);
|
||||||
if (result.latestPSSH) setPssh(result.latestPSSH || "");
|
if (result.licenseURL) setLicenseUrl(result.licenseURL);
|
||||||
if (result.licenseURL) setLicenseUrl(result.licenseURL || "");
|
if (result.latestKeys) {
|
||||||
if (result.manifestURL) setManifestUrl(result.manifestURL || "");
|
try {
|
||||||
if (result.latestKeys) {
|
const parsed = Array.isArray(result.latestKeys)
|
||||||
try {
|
? result.latestKeys
|
||||||
const parsed = Array.isArray(result.latestKeys)
|
: JSON.parse(result.latestKeys);
|
||||||
? result.latestKeys
|
setKeys(parsed);
|
||||||
: JSON.parse(result.latestKeys);
|
} catch (e) {
|
||||||
setKeys(parsed);
|
console.error("Failed to parse keys:", e);
|
||||||
} catch (e) {
|
setKeys([]);
|
||||||
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)
|
||||||
);
|
);
|
||||||
|
|
||||||
// Get current tab URL when component mounts
|
if (activeTab?.id) {
|
||||||
chrome.windows.getAll({ populate: true, windowTypes: ["normal"] }, (windows) => {
|
chrome.tabs.reload(activeTab.id, () => {
|
||||||
if (windows && windows.length > 0) {
|
if (chrome.runtime.lastError) {
|
||||||
const lastFocusedWindow = windows.find((w) => w.focused) || windows[0];
|
console.error("Failed to reload tab:", chrome.runtime.lastError);
|
||||||
if (lastFocusedWindow) {
|
|
||||||
const activeTab = lastFocusedWindow.tabs.find(
|
|
||||||
(tab) => tab.active && tab.url && /^https?:\/\//.test(tab.url)
|
|
||||||
);
|
|
||||||
if (activeTab?.url) {
|
|
||||||
setCurrentTabUrl(activeTab.url);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
const handleChange = (changes, area) => {
|
console.warn("No active tab found in the last focused normal window");
|
||||||
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>
|
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
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>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Results;
|
export default Results;
|
||||||
|
@ -2,143 +2,149 @@ import { useState, useEffect } from "react";
|
|||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
function Settings({ onConfigSaved }) {
|
function Settings({ onConfigSaved }) {
|
||||||
const [instanceUrl, setInstanceUrl] = useState("");
|
const [instanceUrl, setInstanceUrl] = useState("");
|
||||||
const [storedUrl, setStoredUrl] = useState(null);
|
const [storedUrl, setStoredUrl] = useState(null);
|
||||||
const [message, setMessage] = useState(null);
|
const [message, setMessage] = useState(null);
|
||||||
const [messageType, setMessageType] = useState(null);
|
const [messageType, setMessageType] = useState(null);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
chrome.storage.local.get("cdrm_instance", (result) => {
|
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,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
() => {
|
||||||
if (chrome.runtime.lastError) {
|
if (chrome.runtime.lastError) {
|
||||||
console.error("Error fetching CDRM instance:", chrome.runtime.lastError);
|
console.error(
|
||||||
} else if (result.cdrm_instance) {
|
"Error saving to chrome.storage:",
|
||||||
setStoredUrl(result.cdrm_instance);
|
chrome.runtime.lastError
|
||||||
}
|
);
|
||||||
});
|
setMessage("Error saving configuration.");
|
||||||
}, []);
|
setMessageType("error");
|
||||||
|
|
||||||
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,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
() => {
|
|
||||||
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 {
|
} else {
|
||||||
throw new Error("Invalid response from endpoint.");
|
console.log("Configuration saved.");
|
||||||
|
setStoredUrl(trimmedUrl);
|
||||||
|
setInstanceUrl("");
|
||||||
|
if (onConfigSaved) onConfigSaved();
|
||||||
|
navigate("/results"); // Automatically redirect after success
|
||||||
}
|
}
|
||||||
} catch (err) {
|
}
|
||||||
console.error("Connection error:", err);
|
);
|
||||||
setMessage("Invalid endpoint or device info could not be retrieved.");
|
} else {
|
||||||
setMessageType("error");
|
throw new Error("Invalid response from endpoint.");
|
||||||
} finally {
|
}
|
||||||
setLoading(false);
|
} 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/)"
|
||||||
}
|
}
|
||||||
};
|
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>
|
||||||
|
|
||||||
return (
|
{message && (
|
||||||
<div className="w-full h-full overflow-y-auto overflow-x-auto flex flex-col p-4">
|
<p
|
||||||
{storedUrl && (
|
className={`mt-2 text-sm text-center ${
|
||||||
<p className="text-gray-300 mb-2">
|
messageType === "success" ? "text-green-400" : "text-red-400"
|
||||||
Current instance: <span className="text-white font-semibold">{storedUrl}</span>
|
}`}
|
||||||
</p>
|
>
|
||||||
)}
|
{message}
|
||||||
|
</p>
|
||||||
<p className="mt-3 text-white">New instance URL:</p>
|
)}
|
||||||
<input
|
</div>
|
||||||
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;
|
export default Settings;
|
||||||
|
@ -4,45 +4,48 @@ import settingsIcon from "../assets/settings.svg";
|
|||||||
import closeIcon from "../assets/close.svg";
|
import closeIcon from "../assets/close.svg";
|
||||||
|
|
||||||
function SideNav({ onClose }) {
|
function SideNav({ onClose }) {
|
||||||
return (
|
return (
|
||||||
<div className="w-full h-full overflow-y-auto overflow-x-auto flex flex-col bg-black">
|
<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">
|
<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">
|
<button
|
||||||
<img src={closeIcon} alt="Close" className="h-full" />
|
onClick={onClose}
|
||||||
</button>
|
className="h-full ml-auto p-3 hover:cursor-pointer"
|
||||||
</div>
|
>
|
||||||
|
<img src={closeIcon} alt="Close" className="h-full" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="w-full h-16 flex items-center justify-center mt-2">
|
<div className="w-full h-16 flex items-center justify-center mt-2">
|
||||||
<NavLink
|
<NavLink
|
||||||
to="/results"
|
to="/results"
|
||||||
onClick={onClose}
|
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"
|
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
|
<img
|
||||||
src={homeIcon}
|
src={homeIcon}
|
||||||
alt="Home"
|
alt="Home"
|
||||||
className="h-full w-16 p-2 flex items-center cursor-pointer"
|
className="h-full w-16 p-2 flex items-center cursor-pointer"
|
||||||
/>
|
/>
|
||||||
Home
|
Home
|
||||||
</NavLink>
|
</NavLink>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="w-full h-16 flex items-center justify-center mt-2">
|
<div className="w-full h-16 flex items-center justify-center mt-2">
|
||||||
<NavLink
|
<NavLink
|
||||||
to="/settings"
|
to="/settings"
|
||||||
onClick={onClose}
|
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"
|
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
|
<img
|
||||||
src={settingsIcon}
|
src={settingsIcon}
|
||||||
alt="Settings"
|
alt="Settings"
|
||||||
className="h-full w-16 p-2 flex items-center cursor-pointer"
|
className="h-full w-16 p-2 flex items-center cursor-pointer"
|
||||||
/>
|
/>
|
||||||
Settings
|
Settings
|
||||||
</NavLink>
|
</NavLink>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default SideNav;
|
export default SideNav;
|
||||||
|
@ -2,81 +2,84 @@ import { useEffect, useState } from "react";
|
|||||||
import hamburgerIcon from "../assets/hamburger.svg";
|
import hamburgerIcon from "../assets/hamburger.svg";
|
||||||
|
|
||||||
function TopNav({ onMenuClick }) {
|
function TopNav({ onMenuClick }) {
|
||||||
const [injectionType, setInjectionType] = useState("LICENSE");
|
const [injectionType, setInjectionType] = useState("LICENSE");
|
||||||
const [drmOverride, setDrmOverride] = useState("DISABLED");
|
const [drmOverride, setDrmOverride] = useState("DISABLED");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
chrome.storage.local.get(["injection_type", "drm_override"], (result) => {
|
chrome.storage.local.get(["injection_type", "drm_override"], (result) => {
|
||||||
if (result.injection_type !== undefined) {
|
if (result.injection_type !== undefined) {
|
||||||
setInjectionType(result.injection_type);
|
setInjectionType(result.injection_type);
|
||||||
}
|
}
|
||||||
if (result.drm_override !== undefined) {
|
if (result.drm_override !== undefined) {
|
||||||
setDrmOverride(result.drm_override);
|
setDrmOverride(result.drm_override);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleInjectionTypeChange = (type) => {
|
const handleInjectionTypeChange = (type) => {
|
||||||
chrome.storage.local.set({ injection_type: type }, () => {
|
chrome.storage.local.set({ injection_type: type }, () => {
|
||||||
if (chrome.runtime.lastError) {
|
if (chrome.runtime.lastError) {
|
||||||
console.error("Error updating injection_type:", chrome.runtime.lastError);
|
console.error(
|
||||||
} else {
|
"Error updating injection_type:",
|
||||||
setInjectionType(type);
|
chrome.runtime.lastError
|
||||||
console.log(`Injection type updated to ${type}`);
|
);
|
||||||
}
|
} else {
|
||||||
});
|
setInjectionType(type);
|
||||||
};
|
console.log(`Injection type updated to ${type}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const handleDrmOverrideChange = (type) => {
|
const handleDrmOverrideChange = (type) => {
|
||||||
chrome.storage.local.set({ drm_override: type }, () => {
|
chrome.storage.local.set({ drm_override: type }, () => {
|
||||||
if (chrome.runtime.lastError) {
|
if (chrome.runtime.lastError) {
|
||||||
console.error("Error updating drm_override:", chrome.runtime.lastError);
|
console.error("Error updating drm_override:", chrome.runtime.lastError);
|
||||||
} else {
|
} else {
|
||||||
setDrmOverride(type);
|
setDrmOverride(type);
|
||||||
console.log(`DRM Override updated to ${type}`);
|
console.log(`DRM Override updated to ${type}`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full h-full flex flex-row overflow-x-hidden">
|
<div className="w-full h-full flex flex-row overflow-x-hidden">
|
||||||
<img
|
<img
|
||||||
src={hamburgerIcon}
|
src={hamburgerIcon}
|
||||||
alt="Menu"
|
alt="Menu"
|
||||||
className="h-full w-16 p-2 flex items-center cursor-pointer"
|
className="h-full w-16 p-2 flex items-center cursor-pointer"
|
||||||
onClick={onMenuClick}
|
onClick={onMenuClick}
|
||||||
/>
|
/>
|
||||||
<div className="flex flex-row h-full justify-center items-center ml-auto mr-2">
|
<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">
|
<p className="text-white text-lg p-2 mr-2 border-r-2 border-r-white text-nowrap">
|
||||||
Injection Type:
|
Injection Type:
|
||||||
</p>
|
</p>
|
||||||
<button
|
<button
|
||||||
onClick={() => handleInjectionTypeChange("LICENSE")}
|
onClick={() => handleInjectionTypeChange("LICENSE")}
|
||||||
className={`text-white text-lg p-2 rounded-md m-1 cursor-pointer ${
|
className={`text-white text-lg p-2 rounded-md m-1 cursor-pointer ${
|
||||||
injectionType === "LICENSE" ? "bg-sky-500/70" : "bg-black"
|
injectionType === "LICENSE" ? "bg-sky-500/70" : "bg-black"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
License
|
License
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => handleInjectionTypeChange("EME")}
|
onClick={() => handleInjectionTypeChange("EME")}
|
||||||
className={`text-white text-lg p-2 rounded-md m-1 cursor-pointer ${
|
className={`text-white text-lg p-2 rounded-md m-1 cursor-pointer ${
|
||||||
injectionType === "EME" ? "bg-green-500/70" : "bg-black"
|
injectionType === "EME" ? "bg-green-500/70" : "bg-black"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
EME
|
EME
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => handleInjectionTypeChange("DISABLED")}
|
onClick={() => handleInjectionTypeChange("DISABLED")}
|
||||||
className={`text-white text-lg p-2 rounded-md m-1 cursor-pointer ${
|
className={`text-white text-lg p-2 rounded-md m-1 cursor-pointer ${
|
||||||
injectionType === "DISABLED" ? "bg-red-500/70" : "bg-black"
|
injectionType === "DISABLED" ? "bg-red-500/70" : "bg-black"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
Disabled
|
Disabled
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default TopNav;
|
export default TopNav;
|
||||||
|
@ -1,10 +1,8 @@
|
|||||||
@import "tailwindcss";
|
@import "tailwindcss";
|
||||||
|
|
||||||
html,
|
html, body, #root {
|
||||||
body,
|
height: 100%;
|
||||||
#root {
|
width: 100%;
|
||||||
height: 100%;
|
margin: 0;
|
||||||
width: 100%;
|
padding: 0;
|
||||||
margin: 0;
|
}
|
||||||
padding: 0;
|
|
||||||
}
|
|
@ -1,10 +1,10 @@
|
|||||||
import { StrictMode } from "react";
|
import { StrictMode } from 'react'
|
||||||
import { createRoot } from "react-dom/client";
|
import { createRoot } from 'react-dom/client'
|
||||||
import "./index.css";
|
import './index.css'
|
||||||
import App from "./App.jsx";
|
import App from './App.jsx'
|
||||||
|
|
||||||
createRoot(document.getElementById("root")).render(
|
createRoot(document.getElementById('root')).render(
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
<App />
|
<App />
|
||||||
</StrictMode>
|
</StrictMode>,
|
||||||
);
|
)
|
||||||
|
@ -1,21 +1,9 @@
|
|||||||
import tailwindcss from "@tailwindcss/vite";
|
import { defineConfig } from 'vite'
|
||||||
import react from "@vitejs/plugin-react-swc";
|
import react from '@vitejs/plugin-react'
|
||||||
import { readFileSync } from "fs";
|
import tailwindcss from '@tailwindcss/vite'
|
||||||
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/
|
// https://vite.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
base: "./",
|
base: './',
|
||||||
plugins: [react(), tailwindcss(), replaceVersionPlugin()],
|
plugins: [react(), tailwindcss()],
|
||||||
});
|
})
|
||||||
|
813
inject.js
Normal file
813
inject.js
Normal file
@ -0,0 +1,813 @@
|
|||||||
|
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);
|
||||||
|
};
|
||||||
|
})();
|
41
manifest.json
Normal file
41
manifest.json
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
{
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
}
|
@ -1,89 +0,0 @@
|
|||||||
// 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.");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
@ -1,96 +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,
|
|
||||||
},
|
|
||||||
"*"
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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
1143
mv2/inject.js
File diff suppressed because it is too large
Load Diff
@ -1,41 +0,0 @@
|
|||||||
{
|
|
||||||
"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
136
package-lock.json
generated
@ -1,136 +0,0 @@
|
|||||||
{
|
|
||||||
"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
24
package.json
@ -1,24 +0,0 @@
|
|||||||
{
|
|
||||||
"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"
|
|
||||||
}
|
|
||||||
}
|
|
1
react/assets/index-UaipKa9p.css
Normal file
1
react/assets/index-UaipKa9p.css
Normal file
File diff suppressed because one or more lines are too long
52
react/assets/index-ydPQKJSy.js
Normal file
52
react/assets/index-ydPQKJSy.js
Normal file
File diff suppressed because one or more lines are too long
13
react/index.html
Normal file
13
react/index.html
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<!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,88 +0,0 @@
|
|||||||
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.");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
@ -1,96 +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,
|
|
||||||
},
|
|
||||||
"*"
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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
925
src/inject.js
@ -1,925 +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;
|
|
||||||
|
|
||||||
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);
|
|
||||||
};
|
|
||||||
})();
|
|
@ -1,37 +0,0 @@
|
|||||||
{
|
|
||||||
"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"
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,66 +0,0 @@
|
|||||||
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