Skip to content

Commit 2d202e3

Browse files
committed
Initial commit 🔥
1 parent 55bcdd2 commit 2d202e3

File tree

5 files changed

+214
-0
lines changed

5 files changed

+214
-0
lines changed

LICENSE

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
2+
Version 2, December 2004
3+
4+
Copyright (C) 2004 Sam Hocevar <[email protected]>
5+
6+
Everyone is permitted to copy and distribute verbatim or modified copies of this license document, and changing it is allowed as long as the name is changed.
7+
8+
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
9+
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
10+
11+
0. You just DO WHAT THE FUCK YOU WANT TO.

README.md

+3
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,5 @@
11
# msp-py
22
A minimalistic and fast Python handler for requests to the MSP API.
3+
4+
## Installation
5+
Simply just clone the repository and install the requirements.

example.py

+50
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
"""
2+
This example shows how to login to MSP and send a authenticated request
3+
to the LoadActorDetailsExtended method.
4+
"""
5+
6+
from msp import invoke_method, get_session_id, ticket_header
7+
8+
# Set login credentials and server name
9+
USERNAME = ""
10+
PASSWORD = ""
11+
SERVER = "US"
12+
13+
# Call the login method and retrieve the response
14+
code, resp = invoke_method(
15+
SERVER,
16+
"MovieStarPlanet.WebService.User.AMFUserServiceWeb.Login",
17+
[
18+
USERNAME,
19+
PASSWORD,
20+
[],
21+
None,
22+
None,
23+
"MSP1-Standalone:XXXXXX"
24+
],
25+
get_session_id()
26+
)
27+
28+
# Check if login was successful
29+
status = resp.get('loginStatus', {}).get('status')
30+
if status != "Success":
31+
print(f"Login failed, status: {status}")
32+
quit()
33+
34+
# Retrieve the auth ticket and actor ID from the login response
35+
ticket = resp['loginStatus']['ticket']
36+
actor_id = resp['loginStatus']['actor']['ActorId']
37+
38+
# Call the authenticated method and retrieve the response
39+
code, resp = invoke_method(
40+
SERVER,
41+
"MovieStarPlanet.WebService.UserSession.AMFUserSessionService.LoadActorDetailsExtended",
42+
[
43+
ticket_header(ticket),
44+
actor_id
45+
],
46+
get_session_id()
47+
)
48+
49+
# Print the response
50+
print(resp)

msp.py

+149
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
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()

requirements.txt

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Py3AMF==0.8.10

0 commit comments

Comments
 (0)