Skip to content

Commit 31e2cee

Browse files
committed
feat: refactor authentication methods and add getBanks functionality
1 parent c23c1e7 commit 31e2cee

File tree

6 files changed

+156
-98
lines changed

6 files changed

+156
-98
lines changed

mbbank/main.py

Lines changed: 72 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ class MBBank:
4646

4747
FPR = "c7a1beebb9400375bb187daa33de9659"
4848

49-
def __init__(self, *, username: str, password: str, proxy: dict=None, ocr_class=None):
49+
def __init__(self, *, username: str, password: str, proxy: dict = None, ocr_class=None):
5050
self._userid = username
5151
self._password = password
5252
self._wasm_cache = None
@@ -113,53 +113,76 @@ def _get_wasm_file(self):
113113
self._wasm_cache = file_data
114114
return file_data
115115

116+
def get_capcha_image(self) -> bytes:
117+
"""
118+
Get capcha image as bytes
119+
120+
Returns:
121+
success (bytes): capcha image as bytes
122+
"""
123+
rid = f"{self._userid}-{self._get_now_time()}"
124+
json_data = {
125+
'sessionId': "",
126+
'refNo': rid,
127+
'deviceIdCommon': self.deviceIdCommon,
128+
}
129+
headers = headers_default.copy()
130+
headers["X-Request-Id"] = rid
131+
headers["Deviceid"] = self.deviceIdCommon
132+
headers["Refno"] = rid
133+
with requests.Session() as s:
134+
with s.post("https://online.mbbank.com.vn/retail-web-internetbankingms/getCaptchaImage",
135+
headers=headers, json=json_data,
136+
proxies=self.proxy) as r:
137+
data_out = r.json()
138+
return base64.b64decode(data_out["imageString"])
139+
140+
def login(self, captcha_text: str):
141+
"""
142+
Login to MBBank account
143+
144+
Args:
145+
captcha_text (str): capcha text from capcha image
146+
147+
Raises:
148+
MBBankError: if login failed
149+
"""
150+
payload = {
151+
"userId": self._userid,
152+
"password": hashlib.md5(self._password.encode()).hexdigest(),
153+
"captcha": captcha_text,
154+
'sessionId': "",
155+
'refNo': f'{self._userid}-{self._get_now_time()}',
156+
'deviceIdCommon': self.deviceIdCommon,
157+
"ibAuthen2faString": self.FPR,
158+
}
159+
wasm_bytes = self._get_wasm_file()
160+
data_encrypt = wasm_encrypt(wasm_bytes, payload)
161+
with requests.Session() as s:
162+
with s.post("https://online.mbbank.com.vn/retail_web/internetbanking/doLogin",
163+
headers=headers_default, json={"dataEnc": data_encrypt},
164+
proxies=self.proxy) as r:
165+
data_out = r.json()
166+
if data_out["result"]["ok"]:
167+
self.sessionId = data_out["sessionId"]
168+
self._userinfo = data_out
169+
return
170+
else:
171+
raise MBBankError(data_out["result"])
172+
116173
def _authenticate(self):
117174
while True:
118175
self._userinfo = None
119176
self.sessionId = None
120177
self._temp = {}
121-
rid = f"{self._userid}-{self._get_now_time()}"
122-
json_data = {
123-
'sessionId': "",
124-
'refNo': rid,
125-
'deviceIdCommon': self.deviceIdCommon,
126-
}
127-
headers = headers_default.copy()
128-
headers["X-Request-Id"] = rid
129-
headers["Deviceid"] = self.deviceIdCommon
130-
headers["Refno"] = rid
131-
with requests.Session() as s:
132-
with s.post("https://online.mbbank.com.vn/retail-web-internetbankingms/getCaptchaImage",
133-
headers=headers, json=json_data,
134-
proxies=self.proxy) as r:
135-
data_out = r.json()
136-
img_bytes = base64.b64decode(data_out["imageString"])
137-
text = self.ocr_class.process_image(img_bytes)
138-
payload = {
139-
"userId": self._userid,
140-
"password": hashlib.md5(self._password.encode()).hexdigest(),
141-
"captcha": text,
142-
'sessionId': "",
143-
'refNo': f'{self._userid}-{self._get_now_time()}',
144-
'deviceIdCommon': self.deviceIdCommon,
145-
"ibAuthen2faString": self.FPR,
146-
}
147-
wasm_bytes = self._get_wasm_file()
148-
dataEnc = wasm_encrypt(wasm_bytes, payload)
149-
with requests.Session() as s:
150-
with s.post("https://online.mbbank.com.vn/retail_web/internetbanking/doLogin",
151-
headers=headers_default, json={"dataEnc": dataEnc},
152-
proxies=self.proxy) as r:
153-
data_out = r.json()
154-
if data_out["result"]["ok"]:
155-
self.sessionId = data_out["sessionId"]
156-
self._userinfo = data_out
157-
return
158-
elif data_out["result"]["responseCode"] == "GW283":
159-
pass
160-
else:
161-
err_out = data_out["result"]
162-
raise Exception(f"{err_out['responseCode']} | {err_out['message']}")
178+
img_bytes = self.get_capcha_image()
179+
captcha_text = self.ocr_class.process_image(img_bytes)
180+
try:
181+
return self.login(captcha_text)
182+
except MBBankError as e:
183+
if e.code == "GW283":
184+
continue # capcha error, try again
185+
raise e
163186

164187
def getTransactionAccountHistory(self, *, accountNo: str = None, from_date: datetime.datetime,
165188
to_date: datetime.datetime):
@@ -391,3 +414,9 @@ def userinfo(self):
391414
else:
392415
self.getBalance()
393416
return self._userinfo
417+
418+
def getBanks(self):
419+
data_out = self._req("https://online.mbbank.com.vn/api/retail_web/common/getBankList")
420+
return data_out
421+
422+

mbbank/mbasync.py

Lines changed: 70 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
import hashlib
66
import typing
77
import aiohttp
8-
from .capcha_ocr import CapchaProcessing, CapchaOCR
98
from .main import MBBankError, MBBank
109
from .wasm_helper import wasm_encrypt
1110
from .main import headers_default
@@ -30,10 +29,10 @@ class MBBankAsync(MBBank):
3029
username (str): MBBank Account Username
3130
password (str): MBBank Account Password
3231
proxy (str, optional): Proxy url. Example: "http://127.0.0.1:8080". Defaults to None.
33-
ocr_class (CapchaProcessing, optional): CapchaProcessing class. Defaults to TesseractOCR().
32+
ocr_class (CapchaProcessing, optional): CapchaProcessing class. Defaults to CapchaOCR().
3433
"""
3534

36-
def __init__(self, *, username: str, password: str, proxy: dict=None, ocr_class=None):
35+
def __init__(self, *, username: str, password: str, proxy: dict = None, ocr_class=None):
3736
super().__init__(username=username, password=password, proxy=proxy, ocr_class=ocr_class)
3837
# convert proxy dict by requests to aiohttp format
3938
if len(self.proxy.values()):
@@ -50,54 +49,77 @@ async def _get_wasm_file(self):
5049
self._wasm_cache = await r.read()
5150
return self._wasm_cache
5251

52+
async def get_capcha_image(self) -> bytes:
53+
"""
54+
Get capcha image as bytes
55+
56+
Returns:
57+
success (bytes): capcha image as bytes
58+
"""
59+
rid = f"{self._userid}-{get_now_time()}"
60+
json_data = {
61+
'sessionId': "",
62+
'refNo': rid,
63+
'deviceIdCommon': self.deviceIdCommon,
64+
}
65+
headers = headers_default.copy()
66+
headers["X-Request-Id"] = rid
67+
headers["Deviceid"] = self.deviceIdCommon
68+
headers["Refno"] = rid
69+
async with aiohttp.ClientSession() as s:
70+
async with s.post("https://online.mbbank.com.vn/retail-web-internetbankingms/getCaptchaImage",
71+
headers=headers, json=json_data, proxy=self.proxy) as r:
72+
data_out = await r.json()
73+
return base64.b64decode(data_out["imageString"])
74+
75+
async def login(self, captcha_text: str):
76+
"""
77+
Login to MBBank account
78+
79+
Args:
80+
captcha_text (str): capcha text from capcha image
81+
82+
Raises:
83+
MBBankError: if api response not ok
84+
"""
85+
payload = {
86+
"userId": self._userid,
87+
"password": hashlib.md5(self._password.encode()).hexdigest(),
88+
"captcha": captcha_text,
89+
'sessionId': "",
90+
'refNo': f'{self._userid}-{get_now_time()}',
91+
'deviceIdCommon': self.deviceIdCommon,
92+
"ibAuthen2faString": self.FPR,
93+
}
94+
wasm_bytes = await self._get_wasm_file()
95+
loop = asyncio.get_running_loop()
96+
data_encrypt = await loop.run_in_executor(pool, wasm_encrypt, wasm_bytes, payload)
97+
async with aiohttp.ClientSession() as s:
98+
async with s.post("https://online.mbbank.com.vn/retail_web/internetbanking/doLogin",
99+
headers=headers_default, json={"dataEnc": data_encrypt}, proxy=self.proxy) as r:
100+
data_out = await r.json()
101+
if data_out["result"]["ok"]:
102+
self.sessionId = data_out["sessionId"]
103+
self._userinfo = data_out
104+
return
105+
else:
106+
raise MBBankError(data_out["result"])
107+
53108
async def _authenticate(self):
54109
while True:
55110
self._userinfo = None
56111
self.sessionId = None
57112
self._temp = {}
58-
rid = f"{self._userid}-{get_now_time()}"
59-
json_data = {
60-
'sessionId': "",
61-
'refNo': rid,
62-
'deviceIdCommon': self.deviceIdCommon,
63-
}
64-
headers = headers_default.copy()
65-
headers["X-Request-Id"] = rid
66-
headers["Deviceid"] = self.deviceIdCommon
67-
headers["Refno"] = rid
68-
async with aiohttp.ClientSession() as s:
69-
async with s.post("https://online.mbbank.com.vn/retail-web-internetbankingms/getCaptchaImage",
70-
headers=headers, json=json_data, proxy=self.proxy) as r:
71-
data_out = await r.json()
72-
img_bytes = base64.b64decode(data_out["imageString"])
113+
img_bytes = await self.get_capcha_image()
73114
text = await asyncio.get_event_loop().run_in_executor(
74115
pool, self.ocr_class.process_image, img_bytes
75116
)
76-
payload = {
77-
"userId": self._userid,
78-
"password": hashlib.md5(self._password.encode()).hexdigest(),
79-
"captcha": text,
80-
'sessionId': "",
81-
'refNo': f'{self._userid}-{get_now_time()}',
82-
'deviceIdCommon': self.deviceIdCommon,
83-
"ibAuthen2faString": self.FPR,
84-
}
85-
wasm_bytes = await self._get_wasm_file()
86-
loop = asyncio.get_running_loop()
87-
dataEnc = await loop.run_in_executor(pool, wasm_encrypt, wasm_bytes, payload)
88-
async with aiohttp.ClientSession() as s:
89-
async with s.post("https://online.mbbank.com.vn/retail_web/internetbanking/doLogin",
90-
headers=headers_default, json={"dataEnc": dataEnc}, proxy=self.proxy) as r:
91-
data_out = await r.json()
92-
if data_out["result"]["ok"]:
93-
self.sessionId = data_out["sessionId"]
94-
self._userinfo = data_out
95-
return
96-
elif data_out["result"]["responseCode"] == "GW283":
97-
pass
98-
else:
99-
err_out = data_out["result"]
100-
raise Exception(f"{err_out['responseCode']} | {err_out['message']}")
117+
try:
118+
return await self.login(text)
119+
except MBBankError as e:
120+
if e.code == "GW283":
121+
continue # capcha error, try again
122+
raise e
101123

102124
async def _req(self, url, *, json=None, headers=None):
103125
if headers is None:
@@ -275,7 +297,6 @@ async def getSavingDetail(self, accNo: str, accType: typing.Literal["OSA", "SBA"
275297
data_out = await self._req("https://online.mbbank.com.vn/api/retail_web/saving/getDetail", json=json_data)
276298
return data_out
277299

278-
279300
async def getLoanList(self):
280301
"""
281302
Get all loan list from your account
@@ -362,3 +383,8 @@ async def userinfo(self):
362383
else:
363384
await self.getBalance()
364385
return self._userinfo
386+
387+
async def getBanks(self):
388+
data_out = await self._req("https://online.mbbank.com.vn/api/retail_web/common/getBankList")
389+
return data_out
390+

mbbank/wasm_helper/__init__.py

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,11 @@
1414
class globalThis:
1515
def __init__(self):
1616
self.exports = undefined
17-
self.window = dict_warper({"document": dict_warper({})})
17+
self.window = dict_warper({
18+
"document": {
19+
"we_love_mb": True # From CookieGMVN Library :)
20+
}
21+
})
1822
self.fs = fs_object()
1923
self.process = process_object()
2024
self.location = dict_warper({"origin": "https://online.mbbank.com.vn"})
@@ -137,14 +141,7 @@ def exit_process(cls, exitCode):
137141
print("exit code:", exitCode)
138142

139143
def importObject(self, imports_type: list[wasmtime.ImportType]):
140-
def proxy(name):
141-
def fn(*args, **kwargs):
142-
call = getattr(self.go_js, name)
143-
return call(*args, **kwargs)
144-
145-
return fn
146-
147-
return [wasmtime.Func(self.wasm_store, i.type, proxy(i.name)) for i in imports_type]
144+
return [wasmtime.Func(self.wasm_store, i.type, getattr(self.go_js, i.name)) for i in imports_type]
148145

149146
# noinspection PyAttributeOutsideInit
150147
def run(self, inst):
@@ -213,7 +210,7 @@ def _resume(self):
213210

214211
def _makeFuncWrapper(self, ids):
215212
def wrapper(*args, **kwargs):
216-
event = dict_warper({"id": int(ids), "args": hash_list(args), "this": global_this})
213+
event = dict_warper({"id": int(ids), "args": args, "this": global_this})
217214
self._pendingEvent = event
218215
self._resume()
219216
return event.result
@@ -372,7 +369,7 @@ def sysjs_copyBytesToJS(self, *args):
372369
def debug(self, *args):
373370
pass
374371

375-
372+
# All instances shared the same wasm module
376373
def wasm_encrypt(wasm_files, json_data):
377374
if getattr(global_this, 'bder', None) is not None:
378375
return global_this.bder(json.dumps(json_data), "0")

mbbank/wasm_helper/helper.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,10 @@ def getegid(self):
214214
class dict_warper:
215215
def __init__(self, dict_data):
216216
for key, value in dict_data.items():
217+
if isinstance(value, dict):
218+
value = dict_warper(value)
219+
elif isinstance(value, list) or isinstance(value, tuple):
220+
value = hash_list(value)
217221
setattr(self, key, value)
218222

219223
def to_dict(self):

tests/async_test.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ async def main():
99
mb = MBBankAsync(username=os.getenv("MBBANK_USERNAME"), password=os.getenv("MBBANK_PASSWORD"))
1010
end_query_day = datetime.datetime.now()
1111
start_query_day = end_query_day - datetime.timedelta(days=30)
12+
await mb.getBanks()
1213
await mb.getBalance()
1314
await mb.userinfo()
1415
await mb.getInterestRate()

tests/sync_test.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
mb = MBBank(username=os.getenv("MBBANK_USERNAME"), password=os.getenv("MBBANK_PASSWORD"))
66
end_query_day = datetime.datetime.now()
77
start_query_day = end_query_day - datetime.timedelta(days=30)
8+
mb.getBanks()
89
mb.getBalance()
910
mb.userinfo()
1011
mb.getInterestRate()

0 commit comments

Comments
 (0)