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