From b022d8cadef4337d782a13d85385cc7fe1931f95 Mon Sep 17 00:00:00 2001 From: DevLARLEY Date: Sun, 27 Oct 2024 09:51:37 +0100 Subject: [PATCH] Remote CDM support --- README.md | 20 +- background.js | 337 ++++++++++++++++++++++--------- manifest.json | 2 +- panel/panel.css | 10 +- panel/panel.html | 49 +++-- panel/panel.js | 51 ++++- picker/remote/filePicker.html | 7 + picker/remote/filePicker.js | 10 + picker/{ => wvd}/filePicker.html | 12 +- picker/{ => wvd}/filePicker.js | 18 +- remote_cdm.js | 110 ++++++++++ util.js | 125 +++++++++++- 12 files changed, 614 insertions(+), 137 deletions(-) create mode 100644 picker/remote/filePicker.html create mode 100644 picker/remote/filePicker.js rename picker/{ => wvd}/filePicker.html (96%) rename picker/{ => wvd}/filePicker.js (64%) create mode 100644 remote_cdm.js diff --git a/README.md b/README.md index b024bbb..c03f160 100644 --- a/README.md +++ b/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) diff --git a/background.js b/background.js index ca787da..04fd9e0 100644 --- a/background.js +++ b/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, diff --git a/manifest.json b/manifest.json index a11e16c..1032811 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 3, "name": "WidevineProxy2", - "version": "0.6", + "version": "0.7", "permissions": [ "activeTab", "tabs", diff --git a/panel/panel.css b/panel/panel.css index 4ae60ab..4093433 100644 --- a/panel/panel.css +++ b/panel/panel.css @@ -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; } diff --git a/panel/panel.html b/panel/panel.html index 3054531..22caabe 100644 --- a/panel/panel.html +++ b/panel/panel.html @@ -12,23 +12,46 @@
Settings - - - - - - +
+ +
+ + +
+ +
+ + +
+ +
+ Device Type:
+ +
+ +
+
+
Widevine Device -
+ + +

- - + +
+
+ Remote CDM + + +
+
diff --git a/panel/panel.js b/panel/panel.js index d6613d8..25895b9 100644 --- a/panel/panel.js +++ b/panel/panel.js @@ -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(); }); diff --git a/picker/remote/filePicker.html b/picker/remote/filePicker.html new file mode 100644 index 0000000..646076d --- /dev/null +++ b/picker/remote/filePicker.html @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/picker/remote/filePicker.js b/picker/remote/filePicker.js new file mode 100644 index 0000000..832609b --- /dev/null +++ b/picker/remote/filePicker.js @@ -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(); + }); +}); \ No newline at end of file diff --git a/picker/filePicker.html b/picker/wvd/filePicker.html similarity index 96% rename from picker/filePicker.html rename to picker/wvd/filePicker.html index e78cbe8..2b8de4a 100644 --- a/picker/filePicker.html +++ b/picker/wvd/filePicker.html @@ -1,7 +1,7 @@ - - - - - - + + + + + + \ No newline at end of file diff --git a/picker/filePicker.js b/picker/wvd/filePicker.js similarity index 64% rename from picker/filePicker.js rename to picker/wvd/filePicker.js index e5a9a77..41e7af2 100644 --- a/picker/filePicker.js +++ b/picker/wvd/filePicker.js @@ -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(); + }); }); \ No newline at end of file diff --git a/remote_cdm.js b/remote_cdm.js new file mode 100644 index 0000000..d8423ec --- /dev/null +++ b/remote_cdm.js @@ -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; + } +} \ No newline at end of file diff --git a/util.js b/util.js index 84ed0b8..4cafade 100644 --- a/util.js +++ b/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) {