diff --git a/background.js b/background.js new file mode 100644 index 0000000..a3d64d5 --- /dev/null +++ b/background.js @@ -0,0 +1,284 @@ +import { + uint8ArrayToBase64, + SettingsManager, + RemoteCDMManager, + PSSHFromKID, + stringToUTF16LEBytes, +} from "./util.js"; +import { RemoteCdm } from "./remote_cdm.js"; + +let manifests = new Map(); +let requests = new Map(); +let logs = []; + +chrome.webRequest.onBeforeSendHeaders.addListener( + function (details) { + if (details.method === "GET") { + if (!requests.has(details.url)) { + const headers = details.requestHeaders + .filter( + (item) => + !( + item.name.startsWith("sec-ch-ua") || + item.name.startsWith("Sec-Fetch") || + item.name.startsWith("Accept-") || + item.name.startsWith("Host") || + item.name === "Connection" + ) + ) + .reduce((acc, item) => { + acc[item.name] = item.value; + return acc; + }, {}); + requests.set(details.url, headers); + } + } + }, + { urls: [""] }, + [ + "requestHeaders", + chrome.webRequest.OnSendHeadersOptions.EXTRA_HEADERS, + ].filter(Boolean) +); + + + +async function generateChallengeRemote(body, sendResponse) { + try { + // Decode the base64-encoded body into a binary string + const binaryString = decodeBase64(body); // Use the decodeBase64 function + const byteArray = new Uint8Array(binaryString.length); + for (let i = 0; i < binaryString.length; i++) { + byteArray[i] = binaryString.charCodeAt(i); + } + + // Decode using UTF-16LE encoding + const decoder = new TextDecoder("utf-16le"); + var xmlString = decoder.decode(byteArray); + var xmlDecoded; + + // Extract the Challenge element from the XML string + const challengeRegex = /]*>([\s\S]*?)<\/Challenge>/i; + const challengeMatch = challengeRegex.exec(xmlString); + var encoding; + + if (challengeMatch) { + const challengeContent = challengeMatch[1].trim(); + const encodingRegex = /]*encoding="([^"]+)"[^>]*>/i; + const encodingMatch = encodingRegex.exec(xmlString); + encoding = encodingMatch ? encodingMatch[1] : null; + + // If encoding is base64encoded, decode the challenge content + if (encoding === "base64encoded") { + const challengeBinaryString = decodeBase64(challengeContent); // Use the decodeBase64 function + const challengeByteArray = new Uint8Array(challengeBinaryString.length); + for (let i = 0; i < challengeBinaryString.length; i++) { + challengeByteArray[i] = challengeBinaryString.charCodeAt(i); + } + const utf8Decoder = new TextDecoder("utf-8"); + xmlDecoded = utf8Decoder.decode(challengeByteArray); + } + } else { + console.error("Challenge element not found in XML."); + sendResponse(body); + return; + } + + // Extract the KID element + const kidRegex = /([^<]+)<\/KID>/i; + const kidMatch = kidRegex.exec(xmlDecoded); + var kidBase64; + if (kidMatch) { + kidBase64 = kidMatch[1].trim(); + } else { + console.log("[PlayreadyProxy]", "NO_KID_IN_CHALLENGE"); + sendResponse(body); + return; + } + + // Get PSSH from KID + const pssh = PSSHFromKID(kidBase64); + if (!pssh) { + console.log("[PlayreadyProxy]", "NO_PSSH_DATA_IN_CHALLENGE"); + sendResponse(body); + return; + } + + // Fetch the selected remote CDM and load it + const selected_remote_cdm_name = await RemoteCDMManager.getSelectedRemoteCDM(); + if (!selected_remote_cdm_name) { + sendResponse(body); + return; + } + + const selected_remote_cdm = await RemoteCDMManager.loadRemoteCDM(selected_remote_cdm_name); + + let remoteCdmObj; + try { + // Check if the selected_remote_cdm is Base64-encoded XML + if (selected_remote_cdm.startsWith("PD94bWwgdm")) { + const decodedString = decodeBase64(selected_remote_cdm); // Use the decodeBase64 function + const parser = new DOMParser(); + const xmlDoc = parseXML(decodedString); // Use parseXML function + + // Convert the XML document into a RemoteCdm object + remoteCdmObj = RemoteCdm.from_xml(xmlDoc); + } else { + // Otherwise, parse as JSON + remoteCdmObj = JSON.parse(selected_remote_cdm); + } + } catch (e) { + console.error("Error parsing remote CDM:", e); + sendResponse(body); + return; + } + + const remote_cdm = RemoteCdm.from_object(remoteCdmObj); + + // Get the license challenge + const challenge = await remote_cdm.get_license_challenge(pssh); + + // Replace the challenge content in the original XML with the new challenge + const newXmlString = xmlString.replace( + /(]*>)([\s\S]*?)(<\/Challenge>)/i, + `$1${challenge}$3` + ); + + // Convert the new XML string to UTF-16LE and then to base64 + const utf16leBytes = stringToUTF16LEBytes(newXmlString); + const responseBase64 = uint8ArrayToBase64(utf16leBytes); + + // Send the base64-encoded response + sendResponse(responseBase64); + } catch (error) { + console.error("Error in generateChallengeRemote:", error); + sendResponse(body); + } +} + + +async function parseLicenseRemote(body, sendResponse, tab_url) { + const response = atob(body); + + 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); + + const returned_keys = await remote_cdm.get_keys(btoa(response)); + + if (returned_keys.length === 0) { + sendResponse(); + return; + } + + const keys = returned_keys.map((s) => { + return { + k: s.key, + kid: s.key_id, + }; + }); + + console.log("[PlayreadyProxy]", "KEYS", JSON.stringify(keys), tab_url); + + const log = { + type: "PLAYREADY", + keys: keys, + url: tab_url, + timestamp: Math.floor(Date.now() / 1000), + manifests: manifests.has(tab_url) ? manifests.get(tab_url) : [], + }; + logs.push(log); + sendResponse(); +} + +chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { + (async () => { + const tab_url = sender.tab ? sender.tab.url : null; + + switch (message.type) { + case "REQUEST": + if (!(await SettingsManager.getEnabled())) { + sendResponse(message.body); + manifests.clear(); + return; + } + + try { + JSON.parse(atob(message.body)); + sendResponse(message.body); + return; + } catch { + if (message.body) { + await generateChallengeRemote( + message.body, + sendResponse + ); + } + } + break; + + case "RESPONSE": + if (!(await SettingsManager.getEnabled())) { + sendResponse(message.body); + manifests.clear(); + return; + } + + try { + await parseClearKey(message.body, sendResponse, tab_url); + return; + } catch (e) { + await parseLicenseRemote( + message.body, + sendResponse, + tab_url + ); + return; + } + case "GET_LOGS": + sendResponse(logs); + break; + case "OPEN_PICKER": + chrome.windows.create({ + url: "picker/filePicker.html", + type: "popup", + width: 300, + height: 200, + }); + break; + case "CLEAR": + logs = []; + manifests.clear(); + break; + case "MANIFEST": + const parsed = JSON.parse(message.body); + const element = { + type: parsed.type, + url: parsed.url, + headers: requests.has(parsed.url) + ? requests.get(parsed.url) + : [], + }; + + if (!manifests.has(tab_url)) { + manifests.set(tab_url, [element]); + } else { + let elements = manifests.get(tab_url); + if (!elements.some((e) => e.url === parsed.url)) { + elements.push(element); + manifests.set(tab_url, elements); + } + } + sendResponse(); + } + })(); + return true; +}); diff --git a/content_script.js b/content_script.js new file mode 100644 index 0000000..7cb1f1a --- /dev/null +++ b/content_script.js @@ -0,0 +1,338 @@ +function uint8ArrayToBase64(uint8array) { + return btoa(String.fromCharCode.apply(null, uint8array)); +} + +function uint8ArrayToString(uint8array) { + return String.fromCharCode.apply(null, uint8array); +} + +function base64toUint8Array(base64_string) { + return Uint8Array.from(atob(base64_string), (c) => c.charCodeAt(0)); +} + +function compareUint8Arrays(arr1, arr2) { + if (arr1.length !== arr2.length) return false; + return Array.from(arr1).every((value, index) => value === arr2[index]); +} + +function emitAndWaitForResponse(type, data) { + return new Promise((resolve) => { + const requestId = Math.random().toString(16).substring(2, 9); + const responseHandler = (event) => { + const { detail } = event; + if (detail.substring(0, 7) === requestId) { + document.removeEventListener( + "responseReceived", + responseHandler + ); + resolve(detail.substring(7)); + } + }; + document.addEventListener("responseReceived", responseHandler); + const requestEvent = new CustomEvent("response", { + detail: { + type: type, + body: data, + requestId: requestId, + }, + }); + document.dispatchEvent(requestEvent); + }); +} + +const fnproxy = (object, func) => new Proxy(object, { apply: func }); +const proxy = (object, key, func) => + Object.hasOwnProperty.call(object, key) && + Object.defineProperty(object, key, { + value: fnproxy(object[key], func), + }); + +function getEventListeners(type) { + if (this == null) return []; + const store = this[Symbol.for(getEventListeners)]; + if (store == null || store[type] == null) return []; + return store[type]; +} + +class Evaluator { + static isDASH(text) { + return text.includes(""); + } + + static isHLS(text) { + return text.includes("#extm3u"); + } + + static isHLSMaster(text) { + return text.includes("#ext-x-stream-inf"); + } + + static isMSS(text) { + return ( + text.includes("") + ); + } + + static getManifestType(text) { + const lower = text.toLowerCase(); + if (this.isDASH(lower)) { + return "DASH"; + } else if (this.isHLS(lower)) { + if (this.isHLSMaster(lower)) { + return "HLS_MASTER"; + } else { + return "HLS_PLAYLIST"; + } + } else if (this.isMSS(lower)) { + return "MSS"; + } + } +} + +(async () => { + if (typeof EventTarget !== "undefined") { + proxy( + EventTarget.prototype, + "addEventListener", + async (_target, _this, _args) => { + if (_this != null) { + const [type, listener] = _args; + + const storeKey = Symbol.for(getEventListeners); + if (!(storeKey in _this)) _this[storeKey] = {}; + + const store = _this[storeKey]; + if (!(type in store)) store[type] = []; + const listeners = store[type]; + + let wrappedListener = listener; + if ( + type === "message" && + !!listener && + !listener._isWrapped + ) { + wrappedListener = async function (event) { + if (event instanceof MediaKeyMessageEvent) { + if (event._isCustomEvent) { + if (listener.handleEvent) { + listener.handleEvent(event); + } else { + listener.call(this, event); + } + return; + } + + let newBody = new Uint8Array(event.message); + if ( + !compareUint8Arrays( + new Uint8Array([0x08, 0x04]), + new Uint8Array(event.message) + ) + ) { + console.log( + "[PlayreadyProxy]", + "PLAYREADY_PROXY", + "MESSAGE", + listener + ); + if (listener.name !== "messageHandler") { + const oldChallenge = uint8ArrayToBase64( + new Uint8Array(event.message) + ); + const newChallenge = + await emitAndWaitForResponse( + "REQUEST", + oldChallenge + ); + if (oldChallenge !== newChallenge) { + // Playback will fail if the challenges are the same (aka. the background script + // returned the same challenge because the addon is disabled), but I still + // override the challenge anyway, so check beforehand (in base64 form) + newBody = + base64toUint8Array( + newChallenge + ); + } + } else { + // trick EME Logger + // better suggestions for avoiding EME Logger interference are welcome + await emitAndWaitForResponse( + "REQUEST", + "" + ); + } + } + + const newEvent = new MediaKeyMessageEvent( + "message", + { + isTrusted: event.isTrusted, + bubbles: event.bubbles, + cancelBubble: event.cancelBubble, + composed: event.composed, + currentTarget: event.currentTarget, + defaultPrevented: + event.defaultPrevented, + eventPhase: event.eventPhase, + message: newBody.buffer, + messageType: event.messageType, + returnValue: event.returnValue, + srcElement: event.srcElement, + target: event.target, + timeStamp: event.timeStamp, + } + ); + newEvent._isCustomEvent = true; + + _this.dispatchEvent(newEvent); + event.stopImmediatePropagation(); + return; + } + + if (listener.handleEvent) { + listener.handleEvent(event); + } else { + listener.call(this, event); + } + }; + + wrappedListener._isWrapped = true; + wrappedListener.originalListener = listener; + } + + const alreadyAdded = listeners.some( + (storedListener) => + storedListener && + storedListener.originalListener === listener + ); + + if (!alreadyAdded) { + listeners.push(wrappedListener); + _args[1] = wrappedListener; + } + } + return _target.apply(_this, _args); + } + ); + } + + if (typeof MediaKeySession !== "undefined") { + proxy( + MediaKeySession.prototype, + "update", + async (_target, _this, _args) => { + const [response] = _args; + console.log("[PlayreadyProxy]", "PLAYREADY_PROXY", "UPDATE"); + await emitAndWaitForResponse( + "RESPONSE", + uint8ArrayToBase64(new Uint8Array(response)) + ); + return await _target.apply(_this, _args); + } + ); + } +})(); + +const originalFetch = window.fetch; +window.fetch = function () { + return new Promise(async (resolve, reject) => { + originalFetch + .apply(this, arguments) + .then((response) => { + if (response) { + response + .clone() + .text() + .then((text) => { + const manifest_type = + Evaluator.getManifestType(text); + if (manifest_type) { + if (arguments.length === 1) { + emitAndWaitForResponse( + "MANIFEST", + JSON.stringify({ + url: arguments[0].url, + type: manifest_type, + }) + ); + } else if (arguments.length === 2) { + emitAndWaitForResponse( + "MANIFEST", + JSON.stringify({ + url: arguments[0], + type: manifest_type, + }) + ); + } + } + resolve(response); + }) + .catch(() => { + resolve(response); + }); + } else { + resolve(response); + } + }) + .catch(() => { + resolve(); + }); + }); +}; + +const open = XMLHttpRequest.prototype.open; +XMLHttpRequest.prototype.open = function (method, url) { + this._method = method; + return open.apply(this, arguments); +}; + +const send = XMLHttpRequest.prototype.send; +XMLHttpRequest.prototype.send = function (postData) { + this.addEventListener("load", async function () { + if (this._method === "GET") { + let body = void 0; + switch (this.responseType) { + case "": + case "text": + body = this.responseText ?? this.response; + break; + case "json": + // TODO: untested + body = JSON.stringify(this.response); + break; + case "arraybuffer": + // TODO: untested + if (this.response.byteLength) { + const response = new Uint8Array(this.response); + body = uint8ArrayToString( + new Uint8Array([ + ...response.slice(0, 2000), + ...response.slice(-2000), + ]) + ); + } + break; + case "document": + // todo + break; + case "blob": + body = await this.response.text(); + break; + } + if (body) { + const manifest_type = Evaluator.getManifestType(body); + if (manifest_type) { + emitAndWaitForResponse( + "MANIFEST", + JSON.stringify({ + url: this.responseURL, + type: manifest_type, + }) + ); + } + } + } + }); + return send.apply(this, arguments); +}; diff --git a/manifest.json b/manifest.json new file mode 100644 index 0000000..58d075b --- /dev/null +++ b/manifest.json @@ -0,0 +1,48 @@ +{ + "manifest_version": 3, + "name": "PlayreadyProxy", + "version": "1.3.3.7", + "permissions": [ + "activeTab", + "tabs", + "storage", + "unlimitedStorage", + "webRequest" + ], + "host_permissions": ["*://*/*"], + "action": { + "default_popup": "panel/panel.html", + "default_icon": { + "128": "images/icon-128.png" + } + }, + "icons": { + "128": "images/icon-128.png" + }, + "background": { + "service_worker": "background.js", + "type": "module" + }, + "content_scripts": [ + { + "matches": [""], + "js": ["message_proxy.js"], + "run_at": "document_start", + "world": "ISOLATED", + "all_frames": true + }, + { + "matches": [""], + "js": ["content_script.js"], + "run_at": "document_start", + "world": "MAIN", + "all_frames": true + } + ], + "browser_specific_settings": { + "gecko": { + "id": "ThatNotEasy@Deny@DevLARLEY", + "strict_min_version": "58.0" + } + } +} diff --git a/message_proxy.js b/message_proxy.js new file mode 100644 index 0000000..3ba7b15 --- /dev/null +++ b/message_proxy.js @@ -0,0 +1,30 @@ +async function processMessage(detail) { + return new Promise((resolve, reject) => { + chrome.runtime.sendMessage( + { + type: detail.type, + body: detail.body, + }, + (response) => { + if (chrome.runtime.lastError) { + return reject(chrome.runtime.lastError); + } + resolve(response); + } + ); + }); +} + +document.addEventListener("response", async (event) => { + const { detail } = event; + try { + const responseData = await processMessage(detail); + const responseEvent = new CustomEvent("responseReceived", { + detail: detail.requestId.concat(responseData), + }); + document.dispatchEvent(responseEvent); + } catch (error) { + console.error("Error processing message:", error); + // Optionally handle the error, maybe notify the user + } +}); diff --git a/remote_cdm.js b/remote_cdm.js new file mode 100644 index 0000000..8bcfc58 --- /dev/null +++ b/remote_cdm.js @@ -0,0 +1,77 @@ +export class RemoteCdm { + constructor(security_level, host, secret, device_name, proxy = null) { + this.security_level = security_level; + this.host = host; + this.secret = secret; + this.device_name = device_name; + this.proxy = proxy; // Optional proxy parameter + } + + static from_object(obj) { + return new RemoteCdm( + obj.security_level, + obj.host, + obj.secret, + obj.device_name ?? obj.name, + obj.proxy ?? null // Handle proxy from object if present + ); + } + + get_name() { + const type = this.security_level; + return `[${type}] ${this.host}/${this.device_name}`; + } + + async fetch_with_proxy(url, options) { + // If proxy is set, prepend proxy URL to the original host URL + if (this.proxy) { + options.headers = { + ...options.headers, + 'X-Forwarded-For': this.proxy, // Optional: Forward the proxy information + }; + url = `${this.proxy}${url}`; + } + const response = await fetch(url, options); + return response; + } + + async get_license_challenge(pssh) { + const license_request = await this.fetch_with_proxy(`${this.host}/api/playready/extension`, { + method: "POST", + headers: { + "content-type": "application/json", + "X-API-KEY": this.secret, + }, + body: JSON.stringify({ + action: "Challenge?", + pssh: pssh, + }), + }); + console.log( + "[PlayreadyProxy]", + "REMOTE_CDM", + "GET_LICENSE_CHALLENGE", + license_request.status + ); + const license_request_json = await license_request.json(); + + return license_request_json.data; + } + + async get_keys(license_challenge) { + const keys = await this.fetch_with_proxy(`${this.host}/api/playready/extension`, { + method: "POST", + headers: { + "content-type": "application/json", + "X-API-KEY": this.secret, + }, + body: JSON.stringify({ + action: "Keys?", + license: license_challenge, + }), + }); + console.log("[PlayreadyProxy]", "REMOTE_CDM", "GET_KEYS", keys.status); + + return await keys.json(); + } +} \ No newline at end of file diff --git a/util.js b/util.js new file mode 100644 index 0000000..b362574 --- /dev/null +++ b/util.js @@ -0,0 +1,437 @@ +import { RemoteCdm } from "./remote_cdm.js"; + +export class AsyncSyncStorage { + static async setStorage(items) { + return new Promise((resolve, reject) => { + chrome.storage.sync.set(items, () => { + if (chrome.runtime.lastError) { + reject(new Error(chrome.runtime.lastError)); + } else { + resolve(); + } + }); + }); + } + + static async getStorage(keys) { + return new Promise((resolve, reject) => { + chrome.storage.sync.get(keys, (result) => { + if (chrome.runtime.lastError) { + reject(new Error(chrome.runtime.lastError)); + } else { + resolve(result); + } + }); + }); + } + + static async removeStorage(keys) { + return new Promise((resolve, reject) => { + chrome.storage.sync.remove(keys, (result) => { + if (chrome.runtime.lastError) { + reject(new Error(chrome.runtime.lastError)); + } else { + resolve(result); + } + }); + }); + } +} + +export class AsyncLocalStorage { + static async setStorage(items) { + return new Promise((resolve, reject) => { + chrome.storage.local.set(items, () => { + if (chrome.runtime.lastError) { + reject(new Error(chrome.runtime.lastError)); + } else { + resolve(); + } + }); + }); + } + + static async getStorage(keys) { + return new Promise((resolve, reject) => { + chrome.storage.local.get(keys, (result) => { + if (chrome.runtime.lastError) { + reject(new Error(chrome.runtime.lastError)); + } else { + resolve(result); + } + }); + }); + } + + static async removeStorage(keys) { + return new Promise((resolve, reject) => { + chrome.storage.local.remove(keys, (result) => { + if (chrome.runtime.lastError) { + reject(new Error(chrome.runtime.lastError)); + } else { + resolve(result); + } + }); + }); + } +} + +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]); + } + + static async removeSelectedRemoteCDMKey() { + await AsyncSyncStorage.removeStorage(["selected_remote_cdm"]); + } +} + +export class SettingsManager { + static async setEnabled(enabled) { + await AsyncSyncStorage.setStorage({ enabled: enabled }); + } + + static async getEnabled() { + const result = await AsyncSyncStorage.getStorage(["enabled"]); + return result["enabled"] === undefined ? false : result["enabled"]; + } + + static downloadFile(content, filename) { + const blob = new Blob([content], { type: "application/octet-stream" }); + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = filename; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + } + + static async saveDarkMode(dark_mode) { + await AsyncSyncStorage.setStorage({ dark_mode: dark_mode }); + } + + static async getDarkMode() { + const result = await AsyncSyncStorage.getStorage(["dark_mode"]); + return result["dark_mode"] || false; + } + + 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.security_level, + json_file.host, + json_file.key, + json_file.device_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 async getSelectedDeviceType_PRD() { + const result = await AsyncSyncStorage.getStorage(["device_type"]); + return result["device_type"] || "PRD"; + } + + static async saveUseShakaPackager(use_shaka) { + await AsyncSyncStorage.setStorage({ use_shaka: use_shaka }); + } + + static async saveUseDDownloader(use_ddownloader) { + await AsyncSyncStorage.setStorage({ use_ddownloader: use_ddownloader }); + } + + static async getUseShakaPackager() { + const result = await AsyncSyncStorage.getStorage(["use_shaka"]); + return result["use_shaka"] ?? true; + } + + static async getUseDDownloader() { + const result = await AsyncSyncStorage.getStorage(["use_ddownloader"]); + return result["use_ddownloader"] ?? true; + } + + static async saveExecutableName(exe_name) { + await AsyncSyncStorage.setStorage({ exe_name: exe_name }); + } + + static async getExecutableName() { + const result = await AsyncSyncStorage.getStorage(["exe_name"]); + return result["exe_name"] ?? "DDownloader"; + } + + // Proxy methods + static async setProxy(proxyAddress) { + await AsyncSyncStorage.setStorage({ proxyAddress: proxyAddress }); + } + + // Get the proxy address + static async getProxy() { + const result = await AsyncSyncStorage.getStorage(["proxyAddress"]); + return result["proxyAddress"] || null; // Ensure null is returned if proxyAddress is not found + } + + // Save the proxy enabled status (whether proxy is on or off) + static async setProxyEnabled(enabled) { + await AsyncSyncStorage.setStorage({ proxyEnabled: enabled }); + } + + // Get the proxy enabled status + static async getProxyEnabled() { + const result = await AsyncSyncStorage.getStorage(["proxyEnabled"]); + return result["proxyEnabled"] !== undefined ? result["proxyEnabled"] : false; // Default to false if not found + } + + // Save the proxy port + static async saveProxy(port) { + await AsyncSyncStorage.setStorage({ proxyPort: port }); + } + + // Get the proxy port + static async getProxyPort() { + const result = await AsyncSyncStorage.getStorage(["proxyPort"]); + return result["proxyPort"] || null; // Return null if proxyPort is not found + } + + // Save both the proxy address and port together + static async saveProxyConfig(proxyAddress) { + await SettingsManager.setProxy(proxyAddress); // Save the proxy address + const proxyPort = proxyAddress.split(":")[1]; // Extract the port from the address if available + if (proxyPort) { + await SettingsManager.saveProxy(proxyPort); // Save the port if it's present + } else { + await SettingsManager.saveProxy(""); // Clear the port if not available + } + } + + // Get both proxy URL and port together (optional convenience method) + static async getProxyConfig() { + const proxyUrl = await SettingsManager.getProxy(); + const proxyPort = await SettingsManager.getProxyPort(); + return proxyUrl && proxyPort ? `${proxyUrl}:${proxyPort}` : proxyUrl || ''; + } +} + + +export function intToUint8Array(num) { + const buffer = new ArrayBuffer(4); + const view = new DataView(buffer); + view.setUint32(0, num, false); + return new Uint8Array(buffer); +} + +export function compareUint8Arrays(arr1, arr2) { + if (arr1.length !== arr2.length) return false; + return Array.from(arr1).every((value, index) => value === arr2[index]); +} + +export function uint8ArrayToHex(buffer) { + return Array.prototype.map + .call(buffer, (x) => x.toString(16).padStart(2, "0")) + .join(""); +} + +export function uint8ArrayToString(uint8array) { + return String.fromCharCode.apply(null, uint8array); +} + +export function uint8ArrayToBase64(uint8array) { + return btoa(String.fromCharCode.apply(null, uint8array)); +} + +export function stringToUint8Array(string) { + return Uint8Array.from(string.split("").map((x) => x.charCodeAt())); +} + +export function stringToHex(string) { + return string + .split("") + .map((c) => c.charCodeAt(0).toString(16).padStart(2, "0")) + .join(""); +} + +export function PSSHFromKID(kidBase64) { + const kidBytes = base64toUint8Array(kidBase64); + + const kidBase64ForHeader = btoa(String.fromCharCode(...kidBytes)); + const wrmHeaderXml = + `16AESCTR${kidBase64ForHeader}`.trim(); + + const wrmHeaderBytes = stringToUTF16LEBytes(wrmHeaderXml); + + const playReadyObjectLength = 2 + 2 + wrmHeaderBytes.length; + const playReadyObjectBuffer = new ArrayBuffer(playReadyObjectLength); + const playReadyObjectView = new DataView(playReadyObjectBuffer); + let offset = 0; + + playReadyObjectView.setUint16(offset, 0x0001, true); + offset += 2; + + playReadyObjectView.setUint16(offset, wrmHeaderBytes.length, true); + offset += 2; + + new Uint8Array(playReadyObjectBuffer).set(wrmHeaderBytes, offset); + + const recordCount = 1; + const recordListLength = 2 + playReadyObjectLength; + const recordListBuffer = new ArrayBuffer(recordListLength); + const recordListView = new DataView(recordListBuffer); + offset = 0; + + recordListView.setUint16(offset, recordCount, true); + offset += 2; + + new Uint8Array(recordListBuffer).set( + new Uint8Array(playReadyObjectBuffer), + offset + ); + + const systemIDHex = "9a04f07998404286ab92e65be0885f95"; + const systemIDBytes = hexStringToUint8Array(systemIDHex); + + const psshSize = 4 + 4 + 4 + 16 + 4 + recordListLength; + const psshBuffer = new ArrayBuffer(psshSize); + const psshView = new DataView(psshBuffer); + const psshUint8Array = new Uint8Array(psshBuffer); + offset = 0; + + psshView.setUint32(offset, psshSize, false); + offset += 4; + + psshUint8Array.set([0x70, 0x73, 0x73, 0x68], offset); + offset += 4; + + psshView.setUint32(offset, 0, false); + offset += 4; + + psshUint8Array.set(systemIDBytes, offset); + offset += 16; + + psshView.setUint32(offset, recordListLength, false); + offset += 4; + + psshUint8Array.set(new Uint8Array(recordListBuffer), offset); + + return uint8ArrayToBase64(psshUint8Array); +} + +export function base64toUint8Array(base64_string) { + return Uint8Array.from(atob(base64_string), (c) => c.charCodeAt(0)); +} + +export function stringToUTF16LEBytes(str) { + const bytes = new Uint8Array(str.length * 2); + for (let i = 0; i < str.length; i++) { + const code = str.charCodeAt(i); + bytes[i * 2] = code & 0xff; + bytes[i * 2 + 1] = (code >> 8) & 0xff; + } + return bytes; +} + +export function hexStringToUint8Array(hexString) { + const bytes = new Uint8Array(hexString.length / 2); + for (let i = 0; i < bytes.length; i++) { + bytes[i] = parseInt(hexString.substr(i * 2, 2), 16); + } + return bytes; +}