|
| 1 | +""" |
| 2 | +A Python implementation to interact with the MSP API |
| 3 | +""" |
| 4 | + |
| 5 | +import hashlib |
| 6 | +import binascii |
| 7 | +import http.client |
| 8 | +import random |
| 9 | +import base64 |
| 10 | +from typing import List, Union |
| 11 | +from datetime import date, datetime |
| 12 | +from urllib.parse import urlparse |
| 13 | +from pyamf import remoting, ASObject, TypedObject, AMF3 |
| 14 | + |
| 15 | + |
| 16 | +def ticket_header(ticket: str) -> ASObject: |
| 17 | + """ |
| 18 | + Generate a ticket header for the given ticket |
| 19 | + """ |
| 20 | + |
| 21 | + marking_id = int(random.uniform(0.0, 0.1) * 1000) + 1 |
| 22 | + loc1bytes = str(marking_id).encode('utf-8') |
| 23 | + loc5 = hashlib.md5(loc1bytes).hexdigest() |
| 24 | + loc6 = binascii.hexlify(loc1bytes).decode() |
| 25 | + return ASObject({"Ticket": ticket + loc5 + loc6, "anyAttribute": None}) |
| 26 | + |
| 27 | + |
| 28 | +def calculate_checksum(arguments: Union[int, str, bool, bytes, List[Union[int, str, bool, bytes]], |
| 29 | + dict, date, datetime, ASObject, TypedObject]) -> str: |
| 30 | + """ |
| 31 | + Calculate the checksum for the given arguments |
| 32 | + """ |
| 33 | + |
| 34 | + checked_objects = {} |
| 35 | + no_ticket_value = "v1n3g4r" |
| 36 | + salt = "$CuaS44qoi0Mp2qp" |
| 37 | + |
| 38 | + def from_object(obj): |
| 39 | + if obj is None: |
| 40 | + return "" |
| 41 | + |
| 42 | + if isinstance(obj, (int, str, bool)): |
| 43 | + return str(obj) |
| 44 | + |
| 45 | + if isinstance(obj, bytes): |
| 46 | + return from_byte_array(obj) |
| 47 | + |
| 48 | + if isinstance(obj, (date, datetime)): |
| 49 | + return obj.strftime('%Y%m%d') |
| 50 | + |
| 51 | + if isinstance(obj, (list, dict)) and "Ticket" not in obj: |
| 52 | + return from_array(obj) |
| 53 | + |
| 54 | + return "" |
| 55 | + |
| 56 | + def from_byte_array(byte_array): |
| 57 | + if len(byte_array) <= 20: |
| 58 | + return binascii.hexlify(byte_array).decode('utf-8') |
| 59 | + |
| 60 | + bytes_to_check = [byte_array[int(len(byte_array) / 20 * i)] for i in range(20)] |
| 61 | + return binascii.hexlify(bytes(bytes_to_check)).decode('utf-8') |
| 62 | + |
| 63 | + def from_array(arr): |
| 64 | + result = "" |
| 65 | + for item in arr: |
| 66 | + if isinstance(item, (ASObject, TypedObject)): |
| 67 | + result += from_object(item) |
| 68 | + else: |
| 69 | + result += from_object_inner(item) |
| 70 | + return result |
| 71 | + |
| 72 | + def get_ticket_value(arr): |
| 73 | + for obj in arr: |
| 74 | + if isinstance(obj, ASObject) and "Ticket" in obj: |
| 75 | + ticket_str = obj["Ticket"] |
| 76 | + if ',' in ticket_str: |
| 77 | + return ticket_str.split(',')[5][:5] |
| 78 | + return no_ticket_value |
| 79 | + |
| 80 | + def from_object_inner(obj): |
| 81 | + result = "" |
| 82 | + if isinstance(obj, dict): |
| 83 | + for key in sorted(obj.keys()): |
| 84 | + if key not in checked_objects: |
| 85 | + result += from_object(obj[key]) |
| 86 | + checked_objects[key] = True |
| 87 | + else: |
| 88 | + result += from_object(obj) |
| 89 | + return result |
| 90 | + |
| 91 | + result_str = from_object_inner(arguments) + get_ticket_value(arguments) + salt |
| 92 | + return hashlib.sha1(result_str.encode()).hexdigest() |
| 93 | + |
| 94 | + |
| 95 | +def invoke_method(server: str, method: str, params: dict, session_id: str) -> tuple: |
| 96 | + """ |
| 97 | + Invoke a method on the MSP API |
| 98 | + """ |
| 99 | + |
| 100 | + req = remoting.Request(target=method, body=params) |
| 101 | + event = remoting.Envelope(AMF3) |
| 102 | + |
| 103 | + event.headers = remoting.HeaderCollection({ |
| 104 | + ("sessionID", False, session_id), |
| 105 | + ("needClassName", False, False), |
| 106 | + ("id", False, calculate_checksum(params) |
| 107 | + )}) |
| 108 | + |
| 109 | + event['/1'] = req |
| 110 | + encoded_req = remoting.encode(event).getvalue() |
| 111 | + |
| 112 | + full_endpoint = f"https://ws-{server}.mspapis.com/Gateway.aspx?method={method}" |
| 113 | + conn = http.client.HTTPSConnection(urlparse(full_endpoint).hostname) |
| 114 | + |
| 115 | + headers = { |
| 116 | + "Referer": "app:/cache/t1.bin/[[DYNAMIC]]/2", |
| 117 | + "Accept": ("text/xml, application/xml, application/xhtml+xml, " |
| 118 | + "text/html;q=0.9, text/plain;q=0.8, text/css, image/png, " |
| 119 | + "image/jpeg, image/gif;q=0.8, application/x-shockwave-flash, " |
| 120 | + "video/mp4;q=0.9, flv-application/octet-stream;q=0.8, " |
| 121 | + "video/x-flv;q=0.7, audio/mp4, application/futuresplash, " |
| 122 | + "*/*;q=0.5, application/x-mpegURL"), |
| 123 | + "x-flash-version": "32,0,0,100", |
| 124 | + "Content-Length": str(len(encoded_req)), |
| 125 | + "Content-Type": "application/x-amf", |
| 126 | + "Accept-Encoding": "gzip, deflate", |
| 127 | + "User-Agent": "Mozilla/5.0 (Windows; U; en) AppleWebKit/533.19.4 " |
| 128 | + "(KHTML, like Gecko) AdobeAIR/32.0", |
| 129 | + "Connection": "Keep-Alive", |
| 130 | + } |
| 131 | + path = urlparse(full_endpoint).path |
| 132 | + query = urlparse(full_endpoint).query |
| 133 | + conn.request("POST", path + "?" + query, encoded_req, headers=headers) |
| 134 | + |
| 135 | + with conn.getresponse() as resp: |
| 136 | + resp_data = resp.read() if resp.status == 200 else None |
| 137 | + if resp.status != 200: |
| 138 | + return (resp.status, resp_data) |
| 139 | + return (resp.status, remoting.decode(resp_data)["/1"].body) |
| 140 | + |
| 141 | + |
| 142 | +def get_session_id() -> str: |
| 143 | + """ |
| 144 | + Generate a random session id |
| 145 | + """ |
| 146 | + |
| 147 | + session_id = ''.join(f'{random.randint(0, 15):x}' for _ in range(48)) |
| 148 | + session_id = session_id[:46] |
| 149 | + return base64.b64encode(session_id.encode()).decode() |
0 commit comments