def release_license(self, data=None): # pylint: disable=unused-argument """Release the server license""" try: # When you try to play a video while another one is currently in playing, # a new license to be released will be queued, so the oldest license must be released url = self.licenses_release_url.pop() sid = self.licenses_session_id.pop() xid = self.licenses_xid.pop() LOG.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), get_esn()) LOG.debug('License release response: {}', response) except IndexError: # Example the supplemental media type have no license LOG.debug('No license to release')
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 LOG.info('EVENT [{}] - Executing request', event) endpoint_url = ENDPOINTS['events'] + create_req_params( 20 if event.event_type == EVENT_START else 0, 'events/{}'.format(event)) try: response = self.chunked_request(endpoint_url, event.request_data, get_esn(), disable_msl_switch=False) # Malformed/wrong content in requests are ignored without returning error feedback in the response event.set_response(response) except Exception as exc: # pylint: disable=broad-except LOG.error('EVENT [{}] - The request has failed: {}', event, exc) event.set_response('RequestError') if event.event_type == EVENT_STOP and event.status == Event.STATUS_SUCCESS: self.clear_queue() if event.event_data['allow_request_update_loco']: # Calls to nfsession common.make_http_call('update_loco_context', {'context_name': 'continueWatching'}) common.make_http_call('update_videoid_bookmark', {'video_id': event.get_video_id()}) return True
def perform_key_handshake(self): """Perform a key handshake and initialize crypto keys""" esn = get_esn() if not esn: LOG.error('Cannot perform key handshake, missing ESN') return False LOG.info('Performing key handshake with ESN: {}', common.censure(esn) if len(esn) > 50 else esn) try: header, _ = _process_json_response( self._post(ENDPOINTS['manifest'], self.handshake_request(esn))) header_data = self.decrypt_header_data(header['headerdata'], False) self.crypto.parse_key_response(header_data, esn, True) except MSLError as exc: if exc.err_number == 207006 and common.get_system_platform( ) == 'android': msg = ( 'Request failed validation during key exchange\r\n' 'To try to solve this problem read the Wiki FAQ on add-on GitHub.' ) raise MSLError(msg) from exc raise # Delete all the user id tokens (are correlated to the previous mastertoken) self.crypto.clear_user_id_tokens() LOG.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(): LOG.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 get_esn() != self.crypto.bound_esn: LOG.debug( 'Stored MSL MasterToken is bound to a different ESN, ' 'a new key handshake will be performed') is_handshake_required = True else: LOG.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_def(MSL_DATA_FILENAME)) self.crypto.load_msl_data(msl_data) self.crypto.load_crypto_session(msl_data)
def _ensure_forced_subtitle_only_kodi18(self): """Ensures the display of forced subtitles only with the audio language set [KODI 18]""" # With Kodi 18 it is not possible to read the properties of the player streams, # so the only possible way is to read the data from the manifest file from resources.lib.common.cache_utils import CACHE_MANIFESTS from resources.lib.utils.esn import get_esn # Get the manifest cache_identifier = get_esn() + '_' + self.videoid.value manifest = G.CACHE.get(CACHE_MANIFESTS, cache_identifier) common.fix_locale_languages(manifest['timedtexttracks']) # Get the language audio_language = common.get_kodi_audio_language() if audio_language == 'mediadefault': # Netflix do not have a "Media default" track then we rely on the language of current nf profile, # although due to current Kodi locale problems could be not always accurate. profile_language_code = G.LOCAL_DB.get_profile_config('language') audio_language = profile_language_code[0:2] if audio_language == 'original': # Find the language of the original audio track stream = next((audio_track for audio_track in manifest['audio_tracks'] if audio_track['isNative']), None) if not stream: return audio_language = stream['language'] # Check in the manifest if there is a forced subtitle in the specified language if not any( text_track.get('isForcedNarrative', False) and text_track['language'] == audio_language for text_track in manifest['timedtexttracks']): self.sc_settings.update({'subtitleenabled': 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 LOG.debug('Requesting bind events') response = self.msl_requests.chunked_request( ENDPOINTS['manifest'], self.msl_requests.build_request_data('/bind', {}), get_esn(), disable_msl_switch=False) LOG.debug('Bind events response: {}', response)
def __init__(self, *args, **kwargs): # pylint: disable=unused-argument self.changes_applied = False self.esn = get_esn() self.esn_new = None self.wv_force_sec_lev = G.LOCAL_DB.get_value( 'widevine_force_seclev', WidevineForceSecLev.DISABLED, table=TABLE_SESSION) self.wv_sec_lev_new = None self.is_android = common.get_system_platform() == 'android' self.action_exit_keys_id = [ ACTION_PREVIOUS_MENU, ACTION_PLAYER_STOP, ACTION_NAV_BACK ] super().__init__(*args)
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. LOG.debug('Requesting logblog') endpoint_url = ENDPOINTS['logblobs'] + create_req_params(0, 'bind') response = self.chunked_request(endpoint_url, self.build_request_data('/logblob', generate_logblobs_params()), get_esn(), force_auth_credential=True) LOG.debug('Response of logblob request: {}', response)
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. 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 audio_language = common.get_kodi_audio_language() cache_identifier = 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 --- audio_language = common.get_kodi_audio_language( iso_format=xbmc.ISO_639_2, use_fallback=False) if audio_language == 'mediadefault': # Find the language of the default audio track audio_list = self.player_state.get(STREAMS['audio']['list']) for audio_track in audio_list: if audio_track['isdefault']: audio_language = audio_track['language'] break player_stream = self.player_state.get( STREAMS['subtitle']['current']) if player_stream is None: return if audio_language == 'original': # Do nothing is_language_appropriate = True else: is_language_appropriate = player_stream[ 'language'] == audio_language # Check if the current stream is forced and with an appropriate subtitle language if not player_stream['isforced'] or not is_language_appropriate: self.sc_settings.update({'subtitleenabled': False})
def get_license(self, license_data): """ Requests and returns a license for the given challenge and sid :param license_data: The license data provided by isa :return: Base64 representation of the license key or False unsuccessful """ LOG.debug('Requesting license') challenge, sid = license_data.decode('utf-8').split('!') sid = base64.standard_b64decode(sid).decode('utf-8') timestamp = int(time.time() * 10000) xid = str(timestamp + 1610) params = [{ 'drmSessionId': sid, 'clientTime': int(timestamp / 10000), 'challengeBase64': challenge, 'xid': xid }] self.manifest_challenge = challenge endpoint_url = ENDPOINTS['license'] + create_req_params( 0, 'prefetch/license') try: response = self.msl_requests.chunked_request( endpoint_url, self.msl_requests.build_request_data(self.last_license_url, params, 'drmSessionId'), get_esn()) except MSLError as exc: if exc.err_number == '1044' and common.get_system_platform( ) == 'android': msg = ( 'This title is not available to watch instantly. Please try another title.\r\n' 'To try to solve this problem you can force "Widevine L3" from the add-on Expert settings.\r\n' 'More info in the Wiki FAQ on add-on GitHub.') raise MSLError(msg) from exc raise # 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 base64.standard_b64decode(response[0]['licenseResponseBase64'])
def perform_key_handshake(self, data=None): # pylint: disable=unused-argument """Perform a key handshake and initialize crypto keys""" esn = get_esn() if not esn: LOG.warn('Cannot perform key handshake, missing ESN') return False LOG.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() LOG.debug('Key handshake successful') return True
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, 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 return self.__tranform_to_dash(manifest)
def _process_event_request(self, event_type, event_data, player_state): """Build and make the event post request""" if event_type == EVENT_START: # We get at every new video playback a fresh LoCo data self.loco_data = self.nfsession.get_loco_data() url = event_data['manifest']['links']['events']['href'] from resources.lib.services.nfsession.msl.msl_request_builder import MSLRequestBuilder request_data = MSLRequestBuilder.build_request_data( url, self._build_event_params(event_type, event_data, player_state, event_data['manifest'], self.loco_data)) # Request attempts can be made up to a maximum of 3 times per event LOG.info('EVENT [{}] - Executing request', event_type) endpoint_url = ENDPOINTS['events'] + create_req_params( 20 if event_type == EVENT_START else 0, f'events/{event_type}') try: response = self.chunked_request(endpoint_url, request_data, get_esn(), disable_msl_switch=False) # Malformed/wrong content in requests are ignored without returning any error in the response or exception LOG.debug('EVENT [{}] - Request response: {}', event_type, response) if event_type == EVENT_STOP: if event_data['allow_request_update_loco']: if 'list_context_name' in self.loco_data: self.nfsession.update_loco_context( self.loco_data['root_id'], self.loco_data['list_context_name'], self.loco_data['list_id'], self.loco_data['list_index']) else: LOG.warn( 'EventsHandler: LoCo list not updated due to missing list context data' ) video_id = request_data['params']['sessionParams'][ 'uiplaycontext']['video_id'] self.nfsession.update_videoid_bookmark(video_id) self.loco_data = None except Exception as exc: # pylint: disable=broad-except LOG.error('EVENT [{}] - The request has failed: {}', event_type, exc)
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(): LOG.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, get_esn(), disable_msl_switch=False) event.set_response(response) break except Exception as exc: # pylint: disable=broad-except LOG.error('EVENT [{}] - The request has failed: {}', event, exc) if event.event_type == EVENT_STOP: self.clear_queue() if event.event_data['allow_request_update_loco']: # Calls to nfsession common.make_http_call('update_loco_context', {'context_name': 'continueWatching'}) common.make_http_call('update_videoid_bookmark', {'video_id': 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_manifest(self, viewable_id): """ Get 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: esn = get_esn() # When the add-on is installed from scratch or you logout the account the ESN will be empty if not esn: esn = set_esn() manifest = self._get_manifest(viewable_id, esn) except MSLError as exc: if 'Email or password is incorrect' in 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 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 challenge :return: Base64 representation of the license key or False unsuccessful """ LOG.debug('Requesting license') timestamp = int(time.time() * 10000) xid = str(timestamp + 1610) params = [{ 'drmSessionId': sid, 'clientTime': int(timestamp / 10000), 'challengeBase64': challenge, 'xid': xid }] self.manifest_challenge = challenge endpoint_url = ENDPOINTS[ 'license'] + '?reqAttempt=1&reqPriority=0&reqName=prefetch/license' response = self.msl_requests.chunked_request( endpoint_url, self.msl_requests.build_request_data(self.last_license_url, params, 'drmSessionId'), 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 _save_system_info(): # Ask to save to a file filename = 'NFSystemInfo.txt' path = ui.show_browse_dialog( f'{common.get_local_string(30603)} - {filename}') if not path: return # This collect the main data to allow verification checks for problems data = f'Netflix add-on version: {G.VERSION}' data += f'\nDebug enabled: {LOG.is_enabled}' data += f'\nSystem platform: {common.get_system_platform()}' data += f'\nMachine architecture: {common.get_machine()}' data += f'\nUser agent string: {common.get_user_agent()}' data += '\n\n#### Widevine info ####\n' if common.get_system_platform() == 'android': data += f'\nSystem ID: {G.LOCAL_DB.get_value("drm_system_id", "--not obtained--", TABLE_SESSION)}' data += f'\nSecurity level: {G.LOCAL_DB.get_value("drm_security_level", "--not obtained--", TABLE_SESSION)}' data += f'\nHDCP level: {G.LOCAL_DB.get_value("drm_hdcp_level", "--not obtained--", TABLE_SESSION)}' wv_force_sec_lev = G.LOCAL_DB.get_value('widevine_force_seclev', WidevineForceSecLev.DISABLED, TABLE_SESSION) data += f'\nForced security level setting is: {wv_force_sec_lev}' else: try: from ctypes import (CDLL, c_char_p) cdm_lib_file_path = _get_cdm_file_path() try: lib = CDLL(cdm_lib_file_path) data += '\nLibrary status: Correctly loaded' try: lib.GetCdmVersion.restype = c_char_p data += f'\nVersion: {lib.GetCdmVersion().decode("utf-8")}' except Exception: # pylint: disable=broad-except # This can happen if the endpoint 'GetCdmVersion' is changed data += '\nVersion: Reading error' except Exception as exc: # pylint: disable=broad-except # This should not happen but currently InputStream Helper does not perform any verification checks on # downloaded and installed files, so if due to an problem it installs a CDM for a different architecture # or the files are corrupted, the user can no longer play videos and does not know what to do data += '\nLibrary status: Error loading failed' data += '\n>>> It is possible that is installed a CDM of a wrong architecture or is corrupted' data += '\n>>> Suggested solutions:' data += '\n>>> - Restore a previous version of Widevine library from InputStream Helper add-on settings' data += '\n>>> - Report the problem to the GitHub of InputStream Helper add-on' data += f'\n>>> Error details: {exc}' except Exception as exc: # pylint: disable=broad-except data += f'\nThe data could not be obtained. Error details: {exc}' data += '\n\n#### ESN ####\n' esn = get_esn() or '--not obtained--' data += f'\nUsed ESN: {common.censure(esn) if len(esn) > 50 else esn}' data += f'\nWebsite ESN: {get_website_esn() or "--not obtained--"}' data += f'\nAndroid generated ESN: {(generate_android_esn() or "--not obtained--")}' if common.get_system_platform() == 'android': data += '\n\n#### Device system info ####\n' try: import subprocess info = subprocess.check_output(['/system/bin/getprop' ]).decode('utf-8') data += f'\n{info}' except Exception as exc: # pylint: disable=broad-except data += f'\nThe data could not be obtained. Error: {exc}' data += '\n' try: common.save_file(common.join_folders_paths(path, filename), data.encode('utf-8')) ui.show_notification( f'{xbmc.getLocalizedString(35259)}: {filename}') # 35259=Saved except Exception as exc: # pylint: disable=broad-except LOG.error('save_file error: {}', exc) ui.show_notification('Error! Try another path')
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', get_esn())
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 = f'{xbmcgui.getScreenWidth()}x{xbmcgui.getScreenHeight()}' timestamp_utc = time.time() timestamp = int(timestamp_utc * 1000) app_id = int(time.time()) * 10000 + random.SystemRandom().randint( 1, 10001) # Should be used with all log requests # 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': G.LOCAL_DB.get_value('client_version', '', table=TABLE_SESSION), # 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': 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 get_manifest(videoid): """Get the manifest from cache""" cache_identifier = get_esn() + '_' + videoid.value return G.CACHE.get(CACHE_MANIFESTS, cache_identifier)
def view_esn(self, pathitems=None): # pylint: disable=unused-argument """Show the ESN in use""" ui.show_ok_dialog(common.get_local_string(30016), get_esn())
def _verify_esn_existence(): return bool(get_esn())