This repository has been archived on 2024-07-02. You can view files and clone it, but cannot push or open issues or pull requests.
Netflix-4K-Script/pywidevine/clients/netflix/client3.py

944 lines
40 KiB
Python
Raw Normal View History

2021-12-23 10:27:57 +00:00
import base64
from datetime import datetime
import gzip
import zlib
import json
import logging
from io import BytesIO
import random
import time
import os
import re
from itertools import islice
import requests
from Cryptodome.Cipher import AES
from Cryptodome.Cipher import PKCS1_OAEP
from Cryptodome.Hash import HMAC, SHA256
from Cryptodome.PublicKey import RSA
from Cryptodome.Random import get_random_bytes
from Cryptodome.Util import Padding
import pywidevine.downloader.wvdownloaderconfig as wvdl_cfg
import pywidevine.clients.netflix.config as nf_cfg
import pywidevine.clients.netflix.subs as subs
from pywidevine.downloader.tracks import VideoTrack, AudioTrack, SubtitleTrack
# keys are not padded properly
def base64key_decode(payload):
l = len(payload) % 4
if l == 2:
payload += '=='
elif l == 3:
payload += '='
elif l != 0:
raise ValueError('Invalid base64 string')
return base64.urlsafe_b64decode(payload.encode('utf-8'))
class NetflixClient(object):
def __init__(self, client_config):
self.logger = logging.getLogger(__name__)
self.logger.debug("creating NetflixClient object")
self.client_config = client_config
self.session = requests.Session()
self.current_message_id = 0
self.rsa_key = None
self.encryption_key = None
self.sign_key = None
self.sequence_number = None
self.mastertoken = None
self.useridtoken = None
self.playbackContextId = None
self.drmContextId = None
self.tokens = []
self.rndm = random.SystemRandom()
#self.cookies = self.cookie_login()
def login(self):
self.logger.info("acquiring token & key for netflix api")
config_dict = self.client_config.config
#if self.cookies is None:
# self.cookies = self.cookie_login()
if self.file_exists(wvdl_cfg.COOKIES_FOLDER, config_dict['msl_storage']):
self.logger.info("old MSL data found, using")
self.__load_msl_data()
elif self.file_exists(wvdl_cfg.COOKIES_FOLDER, 'rsa_key.bin'):
self.logger.info('old RSA key found, using')
self.__load_rsa_keys()
self.__perform_key_handshake()
else:
self.logger.info('create new RSA Keys')
# Create new Key Pair and save
self.rsa_key = RSA.generate(2048)
self.__save_rsa_keys()
self.__perform_key_handshake()
if self.encryption_key:
self.logger.info("negotiation successful, token & key acquired")
return True
else:
self.logger.error("failed to perform key handshake")
return False
'''def cookie_login(self):
"""Logs into netflix"""
config_dict = self.client_config.config
post_data = {
'email': config_dict['username'],
'password': config_dict['password'],
'rememberMe': 'true',
'mode': 'login',
'action': 'loginAction',
'withFields': 'email,password,rememberMe,nextPage,showPassword',
'nextPage': '',
'showPassword': '',
}
req = self.session.post('https://www.netflix.com/login', post_data)
return req.cookies'''
def get_track_and_init_info(self,all_subs=False,only_subs=False):
config_dict = self.client_config.config
self.logger.info("video information")
id = int(time.time() * 10000)
"""
manifest_request_data = {
'method': 'manifest',
'lookupType': 'PREPARE',
'viewableIds': [self.client_config.viewable_id],
'profiles': config_dict['profiles'],
'drmSystem': 'widevine',
'appId': '14673889385265',
'sessionParams': {
'pinCapableClient': False,
'uiplaycontext': 'null'
},
'sessionId': '14673889385265',
'trackId': 0,
'flavor': 'PRE_FETCH',
'secureUrls': True,
'supportPreviewContent': True,
'forceClearStreams': False,
'languages': ['en-US'],
'clientVersion': '6.0011.474.011',
'uiVersion': 'shakti-v25d2fa21',
'showAllSubDubTracks': all_subs
}
"""
#'profiles': config_dict['profiles'],
#profiles = ['playready-h264mpl30-dash', 'playready-h264mpl31-dash', 'playready-h264mpl40-dash', 'playready-h264hpl30-dash', 'playready-h264hpl31-dash', 'playready-h264hpl40-dash', 'heaac-2-dash', 'dfxp-ls-sdh', 'simplesdh',]
#profiles = ['playready-h264mpl30-dash', 'playready-h264mpl31-dash', 'playready-h264mpl40-dash', 'playready-h264hpl30-dash', 'playready-h264hpl31-dash', 'playready-h264hpl40-dash', 'heaac-2-dash', 'BIF240', 'BIF320']
manifest_request_data = {
'version': 2,
'url': '/manifest',
'id': id,
'esn': self.client_config.config['esn'],
'languages' : ['en-US'],
'uiVersion': 'shakti-v25d2fa21',
'clientVersion': '6.0011.474.011',
'params': {
'type': 'standard',
'viewableId': [self.client_config.viewable_id],
'profiles': config_dict['profiles'],
'flavor': 'PRE_FETCH',
'drmType': 'widevine',
'drmVersion': 25,
'usePsshBox': True,
'isBranching': False,
'useHttpsStreams': False,
'imageSubtitleHeight': 1080,
'uiVersion': 'shakti-vb45817f4',
'clientVersion': '6.0011.511.011',
'supportsPreReleasePin': True,
'supportsWatermark': True,
'showAllSubDubTracks': False,
'titleSpecificData': {},
'videoOutputInfo': [{
'type': 'DigitalVideoOutputDescriptor',
'outputType': 'unknown',
'supportedHdcpVersions': [],
'isHdcpEngaged': False
}],
'preferAssistiveAudio': False,
'isNonMember': False
}
}
self.logger.debug("requesting manifest")
request_data = self.__generate_msl_request_data(manifest_request_data)
resp = self.session.post(nf_cfg.MANIFEST_ENDPOINT, request_data, proxies=self.client_config.get_proxies())
self.logger.debug(manifest_request_data)
data = {}
try:
# if the json() does not fail we have an error because the manifest response is a chuncked json response
resp.json()
self.logger.debug('Error getting Manifest: '+resp.text)
return False, None
except ValueError:
# json() failed so parse the chunked response
global data1
self.logger.debug('Got chunked Manifest Response: ' + resp.text)
resp = self.__parse_chunked_msl_response(resp.text)
#print(resp.text)
self.logger.debug('Parsed chunked Response: ' + json.dumps(resp))
data = self.__decrypt_payload_chunk(resp['payloads'])
data1 = self.__decrypt_payload_chunk(resp['payloads'])
fobj = open("manifest.json", "w")
#fobj.write(str(data['result']))
#data['result']['links']['license']['href']
playlist = json.dumps(data)
fobj.write(playlist)
fobj.close()
self.logger.debug(data['result'])
#[0]
try:
#self.logger.debug("manifest json: %s" % data['result']['viewables'][0])
#self.playbackContextId = data['result']['viewables'][0]['playbackContextId']
#self.drmContextId = data['result']['viewables'][0]['drmContextId']
self.logger.debug("manifest json: %s" % data['result'])
self.playbackContextId = data['result']['playbackContextId']
self.drmContextId = data['result']['drmContextId']
except (KeyError, IndexError):
self.logger.error('No viewables found')
if 'errorDisplayMessage' in data['result']:
self.logger.error('MSL Error Message: {}'.format(data['result']['errorDisplayMessage']))
return False, None
self.logger.debug(data)
self.logger.debug("netflix cannot get title name from manifest")
#subtitle_tracks_js = data['result']['viewables'][0]['textTracks']
subtitle_tracks_js = data['result']['timedtexttracks']
self.logger.debug(len(subtitle_tracks_js))
subtitle_tracks_filtered = [x for x in subtitle_tracks_js if "language" in x]
subtitle_tracks_filtered = list(islice(subtitle_tracks_filtered, len(subtitle_tracks_js)-1))
self.logger.debug(subtitle_tracks_filtered)
self.logger.info("found {} subtitle tracks".format(len(subtitle_tracks_filtered)))
for subtitle_track in subtitle_tracks_filtered:
self.logger.info(
"Name: {} bcp47: {} type: {}".format(subtitle_track['language'], subtitle_track['language'], subtitle_track['trackType'])
)
if only_subs:
wvdl_sts = []
for i, subtitle in enumerate(subtitle_tracks_filtered):
if not subtitle['isForcedNarrative']:
wvdl_sts.append(
SubtitleTrack(i, subtitle['language'], subtitle['language'], True, next(iter(list(subtitle['downloadables'][0]['urls'].values()))),
'srt')
)
return True, wvdl_sts
#video_tracks_js = data['result']['viewables'][0]['videoTracks'][0]['downloadables']
video_tracks_js = data['result']['video_tracks'][0]['streams']
self.logger.info("found {} video tracks".format(len(video_tracks_js)))
for vid_id, video in enumerate(sorted(video_tracks_js, key= lambda v: int(v['bitrate']))):
self.logger.info(
"{} - Bitrate: {} Profile: {} Size: {} Width: {} Height: {}".format(vid_id, video['bitrate'],
video['content_profile'],
video['size'],
video['res_w'],
video['res_h'])
)
audio_tracks_js = data['result']['audio_tracks']
audio_tracks_flattened = []
for audio_track in audio_tracks_js:
for downloadable in audio_track['streams']:
new_track = audio_track.copy()
new_track['downloadables'] = downloadable
audio_tracks_flattened.append(new_track)
self.logger.info("found {} audio tracks".format(len(audio_tracks_flattened)))
for aud_id, audio_track in enumerate(
sorted(audio_tracks_flattened, key=lambda v: int(v['downloadables']['bitrate']))):
self.logger.info(
"{} = Bitrate: {} Profile: {} Channels: {} Language: {} Lang: {} Size: {}".format(aud_id, audio_track[
'downloadables']['bitrate'],
audio_track[
'downloadables'][
'content_profile'],
audio_track[
'channels'],
audio_track[
'language'],
audio_track['language'],
audio_track[
'downloadables'][
'size'])
)
self.logger.info("selected tracks")
if config_dict['video_track'] is not None:
self.logger.debug("VIDEO_TRACK_ID ARGUMENT: {}".format(config_dict['video_track']))
if config_dict['video_track'] >= len(video_tracks_js):
self.logger.error("selected video track does not exist")
return False, []
video_track = sorted(video_tracks_js, key= lambda v: int(v['bitrate']))[int(config_dict['video_track'])]
else:
video_track = sorted(video_tracks_js, key= lambda v: int(v['bitrate']), reverse=True)[0]
self.logger.info("VIDEO - Bitrate: {} Profile: {} Size: {} Width: {} Height: {}".format(video_track['bitrate'],
video_track['content_profile'],
video_track['size'],
video_track['res_w'],
video_track['res_h']))
audio_tracks = []
if config_dict['audio_tracks'] != []:
self.logger.debug("AUDIO_TRACK_ID ARUGMENT: {}".format(config_dict['audio_tracks']))
sorted_aud = sorted(audio_tracks_flattened, key=lambda v: int(v['downloadables']['bitrate']))
for t_id in config_dict['audio_tracks']:
audio_tracks.append(sorted_aud[int(t_id)])
else:
selected_track = None
bitrate = 0
channels = 0
profile = None
#default_audio_lang = data['result']['viewables'][0]['orderedAudioTracks'][0]
audio_lang_selectors = {
'English': 'en',
'French': 'fr',
'German': 'de',
'Italian': 'it',
'Spanish': 'es',
'Flemish': 'nl-BE',
'Finnish': 'fi',
'No Dialogue': 'zxx',
'Czech': 'cs',
'European Spanish': 'es-ES',
'Japanese': 'ja',
'Brazilian Portuguese': 'pt-BR',
'Polish': 'pl',
'Turkish': 'tr',
'Mandarin': 'zh',
'Cantonese': 'yue',
'Arabic': 'ar',
'Korean': 'ko',
'Hebrew': 'he',
'Norwegian': 'nb'
}
aud_profile_table = {
'heaac-2-dash': 0,
'ddplus-2.0-dash': 1,
'ddplus-5.1-dash': 2,
'dd-5.1-dash': 3
}
sorted_aud = sorted(audio_tracks_flattened, key=lambda v: int(v['downloadables']['bitrate']), reverse=True)
if video_track['res_h'] == 480:
sorted_aud = [a_track for a_track in sorted_aud if a_track['channels'] == '2.0']
audio_selected = ['en']
if config_dict['audio_language'] is not None:
audio_selected = config_dict['audio_language']
for audio_select in audio_selected:
for aud_track_sorted in sorted_aud:
if aud_track_sorted['language'] == audio_select:
audio_tracks.append(aud_track_sorted)
break
# if selected_track == None:
# selected_track = aud_track_sorted
# bitrate = aud_track_sorted['downloadables']['bitrate']
# channels = aud_track_sorted['channels']
# profile = aud_profile_table[aud_track_sorted['downloadables']['contentProfile']]
# if (bitrate < aud_track_sorted['downloadables']['bitrate']) and (
# channels <= aud_track_sorted['channels']) and (
# profile <= aud_profile_table[aud_track_sorted['downloadables']['contentProfile']]):
# selected_track = aud_track_sorted
# bitrate = aud_track_sorted['downloadables']['bitrate']
# channels = aud_track_sorted['channels']
# profile = aud_profile_table[aud_track_sorted['downloadables']['contentProfile']]
# audio_tracks.append(selected_track)
# if selected_track == None:
# self.logger.error("netfix cannot auto select audio track, please specify at least one")
# return False, []
for audio_track in audio_tracks:
self.logger.info("AUDIO - Bitrate: {} Profile: {} Channels: {} Language: {} Size: {}".format(
audio_track['downloadables']['bitrate'],
audio_track['downloadables']['content_profile'],
audio_track['channels'],
audio_track['language'],
audio_track['downloadables']['size']))
if config_dict['subtitle_languages'] is not None:
self.logger.debug("SUBTITLE_LANGUAGE_ARGUMENT: {}".format(config_dict['subtitle_languages']))
if 'all' in config_dict['subtitle_languages']:
subtitle_tracks = subtitle_tracks_filtered
else:
subtitle_tracks = [x for x in subtitle_tracks_filtered if
x['language'] in config_dict['subtitle_languages']]
else:
subtitle_tracks = None
if subtitle_tracks:
for subtitle in subtitle_tracks:
self.logger.info("SUBTITLE - Name: {} bcp47: {}".format(subtitle['language'], subtitle['language']))
#init_data_b64 = data['result']['viewables'][0]['psshb64'][0]
init_data_b64 = data['result']['video_tracks'][0]['drmHeader']['bytes']
#init_data_b64 = 'AAAANHBzc2gAAAAA7e+LqXnWSs6jyCfc1R0h7QAAABQIARIQAAAAAAUrePMAAAAAAAAAAA=='
#init_data_b64 = ""
#cert_data_b64 = data['result']['viewables'][0]['cert']
cert_data_b64 = 'CAUSwwUKvQIIAxIQ5US6QAvBDzfTtjb4tU/7QxiH8c+TBSKOAjCCAQoCggEBAObzvlu2hZRsapAPx4Aa4GUZj4/GjxgXUtBH4THSkM40x63wQeyVxlEEo1D/T1FkVM/S+tiKbJiIGaT0Yb5LTAHcJEhODB40TXlwPfcxBjJLfOkF3jP6wIlqbb6OPVkDi6KMTZ3EYL6BEFGfD1ag/LDsPxG6EZIn3k4S3ODcej6YSzG4TnGD0szj5m6uj/2azPZsWAlSNBRUejmP6Tiota7g5u6AWZz0MsgCiEvnxRHmTRee+LO6U4dswzF3Odr2XBPD/hIAtp0RX8JlcGazBS0GABMMo2qNfCiSiGdyl2xZJq4fq99LoVfCLNChkn1N2NIYLrStQHa35pgObvhwi7ECAwEAAToQdGVzdC5uZXRmbGl4LmNvbRKAA4TTLzJbDZaKfozb9vDv5qpW5A/DNL9gbnJJi/AIZB3QOW2veGmKT3xaKNQ4NSvo/EyfVlhc4ujd4QPrFgYztGLNrxeyRF0J8XzGOPsvv9Mc9uLHKfiZQuy21KZYWF7HNedJ4qpAe6gqZ6uq7Se7f2JbelzENX8rsTpppKvkgPRIKLspFwv0EJQLPWD1zjew2PjoGEwJYlKbSbHVcUNygplaGmPkUCBThDh7p/5Lx5ff2d/oPpIlFvhqntmfOfumt4i+ZL3fFaObvkjpQFVAajqmfipY0KAtiUYYJAJSbm2DnrqP7+DmO9hmRMm9uJkXC2MxbmeNtJHAHdbgKsqjLHDiqwk1JplFMoC9KNMp2pUNdX9TkcrtJoEDqIn3zX9p+itdt3a9mVFc7/ZL4xpraYdQvOwP5LmXj9galK3s+eQJ7bkX6cCi+2X+iBmCMx4R0XJ3/1gxiM5LiStibCnfInub1nNgJDojxFA3jH/IuUcblEf/5Y0s1SzokBnR8V0KbA=='
#cert_data_b64 = cert_data_b64.decode("utf-8")
#cert_data_b64 = 'Cr0CCAMSEOVEukALwQ8307Y2+LVP+0MYh/HPkwUijgIwggEKAoIBAQDm875btoWUbGqQD8eAGuBlGY+Pxo8YF1LQR+Ex0pDONMet8EHslcZRBKNQ/09RZFTP0vrYimyYiBmk9GG+S0wB3CRITgweNE15cD33MQYyS3zpBd4z+sCJam2+jj1ZA4uijE2dxGC+gRBRnw9WoPyw7D8RuhGSJ95OEtzg3Ho+mEsxuE5xg9LM4+Zuro/9msz2bFgJUjQUVHo5j+k4qLWu4ObugFmc9DLIAohL58UR5k0XnvizulOHbMMxdzna9lwTw/4SALadEV/CZXBmswUtBgATDKNqjXwokohncpdsWSauH6vfS6FXwizQoZJ9TdjSGC60rUB2t+aYDm74cIuxAgMBAAE6EHRlc3QubmV0ZmxpeC5jb20SgAOE0y8yWw2Win6M2/bw7+aqVuQPwzS/YG5ySYvwCGQd0Dltr3hpik98WijUODUr6PxMn1ZYXOLo3eED6xYGM7Riza8XskRdCfF8xjj7L7/THPbixyn4mULsttSmWFhexzXnSeKqQHuoKmerqu0nu39iW3pcxDV/K7E6aaSr5ID0SCi7KRcL9BCUCz1g9c43sNj46BhMCWJSm0mx1XFDcoKZWhpj5FAgU4Q4e6f+S8eX39nf6D6SJRb4ap7Znzn7preIvmS93xWjm75I6UBVQGo6pn4qWNCgLYlGGCQCUm5tg566j+/g5jvYZkTJvbiZFwtjMW5njbSRwB3W4CrKoyxw4qsJNSaZRTKAvSjTKdqVDXV/U5HK7SaBA6iJ981/aforXbd2vZlRXO/2S+Maa2mHULzsD+S5l4/YGpSt7PnkCe25F+nAovtl/ogZgjMeEdFyd/9YMYjOS4krYmwp3yJ7m9ZzYCQ6I8RQN4x/yLlHG5RH/+WNLNUs6JAZ0fFdCmw='
#cert_data_b64 = ''
"""
self.logger.debug(video_track['urls'][0]['url'])
self.logger.debug(audio_track['downloadables']['urls'][0]['url'])
self.logger.debug(list(subtitle['ttDownloadables']['simplesdh']['downloadUrls'].values())[0])
"""
#iter(video_track['urls'].values())
#next(iter(video_track['urls'][0]['url'])),
wvdl_vt = VideoTrack(True, video_track['size'], 0, video_track['urls'][0]['url'],
video_track['content_profile'], video_track['bitrate'],
video_track['res_w'], video_track['res_h'])
wvdl_ats = []
audio_languages = []
#iter(audio_track['downloadables']['urls'].values())),
#next(iter(audio_track['downloadables']['urls'][0]['url'])),
for id, audio_track in enumerate(audio_tracks):
audio_languages.append(audio_track['language'])
wvdl_ats.append(
AudioTrack(False, audio_track['downloadables']['size'], id, audio_track['downloadables']['urls'][0]['url'],
audio_track['downloadables']['content_profile'], audio_track['downloadables']['bitrate'], audio_track['language'])
)
wvdl_sts = []
use_captions = { 'it': True }
if subtitle_tracks:
for track in subtitle_tracks:
use_captions.update({track['language']: True})
for track in subtitle_tracks:
if track['language'] == 'it' and track['trackType'] == "SUBTITLES" and not track['isForcedNarrative']:
use_captions.update({track['language']:False})
for i, subtitle in enumerate(subtitle_tracks):
if 'it' in audio_languages:
if subtitle['isForcedNarrative'] and subtitle['language'] == 'it' and (subtitle['trackType'] == "SUBTITLES" or use_captions[subtitle['language']] == True):
wvdl_sts.append(
SubtitleTrack(i, 'Forced', subtitle['language'], True, next(iter(list(subtitle['downloadables'][0]['urls'].values()))),
'srt')
)
elif not subtitle['isForcedNarrative'] and (subtitle['trackType'] == "SUBTITLES" or use_captions[subtitle['labguage']] == True):
wvdl_sts.append(
SubtitleTrack(i, subtitle['language'], subtitle['language'], False, next(iter(list(subtitle['downloadables'][0]['urls'].values()))),
'srt')
)
else:
if not subtitle['isForcedNarrative'] and subtitle['language'] == 'it' and (subtitle['trackType'] == "SUBTITLES" or use_captions[subtitle['language']] == True):
wvdl_sts.append(
SubtitleTrack(i, subtitle['language'], subtitle['language'], True, next(iter(list(subtitle['downloadables'][0]['urls'].values()))),
'srt')
)
elif not subtitle['isForcedNarrative'] and (subtitle['trackType'] == "SUBTITLES" or use_captions[subtitle['language']] == True):
wvdl_sts.append(
SubtitleTrack(i, subtitle['language'], subtitle['language'], False, next(iter(list(subtitle['ttDownloadables']['simplesdh']['downloadUrls'].values())[0])),
'srt')
)
#SubtitleTrack(i, subtitle['language'], subtitle['language'], False, next(iter(subtitle['downloadables'][0]['urls'].values())),
#(subtitle['ttDownloadables']['simplesdh']['downloadUrls'].values()[0])
return True, [wvdl_vt, wvdl_ats, wvdl_sts, init_data_b64, cert_data_b64]
#, cert_data_b64
def get_license(self, challenge, session_id):
id = int(time.time() * 10000)
self.logger.info("doing license request")
#self.logger.debug(challenge)
#.decode('latin-1'),
#'challengeBase64': base64.b64encode(challenge).decode('utf-8'),
#challenge = "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"
self.logger.debug("challenge - {}".format(base64.b64encode(challenge)))
license_request_data = {
#'method': 'license',
#'licenseType': 'STANDARD',
#'clientVersion': '4.0004.899.011',
#'uiVersion': 'akira',
#'languages': ['en-US'],
#'playbackContextId': self.playbackContextId,
#'drmContextIds': [self.drmContextId],
#'challenges': [{
# 'dataBase64': base64.b64encode(challenge).decode('utf-8'),
# 'sessionId': "14673889385265"
'version': 2,
'url': data1['result']['links']['license']['href'],
'id': id,
'esn': 'NFCDIE-02-DMT4C1VHTF81YTJY8LXRA4GF9J07H1',
'languages': ['en-US'],
'uiVersion': 'shakti-v25d2fa21',
'clientVersion': '6.0011.511.011',
'params': [{
'sessionId': session_id,
'clientTime': int(id / 10000),
'challengeBase64': base64.b64encode(challenge).decode('utf-8'),
'xid': int(id + 1610)
}],
#self.client_config.config['esn']
#'clientTime': int(time.time()),
#'clientTime': int(id / 10000),
#'sessionId': '14673889385265',
#'clientTime': int(id / 10000),
#'xid': int((int(time.time()) + 0.1612) * 1000)
'echo': 'sessionId'
}
#license_request_data = str(license_request_data)
request_data = self.__generate_msl_request_data_lic(license_request_data)
resp = self.session.post(nf_cfg.LICENSE_ENDPOINT, request_data, proxies=self.client_config.get_proxies())
"""
try:
# If is valid json the request for the licnese failed
resp.json()
self.logger.debug('Error getting license: '+resp.text)
exit(1)
except ValueError:
# json() failed so we have a chunked json response
resp = self.__parse_chunked_msl_response(resp.text)
data = self.__decrypt_payload_chunk(resp['payloads'])
self.logger.debug(data)
#if data['success'] is True:
if 'licenseResponseBase64' in data[0]:
#return data['result']['licenses'][0]['data']
#return response['result'][0]['licenseResponseBase64']
return data[0]['licenseResponseBase64']
else:
self.logger.debug('Error getting license: ' + json.dumps(data))
exit(1)
"""
# json() failed so we have a chunked json response
resp = self.__parse_chunked_msl_response(resp.text)
data = self.__decrypt_payload_chunk(resp['payloads'])
self.logger.debug(data)
fobj = open("license.json", "w")
playlist = str(data)
fobj.write(playlist)
fobj.close()
return data['result'][0]['licenseResponseBase64']
#if data['success'] is True:
#data[0]
#if 'licenseResponseBase64' in data:
#return data['result']['licenses'][0]['data']
#return response['result'][0]['licenseResponseBase64']
#return data['links']['releaseLicense']['href']
# return data['licenseResponseBase64']
def __get_base_url(self, urls):
for key in urls:
return urls[key]
def __decrypt_payload_chunk(self, payloadchunks):
decrypted_payload = ''
for chunk in payloadchunks:
payloadchunk = json.loads(chunk)
try:
encryption_envelope = str(bytes(payloadchunk['payload'], encoding="utf-8").decode('utf8'))
except TypeError:
encryption_envelope = payloadchunk['payload']
# Decrypt the text
cipher = AES.new(self.encryption_key, AES.MODE_CBC, base64.standard_b64decode(json.loads(base64.standard_b64decode(encryption_envelope))['iv']))
plaintext = cipher.decrypt(base64.standard_b64decode(json.loads(base64.standard_b64decode(encryption_envelope))['ciphertext']))
# unpad the plaintext
plaintext = json.loads((Padding.unpad(plaintext, 16)))
data = plaintext['data']
# uncompress data if compressed
data = base64.standard_b64decode(data)
#if plaintext['compressionalgo'] == 'GZIP':
# data = zlib.decompress(base64.standard_b64decode(data), 16 + zlib.MAX_WBITS)
#else:
# data = base64.standard_b64decode(data)
decrypted_payload += data.decode('utf-8')
#decrypted_payload = json.loads(decrypted_payload)[1]['payload']['data']
#decrypted_payload = base64.standard_b64decode(decrypted_payload)
#return json.loads(decrypted_payload)
decrypted_payload = json.loads(decrypted_payload)
return decrypted_payload
def __parse_chunked_msl_response(self, message):
header = message.split('}}')[0] + '}}'
payloads = re.split(',\"signature\":\"[0-9A-Za-z=/+]+\"}', message.split('}}')[1])
payloads = [x + '}' for x in payloads][:-1]
return {
'header': header,
'payloads': payloads
}
def __generate_msl_request_data(self, data):
header_encryption_envelope = self.__encrypt(self.__generate_msl_header())
header = {
'headerdata': base64.standard_b64encode(header_encryption_envelope.encode('utf-8')).decode('utf-8'),
'signature': self.__sign(header_encryption_envelope).decode('utf-8'),
'mastertoken': self.mastertoken,
}
# Serialize the given Data
#serialized_data = json.dumps(data)
#serialized_data = serialized_data.replace('"', '\\"')
#serialized_data = '[{},{"headers":{},"path":"/cbp/cadmium-13","payload":{"data":"' + serialized_data + '"},"query":""}]\n'
#compressed_data = self.__compress_data(serialized_data)
data1 = json.dumps(data)
print(data1)
data1 = data1.encode('utf-8')
# Create FIRST Payload Chunks
first_payload = {
"messageid": self.current_message_id,
#"data": compressed_data.decode('utf-8'),
#"compressionalgo": "GZIP",
"data": (base64.standard_b64encode(data1)).decode('utf-8'),
"sequencenumber": 1,
"endofmsg": True
}
first_payload_encryption_envelope = self.__encrypt(json.dumps(first_payload))
first_payload_chunk = {
'payload': base64.standard_b64encode(first_payload_encryption_envelope.encode('utf-8')).decode('utf-8'),
'signature': self.__sign(first_payload_encryption_envelope).decode('utf-8'),
}
request_data = json.dumps(header) + json.dumps(first_payload_chunk)
return request_data
def __generate_msl_request_data_lic(self, data):
header_encryption_envelope = self.__encrypt(self.__generate_msl_header())
header = {
'headerdata': base64.standard_b64encode(header_encryption_envelope.encode('utf-8')).decode('utf-8'),
'signature': self.__sign(header_encryption_envelope).decode('utf-8'),
'mastertoken': self.mastertoken,
}
# Serialize the given Data
#serialized_data = json.dumps(data)
#serialized_data = serialized_data.replace('"', '\\"')
#serialized_data = '[{},{"headers":{},"path":"/cbp/cadmium-13","payload":{"data":"' + serialized_data + '"},"query":""}]\n'
#compressed_data = self.__compress_data(serialized_data)
print(data)
#print('\n')
#data1 = json.dumps(data)
#print(data1)
#data1 = data1.encode('utf-8')
# Create FIRST Payload Chunks
first_payload = {
"messageid": self.current_message_id,
#"data": compressed_data.decode('utf-8'),
#"compressionalgo": "GZIP",
#"data": (base64.standard_b64encode(data1)).decode('utf-8'),
"data": base64.standard_b64encode(json.dumps(data).encode('utf-8')).decode('utf-8'),
"sequencenumber": 1,
"endofmsg": True
}
first_payload_encryption_envelope = self.__encrypt(json.dumps(first_payload))
first_payload_chunk = {
'payload': base64.standard_b64encode(first_payload_encryption_envelope.encode('utf-8')).decode('utf-8'),
'signature': self.__sign(first_payload_encryption_envelope).decode('utf-8'),
}
request_data = json.dumps(header) + json.dumps(first_payload_chunk)
return request_data
def __compress_data(self, data):
# GZIP THE DATA
out = BytesIO()
with gzip.GzipFile(fileobj=out, mode="w") as f:
f.write(data.encode('utf-8'))
return base64.standard_b64encode(out.getvalue())
def __generate_msl_header(self, is_handshake=False, is_key_request=False, compressionalgo="GZIP", encrypt=True):
"""
Function that generates a MSL header dict
:return: The base64 encoded JSON String of the header
"""
self.current_message_id = self.rndm.randint(0, pow(2, 52))
header_data = {
'sender': self.client_config.config['esn'],
'handshake': is_handshake,
'nonreplayable': False,
'capabilities': {
'languages': ["en-US"]
#'languages': ["en-US"],
#'compressionalgos': [],
#'encoderformats' : ['JSON'],
},
'recipient': 'Netflix',
'renewable': True,
'messageid': self.current_message_id,
'timestamp': time.time()
}
# Add compression algo if not empty
#if compressionalgo is not "":
# header_data['capabilities']['compressionalgos'].append(compressionalgo)
# If this is a keyrequest act diffrent then other requests
if is_key_request:
public_key = base64.standard_b64encode(self.rsa_key.publickey().exportKey(format='DER')).decode('utf-8')
header_data['keyrequestdata'] = [{
'scheme': 'ASYMMETRIC_WRAPPED',
'keydata': {
'publickey': public_key,
'mechanism': 'JWK_RSA',
'keypairid': 'superKeyPair'
}
}]
#header_data['userauthdata'] = {
# 'scheme' : 'NETFLIXID',
# 'authdata' : {
# 'netflixid' : self.cookies['NetflixId'],
# 'securenetflixid' : self.cookies['SecureNetflixId'],
# }
#}
#header_data['keyrequestdata'] = [{
# 'scheme': 'WIDEVINE',
# 'keydata': {
# 'keyrequest':'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',
# },
#}]
else:
if 'usertoken' in self.tokens:
pass
else:
# Auth via email and password
header_data['userauthdata'] = {
'scheme': 'EMAIL_PASSWORD',
'authdata': {
'email': self.client_config.config['username'],
'password': self.client_config.config['password']
}
}
return json.dumps(header_data)
def __encrypt(self, plaintext):
"""
Encrypt the given Plaintext with the encryption key
:param plaintext:
:return: Serialized JSON String of the encryption Envelope
"""
iv = get_random_bytes(16)
encryption_envelope = {
'ciphertext': '',
'keyid': self.client_config.config['esn'] + '_' + str(self.sequence_number),
'sha256': 'AA==',
'iv': base64.standard_b64encode(iv).decode('utf-8')
}
# Padd the plaintext
plaintext = Padding.pad(plaintext.encode('utf-8'), 16)
# Encrypt the text
cipher = AES.new(self.encryption_key, AES.MODE_CBC, iv)
ciphertext = cipher.encrypt(plaintext)
encryption_envelope['ciphertext'] = base64.standard_b64encode(ciphertext).decode('utf-8')
return json.dumps(encryption_envelope)
def __sign(self, text):
"""
Calculates the HMAC signature for the given text with the current sign key and SHA256
:param text:
:return: Base64 encoded signature
"""
signature = HMAC.new(self.sign_key, text.encode('utf-8'), SHA256).digest()
return base64.standard_b64encode(signature)
def __perform_key_handshake(self):
header = self.__generate_msl_header(is_key_request=True, is_handshake=True, compressionalgo="", encrypt=False)
request = {
'entityauthdata': {
'scheme': 'NONE',
'authdata': {
'identity': self.client_config.config['esn']
}
},
'headerdata': base64.standard_b64encode(header.encode('utf-8')).decode('utf-8'),
'signature': '',
}
self.logger.debug('Key Handshake Request:')
self.logger.debug(json.dumps(request))
resp = self.session.post(nf_cfg.MANIFEST_ENDPOINT, json.dumps(request, sort_keys=True), proxies=self.client_config.get_proxies())
if resp.status_code == 200:
resp = resp.json()
if 'errordata' in resp:
self.logger.debug('Key Exchange failed')
self.logger.debug(base64.standard_b64decode(resp['errordata']))
return False
self.logger.debug(resp)
self.logger.debug('Key Exchange Sucessful')
self.__parse_crypto_keys(json.JSONDecoder().decode(base64.standard_b64decode(resp['headerdata']).decode('utf-8')))
else:
self.logger.debug('Key Exchange failed')
self.logger.debug(resp.text)
def __parse_crypto_keys(self, headerdata):
self.__set_master_token(headerdata['keyresponsedata']['mastertoken'])
#self.__set_userid_token(headerdata['useridtoken'])
# Init Decryption
encrypted_encryption_key = base64.standard_b64decode(headerdata['keyresponsedata']['keydata']['encryptionkey'])
encrypted_sign_key = base64.standard_b64decode(headerdata['keyresponsedata']['keydata']['hmackey'])
cipher_rsa = PKCS1_OAEP.new(self.rsa_key)
# Decrypt encryption key
encryption_key_data = json.JSONDecoder().decode(cipher_rsa.decrypt(encrypted_encryption_key).decode('utf-8'))
self.encryption_key = base64key_decode(encryption_key_data['k'])
# Decrypt sign key
sign_key_data = json.JSONDecoder().decode(cipher_rsa.decrypt(encrypted_sign_key).decode('utf-8'))
self.sign_key = base64key_decode(sign_key_data['k'])
self.__save_msl_data()
self.handshake_performed = True
def __load_msl_data(self):
msl_data = json.JSONDecoder().decode(
self.load_file(wvdl_cfg.COOKIES_FOLDER, self.client_config.config['msl_storage']).decode('utf-8'))
# Check expire date of the token
master_token = json.JSONDecoder().decode(
base64.standard_b64decode(msl_data['tokens']['mastertoken']['tokendata']).decode('utf-8'))
valid_until = datetime.utcfromtimestamp(int(master_token['expiration']))
present = datetime.now()
difference = valid_until - present
difference = difference.total_seconds() / 60 / 60
# If token expires in less then 10 hours or is expires renew it
if difference < 10:
self.__load_rsa_keys()
self.__perform_key_handshake()
return
self.__set_master_token(msl_data['tokens']['mastertoken'])
#self.__set_userid_token(msl_data['tokens']['useridtoken'])
self.encryption_key = base64.standard_b64decode(msl_data['encryption_key'])
self.sign_key = base64.standard_b64decode(msl_data['sign_key'])
def __save_msl_data(self):
"""
Saves the keys and tokens in json file
:return:
"""
data = {
"encryption_key": base64.standard_b64encode(self.encryption_key).decode('utf-8'),
'sign_key': base64.standard_b64encode(self.sign_key).decode('utf-8'),
'tokens': {
'mastertoken': self.mastertoken,
#'useridtoken': self.useridtoken,
}
}
serialized_data = json.JSONEncoder().encode(data)
self.save_file(wvdl_cfg.COOKIES_FOLDER, self.client_config.config['msl_storage'], serialized_data.encode('utf-8'))
def __set_master_token(self, master_token):
self.mastertoken = master_token
self.sequence_number = json.JSONDecoder().decode(base64.standard_b64decode(master_token['tokendata']).decode('utf-8'))[
'sequencenumber']
def __set_userid_token(self, userid_token):
self.useridtoken = userid_token
def __load_rsa_keys(self):
loaded_key = self.load_file(wvdl_cfg.COOKIES_FOLDER, self.client_config.config['rsa_key'])
self.rsa_key = RSA.importKey(loaded_key)
def __save_rsa_keys(self):
self.logger.debug('Save RSA Keys')
# Get the DER Base64 of the keys
encrypted_key = self.rsa_key.exportKey()
self.save_file(wvdl_cfg.COOKIES_FOLDER, self.client_config.config['rsa_key'], encrypted_key)
@staticmethod
def file_exists(msl_data_path, filename):
"""
Checks if a given file exists
:param filename: The filename
:return: True if so
"""
return os.path.isfile(os.path.join(msl_data_path, filename))
@staticmethod
def save_file(msl_data_path, filename, content):
"""
Saves the given content under given filename
:param filename: The filename
:param content: The content of the file
"""
with open(os.path.join(msl_data_path,filename), 'wb') as file_:
file_.write(content)
file_.flush()
file_.close()
@staticmethod
def load_file(msl_data_path, filename):
"""
Loads the content of a given filename
:param filename: The file to load
:return: The content of the file
"""
with open(os.path.join(msl_data_path,filename), 'rb') as file_:
file_content = file_.read()
file_.close()
return file_content
def get_track_download(self, track):
return self.session.get(track.url, stream=True, proxies=self.client_config.get_proxies())
def get_subtitle_download(self, track):
try:
req = self.session.get(track.url, proxies=self.client_config.get_proxies())
except:
while True:
try:
req = self.session.get(track.url, proxies=self.client_config.get_proxies())
return req
except:
continue
return req
def get_wvconfig_options(self):
return {'server_cert_required': True, 'pssh_header': True}
def needs_ffmpeg(self):
return True
def finagle_subs(self, subtitles):
return subs.to_srt(subtitles)