Adds init segment length calculation
[plugin.video.netflix.git] / resources / lib / MSL.py
1 import base64
2 import gzip
3 import json
4 import os
5 import pprint
6 import random
7 from StringIO import StringIO
8 import requests
9 import zlib
10
11 import time
12 from Cryptodome.PublicKey import RSA
13 from Cryptodome.Cipher import PKCS1_OAEP
14 from Cryptodome.Cipher import AES
15 from Cryptodome.Random import get_random_bytes
16 from Cryptodome.Hash import HMAC, SHA256
17 from Cryptodome.Util import Padding
18 import xml.etree.ElementTree as ET
19
20 pp = pprint.PrettyPrinter(indent=4)
21
22 def base64key_decode(payload):
23     l = len(payload) % 4
24     if l == 2:
25         payload += '=='
26     elif l == 3:
27         payload += '='
28     elif l != 0:
29         raise ValueError('Invalid base64 string')
30     return base64.urlsafe_b64decode(payload.encode('utf-8'))
31
32
33 class MSL:
34     handshake_performed = False  # Is a handshake already performed and the keys loaded
35     last_drm_context = ''
36     last_playback_context = ''
37     #esn = "NFCDCH-LX-CQE0NU6PA5714R25VPLXVU2A193T36"
38     esn = "WWW-BROWSE-D7GW1G4NPXGR1F0X1H3EQGY3V1F5WE"
39     #esn = "NFCDIE-02-DCH84Q2EK3N6VFVQJ0NLRQ27498N0F"
40     current_message_id = 0
41     session = requests.session()
42     rndm = random.SystemRandom()
43     tokens = []
44     endpoints = {
45         'manifest': 'http://www.netflix.com/api/msl/NFCDCH-LX/cadmium/manifest',
46         'license': 'http://www.netflix.com/api/msl/NFCDCH-LX/cadmium/license'
47     }
48
49     def __init__(self, email, password, kodi_helper):
50         """
51         The Constructor checks for already existing crypto Keys.
52         If they exist it will load the existing keys
53         """
54         self.email = email
55         self.password = password
56         self.kodi_helper = kodi_helper
57         try:
58             os.mkdir(self.kodi_helper.msl_data_path)
59         except OSError:
60             pass
61
62         if self.file_exists(self.kodi_helper.msl_data_path, 'msl_data.json'):
63             self.kodi_helper.log(msg='MSL Data exists. Use old Tokens.')
64             self.__load_msl_data()
65             self.handshake_performed = True
66         elif self.file_exists(self.kodi_helper.msl_data_path, 'rsa_key.bin'):
67             self.kodi_helper.log(msg='RSA Keys do already exist load old ones')
68             self.__load_rsa_keys()
69             self.__perform_key_handshake()
70         else:
71             self.kodi_helper.log(msg='Create new RSA Keys')
72             # Create new Key Pair and save
73             self.rsa_key = RSA.generate(2048)
74             self.__save_rsa_keys()
75             self.__perform_key_handshake()
76
77     def load_manifest(self, viewable_id):
78         """
79         Loads the manifets for the given viewable_id and returns a mpd-XML-Manifest
80         :param viewable_id: The id of of the viewable
81         :return: MPD XML Manifest or False if no success
82         """
83         manifest_request_data = {
84             'method': 'manifest',
85             'lookupType': 'PREPARE',
86             'viewableIds': [viewable_id],
87             'profiles': [
88                 "playready-h264bpl30-dash",
89                 "playready-h264mpl30-dash",
90                 "playready-h264mpl31-dash",
91                 "playready-h264mpl40-dash",
92                 # "hevc-main-L30-dash-cenc",
93                 # "hevc-main-L31-dash-cenc",
94                 # "hevc-main-L40-dash-cenc",
95                 # "hevc-main-L41-dash-cenc",
96                 # "hevc-main-L50-dash-cenc",
97                 # "hevc-main-L51-dash-cenc",
98                 # "hevc-main10-L30-dash-cenc",
99                 # "hevc-main10-L31-dash-cenc",
100                 # "hevc-main10-L40-dash-cenc",
101                 # "hevc-main10-L41-dash-cenc",
102                 # "hevc-main10-L50-dash-cenc",
103                 # "hevc-main10-L51-dash-cenc",
104                 # "hevc-main10-L30-dash-cenc-prk",
105                 # "hevc-main10-L31-dash-cenc-prk",
106                 # "hevc-main10-L40-dash-cenc-prk",
107                 # "hevc-main10-L41-dash-cenc-prk",
108                 # "hevc-main-L30-L31-dash-cenc-tl",
109                 # "hevc-main-L31-L40-dash-cenc-tl",
110                 # "hevc-main-L40-L41-dash-cenc-tl",
111                 # "hevc-main-L50-L51-dash-cenc-tl",
112                 # "hevc-main10-L30-L31-dash-cenc-tl",
113                 # "hevc-main10-L31-L40-dash-cenc-tl",
114                 # "hevc-main10-L40-L41-dash-cenc-tl",
115                 # "hevc-main10-L50-L51-dash-cenc-tl",
116                 # "hevc-dv-main10-L30-dash-cenc",
117                 # "hevc-dv-main10-L31-dash-cenc",
118                 # "hevc-dv-main10-L40-dash-cenc",
119                 # "hevc-dv-main10-L41-dash-cenc",
120                 # "hevc-dv-main10-L50-dash-cenc",
121                 # "hevc-dv-main10-L51-dash-cenc",
122                 # "hevc-dv5-main10-L30-dash-cenc-prk",
123                 # "hevc-dv5-main10-L31-dash-cenc-prk",
124                 # "hevc-dv5-main10-L40-dash-cenc-prk",
125                 # "hevc-dv5-main10-L41-dash-cenc-prk",
126                 # "hevc-dv5-main10-L50-dash-cenc-prk",
127                 # "hevc-dv5-main10-L51-dash-cenc-prk",
128                 # "hevc-hdr-main10-L30-dash-cenc",
129                 # "hevc-hdr-main10-L31-dash-cenc",
130                 # "hevc-hdr-main10-L40-dash-cenc",
131                 # "hevc-hdr-main10-L41-dash-cenc",
132                 # "hevc-hdr-main10-L50-dash-cenc",
133                 # "hevc-hdr-main10-L51-dash-cenc",
134                 # "hevc-hdr-main10-L30-dash-cenc-prk",
135                 # "hevc-hdr-main10-L31-dash-cenc-prk",
136                 # "hevc-hdr-main10-L40-dash-cenc-prk",
137                 # "hevc-hdr-main10-L41-dash-cenc-prk",
138                 # "hevc-hdr-main10-L50-dash-cenc-prk",
139                 # "hevc-hdr-main10-L51-dash-cenc-prk"
140
141                # 'playready-h264mpl30-dash',
142                 #'playready-h264mpl31-dash',
143                 #'playready-h264mpl40-dash',
144                 #'hevc-main10-L41-dash-cenc',
145                 #'hevc-main10-L50-dash-cenc',
146                 #'hevc-main10-L51-dash-cenc',
147
148
149
150                 # Audio
151                 'heaac-2-dash',
152                 'ddplus-2.0-dash',
153                 'ddplus-5.1-dash',
154                 'dfxp-ls-sdh',
155                 'simplesdh',
156                 'nflx-cmisc',
157                 'BIF240',
158                 'BIF320'
159             ],
160             'drmSystem': 'widevine',
161             'appId': '14673889385265',
162             'sessionParams': {
163                 'pinCapableClient': False,
164                 'uiplaycontext': 'null'
165             },
166             'sessionId': '14673889385265',
167             'trackId': 0,
168             'flavor': 'PRE_FETCH',
169             'secureUrls': False,
170             'supportPreviewContent': True,
171             'forceClearStreams': False,
172             'languages': ['de-DE'],
173             'clientVersion': '4.0004.899.011',
174             'uiVersion': 'akira'
175         }
176         request_data = self.__generate_msl_request_data(manifest_request_data)
177         resp = self.session.post(self.endpoints['manifest'], request_data)
178
179         try:
180             # if the json() does not fail we have an error because the manifest response is a chuncked json response
181             resp.json()
182             self.kodi_helper.log(msg='Error getting Manifest: '+resp.text)
183             return False
184         except ValueError:
185             # json() failed so parse the chunked response
186             self.kodi_helper.log(msg='Got chunked Manifest Response: ' + resp.text)
187             resp = self.__parse_chunked_msl_response(resp.text)
188             self.kodi_helper.log(msg='Parsed chunked Response: ' + json.dumps(resp))
189             data = self.__decrypt_payload_chunk(resp['payloads'][0])
190             return self.__tranform_to_dash(data)
191
192     def get_license(self, challenge, sid):
193         """
194         Requests and returns a license for the given challenge and sid
195         :param challenge: The base64 encoded challenge
196         :param sid: The sid paired to the challengew
197         :return: Base64 representation of the license key or False if no success
198         """
199         license_request_data = {
200             'method': 'license',
201             'licenseType': 'STANDARD',
202             'clientVersion': '4.0004.899.011',
203             'uiVersion': 'akira',
204             'languages': ['de-DE'],
205             'playbackContextId': self.last_playback_context,
206             'drmContextIds': [self.last_drm_context],
207             'challenges': [{
208                 'dataBase64': challenge,
209                 'sessionId': sid
210             }],
211             'clientTime': int(time.time()),
212             'xid': int((int(time.time()) + 0.1612) * 1000)
213
214         }
215         request_data = self.__generate_msl_request_data(license_request_data)
216
217         resp = self.session.post(self.endpoints['license'], request_data)
218
219         try:
220             # If is valid json the request for the licnese failed
221             resp.json()
222             self.kodi_helper.log(msg='Error getting license: '+resp.text)
223             return False
224         except ValueError:
225             # json() failed so we have a chunked json response
226             resp = self.__parse_chunked_msl_response(resp.text)
227             data = self.__decrypt_payload_chunk(resp['payloads'][0])
228             if data['success'] is True:
229                 return data['result']['licenses'][0]['data']
230             else:
231                 self.kodi_helper.log(msg='Error getting license: ' + json.dumps(data))
232                 return False
233
234     def __decrypt_payload_chunk(self, payloadchunk):
235         payloadchunk = json.JSONDecoder().decode(payloadchunk)
236         encryption_envelope = json.JSONDecoder().decode(base64.standard_b64decode(payloadchunk['payload']))
237         # Decrypt the text
238         cipher = AES.new(self.encryption_key, AES.MODE_CBC, base64.standard_b64decode(encryption_envelope['iv']))
239         plaintext = cipher.decrypt(base64.standard_b64decode(encryption_envelope['ciphertext']))
240         # unpad the plaintext
241         plaintext = json.JSONDecoder().decode(Padding.unpad(plaintext, 16))
242         data = plaintext['data']
243
244         # uncompress data if compressed
245         if plaintext['compressionalgo'] == 'GZIP':
246             data = zlib.decompress(base64.standard_b64decode(data), 16 + zlib.MAX_WBITS)
247         else:
248             data = base64.standard_b64decode(data)
249
250         data = json.JSONDecoder().decode(data)[1]['payload']['data']
251         data = base64.standard_b64decode(data)
252         return json.JSONDecoder().decode(data)
253
254
255     def __tranform_to_dash(self, manifest):
256
257         self.save_file(self.kodi_helper.msl_data_path, 'manifest.json', json.dumps(manifest))
258         manifest = manifest['result']['viewables'][0]
259
260         self.last_playback_context = manifest['playbackContextId']
261         self.last_drm_context = manifest['drmContextId']
262
263         #Check for pssh
264         pssh = ''
265         if 'psshb64' in manifest:
266             if len(manifest['psshb64']) >= 1:
267                 pssh = manifest['psshb64'][0]
268
269         seconds = manifest['runtime']/1000
270         init_length = seconds / 2 * 12 + 20*1000
271         duration = "PT"+str(seconds)+".00S"
272
273         root = ET.Element('MPD')
274         root.attrib['xmlns'] = 'urn:mpeg:dash:schema:mpd:2011'
275         root.attrib['xmlns:cenc'] = 'urn:mpeg:cenc:2013'
276         root.attrib['mediaPresentationDuration'] = duration
277
278         period = ET.SubElement(root, 'Period', start='PT0S', duration=duration)
279
280         # One Adaption Set for Video
281         for video_track in manifest['videoTracks']:
282             video_adaption_set = ET.SubElement(period, 'AdaptationSet', mimeType='video/mp4', contentType="video")
283             # Content Protection
284             protection = ET.SubElement(video_adaption_set, 'ContentProtection',
285                           schemeIdUri='urn:uuid:EDEF8BA9-79D6-4ACE-A3C8-27DCD51D21ED')
286             if pssh is not '':
287                 ET.SubElement(protection, 'cenc:pssh').text = pssh
288
289             for downloadable in video_track['downloadables']:
290
291                 codec = 'h264'
292                 if 'hevc' in downloadable['contentProfile']:
293                     codec = 'hevc'
294
295                 hdcp_versions = '0.0'
296                 for hdcp in downloadable['hdcpVersions']:
297                     if hdcp != 'none':
298                         hdcp_versions = hdcp
299
300                 rep = ET.SubElement(video_adaption_set, 'Representation',
301                                     width=str(downloadable['width']),
302                                     height=str(downloadable['height']),
303                                     bandwidth=str(downloadable['bitrate']*1024),
304                                     hdcp=hdcp_versions,
305                                     nflxContentProfile=str(downloadable['contentProfile']),
306                                     codecs=codec,
307                                     mimeType='video/mp4')
308
309                 #BaseURL
310                 ET.SubElement(rep, 'BaseURL').text = self.__get_base_url(downloadable['urls'])
311                 # Init an Segment block
312                 segment_base = ET.SubElement(rep, 'SegmentBase', indexRange="0-"+str(init_length), indexRangeExact="true")
313                 ET.SubElement(segment_base, 'Initialization', range='0-'+str(init_length))
314
315
316
317         # Multiple Adaption Set for audio
318         for audio_track in manifest['audioTracks']:
319             audio_adaption_set = ET.SubElement(period, 'AdaptationSet',
320                                                lang=audio_track['bcp47'],
321                                                contentType='audio',
322                                                mimeType='audio/mp4')
323             for downloadable in audio_track['downloadables']:
324                 codec = 'aac'
325                 print downloadable
326                 if downloadable['contentProfile'] == 'ddplus-2.0-dash' or downloadable['contentProfile'] == 'ddplus-5.1-dash':
327                     codec = 'ec-3'
328                 print "codec is: " + codec
329                 rep = ET.SubElement(audio_adaption_set, 'Representation',
330                                     codecs=codec,
331                                     bandwidth=str(downloadable['bitrate']*1024),
332                                     mimeType='audio/mp4')
333
334                 #AudioChannel Config
335                 ET.SubElement(rep, 'AudioChannelConfiguration',
336                               schemeIdUri='urn:mpeg:dash:23003:3:audio_channel_configuration:2011',
337                               value=str(audio_track['channelsCount']))
338
339                 #BaseURL
340                 ET.SubElement(rep, 'BaseURL').text = self.__get_base_url(downloadable['urls'])
341                 # Index range
342                 segment_base = ET.SubElement(rep, 'SegmentBase', indexRange="0-"+str(init_length), indexRangeExact="true")
343                 ET.SubElement(segment_base, 'Initialization', range='0-'+str(init_length))
344
345
346         xml = ET.tostring(root, encoding='utf-8', method='xml')
347         xml = xml.replace('\n', '').replace('\r', '')
348         return xml
349
350     def __get_base_url(self, urls):
351         for key in urls:
352             return urls[key]
353
354     def __parse_chunked_msl_response(self, message):
355         i = 0
356         opencount = 0
357         closecount = 0
358         header = ""
359         payloads = []
360         old_end = 0
361
362         while i < len(message):
363             if message[i] == '{':
364                 opencount = opencount + 1
365             if message[i] == '}':
366                 closecount = closecount + 1
367             if opencount == closecount:
368                 if header == "":
369                     header = message[:i]
370                     old_end = i + 1
371                 else:
372                     payloads.append(message[old_end:i + 1])
373             i += 1
374
375         return {
376             'header': header,
377             'payloads': payloads
378         }
379
380     def __generate_msl_request_data(self, data):
381         header_encryption_envelope = self.__encrypt(self.__generate_msl_header())
382         header = {
383             'headerdata': base64.standard_b64encode(header_encryption_envelope),
384             'signature': self.__sign(header_encryption_envelope),
385             'mastertoken': self.mastertoken,
386         }
387
388         # Serialize the given Data
389         serialized_data = json.dumps(data)
390         serialized_data = serialized_data.replace('"', '\\"')
391         serialized_data = '[{},{"headers":{},"path":"/cbp/cadmium-13","payload":{"data":"' + serialized_data + '"},"query":""}]\n'
392
393         compressed_data = self.__compress_data(serialized_data)
394
395         # Create FIRST Payload Chunks
396         first_payload = {
397             "messageid": self.current_message_id,
398             "data": compressed_data,
399             "compressionalgo": "GZIP",
400             "sequencenumber": 1,
401             "endofmsg": True
402         }
403         first_payload_encryption_envelope = self.__encrypt(json.dumps(first_payload))
404         first_payload_chunk = {
405             'payload': base64.standard_b64encode(first_payload_encryption_envelope),
406             'signature': self.__sign(first_payload_encryption_envelope),
407         }
408
409         request_data = json.dumps(header) + json.dumps(first_payload_chunk)
410         return request_data
411
412
413
414     def __compress_data(self, data):
415         # GZIP THE DATA
416         out = StringIO()
417         with gzip.GzipFile(fileobj=out, mode="w") as f:
418             f.write(data)
419         return base64.standard_b64encode(out.getvalue())
420
421
422     def __generate_msl_header(self, is_handshake=False, is_key_request=False, compressionalgo="GZIP", encrypt=True):
423         """
424         Function that generates a MSL header dict
425         :return: The base64 encoded JSON String of the header
426         """
427         self.current_message_id = self.rndm.randint(0, pow(2, 52))
428
429         header_data = {
430             'sender': self.esn,
431             'handshake': is_handshake,
432             'nonreplayable': False,
433             'capabilities': {
434                 'languages': ["en-US"],
435                 'compressionalgos': []
436             },
437             'recipient': 'Netflix',
438             'renewable': True,
439             'messageid': self.current_message_id,
440             'timestamp': 1467733923
441         }
442
443         # Add compression algo if not empty
444         if compressionalgo is not "":
445             header_data['capabilities']['compressionalgos'].append(compressionalgo)
446
447         # If this is a keyrequest act diffrent then other requests
448         if is_key_request:
449             public_key = base64.standard_b64encode(self.rsa_key.publickey().exportKey(format='DER'))
450             header_data['keyrequestdata'] = [{
451                 'scheme': 'ASYMMETRIC_WRAPPED',
452                 'keydata': {
453                     'publickey': public_key,
454                     'mechanism': 'JWK_RSA',
455                     'keypairid': 'superKeyPair'
456                 }
457             }]
458         else:
459             if 'usertoken' in self.tokens:
460                 pass
461             else:
462                 # Auth via email and password
463                 header_data['userauthdata'] = {
464                     'scheme': 'EMAIL_PASSWORD',
465                     'authdata': {
466                         'email': self.email,
467                         'password': self.password
468                     }
469                 }
470
471         return json.dumps(header_data)
472
473
474
475     def __encrypt(self, plaintext):
476         """
477         Encrypt the given Plaintext with the encryption key
478         :param plaintext:
479         :return: Serialized JSON String of the encryption Envelope
480         """
481         iv = get_random_bytes(16)
482         encryption_envelope = {
483             'ciphertext': '',
484             'keyid': self.esn + '_' + str(self.sequence_number),
485             'sha256': 'AA==',
486             'iv': base64.standard_b64encode(iv)
487         }
488         # Padd the plaintext
489         plaintext = Padding.pad(plaintext, 16)
490         # Encrypt the text
491         cipher = AES.new(self.encryption_key, AES.MODE_CBC, iv)
492         ciphertext = cipher.encrypt(plaintext)
493         encryption_envelope['ciphertext'] = base64.standard_b64encode(ciphertext)
494         return json.dumps(encryption_envelope)
495
496
497     def __sign(self, text):
498         """
499         Calculates the HMAC signature for the given text with the current sign key and SHA256
500         :param text:
501         :return: Base64 encoded signature
502         """
503         signature = HMAC.new(self.sign_key, text, SHA256).digest()
504         return base64.standard_b64encode(signature)
505
506
507     def __perform_key_handshake(self):
508         header = self.__generate_msl_header(is_key_request=True, is_handshake=True, compressionalgo="", encrypt=False)
509         request = {
510             'entityauthdata': {
511                 'scheme': 'NONE',
512                 'authdata': {
513                     'identity': self.esn
514                 }
515             },
516             'headerdata': base64.standard_b64encode(header),
517             'signature': '',
518         }
519         self.kodi_helper.log(msg='Key Handshake Request:')
520         self.kodi_helper.log(msg=json.dumps(request))
521
522         resp = self.session.post(self.endpoints['manifest'], json.dumps(request, sort_keys=True))
523         if resp.status_code == 200:
524             resp = resp.json()
525             if 'errordata' in resp:
526                 self.kodi_helper.log(msg='Key Exchange failed')
527                 self.kodi_helper.log(msg=base64.standard_b64decode(resp['errordata']))
528                 return False
529             self.__parse_crypto_keys(json.JSONDecoder().decode(base64.standard_b64decode(resp['headerdata'])))
530         else:
531             self.kodi_helper.log(msg='Key Exchange failed')
532             self.kodi_helper.log(msg=resp.text)
533
534     def __parse_crypto_keys(self, headerdata):
535         self.__set_master_token(headerdata['keyresponsedata']['mastertoken'])
536         # Init Decryption
537         encrypted_encryption_key = base64.standard_b64decode(headerdata['keyresponsedata']['keydata']['encryptionkey'])
538         encrypted_sign_key = base64.standard_b64decode(headerdata['keyresponsedata']['keydata']['hmackey'])
539         cipher_rsa = PKCS1_OAEP.new(self.rsa_key)
540
541         # Decrypt encryption key
542         encryption_key_data = json.JSONDecoder().decode(cipher_rsa.decrypt(encrypted_encryption_key))
543         self.encryption_key = base64key_decode(encryption_key_data['k'])
544
545         # Decrypt sign key
546         sign_key_data = json.JSONDecoder().decode(cipher_rsa.decrypt(encrypted_sign_key))
547         self.sign_key = base64key_decode(sign_key_data['k'])
548
549         self.__save_msl_data()
550         self.handshake_performed = True
551
552     def __load_msl_data(self):
553         msl_data = json.JSONDecoder().decode(self.load_file(self.kodi_helper.msl_data_path, 'msl_data.json'))
554         self.__set_master_token(msl_data['tokens']['mastertoken'])
555         self.encryption_key = base64.standard_b64decode(msl_data['encryption_key'])
556         self.sign_key = base64.standard_b64decode(msl_data['sign_key'])
557
558     def __save_msl_data(self):
559         """
560         Saves the keys and tokens in json file
561         :return:
562         """
563         data = {
564             "encryption_key": base64.standard_b64encode(self.encryption_key),
565             'sign_key': base64.standard_b64encode(self.sign_key),
566             'tokens': {
567                 'mastertoken': self.mastertoken
568             }
569         }
570         serialized_data = json.JSONEncoder().encode(data)
571         self.save_file(self.kodi_helper.msl_data_path, 'msl_data.json', serialized_data)
572
573     def __set_master_token(self, master_token):
574         self.mastertoken = master_token
575         self.sequence_number = json.JSONDecoder().decode(base64.standard_b64decode(master_token['tokendata']))['sequencenumber']
576
577     def __load_rsa_keys(self):
578         loaded_key = self.load_file(self.kodi_helper.msl_data_path, 'rsa_key.bin')
579         self.rsa_key = RSA.importKey(loaded_key)
580
581     def __save_rsa_keys(self):
582         self.kodi_helper.log(msg='Save RSA Keys')
583         # Get the DER Base64 of the keys
584         encrypted_key = self.rsa_key.exportKey()
585         self.save_file(self.kodi_helper.msl_data_path, 'rsa_key.bin', encrypted_key)
586
587     @staticmethod
588     def file_exists(msl_data_path, filename):
589         """
590         Checks if a given file exists
591         :param filename: The filename
592         :return: True if so
593         """
594         return os.path.isfile(msl_data_path + filename)
595
596     @staticmethod
597     def save_file(msl_data_path, filename, content):
598         """
599         Saves the given content under given filename
600         :param filename: The filename
601         :param content: The content of the file
602         """
603         with open(msl_data_path + filename, 'w') as file_:
604             file_.write(content)
605             file_.flush()
606
607     @staticmethod
608     def load_file(msl_data_path, filename):
609         """
610         Loads the content of a given filename
611         :param filename: The file to load
612         :return: The content of the file
613         """
614         with open(msl_data_path + filename) as file_:
615             file_content = file_.read()
616         return file_content