Files
WidevineProxy2/content_script.js
2024-12-25 15:34:33 +08:00

267 lines
11 KiB
JavaScript

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 && (typeof MediaKeyMessageEvent !== 'undefined')) {
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("[WidevineProxy2]", "WIDEVINE_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("[WidevineProxy2]", "WIDEVINE_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);
};