mirror of
https://github.com/ThatNotEasy/PlayReadyProxy.git
synced 2026-04-02 10:38:18 +00:00
released
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
import {
|
||||
decodeBase64,
|
||||
uint8ArrayToBase64,
|
||||
SettingsManager,
|
||||
RemoteCDMManager,
|
||||
LocalCDMManager,
|
||||
PSSHFromKID,
|
||||
stringToUTF16LEBytes,
|
||||
} from "./util.js";
|
||||
@@ -13,6 +13,7 @@ let requests = new Map();
|
||||
const sessions = new Map();
|
||||
let logs = [];
|
||||
|
||||
|
||||
chrome.webRequest.onBeforeSendHeaders.addListener(
|
||||
function (details) {
|
||||
if (details.method === "GET") {
|
||||
@@ -45,21 +46,25 @@ chrome.webRequest.onBeforeSendHeaders.addListener(
|
||||
|
||||
|
||||
|
||||
async function generateChallengeRemote(body, sendResponse) {
|
||||
async function generateChallengeRemote(body, sendResponse, tab_url) {
|
||||
try {
|
||||
// Decode the base64-encoded body into a binary string
|
||||
console.log("[PlayReadyProxy] generateChallengeRemote called with tab_url:", tab_url);
|
||||
if (!tab_url) {
|
||||
console.error("[PlayReadyProxy] No tab_url provided, cannot store session");
|
||||
sendResponse(body);
|
||||
return;
|
||||
}
|
||||
|
||||
const binaryString = decodeBase64(body);
|
||||
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");
|
||||
let xmlString = decoder.decode(byteArray);
|
||||
let xmlDecoded;
|
||||
|
||||
// Extract the Challenge element from the XML string
|
||||
const challengeRegex = /<Challenge[^>]*>([\s\S]*?)<\/Challenge>/i;
|
||||
const challengeMatch = challengeRegex.exec(xmlString);
|
||||
let encoding;
|
||||
@@ -70,7 +75,6 @@ async function generateChallengeRemote(body, sendResponse) {
|
||||
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);
|
||||
const challengeByteArray = new Uint8Array(challengeBinaryString.length);
|
||||
@@ -86,7 +90,6 @@ async function generateChallengeRemote(body, sendResponse) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Extract the KID element
|
||||
const kidRegex = /<KID>([^<]+)<\/KID>/i;
|
||||
const kidMatch = kidRegex.exec(xmlDecoded);
|
||||
let kidBase64;
|
||||
@@ -98,7 +101,6 @@ async function generateChallengeRemote(body, sendResponse) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get PSSH from KID
|
||||
const pssh = PSSHFromKID(kidBase64);
|
||||
if (!pssh) {
|
||||
console.log("[PlayReadyProxy]", "NO_PSSH_DATA_IN_CHALLENGE");
|
||||
@@ -106,7 +108,6 @@ async function generateChallengeRemote(body, sendResponse) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Fetch the selected remote CDM and load it
|
||||
const selected_remote_cdm_name = await RemoteCDMManager.getSelectedRemoteCDM();
|
||||
if (!selected_remote_cdm_name) {
|
||||
sendResponse(body);
|
||||
@@ -117,15 +118,8 @@ async function generateChallengeRemote(body, sendResponse) {
|
||||
|
||||
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);
|
||||
const parser = new DOMParser();
|
||||
const xmlDoc = parseXML(decodedString);
|
||||
remoteCdmObj = RemoteCdm.from_xml(xmlDoc);
|
||||
} else {
|
||||
remoteCdmObj = JSON.parse(selected_remote_cdm);
|
||||
}
|
||||
// Assume the remote CDM is always in JSON format
|
||||
remoteCdmObj = JSON.parse(selected_remote_cdm);
|
||||
} catch (e) {
|
||||
console.error("Error parsing remote CDM:", e);
|
||||
sendResponse(body);
|
||||
@@ -141,20 +135,18 @@ async function generateChallengeRemote(body, sendResponse) {
|
||||
}
|
||||
|
||||
console.log("[PlayReadyProxy]", "SESSION_ID", session_id);
|
||||
console.log("[PlayReadyProxy] Storing session_id", session_id, "for tab_url", tab_url);
|
||||
sessions.set(tab_url, 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(
|
||||
/(<Challenge[^>]*>)([\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);
|
||||
@@ -165,9 +157,17 @@ async function generateChallengeRemote(body, sendResponse) {
|
||||
|
||||
async function parseLicenseRemote(body, sendResponse, tab_url) {
|
||||
try {
|
||||
const license_b64 = body; // License message is already Base64-encoded
|
||||
console.log("[PlayReadyProxy] parseLicenseRemote called with tab_url:", tab_url);
|
||||
if (!tab_url) {
|
||||
console.error("[PlayReadyProxy] No tab_url provided, cannot retrieve session");
|
||||
sendResponse();
|
||||
return;
|
||||
}
|
||||
|
||||
const license_b64 = body;
|
||||
console.log("[PlayReadyProxy] Current sessions:",
|
||||
[...sessions.entries()].map(([url, id]) => ({ url, id })));
|
||||
|
||||
// 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.");
|
||||
@@ -187,16 +187,17 @@ async function parseLicenseRemote(body, sendResponse, tab_url) {
|
||||
}
|
||||
|
||||
const remote_cdm = RemoteCdm.from_object(remoteCdmObj);
|
||||
const session_id = await remote_cdm.open();
|
||||
if (!session_id) {
|
||||
console.error("[PlayReadyProxy] Failed to open session.");
|
||||
if (!sessions.has(tab_url)) {
|
||||
console.error("[PlayReadyProxy] No previous session found for URL:", tab_url);
|
||||
|
||||
console.error("[PlayReadyProxy] Cannot process license without challenge first");
|
||||
sendResponse();
|
||||
return;
|
||||
}
|
||||
|
||||
const session_id = sessions.get(tab_url);
|
||||
console.log("[PlayReadyProxy] Using existing SESSION_ID", session_id, "for tab_url", tab_url);
|
||||
|
||||
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.");
|
||||
@@ -204,7 +205,6 @@ async function parseLicenseRemote(body, sendResponse, tab_url) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Format the keys correctly
|
||||
const keys = returned_keys.map((s) => ({
|
||||
k: s.key,
|
||||
kid: s.key_id,
|
||||
@@ -212,7 +212,6 @@ async function parseLicenseRemote(body, sendResponse, tab_url) {
|
||||
|
||||
console.log("[PlayReadyProxy]", "KEYS", JSON.stringify(keys), tab_url);
|
||||
|
||||
// Store log data
|
||||
const log = {
|
||||
type: "PLAYREADY",
|
||||
keys: keys,
|
||||
@@ -222,8 +221,8 @@ async function parseLicenseRemote(body, sendResponse, tab_url) {
|
||||
};
|
||||
logs.push(log);
|
||||
|
||||
// Close the session after key retrieval
|
||||
await remote_cdm.close(session_id);
|
||||
sessions.delete(tab_url);
|
||||
|
||||
sendResponse();
|
||||
} catch (error) {
|
||||
@@ -235,6 +234,7 @@ async function parseLicenseRemote(body, sendResponse, tab_url) {
|
||||
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||
(async () => {
|
||||
const tab_url = sender.tab ? sender.tab.url : null;
|
||||
console.log("[PlayReadyProxy] Received message type:", message.type, "for tab_url:", tab_url);
|
||||
|
||||
switch (message.type) {
|
||||
case "REQUEST":
|
||||
@@ -252,7 +252,8 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||
if (message.body) {
|
||||
await generateChallengeRemote(
|
||||
message.body,
|
||||
sendResponse
|
||||
sendResponse,
|
||||
tab_url
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -269,6 +270,7 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||
await parseClearKey(message.body, sendResponse, tab_url);
|
||||
return;
|
||||
} catch (e) {
|
||||
console.log("[PlayReadyProxy] parseClearKey failed, trying parseLicenseRemote", e);
|
||||
await parseLicenseRemote(
|
||||
message.body,
|
||||
sendResponse,
|
||||
@@ -298,6 +300,7 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||
case "CLEAR":
|
||||
logs = [];
|
||||
manifests.clear();
|
||||
sessions.clear();
|
||||
break;
|
||||
case "MANIFEST":
|
||||
const parsed = JSON.parse(message.body);
|
||||
|
||||
@@ -21,10 +21,7 @@ function emitAndWaitForResponse(type, data) {
|
||||
const responseHandler = (event) => {
|
||||
const { detail } = event;
|
||||
if (detail.substring(0, 7) === requestId) {
|
||||
document.removeEventListener(
|
||||
"responseReceived",
|
||||
responseHandler
|
||||
);
|
||||
document.removeEventListener("responseReceived", responseHandler);
|
||||
resolve(detail.substring(7));
|
||||
}
|
||||
};
|
||||
@@ -37,6 +34,7 @@ function emitAndWaitForResponse(type, data) {
|
||||
},
|
||||
});
|
||||
document.dispatchEvent(requestEvent);
|
||||
console.log(`[ContentScript] Dispatched event: ${type}`);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -146,17 +144,12 @@ class Evaluator {
|
||||
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",
|
||||
""
|
||||
@@ -298,11 +291,9 @@ XMLHttpRequest.prototype.send = function (postData) {
|
||||
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(
|
||||
@@ -313,9 +304,6 @@ XMLHttpRequest.prototype.send = function (postData) {
|
||||
);
|
||||
}
|
||||
break;
|
||||
case "document":
|
||||
// todo
|
||||
break;
|
||||
case "blob":
|
||||
body = await this.response.text();
|
||||
break;
|
||||
@@ -335,4 +323,4 @@ XMLHttpRequest.prototype.send = function (postData) {
|
||||
}
|
||||
});
|
||||
return send.apply(this, arguments);
|
||||
};
|
||||
};
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 200 KiB After Width: | Height: | Size: 184 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 247 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 200 KiB After Width: | Height: | Size: 184 KiB |
@@ -45,4 +45,4 @@
|
||||
"strict_min_version": "58.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -27,4 +27,4 @@ document.addEventListener("response", async (event) => {
|
||||
console.error("Error processing message:", error);
|
||||
// Optionally handle the error, maybe notify the user
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,5 @@
|
||||
import {
|
||||
AsyncLocalStorage,
|
||||
base64toUint8Array,
|
||||
stringToUint8Array,
|
||||
RemoteCDMManager,
|
||||
SettingsManager,
|
||||
@@ -219,11 +218,21 @@ chrome.storage.onChanged.addListener(async (changes, areaName) => {
|
||||
|
||||
function checkLogs() {
|
||||
chrome.runtime.sendMessage({ type: "GET_LOGS" }, (response) => {
|
||||
if (response) {
|
||||
response.forEach(async (result) => {
|
||||
await appendLog(result);
|
||||
});
|
||||
console.log("[DEBUG] Received logs response:", response);
|
||||
|
||||
if (!response) {
|
||||
console.error("[ERROR] No response received from GET_LOGS.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Array.isArray(response)) {
|
||||
console.error("[ERROR] Unexpected response format. Expected an array but got:", response);
|
||||
return;
|
||||
}
|
||||
|
||||
response.forEach(async (result) => {
|
||||
await appendLog(result);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -88,9 +88,9 @@ export class RemoteCdm {
|
||||
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`,
|
||||
`${this.host}/api/playready/${this.device_name}/get_challenge`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
@@ -103,11 +103,11 @@ export class RemoteCdm {
|
||||
}),
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -137,4 +137,4 @@ export class RemoteCdm {
|
||||
|
||||
return response.responseData.keys;
|
||||
}
|
||||
}
|
||||
}
|
||||
20
util.js
20
util.js
@@ -435,3 +435,23 @@ export function hexStringToUint8Array(hexString) {
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
export function decodeBase64(base64String) {
|
||||
try {
|
||||
const decodedData = atob(base64String);
|
||||
return decodedData;
|
||||
} catch (error) {
|
||||
console.error("Invalid Base64 string", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function parseXML(xmlStr) {
|
||||
try {
|
||||
const parser = new DOMParser();
|
||||
return parser.parseFromString(xmlStr, "text/xml");
|
||||
} catch (e) {
|
||||
console.error("Error parsing XML:", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user