Esempio n. 1
0
 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')
Esempio n. 2
0
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
Esempio n. 3
0
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)