Remote CDM support

This commit is contained in:
DevLARLEY
2024-10-27 09:51:37 +01:00
parent 9cac5b5035
commit b022d8cade
12 changed files with 614 additions and 137 deletions

View File

@@ -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)

View File

@@ -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,

View File

@@ -1,7 +1,7 @@
{
"manifest_version": 3,
"name": "WidevineProxy2",
"version": "0.6",
"version": "0.7",
"permissions": [
"activeTab",
"tabs",

View File

@@ -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;
}

View File

@@ -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>

View File

@@ -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();
});

View 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>

View 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();
});
});

View File

@@ -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>

View File

@@ -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
View 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
View File

@@ -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) {