def _init_session(self): """Initialize the session to use for all future connections""" try: self.session.close() LOG.info('Session closed') except AttributeError: pass import httpx # (http1=False, http2=True) means that the client know that server support HTTP/2 and avoid to do negotiations, # prior knowledge: https://python-hyper.org/projects/hyper-h2/en/v2.3.1/negotiating-http2.html#prior-knowledge self.session = httpx.Client(http1=False, http2=True) self.session.max_redirects = 10 # Too much redirects should means some problem self.session.headers.update({ 'User-Agent': common.get_user_agent(enable_android_mediaflag_fix=True), 'Accept-Encoding': 'gzip, deflate, br', 'Host': 'www.netflix.com' }) LOG.info('Initialized new session')
def get_inputstream_listitem(videoid): """Return a listitem that has all inputstream relevant properties set for playback of the given video_id""" service_url = 'http://127.0.0.1:{}'.format(G.LOCAL_DB.get_value('nf_server_service_port')) manifest_path = MANIFEST_PATH_FORMAT.format(videoid.value) list_item = xbmcgui.ListItem(path=service_url + manifest_path, offscreen=True) list_item.setContentLookup(False) list_item.setMimeType('application/xml+dash') list_item.setProperty('isFolder', 'false') list_item.setProperty('IsPlayable', 'true') try: import inputstreamhelper is_helper = inputstreamhelper.Helper('mpd', drm='widevine') inputstream_ready = is_helper.check_inputstream() if not inputstream_ready: raise Exception(common.get_local_string(30046)) list_item.setProperty( key=is_helper.inputstream_addon + '.stream_headers', value='user-agent=' + common.get_user_agent()) list_item.setProperty( key=is_helper.inputstream_addon + '.license_type', value='com.widevine.alpha') list_item.setProperty( key=is_helper.inputstream_addon + '.manifest_type', value='mpd') list_item.setProperty( key=is_helper.inputstream_addon + '.license_key', value=service_url + LICENSE_PATH_FORMAT.format(videoid.value) + '||b{SSM}!b{SID}|') list_item.setProperty( key=is_helper.inputstream_addon + '.server_certificate', value=INPUTSTREAM_SERVER_CERTIFICATE) list_item.setProperty( key='inputstream', value=is_helper.inputstream_addon) return list_item except Exception as exc: # pylint: disable=broad-except # Captures all types of ISH internal errors import traceback LOG.error(traceback.format_exc()) raise InputStreamHelperError(str(exc)) from exc
def get_inputstream_listitem(videoid): """Return a listitem that has all inputstream relevant properties set for playback of the given video_id""" service_url = SERVICE_URL_FORMAT.format( port=g.LOCAL_DB.get_value('msl_service_port', 8000)) manifest_path = MANIFEST_PATH_FORMAT.format(videoid=videoid.value) list_item = xbmcgui.ListItem(path=service_url + manifest_path, offscreen=True) list_item.setContentLookup(False) list_item.setMimeType('application/dash+xml') list_item.setProperty('isFolder', 'false') list_item.setProperty('IsPlayable', 'true') import inputstreamhelper is_helper = inputstreamhelper.Helper('mpd', drm='widevine') if not is_helper.check_inputstream(): raise InputstreamError(common.get_local_string(30046)) list_item.setProperty( key=is_helper.inputstream_addon + '.stream_headers', value='user-agent=' + common.get_user_agent()) list_item.setProperty( key=is_helper.inputstream_addon + '.license_type', value='com.widevine.alpha') list_item.setProperty( key=is_helper.inputstream_addon + '.manifest_type', value='mpd') list_item.setProperty( key=is_helper.inputstream_addon + '.license_key', value=service_url + LICENSE_PATH_FORMAT.format(videoid=videoid.value) + '||b{SSM}!b{SID}|') list_item.setProperty( key=is_helper.inputstream_addon + '.server_certificate', value=INPUTSTREAM_SERVER_CERTIFICATE) list_item.setProperty( key='inputstreamaddon', value=is_helper.inputstream_addon) return list_item
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) app_id = int(time.time()) * 10000 + random.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': 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 _save_system_info(): # Ask to save to a file filename = 'NFSystemInfo.txt' path = ui.show_browse_dialog( common.get_local_string(30603) + ' - ' + filename) if not path: return # This collect the main data to allow verification checks for problems data = 'Netflix add-on version: ' + G.VERSION data += '\nDebug logging level: ' + LOG.level data += '\nSystem platform: ' + common.get_system_platform() data += '\nMachine architecture: ' + common.get_machine() data += '\nUser agent string: ' + common.get_user_agent() data += '\n\n' + '#### Widevine info ####\n' if common.get_system_platform() == 'android': data += '\nSystem ID: ' + G.LOCAL_DB.get_value( 'drm_system_id', '--not obtained--', TABLE_SESSION) data += '\nSecurity level: ' + G.LOCAL_DB.get_value( 'drm_security_level', '--not obtained--', TABLE_SESSION) data += '\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 += '\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 += '\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 += '\n>>> Error details: {}'.format(exc) except Exception as exc: # pylint: disable=broad-except data += '\nThe data could not be obtained. Error details: {}'.format( exc) data += '\n\n' + '#### ESN ####\n' esn = get_esn() or '--not obtained--' data += '\nUsed ESN: ' + common.censure(esn) if len(esn) > 50 else esn data += '\nWebsite ESN: ' + (get_website_esn() or '--not obtained--') data += '\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 += '\n' + info except Exception as exc: # pylint: disable=broad-except data += '\nThe data could not be obtained. Error: {}'.format(exc) data += '\n' try: common.save_file(common.join_folders_paths(path, filename), data.encode('utf-8')) ui.show_notification('{}: {}'.format(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')
class MSLRequests(MSLRequestBuilder): """Provides methods to make MSL requests""" HTTP_HEADERS = { 'User-Agent': common.get_user_agent(), 'Content-Type': 'text/plain', 'Accept': '*/*', 'Host': 'www.netflix.com' } def __init__(self, msl_data, nfsession): super().__init__() self.nfsession: 'NFSessionOperations' = nfsession self._load_msl_data(msl_data) self.msl_switch_requested = False def _load_msl_data(self, msl_data): try: self.crypto.load_msl_data(msl_data) self.crypto.load_crypto_session(msl_data) except Exception: # pylint: disable=broad-except import traceback LOG.error(traceback.format_exc()) 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 _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 _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 _check_user_id_token(self, disable_msl_switch, force_auth_credential=False): """ Performs user id token checks and return the auth data checks: uid token validity, get if needed the owner uid token, set when use the switch :param: disable_msl_switch: to be used in requests that cannot make the switch :param: force_auth_credential: force the use of authentication with credentials :return: auth data that will be used in MSLRequestBuilder _add_auth_info """ # Warning: the user id token contains also contains the identity of the netflix profile # therefore it is necessary to use the right user id token for the request current_profile_guid = G.LOCAL_DB.get_active_profile_guid() owner_profile_guid = G.LOCAL_DB.get_guid_owner_profile() use_switch_profile = False user_id_token = None if not force_auth_credential: if current_profile_guid == owner_profile_guid: # The request will be executed from the owner profile # By default MSL is associated to the owner profile, then is not necessary get the owner token id # and it is not necessary use the MSL profile switch user_id_token = self.crypto.get_user_id_token(current_profile_guid) # The user_id_token can return None when the add-on is installed from scratch, # in this case will be used the authentication with the user credentials else: # The request will be executed from a non-owner profile # Get the non-owner profile token id, by checking that exists and it is valid user_id_token = self.crypto.get_user_id_token(current_profile_guid) if not user_id_token and not disable_msl_switch: # The token does not exist/valid, you must set the MSL profile switch use_switch_profile = True # First check if the owner profile token exist and it is valid user_id_token = self.crypto.get_user_id_token(owner_profile_guid) if not user_id_token: # The owner profile token id does not exist/valid, then get it self._get_owner_user_id_token() user_id_token = self.crypto.get_user_id_token(owner_profile_guid) # Mark msl_switch_requested as True in order to make a bind event request self.msl_switch_requested = True return {'use_switch_profile': use_switch_profile, 'user_id_token': user_id_token} @measure_exec_time_decorator(is_immediate=True) def chunked_request(self, endpoint, request_data, esn, disable_msl_switch=True, force_auth_credential=False): """Do a POST request and process the chunked response""" self._mastertoken_checks() auth_data = self._check_user_id_token(disable_msl_switch, force_auth_credential) LOG.debug('Chunked request will be executed with auth data: {}', auth_data) chunked_response = self._process_chunked_response( self._post(endpoint, self.msl_request(request_data, esn, auth_data)), save_uid_token_to_owner=auth_data['user_id_token'] is None) return chunked_response['result'] def _post(self, endpoint, request_data): """Execute a post request""" is_attempts_enabled = 'reqAttempt=' in endpoint retry = 1 while True: try: if is_attempts_enabled: _endpoint = endpoint.replace('reqAttempt=', f'reqAttempt={retry}') else: _endpoint = endpoint LOG.debug('Executing POST request to {}', _endpoint) start = time.perf_counter() response = self.nfsession.session.post(url=_endpoint, data=request_data, headers=self.HTTP_HEADERS, timeout=4) LOG.debug('Request took {}s', time.perf_counter() - start) LOG.debug('Request returned response with status {}', response.status_code) break except httpx.ConnectError as exc: LOG.error('HTTP request error: {}', exc) if retry == 3: raise retry += 1 LOG.warn('Another attempt will be performed ({})', retry) response.raise_for_status() return response.text @measure_exec_time_decorator(is_immediate=True) def _process_chunked_response(self, response, save_uid_token_to_owner=False): """Parse and decrypt an encrypted chunked response. Raise an error if the response is plaintext json""" LOG.debug('Received encrypted chunked response') header, payloads = _process_json_response(response) # TODO: sending for the renewal request is not yet implemented # if self.crypto.get_current_mastertoken_validity()['is_renewable']: # # Check if mastertoken is renewed # self.request_builder.crypto.compare_mastertoken(header['mastertoken']) header_data = self.decrypt_header_data(header['headerdata']) if 'useridtoken' in header_data: # Save the user id token for the future msl requests profile_guid = G.LOCAL_DB.get_guid_owner_profile() if save_uid_token_to_owner else\ G.LOCAL_DB.get_active_profile_guid() self.crypto.save_user_id_token(profile_guid, header_data['useridtoken']) # if 'keyresponsedata' in header_data: # LOG.debug('Found key handshake in response data') # # Update current mastertoken # self.request_builder.crypto.parse_key_response(header_data, True) decrypted_response = _decrypt_chunks(payloads, self.crypto) return _raise_if_error(decrypted_response)