This commit is contained in:
Pari Malam
2025-02-24 23:17:25 +08:00
parent 0bda8c12b3
commit a8f5089927
2 changed files with 191 additions and 87 deletions

View File

@@ -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 = /<Challenge[^>]*>([\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>([^<]+)<\/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;
});
});

View File

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