diff --git a/background.js b/background.js index a3d64d5..4a1472b 100644 --- a/background.js +++ b/background.js @@ -2,6 +2,7 @@ import { uint8ArrayToBase64, SettingsManager, RemoteCDMManager, + LocalCDMManager, PSSHFromKID, stringToUTF16LEBytes, } from "./util.js"; @@ -9,6 +10,7 @@ import { RemoteCdm } from "./remote_cdm.js"; let manifests = new Map(); let requests = new Map(); +const sessions = new Map(); let logs = []; chrome.webRequest.onBeforeSendHeaders.addListener( @@ -46,7 +48,7 @@ chrome.webRequest.onBeforeSendHeaders.addListener( async function generateChallengeRemote(body, sendResponse) { try { // Decode the base64-encoded body into a binary string - const binaryString = decodeBase64(body); // Use the decodeBase64 function + const binaryString = decodeBase64(body); const byteArray = new Uint8Array(binaryString.length); for (let i = 0; i < binaryString.length; i++) { byteArray[i] = binaryString.charCodeAt(i); @@ -54,13 +56,13 @@ async function generateChallengeRemote(body, sendResponse) { // Decode using UTF-16LE encoding const decoder = new TextDecoder("utf-16le"); - var xmlString = decoder.decode(byteArray); - var xmlDecoded; + let xmlString = decoder.decode(byteArray); + let xmlDecoded; // Extract the Challenge element from the XML string const challengeRegex = /]*>([\s\S]*?)<\/Challenge>/i; const challengeMatch = challengeRegex.exec(xmlString); - var encoding; + let encoding; if (challengeMatch) { const challengeContent = challengeMatch[1].trim(); @@ -70,7 +72,7 @@ async function generateChallengeRemote(body, sendResponse) { // If encoding is base64encoded, decode the challenge content if (encoding === "base64encoded") { - const challengeBinaryString = decodeBase64(challengeContent); // Use the decodeBase64 function + const challengeBinaryString = decodeBase64(challengeContent); const challengeByteArray = new Uint8Array(challengeBinaryString.length); for (let i = 0; i < challengeBinaryString.length; i++) { challengeByteArray[i] = challengeBinaryString.charCodeAt(i); @@ -87,11 +89,11 @@ async function generateChallengeRemote(body, sendResponse) { // Extract the KID element const kidRegex = /([^<]+)<\/KID>/i; const kidMatch = kidRegex.exec(xmlDecoded); - var kidBase64; + let kidBase64; if (kidMatch) { kidBase64 = kidMatch[1].trim(); } else { - console.log("[PlayreadyProxy]", "NO_KID_IN_CHALLENGE"); + console.log("[PlayReadyProxy]", "NO_KID_IN_CHALLENGE"); sendResponse(body); return; } @@ -99,7 +101,7 @@ async function generateChallengeRemote(body, sendResponse) { // Get PSSH from KID const pssh = PSSHFromKID(kidBase64); if (!pssh) { - console.log("[PlayreadyProxy]", "NO_PSSH_DATA_IN_CHALLENGE"); + console.log("[PlayReadyProxy]", "NO_PSSH_DATA_IN_CHALLENGE"); sendResponse(body); return; } @@ -117,14 +119,11 @@ async function generateChallengeRemote(body, sendResponse) { 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 decodedString = decodeBase64(selected_remote_cdm); const parser = new DOMParser(); - const xmlDoc = parseXML(decodedString); // Use parseXML function - - // Convert the XML document into a RemoteCdm object + const xmlDoc = parseXML(decodedString); remoteCdmObj = RemoteCdm.from_xml(xmlDoc); } else { - // Otherwise, parse as JSON remoteCdmObj = JSON.parse(selected_remote_cdm); } } catch (e) { @@ -134,9 +133,16 @@ async function generateChallengeRemote(body, sendResponse) { } const remote_cdm = RemoteCdm.from_object(remoteCdmObj); + const session_id = await remote_cdm.open(); + if (!session_id) { + console.error("[PlayReadyProxy] Failed to open session."); + sendResponse(body); + return; + } - // Get the license challenge - const challenge = await remote_cdm.get_license_challenge(pssh); + console.log("[PlayReadyProxy]", "SESSION_ID", session_id); + + const challenge = await remote_cdm.get_license_challenge(session_id, pssh); // Replace the challenge content in the original XML with the new challenge const newXmlString = xmlString.replace( @@ -158,45 +164,72 @@ async function generateChallengeRemote(body, sendResponse) { async function parseLicenseRemote(body, sendResponse, tab_url) { - const response = atob(body); + try { + const license_b64 = body; // License message is already Base64-encoded - const selected_remote_cdm_name = - await RemoteCDMManager.getSelectedRemoteCDM(); - if (!selected_remote_cdm_name) { - sendResponse(); - return; - } + // Fetch the selected remote CDM and load it + const selected_remote_cdm_name = await RemoteCDMManager.getSelectedRemoteCDM(); + if (!selected_remote_cdm_name) { + console.error("[PlayReadyProxy] No remote CDM selected."); + 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 selected_remote_cdm = await RemoteCDMManager.loadRemoteCDM(selected_remote_cdm_name); + let remoteCdmObj; - const returned_keys = await remote_cdm.get_keys(btoa(response)); + try { + remoteCdmObj = JSON.parse(selected_remote_cdm); + } catch (e) { + console.error("[PlayReadyProxy] Error parsing remote CDM JSON:", e); + sendResponse(); + return; + } - if (returned_keys.length === 0) { - sendResponse(); - return; - } + const remote_cdm = RemoteCdm.from_object(remoteCdmObj); + const session_id = await remote_cdm.open(); + if (!session_id) { + console.error("[PlayReadyProxy] Failed to open session."); + sendResponse(); + return; + } - const keys = returned_keys.map((s) => { - return { + console.log("[PlayReadyProxy]", "SESSION_ID", session_id); + + // Fetch keys using get_keys(session_id, license_b64) + const returned_keys = await remote_cdm.get_keys(session_id, license_b64); + if (!returned_keys || returned_keys.length === 0) { + console.log("[PlayReadyProxy] No keys returned."); + sendResponse(); + return; + } + + // Format the keys correctly + const keys = returned_keys.map((s) => ({ k: s.key, kid: s.key_id, + })); + + console.log("[PlayReadyProxy]", "KEYS", JSON.stringify(keys), tab_url); + + // Store log data + 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); - console.log("[PlayreadyProxy]", "KEYS", JSON.stringify(keys), tab_url); + // Close the session after key retrieval + await remote_cdm.close(session_id); - 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(); + sendResponse(); + } catch (error) { + console.error("[PlayReadyProxy] Error in parseLicenseRemote:", error); + sendResponse(); + } } chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { @@ -246,6 +279,14 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { case "GET_LOGS": sendResponse(logs); break; + case "OPEN_PICKER_LOCAL": + chrome.windows.create({ + url: "picker/filePickerLocal.html", + type: "popup", + width: 300, + height: 200, + }); + break; case "OPEN_PICKER": chrome.windows.create({ url: "picker/filePicker.html", @@ -281,4 +322,4 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { } })(); return true; -}); +}); \ No newline at end of file diff --git a/remote_cdm.js b/remote_cdm.js index 8bcfc58..de8bfec 100644 --- a/remote_cdm.js +++ b/remote_cdm.js @@ -4,7 +4,7 @@ export class RemoteCdm { this.host = host; this.secret = secret; this.device_name = device_name; - this.proxy = proxy; // Optional proxy parameter + this.proxy = proxy; } static from_object(obj) { @@ -13,65 +13,128 @@ export class RemoteCdm { obj.host, obj.secret, obj.device_name ?? obj.name, - obj.proxy ?? null // Handle proxy from object if present + obj.proxy ?? null ); } get_name() { - const type = this.security_level; - return `[${type}] ${this.host}/${this.device_name}`; + return `[PlayReady] ${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 + "X-Forwarded-For": this.proxy, }; url = `${this.proxy}${url}`; } - const response = await fetch(url, options); - return response; + + console.log(`[PlayReadyProxy] Fetching: ${options.method} ${url}`); + console.log(`[PlayReadyProxy] Headers:`, options.headers); + if (options.body) console.log(`[PlayReadyProxy] Body:`, options.body); + + try { + const response = await fetch(url, options); + console.log(`[PlayReadyProxy] Response Status: ${response.status}`); + + if (!response.ok) { + console.error(`[PlayReadyProxy] Request failed: ${url} [${response.status}]`); + const errorText = await response.text(); + console.error(`[PlayReadyProxy] Error Response:`, errorText); + throw new Error(`Request failed with status ${response.status}`); + } + + return response; + } catch (error) { + console.error(`[PlayReadyProxy] Network Error:`, error); + throw error; + } } - 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 + async open() { + console.log("[PlayReadyProxy] Opening PlayReady session..."); + + const open_request = await this.fetch_with_proxy( + `${this.host}/api/playready/${this.device_name}/open`, + { + method: "GET", + headers: { "X-API-KEY": this.secret }, + } ); - const license_request_json = await license_request.json(); - return license_request_json.data; + console.log("[PlayReadyProxy]", "REMOTE_CDM", "OPEN", open_request.status); + const response = await open_request.json(); + console.log("[PlayReadyProxy] Open Response:", response); + + return response.responseData.session_id; } - 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); + async close(session_id) { + console.log("[PlayReadyProxy] Closing PlayReady session:", session_id); - return await keys.json(); + await this.fetch_with_proxy( + `${this.host}/api/playready/${this.device_name}/close/${session_id}`, + { + method: "GET", + headers: { "X-API-KEY": this.secret }, + } + ); + + console.log("[PlayReadyProxy]", "REMOTE_CDM", "CLOSE", session_id); } -} \ No newline at end of file + + async get_license_challenge(session_id, pssh) { + console.log("[PlayReadyProxy] Requesting License Challenge..."); + console.log("[PlayReadyProxy] SESSION_ID:", session_id); + console.log("[PlayReadyProxy] PSSH:", pssh); + + const license_request = await this.fetch_with_proxy( + `${this.host}/${this.device_name}/get_challenge`, + { + method: "POST", + headers: { + "content-type": "application/json", + "X-API-KEY": this.secret, + }, + body: JSON.stringify({ + session_id: session_id, + pssh: pssh, + }), + } + ); + + console.log("[PlayReadyProxy]", "REMOTE_CDM", "GET_LICENSE_CHALLENGE", license_request.status); + const response = await license_request.json(); + console.log("[PlayReadyProxy] License Challenge Response:", response); + + return response.responseData.challenge_b64; + } + + async get_keys(session_id, license_b64) { + console.log("[PlayReadyProxy] Requesting Decryption Keys..."); + console.log("[PlayReadyProxy] SESSION_ID:", session_id); + console.log("[PlayReadyProxy] License (Base64):", license_b64); + + const keys_request = await this.fetch_with_proxy( + `${this.host}/api/playready/${this.device_name}/get_keys`, + { + method: "POST", + headers: { + "content-type": "application/json", + "X-API-KEY": this.secret, + }, + body: JSON.stringify({ + session_id: session_id, + license_b64: license_b64, + }), + } + ); + + console.log("[PlayReadyProxy]", "REMOTE_CDM", "GET_KEYS", keys_request.status); + const response = await keys_request.json(); + console.log("[PlayReadyProxy] Keys Response:", response); + + return response.responseData.keys; + } +}