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