mirror of
https://github.com/DevLARLEY/WidevineProxy2.git
synced 2026-04-02 18:49:58 +00:00
267 lines
11 KiB
JavaScript
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);
|
|
};
|