Guest User

Untitled

a guest
Oct 25th, 2019
3,064
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 29.03 KB | None | 0 0
  1. # pylint: skip-file
  2. # -*- coding: utf-8 -*-
  3. # Author: trummerjo
  4. # Module: MSLHttpRequestHandler
  5. # Created on: 26.01.2017
  6. # License: MIT https://goo.gl/5bMj3H
  7.  
  8. import re
  9. import sys
  10. import zlib
  11. import gzip
  12. import json
  13. import time
  14. import base64
  15. import random
  16. import uuid
  17. from StringIO import StringIO
  18. from datetime import datetime
  19. import requests
  20. import xml.etree.ElementTree as ET
  21.  
  22. import xbmcaddon
  23.  
  24. #check if we are on Android
  25. import subprocess
  26. try:
  27. sdkversion = int(subprocess.check_output(
  28. ['/system/bin/getprop', 'ro.build.version.sdk']))
  29. except:
  30. sdkversion = 0
  31.  
  32. if sdkversion >= 18:
  33. from MSLMediaDrm import MSLMediaDrmCrypto as MSLHandler
  34. else:
  35. from MSLCrypto import MSLCrypto as MSLHandler
  36.  
  37. class MSL(object):
  38. # Is a handshake already performed and the keys loaded
  39. handshake_performed = False
  40. last_drm_context = ''
  41. last_playback_context = ''
  42. current_message_id = 0
  43. session = requests.session()
  44. rndm = random.SystemRandom()
  45. tokens = []
  46. base_url = 'http://www.netflix.com/api/msl/NFCDCH-LX/cadmium/'
  47. endpoints = {
  48. 'manifest': base_url + 'manifest',
  49. 'license': base_url + 'license'
  50. }
  51.  
  52. def __init__(self, nx_common):
  53.  
  54. """
  55. The Constructor checks for already existing crypto Keys.
  56. If they exist it will load the existing keys
  57. """
  58. self.nx_common = nx_common
  59.  
  60. self.crypto = MSLHandler(nx_common)
  61.  
  62. if self.nx_common.file_exists(self.nx_common.data_path, 'msl_data.json'):
  63. self.init_msl_data()
  64. else:
  65. self.crypto.fromDict(None)
  66. self.__perform_key_handshake()
  67.  
  68. def load_manifest(self, viewable_id, dolby, hevc, hdr, dolbyvision, vp9):
  69. """
  70. Loads the manifets for the given viewable_id and
  71. returns a mpd-XML-Manifest
  72.  
  73. :param viewable_id: The id of of the viewable
  74. :return: MPD XML Manifest or False if no success
  75. """
  76. manifest_request_data = {
  77. 'method': 'manifest',
  78. 'lookupType': 'PREPARE',
  79. 'viewableIds': [viewable_id],
  80. 'profiles': [
  81. # Video
  82. 'playready-h264bpl30-dash',
  83. 'playready-h264mpl30-dash',
  84. 'playready-h264mpl31-dash',
  85. 'playready-h264mpl40-dash',
  86.  
  87. # Audio
  88. 'heaac-2-dash',
  89.  
  90. # Subtiltes (handled separately)
  91. # 'dfxp-ls-sdh',
  92. # 'simplesdh',
  93. # 'nflx-cmisc',
  94.  
  95. # Unkown
  96. 'BIF240',
  97. 'BIF320'
  98. ],
  99. 'drmSystem': 'widevine',
  100. 'appId': '14673889385265',
  101. 'sessionParams': {
  102. 'pinCapableClient': False,
  103. 'uiplaycontext': 'null'
  104. },
  105. 'sessionId': '14673889385265',
  106. 'trackId': 0,
  107. 'flavor': 'PRE_FETCH',
  108. 'secureUrls': False,
  109. 'supportPreviewContent': True,
  110. 'forceClearStreams': False,
  111. 'languages': ['de-DE'],
  112. 'clientVersion': '4.0004.899.011',
  113. 'uiVersion': 'akira'
  114. }
  115.  
  116. # subtitles
  117. addon = xbmcaddon.Addon('inputstream.adaptive')
  118. if addon and self.nx_common.compare_versions(map(int, addon.getAddonInfo('version').split('.')), [2, 3, 8]):
  119. manifest_request_data['profiles'].append('webvtt-lssdh-ios8')
  120. else:
  121. manifest_request_data['profiles'].append('simplesdh')
  122.  
  123. # add hevc profiles if setting is set
  124. if hevc is True:
  125. main = 'hevc-main-'
  126. main10 = 'hevc-main10-'
  127. prk = 'dash-cenc-prk'
  128. cenc = 'dash-cenc'
  129. ctl = 'dash-cenc-ctl'
  130. manifest_request_data['profiles'].append(main10 + 'L41-' + cenc)
  131. manifest_request_data['profiles'].append(main10 + 'L50-' + cenc)
  132. manifest_request_data['profiles'].append(main10 + 'L51-' + cenc)
  133. manifest_request_data['profiles'].append(main + 'L30-' + cenc)
  134. manifest_request_data['profiles'].append(main + 'L31-' + cenc)
  135. manifest_request_data['profiles'].append(main + 'L40-' + cenc)
  136. manifest_request_data['profiles'].append(main + 'L41-' + cenc)
  137. manifest_request_data['profiles'].append(main + 'L50-' + cenc)
  138. manifest_request_data['profiles'].append(main + 'L51-' + cenc)
  139. manifest_request_data['profiles'].append(main10 + 'L30-' + cenc)
  140. manifest_request_data['profiles'].append(main10 + 'L31-' + cenc)
  141. manifest_request_data['profiles'].append(main10 + 'L40-' + cenc)
  142. manifest_request_data['profiles'].append(main10 + 'L41-' + cenc)
  143. manifest_request_data['profiles'].append(main10 + 'L50-' + cenc)
  144. manifest_request_data['profiles'].append(main10 + 'L51-' + cenc)
  145. manifest_request_data['profiles'].append(main10 + 'L30-' + prk)
  146. manifest_request_data['profiles'].append(main10 + 'L31-' + prk)
  147. manifest_request_data['profiles'].append(main10 + 'L40-' + prk)
  148. manifest_request_data['profiles'].append(main10 + 'L41-' + prk)
  149. manifest_request_data['profiles'].append(main + 'L30-L31-' + ctl)
  150. manifest_request_data['profiles'].append(main + 'L31-L40-' + ctl)
  151. manifest_request_data['profiles'].append(main + 'L40-L41-' + ctl)
  152. manifest_request_data['profiles'].append(main + 'L50-L51-' + ctl)
  153. manifest_request_data['profiles'].append(main10 + 'L30-L31-' + ctl)
  154. manifest_request_data['profiles'].append(main10 + 'L31-L40-' + ctl)
  155. manifest_request_data['profiles'].append(main10 + 'L40-L41-' + ctl)
  156. manifest_request_data['profiles'].append(main10 + 'L50-L51-' + ctl)
  157.  
  158. if hdr is True:
  159. hdr = 'hevc-hdr-main10-'
  160. manifest_request_data['profiles'].append(hdr + 'L30-' + cenc)
  161. manifest_request_data['profiles'].append(hdr + 'L31-' + cenc)
  162. manifest_request_data['profiles'].append(hdr + 'L40-' + cenc)
  163. manifest_request_data['profiles'].append(hdr + 'L41-' + cenc)
  164. manifest_request_data['profiles'].append(hdr + 'L50-' + cenc)
  165. manifest_request_data['profiles'].append(hdr + 'L51-' + cenc)
  166. manifest_request_data['profiles'].append(hdr + 'L30-' + prk)
  167. manifest_request_data['profiles'].append(hdr + 'L31-' + prk)
  168. manifest_request_data['profiles'].append(hdr + 'L40-' + prk)
  169. manifest_request_data['profiles'].append(hdr + 'L41-' + prk)
  170. manifest_request_data['profiles'].append(hdr + 'L50-' + prk)
  171. manifest_request_data['profiles'].append(hdr + 'L51-' + prk)
  172.  
  173.  
  174. if dolbyvision is True:
  175. dv = 'hevc-dv-main10-'
  176. dv5 = 'hevc-dv5-main10-'
  177. manifest_request_data['profiles'].append(dv + 'L30-' + cenc)
  178. manifest_request_data['profiles'].append(dv + 'L31-' + cenc)
  179. manifest_request_data['profiles'].append(dv + 'L40-' + cenc)
  180. manifest_request_data['profiles'].append(dv + 'L41-' + cenc)
  181. manifest_request_data['profiles'].append(dv + 'L50-' + cenc)
  182. manifest_request_data['profiles'].append(dv + 'L51-' + cenc)
  183. manifest_request_data['profiles'].append(dv5 + 'L30-' + prk)
  184. manifest_request_data['profiles'].append(dv5 + 'L31-' + prk)
  185. manifest_request_data['profiles'].append(dv5 + 'L40-' + prk)
  186. manifest_request_data['profiles'].append(dv5 + 'L41-' + prk)
  187. manifest_request_data['profiles'].append(dv5 + 'L50-' + prk)
  188. manifest_request_data['profiles'].append(dv5 + 'L51-' + prk)
  189.  
  190. if hevc is False or vp9 is True:
  191. manifest_request_data['profiles'].append('vp9-profile0-L30-dash-cenc')
  192. manifest_request_data['profiles'].append('vp9-profile0-L31-dash-cenc')
  193.  
  194. # Check if dolby sound is enabled and add to profles
  195. if dolby:
  196. manifest_request_data['profiles'].append('ddplus-2.0-dash')
  197. manifest_request_data['profiles'].append('ddplus-5.1-dash')
  198.  
  199. request_data = self.__generate_msl_request_data(manifest_request_data)
  200.  
  201. try:
  202. resp = self.session.post(self.endpoints['manifest'], request_data)
  203. except:
  204. resp = None
  205. exc = sys.exc_info()
  206. msg = '[MSL][POST] Error {} {}'
  207. self.nx_common.log(msg=msg.format(exc[0], exc[1]))
  208.  
  209. if resp:
  210. try:
  211. # if the json() does not fail we have an error because
  212. # the manifest response is a chuncked json response
  213. resp.json()
  214. self.nx_common.log(
  215. msg='Error getting Manifest: ' + resp.text)
  216. return False
  217. except ValueError:
  218. # json() failed so parse the chunked response
  219. #self.nx_common.log(
  220. # msg='Got chunked Manifest Response: ' + resp.text)
  221. resp = self.__parse_chunked_msl_response(resp.text)
  222. #self.nx_common.log(
  223. # msg='Parsed chunked Response: ' + json.dumps(resp))
  224. data = self.__decrypt_payload_chunks(resp['payloads'])
  225. return self.__tranform_to_dash(data)
  226. return False
  227.  
  228. def get_license(self, challenge, sid):
  229. """
  230. Requests and returns a license for the given challenge and sid
  231. :param challenge: The base64 encoded challenge
  232. :param sid: The sid paired to the challengew
  233. :return: Base64 representation of the licensekey or False unsuccessfull
  234. """
  235. license_request_data = {
  236. 'method': 'license',
  237. 'licenseType': 'STANDARD',
  238. 'clientVersion': '4.0004.899.011',
  239. 'uiVersion': 'akira',
  240. 'languages': ['de-DE'],
  241. 'playbackContextId': self.last_playback_context,
  242. 'drmContextIds': [self.last_drm_context],
  243. 'challenges': [{
  244. 'dataBase64': challenge,
  245. 'sessionId': sid
  246. }],
  247. 'clientTime': int(time.time()),
  248. 'xid': int((int(time.time()) + 0.1612) * 1000)
  249.  
  250. }
  251. request_data = self.__generate_msl_request_data(license_request_data)
  252.  
  253. try:
  254. resp = self.session.post(self.endpoints['license'], request_data)
  255. except:
  256. resp = None
  257. exc = sys.exc_info()
  258. self.nx_common.log(
  259. msg='[MSL][POST] Error {} {}'.format(exc[0], exc[1]))
  260.  
  261. if resp:
  262. try:
  263. # If is valid json the request for the licnese failed
  264. resp.json()
  265. self.nx_common.log(msg='Error getting license: '+resp.text)
  266. return False
  267. except ValueError:
  268. # json() failed so we have a chunked json response
  269. resp = self.__parse_chunked_msl_response(resp.text)
  270. data = self.__decrypt_payload_chunks(resp['payloads'])
  271. if data['success'] is True:
  272. return data['result']['licenses'][0]['data']
  273. else:
  274. self.nx_common.log(
  275. msg='Error getting license: ' + json.dumps(data))
  276. return False
  277. return False
  278.  
  279. def __decrypt_payload_chunks(self, payloadchunks):
  280. decrypted_payload = ''
  281. for chunk in payloadchunks:
  282. payloadchunk = json.JSONDecoder().decode(chunk)
  283. payload = payloadchunk.get('payload')
  284. decoded_payload = base64.standard_b64decode(payload)
  285. encryption_envelope = json.JSONDecoder().decode(decoded_payload)
  286. # Decrypt the text
  287. plaintext = self.crypto.decrypt(base64.standard_b64decode(encryption_envelope['iv']),
  288. base64.standard_b64decode(encryption_envelope.get('ciphertext')))
  289. # unpad the plaintext
  290. plaintext = json.JSONDecoder().decode(plaintext)
  291. data = plaintext.get('data')
  292.  
  293. # uncompress data if compressed
  294. if plaintext.get('compressionalgo') == 'GZIP':
  295. decoded_data = base64.standard_b64decode(data)
  296. data = zlib.decompress(decoded_data, 16 + zlib.MAX_WBITS)
  297. else:
  298. data = base64.standard_b64decode(data)
  299. decrypted_payload += data
  300.  
  301. decrypted_payload = json.JSONDecoder().decode(decrypted_payload)[1]['payload']['data']
  302. decrypted_payload = base64.standard_b64decode(decrypted_payload)
  303. return json.JSONDecoder().decode(decrypted_payload)
  304.  
  305. def __tranform_to_dash(self, manifest):
  306.  
  307. self.nx_common.save_file(
  308. data_path=self.nx_common.data_path,
  309. filename='manifest.json',
  310. content=json.dumps(manifest))
  311. manifest = manifest['result']['viewables'][0]
  312.  
  313. self.last_playback_context = manifest['playbackContextId']
  314. self.last_drm_context = manifest['drmContextId']
  315.  
  316. # Check for pssh
  317. pssh = ''
  318. keyid = None
  319. if 'psshb64' in manifest:
  320. if len(manifest['psshb64']) >= 1:
  321. pssh = manifest['psshb64'][0]
  322. psshbytes = base64.standard_b64decode(pssh)
  323. if len(psshbytes) == 52:
  324. keyid = psshbytes[36:]
  325.  
  326. seconds = manifest['runtime']/1000
  327. init_length = seconds / 2 * 12 + 20*1000
  328. duration = "PT"+str(seconds)+".00S"
  329.  
  330. root = ET.Element('MPD')
  331. root.attrib['xmlns'] = 'urn:mpeg:dash:schema:mpd:2011'
  332. root.attrib['xmlns:cenc'] = 'urn:mpeg:cenc:2013'
  333. root.attrib['mediaPresentationDuration'] = duration
  334.  
  335. period = ET.SubElement(root, 'Period', start='PT0S', duration=duration)
  336.  
  337. # One Adaption Set for Video
  338. for video_track in manifest['videoTracks']:
  339. video_adaption_set = ET.SubElement(
  340. parent=period,
  341. tag='AdaptationSet',
  342. mimeType='video/mp4',
  343. contentType="video")
  344.  
  345. # Content Protection
  346. if keyid:
  347. protection = ET.SubElement(
  348. parent=video_adaption_set,
  349. tag='ContentProtection',
  350. value='cenc',
  351. schemeIdUri='urn:mpeg:dash:mp4protection:2011')
  352. protection.set('cenc:default_KID', str(uuid.UUID(bytes=keyid)))
  353.  
  354. protection = ET.SubElement(
  355. parent=video_adaption_set,
  356. tag='ContentProtection',
  357. schemeIdUri='urn:uuid:EDEF8BA9-79D6-4ACE-A3C8-27DCD51D21ED')
  358.  
  359. ET.SubElement(
  360. parent=protection,
  361. tag='widevine:license',
  362. robustness_level='HW_SECURE_CODECS_REQUIRED')
  363.  
  364. if pssh is not '':
  365. ET.SubElement(protection, 'cenc:pssh').text = pssh
  366.  
  367. for downloadable in video_track['downloadables']:
  368.  
  369. codec = 'h264'
  370. if 'hevc' in downloadable['contentProfile']:
  371. codec = 'hevc'
  372. elif downloadable['contentProfile'] == 'vp9-profile0-L30-dash-cenc':
  373. codec = 'vp9.0.30'
  374. elif downloadable['contentProfile'] == 'vp9-profile0-L31-dash-cenc':
  375. codec = 'vp9.0.31'
  376.  
  377. hdcp_versions = '0.0'
  378. for hdcp in downloadable['hdcpVersions']:
  379. if hdcp != 'none':
  380. hdcp_versions = hdcp if hdcp != 'any' else '1.0'
  381.  
  382. rep = ET.SubElement(
  383. parent=video_adaption_set,
  384. tag='Representation',
  385. width=str(downloadable['width']),
  386. height=str(downloadable['height']),
  387. bandwidth=str(downloadable['bitrate']*1024),
  388. hdcp=hdcp_versions,
  389. nflxContentProfile=str(downloadable['contentProfile']),
  390. codecs=codec,
  391. mimeType='video/mp4')
  392.  
  393. # BaseURL
  394. base_url = self.__get_base_url(downloadable['urls'])
  395. ET.SubElement(rep, 'BaseURL').text = base_url
  396. # Init an Segment block
  397. segment_base = ET.SubElement(
  398. parent=rep,
  399. tag='SegmentBase',
  400. indexRange='0-' + str(init_length),
  401. indexRangeExact='true')
  402.  
  403. # Multiple Adaption Set for audio
  404. language = None
  405. for audio_track in manifest['audioTracks']:
  406. impaired = 'false'
  407. original = 'false'
  408. default = 'false'
  409.  
  410. if audio_track.get('trackType') == 'ASSISTIVE':
  411. impaired = 'true'
  412. elif not language or language == audio_track.get('language'):
  413. language = audio_track.get('language')
  414. default = 'true'
  415. if audio_track.get('language').find('[') > 0:
  416. original = 'true'
  417.  
  418. audio_adaption_set = ET.SubElement(
  419. parent=period,
  420. tag='AdaptationSet',
  421. lang=audio_track['bcp47'],
  422. contentType='audio',
  423. mimeType='audio/mp4',
  424. impaired=impaired,
  425. original=original,
  426. default=default)
  427. for downloadable in audio_track['downloadables']:
  428. codec = 'aac'
  429. #self.nx_common.log(msg=downloadable)
  430. is_dplus2 = downloadable['contentProfile'] == 'ddplus-2.0-dash'
  431. is_dplus5 = downloadable['contentProfile'] == 'ddplus-5.1-dash'
  432. if is_dplus2 or is_dplus5:
  433. codec = 'ec-3'
  434. #self.nx_common.log(msg='codec is: ' + codec)
  435. rep = ET.SubElement(
  436. parent=audio_adaption_set,
  437. tag='Representation',
  438. codecs=codec,
  439. bandwidth=str(downloadable['bitrate']*1024),
  440. mimeType='audio/mp4')
  441.  
  442. # AudioChannel Config
  443. uri = 'urn:mpeg:dash:23003:3:audio_channel_configuration:2011'
  444. ET.SubElement(
  445. parent=rep,
  446. tag='AudioChannelConfiguration',
  447. schemeIdUri=uri,
  448. value=str(audio_track.get('channelsCount')))
  449.  
  450. # BaseURL
  451. base_url = self.__get_base_url(downloadable['urls'])
  452. ET.SubElement(rep, 'BaseURL').text = base_url
  453. # Index range
  454. segment_base = ET.SubElement(
  455. parent=rep,
  456. tag='SegmentBase',
  457. indexRange='0-' + str(init_length),
  458. indexRangeExact='true')
  459.  
  460. # Multiple Adaption Sets for subtiles
  461. for text_track in manifest.get('textTracks'):
  462. is_downloadables = 'downloadables' not in text_track
  463. if is_downloadables or text_track.get('downloadables') is None:
  464. continue
  465. # Only one subtitle representation per adaptationset
  466. downloadable = text_track['downloadables'][0]
  467.  
  468. subtiles_adaption_set = ET.SubElement(
  469. parent=period,
  470. tag='AdaptationSet',
  471. lang=text_track.get('bcp47'),
  472. codecs='wvtt' if downloadable.get('contentProfile') == 'webvtt-lssdh-ios8' else 'stpp',
  473. contentType='text',
  474. mimeType='text/vtt' if downloadable.get('contentProfile') == 'webvtt-lssdh-ios8' else 'application/ttml+xml')
  475. role = ET.SubElement(
  476. parent=subtiles_adaption_set,
  477. tag = 'Role',
  478. schemeIdUri = 'urn:mpeg:dash:role:2011',
  479. value = 'forced' if text_track.get('isForced') == True else 'main')
  480. rep = ET.SubElement(
  481. parent=subtiles_adaption_set,
  482. tag='Representation',
  483. nflxProfile=downloadable.get('contentProfile'))
  484. base_url = self.__get_base_url(downloadable['urls'])
  485. ET.SubElement(rep, 'BaseURL').text = base_url
  486.  
  487. xml = ET.tostring(root, encoding='utf-8', method='xml')
  488. xml = xml.replace('\n', '').replace('\r', '')
  489.  
  490. self.nx_common.save_file(
  491. data_path=self.nx_common.data_path,
  492. filename='manifest.mpd',
  493. content=xml)
  494.  
  495. return xml
  496.  
  497. def __get_base_url(self, urls):
  498. for key in urls:
  499. return urls[key]
  500.  
  501. def __parse_chunked_msl_response(self, message):
  502. header = message.split('}}')[0] + '}}'
  503. payloads = re.split(',\"signature\":\"[0-9A-Za-z=/+]+\"}', message.split('}}')[1])
  504. payloads = [x + '}' for x in payloads][:-1]
  505.  
  506. return {
  507. 'header': header,
  508. 'payloads': payloads
  509. }
  510.  
  511. def __generate_msl_request_data(self, data):
  512. #self.__load_msl_data()
  513. header_encryption_envelope = self.__encrypt(
  514. plaintext=self.__generate_msl_header())
  515. headerdata = base64.standard_b64encode(header_encryption_envelope)
  516. header = {
  517. 'headerdata': headerdata,
  518. 'signature': self.__sign(header_encryption_envelope),
  519. 'mastertoken': self.mastertoken,
  520. }
  521.  
  522. # Serialize the given Data
  523. raw_marshalled_data = json.dumps(data)
  524. marshalled_data = raw_marshalled_data.replace('"', '\\"')
  525. serialized_data = '[{},{"headers":{},"path":"/cbp/cadmium-13"'
  526. serialized_data += ',"payload":{"data":"'
  527. serialized_data += marshalled_data
  528. serialized_data += '"},"query":""}]\n'
  529.  
  530. compressed_data = self.__compress_data(serialized_data)
  531.  
  532. # Create FIRST Payload Chunks
  533. first_payload = {
  534. 'messageid': self.current_message_id,
  535. 'data': compressed_data,
  536. 'compressionalgo': 'GZIP',
  537. 'sequencenumber': 1,
  538. 'endofmsg': True
  539. }
  540. first_payload_encryption_envelope = self.__encrypt(
  541. plaintext=json.dumps(first_payload))
  542. payload = base64.standard_b64encode(first_payload_encryption_envelope)
  543. first_payload_chunk = {
  544. 'payload': payload,
  545. 'signature': self.__sign(first_payload_encryption_envelope),
  546. }
  547. request_data = json.dumps(header) + json.dumps(first_payload_chunk)
  548. return request_data
  549.  
  550. def __compress_data(self, data):
  551. # GZIP THE DATA
  552. out = StringIO()
  553. with gzip.GzipFile(fileobj=out, mode="w") as f:
  554. f.write(data)
  555. return base64.standard_b64encode(out.getvalue())
  556.  
  557. def __generate_msl_header(
  558. self,
  559. is_handshake=False,
  560. is_key_request=False,
  561. compressionalgo='GZIP',
  562. encrypt=True):
  563. """
  564. Function that generates a MSL header dict
  565. :return: The base64 encoded JSON String of the header
  566. """
  567. self.current_message_id = self.rndm.randint(0, pow(2, 52))
  568. esn = self.nx_common.get_esn()
  569.  
  570. # Add compression algo if not empty
  571. compression_algos = [compressionalgo] if compressionalgo != '' else []
  572.  
  573. header_data = {
  574. 'sender': esn,
  575. 'handshake': is_handshake,
  576. 'nonreplayable': False,
  577. 'capabilities': {
  578. 'languages': ['en-US'],
  579. 'compressionalgos': compression_algos
  580. },
  581. 'recipient': 'Netflix',
  582. 'renewable': True,
  583. 'messageid': self.current_message_id,
  584. 'timestamp': 1467733923
  585. }
  586.  
  587. # If this is a keyrequest act diffrent then other requests
  588. if is_key_request:
  589. header_data['keyrequestdata'] = self.crypto.get_key_request()
  590. else:
  591. if 'usertoken' in self.tokens:
  592. pass
  593. else:
  594. account = self.nx_common.get_credentials()
  595. # Auth via email and password
  596. header_data['userauthdata'] = {
  597. 'scheme': 'EMAIL_PASSWORD',
  598. 'authdata': {
  599. 'email': account['email'],
  600. 'password': account['password']
  601. }
  602. }
  603.  
  604. return json.dumps(header_data)
  605.  
  606. def __encrypt(self, plaintext):
  607. return json.dumps(self.crypto.encrypt(plaintext, self.nx_common.get_esn(), self.sequence_number))
  608.  
  609. def __sign(self, text):
  610. """
  611. Calculates the HMAC signature for the given
  612. text with the current sign key and SHA256
  613.  
  614. :param text:
  615. :return: Base64 encoded signature
  616. """
  617. return base64.standard_b64encode(self.crypto.sign(text))
  618.  
  619. def perform_key_handshake(self):
  620. self.__perform_key_handshake()
  621.  
  622. def __perform_key_handshake(self):
  623. esn = self.nx_common.get_esn()
  624. self.nx_common.log(msg='perform_key_handshake: esn:' + esn)
  625.  
  626. if not esn:
  627. return False
  628.  
  629. header = self.__generate_msl_header(
  630. is_key_request=True,
  631. is_handshake=True,
  632. compressionalgo='',
  633. encrypt=False)
  634.  
  635. request = {
  636. 'entityauthdata': {
  637. 'scheme': 'NONE',
  638. 'authdata': {
  639. 'identity': esn
  640. }
  641. },
  642. 'headerdata': base64.standard_b64encode(header),
  643. 'signature': '',
  644. }
  645. #self.nx_common.log(msg='Key Handshake Request:')
  646. #self.nx_common.log(msg=json.dumps(request))
  647.  
  648. try:
  649. resp = self.session.post(
  650. url=self.endpoints['manifest'],
  651. data=json.dumps(request, sort_keys=True))
  652. except:
  653. resp = None
  654. exc = sys.exc_info()
  655. self.nx_common.log(
  656. msg='[MSL][POST] Error {} {}'.format(exc[0], exc[1]))
  657.  
  658. if resp and resp.status_code == 200:
  659. resp = resp.json()
  660. if 'errordata' in resp:
  661. self.nx_common.log(msg='Key Exchange failed')
  662. self.nx_common.log(
  663. msg=base64.standard_b64decode(resp['errordata']))
  664. return False
  665. base_head = base64.standard_b64decode(resp['headerdata'])
  666.  
  667. headerdata=json.JSONDecoder().decode(base_head)
  668. self.__set_master_token(headerdata['keyresponsedata']['mastertoken'])
  669. self.crypto.parse_key_response(headerdata)
  670. self.__save_msl_data()
  671. else:
  672. self.nx_common.log(msg='Key Exchange failed')
  673. self.nx_common.log(msg=resp.text)
  674.  
  675. def init_msl_data(self):
  676. self.nx_common.log(msg='MSL Data exists. Use old Tokens.')
  677. self.__load_msl_data()
  678. self.handshake_performed = True
  679.  
  680. def __load_msl_data(self):
  681. raw_msl_data = self.nx_common.load_file(
  682. data_path=self.nx_common.data_path,
  683. filename='msl_data.json')
  684. msl_data = json.JSONDecoder().decode(raw_msl_data)
  685. # Check expire date of the token
  686. raw_token = msl_data['tokens']['mastertoken']['tokendata']
  687. base_token = base64.standard_b64decode(raw_token)
  688. master_token = json.JSONDecoder().decode(base_token)
  689. exp = int(master_token['expiration'])
  690. valid_until = datetime.utcfromtimestamp(exp)
  691. present = datetime.now()
  692. difference = valid_until - present
  693. # If token expires in less then 10 hours or is expires renew it
  694. self.nx_common.log(msg='Expiration time: Key:' + str(valid_until) + ', Now:' + str(present) + ', Diff:' + str(difference.total_seconds()))
  695. difference = difference.total_seconds() / 60 / 60
  696. if self.crypto.fromDict(msl_data) or difference < 10:
  697. self.__perform_key_handshake()
  698. return
  699.  
  700. self.__set_master_token(msl_data['tokens']['mastertoken'])
  701.  
  702. def save_msl_data(self):
  703. self.__save_msl_data()
  704.  
  705. def __save_msl_data(self):
  706. """
  707. Saves the keys and tokens in json file
  708. :return:
  709. """
  710. data = {
  711. 'tokens': {
  712. 'mastertoken': self.mastertoken
  713. }
  714. }
  715. data.update(self.crypto.toDict())
  716.  
  717. serialized_data = json.JSONEncoder().encode(data)
  718. self.nx_common.save_file(
  719. data_path=self.nx_common.data_path,
  720. filename='msl_data.json',
  721. content=serialized_data)
  722.  
  723. def __set_master_token(self, master_token):
  724. self.mastertoken = master_token
  725. raw_token = master_token['tokendata']
  726. base_token = base64.standard_b64decode(raw_token)
  727. decoded_token = json.JSONDecoder().decode(base_token)
  728. self.sequence_number = decoded_token.get('sequencenumber')
Advertisement
Add Comment
Please, Sign In to add comment