Skip to content

Commit 1ead53a

Browse files
committed
Refactor http module
Use an API client; cleaner code; get rid of PATHS from constants.
1 parent 1fc110e commit 1ead53a

2 files changed

Lines changed: 56 additions & 63 deletions

File tree

bna/constants.py

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,3 @@
99
# "US": "us.mobile-service.blizzard.com",
1010
"default": "mobile-service.blizzard.com",
1111
}
12-
13-
PATHS = {
14-
"init_restore": "/enrollment/initiatePaperRestore.htm",
15-
"validate_restore": "/enrollment/validatePaperRestore.htm",
16-
"enroll": "/enrollment/enroll.htm",
17-
"time": "/enrollment/time.htm",
18-
}

bna/http.py

Lines changed: 56 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from time import time
88
from typing import Optional, Tuple
99

10-
from .constants import ENROLL_HOSTS, PATHS
10+
from .constants import ENROLL_HOSTS
1111
from .crypto import decrypt, encrypt, restore_code_to_bytes
1212
from .utils import normalize_serial
1313

@@ -18,27 +18,48 @@ def __init__(self, msg, response):
1818
super().__init__(msg)
1919

2020

21-
def _post(host: str, path: str, *, data: Optional[str] = None) -> bytes:
22-
"""
23-
Send computed data to Blizzard servers
24-
Return the answer from the server
25-
"""
26-
conn = HTTPConnection(host)
27-
conn.request("POST", path, data)
28-
response = conn.getresponse()
21+
class APIClient:
22+
def __init__(self, *, region: str = "US", host: str = ""):
23+
self.region = region
24+
self.host = host or ENROLL_HOSTS.get(region, ENROLL_HOSTS["default"])
25+
26+
def post(self, path: str, *, data: Optional[str] = None) -> bytes:
27+
conn = HTTPConnection(self.host)
28+
conn.request("POST", path, data)
29+
response = conn.getresponse()
30+
31+
if response.status != 200:
32+
raise HTTPError("%s returned status %i" % (self.host, response.status), response)
2933

30-
if response.status != 200:
31-
raise HTTPError("%s returned status %i" % (host, response.status), response)
34+
ret = response.read()
35+
conn.close()
36+
return ret
3237

33-
ret = response.read()
34-
conn.close()
35-
return ret
38+
def enroll(self, data):
39+
return self.post("/enrollment/enroll.htm", data=data)
3640

41+
def get_time(self) -> int:
42+
response = self.post("/enrollment/time.htm")
43+
return int(struct.unpack(">Q", response)[0])
3744

38-
def enroll(
39-
data: str, host: str = ENROLL_HOSTS["default"], path: str = PATHS["enroll"]
40-
) -> bytes:
41-
return _post(host, path, data=data)
45+
def initiate_paper_restore(self, serial: str):
46+
response = self.post("/enrollment/initiatePaperRestore.htm", data=serial)
47+
resp_size = len(response)
48+
if resp_size != 32:
49+
raise ValueError("Bad challenge response (%i bytes)" % (resp_size))
50+
51+
return response
52+
53+
def validate_paper_restore(self, serial: str, encrypted_data: str):
54+
data = serial + encrypted_data
55+
try:
56+
response = self.post("/enrollment/validatePaperRestore.htm", data=data)
57+
except HTTPError as e:
58+
if e.response.status == 600:
59+
raise HTTPError("Invalid serial or restore key", e.response)
60+
else:
61+
raise
62+
return response
4263

4364

4465
def request_new_serial(
@@ -57,14 +78,15 @@ def base_msg(otp, region, model):
5778

5879
otp = token_bytes(37)
5980
data = base_msg(otp, region, model)
81+
encrypted_data = encrypt(data)
82+
83+
client = APIClient(region=region)
84+
response = client.enroll(encrypted_data)[8:]
6085

61-
e = encrypt(data)
62-
# get the host, or fallback to default
63-
host = ENROLL_HOSTS.get(region, ENROLL_HOSTS["default"])
64-
response = decrypt(enroll(e, host)[8:], otp)
86+
decrypted_response = decrypt(response, otp)
6587

66-
secret = b32encode(response[:20]).decode()
67-
serial = response[20:].decode()
88+
secret = b32encode(decrypted_response[:20]).decode()
89+
serial = decrypted_response[20:].decode()
6890

6991
region = serial[:2]
7092
if region not in ("CN", "EU", "US"):
@@ -73,7 +95,7 @@ def base_msg(otp, region, model):
7395
return serial, secret
7496

7597

76-
def get_time_offset(region: str = "US", path: str = PATHS["time"]) -> int:
98+
def get_time_offset(region: str = "US") -> int:
7799
"""
78100
Calculates the time difference in seconds as a float
79101
between the local host and a remote server
@@ -82,52 +104,30 @@ def get_time_offset(region: str = "US", path: str = PATHS["time"]) -> int:
82104
Negative numbers indicate the local clock is ahead of the
83105
server clock.
84106
"""
85-
host = ENROLL_HOSTS.get(region, ENROLL_HOSTS["default"])
86-
response = _post(host, path)
87-
t = time()
107+
client = APIClient(region=region)
108+
server_time = client.get_time()
109+
local_time = time()
88110

89111
# NOTE: The server returns time in milliseconds as an int whereas
90112
# Python returns it as a float, in seconds.
91-
server_time = int(struct.unpack(">Q", response)[0])
92-
93-
return server_time - int(t * 1000)
113+
return server_time - int(local_time * 1000)
94114

95115

96116
def restore(serial: str, restore_code: str) -> str:
97-
restore_code = restore_code.upper()
98117
serial = normalize_serial(serial)
118+
restore_code = restore_code.upper()
99119
if len(restore_code) != 10:
100120
raise ValueError(f"invalid restore code (should be 10 characters): {restore_code}")
101121

102-
challenge = initiate_paper_restore(serial)
103-
if len(challenge) != 32:
104-
raise ValueError("Bad challenge length (expected 32, got %i)" % (len(challenge)))
122+
client = APIClient()
123+
challenge = client.initiate_paper_restore(serial)
105124

106125
code = restore_code_to_bytes(restore_code)
107126
hash = hmac.new(code, serial.encode() + challenge, digestmod=sha1).digest()
108127

109128
otp = token_bytes(20)
110-
e = encrypt(hash + otp)
111-
response = validate_paper_restore(serial + e)
129+
encrypted_data = encrypt(hash + otp)
130+
response = client.validate_paper_restore(serial, encrypted_data)
112131
secret = decrypt(response, otp)
113132

114133
return b32encode(secret).decode()
115-
116-
117-
def initiate_paper_restore(
118-
serial: str, host: str = ENROLL_HOSTS["default"], path: str = PATHS["init_restore"]
119-
) -> bytes:
120-
return _post(host, path, data=serial)
121-
122-
123-
def validate_paper_restore(
124-
data: str, host: str = ENROLL_HOSTS["default"], path: str = PATHS["validate_restore"]
125-
) -> bytes:
126-
try:
127-
response = _post(host, path, data=data)
128-
except HTTPError as e:
129-
if e.response.status == 600:
130-
raise HTTPError("Invalid serial or restore key", e.response)
131-
else:
132-
raise
133-
return response

0 commit comments

Comments
 (0)