mirror of
https://github.com/DevLARLEY/WidevineProxy2.git
synced 2026-04-02 02:28:34 +00:00
Remote CDM support
This commit is contained in:
20
README.md
20
README.md
@@ -11,6 +11,7 @@ Modifies the challenge before it reaches the web player and obtains the decrypti
|
||||
|
||||
## Widevine Devices
|
||||
This addon requires a Widevine Device file to work, which is not provided by this project.
|
||||
+ Use an existing Remote CDM like [this one](https://remote-cdm.cdrm-project.com/remote_cdm)
|
||||
+ Follow [this](https://forum.videohelp.com/threads/408031) guide if you want to dump your own device.
|
||||
+ Ready-to-use Widevine Devices can be found on the [VideoHelp forum](https://forum.videohelp.com/forums/48).
|
||||
|
||||
@@ -36,9 +37,21 @@ This addon requires a Widevine Device file to work, which is not provided by thi
|
||||
3. Click `Load Temporary Add-on...` and select the downloaded file
|
||||
|
||||
## Setup
|
||||
+ Once installed, open the extension, click `Choose File` and select your Widevine Device file.
|
||||
### Widevine Device
|
||||
If you only have a `device_client_id_blob` and `device_private_key`, run this command to create a .wvd file:
|
||||
```
|
||||
pywidevine create-device -k device_private_key -c device_client_id_blob -t "ANDROID" -l 3
|
||||
```
|
||||
Now, open the extension, click `Choose File` and select your Widevine Device file.
|
||||
|
||||
### Remote CDM
|
||||
If you don't already have a `remote.json` file, open the API URL in the browser (if provided) and save the response as `remote.json`. \ n
|
||||
Now, open the extension, click `Choose remote.json` and select the json file provided by your API.
|
||||
|
||||
|
||||
+ Select the type of device you're using in the top right hand corner
|
||||
+ The files are saved in the extension's `chrome.storage.sync` storage and will be synchronized across any browsers into which the user is signed in with their Google account.
|
||||
+ Due to the sync storage limit of 100KB, the maximum number of installable devices at the same time is ~30.
|
||||
+ The maximum number of Widevine devices is ~25 **OR** ~200 Remote CDMs
|
||||
+ Check `Enabled` to activate the message interception and you're done.
|
||||
|
||||
## Usage
|
||||
@@ -62,8 +75,7 @@ No, the extension works with Manifest V3, which does not have access to the webR
|
||||
This automatically means that the license server is blocking your CDM and that you either need a CDM from a physical device, a ChromeCDM, or an L1 Android CDM. Don't ask where you can get these
|
||||
|
||||
## Issues
|
||||
+ DRM playback won't work when the extension is disabled and EME Logger is active. This is caused by my fix for dealing with EME Logger interference (solutions are welcome).
|
||||
+ Having the extension installed causes the UnRAID dashboard not to load
|
||||
+ DRM playback won't work when the extension is disabled and EME Logger is active. This is caused by my fix for dealing with EME Logger interference (solutions are welcome).
|
||||
|
||||
## Demo
|
||||
[Widevineproxy2.webm](https://github.com/user-attachments/assets/8f51cee3-50e2-4aa4-b244-afa2d0b2987e)
|
||||
|
||||
337
background.js
337
background.js
@@ -9,15 +9,220 @@ import {
|
||||
uint8ArrayToBase64,
|
||||
uint8ArrayToHex,
|
||||
SettingsManager,
|
||||
AsyncLocalStorage
|
||||
AsyncLocalStorage, RemoteCDMManager
|
||||
} from "./util.js";
|
||||
import { WidevineDevice } from "./device.js";
|
||||
import { RemoteCdm } from "./remote_cdm.js";
|
||||
|
||||
const { LicenseType, SignedMessage, LicenseRequest, License } = protobuf.roots.default.license_protocol;
|
||||
|
||||
let sessions = new Map();
|
||||
let logs = [];
|
||||
|
||||
async function parseClearKey(body, sendResponse, tab_url) {
|
||||
const clearkey = JSON.parse(atob(body));
|
||||
|
||||
const formatted_keys = clearkey["keys"].map(key => ({
|
||||
...key,
|
||||
kid: uint8ArrayToHex(base64toUint8Array(key.kid.replace(/-/g, "+").replace(/_/g, "/") + "==")),
|
||||
k: uint8ArrayToHex(base64toUint8Array(key.k.replace(/-/g, "+").replace(/_/g, "/") + "=="))
|
||||
}));
|
||||
const pssh_data = btoa(JSON.stringify({kids: clearkey["keys"].map(key => key.k)}));
|
||||
|
||||
if (logs.filter(log => log.pssh_data === pssh_data).length > 0) {
|
||||
console.log("[WidevineProxy2]", `KEYS_ALREADY_RETRIEVED: ${pssh_data}`);
|
||||
sendResponse();
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("[WidevineProxy2]", "CLEARKEY KEYS", formatted_keys);
|
||||
const log = {
|
||||
type: "CLEARKEY",
|
||||
pssh_data: pssh_data,
|
||||
keys: formatted_keys,
|
||||
url: tab_url,
|
||||
timestamp: Math.floor(Date.now() / 1000)
|
||||
}
|
||||
logs.push(log);
|
||||
|
||||
await AsyncLocalStorage.setStorage({[pssh_data]: log});
|
||||
sendResponse();
|
||||
}
|
||||
|
||||
async function generateChallenge(body, sendResponse) {
|
||||
const signed_message = SignedMessage.decode(base64toUint8Array(body));
|
||||
const license_request = LicenseRequest.decode(signed_message.msg);
|
||||
const pssh_data = license_request.contentId.widevinePsshData.psshData[0];
|
||||
|
||||
if (!pssh_data) {
|
||||
console.log("[WidevineProxy2]", "NO_PSSH_DATA_IN_CHALLENGE");
|
||||
sendResponse(body);
|
||||
return;
|
||||
}
|
||||
|
||||
if (logs.filter(log => log.pssh_data === Session.psshDataToPsshBoxB64(pssh_data)).length > 0) {
|
||||
console.log("[WidevineProxy2]", `KEYS_ALREADY_RETRIEVED: ${uint8ArrayToBase64(pssh_data)}`);
|
||||
sendResponse(body);
|
||||
return;
|
||||
}
|
||||
|
||||
const selected_device_name = await DeviceManager.getSelectedWidevineDevice();
|
||||
if (!selected_device_name) {
|
||||
sendResponse(body);
|
||||
return;
|
||||
}
|
||||
|
||||
const device_b64 = await DeviceManager.loadWidevineDevice(selected_device_name);
|
||||
const widevine_device = new WidevineDevice(base64toUint8Array(device_b64).buffer);
|
||||
|
||||
const private_key = `-----BEGIN RSA PRIVATE KEY-----${uint8ArrayToBase64(widevine_device.private_key)}-----END RSA PRIVATE KEY-----`;
|
||||
const session = new Session(
|
||||
{
|
||||
privateKey: private_key,
|
||||
identifierBlob: widevine_device.client_id_bytes
|
||||
},
|
||||
pssh_data
|
||||
);
|
||||
|
||||
const [challenge, request_id] = session.createLicenseRequest(LicenseType.STREAMING, widevine_device.type === 2);
|
||||
sessions.set(uint8ArrayToBase64(request_id), session);
|
||||
|
||||
sendResponse(uint8ArrayToBase64(challenge));
|
||||
}
|
||||
|
||||
async function parseLicense(body, sendResponse, tab_url) {
|
||||
const license = base64toUint8Array(body);
|
||||
const signed_license_message = SignedMessage.decode(license);
|
||||
|
||||
if (signed_license_message.type !== SignedMessage.MessageType.LICENSE) {
|
||||
console.log("[WidevineProxy2]", "INVALID_MESSAGE_TYPE", signed_license_message.type.toString())
|
||||
sendResponse();
|
||||
return;
|
||||
}
|
||||
|
||||
const license_obj = License.decode(signed_license_message.msg);
|
||||
const loaded_request_id = uint8ArrayToBase64(license_obj.id.requestId);
|
||||
|
||||
if (!sessions.has(loaded_request_id)) {
|
||||
sendResponse();
|
||||
return;
|
||||
}
|
||||
|
||||
const loadedSession = sessions.get(loaded_request_id);
|
||||
const keys = await loadedSession.parseLicense(license);
|
||||
const pssh = loadedSession.getPSSH();
|
||||
|
||||
console.log("[WidevineProxy2]", "KEYS", JSON.stringify(keys), tab_url);
|
||||
const log = {
|
||||
type: "WIDEVINE",
|
||||
pssh_data: pssh,
|
||||
keys: keys,
|
||||
url: tab_url,
|
||||
timestamp: Math.floor(Date.now() / 1000)
|
||||
}
|
||||
logs.push(log);
|
||||
await AsyncLocalStorage.setStorage({[pssh]: log});
|
||||
|
||||
sessions.delete(loaded_request_id);
|
||||
sendResponse();
|
||||
}
|
||||
|
||||
async function generateChallengeRemote(body, sendResponse) {
|
||||
const signed_message = SignedMessage.decode(base64toUint8Array(body));
|
||||
const license_request = LicenseRequest.decode(signed_message.msg);
|
||||
const pssh_data = license_request.contentId.widevinePsshData.psshData[0];
|
||||
|
||||
if (!pssh_data) {
|
||||
console.log("[WidevineProxy2]", "NO_PSSH_DATA_IN_CHALLENGE");
|
||||
sendResponse(body);
|
||||
return;
|
||||
}
|
||||
|
||||
const pssh = Session.psshDataToPsshBoxB64(pssh_data);
|
||||
|
||||
if (logs.filter(log => log.pssh_data === pssh).length > 0) {
|
||||
console.log("[WidevineProxy2]", `KEYS_ALREADY_RETRIEVED: ${uint8ArrayToBase64(pssh_data)}`);
|
||||
sendResponse(body);
|
||||
return;
|
||||
}
|
||||
|
||||
const selected_remote_cdm_name = await RemoteCDMManager.getSelectedRemoteCDM();
|
||||
if (!selected_remote_cdm_name) {
|
||||
sendResponse(body);
|
||||
return;
|
||||
}
|
||||
|
||||
const selected_remote_cdm = JSON.parse(await RemoteCDMManager.loadRemoteCDM(selected_remote_cdm_name));
|
||||
const remote_cdm = RemoteCdm.from_object(selected_remote_cdm);
|
||||
|
||||
const session_id = await remote_cdm.open();
|
||||
const challenge_b64 = await remote_cdm.get_license_challenge(session_id, pssh, true);
|
||||
|
||||
const signed_challenge_message = SignedMessage.decode(base64toUint8Array(challenge_b64));
|
||||
const challenge_message = LicenseRequest.decode(signed_challenge_message.msg);
|
||||
|
||||
sessions.set(uint8ArrayToBase64(challenge_message.contentId.widevinePsshData.requestId), {
|
||||
id: session_id,
|
||||
pssh: pssh
|
||||
});
|
||||
sendResponse(challenge_b64);
|
||||
}
|
||||
|
||||
async function parseLicenseRemote(body, sendResponse, tab_url) {
|
||||
const license = base64toUint8Array(body);
|
||||
const signed_license_message = SignedMessage.decode(license);
|
||||
|
||||
if (signed_license_message.type !== SignedMessage.MessageType.LICENSE) {
|
||||
console.log("[WidevineProxy2]", "INVALID_MESSAGE_TYPE", signed_license_message.type.toString())
|
||||
sendResponse();
|
||||
return;
|
||||
}
|
||||
|
||||
const license_obj = License.decode(signed_license_message.msg);
|
||||
const loaded_request_id = uint8ArrayToBase64(license_obj.id.requestId);
|
||||
|
||||
if (!sessions.has(loaded_request_id)) {
|
||||
sendResponse();
|
||||
return;
|
||||
}
|
||||
|
||||
const session_id = sessions.get(loaded_request_id);
|
||||
|
||||
const selected_remote_cdm_name = await RemoteCDMManager.getSelectedRemoteCDM();
|
||||
if (!selected_remote_cdm_name) {
|
||||
sendResponse();
|
||||
return;
|
||||
}
|
||||
|
||||
const selected_remote_cdm = JSON.parse(await RemoteCDMManager.loadRemoteCDM(selected_remote_cdm_name));
|
||||
const remote_cdm = RemoteCdm.from_object(selected_remote_cdm);
|
||||
|
||||
await remote_cdm.parse_license(session_id.id, body);
|
||||
const returned_keys = await remote_cdm.get_keys(session_id.id, "CONTENT");
|
||||
await remote_cdm.close(session_id.id);
|
||||
|
||||
if (returned_keys.length === 0) {
|
||||
sendResponse();
|
||||
return;
|
||||
}
|
||||
|
||||
const keys = returned_keys.map(({ key, key_id }) => ({ k: key, kid: key_id }));
|
||||
|
||||
console.log("[WidevineProxy2]", "KEYS", JSON.stringify(keys), tab_url);
|
||||
const log = {
|
||||
type: "WIDEVINE",
|
||||
pssh_data: session_id.pssh,
|
||||
keys: keys,
|
||||
url: tab_url,
|
||||
timestamp: Math.floor(Date.now() / 1000)
|
||||
}
|
||||
logs.push(log);
|
||||
await AsyncLocalStorage.setStorage({[session_id.pssh]: log});
|
||||
|
||||
sessions.delete(loaded_request_id);
|
||||
sendResponse();
|
||||
}
|
||||
|
||||
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||
(async () => {
|
||||
switch (message.type) {
|
||||
@@ -26,49 +231,22 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||
sendResponse(message.body);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
JSON.parse(atob(message.body));
|
||||
sendResponse(message.body);
|
||||
return;
|
||||
} catch {
|
||||
if (message.body) {
|
||||
const signed_message = SignedMessage.decode(base64toUint8Array(message.body));
|
||||
const license_request = LicenseRequest.decode(signed_message.msg);
|
||||
const pssh_data = license_request.contentId.widevinePsshData.psshData[0];
|
||||
|
||||
if (!pssh_data) {
|
||||
sendResponse(message.body); // TODO: send report message back or just log from background script
|
||||
return;
|
||||
const device_type = await SettingsManager.getSelectedDeviceType();
|
||||
switch (device_type) {
|
||||
case "WVD":
|
||||
await generateChallenge(message.body, sendResponse);
|
||||
break;
|
||||
case "REMOTE":
|
||||
await generateChallengeRemote(message.body, sendResponse);
|
||||
break;
|
||||
}
|
||||
|
||||
if (logs.filter(log => log.pssh_data === Session.psshDataToPsshBoxB64(pssh_data)).length > 0) {
|
||||
console.log("[WidevineProxy2]", `KEYS_ALREADY_RETRIEVED: ${uint8ArrayToBase64(pssh_data)}`);
|
||||
sendResponse(message.body);
|
||||
return;
|
||||
}
|
||||
|
||||
const selected_device_name = await DeviceManager.getSelectedWidevineDevice();
|
||||
if (!selected_device_name) {
|
||||
sendResponse(message.body);
|
||||
return;
|
||||
}
|
||||
|
||||
const device_b64 = await DeviceManager.loadWidevineDevice(selected_device_name);
|
||||
const widevine_device = new WidevineDevice(base64toUint8Array(device_b64).buffer);
|
||||
|
||||
const private_key = `-----BEGIN RSA PRIVATE KEY-----${uint8ArrayToBase64(widevine_device.private_key)}-----END RSA PRIVATE KEY-----`;
|
||||
const session = new Session(
|
||||
{
|
||||
privateKey: private_key,
|
||||
identifierBlob: widevine_device.client_id_bytes
|
||||
},
|
||||
pssh_data
|
||||
);
|
||||
|
||||
const [challenge, request_id] = session.createLicenseRequest(LicenseType.STREAMING, widevine_device.type === 2);
|
||||
sessions.set(uint8ArrayToBase64(request_id), session);
|
||||
|
||||
sendResponse(uint8ArrayToBase64(challenge));
|
||||
}
|
||||
}
|
||||
break;
|
||||
@@ -78,79 +256,40 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||
sendResponse(message.body);
|
||||
return;
|
||||
}
|
||||
|
||||
const tab_url = sender.tab ? sender.tab.url : null;
|
||||
|
||||
try {
|
||||
const clearkey = JSON.parse(atob(message.body));
|
||||
|
||||
const formatted_keys = clearkey["keys"].map(key => ({
|
||||
...key,
|
||||
kid: uint8ArrayToHex(base64toUint8Array(key.kid.replace(/-/g, "+").replace(/_/g, "/") + "==")),
|
||||
k: uint8ArrayToHex(base64toUint8Array(key.k.replace(/-/g, "+").replace(/_/g, "/") + "=="))
|
||||
}));
|
||||
const pssh_data = btoa(JSON.stringify({kids: clearkey["keys"].map(key => key.k)}));
|
||||
|
||||
if (logs.filter(log => log.pssh_data === pssh_data).length > 0) {
|
||||
console.log("[WidevineProxy2]", `KEYS_ALREADY_RETRIEVED: ${pssh_data}`);
|
||||
sendResponse();
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("keys", formatted_keys);
|
||||
const log = {
|
||||
type: "CLEARKEY",
|
||||
pssh_data: pssh_data,
|
||||
keys: formatted_keys,
|
||||
url: tab_url,
|
||||
timestamp: Math.floor(Date.now() / 1000)
|
||||
}
|
||||
logs.push(log);
|
||||
|
||||
await AsyncLocalStorage.setStorage({[pssh_data]: log});
|
||||
sendResponse();
|
||||
await parseClearKey(message.body, sendResponse, tab_url);
|
||||
return;
|
||||
} catch (e) {
|
||||
const license = base64toUint8Array(message.body);
|
||||
const signed_license_message = SignedMessage.decode(license);
|
||||
if (signed_license_message.type !== SignedMessage.MessageType.LICENSE) {
|
||||
console.log("[WidevineProxy2]", "INVALID_MESSAGE_TYPE", signed_license_message.type.toString())
|
||||
sendResponse();
|
||||
return;
|
||||
const device_type = await SettingsManager.getSelectedDeviceType();
|
||||
switch (device_type) {
|
||||
case "WVD":
|
||||
await parseLicense(message.body, sendResponse, tab_url);
|
||||
break;
|
||||
case "REMOTE":
|
||||
await parseLicenseRemote(message.body, sendResponse, tab_url);
|
||||
|
||||
// temporary
|
||||
sendResponse();
|
||||
break;
|
||||
}
|
||||
|
||||
const license_obj = License.decode(signed_license_message.msg);
|
||||
const loaded_request_id = uint8ArrayToBase64(license_obj.id.requestId);
|
||||
|
||||
if (!sessions.has(loaded_request_id)) {
|
||||
sendResponse();
|
||||
return;
|
||||
}
|
||||
|
||||
const loadedSession = sessions.get(loaded_request_id);
|
||||
const keys = await loadedSession.parseLicense(license);
|
||||
const pssh = loadedSession.getPSSH();
|
||||
|
||||
console.log("[WidevineProxy2]", "KEYS", JSON.stringify(keys), tab_url);
|
||||
const log = {
|
||||
type: "WIDEVINE",
|
||||
pssh_data: pssh,
|
||||
keys: keys,
|
||||
url: tab_url,
|
||||
timestamp: Math.floor(Date.now() / 1000)
|
||||
}
|
||||
logs.push(log);
|
||||
await AsyncLocalStorage.setStorage({[pssh]: log});
|
||||
|
||||
sessions.delete(loaded_request_id);
|
||||
sendResponse();
|
||||
return;
|
||||
}
|
||||
case "GET_LOGS":
|
||||
sendResponse(logs);
|
||||
break;
|
||||
case "OPEN_PICKER":
|
||||
case "OPEN_PICKER_WVD":
|
||||
chrome.windows.create({
|
||||
url: 'picker/filePicker.html',
|
||||
url: 'picker/wvd/filePicker.html',
|
||||
type: 'popup',
|
||||
width: 300,
|
||||
height: 200,
|
||||
});
|
||||
break;
|
||||
case "OPEN_PICKER_REMOTE":
|
||||
chrome.windows.create({
|
||||
url: 'picker/remote/filePicker.html',
|
||||
type: 'popup',
|
||||
width: 300,
|
||||
height: 200,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"manifest_version": 3,
|
||||
"name": "WidevineProxy2",
|
||||
"version": "0.6",
|
||||
"version": "0.7",
|
||||
"permissions": [
|
||||
"activeTab",
|
||||
"tabs",
|
||||
|
||||
@@ -98,6 +98,14 @@ input:checked + .slider:before {
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
|
||||
#type-select {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
#settings {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
@@ -107,7 +115,7 @@ input:checked + .slider:before {
|
||||
margin: 5px;
|
||||
}
|
||||
|
||||
#wvd > * {
|
||||
#wvd > *, #remote > * {
|
||||
margin: 5px;
|
||||
}
|
||||
|
||||
|
||||
@@ -12,23 +12,46 @@
|
||||
</div>
|
||||
<fieldset>
|
||||
<legend>Settings</legend>
|
||||
<!-- Enabled -->
|
||||
<input type="checkbox" id="enabled">
|
||||
<label for="enabled"> Enabled</label>
|
||||
<!-- Dark mode -->
|
||||
<label class="switch">
|
||||
<input type="checkbox" id="darkModeToggle">
|
||||
<span class="slider"></span>
|
||||
</label>
|
||||
<label for="enabled"> Dark mode</label>
|
||||
<div id="settings">
|
||||
<!-- Enabled -->
|
||||
<div>
|
||||
<input type="checkbox" id="enabled">
|
||||
<label for="enabled"> Enabled</label>
|
||||
</div>
|
||||
<!-- Dark mode -->
|
||||
<div>
|
||||
<label class="switch">
|
||||
<input type="checkbox" id="darkModeToggle">
|
||||
<span class="slider"></span>
|
||||
</label>
|
||||
<label for="enabled"> Dark mode</label>
|
||||
</div>
|
||||
<!-- Type select -->
|
||||
<div id="type-select">
|
||||
<a>Device Type:</a><br>
|
||||
<input type="radio" id="wvd_select" name="cdm_type" value="wvd">
|
||||
<label for="wvd_select">Widevine Device</label><br>
|
||||
<input type="radio" id="remote_select" name="cdm_type" value="remote">
|
||||
<label for="remote_select">Remote CDM</label><br>
|
||||
</div>
|
||||
</div>
|
||||
<fieldset id="wvd">
|
||||
<legend>Widevine Device</legend>
|
||||
<button type="button" id="fileInput">Choose File</button><br>
|
||||
<button type="button" id="fileInput">Choose File</button>
|
||||
<button type="button" id="remove">Remove</button>
|
||||
<button type="button" id="download">Download</button><br>
|
||||
<label>
|
||||
<select id="wvd-combobox"></select>
|
||||
</label><br>
|
||||
<button type="button" id="remove">Remove</button>
|
||||
<button type="button" id="download">Download</button>
|
||||
</label>
|
||||
</fieldset>
|
||||
<fieldset id="remote">
|
||||
<legend>Remote CDM</legend>
|
||||
<button type="button" id="remoteInput">Choose remote.json</button>
|
||||
<button type="button" id="remoteRemove">Remove</button>
|
||||
<button type="button" id="remoteDownload">Download</button><br>
|
||||
<label>
|
||||
<select id="remote-combobox"></select>
|
||||
</label>
|
||||
</fieldset>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import "../protobuf.min.js";
|
||||
import "../license_protocol.js";
|
||||
import { base64toUint8Array, DeviceManager, SettingsManager } from "../util.js";
|
||||
import { base64toUint8Array, DeviceManager, RemoteCDMManager, SettingsManager } from "../util.js";
|
||||
|
||||
const key_container = document.getElementById('key-container');
|
||||
|
||||
@@ -15,11 +15,30 @@ enabled.addEventListener('change', async function (){
|
||||
await SettingsManager.setEnabled(enabled.checked);
|
||||
});
|
||||
|
||||
const wvd_select = document.getElementById('wvd_select');
|
||||
wvd_select.addEventListener('change', async function (){
|
||||
if (wvd_select.checked) {
|
||||
await SettingsManager.saveSelectedDeviceType("WVD");
|
||||
}
|
||||
});
|
||||
|
||||
const remote_select = document.getElementById('remote_select');
|
||||
remote_select.addEventListener('change', async function (){
|
||||
if (remote_select.checked) {
|
||||
await SettingsManager.saveSelectedDeviceType("REMOTE");
|
||||
}
|
||||
});
|
||||
|
||||
const wvd_combobox = document.getElementById('wvd-combobox');
|
||||
wvd_combobox.addEventListener('change', async function() {
|
||||
await DeviceManager.saveSelectedWidevineDevice(wvd_combobox.options[wvd_combobox.selectedIndex].text);
|
||||
});
|
||||
|
||||
const remote_combobox = document.getElementById('remote-combobox');
|
||||
remote_combobox.addEventListener('change', async function() {
|
||||
await RemoteCDMManager.saveSelectedRemoteCDM(remote_combobox.options[remote_combobox.selectedIndex].text);
|
||||
});
|
||||
|
||||
const remove = document.getElementById('remove');
|
||||
remove.addEventListener('click', async function() {
|
||||
await DeviceManager.removeSelectedWidevineDevice();
|
||||
@@ -31,6 +50,17 @@ remove.addEventListener('click', async function() {
|
||||
}
|
||||
});
|
||||
|
||||
const remote_remove = document.getElementById('remoteRemove');
|
||||
remote_remove.addEventListener('click', async function() {
|
||||
await RemoteCDMManager.removeSelectedRemoteCDM();
|
||||
remote_combobox.innerHTML = '';
|
||||
await RemoteCDMManager.loadSetAllRemoteCDMs();
|
||||
const selected_option = remote_combobox.options[remote_combobox.selectedIndex];
|
||||
if (selected_option) {
|
||||
await RemoteCDMManager.saveSelectedRemoteCDM(selected_option.text);
|
||||
}
|
||||
})
|
||||
|
||||
const download = document.getElementById('download');
|
||||
download.addEventListener('click', async function() {
|
||||
const widevine_device = await DeviceManager.getSelectedWidevineDevice();
|
||||
@@ -40,6 +70,15 @@ download.addEventListener('click', async function() {
|
||||
)
|
||||
});
|
||||
|
||||
const remote_download = document.getElementById('remoteDownload');
|
||||
remote_download.addEventListener('click', async function() {
|
||||
const remote_cdm = await RemoteCDMManager.getSelectedRemoteCDM();
|
||||
SettingsManager.downloadFile(
|
||||
await RemoteCDMManager.loadRemoteCDM(remote_cdm),
|
||||
remote_cdm + ".json"
|
||||
)
|
||||
});
|
||||
|
||||
const clear = document.getElementById('clear');
|
||||
clear.addEventListener('click', async function() {
|
||||
chrome.runtime.sendMessage({ type: "CLEAR" });
|
||||
@@ -47,7 +86,12 @@ clear.addEventListener('click', async function() {
|
||||
});
|
||||
|
||||
document.getElementById('fileInput').addEventListener('click', () => {
|
||||
chrome.runtime.sendMessage({ type: "OPEN_PICKER" });
|
||||
chrome.runtime.sendMessage({ type: "OPEN_PICKER_WVD" });
|
||||
window.close();
|
||||
});
|
||||
|
||||
document.getElementById('remoteInput').addEventListener('click', () => {
|
||||
chrome.runtime.sendMessage({ type: "OPEN_PICKER_REMOTE" });
|
||||
window.close();
|
||||
});
|
||||
|
||||
@@ -117,7 +161,10 @@ function checkLogs() {
|
||||
document.addEventListener('DOMContentLoaded', async function () {
|
||||
enabled.checked = await SettingsManager.getEnabled();
|
||||
SettingsManager.setDarkMode(await SettingsManager.getDarkMode());
|
||||
await SettingsManager.setSelectedDeviceType(await SettingsManager.getSelectedDeviceType());
|
||||
await DeviceManager.loadSetAllWidevineDevices();
|
||||
await DeviceManager.selectWidevineDevice(await DeviceManager.getSelectedWidevineDevice());
|
||||
await RemoteCDMManager.loadSetAllRemoteCDMs();
|
||||
await RemoteCDMManager.selectRemoteCDM(await RemoteCDMManager.getSelectedRemoteCDM());
|
||||
checkLogs();
|
||||
});
|
||||
|
||||
7
picker/remote/filePicker.html
Normal file
7
picker/remote/filePicker.html
Normal file
@@ -0,0 +1,7 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<body>
|
||||
<input type="file" id="fileInput" accept=".json"/>
|
||||
<script type="module" src="filePicker.js" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
10
picker/remote/filePicker.js
Normal file
10
picker/remote/filePicker.js
Normal file
@@ -0,0 +1,10 @@
|
||||
import "../../protobuf.min.js";
|
||||
import "../../license_protocol.js";
|
||||
import { SettingsManager } from "../../util.js";
|
||||
|
||||
document.getElementById('fileInput').addEventListener('change', async (event) => {
|
||||
const file = event.target.files[0];
|
||||
await SettingsManager.loadRemoteCDM(file).then(() => {
|
||||
window.close();
|
||||
});
|
||||
});
|
||||
@@ -1,7 +1,7 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<body>
|
||||
<input type="file" id="fileInput" accept=".wvd"/>
|
||||
<script type="module" src="filePicker.js" defer></script>
|
||||
</body>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<body>
|
||||
<input type="file" id="fileInput" accept=".wvd"/>
|
||||
<script type="module" src="filePicker.js" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,10 +1,10 @@
|
||||
import "../protobuf.min.js";
|
||||
import "../license_protocol.js";
|
||||
import { SettingsManager } from "../util.js";
|
||||
|
||||
document.getElementById('fileInput').addEventListener('change', async (event) => {
|
||||
const file = event.target.files[0];
|
||||
await SettingsManager.importDevice(file).then(() => {
|
||||
window.close();
|
||||
});
|
||||
import "../../protobuf.min.js";
|
||||
import "../../license_protocol.js";
|
||||
import { SettingsManager } from "../../util.js";
|
||||
|
||||
document.getElementById('fileInput').addEventListener('change', async (event) => {
|
||||
const file = event.target.files[0];
|
||||
await SettingsManager.importDevice(file).then(() => {
|
||||
window.close();
|
||||
});
|
||||
});
|
||||
110
remote_cdm.js
Normal file
110
remote_cdm.js
Normal file
@@ -0,0 +1,110 @@
|
||||
export class RemoteCdm {
|
||||
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;
|
||||
}
|
||||
|
||||
static from_object(obj) {
|
||||
return new RemoteCdm(
|
||||
obj.device_type,
|
||||
obj.system_id,
|
||||
obj.security_level,
|
||||
obj.host,
|
||||
obj.secret,
|
||||
obj.device_name ?? obj.name,
|
||||
);
|
||||
}
|
||||
|
||||
get_name() {
|
||||
const type = this.device_type === "CHROME" ? "CHROME" : `L${this.security_level}`
|
||||
return `[${type}] ${this.host}/${this.device_name} (${this.system_id})`;
|
||||
}
|
||||
|
||||
async open() {
|
||||
const open_request = await fetch(
|
||||
`${this.host}/${this.device_name}/open`,
|
||||
{
|
||||
method: 'GET',
|
||||
}
|
||||
);
|
||||
console.log("[WidevineProxy2]", "REMOTE_CDM", "OPEN", open_request.status);
|
||||
const open_json = await open_request.json();
|
||||
|
||||
return open_json.data.session_id;
|
||||
}
|
||||
|
||||
async close(session_id) {
|
||||
const close_request = await fetch(
|
||||
`${this.host}/${this.device_name}/close/${session_id}`,
|
||||
{
|
||||
method: 'GET',
|
||||
}
|
||||
);
|
||||
console.log("[WidevineProxy2]", "REMOTE_CDM", "CLOSE", close_request.status);
|
||||
}
|
||||
|
||||
// TODO:
|
||||
// + get_service_certificate
|
||||
// + set_service_certificate
|
||||
|
||||
async get_license_challenge(session_id, pssh, privacy_mode) {
|
||||
const license_request = await fetch(
|
||||
`${this.host}/${this.device_name}/get_license_challenge/STREAMING`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
session_id: session_id,
|
||||
init_data: pssh,
|
||||
privacy_mode: privacy_mode
|
||||
})
|
||||
}
|
||||
)
|
||||
console.log("[WidevineProxy2]", "REMOTE_CDM", "GET_LICENSE_CHALLENGE", license_request.status);
|
||||
const license_request_json = await license_request.json();
|
||||
|
||||
return license_request_json.data.challenge_b64;
|
||||
}
|
||||
|
||||
async parse_license(session_id, license_b64) {
|
||||
const license = await fetch(
|
||||
`${this.host}/${this.device_name}/parse_license`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
session_id: session_id,
|
||||
license_message: license_b64
|
||||
})
|
||||
}
|
||||
)
|
||||
console.log("[WidevineProxy2]", "REMOTE_CDM", "PARSE_LICENSE", license.status);
|
||||
}
|
||||
|
||||
async get_keys(session_id, type) {
|
||||
const key_request = await fetch(
|
||||
`${this.host}/${this.device_name}/get_keys/${type}`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
session_id: session_id
|
||||
})
|
||||
}
|
||||
)
|
||||
console.log("[WidevineProxy2]", "REMOTE_CDM", "GET_KEYS", key_request.status);
|
||||
const key_request_json = await key_request.json();
|
||||
|
||||
return key_request_json.data.keys;
|
||||
}
|
||||
}
|
||||
125
util.js
125
util.js
@@ -1,4 +1,5 @@
|
||||
import { WidevineDevice } from "./device.js";
|
||||
import { RemoteCdm } from "./remote_cdm.js";
|
||||
|
||||
export class AsyncSyncStorage {
|
||||
static async setStorage(items) {
|
||||
@@ -82,7 +83,6 @@ export class DeviceManager {
|
||||
const array = result.devices === undefined ? [] : result.devices;
|
||||
array.push(name);
|
||||
await AsyncSyncStorage.setStorage({ devices: array });
|
||||
|
||||
await AsyncSyncStorage.setStorage({ [name]: value });
|
||||
}
|
||||
|
||||
@@ -138,6 +138,67 @@ export class DeviceManager {
|
||||
}
|
||||
}
|
||||
|
||||
export class RemoteCDMManager {
|
||||
static async saveRemoteCDM(name, obj) {
|
||||
const result = await AsyncSyncStorage.getStorage(['remote_cdms']);
|
||||
const array = result.remote_cdms === undefined ? [] : result.remote_cdms;
|
||||
array.push(name);
|
||||
await AsyncSyncStorage.setStorage({ remote_cdms: array });
|
||||
await AsyncSyncStorage.setStorage({ [name]: obj });
|
||||
}
|
||||
|
||||
static async loadRemoteCDM(name) {
|
||||
const result = await AsyncSyncStorage.getStorage([name]);
|
||||
return JSON.stringify(result[name] || {});
|
||||
}
|
||||
|
||||
static setRemoteCDM(name, value){
|
||||
const remote_combobox = document.getElementById('remote-combobox');
|
||||
const remote_element = document.createElement('option');
|
||||
|
||||
remote_element.text = name;
|
||||
remote_element.value = value;
|
||||
|
||||
remote_combobox.appendChild(remote_element);
|
||||
}
|
||||
|
||||
static async loadSetAllRemoteCDMs() {
|
||||
const result = await AsyncSyncStorage.getStorage(['remote_cdms']);
|
||||
const array = result.remote_cdms || [];
|
||||
for (const item of array) {
|
||||
this.setRemoteCDM(item, await this.loadRemoteCDM(item));
|
||||
}
|
||||
}
|
||||
|
||||
static async saveSelectedRemoteCDM(name) {
|
||||
await AsyncSyncStorage.setStorage({ selected_remote_cdm: name });
|
||||
}
|
||||
|
||||
static async getSelectedRemoteCDM() {
|
||||
const result = await AsyncSyncStorage.getStorage(["selected_remote_cdm"]);
|
||||
return result["selected_remote_cdm"] || "";
|
||||
}
|
||||
|
||||
static async selectRemoteCDM(name) {
|
||||
document.getElementById('remote-combobox').value = await this.loadRemoteCDM(name);
|
||||
}
|
||||
|
||||
static async removeSelectedRemoteCDM() {
|
||||
const selected_remote_cdm_name = await RemoteCDMManager.getSelectedRemoteCDM();
|
||||
|
||||
const result = await AsyncSyncStorage.getStorage(['remote_cdms']);
|
||||
const array = result.remote_cdms === undefined ? [] : result.remote_cdms;
|
||||
|
||||
const index = array.indexOf(selected_remote_cdm_name);
|
||||
if (index > -1) {
|
||||
array.splice(index, 1);
|
||||
}
|
||||
|
||||
await AsyncSyncStorage.setStorage({ remote_cdms: array });
|
||||
await AsyncSyncStorage.removeStorage([selected_remote_cdm_name]);
|
||||
}
|
||||
}
|
||||
|
||||
export class SettingsManager {
|
||||
static async setEnabled(enabled) {
|
||||
await AsyncSyncStorage.setStorage({ enabled: enabled });
|
||||
@@ -178,7 +239,7 @@ export class SettingsManager {
|
||||
resolve();
|
||||
};
|
||||
reader.readAsArrayBuffer(file);
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
static async saveDarkMode(dark_mode) {
|
||||
@@ -197,6 +258,66 @@ export class SettingsManager {
|
||||
document.body.classList.toggle('dark-mode', dark_mode);
|
||||
textImage.src = dark_mode ? "../images/proxy_text_dark.png" : "../images/proxy_text.png";
|
||||
}
|
||||
|
||||
static async loadRemoteCDM(file) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = async function (loaded) {
|
||||
const result = loaded.target.result;
|
||||
|
||||
let json_file = void 0;
|
||||
try {
|
||||
json_file = JSON.parse(result);
|
||||
} catch {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("LOADED DEVICE:", json_file);
|
||||
const remote_cdm = new RemoteCdm(
|
||||
json_file.device_type,
|
||||
json_file.system_id,
|
||||
json_file.security_level,
|
||||
json_file.host,
|
||||
json_file.secret,
|
||||
json_file.device_name ?? json_file.name,
|
||||
|
||||
);
|
||||
const device_name = remote_cdm.get_name();
|
||||
console.log("NAME:", device_name);
|
||||
|
||||
if (await RemoteCDMManager.loadRemoteCDM(device_name) === "{}") {
|
||||
await RemoteCDMManager.saveRemoteCDM(device_name, json_file);
|
||||
}
|
||||
|
||||
await RemoteCDMManager.saveSelectedRemoteCDM(device_name);
|
||||
resolve();
|
||||
};
|
||||
reader.readAsText(file);
|
||||
});
|
||||
}
|
||||
|
||||
static async saveSelectedDeviceType(selected_type) {
|
||||
await AsyncSyncStorage.setStorage({ device_type: selected_type });
|
||||
}
|
||||
|
||||
static async getSelectedDeviceType() {
|
||||
const result = await AsyncSyncStorage.getStorage(["device_type"]);
|
||||
return result["device_type"] || "WVD";
|
||||
}
|
||||
|
||||
static setSelectedDeviceType(device_type) {
|
||||
switch (device_type) {
|
||||
case "WVD":
|
||||
const wvd_select = document.getElementById('wvd_select');
|
||||
wvd_select.checked = true;
|
||||
break;
|
||||
case "REMOTE":
|
||||
const remote_select = document.getElementById('remote_select');
|
||||
remote_select.checked = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function intToUint8Array(num) {
|
||||
|
||||
Reference in New Issue
Block a user