mirror of
https://github.com/ThatNotEasy/PlayReadyProxy.git
synced 2026-04-02 10:38:18 +00:00
Add files via upload
This commit is contained in:
committed by
GitHub
parent
3bc500dba2
commit
2e84bd28ce
284
background.js
Normal file
284
background.js
Normal file
@@ -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: ["<all_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 = /<Challenge[^>]*>([\s\S]*?)<\/Challenge>/i;
|
||||
const challengeMatch = challengeRegex.exec(xmlString);
|
||||
var encoding;
|
||||
|
||||
if (challengeMatch) {
|
||||
const challengeContent = challengeMatch[1].trim();
|
||||
const encodingRegex = /<Challenge[^>]*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>([^<]+)<\/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(
|
||||
/(<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);
|
||||
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;
|
||||
});
|
||||
338
content_script.js
Normal file
338
content_script.js
Normal file
@@ -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("<mpd") && text.includes("</mpd>");
|
||||
}
|
||||
|
||||
static isHLS(text) {
|
||||
return text.includes("#extm3u");
|
||||
}
|
||||
|
||||
static isHLSMaster(text) {
|
||||
return text.includes("#ext-x-stream-inf");
|
||||
}
|
||||
|
||||
static isMSS(text) {
|
||||
return (
|
||||
text.includes("<smoothstreamingmedia") &&
|
||||
text.includes("</smoothstreamingmedia>")
|
||||
);
|
||||
}
|
||||
|
||||
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);
|
||||
};
|
||||
48
manifest.json
Normal file
48
manifest.json
Normal file
@@ -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": ["<all_urls>"],
|
||||
"js": ["message_proxy.js"],
|
||||
"run_at": "document_start",
|
||||
"world": "ISOLATED",
|
||||
"all_frames": true
|
||||
},
|
||||
{
|
||||
"matches": ["<all_urls>"],
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
30
message_proxy.js
Normal file
30
message_proxy.js
Normal file
@@ -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
|
||||
}
|
||||
});
|
||||
77
remote_cdm.js
Normal file
77
remote_cdm.js
Normal file
@@ -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();
|
||||
}
|
||||
}
|
||||
437
util.js
Normal file
437
util.js
Normal file
@@ -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 =
|
||||
`<WRMHEADER xmlns="http://schemas.microsoft.com/DRM/2007/03/PlayReadyHeader" version="4.0.0.0"><DATA><PROTECTINFO><KEYLEN>16</KEYLEN><ALGID>AESCTR</ALGID></PROTECTINFO><KID>${kidBase64ForHeader}</KID></DATA></WRMHEADER>`.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;
|
||||
}
|
||||
Reference in New Issue
Block a user