def get_license(self, challenge, sid): """ Requests and returns a license for the given challenge and sid :param challenge: The base64 encoded challenge :param sid: The sid paired to the challengew :return: Base64 representation of the licensekey or False unsuccessfull """ common.debug('Requesting license') id = int(time.time() * 10000) license_request_data = { 'version': 2, 'url': self.last_license_url, 'id': id, 'esn': g.get_esn(), 'languages': [g.PERSISTENT_STORAGE['locale_id']], 'uiVersion': 'shakti-v5bca5cd3', 'clientVersion': '6.0013.315.051', 'params': [{ 'sessionId': sid, 'clientTime': int(id / 10000), 'challengeBase64': challenge, 'xid': str(id + 1610) }], 'echo': 'sessionId' } response = self._chunked_request(ENDPOINTS['license'], license_request_data, g.get_esn()) return response[0]['licenseResponseBase64']
def _login(self, modal_error_message=False): """Perform account login""" # If exists get the current esn value before extract a new session data current_esn = g.get_esn() try: # First we get the authentication url without logging in, required for login API call react_context = website.extract_json(self._get('profiles'), 'reactContext') auth_url = website.extract_api_data(react_context)['auth_url'] common.debug('Logging in...') login_response = self._post('login', data=_login_payload( common.get_credentials(), auth_url)) validate_msg = website.validate_login(login_response) if validate_msg: self.session.cookies.clear() common.purge_credentials() if modal_error_message: ui.show_ok_dialog(common.get_local_string(30008), validate_msg) else: ui.show_notification(common.get_local_string(30009)) return False website.extract_session_data(login_response) except Exception as exc: common.error(traceback.format_exc()) self.session.cookies.clear() raise exc common.info('Login successful') ui.show_notification(common.get_local_string(30109)) self.update_session_data(current_esn) return True
def generate_logblobs_params(): """Generate the initial log blog""" # It seems that this log is sent when logging in to a profile the first time # i think it is the easiest to reproduce, the others contain too much data screen_size = str(xbmcgui.getScreenWidth()) + 'x' + str(xbmcgui.getScreenHeight()) timestamp_utc = time.time() timestamp = int(timestamp_utc * 1000) client_ver = g.LOCAL_DB.get_value('asset_core', '', table=TABLE_SESSION) app_id = int(time.time()) * 10000 + random.randint(1, 10001) # Should be used with all log requests if client_ver: result = re.search(r'-([0-9\.]+)\.js$', client_ver) client_ver = result.groups()[0] # Here you have to enter only the real data, falsifying the data would cause repercussions in netflix server logs # therefore since it is possible to exclude data, we avoid entering data that we do not have blob = { 'browserua': common.get_user_agent().replace(' ', '#'), 'browserhref': 'https://www.netflix.com/browse', # 'initstart': 988, # 'initdelay': 268, 'screensize': screen_size, # '1920x1080', 'screenavailsize': screen_size, # '1920x1040', 'clientsize': screen_size, # '1920x944', # 'pt_navigationStart': -1880, # 'pt_fetchStart': -1874, # 'pt_secureConnectionStart': -1880, # 'pt_requestStart': -1853, # 'pt_domLoading': -638, # 'm_asl_start': 990, # 'm_stf_creat': 993, # 'm_idb_open': 993, # 'm_idb_succ': 1021, # 'm_msl_load_no_data': 1059, # 'm_asl_comp': 1256, 'type': 'startup', 'sev': 'info', 'devmod': 'chrome-cadmium', 'clver': client_ver, # e.g. '6.0021.220.051' 'osplatform': g.LOCAL_DB.get_value('browser_info_os_name', '', table=TABLE_SESSION), 'osver': g.LOCAL_DB.get_value('browser_info_os_version', '', table=TABLE_SESSION), 'browsername': 'Chrome', 'browserver': g.LOCAL_DB.get_value('browser_info_version', '', table=TABLE_SESSION), 'appLogSeqNum': 0, 'uniqueLogId': common.get_random_uuid(), 'appId': app_id, 'esn': g.get_esn(), 'lver': '', # 'jssid': '15822792997793', # Same value of appId # 'jsoffms': 1261, 'clienttime': timestamp, 'client_utc': int(timestamp_utc), 'uiver': g.LOCAL_DB.get_value('ui_version', '', table=TABLE_SESSION) } blobs_container = { 'entries': [blob] } blobs_dump = json.dumps(blobs_container) blobs_dump = blobs_dump.replace('"', '\"').replace(' ', '').replace('#', ' ') return {'logblobs': blobs_dump}
def _login(self, modal_error_message=False): """Perform account login""" # If exists get the current esn value before extract a new session data current_esn = g.get_esn() try: # First we get the authentication url without logging in, required for login API call react_context = website.extract_json(self._get('login'), 'reactContext') auth_url = website.extract_api_data(react_context)['auth_url'] common.debug('Logging in...') login_response = self._post( 'login', data=_login_payload(common.get_credentials(), auth_url)) try: website.extract_session_data(login_response, validate=True, update_profiles=True) common.info('Login successful') ui.show_notification(common.get_local_string(30109)) self.update_session_data(current_esn) return True except (LoginValidateError, LoginValidateErrorIncorrectPassword) as exc: self.session.cookies.clear() common.purge_credentials() if not modal_error_message: raise ui.show_ok_dialog(common.get_local_string(30008), unicode(exc)) except InvalidMembershipStatusError: ui.show_error_info(common.get_local_string(30008), common.get_local_string(30180), False, True) except Exception: # pylint: disable=broad-except import traceback common.error(g.py2_decode(traceback.format_exc(), 'latin-1')) self.session.cookies.clear() raise return False
def release_license(self, data=None): # pylint: disable=unused-argument """Release the server license""" try: # When UpNext is used a new video is loaded while another one is running and not yet released, # so you need to take the right data of first added license url = self.licenses_release_url.pop() sid = self.licenses_session_id.pop() xid = self.licenses_xid.pop() common.debug('Requesting releasing license') params = [{ 'url': url, 'params': { 'drmSessionId': sid, 'xid': xid }, 'echo': 'drmSessionId' }] response = self.msl_requests.chunked_request( ENDPOINTS['license'], self.msl_requests.build_request_data('/bundle', params), g.get_esn()) common.debug('License release response: {}', response) except IndexError: # Example the supplemental media type have no license common.debug('No license to release')
def update_session_data(self, old_esn=g.get_esn()): self.session.headers.update({ 'x-netflix.request.client.user.guid': g.LOCAL_DB.get_active_profile_guid() }) cookies.save(self.account_hash, self.session.cookies) _update_esn(old_esn)
def load_manifest(self, viewable_id): """ Loads the manifests for the given viewable_id and returns a mpd-XML-Manifest :param viewable_id: The id of of the viewable :return: MPD XML Manifest or False if no success """ try: manifest = self._load_manifest(viewable_id, g.get_esn()) except MSLError as exc: if 'Email or password is incorrect' in g.py2_decode(str(exc)): # Known cases when MSL error "Email or password is incorrect." can happen: # - If user change the password when the nf session was still active # - Netflix has reset the password for suspicious activity when the nf session was still active # Then clear the credentials and also user tokens. common.purge_credentials() self.msl_requests.crypto.clear_user_id_tokens() raise # Disable 1080p Unlock for now, as it is broken due to Netflix changes # if (g.ADDON.getSettingBool('enable_1080p_unlock') and # not g.ADDON.getSettingBool('enable_vp9_profiles') and # not has_1080p(manifest)): # common.debug('Manifest has no 1080p viewables, trying unlock') # manifest = self.get_edge_manifest(viewable_id, manifest) return self.__tranform_to_dash(manifest)
def get_license(self, challenge, sid): """ Requests and returns a license for the given challenge and sid :param challenge: The base64 encoded challenge :param sid: The sid paired to the challengew :return: Base64 representation of the licensekey or False unsuccessfull """ common.debug('Requesting license') timestamp = int(time.time() * 10000) license_request_data = { 'version': 2, 'url': self.last_license_url, 'id': timestamp, 'languages': [g.LOCAL_DB.get_value('locale_id')], 'params': [{ 'sessionId': sid, 'clientTime': int(timestamp / 10000), 'challengeBase64': challenge, 'xid': str(timestamp + 1610) }], 'echo': 'sessionId' } response = self._chunked_request(ENDPOINTS['license'], license_request_data, g.get_esn()) return response[0]['licenseResponseBase64']
def _show_only_forced_subtitle(self): # Forced stream not found, then fix Kodi bug if user chose to apply the workaround # Kodi bug???: # If the kodi player is set with "forced only" subtitle setting, Kodi use this behavior: # 1) try to select forced subtitle that matches audio language # 2) if missing the forced subtitle in language, then # Kodi try to select: The first "forced" subtitle or the first "regular" subtitle # that can respect the chosen language or not, depends on the available streams # So can cause a wrong subtitle language or in a permanent display of subtitles! # This does not reflect the setting chosen in the Kodi player and is very annoying! # There is no other solution than to disable the subtitles manually. audio_language = common.get_kodi_audio_language() if self.legacy_kodi_version: # --- ONLY FOR KODI VERSION 18 --- # NOTE: With Kodi 18 it is not possible to read the properties of the streams # so the only possible way is to read the data from the manifest file cache_identifier = g.get_esn() + '_' + self.videoid.value manifest_data = g.CACHE.get(CACHE_MANIFESTS, cache_identifier) common.fix_locale_languages(manifest_data['timedtexttracks']) if not any( text_track.get('isForcedNarrative', False) is True and text_track['language'] == audio_language for text_track in manifest_data['timedtexttracks']): self.sc_settings.update({'subtitleenabled': False}) else: # --- ONLY FOR KODI VERSION 19 --- # Check the current stream player_stream = self.player_state.get( STREAMS['subtitle']['current']) if not player_stream[ 'isforced'] or player_stream['language'] != audio_language: self.sc_settings.update({'subtitleenabled': False})
def _process_event_request(self, event): """Do the event post request""" event.status = Event.STATUS_REQUESTED # Request attempts can be made up to a maximum of 3 times per event while event.is_attempts_granted(): common.info('EVENT [{}] - Executing request (attempt {})', event, event.req_attempt) params = {'reqAttempt': event.req_attempt, 'reqPriority': 20 if event.event_type == EVENT_START else 0, 'reqName': 'events/{}'.format(event)} url = ENDPOINTS['events'] + '?' + urlencode(params).replace('%2F', '/') try: response = self.chunked_request(url, event.request_data, g.get_esn(), disable_msl_switch=False) event.set_response(response) break except Exception as exc: # pylint: disable=broad-except common.error('EVENT [{}] - The request has failed: {}', event, exc) if event.event_type == EVENT_STOP: self.clear_queue() if event.event_data['is_in_mylist']: # If video is in my list, invalidate the continueWatching list (update lolomo context data) api.update_lolomo_context('continueWatching') else: # Else invalidate the 'queue' list (update lolomo context data) # Todo: get 'queue' lolomo id/index # api.update_lolomo_context('queue') pass api.update_videoid_bookmark(event.get_video_id()) # Below commented lines: let future requests continue to be sent, unstable connections like wi-fi cause problems # if not event.is_response_success(): # The event request is unsuccessful then there is some problem, # no longer make any future requests from this event id # return False return True
def get_license(self, challenge, sid): """ Requests and returns a license for the given challenge and sid :param challenge: The base64 encoded challenge :param sid: The sid paired to the challenge :return: Base64 representation of the license key or False unsuccessful """ common.debug('Requesting license') timestamp = int(time.time() * 10000) xid = str(timestamp + 1610) params = [{ 'drmSessionId': sid, 'clientTime': int(timestamp / 10000), 'challengeBase64': challenge, 'xid': xid }] response = self.msl_requests.chunked_request( ENDPOINTS['license'], self.msl_requests.build_request_data(self.last_license_url, params, 'drmSessionId'), g.get_esn()) # This xid must be used also for each future Event request, until playback stops g.LOCAL_DB.set_value('xid', xid, TABLE_SESSION) self.licenses_xid.insert(0, xid) self.licenses_session_id.insert(0, sid) self.licenses_release_url.insert( 0, response[0]['links']['releaseLicense']['href']) if self.msl_requests.msl_switch_requested: self.msl_requests.msl_switch_requested = False self.bind_events() return response[0]['licenseResponseBase64']
def get_license(self, challenge, sid): """ Requests and returns a license for the given challenge and sid :param challenge: The base64 encoded challenge :param sid: The sid paired to the challengew :return: Base64 representation of the licensekey or False unsuccessfull """ common.debug('Requesting license') license_request_data = { 'method': 'license', 'licenseType': 'STANDARD', 'clientVersion': '4.0004.899.011', 'uiVersion': 'akira', 'languages': ['de-DE'], 'playbackContextId': self.last_playback_context, 'drmContextIds': [self.last_drm_context], 'challenges': [{ 'dataBase64': challenge, 'sessionId': sid }], 'clientTime': int(time.time()), 'xid': int((int(time.time()) + 0.1612) * 1000) } response = self._chunked_request(ENDPOINTS['license'], license_request_data, g.get_esn()) return response['result']['licenses'][0]['data']
def _set_esn(esn): """ Set the ESN in settings if it hasn't been set yet. Return True if the new ESN has been set, False otherwise """ if not g.get_esn() and esn: g.LOCAL_DB.set_value('esn', esn, table=TABLE_SESSION) return True return False
def bind_events(self): """Bind events""" # I don't know the real purpose of its use, it seems to be requested after the license and before starting # playback, and only the first time after a switch, # in the response you can also understand if the msl switch has worked common.debug('Requesting bind events') response = self.msl_requests.chunked_request(ENDPOINTS['events'], self.msl_requests.build_request_data('/bind', {}), g.get_esn(), disable_msl_switch=False) common.debug('Bind events response: {}', response)
def _esn_checks(): # Check if the custom esn is changed custom_esn = g.ADDON.getSetting('esn') custom_esn_old = g.LOCAL_DB.get_value('custom_esn', '', TABLE_SETTINGS_MONITOR) if custom_esn != custom_esn_old: g.LOCAL_DB.set_value('custom_esn', custom_esn, TABLE_SETTINGS_MONITOR) common.send_signal(signal=common.Signals.ESN_CHANGED, data=g.get_esn()) if not custom_esn: # Check if "Force identification as L3 Widevine device" is changed (ANDROID ONLY) is_l3_forced = bool(g.ADDON.getSettingBool('force_widevine_l3')) is_l3_forced_old = g.LOCAL_DB.get_value('force_widevine_l3', False, TABLE_SETTINGS_MONITOR) if is_l3_forced != is_l3_forced_old: g.LOCAL_DB.set_value('force_widevine_l3', is_l3_forced, TABLE_SETTINGS_MONITOR) # If user has changed setting is needed clear previous ESN and perform a new handshake with the new one g.LOCAL_DB.set_value('esn', common.generate_android_esn() or '', TABLE_SESSION) common.send_signal(signal=common.Signals.ESN_CHANGED, data=g.get_esn())
def _load_msl_data(self, msl_data): try: self.crypto.load_msl_data(msl_data) self.crypto.load_crypto_session(msl_data) # Add-on just installed, the service starts but there is no esn if g.get_esn(): # This is also done here only try to speed up the loading of manifest self._check_mastertoken_validity() except MSLError: raise except Exception: # pylint: disable=broad-except import traceback common.error(traceback.format_exc())
def load_manifest(self, viewable_id): """ Loads the manifests for the given viewable_id and returns a mpd-XML-Manifest :param viewable_id: The id of of the viewable :return: MPD XML Manifest or False if no success """ manifest = self._load_manifest(viewable_id, g.get_esn()) # Disable 1080p Unlock for now, as it is broken due to Netflix changes # if (g.ADDON.getSettingBool('enable_1080p_unlock') and # not g.ADDON.getSettingBool('enable_vp9_profiles') and # not has_1080p(manifest)): # common.debug('Manifest has no 1080p viewables, trying unlock') # manifest = self.get_edge_manifest(viewable_id, manifest) return self.__tranform_to_dash(manifest)
def perform_key_handshake(self, data=None): # pylint: disable=unused-argument """Perform a key handshake and initialize crypto keys""" esn = g.get_esn() if not esn: common.warn('Cannot perform key handshake, missing ESN') return False common.info('Performing key handshake with ESN: {}', common.censure(esn) if g.ADDON.getSetting('esn') else esn) response = _process_json_response(self._post(ENDPOINTS['manifest'], self.handshake_request(esn))) header_data = self.decrypt_header_data(response['headerdata'], False) self.crypto.parse_key_response(header_data, esn, True) # Delete all the user id tokens (are correlated to the previous mastertoken) self.crypto.clear_user_id_tokens() common.debug('Key handshake successful') return True
def release_license(self, data=None): # pylint: disable=unused-argument """Release the server license""" common.debug('Requesting releasing license') params = [{ 'url': self.last_license_release_url, 'params': { 'sessionId': self.last_license_session_id, 'xid': g.LOCAL_DB.get_value('xid', table=TABLE_SESSION) }, 'echo': 'sessionId' }] response = self.msl_requests.chunked_request( ENDPOINTS['license'], self.msl_requests.build_request_data('/bundle', params), g.get_esn()) common.debug('License release response: {}', response)
def _get_owner_user_id_token(self): """A way to get the user token id of owner profile""" # In order to get a user id token of another (non-owner) profile you must make a request with SWITCH_PROFILE # authentication scheme (a custom authentication for netflix), and this request can be directly included # in the MSL manifest request. # But in order to execute this switch profile, you need to have the user id token of the main (owner) profile. # The only way (found to now) to get it immediately, is send a logblob event request, and save the # user id token obtained in the response. common.debug('Requesting logblog') params = {'reqAttempt': 1, 'reqPriority': 0, 'reqName': EVENT_BIND} url = ENDPOINTS['logblobs'] + '?' + urlencode(params).replace('%2F', '/') response = self.chunked_request(url, self.build_request_data('/logblob', generate_logblobs_params()), g.get_esn(), force_auth_credential=True) common.debug('Response of logblob request: {}', response)
def __init__(self): # pylint: disable=broad-except self.request_builder = None try: msl_data = json.loads(common.load_file('msl_data.json')) common.info('Loaded MSL data from disk') except Exception: msl_data = None try: self.request_builder = MSLRequestBuilder(msl_data) # Addon just installed, the service starts but there is no esn if g.get_esn(): self.check_mastertoken_validity() except Exception: import traceback common.error(traceback.format_exc()) common.register_slot(signal=common.Signals.ESN_CHANGED, callback=self.perform_key_handshake)
def perform_key_handshake(self, data=None): """Perform a key handshake and initialize crypto keys""" # pylint: disable=unused-argument esn = data or g.get_esn() if not esn: common.info('Cannot perform key handshake, missing ESN') return common.debug('Performing key handshake. ESN: {}'.format(esn)) response = _process_json_response( self._post(ENDPOINTS['manifest'], self.request_builder.handshake_request(esn))) headerdata = json.loads( base64.standard_b64decode(response['headerdata'])) self.request_builder.crypto.parse_key_response( headerdata, not common.is_edge_esn(esn)) common.debug('Key handshake successful')
def perform_key_handshake(self, data=None): """Perform a key handshake and initialize crypto keys""" # pylint: disable=unused-argument esn = data or g.get_esn() if not esn: common.info('Cannot perform key handshake, missing ESN') return False common.debug('Performing key handshake. ESN: {}', esn) response = _process_json_response( self._post(ENDPOINTS['manifest'], self.request_builder.handshake_request(esn))) header_data = self.request_builder.decrypt_header_data( response['headerdata'], False) self.request_builder.crypto.parse_key_response( header_data, not common.is_edge_esn(esn)) # Reset the user id token self.request_builder.user_id_token = None common.debug('Key handshake successful') return True
def _mastertoken_checks(self): """Perform checks to the MasterToken and executes a new key handshake when necessary""" is_handshake_required = False if self.crypto.mastertoken: if self.crypto.is_current_mastertoken_expired(): common.debug('Stored MSL MasterToken is expired, a new key handshake will be performed') is_handshake_required = True else: # Check if the current ESN is same of ESN bound to MasterToken if g.get_esn() != self.crypto.bound_esn: common.debug('Stored MSL MasterToken is bound to a different ESN, ' 'a new key handshake will be performed') is_handshake_required = True else: common.debug('MSL MasterToken is not available, a new key handshake will be performed') is_handshake_required = True if is_handshake_required: if self.perform_key_handshake(): msl_data = json.loads(common.load_file(MSL_DATA_FILENAME)) self.crypto.load_msl_data(msl_data) self.crypto.load_crypto_session(msl_data)
def _process_event_request(self, event): """Do the event post request""" event.status = Event.STATUS_REQUESTED # Request attempts can be made up to a maximum of 3 times per event while event.is_attempts_granted(): common.info('EVENT [{}] - Executing request (attempt {})', event, event.req_attempt) params = {'reqAttempt': event.req_attempt, 'reqPriority': 20 if event.event_type == EVENT_START else 0, 'reqName': 'events/{}'.format(event)} url = ENDPOINTS['events'] + '?' + urlencode(params).replace('%2F', '/') try: response = self.chunked_request(url, event.request_data, g.get_esn(), disable_msl_switch=False) event.set_response(response) break except Exception as exc: # pylint: disable=broad-except common.error('EVENT [{}] - The request has failed: {}', event, exc) if event.event_type == EVENT_STOP: self.clear_queue() # api.update_lolomo_context('continueWatching', video_id=event.get_video_id()) if not event.is_response_success(): # The event request is unsuccessful then there is some problem, # no longer make any future requests from this event id return False return True
def _update_esn(old_esn): """Perform key handshake if the esn has changed on Session initialization""" current_esn = g.get_esn() if old_esn != current_esn: common.send_signal(signal=common.Signals.ESN_CHANGED, data=current_esn)
def _verify_esn_existence(self): # if for any reason esn is no longer exist get one if not g.get_esn(): return self._refresh_session_data() return True
def update_session_data(self, old_esn=None): old_esn = old_esn or g.get_esn() self.set_session_header_data() cookies.save(self.account_hash, self.session.cookies) _update_esn(old_esn)
def load_msl_data(self, msl_data=None): self._msl_data = msl_data if msl_data else {} if msl_data: self.set_mastertoken(msl_data['tokens']['mastertoken']) self.bound_esn = msl_data.get('bound_esn', g.get_esn())
def view_esn(self, pathitems=None): # pylint: disable=unused-argument """Show the ESN in use""" ui.show_ok_dialog(common.get_local_string(30016), g.get_esn())