Inject EME interface and log its function calls.
// ==UserScript== // @name EME Logger // @namespace http://greasyfork.org/ // @version 2.0 // @description Inject EME interface and log its function calls. // @author cramer // @match *://*/* // @run-at document-start // @grant GM_getValue // @grant GM_setValue // @grant GM_registerMenuCommand // ==/UserScript== (async () => { const disabledKeySystems = GM_getValue('disabledKeySystems', []); const commonKeySystems = { 'Widevine': /widevine/i, 'PlayReady': /playready/i, 'FairPlay': /fairplay|fps/i, 'ClearKey': /clearkey/i, }; for (const [keySystem, rule] of Object.entries(commonKeySystems)) { if (disabledKeySystems.indexOf(keySystem) >= 0) { GM_registerMenuCommand(`Enable ${keySystem} (and refresh)`, function() { GM_setValue('disabledKeySystems', disabledKeySystems.filter(k => k !== keySystem)); location.reload(); }); } else { GM_registerMenuCommand(`Disable ${keySystem} (and refresh)`, function() { GM_setValue('disabledKeySystems', [...disabledKeySystems, keySystem]); location.reload(); }); } } function isKeySystemDisabled(keySystem) { for (const disabledKeySystem of disabledKeySystems) { if (keySystem.match(commonKeySystems[disabledKeySystem])) return true; } return false } // Color constants const $ = { INFO: '#66d9ef', VALUE: '#4ec9a4', METHOD: '#569cd6', SUCCESS: '#a6e22e', FAILURE: '#6d1212', WARNING: '#fd971f', }; const indent = (s,n=4) => s.split('\n').map(l=>Array(n).fill(' ').join('')+l).join('\n'); const b64 = { decode: s => Uint8Array.from(atob(s), c => c.charCodeAt(0)), encode: b => btoa(String.fromCharCode(...new Uint8Array(b))) }; 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 messageHandler(event) { const keySession = event.target; const {sessionId} = keySession; const {message, messageType} = event; const listeners = keySession.getEventListeners('message').filter(l => l !== messageHandler); console.groupCollapsed( `%c[EME] (EVENT)%c MediaKeySession::message%c\n` + `Session ID: %c%s%c\n` + `Message Type: %c%s%c\n` + `Message: %c%s%c\n` + `Listeners:`, `color: ${$.INFO}; font-weight: bold;`, `color: ${$.METHOD};`, `font-weight: normal;`, `color: ${$.VALUE}; font-weight: bold;`, sessionId || '(not available)', `color: inherit; font-weight: normal;`, `color: ${$.VALUE}; font-weight: bold;`, messageType, `color: inherit; font-weight: normal;`, `font-weight: bold;`, b64.encode(message), `font-weight: normal;`, listeners, ); console.trace(); console.groupEnd(); } function keyStatusColor(status) { switch(status.toLowerCase()) { case 'usable': return $.SUCCESS; case 'output-restricted': case 'output-downscaled': case 'usable-in-future': case 'status-pending': return $.WARNING; case 'expired': case 'released': case 'internal-error': default: return $.FAILURE; } } function keystatuseschangeHandler(event) { const keySession = event.target; const {sessionId} = keySession; const listeners = keySession.getEventListeners('keystatuseschange').filter(l => l !== keystatuseschangeHandler); let keysFmt = ''; const keysText = []; keySession.keyStatuses.forEach((status, keyId) => { keysFmt += ` %c[%s]%c %s%c\n`; keysText.push( `color: ${keyStatusColor(status)}; font-weight: bold;`, status.toUpperCase(), `color: ${$.VALUE};`, b64.encode(keyId), `color: inherit; font-weight: normal;`, ); }); console.groupCollapsed( `%c[EME] (EVENT)%c MediaKeySession::keystatuseschange%c\n` + `Session ID: %c%s%c\n` + `Key Statuses:\n` + keysFmt + 'Listeners:', `color: ${$.INFO}; font-weight: bold;`, `color: ${$.METHOD};`, `font-weight: normal;`, `color: ${$.VALUE}; font-weight: bold;`, sessionId || '(not available)', `color: inherit; font-weight: normal;`, ...keysText, listeners, ); console.trace(); console.groupEnd(); } function getEventListeners(type) { if (this == null) return []; const store = this[Symbol.for(getEventListeners)]; if (store == null || store[type] == null) return []; return store[type]; } EventTarget.prototype.getEventListeners = getEventListeners; typeof Navigator !== 'undefined' && proxy(Navigator.prototype, 'requestMediaKeySystemAccess', async (_target, _this, _args) => { const [keySystem, supportedConfigurations] = _args; const enterMessage = [ `%c[EME] (CALL)%c Navigator::requestMediaKeySystemAccess%c\n` + `Key System: %c%s%c\n` + `Supported Configurations:\n`, `color: ${$.INFO}; font-weight: bold;`, `color: ${$.METHOD};`, `font-weight: normal;`, `color: ${$.VALUE}; font-weight: bold;`, keySystem, `color: inherit; font-weight: normal;`, indent(JSON.stringify(supportedConfigurations, null, ' ')), ]; let r###lt, err; try { if (isKeySystemDisabled(keySystem)) { throw new DOMException(`Unsupported keySystem or supportedConfigurations.`, `NotSupportedError`); } r###lt = await _target.apply(_this, _args); return r###lt; } catch(e) { err = e; throw e; } finally { console.groupCollapsed(...enterMessage); if (err) { console.error(`%c[FAILURE]`, `color: ${$.FAILURE}; font-weight: bold;`, err); } else { console.log(`%c[SUCCESS]`, `color: ${$.SUCCESS}; font-weight: bold;`, r###lt); } console.trace(); console.groupEnd(); } }); typeof MediaKeySystemAccess !== 'undefined' && proxy(MediaKeySystemAccess.prototype, 'createMediaKeys', async (_target, _this, _args) => { const enterMessage = [ `%c[EME] (CALL)%c MediaKeySystemAccess::createMediaKeys%c\n` + `Key System: %c%s%c\n` + `Configurations:\n`, `color: ${$.INFO}; font-weight: bold;`, `color: ${$.METHOD};`, `font-weight: normal;`, `color: ${$.VALUE}; font-weight: bold;`, _this.keySystem, `color: inherit; font-weight: normal;`, indent(JSON.stringify(_this.getConfiguration(), null, ' ')), ]; let r###lt, err; try { r###lt = await _target.apply(_this, _args); return r###lt; } catch(e) { err = e; throw e; } finally { console.groupCollapsed(...enterMessage); if (err) { console.error(`%c[FAILURE]`, `color: ${$.FAILURE}; font-weight: bold;`, err); } else { console.log(`%c[SUCCESS]`, `color: ${$.SUCCESS}; font-weight: bold;`, r###lt); } console.trace(); console.groupEnd(); } }); if (typeof MediaKeys !== 'undefined') { proxy(MediaKeys.prototype, 'setServerCertificate', async (_target, _this, _args) => { const [serverCertificate] = _args; const enterMessage = [ `%c[EME] (CALL)%c MediaKeys::setServerCertificate%c\n` + `Server Certificate:`, `color: ${$.INFO}; font-weight: bold;`, `color: ${$.METHOD};`, `font-weight: normal;`, b64.encode(serverCertificate), ]; let r###lt, err; try { r###lt = await _target.apply(_this, _args); return r###lt; } catch(e) { err = e; throw e; } finally { console.groupCollapsed(...enterMessage); if (err) { console.error(`%c[FAILURE]`, `color: ${$.FAILURE}; font-weight: bold;`, err); } else { console.log(`%c[SUCCESS]`, `color: ${$.SUCCESS}; font-weight: bold;`, r###lt); } console.trace(); console.groupEnd(); } }); proxy(MediaKeys.prototype, 'createSession', (_target, _this, _args) => { const [sessionType] = _args; const enterMessage = [ `%c[EME] (CALL)%c MediaKeys::createSession%c\n` + `Session Type: %c%s`, `color: ${$.INFO}; font-weight: bold;`, `color: ${$.METHOD};`, `font-weight: normal;`, `color: ${$.VALUE}; font-weight: bold;`, sessionType || 'temporary (default)', ]; let session, err; try { session = _target.apply(_this, _args); session.addEventListener('message', messageHandler); session.addEventListener('keystatuseschange', keystatuseschangeHandler); return session; } catch(e) { err = e; throw e; } finally { console.groupCollapsed(...enterMessage); if (err) { console.error(`%c[FAILURE]`, `color: ${$.FAILURE}; font-weight: bold;`, err); } else { console.log(`%c[SUCCESS]`, `color: ${$.SUCCESS}; font-weight: bold;`, session); } console.trace(); console.groupEnd(); } }); } 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]; if (listeners.indexOf(listener) < 0) { listeners.push(listener); } } return _target.apply(_this, _args); }); proxy(EventTarget.prototype, 'removeEventListener', async (_target, _this, _args) => { if (_this != null) { const [type, listener] = _args; const storeKey = Symbol.for(getEventListeners); if (!(storeKey in _this)) return; const store = _this[storeKey]; if (!(type in store)) return; const listeners = store[type]; const index = listeners.indexOf(listener); if (index >= 0) { if (listeners.length === 1) { delete store[type]; } else { listeners.splice(index, 1); } } } return _target.apply(_this, _args); }); } if (typeof MediaKeySession !== 'undefined') { proxy(MediaKeySession.prototype, 'generateRequest', async (_target, _this, _args) => { const [initDataType, initData] = _args; const enterMessage = [ `%c[EME] (CALL)%c MediaKeySession::generateRequest%c\n` + `Session ID: %c%s%c\n` + `Init Data Type: %c%s%c\n` + `Init Data:`, `color: ${$.INFO}; font-weight: bold;`, `color: ${$.METHOD};`, `font-weight: normal;`, `color: ${$.VALUE}; font-weight: bold;`, _this.sessionId || '(not available)', `color: inherit; font-weight: normal;`, `color: ${$.VALUE}; font-weight: bold;`, initDataType, `color: inherit; font-weight: normal;`, b64.encode(initData), ]; let r###lt, err; try { r###lt = await _target.apply(_this, _args); return r###lt; } catch(e) { err = e; throw e; } finally { console.groupCollapsed(...enterMessage); if (err) { console.error(`%c[FAILURE]`, `color: ${$.FAILURE}; font-weight: bold;`, err); } else { console.log(`%c[SUCCESS]`, `color: ${$.SUCCESS}; font-weight: bold;`, r###lt); } console.trace(); console.groupEnd(); } }); proxy(MediaKeySession.prototype, 'load', async (_target, _this, _args) => { const [sessionId] = _args; const enterMessage = [ `%c[EME] (CALL)%c MediaKeySession::load%c\n` + `Session ID: %c%s`, `color: ${$.INFO}; font-weight: bold;`, `color: ${$.METHOD};`, `font-weight: normal;`, `color: ${$.VALUE}; font-weight: bold;`, _this.sessionId || '(not available)', ]; let r###lt, err; try { r###lt = await _target.apply(_this, _args); return r###lt; } catch(e) { err = e; throw e; } finally { console.groupCollapsed(...enterMessage); if (err) { console.error(`%c[FAILURE]`, `color: ${$.FAILURE}; font-weight: bold;`, err); } else { console.log(`%c[SUCCESS]`, `color: ${$.SUCCESS}; font-weight: bold;`, r###lt); } console.trace(); console.groupEnd(); } }); proxy(MediaKeySession.prototype, 'update', async (_target, _this, _args) => { const [response] = _args; const enterMessage = [ `%c[EME] (CALL)%c MediaKeySession::update%c\n` + `Session ID: %c%s%c\n` + `Response:`, `color: ${$.INFO}; font-weight: bold;`, `color: ${$.METHOD};`, `font-weight: normal;`, `color: ${$.VALUE}; font-weight: bold;`, _this.sessionId || '(not available)', `color: inherit; font-weight: normal;`, b64.encode(response), ]; let err; try { return await _target.apply(_this, _args); } catch(e) { err = e; throw e; } finally { console.groupCollapsed(...enterMessage); if (err) { console.error(`%c[FAILURE]`, `color: ${$.FAILURE}; font-weight: bold;`, err); } else { console.log(`%c[SUCCESS]`, `color: ${$.SUCCESS}; font-weight: bold;`); } console.trace(); console.groupEnd(); } }); proxy(MediaKeySession.prototype, 'close', async (_target, _this, _args) => { const enterMessage = [ `%c[EME] (CALL)%c MediaKeySession::close%c\n` + `Session ID: %c%s`, `color: ${$.INFO}; font-weight: bold;`, `color: ${$.METHOD};`, `font-weight: normal;`, `color: ${$.VALUE}; font-weight: bold;`, _this.sessionId || '(not available)', ]; let err; try { return await _target.apply(_this, _args); } catch(e) { err = e; throw e; } finally { console.groupCollapsed(...enterMessage); if (err) { console.error(`%c[FAILURE]`, `color: ${$.FAILURE}; font-weight: bold;`, err); } else { console.log(`%c[SUCCESS]`, `color: ${$.SUCCESS}; font-weight: bold;`); } console.trace(); console.groupEnd(); } }); proxy(MediaKeySession.prototype, 'remove', async (_target, _this, _args) => { const enterMessage = [ `%c[EME] (CALL)%c MediaKeySession::remove%c\n` + `Session ID: %c%s`, `color: ${$.INFO}; font-weight: bold;`, `color: ${$.METHOD};`, `font-weight: normal;`, `color: ${$.VALUE}; font-weight: bold;`, _this.sessionId || '(not available)', ]; let err; try { return await _target.apply(_this, _args); } catch(e) { err = e; throw e; } finally { console.groupCollapsed(...enterMessage); if (err) { console.error(`%c[FAILURE]`, `color: ${$.FAILURE}; font-weight: bold;`, err); } else { console.log(`%c[SUCCESS]`, `color: ${$.SUCCESS}; font-weight: bold;`); } console.trace(); console.groupEnd(); } }); } })();